[FEAT] : 거래 장부를 위한 CoinTransaction 및 CoinTransactionEntity 엔티티 구현 #272
[FEAT] : 거래 장부를 위한 CoinTransaction 및 CoinTransactionEntity 엔티티 구현 #272yooooonshine merged 6 commits intodevelopfrom
Conversation
|
Caution Review failedThe pull request is closed. Walkthrough거래 도메인에 새로운 엔티티와 열거형을 추가하고 Account 엔티티에 낙관적 잠금( Changes
Sequence Diagram(s)sequenceDiagram
actor Client
participant AccountService
participant Account
participant CoinTransaction
participant CoinTransactionEntity
Client->>AccountService: 거래 생성 요청
AccountService->>Account: 계정 조회 (버전 포함)
Account-->>AccountService: Account 반환 (version)
AccountService->>CoinTransaction: CoinTransaction 생성 (PENDING)
CoinTransaction-->>AccountService: CoinTransaction 반환
AccountService->>CoinTransactionEntity: createPendingTransaction(accountId, accountVersion, ...)
CoinTransactionEntity-->>AccountService: CoinTransactionEntity(PENDING) 반환
Note right of AccountService: 이후 포스팅 시
AccountService->>CoinTransaction: effectiveAt 설정 -> status POSTED
AccountService->>CoinTransactionEntity: createPostedTransaction(..., effectiveAt)
CoinTransactionEntity-->>AccountService: CoinTransactionEntity(POSTED) 반환
alt 필요시 취소
AccountService->>CoinTransactionEntity: discardTransaction(discardedAt)
CoinTransactionEntity-->>AccountService: discarded
end
AccountService-->>Client: 처리 결과 반환
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes
Possibly related issues
Possibly related PRs
Poem
Pre-merge checks and finishing touches✅ Passed checks (5 passed)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (4)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (3)
src/main/java/hanium/modic/backend/domain/transaction/entity/CoinTransaction.java (1)
39-39: 불필요한 nullable 속성 지정
nullable = true는 JPA의 기본값이므로 명시할 필요가 없습니다.- @Column(name = "effective_at", nullable = true) + @Column(name = "effective_at")src/main/java/hanium/modic/backend/domain/transaction/entity/CoinTransactionEntity.java (2)
55-55: 불필요한 nullable 속성 지정
nullable = true는 JPA의 기본값이므로 명시할 필요가 없습니다.- @Column(name = "discarded_at", nullable = true) + @Column(name = "discarded_at") private LocalDateTime discardedAt; - @Column(name = "effective_at", nullable = true) + @Column(name = "effective_at") private LocalDateTime effectiveAt;Also applies to: 58-58
35-42: 외래 키 관계를 JPA 연관관계로 명시하는 것을 권장합니다.현재
accountId와coinTransactionId를Long타입으로 관리하고 있으나, JPA의@ManyToOne관계를 사용하면 타입 안정성과 참조 무결성을 향상시킬 수 있습니다.다음과 같이 리팩토링을 고려하세요:
+ @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "account_id", nullable = false, updatable = false) + private Account account; - @Column(name = "account_id", nullable = false, updatable = false) - private Long accountId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "coin_transaction_id", nullable = false, updatable = false) + private CoinTransaction coinTransaction; - @Column(name = "coin_transaction_id", nullable = false, updatable = false) - private Long coinTransactionId;이렇게 변경하면 정적 팩토리 메서드도 엔티티 참조를 받도록 수정해야 합니다. 다만 현재 아키텍처에서 ID 기반 참조가 의도된 설계라면 무시해도 됩니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
src/main/java/hanium/modic/backend/domain/transaction/entity/Account.java(2 hunks)src/main/java/hanium/modic/backend/domain/transaction/entity/CoinTransaction.java(1 hunks)src/main/java/hanium/modic/backend/domain/transaction/entity/CoinTransactionEntity.java(1 hunks)src/main/java/hanium/modic/backend/domain/transaction/enums/TransactionDirection.java(1 hunks)src/main/java/hanium/modic/backend/domain/transaction/enums/TransactionStatus.java(1 hunks)
🔇 Additional comments (3)
src/main/java/hanium/modic/backend/domain/transaction/enums/TransactionDirection.java (1)
3-6: LGTM!거래 방향을 나타내는 enum 정의가 명확하고 적절합니다.
src/main/java/hanium/modic/backend/domain/transaction/entity/Account.java (1)
35-37: 거래 추적 계층이 완전히 통합되지 않았습니다.
CoinTransactionEntity.createPendingTransaction/createPostedTransaction팩토리 메서드는 설계상accountVersion을 올바르게 캡처하나, 코드베이스 어디에서도 호출되지 않습니다.AccountService에 거래 생성 및 영속화 로직이 없어 현재 상태에서는 다음을 검증할 수 없습니다:
CoinTransactionEntity의accountVersion캡처 및 검증 로직Account업데이트와 거래 생성의 원자적 처리또한,
Account.version = 0L명시적 초기화는 JPA@Version어노테이션이 자동 관리하므로 제거해야 합니다.src/main/java/hanium/modic/backend/domain/transaction/entity/CoinTransaction.java (1)
28-49: 도메인 모델 설계는 완벽하며 PR 요구사항을 모두 충족합니다.검증 결과, CoinTransaction의 구조는 의도적이고 올바른 설계입니다:
- CoinTransaction: 단일 거래 단위(헤더)로, 상태와 유효성 시간만 포함
- CoinTransactionEntity: 개별 계좌 엔트리(debit/credit)를 나타내며, coinTransactionId 외래키로 CoinTransaction과 연결
- 관계 정의: @onetomany 어노테이션 대신 외래키 패턴으로 구현 (의도적 선택)
- 복식부기 지원: direction 필드(DEBIT/CREDIT)를 통해 완벽하게 구현됨
"entries" 필드는 실제로 필요하지 않습니다. CoinTransactionEntity 엔티티들이 그 역할을 수행합니다. 현재 구현은 금융 거래의 표준 패턴으로 적절하고 완전합니다.
Likely an incorrect or invalid review comment.
src/main/java/hanium/modic/backend/domain/transaction/entity/CoinTransaction.java
Outdated
Show resolved
Hide resolved
src/main/java/hanium/modic/backend/domain/transaction/entity/CoinTransactionEntity.java
Outdated
Show resolved
Hide resolved
src/main/java/hanium/modic/backend/domain/transaction/entity/CoinTransactionEntity.java
Show resolved
Hide resolved
src/main/java/hanium/modic/backend/domain/transaction/entity/CoinTransactionEntity.java
Show resolved
Hide resolved
| public static CoinTransactionEntity createPendingTransaction( | ||
| Long accountId, | ||
| Long accountVersion, | ||
| Long coinTransactionId, | ||
| TransactionDirection direction, | ||
| Long amount | ||
| ) { | ||
| return new CoinTransactionEntity( | ||
| accountId, | ||
| accountVersion, | ||
| coinTransactionId, | ||
| TransactionStatus.PENDING, | ||
| direction, | ||
| amount, | ||
| null, | ||
| null | ||
| ); | ||
| } | ||
|
|
||
| // 거래가 실제로 반영된 시간도 함께 설정 | ||
| // Posted를 생성 시에는 기존 Pending 거래의 discardedAt 설정해야 함 | ||
| public static CoinTransactionEntity createPostedTransaction( | ||
| Long accountId, | ||
| Long accountVersion, | ||
| Long coinTransactionId, | ||
| TransactionDirection direction, | ||
| Long amount, | ||
| LocalDateTime effectiveAt | ||
| ) { | ||
| return new CoinTransactionEntity( | ||
| accountId, | ||
| accountVersion, | ||
| coinTransactionId, | ||
| TransactionStatus.POSTED, | ||
| direction, | ||
| amount, | ||
| null, | ||
| effectiveAt | ||
| ); | ||
| } |
There was a problem hiding this comment.
복식부기 원칙의 강제가 누락되었습니다.
PR 목표에서 복식부기 지원을 언급했으나, 현재 구조에서는 단일 거래 엔트리만 생성할 수 있어 차변(DEBIT)과 대변(CREDIT)의 균형을 보장하지 못합니다.
복식부기를 제대로 구현하려면:
CoinTransaction엔티티에@OneToMany관계로 여러CoinTransactionEntity를 관리하고- 거래 생성 시 차변과 대변의 합계가 일치하는지 검증하는 로직을
CoinTransaction레벨에 추가해야 합니다.
예시:
// CoinTransaction에 추가
public void validateBalanced() {
long debitSum = entries.stream()
.filter(e -> e.getDirection() == TransactionDirection.DEBIT)
.mapToLong(CoinTransactionEntity::getAmount)
.sum();
long creditSum = entries.stream()
.filter(e -> e.getDirection() == TransactionDirection.CREDIT)
.mapToLong(CoinTransactionEntity::getAmount)
.sum();
if (debitSum != creditSum) {
throw new IllegalStateException("차변과 대변의 합계가 일치하지 않습니다");
}
}도메인 설계를 재검토하여 복식부기 무결성을 강제하는 방안을 마련하세요.
🤖 Prompt for AI Agents
In
src/main/java/hanium/modic/backend/domain/transaction/entity/CoinTransactionEntity.java
around lines 81 to 120, the factory methods create single entry instances which
fail to enforce double-entry bookkeeping; refactor the domain so a
CoinTransaction aggregate owns a OneToMany collection of CoinTransactionEntity
entries, change creation flow to build a CoinTransaction with both debit and
credit entries (or multiple entries) rather than standalone entries, and add a
validation method on the CoinTransaction aggregate (e.g., validateBalanced) that
sums amounts by TransactionDirection and throws when debitSum != creditSum;
ensure the factory/creation path invokes this validation before persisting and
update JPA mappings (OneToMany/ManyToOne and cascade/ownership) accordingly.
개요
작업사항
Account 엔티티 구현
version은 CoinTransactionEntity 목록 조회와 Account의 coin 잔액 조회의 동시성 문제를 해결하고자 도입하였다.
동시성 발생 예시
1) t 시점
클라이언트가 Annie 계좌 잔고를 읽음
→ 예: 잔고=1000
2) t+1 시점
Ledger에 거래가 새로 기록됨:
즉, t 시점 잔고는 사실 900이었어야 함
하지만 Ledger는 나중에 알게 됨.
3) t+2 시점
클라이언트가 이렇게 요청함: t 시점 이전의 모든 거래를 주세요.
이 경우 잔고는 1000원이지만, Entity(거래 장부) 상 900 시점의 최신 데이터 존재.
해결 방안
version 필드를 추가하여, 실제 유저가 조회할 때는
위와 같이
특정 시점에 Account를 읽었을 때, version이 10이라면
Entry에서 account_id에 해당하며, 상태가 posted이고, effective_at이 이 시점 이하이고, accout_version이 10이하인 것들에 대해 조회한다.
(이 일관성을 위해 Account 변경과 Transaction이 posted되는 것은 둘이 atomicity하게 일어나도록 한다. 이로 인해 해당 version에 해당하는 Entry들이 반드시 존재한다.)
이로 인해 항상 코인 잔액과, 거래 기록은 일관성있게 조회된다.
CoinTransactionEntity(단위 거래 장부) 엔티티 구현
version에 대하여
Account와 CoinTranstionEntity 조회 일관성을 위해 account_version을 명시했다.
status에 대하여
status는 pending, posted, archived 상태로 구분했다. (당장은 내부적으로만 거래가 발생하며 필요없지만, 추후 PG 사를 도입하여 외부 API를 통해 결제가 이뤄지므로 추가했다. pending은 PG사에 요청한 상황, posted는 PG사 결제가 끝난 상황)
direction에 대하여
복식부기를 위하여, debit과 credit으로 구분하였다.
추후 매일 자정에 Entity를 조회하며 복식부기상 장부가 일치하는지 체크할 예정이다.
discared_at에 대하여
CoinTransactionEntity 같은 경우, discarded_at과 effective_at 제외 수정이 불가하게 해야 한다.(장부는 변경이 있으면 안되기에)
거래가 pending에서 posted로 변경되면 discarded_at 필드를 채우고 posted 에 맞는 Entity를 새로 생성한다.
추후 장부 조회 시 discarded_at이 null이 아닌 장부는 조회에서 제외시킨다.
effective_at에 대하여
PG사를 통해 결제하면, 실제 장부 기록 시점과 거래 시점이 다르다. 거래 시점에 맞게 조회시키고자 추가했다.
Transaction 엔티티 구현
유의사항
Summary by CodeRabbit
릴리즈 노트
새로운 기능
버그 수정
✏️ Tip: You can customize this high-level summary in your review settings.