≣ 목차
데이터 접근 기술들은 다양한 방식으로 트랜잭션을 처리합니다. 그러나 Spring은 다양한 데이터 접근 기술에서 사용하는 트랜잭션 추상화를 제공하며, 트랜잭션 매니저의 구현체도 제공합니다. 이는 Java 개발자가 트랜잭션 관련 코드를 작성할 필요가 없는 걸 의미합니다. 즉 @Transactional 애노테이션을 사용하여 트랜잭션을 쉽게 관리할 수 있습니다.
트랜잭션에 대해서 잘 모르시는 분은 아래 링크를 참조하시면 좋을 것 같습니다.
https://pjstudyblog.tistory.com/41
[DB] 트랜잭션 정리 1편
≣ 목차1. 트랜잭션이란?데이터를 데이터베이스에 저장하는 이유는 여러 가지가 있지만, 가장 일반적인 이유는 트랜잭션입니다. 트랜잭션은 데이터베이스 관리 시스템 또는 유사한 시스템에서
pjstudyblog.tistory.com
1 스프링이 제공하는 트랜잭션 관련 자동 구성 기능
다양한 데이터 접근 기술에 트랜잭션 추상화 기능(JDBC, JPA, 하이버네이트)을 제공할 뿐만 아니라, 트랜잭션 매니저의 구현제도 제공합니다. 이제 그럼 필요한 구현체를 스프링에 등록하고 사용하면 된다고 생각하실 수 있으실텐데 스프링 부트는 라이브러리를 확인해서 어떤 데이터 접근 기술을 사용하는지 자동으로 인식해서 적절한 구현체를 스프링에 등록해줍니다.
2 @Transactional 동작 설명
@Transactional 애노테이션을 적용하면 해당 메소드는 작업이 성공적으로 완료되어야만 변경 사항이 커밋되고, 하나라도 실패하면 모든 작업이 롤백되어 데이터의 일관성을 유지합니다.
@Transactional 애노테이션을 사용할 때, 프록시 방식과 AOP(Aspect-Oriented Programming)를 적용합니다. 그럼 @Transactional이 어떻게 동작하는지 설명해보겠습니다.
1. 프록시 호출
@Transactional를 적용한 메서드를 호출되면 메서드가 실행되기 전에 먼저 스프링이 생성한 AOP 프록시가 가로챕니다.
2. 스프링 컨테이너 호출
트랜잭션 관련 프록시는 스프링 컨테이너에 등록된 트랜잭션 매니저를 통해 트랜잭션 관리 작업을 준비합니다.
3. 트랜잭션 매니저로부터 트랜잭션 획득
프록시는 트랜잭션 매니저의 getTransaction() 메서드를 호출하여 새로운 트랜잭션을 시작합니다. 이 과정에서 트랜잭션의 속성(전파, 격리 수준 등)이 설정됩니다.
4. 데이터소스에서 커넥션 생성
트랜잭션 매니저는 데이터베이스 연결을 위해 데이터소스에서 커넥션을 생성합니다. 이 커넥션은 트랜잭션이 유지되는 동안 데이터베이스와의 모든 작업을 처리하게 됩니다.
5. AutoCommit 설정 비활성화
데이터의 일관성을 유지하기 위해, 생성된 커넥션의 AutoCommit 설정을 false로 변경합니다. 이를 통해 데이터가 커밋될 때까지 모든 변경 사항이 데이터베이스에 반영되지 않도록 합니다.
6. 커넥션 보관
트랜잭션 매니저는 생성된 커넥션을 보관하고, 이후 트랜잭션 처리 중에 이 커넥션을 사용합니다. 이 커넥션은 동일한 트랜잭션 범위 내에서 계속 사용됩니다.
7. 보관된 커넥션 반환
트랜잭션 동기화 매니저는 보관된 커넥션을 필요에 따라 반환합니다. 이는 메소드 내에서 여러 데이터 접근이 일관되게 처리되도록 보장합니다.
8. 실제 서비스 호출
트랜잭션이 시작된 후, 비즈니스 로직이 포함된 실제 서비스 메서드가 호출됩니다. 여기서는 트랜잭션 내에서 이루어지는 데이터 접근 로직이 실행됩니다.
9. 트랜잭션 동기화 커넥션 획득
서비스 로직에서 데이터 접근 로직이 호출되면, 이 데이터 접근 로직은 트랜잭션 동기화 매니저를 통해 이미 보관된 커넥션을 사용합니다. 이를 통해 트랜잭션의 일관성이 유지합니다.
10. 트랜잭션 종료
서비스 메서드가 정상적으로 완료되면 프록시는 트랜잭션 매니저를 통해 트랜잭션을 종료합니다. 트랜잭션 종료 시점에 트랜잭션이 커밋되며, 조금이라도 예외가 발생한 경우 롤백됩니다.
@Transactional 동작 원리에 핵심은 실체 객체를 상속해서 만들어진 프록시가 스프링 컨테이너에 스프링 빈으로 등록되는 점입니다.
3 @Transactional 옵션
1. readOnly
트랜잭션을 읽기전용으로 설정하는 옵션이다. true로 설정하면 insert, update, delete 실행할 때 예외가 발생합니다.
기본 값은 false입니다.
@Transactional(readOnly = true)
public void findAll() {
}
2.timeout
지정한 시간 내에 메서드 수행이 완료되지 않으면 rollback 하게 하는 옵션이며, 운영 환경에 따라 동작이 안할 수 있습니다.
기본 값은 -1이며, 해당 의미는 메서드 실행에 시간 제한이 없음을 의미합니다.
@Transactional(timeout = 10)
public void findById() {
}
3. noRollbackFor, rollbackFor
rollbackFor 속성은 지정된 예외가 발생했을 때 트랜잭션을 롤백하도록 설정하는 옵션
noRollbackFor 속성은 지정된 예외가 발생하더라도 트랜잭션을 롤백하지 않도록 설정하는 옵션
@Transactional(rollbackFor = {CustomException.class, AnotherException.class})
public void rollback() {
}
@Transactional(noRollbackFor = {NonException.class})
public void nonRollback() {
}
참고로 Spring은 체크 예외는 커밋하고, 언체크(런타임) 예외는 롤백합니다. 그러면 Spring이 왜 이런 방식으로 예외를 처리하는지 궁금할 것입니다. 그 이유는 Spring이 다양한 유형의 예외를 구별하는 방법에 있습니다.
- 언체크(시스템) 예외: Spring은 이러한 문제가 네트워크 오류나 시스템 오류와 같은 심각하고 복구할 수 없는 문제라고 가정합니다. 이러한 예외는 시스템으로 인한 생긴 예외는 설계 상 작업이 부분적으로 완료될 가능성이 높기 때문에 데이터 무결성을 위해 롤백합니다.
- 체크(비즈니스) 예외: 이러한 예외는 복구 가능한 문제나 비즈니스 논리 오류를 나타내는 경우가 많습니다. 예를 들어, 고객이 주문을 완료할 금액이 충분하지 않은 경우 xxxException이 발생할 수 있습니다. 이 예외는 시스템이 올바르게 작동하지만 비즈니스 규칙을 위반했음을 나타냅니다. 이러한 경우 클라이언트 요청이 있다면 주문 세부 정보 저장, 결제 상태를 보류 중으로 표시 등 일부 데이터를 커밋해야 할 수도 있습니다.
주문 프로세스와 관련된 예를 가정해 보겠습니다.
정상동작: 결제 성공 후 주문이 저장되며, 결제 상태가 완료로 표시됩니다.
시스템 예외: 네트워크 오류와 같은 시스템 문제로 인해 발생하는 경우 일관성 없는 데이터를 방지하기 위해 전체 트랜잭션을 롤백해야 합니다.
비즈니스 예외: 고객의 금액이 충분하지 않은 경우(xxxException 발생) 클라이언트 요청이 있다면 주문 관련 데이터는 저장되지만 결제 상태는 대기 중으로 설정되어 추가적으로 작업을 할 수 있습니다.
체크 예외를 커밋하는 Spring의 기본 동작은 이러한 예외가 부분 데이터 처리가 필요할 수 있는 복구 가능한 조건을 나타낸다는 가정을 반영합니다. 이러한 유연성을 통해 애플리케이션은 데이터 일관성을 유지하면서 복잡한 비즈니스 시나리오를 효과적으로 처리할 수 있습니다.
4. propagation
기존 트랜잭션이 동작할 때 다른 트랜잭션이 호출되면 어떻게 처리할지를 정하는 옵션이며, 기존의 트랜잭션 여부에 따라 새로운 트랜잭션을 생성할지, 기존 트랜잭션을 사용할지를 결정하는 옵션입니다.
@Transactional(propagation = Propagation.NEVER)
public void addMoney() {
}
propagation 옵션 종류
REQUIRES_NEW
항상 새로운 트랜잭션을 생성합니다. 현재 트랜잭션이 존재하면 일시 중지되고, 새로운 트랜잭션이 완료된 후에 다시 복원됩니다.
NESTED
현재 트랜잭션이 존재하면 중첩 트랜잭션을 생성합니다. 중첩 트랜잭션은 부모 트랜잭션과 함께 커밋되거나 롤백됩니다. 부모 트랜잭션이 롤백되면 중첩 트랜잭션도 롤백됩니다.
SUPPORTS
현재 트랜잭션이 존재하면 그 트랜잭션을 사용하고, 없으면 트랜잭션 없이 실행됩니다.
NOT_SUPPORTED
항상 트랜잭션 없이 실행됩니다. 현재 트랜잭션이 존재하면 일시 중지됩니다.
MANDATORY
현재 트랜잭션이 반드시 존재해야 하며, 없으면 예외가 발생합니다.
NEVER
항상 트랜잭션 없이 실행됩니다. 현재 트랜잭션이 존재하면 예외가 발생합니다.
5. isolation
isolation은 트랜잭션의 격리 수준을 설정하는 데 사용됩니다.
@Transactional(isolation = Isolation.DEFAULT)
public void addMoney() {
}
isolation 옵션 종류
DEFAULT
데이터베이스의 기본 격리 수준을 사용합니다.
READ_UNCOMMITTED
다른 트랜잭션에서 커밋되지 않은 데이터를 읽을 수 있습니다. 즉, 한 트랜잭션이 변경한 데이터가 다른 트랜잭션에서 읽힐 수 있습니다.
READ_COMMITTED
다른 트랜잭션에서 커밋된 데이터만 읽을 수 있습니다. 즉, 같은 트랜잭션 내에서 동일한 쿼리를 실행할 때 결과가 달라질 수 있습니다.
REPEATABLE_READ
트랜잭션이 시작된 시점의 데이터를 읽을 수 있으며, 같은 트랜잭션 내에서 동일한 쿼리를 실행할 때 항상 같은 결과를 반환합니다.
SERIALIZABLE
가장 높은 격리 수준으로, 순차적으로 실행되는 것처럼 보이게 할 수 있지만 성능 저하가 발생할 수 있습니다.
해당 옵션에 대해서 자세히 알고 싶으시면 아래 링크를 참고해주세요
https://mangkyu.tistory.com/299
4 @Transcational 사용 시 주의점 𖤐𖤐 - 내부 호출
앞에 설명했듯이 @Transactional 애노테이션의 동작 방식은 프록시 객체를 생성하여 실제 객체를 호출하는 것입니다. 이로 인해 문제가 발생할 수 있습니다. 외부에서 @Transactional이 적용된 메소드를 호출할 때와 내부에서 호출할 때의 차이입니다.
- 외부 호출: 클래스 외부에서 @Transactional이 적용된 메소드를 호출하면, 프록시 객체가 실제 객체를 대신 호출하기 때문에 트랜잭션이 정상적으로 적용됩니다. 이때 트랜잭션이 예상대로 동작합니다.
- 내부 호출: 반면에, 동일한 클래스 내의 다른 메소드에서 @Transactional이 적용된 메소드를 내부적으로 호출할 경우, 프록시 객체가 아닌 실제 객체가 호출됩니다. 당연히 그러면 트랜잭션이 제대로 적용되지 않습니다.
많은 사람들이 놓치고 있는 부분이 있는데 내부에 트랜잭션이 적용된 메소드를 호출 시 사실 this 키워드를 사용하지 않아도 자동으로 적용이 됩니다. this는 현재 객체 인스턴스를 직접 참조하는 키워드입니다. 그래서 이 경우 @Transactional이 적용된 프록시 객체가 아닌 실제 객체가 호출하기 때문에 문제가 발생하는것 입니다.
4-1 해결 방법
@Transactional의 주요 문제는 메소드가 동일한 클래스 내에서 내부적으로 호출될 때 메소드가 프록시가 아닌 실제 객체에서 호출되기 때문에 Spring에서 제공하는 프록시 AOP 메커니즘이 작동하지 않는다는 것입니다.
이 문제를 해결하는 간단한 방법은 메서드를 다른 클래스로 이동하여 메서드가 외부에서 호출되도록 하는 것입니다. 이렇게 하면 Spring에서 제공하는 프록시 AOP 기능이 적용됩니다.
5 @Transcational 사용 시 주의점 𖤐𖤐 - 초기화에 적용
초기화 코드(예: @PostConstruct)를 @Transactional과 결합하면 트랜잭션이 적용되지 않습니다. 이는 트랜잭션 AOP가 설정되기 전에 초기화 코드가 실행되기 때문에 발생합니다.
5-1 해결 방법
해당 문제를 간단하게 해결하는 방법은 @EventListener 애노테이션을 사용하는 것 입니다. @EventListener옵션 중 ApplicationReadyEvent 이벤트를 사용하면 Spring이 트랜잭션 AOP를 포함하여 애플리케이션 초기 설정(초기화)이 끝나고 난 뒤 해당 메서드를 동작하기 때문에 트랜잭션 AOP가 올바르게 적용됩니다.
@EventListener(value = ApplicationReadyEvent.class)
@Transactional
public void init() {
log.info("init ApplicationReadyEvent");
}
'Spring' 카테고리의 다른 글
[Spring] Spring Security 6.x버전 달라진 사항 (0) | 2024.09.15 |
---|---|
[Spring] 트랜잭션 전파 속성 정리 - propagation (0) | 2024.09.02 |
[Spring + DB] 스프링의 DB 예외 추상화 - ExceptionTranslator (0) | 2024.08.15 |
[Spring+DB] 트랜잭션 추상화 및 동기화- TransactionManager (0) | 2024.08.10 |
[Spring]PathPattern 공식 문서 내용에 대한 예시 (0) | 2024.08.01 |