[SpringFramework] - @Transactional을 왜 붙이는걸까?

Gates·2022년 6월 20일
1

SpringFramework

목록 보기
1/1

서론

우리는 스프링 프레임워크로 어플리케이션을 개발할 때 거의 항상 DB에 연결해서 데이터를 저장 및 조회합니다. 그러나 데이터를 저장 후 후처리를 진행하던 과정에서 오류가 발생하면 저장된 데이터의 무결성을 어떻게 보장할 수 있을까요? 예를 들어 사용자가 회원가입을 하고 회원가입 한 데이터를 토대로 사용자의 로그를 작성하던 중 오류가 발생하면 이 사용자는 로그가 없는 사용자가 되어 추적이 불가능해질 것입니다. 이는 1:N 관계가 필요한 사용자:사용자 로그 데이터의 관계가 깨져 무결성이 깨지는 결과가 발생할 것입니다. 이를 방지하기 위해 우리는 오늘 스프링 프레임워크의 Transactional 이라는 기능을 알아볼 것입니다.

본론

1. rollbackFor

우리는 스프링 개발을 할 때 url 요청을 받으면 다음과 같은 흐름으로 모듈을 개발합니다.
Controller -> Service -> Dao/Repository
그 중 Service를 보면 메소드에 @Transactional이라는 어노테이션을 확인 할 수 있습니다. 우리는 이 메소드를 왜 쓰는걸까요? 그걸 알기 위해서는 Transactional 어노테이션의 속성을 알아야 합니다. 아래는 Transactional 어노테이션의 소스코드입니다.

package org.springframework.transaction.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.core.annotation.AliasFor;
import org.springframework.transaction.TransactionDefinition;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
	@AliasFor("transactionManager")
	String value() default "";

	@AliasFor("value")
	String transactionManager() default "";

	String[] label() default {};

	Propagation propagation() default Propagation.REQUIRED;

	Isolation isolation() default Isolation.DEFAULT;

	int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

	String timeoutString() default "";

	boolean readOnly() default false;

	Class<? extends Throwable>[] rollbackFor() default {};

	String[] rollbackForClassName() default {};

	Class<? extends Throwable>[] noRollbackFor() default {};

	String[] noRollbackForClassName() default {};

}

속성이 많습니다. 그렇다는건 Transactional이 많은 기능을 지원한다는 의미일 것입니다.
오늘 우리는 이 많은 기능 중 rollbackFor에 주목할 것입니다. 이 rollbackFor는 예외가 발생 시 트랜잭션을 commit 하지 않고 rollback 하겠다는 의미입니다. 보통은 DB에 데이터를 저장하거나 수정, 삭제하는 트랜잭션을 발생시킬 때 commit 까지 해야 DB에 반영이 된다. 그렇지 않으면 반영이 되지 않고 임시데이터로만 존재하게 됩니다.
그러나 DB에 데이터를 저장하는 중에 오류가 발생하면 데이터의 무결성에 치명적인 오류를 범할 수 있습니다. 이때 rollbackFor를 사용하여 데이터가 마치 없었던 것처럼 rollback을 할 수 있습니다.

2. checkedException? uncheckedException!

다음은 스프링 프레임워크의 Transactional 문서의 rollbackFor 에 대한 설명 중 일부를 발췌한 것입니다.

Defines zero (0) or more exception classes, which must be subclasses of Throwable, indicating which exception types must cause a transaction rollback.
(0개, 또는 그 이상의 Throwable을 상속받는 클래스 중에 트랜잭션 롤백이 발생하는 예외타입의 클래스를 정의한다. )
By default, a transaction will be rolled back on RuntimeException and Error but not on checked exceptions (business exceptions). See DefaultTransactionAttribute.rollbackOn(Throwable) for a detailed explanation.
(기본값으로, 트랜잭션은 RuntimeException과 Error에 대해서만 롤백되고, checked exception에서는 그렇지 않다.
더 자세한 설명은 DefaultTransactionAttribute.RollbackOn 를 참고하길)
출처 : 스프링 프레임워크 Transactional 문서

RuntimeException, Error에 대해서만 롤백이 되고, checked exception에서는 롤백이 되지 않는다고 적혀있습니다. RuntimeException, checked exception, Error의 차이는 무엇일까요? 아래 글은 오라클 자바 문서에서 RuntimeException, Exception 문서의 일부를 발췌한 것입니다.

RuntimeException is the superclass of those exceptions that can be thrown during the normal operation of the Java Virtual Machine.
(RuntimeException은 자바 가상머신의 흔한 명령을 실행하는 동안 발생할 수 있는 예외의 서브클래스이다. )
RuntimeException and its subclasses are unchecked exceptions. Unchecked exceptions do not need to be declared in a method or constructor's throws clause if they can be thrown by the execution of the method or constructor and propagate outside the method or constructor boundary.
(RuntimeException과 그 서브클래스들은 unchecked exception 이다. Unchecked exception은 메소드나 생성자 외부로 전파되거나 실행 중 발생할 수 있는 경우, 메소드나 생성자의 throws 절에 선언할 필요가 없다. )
출처 : 오라클 Java RuntimeException 문서

The class Exception and its subclasses are a form of Throwable that indicates conditions that a reasonable application might want to catch.
(Exception 클래스와 그 서브클래스는 합리적인 응용프로그램이 catch 하길 원하는 상태를 나타내는 Throwable의 한 형태이다. )
The class Exception and any subclasses that are not also subclasses of RuntimeException are checked exceptions. Checked exceptions need to be declared in a method or constructor's throws clause if they can be thrown by the execution of the method or constructor and propagate outside the method or constructor boundary.
(Exception과 RuntimeException의 서브클래스가 아닌 서브클래스는 checked exception이다. checked exception은 메소드나 생성자 외부로 전파되거나 실행 중 발생할 수 있는 경우, 메소드나 생성자의 throws 절에 선언할 필요가 있다. )
출처 : 오라클 Java Exception 문서

위 문서에서는 Java의 RuntimeException 클래스가 unchecked exception이고 RuntimeException과 그 서브클래스를 제외한 Exception은 checked exception이라고 합니다. 즉, 기본적으로 Transactional은 unchecked exception, RuntimeException과 그 서브클래스, 그리고 Error에 대해서 롤백되고, RuntimeException과 그 서브클래스를 제외한 Exception, checked exception에 대해서는 롤백이 되지 않습니다.

그러나 우리는 개발을 하면서 일반적인 Exception의 서브클래스의 예외가 발생할 때도 롤백이 필요할 때가 있을 것입니다. 이런 경우에 rollbackFor 옵션을 다음과 같이 사용하면 됩니다.

@Transactional(rollbackFor=Exception.class)

이렇게 하면 unchecked exception에서도 rollbackFor가 적용됩니다.

설명을 미처 하지 못한 Throwable, Error, Exceptions 의 관계는 아래 사진을 참고하시면 됩니다.

출처 : W3School

3. 테스트

자 그러면 실제로 어떻게 작동하는지 테스트해봅시다.
아래는 순서대로 서비스코드, 테스트 코드입니다.
데이터의 추가 및 조회를 간편하게 하기 위해 JPA를 사용하여 테스트를 진행하였습니다.

@Transactional
public List<Student> findAllStudents() {
  return studentRepository.findAll();
}
 
 @Transactional
public void save() {
  Student student = Student.builder()
          .name("홍길동")
          .grade(1)
          .build();
  studentRepository.save(student);
  }
@Test
void TransactionalTest() {
  studentService.save();
  studentService.findAllStudents()
          .forEach(System.out::println);
  }
}

아래는 실행 결과입니다.

id : 1
name : 홍길동
grade : 1

제대로 저장되어 조회까지 되는 것을 알 수 있습니다.
그럼 여기서 save() 메소드에서 NullPointerException을 발생시켜보도록 하겟습니다.

  @Transactional
  public void save() {
    Student student = Student.builder()
            .name("홍길동")
            .grade(1)
            .build();

    studentRepository.save(student);
    throw new NullPointerException("NPE 발생");
  }
  @Test
  void TransactionalTest() {
    try {
      studentService.save();
    } catch(Throwable throwable) {
      System.out.println(throwable.getMessage());
    } finally {
      System.out.println("학생 출력");
      studentService
              .findAllStudents()
              .forEach(System.out::println);
    }
  }

아래는 실행 결과입니다.

NPE 발생
학생 출력

학생을 저장하고 NullPointerException이 발생하여 저장된 학생이 한 명도 없습니다. 이는 Transactional이 제대로 작동한 것입니다.

결론

우리는 오늘 데이터를 저장하는 동안 무결성이 깨질 수 있음을 확인했고, 이를 방지할 수 있는 Transactional의 rollbackFor 기능을 알아보았습니다. Transactional의 기능은 위에 있는 소스코드를 봤듯이 정말 많고, 앞으로도 그 기능들에 대해 세세히 알아볼 것입니다.
이번에는 공식문서에서 발췌를 많이 했는데, 앞으로 제가 작성하는 글에서는 공식 문서를 자주 발췌할 것입니다. 이것은 공식문서를 읽는 것을 습관화하기 위한 목적일 뿐만 아니라 어떤 기능인지 정확하게 파악하기 위한 것입니다. 여러분도 자신이 사용하는 프레임워크나 라이브러리에 대해서 자세히 알고 싶다면 공식문서를 참고하는 것이 가장 좋습니다.

저는 새로운 프레임워크, 라이브러리를 사용할 때 항상 공식문서를 먼저 찾아봅니다. 어떤 기능이 있는지, 어떻게 작동하는지 등을 알아보기 위해서입니다. 또한, 공식문서를 찾아보는 것은 정말 재미있는 일이 아닐 수 없습니다. 내가 사용하던 프레임워크의 새로운 기능을 찾아냈을 때의 기쁨, 실제로 적용해보면 어떻게 될까 에 대한 기대 등이 느껴집니다. 저도 이 Transactional의 기능들을 하나 하나 알아가면서 느낀 흥분을 아직도 생생히 기억합니다.

이제 마무리하겠습니다. 읽어주셔서 감사합니다.

profile
어제보다 성장한 개발자의 DEBUG 로그

0개의 댓글