트랜잭션의 사전적 의미는 거래이다.
하지만 컴퓨터 과학 분야에서는 “더 이상 분할이 불가능한 업무처리의 단위”를 의미한다.
즉 한꺼번에 수행되어야 할 일련의 연산 모음을 의미한다.
트랜잭션은 ACID라 하는 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)을 보장해야한다.
- 원자성: 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것 처럼 모두 성공하거나 모두 실패해야 한다.
- 일관성: 모든 트랜잭션은 일관성이 있는 데이터베이스 상태를 유지해야 한다.
- 예를 들어 데이터베이스에서 정한 무결성 제약조건을 항상 만족해야 한다.
- 격리성: 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다.
- 예를 들어 동시에 같은 데이터를 수정하지 못하도록 해야 한다.
- 격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준을 선택할 수 있다.
- 지속성: 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다.
- 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해 성공한 트랜잭션 내용을 복구 해야한다.
💡트랜잭션이란
원자성, 일관성, 지속성을 보장한다. 문제는 격리성인데 트랜잭션 간에 격리성을 완전히 보장하려면 트랜잭션을 거의 순서대로 실행해야 한다. 이렇게 하면 동시 처리 성능이 매우 나빠진다. 이런 문제로 인해 ANSI 표준은 트랜잭션의 격리 수준을 4단계로 나누어 정의했다.
💡Commit 이란
Commit 이란, 모든 작업들을 정상 처리하겠다고 확정하는 명령어로서, 해당 처리과정을 DB에 영구 저장하겠다는 의미이며, Commit을 수행하면 하나의 트랜잭션 과정이 종료되는 것이다. Commit을 수행하면 이전 데이터가 완전히 반영되어 UPDATE 된다.
💡Roll-back 이란
작업 중 문제가 발생되어 트랜잭션의 처리 과정에서 발생한 변경사항을 취소하는 명령어.
해당 명령을 트랜잭션에게 하달하면, 트랜잭션은 시작되기 이전의 상태로 되돌아간다.
즉, Rollback은 Commit 하여 저장한 예정 상태를 복구하는 것이다.
커밋 하지 않은 데이터 읽기
// 테이블 생성
create table member (
member_id varchar(10),
money integer not null default 0,
primary key (member_id)
);
// T1
set autocommit false;
insert into member(member_id, money) values ('newId1',10000);
insert into member(member_id, money) values ('newId2',10000);
SELECT * FROM MEMBER
// T2
SELECT * FROM MEMBER
계좌이체 상황 시나리오 (롤백 실습)
// 데이터 초기화
DELETE FROM member
insert into member(member_id, money) values ('memberA',10000);
insert into member(member_id, money) values ('memberB',10000);
// T1
set autocommit false;
update member set money=10000 - 2000 where member_id = 'memberA';
update member set money=10000 + 2000 where member_id = 'memberB';
commit;// 데이터 초기화
DELETE FROM member
insert into member(member_id, money) values ('memberA',10000);
insert into member(member_id, money) values ('memberB',10000);
// T1
set autocommit false;
update member set money=10000 - 2000 where member_id = 'memberA';
update member set money=10000 + 2000 where member_iiiid = 'memberB';
commit;set autocommit false;
update member set money=10000 - 2000 where member_id = 'memberA';
update member set money=10000 + 2000 where member_iiiid = 'memberB';
rollback;
트랜잭션이 정상적으로 실행중인 상태
- 트랜잭션이 시작되면, 해당 트랜잭션의 상태는 활동 상태가 된다.
- 해당 상태는 설계자가 설계한 대로 연산들이 정상적으로 실행중인 상태를 의미한다.
트랜잭션의 마지막까지 실행되었지만, Commit 연산이 실행되기 직전의 상태
- 설계된 트랜잭션대로 명령을 성공적으로 수행하면 그 다음 상태는 부분적 완료 상태가 된다.
- 작업이 성공하였다고 무조건 반영하는 것이 아니라, 설계자의 최종 승인이 있을 때까지 실제 데이터베이스에 작업 내용을 반영하지 않고 기다리고 있는 상태이다.
트랜잭션 내의 모든 작업이 성공적으로 수행되어 커밋 명령을 통해 데이터베이스에 영구 반영되고 트랜잭션이 종료된 상태
트랜잭션 실행에 오류가 발생하여 중단된 상태
- 트랜잭션을 수행하는 중간에 모종의 원인으로 인하여 오류가 발생하여 중단된 상태를 실패 상태라고 한다
트랜잭션이 비정상적으로 종료되어 Rollback 연산을 수행한 상태
- 트랜잭션이 비정상적으로 종료되었으니 설계되어있는 트랜잭션 내부의 작업을 다시 수행 이전의 상태로 되돌리는 Rollback 연산을 수행하면 그 상태를 철회 라고 한다.
다른 트랜잭션이 아직 커밋하지 않은 데이터(Dirty Data) 도 읽을 수 있다.

https://tlatmsrud.tistory.com/118
→ 10번 트랜잭션이 Update 한 후 Commit 하지 않았을 때
다른 트랜잭션에서 커밋된 데이터로만 접근할 수 있게 하는 격리 수준이다.

→ 10번 트랜잭션이 Update 한 후 Commit 하지 않았을 때
이 경우
13번 트랜잭션이 데이터를 조회할 경우 Update 전의 데이터를 조회한다.
지속성Undo 영역이란 변경 전 데이터가 저장된 영역이고, Commit 하기 전 데이터를 읽어올 수 있는 이유는 Undo 영역에 있는 데이터를 읽어오기 때문이다.
그러나 Non Repeatable Read(반복 가능하지 않은 읽기) 현상 발생한다
Non Repeatable Read
하나의 트랜잭션에서 동일한 select 쿼리를 실행시켰을 때 다른 결과가 나오는 것을 말한다.

Non Repeatable Read 문제를 해결하는 격리 수준으로 커밋된 데이터만 읽을 수 있되 자신보다 낮은 트랜잭션 번호를 갖는 트랜잭션에서 커밋한 데이터만 읽을 수 있는 격리수준이다.
💡이게 가능한 이유는 Undo 로그 때문이다.
트랜잭션 ID를 통해 Undo 영역의 데이터를 스냅샷처럼 관리하여 동일한 데이터를 보장하는 것을 MVCC(Multi Version Concurrency Control) 라고 한다.

💡Repeatable Read를 지원하지 않는 오라클?
오라클은REPEATABLE READ격리 수준을 명시적으로 지원하지 않는다. 그럼 Non Repeatable Read 문제를 해결할 수 없을까?
→ 해결할 수 있는 방법이 있다. 오라클은 MVCC(Undo 기반 일관된 읽기) 와 Exclusive Lock을 함께 사용해 사실상REPEATABLE READ이상의 일관성을 구현한다.Exclusive Lock
- 특정 레코드나 테이블에 대해 다른 트랜잭션에서 수정(쓰기) 작업을 할 수 없도록 하는 Lock 이다.
Select ~ For Update구문을 통해서 사용한다.- Exclusive Lock이 걸린 레코드는 다른 트랜잭션에서 UPDATE나 DELETE 시도 시 대기 상태가 된다.
- 다만, 오라클은 MVCC를 사용하기 때문에 다른 트랜잭션이 해당 레코드를 조회(SELECT)하는 것은 가능하다.
- 이때는 Undo 영역의 이전 버전(snapshot)을 읽는다.
→ 즉 오라클의 READ COMMITTED = 타 DB의 REPEATABLE READ에 가까운 동작을 한다.
그래서 오라클이 굳이 REPEATABLE READ 를 지원하지 않는다고 하는 것이다.
REPEATABLE READ도 완전히는 완벽하지 않다.
다른 트랜잭션의 수정 작업은 막았지만 Insert 작업이 일어나면 어떻게 될까?
→ 같은 행을 여러 번 읽더라도 항상 동일한 값을 반환하지만, 새로운 행 자체가 생기거나 사라지는 경우는 막지 못한다.
예를 들어, 특정 조건(
age > 20)으로 조회했을 때, 다른 트랜잭션이 그 사이에 새로운 데이터를 삽입하면 두 번째 조회 시 “새로운 행(Phantom)”이 나타날 수 있다. 이 현상을 Phantom Read(팬텀 리드) 라고 한다.
트랜잭션을 순차적으로(직렬화) 수행하도록 강제하는 가장 높은 격리 수준이다.
트랜잭션 간의 간섭이 완전히 차단되어, Dirty Read, Non-Repeatable Read, Phantom Read 등 모든 데이터 부정합 문제가 발생하지 않는다.
하지만 동시에 여러 트랜잭션이 수행되지 못하므로 동시성(Concurrency) 이 크게 떨어지고 처리 속도가 느려지는 단점이 있다.
SERIALIZABLE 수준에서는 DBMS가 읽기와 쓰기 모두를 잠금(Lock) 으로 제어한다.
Shared LockExclusive Lock💡Shared Lock
데이터를 읽을 때 사용되는 잠금.
여러 트랜잭션이 동시에 같은 데이터를 읽을 수 있지만 그 데이터에 대한 수정(쓰기)은 불가능하다.트랜잭션이 종료되면(Commit / Rollback) 잠금이 해제된다.
CREATE TABLE account (
id BIGINT PRIMARY KEY,
name VARCHAR(50),
balance INT
);
INSERT INTO account VALUES (1, '맥스', 10000);
커밋되지 않은 데이터를 다른 트랜잭션이 읽을 수 있는 수준
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
UPDATE account SET balance = 5000 WHERE id = 1;
-- 커밋하지 말고 대기
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT balance FROM account WHERE id = 1;
ROLLBACK; 하면 실제 DB 값은 10000으로 복귀커밋된 데이터만 읽을 수 있는 수준 (Dirty Read 방지)
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
UPDATE account SET balance = 7000 WHERE id = 1;
-- 커밋하지 않고 대기
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT balance FROM account WHERE id = 1;
COMMIT; 한 후 다시 SELECT 하면 → 7000으로 바뀜같은 트랜잭션 내에서는 항상 동일한 데이터를 읽을 수 있음
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM account WHERE id = 1; -- 결과: 7000
-- 커밋하지 말고 대기
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
UPDATE account SET balance = 9000 WHERE id = 1;
COMMIT;
SELECT balance FROM account WHERE id = 1;
모든 트랜잭션을 순차적으로 실행한 것처럼 동작함
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
SELECT * FROM account;
-- 커밋하지 않고 대기
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
INSERT INTO account VALUES (2, '새계좌', 2000);
COMMIT; 해야 실행됨💡간단한 연산에 경우에 오류가 나면 해당 트랜잭션을 롤백하면 된다. 하지만 이 트랜잭션이 여러개 연결된다면? 어디서 부터 어디까지 끊고 롤백시켜야 할까? 또한 성공한 경우에 어디까지 커밋 해야 할까?
이미 트랜잭션이 진행중일 때 추가 트랜잭션 진행을 어떻게 할지 결정하는 것이 전파 속성(Propagation)이다.
물리 트랜잭션
트랜잭션의 원래 개념은 DBMS 에서 출발했다.
실제로 데이터의 commit 과 rollback을 수행하는 주체는 DB 이다.
이 트랜잭션은 DB Connection 단위로 관리된다.
→ 이런 식으로 DB 커넥션 단에서 직접 제어되는 것이 바로 물리 트랜잭션이다.
논리 트랜잭션
현대 애플리케이션은 단일 DB만 다루지 않고 JDBC, JPA, Redis 등 다양한 자원을 활용하는데
이 때 비즈니스 로직 단위에서 트랜잭션을 일관되게 관리해야할 필요에 생긴 것이 논리 트랜잭션이다.
→ 스프링이 내부적으로 물리 트랜잭션을 대신 시작하고, 여러 기술 계층을 하나로 묶어서 관리한다.

https://mangkyu.tistory.com/269
다음과 같은 경우에는 2개의 트랜잭션 범위가 존재하기 때문에 개별 논리 트랜잭션이 존재하지만 실제로는 물리 트랜잭션이 사용된다.
💡왜 트랜잭션을 이렇게 나눠서 관리할까?
기존의 트랜잭션이 진행중일 때 또 다른 트랜잭션이 사용되면 복잡한 상황이 발생한다. 하지만 스프링은 논리 트랜잭션이라는 개념을 도입함으로써 상황에 대한 설명을 쉽게 만들고, 다음과 같은 원칙을 세울수 있다.
논리 트랜잭션을 기반으로 단순한 원칙을 세움으로써 2개 이상의 트랜잭션을 다루는 경우에 대한 이해가 상당히 쉬워진다.
“트랜잭션 전파(Propagation)”란,
이미 진행 중인 트랜잭션이 있을 때 새로 시작된 메서드가 그 트랜잭션에 참여할지(join) 아니면 새로운 트랜잭션을 새로 만들지를 결정하는 규칙이다.

내부 트랜잭션이 참여할 때 벌어지는 일
outer() 실행 시 → 물리 트랜잭션 시작inner() 실행 시 → “이미 트랜잭션이 있네?” 하고outer()와 inner()는 하나의 물리 트랜잭션을 공유하지만, @Transactional 단위로 각각 논리 트랜잭션이 존재한다.@Transactional은 AOP 프록시로 감싸져 있어서,자기 경계가 끝나면 트랜잭션 매니저에게 커밋을 요청한다.
그래서 아래처럼 된다.
inner() 종료 → 트랜잭션 매니저에 “커밋 요청”outer() 종료 → 다시 “커밋 요청”💡inner() 에서 예외가 터져서 롤백이 발생한다면?
트랜잭션 매니저는 내부 트랜잭션에서 문제가 발생했음을 체크한다.
- “rollback-only” 플래그를 설정
그 상태에서 외부 트랜잭션이 커밋을 시도하면, 매니저는 이미 내부에서 롤백됐음을 감지하고 전체 롤백을 수행한다.
💡outer() 에서 예외가 터져 롤백이 발생한다면?
트랜잭션 매니저는 외부 트랜잭션의 실패를 감지하고 현재 진행 중인 물리 트랜잭션을 롤백한다.
- 내부 트랜잭션이 있었다면 같은 물리 트랜잭션을 공유함하므로 함께 롤백된다. (REQUIRED)
- 별도의 트랜잭션으로 이미 커밋되었다면 이미 커밋되어 유지된다. (REQUIRES_NEW)

💡SUPPORTS는 트랜잭션이 있으면 자엽스럽게 참여하고, 없으면 Auto Commit 모드로 즉시 실행하는 구조다.
→ 데이터 변경 작업(INSERT/UPDATE/DELETE) 에는 부적합ex) 핵심이 아닌 로직을 트랜잭션 경계에서 유연하게 처리 할 수 있다.
💡이미 진행 중인 트랜잭션이 있더라도 그 트랜잭션을 잠시 중단하고 Auto Commit 모드로 별도로 실행한다.
즉 트랜잭션이 커밋되거나 롤백되어도 이 메서드의 작업 결과를 영향을 받지 않는다.
그래서 비즈니스 트랜잭션과 분리되어야 하는 부수 로직에 자주 사용된다.ex) 실패 하더라도 본 트랜잭션의 롤백에 휘룰리면 안되는 로직에서 사용된다.
IllegalTransactionStateException 발생💡새로운 트랜잭션을 만들지 않고, 항상 이미 존재하는 트랜잭션에 참여해야만 동작한다.
즉, 트랜잭션 경계 내부에서만 실행되도록 강제하는 정책적 제약이다.(트랜잭션 없이 실행되면 예외를 던짐)
이 설정은 해당 로직이 반드시 커밋/롤백 단위로 함께 묶여야 한다는 의도를 명확히 표현할 때 사용된다.
💡트랜잭션 경계 밖에서만 실행되도록 강제하는 옵션이다. 트랜잭션이 걸린 상태에서 실행되면 오히려 비즈니스 영향을 주거나 불필요한 락, 롤백 전파가 발생할 수 있기 때문이다.
ex)비즈니스 트랜잭션과 묶이면 안 되는(non-transactional)대표적인 케이스에서 사용된다.
따라서 트랜잭션이 걸려 있으면 예외를 발생시켜, 절대 트랜잭션 안에서 실행되지 않게 만든다.
Savepoint를 지원해야 동작 (H2, PostgreSQL 지원 / 일부 DB 미지원)💡하나의 물리 트랜잭션 안에서 부분 복구가 필요한 경우 사용된다. 기본적으로 Spring 은 논리 트랜잭션만 관리하지만 NESTED는 그 내부에서 저장점을 만들어 특정 구간만 롤백할 수 있게 해준다.
NESTED 는 REQUIRED 처럼 같은 커넥션을 공유하지만, 내부 트랜잭션 실패 시 전체를 날리지 않고 부분적으로 북구할 수 있다는 점이 다르다.
스프링에서 트랜잭션을 관리하는 방법은 크게 세 가지가 있다.
가장 많이 쓰는 건 @Transactional인데, 사실 그 아래엔 PlatformTransactionManager라는 애가 실제로 트랜잭션을 관리하고 있다.
그냥 메서드 위에 @Transactional 붙이면 스프링이 알아서 트랜잭션을 열고 닫는다.
@Transactional(transactionManager = "transactionManager1")
public void saveData() {
jdbcTemplate.update("INSERT INTO table_a(value) VALUES ('A')");
}
내가 따로 commit()이나 rollback()을 호출하지 않아도 된다.
스프링이 내부적으로 PlatformTransactionManager를 찾아서 메서드 시작할 때 트랜잭션을 열고,
정상 끝나면 commit, 예외 나면 rollback 해준다.
“@Transactional은 PlatformTransactionManager를 자동으로 불러주는 도우미 역할”
그래서 트랜잭션 매니저를 여러 개 쓸 땐 transactionManager = "이름" 이렇게 명시해줘야
스프링이 어떤 매니저를 쓸지 헷갈리지 않는다.
→ 스프링 컨테이너에 등록된 Bean 이름을 명시한다
조금 더 직접 제어하고 싶으면 TransactionTemplate을 쓴다.
이건 코드로 트랜잭션 범위를 감쌀 수 있다.
transactionTemplate.execute(status -> {
jdbcTemplate.update("INSERT INTO table_a(value) VALUES ('A')");
return null;
});
이 안에서 예외가 나면 자동으로 롤백된다.
성공하면 commit.
사실 내부적으로도 똑같이 PlatformTransactionManager를 사용한다.
그냥 코드로 명확하게 보이게 쓴 버전이라고 보면 된다.
마지막은 진짜 수동으로,
스프링이 대신 해주던 걸 내가 직접 하는 방법이다.
TransactionStatus tx = txManager.getTransaction(new DefaultTransactionDefinition());
try {
jdbcTemplate.update("INSERT INTO a_tbl(v) VALUES ('A')");
txManager.commit(tx);
} catch (Exception e) {
txManager.rollback(tx);
}
이게 바로 @Transactional이 내부에서 실제로 하는 일이다.
스프링은 이 코드를 AOP 프록시로 감싸서 자동으로 실행해주는 것뿐이다.
그래서 트랜잭션 동작 원리를 배우고 싶을 땐 이걸 직접 써보는 게 제일 좋다.
스프링 트랜잭션의 “진짜 관리자”. 트랜잭션을 시작하고, 커밋하고, 롤백하는 주체다.
우리가 어떤 기술을 쓰느냐에 따라 구현체가 다르다.
DataSourceTransactionManagerJpaTransactionManagerHibernateTransactionManagerJtaTransactionManagerPlatformTransactionManager는 트랜잭션의 공통 인터페이스고,
실제 동작은 각각의 기술에 맞는 매니저가 맡는다.