한 줄 정의

트랜잭션은 논리적인 작업 셋이 100% 적용되거나 아무것도 적용되지 않음 을 보장하여, 부분 업데이트(Partial Update) 문제를 원천적으로 방지합니다.

쉽게 말하면

ATM에서 이체할 때 “내 계좌에서 출금”은 됐는데 “상대 계좌에 입금”이 실패한다면? 트랜잭션은 이런 상황에서 출금까지 자동으로 취소해줍니다. 트랜잭션이 없으면 이 문제를 직접 수동으로 처리해야 합니다.

핵심 내용

InnoDB vs MyISAM: 부분 업데이트 문제

트랜잭션의 가치를 가장 잘 보여주는 예제입니다.

-- 테스트 테이블 준비 (각각 PK=3인 레코드 1건 존재)
CREATE TABLE tab_myisam ( fdpk INT NOT NULL, PRIMARY KEY (fdpk) ) ENGINE=MyISAM;
CREATE TABLE tab_innodb ( fdpk INT NOT NULL, PRIMARY KEY (fdpk) ) ENGINE=INNODB;
INSERT INTO tab_myisam (fdpk) VALUES (3);
INSERT INTO tab_innodb (fdpk) VALUES (3);
 
-- AUTO-COMMIT 상태에서 PK 중복 INSERT 시도
SET autocommit=ON;
INSERT INTO tab_myisam (fdpk) VALUES (1),(2),(3);  -- ERROR: Duplicate entry '3'
INSERT INTO tab_innodb (fdpk) VALUES (1),(2),(3);  -- ERROR: Duplicate entry '3'
결과 차이
스토리지 엔진에러 발생 후 테이블 상태이유
MyISAM1, 2, 3이 모두 존재1과 2를 INSERT한 뒤 3에서 실패하지만, 이미 들어간 데이터는 그대로 남음
InnoDB3만 존재 (원래 상태 유지)쿼리 중 일부라도 실패하면 전체를 롤백

MyISAM의 이런 현상이 바로 부분 업데이트(Partial Update) 입니다. 부분 업데이트가 발생하면 실패한 쿼리로 인해 남은 데이터를 수동으로 정리해야 합니다.

트랜잭션 없이 복수 테이블 INSERT 처리
INSERT INTO tab_a ...;
IF(_is_insert1_succeed) {
    INSERT INTO tab_b ...;
    IF(_is_insert2_succeed) {
        // 처리 완료
    } ELSE {
        DELETE FROM tab_a WHERE ...;
        IF(_is_delete_succeed) {
            // 원상 복구 완료
        } ELSE {
            // 해결 불가능한 심각한 상황 발생
        }
    }
}

트랜잭션이 지원되면 이 복잡한 코드가 다음으로 단순화됩니다:

try {
    START TRANSACTION;
    INSERT INTO tab_a ...;
    INSERT INTO tab_b ...;
    COMMIT;
} catch(exception) {
    ROLLBACK;
}

트랜잭션 범위 최소화 원칙

트랜잭션은 꼭 필요한 최소의 코드에만 적용해야 합니다. 다음은 게시물 저장 로직의 잘못된 예와 올바른 예입니다.

잘못된 설계
1) 처리 시작
   => 데이터베이스 커넥션 생성
   => 트랜잭션 시작                  ← 너무 일찍 시작
2) 사용자의 로그인 여부 확인          ← DB 작업 아님
3) 사용자의 글쓰기 내용 오류 확인     ← DB 작업 아님
4) 첨부 파일 확인 및 저장            ← DB 작업 아님
5) 사용자의 입력 내용을 DBMS에 저장
6) 첨부 파일 정보를 DBMS에 저장
7) 저장된 내용을 DBMS에서 조회
8) 게시물 등록 알림 메일 발송         ← 네트워크 작업이 트랜잭션 안에!
9) 알림 메일 발송 이력을 DBMS에 저장
   <= 트랜잭션 종료(COMMIT)
   <= 데이터베이스 커넥션 반납
10) 처리 완료
올바른 설계
1) 처리 시작
2) 사용자의 로그인 여부 확인
3) 사용자의 글쓰기 내용 오류 확인
4) 첨부 파일 확인 및 저장
   => 데이터베이스 커넥션 생성
   => 트랜잭션 시작                  ← 실제 DB 작업 직전에 시작
5) 사용자의 입력 내용을 DBMS에 저장
6) 첨부 파일 정보를 DBMS에 저장
   <= 트랜잭션 종료(COMMIT)          ← 핵심 DB 작업만 묶기
7) 저장된 내용을 DBMS에서 조회
8) 게시물 등록 알림 메일 발송         ← 트랜잭션 밖으로!
   => 트랜잭션 시작
9) 알림 메일 발송 이력을 DBMS에 저장  ← 별도 트랜잭션으로 분리
   <= 트랜잭션 종료(COMMIT)
   <= 데이터베이스 커넥션 반납
10) 처리 완료
핵심 원칙 3가지
  1. DB 커넥션 보유 시간을 최소화하라 — 커넥션 풀은 유한한 자원입니다. 불필요하게 오래 잡고 있으면 다른 요청이 대기합니다
  2. 네트워크 작업은 반드시 트랜잭션 밖으로 — 메일 서버, 외부 API 호출이 트랜잭션 안에 있으면, 해당 서버 장애가 DB 커넥션 고갈로 이어집니다
  3. 성격이 다른 DB 작업은 별도 트랜잭션으로 분리하라 — 핵심 데이터 저장과 부가 이력 저장은 분리하는 것이 안전합니다

내 생각

  • Spring의 @Transactional을 Service 메서드에 무분별하게 붙이는 경우가 많습니다. 메서드 안에 외부 API 호출이나 메일 발송이 포함되어 있다면, 트랜잭션 범위를 반드시 점검해야 합니다.

  • 트랜잭션 범위 최소화는 단순히 “코드를 짧게”가 아니라, 장애 전파를 차단하는 아키텍처적 결정 입니다. 메일 서버 장애 → DB 커넥션 고갈 → 서비스 전체 장애로 이어지는 연쇄 반응을 끊어야 합니다.

관련 개념

출처

  • Real MySQL 8.0 (1권), 5.1 트랜잭션