catsridingCATSRIDING|OCEANWAVES
Dev

Spring 트랜잭션 아키텍처 파고들기

jynn@catsriding.com
Apr 15, 2024
Published byJynn
999
Spring 트랜잭션 아키텍처 파고들기

Transaction Management in Spring

Spring Framework를 사용하는 주요한 이유 중 하나로, 공식 문서에서는 트랜잭션 관리의 편의성을 강조하고 있습니다. 이는 개발자들이 저수준의 API를 직접 다루는 것을 피하고 대신 훨씬 편리하게 트랜잭션 로직을 구현하도록 지원함으로써 실현됩니다.

특히, @Transactional 어노테이션을 이용한 선언적 트랜잭션 관리를 통해 개발자들은 전파 전략, 롤백 설정, 읽기 전용 설정 등과 같은 고급 트랜잭션 기능을 쉽게 구성할 수 있습니다. 이런 추상화 과정은 트랜잭션 관리를 간소화하고 데이터베이스 트랜잭션 처리를 효율적이고 간편하게 만들지만, 결국 견고하고 효율적인 코드를 작성하기 위해서는 추상화 뒤의 내부 원리 이해는 필수적입니다.

Confronted Challenges

JDBC 및 DataSource 추상화의 도입으로 인해 여러 문제점이 해결되었으나, 비즈니스 로직을 처리하는 애플리케이션 계층과 데이터 접근을 관리하는 인프라 계층에서는 여전히 미해결된 이슈들이 남아있었습니다.

Challenges in Transaction Processing

비즈니스 로직의 결과에 따라 데이터 처리가 결정되는 트랜잭션은 애플리케이션 계층에서 주도하는 것이 효율적입니다. 하지만 트랜잭션을 시작하고 종료하는 로직은 대체적으로 핵심 비즈니스 로직을 둘러싸고 있는 형태로 코드가 작성됩니다.

MemberService.java
@Slf4j
@RequiredArgsConstructor
public class MemberService {

    private final DataSource dataSource;
    private final MemberRepository memberRepository;

    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        Connection connection = dataSource.getConnection();
        try {
            connection.setAutoCommit(false);
            businessLogic(connection, fromId, toId, money);
            connection.commit();
        } catch (Exception e) {
            connection.rollback();
            throw new IllegalStateException(e);
        } finally {
            release(connection);
        }
    }

    private void businessLogic(Connection connection, String fromId, String toId, int money) throws SQLException {...}
    private void validation(Member toMember) {...}
    private void release(Connection connection) {...}
}

이 구조에는 핵심 비즈니스 로직 보다도 트랜잭션을 처리하는 코드가 더 방대하다는 점과 그 이외에도 다음과 같이 여러 문제점들이 있습니다:

  • 특정 데이터베이스 접근 기술에 의존: 서비스 계층에서 JDBC 코드를 분리하려는 시도가 있었지만, 트랜잭션 처리는 여전히 JDBC에 의존하고 있습니다. 이로 인해 서비스 계층의 확장성에 제약이 발생합니다.
  • 트랜잭션 동기화 문제: 여러 비즈니스 로직들은 동일한 데이터베이스 커넥션을 공유하게 됩니다. 이로 인해 트랜잭션을 유지할 필요 없는 기능도 반드시 같은 커넥션을 사용하게되어 문제가 발생합니다.
  • 트랜잭션 적용의 반복: 각각의 기능에서 핵심 비즈니스 로직을 감싸는 try catch finally 구조를 지속적으로 반복해야 합니다.

Redundancy Concerns in JDBC

순수한 JDBC를 사용하여 쿼리를 실행하면 반복적으로 전처리 및 후처리 작업이 필요합니다.

MemberRepository.java
public Member save(Member member) throws SQLException {
    String sql = "insert into member(member_id, money) values (?, ?)";

    Connection connection = null;
    PreparedStatement preparedStatement = null;

    try {
        connection = getConnection();
        preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setString(1, member.getMemberId());
        preparedStatement.setInt(2, member.getMoney());
        preparedStatement.executeUpdate();
        return member;
    } catch (SQLException e) {
        log.error("MemberRepositoryV0.save : [{}]", e.getMessage());
        throw e;
    } finally {
        close(connection, preparedStatement, null);
    }
}

개발자는 아래와 같은 일련의 과정을 반복적으로 코드로 작성해야 합니다:

  1. 커넥션 획득: 데이터베이스와의 연결을 설정합니다. 이 연결은 쿼리의 실행을 가능하게 합니다.
  2. PreparedStatement 준비: SQL 쿼리를 위한 객체를 초기화하고, 쿼리에 필요한 파라미터를 설정합니다.
  3. 결과 매핑: 쿼리의 결과를 어플리케이션에 필요한 형태로 매핑하고 변환합니다.
  4. 사용한 리소스 정리: 쿼리 실행 후 사용되었던 모든 리소스를 정리하고, 영속화된 커넥션을 닫아줍니다.
  5. 예외 발생시 예외 처리: SQL 쿼리의 실행 도중 발생할 수 있는 다양한 예외 상황들을 처리합니다.

이 과정들이 각 쿼리 실행시마다 반복되며, 이로 인해 코드의 중복성이 증가합니다. 이렇게 복잡한 과정들은 실수의 여지를 늘리고, 개발 과정에서 불필요한 리소스를 낭비하게 만들 수 있습니다. 결국, 이는 효율성이 떨어지는 코드를 생성하고, 에러 발생의 위험성을 증가시킵니다.

Exception Leakage Phenomenon

아래 코드는 JDBC 구현 기술에서 발생한 SQLException 예외가 서비스 계층까지 전파된 상황입니다. SQLExceptionChecked Exception 유형에 속해 반드시 예외 처리를 수행해야 합니다. 이 과정을 생략할 경우, 이 예외는 명시적으로 외부로 전달되어야 합니다.

Service.java
import java.sql.SQLException;

@Slf4j
@RequiredArgsConstructor
public class Service {...}

만약에 JDBC 방식에서 JPA와 같은 다른 데이터 접근 기술로 변환하게 된다면, 이전에 JDBC에 의존하던 모든 예외 처리 코드를 수정해야만 합니다. 이는 특히 프로젝트의 규모가 큰 경우, 엄청난 리소스와 시간을 필요로 하게 만듭니다.

Spring Transaction Manager

지금까지 살펴본 세 가지 주요 문제점들을 해결하기 위해, Spring Framework는 트랜잭션 매니저를 도입하였습니다. 이는 데이터 접근 기술에 관계없이 일관된 트랜잭션 관리를 가능하게 하며, 예외 처리를 개선하고, 코드 작성 및 유지 보수의 효율도 향상시킵니다.

Transaction Abstraction

JDBC를 사용하는 경우, 트랜잭션을 적용하기 위해 서비스 계층에서 JDBC 기술에 의존해야 합니다. 그러나 데이터 접근 기술에 따라 트랜잭션의 사용 방식이 다르고 이로 인해 변경 사항이 발생하는 경우 수동으로 모든 코드를 수정해야하는 불편함이 발생할 수 있습니다.

//  JDBC Transaction
public void jdbc() throws SQLException {
    Connection connection = dataSource.getConnection();
    try {
        connection.setAutoCommit(false);
        //  business logic start
        businessLogic();
        //  business logic end
        connection.commit();
    } catch (Exception e) {
        connection.rollback();
    } finally {
        release(connection);
    }
}

//  JPA Transaction
public void jpa() throws SQLException {
    EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("jpabook");
    EntityManager entityManager = entityManagerFactory.createEntityManager();
    EntityTransaction entityTransaction = entityManager.getTransaction();

    try {
        entityTransaction.begin();
        //  business logic start
        businessLogic();
        //  business logic end
        entityTransaction.commit();
    } catch (Exception e) {
        entityTransaction.rollback();
    } finally {
        entityTransaction.close();
    }
    entityManagerFactory.close();
}

위 예제에서 볼 수 있듯이, JDBC와 JPA는 각각 다른 방식으로 트랜잭션을 처리합니다. 이는 한 기술에서 다른 기술로 전환할 때 불필요한 코드 변경을 초래할 수 있습니다.

이런 문제를 해결하기 위해 추상화(Abstraction) 기법을 사용합니다. 추상화는 특정 구현의 세부사항을 감추고 일반화된 인터페이스만을 통해 기능에 접근할 수 있게 하는 원칙을 의미합니다.

이처럼 추상화를 데이터 접근 기술에 적용하게 되면, 세부적인 변경 사항들을 직접 다룰 필요가 없게 됩니다. 새로운 데이터 접근 기술이 도입되거나 기존 기술이 업데이트되더라도, 해당 변경 사항은 구현 레벨에서 처리되므로 애플리케이션 코드의 전반적인 구조는 크게 변동이 없습니다. 이런 접근 방식은 소프트웨어 설계 원칙인 개방-폐쇄 원칙을 철저히 준수하게 합니다.

Spring에서는 트랜잭션 매니저를 통해 이러한 추상화를 지원하고 있습니다.

PlatformTransactionManger.java
package org.springframework.transaction;

public interface PlatformTransactionManager extends TransactionManager {

    TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;

    void commit(TransactionStatus status) throws TransactionException;

    void rollback(TransactionStatus status) throws TransactionException;

}

PlatformTransactionManagerTransactionManager 인터페이스를 확장하여 다음을 제공합니다:

  • getTransaction(): 트랜잭션 전파 전략에 따라 현재 트랜잭션을 반환하거나 새로운 트랜잭션을 생성합니다.
  • commit(): 트랜잭션을 커밋합니다.
  • rollback(): 트랜잭션을 롤백합니다.

PlatformTransactionManager를 애플리케이션 계층에 적용함으로서, 비즈니스 로직은 데이터 접근 기술에 직접적으로 의존하지 않는 구조를 유지할 수 있게 됩니다. 이로써 다양한 트랜잭션 관리 구현체 간의 전환을 더욱 자연스럽게 수행할 수 있게 되며, 이는 시스템의 확장성과 유연성을 크게 증진시킵니다.

transaction-management-in-spring_00.png

Transaction Synchronization

트랜잭션 매니저가 수행하는 또 다른 핵심 역할은 바로 트랜잭션 동기화 문제를 해결하는 것입니다. 이는 복잡한 비즈니스 프로세스에서 계좌 이체나 주문 처리와 같이 작업들이 연이어 일어날 때 매우 중요합니다. 이런 상황에서는 한 프로세스 내에서 발생하는 여러 작업들이 일관성 있게 처리되어야 합니다. 즉, 작업 중 하나라도 실패한다면 그때까지의 모든 데이터 변경사항이 롤백(Rollback)되어야 합니다.

이 요구사항을 구현하기 위해 생각할 수 있는 가장 간단한 방법은 파라미터로 커넥션을 직접 전달하여 공유하는 방식입니다.

MemberService.java
private void businessLogic(
        Connection connection, 
        String fromId, 
        String toId, 
        int money) throws SQLException {
    Member fromMember = memberRepository.findById(connection, fromId);
    Member toMember = memberRepository.findById(connection, toId);

    memberRepository.update(connection, fromId, fromMember.getMoney() - money);
    memberRepository.update(connection, toId, toMember.getMoney() + money);
}

이러한 방식은 매개변수의 추가로 인한 코드의 복잡성 증가, 그리고 커넥션을 전달하는 메서드와 그렇지 않은 메서드를 별도로 관리해야 하는 등의 문제를 야기할 수 있습니다. 이는 코드의 가독성을 저해하며, 유지 보수의 어려움을 늘리는 요인이 될 수 있습니다.

이런 문제점을 처리하기 위해 Spring은 트랜잭션 동기화 매니저를 도입했습니다. 트랜잭션이 시작될 때 획득한 커넥션을 임시 저장소에 보관하고, 필요할 때 꺼내 사용하도록 합니다. 이렇게 되면, 각 메서드에서 커넥션을 직접 관리하지 않아도 되므로 코드의 복잡성을 줄이고 유지 보수를 쉽게 할 수 있습니다.

TransactionSynchronizationManager.java
package org.springframework.transaction.support;

public abstract class TransactionSynchronizationManager {...}

트랜잭션 매니저의 도입으로 복잡성이 감소하고 유지보수가 향상되었습니다. 이제 Spring 트랜잭션 매니저를 통한 트랜잭션 처리 프로세스에 대해 알아보도록 하겠습니다.

Transaction Manager Workflow

Spring의 트랜잭션 매니저를 통한 전반적인 트랜잭션 관리 프로세스는 다음과 같습니다:

transaction-management-in-spring_01.png

  1. 서비스 계층에서 트랜잭션 매니저의 getTransaction()을 호출하여 트랜잭션을 시작합니다.
  2. 트랜잭션 매니저 내부에서 DataSource를 사용해 커넥션을 생성합니다.
  3. 커넥션 수동 커밋 모드로 변경합니다.
  4. 트랜잭션 매니저에서 트랜잭션 동기화 매니저로 커넥션을 보관하도록 요청합니다.
  5. 트랜잭션 동기화 매니저는 쓰레드 로컬에 커넥션을 보관합니다.

transaction-management-in-spring_02.png

  1. 서비스에서 비즈니스 로직을 위해 리포지토리로 데이터 접근 메서드를 호출하게 됩니다.
  2. 그러면 리포지토리 로직에서는 DataSourceUtils.getConnection()을 사용해 트랜잭션 동기화 매니저에 보관된 커넥션을 획득하고,
  3. 획득한 커넥션을 사용해 데이터베이스로 SQL을 전달하여 실행합니다.

transaction-management-in-spring_03.png

  1. 비즈니스 로직이 끝나면 트랜잭션 매니저에서 트랜잭션 종료를 위해 commit or rollback를 호출합니다.
  2. 트랜잭션 매니저는 트랜잭션 동기화 매니저에 보관된 커넥션 획득하고,
  3. 획득한 커넥션을 통해 데이터베이스 트랜잭션 커밋 또는 롤백을 실행합니다.
  4. 마지막으로 사용한 리소스를 정리합니다.
    1. 트랜잭션 동기화 매니저 정리
    2. 획득한 커넥션을 자동 커밋 모드로 변경
    3. 커넥션 종료 또는 커넥션 풀로 반환

파라미터로 커넥션을 전달하는 방식에서 트랜잭션 동기화 매니저에 보관하는 방식으로 변경하면서 코드가 간결해지고, 불필요한 작업을 줄일 수 있게 되었습니다.

MemberService.java
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
    TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

    try {
        businessLogic(fromId, toId, money);
        transactionManager.commit(status);
    } catch (Exception e) {
        transactionManager.rollback(status);
        throw new IllegalStateException(e);
    }
}

private void businessLogic(String fromId, String toId, int money) throws SQLException {
    Member fromMember = memberRepository.findById(fromId);
    Member toMember = memberRepository.findById(toId);

    memberRepository.update(fromId, fromMember.getMoney() - money);
    memberRepository.update(toId, toMember.getMoney() + money);
}

Transaction Templates

Spring의 트랜잭션 매니저는 데이터베이스 커넥션의 획득 및 트랜잭션의 제어를 단순화하여 코드를 간결하게 만드는 데 도움을 줍니다. 하지만 개발자는 여전히 비즈니스 로직의 성공 또는 실패에 따라 트랜잭션을 커밋하거나 롤백하는 코드를 try-catch 블록 내에 작성해야 합니다. 이러한 트랜잭션 관리 코드는 모든 비즈니스 로직에서 반복적으로 사용되는 패턴이며, 이는 개발의 생산성을 제한하며 코드의 가독성을 저해합니다.

MemberService.java
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
	TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

	try {
		businessLogic(fromId, toId, money);
		transactionManager.commit(status);
	} catch (Exception e) {
		transactionManager.rollback(status);
		throw new IllegalStateException(e);
	}
}

반복되는 트랜잭션 관리 코드를 줄이기 위해, Spring은 이에 대응하는 트랜잭션 템플릿을 제공하고 있습니다.

TransactionTemplate.java
package org.springframework.transaction.support;

public class TransactionTemplate extends DefaultTransactionDefinition implements TransactionOperations, InitializingBean {...}

TransactionTemplate은 기존의 try-catch를 사용한 트랜잭션 관리 로직을 간결화하여 코드 중복을 줄이는 것을 목표로 설계되었습니다.

MemberService.java
private final TransactionTemplate transactionTemplate;
private final MemberRepository memberRepository;

public MemberService(
        PlatformTransactionManager transactionManager,
        MemberRepository memberRepository) {
    this.transactionTemplate = new TransactionTemplate(transactionManager);
    this.memberRepository = memberRepository;
}

public void accountTransfer(String fromId, String toId, int money) throws SQLException {
    transactionTemplate.executeWithoutResult(transactionStatus -> {
        try {
            businessLogic(fromId, toId, money);
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        }
    });
}

TransactionTemplate의 도입으로 비즈니스 로직과 트랜잭션 관리 로직의 분리가 가능해져, 코드의 가독성이 크게 향상되었습니다. 트랜잭션 관리 코드가 간결해지니 실제 비즈니스 로직에 더욱 집중하여 작업을 수정하거나 확장할 수 있게 되었습니다.

그러나 완전한 분리는 아직 이루어지지 않았습니다. 서비스 계층은 순수한 비즈니스 로직에만 집중해야 하지만 현재는 기술적인 부분인 트랜잭션 관리 코드가 여전히 포함되어 있습니다. 트랜잭션 관리 코드와 비즈니스 로직을 더욱 체계적으로 분리하여, 서비스 계층에서 비즈니스 로직에만 집중할 수 있도록 개선이 필요합니다.

AOP Based Transactions

지금까지 서비스 계층에서 트랜잭션을 시작하려면, 중요한 비즈니스 로직을 트랜잭션 처리와 관련된 코드로 감싸는 형태로 코드를 작성해야 했습니다. 🍔 이는 코드 중복을 불러오고 관리 부담을 늘리는 결과를 초래합니다. 이러한 문제를 효과적으로 해결하기 위해, 트랜잭션 처리를 위한 AOP 방식을 고려해볼 수 있습니다.

Aspect Oriented Programming
관점 지향 프로그래밍은 애플리케이션 전반에 적용되는 공통 관심사, 즉 횡단 관심사(Cross-Cutting Concerns)를 분리하여 코드의 모듈성을 높이는 프로그래밍 패러다임입니다. 이는 트랜잭션 관리와 같이 여러 기능 간에 공유하는 관심사를 분리함으로써 코드 재사용성을 높이고 관리를 효율화하는 데 도움이 됩니다.

Transaction Proxy

컨트롤러에서 서비스 객체에 요청을 위임하게 되면, 트랜잭션은 비즈니스 로직 수행 전에 시작되며, 로직 완료 후에 종료됩니다.

transaction-management-in-spring_04.png

컨트롤러가 서비스 계층에 요청을 직접 위임하는 대신, 대리인이라는 중간 개체를 통해 간접적으로 요청을 위임하면 기존의 코드를 변경하지 않아도 트랜잭션에 대한 관심사를 분리할 수 있게 됩니다.

transaction-management-in-spring_05.png

이와 같이 중간에서 대리인 역할을 하는 이 객체를 프록시(Proxy)라고 합니다. 프록시 객체는 대상(Target) 객체에 접근하기 이전과 이후에 다양한 보조 작업들을 수행할 수 있습니다.

예를 들어, 외부의 요청을 받는 프록시가 필요한 작업을 처리하고 난 다음, 프록시가 대상 객체를 호출하거나, 대상 객체의 처리 결과에 따라 추가 작업을 수행하거나, 반환 값을 변경하는 등의 일을 할 수 있습니다.

프록시를 도입하면 현재 직면한 문제를 해결할 수 있습니다. 프록시는 트랜잭션을 시작하고, 원래의 서비스 계층에 구현된 비즈니스 로직을 호출합니다. 로직 호출 후에는 요청 처리의 성공 여부에 따라 트랜잭션을 종료합니다. 이러한 방식을 통해 서비스 계층에서 트랜잭션 처리 코드를 완전히 분리할 수 있게 됩니다.

TransactionProxy.java
private MemberService target;

public void logic() {
    TransactionStatus status = transactionManager.getTransaction();
    try {
        target.businessLogic();
        transactionManager.commit(status);
    } catch (Exception e) {
        transactionManager.rollback(status);
        throw new IllegalStateException(e);
    }
}

프록시의 도입으로 서비스 객체는 이제 순수한 비즈니스 로직에 집중할 수 있게 되었습니다:

MemberService.java
@Service
public class MemberService {

    public void businessLogic() {...} 

}

그런데, 이 방식을 적용하려면 각 비즈니스 로직에 대하여 프록시 객체를 만들어야 하는 문제가 있습니다. 이는 코드의 복잡성을 증가시키는 요인입니다. 이 문제를 효과적으로 해결하는 한 가지 방법은 관점 지향 프로그래밍(AOP)을 활용하는 것입니다. Spring Framework에서는 이를 어떻게 적용했는지 살펴보겠습니다.

@Transactional Annotation

Spring AOP의 @Transactional 어노테이션을 활용하면, AOP를 기반으로 동적으로 생성된 프록시 객체를 통해 트랜잭션 처리를 더 편리하게 할 수 있습니다.

Spring에서 트랜잭션의 관리는 일반적으로 다음 두 가지 접근 방식을 따릅니다:

  • 선언적 트랜잭션 관리(Declarative Transaction Management)
    • 이 방식은 트랜잭션을 적용할 대상에 @Transactional 어노테이션을 선언하여 사용합니다.
    • 프로그래밍적 방식에 비해 훨씬 간편하고 실용적이기 때문에 실무에서는 대부분 선언적 방식을 사용하고 있습니다.
  • 프로그래밍적 트랜잭션 관리(Programmatic Transaction Management)
    • 이 방식은 트랜잭션 매니저 또는 트랜잭션 템플릿 등을 직접 제어하여 사용합니다.
    • 단, 이 경우 트랜잭션 처리와 비즈니스 로직이 강하게 결합되는 문제점이 있습니다.

Spring에서 제공하는 이 @Transactional 어노테이션은 기본적으로 프록시 방식의 AOP가 적용되기 때문에, 간편하게 원하는 곳에서 트랜잭션을 시작하고 종료할 수 있으며, 또한 핵심 비즈니스 영역과 트랜잭션을 처리하는 객체를 명확하게 분리할 수 있습니다.

transaction-management-in-spring_06.png

아래와 같이, Spring의 @Transactional 어노테이션을 선언함으로써 개발자는 트랜잭션을 명시적으로 관리하는 코드 없이 비즈니스 로직에 집중할 수 있게 되었습니다.

MemberService.java
@Service
public class MemberService {

    @Transactional
    public void businessLogic() {...}

}
Applying Transactions

개발 과정을 진행하면서, 단일 요청 처리에 있어서 @Transactional 어노테이션을 여러 위치에 선언하는 상황을 종종 경험하게 됩니다.

  • 클래스와 해당 메서드에서 모두 선언되어 있는 경우
  • 인터페이스와 그에 대응하는 구현 객체에서 모두 선언되어 있는 경우
  • 서비스와 관련 리포지토리 객체에서 모두 선언되어 있는 경우

위와 같은 상황에서 이해해야 할 중요한 Spring의 원칙은 더 구체적이고 자세한 범위가 더 높은 우선순위를 갖는다는 것입니다. 이 원칙을 @Transactional 어노테이션에 적용하면, 우선순위는 아래와 같이 결정됩니다:

  • 클래스 < 메서드
  • 인터페이스 < 구현 객체 구현

그리고, 단일 요청이 처리되는 동안 다수의 객체를 거치면서 @Transactional 어노테이션이 반복적으로 등장할 수 있습니다. 이러한 상황에서는, 트랜잭션 전파 규칙이 적용되며, 이를 통해 트랜잭션의 지정된 범위를 벗어나지 않고 안정적인 연산을 유지할 수 있으며 더 효과적으로 제어할 수 있게 됩니다.

Transaction Propagation Behavior

API를 호출하는 클라이언트는 각 쓰레드에서다 커넥션을 확보하고 비즈니스 로직을 수행합니다. 그래서 쓰레드들이 서로 트랜잭션을 공유할 필요 없이 독립적으로 작동할 수 있습니다.

그러나, 하나의 쓰레드 내에서 비즈니스 로직을 수행하는 과정에서 여러 개의 @Transactional 어노테이션을 만날 수 있습니다.

// Service.java
@Transactional
public class Service {

    @Transactional
    public ResponseEntity<?> businessLogic() {
        User user = repository.findById(id);
        ...
    }
}

// Repository.java
@Transactional
public class Repository {

    @Transactional
    public User findById(id) {
        return repository.findById(id);
    }
}

@Transactional 어노테이션으로 선언된 각각의 작업이 독립된 커넥션을 이용해 트랜잭션을 관리하면, 일부 작업이 실패했을 때 이전에 수행한 작업은 이미 커밋된 상태가 될 수 있습니다. 이러한 경우 데이터의 정합성이 깨질 수 있습니다. 그러나 특정 상황에서는, 일부 작업이 실패하더라도 다른 작업들이 커밋되어야 하는 경우도 있습니다. 따라서 트랜잭션 관리 방식은 상황에 따라 달라질 수 있습니다.

위와 같은 상황을 해결하기 위해 Spring은 @Transactional 어노테이션에 다양한 트랜잭션 전파 전파 옵션을 제공하고 있습니다.

Propagation.java
package org.springframework.transaction.annotation;

public enum Propagation {
    REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
    SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
    MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),
    REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
    NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),
    NEVER(TransactionDefinition.PROPAGATION_NEVER),
    NESTED(TransactionDefinition.PROPAGATION_NESTED);
}

이러한 옵션 중 가장 일반적으로 사용되며, @Transactional의 기본 전파 전략 옵션은 바로 REQUIRED입니다. 이 전략은 논리적인 내부와 외부 트랜잭션을 하나의 물리적 트랜잭션으로 취급합니다. 즉, 처음으로 트랜잭션이 시작되는 지점부터 모든 작업이 단일 트랜잭션 내에서 실행됩니다. 만약, 어떤 이유로든 하나의 논리적 트랜잭션에서 롤백이 발생하게 될 경우, 해당 트랜잭션에서 발생한 모든 데이터 변경 작업이 롤백됩니다.

+ --------------------- + ------------------------ + -------------------- + ------- +
| Propagation Behaviors | Outer Transaction Exists | No Outer Transaction | Default |
+ --------------------- + ------------------------ + -------------------- + ------- +
| REQUIRED ✱            | Join                     | Create New           ||
| SUPPORTS              | Join                     | Do Nothing           |         |
| MANDATORY             | Do Nothing               | Throws Excpetion     |         |
| REQUIRES_NEW ✱        | Create New               | Create New           |         |
| NOT_SUPPORTED         | Do Nothing               | Do Nothing           |         |
| NEVER                 | Throws Excpetion         | Do Nothing           |         |
| NESTED                | Create Nested            | Create New           |         |
+ --------------------- + ------------------------ + -------------------- + ------- +

# Outer Transaction : 논리적으로 외부 트랜잭션을 의미
# Inner Transaction : 논리적으로 내부 트랜잭션을 의미
# Join : 외부 트랜잭션이 존재하는 경우 참여
# Create New : 새로운 트랜잭션 생성
# Create Nested : 중첩 트랜잭션 생성 (중첩 트랜잭션은 외부 트랜잭션의 영향을 받지만, 외부에 영향을 주지 않음)
# Throws Exception : IllegalTransactionStateException 예외 발생
# Do Nothing : 아무것도 하지 않음

다음은 REQUIRED 전략의 전반적인 동작 흐름입니다:

transaction-management-in-spring_07.png

  1. getTransaction() 메소드를 호출하여 트랜잭션 매니저가 외부 트랜잭션을 시작합니다.
  2. 이 과정에서 트랜잭션 매니저는 DataSource로부터 커넥션을 생성합니다.
  3. 생성된 커넥션을 setAutoCommit(false)를 사용하여 수동 커밋 모드로 설정합니다. 이 단계는 물리적인 트랜잭션의 시작을 나타냅니다.
  4. 이어서 트랜잭션 매니저는 생성한 커넥션을 트랜잭션 동기화 매니저에 보관합니다.
  5. 트랜잭션 생성 결과는 TransactionStatus에 저장하며, 이 안에는 신규 트랜잭션 여부를 알 수 있는 정보가 포함되어 있습니다. transactionStatus.isNewTransaction() 메소드를 통해 신규 트랜잭션 여부를 확인할 수 있습니다.
  6. 이후 외부 비즈니스 로직이 실행되며, 필요한 커넥션은 트랜잭션 동기화 매니저를 통해 획득하여 사용합니다.
  7. 다음으로, 트랜잭션 매니저가 getTransaction()을 호출하여 내부 트랜잭션을 시작합니다.
  8. 이때 트랜잭션 매니저는 트랜잭션 동기화 매니저를 사용하여 기존 트랜잭션의 존재 여부를 확인합니다.
  9. 이미 기존 트랜잭션이 있으므로, 해당 트랜잭션에 참여하게 됩니다. 이는 실질적으로는 별도의 행동을 하지 않는 것을 의미합니다.
  10. transactionStatus.isNewTransaction()를 사용하여 신규 트랜잭션인지 확인할 수 있습니다. 이 경우에는 신규 트랜잭션이 아니므로 false가 반환됩니다.
  11. 내부 비즈니스 로직이 실행됩니다. 이 과정에서 필요한 커넥션은 트랜잭션 동기화 매니저를 통해 외부 트랜잭션에서 보관된 커넥션을 획득하여 사용하게 됩니다.
  12. 내부 비즈니스 로직이 모두 처리되면, 내부 트랜잭션 커밋을 트랜잭션 매니저가 호출합니다.
  13. 이 상황에서 트랜잭션 매니저는 먼저 신규 트랜잭션인지를 확인하고, 그 결과에 따라 다음 동작을 결정합니다. 내부 트랜잭션이 신규 트랜잭션이 아니기 때문에, 트랜잭션 매니저는 실제 데이터베이스에 바로 커밋을 실행하지 않습니다. 이 단계에서 실제 커밋이 수행된다면, 물리적 트랜잭션이 종료되어 외부 트랜잭션의 변경 사항들이 적절히 반영되지 못하게 됩니다. 따라서, 트랜잭션 매니저에서만 이루어진 이런 커밋을 논리적 커밋(Logical Commit)이라고 부르게 됩니다.
  14. 외부 비즈니스 로직 또한 모두 처리되면, 외부 트랜잭션 커밋이 트랜잭션 매니저에 의해 호출됩니다.
  15. 외부 트랜잭션은 신규 트랜잭션이므로, 트랜잭션 매니저는 실제 데이터베이스에 커밋을 실행합니다. 이렇게 실제 데이터베이스 연결을 통한 커밋을 수행하는 것을 물리적 커밋(Physical Commit)이라고 할 수 있습니다.
  16. 마지막으로, 데이터베이스에 데이터 변경사항이 최종적으로 반영되면서 물리 트랜잭션은 종료됩니다. 이 과정은 물리 트랜잭션의 완전한 종료를 의미합니다.

또한, 두 번째로 가장 많이 사용되는 전파 옵션은 REQUIRES_NEW입니다. 이 옵션은 트랜잭션을 전파하지 않으며 항상 새로운 트랜잭션을 시작합니다. 이를 통해 외부 트랜잭션과 내부 트랜잭션을 명확히 분리할 수 있습니다. 서로 다른 트랜잭션을 사용하므로, 내부 로직에서 예외가 발생하여 실제 물리적인 롤백이 발생하더라도, 외부 로직은 이와 독립적으로 커밋을 진행할 수 있습니다.

다음으로, REQUIRES_NEW 전략을 따르는 일반적인 처리 흐름을 살펴보겠습니다:

transaction-management-in-spring_08.png

  1. getTransaction() 메소드를 호출하여 트랜잭션 매니저가 외부 트랜잭션을 시작합니다.
  2. 트랜잭션 매니저가 DataSource를 통해 커넥션을 생성합니다.
  3. 이 생성된 커넥션을 setAutoCommit(false)를 이용하여 수동 커밋 모드로 설정합니다. 이는 외부 물리 트랜잭션의 시작을 표시합니다.
  4. 트랜잭션 매니저는 생성된 커넥션을 트랜잭션 동기화 매니저에 보관합니다.
  5. TransactionStatus에는 트랜잭션 생성 결과가 저장되며, 이는 transactionStatus.isNewTransaction()를 통해 신규 트랜잭션의 여부를 확인할 수 있습니다.
  6. 외부 비즈니스 로직이 실행됩니다. 필요한 커넥션들은 트랜잭션 동기화 매니저를 통해 획득하게 됩니다.
  7. getTransaction()을 이용하여 내부 트랜잭션을 시작합니다. 이때, REQUIRES_NEW 옵션을 통해 기존 트랜잭션에 참여하지 않고 새로운 트랜잭션을 시작합니다.
  8. DataSource를 통해 트랜잭션 매니저가 새로운 커넥션을 생성합니다.
  9. 이 새롭게 생성된 커넥션은 setAutoCommit(false)를 통해 수동 커밋 모드로 설정됩니다. 이는 내부 물리 트랜잭션의 시작을 나타냅니다.
  10. 새로운 커넥션은 트랜잭션 매니저에 의해 트랜잭션 동기화 매니저에 보관됩니다. 여기서 외부 커넥션들은 내부 트랜잭션이 완료될 때까지 잠시 보류됩니다.
  11. TransactionStatus는 트랜잭션 생성 결과를 반환하며, 내부 트랜잭션 또한 신규 트랜잭션 이므로 transactionStatus.isNewTransaction()의 결과는 true가 됩니다.
  12. 내부 비즈니스 로직이 실행되고, 필요한 커넥션들은 트랜잭션 동기화 매니저에 있는 커넥션들을 획득하여 사용하게 됩니다.
  13. 만약 내부 비즈니스 로직 처리 도중 문제가 발생하여 예외가 발생한다면,
  14. 롤백 요청이 트랜잭션 매니저로 전달됩니다.
  15. 트랜잭션 매니저는 롤백을 요청하기 전에 먼저 신규 트랜잭션 여부를 확인하고, REQUIRES_NEW로 새로 생성된 트랜잭션임을 확인한 다음, 내부 비즈니스 로직에서 변경한 데이터를 롤백하고 내부 트랜잭션을 종료합니다.
  16. 외부 트랜잭션은 내부 비즈니스 로직의 실패 여부와는 무관하게 동작합니다. 커밋 요청을 트랜잭션 매니저로 전달하면,
  17. 트랜잭션 매니저는 신규 트랜잭션의 여부를 확인하고 물리 커밋을 요청합니다. 내부 트랜잭션의 결과에 따라 롤백도 가능하도록 처리할 수 있지만, 이런 경우에는 기본값인 REQUIRED 옵션의 사용이 더 효율적입니다.

이렇게 REQUIRES_NEW 옵션은 하나의 API를 처리하면서 다수의 커넥션을 가져갈 수 있습니다. 그러나 이로 인해 리소스 누수가 발생할 수 있기 때문에 필요한 상황에서만 선택적으로 사용하는 것이 합리적입니다.

Additional Transaction Options

스프링 트랜잭션은 전파 전략 이외에도 다양한 옵션을 제공하고 있습니다.

OptionDescription
value트랜잭션 매니저를 지정하며, 생략시 기본 트랜잭션 매니저를 사용합니다. 둘 이상의 트랜잭션 매니저가 있는 경우 구별하기 위해 이름을 지정할 수 있습니다.
rollbackFor롤백 처리가 필요한 예외 목록을 추가합니다. 체크 예외(Checked Exception)가 발생하더라도 롤백을 설정할 수 있습니다. noRollbackFor 옵션의 반대 개념입니다.
noRollbackFor롤백이 처리되지 않는 예외 목록을 추가합니다. 런타임 예외(Runtime Exception)가 발생해도 롤백되지 않도록 설정할 수 있습니다. rollbackFor 옵션의 반대 개념입니다.
propagation트랜잭션 전파 전략을 설정합니다.
isolation트랜잭션 격리 수준을 지정합니다. DEFAULT, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE의 옵션을 사용할 수 있습니다.
timeout트랜잭션 수행 시간 타임아웃을 지정합니다. 환경에 따라 동작하지 않을 수 있기 때문에 사전에 확인이 필요합니다.
label어노테이션에 있는 값을 직접 읽어 특정 동작을 설정할 수 있는 옵션입니다. 일반적으로 잘 사용되진 않습니다.
readOnly읽기 전용 트랜잭션을 생성합니다. 등록, 수정, 삭제가 불가하며 성능 최적화에 도움이 될 수 있습니다. 드라이버 또는 데이터베이스 엔진에 따라 정상 동작하지 않을 수 있습니다.

다음과 같이 @Transactional 어노테이션에 설정할 수 있는 다양한 속성들이 제공됩니다. 각 속성의 값은 고유한 트랜잭 션 특성과 요건을 정의하는데 사용됩니다. 이러한 속성들은 트랜잭션 관리의 유연성을 높이는 역할을 합니다.

@Transactional.java
@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 Precautions

트랜잭션 AOP를 활용하면서 특별히 주의해야 하는 부분들이 있습니다. 간혹 이를 놓치게 되면 컴파일 에러는 발생하지 않는데 로직이 예상한 대로 동작하지 않고, 디버깅 열심히 해서 결국 찾아내면 개운하기보다는 진만 빠지는 그런 상황을 마주할 수 있습니다.

Calling Through Proxies

@Transactional을 사용하여 트랜잭션 관리를 할 경우, 트랜잭션 AOP가 적용됩니다. 이 경우, Spring은 대상 객체의 프록시를 동적으로 생성하고, 이를 의존성 주입 시 사용합니다. 따라서 대부분의 경우, 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제는 발생하지 않습니다.

그러나, 대상 객체 내부에서 메소드를 호출하는 경우에는 프록시 대신 대상 객체가 직접 호출되는 문제가 발생합니다.

transaction-management-in-spring_09.png

이는 프록시 패턴의 원리와 어긋나는 현상으로, 프록시 패턴은 클라이언트가 객체의 메소드를 직접 호출하는 것이 아니라 중간의 프록시가 호출을 제어하는 것을 기본으로 합니다. 즉, 프록시는 메소드 호출 전후에 추가 작업을 수행하였으나, 대상 객체 내부에서 직접 호출 경우는 이런 중간 처리 과정이 누락되어 의도치 않은 결과를 초래할 수 있습니다.

이런 한계를 극복하기 위한 다양한 방법들이 있습니다. 자기 주입, 지연 조회, 구조 변경 등이 그 예시이며, 실무에서는 주로 별도의 클래스로 분리하는 구조 변경을 통해 프록시 내부 호출 문제를 해결하곤 합니다.

Limitations with Access Modifiers

Spring의 트랜잭션 AOP는 기본적으로 public 메서드에만 트랜잭션을 적용합니다. 이런 제한이 있는 이유는 클래스 레벨에서 @Transactional이 적용될 때, private 메서드 같은 불필요한 곳에 트랜잭션을 과도하게 적용하지 않도록 하기 위함입니다.

@Transactional
public class HelloSpringTransaction {
    public method1();
    method2();
    protected method3();
    private method4();
}

@Transactional 어노테이션이 public이 아닌 메서드에 선언될 경우, 예외가 발생하지 않아 프록시 내부 호출 문제와 유사성을 가지며 디버깅에 어려움이 생깁니다. 이와 같은 미묘한 이슈는 개발을 진행하는 동안 중요한 주의를 요구합니다. 이에 대응하여, IntelliJ IDEA는 개발자에게 이런 문제를 쉽게 감지할 수 있도록 IDE 수준에서 경고 메시지를 제공하는 기능을 내장하고 있습니다. 그렇기 때문에 개발 과정에서 IntelliJ IDEA와 같은 강력한 IDE를 사용하면, 더욱 효과적으로 문제를 파악하고 해결하는데 도움이 됩니다.

Initializing Proxies

Spring 초기화 단계에서는 트랜잭션 AOP가 적용되지 않을 수 있습니다. 예를 들어, @PostConstruct@Transactional을 동시에 사용하면, 트랜잭션은 적용되지 않습니다.

이는 초기화 코드가 먼저 호출되고 그 후에 트랜잭션 AOP가 적용되기 때문입니다.

@PostConstruct
@Transactional
public void init() {
    boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
    assertThat(isActive).isFalse();
}

ApplicationReadyEvent를 사용하여 이 문제를 해결할 수 있습니다. 이 이벤트는 Spring 컨테이너가 완전하게 생성되고 나서 트랜잭션 AOP를 비롯한 모든 설정이 적용된 후에 호출됩니다. 따라서, 이 접근 방식을 사용하면 프록시 초기화 문제가 해결됩니다.

@EventListener(ApplicationReadyEvent.class)
@Transactional
public void init() {
    boolean actualTransactionActive = TransactionSynchronizationManager.isActualTransactionActive();
    assertThat(actualTransactionActive).isTrue();
}

이렇게 하면, ApplicationReadyEvent가 발생하는 순간에 트랜잭션 활성화 여부를 확인하는 것이 가능해집니다.

Timing of Applying Options

애플리케이션의 요구 사항에 따라 트랜잭션 전파 전략을 설정하게 되는데, 여기서 유의해야 할 점은 트랜잭션 옵션에 대한 것입니다. isolation, timeout, readOnly와 같은 옵션들은 트랜잭션이 처음 시작될 때만 적용됩니다. 다시 말해, REQUIRED 또는 REQUIRES_NEW 전략을 통해 새로운 트랜잭션이 시작되는 그 시점에만 이러한 옵션들이 적용됩니다. 이미 진행 중인 트랜잭션에 이러한 옵션들을 변경하려 해도 반영되지 않습니다.

Transaction Exceptions

데이터 접근 기술에 따라서 예외도 각각 별도로 제공하고 있습니다. JDBC 도입 배경에서도 알 수 있듯이, 예외 처리도 특정 기술에 종속적이게 되면 애플리케이션 확장이나 다른 기술로 변경하는 것이 매우 어려워집니다.

그래서 Spring에서는 데이터 접근 기술들의 예외를 추상화해서 제공하고 있습니다.

Abstracting Exceptions in Spring

Spring은 데이터 접근 계층의 여러 가지 예외를 통합한 DataAccessException을 제공함으로써 특정 기술에 종속되지 않은 일관된 예외 계층을 제공합니다.

DataAccessException.java
public abstract class DataAccessException extends NestedRuntimeException {

    public DataAccessException(String msg) {
        super(msg);
    }
    
    public DataAccessException(@Nullable String msg, @Nullable Throwable cause) {
        super(msg, cause);
    }
}

DataAccessException은 크게 두 가지로 분류됩니다.

public abstract class NonTransientDataAccessException extends DataAccessException {...}
public abstract class TransientDataAccessException extends DataAccessException {...}
  • NonTransient: 이 분류에 속하는 예외는 재시도 하더라도 계속 실패할 것으로 예상되는 상황을 나타냅니다. SQL 문법 오류, 데이터베이스 제약 조건 위반 등이 포함됩니다.
  • Transient: 이 분류에 속하는 예외는 임시적인 문제로 재시도하면 성공할 가능성이 있는 상황을 나타냅니다. 쿼리 타임아웃, 락 관련 오류 등이 있습니다.

transaction-management-in-spring_10.png

데이터베이스 접근 기술의 다양한 예외들을 분류하고 추상화하기 위해서는 각 DB 드라이버가 제공하는 예외들을 분석하고 Spring의 예외 계층으로 변환하는 역할을 하는 예외 변환기가 필수적입니다.

SQLExceptionTranslator.java
@FunctionalInterface
public interface SQLExceptionTranslator {

    @Nullable
    DataAccessException translate(String task, @Nullable String sql, SQLException ex);

}

SQLExceptionTranslator는 특정 에러 코드를 기반으로 Spring의 데이터 접근 예외로 변환합니다. 이 변환 과정에서 참조하는 에러 코드 정보는 sql-error-codes.xml 파일에 정의되어 있습니다. 이 파일은 데이터베이스 벤더별로 정의된 에러 코드를 표시하며, 각 벤더에서 발생 가능한 특정 예외 유형의 에러 코드 값을 담고 있습니다.

sql-error-codes.xml 파일은 Spring JDBC Framework 포함되어 있으며, 이 파일은 여러 <bean> 정의를 포함하는 XML 파일입니다. 각각의 <bean>은 특정 데이터베이스 벤더와 연결되어 있고, 벤더별로 발생 가능한 주요 예외 유형에 관련된 에러 코드를 속성으로 가지고 있습니다. 예를 들어, MySQL 데이터베이스를 위한 에러 코드를 정의하는 <bean> 부분은 다음과 같습니다:

sql-error-codes.xml
<bean id="MySQL" class="org.springframework.jdbc.support.SQLErrorCodes">
  <property name="databaseProductNames">
    <list>
      <value>MySQL</value>
      <value>MariaDB</value>
    </list>
  </property>
  <property name="badSqlGrammarCodes">
    <value>1054,1064,1146</value>
  </property>
  <property name="duplicateKeyCodes">
    <value>1062</value>
  </property>
  <property name="dataIntegrityViolationCodes">
    <value>630,839,840,893,1169,1215,1216,1217,1364,1451,1452,1557</value>
  </property>
  <property name="dataAccessResourceFailureCodes">
    <value>1</value>
  </property>
  <property name="cannotAcquireLockCodes">
    <value>1205,3572</value>
  </property>
  <property name="deadlockLoserCodes">
    <value>1213</value>
  </property>
</bean>

이런 방식으로 데이터 접근 예외를 추상화하면, 특정 데이터베이스 드라이버에 종속되는 문제를 해결할 수 있습니다. 물론, 이 과정에서 Spring Framework에 대한 종속성이 증가하겠지만, 실제로 이는 큰 문제로 간주되지 않습니다.

Strategies for Exception Rollbacks

Spring 공식 문서에서는 예외 발생 시 이에 대응하는 트랜잭션의 기본 롤백 정책을 다음과 같이 안내하고 있습니다.

Rolling back a declarative transaction
The recommended way to indicate to the Spring Framework’s transaction infrastructure that a transaction’s work is to be rolled back is to throw an Exception from code that is currently executing in the context of a transaction. The Spring Framework’s transaction infrastructure code will catch any unhandled Exception as it bubbles up the call stack, and make a determination whether to mark the transaction for rollback.

In its default configuration, the Spring Framework’s transaction infrastructure code only marks a transaction for rollback in the case of runtime, unchecked exceptions; that is, when the thrown exception is an instance or subclass of RuntimeException. ( Errors will also - by default - result in a rollback). Checked exceptions that are thrown from a transactional method do not result in rollback in the default configuration.

해당 문서에 의하면, Java의 주요 예외 계층인 런타임 예외(Unchecked Exception)와 체크 예외(Checked Exception)가 Spring 트랜잭션 예외 처리 정책에 사용되고 있습니다.

TypePolicyDescription
Unchecked Exception롤백(Rollback)Spring은 런타임 예외에 대해 애플리케이션 수준에서 해결 불가능한 심각한 문제로 받아들입니다. 그래서 이러한 예외가 발생하면, 그동안의 모든 변동 사항을 롤백하여 문제 발생 전 상태로 되돌립니다.
Checked Exception커밋(Commit)Spring은 체크 예외를 비즈니스 로직에서 발생 가능한 예외로 취급합니다. 이는 개발자가 비즈니스 수준에서 이를 처리할 수 있다는 가정하에, 이러한 예외가 발생했더라도 현재의 작업 진행상황에는 아무런 문제가 없다고 판단하고 변동 사항을 커밋하는 방식입니다.

그러나 개발하다 보면, 이 기본 정책에 적합하지 않은 경우도 있습니다. 이런 상황에서는 @Transactional 어노테이션의 rollbackFor 또는 noRollbackFor 속성을 활용하여 비즈니스 요구사항에 맞게 정책을 재정의할 수 있습니다.

Wisdom is the Conqueror of Fortune

지금까지 트랜잭션 관리와 관련한 다양한 이슈를 경험하고, 그 이슈들을 체계적으로 해결해 나가는 과정에서 Spring Framework의 @Transactional에 도달했습니다. Spring의 트랜잭션 관리 기능이 없었던 시절을 떠올려보며, 표준화와 추상화의 중요성을 다시 한 번 깨달았습니다.

기술의 도입 배경과 세부적인 동작 원리와 대해 이해하는 것은 정말 중요한 것 같습니다. 이런 지식은 예상치 못한 문제나 새로운 도전에 직면했을 때 효과적인 해결책을 제시하고, 기술을 최대한 효율적으로 활용하는 데 큰 도움이 될 것입니다. 🪄


  • Spring
  • Architecture