Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ out/
crew-service/src/main/resources/properties/env.properties
competition-service/src/main/resources/properties/env.properties
logs/competition-service.log
logs/competition-service.*
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com._42195km.msa.competitionservice.application.context;

import org.springframework.stereotype.Component;

@Component
public class SagaContextHolder {
private static final ThreadLocal<String> CURRENT_SAGA_ID = new ThreadLocal<>();

/**
* 현재 스레드의 Saga ID 설정
* @param sagaId 설정할 Saga ID
*/
public static void setCurrentSagaId(String sagaId) {
CURRENT_SAGA_ID.set(sagaId);
}

/**
* 현재 스레드의 Saga ID 조회
* @return 현재 Saga ID, 없으면 null
*/
public static String getCurrentSagaId() {
return CURRENT_SAGA_ID.get();
}

/**
* 현재 스레드의 Saga 컨텍스트 정보 제거
*/
public static void clear() {
CURRENT_SAGA_ID.remove();
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com._42195km.msa.competitionservice.application.facade;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
Expand All @@ -15,17 +17,73 @@
import com._42195km.msa.competitionservice.application.service.CompetitionService;
import com._42195km.msa.competitionservice.application.service.ParticipantService;
import com._42195km.msa.competitionservice.application.service.SagaService;
import com._42195km.msa.competitionservice.domain.model.ApplicationSession;
import com._42195km.msa.competitionservice.domain.model.ApplicationStep;
import com._42195km.msa.competitionservice.domain.repository.ApplicationSessionRepository;
import com._42195km.msa.competitionservice.presentation.dto.request.CancelParticipantRequestDto;

import lombok.RequiredArgsConstructor;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Timer;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@RequiredArgsConstructor
public class CompetitionApplicationFacade {
private final CompetitionService competitionService;
private final ParticipantService participantService;
private final SagaService sagaService;

private final ApplicationSessionRepository sessionRepository; // 모니터링을 위한 세션 추가

private final Counter applicationStartCounter;
private final Counter applicationCompleteCounter;
private final Counter applicationFailCounter;
private final Counter termsAgreementCounter;
private final Counter souvenirSelectionCounter;
private final Counter shippingAddressCounter;
private final Counter paymentCounter;
private final Timer applicationTotalTimeTimer;
private final Timer termsStepTimeTimer;
private final Timer souvenirStepTimeTimer;
private final Timer shippingStepTimeTimer;
private final Timer paymentStepTimeTimer;

public CompetitionApplicationFacade(
CompetitionService competitionService,
ParticipantService participantService,
SagaService sagaService,
ApplicationSessionRepository sessionRepository,
@Qualifier("applicationStartCounter") Counter applicationStartCounter,
@Qualifier("applicationCompleteCounter") Counter applicationCompleteCounter,
@Qualifier("applicationFailCounter") Counter applicationFailCounter,
@Qualifier("termsAgreementCounter") Counter termsAgreementCounter,
@Qualifier("souvenirSelectionCounter") Counter souvenirSelectionCounter,
@Qualifier("shippingAddressCounter") Counter shippingAddressCounter,
@Qualifier("paymentCounter") Counter paymentCounter,
@Qualifier("applicationTotalTimeTimer") Timer applicationTotalTimeTimer,
@Qualifier("termsStepTimeTimer") Timer termsStepTimeTimer,
@Qualifier("souvenirStepTimeTimer") Timer souvenirStepTimeTimer,
@Qualifier("shippingStepTimeTimer") Timer shippingStepTimeTimer,
@Qualifier("paymentStepTimeTimer") Timer paymentStepTimeTimer
) {
this.competitionService = competitionService;
this.participantService = participantService;
this.sagaService = sagaService;
this.sessionRepository = sessionRepository;
this.applicationStartCounter = applicationStartCounter;
this.applicationCompleteCounter = applicationCompleteCounter;
this.applicationFailCounter = applicationFailCounter;
this.termsAgreementCounter = termsAgreementCounter;
this.souvenirSelectionCounter = souvenirSelectionCounter;
this.shippingAddressCounter = shippingAddressCounter;
this.paymentCounter = paymentCounter;
this.applicationTotalTimeTimer = applicationTotalTimeTimer;
this.termsStepTimeTimer = termsStepTimeTimer;
this.souvenirStepTimeTimer = souvenirStepTimeTimer;
this.shippingStepTimeTimer = shippingStepTimeTimer;
this.paymentStepTimeTimer = paymentStepTimeTimer;
}
Comment on lines +51 to +85
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

메트릭 Bean 주입의 가독성 및 유지보수성 개선 제안

현재 생성자에 10개 이상의 Counter, Timer가 개별 주입되고 있어 서명과 DI 설정이 장황합니다.
빈 수가 늘어날 경우 생성자가 급격히 비대해지므로, 전용 DTO/컨테이너(MetricsHolder)로 래핑하거나
Map<String, Counter> 형태로 주입받아 이름으로 조회하도록 리팩터링을 고려해 보세요.

장점

  1. 생성자 간결화 및 테스트 용이성 향상
  2. 메트릭 추가‧삭제 시 파사드 코드 변경 최소화
  3. Bean Qualifier 남발로 인한 오타‧중복 위험 감소

선택 사항이지만 장기적으로 운영 서비스의 가독성을 크게 높일 수 있습니다.


public void createCompetition(CreateCompetitionCommandDto command) {
competitionService.createCompetition(command);
}
Expand Down Expand Up @@ -60,7 +118,47 @@ public void deleteCompetition(UUID competitionId) {
* 대회 신청 프로세스
*/
public String applyForCompetition(CompleteAppDto appDto) {
return sagaService.processCompleteApplication(appDto);
// 세션 조회 또는 생성
ApplicationSession session = sessionRepository.findByCompetitionAndParticipant(
appDto.getCompetitionId(), appDto.getParticipantId());

if (session == null) {
// 새로운 신청 세션 시작
session = ApplicationSession.start(appDto.getCompetitionId(), appDto.getParticipantId());
applicationStartCounter.increment();
log.info("새로운 대회 신청 세션 시작: competitionId={}, participantId={}, sessionId={}",
appDto.getCompetitionId(), appDto.getParticipantId(), session.getSessionId());
}

try {
// 현재 단계 결정
ApplicationStep currentStep = determineCurrentStep(appDto);

// 서비스 호출하여 실제 처리
String response = sagaService.processCompleteApplication(appDto);

// 결과에 따라 세션 업데이트 및 메트릭 기록
updateSessionAndMetrics(session, currentStep, appDto, response);

Comment on lines +134 to +142
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

사용되지 않는 매개변수 제거 또는 활용

currentStepresponseupdateSessionAndMetrics 인자로 전달하지만 내부에서 전혀 사용되지 않습니다.
경고를 유발할 뿐 아니라 향후 유지보수 시 혼란을 초래하므로 제거하거나 실제 로깅/메트릭에 활용하세요.

-ApplicationStep currentStep = determineCurrentStep(appDto);
-...
-updateSessionAndMetrics(session, currentStep, appDto, response);
+// currentStep가 불필요하다면 아래처럼 단순화
+updateSessionAndMetrics(session, appDto);
-private void updateSessionAndMetrics(ApplicationSession session, ApplicationStep currentStep,
-    CompleteAppDto appDto, String response) {
+private void updateSessionAndMetrics(ApplicationSession session, CompleteAppDto appDto) {

불필요한 인자를 제거하면 코드가 간결해지고 의도가 명확해집니다.

Also applies to: 217-220

// 세션 저장
sessionRepository.saveSession(session);

log.debug("Session state after processing: termsAgreedTime={}, souvenirSelectedTime={}, shippingEnteredTime={}, paymentCompletedTime={}, completedTime={}",
session.getTermsAgreedTime(),
session.getSouvenirSelectedTime(),
session.getShippingEnteredTime(),
session.getPaymentCompletedTime(),
session.getCompletedTime());

return response;
} catch (Exception e) {
// 실패 처리
session.fail(e.getMessage());
sessionRepository.saveSession(session);
applicationFailCounter.increment();
log.error("대회 신청 처리 실패: sessionId={}, error={}", session.getSessionId(), e.getMessage(), e);
throw e;
}
}

/**
Expand All @@ -83,7 +181,8 @@ public Page<ParticipantAppResponseDto> getParticipants(Pageable pageable, UUID c
return participantService.getParticipants(pageable, competitionId);
}

public Page<SearchParticipantAppResponseDto> searchParticipants(String keyword, String searchType, Pageable pageable) {
public Page<SearchParticipantAppResponseDto> searchParticipants(String keyword, String searchType,
Pageable pageable) {
return participantService.searchParticipants(keyword, searchType, pageable);
}

Expand All @@ -98,4 +197,86 @@ public void cancelParticipantByCompany(CancelParticipantRequestDto requestDto) {
public void cancelParticipant(CancelParticipantRequestDto requestDto) {
participantService.cancelParticipant(requestDto);
}

// 현재 단계 결정 메서드
private ApplicationStep determineCurrentStep(CompleteAppDto appDto) {
if (appDto.getTermsAgreed() != null)
return ApplicationStep.TERMS_AGREEMENT;
if (appDto.getSouvenirSelection() != null)
return ApplicationStep.SOUVENIR_SELECTION;
if (appDto.getShippingAddress() != null)
return ApplicationStep.SHIPPING_ADDRESS;
if (appDto.getPaymentMethod() != null)
return ApplicationStep.PAYMENT_PENDING;
if (appDto.getPaymentStatus() != null && appDto.getTransactionId() != null)
return ApplicationStep.PAYMENT_COMPLETED;
return ApplicationStep.TERMS_AGREEMENT;
}

// 세션 업데이트 및 메트릭 기록 메서드
private void updateSessionAndMetrics(ApplicationSession session, ApplicationStep currentStep,
CompleteAppDto appDto, String response) {

// 세션에 단계별 데이터 확인 및 업데이트를 순차적으로 진행

// 1. 약관 동의 단계 (항상 처음부터 확인)
if (appDto.getTermsAgreed() != null && appDto.getTermsAgreed()) {
if (session.getTermsAgreedTime() == null) {
session.completeTermsAgreement();
termsAgreementCounter.increment();
termsStepTimeTimer.record(session.getTermsStepTimeMillis(), TimeUnit.MILLISECONDS);
log.info("약관 동의 단계 완료: sessionId={}, 소요시간={}ms",
session.getSessionId(), session.getTermsStepTimeMillis());
}

// 2. 약관 동의가 완료된 후에만 기념품 선택 단계 처리
if (appDto.getSouvenirSelection() != null && session.getTermsAgreedTime() != null) {
if (session.getSouvenirSelectedTime() == null) {
session.completeSouvenirSelection();
souvenirSelectionCounter.increment();
souvenirStepTimeTimer.record(session.getSouvenirStepTimeMillis(), TimeUnit.MILLISECONDS);
log.info("기념품 선택 단계 완료: sessionId={}, 소요시간={}ms",
session.getSessionId(), session.getSouvenirStepTimeMillis());
}

// 3. 기념품 선택이 완료된 후에만 배송지 입력 단계 처리
if (appDto.getShippingAddress() != null && session.getSouvenirSelectedTime() != null) {
if (session.getShippingEnteredTime() == null) {
session.completeShippingAddress();
shippingAddressCounter.increment();
shippingStepTimeTimer.record(session.getShippingStepTimeMillis(), TimeUnit.MILLISECONDS);
log.info("배송지 입력 단계 완료: sessionId={}, 소요시간={}ms",
session.getSessionId(), session.getShippingStepTimeMillis());
}

// 4. 배송지 입력이 완료된 후에만 결제 완료 단계 처리
if (appDto.getPaymentStatus() != null && "SUCCESS".equals(appDto.getPaymentStatus())
&& session.getShippingEnteredTime() != null) {
if (session.getPaymentCompletedTime() == null) {
session.completePayment();
paymentCounter.increment();
paymentStepTimeTimer.record(session.getPaymentStepTimeMillis(), TimeUnit.MILLISECONDS);

// 모든 단계가 완료되었는지 확인하여 신청 완료 처리
if (isAllStepsCompleted(session)) {
session.complete();
applicationCompleteCounter.increment();
applicationTotalTimeTimer.record(session.getTotalTimeMillis(), TimeUnit.MILLISECONDS);
log.info("대회 신청 프로세스 완료: sessionId={}, 총 소요시간={}ms",
session.getSessionId(), session.getTotalTimeMillis());
}
}
}
}
}
}
}
Comment on lines +222 to +273
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

후속 단계가 처리되지 않는 논리적 결함 수정 필요

updateSessionAndMetrics 메서드의 최상위 조건문이 termsAgreed 체크로 감싸져 있어,
약관 동의 이후 단계(기념품 선택, 배송지 입력, 결제 완료)가 요청 DTO에 termsAgreed 필드가 다시 전달되지 않으면 절대 실행되지 않습니다.
프론트엔드가 단계별로 필요한 필드만 전송할 가능성이 높기 때문에, 실제 운영 시 후속 단계가 진행되지 않는 치명적 버그가 발생합니다.

-// 1. 약관 동의 단계 (항상 처음부터 확인)
-if (appDto.getTermsAgreed() != null && appDto.getTermsAgreed()) {
-    ...
-}
+// 1. 약관 동의 단계
+if (appDto.getTermsAgreed() != null && appDto.getTermsAgreed()
+    && session.getTermsAgreedTime() == null) {
+    session.completeTermsAgreement();
+    ...
+}
+
+// 2. 기념품 선택 단계 (선행 단계 완료 여부만 확인)
+if (appDto.getSouvenirSelection() != null
+    && session.getTermsAgreedTime() != null
+    && session.getSouvenirSelectedTime() == null) {
+    ...
+}
+
+// 3. 배송지 입력 단계
+if (appDto.getShippingAddress() != null
+    && session.getSouvenirSelectedTime() != null
+    && session.getShippingEnteredTime() == null) {
+    ...
+}
+
+// 4. 결제 완료 단계
+if ("SUCCESS".equals(appDto.getPaymentStatus())
+    && session.getShippingEnteredTime() != null
+    && session.getPaymentCompletedTime() == null) {
+    ...
+}

위와 같이 각 단계별로 독립적인 if 블록을 두고, 선행 단계 완료 여부만 확인하도록 수정해야 합니다.
그렇지 않으면 메트릭 누락 및 사용자 불만이 발생할 수 있습니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 1. 약관 동의 단계 (항상 처음부터 확인)
if (appDto.getTermsAgreed() != null && appDto.getTermsAgreed()) {
if (session.getTermsAgreedTime() == null) {
session.completeTermsAgreement();
termsAgreementCounter.increment();
termsStepTimeTimer.record(session.getTermsStepTimeMillis(), TimeUnit.MILLISECONDS);
log.info("약관 동의 단계 완료: sessionId={}, 소요시간={}ms",
session.getSessionId(), session.getTermsStepTimeMillis());
}
// 2. 약관 동의가 완료된 후에만 기념품 선택 단계 처리
if (appDto.getSouvenirSelection() != null && session.getTermsAgreedTime() != null) {
if (session.getSouvenirSelectedTime() == null) {
session.completeSouvenirSelection();
souvenirSelectionCounter.increment();
souvenirStepTimeTimer.record(session.getSouvenirStepTimeMillis(), TimeUnit.MILLISECONDS);
log.info("기념품 선택 단계 완료: sessionId={}, 소요시간={}ms",
session.getSessionId(), session.getSouvenirStepTimeMillis());
}
// 3. 기념품 선택이 완료된 후에만 배송지 입력 단계 처리
if (appDto.getShippingAddress() != null && session.getSouvenirSelectedTime() != null) {
if (session.getShippingEnteredTime() == null) {
session.completeShippingAddress();
shippingAddressCounter.increment();
shippingStepTimeTimer.record(session.getShippingStepTimeMillis(), TimeUnit.MILLISECONDS);
log.info("배송지 입력 단계 완료: sessionId={}, 소요시간={}ms",
session.getSessionId(), session.getShippingStepTimeMillis());
}
// 4. 배송지 입력이 완료된 후에만 결제 완료 단계 처리
if (appDto.getPaymentStatus() != null && "SUCCESS".equals(appDto.getPaymentStatus())
&& session.getShippingEnteredTime() != null) {
if (session.getPaymentCompletedTime() == null) {
session.completePayment();
paymentCounter.increment();
paymentStepTimeTimer.record(session.getPaymentStepTimeMillis(), TimeUnit.MILLISECONDS);
// 모든 단계가 완료되었는지 확인하여 신청 완료 처리
if (isAllStepsCompleted(session)) {
session.complete();
applicationCompleteCounter.increment();
applicationTotalTimeTimer.record(session.getTotalTimeMillis(), TimeUnit.MILLISECONDS);
log.info("대회 신청 프로세스 완료: sessionId={}, 총 소요시간={}ms",
session.getSessionId(), session.getTotalTimeMillis());
}
}
}
}
}
}
}
// 1. 약관 동의 단계
if (appDto.getTermsAgreed() != null
&& appDto.getTermsAgreed()
&& session.getTermsAgreedTime() == null) {
session.completeTermsAgreement();
termsAgreementCounter.increment();
termsStepTimeTimer.record(session.getTermsStepTimeMillis(), TimeUnit.MILLISECONDS);
log.info("약관 동의 단계 완료: sessionId={}, 소요시간={}ms",
session.getSessionId(), session.getTermsStepTimeMillis());
}
// 2. 기념품 선택 단계 (선행 단계 완료 여부만 확인)
if (appDto.getSouvenirSelection() != null
&& session.getTermsAgreedTime() != null
&& session.getSouvenirSelectedTime() == null) {
session.completeSouvenirSelection();
souvenirSelectionCounter.increment();
souvenirStepTimeTimer.record(session.getSouvenirStepTimeMillis(), TimeUnit.MILLISECONDS);
log.info("기념품 선택 단계 완료: sessionId={}, 소요시간={}ms",
session.getSessionId(), session.getSouvenirStepTimeMillis());
}
// 3. 배송지 입력 단계
if (appDto.getShippingAddress() != null
&& session.getSouvenirSelectedTime() != null
&& session.getShippingEnteredTime() == null) {
session.completeShippingAddress();
shippingAddressCounter.increment();
shippingStepTimeTimer.record(session.getShippingStepTimeMillis(), TimeUnit.MILLISECONDS);
log.info("배송지 입력 단계 완료: sessionId={}, 소요시간={}ms",
session.getSessionId(), session.getShippingStepTimeMillis());
}
// 4. 결제 완료 단계
if ("SUCCESS".equals(appDto.getPaymentStatus())
&& session.getShippingEnteredTime() != null
&& session.getPaymentCompletedTime() == null) {
session.completePayment();
paymentCounter.increment();
paymentStepTimeTimer.record(session.getPaymentStepTimeMillis(), TimeUnit.MILLISECONDS);
// 모든 단계가 완료되었는지 확인하여 신청 완료 처리
if (isAllStepsCompleted(session)) {
session.complete();
applicationCompleteCounter.increment();
applicationTotalTimeTimer.record(session.getTotalTimeMillis(), TimeUnit.MILLISECONDS);
log.info("대회 신청 프로세스 완료: sessionId={}, 총 소요시간={}ms",
session.getSessionId(), session.getTotalTimeMillis());
}
}


// 모든 필수 단계가 완료되었는지 확인하는 헬퍼 메서드
private boolean isAllStepsCompleted(ApplicationSession session) {
return session.getTermsAgreedTime() != null &&
session.getSouvenirSelectedTime() != null &&
session.getShippingEnteredTime() != null &&
session.getPaymentCompletedTime() != null;
}
}
Loading