Conversation
|
Caution Review failedThe pull request is closed. Walkthrough결제 관련 신규 모듈(도메인/애플리케이션/어댑터/API)을 추가하고 바우처 공개 조회 흐름(Controller → UseCase(Service) → Ports → JPA 리포지토리 → DB → DTO 응답)을 구현했습니다. 도메인 패키지 재배치, 일부 기존 도메인 파일 삭제, Gradle 설정 및 CI 권한이 갱신되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Client
participant API as Payment API
participant Ctr as VoucherController
participant UC as VoucherUseCase (VoucherService)
participant VR as VoucherReader (port)
participant VCR as VoucherContentReader (port)
participant VRepo as VoucherJpaRepository
participant VCRepo as VoucherContentJpaRepository
participant DB as Database
Client->>API: HTTP GET /api/v1/vouchers
API->>Ctr: dispatch
Ctr->>UC: getPublishedVouchers()
UC->>VR: readByStatus(PUBLISHED)
VR->>VRepo: findByStatus(PUBLISHED)
VRepo->>DB: SELECT vouchers WHERE status='PUBLISHED'
DB-->>VRepo: VoucherJpaEntity[]
VRepo-->>VR: entities
VR-->>UC: Voucher[]
alt published vouchers exist
UC->>VCR: readCurrentContents(voucherIds, now)
VCR->>VCRepo: findActiveContentsByVoucherIds(...)
VCRepo->>DB: SELECT voucher_contents WHERE active range
DB-->>VCRepo: VoucherContentJpaEntity[]
VCRepo-->>VCR: entities
VCR-->>UC: VoucherContent[]
UC->>UC: merge → CurrentPublishedVouchers[]
UC-->>Ctr: CurrentPublishedVouchers[]
Ctr-->>Client: 200 { vouchers: [...] }
else none
UC-->>Ctr: []
Ctr-->>Client: 200 { vouchers: [] }
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 📒 Files selected for processing (37)
Comment |
f7c492b to
82f6f38
Compare
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (44)
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/JpaConfiguration.kt (1)
6-8: 중복 선언 없음 — 선택적 리팩터링 권고리포지토리 전체 검색 결과: payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/JpaConfiguration.kt 외에 @EnableJpaAuditing 또는 class JpaConfiguration 선언 없음.
권장(선택):
- 충돌 대비 고유 클래스명 사용 및 CGLIB 오버헤드 감소: proxyBeanMethods = false 추가 및 클래스명 변경 권장.
- @EnableJpaAuditing은 가능하면 애플리케이션(부트스트랩) 모듈 한 곳에서만 활성화(그럴 경우 이 파일에서 제거).
권장 예시:
-@Configuration -@EnableJpaAuditing -class JpaConfiguration +@Configuration(proxyBeanMethods = false) +@EnableJpaAuditing +class VoucherJpaAuditingConfigurationpayment/voucher/domain/build.gradle.kts (1)
20-22: JUnit Platform 설정 중복 제거 제안루트 build.gradle.kts의 subprojects 블록에서 이미
useJUnitPlatform()을 적용하고 있어, 이 블록은 불필요합니다.아래처럼 제거를 권장합니다:
-tasks.withType<Test> { - useJUnitPlatform() -}payment/services/api/src/test/resources/application-test.yml (2)
3-3: H2 인메모리 유지 옵션 추가 권장연결 해제 시 스키마가 사라지는 이슈를 방지하려면
DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE를 추가하세요.- url: jdbc:h2:mem:testdb;MODE=MySQL + url: jdbc:h2:mem:testdb;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
14-14: SQL 로깅 중복 제거
hibernate.show_sql: true와logging.level.org.hibernate.SQL: DEBUG는 중복입니다. 로깅만 유지하는 것을 권장합니다.- show_sql: truepayment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherContent.kt (1)
5-14: 도메인 불변조건 검증 추가 권장활성 기간/버전/타이틀 등 기본 유효성 검증을 클래스 내부에서 보장하세요.
-public class VoucherContent( +public class VoucherContent( public val id: Long, public val voucherId: Long, public val version: Int, public val title: String, public val description: String, public val activeFrom: OffsetDateTime, public val activeUntil: OffsetDateTime, public val createdAt: OffsetDateTime, -) +) { + init { + require(title.isNotBlank()) { "title must not be blank" } + require(description.isNotBlank()) { "description must not be blank" } + require(version >= 1) { "version must be >= 1" } + require(!activeUntil.isBefore(activeFrom)) { "activeUntil must be >= activeFrom" } + } +}payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVoucherResponse.kt (1)
3-8: String 대신 enum 사용(또는 허용 값 명시)으로 타입 안정성 강화 제안API DTO에서
consumptionType을 String으로 노출하면 오타/케이스 문제 위험이 있습니다. enum 사용 또는 OpenAPI에서 허용 값을 명시하세요.예시(enum 사용):
+import app.payment.voucher.domain.ConsumptionType data class GetVoucherResponse( val id: Long, - val consumptionType: String, + val consumptionType: ConsumptionType, val title: String, val description: String, )(동일 변경이 GetVouchersResponse의 매핑에도 필요합니다:
.name제거)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVouchersResponse.kt (1)
9-20: 매핑 로직 명확하고 간결합니다도메인 -> DTO 변환이 깔끔합니다. 위 제안대로 enum을 쓰는 경우
.name제거 필요합니다.payment/voucher/adapter/src/test/resources/application-test.yml (2)
3-3: H2 메모리 DB가 테스트 중 닫히는 문제 방지: DB_CLOSE_DELAY 옵션 추가 제안연결 종료 시 스키마/데이터가 사라지는 경우를 방지하려면 H2 URL에 유지 옵션을 추가하는 것이 안전합니다.
- url: jdbc:h2:mem:testdb;MODE=MySQL + url: jdbc:h2:mem:testdb;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
17-19: Hibernate SQL 바인딩 값 로깅 필요 시 로거 추가현재 show_sql=false + org.hibernate.SQL: DEBUG 설정으로 SQL 문은 보이지만 바인딩 값은 보이지 않습니다. 필요하다면 다음 중 환경에 맞는 로거를 추가하세요.
- Hibernate 6: org.hibernate.orm.jdbc.bind = TRACE
- Hibernate 5: org.hibernate.type.descriptor.sql.BasicBinder = TRACE
payment/services/api/build.gradle.kts (2)
2-7: Spring Boot가 dependency-management를 포함하므로 중복 플러그인 제거 권장org.springframework.boot 플러그인 적용 시 io.spring.dependency-management는 자동 적용됩니다.
plugins { - id("io.spring.dependency-management") id("org.springframework.boot") kotlin("jvm") kotlin("plugin.spring") kotlin("plugin.jpa") }
16-19: 모듈 간 테스트 모킹 프레임워크 일관성 정렬 제안다른 모듈(예: adapter, application)은 Mockito를 제외하고 springmockk를 사용합니다. 동일한 정책을 원한다면 이 모듈도 일치시키는 것이 관리에 유리합니다.
- testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.springframework.boot:spring-boot-starter-test") { + exclude(group = "org.mockito") + } + testImplementation("com.ninja-squad:springmockk:4.0.2")payment/services/api/src/main/kotlin/app/payment/PaymentApiApplication.kt (1)
6-6: scanBasePackages 지정은 불필요 — 기본 스캐닝으로 단순화 가능패키지가 app.payment이므로 기본 스캐닝으로 하위 패키지가 모두 탐색됩니다. 유지보수성 측면에서 파라미터 제거 권장.
-@SpringBootApplication(scanBasePackages = ["app.payment"]) +@SpringBootApplicationpayment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherStatus.kt (1)
3-8: 불필요한 public 한정자 제거 제안Kotlin에서 최상위 선언은 기본 public입니다. 한정자 제거로 노이즈를 줄일 수 있습니다.
-public enum class VoucherStatus { +enum class VoucherStatus {payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherType.kt (1)
3-7: 불필요한 public 제거Kotlin 기본 가시성은 public입니다. 명시 제거로 일관성과 간결성 확보를 권장합니다.
-public enum class VoucherType { +enum class VoucherType {payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/TestConfiguration.kt (1)
3-10: 패키지 문자열 대신 타입 기준 스캔으로 리팩터링문자열 기반 패키지 스캔은 리팩터 시 깨지기 쉽습니다. basePackageClasses로 엔티티/레포지토리를 안전하게 지정하는 것을 권장합니다.
package app.payment.voucher.adapter +import app.payment.voucher.adapter.outbound.VoucherContentJpaRepository +import app.payment.voucher.adapter.outbound.VoucherJpaEntity +import app.payment.voucher.adapter.outbound.VoucherJpaRepository import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.data.jpa.repository.config.EnableJpaRepositories @SpringBootApplication -@EntityScan("app.payment.voucher.adapter.outbound") -@EnableJpaRepositories("app.payment.voucher.adapter.outbound") +@EntityScan(basePackageClasses = [VoucherJpaEntity::class]) +@EnableJpaRepositories(basePackageClasses = [VoucherJpaRepository::class, VoucherContentJpaRepository::class]) class TestConfigurationpayment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/CurrentPublishedVouchers.kt (1)
7-14: 클래스 명칭 단수형 검토객체 한 건을 나타내므로 CurrentPublishedVoucher(단수) 명명 검토를 제안합니다. API/매핑 전반 영향이 있어 추후 리팩터링 대상으로 적합합니다.
payment/voucher/domain/src/test/kotlin/app/payment/voucher/domain/VoucherTest.kt (1)
16-21: 도메인 행위 메서드 도입 고려직접 필드 변경 대신 Voucher.changeStatus(...) 같은 행위 메서드를 두고 updatedAt 동기화 등 부수효과를 캡슐화하면 도메인 규칙 유지가 수월합니다. 테스트도 해당 메서드를 통해 검증하도록 리팩터링을 권장합니다.
Also applies to: 31-33
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/VoucherController.kt (2)
3-6: 응답 콘텐츠 타입 명시produces를 명시해 클라이언트/문서화 일관성을 높이는 것을 권장합니다.
package app.payment.voucher.adapter.inbound import app.payment.voucher.application.port.inbound.VoucherUseCase +import org.springframework.http.MediaType import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @@ - @GetMapping + @GetMapping(produces = [MediaType.APPLICATION_JSON_VALUE]) fun getVouchers(): GetVouchersResponse = GetVouchersResponse.from(voucherUseCase.getPublishedVouchers())Also applies to: 13-15
9-15: 목록 엔드포인트 확장성 고려: 페이지네이션/캐싱바우처 수 증가 시 응답 크기와 DB 부하가 커질 수 있습니다. page/size 파라미터 도입과 캐시(예: ETag/Cache-Control 또는 서버 캐시) 적용을 검토하세요.
payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/Voucher.kt (2)
5-12: 불필요한 public 제거가시성 지정자 제거로 간결화 가능합니다.
-public class Voucher( - public val id: Long, - public val type: VoucherType, - public val consumptionType: ConsumptionType, - public var status: VoucherStatus, - public val createdAt: OffsetDateTime, - public var updatedAt: OffsetDateTime, +class Voucher( + val id: Long, + val type: VoucherType, + val consumptionType: ConsumptionType, + var status: VoucherStatus, + val createdAt: OffsetDateTime, + var updatedAt: OffsetDateTime, )
9-11: 상태 변경 도메인 메서드 추가 제안상태 전이와 updatedAt 동기화를 보장하는 메서드를 추가하면 무결성이 좋아집니다.
예시:
class Voucher(/* ... */) { fun changeStatus(newStatus: VoucherStatus, now: OffsetDateTime = OffsetDateTime.now()) { // 전이 검증 로직 필요 시 추가 this.status = newStatus this.updatedAt = now } }payment/services/api/src/test/kotlin/app/payment/integration/VoucherIntegrationTest.kt (2)
48-56: 저장 직후 조회 플래키니스 방지: saveAndFlush 사용 권장HTTP 호출 직전 DB flush 보장(동일 트랜잭션이라도 구현체/flush 모드에 따라 미세한 차이 가능)을 위해 save → saveAndFlush로 교체를 권장합니다.
- val savedVoucher = voucherRepository.save( + val savedVoucher = voucherRepository.saveAndFlush( VoucherJpaEntity( id = null, type = VoucherType.AI_POSTER_GENERATE, consumptionType = ConsumptionType.SINGLE_USE, status = VoucherStatus.PUBLISHED // createdAt, updatedAt은 @CreatedDate, @LastModifiedDate가 자동 설정 ) ) ... - voucherContentRepository.save( + voucherContentRepository.saveAndFlush( VoucherContentJpaEntity( id = null, voucherId = savedVoucher.id!!, version = 1, title = "테스트 바우처", description = "통합 테스트용 바우처입니다", activeFrom = OffsetDateTime.now().minusDays(1), activeUntil = OffsetDateTime.now().plusDays(30) // createdAt은 @CreatedDate가 자동 설정 ) )Also applies to: 58-69
38-42: @transactional과 데이터 정리 중복클래스 레벨 @transactional로 테스트 롤백이 보장된다면, 매 테스트 deleteAll()은 중복일 수 있습니다(특히 외래키로 인해 순서 제약까지 있습니다). 유지 보수 간소화를 위해 하나만 유지하는 것을 고려해 주세요.
payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherContentReader.kt (1)
6-11: 경계 포함 규칙을 인터페이스에 명시해주세요활성 구간 포함 여부(activeFrom ≤ now ≤ activeUntil)가 구현체/테스트 간 일관성의 핵심입니다. KDoc으로 계약을 명시하면 혼선을 줄일 수 있습니다.
interface VoucherContentReader { - fun readCurrentContents( + /** + * publishedVoucherIds에 해당하는 바우처 컨텐츠 중, + * activeFrom <= now <= activeUntil (경계 포함) 인 "현재 활성" 컨텐츠를 반환합니다. + * 각 바우처당 0개 또는 1개를 반환하는 것이 권장됩니다(다중 매칭 시 결정 규칙 필요). + */ + fun readCurrentContents( publishedVoucherIds: List<Long>, now: OffsetDateTime, ): List<VoucherContent> }payment/voucher/application/src/main/kotlin/app/payment/voucher/application/service/VoucherService.kt (3)
16-21: 읽기 전용 트랜잭션 명시로 JPA 최적화서비스 메서드에 @transactional(readOnly = true)를 권장합니다. flush 방지 및 읽기 최적화에 도움이 됩니다.
@Service class VoucherService( private val voucherReader: VoucherReader, private val voucherContentReader: VoucherContentReader, ) : VoucherUseCase { - override fun getPublishedVouchers(): List<CurrentPublishedVouchers> { + @org.springframework.transaction.annotation.Transactional(readOnly = true) + override fun getPublishedVouchers(): List<CurrentPublishedVouchers> {
23-24: 시계 주입으로 시간 의존성 제어OffsetDateTime.now() 대신 Clock 주입을 권장합니다. 테스트 용이성, 타임존 일관성 확보에 유리합니다.
예시(구현 방향):
- 생성자에 Clock 주입
- now = OffsetDateTime.now(clock)
// 제안 예시 import java.time.Clock ... class VoucherService( private val voucherReader: VoucherReader, private val voucherContentReader: VoucherContentReader, private val clock: Clock, ) : VoucherUseCase { ... val now = OffsetDateTime.now(clock) }애플리케이션 구성에 Clock 빈(예: Clock.systemUTC()) 추가가 필요합니다.
25-28: 여러 컨텐츠 매칭 시 결정 규칙 확인 필요associateBy는 동일 voucherId가 여러 개일 때 마지막 항목만 남습니다. 구현체가 "바우처당 정확히 하나"만 반환한다는 전제가 없으면 결과가 비결정적일 수 있습니다. 리더 단에서 유일성 보장 또는 서비스에서 명시적 선택 규칙(예: 최신 version 우선)을 적용해 주세요.
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentReaderImpl.kt (1)
20-29: 널 강제 연산자(!!)id, createdAtOffset에 !! 사용은 NPE 위험이 있습니다. JPA 매핑을 non-null로 강제하거나, requireNotNull로 명시적 가드/메시지를 남기는 편이 안전합니다.
- id = entity.id!!, + id = requireNotNull(entity.id) { "VoucherContent.id must not be null" }, ... - createdAt = entity.createdAtOffset!! + createdAt = requireNotNull(entity.createdAtOffset) { "VoucherContent.createdAt must not be null" }payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherReader.kt (1)
6-8: 인터페이스는 명확합니다상태 기반 조회 포트 정의 적절합니다. 추후 대량 데이터 대비를 위해 페이징/정렬 파라미터 확장 여지를 고려해 주세요.
payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/inbound/VoucherControllerTest.kt (1)
30-53: 컨트롤러 경로/매핑 테스트 적절핵심 필드(id, title, consumptionType) 검증이 명확합니다. 추가로 Content-Type 검증을 넣으면 안정성이 높아집니다.
mockMvc.perform(get("/api/v1/vouchers")) .andExpect(status().isOk) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.vouchers[0].id").value(1)) .andExpect(jsonPath("$.vouchers[0].title").value("AI 포스터 생성")) .andExpect(jsonPath("$.vouchers[0].consumptionType").value("SINGLE_USE"))payment/voucher/application/src/test/kotlin/app/payment/voucher/application/service/VoucherServiceTest.kt (2)
34-57: 서비스 합성 로직 테스트 잘 구성됨발행 바우처 + 컨텐츠 병합이 의도대로 동작하는지 검증이 명확합니다. 한 케이스만 추가되면 더 견고해집니다: 컨텐츠가 없는 발행 바우처가 있을 때 해당 항목이 결과에서 제외되는지.
예시 테스트 추가:
@Test @DisplayName("컨텐츠가 없는 발행 바우처는 결과에서 제외된다") fun `should drop published voucher without content`() { // given every { voucherReader.readByStatus(VoucherStatus.PUBLISHED) } returns listOf( createVoucher(1L), createVoucher(2L) ) every { voucherContentReader.readCurrentContents(listOf(1L, 2L), any()) } returns listOf( createVoucherContent(1L) // 2L에 대한 컨텐츠 없음 ) // when val result = voucherService.getPublishedVouchers() // then assertEquals(1, result.size) assertEquals(1L, result[0].id) }
29-33: clearAllMocks()는 선택 사항MockKExtension과 @MockK/@InjectMockKs 조합에서는 각 테스트 격리가 보장됩니다. 특별한 부작용이 없다면 clearAllMocks()은 생략 가능해 테스트 가독성이 좋아집니다.
payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImplTest.kt (1)
26-56: 리포지토리 단의 필터를 DB로 위임하는 리팩터 시 테스트 업데이트 제안현재 구현/테스트는 findAll() → 메모리 필터링 모델을 전제로 합니다. 실서비스에서는 상태 기반 쿼리 메서드(예: findByStatus)로 위임하는 편이 바람직합니다. 변화에 대비해 “특정 메서드를 호출했는지”도 검증하도록 테스트를 보완해 주세요.
예시:
every { voucherRepository.findByStatus(VoucherStatus.PUBLISHED) } returns listOf(publishedEntity) val result = voucherReader.readByStatus(VoucherStatus.PUBLISHED) verify(exactly = 1) { voucherRepository.findByStatus(VoucherStatus.PUBLISHED) }payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaEntity.kt (6)
11-14: 필요 import 누락위 변환 수정 시
ZoneIdimport가 필요합니다.import java.time.LocalDateTime import java.time.OffsetDateTime import java.time.ZoneOffset +import java.time.ZoneId
28-33: 감사 필드는 OffsetDateTime으로 저장하고 보조 getter 제거 권장생성 시각을 UTC로 일관 저장하면 변환 오류/혼선을 줄일 수 있습니다. DB 타입도 Timestamptz/Offset 지원으로 맞추는 것을 권장합니다.
다음처럼 바꾸면
createdAtOffset가 불필요합니다. 추가로@Column(updatable = false, nullable = false)로 무결성 보강:- @CreatedDate - var createdAt: LocalDateTime? = null -) { - val createdAtOffset: OffsetDateTime? - get() = createdAt?.atOffset(ZoneOffset.UTC) -} + @CreatedDate + @jakarta.persistence.Column(nullable = false, updatable = false) + var createdAt: OffsetDateTime? = null +)
22-29: DB 제약 명시로 무결성 강화 및 컬럼 타입 최적화Kotlin의 non-null과 별개로 DB 레벨 제약을 명시하세요. description은 길이 초과 우려가 있어 LOB 또는 TEXT가 적합합니다.
필드에
@Column/@Lob를 추가하세요:- val voucherId: Long, - val version: Int, - val title: String, - val description: String, - val activeFrom: OffsetDateTime, - val activeUntil: OffsetDateTime, + @jakarta.persistence.Column(nullable = false) + val voucherId: Long, + @jakarta.persistence.Column(nullable = false) + val version: Int, + @jakarta.persistence.Column(nullable = false, length = 200) + val title: String, + @jakarta.persistence.Lob + @jakarta.persistence.Column(nullable = false) + val description: String, + @jakarta.persistence.Column(nullable = false) + val activeFrom: OffsetDateTime, + @jakarta.persistence.Column(nullable = false) + val activeUntil: OffsetDateTime,설명 컬럼은 운영 DB에 따라
@Lob대신@Column(columnDefinition = "TEXT")를 선택할 수 있습니다.
3-14: JPA/컬럼 관련 import 보강위 주석들 적용 시 필요한 import를 추가하세요.
import jakarta.persistence.Entity import jakarta.persistence.EntityListeners import jakarta.persistence.GeneratedValue import jakarta.persistence.GenerationType import jakarta.persistence.Id import jakarta.persistence.Table +import jakarta.persistence.Column +import jakarta.persistence.Lob import org.springframework.data.annotation.CreatedDate import org.springframework.data.jpa.domain.support.AuditingEntityListener import java.time.LocalDateTime import java.time.OffsetDateTime import java.time.ZoneOffset +import java.time.ZoneId
16-17: 조회 패턴을 고려한 인덱스/제약 설계 권장(voucherId, version) 및 (activeFrom, activeUntil) 기반 조회가 예상됩니다. DB 마이그레이션에서 다음을 고려하세요:
- FK 제약: voucher_contents.voucher_id → vouchers.id
- 인덱스: (voucher_id, version), (active_from, active_until)
23-24:version필드 네이밍 주의 (@Version과의 혼동 가능성)동시성 락킹을 도입할 예정이라면 JPA
@Version필드와 혼동됩니다. 도메인 버전이라면contentVersion등으로 변경을 고려하세요.해당 필드가 도메인 버전인지, 낙관적 락 버전인지 확인 부탁드립니다.
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaEntity.kt (5)
17-20: 필요 import 누락위 변환 수정 시
ZoneIdimport를 추가하세요.import java.time.LocalDateTime import java.time.OffsetDateTime import java.time.ZoneOffset +import java.time.ZoneId
34-37: 감사 필드는 OffsetDateTime 사용 권장UTC 일관 저장을 위해 감사 필드를 OffsetDateTime으로 바꾸고, 컬럼 제약을 강화하세요.
- @CreatedDate - var createdAt: LocalDateTime? = null, - @LastModifiedDate - var updatedAt: LocalDateTime? = null + @CreatedDate + @jakarta.persistence.Column(nullable = false, updatable = false) + var createdAt: OffsetDateTime? = null, + @LastModifiedDate + @jakarta.persistence.Column(nullable = false) + var updatedAt: OffsetDateTime? = null변경 시
createdAtOffset/updatedAtOffset는 단순 전달로 대체 가능하거나 제거 가능합니다.
28-37: DB 무결성 명시: Enum/상태 컬럼에 nullable=false 부여런타임 NPE와 별개로 DB 제약을 명확히 해야 데이터 오염을 방지할 수 있습니다.
@Enumerated(EnumType.STRING) - val type: VoucherType, + @jakarta.persistence.Column(nullable = false) + val type: VoucherType, @Enumerated(EnumType.STRING) - val consumptionType: ConsumptionType, + @jakarta.persistence.Column(nullable = false) + val consumptionType: ConsumptionType, @Enumerated(EnumType.STRING) - var status: VoucherStatus, + @jakarta.persistence.Column(nullable = false) + var status: VoucherStatus,
24-38: 낙관적 락킹(@Version) 추가로 상태 업데이트 경쟁 방지상태 전이가 잦은 엔티티입니다. Lost Update 방지를 위해 버전 칼럼을 추가하세요.
class VoucherJpaEntity( @@ @LastModifiedDate var updatedAt: LocalDateTime? = null ) { + @jakarta.persistence.Version + var rowVersion: Long? = null추가 import:
jakarta.persistence.Version.
21-23: 조회 성능/운영을 위한 인덱스 제안상태/타입/소비유형 기반 목록 조회가 예상됩니다. DB 마이그레이션에서 다음 인덱스를 고려하세요:
- (status)
- (type, status)
- (consumption_type, status)
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (39)
build.gradle.kts(1 hunks)payment/services/api/build.gradle.kts(1 hunks)payment/services/api/src/main/kotlin/app/payment/PaymentApiApplication.kt(1 hunks)payment/services/api/src/test/kotlin/app/payment/integration/VoucherIntegrationTest.kt(1 hunks)payment/services/api/src/test/resources/application-test.yml(1 hunks)payment/src/main/kotlin/app/payment/domain/voucher/ConsumptionType.kt(0 hunks)payment/src/main/kotlin/app/payment/domain/voucher/Voucher.kt(0 hunks)payment/src/main/kotlin/app/payment/domain/voucher/VoucherContent.kt(0 hunks)payment/src/main/kotlin/app/payment/domain/voucher/VoucherStatus.kt(0 hunks)payment/voucher/adapter/build.gradle.kts(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVoucherResponse.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVouchersResponse.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/VoucherController.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/JpaConfiguration.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaEntity.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaRepository.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentReaderImpl.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaEntity.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaRepository.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImpl.kt(1 hunks)payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/TestConfiguration.kt(1 hunks)payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/inbound/VoucherControllerTest.kt(1 hunks)payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImplTest.kt(1 hunks)payment/voucher/adapter/src/test/resources/application-test.yml(1 hunks)payment/voucher/application/build.gradle.kts(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/CurrentPublishedVouchers.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/VoucherUseCase.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherContentReader.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherReader.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/service/VoucherService.kt(1 hunks)payment/voucher/application/src/test/kotlin/app/payment/voucher/application/service/VoucherServiceTest.kt(1 hunks)payment/voucher/domain/build.gradle.kts(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/ConsumptionType.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/Voucher.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherContent.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherStatus.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherType.kt(1 hunks)payment/voucher/domain/src/test/kotlin/app/payment/voucher/domain/VoucherTest.kt(1 hunks)settings.gradle.kts(1 hunks)
💤 Files with no reviewable changes (4)
- payment/src/main/kotlin/app/payment/domain/voucher/VoucherStatus.kt
- payment/src/main/kotlin/app/payment/domain/voucher/Voucher.kt
- payment/src/main/kotlin/app/payment/domain/voucher/VoucherContent.kt
- payment/src/main/kotlin/app/payment/domain/voucher/ConsumptionType.kt
🧰 Additional context used
🧬 Code graph analysis (5)
payment/voucher/domain/src/test/kotlin/app/payment/voucher/domain/VoucherTest.kt (1)
payment/voucher/application/src/test/kotlin/app/payment/voucher/application/service/VoucherServiceTest.kt (1)
createVoucher(75-82)
payment/voucher/adapter/build.gradle.kts (4)
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVoucherResponse.kt (1)
id(3-8)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/CurrentPublishedVouchers.kt (1)
id(7-14)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/Voucher.kt (1)
id(5-12)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherContent.kt (1)
id(5-14)
payment/voucher/domain/build.gradle.kts (2)
payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/Voucher.kt (1)
id(5-12)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherContent.kt (1)
id(5-14)
payment/voucher/application/src/test/kotlin/app/payment/voucher/application/service/VoucherServiceTest.kt (1)
payment/voucher/domain/src/test/kotlin/app/payment/voucher/domain/VoucherTest.kt (1)
createVoucher(35-44)
payment/services/api/build.gradle.kts (4)
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVoucherResponse.kt (1)
id(3-8)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/CurrentPublishedVouchers.kt (1)
id(7-14)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/Voucher.kt (1)
id(5-12)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherContent.kt (1)
id(5-14)
🔇 Additional comments (12)
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/JpaConfiguration.kt (1)
6-8: 확인: kotlin-spring 플러그인 적용됨 — 추가 변경 불필요payment/voucher/adapter/build.gradle.kts:3에 kotlin("plugin.spring") 선언이 있어 @configuration 클래스에
open을 추가할 필요가 없습니다.payment/voucher/domain/build.gradle.kts (1)
7-10: JVM Toolchain 21 + explicitApi 설정 좋습니다도메인 모듈에 적합한 선택입니다.
payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/ConsumptionType.kt (1)
3-6: 도메인 enum 도입 적절합니다표현력이 좋아졌고, 어댑터/애플리케이션에서의 사용도 명확해집니다.
build.gradle.kts (1)
31-33: JUnit Platform Launcher 버전 BOM 위임 변경 LGTMBoot BOM으로 관리하도록 한 점 적절합니다.
settings.gradle.kts (1)
24-35: include("payment") 유지 — payment 루트(aggregator)와 하위 모듈 빌드스크립트 존재 확인
payment/build.gradle.kts가 존재하고 payment/voucher/domain, payment/voucher/application, payment/voucher/adapter, payment/services/api에 각 모듈의 빌드 스크립트가 있으므로 include("payment")는 유지하세요.payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/VoucherUseCase.kt (1)
3-5: 포트 인터페이스 정의 깔끔합니다계약이 명확하고 의존성 방향이 적절합니다. LGTM.
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaRepository.kt (1)
5-5: LGTMSpring Data 인터페이스 선언 적절합니다.
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaRepository.kt (1)
5-5: LGTM표준 JpaRepository 확장 적절합니다.
payment/services/api/src/test/kotlin/app/payment/integration/VoucherIntegrationTest.kt (2)
71-77: 엔드투엔드 관점에서 테스트 시나리오 적절합니다발행( PUBLISHED ) + 활성 기간 내 컨텐츠만 노출되는 경로를 잘 검증하고 있습니다.
71-77: 확인 완료 — GET /api/v1/vouchers 매핑이 맞습니다컨트롤러에 @RequestMapping("/api/v1/vouchers")와 메서드 수준의 @GetMapping이 있어 테스트가 호출하는 경로가 정확합니다. 파일: payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/VoucherController.kt, 관련 테스트: payment/services/api/src/test/kotlin/app/payment/integration/VoucherIntegrationTest.kt, payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/inbound/VoucherControllerTest.kt
payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImplTest.kt (1)
58-69: 빈 결과 케이스 커버 OK상태에 해당하는 바우처가 없을 때 빈 리스트 반환 검증이 명확합니다.
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaEntity.kt (1)
1-48: 확인: kotlin-jpa 플러그인 및 JPA Auditing 활성화됨root build.gradle.kts에 kotlin("plugin.jpa")(v1.9.25) 선언 및 payment/voucher/adapter/build.gradle.kts에 kotlin("plugin.jpa") 적용이 확인되며, payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/JpaConfiguration.kt에 @EnableJpaAuditing가 있고 VoucherJpaEntity/VoucherContentJpaEntity에 @EntityListeners(AuditingEntityListener::class) 사용이 확인됩니다.
...cher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaEntity.kt
Outdated
Show resolved
Hide resolved
...her/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentReaderImpl.kt
Outdated
Show resolved
Hide resolved
...ent/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaEntity.kt
Outdated
Show resolved
Hide resolved
...nt/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImpl.kt
Outdated
Show resolved
Hide resolved
...nt/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImpl.kt
Outdated
Show resolved
Hide resolved
...ent/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/VoucherController.kt
Show resolved
Hide resolved
...her/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentReaderImpl.kt
Outdated
Show resolved
Hide resolved
...her/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentReaderImpl.kt
Outdated
Show resolved
Hide resolved
...er/adapter/src/test/kotlin/app/payment/voucher/adapter/integration/VoucherIntegrationTest.kt
Outdated
Show resolved
Hide resolved
82f6f38 to
2a0c0a8
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (8)
payment/services/api/src/test/kotlin/app/payment/integration/VoucherIntegrationTest.kt (4)
48-56: 테스트 데이터 생성 시 null ID를 명시적으로 전달하고 있습니다.
id = null을 명시적으로 전달하는 것은 중복적입니다. Kotlin의 기본 매개변수나 빌더 패턴을 활용하면 더 간결하게 작성할 수 있습니다.- VoucherJpaEntity( - id = null, - type = VoucherType.AI_POSTER_GENERATE, + VoucherJpaEntity( + type = VoucherType.AI_POSTER_GENERATE,
58-69: 시간 기반 테스트 데이터가 불안정할 수 있습니다.
OffsetDateTime.now()를 사용하면 테스트 실행 시점에 따라 결과가 달라질 수 있습니다. 특히 타임존이나 시스템 시간 변경에 취약합니다.고정된 시간 값을 사용하거나 Clock을 모킹하는 것을 권장합니다:
val fixedTime = OffsetDateTime.parse("2025-09-24T00:00:00Z") VoucherContentJpaEntity( // ... activeFrom = fixedTime.minusDays(1), activeUntil = fixedTime.plusDays(30) )
72-77: 응답의 다른 필드들도 검증하는 것을 권장합니다.현재는
title,description,consumptionType만 검증하고 있습니다.type,status,activeFrom,activeUntil등 다른 중요한 필드들도 함께 검증하면 더 견고한 테스트가 됩니다..andExpect(jsonPath("$.vouchers[0].title").value("테스트 바우처")) .andExpect(jsonPath("$.vouchers[0].description").value("통합 테스트용 바우처입니다")) .andExpect(jsonPath("$.vouchers[0].consumptionType").value("SINGLE_USE")) .andExpect(jsonPath("$.vouchers[0].type").value("AI_POSTER_GENERATE")) .andExpect(jsonPath("$.vouchers[0].activeFrom").exists()) .andExpect(jsonPath("$.vouchers[0].activeUntil").exists())
83-91: DRAFT 바우처에 컨텐츠가 없는 경우도 함께 테스트하고 있습니다.테스트 의도가 "DRAFT 상태의 바우처는 조회되지 않는다"인데, 컨텐츠를 추가하지 않아 두 가지 조건이 혼재되어 있습니다. DRAFT 상태 때문에 필터링되는 것인지, 컨텐츠가 없어서 필터링되는 것인지 명확하지 않을 수 있습니다.
더 명확한 테스트를 위해 DRAFT 바우처에도 컨텐츠를 추가하여 상태(status)만으로 필터링됨을 검증하는 것을 권장합니다:
val savedVoucher = voucherRepository.save( VoucherJpaEntity( id = null, type = VoucherType.AI_POSTER_PURCHASE, consumptionType = ConsumptionType.SUBSCRIPTION, status = VoucherStatus.DRAFT ) ) voucherContentRepository.save( VoucherContentJpaEntity( id = null, voucherId = savedVoucher.id!!, version = 1, title = "Draft 바우처", description = "Draft 상태 테스트", activeFrom = OffsetDateTime.now().minusDays(1), activeUntil = OffsetDateTime.now().plusDays(30) ) )payment/voucher/application/src/test/kotlin/app/payment/voucher/application/service/VoucherServiceTest.kt (2)
29-32: @beforeeach의 clearAllMocks() 호출이 불필요할 수 있습니다.MockKExtension이 각 테스트 간 mock 정리를 자동으로 처리하므로, 명시적인
clearAllMocks()호출은 일반적으로 불필요합니다. 제거를 고려하세요.- @BeforeEach - fun setUp() { - clearAllMocks() - } -
55-56: verify 구문을 더 구체적으로 작성하는 것을 고려하세요.Line 56의
readCurrentContents검증에서any(), any()를 사용하는 대신, 실제 전달된 인자(listOf(1L, 2L))를 명시하면 테스트가 더 엄격해지고 의도를 명확히 할 수 있습니다.- verify(exactly = 1) { voucherContentReader.readCurrentContents(any(), any()) } + verify(exactly = 1) { voucherContentReader.readCurrentContents(listOf(1L, 2L), any()) }payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/TestConfiguration.kt (1)
7-10: 테스트 설정에서 @SpringBootApplication 사용 검토가 필요합니다.
@SpringBootApplication은 컴포넌트 스캔, 자동 설정 등을 모두 활성화하여 테스트 실행 시간이 늘어날 수 있습니다. 통합 테스트가 아닌 경우@TestConfiguration과 필요한 설정만 조합하는 것이 권장됩니다.대안으로 다음과 같이 최소한의 설정만 사용할 수 있습니다:
-@SpringBootApplication -@EntityScan("app.payment.voucher.adapter.outbound") -@EnableJpaRepositories("app.payment.voucher.adapter.outbound") -class TestConfiguration +@TestConfiguration +@EntityScan("app.payment.voucher.adapter.outbound") +@EnableJpaRepositories("app.payment.voucher.adapter.outbound") +class TestJpaConfiguration단, 전체 통합 테스트용 설정이 의도된 경우라면 현재 구조가 적절합니다.
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaEntity.kt (1)
17-19: 사용되지 않는 import 제거 필요
OffsetDateTime과ZoneOffset이 import 되어 있으나 현재 코드에서 사용되지 않습니다. 감사 필드를OffsetDateTime으로 변경한 후에는LocalDateTimeimport를 제거하고,ZoneOffset은 필요 없다면 제거하세요.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (39)
build.gradle.kts(1 hunks)payment/services/api/build.gradle.kts(1 hunks)payment/services/api/src/main/kotlin/app/payment/PaymentApiApplication.kt(1 hunks)payment/services/api/src/test/kotlin/app/payment/integration/VoucherIntegrationTest.kt(1 hunks)payment/services/api/src/test/resources/application-test.yml(1 hunks)payment/src/main/kotlin/app/payment/domain/voucher/ConsumptionType.kt(0 hunks)payment/src/main/kotlin/app/payment/domain/voucher/Voucher.kt(0 hunks)payment/src/main/kotlin/app/payment/domain/voucher/VoucherContent.kt(0 hunks)payment/src/main/kotlin/app/payment/domain/voucher/VoucherStatus.kt(0 hunks)payment/voucher/adapter/build.gradle.kts(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVoucherResponse.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVouchersResponse.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/VoucherController.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/JpaConfiguration.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaEntity.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaRepository.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentReaderImpl.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaEntity.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaRepository.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImpl.kt(1 hunks)payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/TestConfiguration.kt(1 hunks)payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/inbound/VoucherControllerTest.kt(1 hunks)payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImplTest.kt(1 hunks)payment/voucher/adapter/src/test/resources/application-test.yml(1 hunks)payment/voucher/application/build.gradle.kts(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/CurrentPublishedVouchers.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/VoucherUseCase.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherContentReader.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherReader.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/service/VoucherService.kt(1 hunks)payment/voucher/application/src/test/kotlin/app/payment/voucher/application/service/VoucherServiceTest.kt(1 hunks)payment/voucher/domain/build.gradle.kts(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/ConsumptionType.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/Voucher.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherContent.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherStatus.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherType.kt(1 hunks)payment/voucher/domain/src/test/kotlin/app/payment/voucher/domain/VoucherTest.kt(1 hunks)settings.gradle.kts(1 hunks)
💤 Files with no reviewable changes (4)
- payment/src/main/kotlin/app/payment/domain/voucher/Voucher.kt
- payment/src/main/kotlin/app/payment/domain/voucher/VoucherContent.kt
- payment/src/main/kotlin/app/payment/domain/voucher/ConsumptionType.kt
- payment/src/main/kotlin/app/payment/domain/voucher/VoucherStatus.kt
🚧 Files skipped from review as they are similar to previous changes (23)
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaRepository.kt
- payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/Voucher.kt
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/VoucherUseCase.kt
- payment/voucher/adapter/build.gradle.kts
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImpl.kt
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/CurrentPublishedVouchers.kt
- payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherContent.kt
- payment/services/api/src/main/kotlin/app/payment/PaymentApiApplication.kt
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/JpaConfiguration.kt
- payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/ConsumptionType.kt
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/VoucherController.kt
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherContentReader.kt
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentReaderImpl.kt
- payment/services/api/src/test/resources/application-test.yml
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherReader.kt
- payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherStatus.kt
- payment/voucher/application/build.gradle.kts
- settings.gradle.kts
- payment/voucher/adapter/src/test/resources/application-test.yml
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaRepository.kt
- payment/voucher/domain/src/test/kotlin/app/payment/voucher/domain/VoucherTest.kt
- payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImplTest.kt
- payment/services/api/build.gradle.kts
🧰 Additional context used
🧬 Code graph analysis (2)
payment/voucher/application/src/test/kotlin/app/payment/voucher/application/service/VoucherServiceTest.kt (1)
payment/voucher/domain/src/test/kotlin/app/payment/voucher/domain/VoucherTest.kt (1)
createVoucher(35-44)
payment/voucher/domain/build.gradle.kts (3)
payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/CurrentPublishedVouchers.kt (1)
id(7-14)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/Voucher.kt (1)
id(5-12)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherContent.kt (1)
id(5-14)
🔇 Additional comments (15)
payment/services/api/src/test/kotlin/app/payment/integration/VoucherIntegrationTest.kt (2)
23-26:@Transactional사용이 실제 환경과 다른 동작을 유발할 수 있습니다.통합 테스트에서
@Transactional을 사용하면 각 테스트 메서드가 롤백되어 데이터베이스가 깨끗하게 유지되지만, 이는 실제 프로덕션 환경의 트랜잭션 경계와 다를 수 있습니다. 특히 지연 로딩이나 트랜잭션 커밋 시점의 이슈를 감지하지 못할 수 있습니다.
@Transactional대신@BeforeEach에서 명시적으로 데이터를 정리하는 방식(현재 Lines 40-41)을 사용하고 있으므로,@Transactional어노테이션 제거를 고려해보시기 바랍니다.
96-96: 빈 배열 검증 방식을 확인해주세요.
jsonPath("$.vouchers").isEmpty()가 의도대로 동작하는지 확인이 필요합니다. MockMvc의 JsonPath matcher에서는 빈 배열 검증에isEmpty()대신 다른 방식이 필요할 수 있습니다.다음과 같은 방식도 고려해보세요:
.andExpect(jsonPath("$.vouchers").isArray()) .andExpect(jsonPath("$.vouchers").isEmpty()) // 또는 .andExpect(jsonPath("$.vouchers.length()").value(0))실제 테스트를 실행하여 현재 코드가 의도대로 동작하는지 확인해주시기 바랍니다.
payment/voucher/domain/build.gradle.kts (2)
1-10: 도메인 모듈 설정이 헥사고날 아키텍처 원칙을 잘 준수하고 있습니다.순수 도메인 모듈로서 Spring 의존성 없이 구성되었으며,
explicitApi()를 통해 명시적 가시성 제어를 강제하는 것은 도메인 API 경계를 명확히 하는 좋은 선택입니다.
12-18: 테스트 의존성 구성이 적절합니다.순수 도메인 모듈에 적합하게 최소한의 테스트 라이브러리(JUnit Jupiter, AssertJ)만 추가했으며, BOM을 통한 버전 관리를 활용하고 있습니다.
build.gradle.kts (1)
32-32: 수동 확인 필요
Spring Boot 3.5.4 BOM의 POM(https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-dependencies/3.5.4/spring-boot-dependencies-3.5.4.pom) dependencyManagement 섹션에서
<artifactId>junit-platform-launcher</artifactId>바로 아래<version>값을 확인하고, 해당 버전이 기존1.13.3과 호환되는지 검토해주세요.payment/voucher/application/src/test/kotlin/app/payment/voucher/application/service/VoucherServiceTest.kt (2)
59-73: 엣지 케이스를 잘 다루고 있습니다!발행된 바우처가 없을 때 빈 목록을 반환하고
voucherContentReader가 호출되지 않는지 확인하는 테스트가 올바르게 작성되었습니다.
75-93: 헬퍼 메서드가 잘 구성되어 있습니다!테스트 데이터 생성 헬퍼들이 명확하고 간결하게 작성되었으며, 테스트의 가독성을 높입니다.
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVoucherResponse.kt (1)
3-8: LGTM!바우처 단일 응답 DTO가 명확하게 정의되어 있습니다.
consumptionType을 문자열로 반환하는 것은 API 계층에 적합한 설계입니다.payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/inbound/VoucherControllerTest.kt (3)
24-28: 테스트 설정이 적절합니다.
standaloneSetup을 사용하여 컨트롤러 단위 테스트를 구성한 것은 적절합니다. MockK를 활용한 유즈케이스 모킹도 올바르게 구현되었습니다.
30-53: LGTM!발행된 바우처 조회 시나리오가 명확하게 테스트되고 있습니다. Mock 데이터 설정과 응답 검증이 올바르게 구현되었으며, enum에서 문자열로의 변환도 제대로 검증하고 있습니다.
55-65: LGTM!빈 목록 반환 시나리오를 검증하는 엣지 케이스 테스트가 적절하게 구현되었습니다. HTTP 200 상태 코드와 빈 배열 검증이 올바릅니다.
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVouchersResponse.kt (1)
9-20: 매핑 로직은 적절하지만, 응답 DTO에type(바우처 타입)과activeUntil(유효기간) 필드가 포함되지 않았습니다.
클라이언트가 해당 정보가 필요한지 확인해주시고, 필요하다면GetVoucherResponse에 해당 필드를 추가해주세요.payment/voucher/application/src/main/kotlin/app/payment/voucher/application/service/VoucherService.kt (1)
16-39: 로직이 올바르게 구현되었습니다.Published 상태의 바우처와 현재 유효한 컨텐츠를 효율적으로 조합하는 로직이 잘 구현되어 있습니다.
mapNotNull을 사용하여 컨텐츠가 없는 바우처를 안전하게 필터링하고 있습니다.다만, Line 23의
OffsetDateTime.now()는 시스템의 기본 시간대를 사용합니다. 서버가 여러 타임존에 배포되는 경우 일관성 문제가 발생할 수 있으니, 명시적으로 UTC를 사용하는 것을 권장합니다:- val now = OffsetDateTime.now() + val now = OffsetDateTime.now(ZoneOffset.UTC)추가 import가 필요합니다:
java.time.ZoneOffset(이미 임포트되어 있지 않은 경우)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaEntity.kt (1)
1-28: 타임존 처리가 올바르게 개선되었습니다.이전 리뷰에서 지적된
LocalDateTime의 UTC 변환 문제가 해결되었습니다. 모든 타임스탬프 필드(activeFrom,activeUntil,createdAt)가OffsetDateTime을 직접 사용하여 타임존 정보를 보존하고 있습니다.payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherType.kt (1)
1-7: 헥사고날 아키텍처로의 패키지 재구성이 적절합니다.도메인 enum을
app.payment.voucher.domain패키지로 이동하고public가시성을 부여한 것은 멀티모듈 구조에서 adapter와 application 레이어가 도메인 타입에 접근할 수 있도록 하는 올바른 변경입니다.
...er/adapter/src/test/kotlin/app/payment/voucher/adapter/integration/VoucherIntegrationTest.kt
Outdated
Show resolved
Hide resolved
...er/adapter/src/test/kotlin/app/payment/voucher/adapter/integration/VoucherIntegrationTest.kt
Outdated
Show resolved
Hide resolved
...ent/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaEntity.kt
Outdated
Show resolved
Hide resolved
...er/application/src/test/kotlin/app/payment/voucher/application/service/VoucherServiceTest.kt
Outdated
Show resolved
Hide resolved
2a0c0a8 to
a845d05
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (5)
payment/services/api/src/test/kotlin/app/payment/integration/VoucherIntegrationTest.kt (5)
3-6: 어댑터 계층의 구현 세부사항을 직접 import하고 있습니다.헥사고날 아키텍처에서
services/api모듈은 인바운드 포트(컨트롤러/API)를 통해 테스트해야 하며, 아웃바운드 어댑터의 JPA 엔티티와 리포지토리를 직접 의존해서는 안 됩니다. 이는 계층 간 경계를 위반하며, 이전 리뷰에서도 반복적으로 지적된 사항입니다.다음 중 하나의 방법으로 해결해주세요:
- 이 테스트를
voucher/adapter모듈로 이동: 어댑터 계층의 동작을 직접 검증- 도메인/애플리케이션 포트를 통한 테스트 데이터 설정: API 엔드포인트나 애플리케이션 서비스를 통해 테스트 데이터를 생성
Based on past review comments.
32-36: 서비스 API 계층에서 JPA 리포지토리를 직접 주입하고 있습니다.
VoucherJpaRepository와VoucherContentJpaRepository는 아웃바운드 어댑터의 구현체입니다.services/api모듈이 이들을 직접 의존하면 bootjar 분리의 의도가 희석되고 헥사고날 아키텍처 경계가 무너집니다.Based on past review comments.
38-42: 테스트 설정에서 어댑터 리포지토리를 직접 사용하고 있습니다.
setUp메서드에서 JPA 리포지토리를 직접 호출하여 데이터를 정리하는 것은 어댑터 계층에 대한 직접 의존성을 만듭니다. 통합 테스트는 애플리케이션 계층의 포트나 API 엔드포인트를 통해 테스트 데이터를 관리해야 합니다.
44-77: 테스트 로직은 올바르나 어댑터 의존성 문제가 있습니다.테스트 시나리오와 검증 로직은 적절합니다:
- PUBLISHED 상태의 바우처와 컨텐츠가 정상 조회되는지 확인
- MockMvc를 통한 API 응답 검증
하지만 테스트 데이터 설정(lines 48-69)에서
VoucherJpaEntity와VoucherContentJpaEntity를 직접 사용하는 것은 헥사고날 아키텍처를 위반합니다. 어댑터 모듈로 이동하거나, 도메인/애플리케이션 계층의 인터페이스를 통해 테스트 데이터를 생성하도록 수정이 필요합니다.
79-97: 테스트 로직은 올바르나 어댑터 의존성 문제가 있습니다.DRAFT 상태의 바우처가 필터링되는지 확인하는 테스트 시나리오는 적절합니다. 하지만 테스트 데이터 설정(lines 83-91)에서
VoucherJpaEntity를 직접 사용하는 것은 헥사고날 아키텍처 원칙을 위반합니다.
🧹 Nitpick comments (8)
payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/ConsumptionType.kt (1)
3-6:public수정자는 불필요하며, KDoc 추가를 고려하세요.Kotlin에서 클래스는 기본적으로
public이므로public수정자는 중복입니다. 또한 도메인 enum에 각 값의 의미를 설명하는 KDoc을 추가하면 코드 가독성과 유지보수성이 향상됩니다.다음 diff를 적용하여 리팩토링하세요:
-public enum class ConsumptionType { +/** + * 바우처 소비 유형을 나타내는 열거형 + */ +enum class ConsumptionType { + /** 1회성 사용 바우처 */ SINGLE_USE, + /** 구독형 바우처 */ SUBSCRIPTION, }payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherType.kt (1)
3-3:public수식어는 생략 가능합니다.Kotlin에서 최상위 선언은 기본적으로 public입니다.
public키워드를 명시하지 않아도 동일한 가시성을 가지므로, 코드를 더 간결하게 유지하기 위해 생략하는 것을 권장합니다.다음 diff를 적용하여 수정할 수 있습니다:
-public enum class VoucherType { +enum class VoucherType {payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherReader.kt (1)
6-8: 헥사고날 아키텍처 outbound port 인터페이스가 잘 설계되었습니다.인터페이스가 단일 책임 원칙을 따르며, 도메인 타입에만 의존하고 있어 깔끔합니다.
선택적 개선사항:
readByStatus가List<Voucher>를 반환하는데, 향후 바우처 데이터가 증가할 경우 메모리 부담이 될 수 있습니다. 현재는 문제없지만, 추후 페이지네이션 지원을 고려해보시기 바랍니다.// 예시: 향후 페이지네이션 지원을 위한 시그니처 fun readByStatus(status: VoucherStatus, page: Int, size: Int): Page<Voucher> // 또는 fun readByStatus(status: VoucherStatus, pageable: Pageable): Page<Voucher>payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherStatus.kt (2)
3-3:public키워드가 불필요합니다.Kotlin에서 최상위 선언은 기본적으로 public이므로
public키워드를 제거하는 것이 관례입니다.다음 diff를 적용하여 수정할 수 있습니다:
-public enum class VoucherStatus { +enum class VoucherStatus {
3-8: 도메인 enum에 문서화를 추가하는 것을 권장합니다.각 상태의 의미와 바우처 생명주기에서의 역할을 설명하는 KDoc을 추가하면 코드 가독성과 유지보수성이 향상됩니다. 특히 PAUSED와 DISCONTINUED의 차이점을 명확히 하는 것이 좋습니다.
예시:
/** * 바우처의 상태를 나타내는 enum 클래스 */ enum class VoucherStatus { /** 초안 상태 - 아직 공개되지 않음 */ DRAFT, /** 공개됨 - 사용자에게 노출되어 사용 가능 */ PUBLISHED, /** 일시 중지 - 일시적으로 사용 불가 */ PAUSED, /** 종료됨 - 영구적으로 사용 불가 */ DISCONTINUED, }payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVouchersResponse.kt (1)
9-20: consumptionType 매핑 전략을 고려해보세요.현재
consumptionType.name을 사용하여 enum을 문자열로 변환하고 있습니다. 이 방식은 간단하지만 다음을 고려할 수 있습니다:
- enum 이름 변경 시 API 응답이 변경되어 breaking change가 발생할 수 있습니다
- API와 내부 구현이 강하게 결합됩니다
내부 API이거나 enum 이름이 안정적이라면 현재 구현도 괜찮습니다. 하지만 외부 API라면 명시적인 매핑 전략(예: when 표현식 또는 별도의 toApiString() 메서드)을 고려해볼 수 있습니다.
예시:
consumptionType = when (it.consumptionType) { ConsumptionType.SINGLE_USE -> "SINGLE_USE" ConsumptionType.MULTI_USE -> "MULTI_USE" // 명시적 매핑으로 API 안정성 확보 }payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherContent.kt (2)
5-14: 도메인 불변 조건(invariant) 검증을 고려하세요.도메인 계층에서는 비즈니스 규칙을 강제하는 것이 좋습니다. 예를 들어:
title과description이 빈 문자열이 아닌지activeFrom이activeUntil보다 이전인지version이 0 이상인지init 블록이나 팩토리 함수를 통해 검증 로직을 추가하는 것을 권장합니다.
예시:
public data class VoucherContent( public val id: Long, public val voucherId: Long, public val version: Int, public val title: String, public val description: String, public val activeFrom: OffsetDateTime, public val activeUntil: OffsetDateTime, public val createdAt: OffsetDateTime, ) { init { require(title.isNotBlank()) { "Title must not be blank" } require(description.isNotBlank()) { "Description must not be blank" } require(activeFrom.isBefore(activeUntil)) { "activeFrom must be before activeUntil" } require(version >= 0) { "Version must be non-negative" } } }
5-14: Kotlin에서 public 키워드는 생략 가능합니다.Kotlin에서 클래스와 프로퍼티의 기본 가시성은
public이므로 명시적으로 작성할 필요가 없습니다. 코드를 더 간결하게 만들 수 있습니다.다음 diff를 적용하세요:
-public data class VoucherContent( - public val id: Long, - public val voucherId: Long, - public val version: Int, - public val title: String, - public val description: String, - public val activeFrom: OffsetDateTime, - public val activeUntil: OffsetDateTime, - public val createdAt: OffsetDateTime, +data class VoucherContent( + val id: Long, + val voucherId: Long, + val version: Int, + val title: String, + val description: String, + val activeFrom: OffsetDateTime, + val activeUntil: OffsetDateTime, + val createdAt: OffsetDateTime, )
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (39)
build.gradle.kts(1 hunks)payment/services/api/build.gradle.kts(1 hunks)payment/services/api/src/main/kotlin/app/payment/PaymentApiApplication.kt(1 hunks)payment/services/api/src/test/kotlin/app/payment/integration/VoucherIntegrationTest.kt(1 hunks)payment/services/api/src/test/resources/application-test.yml(1 hunks)payment/src/main/kotlin/app/payment/domain/voucher/ConsumptionType.kt(0 hunks)payment/src/main/kotlin/app/payment/domain/voucher/Voucher.kt(0 hunks)payment/src/main/kotlin/app/payment/domain/voucher/VoucherContent.kt(0 hunks)payment/src/main/kotlin/app/payment/domain/voucher/VoucherStatus.kt(0 hunks)payment/voucher/adapter/build.gradle.kts(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVoucherResponse.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVouchersResponse.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/VoucherController.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/JpaConfiguration.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaEntity.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaRepository.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentReaderImpl.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaEntity.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaRepository.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImpl.kt(1 hunks)payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/TestConfiguration.kt(1 hunks)payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/inbound/VoucherControllerTest.kt(1 hunks)payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImplTest.kt(1 hunks)payment/voucher/adapter/src/test/resources/application-test.yml(1 hunks)payment/voucher/application/build.gradle.kts(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/CurrentPublishedVouchers.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/VoucherUseCase.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherContentReader.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherReader.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/service/VoucherService.kt(1 hunks)payment/voucher/application/src/test/kotlin/app/payment/voucher/application/service/VoucherServiceTest.kt(1 hunks)payment/voucher/domain/build.gradle.kts(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/ConsumptionType.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/Voucher.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherContent.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherStatus.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherType.kt(1 hunks)payment/voucher/domain/src/test/kotlin/app/payment/voucher/domain/VoucherTest.kt(1 hunks)settings.gradle.kts(1 hunks)
💤 Files with no reviewable changes (4)
- payment/src/main/kotlin/app/payment/domain/voucher/Voucher.kt
- payment/src/main/kotlin/app/payment/domain/voucher/ConsumptionType.kt
- payment/src/main/kotlin/app/payment/domain/voucher/VoucherContent.kt
- payment/src/main/kotlin/app/payment/domain/voucher/VoucherStatus.kt
🚧 Files skipped from review as they are similar to previous changes (24)
- payment/services/api/src/test/resources/application-test.yml
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaRepository.kt
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/VoucherUseCase.kt
- payment/voucher/application/build.gradle.kts
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/JpaConfiguration.kt
- settings.gradle.kts
- payment/services/api/src/main/kotlin/app/payment/PaymentApiApplication.kt
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImpl.kt
- payment/voucher/application/src/test/kotlin/app/payment/voucher/application/service/VoucherServiceTest.kt
- payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/Voucher.kt
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/VoucherController.kt
- payment/voucher/adapter/build.gradle.kts
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherContentReader.kt
- payment/voucher/adapter/src/test/resources/application-test.yml
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVoucherResponse.kt
- payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/inbound/VoucherControllerTest.kt
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/CurrentPublishedVouchers.kt
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/service/VoucherService.kt
- payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/TestConfiguration.kt
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaEntity.kt
- payment/voucher/domain/src/test/kotlin/app/payment/voucher/domain/VoucherTest.kt
- payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImplTest.kt
- payment/services/api/build.gradle.kts
- payment/voucher/domain/build.gradle.kts
🧰 Additional context used
🪛 GitHub Actions: CI
build.gradle.kts
[error] Execution failed for task ':voucher-adapter:compileKotlin'. Compilation error. See log for details.
🔇 Additional comments (5)
payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherType.kt (1)
1-7: 헥사고날 아키텍처로의 마이그레이션이 깔끔하게 수행되었습니다.패키지 구조 변경과 enum class의 가시성 조정이 멀티모듈 구조와 도메인 모델 노출 측면에서 적절합니다. enum 값들도 변경 없이 유지되어 기존 로직과의 호환성이 보장됩니다.
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVouchersResponse.kt (1)
5-7: LGTM!불변 속성을 사용한 간결한 데이터 클래스 구조가 적절합니다.
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentReaderImpl.kt (1)
17-17: 성능 문제가 해결되었습니다.이전 리뷰에서 지적된
findAll()후 메모리 필터링 문제가 커스텀 쿼리 메서드findActiveContentsByVoucherIds를 사용하도록 수정되어 해결되었습니다. DB 레벨에서 필터링이 수행되어 성능과 메모리 효율성이 크게 개선되었습니다.payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaRepository.kt (1)
9-18: 쿼리 구현이 올바릅니다.커스텀 JPQL 쿼리가 DB 레벨에서 필터링을 수행하여 이전에 지적된 성능 문제를 해결했습니다. 활성 기간 체크 로직(
activeFrom <= :now AND activeUntil >= :now)도 경계값을 포함하여 올바르게 구현되어 있습니다.payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaEntity.kt (1)
13-28: 엔티티 구현 및 UTC 변환 로직은 적절합니다.
다만VoucherContentJpaEntity의id: Long?및createdAt: OffsetDateTime?필드가 nullable이므로,VoucherContentReaderImpl에서entity.id!!·entity.createdAt!!사용 시 NPE 위험이 있습니다.requireNotNull(entity.id) { "…" }등으로 명시적 null 검사나 안전 호출을 도입해 null 안전성을 보장하세요.
...her/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentReaderImpl.kt
Outdated
Show resolved
Hide resolved
payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherContent.kt
Outdated
Show resolved
Hide resolved
bfcf81b to
1faee08
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
build.gradle.kts (1)
31-33: BOM 미적용 모듈에서는 버전 해석 불가 위험이 좌표에서 버전을 제거하면 의존성 관리(BOM)를 적용한 모듈에서만 정상 동작합니다. kotlin("jvm")만 사용하는 순수 도메인·라이브러리 모듈 가운데 아직
io.spring.dependency-management플러그인을 붙이지 않은 곳이 있다면 테스트 런타임 클래스패스 해석이 바로 깨집니다. 해당 모듈에도 플러그인을 추가하거나, 이 블록을 BOM 적용 모듈로 한정해 주세요.#!/bin/bash set -euo pipefail echo "=== BOM 미적용 Kotlin 모듈 검사 ===" fd build.gradle.kts | while read -r file; do if grep -q 'kotlin("jvm")' "$file"; then if ! grep -Eq 'io\.spring\.dependency-management' "$file"; then echo "⚠️ BOM 플러그인 없음: $file" fi fi doneBased on learnings
🧹 Nitpick comments (3)
payment/voucher/adapter/src/test/resources/application-test.yml (1)
3-6: 테스트 DB 유지 옵션을 추가하면 좋겠습니다.H2 인메모리 DB는 마지막 커넥션이 닫히면 스키마가 바로 사라집니다. 테스트에서 커넥션 풀이 재가동되거나 애플리케이션 컨텍스트가 재시작될 때
Table not found같은 오류를 막으려면DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE옵션을 URL에 붙여 지속성을 보장하는 편이 안전합니다.- url: jdbc:h2:mem:testdb;MODE=MySQL + url: jdbc:h2:mem:testdb;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSEpayment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImplTest.kt (1)
25-47: toDomain() 매핑 검증을 보강하면 좋습니다.현재 테스트는
status와id만 검증하지만, 엔티티에는type,consumptionType,createdAt,updatedAt등 추가 필드가 있습니다. 도메인 변환의 완전성을 검증하기 위해 더 많은 필드를 assert하는 것을 고려하세요.예시:
assertEquals(1, result.size) assertEquals(VoucherStatus.PUBLISHED, result[0].status) assertEquals(1L, result[0].id) + assertEquals(VoucherType.AI_POSTER_GENERATE, result[0].type) + assertEquals(ConsumptionType.SINGLE_USE, result[0].consumptionType)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaEntity.kt (1)
14-29: @CreatedDate와 OffsetDateTime 조합 시 UTC 사용 고려
현재@EnableJpaAuditing설정에는 커스텀 DateTimeProvider가 없어 시스템 기본 타임존이 적용됩니다. 서버 간 offset 차이를 방지하려면 UTC로 정규화된 DateTimeProvider 빈을 정의하세요.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (39)
build.gradle.kts(1 hunks)payment/services/api/build.gradle.kts(1 hunks)payment/services/api/src/main/kotlin/app/payment/PaymentApiApplication.kt(1 hunks)payment/services/api/src/test/kotlin/app/payment/integration/VoucherIntegrationTest.kt(1 hunks)payment/services/api/src/test/resources/application-test.yml(1 hunks)payment/src/main/kotlin/app/payment/domain/voucher/ConsumptionType.kt(0 hunks)payment/src/main/kotlin/app/payment/domain/voucher/Voucher.kt(0 hunks)payment/src/main/kotlin/app/payment/domain/voucher/VoucherContent.kt(0 hunks)payment/src/main/kotlin/app/payment/domain/voucher/VoucherStatus.kt(0 hunks)payment/voucher/adapter/build.gradle.kts(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVoucherResponse.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVouchersResponse.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/VoucherController.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/JpaConfiguration.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaEntity.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaRepository.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentReaderImpl.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaEntity.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaRepository.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImpl.kt(1 hunks)payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/TestConfiguration.kt(1 hunks)payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/inbound/VoucherControllerTest.kt(1 hunks)payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImplTest.kt(1 hunks)payment/voucher/adapter/src/test/resources/application-test.yml(1 hunks)payment/voucher/application/build.gradle.kts(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/CurrentPublishedVouchers.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/VoucherUseCase.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherContentReader.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherReader.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/service/VoucherService.kt(1 hunks)payment/voucher/application/src/test/kotlin/app/payment/voucher/application/service/VoucherServiceTest.kt(1 hunks)payment/voucher/domain/build.gradle.kts(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/ConsumptionType.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/Voucher.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherContent.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherStatus.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherType.kt(1 hunks)payment/voucher/domain/src/test/kotlin/app/payment/voucher/domain/VoucherTest.kt(1 hunks)settings.gradle.kts(1 hunks)
💤 Files with no reviewable changes (4)
- payment/src/main/kotlin/app/payment/domain/voucher/ConsumptionType.kt
- payment/src/main/kotlin/app/payment/domain/voucher/VoucherStatus.kt
- payment/src/main/kotlin/app/payment/domain/voucher/VoucherContent.kt
- payment/src/main/kotlin/app/payment/domain/voucher/Voucher.kt
✅ Files skipped from review due to trivial changes (1)
- payment/services/api/src/test/resources/application-test.yml
🚧 Files skipped from review as they are similar to previous changes (22)
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/VoucherUseCase.kt
- payment/voucher/domain/build.gradle.kts
- payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/ConsumptionType.kt
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/CurrentPublishedVouchers.kt
- payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherContent.kt
- settings.gradle.kts
- payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/Voucher.kt
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/service/VoucherService.kt
- payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/inbound/VoucherControllerTest.kt
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/JpaConfiguration.kt
- payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherStatus.kt
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherContentReader.kt
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVoucherResponse.kt
- payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/TestConfiguration.kt
- payment/services/api/src/test/kotlin/app/payment/integration/VoucherIntegrationTest.kt
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/VoucherController.kt
- payment/voucher/domain/src/test/kotlin/app/payment/voucher/domain/VoucherTest.kt
- payment/voucher/application/build.gradle.kts
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVouchersResponse.kt
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaEntity.kt
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaRepository.kt
- payment/voucher/adapter/build.gradle.kts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-01T02:29:11.742Z
Learnt from: inpink
PR: SW-rocket-dan/card-capture-server#32
File: build.gradle.kts:32-34
Timestamp: 2025-10-01T02:29:11.742Z
Learning: In the card-capture-server repository, domain modules (e.g., payment/voucher/domain, platform/auth/domain) intentionally avoid Spring dependencies in production code to maintain framework independence per hexagonal architecture principles. However, they may apply io.spring.dependency-management plugin solely for test dependency version management via BOM, without adding any Spring production dependencies. This keeps the domain pure while enabling consistent test dependency versions.
Applied to files:
build.gradle.kts
🧬 Code graph analysis (2)
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentReaderImpl.kt (3)
payment/src/main/kotlin/app/payment/domain/voucher/VoucherContent.kt (1)
id(5-14)payment/src/main/kotlin/app/payment/domain/voucher/Voucher.kt (1)
id(5-12)payment/src/main/kotlin/app/payment/domain/voucher/VoucherStatus.kt (1)
DRAFT(3-8)
payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherReader.kt (3)
payment/src/main/kotlin/app/payment/domain/voucher/Voucher.kt (1)
id(5-12)payment/src/main/kotlin/app/payment/domain/voucher/VoucherStatus.kt (1)
DRAFT(3-8)payment/src/main/kotlin/app/payment/domain/voucher/VoucherType.kt (1)
AI_POSTER_GENERATE(3-7)
🔇 Additional comments (9)
payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherType.kt (1)
1-7: LGTM! 헥사고날 아키텍처로의 전환이 적절합니다.패키지 구조와 가시성 변경이 멀티모듈 환경에서 도메인 타입의 cross-module 접근을 위해 올바르게 적용되었습니다.
public enum class로의 변경은 voucher-domain, voucher-application, voucher-adapter 레이어 간 의존성 관리에 적합합니다.payment/services/api/src/main/kotlin/app/payment/PaymentApiApplication.kt (3)
1-4: LGTM!표준 Spring Boot Kotlin 애플리케이션에 필요한 최소한의 임포트만 포함하고 있습니다.
6-7: LGTM!명시적인
scanBasePackages설정으로 컴포넌트 스캐닝 범위를app.payment패키지로 제한하고 있습니다. 멀티모듈 환경에서 의도하지 않은 패키지의 빈이 스캔되는 것을 방지하는 좋은 접근입니다.
9-11: LGTM!표준 Spring Boot Kotlin 애플리케이션의 진입점 구현이 올바릅니다.
payment/voucher/application/src/test/kotlin/app/payment/voucher/application/service/VoucherServiceTest.kt (1)
34-72: 테스트 커버리지가 핵심 동작을 잘 검증합니다.발행된 바우처가 있을 때와 없을 때의 흐름, 그리고 각 리더가 호출되는 횟수를 모두 체크해 회귀 위험이 크게 줄었습니다.
payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImplTest.kt (2)
1-23: 테스트 설정이 올바르게 구성되어 있습니다.MockK를 사용한 목 객체 초기화와 BeforeEach를 통한 테스트 픽스처 설정이 적절합니다.
49-60: 빈 목록 시나리오를 정확히 검증합니다.해당 상태의 바우처가 없을 때 빈 리스트 반환을 올바르게 테스트하고 있습니다.
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaEntity.kt (2)
1-13: 필요한 의존성이 적절히 임포트되어 있습니다.Jakarta Persistence API와 Spring Data JPA 감사 기능을 올바르게 사용하고 있습니다.
30-44: 도메인 변환 로직이 올바르게 구현되어 있습니다.
requireNotNull을 통한 null 체크와 필드 매핑이 적절합니다.OffsetDateTime을 그대로 전달하므로 타임존 변환 관련 이슈가 없습니다.참고: 이전 리뷰 코멘트에서 언급된
LocalDateTime.atOffset(UTC)이슈는 현재 코드에는 해당되지 않습니다. 현재는OffsetDateTime을 직접 사용하고 있어 해당 문제가 발생하지 않습니다.
b42b727 to
ee5b8aa
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
payment/voucher/application/build.gradle.kts (1)
18-19: mockk 의존성 버전 누락으로 빌드 실패
버전이 지정되지 않아 Gradle이 의존성을 해석할 수 없습니다. dependency-management BOM에서도 mockk을 관리하지 않으므로 현재 상태로는 테스트 컴파일이 불가능합니다. 명시적으로 버전을 지정하거나 공통 dependencyManagement에 추가해주세요.- testImplementation("io.mockk:mockk") + testImplementation("io.mockk:mockk:1.13.12")
🧹 Nitpick comments (1)
payment/services/api/src/main/kotlin/app/payment/PaymentApiApplication.kt (1)
6-7: scanBasePackages 설정 제거를 고려하세요.
@SpringBootApplication의 기본 스캔 범위는 선언 클래스의 패키지(app.payment) 및 하위 패키지이므로,scanBasePackages = ["app.payment"]는 중복 설정입니다. 제거해도 voucher 모듈(app.payment.voucher.*)이 정상적으로 스캔됩니다.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (40)
.github/workflows/ci.yml(1 hunks)build.gradle.kts(1 hunks)payment/services/api/build.gradle.kts(1 hunks)payment/services/api/src/main/kotlin/app/payment/PaymentApiApplication.kt(1 hunks)payment/services/api/src/test/kotlin/app/payment/integration/VoucherIntegrationTest.kt(1 hunks)payment/services/api/src/test/resources/application-test.yml(1 hunks)payment/src/main/kotlin/app/payment/domain/voucher/ConsumptionType.kt(0 hunks)payment/src/main/kotlin/app/payment/domain/voucher/Voucher.kt(0 hunks)payment/src/main/kotlin/app/payment/domain/voucher/VoucherContent.kt(0 hunks)payment/src/main/kotlin/app/payment/domain/voucher/VoucherStatus.kt(0 hunks)payment/voucher/adapter/build.gradle.kts(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVoucherResponse.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVouchersResponse.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/VoucherController.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/JpaConfiguration.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaEntity.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaRepository.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentReaderImpl.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaEntity.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaRepository.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImpl.kt(1 hunks)payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/TestConfiguration.kt(1 hunks)payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/inbound/VoucherControllerTest.kt(1 hunks)payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImplTest.kt(1 hunks)payment/voucher/adapter/src/test/resources/application-test.yml(1 hunks)payment/voucher/application/build.gradle.kts(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/CurrentPublishedVouchers.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/VoucherUseCase.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherContentReader.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherReader.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/service/VoucherService.kt(1 hunks)payment/voucher/application/src/test/kotlin/app/payment/voucher/application/service/VoucherServiceTest.kt(1 hunks)payment/voucher/domain/build.gradle.kts(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/ConsumptionType.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/Voucher.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherContent.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherStatus.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherType.kt(1 hunks)payment/voucher/domain/src/test/kotlin/app/payment/voucher/domain/VoucherTest.kt(1 hunks)settings.gradle.kts(1 hunks)
💤 Files with no reviewable changes (4)
- payment/src/main/kotlin/app/payment/domain/voucher/ConsumptionType.kt
- payment/src/main/kotlin/app/payment/domain/voucher/VoucherContent.kt
- payment/src/main/kotlin/app/payment/domain/voucher/VoucherStatus.kt
- payment/src/main/kotlin/app/payment/domain/voucher/Voucher.kt
🚧 Files skipped from review as they are similar to previous changes (26)
- payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/ConsumptionType.kt
- payment/voucher/application/src/test/kotlin/app/payment/voucher/application/service/VoucherServiceTest.kt
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVoucherResponse.kt
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/VoucherController.kt
- payment/services/api/src/test/resources/application-test.yml
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/service/VoucherService.kt
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherReader.kt
- payment/services/api/build.gradle.kts
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherContentReader.kt
- payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/TestConfiguration.kt
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentReaderImpl.kt
- payment/voucher/domain/src/test/kotlin/app/payment/voucher/domain/VoucherTest.kt
- payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherStatus.kt
- payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImplTest.kt
- payment/voucher/domain/build.gradle.kts
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/VoucherUseCase.kt
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaRepository.kt
- payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/Voucher.kt
- payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/inbound/VoucherControllerTest.kt
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImpl.kt
- payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherContent.kt
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/CurrentPublishedVouchers.kt
- build.gradle.kts
- payment/voucher/adapter/build.gradle.kts
- payment/voucher/adapter/src/test/resources/application-test.yml
- payment/services/api/src/test/kotlin/app/payment/integration/VoucherIntegrationTest.kt
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-01T02:29:11.742Z
Learnt from: inpink
PR: SW-rocket-dan/card-capture-server#32
File: build.gradle.kts:32-34
Timestamp: 2025-10-01T02:29:11.742Z
Learning: In the card-capture-server repository, domain modules (e.g., payment/voucher/domain, platform/auth/domain) intentionally avoid Spring dependencies in production code to maintain framework independence per hexagonal architecture principles. However, they may apply io.spring.dependency-management plugin solely for test dependency version management via BOM, without adding any Spring production dependencies. This keeps the domain pure while enabling consistent test dependency versions.
Applied to files:
payment/voucher/application/build.gradle.kts
🧬 Code graph analysis (1)
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVouchersResponse.kt (5)
payment/src/main/kotlin/app/payment/domain/voucher/Voucher.kt (1)
id(5-12)payment/src/main/kotlin/app/payment/domain/voucher/VoucherContent.kt (1)
id(5-14)payment/src/main/kotlin/app/payment/domain/voucher/VoucherStatus.kt (1)
DRAFT(3-8)payment/src/main/kotlin/app/payment/domain/voucher/VoucherType.kt (1)
AI_POSTER_GENERATE(3-7)payment/src/main/kotlin/app/payment/domain/voucher/ConsumptionType.kt (1)
SINGLE_USE(3-6)
🔇 Additional comments (13)
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVouchersResponse.kt (1)
5-22: 깔끔한 DTO 매핑 구현입니다.헥사고날 아키텍처의 어댑터 레이어에서 애플리케이션 포트 타입(
CurrentPublishedVouchers)을 REST 응답 DTO로 변환하는 로직이 명확하고 간결합니다.consumptionType.name을 통해 열거형을 문자열로 직렬화하는 방식도 API 응답에 적합합니다.settings.gradle.kts (1)
24-34: 모듈 include 구성 적절합니다
결제 바우처 계층을 도메인·애플리케이션·어댑터·API로 분리한 include 및 projectDir 매핑이 기존 플랫폼 구성 패턴과 일관되고, 추후 서브모듈 설정과의 충돌 우려가 없습니다.payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/JpaConfiguration.kt (1)
6-8: @EnableJpaRepositories 생략해도 됩니다. Spring Boot Data JPA 자동 설정(JpaRepositoriesAutoConfiguration)이app.payment패키지 하위를 스캔해app.payment.voucher.adapter.outbound리포지토리도 자동으로 감지하므로, 별도 @EnableJpaRepositories 설정은 필요 없습니다.payment/services/api/src/main/kotlin/app/payment/PaymentApiApplication.kt (2)
1-5: LGTM!패키지 선언과 import 구문이 올바르게 구성되어 있습니다.
9-11: LGTM!Spring Boot Kotlin 애플리케이션의 표준적인 진입점 구현입니다.
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaEntity.kt (4)
1-12: LGTM: 임포트 및 패키지 구성이 적절합니다.헥사고날 아키텍처의 어댑터 레이어에 적합한 패키지 구조이며, Jakarta Persistence API와 Spring Data JPA 오디팅을 올바르게 사용하고 있습니다.
OffsetDateTime타입은 타임존 정보를 포함하여 저장하므로 적절한 선택입니다.
14-16: LGTM: 엔티티 어노테이션이 올바르게 구성되었습니다.
@EntityListeners(AuditingEntityListener::class)를 통해@CreatedDate자동 관리가 활성화되며, 명시적인 테이블 이름 매핑도 적절합니다.
30-43: LGTM: 도메인 변환 로직이 올바릅니다.
requireNotNull을 사용하여 영속화되지 않은 엔티티에 대한 변환을 방지하고, 모든 필드가 도메인 객체로 정확하게 매핑됩니다.OffsetDateTime필드를 별도 변환 없이 그대로 전달하는 것이 적절합니다.
17-29: JPA 오디팅 활성화 확인됨
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/JpaConfiguration.kt7행에@EnableJpaAuditing선언이 있어@CreatedDate가 정상 작동합니다. 추가 설정 불필요합니다.payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherType.kt (1)
1-7: LGTM! 헥사고날 아키텍처 모듈 분리에 맞춰 패키지와 가시성이 적절하게 변경되었습니다.도메인 모델을
public으로 노출하고 패키지를app.payment.voucher.domain으로 재구성한 것은 멀티모듈 구조에서 올바른 접근입니다. 열거형 값들도 명확하고 비즈니스 의미가 분명합니다.payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaRepository.kt (1)
6-8: LGTM! Spring Data JPA 레포지토리 인터페이스가 올바르게 구현되었습니다.헥사고날 아키텍처의 outbound adapter로서 적절한 위치에 배치되었으며, 쿼리 메서드 명명 규칙도 Spring Data 표준을 따릅니다.
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaEntity.kt (2)
20-37: LGTM! JPA 엔티티가 헥사고날 아키텍처의 outbound adapter로 적절하게 설계되었습니다.
@Enumerated(EnumType.STRING)사용으로 데이터베이스 호환성 확보OffsetDateTime사용으로 타임존 정보 보존 (이전 리뷰 피드백 반영 완료)@EntityListeners와 감사(audit) 어노테이션 적용
38-49: LGTM!toDomain()메서드가 도메인 모델로의 안전한 변환을 보장합니다.
requireNotNull을 사용한 null-check는 영속화된 엔티티의 불변성을 명확하게 검증하며, 헥사고날 아키텍처에서 adapter → domain 변환의 모범 사례입니다.
...ent/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaEntity.kt
Outdated
Show resolved
Hide resolved
...voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaRepository.kt
Show resolved
Hide resolved
ee5b8aa to
95a0ace
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (4)
payment/voucher/adapter/build.gradle.kts (1)
19-24: H2 테스트 드라이버 누락으로 테스트 컨텍스트 부팅이 실패합니다.
application-test.yml에서 H2를 사용하도록 설정했으면, 테스트 클래스패스에com.h2database:h2가 없으면 내장 DB 드라이버를 찾지 못해Failed to replace DataSource with an embedded database예외가 발생합니다.(stackoverflow.com) 기존 리뷰에서 이미 지적된 동일 이슈이므로 이번 커밋에서 반드시 반영해주세요. 다음 수정안을 적용하면 됩니다:// --- test --- testImplementation("org.springframework.boot:spring-boot-starter-test") { exclude(group = "org.mockito") } testImplementation("com.ninja-squad:springmockk:4.0.2") + testImplementation("com.h2database:h2")payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherContent.kt (3)
5-14: data class로 변경하여 불변 값 객체 의미론을 명확히 하세요.도메인 모델은
data class로 선언하는 것이 권장됩니다.equals(),hashCode(),copy(),toString()이 자동 생성되어 값 객체(Value Object)로서 올바르게 동작합니다. 또한 명시적인public수식어는 Kotlin에서 기본값이므로 제거 가능합니다.다음 diff를 적용하세요:
-class VoucherContent( - public val id: Long, - public val voucherId: Long, - public val version: Int, - public val title: String, - public val description: String, - public val activeFrom: OffsetDateTime, - public val activeUntil: OffsetDateTime, - public val createdAt: OffsetDateTime, +data class VoucherContent( + val id: Long, + val voucherId: Long, + val version: Int, + val title: String, + val description: String, + val activeFrom: OffsetDateTime, + val activeUntil: OffsetDateTime, + val createdAt: OffsetDateTime, )
5-14: 도메인 모델을 data class로 변경하세요.값 객체(Value Object)로 사용되는 도메인 모델은
data class로 선언하여equals(),hashCode(),copy(),toString()을 자동 생성하는 것이 권장됩니다.다음 diff를 적용하세요:
-class VoucherContent( +data class VoucherContent( public val id: Long, public val voucherId: Long, public val version: Int, public val title: String, public val description: String, public val activeFrom: OffsetDateTime, public val activeUntil: OffsetDateTime, public val createdAt: OffsetDateTime, )
5-14: data class로 변경하여 값 객체 의미론을 보장하세요.도메인 모델로 사용되는 클래스는
data class로 선언하는 것이 권장됩니다.equals(),hashCode(),copy(),toString()등이 자동 생성되어 값 객체(Value Object)로서 올바르게 동작합니다.다음 diff를 적용하세요:
-class VoucherContent( +data class VoucherContent( public val id: Long, public val voucherId: Long, public val version: Int, public val title: String, public val description: String, public val activeFrom: OffsetDateTime, public val activeUntil: OffsetDateTime, public val createdAt: OffsetDateTime, )
🧹 Nitpick comments (13)
payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherContentReader.kt (1)
6-11: 인터페이스 설계가 헥사고날 아키텍처 원칙을 잘 따르고 있습니다.아웃바운드 포트로서 역할이 명확하고, 도메인 모델을 반환하는 등 레이어 분리가 적절합니다. 메서드 시그니처도 타입 안전하고 Kotlin 관례를 잘 따릅니다.
선택적 개선사항: 포트는 레이어 간 계약이므로 KDoc을 추가하면 계약 명세를 명확히 할 수 있습니다.
+/** + * 현재 활성화된 바우처 콘텐츠를 조회하는 아웃바운드 포트 + */ interface VoucherContentReader { + /** + * 주어진 시점에 활성화된 바우처 콘텐츠 목록을 조회합니다. + * + * @param publishedVoucherIds 조회할 바우처 ID 목록 + * @param now 조회 기준 시점 + * @return 활성화된 바우처 콘텐츠 목록 (결과가 없으면 빈 리스트) + */ fun readCurrentContents( publishedVoucherIds: List<Long>, now: OffsetDateTime, ): List<VoucherContent> }payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImplTest.kt (2)
25-47: Mock 호출 검증을 추가하세요.테스트가 given-when-then 구조를 잘 따르고 있으나,
voucherRepository.findByStatus가 실제로 호출되었는지 검증하는 로직이 누락되었습니다. MockK의verify를 사용하여 Mock 상호작용을 확인하는 것이 좋습니다.다음 diff를 적용하여 Mock 호출 검증을 추가하세요:
+import io.mockk.verify + // then assertEquals(1, result.size) assertEquals(VoucherStatus.PUBLISHED, result[0].status) assertEquals(1L, result[0].id) +verify(exactly = 1) { voucherRepository.findByStatus(VoucherStatus.PUBLISHED) }추가로, 도메인 매핑이 완전한지 확인하기 위해
type,consumptionType등 추가 필드도 검증하는 것을 고려해보세요.
49-60: Mock 호출 검증을 추가하세요.빈 목록 시나리오를 올바르게 테스트하고 있으나, 첫 번째 테스트와 마찬가지로
voucherRepository.findByStatus가 호출되었는지 검증하는 로직이 누락되었습니다.다음 diff를 적용하여 Mock 호출 검증을 추가하세요:
+import io.mockk.verify + // then assertTrue(result.isEmpty()) +verify(exactly = 1) { voucherRepository.findByStatus(VoucherStatus.PAUSED) }payment/voucher/domain/src/test/kotlin/app/payment/voucher/domain/VoucherTest.kt (2)
10-21: 도메인 캡슐화 개선: 상태 변경 메서드 추가 권장현재
voucher.status를 직접 변경하고 있는데, 헥사고날 아키텍처와 DDD 원칙에서는 도메인 엔티티가 상태 변경 로직을 메서드로 캡슐화하는 것이 권장됩니다.
Voucher도메인 엔티티에changeStatus(newStatus: VoucherStatus)같은 메서드를 추가하여 상태 전이 규칙(예: DISCONTINUED에서 PUBLISHED로 변경 불가)을 검증할 수 있습니다.다음과 같이 개선할 수 있습니다:
// Voucher.kt에 추가 +fun changeStatus(newStatus: VoucherStatus) { + // 상태 전이 검증 로직 추가 가능 + this.status = newStatus +}// 테스트 코드 수정 -voucher.status = VoucherStatus.PAUSED +voucher.changeStatus(VoucherStatus.PAUSED)
23-33: 테스트 의도와 구현 불일치:isActive()메서드 추가 고려테스트 이름은 "바우처가 활성 상태인지 확인"이지만, 실제로는 단순히 상태값 비교만 하고 있습니다.
도메인 모델에
isActive(): Boolean메서드를 추가하여 활성 상태 판별 로직을 캡슐화하면, 코드 가독성과 유지보수성이 향상됩니다. 또한DISCONTINUED상태에 대한 테스트도 추가하는 것이 좋습니다.다음과 같이 개선할 수 있습니다:
// Voucher.kt에 추가 +fun isActive(): Boolean = status == VoucherStatus.PUBLISHED// 테스트 코드 수정 @Test @DisplayName("바우처가 활성 상태인지 확인할 수 있다") fun `should check if voucher is active`() { // given val activeVoucher = createVoucher(status = VoucherStatus.PUBLISHED) val pausedVoucher = createVoucher(status = VoucherStatus.PAUSED) + val discontinuedVoucher = createVoucher(status = VoucherStatus.DISCONTINUED) // then - assertThat(activeVoucher.status).isEqualTo(VoucherStatus.PUBLISHED) - assertThat(pausedVoucher.status).isNotEqualTo(VoucherStatus.PUBLISHED) + assertThat(activeVoucher.isActive()).isTrue() + assertThat(pausedVoucher.isActive()).isFalse() + assertThat(discontinuedVoucher.isActive()).isFalse() }payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaEntity.kt (1)
23-52: 헥사고날 아키텍처: 도메인→JPA 변환 메서드 추가 권장현재
toDomain()메서드만 있어 JPA → 도메인 방향의 변환만 가능합니다. 헥사고날 아키텍처의 outbound 어댑터에서는 도메인 객체를 영속화하기 위해 양방향 변환이 필요한 경우가 많습니다.신규 바우처 생성이나 업데이트 시 도메인 객체로부터 JPA 엔티티를 생성하는
fromDomain()팩토리 메서드 추가를 고려하세요.다음과 같이 companion object에 팩토리 메서드를 추가할 수 있습니다:
companion object { fun fromDomain(voucher: Voucher): VoucherJpaEntity { return VoucherJpaEntity( id = voucher.id, type = voucher.type, consumptionType = voucher.consumptionType, status = voucher.status, createdAt = voucher.createdAt, updatedAt = voucher.updatedAt ) } }payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaRepository.kt (2)
15-18: 빈 voucherIds 리스트 처리와 결과 정렬을 고려하세요.현재 구현에서 개선 가능한 사항:
voucherIds가 빈 리스트일 경우 SQL의IN ()구문이 DB에 따라 오류를 발생시킬 수 있습니다- 결과 순서가 보장되지 않아 클라이언트 동작이 비결정적일 수 있습니다
결과 정렬 추가 예시:
@Query(""" SELECT vc FROM VoucherContentJpaEntity vc WHERE vc.voucherId IN :voucherIds AND vc.activeFrom <= :now AND vc.activeUntil >= :now + ORDER BY vc.voucherId, vc.version DESC """) fun findActiveContentsByVoucherIds(
8-19: LGTM! 쿼리 로직이 올바릅니다.시간 범위 조건(
activeFrom <= :now AND activeUntil >= :now)과 IN 절을 사용한 배치 조회가 적절합니다.성능 최적화를 위해
voucher_contents테이블에(voucherId, activeFrom, activeUntil)복합 인덱스 추가를 권장합니다.payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/inbound/VoucherControllerTest.kt (2)
19-66: 테스트 커버리지를 확장하세요.현재 테스트는 기본 동작을 검증하지만 다음을 추가하면 좋습니다:
- 여러 바우처 반환 케이스
- 다른
ConsumptionType값 (SUBSCRIPTION) 테스트- 다양한
VoucherType값 테스트여러 바우처 테스트 예시:
@Test @DisplayName("여러 바우처를 반환한다") fun `should return multiple vouchers`() { val vouchers = listOf( CurrentPublishedVouchers( id = 1L, type = VoucherType.AI_POSTER_GENERATE, consumptionType = ConsumptionType.SINGLE_USE, title = "AI 포스터 생성", description = "설명1", activeUntil = OffsetDateTime.now().plusDays(30) ), CurrentPublishedVouchers( id = 2L, type = VoucherType.AI_POSTER_GENERATE, consumptionType = ConsumptionType.SUBSCRIPTION, title = "구독형 바우처", description = "설명2", activeUntil = OffsetDateTime.now().plusDays(60) ) ) every { voucherUseCase.getPublishedVouchers() } returns vouchers mockMvc.perform(get("/api/v1/vouchers")) .andExpect(status().isOk) .andExpect(jsonPath("$.vouchers.length()").value(2)) .andExpect(jsonPath("$.vouchers[1].consumptionType").value("SUBSCRIPTION")) }
30-53: 테스트 로직이 올바르지만 description 필드 검증을 추가하는 것을 고려하세요.테스트가
id,title,consumptionType을 검증하지만description필드는 설정만 하고 검증하지 않습니다.다음과 같이 검증을 추가할 수 있습니다:
mockMvc.perform(get("/api/v1/vouchers")) .andExpect(status().isOk) .andExpect(jsonPath("$.vouchers[0].id").value(1)) .andExpect(jsonPath("$.vouchers[0].title").value("AI 포스터 생성")) .andExpect(jsonPath("$.vouchers[0].consumptionType").value("SINGLE_USE")) + .andExpect(jsonPath("$.vouchers[0].description").value("AI를 사용한 포스터 생성 바우처"))payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/integration/VoucherIntegrationTest.kt (1)
45-78: 테스트 커버리지를 강화하는 것을 고려하세요.현재 테스트는 기본적인 동작을 검증하지만, 다음 항목을 추가로 검증하면 더욱 견고해집니다:
voucherId,type,status,version필드activeFrom,activeUntil날짜 범위- 응답 배열의 크기 검증
다음과 같이 추가 검증을 고려해보세요:
mockMvc.perform(get("/api/v1/vouchers")) .andExpect(status().isOk) + .andExpect(jsonPath("$.vouchers.length()").value(1)) + .andExpect(jsonPath("$.vouchers[0].voucherId").value(savedVoucher.id)) .andExpect(jsonPath("$.vouchers[0].title").value("테스트 바우처")) .andExpect(jsonPath("$.vouchers[0].description").value("통합 테스트용 바우처입니다")) .andExpect(jsonPath("$.vouchers[0].consumptionType").value("SINGLE_USE")) + .andExpect(jsonPath("$.vouchers[0].type").value("AI_POSTER_GENERATE"))payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaEntity.kt (1)
27-28: UTC 기준 JPA Auditing 타임존 설정 추가
application.yml또는application.properties에 아래 설정을 추가해 JPA Auditing(@CreatedDate)이 UTC 기준으로 동작하도록 보장하세요.또는spring: jpa: properties: hibernate: jdbc: time_zone: UTCspring.jpa.properties.hibernate.jdbc.time_zone=UTCpayment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherContent.kt (1)
6-13: public 키워드 제거로 코드를 간결하게 만드세요.Kotlin에서 클래스 멤버의 가시성 기본값은
public이므로 명시적인public키워드는 불필요합니다.다음 diff를 적용하세요:
data class VoucherContent( - public val id: Long, - public val voucherId: Long, - public val version: Int, - public val title: String, - public val description: String, - public val activeFrom: OffsetDateTime, - public val activeUntil: OffsetDateTime, - public val createdAt: OffsetDateTime, + val id: Long, + val voucherId: Long, + val version: Int, + val title: String, + val description: String, + val activeFrom: OffsetDateTime, + val activeUntil: OffsetDateTime, + val createdAt: OffsetDateTime, )
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (39)
.github/workflows/ci.yml(1 hunks)build.gradle.kts(1 hunks)payment/services/api/build.gradle.kts(1 hunks)payment/services/api/src/main/kotlin/app/payment/PaymentApiApplication.kt(1 hunks)payment/services/api/src/test/resources/application-test.yml(1 hunks)payment/src/main/kotlin/app/payment/domain/voucher/Voucher.kt(0 hunks)payment/src/main/kotlin/app/payment/domain/voucher/VoucherContent.kt(0 hunks)payment/voucher/adapter/build.gradle.kts(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVoucherResponse.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVouchersResponse.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/VoucherController.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/JpaConfiguration.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaEntity.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaRepository.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentReaderImpl.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaEntity.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaRepository.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImpl.kt(1 hunks)payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/TestConfiguration.kt(1 hunks)payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/config/TestConfig.kt(1 hunks)payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/inbound/VoucherControllerTest.kt(1 hunks)payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/integration/VoucherIntegrationTest.kt(1 hunks)payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImplTest.kt(1 hunks)payment/voucher/adapter/src/test/resources/application-test.yml(1 hunks)payment/voucher/application/build.gradle.kts(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/CurrentPublishedVouchers.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/VoucherUseCase.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherContentReader.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherReader.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/service/VoucherService.kt(1 hunks)payment/voucher/application/src/test/kotlin/app/payment/voucher/application/service/VoucherServiceTest.kt(1 hunks)payment/voucher/domain/build.gradle.kts(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/ConsumptionType.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/Voucher.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherContent.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherStatus.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherType.kt(1 hunks)payment/voucher/domain/src/test/kotlin/app/payment/voucher/domain/VoucherTest.kt(1 hunks)settings.gradle.kts(1 hunks)
💤 Files with no reviewable changes (2)
- payment/src/main/kotlin/app/payment/domain/voucher/VoucherContent.kt
- payment/src/main/kotlin/app/payment/domain/voucher/Voucher.kt
✅ Files skipped from review due to trivial changes (1)
- payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/config/TestConfig.kt
🚧 Files skipped from review as they are similar to previous changes (23)
- build.gradle.kts
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaRepository.kt
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/VoucherController.kt
- payment/services/api/src/test/resources/application-test.yml
- payment/services/api/src/main/kotlin/app/payment/PaymentApiApplication.kt
- payment/voucher/application/build.gradle.kts
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherReader.kt
- settings.gradle.kts
- payment/services/api/build.gradle.kts
- payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/Voucher.kt
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/CurrentPublishedVouchers.kt
- payment/voucher/application/src/test/kotlin/app/payment/voucher/application/service/VoucherServiceTest.kt
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/JpaConfiguration.kt
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/service/VoucherService.kt
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVoucherResponse.kt
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/VoucherUseCase.kt
- .github/workflows/ci.yml
- payment/voucher/domain/build.gradle.kts
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImpl.kt
- payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/TestConfiguration.kt
- payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/ConsumptionType.kt
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentReaderImpl.kt
- payment/voucher/adapter/src/test/resources/application-test.yml
🧰 Additional context used
🧬 Code graph analysis (5)
payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherContentReader.kt (4)
payment/src/main/kotlin/app/payment/domain/voucher/VoucherContent.kt (1)
id(5-14)payment/src/main/kotlin/app/payment/domain/voucher/Voucher.kt (1)
id(5-12)payment/src/main/kotlin/app/payment/domain/voucher/VoucherStatus.kt (1)
DRAFT(3-8)payment/src/main/kotlin/app/payment/domain/voucher/VoucherType.kt (1)
AI_POSTER_GENERATE(3-7)
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVouchersResponse.kt (2)
payment/src/main/kotlin/app/payment/domain/voucher/Voucher.kt (1)
id(5-12)payment/src/main/kotlin/app/payment/domain/voucher/ConsumptionType.kt (1)
SINGLE_USE(3-6)
payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherType.kt (3)
payment/src/main/kotlin/app/payment/domain/voucher/Voucher.kt (1)
id(5-12)payment/src/main/kotlin/app/payment/domain/voucher/VoucherType.kt (1)
AI_POSTER_GENERATE(3-7)payment/src/main/kotlin/app/payment/domain/voucher/VoucherContent.kt (1)
id(5-14)
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaEntity.kt (4)
payment/src/main/kotlin/app/payment/domain/voucher/Voucher.kt (1)
id(5-12)payment/src/main/kotlin/app/payment/domain/voucher/VoucherStatus.kt (1)
DRAFT(3-8)payment/src/main/kotlin/app/payment/domain/voucher/VoucherType.kt (1)
AI_POSTER_GENERATE(3-7)payment/src/main/kotlin/app/payment/domain/voucher/ConsumptionType.kt (1)
SINGLE_USE(3-6)
payment/voucher/adapter/build.gradle.kts (4)
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVoucherResponse.kt (1)
id(3-8)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/CurrentPublishedVouchers.kt (1)
id(7-14)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/Voucher.kt (1)
id(5-12)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherContent.kt (1)
id(5-14)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: test-payment
🔇 Additional comments (25)
payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImplTest.kt (2)
1-13: 패키지 선언과 임포트가 적절합니다.필요한 도메인 클래스, MockK, JUnit 5 의존성이 올바르게 임포트되었습니다.
14-23: 테스트 픽스처 설정이 올바릅니다.MockK를 활용한 표준적인 JUnit 5 테스트 설정 패턴을 따르고 있습니다.
payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherType.kt (1)
1-1: ```shell
#!/bin/bash
echo "=== 이전 패키지 경로 참조 (app.payment.domain.voucher) 검색 ==="
rg -n "app\.payment\.domain\.voucher" --glob '*.kt'echo -e "\n=== VoucherType import 검색 (모든 import 라인) ==="
rg -n "import .VoucherType" --glob '.kt'</blockquote></details> <details> <summary>payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherStatus.kt (1)</summary><blockquote> `1-1`: **LGTM: 헥사고날 아키텍처로의 패키지 구조 변경** 도메인 모듈의 새로운 패키지 구조로 깔끔하게 이동되었습니다. </blockquote></details> <details> <summary>payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaEntity.kt (2)</summary><blockquote> `20-37`: **LGTM: JPA 감사 필드 구성이 올바르게 수정됨** 이전 리뷰에서 지적된 감사 필드 이슈가 해결되었습니다: - `createdAt`/`updatedAt`이 `var`로 선언되어 JPA가 값을 주입할 수 있음 - `OffsetDateTime`을 사용하여 타임존 정보 보존 - `nullable` + `default null`로 JPA 감사 기능이 정상 작동 --- `38-51`: **toDomain 메서드 매핑 검증 완료** `toDomain()` 메서드가 새로운 `Voucher` 생성자(id, type, consumptionType, status, createdAt, updatedAt)와 정확히 일치하며, 추가 수정 불필요합니다. </blockquote></details> <details> <summary>payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaEntity.kt (5)</summary><blockquote> `30-44`: **LGTM! 도메인 매핑 로직이 안전합니다.** `requireNotNull`을 사용하여 null 불가능한 필드를 검증하고 명확한 오류 메시지를 제공합니다. 도메인 객체로의 매핑이 정확합니다. --- `27-28`: **JPA Auditing 기본 DateTimeProvider 미정의** 현재 `JpaConfiguration.kt`에 `@EnableJpaAuditing`만 설정되어 있어 기본 `DateTimeProvider`가 사용됩니다. 필요한 경우 UTC 혹은 시스템 타임존 기반의 `OffsetDateTime`을 반환하는 커스텀 `DateTimeProvider` 빈을 정의하거나, 기본 설정이 의도한 동작인지 검증해주세요. --- `30-44`: **LGTM! toDomain() 매핑 로직이 올바릅니다.** null 체크와 필드 매핑이 적절하며, 도메인 모델로의 변환이 정확합니다. --- `14-29`: **LGTM!** JPA 엔티티 구조가 올바르며, `@CreatedDate`를 통한 감사(auditing) 설정이 적절합니다. `OffsetDateTime`을 직접 사용하여 시간대 처리가 명확합니다. --- `30-44`: **LGTM!** `toDomain()` 메서드가 `requireNotNull`로 필수 필드를 검증하고 도메인 객체로 올바르게 매핑합니다. </blockquote></details> <details> <summary>payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVouchersResponse.kt (3)</summary><blockquote> `15-15`: **LGTM! enum을 String으로 안전하게 변환합니다.** `consumptionType.name`을 사용하여 enum을 문자열로 변환하는 것이 적절합니다. --- `5-22`: **LGTM! DTO 매핑 로직이 깔끔합니다.** Companion object factory 패턴과 enum name 변환이 적절하게 구현되었습니다. --- `5-22`: **LGTM!** 도메인 타입을 DTO로 매핑하는 로직이 명확하며, `consumptionType.name`을 통한 enum 변환이 적절합니다. </blockquote></details> <details> <summary>payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/inbound/VoucherControllerTest.kt (6)</summary><blockquote> `24-28`: **LGTM! MockMvc standalone 설정이 적절합니다.** 단위 테스트에 MockMvc standalone 설정을 사용하는 것이 적절합니다. 빠른 테스트 실행과 격리된 컨트롤러 검증이 가능합니다. --- `24-28`: **LGTM! 테스트 설정이 적절합니다.** MockK와 standalone MockMvc를 사용한 단위 테스트 구성이 올바릅니다. --- `30-53`: **LGTM! 발행된 바우처 조회 테스트가 정확합니다.** JSON path 검증과 예상 값 확인이 적절하게 구현되었습니다. --- `55-65`: **빈 목록 검증 방식을 확인하세요.** Line 64의 `.andExpect(jsonPath("$.vouchers").isEmpty())`는 의도한 대로 동작하지 않을 수 있습니다. 빈 배열 검증은 `.isEmpty`가 아닌 `.isEmpty()` 또는 `.isArray` + 크기 체크를 사용해야 합니다. 다음 diff를 적용하거나 테스트 실행 결과를 확인하세요: ```diff mockMvc.perform(get("/api/v1/vouchers")) .andExpect(status().isOk) - .andExpect(jsonPath("$.vouchers").isEmpty()) + .andExpect(jsonPath("$.vouchers").isArray) + .andExpect(jsonPath("$.vouchers").isEmpty)
19-28: LGTM!MockK와 standalone MockMvc를 사용한 단위 테스트 설정이 적절합니다.
55-65: LGTM!빈 목록 시나리오를 테스트하는 로직이 올바릅니다.
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaRepository.kt (2)
9-18: OffsetDateTime 비교 시 DB 타임존 처리 확인 필요
JPQL 로직 자체는 적절하나,OffsetDateTime필드(activeFrom,activeUntil)를 비교할 때 실제 DB 컬럼(voucher_contents.active_from,voucher_contents.active_until)이TIMESTAMP WITH TIME ZONE인지, 애플리케이션과 DB의 타임존 설정이 일치하는지 반드시 검증하세요. 마이그레이션 파일이 코드베이스에 포함되어 있지 않다면 DBA나 인프라팀에 문의해 스키마 정의를 확인하십시오.
8-19: LGTM!JPQL 쿼리 로직이 올바르며, 시간 범위 조건(
activeFrom <= :now AND activeUntil >= :now)이 현재 활성 콘텐츠를 정확하게 필터링합니다.payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/integration/VoucherIntegrationTest.kt (3)
1-28: 테스트 위치가 적절하게 수정되었습니다.이전 리뷰에서 지적된 계층 경계 위반 문제가 해결되었습니다. 테스트가
voucher/adapter모듈에 위치하여 어댑터 계층의 JPA 엔티티와 리포지토리를 직접 테스트하는 것이 적절합니다. SpringBootTest 설정과 트랜잭션 관리도 올바르게 구성되어 있습니다.
30-37: 의존성 주입이 적절합니다.어댑터 계층 통합 테스트에서 MockMvc와 JPA 리포지토리를 직접 주입하는 것은 적절합니다.
80-98: DRAFT 필터링 테스트가 올바릅니다.DRAFT 상태의 바우처가 API 응답에서 제외되는 것을 정확하게 검증하고 있습니다.
선택사항: 더 명시적인 검증을 위해 배열 크기를 확인할 수도 있습니다:
.andExpect(jsonPath("$.vouchers.length()").value(0))
...t/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVouchersResponse.kt
Show resolved
Hide resolved
...voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/inbound/VoucherControllerTest.kt
Show resolved
Hide resolved
95a0ace to
e4d45c4
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherContent.kt (1)
5-14: data class 변경 권장사항은 여전히 유효합니다.이전 리뷰에서 지적된 대로, 도메인 값 객체는
data class로 선언하여equals(),hashCode(),copy(),toString()등을 자동 생성하는 것이 권장됩니다.다음 diff를 적용하세요:
-class VoucherContent( +data class VoucherContent( public val id: Long, public val voucherId: Long, public val version: Int, public val title: String, public val description: String, public val activeFrom: OffsetDateTime, public val activeUntil: OffsetDateTime, public val createdAt: OffsetDateTime, )payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/inbound/VoucherControllerTest.kt (1)
48-53: description 필드 검증을 추가하세요.테스트 데이터에서
description을 설정하고 있지만(40번 라인), 응답 검증에는 포함되지 않았습니다. 매핑이 올바른지 확인하기 위해 assertion을 추가하세요.mockMvc.perform(get("/api/v1/vouchers")) .andExpect(status().isOk) .andExpect(jsonPath("$.vouchers[0].id").value(1)) .andExpect(jsonPath("$.vouchers[0].title").value("AI 포스터 생성")) .andExpect(jsonPath("$.vouchers[0].consumptionType").value("SINGLE_USE")) + .andExpect(jsonPath("$.vouchers[0].description").value("AI를 사용한 포스터 생성 바우처"))
🧹 Nitpick comments (7)
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/VoucherController.kt (2)
15-19: 예외 처리 추가를 고려해보세요.현재 use case에서 발생할 수 있는 예외에 대한 처리가 없습니다.
@ExceptionHandler또는 전역 예외 핸들러를 통해 예외 처리를 추가하면 더 안정적인 API가 될 것입니다.
14-19: 바우처 목록 조회에 페이지네이션 고려를 권장합니다.현재 모든 발행된 바우처를 한 번에 반환하고 있습니다. 바우처 수가 증가할 경우 성능 문제가 발생할 수 있으므로, 향후 페이지네이션이나 커서 기반 조회를 고려해보세요.
payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/TestConfiguration.kt (1)
7-10: 테스트 설정에서 @SpringBootApplication 사용을 재검토하세요.테스트 설정 클래스에
@SpringBootApplication을 사용하면 전체 자동 구성이 활성화되어 테스트 실행 시간이 느려질 수 있습니다. 어댑터 모듈의 단위/통합 테스트에는 필요한 구성만 로드하는 것이 좋습니다.다음과 같이 테스트 범위에 맞게 리팩토링하는 것을 고려하세요:
-@SpringBootApplication +@TestConfiguration @EntityScan("app.payment.voucher.adapter.outbound") @EnableJpaRepositories("app.payment.voucher.adapter.outbound") +@EnableAutoConfiguration( + exclude = [ + // 테스트에 불필요한 자동 구성 제외 + ] +) class TestConfiguration또는 JPA 리포지토리 테스트만 필요한 경우:
@DataJpaTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) class TestConfiguration개별 테스트 클래스에서
@Import(TestConfiguration::class)로 필요한 빈만 선택적으로 로드하는 방법도 권장됩니다.payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImplTest.kt (1)
25-60: 테스트 커버리지 보강을 권장합니다.기본 시나리오는 잘 테스트되었으나, 다음 케이스를 추가하면 더 견고해집니다:
- 여러 바우처가 반환되는 경우
- 다른 상태값들(DRAFT, DISCONTINUED) 테스트
- MockK의
verify를 사용한 repository 호출 검증추가 테스트 예시:
@Test @DisplayName("동일 상태의 여러 바우처를 조회할 수 있다") fun `should find multiple vouchers with same status`() { // given val entities = listOf( VoucherJpaEntity(id = 1L, type = VoucherType.AI_POSTER_GENERATE, consumptionType = ConsumptionType.SINGLE_USE, status = VoucherStatus.PUBLISHED, createdAt = OffsetDateTime.now(), updatedAt = OffsetDateTime.now()), VoucherJpaEntity(id = 2L, type = VoucherType.AI_POSTER_FAST_QUEUE, consumptionType = ConsumptionType.SINGLE_USE, status = VoucherStatus.PUBLISHED, createdAt = OffsetDateTime.now(), updatedAt = OffsetDateTime.now()) ) every { voucherRepository.findByStatus(VoucherStatus.PUBLISHED) } returns entities // when val result = voucherReader.readByStatus(VoucherStatus.PUBLISHED) // then assertEquals(2, result.size) verify(exactly = 1) { voucherRepository.findByStatus(VoucherStatus.PUBLISHED) } }payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaEntity.kt (1)
27-32: 엔티티 제약 조건 추가를 권장합니다.비즈니스 로직상 필수 필드인
type,consumptionType,status에 대해@Column(nullable = false)어노테이션을 추가하면 데이터베이스 레벨에서도 무결성을 보장할 수 있습니다.적용 예시:
@Enumerated(EnumType.STRING) +@Column(nullable = false) val type: VoucherType, @Enumerated(EnumType.STRING) +@Column(nullable = false) val consumptionType: ConsumptionType, @Enumerated(EnumType.STRING) +@Column(nullable = false) var status: VoucherStatus,payment/voucher/application/src/test/kotlin/app/payment/voucher/application/service/VoucherServiceTest.kt (2)
34-56: 테스트 검증을 더 엄격하게 작성하세요.현재 테스트는 다음과 같은 개선이 필요합니다:
- Line 43:
any()를 사용하지 말고 실제OffsetDateTime값을 검증하세요.- Line 52: 첫 번째 항목만 검증하고 있습니다. 두 번째 항목도 검증하거나 더 많은 속성을 확인하세요.
- Line 55:
any()를 사용하지 말고 정확한 파라미터를 검증하세요.다음과 같이 개선할 수 있습니다:
every { - voucherContentReader.readCurrentContents(listOf(1L, 2L), any()) + voucherContentReader.readCurrentContents(listOf(1L, 2L), match { it.isBefore(OffsetDateTime.now().plusSeconds(1)) }) } returns contents // when val result = voucherService.getPublishedVouchers() // then assertEquals(2, result.size) assertEquals(1L, result[0].id) assertEquals("Test Title", result[0].title) + assertEquals(2L, result[1].id) + assertEquals("Test Title", result[1].title) verify(exactly = 1) { voucherReader.readByStatus(VoucherStatus.PUBLISHED) } - verify(exactly = 1) { voucherContentReader.readCurrentContents(any(), any()) } + verify(exactly = 1) { voucherContentReader.readCurrentContents(listOf(1L, 2L), any()) }
74-92: 테스트의 결정성(determinism)을 위해 고정된 시간을 사용하세요.
OffsetDateTime.now()를 사용하면 테스트가 실행 시점에 따라 다른 값을 가지게 되어 불안정한 테스트가 될 수 있습니다.다음과 같이 고정된 시간을 사용하는 것을 고려하세요:
+ companion object { + private val FIXED_TIME = OffsetDateTime.parse("2025-01-01T00:00:00Z") + } + private fun createVoucher(id: Long) = Voucher( id = id, type = VoucherType.AI_POSTER_GENERATE, consumptionType = ConsumptionType.SINGLE_USE, status = VoucherStatus.PUBLISHED, - createdAt = OffsetDateTime.now(), - updatedAt = OffsetDateTime.now() + createdAt = FIXED_TIME, + updatedAt = FIXED_TIME ) private fun createVoucherContent(voucherId: Long) = VoucherContent( id = voucherId * 10, voucherId = voucherId, version = 1, title = "Test Title", description = "Test Description", - activeFrom = OffsetDateTime.now().minusDays(1), - activeUntil = OffsetDateTime.now().plusDays(1), - createdAt = OffsetDateTime.now() + activeFrom = FIXED_TIME.minusDays(1), + activeUntil = FIXED_TIME.plusDays(1), + createdAt = FIXED_TIME )
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (37)
.github/workflows/ci.yml(1 hunks)build.gradle.kts(1 hunks)payment/services/api/build.gradle.kts(1 hunks)payment/services/api/src/main/kotlin/app/payment/PaymentApiApplication.kt(1 hunks)payment/services/api/src/test/resources/application-test.yml(1 hunks)payment/src/main/kotlin/app/payment/domain/voucher/Voucher.kt(0 hunks)payment/src/main/kotlin/app/payment/domain/voucher/VoucherContent.kt(0 hunks)payment/voucher/adapter/build.gradle.kts(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVoucherResponse.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVouchersResponse.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/VoucherController.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/JpaConfiguration.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaEntity.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaRepository.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentReaderImpl.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaEntity.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaRepository.kt(1 hunks)payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImpl.kt(1 hunks)payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/TestConfiguration.kt(1 hunks)payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/inbound/VoucherControllerTest.kt(1 hunks)payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImplTest.kt(1 hunks)payment/voucher/adapter/src/test/resources/application-test.yml(1 hunks)payment/voucher/application/build.gradle.kts(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/CurrentPublishedVouchers.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/VoucherUseCase.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherContentReader.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherReader.kt(1 hunks)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/service/VoucherService.kt(1 hunks)payment/voucher/application/src/test/kotlin/app/payment/voucher/application/service/VoucherServiceTest.kt(1 hunks)payment/voucher/domain/build.gradle.kts(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/ConsumptionType.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/Voucher.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherContent.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherStatus.kt(1 hunks)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherType.kt(1 hunks)payment/voucher/domain/src/test/kotlin/app/payment/voucher/domain/VoucherTest.kt(1 hunks)settings.gradle.kts(1 hunks)
💤 Files with no reviewable changes (2)
- payment/src/main/kotlin/app/payment/domain/voucher/VoucherContent.kt
- payment/src/main/kotlin/app/payment/domain/voucher/Voucher.kt
✅ Files skipped from review due to trivial changes (1)
- payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherType.kt
🚧 Files skipped from review as they are similar to previous changes (19)
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherContentReader.kt
- .github/workflows/ci.yml
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVouchersResponse.kt
- payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/ConsumptionType.kt
- payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/Voucher.kt
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/VoucherUseCase.kt
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/outbound/VoucherReader.kt
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/CurrentPublishedVouchers.kt
- payment/voucher/domain/src/test/kotlin/app/payment/voucher/domain/VoucherTest.kt
- settings.gradle.kts
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaRepository.kt
- payment/voucher/application/build.gradle.kts
- payment/services/api/build.gradle.kts
- payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/JpaConfiguration.kt
- build.gradle.kts
- payment/voucher/application/src/main/kotlin/app/payment/voucher/application/service/VoucherService.kt
- payment/voucher/adapter/build.gradle.kts
- payment/voucher/adapter/src/test/resources/application-test.yml
- payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherStatus.kt
🧰 Additional context used
🧬 Code graph analysis (5)
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaRepository.kt (4)
payment/src/main/kotlin/app/payment/domain/voucher/Voucher.kt (1)
id(5-12)payment/src/main/kotlin/app/payment/domain/voucher/VoucherStatus.kt (1)
DRAFT(3-8)platform/member/adapter/src/main/kotlin/app/cardcapture/member/adapter/outbound/persistence/MemberJpaRepository.kt (1)
findByOauthIdAndOauthProvider(6-9)payment/src/main/kotlin/app/payment/domain/voucher/VoucherType.kt (1)
AI_POSTER_GENERATE(3-7)
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentReaderImpl.kt (1)
payment/src/main/kotlin/app/payment/domain/voucher/VoucherContent.kt (1)
id(5-14)
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVoucherResponse.kt (2)
payment/src/main/kotlin/app/payment/domain/voucher/Voucher.kt (1)
id(5-12)payment/src/main/kotlin/app/payment/domain/voucher/ConsumptionType.kt (1)
SINGLE_USE(3-6)
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/VoucherController.kt (4)
payment/src/main/kotlin/app/payment/domain/voucher/Voucher.kt (1)
id(5-12)payment/src/main/kotlin/app/payment/domain/voucher/VoucherContent.kt (1)
id(5-14)payment/src/main/kotlin/app/payment/domain/voucher/VoucherStatus.kt (1)
DRAFT(3-8)payment/src/main/kotlin/app/payment/domain/voucher/VoucherType.kt (1)
AI_POSTER_GENERATE(3-7)
payment/voucher/application/src/test/kotlin/app/payment/voucher/application/service/VoucherServiceTest.kt (1)
payment/voucher/domain/src/test/kotlin/app/payment/voucher/domain/VoucherTest.kt (1)
createVoucher(35-44)
🔇 Additional comments (15)
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaEntity.kt (2)
14-29: LGTM! JPA 엔티티 설계가 적절합니다.
OffsetDateTime을 필드 타입으로 직접 사용하여 시간대 변환 문제를 원천적으로 방지하고 있습니다.@CreatedDate와 함께 올바르게 설정되어 있습니다.
30-44: LGTM! null 안전성이 올바르게 구현되었습니다.
requireNotNull을 사용하여 명시적인 null 체크와 에러 메시지를 제공하고 있어, 이전 리뷰에서 지적된!!연산자 사용 문제가 해결되었습니다.payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentReaderImpl.kt (1)
12-19: LGTM! 이전 리뷰 피드백이 모두 반영되었습니다.DB 쿼리 메서드(
findActiveContentsByVoucherIds)를 사용하여 성능 문제를 해결하고, 엔티티의toDomain()메서드를 활용하여 null 안전성을 보장하고 있습니다. 빈 리스트에 대한 조기 반환도 적절합니다.payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVoucherResponse.kt (1)
1-8: LGTM!DTO 구조가 명확하고 불변성을 잘 유지하고 있습니다. 도메인 모델(Voucher + VoucherContent)의 필드들이 적절하게 매핑되어 있습니다.
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/VoucherController.kt (1)
14-19: ResponseEntity 사용이 인증 모듈과 컨벤션을 맞춰 잘 적용되었습니다.과거 리뷰 코멘트가 반영되어
ResponseEntity로 응답을 감싸고 있습니다.payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/inbound/VoucherControllerTest.kt (1)
55-65: LGTM!빈 목록 시나리오에 대한 테스트가 잘 작성되어 있습니다.
payment/voucher/adapter/src/test/kotlin/app/payment/voucher/adapter/TestConfiguration.kt (1)
8-9: 검증 완료: JPA 엔티티와 리포지토리가 모두app.payment.voucher.adapter.outbound패키지에 위치하며, 인바운드/아웃바운드 구조가 헥사고날 아키텍처와 일치합니다.payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherReaderImpl.kt (1)
8-16: LGTM! 이전 리뷰 피드백이 모두 반영되었습니다.Repository에서 직접 상태별 필터링을 수행하고,
toDomain()메서드를 통해 안전하게 매핑하는 구조로 개선되었습니다. 헥사고날 아키텍처 패턴을 잘 따르고 있습니다.payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaRepository.kt (1)
6-8: 구현은 올바르나, 이전 리뷰의 인덱스 추가 권고사항을 확인하세요.Spring Data JPA의 파생 쿼리 메서드가 올바르게 정의되었습니다. 다만 이전 리뷰에서 지적된
vouchers.status컬럼에 대한 데이터베이스 인덱스 추가가 필요합니다. 바우처 데이터가 증가할 경우 성능 저하를 방지하기 위해 마이그레이션 파일에 인덱스 생성을 추가하세요.Based on learnings
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherJpaEntity.kt (2)
20-37: LGTM! 이전 리뷰의 모든 피드백이 반영되었습니다.감사 필드가
var로 변경되고 nullable로 선언되어 JPA 감사 기능이 정상 작동합니다.OffsetDateTime사용으로 타임존 정보도 보존됩니다.
38-51: LGTM! 안전한 도메인 매핑 구현입니다.
requireNotNull을 사용하여 null 안정성을 확보했고, 명확한 에러 메시지를 제공합니다. 이전 리뷰의 NPE 위험이 해결되었습니다.payment/voucher/application/src/test/kotlin/app/payment/voucher/application/service/VoucherServiceTest.kt (1)
58-72: 잘 작성된 테스트입니다.빈 목록 시나리오를 올바르게 검증하고 있으며,
voucherContentReader가 호출되지 않음을 확인하는 것이 좋습니다.payment/services/api/src/main/kotlin/app/payment/PaymentApiApplication.kt (3)
1-4: LGTM! 깔끔한 설정입니다.패키지 선언과 임포트가 올바르게 구성되어 있습니다.
9-11: LGTM! 표준 Kotlin Spring Boot 진입점입니다.
runApplication함수와 spread operator를 사용한 구현이 올바르고 관용적입니다.
6-7: 컴포넌트 스캔 범위 확인됨. 모든 Spring 컴포넌트가app.payment하위에 있어scanBasePackages설정이 올바릅니다.
...er/application/src/test/kotlin/app/payment/voucher/application/service/VoucherServiceTest.kt
Show resolved
Hide resolved
e4d45c4 to
8356739
Compare
8356739 to
93ad13e
Compare
Summary by CodeRabbit