Spring Transaction(트랜잭션)의 개념과 중요성, 활용 예시

이월(0216tw)·2025년 3월 22일

이 글은

  • 트랜잭션의 개념을 설명합니다.

  • 트랜잭션이 중요한 이유를 설명합니다.

  • 트랜잭션을 Spring에서 활용한 예시를 통해 이해해봅니다.


트랜잭션(Transaction) ?

트랜잭션이란 "모두 성공하거나, 하나라도 실패하면 전부 취소되는 하나의 작업 묶음"을 의미합니다.
모두 성공 시 commit 명령으로 작업을 확정하고, 실패 시 rollback 명령으로 작업을 되돌립니다.


송금에서 본 트랜잭션 예시

송금이라는 작업을 할 때에도 트랜잭션의 개념을 볼 수 있습니다.
A라는 사람이 B라는 사람에게 100만원을 입금한다고 가정해봅시다.
송금은 다음 순서를 따르게 됩니다:

A의 계좌 잔액에서 100만원을 차감한다.

B의 계좌 잔액에 100만원을 더한다.

송금이 완료되었다.

만약 1번 작업은 수행되었지만, 2번 작업에서 오류가 발생해 프로그램이 중단된다면 금액은 어떻게 될까요?

트랜잭션의 개념이 적용되지 않았다면, 100만원이 증발하게 됩니다.

A: 난 분명히 100만원 보냈다니까??
B: 아니, 안 받았다니까??

하지만 트랜잭션이 적용된다면?

2번 작업에서 오류가 발생하는 순간, 이전에 성공한 1번 작업도 함께 취소됩니다.
즉, A의 계좌에서 100만원을 차감한 기록도 사라지게 됩니다.


트랜잭션이 중요한 이유

트랜잭션은 데이터의 무결성을 보장해줍니다.

무결성이란, 데이터가 신뢰할 수 있는 상태로 유지된다는 것을 의미합니다.

앞서 예시처럼 A 계좌에서는 100만원이 빠졌지만 B 계좌에는 입금이 안 되었다면,
그 데이터를 우리는 신뢰할 수 없게 되고, 결국 그 서비스를 떠나게 됩니다.

데이터가 곧 자산인 시대에서, 신뢰할 수 없는 데이터는 매우 큰 위험 요소가 됩니다.
따라서 트랜잭션은 선택이 아닌 필수로 고려되어야 하는 중요한 요소입니다.


Spring에서 트랜잭션 보장을 어떻게 하나?

Spring에서는 @Transactional 애노테이션을 통해 트랜잭션을 선언적으로 쉽게 적용할 수 있습니다.

@Service
public class AccountService {

    @Transactional 
    public void transferMoney(String fromId, String toId, int amount) {
        accountRepository.decreaseBalance(fromId, amount); // A의 계좌에서 차감
        accountRepository.increaseBalance(toId, amount);   // B의 계좌에 추가
    }
}

@Transactional이 적용된 메서드는 내부의 비즈니스 로직이 모두 성공하면 commit,
도중에 예외가 발생하면 전체 작업을 rollback하여 전부 취소해줍니다.


좀 더 깊게 설명해보면

@Transactional은 Spring의 프록시(Proxy) 기술을 기반으로 동작합니다.
프록시는 실제 메서드를 호출하기 전에 가짜 객체(프록시 객체)를 끼워 넣어,
그 사이에서 부가적인 기능(트랜잭션 시작, 커밋, 롤백 등)을 수행할 수 있도록 해줍니다.

예를 들어, 어떤 서비스 클래스에 @Transactional을 붙이면,
Spring은 해당 클래스의 실제 객체를 직접 사용하는 대신,
그 객체를 감싼 프록시 객체를 생성하여 대신 사용합니다.

이 프록시 객체는 다음과 같은 흐름으로 동작합니다:

  1. 클라이언트가 메서드를 호출하면
  2. 프록시 객체가 먼저 요청을 가로채 트랜잭션을 시작하고
  3. 실제 메서드를 실행한 뒤
  4. 예외가 없다면 커밋
  5. 예외가 발생하면 롤백을 수행합니다

트랜잭션을 적용한 코드

항해 플러스 당시 @Transactional 을 적용한 코드입니다.
수강 신청을 하는 기능인데 세부적으로 5가지 체크사항이 있습니다.

1. 강의 일정이 존재하는지 확인
2. 이미 신청한 강의인지 확인 
3. 현재 수강 인원을 조회해 신청마감인지 확인
4. 수강 신청 처리 
5. 수강 신청 이력 적재 

1~3은 단순 조회이기 때문에 데이터 변경이 없으며, 트랜잭션과는 무관합니다.
하지만 4와 5는 데이터를 변경/저장하기 때문에,
하나만 성공하거나 하나만 실패하는 상황이 생기면 데이터 무결성이 깨질 수 있습니다.

@Transactional을 적용함으로써 4, 5 작업이 모두 성공하거나 모두 실패하게 하여
데이터의 일관성과 신뢰성을 지킬 수 있었습니다.
(지금 보니 1~3은 트랜잭션 범위 밖으로 분리해 별도의 메서드로 분리하는 것이 더 적절하다는 생각이 듭니다.)


@Override
@Transactional
public ResponseDTO applylecture(EnrollmentDTO enrollmentDTO) {

    long userId = enrollmentDTO.getUserId();
    long lectureId = enrollmentDTO.getLectureId();
    String lectureDy = enrollmentDTO.getLectureDy();

    EnrollmentId enrollmentId = new EnrollmentId(userId , lectureId , lectureDy);
    Enrollment enrollment = new Enrollment(enrollmentId);
    LectureScheduleId lectureScheduleId = new LectureScheduleId(lectureId, lectureDy);

    //강의일정이 존재하는지 확인
    lectureScheduleRepository.findById(lectureScheduleId)
            .orElseThrow(() -> new BusinessException(ResponseMessage.NO_SUCH_LECTURE_SCHEDULE.getMessage()));

    //이미 신청했는지 확인
    if(enrollmentRepository.findById(enrollmentId).isPresent()) {
        throw new BusinessException(ResponseMessage.ALREADY_ENROLLED.getMessage());
    }

    //현재 수강 인원 조회 (비관 락)
    long enrollCount = lectureScheduleRepository.selectEnrollCount(enrollment);
    System.out.println("현재 수강 인원: " + enrollCount);
    if (enrollCount >= 30) {
        throw new BusinessException(ResponseMessage.MAXIMUM_CAPACITY_EXCEEDED.getMessage());
    }

    lectureScheduleRepository.updateEnrollCount(enrollment);
    enrollmentRepository.save(enrollment);
    enrollmentHistoryRepository.save( //이력적재
            new EnrollmentHistory(
                    enrollment.getId().getUserId(),
                    enrollment.getId().getLectureId(),
                    enrollment.getId().getLectureDy(),
                    "reg" //취소는 cnl
            ));

    return new ResponseDTO("200" , ResponseMessage.ENROLLMENT_SUCCESS.getMessage());
}

느낀점

예전에는 트랜잭션이 단지 데이터베이스에서만 처리되면 되는 문제라고 생각했습니다.
하지만 Spring의 @Transactional을 사용해보며, 트랜잭션은 단순한 DB 작업 그 이상이라는 것을 알게 되었습니다.

서비스 로직 전체의 흐름 속에서 어디까지가 "성공"인지,
어디서 실패하면 전부 취소돼야 하는지를 명확히 정의하게 되었고,
그 흐름을 코드 한 줄로 선언적으로 처리할 수 있다는 점에서
@Transactional은 매우 강력하고 매력적인 기능이라는 것을 느꼈습니다.

앞으로는 단순히 데이터 무결성을 넘어서,
서비스 전체의 흐름 안정성까지 고려하여
트랜잭션을 더 전략적으로 활용할 수 있도록 노력해야겠다는 생각이 들었습니다.

profile
개발도 하고 강의도 하고 고민을 제일 많이 합니다

0개의 댓글