본문 바로가기
개발/스프링

@Transactional 어노테이션

by 카펀 2022. 7. 3.

Spring을 이용해 백엔드 개발을 하다 보면, Service 단에서 @Transactional 이라는 어노테이션을 볼 수 있습니다.

오늘은 @Transactional 어노테이션에 대해 다루어 보겠습니다.

 

0. 배경

1. @Transactional 어노테이션이란?

2. @Transactional 세부 옵션

0. 배경

회사에서 개발을 하면서, 화면 상에서 특정 항목 추가, 수정, 삭제를 하는 경우를 만들게 되었습니다.

앞단에서의 형식은 grid로 되어 있고, 저장 버튼을 클릭하면 각 row마다 상태가 "CREATE", "INSERT", "DELETE", "" 네 가지 값 중 하나가 담긴 상태로 Controller 단계로 넘어갑니다.

Controller에서는 이를 List<HashMap<String, String> > 형태로 받고, 이를 Service로 넘겨 줍니다.

Service에서는 이 List의 각 원소를 확인해 봅니다.

각 HashMap의 CRUD 상태값이 "CREATE" 라면 INSERT 쿼리, "UPDATE" 라면 UPDATE 쿼리, "DELETE" 라면 DELETE 쿼리를 거치도록 Dao에 전달합니다.

 

이렇게 개발을 해 놓고 나서, 이를 테스트 코드를 통해 검증을 하고 싶어졌습니다. Service 단에서 적절한 데이터셋을 파라미터로 받았다면, INSERT, UPDATE, DELETE 쿼리가 정상적으로 실행되는지 검증하는 테스트 코드를 작성하려고 했습니다.

하지만 저 쿼리가 실행되는 DB는 실제로 개발 환경에서 사용하는 DB이기 때문에, 테스트의 실행 여부가 실제 DB 내의 데이터에 영향을 미치면 안됩니다.

즉, DB에 가해진 변경점이 commit 되면 안 된다는 의미입니다.

이를 Spring에서 지원해 주는 기능이 있는지 찾아보다가, @Transactional 어노테이션에 대해 알아보게 되었습니다.

 

1. @Transactional 어노테이션이란?

@Transactional

@Transactional 어노테이션을 확인해 보면, 위와 같은 내용이 나타납니다.

뜻을 확인해 보면 아래와 같습니다.

 

개별 메서드 또는 클라스에 대한 트랜잭션 특성을 설명합니다.

이 어노테이션이 클라스 수준에서 선언되면, 선언 클라스 및 하위 클라스의 모든 메서드에 기본값으로 적용됩니다. 상위 클라스에는 적용되지 않으므로, 상속된 메소드는 로컬에서 재정의되어야 subclass-level 어노테이션에 참여할 수 있습니다. 방법 가시성 제약 조건 (method visibility constraints)에 대한 자세한 내용은 참조 매뉴얼의 트랜잭션 관리 (Transaction Management) 섹션을 참조하십시오.

이 어노테이션 유형은 일반적으로 Spring의 org.springframework.transaction.interceptor.RuleBasedTransactionAttribute 클라스와 비교되며, 실제로 AnnotationTransactionAttributeSource는 Spring의 트랜잭션 지원 코드가 어노테이션을 알 필요가 없도록 자동으로 데이터를 후자 클라스로 직접 변환합니다.적용되는 사용자 지정 롤백 규칙이 없다면, 트랜잭션은 RuntimeException과 Error에서는 롤백되지만, checked exceptions에서는 롤백되지 않습니다.

이 어노테이션의 속성에 대한 자세한 내용은 TransactionDefinition org.springframework.transaction.interceptor.TransactionAttribute javadocs를 확인해 주십시오.

이 어노테이션은 일반적으로 org.springframework.transaction.PlatformTransactionManager에 의해 관리되는 스레드 바인딩 (thread-bound) 트랜잭션에서 작동하며, 현재 실행 중인 스레드 내의 모든 데이터 액세스 작업에 기여하는 트랜잭션을 노출시킵니다.
Note: 위 사항은 메소드 내에서 새로 시작한 스레드에 전파되지 않습니다.

또는, 이 어노테이션은 thread-local 변수 대신 Reactor context를 사용하는 org.springframework.transaction에 의해 관리되는 반응형 트랜잭션을 정의할 수 있습니다. 따라서 참여하는 모든 데이터 액세스 작업은 동일한 반응형 파이프라인에서 동일한 원자로 컨텍스트 내에서 실행해야 합니다. 따라서, 참여하는 모든 데이터 액세스 작업은 같은 reactive pipeline 내의 Reactor context 내에서 실행되어야 합니다.

(직접 번역하였습니다. 제가 번역은 잘 못 해서... 문맥상 어색한 번역이 많네요 ㅠ)

 

트랜잭션이란, DB의 상태를 변경하는 작업을 통틀어 일컫는 말입니다. 기본적인 CRUD (INSERT, SELECT, UPDATE, DELETE)가 이에 포함됩니다.

DB의 트랜잭션에는 크게 4가지 isolation level (고립 단계)이 있는데, 자세한 내용은 여기를 참고해 주세요.

 

Spring의 @Transactional 어노테이션은, 이렇게 DB의 상태를 변경하는 작업에 대한 설정을 손쉽게 하도록 도와 줍니다.

작업 시 begin, commit을 자동으로 수행해 주고, 예외가 발생한다면 rollback 역시 자동으로 해 줍니다.

 

제가 작성한 테스트 코드는 테스트가 끝난 후에 rollback을 자동으로 해 주는 것이 목표입니다.

따라서 @Transactional을 잘 사용하면 이를 쉽게 구현할 수 있습니다.

 

@Transactional은 Spring 내의 클라스, 메소드, 인터페이스 위에 붙여서 사용합니다.

즉 어디에 붙여도 동작은 하지만, 보통은 Service 단에 붙여서 사용하는 편입니다.

정석적인 MVC 구조에서는, @Controller는 데이터 정합성에는 관여하지 않습니다. 따라서 Controller에 붙여서 사용해도 동작은 하겠지만, 유지보수 하기에 좋은 구조가 아니게 되는 것이죠. (출처: StackOverFlow)

 

아래와 같이 신규 회원의 가입을 처리하는 Service가 있다고 해 봅시다.

 

@Transactional
public class MemberService {

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    /**
     * 회원 가입
     *
     * @param member
     * @return
     */
    public Long join(Member member) {
        memberRepository.save(member);
        return member.getId();
    }
}

member 형태의 인자를 받아서 repository의 save로 전달합니다.

이 Service 클라스에 @Transactional 어노테이션을 붙여 놓았습니다.

 

다음으로, 이 Service를 검증하기 위한 테스트 코드를 작성합니다.

@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {

    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;


    @Test
    void 회원가입() {
        //given
        Member member = new Member();
        member.setName("spring");

        //when
        Long saveId = memberService.join(member);

        //then
        Member findMember = memberService.findOne(saveId).get();
        assertEquals(member.getName(),findMember.getName());
    }

테스트 클라스 MemberServiceIntegrationTest 위에도 마찬가지로 @Transactional 어노테이션을 붙입니다.

회원가입 테스트는 신규 member를 만들고, join을 통해 DB에 insert 합니다. 이후, 얻은 ID를 이용하여 name을 조회하고 (select), 이 값이 앞에서 insert 한 값과 일치하는지 확인합니다.

즉, 테스트가 완료된 후에는 DB에 'name = "spring"'인 값이 하나 insert 되어있게 됩니다.

하지만 Service 클라스와 테스트 클라스에 모두 @Transactional 어노테이션이 붙어 있기 때문에, 테스트가 완료되면서 DB에 가해진 변경점은 commit 되지 않고 rollback되게 됩니다.

(참고: 유사한 질문에 대한 Inflearn 김영한 님의 답변)

 

2. @Transactional 세부 옵션

@Transactional은 위에서 사용한 것처럼 기본 설정 상태로 그냥 사용할 수도 있지만, 제대로 사용하려면 구체적인 옵션에 대해서도 이해해야 합니다.

아래와 같은 옵션이 존재합니다.

  1. isolation
  2. propagation
  3. noRollBackFor
  4. rollBackFor
  5. timeout
  6. readOnly

1. isolation

지정한 트랜잭션의 isolation level (격리 단계)을 지정합니다.

@Transactional(isolation=Isolation.DEFAULT)
public class MemberService() {

}
  • default: 해당 DB의 기본 Isolation level을 따른다.
  • read_uncomitted
  • read_comitted
  • repeatable_read
  • serializable

2. propagation (전파 속성)

@Transactional(propagation=Propagation.REQUIRED)
public class MemberService() {

}
  • default - required: 이미 진행 중인 트랜잭션이 있다면, 해당 트랜잭션의 속성을 따릅니다. 그렇지 않다면, 새로운 트랜잭션을 생성합니다.)
  • requires_new: 항상 새로운 트랜잭션을 생성합니다. 이미 진행 중인 트랜잭션이 있다면, 해당 트랜잭션을 잠시 일시정지 하고, 생성한 트랜잭션을 우선하여 진행합니다.
  • support: 이미 실행 중인 트랜잭션이 있다면, 해당 트랜잭션 속성을 따릅니다. 실행 중인 트랜잭션이 없다면, 트랜잭션을 설정하지 않는다.
  • not_support: 이미 진행 중인 트랜잭션이 있다면, 보류합니다. 트랜잭션 없이 작업을 수행합니다.
  • mandatory: 이미 진행 중인 트랜잭션이 없다면, Exception을 발생시킵니다. 트랜잭션이 있다면, 작업을 수행합니다.
  • never: 트랜잭션이 진행 중이라면, Exception을 발생시킵니다. 진행 중이지 않다면, 작업을 수행합니다.
  • nested: 진행 중인 트랜잭션이 있다면, 중첩된 트랜잭션이 실행됩니다. 진행 중인 트랜잭션이 없다면, required와 동일하게 동작합니다.

3. noRollbackFor (예외 무시)

지정한 예외가 발생하는 경우, rollback 처리를 하지 않습니다.

@Transactional(noRollbackFor=Exception.class)
public class MemberService() {

}

 

4. rollbackFor (예외 추가)

지정한 예외가 발생하는 경우, rollback 처리합니다.

@Transactional(rollbackFor=Exception.class)
public class MemberService() {

}

default: Unchecked Exception, Error 발생 시 rollback

 

5. timeout (시간 지정)

지정한 시간 내에 트랜잭션이 완료되지 않으면, rollback 처리합니다. -1일 경우, timeout을 처리하지 않습니다.

@Transactional(timeout=10)
public class MemberService() {

}

default: -1

 

6. readOnly (읽기 전용)

INSERT, UPDATE, DELETE 쿼리가 실행될 경우 Exception을 발생시킵니다.

@Transactional(readonly = true)
public class MemberService() {

}

default: false

 

위 내용을 알아 두고, @Transactional을 사용할 때, 필요한 옵션을 적재적소에 사용하면 보다 완성도 있는 구조를 완성할 수 있습니다.

제가 작성한 테스트 코드의 경우, 다른 옵션은 따로 지정하지 않아도 되며, timeout 만 어느 정도로 지정해 주면 좋을 듯 합니다.

댓글