[SpringData] Service에서 @Transactional 올바르게 사용하기

Ogu·2024년 1월 7일
0

SpringData

목록 보기
2/3

최근 여러 책과 깃허브의 다른 코드들을 참고하며 사이드 프로젝트를 진행하던 중, @Transactional 이 어디에는 붙어있고, 어디에는 빠져있고, 또 어떤 곳에는 전부 붙어있고.. 혼란이 왔습니다. 제대로 된 판단이 서지 않아 개념 이해의 필요성을 느끼게 되었습니다.

Service 레이어에서 사용하는 @Transactional의 역할에 대해 정리해보겠습니다.

📕 트랜잭션

트랜잭션은 데이터베이스의 상태 변화를 위해 수행하는 작업의 단위입니다.
비즈니스 로직 도중 어떠한 이유로 중단된다면, 그동안의 쿼리들을 모두 롤백해야 합니다. 이때 트랜잭션을 사용합니다.

📕 @Transaction 어노테이션

@Transactional 은 클래스 및 개별 메서드에 붙일 수 있습니다.
class 레벨에서의 어노테이션은 선언된 클래스와 그 하위 클래스 내부에 있는 모든 메서드에 적용됩니다.

적용 우선순위

  1. 클래서의 메서드
  2. 클래스
  3. 인터페이스 메서드
  4. 인터페이스

해당 메서드를 실행할 때 스프링은 트랜잭션을 begin하는데, 메서드가 정상적으로 종료되면 트랜잭션을 commit하고, 예외가 발생하면 트랜잭션을 rollback합니다.

🌱 @Transaction 옵션 - ACID

isolation : 격리성

그 트랜잭션 성질 ACID에서 I부분으로, 격리 단계(isolation level)을 지정합니다.
default값은 해당 DB의 기본 isolation level을 따르며, 아래 4단계의 격리 수준이 있고, 아래로 갈수록 트랜잭션간 고립 정도가 높아지고, 성능이 떨어지는 것이 일반적입니다.

  • 💡 read_uncomitted
    어떤 트랜잭션의 변경내용이 commit이나 rollback과 상관 없이 다른 트랙잭션에서 보여짐
  • 💡 read_comitted
    어떤 트랜잭션의 변경 내용이 commit되어야만 다른 트랜잭션에서 조회 가능
  • 💡 repeatable_read
    트랜잭션이 시작되기 전에 커밋된 내용에 대해서만 조회 가능
  • 💡 serializable
    가장 엄격한 격리 수준, 읽기 작업에도 공유 잠금을 하므로 동시에 다른 트랜잭션에서 해당 레코드를 변경할 수 없음.

일반적인 서비스에서는 READ_COMITTED(mysql)나 REPEATALE_READ(mysql)중 하나를 사용합니다.

propagation : 전파규칙

동작 도중 다른 트랜잭션을 호출 시, 어떻게 할 것인지 전파 옵션을 지정합니다.

  • 💡 REQUIRED (default)
    활성 트랜잭션이 있는지 확인하고, 아무것도 없으면 새 트랜잭션 생성

  • 💡 SUPPORTS
    활성 트랜잭션이 있는지 확인하고, 있으면 기존 트랜잭션 따름. 없으면 트랜잭션 설정 없이 실행

  • 💡 MANDATORY
    활성 트랜잭션이 있으면 사용하고, 없으면 예외 발생
    독립적으로 트랜잭션을 진행하면 안 되는 경우 사용

  • 💡 NEVER
    - 잭션이 진행중인 경우, Exception 발생
    - 잭션을 사용하지 않도록 제어

  • 💡 NOT_SUPPORTED
    현재 트랜잭션이 존재하면 트랜잭션을 일시 중단하고 랜잭션 없이 비즈니스 로직 수행

  • 💡 REQUIRES_NEW
    - 항상 새로운 트랜잭션 생성
    - 현재 트랜잭션이 존재하는 경우, 현재 트랜잭션을 일시 중단하고 새 트랜잭션을 생성

  • 💡 NESTED
    트랜잭션이 존재하는지 확인하고 존재하는 경우 저장점을 표시
    - 비즈니스 로직 실행에서 예외가 발생하면 트랜잭션이 이 저장 지점으로 롤백
    - 활성 트랜잭션이 없으면 REQUIRED 처럼 작동
    출처: https://data-make.tistory.com/738 [Data Makes Our Future:티스토리]

readOnly : 읽기 전용 모드

트랜잭션을 읽기 전용으로 설정하며, default는 false 입니다.
INSERT, UPDATE, DELETE 쿼리가 실행되면 Exception을 발생시키므로, 보통 조회 메서드에 사용합니다.

noRollbackFor : 해당 예외 발생시 롤백안함

특정 예외 발생 시 rollback이 동작하지 않도록 설정합니다.

rollbackFor : 해당 예외 발생시 롤백함

특정 예외 발생 시 rollback이 동작하도록 설정합니다.

timeout : 타임아웃 설정 가능

지정한 시간 내에 메서드 수행이 완료되지 않으면 rollback 처리 합니다.
(default: -1로 timeout 처리 x)

value : 트랜잭션 매니저 설정

🌱 더티 체킹(Dirty Checking)이란?

update 문에서 자주 보이는 개념인 Dirty Checking은 무엇일까요?

JPA에서는 값을 갱신할 때 update라는 키워드를 따로 제공하지 않습니다.
엔티티 객체의 값을 변경하는 로직만 작성하죠.

아래 update문을 postman에서 다음과 같이 테스트해보겠습니다.

@Transactional
    public Article update(long id, UpdateArticleRequest request) {
        Article article = articleRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("not found: " + id));

        article.update(request.getTitle(), request.getContent());

        return article;
    }

update메서드는 다음과 같습니다.

스프링 부트에서 쿼리를 어떻게 날렸는지 확인해볼까요?

우리는 save() 메서드를 따로 실행하지 않았음에도 update 쿼리가 날려졌습니다.
바로 Dirty Checking 덕분입니다.

영속성 컨텍스트 상태가 유지되는 상황에서 객체의 값을 변경하고 트랜잭션이 끝날 때 JPA에서는 더티 체크(Dirty Check) 라고 하는 변경 감지를 수행합니다. 이후 변경 사항이 있는 모든 엔티티 객체를 DB에 자동 반영합니다.

변경 감지의 기준은 최조 조회 상태로, JPA에서는 엔티티를 조회하면 그 조회 상태에 대해 스냅샷을 남깁니다. 이후 트랜잭션이 끝나는 시점에 해당 스냅샷과 비교해 변경점이 감지되면 Update 쿼리를 날립니다.

물론, 이 변경 감지의 대상은 영속성 컨텍스트가 관리하는 엔티티에만 해당합니다.
detached(준영속), New(비영속) 상태의 엔티티는 감지 대상이 아니어서 값을 변경해도 반영되지 않습니다.

🤔 변경 부분만 update - @DynamicUpdate

Dirty Checking으로 생성되는 Update 쿼리는 기본적으로 모든 필드를 업데이트 합니다.
하지만 필드의 개수가 많을 경우, 이러한 전체 필드 update는 부담스러울 수 있습니다.
이러한 경우에는 @DynamicUpdate 어노테이션을 붙여 변경 필드만 반영할 수 있습니다.

📕 Service에서 @Transactional을 처리하는 이유

비즈니스 로직은 보통 여러 repository를 호출합니다. 만약, 해당 비즈니스 로직에 문제가 발생했을 경우, 해당 비즈니스 로직과 관련된 모든 부분을 롤백해야 하기 때문에 비즈니스 로직의 시작점인 Service에 트랙잭션을 사용합니다.

📕 Test코드 작성에 @Transactional을 사용하는 이유

테스트는 대게 여러번 수행됩니다. 기존 데이터가 누적되면 다음 테스트에 영향을 줄 수 있으므로, 테스트 이후 저장된 데이터를 모두 삭제합니다.

이를 위해 테스트 클래스에 @Transactional 을 붙여주게 되면, DB에 가해진 변경 사항을 commit하지 않고 rollback해줍니다. 따라서 테스트를 동일한 환경에서 반복할 수 있습니다.

참고

profile
私はゲームと日本が好きなBackend Developer志望生のOguです🐤🐤

0개의 댓글