Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2f7ba51
feat: 송금 도메인 모델
donggi-lee-bit Mar 20, 2025
8646f67
feat: Data access 를 위한 Repository 클래스 뼈대
donggi-lee-bit Mar 20, 2025
cc0a7dd
feat: 계좌 도메인 모델에 대기 금액 필드 추가
donggi-lee-bit Mar 20, 2025
ee58548
feat: Remittance 도메인 생성 기능
donggi-lee-bit Mar 20, 2025
6c9284d
feat: 송금 기능 개발
donggi-lee-bit Mar 20, 2025
fb32e3e
test: Account 클래스 출금 기능 테스트
donggi-lee-bit Mar 23, 2025
b63ede1
refactor: 도메인 객체 생성 후 ID 반환 로직 변경
donggi-lee-bit Mar 24, 2025
235f43d
test: 송금 기능 테스트 코드 작성과 누락된 비즈니스 로직 추가
donggi-lee-bit Mar 24, 2025
f7327ed
refactor: 회원 조회 테스트 시 객체 간 동등 비교를 통해 검증하도록 변경
donggi-lee-bit Mar 24, 2025
043d5ee
feat: 송금 요청 API
donggi-lee-bit Mar 24, 2025
24b9b55
refactor: 회원 등록 시 반환되는 값 대신 저장된 회원을 직접 호출하여 ID를 사용하도록 변경
donggi-lee-bit Mar 24, 2025
f85a008
chore: origin 환경에서 테스트가 실패하는 문제, 원인 파악을 위해 주석 처리
donggi-lee-bit Mar 24, 2025
86efbeb
chore: origin 환경에서 테스트가 실패하는 문제, 원인 파악을 위해 객체 간 비교가 아닌 ID 비교
donggi-lee-bit Mar 24, 2025
4d8c037
feat: 송금 요청 시 발생하는 예외 핸들링
donggi-lee-bit Mar 25, 2025
dd3f346
chore: 사용하지 않는 EqualsAndHashCode 어노테이션 제거
donggi-lee-bit Mar 25, 2025
0e1c691
refactor: Account 에서 출금 검증 로직을 메서드로 추출해 의미를 명확하게 표현
donggi-lee-bit Mar 27, 2025
1786e55
refactor: 데이터가 조회되지 않을 경우 예외를 던지는 조회 방식의 의도를 드러내도록 네이밍 변경
donggi-lee-bit Mar 27, 2025
43d7744
refactor: RemittacneHistory 조회 결과를 List 타입으로 변경하여 복수 건 조회가 가능하도록 수정
donggi-lee-bit Mar 27, 2025
51c9278
refactor: 송금 요청 처리 후 관련 정보 확인 방식 변경
donggi-lee-bit Mar 27, 2025
f9159ee
remove: 사용되지 않는 findBySenderId 메서드 삭제
donggi-lee-bit Mar 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/main/java/com/donggi/sendzy/account/domain/Account.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.donggi.sendzy.account.domain;

import com.donggi.sendzy.account.exception.InvalidWithdrawalException;
import com.donggi.sendzy.common.utils.Validator;
import lombok.AccessLevel;
import lombok.Getter;
Expand All @@ -12,11 +13,26 @@ public class Account {
private Long id;
private Long memberId;
private Long balance;
private Long pendingAmount;

public Account(final Long memberId) {
validate(memberId);
this.memberId = memberId;
this.balance = 0L;
this.pendingAmount = 0L;
}

public void withdraw(final Long amount) {
validateWithdraw(amount);
this.pendingAmount += amount;
}

public void deposit(final Long amount) {
final var fieldName = "amount";
Validator.notNull(amount, fieldName);
Validator.notNegative(amount, fieldName);

this.balance += amount;
}

private void validate(final Long memberId) {
Expand All @@ -27,4 +43,33 @@ private void validateMemberId(final Long memberId) {
final var fieldName = "memberId";
Validator.notNull(memberId, fieldName);
}

private void validateWithdraw(final Long amount) {
final var fieldName = "amount";
Validator.notNull(amount, fieldName);

if (!isAmountPositive(amount)) {
throw new InvalidWithdrawalException(amount);
}

if (!hasSufficientBalanceFor(amount)) {
throw new InvalidWithdrawalException();
}

if (!hasSufficientBalanceIncludingPending(amount)) {
throw new InvalidWithdrawalException();
}
}

private boolean hasSufficientBalanceIncludingPending(long amount) {
return this.pendingAmount + amount <= this.balance;
}

private boolean hasSufficientBalanceFor(final long amount) {
return amount <= this.balance;
}

private boolean isAmountPositive(final long amount) {
return 0 < amount;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,33 @@ public interface AccountRepository {
* @param account 생성할 계좌
* @return 생성된 계좌의 ID
*/
Long create(Account account);
Long create(final Account account);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

👍


/**
* 회원 ID로 계좌를 조회합니다.
* @param memberId 회원 ID
* @return 조회된 계좌
*/
Optional<Account> findByMemberId(Long memberId);
Optional<Account> findByMemberId(final Long memberId);

/**
* 회원 ID로 계좌를 조회하고, 조회된 계좌에 배타적 잠금(Exclusive Lock)을 겁니다.
* @param memberId 회원 ID
* @return 잠금이 설정된 계좌(Optional)
*/
Optional<Account> findByIdForUpdate(final Long memberId);

/**
* 계좌의 대기 중인 금액을 업데이트합니다.
* @param id 계좌 ID
* @param pendingAmount 대기 중인 금액
*/
void updatePendingAmount(final Long id, final Long pendingAmount);

/**
* 계좌의 잔액을 업데이트합니다.
* @param id 계좌 ID
* @param balance 잔액
*/
void updateBalance(final Long id, final Long balance);
}
24 changes: 21 additions & 3 deletions src/main/java/com/donggi/sendzy/account/domain/AccountService.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.donggi.sendzy.account.domain;

import com.donggi.sendzy.member.exception.MemberNotFoundException;
import com.donggi.sendzy.account.exception.AccountNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -12,8 +12,26 @@ public class AccountService {
private final AccountRepository accountRepository;

@Transactional(readOnly = true)
public Account getByMemberId(final Long memberId) {
public Account getByMemberId(final long memberId) {
return accountRepository.findByMemberId(memberId)
.orElseThrow(() -> new MemberNotFoundException("회원을 찾을 수 없습니다. :" + memberId));
.orElseThrow(() -> new AccountNotFoundException(memberId));
}

@Transactional
public void withdraw(final Account account, final long amount) {
account.withdraw(amount);
accountRepository.updatePendingAmount(account.getId(), account.getPendingAmount());
}

@Transactional
public void deposit(final Account account, final long amount) {
account.deposit(amount);
accountRepository.updateBalance(account.getId(), account.getBalance());
}

@Transactional(readOnly = true)
public Account getByIdForUpdate(final long memberId) {
return accountRepository.findByIdForUpdate(memberId)
.orElseThrow(() -> new AccountNotFoundException(memberId));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.donggi.sendzy.account.exception;

import com.donggi.sendzy.common.exception.NotFoundException;

public class AccountNotFoundException extends NotFoundException {
public AccountNotFoundException(final Long memberId) {
super("계좌를 찾을 수 없습니다. :" + memberId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.donggi.sendzy.account.exception;

public class InvalidWithdrawalException extends RuntimeException {
public InvalidWithdrawalException() {
super("잔액이 부족합니다.");
}

public InvalidWithdrawalException(final Long amount) {
super("송금액은 0원 이상이어야 합니다. 사용자 요청 금액: " + amount);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@
import com.donggi.sendzy.account.domain.AccountRepository;
import com.donggi.sendzy.account.domain.TestAccountRepository;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.Optional;

@Mapper
public interface AccountMapper extends AccountRepository, TestAccountRepository {
Long create(Account account);
Long create(final Account account);

Optional<Account> findByMemberId(Long memberId);
Optional<Account> findByMemberId(final Long memberId);

void deleteAll();

void updatePendingAmount(@Param("id") final Long id, @Param("pendingAmount") final Long pendingAmount);

void updateBalance(@Param("id") final Long id, @Param("balance") final Long balance);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.donggi.sendzy.common.exception;

public class BadRequestException extends RuntimeException {
public BadRequestException(final String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.donggi.sendzy.common.exception;

import com.donggi.sendzy.account.exception.InvalidWithdrawalException;
import com.donggi.sendzy.member.exception.EmailDuplicatedException;
import com.donggi.sendzy.member.exception.InvalidPasswordException;
import com.donggi.sendzy.member.exception.MemberNotFoundException;
Expand Down Expand Up @@ -46,10 +47,10 @@ public ProblemDetail handleInvalidPasswordException(InvalidPasswordException e)
}

/**
* 클라이언트가 요청한 회원이 존재하지 않는 경우
* 클라이언트가 요청한 데이터가 존재하지 않는 경우
*/
@ExceptionHandler(MemberNotFoundException.class)
public ProblemDetail handleMemberNotFoundException(MemberNotFoundException e) {
@ExceptionHandler(NotFoundException.class)
public ProblemDetail handleMemberNotFoundException(NotFoundException e) {
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
}

Expand All @@ -68,4 +69,20 @@ public ProblemDetail handleAuthenticationException(AuthenticationException e) {
public ProblemDetail handleAccessDeniedException(AccessDeniedException e) {
return ProblemDetail.forStatusAndDetail(HttpStatus.FORBIDDEN, e.getMessage());
}

/**
* 클라이언트가 출금 요청을 잘못한 경우
*/
@ExceptionHandler(InvalidWithdrawalException.class)
public ProblemDetail handleInvalidWithdrawalException(final InvalidWithdrawalException e) {
return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage());
}

/**
* 클라이언트 요청이 잘못된 경우
*/
@ExceptionHandler(BadRequestException.class)
public ProblemDetail handleBadRequestException(final BadRequestException e) {
return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.donggi.sendzy.common.exception;

public class NotFoundException extends RuntimeException {
public NotFoundException(final String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,28 @@ public interface MemberRepository {
/**
* 회원을 저장합니다.
* @param member 저장할 회원
* @return 저장된 회원의 ID
* @return 저장된 행의 수
*/
Long create(Member member);
Long create(final Member member);

/**
* 이메일로 회원이 존재하는지 확인합니다.
* @param email 이메일
* @return 회원이 존재하면 true, 존재하지 않으면 false
*/
boolean existsByEmail(String email);
boolean existsByEmail(final String email);

/**
* 이메일로 회원을 조회합니다.
* @param email 이메일
* @return 조회된 회원
*/
Optional<Member> findByEmail(String email);
Optional<Member> findByEmail(final String email);

/**
* ID로 회원을 조회합니다.
* @param id 조회할 회원의 ID
* @return 조회된 회원
*/
Optional<Member> findById(final Long id);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.donggi.sendzy.member.domain;

import com.donggi.sendzy.member.exception.MemberNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -14,7 +15,8 @@ public class MemberService {

@Transactional
public Long registerMemberAndGetId(final Member member) {
return memberRepository.create(member);
memberRepository.create(member);
return member.getId();
}

@Transactional(readOnly = true)
Expand All @@ -26,4 +28,10 @@ public boolean existsByEmail(final String email) {
public Optional<Member> findByEmail(final String email) {
return memberRepository.findByEmail(email);
}

@Transactional(readOnly = true)
public Member findById(final Long memberId) {
return memberRepository.findById(memberId)
.orElseThrow(() -> new MemberNotFoundException(memberId));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

public class MemberNotFoundException extends BusinessException {

public MemberNotFoundException(String message) {
super(message);
public MemberNotFoundException(final Long memberId) {
super("존재하지 않는 회원입니다. memberId: " + memberId);
}
}
Loading
Loading