Skip to content

[FEAT] : 거래 장부를 위한 CoinTransaction 및 CoinTransactionEntity 엔티티 구현 #272

Merged
yooooonshine merged 6 commits intodevelopfrom
feature/268-implement-transaction-and-entity
Nov 23, 2025
Merged

[FEAT] : 거래 장부를 위한 CoinTransaction 및 CoinTransactionEntity 엔티티 구현 #272
yooooonshine merged 6 commits intodevelopfrom
feature/268-implement-transaction-and-entity

Conversation

@yooooonshine
Copy link
Contributor

@yooooonshine yooooonshine commented Nov 23, 2025

개요

작업사항

Account 엔티티 구현

  • id
  • coin
  • version (추가)

version은 CoinTransactionEntity 목록 조회와 Account의 coin 잔액 조회의 동시성 문제를 해결하고자 도입하였다.

동시성 발생 예시

1) t 시점

클라이언트가 Annie 계좌 잔고를 읽음

→ 예: 잔고=1000

2) t+1 시점

Ledger에 거래가 새로 기록됨:

  • transaction_1
  • effective_at = t-1 (이 거래는 실제로 t-1에 발생한 거래임, 실제 거래 성사 시간의 불일치로 인해 실제 거래 발생 시간을 기록)
  • amount = -100

즉, t 시점 잔고는 사실 900이었어야 함
하지만 Ledger는 나중에 알게 됨.

3) t+2 시점

클라이언트가 이렇게 요청함: t 시점 이전의 모든 거래를 주세요.

이 경우 잔고는 1000원이지만, Entity(거래 장부) 상 900 시점의 최신 데이터 존재.

해결 방안

version 필드를 추가하여, 실제 유저가 조회할 때는

  WHERE account_id = 'account_a'
  AND effective_at <= 'timestamp_a'
  AND (discarded_at IS NULL OR discarded_at >= 'timestamp_a')

위와 같이
특정 시점에 Account를 읽었을 때, version이 10이라면

Entry에서 account_id에 해당하며, 상태가 posted이고, effective_at이 이 시점 이하이고, accout_version이 10이하인 것들에 대해 조회한다.
(이 일관성을 위해 Account 변경과 Transaction이 posted되는 것은 둘이 atomicity하게 일어나도록 한다. 이로 인해 해당 version에 해당하는 Entry들이 반드시 존재한다.)

이로 인해 항상 코인 잔액과, 거래 기록은 일관성있게 조회된다.





CoinTransactionEntity(단위 거래 장부) 엔티티 구현

  • id
  • account_id
  • account_version
  • status
    • pending, posted, archived 중 하나
    • pending : 초기상태
      • 트랜잭션이 처음 상생되었을 때, 예금과 출금에 대해 pending 상태를 부여한다.
    • posted : 확정된 상태
    • archived: 포스팅되기 전에 취소된 상태
  • direction
    • credit or debit 중 하나
  • amount
  • discarded_at
    • discarded_at은 기본적으로 null이나, 버려졌으면 실제 명시되며, 해당 Transaction에서 discared되지 않은 모든 Entry들은 모두 Transaction과 같은 상태로 움직여야 한다.
  • effective_at
    • 실제 거래가 된 시점

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 엔티티 구현

  • id
  • status
    • pending, posted, archived 중 하나
  • entries
    • 트랜잭션 안에 속한 entries 목록
  • effective_at
    • 실제 거래가 된 시점




유의사항

  • 애플리케이션 계층에서는 updatable을 false로 설정하면 변경을 막았지만, DB 계층에서는 쿼리로 수정이 가능하다. 이를 어떻게 막을 지 고민해야 한다.

Summary by CodeRabbit

릴리즈 노트

  • 새로운 기능

    • 거래 상태 추적 및 방향 지정을 포함한 향상된 거래 관리 시스템 추가
    • 거래 생명주기 관리를 위한 보류 및 완료 상태 구분
    • 계정에 낙관적 잠금 지원 추가
  • 버그 수정

    • 유효 시간 검증 추가로 거래 데이터 무결성 강화

✏️ Tip: You can customize this high-level summary in your review settings.

@yooooonshine yooooonshine linked an issue Nov 23, 2025 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Nov 23, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

거래 도메인에 새로운 엔티티와 열거형을 추가하고 Account 엔티티에 낙관적 잠금(@Version) 필드를 도입합니다.

Changes

Cohort / File(s) 변경 요약
Account 엔티티 개선
src/main/java/hanium/modic/backend/domain/transaction/entity/Account.java
private Long version 필드 추가, @Version, @Column(name = "version", nullable = false) 적용, 초기값 0L
기본 거래 엔티티
src/main/java/hanium/modic/backend/domain/transaction/entity/CoinTransaction.java
새로운 JPA 엔티티 CoinTransaction 추가 (@Entity, @Table(name = "coin_transactions")); id(PK, IDENTITY), status(TransactionStatus, enum STRING, nullable=false, updatable=false), effectiveAt(LocalDateTime, nullable) 필드 및 Builder 제공
거래 엔티티 레코드
src/main/java/hanium/modic/backend/domain/transaction/entity/CoinTransactionEntity.java
새로운 엔티티 CoinTransactionEntity 추가 (BaseEntity 확장); id, accountId, accountVersion, coinTransactionId, status, direction, amount, discardedAt, effectiveAt 필드; createPendingTransaction(), createPostedTransaction() 정적 팩토리 및 discardTransaction() 메서드 추가; 유효성 검사 및 상태 제어 로직 포함
거래 방향 열거형
src/main/java/hanium/modic/backend/domain/transaction/enums/TransactionDirection.java
DEBIT, CREDIT 상수를 갖는 TransactionDirection enum 추가
거래 상태 열거형
src/main/java/hanium/modic/backend/domain/transaction/enums/TransactionStatus.java
PENDING, POSTED, ARCHIVED 상수를 갖는 TransactionStatus enum 추가
에러 코드 확장
src/main/java/hanium/modic/backend/common/error/ErrorCode.java
EFFECTIVE_AT_CANT_NOT_BE_NULL(HttpStatus.BAD_REQUEST, "ACC-005", "유효 시간이 null일 수 없습니다.") 에러 코드 추가

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: 처리 결과 반환
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • 주의할 파일/영역:
    • Account.java@Version 추가로 인한 낙관적 잠금 호환성(트랜잭션 경계, 동시성 시나리오)
    • CoinTransactionEntity의 상태 전이/유효성 검사 로직 및 예외 처리(AppException) 검증
    • 테이블명/컬럼명 오타(예: coin_transcation vs coin_transactions) 확인

Possibly related issues

Possibly related PRs

Poem

🐰 새벽 들녘에 코인 춤추네,
펜딩에서 포스팅까지 발을 맞추고,
버전 한 줄로 동시성 안심하며,
방향 정해 금액 실어 보내고,
토끼는 깡충, 새 엔티티 축하 인사! 🎉

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed 제목은 CoinTransaction 및 CoinTransactionEntity 엔티티 구현이라는 주요 변경사항을 명확하게 요약하고 있으며, 간결하고 구체적입니다.
Description check ✅ Passed PR 설명이 템플릿 구조(개요, 작업사항)를 따르고 있으며, 엔티티 설계, 동시성 문제 해결 방안, 각 필드의 목적 등을 상세히 기술하고 있습니다.
Linked Issues check ✅ Passed PR의 코드 변경사항이 Issue #268의 목표인 Transaction 및 Entity 엔티티 구현을 충족하고 있습니다. CoinTransaction, CoinTransactionEntity 엔티티 구현, TransactionStatus/TransactionDirection 열거형 추가, Account에 version 필드 추가 등이 모두 포함되어 있습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 Transaction 및 Entity 엔티티 구현이라는 PR 목표 및 Issue #268과 직접적으로 관련되어 있으며, 범위를 벗어난 변경사항이 없습니다.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ffa5fb6 and 9b6a6ef.

📒 Files selected for processing (4)
  • src/main/java/hanium/modic/backend/common/error/ErrorCode.java (1 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/TransactionStatus.java (1 hunks)

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 연관관계로 명시하는 것을 권장합니다.

현재 accountIdcoinTransactionIdLong 타입으로 관리하고 있으나, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2be5e76 and ffa5fb6.

📒 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에 거래 생성 및 영속화 로직이 없어 현재 상태에서는 다음을 검증할 수 없습니다:

  • CoinTransactionEntityaccountVersion 캡처 및 검증 로직
  • 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.

Comment on lines 81 to 120
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
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

복식부기 원칙의 강제가 누락되었습니다.

PR 목표에서 복식부기 지원을 언급했으나, 현재 구조에서는 단일 거래 엔트리만 생성할 수 있어 차변(DEBIT)과 대변(CREDIT)의 균형을 보장하지 못합니다.

복식부기를 제대로 구현하려면:

  1. CoinTransaction 엔티티에 @OneToMany 관계로 여러 CoinTransactionEntity를 관리하고
  2. 거래 생성 시 차변과 대변의 합계가 일치하는지 검증하는 로직을 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.

@yooooonshine yooooonshine merged commit c1fdb8f into develop Nov 23, 2025
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FEAT : Transaction과 Entity 엔티티 구현

1 participant