라이징테스트 원티드 클론코딩 프로젝트를 하면서 멘토님께서 트랜잭션 잘 반영했는지 체크하면 좋겠다는 피드백을 받았다.
이번 기회에 스트링부트에서 트랜잭션 정의와 DB의 트랜잭션을 관리하는 방법을 익히며 현재 Dao단의 쿼리문을 검토하며 적용해볼 예정이다!
Spring Transaction Management
트랜잭션은 완전히 성공하거나 완전히 실패하는 일련의 논리적 작업단위이자, 데이터베이스에서 SQL을 실행하는 작업 단위를 뜻한다.
보통 데이터베이스에 데이터 CRUD 연산을 요청할 때 SQL 쿼리를 작성해서 실행한다. 단순하게 하나의 SQL 쿼리 실행이 실패한 경우 문제가 없다. 그러나 여러 건의 데이터를 처리하는 쿼리가 실행되던 중 오류가 발생하는 경우도 찾아온다. 이런 경우 오류 발생 전 완료된 작업은 DB에 저장할 것인지, 혹은 작업 전체를 오류로 판단하여 작업 내용을 원상 복구할지 처리해야한다. 중간에 오류가 발생하면 트랜잭션의 모든 단계를 이전으로 돌리는 것을 롤백이라고 부른다.
스프링부트에서는 DB의 트랜잭션을 처리할 수 있는 기능을 지원한다.
트랜잭션의 성질
- 원자성 (Atomicity) : 한 트랜잭션 내 실행한 작업들은 하나로 간주한다. 모두 성공하거나 모두 실패한다.
- 일관성 (Consistency) : 트랜잭션은 일관성 있는 데이터베이스 상태를 유지한다. (Data integrity 만족 등)
- 격리성 (Isolation) : 동시에 실행되는 트랜잭션들이 서로 영향을 미치지 않도록 격리해야 한다.
- 지속성 (Durability) : 트랜잭션을 성공적으로 마치면 결과가 항상 저장되어야 한다.
트랜잭션의 종류
1. Global Transactions
드물지만 서로 다른 데이터베이스 간에 트랜잭션이 발생할 수 있는 응용프로그램이 있을 수 있다. 이를 분산 트랜잭션(distributed transaction) 처리라고 한다.
트랜잭션 관리자는 이를 처리하기 위해서 애플리케이션 내에 있을 수 없고 애플리케이션 서버 수준에 있다. JTA 또는 Java Transaction API는 JNDI의 지원과 함께 다른 데이터베이스를 조회하는데 필요하며 트랜잭션 관리자는 분산 트랜잭션의 커밋 또는 롤백을 결정한다. 이는 복잡한 프로세스이며 애플리케이션 서버 수준의 지식을 필요로 한다.
2. Local Transactions
로컬 트랜잭션은 간단한 JDBC 연결과 같은 단일 RDBMS와 애플리케이션 사이에서 발생한다. 로컬 트랜잭션을 사용하면 모든 트랜잭션 코드가 우리 코드 내에 있다.
만약 jdbc를 사용한다면 트랜잭션 관리 API는 JDBC용이다. Hibernate (JPA API 중 하나)를 사용한다면 애플리케이션 서버의 hibernate 트랜잭션 API와 JTA는 global 트랜잭션을 위한 것이다.
예시를 보자면,
(참고 코드 : https://it-techtree.tistory.com/entry/springboot-manage-database-transactions)
@Component
public class BookingService {
private final static Logger logger = LoggerFactory.getLogger(BookingService.class);
private final JdbcTemplate jdbcTemplate;
public BookingService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Transactional
public void book(String... persons) {
for (String person : persons) {
logger.info("Booking " + person + " in a seat...");
jdbcTemplate.update("insert into BOOKINGS(FIRST_NAME) values (?)", person);
}
}
public List<String> findAllBookings() {
return jdbcTemplate.query("select FIRST_NAME from BOOKINGS",
(rs, rowNum) -> rs.getString("FIRST_NAME"));
}
}
BookingService클래스에 @Component 어노테이션을 추가하여 스프링 빈 클래스로 추가한다. book() 메서드는 JDBC Template을 이용하여 insert 쿼리를 실행한다.
스프링의 트랜잭션은 프로그래밍 방식과 선언적 방식 (어노테이션)의 두가지 방식으로 구분할 수 있다.
프로그래밍 방식의 경우 TransactionTemplate을 활용하거나 직접 PlatformTransacitonManager를 구현한다.
book() 메서드 위에 추가된 @Transactional은 데이터베이스의 트랜잭션 관리를 수행하는 어노테이션이다. 클래스, 메서드위에 이 어노테이션이 추각되면 해당 클래스에 트랜잭션 기능이 적용된 프록시 객체가 생성된다.
이 프록시 객체는 @Transactional이 포함된 메소드가 호출될 경우 PlatformTransactionManager를 사용하여 트랜잭션을 시작하고 정상 여부에 따라 Commit 또는 Rollback한다.
프록시 객체의 프록시 패턴은 해당 클래스가 다른 클래스에 주입되면 Spring에서 내부적으로 target(본체)를 호출하여 대신 주입하는 것이다.
book() 메서드에서 SQL 쿼리 실행 중 실패가 발생하면, 메서드 내에서 실행되었던 쿼리 내용은 모두 Rollback(=원상복귀) 된다. @Transactional 어노테이션 추가만으로 데이터베이스의 트랜잭션을 손쉽게 관리할 수 있다.
@Transactional 어노테이션 속성
@Transactional 어노테이션 속성 설정을 변경하여 트랜잭션 설정을 하는 방법을 알아보자.
propagation
- REQUIRED (default) : 현재 트랜잭션 지원, 존재하지 않는 경우 새 트랜잭션 생성
- REQUIRES_NEW : 새로운 트랜잭션을 생성하고 존재하지 않는 경우 현재 트랜잭션을 일시 중단합니다.
- MANDATORY : 현재 트랜잭션을 지원하고 존재하지 않는 경우 예외를 던집니다.
- NESTED : 현재 트랜잭션이 있는 경우 중첩된 트랜잭션 내에서 실행
- SUPPORTS : 현재 트랜잭션을 지원하지만 존재하지 않는 경우 비트랜잭션으로 실행
- DEFAULT : 데이터 소스의 기본 격리 수준
- READ_UNCOMMITTED : Dirty Read, Non-Repeatable Read 및 Phantom Read가 발생할 수 있음을 나타냅니다. 다른 트랜잭션의 커밋되지 않은 데이터도 읽을 수 있음.
- READ_COMMITTED : Dirty Read를 방지하고 반복할 수 없으며 Phantom Read가 발생할 수 있음을 나타냅니다. 커밋된 데이터만 읽음. 반복조회시 커밋 시점에 따라 데이터 상이.
- REPEATABLE_READ : Dirty Read와 Non-Repeatable Read가 방지되지만 Phantom Read가 발생할 수 있음을 나타냅니다. 반복적으로 조회하여도 동일한 데이터를 보장.
- SERIALIZABLE : Dirty Read와 Non-Repeatable Read, Phantom Read가 방지될 수 있음을 나타냅니다. 데이터 처리의 직렬화를 보장.
readOnly : 트랜잭션이 읽기 전용인지 또는 읽기/쓰기인지 여부
timeout : 트랜잭션 타임아웃(처리 시간초과)
rollbackFor : 트랜잭션의 롤백을 발생시켜야 하는 예외(Exception) 클래스의 배열
rollbackForClassName : 트랜잭션의 롤백을 발생시켜야 하는 예외 클래스 이름의 배열
noRollbackFor : 트랜잭션 롤백을 유발하지 않아야 하는 예외 클래스 개체의 배열
noRollbackForClassName : 트랜잭션 롤백을 유발하지 않아야 하는 예외 클래스 이름의 배열
다수의 트랜잭션이 경쟁시 발생할 수 있는 문제
다수의 트랜잭션이 동시에 실행되는 상황에선 트랜잭션 처리방식을 좀 더 고려해야 한다.
예를들어 특정 트랜잭션이 처리중이고 아직 커밋되지 않았는데 다른 트랜잭션이 그 레코드에 접근한 경우 다음과 같은 문제가 발생할 수 있다.
1) Dirty Read
- 트랜잭션 A가 어떤 값을 1에서 2로 변경하고 아직 커밋하지 않은 상황에서 트랜잭션 B가 같은 값을 읽는 경우 트랜잭션 B는 2가 조회 된다.
- 트랜잭션 B가 2를 조회 한 후 A가 롤백되면 트랜잭션 B는 잘못된 값을 읽게 된 것이다. 즉, 아직 트랜잭션이 완료되지 않은 상황에서 데이터에 접근을 허용할 경우 발생할 수 있는 데이터 불일치이다.
2) Non-Repeatable Read
- 트랜잭션 A가 어떤 값 1을 읽었다. 이후 A는 같은 쿼리를 실행할 예정인데, 그 사이에 트랜잭션 B가 값 1을 2로 바꾸고 커밋해버리면 A가 같은 쿼리 두번을 날리는 사이 두 쿼리의 결과가 다르게 되어 버린다.
- 즉, 한 트랜잭션에서 같은 쿼리를 두번 실행했을 때 발생할 수 있는 데이터 불일치이다. (Dirty Read에 비해서는 발생 확률이 적다.)
3) Phantom Read
- 트랜잭션 A가 어떤 조건을 사용하여 특정 범위의 값들 [0, 1, 2, 3, 4]을 읽었다.
- 이후 A는 같은 쿼리를 실행할 예정인데, 그 사이에 트랜잭션 B가 같은 테이블에 값 [5, 6, 7]을 추가해버리면 A가 같은 쿼리 두번을 날리는 사이 두 쿼리의 결과가 다르게 되어 버린다.
- 즉, 한 트랜잭션에서 일정 범위의 레코드를 두번 이상 읽을 때 발생하는 데이터 불일치이다.
# 레퍼런스
https://it-techtree.tistory.com/entry/springboot-manage-database-transactions
https://goddaehee.tistory.com/167
https://sas-study.tistory.com/443
'Backend > springboot' 카테고리의 다른 글
[SpringBoot] JPA Spring Security + refresh token으로 회원가입 구현하기 (2) (0) | 2023.01.23 |
---|---|
[SpringBoot] JPA Spring Security + refresh token으로 회원가입 구현하기 (1) (0) | 2023.01.23 |
[SpringBoot] 스프링부트 구글 로그인 API REST 방식으로 구현하기 (2) | 2023.01.11 |
[SpringBoot] 영속성 컨텍스트, 변경 감지와 병합(merge) (0) | 2023.01.06 |
[SpringBoot] JWT 토큰 인증 방식으로 구글 소셜로그인하기 (로직) (0) | 2023.01.01 |