티스토리 뷰
트랜잭션(Transaction)
우리가 데이터베이스를 사용하는 이유 중 하나인 개념 트랜잭션
트랜잭션은 하나의 작업을 안전하게 처리하도록 보장해주는 개념이다.
작업이 성공하여 데이터베이스에 정상 반영하는 것을 커밋, Commit
작업이 실패하여 이전으로 되돌리는 것을 롤백, Rollback
트랜잭션의 ACID
- Atomicity, 원자성 - 트랜잭션 내에서 실행한 작업은 모두 실패하거나 모두 성공해야 한다.
- Consistency, 일관성 - 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다.
- Isolation, 격리성 - 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않게 격리한다. 격리 수준을 설정함
- Durability, 지속성 - 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다.
격리 수준( Isolation level )
트랜잭션 간에 격리성을 완벽히 보장한다면 트랜잭션을 순서대로 실행해야 하며, 이는 동시 처리 성능이 좋지 않다.
이를 위해 존재하는 것이 격리 수준
- Read Uncommited(커밋되지 않은 읽기, 제일 빠름), Read Commited(커밋된 읽기)
- Repeatable Read(반복 가능한 읽기), Serializable(직렬화 가능, 매우 느린 수준)
데이터베이스 연결 구조와 DB 세션
클라이언트에서 데이터베이스 서버에 연겨을 요청하고 커넥션을 맺는다. 이 시점에서 내부에 세션이라는 것을 만든다.
세션을 통하여 모든 요청이 처리되고, 트랜잭션을 제어한다. SQL도 실행한다.
커넥션을 닫거나, 강제 종료하는 방법으로 세션을 종료할 수 있다.
자동 커밋, 수동 커밋
자동 커밋은 각각의 쿼리 실행 직후 자동으로 커밋을 호출, 원하는 기능이 제대로 작동하지 않을 수 있음
수동 커밋을 사용하면 쿼리를 실행하고 이후에 커밋과 롤백을 호출하여 트랜잭션을 수행한다.
DB 락
세션 1에서 트랜잭션을 시작하고 데이터를 처리하는 동안, 세션 2에서 동시에 데이터 접근을 하게 되면
트랜잭션의 원자성이 깨지게 된다. 이를 위해 존재하는 개념이 락, lock이다.
쉽게 말해, 동일한 데이터를 동시에 접근하는 트랜잭션의 경우 '락'을 소유하여야 작업을 처리할 수 있는 것이다.
+) 무한정 대기하는 것이 아닌, LOCK_TIMEOUT 까지 대기한다. 초과 시 락 타임아웃 오류가 발생한다.
일반적으로 데이터 변경 시, 락을 사용하고 데이터 조회 시 락을 사용하지 않는다.
조회 시점에서 락이 필요한 경우? -> 정산 시점에서 데이터(돈)가 변경되지 않아야 한다.
select ~ for update 구문을 사용하면 조회에도 락을 소유할 수 있다.
스프링과 트랜잭션
프레젠테이션 계층
- UI와 관련된 처리 담당
- 웹 요청과 응답, 사용자 요청을 검증
- 주 사용 기술: 서블릿과 HTTP 같은 웹 기술, 스프링 MVC
서비스 계층
- 비즈니스 로직을 담당
- 주 사용 기술: 가급적 특정 기술에 의존하지 않고, 순수 자바 코드로 작성
데이터 접근 계층
- 실제 데이터베이스에 접근하는 코드
- 주 사용 기술: JDBC, JPA, File, Redis, Mongo ...
UI, DB는 변경된다 하더라도 서비스 계층의 비즈니스 로직은 가급적 변경이 없어야 한다.
서비스 계층을 순수하게 유지(순수 자바 코드)하는 것이 중요하다.
위 코드를 보면 accountTransfer의 메인 비즈니스 로직은 한 줄이며, 나머지는 트랜잭션을 설정하기 위한 코드들이다.
JDBC를 사용하기 위해 connection을 연결하는 코드를 반복적으로 작성해야 하고,
예외 처리 문장이 반복적으로 발생하며,
가급적 특정 기술에 의존하지 않아야 하는 서비스 계층이 트랜잭션 누수가 발생하였다.
이를 위해 존재하는 것이 트랜잭션 추상화 이다.
트랜잭션을 사용하기 위한 접근 방법은 구현 기술마다 다 다른데,
트랜잭션 추상화를 통하여 각각의 구현체를 사용한다면 추상화된 인터페이스에 의존할 수 있고
각 구현체에 DI를 통하여 주입하여 사용할 수 있다.
( * 특정 기술에 의존하는 것이 아닌, 추상화된 인터페이스에 의존 <- OOP ! * )
트랜잭션을 유지하기 위해서는 데이터베이스 커넥션을 유지해야 한다.
이를 위해 위에서는 파라미터로 커넥션을 전달하는 방법을 사용했다.
이를 위해 트랜잭션 동기화 매니저는 쓰레드 로컬을 사용해서 커넥션을 동기화해준다.
스프링의 트랜잭션 매니저는 트랜잭션 동기화를 보장해주고, 트랜잭션 추상화를 가능하게 해준다.
커넥션 생성 - DataSourceUtils.getConnection(dataSource)
커넥션 반환 - DataSourceUtils.releaseConnection(conn, dataSource)
트랜잭션 매니저 주입 - PlatformTransactionManager <- 구현체를 주입받는다.
트랜잭션 매니저를 도입하여 JDBC 의존 현상을 해결할 수 있었다.
하지만 비즈니스 로직인 bizLoigc을 제외한 나머지 트랜잭션을 위한 부분이 반복되는데,
이 부분을 스프링이 제공하는 TransactionTemplate를 사용하여 템플릿 콜백 패턴을 적용할 수 있다.
excute() -> 반환 값이 있는 경우 , excuteWithoutResult() -> 반환 값이 없는 경우
try ~ catch문을 제외한 나머지 트랜잭션 코드(commit, rollback)이 제거된 것을 볼 수 있다.
위에서 비즈니스 로직(bizLogic)은 핵심 기능이고, 트랜잭션을 사용하는 코드는 부가 기능이다.
비즈니스 로직과 트랜잭션을 하나의 클래스에서 처리하게 되면 유지보수가 어려워 진다.
트랜잭션 프록시
@Transaction을 통하여 (클래스나 메서드에 사용 가능) 중복되는, 그리고 순수 비즈니스 로직이 아닌 트랜잭션 코드를
간소화할 수 있다. (실제 작성했던 try catch문이 @Transactional을 사용하면 그대로 사용 가능하다.)
본 포스팅은 인프런 강의 김영한님의
[ 스프링 DB 1편 - 데이터 접근 핵심 원리 ] 를 수강하며 작성한 내용입니다.