Conversation
Summary by CodeRabbit
WalkthroughKotlin 기반 구성과 코드를 전반적으로 Java로 전환했습니다. 두 앱(commerce-streamer, pg-simulator)에서 Kotlin 파일을 제거하고 동등한 Java 진입점/구성/구현을 추가했으며, Kafka 모듈에서도 Kotlin 설정을 Java로 대체했습니다. Gradle에서 Kotlin 플러그인과 관련 의존성을 제거하고 Java 전용 구성을 유지했습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as Client
participant API as PaymentApi
participant SVC as PaymentApplicationService
participant REPO as PaymentRepository
participant EVT as PaymentEventPublisher
participant LSN as PaymentEventListener
participant RELAY as PaymentRelay
U->>API: POST /api/v1/payments
API->>SVC: createTransaction(command)
SVC->>REPO: save(Payment)
SVC->>EVT: publish(PaymentCreated)
Note right of EVT: AFTER_COMMIT
EVT-->>LSN: PaymentCreated
rect rgba(230,245,255,0.5)
note over LSN: 비동기 처리
LSN->>SVC: handle(transactionKey)
SVC->>REPO: findByTransactionKey()
SVC->>REPO: save(updated Payment)
SVC->>EVT: publish(PaymentHandled)
end
EVT-->>LSN: PaymentHandled
LSN->>SVC: notifyTransactionResult(transactionKey)
SVC->>REPO: findByTransactionKey()
SVC->>RELAY: notify(callbackUrl, TransactionInfo)
API-->>U: ApiResponse(TransactionResponse)
sequenceDiagram
autonumber
participant K as Kafka
participant C as DemoKafkaConsumer
participant DLT as DLT Handler
K-->>C: batch messages (RetryableTopic)
C->>C: process batch
alt success
C->>K: manual ack
else failure after retries
K-->>DLT: route to DLT
DLT->>DLT: log metadata/payload
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 12
🧹 Nitpick comments (40)
apps/pg-simulator/src/main/java/com/loopers/PaymentGatewayApplication.java (3)
7-7: @EnableAsync 사용 시 실행자(Executor) 명시 및 스레드풀 한도 설정 권장기본 SimpleAsyncTaskExecutor는 무제한 스레드를 만들 수 있어 시뮬레이션 부하에서 폭주 위험이 있습니다. ThreadPoolTaskExecutor 빈을 명시하거나, @async를 사용하지 않는다면 @EnableAsync 제거를 고려해 주세요.
다음과 같이 최소 실행자를 추가하는 것을 제안드립니다:
@@ import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import java.util.concurrent.Executor; @@ public class PaymentGatewayApplication { + @Bean(name = "applicationTaskExecutor") + public Executor applicationTaskExecutor() { + ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor(); + ex.setCorePoolSize(4); + ex.setMaxPoolSize(16); + ex.setQueueCapacity(1000); + ex.setThreadNamePrefix("pg-async-"); + ex.initialize(); + return ex; + }Also applies to: 12-12
16-23: 타임존 초기화 시점은 main에서 설정 권장일부 서드파티/드라이버는 빈 초기화 이전에 시스템 타임존을 읽을 수 있어 @PostConstruct보다 main에서 먼저 설정하는 편이 안전합니다. 아래와 같이 이동하면 부트스트랩 초기에 확정됩니다. (여러 앱에서 공통 패턴이므로, 장기적으로는 공통 스타터/구성으로의 추출도 고려해 볼 만합니다.)
@@ - @PostConstruct - public void started() { - TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); - } - - public static void main(String[] args) { - SpringApplication.run(PaymentGatewayApplication.class, args); - } + public static void main(String[] args) { + // JVM 전역 타임존을 가장 이른 시점에 설정 + TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); + SpringApplication.run(PaymentGatewayApplication.class, args); + }
13-13: 컴포넌트 스캔 범위 검토(선택)현재 루트 패키지 기준 스캔으로도 문제는 없어 보이지만, 모듈 의존이 넓다면 의도치 않은 빈 포함 가능성이 있습니다. 필요 시 이 앱 전용 하위 패키지로 스캔 범위를 좁히는 것을 고려해 주세요.
docs/retrospective-10weeks.md (5)
241-242: 예외 삼키기 금지: 관측 가능성 저하
catch (Exception ignored) {}는 실패를 은닉합니다. 최소 WARN 로그 또는 메트릭을 남겨 주세요.-private void publishUserActionEvent(UserActionEvent event) { - try { eventPublisher.publishEvent(event); } catch (Exception ignored) {} -} +private void publishUserActionEvent(UserActionEvent event) { + try { + eventPublisher.publishEvent(event); + } catch (Exception e) { + log.warn("Failed to publish event: type={}, id={}", event.getClass().getSimpleName(), event.getId(), e); + meterRegistry.counter("events.publish.fail", "type", event.getClass().getSimpleName()).increment(); + } +}
255-261: AFTER_COMMIT + 비트랜잭션 카프카 전송: 손실 창구 주의사항 명시 권장DB 커밋 후 전송이 실패하면 이벤트 손실 가능성이 있습니다. 문서에 운영 가드(acks=all, 적절한 retries/backoff, delivery.timeout.ms, 재시도/알람) 또는 Outbox 전환 기준을 명시해 주세요.
280-283: 컨슈머 트랜잭션 사용 여부 문서 내 일관성 필요위 예시(라인 69–76)는
@Transactional을 사용하지만, 아래 축약 예시(라인 280–283)에는 빠져 있습니다. 동일 정책으로 정리해 주세요(사용 또는 비사용).-@KafkaListener(... ) -@RetryableTopic(...) -public void handleRankingEvents(List<ConsumerRecord<String, String>> messages, Acknowledgment ack) { /* ... */ } +@KafkaListener(... ) +@RetryableTopic(...) +@Transactional +public void handleRankingEvents(List<ConsumerRecord<String, String>> messages, Acknowledgment ack) { /* ... */ }Also applies to: 69-76
149-155: Rawconn.execute대신 타입세이프 API 사용 권장문자열 기반 커맨드는 드라이버 교체/인자 순서 오류에 취약합니다.
ZSetOperations또는 커넥션의 전용 API로 교체해 주세요(가중치 필요 시 커넥션 레벨).예시(가중치 불필요 시):
ZSetOperations<String, String> zset = redisTemplate.opsForZSet(); zset.unionAndStore(src, List.of(), dest); redisTemplate.expire(dest, TTL);예시(가중치 필요 시, 드라이버별 API 사용):
redisTemplate.execute(conn -> { var c = (RedisZSetCommands) conn; c.zUnionStore(dest.getBytes(UTF_8), ZAggregateArgs.weights(new double[]{weight}).sum(), List.of(src.getBytes(UTF_8))); conn.keyCommands().expire(dest.getBytes(UTF_8), TTL.getSeconds()); return null; });
124-132: “간소화된 아웃박스” 표현에 리스크 각주 추가 권장현재 방식은 트랜잭션 외부 전송이라 완전한 Outbox가 아닙니다. 장애/재배포 시 손실 가능성, 전환 조건(Outbox 테이블, 전용 릴레이어, 재처리 플로우)을 각주로 명시하면 좋습니다.
apps/pg-simulator/src/main/java/com/loopers/domain/payment/CardType.java (1)
3-7: 외부 입력 역직렬화 보호(UNKNOWN 추가 또는 DTO 매핑 고정) 제안직접 enum 역직렬화 시 알 수 없는 값으로 400이 날 수 있습니다. UNKNOWN 추가 또는 DTO에서 문자열 매핑을 고정하고 도메인 변환 시 검증을 권장합니다.
public enum CardType { + UNKNOWN, SAMSUNG, KB, HYUNDAI }apps/pg-simulator/src/main/java/com/loopers/domain/payment/PaymentEventPublisher.java (1)
3-6: 오버로드 사용 괜찮음 — 확장성 고려 시 단일 수퍼타입도 옵션현재 형태로 충분합니다. 이벤트가 늘어날 경우
publish(PaymentEvent event)단일 시그니처로의 수렴을 검토해도 좋습니다.apps/pg-simulator/src/main/java/com/loopers/domain/user/UserInfo.java (1)
3-11: VO로서의 방어적 검증/동등성 구현 추가 제안널/공백 방지와 equals/hashCode/toString을 구현하면 컬렉션 키 등에서 안전합니다.
package com.loopers.domain.user; +import java.util.Objects; + public class UserInfo { private final String userId; public UserInfo(String userId) { - this.userId = userId; + if (userId == null || userId.isBlank()) { + throw new IllegalArgumentException("userId must not be blank"); + } + this.userId = userId; } public String getUserId() { return userId; } + + @Override public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof UserInfo)) return false; + UserInfo that = (UserInfo) o; + return userId.equals(that.userId); + } + @Override public int hashCode() { return userId.hashCode(); } + @Override public String toString() { return "UserInfo{userId='" + userId + "'}"; } }apps/pg-simulator/src/main/java/com/loopers/application/payment/OrderInfo.java (1)
9-16: 불변성 보강(방어적 복사/래핑) 제안리스트를 그대로 보관하면 외부에서 변경 가능합니다. 방어적 복사 후 불변 래핑을 권장합니다.
import java.util.List; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Objects; public class OrderInfo { private final String orderId; private final List<TransactionInfo> transactions; public OrderInfo(String orderId, List<TransactionInfo> transactions) { - this.orderId = orderId; - this.transactions = transactions; + this.orderId = Objects.requireNonNull(orderId, "orderId"); + this.transactions = Collections.unmodifiableList(new ArrayList<>(Objects.requireNonNull(transactions, "transactions"))); }apps/pg-simulator/src/main/java/com/loopers/domain/payment/PaymentRelay.java (1)
5-7: 반환/피드백 채널 고려현재 fire-and-forget입니다. 재시도/백오프/데드레터 등 운영정책을 넣을 계획이라면 결과(boolean/enum) 또는 예외 전파가 유용할 수 있습니다. 현 단계에선 OK.
apps/pg-simulator/src/main/java/com/loopers/config/web/WebMvcConfig.java (1)
13-16: new로 직접 생성 대신 Bean/주입으로 등록하세요직접
new UserInfoArgumentResolver()하면 DI/프록시 혜택을 못 받아 테스트/확장성이 떨어집니다. Bean으로 노출하고 주입해 사용하는 방식을 권장합니다. (선호도에 따라resolvers.add(0, ...)로 우선순위도 줄 수 있습니다.)적용 예시:
@@ -import com.loopers.interfaces.api.argumentresolver.UserInfoArgumentResolver; +import com.loopers.interfaces.api.argumentresolver.UserInfoArgumentResolver; +import org.springframework.context.annotation.Bean; @@ public class WebMvcConfig implements WebMvcConfigurer { + @Bean + public UserInfoArgumentResolver userInfoArgumentResolver() { + return new UserInfoArgumentResolver(); + } + @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { - resolvers.add(new UserInfoArgumentResolver()); + resolvers.add(userInfoArgumentResolver()); } }추가로, Resolver를 컴포넌트 스캔으로 관리하고 싶다면 해당 클래스에
@Component를 부여하는 방법도 있습니다.apps/pg-simulator/src/main/java/com/loopers/infrastructure/payment/PaymentCoreRepository.java (2)
11-12: Repository 계층에는 @repository 사용 권장 (예외 번역 이점)
@Component대신@Repository를 사용하면 Spring 데이터 접근 예외 번역이 적용됩니다.-import org.springframework.stereotype.Component; +import org.springframework.stereotype.Repository; @@ -@Component +@Repository public class PaymentCoreRepository implements PaymentRepository {
8-10: 정렬은 DB로 위임해 I/O/메모리 비용 및 코드 복잡도 감소애플리케이션에서 정렬하지 말고 파생 쿼리 메서드로 DB 정렬을 요청하세요.
@@ -import java.util.Comparator; import java.util.List; @@ public List<Payment> findByOrderId(String userId, String orderId) { - List<Payment> list = paymentJpaRepository.findByUserIdAndOrderId(userId, orderId); - list.sort(Comparator.comparing(Payment::getUpdatedAt).reversed()); - return list; + return paymentJpaRepository.findByUserIdAndOrderIdOrderByUpdatedAtDesc(userId, orderId); }해당 변경에 맞춰
PaymentJpaRepository에 정렬 메서드를 추가해야 합니다(아래 코멘트 참고).Also applies to: 40-44
apps/pg-simulator/src/main/java/com/loopers/infrastructure/payment/PaymentJpaRepository.java (1)
8-11: 정렬 포함 파생 쿼리 추가로 애플리케이션 정렬 제거서비스/레포에서의 수동 정렬을 대체하기 위해 정렬 포함 메서드를 추가하세요.
public interface PaymentJpaRepository extends JpaRepository<Payment, String> { Payment findByUserIdAndTransactionKey(String userId, String transactionKey); List<Payment> findByUserIdAndOrderId(String userId, String orderId); + List<Payment> findByUserIdAndOrderIdOrderByUpdatedAtDesc(String userId, String orderId); }apps/pg-simulator/src/main/java/com/loopers/domain/payment/PaymentRepository.java (1)
5-10: Optional 반환 고려(널 회피 및 의도 명확화)
findBy*가null을 반환하는 계약은 오류 유발 여지가 있습니다.Optional<Payment>로 바꾸면 호출 측 의도가 분명해집니다. 현재 호출부가null체크를 하고 있어 즉시 변경은 비권장이나, 후속 리팩터의 방향성으로 제안드립니다.apps/pg-simulator/src/main/java/com/loopers/application/payment/PaymentCommand.java (1)
25-29: 필수 필드 검증 보강 필요 (NPE/불완전 요청 방지)
amount외에도userId/orderId/cardType/cardNo/callbackUrl의 null/blank 검증이 필요합니다.public void validate() { - if (amount <= 0L) { - throw new CoreException(ErrorType.BAD_REQUEST, "요청 금액은 0 보다 큰 정수여야 합니다."); - } + if (userId == null || userId.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "userId는 필수입니다."); + } + if (orderId == null || orderId.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "orderId는 필수입니다."); + } + if (cardType == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "cardType은 필수입니다."); + } + if (cardNo == null || cardNo.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "cardNo는 필수입니다."); + } + if (amount <= 0L) { + throw new CoreException(ErrorType.BAD_REQUEST, "요청 금액은 0 보다 큰 정수여야 합니다."); + } + if (callbackUrl == null || callbackUrl.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "callbackUrl은 필수입니다."); + } }apps/pg-simulator/src/main/java/com/loopers/domain/payment/PaymentEvent.java (1)
3-40: 이벤트 DTO 구성 깔끔합니다불변 필드, 팩토리 메서드 사용 모두 적절합니다. Java 17+ 사용 시
record로 단순화하는 것도 고려할 수 있습니다.apps/pg-simulator/src/main/java/com/loopers/support/error/CoreException.java (2)
7-17: 생성자 공통화 + null 방어 로직 추가 제안
errorType가 null이면 NPE 위험이 있습니다. 생성자 위임(this(...))으로 공통화하고, 메시지 계산 시에만 null 체크를 수행하도록 정리하면 안전합니다.다음 패치를 제안합니다:
+import java.util.Objects; public class CoreException extends RuntimeException { private final ErrorType errorType; private final String customMessage; public CoreException(ErrorType errorType) { - super(errorType.getMessage()); - this.errorType = errorType; - this.customMessage = null; + this(errorType, null); } public CoreException(ErrorType errorType, String customMessage) { - super(customMessage != null ? customMessage : errorType.getMessage()); + super(message(errorType, customMessage)); this.errorType = errorType; this.customMessage = customMessage; }추가: 클래스 내부에 아래 헬퍼 메서드를 정의해주세요(파일 내 적절한 위치).
private static String message(ErrorType errorType, String customMessage) { Objects.requireNonNull(errorType, "errorType"); return customMessage != null ? customMessage : errorType.getMessage(); }
3-5: 직렬화 경고 방지용 serialVersionUID 권장런타임 예외는
Serializable이므로serialVersionUID를 명시해두면 경고를 줄일 수 있습니다.public class CoreException extends RuntimeException { + private static final long serialVersionUID = 1L; private final ErrorType errorType; private final String customMessage;apps/pg-simulator/src/main/java/com/loopers/interfaces/api/payment/PaymentApi.java (2)
50-53: orderId 파라미터는 필수로 받는 편이 안전현재
required = false인데, null/blank로 들어오면 서비스에서 404를 던지거나 저장소 레벨로 null이 전달됩니다. API 계약상 필수라면 명시적으로 required=true(기본값)로 두는 것이 좋습니다.- @RequestParam(value = "orderId", required = false) String orderId + @RequestParam("orderId") String orderId
20-38: 중복 결제 방지(멱등성) 고려 필요POST 재시도/중복 클릭 시 같은 주문에 다수의 트랜잭션이 생성될 수 있습니다. 헤더 기반 멱등 키 또는 (userId, orderId, PENDING) 유니크 제약/검증을 통해 중복 생성을 차단하는 방안을 제안합니다.
멱등 키 정책(예:
Idempotency-Key헤더) 도입 여부를 결정해 주세요. 원하시면 서비스/저장소 단까지 포함한 패치를 준비하겠습니다.apps/pg-simulator/src/main/java/com/loopers/interfaces/event/payment/PaymentEventListener.java (1)
19-25: @async 메서드에서 InterruptedException 처리비동기 스레드에서 인터럽트는 캡처 후 인터럽트 플래그를 복구하는 것이 안전합니다. 또한
throws InterruptedException를 제거하면 호출자에 예외 전파되지 않아 로그가 조용히 누락되는 것을 방지할 수 있습니다. 지연 계산은ThreadLocalRandom이 경량입니다.import com.loopers.application.payment.PaymentApplicationService; import com.loopers.domain.payment.PaymentEvent; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalEventListener; +import java.util.concurrent.ThreadLocalRandom; @Component public class PaymentEventListener { @@ - public void handle(PaymentEvent.PaymentCreated event) throws InterruptedException { - long thresholdMillis = 1000L + (long) (Math.random() * 4000L); // 1000..5000 - Thread.sleep(thresholdMillis); - paymentApplicationService.handle(event.getTransactionKey()); - } + public void handle(PaymentEvent.PaymentCreated event) { + long thresholdMillis = 1000L + ThreadLocalRandom.current().nextLong(4000L); // 1000..4999 + try { + Thread.sleep(thresholdMillis); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + return; + } + paymentApplicationService.handle(event.getTransactionKey()); + }apps/pg-simulator/src/main/java/com/loopers/application/payment/PaymentApplicationService.java (2)
62-69: orderId 유효성 사전 검증서비스 단에서도
orderIdnull/blank를 BAD_REQUEST로 빠르게 반환하면 컨트롤러/저장소의 예외 흐름이 단순해집니다.@Transactional(readOnly = true) public OrderInfo findTransactionsByOrderId(UserInfo userInfo, String orderId) { + if (orderId == null || orderId.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "orderId는 필수입니다."); + } List<Payment> payments = paymentRepository.findByOrderId(userInfo.getUserId(), orderId); if (payments == null || payments.isEmpty()) { throw new CoreException(ErrorType.NOT_FOUND, "(orderId: " + orderId + ") 에 해당하는 결제건이 존재하지 않습니다."); } return new OrderInfo(orderId, payments.stream().map(TransactionInfo::from).collect(Collectors.toList())); }
89-95: 외부 콜백 호출 타임아웃/재시도 설정 (인프라 파일 변경 필요)현재
PaymentCoreRelay의RestTemplate는 기본 타임아웃 미설정입니다. 장애 전파/스레드 고갈을 막기 위해 연결/읽기 타임아웃과 재시도/백오프를 설정하는 것을 권장합니다(해당 클래스에서RestTemplateBuilder주입 후 설정).원하시면
PaymentCoreRelay에 타임아웃(예: 2s/5s), 지수 백오프 재시도, 실패시 DLQ/로그 지표 추가 패치를 제안드리겠습니다.apps/pg-simulator/src/main/java/com/loopers/interfaces/api/ApiResponse.java (1)
3-52: 응답 래퍼 구현 양호간결하고 직관적입니다. commerce-api와의 형태 차이는 허용 가능하나, 성공 메시지 포함 응답이 필요하면 오버로드 추가를 고려하세요.
public static class Metadata { public enum Result { SUCCESS, FAIL } @@ public static Metadata success() { return new Metadata(Result.SUCCESS, null, null); } + + public static Metadata success(String message) { + return new Metadata(Result.SUCCESS, null, message); + } @@ public static <T> ApiResponse<T> success(T data) { return new ApiResponse<>(Metadata.success(), data); } + + public static <T> ApiResponse<T> success(String message, T data) { + return new ApiResponse<>(Metadata.success(message), data); + }apps/pg-simulator/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java (2)
62-66: 내부 매핑 상세 메시지 노출 최소화 제안
jme.getOriginalMessage()는 내부 구조/타입명을 과도하게 노출할 수 있습니다. 운영 환경에서는 일반화된 메시지로 축약을 권장합니다.
25-29: 클라이언트 오류(CoreException) 스택트레이스 로깅 축소 권장예상 가능한 4xx에 스택트레이스를 남기면 로그 노이즈가 큽니다. 메시지만 warn, 예외는 debug로 조정 권장.
- log.warn("CoreException : {}", e.getCustomMessage() != null ? e.getCustomMessage() : e.getMessage(), e); + log.warn("CoreException : {}", e.getCustomMessage() != null ? e.getCustomMessage() : e.getMessage()); + if (log.isDebugEnabled()) { + log.debug("CoreException stacktrace", e); + }apps/pg-simulator/src/main/java/com/loopers/interfaces/api/payment/PaymentDto.java (4)
24-26: 환경 고정 상수(콜백 URL 프리픽스) 외부화 추천
"http://localhost:8080"하드코딩은 환경 전환을 막습니다. 설정값(예:@Value/ConfigurationProperties)로 외부화하고 DTO 검증 시 허용 호스트/스킴 기준으로 검사하세요.
42-44: NPE 가드 재확인위 검증 추가로 NPE 가능성은 줄었지만, 방어적 프로그래밍 차원에서
cardType을 다시 한 번Objects.requireNonNull로 보강해도 좋습니다.
137-143: null‑safe 매핑 보강 제안
CardTypeDto.from(CardType)에 null 입력 시 NPE입니다. API 응답에서 카드타입이 없을 수 있다면 안전하게 처리하세요.- public static CardTypeDto from(CardType cardType) { - return switch (cardType) { + public static CardTypeDto from(CardType cardType) { + if (cardType == null) return null; + return switch (cardType) { case SAMSUNG -> SAMSUNG; case KB -> KB; case HYUNDAI -> HYUNDAI; }; }
27-40: Bean Validation(@Valid) 전환 권장수동 validate() 대신 jakarta.validation(@notblank, @pattern, @positive 등) + 컨트롤러의 @Valid로 검증/메시지 처리를 일원화하세요. build.gradle.kts:50에 org.springframework.boot:spring-boot-starter-validation이 runtimeOnly로 선언되어 있으니, DTO에 애너테이션을 붙여 컴파일하려면 runtimeOnly → implementation으로 변경하는 것을 권장합니다.
apps/pg-simulator/src/main/java/com/loopers/domain/payment/Payment.java (4)
10-15: 중복/비효율 인덱스 구성PK(
transaction_key)가 유니크인 상황에서user_id, order_id, transaction_key의 유니크 인덱스는 실효성이 낮고 중복에 가깝습니다. 조회 패턴에 맞춰 비유니크 복합 인덱스만 유지하는 것을 권장합니다.- @Index(name = "idx_unique_user_order_transaction", columnList = "user_id, order_id, transaction_key", unique = true) + @Index(name = "idx_user_order_transaction", columnList = "user_id, order_id, transaction_key")
48-53: 타임스탬프 자동화(@PrePersist/@PreUpdate) 권장필드 초기값만으로는 업데이트 시점 보장이 약합니다. JPA 라이프사이클 콜백으로 일관되게 관리하세요.
@PrePersist void onCreate() { this.createdAt = LocalDateTime.now(); this.updatedAt = this.createdAt; } @PreUpdate void onUpdate() { this.updatedAt = LocalDateTime.now(); }
66-91: 도메인 오류 타입 적합성상태 전이 불가 상황에
INTERNAL_ERROR는 부적절할 수 있습니다. 클라이언트 행위에 기인한 경우CONFLICT또는BAD_REQUEST계열을 고려해 주세요.
7-8: 시간대/저장 타입 검토(운영 환경 UTC 권장)
LocalDateTime은 TZ 정보가 없어 서버 타임존에 종속됩니다. DB/레포팅 일관성을 위해Instant(+DB UTC) 또는OffsetDateTime(UTC)사용을 검토하세요.Also applies to: 48-53
apps/pg-simulator/build.gradle.kts (1)
1-1: 빈 plugins 블록보다는 필요한 플러그인 명시가 필요합니다.commerce-streamer와 동일하게 빈
plugins {}블록보다는 Java 애플리케이션에 필요한 기본 플러그인들을 명시적으로 선언해야 합니다.다음과 같이 필요한 플러그인들을 명시하는 것이 좋습니다:
-plugins {} +plugins { + java + id("org.springframework.boot") +}apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/DemoKafkaConsumer.java (1)
31-31: 프로덕션 환경에서는 구조화된 로깅을 고려해보세요.현재
System.out.println과System.out.printf를 사용하고 있는데, 프로덕션 환경에서는 SLF4J 로거를 사용하는 것이 좋습니다.다음과 같이 SLF4J 로거를 사용하는 것을 고려해보세요:
+import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + @Component public class DemoKafkaConsumer { + private static final Logger log = LoggerFactory.getLogger(DemoKafkaConsumer.class); // ... - System.out.println(messages); + log.info("Processing {} messages", messages.size()); // ... - System.out.printf( - "DLT message received - topic=%s, originalTopic=%s, partition=%s, offset=%s, payload=%s%n", - topic, originalTopic, String.valueOf(originalPartition), String.valueOf(originalOffset), payload - ); + log.warn("DLT message received - topic={}, originalTopic={}, partition={}, offset={}, payload={}", + topic, originalTopic, originalPartition, originalOffset, payload);Also applies to: 43-46, 54-61
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (66)
apps/commerce-streamer/build.gradle.kts(2 hunks)apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java(1 hunks)apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/DemoKafkaConsumer.java(1 hunks)apps/commerce-streamer/src/main/kotlin/com/loopers/CommerceStreamerApplication.kt(0 hunks)apps/commerce-streamer/src/main/kotlin/com/loopers/interfaces/consumer/DemoKafkaConsumer.kt(0 hunks)apps/pg-simulator/build.gradle.kts(2 hunks)apps/pg-simulator/src/main/java/com/loopers/PaymentGatewayApplication.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/application/payment/OrderInfo.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/application/payment/PaymentApplicationService.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/application/payment/PaymentCommand.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/application/payment/TransactionInfo.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/config/web/WebMvcConfig.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/domain/payment/CardType.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/domain/payment/Payment.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/domain/payment/PaymentEvent.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/domain/payment/PaymentEventPublisher.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/domain/payment/PaymentRelay.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/domain/payment/PaymentRepository.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/domain/payment/TransactionKeyGenerator.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/domain/payment/TransactionStatus.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/domain/user/UserInfo.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/infrastructure/payment/PaymentCoreEventPublisher.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/infrastructure/payment/PaymentCoreRelay.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/infrastructure/payment/PaymentCoreRepository.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/infrastructure/payment/PaymentJpaRepository.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/infrastructure/payment/TransactionKeyGeneratorImpl.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/interfaces/api/ApiResponse.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/interfaces/api/argumentresolver/UserInfoArgumentResolver.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/interfaces/api/payment/PaymentApi.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/interfaces/api/payment/PaymentDto.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/interfaces/event/payment/PaymentEventListener.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/support/error/CoreException.java(1 hunks)apps/pg-simulator/src/main/java/com/loopers/support/error/ErrorType.java(1 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/OrderInfo.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentCommand.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/TransactionInfo.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/config/web/WebMvcConfig.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/CardType.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentEvent.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentEventPublisher.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentRelay.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentRepository.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionKeyGenerator.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionStatus.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/domain/user/UserInfo.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentCoreEventPublisher.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentCoreRelay.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentCoreRepository.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentJpaRepository.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiResponse.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/argumentresolver/UserInfoArgumentResolver.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentApi.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentDto.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/event/payment/PaymentEventListener.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/support/error/CoreException.kt(0 hunks)apps/pg-simulator/src/main/kotlin/com/loopers/support/error/ErrorType.kt(0 hunks)build.gradle.kts(1 hunks)docs/retrospective-10weeks.md(1 hunks)modules/kafka/build.gradle.kts(0 hunks)modules/kafka/src/main/java/com/loopers/config/kafka/KafkaConfig.java(1 hunks)modules/kafka/src/main/kotlin/com/loopers/config/kafka/KafkaConfig.kt(0 hunks)
💤 Files with no reviewable changes (31)
- modules/kafka/build.gradle.kts
- apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/OrderInfo.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionStatus.kt
- apps/commerce-streamer/src/main/kotlin/com/loopers/CommerceStreamerApplication.kt
- modules/kafka/src/main/kotlin/com/loopers/config/kafka/KafkaConfig.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/CardType.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/config/web/WebMvcConfig.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentRepository.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/TransactionInfo.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiResponse.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentCoreEventPublisher.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/domain/user/UserInfo.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentCoreRepository.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentCoreRelay.kt
- apps/commerce-streamer/src/main/kotlin/com/loopers/interfaces/consumer/DemoKafkaConsumer.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentApi.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentEventPublisher.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/event/payment/PaymentEventListener.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentDto.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentEvent.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentJpaRepository.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/argumentresolver/UserInfoArgumentResolver.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionKeyGenerator.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/support/error/ErrorType.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/support/error/CoreException.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentRelay.kt
- apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentCommand.kt
🧰 Additional context used
🧬 Code graph analysis (23)
apps/pg-simulator/src/main/java/com/loopers/domain/payment/TransactionKeyGenerator.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/event/OrderEvent.java (1)
getTransactionId(105-105)
apps/pg-simulator/src/main/java/com/loopers/domain/payment/PaymentRelay.java (1)
apps/pg-simulator/src/main/java/com/loopers/application/payment/TransactionInfo.java (1)
TransactionInfo(7-46)
apps/pg-simulator/src/main/java/com/loopers/application/payment/PaymentCommand.java (1)
apps/pg-simulator/src/main/java/com/loopers/support/error/CoreException.java (1)
CoreException(3-21)
build.gradle.kts (2)
modules/jpa/build.gradle.kts (2)
api(6-21)java-library(1-4)supports/jackson/build.gradle.kts (1)
implementation(5-11)
apps/pg-simulator/src/main/java/com/loopers/config/web/WebMvcConfig.java (2)
apps/pg-simulator/src/main/java/com/loopers/interfaces/api/argumentresolver/UserInfoArgumentResolver.java (1)
UserInfoArgumentResolver(12-28)apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserController.java (1)
RestController(10-53)
apps/pg-simulator/src/main/java/com/loopers/PaymentGatewayApplication.java (4)
apps/commerce-batch/src/main/java/com/loopers/batch/CommerceBatchApplication.java (1)
SpringBootApplication(7-13)apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java (1)
ConfigurationPropertiesScan(13-29)apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java (3)
ConfigurationPropertiesScan(10-24)main(21-23)PostConstruct(15-19)apps/commerce-streamer/src/main/kotlin/com/loopers/CommerceStreamerApplication.kt (2)
basePackages(12-24)started(19-23)
apps/pg-simulator/src/main/java/com/loopers/interfaces/api/payment/PaymentApi.java (4)
apps/pg-simulator/src/main/java/com/loopers/interfaces/api/ApiResponse.java (1)
ApiResponse(3-52)apps/pg-simulator/src/main/java/com/loopers/domain/user/UserInfo.java (1)
UserInfo(3-11)apps/pg-simulator/src/main/java/com/loopers/support/error/CoreException.java (1)
CoreException(3-21)apps/pg-simulator/src/main/java/com/loopers/interfaces/api/payment/PaymentDto.java (4)
PaymentDto(15-159)TransactionResponse(89-105)TransactionDetailResponse(59-87)OrderResponse(107-122)
apps/commerce-streamer/build.gradle.kts (2)
modules/jpa/build.gradle.kts (2)
api(6-21)java-library(1-4)apps/commerce-api/build.gradle.kts (1)
implementation(1-38)
apps/pg-simulator/src/main/java/com/loopers/domain/payment/PaymentRepository.java (2)
apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java (2)
OrderRepository(6-10)save(8-8)apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java (1)
PointRepository(5-7)
apps/pg-simulator/src/main/java/com/loopers/application/payment/TransactionInfo.java (2)
apps/commerce-api/src/main/java/com/loopers/domain/event/OrderEvent.java (2)
paymentProcessed(55-75)OrderEvent(10-123)apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java (1)
Getter(18-113)
apps/pg-simulator/src/main/java/com/loopers/support/error/CoreException.java (5)
apps/commerce-api/src/main/java/com/loopers/support/error/CoreException.java (4)
Getter(5-23)CoreException(14-18)CoreException(10-12)getErrorType(20-22)apps/commerce-api/src/test/java/com/loopers/support/error/CoreExceptionTest.java (3)
CoreExceptionTest(8-34)DisplayName(9-20)DisplayName(22-33)apps/commerce-api/src/main/java/com/loopers/support/error/GlobalExceptionHandler.java (1)
ExceptionHandler(12-16)apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java (1)
Getter(6-66)apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java (1)
ErrorType(81-81)
apps/pg-simulator/src/main/java/com/loopers/infrastructure/payment/TransactionKeyGeneratorImpl.java (1)
apps/pg-simulator/src/main/java/com/loopers/application/payment/PaymentApplicationService.java (1)
Component(14-96)
apps/pg-simulator/src/main/java/com/loopers/interfaces/api/argumentresolver/UserInfoArgumentResolver.java (2)
apps/pg-simulator/src/main/java/com/loopers/domain/user/UserInfo.java (1)
UserInfo(3-11)apps/pg-simulator/src/main/java/com/loopers/support/error/CoreException.java (1)
CoreException(3-21)
apps/pg-simulator/src/main/java/com/loopers/interfaces/api/ApiResponse.java (1)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiResponse.java (10)
ApiResponse(3-40)Metadata(4-20)success(30-32)success(9-11)success(26-28)fail(17-19)success(13-15)success(22-24)fail(34-39)Result(5-7)
apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java (2)
apps/commerce-batch/src/main/java/com/loopers/batch/CommerceBatchApplication.java (1)
SpringBootApplication(7-13)apps/pg-simulator/src/main/java/com/loopers/PaymentGatewayApplication.java (1)
ConfigurationPropertiesScan(11-24)
apps/pg-simulator/src/main/java/com/loopers/infrastructure/payment/PaymentCoreRepository.java (1)
apps/pg-simulator/src/main/java/com/loopers/application/payment/PaymentApplicationService.java (1)
Component(14-96)
apps/pg-simulator/src/main/java/com/loopers/infrastructure/payment/PaymentCoreRelay.java (3)
apps/pg-simulator/src/main/java/com/loopers/application/payment/TransactionInfo.java (1)
TransactionInfo(7-46)apps/pg-simulator/src/main/java/com/loopers/application/payment/PaymentApplicationService.java (1)
Component(14-96)apps/pg-simulator/src/main/java/com/loopers/interfaces/event/payment/PaymentEventListener.java (1)
Component(10-32)
apps/pg-simulator/src/main/java/com/loopers/infrastructure/payment/PaymentCoreEventPublisher.java (3)
apps/pg-simulator/src/main/java/com/loopers/domain/payment/PaymentEvent.java (1)
PaymentEvent(3-40)apps/pg-simulator/src/main/java/com/loopers/application/payment/PaymentApplicationService.java (1)
Component(14-96)apps/pg-simulator/src/main/java/com/loopers/interfaces/event/payment/PaymentEventListener.java (1)
Component(10-32)
apps/pg-simulator/src/main/java/com/loopers/interfaces/event/payment/PaymentEventListener.java (4)
apps/pg-simulator/src/main/java/com/loopers/domain/payment/PaymentEvent.java (1)
PaymentEvent(3-40)apps/pg-simulator/src/main/java/com/loopers/application/payment/PaymentApplicationService.java (1)
Component(14-96)apps/pg-simulator/src/main/java/com/loopers/infrastructure/payment/PaymentCoreEventPublisher.java (1)
Component(8-26)apps/pg-simulator/src/main/java/com/loopers/infrastructure/payment/PaymentCoreRelay.java (1)
Component(10-23)
apps/pg-simulator/src/main/java/com/loopers/interfaces/api/payment/PaymentDto.java (4)
apps/pg-simulator/src/main/java/com/loopers/application/payment/OrderInfo.java (1)
OrderInfo(5-16)apps/pg-simulator/src/main/java/com/loopers/application/payment/PaymentCommand.java (1)
PaymentCommand(7-38)apps/pg-simulator/src/main/java/com/loopers/application/payment/TransactionInfo.java (1)
TransactionInfo(7-46)apps/pg-simulator/src/main/java/com/loopers/support/error/CoreException.java (1)
CoreException(3-21)
apps/pg-simulator/src/main/java/com/loopers/domain/payment/Payment.java (1)
apps/pg-simulator/src/main/java/com/loopers/support/error/CoreException.java (1)
CoreException(3-21)
apps/pg-simulator/src/main/java/com/loopers/application/payment/PaymentApplicationService.java (9)
apps/pg-simulator/src/main/java/com/loopers/domain/user/UserInfo.java (1)
UserInfo(3-11)apps/pg-simulator/src/main/java/com/loopers/support/error/CoreException.java (1)
CoreException(3-21)apps/pg-simulator/src/main/java/com/loopers/infrastructure/payment/PaymentCoreEventPublisher.java (1)
Component(8-26)apps/pg-simulator/src/main/java/com/loopers/infrastructure/payment/PaymentCoreRelay.java (1)
Component(10-23)apps/pg-simulator/src/main/java/com/loopers/infrastructure/payment/PaymentCoreRepository.java (1)
Component(11-45)apps/pg-simulator/src/main/java/com/loopers/infrastructure/payment/TransactionKeyGeneratorImpl.java (1)
Component(10-21)apps/pg-simulator/src/main/java/com/loopers/interfaces/event/payment/PaymentEventListener.java (1)
Component(10-32)apps/pg-simulator/src/main/java/com/loopers/domain/payment/PaymentEvent.java (3)
PaymentEvent(3-40)PaymentCreated(4-16)PaymentHandled(18-39)apps/pg-simulator/src/main/java/com/loopers/application/payment/TransactionInfo.java (1)
TransactionInfo(7-46)
apps/pg-simulator/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java (2)
apps/pg-simulator/src/main/java/com/loopers/support/error/CoreException.java (1)
CoreException(3-21)apps/pg-simulator/src/main/java/com/loopers/interfaces/api/ApiResponse.java (1)
ApiResponse(3-52)
🔇 Additional comments (25)
apps/pg-simulator/src/main/java/com/loopers/PaymentGatewayApplication.java (1)
11-14: 엔트리포인트/어노테이션 구성 적절합니다Java 전환에 따른 부트스트랩 구성이 깔끔하고, 다른 앱들과의 일관성도 유지됩니다.
docs/retrospective-10weeks.md (1)
218-221: Redis 직렬화 확인 필요자바 객체(
ProductDetailResponse)를 Value에 저장합니다. JDK 직렬화 비활성/JSON 직렬화 설정 여부를 문서에 명시해 주세요(Jackson2JsonRedisSerializer 등).apps/pg-simulator/src/main/java/com/loopers/domain/payment/TransactionKeyGenerator.java (1)
3-5: 키 생성의 보장사항(Javadoc/주석) 명시 부탁고유성/충돌 확률, 길이/문자셋, 시간·스레드 안전성 보장을 인터페이스 수준에서 문서화하면 구현/테스트 기준이 명확해집니다. 구현체가 난수/시간에 의존한다면 재현 가능성도 표기 권장.
apps/pg-simulator/src/main/java/com/loopers/domain/payment/TransactionStatus.java (1)
3-7: 상태 정의 OK, 외부 노출 매핑 확인API/이벤트 페이로드에 이 enum이 직렬화된다면 문자열 값 고정(예: 스네이크/카멜/한글 매핑)과 향후 추가 상태(예: CANCELED/REVERSED) 호환성 전략만 확인해주세요.
apps/pg-simulator/src/main/java/com/loopers/infrastructure/payment/PaymentCoreRelay.java (1)
12-21: SSRF 위험·타임아웃 미설정·정적 RestTemplate 사용 — 수동 확인 필요rg 실행 결과 "No files were searched"로 리포지토리 검증 불가. 다음을 수동 확인하거나 출력/대상 파일을 제공해 주세요:
- callbackUrl 출처 확인: 외부(사용자) 입력이면 SSRF 위험 — 스킴(http/https)·호스트 검증 및 사설/루프백/링크로컬/메타데이터 IP 차단 또는 도메인 allowlist 필요.
- RestTemplate에 타임아웃 미설정 — connect/read 타임아웃(예: connect 2s, read 5s) 설정 필요.
- 정적 RestTemplate 대신 RestTemplateBuilder로 빈 주입해 타임아웃·인증·로깅·메트릭·리트라이 등 공통 설정 적용 권장.
- 대상 파일 확인: apps/pg-simulator/src/main/java/com/loopers/infrastructure/payment/PaymentCoreRelay.java 존재 여부 또는 notify(callbackUrl, ...) 호출 지점(콜백을 생성/전달하는 코드) 제공.
권장 검색 명령(실행 후 출력 제공):
rg -nC2 -S 'PaymentCoreRelay|callbackUrl|notify('apps/pg-simulator/src/main/java/com/loopers/infrastructure/payment/PaymentCoreEventPublisher.java (1)
8-26: 이벤트 퍼블리셔 구현 적절 — @EnableAsync 활성화 확인 완료프로젝트에 @EnableAsync가 존재(apps/pg-simulator/src/main/java/com/loopers/PaymentGatewayApplication.java, apps/commerce-api/src/main/java/com/loopers/config/AsyncConfig.java)하며, PaymentEventListener(apps/pg-simulator/src/main/java/com/loopers/interfaces/event/payment/PaymentEventListener.java)에 @async가 적용되어 비동기 리스너가 동작합니다.
apps/pg-simulator/src/main/java/com/loopers/support/error/CoreException.java (1)
19-21: 전반적으로 구현 깔끔합니다게터 노출, 메시지 위임 모두 일관적입니다. 상위 모듈(commerce-api)의 동작과도 정합적입니다.
apps/pg-simulator/src/main/java/com/loopers/support/error/ErrorType.java (1)
5-25: 에러 타입 정의 적절HTTP 매핑과 메시지 구성이 단순·명확합니다. 코어 예외와도 일관적입니다.
apps/pg-simulator/src/main/java/com/loopers/interfaces/api/argumentresolver/UserInfoArgumentResolver.java (1)
12-28: 구현 적절합니다 — 등록 확인됨타입 매칭(supportsParameter)과 헤더 조회/검증 흐름이 간결하고 명확합니다. apps/pg-simulator/src/main/java/com/loopers/config/web/WebMvcConfig.java의 addArgumentResolvers(라인 14-15)에서 resolvers.add(new UserInfoArgumentResolver())로 등록되어 있어 프로덕션 동작에 필요한 구성이 적용되어 있습니다.
apps/pg-simulator/src/main/java/com/loopers/interfaces/event/payment/PaymentEventListener.java (1)
10-32: @EnableAsync 활성화 확인 완료apps/pg-simulator/src/main/java/com/loopers/PaymentGatewayApplication.java 및 apps/commerce-api/src/main/java/com/loopers/config/AsyncConfig.java에서 @EnableAsync와 getAsyncExecutor 설정이 확인되어 @async 사용에 문제가 없습니다.
apps/pg-simulator/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java (3)
72-85: WebFlux 사용 여부 확인 필요 — ServerWebInputException 핸들러 존재apps/pg-simulator/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java 및 apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java에서 ServerWebInputException을 import하고 handleBadRequest 핸들러를 정의하고 있습니다. 제공하신 스크립트 결과로는 gradle 의존성에서 spring-boot-starter-webflux가 발견되지 않았습니다. WebFlux를 의도적으로 사용 중이면 유지하고, 아니라면 MVC 예외 타입(예: MethodArgumentNotValidException 등)으로 대체하거나 해당 핸들러를 제거하세요.
87-90: NoResourceFoundException 동작 조건 정정 및 설정 안내NoResourceFoundException은 ResourceHttpRequestHandler가 정적 리소스를 찾지 못할 때 발생하므로 spring.mvc.throw-exception-if-no-handler-found 설정은 필요하지 않습니다. 반대로 "핸들러 없음" 상황을 예외로 받으려면 spring.mvc.throw-exception-if-no-handler-found=true를 추가해야 하며(정적 리소스 자동 매핑과 충돌하면 spring.resources.add-mappings=false도 함께 고려), 현재 리포지토리(application.yml들)에는 해당 설정이 없습니다. 확인/수정 대상: apps/pg-simulator/src/main/resources/application.yml (ApiControllerAdvice는 apps/pg-simulator/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java에 존재).
Likely an incorrect or invalid review comment.
52-58: 패턴 매칭 instanceof 사용 — 호환성 확인 완료build.gradle.kts에서 Java toolchain이 JavaLanguageVersion.of(21)로 설정되어 있어 JDK 16+ 문법(instanceof 패턴 매칭)이 지원됩니다.
build.gradle.kts (1)
63-63: Kotlin 의존성 제거가 일관성 있게 적용되었습니다.springmockk 라이브러리는 Kotlin 기반 Spring 테스트를 위한 모킹 라이브러리로, Java 중심 코드베이스로 전환한 이번 PR 목적에 맞게 제거되었습니다. 대신 Mockito를 사용하고 있어 적절합니다.
apps/commerce-streamer/build.gradle.kts (1)
19-19: 적절한 Kotlin 관련 의존성 정리입니다.QueryDSL의 kapt 의존성 제거가 Java 전용 모듈로 전환하는 이번 PR의 목적과 일치합니다. Java 환경에서는 annotation processor를 사용하게 될 것입니다.
apps/pg-simulator/build.gradle.kts (1)
15-15: Kotlin 의존성 정리가 적절합니다.Kotlin 관련 의존성들(kotlin-reflect, kotlin-stdlib-jdk8, kapt)의 제거가 Java 전용 모듈로 전환하는 목적에 부합합니다.
apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java (3)
13-18: Spring Boot 애플리케이션 설정이 적절합니다.JPA 저장소, 감사, 스케줄링 기능을 활성화하는 어노테이션들이 올바르게 구성되어 있습니다.
@EnableJpaRepositories의 basePackages 설정도 적절합니다.
20-24: 타임존 설정이 적절하게 구현되었습니다.
@PostConstruct를 통한 Asia/Seoul 타임존 설정이 다른 애플리케이션들과 일관성 있게 구현되어 있습니다.
26-28: 표준적인 Spring Boot 진입점입니다.main 메서드의 구현이 Spring Boot 표준을 따르고 있어 적절합니다.
modules/kafka/src/main/java/com/loopers/config/kafka/KafkaConfig.java (3)
25-30: Kafka 설정 상수들이 적절하게 정의되었습니다.배치 처리에 최적화된 상수값들이 잘 설정되어 있습니다. 특히 MAX_POLLING_SIZE 3000건, FETCH_MIN_BYTES 1MB는 배치 처리에 적합한 값입니다.
49-68: 배치 리스너 설정이 잘 구성되었습니다.수동 확인 모드(Manual ACK), 동시성 3, 배치 리스닝 활성화 등 배치 처리에 필요한 설정들이 적절하게 구성되어 있습니다. Consumer 설정들도 배치 처리 성능을 고려해 잘 튜닝되어 있습니다.
32-47: Kafka 팩토리와 템플릿 빈 설정이 표준을 따릅니다.Producer, Consumer 팩토리와 KafkaTemplate의 구성이 Spring Kafka 표준에 맞게 잘 구현되어 있습니다.
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/DemoKafkaConsumer.java (3)
19-33: 배치 Kafka 리스너가 올바르게 구현되었습니다.KafkaConfig.BATCH_LISTENER를 사용한 배치 리스닝 설정과 재시도 정책(3회, 지수 백오프)이 적절하게 구성되어 있습니다. 수동 확인(acknowledgment.acknowledge())도 올바르게 처리하고 있습니다.
35-47: DLT 핸들러가 적절하게 구현되었습니다.단일 메시지 DLT 핸들러에서 원본 토픽, 파티션, 오프셋 정보를 포함한 상세한 로깅이 잘 구현되어 있습니다.
49-63: 배치 DLT 핸들러가 안전하게 구현되었습니다.null 체크와 함께 배치 크기를 로깅하고, 첫 번째 레코드의 메타데이터를 안전하게 추출하는 로직이 잘 구현되어 있습니다.
| kotlin("kapt") version "2.0.20" | ||
| kotlin("plugin.jpa") version "2.0.20" | ||
| } | ||
| plugins {} |
There was a problem hiding this comment.
🧩 Analysis chain
빈 plugins 블록보다는 필요한 플러그인만 명시하는 것이 좋습니다.
현재 빈 plugins {} 블록보다는 필요한 플러그인을 명시적으로 선언하는 것이 좋습니다. Java 애플리케이션이므로 최소한 java 플러그인이 필요할 것으로 보입니다.
Java 애플리케이션에 필요한 기본 플러그인들을 확인하기 위해 다음 스크립트로 검증해보겠습니다:
🏁 Script executed:
#!/bin/bash
# 프로젝트의 다른 앱 모듈들이 어떤 플러그인을 사용하는지 확인
echo "=== 다른 앱 모듈들의 플러그인 설정 확인 ==="
fd -t f "build.gradle.kts" apps/ --exec cat {}
echo -e "\n=== Commerce Streamer의 Java 소스 파일 존재 확인 ==="
fd -t f "\.java$" apps/commerce-streamer/src/ --exec echo "Found Java file: {}"Length of output: 5892
🏁 Script executed:
#!/bin/bash
set -euo pipefail
f="apps/commerce-streamer/build.gradle.kts"
if [ ! -f "$f" ]; then
echo "FILE_NOT_FOUND: $f"
exit 0
fi
echo "=== FILE: $f (with line numbers) ==="
nl -ba "$f" | sed -n '1,300p'
echo
echo "=== EXTRACT: plugins { ... } blocks (braces-aware) ==="
awk '
/plugins[[:space:]]*\{/ {
in=1; count=1;
print "START_PLUGIN_BLOCK at line " NR;
print NR ":" $0;
next
}
in {
print NR ":" $0;
n_open = gsub(/\{/, "{")
n_close = gsub(/\}/, "}")
count += n_open - n_close
if (count==0) { print "END_PLUGIN_BLOCK at line " NR; in=0 }
}
' "$f" || true
echo
echo "=== SEARCH: occurrences of 'java' or plugin ids in file ==="
grep -nE --color=never '\bjava\b|id\(|org\.springframework\.boot' "$f" || trueLength of output: 115
🏁 Script executed:
#!/bin/bash
set -euo pipefail
f="apps/commerce-streamer/build.gradle.kts"
if [ ! -f "$f" ]; then
echo "FILE_NOT_FOUND: $f"
exit 0
fi
echo "=== FILE INFO ==="
ls -l "$f" || true
echo
echo "=== FULL CONTENT ==="
sed -n '1,400p' "$f" || true
echo
echo "=== LINES CONTAINING 'plugins' OR 'java' OR 'id(' OR 'spring-boot' ==="
grep -nE --color=never 'plugins|id\(|\bjava\b|spring-boot' "$f" || true
echo
echo "=== PLUGIN BLOCK(S) (parsed with python) ==="
python3 - <<'PY'
import sys
f="apps/commerce-streamer/build.gradle.kts"
try:
with open(f,encoding='utf-8') as fh:
lines=fh.readlines()
except Exception as e:
print("PY_ERR",e)
sys.exit(1)
i=0
n=len(lines)
while i<n:
line=lines[i]
if 'plugins' in line and '{' in line:
start=i+1
brace=line.count('{')-line.count('}')
print(f"START_PLUGIN_BLOCK at line {start}")
print(f"{start}: {line.rstrip()}")
i+=1
while i<n and brace>0:
l=lines[i]
print(f"{i+1}: {l.rstrip()}")
brace += l.count('{')-l.count('}')
i+=1
print(f"END_PLUGIN_BLOCK at line {i}")
else:
i+=1
PYLength of output: 1575
plugins 블록이 비어있습니다 — 최소한 'java' 플러그인 추가 필요
apps/commerce-streamer/build.gradle.kts의 plugins {}가 비어 있습니다. 이 모듈에 Java 소스가 있으므로 plugins { java }를 추가하거나, Spring Boot 사용 시 org.springframework.boot 및 의존성 관리 플러그인도 함께 선언하세요. 루트에서 플러그인을 상속하고 있다면 적용 위치를 커밋/설명에 명시하세요.
🤖 Prompt for AI Agents
In apps/commerce-streamer/build.gradle.kts around line 1, the plugins block is
empty which is invalid for a module containing Java sources; add at minimum the
Java plugin by declaring plugins { java } or, if this is a Spring Boot module,
declare the Spring Boot and dependency-management plugins (e.g.,
org.springframework.boot and io.spring.dependency-management) instead; if these
plugins are intentionally provided from the root build, add a comment in the
file/commit message stating that the module inherits plugins from the root and
ensure the root build actually applies them to this subproject.
| @Transactional | ||
| public TransactionInfo createTransaction(PaymentCommand.CreateTransaction command) { | ||
| command.validate(); | ||
|
|
||
| String transactionKey = transactionKeyGenerator.generate(); | ||
| Payment payment = paymentRepository.save(new Payment( | ||
| transactionKey, | ||
| command.getUserId(), | ||
| command.getOrderId(), | ||
| command.getCardType(), | ||
| command.getCardNo(), | ||
| command.getAmount(), | ||
| command.getCallbackUrl() | ||
| )); | ||
|
|
||
| paymentEventPublisher.publish(PaymentEvent.PaymentCreated.from(payment)); | ||
| return TransactionInfo.from(payment); | ||
| } |
There was a problem hiding this comment.
중복 결제 생성 방지 로직 추가 제안
같은 사용자/주문에 진행 중(PENDING) 결제가 있는 경우 새 결제 생성을 막아주세요. 재시도/중복 클릭 대응입니다.
@Transactional
public TransactionInfo createTransaction(PaymentCommand.CreateTransaction command) {
command.validate();
+ // 동일 주문에 진행 중 결제(PENDING) 존재 시 거부
+ List<Payment> existing = paymentRepository.findByOrderId(command.getUserId(), command.getOrderId());
+ if (existing != null && existing.stream().anyMatch(p -> p.getStatus() == TransactionStatus.PENDING)) {
+ throw new CoreException(ErrorType.CONFLICT, "(orderId: " + command.getOrderId() + ") 동일 주문에 진행 중인 결제가 있습니다.");
+ }
+
String transactionKey = transactionKeyGenerator.generate();
Payment payment = paymentRepository.save(new Payment(
transactionKey,
command.getUserId(),
command.getOrderId(),
command.getCardType(),
command.getCardNo(),
command.getAmount(),
command.getCallbackUrl()
));Committable suggestion skipped: line range outside the PR's diff.
| private final String cardNo; | ||
| private final long amount; | ||
| private final TransactionStatus status; | ||
| private final String reason; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
🧩 Analysis chain
민감 정보(카드번호) 직접 노출 — 마스킹 또는 제거 필요
cardNo 원문을 API/콜백에 그대로 반환하고 있습니다. 실서비스 기준으로는 PCI/보안 이슈이며, 시뮬레이터라도 로그/전파로 유출될 수 있습니다. 최소한 응답과 콜백에서는 마지막 4자리만 노출되도록 마스킹을 권장합니다.
public static TransactionInfo from(Payment payment) {
return new TransactionInfo(
payment.getTransactionKey(),
payment.getOrderId(),
payment.getCardType(),
- payment.getCardNo(),
+ maskCardNo(payment.getCardNo()),
payment.getAmount(),
payment.getStatus(),
payment.getReason()
);
}파일 내 추가 메서드(헬퍼):
private static String maskCardNo(String cardNo) {
if (cardNo == null || cardNo.isBlank()) return null;
// "1234-5678-9012-3456" -> "****-****-****-3456"
String digits = cardNo.replaceAll("[^0-9]", "");
if (digits.length() < 4) return "****";
String last4 = digits.substring(digits.length() - 4);
return "****-****-****-" + last4;
}시뮬레이터 특성상 원문 노출이 꼭 필요하다면(예: 데모), 범위를 최소화하거나 샘플 카드번호만 허용하도록 별도 가드를 넣을까요?
Also applies to: 27-36
🏁 Script executed:
#!/bin/bash
set -euo pipefail
TARGET="apps/pg-simulator/src/main/java/com/loopers/application/payment/TransactionInfo.java"
echo "PWD: $(pwd)"
if [ -f "$TARGET" ]; then
echo "FOUND: $TARGET"
echo "---- FILE CONTENT (numbered, first 300 lines) ----"
nl -ba "$TARGET" | sed -n '1,300p'
exit 0
fi
echo "$TARGET not found. Searching repository for TransactionInfo and cardNo..."
if command -v rg >/dev/null 2>&1; then
echo "rg results for class TransactionInfo:"
rg -n --hidden -S 'class\s+TransactionInfo\b' || true
echo "rg results for files named TransactionInfo.java:"
rg -n --hidden -S 'TransactionInfo\.java' || true
echo "rg results for 'cardNo':"
rg -n --hidden -S '\bcardNo\b' || true
echo "rg results for 'getCardNo(':"
rg -n --hidden -S 'getCardNo\(' || true
echo "rg results for 'from(Payment':"
rg -n --hidden -S 'from\s*\(\s*Payment\s+' || true
else
echo "rg not available — falling back to find+grep"
find . -type f -name "TransactionInfo.java" -print || true
find . -type f -name "*.java" -print0 | xargs -0 grep -n --color=never 'class TransactionInfo' || true
find . -type f -name "*.java" -print0 | xargs -0 grep -n --color=never '\bcardNo\b' || true
fiLength of output: 207
🏁 Script executed:
#!/bin/bash
set -euo pipefail
TARGET="apps/pg-simulator/src/main/java/com/loopers/application/payment/TransactionInfo.java"
echo "PWD: $(pwd)"
echo "--- check target ---"
if [ -f "$TARGET" ]; then
echo "FOUND: $TARGET"
echo "---- FILE CONTENT (first 400 lines) ----"
sed -n '1,400p' "$TARGET" || true
else
echo "NOT FOUND: $TARGET"
fi
echo "---- repo-wide searches (rg) ----"
if command -v rg >/dev/null 2>&1; then
echo "Occurrences of 'cardNo':"
rg -n --hidden -S '\bcardNo\b' || true
echo "Occurrences of 'getCardNo(':"
rg -n --hidden -S 'getCardNo\(' || true
echo "Occurrences of 'from(Payment':"
rg -n --hidden -S 'from\s*\(\s*Payment' || true
echo "Occurrences of 'maskCard' or 'mask.*card':"
rg -n --hidden -S 'maskCard|mask.*card' || true
echo "Occurrences of 'TransactionInfo':"
rg -n --hidden -S '\bTransactionInfo\b' || true
else
echo "rg not available — falling back to grep"
find . -type f -name "*.java" -print0 | xargs -0 grep -n --color=never '\bcardNo\b' || true
fiLength of output: 12754
TransactionInfo.from에서 카드번호 원본 노출 중지 및 마스킹 적용 필요
- apps/pg-simulator/src/main/java/com/loopers/application/payment/TransactionInfo.java의
from(Payment)메서드가payment.getCardNo()를 그대로 사용해 API 응답·콜백으로 민감 정보가 유출됩니다.maskCardNo()헬퍼를 추가하여 마지막 4자리만 노출하도록 수정해야 합니다. - API 레이어(
PaymentDto.from(TransactionInfo)) 또한info.getCardNo()대신 마스킹된 값을 사용하도록 변경하세요. - 시뮬레이터 특성상 원본 카드번호 노출이 꼭 필요할 경우, 환경 변수나 설정 토글로 가드하거나 샘플 카드번호만 허용하는 별도 로직을 도입해 범위를 최소화하십시오.
🤖 Prompt for AI Agents
In
apps/pg-simulator/src/main/java/com/loopers/application/payment/TransactionInfo.java
around lines 11-14, TransactionInfo.from(Payment) currently copies
payment.getCardNo() directly which exposes the raw card number; add a private
helper maskCardNo(String) that returns only the last 4 digits (e.g., replace
preceding characters with '*' or 'X'), update TransactionInfo.from(Payment) to
set the cardNo field to the masked value instead of the raw value, and update
the API layer PaymentDto.from(TransactionInfo) to use the masked card number
(info.getCardNo()) rather than any raw source; if raw card numbers must be
available in special cases, gate that behavior behind an environment/config
toggle or restrict to known sample numbers only.
| @Column(name = "card_no", nullable = false) | ||
| private String cardNo; | ||
|
|
There was a problem hiding this comment.
민감정보 저장 위험: 카드번호 평문 저장
cardNo를 평문으로 영속화하면 컴플라이언스/보안 리스크가 큽니다(PCI DSS). 최소화/마스킹/토큰화 혹은 암호화(키 관리 포함)를 적용하세요. 저장은 마지막 4자리만, 원문은 보관하지 않는 전략을 권장합니다.
| public void notify(String callbackUrl, TransactionInfo transactionInfo) { | ||
| try { | ||
| restTemplate.postForEntity(callbackUrl, transactionInfo, Object.class); | ||
| } catch (Exception e) { |
There was a problem hiding this comment.
🧩 Analysis chain
PCI/개인정보 유출 위험: 콜백 페이로드의 cardNo 포함
TransactionInfo에 cardNo가 포함되어 외부 콜백으로 전송됩니다. 실제/준실제 환경이라면 마스킹(예: 마지막 4자리만) 또는 토큰화로 대체해야 합니다.
추가 DTO(예: TransactionResultPayload)로 카드번호 제거/마스킹 후 전송하도록 API를 분리하는 것을 권장합니다.
🏁 Script executed:
#!/bin/bash
# cardNo 외부 전파 여부 점검
rg -nC2 -g '!**/build/**' '\bgetCardNo\('Length of output: 148
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "PWD: $(pwd)"
echo "Listing repo root:"
ls -la | sed -n '1,120p' || true
echo
echo "rg version:"
rg --version || true
echo
echo "Find PaymentCoreRelay.java (by name)"
file=$(find . -type f -name 'PaymentCoreRelay.java' -print -quit || true)
echo "found: $file"
if [ -n "$file" ]; then
echo "==== $file (first 220 lines) ===="
sed -n '1,220p' "$file" || true
fi
echo
echo "rg search for class PaymentCoreRelay and notify with TransactionInfo"
rg --hidden --no-ignore --no-ignore-vcs -n -S 'class PaymentCoreRelay' -g '!**/build/**' -C3 || true
rg --hidden --no-ignore --no-ignore-vcs -n -S 'notify\([^)]*TransactionInfo' -g '!**/build/**' -C5 || true
echo
echo "Find TransactionInfo.java (by name)"
tfile=$(find . -type f -name 'TransactionInfo.java' -print -quit || true)
echo "found: $tfile"
if [ -n "$tfile" ]; then
echo "==== $tfile (first 420 lines) ===="
sed -n '1,420p' "$tfile" || true
fi
echo
echo "Search for card-related fields/usages (cardNo, cardNumber, pan, primaryAccountNumber)"
rg --hidden --no-ignore --no-ignore-vcs -n -S 'cardNo|cardNumber|card_number|pan|primaryAccountNumber' -g '!**/build/**' -C3 || true
echo
echo "Search for postForEntity usage with transactionInfo"
rg --hidden --no-ignore --no-ignore-vcs -n -S 'postForEntity.*transactionInfo|postForEntity.*TransactionInfo' -g '!**/build/**' -C5 || true
echo
echo "Done"Length of output: 5708
심각: 콜백 페이로드에 카드번호(cardNo)가 포함되어 외부로 전송됩니다 — 즉시 조치 필요
- 문제: TransactionInfo.cardNo가 존재하며 PaymentCoreRelay.notify에서 TransactionInfo 객체를 그대로 restTemplate.postForEntity로 전송해 민감정보 유출(PCI 위험)이 확인됨.
- 조치(필수): 외부 콜백에 보내는 페이로드에서 cardNo를 제거하거나 토큰화/마스킹(예: 마지막 4자리만)으로 대체. 권장 구현: TransactionResultPayload 등 별도 DTO를 만들어 cardNo를 제외하거나 마스킹해 전송하도록 notify를 변경.
- 위치(수정 포인터): apps/pg-simulator/src/main/java/com/loopers/infrastructure/payment/PaymentCoreRelay.java (notify) 및 apps/pg-simulator/src/main/java/com/loopers/application/payment/TransactionInfo.java (cardNo 필드/생성자/getCardNo/from).
- 검증: 외부 전송 페이로드에 민감정보가 포함되지 않음을 확인하는 단위/통합 테스트와 코드 리뷰 추가.
🤖 Prompt for AI Agents
In
apps/pg-simulator/src/main/java/com/loopers/infrastructure/payment/PaymentCoreRelay.java
around lines 16-19 and related TransactionInfo in
apps/pg-simulator/src/main/java/com/loopers/application/payment/TransactionInfo.java,
the notify method currently posts the full TransactionInfo (including sensitive
cardNo) to external callbacks; replace this by creating and using a safe DTO
(e.g., TransactionResultPayload) that either omits cardNo or contains a masked
token (e.g., only last 4 digits) and map TransactionInfo ->
TransactionResultPayload before calling restTemplate.postForEntity; update
TransactionInfo so its cardNo is not exposed for external serialization
(remove/mark getter or annotate to prevent serialization if appropriate), modify
notify to send only the safe DTO, and add unit/integration tests to assert that
outgoing payloads do not contain raw card numbers.
| // 100ms ~ 500ms 지연 | ||
| long delay = 100 + (long)(Math.random() * 401); | ||
| Thread.sleep(delay); | ||
|
|
||
| // 40% 확률로 요청 실패 | ||
| if (Math.random() <= 0.4) { | ||
| throw new CoreException(ErrorType.INTERNAL_ERROR, "현재 서버가 불안정합니다. 잠시 후 다시 시도해주세요."); | ||
| } | ||
|
|
There was a problem hiding this comment.
요청 쓰레드에서 지연/실패 주입은 성능 저하 위험 — 플래그로 게이팅
Thread.sleep과 40% 실패 주입이 요청 쓰레드에서 수행되어, 동시 요청 시 스레드 고갈/지연을 유발할 수 있습니다. 시뮬레이션이 목적이라면 구성 플래그로 켜고 끄도록 게이팅을 권장합니다(기본 OFF).
package com.loopers.interfaces.api.payment;
import com.loopers.application.payment.PaymentApplicationService;
import com.loopers.interfaces.api.ApiResponse;
import com.loopers.domain.user.UserInfo;
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/payments")
public class PaymentApi {
private final PaymentApplicationService paymentApplicationService;
+ private final boolean chaosEnabled;
- public PaymentApi(PaymentApplicationService paymentApplicationService) {
- this.paymentApplicationService = paymentApplicationService;
- }
+ public PaymentApi(PaymentApplicationService paymentApplicationService,
+ @Value("${pg.simulator.chaos-enabled:false}") boolean chaosEnabled) {
+ this.paymentApplicationService = paymentApplicationService;
+ this.chaosEnabled = chaosEnabled;
+ }
@PostMapping
public ApiResponse<PaymentDto.TransactionResponse> request(
UserInfo userInfo,
@RequestBody PaymentDto.PaymentRequest request
) throws InterruptedException {
request.validate();
- // 100ms ~ 500ms 지연
- long delay = 100 + (long)(Math.random() * 401);
- Thread.sleep(delay);
-
- // 40% 확률로 요청 실패
- if (Math.random() <= 0.4) {
- throw new CoreException(ErrorType.INTERNAL_ERROR, "현재 서버가 불안정합니다. 잠시 후 다시 시도해주세요.");
- }
+ if (chaosEnabled) {
+ long delay = 100 + (long)(Math.random() * 401);
+ Thread.sleep(delay);
+ if (Math.random() <= 0.4) {
+ throw new CoreException(ErrorType.INTERNAL_ERROR, "현재 서버가 불안정합니다. 잠시 후 다시 시도해주세요.");
+ }
+ }
var info = paymentApplicationService.createTransaction(request.toCommand(userInfo.getUserId()));
return ApiResponse.success(PaymentDto.TransactionResponse.from(info));
}Also applies to: 1-18
🤖 Prompt for AI Agents
In apps/pg-simulator/src/main/java/com/loopers/interfaces/api/payment/PaymentApi
around lines 1-35, the code injects Thread.sleep and a 40% failure directly on
the request thread causing potential thread exhaustion under load; add a
configuration flag (default OFF) to enable simulation and wrap the delay/failure
logic in an if (enableSimulation) guard, and avoid blocking the request thread
by executing the delay/failure decision off the request thread (e.g., use a
shared ScheduledExecutorService or CompletableFuture.delayedExecutor) so the
main request thread is not blocked; ensure the flag is injectable via
application properties/env and documented, and keep the default behavior
unchanged when the flag is false.
| public void validate() { | ||
| if (orderId == null || orderId.isBlank() || orderId.length() < 6) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "주문 ID는 6자리 이상 문자열이어야 합니다."); | ||
| } | ||
| if (cardNo == null || !REGEX_CARD_NO.matcher(cardNo).matches()) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "카드 번호는 xxxx-xxxx-xxxx-xxxx 형식이어야 합니다."); | ||
| } | ||
| if (amount <= 0) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "결제금액은 양의 정수여야 합니다."); | ||
| } | ||
| if (callbackUrl == null || !callbackUrl.startsWith(PREFIX_CALLBACK_URL)) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "콜백 URL 은 " + PREFIX_CALLBACK_URL + " 로 시작해야 합니다."); | ||
| } | ||
| } |
There was a problem hiding this comment.
검증 누락: cardType null 및 callbackUrl 공백 케이스
toCommand에서 cardType.toCardType() 호출 시 NPE 위험이 있습니다. 또한 callbackUrl 공백 문자열도 거르지 않습니다.
public void validate() {
if (orderId == null || orderId.isBlank() || orderId.length() < 6) {
throw new CoreException(ErrorType.BAD_REQUEST, "주문 ID는 6자리 이상 문자열이어야 합니다.");
}
+ if (cardType == null) {
+ throw new CoreException(ErrorType.BAD_REQUEST, "카드 타입은 필수입니다.");
+ }
if (cardNo == null || !REGEX_CARD_NO.matcher(cardNo).matches()) {
throw new CoreException(ErrorType.BAD_REQUEST, "카드 번호는 xxxx-xxxx-xxxx-xxxx 형식이어야 합니다.");
}
if (amount <= 0) {
throw new CoreException(ErrorType.BAD_REQUEST, "결제금액은 양의 정수여야 합니다.");
}
- if (callbackUrl == null || !callbackUrl.startsWith(PREFIX_CALLBACK_URL)) {
+ if (callbackUrl == null || callbackUrl.isBlank() || !callbackUrl.startsWith(PREFIX_CALLBACK_URL)) {
throw new CoreException(ErrorType.BAD_REQUEST, "콜백 URL 은 " + PREFIX_CALLBACK_URL + " 로 시작해야 합니다.");
}
}📝 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.
| public void validate() { | |
| if (orderId == null || orderId.isBlank() || orderId.length() < 6) { | |
| throw new CoreException(ErrorType.BAD_REQUEST, "주문 ID는 6자리 이상 문자열이어야 합니다."); | |
| } | |
| if (cardNo == null || !REGEX_CARD_NO.matcher(cardNo).matches()) { | |
| throw new CoreException(ErrorType.BAD_REQUEST, "카드 번호는 xxxx-xxxx-xxxx-xxxx 형식이어야 합니다."); | |
| } | |
| if (amount <= 0) { | |
| throw new CoreException(ErrorType.BAD_REQUEST, "결제금액은 양의 정수여야 합니다."); | |
| } | |
| if (callbackUrl == null || !callbackUrl.startsWith(PREFIX_CALLBACK_URL)) { | |
| throw new CoreException(ErrorType.BAD_REQUEST, "콜백 URL 은 " + PREFIX_CALLBACK_URL + " 로 시작해야 합니다."); | |
| } | |
| } | |
| public void validate() { | |
| if (orderId == null || orderId.isBlank() || orderId.length() < 6) { | |
| throw new CoreException(ErrorType.BAD_REQUEST, "주문 ID는 6자리 이상 문자열이어야 합니다."); | |
| } | |
| if (cardType == null) { | |
| throw new CoreException(ErrorType.BAD_REQUEST, "카드 타입은 필수입니다."); | |
| } | |
| if (cardNo == null || !REGEX_CARD_NO.matcher(cardNo).matches()) { | |
| throw new CoreException(ErrorType.BAD_REQUEST, "카드 번호는 xxxx-xxxx-xxxx-xxxx 형식이어야 합니다."); | |
| } | |
| if (amount <= 0) { | |
| throw new CoreException(ErrorType.BAD_REQUEST, "결제금액은 양의 정수여야 합니다."); | |
| } | |
| if (callbackUrl == null || callbackUrl.isBlank() || !callbackUrl.startsWith(PREFIX_CALLBACK_URL)) { | |
| throw new CoreException(ErrorType.BAD_REQUEST, "콜백 URL 은 " + PREFIX_CALLBACK_URL + " 로 시작해야 합니다."); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
apps/pg-simulator/src/main/java/com/loopers/interfaces/api/payment/PaymentDto.java
around lines 27 to 40, the validate() method currently omits checks for cardType
being null (which causes a potential NPE when calling cardType.toCardType() in
toCommand()) and does not treat blank callbackUrl strings as invalid. Update
validate() to ensure cardType is not null (throwing CoreException with
BAD_REQUEST if it is) before any downstream conversions, and change the
callbackUrl check to reject null or blank values (use isBlank or trim/length
check) in addition to ensuring it starts with PREFIX_CALLBACK_URL; keep the same
error semantics and messages consistent with other validations.
| r.cardNo = info.getCardNo(); | ||
| r.amount = info.getAmount(); | ||
| r.status = TransactionStatusResponse.from(info.getStatus()); |
There was a problem hiding this comment.
개인정보(카드번호) 원문 노출 — 마스킹 필요
응답 DTO에 전체 카드번호를 그대로 노출하고 있습니다. 최소한 마지막 4자리만 노출하도록 마스킹하세요.
[ suggest_recommended_refactor ]
- r.cardNo = info.getCardNo();
+ r.cardNo = maskCardNo(info.getCardNo());헬퍼 추가(클래스 내부 정적 메서드):
private static String maskCardNo(String raw) {
if (raw == null) return null;
String digits = raw.replaceAll("\\D", "");
if (digits.length() < 4) return "****";
String last4 = digits.substring(digits.length() - 4);
return "****-****-****-" + last4;
}🤖 Prompt for AI Agents
In
apps/pg-simulator/src/main/java/com/loopers/interfaces/api/payment/PaymentDto.java
around lines 73-75 the code assigns the raw card number to the response DTO
(r.cardNo = info.getCardNo()), which exposes sensitive data; replace this with a
call to a static masking helper (e.g., r.cardNo = maskCardNo(info.getCardNo()))
and add the helper method inside the class that returns null if input is null,
strips non-digits, returns a default masked value when fewer than 4 digits, and
otherwise returns a masked format exposing only the last 4 digits (for example
"****-****-****-1234").
| - 배치 리스너/수동 ack/동시성 설정으로 처리량·재처리 균형(`modules/kafka/KafkaConfig.kt`). | ||
| - `@RetryableTopic`로 단계적 재시도, 최종 실패는 `@DltHandler`에 위임. | ||
| - Redis ZSET 랭킹 운영 |
There was a problem hiding this comment.
🧩 Analysis chain
문서가 PR 방향(코틀린→자바)과 불일치: .kt 경로/코틀린 예제 잔존
코틀린 파일 경로와 코틀린 코드 블록이 남아 있어 실제 코드베이스(Java)와 불일치합니다. 독자 혼선을 막기 위해 Java 파일 경로/예제로 통일해 주세요.
아래처럼 Java 예제로 교체 제안:
-```kotlin
-// modules/kafka/src/main/kotlin/com/loopers/config/kafka/KafkaConfig.kt
-@Bean(BATCH_LISTENER)
-fun defaultBatchListenerContainerFactory(...): ConcurrentKafkaListenerContainerFactory<*, *> =
- ConcurrentKafkaListenerContainerFactory<Any, Any>().apply {
- consumerFactory = DefaultKafkaConsumerFactory(consumerConfig)
- containerProperties.ackMode = ContainerProperties.AckMode.MANUAL
- setConcurrency(3)
- isBatchListener = true
- }
-```
+```java
+// modules/kafka/src/main/java/com/loopers/config/kafka/KafkaConfig.java
+@Configuration
+public class KafkaConfig {
+ public static final String BATCH_LISTENER = "batchListener";
+
+ @Bean(name = BATCH_LISTENER)
+ public ConcurrentKafkaListenerContainerFactory<Object, Object> defaultBatchListenerContainerFactory(
+ ConsumerFactory<Object, Object> consumerFactory) {
+ var factory = new ConcurrentKafkaListenerContainerFactory<Object, Object>();
+ factory.setConsumerFactory(consumerFactory);
+ factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL);
+ factory.setBatchListener(true);
+ factory.setConcurrency(3);
+ return factory;
+ }
+}
+```
레퍼런스 경로 표기도 `.java`로 정정해 주세요:
```diff
- modules/kafka/config/kafka/KafkaConfig.kt
+ modules/kafka/config/kafka/KafkaConfig.java레퍼런스 일괄 점검용:
Also applies to: 266-276, 279-283, 349-350
🏁 Script executed:
#!/bin/bash
# 잔존 .kt 경로/코틀린 코드펜스 탐지
rg -n --hidden -C1 '\.kt\b|```kotlin' docs/ modules/ apps/Length of output: 734
문서 불일치 — Kotlin(.kt) 참조/코틀린 예제 삭제하고 Java 경로/예제로 통일하세요.
docs/retrospective-10weeks.md에서 Kotlin 참조가 확인되었습니다. 수정 위치:
- docs/retrospective-10weeks.md:143 —
modules/kafka/KafkaConfig.kt경로 참조 - docs/retrospective-10weeks.md:266-268 — ```kotlin 코드 블록(경로 주석 포함)
- docs/retrospective-10weeks.md:349 —
modules/kafka/config/kafka/KafkaConfig.kt참조
대체 제안(원본 diff):
-```kotlin
-// modules/kafka/src/main/kotlin/com/loopers/config/kafka/KafkaConfig.kt
-@Bean(BATCH_LISTENER)
-fun defaultBatchListenerContainerFactory(...): ConcurrentKafkaListenerContainerFactory<*, *> =
- ConcurrentKafkaListenerContainerFactory<Any, Any>().apply {
- consumerFactory = DefaultKafkaConsumerFactory(consumerConfig)
- containerProperties.ackMode = ContainerProperties.AckMode.MANUAL
- setConcurrency(3)
- isBatchListener = true
- }
-```
+```java
+// modules/kafka/src/main/java/com/loopers/config/kafka/KafkaConfig.java
+@Configuration
+public class KafkaConfig {
+ public static final String BATCH_LISTENER = "batchListener";
+
+ @Bean(name = BATCH_LISTENER)
+ public ConcurrentKafkaListenerContainerFactory<Object, Object> defaultBatchListenerContainerFactory(
+ ConsumerFactory<Object, Object> consumerFactory) {
+ var factory = new ConcurrentKafkaListenerContainerFactory<Object, Object>();
+ factory.setConsumerFactory(consumerFactory);
+ factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL);
+ factory.setBatchListener(true);
+ factory.setConcurrency(3);
+ return factory;
+ }
+}
+```레퍼런스 경로 정정 제안:
- modules/kafka/config/kafka/KafkaConfig.kt
+ modules/kafka/config/kafka/KafkaConfig.java잔존 점검(한 번 더 실행 권장):
rg -n --hidden -C1 '\.kt\b|```kotlin' docs/ modules/ apps/🤖 Prompt for AI Agents
docs/retrospective-10weeks.md lines 143-145, 266-268, 349: the doc currently
references Kotlin files and includes a kotlin code block; replace those Kotlin
references and the kotlin code block with the Java path and Java example shown
in the review (use modules/kafka/src/main/java/.../KafkaConfig.java and the Java
ConcurrentKafkaListenerContainerFactory bean example), update the three specific
locations (line ~143: change modules/kafka/KafkaConfig.kt to the Java path,
lines ~266-268: replace the ```kotlin code block with the provided ```java code
block and Java code, and line ~349: change
modules/kafka/config/kafka/KafkaConfig.kt to the Java path), and then run the
suggested rg command to verify no remaining .kt references or ```kotlin blocks.
| public void cacheProductDetail(Long productId, ProductDetailResponse detail) { | ||
| redisTemplate.opsForValue().set("product:detail:" + productId, detail, Duration.ofMinutes(10)); | ||
| } | ||
| public void evictProductCaches(Long productId) { | ||
| redisTemplate.delete("product:detail:" + productId); | ||
| redisTemplate.delete("product:popular"); | ||
| redisTemplate.delete(redisTemplate.keys("product:list:*")); | ||
| } | ||
| ``` |
There was a problem hiding this comment.
Redis KEYS 사용은 운영 중단 위험(O(N) 블로킹); SCAN/인덱스 키로 교체 필요
redisTemplate.keys("product:list:*")는 큰 키스페이스에서 블로킹됩니다. SCAN 기반 삭제 또는 인덱스 세트 관리로 바꿔주세요.
예시(간단 SCAN + 파이프라인):
public void evictProductListCaches() {
ScanOptions options = ScanOptions.scanOptions().match("product:list:*").count(1000).build();
redisTemplate.execute((RedisCallback<Void>) conn -> {
try (Cursor<byte[]> c = conn.scan(options)) {
List<byte[]> batch = new ArrayList<>(500);
while (c.hasNext()) {
batch.add(c.next());
if (batch.size() == 500) { conn.pipelined(p -> { batch.forEach(p::del); return null; }); batch.clear(); }
}
if (!batch.isEmpty()) conn.pipelined(p -> { batch.forEach(p::del); return null; });
}
return null;
});
}대안: SADD product:list:index {keys...}로 인덱스 세트를 유지하고 SMEMBERS → DEL 후 세트 비우기.
No description provided.