🏗️ Build Up트랜잭션이란특징트랜잭션은 Exception 타입에 따라 처리하는 방식이 다르다.💨What [스프링에서 제공하는 트랜잭션]⚠️스프링 트랜잭션 사용 주의사항Self - Invocation 문제@Transactional 우선 순위Transactional (readOnly =ture) 가 적용된 메소드에서📌 REFER
🏗️ Build Up
트랜잭션이란
- 일련의 작업을 의미한다.
특징
- 원자성
- 트랜잭션의 모두 하나의 단위로 성공과 실패 두가지로 처리되어야 한다.
- 일관성
- 트랜잭션 수행 후 데이터는 일관성을 유지해야 한다
- 독립성
- 다른 트랜잭션이 끼어들수 없다.
- 영속성
- 트랜잭션이 수행된 후 데이터는 영속적이여야 한다.

트랜잭션은 Exception 타입에 따라 처리하는 방식이 다르다.
예외의 종류

- 예외는 크게 CheckdException과 UncheckedException으로 나눌 수 있다.
- 이를 구분 짓는 특징은 RuntimeException의 상속 여부로 나뉜다.

💨What [스프링에서 제공하는 트랜잭션]
기본적으로 UnCheckedException은 롤백이 가능하고 Checked Exception은 롤백 되지 않는다.
하지만 @Transactional 에서 rollbackFor 옵션을 이용하고 rollback이 되는 클래스를 지정 가능하다.
그리고 unCheckedException에서 noRollbackFor를 사용하여 해당 예외를 추가하면 롤백이 되지 않도록 할 수 도 있다.
⚠️스프링 트랜잭션 사용 주의사항
- 트랜잭션은 AOP 기반으로 구성되어 있다.
- 실제 핵심 로직을 수행하면서 발생하는 횡단 관심사를 한데 모아 처리하는 것을 aop 라 한다.
- 즉, @Transactional 을 통해 프록시 객체를 생성함으로써 트랜잭션을 수행할 때마다, 커밋 또는 롤백 후 트랜잭션을 닫는 등의 부수적인 작업을 프록시 객체에게 위임할 수 있게 된다.
핵심 기능
은 메소드가 Invocation 될 때, 이 메소드를 가로채어 부가 기능들을 추가할 수 있도록 지원하는 것이다.
Self - Invocation 문제
Q. 정삭적으로 트랜잭션이 적용되어 롤백이 될까?
@Service @RequiredArgsConstructor public class JpaRunner { private final PostRepository postRepository; public void run() { for(int i=0; i<5; i++) { savePost(i); } } @Transactional public void savePost(int i) { // 현재 적용된 트랜잭션 이름을 확인 System.out.println("CurrentTransactionName:"+TransactionSynchronizationManager.getCurrentTransactionName()); postRepository.save(new Post(i)); if(i == 3) throw new RuntimeException(); // 예외 발생 } }
정답은 모두 롤백되지 않는다.
- 그 이유는 스프링 AOP는 프록시를 기반으로 동작하기 때문이다.
- AOP의 단점 중 하나인 프록시 내부를 호출 할 때는 부가적인 서비스가 적용되지 않는다.
- 호출하려는 Target을 감싸고 있는 프록시를 통해야만 부가적인 기능이 적용이 되는데 프록시 내부에서 내부를 호출 할 때는 감싸고 있는 proxy를 호출하지 않고 실제 구현체 영역으로 가기 때문에 그렇다.
- Service code
- Test code
- 결과
실제 코드
package com.sample.api.service; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.sample.core.domain.Member; import com.sample.core.repository.MemberRepository; @Service public class MemberService { private MemberRepository memberRepository; public MemberService(MemberRepository memberRepository) { this.memberRepository = memberRepository; } public void call() { for (int i = 1; i <= 5; i++) { save(i); } } @Transactional public void save(int i) { Member build = Member.builder() .name("module-api 엘리하이해") .build(); Member save = memberRepository.save(build); if (i == 3) { throw new RuntimeException("RuntimeException 이라 롤백이 되겠지 ?? (기대중..)"); } System.out.println("저장중 ... : " + save.getName()); } }
package com.sample.api.service; import static org.junit.jupiter.api.Assertions.*; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import com.sample.core.domain.Member; import com.sample.core.repository.MemberRepository; @SpringBootTest class MemberServiceTest { @Autowired private MemberService memberService; @Autowired private MemberRepository memberRepository; @Test @DisplayName("트랜잭션 롤백 처리 기대중") void testRollBack(){ try { memberService.call(); } catch (RuntimeException e) { System.out.println(e.getMessage()); List<Member> members = memberRepository.findAll(); System.out.println(members); } } }


this.save() 도 마찬가지 이다.
- 해결 방안
- Transactional 위치를 public void run() 메소드로 바꾼다.
- Proxy로 호출 될 수 있도록 만든다.
- 다른 클래스로 추가
- 내부 로직 변경 (MemberService)
- 결과
다른 클래스에서 호출 할 수 있도록 한다.
@Service class MemberService2{ private final MemberRepository memberRepository; public MemberService2(MemberRepository memberRepository) { this.memberRepository = memberRepository; } @Transactional public void save(int i) { if (i == 3) { throw new RuntimeException("RuntimeException 이라 롤백이 되겠지 ?? (기대중..)"); } Member build = Member.builder() .name("module-api 엘리하이해") .build(); Member save = memberRepository.save(build); System.out.println("저장중 ... : " + save.getName()); } }


@Transactional 우선 순위
- class Method
- Class
- Interface Method
- Interface
JPA 구현체인 SimpleJpaRepository 코드를 살펴보면 클래스 상단에 readOnly = true로 설정되어 있고 deleteById(ID id) 에서는 readOnly가 없는 트랜잭션이 선언된 것을 볼 수 있다.
즉, 전체는 readOnly로 활성화 되어있지만 CUD 될 때에는 false를 선언하면서 이를 우선으로 적용시킨 것이다.
@Repository @Transactional(readOnly = true) public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> { // ... private final EntityManager em; // ... @Transactional @Override public void deleteById(ID id) { Assert.notNull(id, ID_MUST_NOT_BE_NULL); delete(findById(id).orElseThrow(() -> new EmptyResultDataAccessException( String.format("No %s entity with id %s exists!", entityInformation.getJavaType(), id), 1))); }
Transactional (readOnly =ture) 가 적용된 메소드에서
@Transactional 혹은 @Transactioanl(readOnly=false)가 적용된 메소드를 호출할 경우 무조건 read-only Transactional이 적용된다.
트랜잭션이 전파되는 것은 맞지만 그렇지 않은 벤더들도 있기 때문에 예외에 주의 해야 한다.
이와 반대로 readOnly = false가 적용된 메소드에서 readOnly = true 가 적용된 메소드를 호출할 경우 문제가 발생할 수 있다.