스프링 앱에서 트랜잭션 사용(2)

김우진·2026년 2월 22일

Spring

목록 보기
15/17
post-thumbnail

[Spring] 13. 트랜잭션 사용 - 계좌 이체 구현 예제

비즈니스 로직의 원자성을 보장해야 하는 대표적인 사례인 '계좌 이체' 기능을 통해 리포지토리, 서비스, 컨트롤러의 구현 방법을 정리했습니다.


1. 비즈니스 시나리오

  1. 출금: 원본 계좌에서 지정된 금액을 차감합니다.
  2. 입금: 대상 계좌에 해당 금액을 추가합니다.
  • 핵심: 두 연산은 반드시 하나의 트랜잭션으로 묶여야 하며, 하나라도 실패 시 전체 작업이 취소되어야 합니다.

2. 리포지토리(Repository) 계층

리포지토리는 데이터베이스에 직접 접근하여 계좌 정보를 조회하거나 상태를 업데이트합니다.

  • 조회 로직: JdbcTemplate.queryForObject()를 사용하여 특정 계좌의 상세 정보를 가져옵니다.
  • RowMapper 활용: * 데이터베이스의 행(Row)을 자바 객체(Account)로 변환하기 위해 RowMapper 인터페이스를 구현해야 합니다.
    • 결과 집합(ResultSet)의 데이터를 모델 객체의 필드에 매핑하는 역할을 합니다.
  • 업데이트 로직: 특정 계좌의 금액을 변경하는 쿼리를 수행합니다.
@Repository
public class AccountRepository {
    private final JdbcTemplate jdbc;

    public AccountRepository(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }

    //계좌 ID를 사용하여 계좌 상세 정보를 찾는다.
    public Account findAccountById(long id){ //1)
        String sql = "SELECT * FROM account WHERE id = ?";
        return jdbc.queryForObject(sql,new AccountRowMapper(), id);
    }

    //특정 계좌의 금액을 업데이트 한다.
    public void changeAmount(long id, BigDecimal amount){
        String sql = "UPDATE account SET amount = ? WHERE id = ?";
        jdbc.update(sql, amount, id);
    }

    //모든 계좌 상세 정보 가져오기.
    public List<Account> findAllAcounts(){
        String sql = "SELECT * FROM account";
        return jdbc.query(sql,new AccountRowMapper());
    }
}
public class AccountRowMapper implements RowMapper<Account> {
//RowMapper 계약을 구현하고 결과 행을 매핑할 모델 클래스를 제네릭 프로그래밍타입으로 제공

    @Override
    public Account mapRow(ResultSet resultSet, int i) 
    //쿼리 결과를 매개변수(ResultSet 객체 형태)로 가져와 현재 행을 매핑한 Account 인스턴스를
    //반환하는 mapRow()메서드 구현
    
        throws SQLException{

        Account a = new Account(); //현재 결과 행의 값을 Account 속성에 매핑
        a.setId(resultSet.getInt("id"));
        a.setName(resultSet.getString("name"));
        a.setAmount(resultSet.getBigDecimal("amount"));
        return a; //결과 값을 매핑한 후 Account 인스턴스 반환
    }
}

3. 서비스(Service) 계층 : 트랜잭션 적용

서비스 계층은 비즈니스 로직을 수행하며, @Transactional을 통해 데이터의 무결성을 보장합니다.

  • @Transactional 특징: 클래스 레벨에 선언하면 해당 클래스 내의 모든 public 메서드에 트랜잭션 애스펙트가 적용됩니다.
  • 이체 프로세스:
    1. 원본 및 대상 계좌의 상세 정보를 조회하여 잔액을 확인합니다.
    2. 원본 계좌에서 금액을 차감하여 저장(Update)합니다.
    3. 대상 계좌에 금액을 추가하여 저장(Update)합니다.
  • 원자성 유지: 2단계와 3단계 작업 중 하나라도 런타임 예외가 발생하면 스프링이 자동으로 롤백을 수행합니다.
@Service
public class TransferService {

    private final AccountRepository accountRepository;

    public TransferService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }

    @Transactional
    public void transferMoney(long idSender, long idReceiver, BigDecimal amount){
        Account sender = accountRepository.findAccountById(idSender);
        Account receiver = accountRepository.findAccountById(idReceiver);

        BigDecimal senderNewAmount = sender.getAmount().subtract(amount);
        BigDecimal receiverNewAmount = receiver.getAmount().add(amount);

        accountRepository.changeAmount(idSender, senderNewAmount);
        accountRepository.changeAmount(idReceiver, receiverNewAmount);

        throw new RuntimeException("Oh no! Something went wrong!"); //트랜잭션 시뮬레이션
    }

    public List<Account> getAllAccounts(){
        return accountRepository.findAllAcounts();
    }
}

4. 컨트롤러(Controller) 및 DTO

클라이언트의 요청을 받고, 서비스 계층으로 데이터를 전달하는 입구 역할을 합니다.

  • DTO (Data Transfer Object):
    • 두 애플리케이션 간(클라이언트-서버) 데이터 교환을 모델링하는 객체입니다.
    • 예제에서는 이체 요청 정보를 담는 TransferRequest가 DTO 역할을 수행합니다.
  • 컨트롤러 액션:
    • @RequestBody를 통해 HTTP 요청 본문의 데이터를 DTO로 변환하여 수신합니다.
    • 수신된 데이터를 바탕으로 서비스의 transferMoney() 메서드를 호출합니다.
@RestController
public class AccountController {
    private final TransferService transferService;

    public AccountController(TransferService transferService) {
        this.transferService = transferService;
    }

    @PostMapping("/transfer")
    public void transferMoney(
            @RequestBody TransferRequest request
    ){
        transferService.transferMoney(
                request.getSenderAccountId(),
                request.getReceiverAccountId(),
                request.getAmount());
    }

    @GetMapping("/accounts")
    public List<Account> getAllAccounts(){
        return transferService.getAllAccounts();
    }
}
//DTO
public class TransferRequest {
    private long senderAccountId;
    private long receiverAccountId;
    private BigDecimal amount;

    public BigDecimal getAmount() {
        return amount;
    }

    public long getReceiverAccountId() {
        return receiverAccountId;
    }

    public long getSenderAccountId() {
        return senderAccountId;
    }

}

💡 최종 요약

  • 리포지토리: RowMapper를 사용해 DB 데이터와 객체를 매핑하고 쿼리를 실행한다.
  • 서비스: @Transactional로 여러 단계의 DB 변경 작업을 하나의 단위로 묶어 안전하게 처리한다.
  • 컨트롤러: 클라이언트와의 소통 창구로서 DTO를 활용해 데이터를 주고받는다.

#Spring #Transaction #JdbcTemplate #RowMapper #DTO #계좌이체구현 #백엔드공부

0개의 댓글