트랜잭션은 ACID라 하는 원자성, 일관성, 격리성, 지속성을 보장해야 한다
원자성 Atomicity
트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공 하거나 모두 실패해야 한다
일관성 Consistency
모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다. 예를 들어 데이터베이스에서
정한 무결성 제약 조건을 항상 만족해야 한다
격리성 Isolation
동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다. 예를 들어 동시에 같은
데이터를 수정하지 못하도록 해야 한다. 격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준
(Isolation level)을 선택할 수 있다.
지속성 Durability
트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다. 중간에 시스템에 문제가 발생해도
데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다
트랜잭션 간에 격리성을 완벽히 보장하려면 트랜잭션을 거의 순서대로 실행해야 하는데 이 경우 동시 처리 성능이 매우 나빠진다. ANSI 표준은 트랜잭션의 격리 수준을 4단계로 나누어 정의했다
READ UNCOMMITED(커밋되지 않은 읽기)
READ COMMITTED(커밋된 읽기) : 일반적으로 많이 사용
REPEATABLE READ(반복 가능한 읽기)
SERIALIZABLE(직렬화 가능)
사용자는 WAS나 DB 접근 툴 같은 클라이언트를 사용해 DB 서버에 접근 가능.
클라이언트는 DB 서버에 연결을 요청하고 커넥션을 맺고, 이때 DB 서버는 내부에 세션이라는 것을 생성. 이후 해당 커넥션을 통한 모든 요청은 이 세션을 통해서 실행
즉 개발자가 클라이언트를 통해 SQL을 전달하면 현재 커넥션에 연결된 세션이 SQL을
실행
세션은 트랜잭션을 시작하고, 커밋 또는 롤백을 통해 트랜잭션을 종료
사용자가 커넥션을 닫거나, DBA(DB 관리자)가 세션을 강제로 종료하면 세션은 종료됨
커넥션 풀이 10개의 커넥션을 생성하면, 세션도 10개 만들어진다
commit : 데이터 변경 쿼리를 실행하고 데이터베이스에 그 결과를 반영
rollback : 트랜잭션을 시작하기 직전의 상태로 복구
set autocommit true : 자동 커밋 모드 설정
set autocommit false : 수동 커밋 모드 설정
여러 세션에서 동시에 같은 데이터를 수정하게 되면 여러 문제가 발생
락(Lock)은 이를 방지한다
SET LOCK_TIMEOUT <milliseconds>
시간 설정 가능select for update
set autocommit false;
select * from member where member_id='memberA' for update;
애플리케이션에서 트랜잭션을 어떤 계층에 걸어야 하는가?
커넥션을 유지하려면?
가장 간단한 방법은 파라미터로 전달하는 것
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
Connection con = dataSource.getConnection();
try {
con.setAutoCommit(false); //트랜잭션 시작
//비즈니스 로직
con.commit(); //성공시 커밋
} catch (Exception e) {
con.rollback(); //실패시 롤백
throw new IllegalStateException(e);
} finally {
release(con); //커넥션 종료
}
}
역할에 따른 3가지 계층
프레젠테이션 계층
서비스 계층
데이터 접근 계층
여기서 가장 중요한 곳은 서비스 계층
시간이 흘러 UI(웹)와 관련된 부분이 변하고, 데이터 저장 기술을 다른 기술로 변경해도, 비즈니스 로직은 최대한 변경없이 유지되어야 한다
그러려면 서비스 계층을 특정 기술에 종속적이지 않게 개발
트랜잭션은 비즈니스 로직이 있는 서비스 계층에서 시작하는 것이 좋다
트랜잭션 문제
예외 누수 문제
JDBC 반복 문제
트랜잭션 추상화
PlatformTransactionManager 인터페이스
org.springframework.transaction.PlatformTransactionManager
💡 참고
스프링 5.3부터는 JDBC 트랜잭션을 관리할 때 DataSourceTransactionManager 를 상속받아서 약간의 기능을 확장한 JdbcTransactionManager 를 제공
package org.springframework.transaction;
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
getTransaction() : 트랜잭션을 시작
기존에 이미 진행중인 트랜잭션이 있는 경우 해당 트랜잭션에 참여 가능
리소스 동기화
스프링은 트랜잭션 동기화 매니저를 제공
트랜잭션 동기화 매니저는 쓰레드 로컬(ThreadLocal)을 사용해 커넥션을 동기화
동작 방식
1. 트랜잭션 매니저는 데이터 소스를 통해 커넥션을 만들고 트랜잭션을 시작
2. 트랜잭션 매니저는 트랜잭션이 시작된 커넥션을 트랜잭션 동기화 매니저에 보관
3. 리포지토리는 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용
4. 트랜잭션이 종료되면 트랜잭션 매니저는 트랜잭션 동기화 매니저에 보관된 커넥션을 통해 트랜잭션 종료, 커넥션을 닫음
💡 참고
쓰레드 로컬을 사용하면 각각의 쓰레드마다 별도의 저장소가 부여된다. 따라서 해당 쓰레드만 해당 데이터에 접근 가능
DataSourceUtils.getConnection()
DataSourceUtils.releaseConnection()
트랜잭션 템플릿
반복되는 패턴의 문제 해결
public class TransactionTemplate {
private PlatformTransactionManager transactionManager;
public <T> T execute(TransactionCallback<T> action){..}
void executeWithoutResult(Consumer<TransactionStatus> action){..}
}
execute()
: 응답 값이 있을 때 사용
executeWithoutResult()
: 응답 값이 없을 때 사용
but
반복하는 코드는 제거할 수 있지만 서비스 로직에 비즈니스 로직과 트랜잭션을 처리하는 기술 로직이 섞이게 된다
이것을 스프링 AOP를 통해 프록시를 도입하여 해결
트랜잭션 AOP
org.springframework.transaction.annotation.Transactional
@SpringBootTest
DataSourceTransactionManager
ex. AOP 프록시 적용 확인
@Test
void AopCheck() {
log.info("memberService class={}", memberService.getClass());
log.info("memberRepository class={}", memberRepository.getClass());
Assertions.assertThat(AopUtils.isAopProxy(memberService)).isTrue();
Assertions.assertThat(AopUtils.isAopProxy(memberRepository)).isFalse();
}
테스트에서 @Transactional 을 사용하면
강제 커밋
스프링 부트의 자동 리소스 등록
스프링 부트 등장 이전에는 데이터소스와 트랜잭션 매니저를 개발자가 직접 스프링 빈으로 등록하여 사용했었다
@Bean
DataSource dataSource() {
return new DriverManagerDataSource(URL, USERNAME, PASSWORD);
}
@Bean
PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
dataSource
이름으로 스프링 빈에 자동 등록application.properties
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
transactionManager
이름으로 스프링 빈에 자동 등록예외 포함과 스택 트레이스
예외를 전환할 때는 꼭 기존 예외를 포함
@Test
void printEx() {
Controller controller = new Controller();
try {
controller.request();
} catch (Exception e) {
//e.printStackTrace(); //실무에서는 로그 사용
log.info("ex", e);
}
}
log.info("message={}", "message", ex)
, log.info("ex", ex)
public void call() {
try {
runSQL();
} catch (SQLException e) {
throw new RuntimeSQLException(e); //기존 예외(e)를 감싸서 변환
}
}
static class RuntimeSQLException extends RuntimeException {
public RuntimeSQLException(Throwable cause) { //원인 필요
super(cause);
}
}
java.sql.SQLException
과 스택 트레이스를 확인 가능RuntimeSQLException
부터 예외를 확인 가능스프링 예외 추상화
org.springframework.dao.DataAccessException
DataAccessException 은 크게 2가지로 구분한다
Transient
NonTransient
데이터베이스에서 발생하는 오류 코드를 스프링이 정의한 예외로 자동으로 변환
SQLExceptionTranslator exTranslator = new
SQLErrorCodeSQLExceptionTranslator(dataSource);
DataAccessException resultEx = exTranslator.translate("select", sql, e);
sql-error-codes.xml
위처럼 Repository에서 반복되는 문제들을 템플릿 콜백 패턴으로 처리할 수 있다
public class MemberRepositoryV5 implements MemberRepository {
private final JdbcTemplate template;
public MemberRepository(DataSource dataSource) {
template = new JdbcTemplate(dataSource);
}
@Override
public Member save(Member member) {
String sql = "insert into member(member_id, money) values(?, ?)";
template.update(sql, member.getMemberId(), member.getMoney());
return member;
}
@Override
public void update(String memberId, int money) {
String sql = "update member set money=? where member_id=?";
template.update(sql, money, memberId);
}
private RowMapper<Member> memberRowMapper() {
return (rs, rowNum) -> {
Member member = new Member();
member.setMemberId(rs.getString("member_id"));
member.setMoney(rs.getInt("money"));
return member;
};
}
}