일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- @Aspect
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch5
- 스프링 mvc1 - 스프링 mvc
- 자바의 정석 기초편 ch7
- 스프링 db1 - 스프링과 문제 해결
- 자바의 정석 기초편 ch4
- 2024 정보처리기사 시나공 필기
- 2024 정보처리기사 수제비 실기
- 타임리프 - 기본기능
- 자바의 정석 기초편 ch6
- 스프링 mvc2 - 검증
- 자바의 정석 기초편 ch9
- 스프링 mvc1 - 서블릿
- 스프링 입문(무료)
- 자바의 정석 기초편 ch14
- 자바의 정석 기초편 ch12
- 게시글 목록 api
- 코드로 시작하는 자바 첫걸음
- 자바의 정석 기초편 ch1
- 자바의 정석 기초편 ch2
- 자바의 정석 기초편 ch8
- 자바의 정석 기초편 ch3
- jpa 활용2 - api 개발 고급
- jpa - 객체지향 쿼리 언어
- 스프링 mvc2 - 로그인 처리
- 자바의 정석 기초편 ch13
- 스프링 mvc2 - 타임리프
- 스프링 db2 - 데이터 접근 기술
- 스프링 고급 - 스프링 aop
- Today
- Total
나구리의 개발공부기록
스프링 트랜잭션 이해, 트랜잭션 옵션 소개, 트랜잭션 커밋/롤백 - 기본, 트랜잭션 커밋/롤백 - 활용 본문
스프링 트랜잭션 이해, 트랜잭션 옵션 소개, 트랜잭션 커밋/롤백 - 기본, 트랜잭션 커밋/롤백 - 활용
소소한나구리 2024. 9. 24. 18:02 출처 : 인프런 - 스프링 DB 2편 데이터 접근 핵심 원리 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2
1. 트랜잭션 옵션 소개
1) @Transaction - 코드, 설명 순서에 따라 약간 수정됨
- 전체 옵션 예시
public @interface Transactional {
String value() default "";
String transactionManager() default "";
Class<? extends Throwable>[] rollbackFor() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
boolean readOnly() default false;
String[] label() default {};
2) value, transactionManager
- 트랜잭션을 사용하려면 먼저 스프링 빈에 등록된 어떤 트랜잭션 매니저를 사용할지 알아야 함
- 코드로 직접 트랜잭션을 사용할 때 트랜잭션 매니저를 주입 받아서 사용했는데, @Transactional에서도 트랜잭션 프록시가 사용할 트랜잭션 매니저를 지정해주어야함
- 사용할 트랜잭션 매니저를 지정할 때는 value, transactionManager 둘 중 하나에 트랜잭션 매니저의 스프링 빈의 이름을 적어주면 되는데, 해당 값을 생략하면 기본으로 등록된 트랜잭션 매니저를 사용하기 때문에 대부분 생략함
- 하지만 사용하는 트랜잭션 매니저가 둘 이상이라면 아래 코드처럼 트랜잭션 매니저의 이름을 지정해 줘야함
- 애노테이션에서 속성이 하나인경우에는 value, transactionManger를 생략하고 값을 바로 입력할 수 있음
public class TxService {
@Transactional("memberTxManager")
public void member() {...}
@Transactional("orderTxManager")
public void order() {...}
}
3) rollbackFor
(1) 예외 발생 시 스프링 트랜잭션의 기본 정책
- 언체크 예외인 RuntimeExcetion, Error와 그 하위 예외가 발생하면 롤백함
- 체크 예외인 Exception과 그 하위 예외들은 커밋함
(2) 옵션 사용으로 기본 정책에 롤백 예외를 추가
- 아래처코드처럼 지정하면 체크 예외인 Exception이 발생해도 Exception과 그 하위 예외들이 발생하면 롤백하게 됨
@Transactional(rollbackFor = Exception.class) //Exception과 그 하위 예외 발생시 롤백 되도록 추가
- rollbackFor는 예외 클래스를 직접 지정하고 rollbackForClassName으로 예외 이름을 문자로 입력할 수도 있지만 rollbackForClassName은 잘 안쓴다고 함
3) noRollbackFor
- rollbackFor와 반대로 동작하는 옵션
- 기본 정책에 추가로 어떤 예외가 발생했을 때 롤백하면 안되는지 지정할 수 있으며, 예외 이름을 문자로 넣을 수 있도록 noRollbackForClassName도 있음
- 그러나 보통은 rollbackFor로 기본 정책에서 추가로 더 롤백 시킬 예외를 지정하는 편이지 noRollbackFor나 noRollbackForClassName은 안씀
** 롤백 관련 자세한 내용은 예외와 트랜잭션 커밋/롤백에서 더 설명함
4) Propagation
- 트랜잭션 전파에 관련한 옵션
- 중요한 내용이므로 별도의 강의에서 자세히 설명
5) isolation
- 트랜잭션 격리 수준을 지정할 수 있으나 대부분 데이터베이스에서 설정한 기준(DEFAULT)으로 사용하며 애플리케이션 개발자가 트랜잭션 격리 수준을 직접 지정하는 경우는 드묾
- 기본값은 DEFAULT
- DEFAULT: 데이터베이스에서 설정한 격리 수준을 따름
- READ_UNCOMMITED: 커밋되지 않은 읽기
- READ_COMMITED: 커밋된 읽기
- REPEATABLE_READ: 반복 가능한 읽기
- SERIALIZABLE: 직렬화 기능
** 참고
- 강의에서는 일반적으로 많이 사용하는 READ COMMITTED(커밋된 읽기) 트랜잭션 격리 수준을 기준으로 설명
- 트랜잭션 격리 수준은 데이터베이스에 자체에 관한 부분이므로 강의 내용을 넘어서기 때문에 트랜잭션 격리 수준에 대한 자세한 내용은 데이터베이스 메뉴얼이나 검색 내용을 참고
6) timeout
- 트랜잭션 수행 시간에 대한 타임아웃을 초 단위로 지정
- 기본 값은 트랜잭션 시스템의 타임아웃을 사용함
- 운영환경에 따라 동작하는 경우도 있고 그렇지 않은 경우도 있기 때문에 꼭 확인하고 사용해야함
- 숫자 대신 문자 값으로 지정할 수 있는 timeoutString도 있음, 잘 사용하지 않음
7) label
- 트랜잭션 애노테이션에 있는 값을 직접 읽어서 어떤 동작을 하고 싶을 때 사용할 수 있음
- AOP를 잘아야 사용할 수 있으며, 일반적으로는 사용하지 않음
8) readOnly
- 기본적으로 트랜잭션은 읽기 쓰기가 모두 가능한 트랜잭션이 생성됨
- readOnly = true 옵션을 사용하면 읽기 전용 트랜잭션이 생성되는데 이 경우에는 등록, 수정, 삭제가 안되고 읽기 기능만 작동함(단, 드라이버나 데이터베이스에 따라 정상 동작하지 않는 경우도 있어서 주의해야함)
- readOnly 옵션을 사용하면 읽기에서 다양한 성능 최적화가 발생할 수 있음 -> 이것때문에 사용함
- 크게 아래 3곳에서 적용됨
(1) 프레임워크
- JdbcTemplate은 읽기 전용 트랜잭션 안에서 변경 기능을 실행하면 예외를 던짐
- JPA(하이버네이트)는 읽기 전용 트랜잭션의 경우 커밋 시점에 플러시를 호출하지 않음 -> 읽기 전용이니 변경에 사용되는 플러시를 호출할 필요가 없고, 변경이 필요 없으니 변경 감지를 위한 스냅샷 객체도 생성하지 않아서 JPA에서는 다양한 최적화가 발생함
- JPA관련 내용은 JPA를 더 학습해야 이해할 수 있으므로 지금은 이정도로만 이해하면 되며 JPA를 사용시 읽기 관련된 곳에서는 readOnly를 사용하면 좋음
(2) JDBC 드라이버
- 해당 설명은 DB와 드라이버 버전에 따라서 다르게 동작하기 때문에 사전에 확인이 꼭 필요함
- 읽기 전용 트랜잭션에서 변경 쿼리가 발생하면 예외를 던짐
- 읽기, 쓰기(마스터, 슬레이브) 데이터베이스를 구분해서 요청함 -> 읽기 전용 트랜잭션의 경우 읽기(슬레이브) 데이터베이스의 커넥션을 획득해서 사용함
(3) 데이터베이스
- 데이터베이스에 따라 읽기 전용 트랜잭션의 경우 읽기만 하면 되므로 내부에서 성능 최적화가 발생할 수 있음
** 개발에서는 제약이 중요한 이유는 읽기, 쓰기가 모두 가능하면 두가지 상황을 내부에서 모두 고려해서 동작해야하지만, 읽기전용인 경우 내부에서는 쓰기에 대한 동작을 모두 배제하며 동작할 수 있기 때문에 최적화가 발생될 수 있기 때문 **
2. 트랜잭션 커밋, 롤백 - 기본
- 예외가 발생하면 트랜잭션의 기본 정책은 언체크 예외가 발생하면 롤백, 체크 예외 혹은 정상 응답이면 커밋하는데 실제로 그런지 로그로 확인 해보기
1) RollbackTest
- assertThatThrowBy().isInstanceOf()로 터진 예외가 예상 예외와 동일할 경우 테스트가 통과되도록 설정
- runtimeException() : RuntimeException(언체크드 예외) 발생 -> 롤백 예상
- checkedException() : Exception (체크드 예외)를 발생시키는 MyException을 던짐 -> 커밋 예상
- rollbackFor() : MyException을 던져서 커밋되야 하지만 @Transactional(rollbackFor = MyException.class) 옵션을 주어 MyException.class 예외가 발생하면 롤백 되도록 설정 -> 체크드 예외지만 롤백 예상
package hello.springtx.exception;
@SpringBootTest
public class RollbackTest {
@Autowired RollbackService service;
@Test
void runtimeException() {
assertThatThrownBy(() -> service.runtimeException()).isInstanceOf(RuntimeException.class);
}
@Test
void checkedException() {
assertThatThrownBy(() -> service.checkedException()).isInstanceOf(MyException.class);
}
@Test
void rollbackFor() {
assertThatThrownBy(() -> service.rollbackFor()).isInstanceOf(MyException.class);
}
@TestConfiguration
static class RollbackTestConfig {
@Bean
RollbackService rollbackService() {
return new RollbackService();
}
}
@Slf4j
static class RollbackService {
// 런타임 예외 발생: 롤백
@Transactional
public void runtimeException() {
log.info("call runtimeException");
throw new RuntimeException();
}
// 체크 예외 발생: 커밋
@Transactional
public void checkedException() throws MyException {
log.info("call checkedException");
throw new MyException();
}
// 체크 예외 rollbackFor 지정 : 롤백
@Transactional(rollbackFor = MyException.class)
public void rollbackFor() throws MyException {
log.info("call rollbackFor");
throw new MyException();
}
}
static class MyException extends Exception {
}
}
2) application.properties 수정
- 로그로 트랜잭션이 커밋이 되었는지, 롤백 되었는지 확인할 수 있도록 로그 설정을 추가
- 지금은 JPA를 사용하므로 JpaTransactionManager가 실행되고 여기서 로그를 출력하게됨
- 참고로 지금 실습과는 관계 없지만 Jdbc를 사용했는데 기본 트랜잭션 매니저가 JdbcTransactionManager면 DataSourceTransactionManager를 JdbcTransactionManager로 변경하면 로그를 볼 수 있음
# 트랜잭션이 커밋되었는지 롤백 되었는지 로그로 확인
logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=DEBUG
# JPA를 사용하므로 JpaTransactionManager의 로그를 확인
logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG
logging.level.org.hibernate.resource.transaction=DEBUG
3) 결과 확인
(1) runtimeException() - 런타임 예외 -> 롤백
- 트랜잭션이 시작되고 RuntimeException이 발생한 뒤 Initiating transaction rollback으로 트랜잭션이 롤백된 로그가 출력됨
(2) checkedException() - MyException(Exception 예외) -> 커밋
- 트랜잭션이 시작되고 MyException이 발생한 뒤 Initiating transaction commit으로 트랜잭션이 커밋된 로그가 출력됨
(3) rollbackFor() - 체크예외를 옵션을 통해 강제로 롤백 -> 롤백
- 트랜잭션이 시작되고 MyException이 발생한 뒤 Initiating transaction rollback으로 트랜잭션이 롤백된 로그가 출력됨
- 기본 정책과 무관하게 특정 예외를 강제로 롤백시키도록 rollbackFor 옵션을 사용했으므로 롤백이 됨(적용된 예외와 그 자식 예외 모두 포함됨)
3. 예외와 트랜잭션 커밋, 롤백 - 활용
- 스프링은 왜 체크 예외는 커밋, 언체크 예외는 롤백하도록 기본 설정을 한 이유는 체크 예외는 비즈니스 의미가 있을 때 사용하고, 언체크 예외는 복구가 불가능한 예외로 가정 했기 때문
- 체크 예외 : 비즈니스 의미가 있을 때 사용
- 언체크 예외 : 복구 불가능한 예외(시스템에서 발생한 오류, 문법 오류 등)
- 참고로 이런 정책을 꼭 따를 필요는 없어서 rollbackFor 옵션을 사용해서 요구사항에 맞춰서 체크 예외도 롤백해서 사용하면 됨
- 그런데 비즈니스 의미가 있는 비즈니스 예외라는 것이 무슨뜻인지 알아야 위 상황을 고려하여 적용할 수 있으므로 예제로 알아보기
1) 비즈니스 요구사항
주문 시 상황에 따른 조치 사항 예시
- 정상: 주문시 결제를 성공하면 주문 데이터를 저장하고 결제 상태를 완료로 처리
- 시스넴 예외: 주문시 내부에 복구 불가능한 예외가 발생하면 전체 데이터를 롤백
- 비즈니스 예외: 주문시 결제 잔고가 부족하면 주문 데이터를 저장하고, 결제 상태를 대기로 처리 -> 고객에게 잔고 부족을 알리고 별도의 계좌로 입금하도록 안내
- 결제 잔고가 부족하면 NotEnoughMoneyException이라는 체크 예외가 발생하도록 개발했다고 가정했을 때, 이 예외는 시스템에 문제가 있어서 발생하는 시스템 예외가 아닌 비즈니스 상황에서 문제가 되었기 때문에 발생한 예외임
- 고객의 잔고가 부족한 것은 시스템에 문제가 있는 것이 아니라 시스템은 문제없이 동작했지만 비즈니스 상황이 예외인 것
- 이렇게 발생한 예외를 비즈니스 예외라고 함
- 비즈니스 예외는 매우 중요하고 반드시 처리해야하는 경우가 많으므로 체크 예외를 고려할 수 있음
- 항상 개발을할때 예외는 시스템에 진짜 장애가 있어서 발생하는 예외와, 비즈니스 상황에서 문제가 있어 발생하는 예외, 이렇게 2가지를 구분해서 생각을 해야함
2) NotEnoughMoneyException
- 결제 잔고가 부족하면 발생하는 비즈니스 예외를 Exception을 상속받아 체크 예외로 생성
- 비즈니스 요구사항 3번을 적용해야 하려면 데이터가 커밋되어야 하므로 비즈니스 예외를 체크드 예외로 설정한 것
package hello.springtx.order;
public class NotEnoughMoneyException extends Exception {
public NotEnoughMoneyException(String message) {
super(message);
}
}
3) Order
- JPA를 사용하는 Order 엔터티
- 예제를 단순하게 하기 위해 @Getter, @Setter를 사용했지만 실무에서 엔터티에 @Setter를 남발해서 불필요한 변경 포인트를 노출하는 것은 좋지 않음
- 주의 사항! @Table(name = "orders"): 테이블 이름을 이렇게 지정하지 않으면 테이블 이름이 클래스 이름인 order로 지정되는데, order는 데이터베이스 예약어(ORDER BY)이기 때문에 사용할 수 없음, 그래서 orders라는 테이블 명으로 지정함
- 실무에서도 보통 주문 테이블은 orders라고 많이 작성한다고 함
package hello.springtx.order;
@Entity
// 테이블명을 orders로 매핑(SQL문법에 ORDER BY 문법이 있어 예약어로 ORDER를 사용할 수 없어서 보통 orders로 테이블명을 사용한다고 함)
@Table(name = "orders")
@Getter
@Setter
public class Order {
@Id
@GeneratedValue
private Long id;
private String username; // 정상, 예외, 잔고부족
private String payStatus; // 대기, 완료
}
4) OrderRepository
- 스프링 데이터 JPA를 사용하는 리포지토리
package hello.springtx.order;
public interface OrderRepository extends JpaRepository<Order, Long> {
}
5) OrderService
(1) 사용자 이름(username)에 따른 처리 프로세스
- 기본: payStatus를 완료 상태로 처리하고 정상 처리
- 예외: RuntimeException("시스템 예약") 런타임 예외 발생
- 잔고부족: payStatus를 대기 상태로 처리하고 NotEnoughMoneyException("잔고가 부족합니다") 체크 예외 발생
- 잔고부족은 체크 예외가 발생하지만 order 데이터는 커밋되는 것을 기대함
package hello.springtx.order;
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
// JPA는 트랜잭션 커밋 시점에 Order 데이터를 DB에 반영함
// 원래는.. 이런 시스템을 구현하려면 복잡하지만 상황을 보여주기 위한 것이므로 간편하게 작성
@Transactional
public void order(Order order) throws NotEnoughMoneyException {
log.info("order 호출");
orderRepository.save(order); // order 저장
log.info("결제 프로세스 진입");
if (order.getUsername().equals("예외")) {
log.info("시스템 예외 발생");
throw new RuntimeException("시스템 예외"); // order.getUsername()이 "예외"면 런타임예외 발생
} else if (order.getUsername().equals("잔고부족")) {
log.info("잔고 부족 비즈니스 예외 발생");
// order.getUsername()이 "잔고부족"이면 상태를 대기로 저장후 만들어둔 비즈니스 예외(체크예외)를 던짐
order.setPayStatus("대기");
throw new NotEnoughMoneyException("잔고가 부족합니다.");
} else {
// 정상 승인
log.info("정상 승인");
order.setPayStatus("완료");
}
log.info("결제 프로세스 완료");
}
}
6) OrderServiceTest
(1) complete() -> 정상처리
- 사용자이름을 정상으로 설정하여 프로세스가 정상 수행됨
- 리포지토리의 값을 꺼내서 데이터가 완료 상태로 저장되었는지 검증
(2) runtimeException() -> 런타임 예외 발생, 롤백
- 사용자이름을 예외로 설정하여 RuntimeException("시스템 예외")가 발생
- 런타임 예외 발생으로 롤백이 수행되어 리포지토리에서 꺼낸 Order의 데이터가 비어있는 것을 검증
(3) bizException() -> 비즈니스 예외 발생, 데이터는 커밋, 예외 상황에 따른 로직을 수행
- 사용자이름을 잔고부족으로 설정하여 NotEnoughMoneyException("잔고가 부족합니다")라는 비즈니스 예외가 발생
- 체크 예외로 커밋이 수행되어 리포지토리에는 데이터가 저장되었으므로 데이터가 대기 상타로 저장 되었는지 검증
- try-catch로 비즈니스 예외를 컨트롤러에서 처리하는 상황을 가정
(4) application.properties 설정
- JPA(하이버네이트)가 실행하는 SQL를 로그로 확인하도록 로그를 추가
# JPA SQL
logging.level.org.hibernate.SQL=DEBUG
package hello.springtx.order;
@Slf4j
@SpringBootTest
public class OrderServiceTest {
@Autowired OrderService orderService;
@Autowired OrderRepository orderRepository;
@Test
void complete() throws NotEnoughMoneyException {
// given
Order order = new Order();
order.setUsername("정상");
// when
orderService.order(order);
// then
Order findOrder = orderRepository.findById(order.getId()).get(); // findById 반환 타입이 Optional이여서 .get()으로 바로 꺼내기
assertThat(findOrder.getPayStatus()).isEqualTo("완료");
}
@Test
void runtimeException() {
// given
Order order = new Order();
order.setUsername("예외");
// when
assertThatThrownBy(() -> orderService.order(order)).isInstanceOf(RuntimeException.class);
// then
Optional<Order> orderOptional = orderRepository.findById(order.getId());
assertThat(orderOptional.isEmpty()).isTrue(); // 저장된 값이 없는지를 확인 -> 롤백이 되었기 때문에 DB에 반영이 안되었을 것이기 때문
}
@Test
void bizException () {
// given
Order order = new Order();
order.setUsername("잔고부족");
// when
try {
orderService.order(order);
} catch (NotEnoughMoneyException e) {
// 컨트롤러에서 이렇게 처리한다고 가정
log.info("고객에게 잔고 부족을 알리고 별도의 계좌로 입금하도록 안내");
}
// then
Order findOrder = orderRepository.findById(order.getId()).get(); // commit 되었으므로 DB의 값을 꺼냄
assertThat(findOrder.getPayStatus()).isEqualTo("대기"); // PayStatus()가 "대기"일 것을 기대
}
}
7) 테스트 실행 결과
(1) 테스트 테이블 자동 생성 옵션
- 테스트를 실행해보면 테이블을 생성하지 않았는데 모든 테스트가 성공하는데, 메모리 DB를 통해 테스트를 수행하면 테이블 자동 생성 옵션이 활성화 되기 때문임
- JPA는 엔터티 정보를 참고해서 테이블을 자동생성하며 application.properties에 spring.jpa.hibernate.ddl-auto 옵션을 조정하여 조정할 수 있음
- none: 테이블을 생성하지 않음
- create: 애플리케이션 시작 시점에 테이블을 생성
(2) 테스트 결과 - 코드에 오류가 없다면 모든 테스트는 통과됨
- complete() : 정상 처리되는 로그를 확인할 수 있음
- runtimeException():시스템 예외가 발생하여 트랜잭션이 롤백되는 로그를 확인할 수 있음
- bixException(): 잔고부족이라는 비즈니스 예외가 발생하여 데이터는 커밋하지만, 요사항으로 정의한 고객에게 잔고 부족을 알리는 로직을 수행하는 로그를 확인할 수 있음
** 참고
- JPA가 출력한 로그의 순서대로 JPA가 실행되는 것은 아니며, 이부분은 JPA를 알아야 이해할 수 있으므로 지금은 해당 케이스에 대한 결과를 로그로 확인하는 데에 의의를 두도록 하자
8) 정리
- NotEnoughMoneyException은 시스템에서 문제가 발생한 것이 아니라 비즈니스 문제 상황을 예외를 통해 알려주고, 마치 예외가 리턴 값 처럼 사용됨
- 이런 상황에서 트랜잭션을 롤백 시켜버리면 생성한 Order에 대한 데이터 자체가 사라지므로 고객에게 잔고 부족을 알리고 별도 계좌로 입금을 안내해도 주문 내역 자체가 사라져 문제가 되기 때문에 트랜잭션을 커밋하는게 지금 상황에서는 맞음
- 비즈니스 상황에 따라 체크 예외의 경우에도 트랜잭션을 커밋하지 않고 롤백을 하고자 하는 경우에는 위에서 배웠던 rollbackFor옵션을 사용해도 되지만, 기본적으로는 옵션을 사용하지 않고 언체크드 예외 등을 사용해서 스프링의 기본 기능으로 해결해 보는 것을 권장함
- 비즈니스 예외에 대한 상황은 지금처럼 예외를 별도로 정의해서 비즈니스 예외로 터트릴 수도 있지만, 예외로 처리하기 싫다면 enum 으로 별도로 정의를해서 return으로 처리해도 상관없음
- 처리를 어떻게 할 것인가에 대한 부분은 상황에 따라서 개발자들이 선택하는 부분이며 처리에 대한 정답은 없음