Spring Transaction에 대한 기본 개념

공병주(Chris)·2022년 10월 9일
8

Spring Transaction에 대한 기본 개념

@Transactional 어노테이션을 통해서 Transaction을 사용하고 있음에도, Transaction에 대해 정확하게 짚지 못했습니다. 우아한테크코스 레벨 4 미션에서 Transaction에 대한 개념을 정립할 수 있는 기회가 생겨 Transaction에 대해 알아보았습니다.

Transaction이란?

Transaction(트랜잭션)은 쪼갤 수 없는 여러 작업들을 논리적으로 묶은 최소 단위로 묶은 것입니다.

Transaction의 특징

원자성(Atomicity)

트랙잭션의 연산은 DB에 모두 반영되던지, 모두 반영되지 않아야 한다. 하나라도 실패한다면 앞서 성공한 것들을원상 복구 시켜야한다.

일관성(Consistency)

트랜잭션의 작업 처리 결과는 항상 일관성 있어야 한다.

독립성(Isolation)

어떤 트랜잭션도 다른 트랜잭션의 작업에 끼어들 수 없다.

지속성(Durability)

트랜잭션이 완료된다면, 결과는 영구적이어야 한다.

트랜잭션의 원자성을 보장하는 방법

트랜잭션은 로직을 수행하고 모든 로직이 성공적으로 수행되었을 경우에는 모든 결과를 DB에 일괄적으로 commit 하고, 하나라도 실패한다면 모든 작업을 원상 복구(rollback) 시킵니다.

아래와 같이 try문 안에 원자성을 보장해야 하는 로직을 작성하고, try 문 안에서 예외가 발생했을 경우에 모든 것을 rollback 시키는 방법으로 구현할 수 있습니다.

public void sendMoney(Long senderId, Long receiverId, Long value) {
		Connection connection = dataSource.getConnection();
		connection.setAutoCommit(false);

		try {
        // 한번에 처리되어야 하는 로직
				connection.commit();	
		} catch(SQLException e) {
				connection.rollback();
				throw new RemittanceException();
		}
}

Transaction과 AOP

위와 같이 Transaction에 관한 처리를 해줄 수 있지만, 위의 방식에는 문제가 있습니다.

  1. Transactionan의 원자성을 보장해주기 위한 코드가 비즈니스 로직과 함께 존재합니다. 따라서, 개발자는 핵심 비지니스 로직에 집중할 수 없습니다.

  2. Transaction에 관한 connection 관련 코드와 try~catch 코드가 중복 된다.
    Transaction을 처리해주어야 하는 곳이 여러 곳이라면 해당 try~catch를 반복적으로 작성해줘야 합니다.

    여기서 생각할 수 있는 개념이 AOP(Aspect Oriented Programming)로, 관점 지향 프로그래밍이라는 뜻입니다.

관점 지향 프로그래밍이란, 횡단 관심사에 집중하는 것입니다. 사실 이 말만 들었을 때는 이해가 잘 되지 않습니다.

위와 같이 계좌 이체, 입출금, 이자 계산이라는 기능이 있습니다. 세 기능은 로깅, 트랜잭션, 보안에 대한 처리를 해주어야합니다. 비즈니스 로직을 제외하고 공통적으로 가지는 부가 기능을 따로 분리하여 관리하는 것이 관점 지향 프로그래밍입니다.

Spring에서 사용하는 @Transactional 어노테이션도 AOP의 개념이 적용된 것 입니다. 트랜잭션을 적용해야 하는 메서드 혹은 클래스에 @Transactional 어노테이션만 작성해주면 됩니다.

@Service
public class PostService {

    @Transactional
    public Long addPost(Long boardId, NewPostRequest newPostRequest, AuthInfo authInfo) {
        // 새로운 게시글을 등록하는 비즈니스 로직만을 작성
    }

    @Transactional
    public void updatePost(Long postId, PostUpdateRequest postUpdateRequest, AuthInfo authInfo) {
        // 게시글을 수정하는 비즈니스 로직만을 작성
    }

AOP 개념이 적용된 Transaction 처리에서는 아래와 같은 장점이 있습니다.

  1. 메서드에는 서비스의 비즈니스 로직만을 작성할 수 있기 때문에, 비즈니스 로직에 집중된 코드를 작성할 수 있습니다.

  2. 반복적인 Transaction 처리에 대해, @Transactional 어노테이션을 통해 중복된 트랜잭션 처리 코드를 제거할 수 있습니다.

Spring의 Transaction 동작 방식에 대한 기본 개념

Spring에서는 @Transactional 어노테이션을 통해서 쉽고 깔끔하게 트랜잭션 처리를 해줄 수 있습니다.

하지만, 위 어노테이션이 어떻게 동작함으로써 트랜잭션을 보장해주는지 궁금합니다.

Proxy

스프링은 기본적으로 트랜잭션을 Proxy를 통해서 처리합니다.

실제로 PostService의 addPost 메서드를 요청하면, 동작 전의 코드로는 위와 같이 보입니다.

하지만, 실제로 런타임에는 아래와 같이 동작합니다.

스프링이 어플리케이션이 동작할 때, Transaction 전후 처리를 하는 Proxy를 동적으로 생성해줍니다.

@Transaction이 선언된 Class(Target)에 대해 Target을 참조하는 Proxy를 스프링이 생성합니다. Target의 트랜잭션 처리가 필요한 요청은 Proxy에게 요청되고 Proxy가 Transaction 전후 처리를 하는 방식입니다.

class PostServiceTest extends ServiceTest {

    @Autowired
    private PostService postService;
    
    @DisplayName("익명으로 글을 작성할 수 있다.")
    @Test
    void addPost_Anonymous() {
        NewPostRequest newPostRequest = new NewPostRequest("제목", "본문", true, Collections.emptyList());

        Long postId = postService.addPost(FREE_BOARD_ID, newPostRequest, AUTH_INFO);
        Post actual = postRepository.findById(postId).orElseThrow();

        assertAll(
                () -> assertThat(actual.getTitle()).isEqualTo(newPostRequest.getTitle()),
                () -> assertThat(actual.getContent()).isEqualTo(newPostRequest.getContent()),
                () -> assertThat(actual.getMember().getId()).isEqualTo(member.getId()),
                () -> assertThat(actual.getNickname()).isNotEqualTo(actual.getMember().getNickname()),
                () -> assertThat(actual.getCreatedAt()).isNotNull(),
                () -> assertThat(actual.getPostBoards().get(0).getBoard().getTitle()).isNotNull()
        );
    }
}

실제로 @Transaction이 존재하는 객체에 대해 디버깅을 해보면 아래와 같이 Proxy 객체가 할당되어 있는 것을 확인할 수 있습니다. 실제로 PostService가 아닌, PostServiceProxy에게 먼저 요청되는 것입니다.

JDK Dynamic Proxy와 CGLIB

주제에서 조금 벗어나서, 위의 PostService에 대한 Proxy 객체의 타입을 보면 CGLIB라는 단어가 보입니다. 이는 무엇일까요?

스프링에서 AOP를 구현하는 방식은 JDK Dynamic ProxyCGLIB가 있습니다.

JDK Dynamic Proxy

Reflection 기술을 통해서 동적으로 Proxy 객체를 생성하는 방식입니다. JDK Dynamic Proxy는 Target 객체가 Interface를 구현해야 한다는 제약이 있습니다.

CGLIB Proxy

바이트 코드를 조작하는 기술로 Proxy 객체를 생성하는 방식입니다. CGLIB는 Target 객체가 Interface를 구현하지 않아도 Proxy를 생성할 수 있습니다. Proxy를 Target의 서브 클래스로 생성함으로써 DI를 가능하게 합니다.

Spring Boot는 기본적으로 Proxy 생성 전략을 CGLIB로 채택하고 있습니다. JDK Dynamic Proxy는 Interface가 필수적이고 성능 이슈가 존재하는 Reflection 기술을 사용하기 때문에 CGLIB가 채택되지 않았나 하는 개인적인 생각입니다.

둘의 차이는 추후에 조금 더 자세히 알아봐야 할 것 같습니다. 궁금하신 분들은 아래의 테코톡을 보시는 것을 추천합니다

https://www.youtube.com/watch?v=MFckVKrJLRQ

Transaction 경계 설정

선언적 트랜잭션

@Service
public class PostService {

    @Transactional
    public Long addPost(Long boardId, NewPostRequest newPostRequest, AuthInfo authInfo) {
        // 새로운 게시글을 등록하는 비즈니스 로직만을 작성
    }
}

@Transactional 어노테이션과 같은 선언적 트랜잭션을 사용한다면, Target 객체의 메서드가 호출되는 전 후로 트랜잭션의 경계가 설정됩니다. 따라서, 개발자가 따로 신경 쓸 필요도 없고 경계에 대해 따로 control 할 수 있는 것이 없다고 생각합니다. 실제 서비스 개발에서 가장 많이 쓰이는 방식이라고 생각합니다.

PlatformTransactionManager 사용

PlatformTransactionManager는 스프링 트랙잭션 구조의 중심이 되는 인터페이스입니다. 해당 인터페이스의 구현체는 DataSourceTransactionManager, HibernateTransactionManager, JpaTransactionManager 등으로 Spring Boot를 사용하면, Boot가 DI를 해줍니다.

public interface PlatformTransactionManager extends TransactionManager {

    TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
            throws TransactionException;

	  void commit(TransactionStatus status) throws TransactionException;

	  void rollback(TransactionStatus status) throws TransactionException;

}

PlatformTransactionManager를 사용하면, 아래와 같이 트랜잭션의 시작과 끝을 개발자가 직접 지정할 수 있습니다.

public class PostServiceProxy implements PostService {

    private final PlatformTransactionManager transactionManager;
    private final PostService postService;

    public TxUserService(PlatformTransactionManager transactionManager, PostService postService) {
        this.transactionManager = transactionManager;
        this.postService = postService; // 할당되는 PostService는 PostService의 구현체인 PostServiceImpl
    }

    @Override
    public Long addPost(Long boardId, NewPostRequest newPostRequest, AuthInfo authInfo) {
        TransactionStatus transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            postService.addPost(boardId, newPostRequest, authInfo);
            transactionManager.commit(transactionStatus);
        } catch (DataAccessException e) {
            transactionManager.rollback(transactionStatus);
            throw new DataAccessException();
        }
    }

PlatformTransactionManager의 getTransaction 메서드가 호출되면 Transaction이 시작되면서 TransactionStatus를 반환합니다. TransactionStatus는 트랜잭션에 대한 상태 정보와 관련한 인터페이스입니다. 위 객체를 토대로 commit과 rollback이 진행됩니다.

이걸 어디에 쓰지?

서비스를 개발하면서 대부분 @Transactional을 사용하여 Transaction을 처리했기 때문에, PlatformTransactionManager를 사용해본 적이 없습니다. 그래서 이를 어디에 쓸 수 있을지 생각해보았습니다.

Service 레이어에 대한 테스트를 아래와 같이 작성하는 경우가 있습니다. 테스트의 전후에 Transaction의 경계를 설정한 것입니다. 따라서, 실제로 Test를 해야하는 메서드의 Transaction은 무시가 됩니다. 100%의 테스트가 진행된 것은 아니라고 생각합니다.

@DisplayName("게시글 수정 기능")
@Test
@Transactional
void updatePost() {
    // given
    Long postId = postRepository.save(post).getId();
    PostUpdateRequest postUpdateRequest = new PostUpdateRequest("변경된 제목", "변경된 본문", Collections.emptyList());

    // when
    postService.updatePost(postId, postUpdateRequest, AUTH_INFO);

    // then
    Post foundPost = postRepository.findById(postId)
            .orElseThrow(PostNotFoundException::new);
    assertAll(
            () -> assertThat(foundPost.getTitle()).isEqualTo("변경된 제목"),
            () -> assertThat(foundPost.getContent()).isEqualTo("변경된 본문"),
            () -> assertThat(foundPost.isModified()).isTrue()
    );
}

위와 같은 경우에 @Transactional 어노테이션을 제거하고 PlatformTransactionManager를 이용해 given과 then의 Transaction을 여닫아 줄 수 있을 것이라고 생각합니다. 그렇다면, 검증하려는 메서드(updatePost)에 대한 테스트가 더 정확히 이루어질 것이라고 생각합니다.

@Transactional 사용시 주의점

https://velog.io/@byeongju/Transactional이-왜-안되지

@Transactional을 사용하면서 겪었던 문제입니다. 참고하시면 좋을 것 같습니다!

스프링의 트랜잭션에 대한 기본 개념에 대해 알아보았습니다.

트랜잭션은 전파 레벨, 격리 수준 등등 정말 많은 개념들이 존재하는데요. 앞으로 차근차근 알아보면 좋을 것 같습니다!

참고 자료

https://steady-coding.tistory.com/610

https://flowarc.tistory.com/entry/Spring-트랜잭션에-대해-알아보자

https://springsource.tistory.com/127

https://jypthemiracle.medium.com/spring-transaction-관리에-대한-메모-f391fd2885b4

https://minkukjo.github.io/framework/2021/05/23/Spring/

1개의 댓글

comment-user-thumbnail
2024년 4월 9일

음... Dynamic proxy... 어렵네요 ㅠㅠ

답글 달기