한 줄 정의
트랜잭션은 논리적인 작업 셋이 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'결과 차이
| 스토리지 엔진 | 에러 발생 후 테이블 상태 | 이유 |
|---|---|---|
| MyISAM | 1, 2, 3이 모두 존재 | 1과 2를 INSERT한 뒤 3에서 실패하지만, 이미 들어간 데이터는 그대로 남음 |
| InnoDB | 3만 존재 (원래 상태 유지) | 쿼리 중 일부라도 실패하면 전체를 롤백 |
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가지
- DB 커넥션 보유 시간을 최소화하라 — 커넥션 풀은 유한한 자원입니다. 불필요하게 오래 잡고 있으면 다른 요청이 대기합니다
- 네트워크 작업은 반드시 트랜잭션 밖으로 — 메일 서버, 외부 API 호출이 트랜잭션 안에 있으면, 해당 서버 장애가 DB 커넥션 고갈로 이어집니다
- 성격이 다른 DB 작업은 별도 트랜잭션으로 분리하라 — 핵심 데이터 저장과 부가 이력 저장은 분리하는 것이 안전합니다
내 생각
-
Spring의
@Transactional을 Service 메서드에 무분별하게 붙이는 경우가 많습니다. 메서드 안에 외부 API 호출이나 메일 발송이 포함되어 있다면, 트랜잭션 범위를 반드시 점검해야 합니다. -
트랜잭션 범위 최소화는 단순히 “코드를 짧게”가 아니라, 장애 전파를 차단하는 아키텍처적 결정 입니다. 메일 서버 장애 → DB 커넥션 고갈 → 서비스 전체 장애로 이어지는 연쇄 반응을 끊어야 합니다.
관련 개념
- Ch04-2 InnoDB 스토리지 엔진 — MVCC, 언두 로그
- Ch05-4 MySQL의 격리 수준 — 트랜잭션의 격리 수준에 따른 읽기 동작
출처
- Real MySQL 8.0 (1권), 5.1 트랜잭션