◇ PART · DB

@Transactional 기본

전부 성공하거나 전부 취소

학습 목표

  • 트랜잭션 이 무엇인지 안다
  • @Transactional 의 효과를 안다
  • 왜 Service 계층에 붙이는지 안다

⚠️ 송금 시나리오

트랜잭션 없는 송금 코드

public void transfer(int from, int to, int amount) {
    accountMapper.withdraw(from, amount);  // ① 출금 성공
    // 여기서 시스템 다운!
    accountMapper.deposit(to, amount);     // ② 입금 못 됨
}

출금만 되고 입금 안 됨 = 돈이 사라짐. 시스템 사고.

🛠️ 트랜잭션 — 원자성

「전부 성공 또는 전부 취소」

여러 DB 작업을 「나눌 수 없는 한 단위」 로 묶는 약속. 하나라도 실패하면 모두 롤백.

트랜잭션 = 하나의 비즈니스 단위 ① 출금 ② 입금 ③ 거래 기록 → 셋 다 성공 → 커밋 (DB 에 영구 반영) → 하나라도 실패 → 롤백 (모든 변경 취소) → 「반쯤 된 상태」 는 존재하지 않음

@Transactional 사용


@Service
public class TransferService {

    @Autowired private AccountMapper mapper;

    @Transactional
    public void transfer(int from, int to, int amount) {
        mapper.withdraw(from, amount);
        mapper.deposit(to, amount);
        mapper.recordHistory(from, to, amount);
    }
}

👉 메서드 시작 = 트랜잭션 시작 / 메서드 정상 종료 = 커밋 / 예외 발생 = 자동 롤백.

설정 — root-context.xml


<bean id="transactionManager"
      class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
</bean>

<tx:annotation-driven />

👉 위 두 줄로 @Transactional 활성화. tx:annotation-driven 누락 시 — 어노테이션 무시됨.

ACID 4 가지 약속

속성의미
Atomicity (원자성)전부 성공 또는 전부 취소
Consistency (일관성)제약 조건(NOT NULL 등) 항상 만족
Isolation (고립성)다른 트랜잭션과 격리
Durability (지속성)커밋 후 영구 보존 (시스템 다운돼도)

👉 본 과정은 A 만 깊이. C·I·D 는 후속 과정.

롤백 조건 — 기본 동작

@Transactional 의 기본 롤백 정책:

  • RuntimeException 발생 → 롤백
  • Error 발생 → 롤백
  • Checked Exception (IOException 등) → 기본은 롤백 X

@Transactional(rollbackFor = Exception.class)  // 모든 예외에서 롤백
public void method() throws IOException { ... }

👉 본 과정은 RuntimeException 만 던지므로 기본 동작으로 충분.

왜 Service 계층에?

@Transactional 은 일반적으로 Service 메서드에 붙임:

  • Controller 에 — HTTP 처리도 묶이는 게 어색
  • Mapper 에 — 한 SQL 단위는 너무 작음. 여러 Mapper 를 묶을 수 없음
  • Service 에 — 「의미 있는 비즈니스 단위」 의 자연스러운 경계

본 과정에서 사용할 자리

메서드이유
회원가입회원 INSERT 후 권한 INSERT 등 (확장 시)
게시글 작성글 INSERT + 첨부파일 저장
조회수 + 통계여러 테이블 동시 갱신

👉 단일 SELECT 같은 단순 메서드엔 굳이 안 붙여도 됨 (성능 미세 차이만).

Self-invocation 함정 (재방문)

⚠️ 같은 클래스 내부 호출

@Service
public class MemberService {
    public void register(Member m) {
        save(m);    // ⚠️ @Transactional 적용 안 됨!
    }

    @Transactional
    public void save(Member m) { ... }
}

Spring 의 AOP 프록시 메커니즘 때문. 우회 — 다른 빈에서 호출 또는 클래스 분리.

실험 — 일부러 예외 발생


@Transactional
public void transferBuggy(int from, int to, int amount) {
    mapper.withdraw(from, amount);   // ① 출금
    if (amount > 1000) {
        throw new RuntimeException("금액 너무 큼");  // ② 강제 예외
    }
    mapper.deposit(to, amount);
}

// 호출
service.transferBuggy(1, 2, 5000);

// 결과 — DB 에서 확인
SELECT * FROM account WHERE id = 1;
// 잔액 그대로 (롤백됨)

SQL 로그로 확인


JDBC 트랜잭션 활성 시 콘솔:

==>  Preparing: UPDATE account SET balance = ... WHERE id = ?
==> Parameters: 5000(Integer), 1(Integer)
<==    Updates: 1

(예외 발생)

(자동 롤백 — 위 UPDATE 가 취소됨)

readOnly 옵션 — 읽기 전용


@Transactional(readOnly = true)
public List<Board> findAll() {
    return mapper.findAll();
}

👉 단순 SELECT 만 하는 메서드에 — DB 성능 미세 향상.

🔄 Before / After

전 차시 끝

트랜잭션 단어를 들어본 적은 있지만 코드에 어떻게 적용하는지 막연.

이번 차시 끝

@Transactional 한 줄의 효과를 안다. Service 메서드에 적절히 붙일 수 있다.

정리

오늘 들고 가는 것

  • 트랜잭션 = 「전부 성공 또는 전부 취소」
  • @Transactional 어노테이션 한 줄
  • RuntimeException = 자동 롤백
  • Service 계층에 붙임 — 비즈니스 단위 경계
  • Self-invocation 함정 주의