Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2113712
test: 배포서버와 파라미터 불일치 테스트
rud15dns Jan 6, 2026
a7f41ac
test: docker compose 버전 불일치
rud15dns Jan 6, 2026
2604369
test: 대기열 등록 쿼리 개선 테스트
rud15dns Jan 22, 2026
7e9346d
test: System.out.println 과 log 출력 X
rud15dns Jan 28, 2026
07b52c9
test: hikari connection pool 60으로 확장
rud15dns Jan 28, 2026
3d36aca
test: 커넥션 풀 X
rud15dns Jan 28, 2026
6f7bdf3
test: 히카리 로깅 레벨 추가
rud15dns Jan 28, 2026
d6ce437
test: open-in-view false로 변경
rud15dns Jan 28, 2026
3a87b67
test: Redis로만 줄세우기 확인/DB 제외
rud15dns Jan 28, 2026
dcc862e
test: redis pool 늘리기
rud15dns Jan 28, 2026
ad0a05b
test: redis lettuce 설정되는지 확인
rud15dns Jan 28, 2026
6a77487
test: redis lettuce 설정되는지 확인
rud15dns Jan 28, 2026
71083b7
test: redis lettuce 설정되는지 확인
rud15dns Jan 28, 2026
56d9dcb
test: redisson 삭제
rud15dns Jan 28, 2026
26b9ebf
test: redisson 삭제
rud15dns Jan 28, 2026
a836fed
test: lettuce pool 늘리기 시도
rud15dns Jan 28, 2026
f5b977b
test: lettuce pool 늘리기 시도
rud15dns Jan 28, 2026
e00bcda
test: lettuce pool 늘리기 시도
rud15dns Jan 28, 2026
de4161f
test: tomcat 스레드 늘려보기
rud15dns Jan 28, 2026
3238bdd
test: t3.medium으로 하드웨어 성능 올리고, DB 로직 없애보기
rud15dns Jan 28, 2026
61dd9da
test: fixedDelay -> fixedRate으로 변경
rud15dns Jan 28, 2026
6eefc14
test: redis가 문제인지를 확인하기 위해서 scheduled를 비활성화 해봄
rud15dns Jan 28, 2026
e414813
test: 톰캣 스레드 줄여보기
rud15dns Jan 28, 2026
b47988b
test: redis <-> springboot 시간 로깅
rud15dns Jan 28, 2026
9365b9a
test: 레디스 커넥션 풀 없앰
rud15dns Jan 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/cd-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
echo "[INFO] Deploying test build with IMAGE_TAG=$IMAGE_TAG"

# app만 최신 SHA 태그로 pull & restart
sudo -E docker compose -f docker-compose.prod.yml pull app
sudo -E docker compose -f docker-compose.prod.yml up -d --no-deps app
sudo -E docker-compose -f docker-compose.prod.yml pull app
sudo -E docker-compose -f docker-compose.prod.yml up -d --no-deps app

sudo docker image prune -f
7 changes: 4 additions & 3 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ jobs:
export DB_USERNAME=${{ secrets.DB_USERNAME }}
export DB_PASSWORD=${{ secrets.DB_PASSWORD }}

# spring boot 이미지 최신화 및 업데이트
sudo -E docker-compose -f docker-compose.prod.yml pull app
sudo -E docker-compose -f docker-compose.prod.yml up -d --no-deps app
# 이미지 최신화 및 컨테이너 재시작
sudo docker-compose pull
sudo docker-compose down
sudo docker-compose up -d --remove-orphans

# 필요 없는 이미지 정리
sudo docker image prune -f
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,10 @@ dependencies {

// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.apache.commons:commons-pool2'

// redisson
implementation 'org.redisson:redisson-spring-boot-starter:3.27.2'
// implementation 'org.redisson:redisson-spring-boot-starter:3.27.2'
}

tasks.named('test') {
Expand Down
60 changes: 55 additions & 5 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,58 @@ services:
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: 'broker,controller'
KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka-1:9093'
KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka-1:9093,2@kafka-2:9093,3@kafka-3:9093'
KAFKA_LISTENERS: 'PLAINTEXT://kafka-1:9092,CONTROLLER://kafka-1:9093,PLAINTEXT_HOST://0.0.0.0:29092'
KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka-1:9092,PLAINTEXT_HOST://${EC2_PRIVATE_IP}:29092'
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'
KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 2
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 2
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
CLUSTER_ID: 'ciWo7IWazngRchmPES6q5A=='
KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'
networks:
- app-network

kafka-2:
image: confluentinc/cp-kafka:latest
container_name: kafka-2
ports:
- "29093:29093"
environment:
KAFKA_NODE_ID: 2
KAFKA_PROCESS_ROLES: 'broker,controller'
KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka-1:9093,2@kafka-2:9093,3@kafka-3:9093'
KAFKA_LISTENERS: 'PLAINTEXT://kafka-2:9092,CONTROLLER://kafka-2:9093,PLAINTEXT_HOST://0.0.0.0:29093'
KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka-2:9092,PLAINTEXT_HOST://${EC2_PRIVATE_IP}:29093'
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'
KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 2
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 2
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
CLUSTER_ID: 'ciWo7IWazngRchmPES6q5A=='
KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'
networks:
- app-network

kafka-3:
image: confluentinc/cp-kafka:latest
container_name: kafka-3
ports:
- "29094:29094"
environment:
KAFKA_NODE_ID: 3
KAFKA_PROCESS_ROLES: 'broker,controller'
KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka-1:9093,2@kafka-2:9093,3@kafka-3:9093'
KAFKA_LISTENERS: 'PLAINTEXT://kafka-3:9092,CONTROLLER://kafka-3:9093,PLAINTEXT_HOST://0.0.0.0:29094'
KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka-3:9092,PLAINTEXT_HOST://${EC2_PRIVATE_IP}:29094'
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'
KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 2
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 2
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
CLUSTER_ID: 'ciWo7IWazngRchmPES6q5A=='
KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'
Expand All @@ -28,9 +72,11 @@ services:
- "8090:8080"
environment:
KAFKA_CLUSTERS_0_NAME: local
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka-1:9092
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka-1:9092,kafka-2:9092,kafka-3:9092
depends_on:
- kafka-1
- kafka-2
- kafka-3
networks:
- app-network

Expand All @@ -57,7 +103,7 @@ services:
- "80:8080"
environment:
SPRING_PROFILES_ACTIVE: prod
SPRING_KAFKA_BOOTSTRAP_SERVERS: kafka-1:9092
SPRING_KAFKA_BOOTSTRAP_SERVERS: kafka-1:9092,kafka-2:9092,kafka-3:9092
DB_HOST: ${DB_HOST}
DB_PORT: ${DB_PORT}
DB_NAME: ${DB_NAME}
Expand All @@ -68,6 +114,10 @@ services:
condition: service_healthy
kafka-1:
condition: service_started
kafka-2:
condition: service_started
kafka-3:
condition: service_started
restart: unless-stopped
networks:
- app-network
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.DynamicUpdate;

import java.util.ArrayList;
import java.util.List;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ public interface MemberRepository extends JpaRepository<Member, Long> {

Member findByLoginId(String loginId);

@Query("select m from Member m " +
"left join fetch m.memberSubjects " +
"where m.id = :id")
Optional<Member> findWithSubjectsById(@Param("id") Long id);
@Query("select m from Member m " +
"left join fetch m.memberSubjects ms " +
"left join fetch ms.subject " + // 어차피 membersubject에서 eager로 subject를 들고와서 N+1 이 생긴다면, 한번에 fetch join 하는 게?
"where m.id = :id")
Optional<Member> findWithSubjectsById(@Param("id") Long id);

Optional<Member> findById(long id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,13 @@ public ResponseEntity<Map<String, Object>> applyCourse(@RequestParam String code
Map<String, Object> response = new HashMap<>();

try {
long start = System.currentTimeMillis();
log.debug("Start to apply course with code {}", code);
// 대기열 등록
subjectService.enqueueCourseRequest(memberId, code);
long end = System.currentTimeMillis();

log.debug("Apply course request took " + (end - start) + " ms");

// 성공 시 JSON 응답
response.put("status", "WAITING");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class MemberSubject extends BaseEntity {
@JoinColumn(name = "member_id")
private Member member;

@ManyToOne(fetch = FetchType.EAGER)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "subject_id")
private Subject subject;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,15 @@ public class SubjectQueryService {

public Page<SubjectResponseDTO> searchAllSubject(Long memberId, CourseFilterRequestDTO filters, Pageable pageable) {

log.info("===========search 시작==============");
//log.info("===========search 시작==============");
Member member = findById(memberId);

String code = nullIfBlank(filters.getCode());
String professorName = nullIfBlank(filters.getProfessorName());
String subjectName = nullIfBlank(filters.getSubjectName());


log.info("===========code: " + code + " =======professorName : " + professorName + " ========subjectName" + subjectName + " \n");
//log.info("===========code: " + code + " =======professorName : " + professorName + " ========subjectName" + subjectName + " \n");
Page<Subject> subjects
= subjectRepository.findAllByCodeAndProfessorNameAndSubjectName(code, professorName, subjectName, pageable);

Expand All @@ -60,7 +60,7 @@ public Page<SubjectResponseDTO> searchAllSubject(Long memberId, CourseFilterRequ
List<Long> subjectIds = subjects.getContent().stream()
.map(Subject::getId)
.toList();
log.info("===========페이지 크기 : " + subjectIds.size());
//log.info("===========페이지 크기 : " + subjectIds.size());

Set<Long> registeredIds = memberSubjectRepository.findAllIdByMemberAndSubject(member, subjectIds);
Set<Long> likedIds = likeSubjectRepository.findAllByMemberAndSubject(member, subjectIds);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,18 @@ public void enqueueCourseRequest(Long memberId, String code) {
boolean held = false;
try {
// 멤버 찾기
Member member = findMemberById(memberId);
Member member = findMemberById(memberId);

// 해당 과목 찾기
Subject subject = findByCode(code);
Subject subject = findByCode(code);

// 유효성 검사
validateCheck(member, subject);
validateCheck(member, subject);

// 원자성 추가
waitQueueService.enqueueGlobal(memberId, subject.getId(), System.currentTimeMillis());
log.info("수강신청 접수 성공 (대기열 삽입). Course: {}, Member: {}", subject.getId(), memberId);
waitQueueService.enqueueGlobal(memberId, subject.getId(), System.currentTimeMillis());
// waitQueueService.enqueueGlobal(memberId, 1L, System.currentTimeMillis());
//log.info("수강신청 접수 성공 (대기열 삽입). Course: {}, Member: {}", subject.getId(), memberId);

} catch (ErrorHandler e) {
idempotencyService.releaseIdempotency(memberId, code);
Expand Down Expand Up @@ -140,14 +141,16 @@ public void applyCourseWithToken(Long memberId, String code) {
* - 신청가능학점을 넘긴경우 -> 위 코드에서 lua 결과로 판단
* */
private void validateCheck(Member member, Subject subject) {
if (memberSubjectRepository.findByMemberAndSubject(member, subject).isPresent()) {
boolean alreadyApplied = member.getMemberSubjects().stream()
.anyMatch(ms -> ms.getSubject().getId().equals(subject.getId()));

if (alreadyApplied) {
throw new ErrorHandler(ErrorStatus.ALREADY_APPLY_SUBJECT);
}

if (member.getRegisteredScore() + subject.getScore() > MAX_SCORE) {
throw new ErrorHandler(ErrorStatus.OVER_SOCRE_POSSIBLE);
}

boolean conflict = member.getMemberSubjects().stream()
.map(MemberSubject::getSubject)
.filter(subj ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class IdempotencyService {
public boolean rateLimitAllow(Long memberId, int limit, Duration ttl) {
String key = RedisKeyUtils.rateLimitKey(memberId);
Long cnt = stringRedisTemplate.opsForValue().increment(key);
System.out.println("cnt : " + cnt);
// System.out.println("cnt : " + cnt);
// 첫 요청인 경우 -> TTL 1로 세팅
if (cnt != null && cnt == 1L) {
stringRedisTemplate.expire(key, ttl);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ public class PublishIssueTokenScheduler {

private final LuaRepository luaRepository;

private static final int PERMITS_PER_TICKS = 50; // tick당 최대 발급 수
private static final int PERMITS_PER_TICKS = 100; // tick당 최대 발급 수
private static final int TOKEN_TTL = 5; // token ttl

@Scheduled(fixedDelay = 100)
// @Scheduled(fixedRate = 20)
public void issue() {
try {
long cnt = luaRepository.publishIssueTokens(PERMITS_PER_TICKS, TOKEN_TTL);
if (cnt > 0) {
log.info("토큰 발급 성공: count={}, sample={}", cnt);
// log.info("토큰 발급 성공: count={}, sample={}", cnt);
}
} catch (Exception e) {
log.error("토큰 발급 실패", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ public class WaitQueueService {

// 전역 대기열에 요청 삽입
public void enqueueGlobal(Long memberId, Long subjectId, long nowMillis) {
long start = System.currentTimeMillis(); // 측정 시작
String queueKey = RedisKeyUtils.globalApplyQueueKey();
String value = memberId + ":" + subjectId; // payload
redisTemplate.opsForZSet().add(queueKey, value, nowMillis);

Long size = redisTemplate.opsForZSet().size(queueKey);
log.info("대기열 등록: {}, queueSize={}", value, size);
long end = System.currentTimeMillis(); // 측정 종료
log.info("Redis [enqueueGlobal] 소요시간: {}ms", (end - start));
// Long size = redisTemplate.opsForZSet().size(queueKey);
// log.info("대기열 등록: {}, queueSize={}", value, size);
}

// 사용자에게 보여줄 대기열 순번 조회
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public Long getUserId() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();

String userId = request.getHeader("userId");
// System.out.println("userId = " + userId);
if (userId == null || userId.isEmpty()){
throw new ErrorHandler(ErrorStatus._UNAUTHORIZED);
}
Expand Down
11 changes: 11 additions & 0 deletions src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ spring:
resources:
add-mappings: false

logging:
level:
io.lettuce.core.protocol: DEBUG
org.springframework.data.redis: DEBUG
org:
hibernate:
SQL: debug
# apache.tomcat.util.http.Parameters: debug
# springframework.web.servlet.DispatcherServlet: debug
# com.practice.course_registration: debug

server:
servlet:
session:
Expand Down
15 changes: 6 additions & 9 deletions src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
spring:
data:
redis:
host: ${SPRING_DATA_REDIS_HOST:redis}
port: 6379

h2:
console:
enabled: false
Expand All @@ -20,16 +25,8 @@ spring:
properties:
hibernate:
format_sql: true
open-in-view: false

data:
redis:
host: redis
port: 6379
connect-timeout: 2000
database: 0

# mvc:
# throw-exception-if-no-handler-found: true
web:
resources:
add-mappings: false
Expand Down