[Spring] 트랜잭션 관리 (@Transactional, 동적 프록시, JDK Dynamic Proxy vs GCLIB)

반응형
728x90
반응형

트랜잭션 관리자

스프링 프레임워크에서는 트랜잭션 적용을 비교적 쉽게 구현하도록 도와주는 기능이 있다. 예시로, 트랜잭션 관리를 위한 코드를 비즈니스 로직에서 분리하기 위한 구조나 다른 트랜잭션을 투명하게 처리할 수 있게 하는 API 등이 있다.

 

스프링 트랜잭션 처리의 중심 인터페이스는 PlatformTransactionManager 이다. 이 인터페이스는 트랜잭션 처리에 필요한 API를 제공하며 개발자가 API를 호출하는 것으로 트랜잭션을 수행할 수 있다.

PlatformTransactionManager는 트랜잭션 관리의 구현 방식을 추상화하기 위한 인터페이스이기 때문에 개발자는 서로 다른 종류의 트랜잭션을 사용하더라도 각각의 차이점을 의식할 필요 없이 같은 API로 조작할 수 있다.

public interface PlatformTransactionManager {

    TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;

    void commit(TransactionStatus status) throws TransactionException;

    void rollback(TransactionStatus status) throws TransactionException;
}

 

 

스프링 프레임워크는 다양한 환경과 제품에 대응하는 PlatformTransactionManager의 구현 클래스를 제공한다.

 

https://gngsn.tistory.com/152

 

클래스명 설명
DataSourceTransactionManager JDBC 및 Mybatis 등의 JDBC 기반 라이브러리로 데이터베이스에 접근하는 경우에 이용한다.
HibernateTransactionManager 하이버네이트를 이용해 데이터베이스에 접근하는 경우에 이용한다.
JpaTransactionManager JPA로 데이터베이스에 접근하는 경우에 이용한다.
JtaTransactionManager JTA에서 트랜잭션을 관리하는 경우에 이용한다.
WebLoginJtaTransactionManager 애플리케이션 서버인 웹로직(WebLogic)의 JTA에서 트랜잭션을 관리하는 경우에 이용한다.
WebSphereUowTransactionManager 애플리케이션 서버인 웹스피어(WebSphere)의 JTA에서 트랜잭션을 관리하는 경우에 이용한다.

 

 

트랜잭션 관리자 정의

스프링 프레임워크의 트랜잭션 관리자를 사용할 때는 다음의 2가지를 작업해야한다.

  • PlatformTransactionManager의 빈을 정의한다.
  • 트랜잭션을 관리해야하는 메서드를 정의한다.

 

로컬 트랜잭션을 이용하는 경우

  • 로컬 트랜잭션 : 단일 데이터 저장소에 대한 트랜잭션으로 일반적으로 자주 사용되는 트랜잭션
  • 로컬 트랜잭션을 사용하는 경우 JDBC API를 호출하고 트랜잭션 제어를 수행하는 DataSourceTransactionManager를 사용한다. 
  • ex) 단일 데이터 저장소에 대한 여러 조작을 하나의 논리적 단위로 처리하고 싶을때 사용한다.  

PlatformTransactionManager의 빈 ID를 'transactionManager'로 사용하는 것이 좋다. 이유는, 스프링 프레임워크에서 기본적으로 트랜잭션 관리자의 빈 ID를 'transactionManager'로 가정하고있기 때문이다. 

 

▶ XML 기반 설정 방식을 이용한 빈 정의

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionMager">
    <property name="dataSource" ref="dataSource" />
</bean>

<!-- @Transactional 애너테이션을 사용하는 경우 -->
<tx:annotation-driven />

 

1) PlatformTransactionManager로서 DataSourceTransactionManager를 지정한다.

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionMager">
    ...
</bean>

 

2) dataSource 프로퍼티에 설정 완료된 데이터 소스의 빈을 지정한다.

<property name="dataSource" ref="dataSource" />

 

3) 애노테이션 트랜잭션 제어를 활성화하기 위해 <tx:annotation-driven> 요소를 추가한다.

만약 어노테이션 사용을 하지 않는다면, <tx:annotation-driven> 요소의 정의 자체가 필요없다.

<!-- @Transactional 애너테이션을 사용하는 경우 -->
<tx:annotation-driven />

 

4) 'transactionManager'가 아닌 다른 ID를 사용할 경우

<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionMager">
    <property name="dataSource" ref="dataSource" />
</bean>

<!-- @Transactional 애너테이션을 사용하는 경우 -->
<tx:annotation-driven transaction-manager="txManager" />

 

 

글로벌 트랜잭션을 이용하는 경우

  • 글로벌 트랜잭션 : 여러 데이터 저장소에 걸쳐서 적용되는 트랜잭션
  • 예를들어, 여러 데이터베이스를 사용하되, 각 데이터베이스에서 각각의 조작을 수행하고 그 조작들을 하나의 트랜잭션으로 묶어 모두 성공하거나, 모두 실패한 것으로 처리해야하는 경우에는 앞에서 설명한 로컬 트랜잭션을 사용할 수 없다.
JTA (Java Transaction API)

JAVA EE 사양으로 표준화돼 있고, 애플리케이션 서버가 JTA의 구현 클래스를 제공한다. JTA를 스프링 프레임워크에서 사용하려면 PlatformTransactionManager의 구현 클래스로 JtaTransactionManager를 선택하면 된다. 몇년 전에, 여러 데이터베이스를 적용해야했던 적이 있었는데, 그때 JTA를 사용했던 기억이 있다. 

 

XML 기반 설정 방식을 이용한 빈 정의

<tx:jta-transaction-manager />

 

위처럼 지정하면 애플리케이션 서버에서 제공하는 JtaTransactionManager를 빈 형태로 사용할 수 있다.

 

 

선언적 트랜잭션

  • 선언적 트랜잭션 : 미리 선언된 룰에 따라 트랜잭션을 제어하는 방법

선언전 트랜잭션의 장점은 정해진 룰을 준수함으로써 트랜잭션의 시작, 커밋, 롤백 등의 일반적인 처리를 비즈니스 로직 안에 기술할 필요가 없다는 점이다.

 

스프링 프레임워크는 선언적 트랜잭션을 이용하는 방법으로 아래 2가지를 제공한다.

  • @Transactional
  • XML 설정을 이용

 

@Transactional을 이용한 선언적 트랜잭션

public 메서드에 추가하는 것으로 대상 메서드의 시작 종료에 맞춰 트랜잭션을 시작, 커밋할 수 있다. 

스프링 프레임워크의 기본 상태에서는 메서드 안의 처리에서 데이터 접근 예외와 같은 비검사 예외(unchecked exception)가 발생해서 메서드 안에 처리가 중단될 때 트랜잭션이 자동으로 롤백된다.

 

 

트랜잭션 제어에 필요한 정보 (@Transactional 애노테이션 속성)
속성명 설명
value 여러 트랜잭션 관리자를 이용하는 경우 이용하는 트랜잭션 관리자의 qualifier를 지정한다.
기본 트랜잭션 관리자를 이용하는 경우에는 생략할 수 있다.
transactionManager value의 별칭
propagation 트랜잭션의 전파방식 지정
isolation 트랜잭션의 격리수준 지정
timeout 트랜잭션 제한 시간(초)를 지정
readOnly 트랜잭션의 읽기 전용 플래그를 지정한다. 기본값은 false(읽기 전용이 아님)다.
rollbackFor 여기에 지정한 예외가 발생하면 트랜잭션을 롤백시킨다.
예외 클래스명을 여러개 나열할 수 있으며, ","로 구분한다.
따로 지정해주지 않았다면 RuntimeException과 같은 비검사 예외가 발생할때 트랜잭션이 롤백된다.
rollbackForClassName 여기에 지정한 예외가 발생하면 트랜잭션을 롤백시킨다.
예외 이름을 여러개 나열할 수 있으며 ","로 구분한다.
noRollbackFor 여기에 지정한 예외가 발생하더라도 롤백시키지 않는다.
예외 클래스를 여러개 나열할 수 있으며 ","로 구분한다.
noRollbackForClassName 여기에 지정한 예외가 발생하더라도 트랜잭션을 롤백시키지 않는다.
예외 이름을 여러개 나열할 수 있으며 ","로 구분한다.

 

사용 예시
설정 예 설명
@Transactional 기본 트랜잭션 관리자를 기본 설정에서 이용한다.
@Transactional(readOnly = true, timeout = 60) 기본 트랜잭션 관리자를 읽기 전용 트랜잭션으로 이용한다.
제한 시간은 60초로 변경한다.
@Transactional("tx1") "tx1" 트랜잭션 관리자를 이용한다.
@Transactional(value = "tx2", propagation = Propagation.REQUIRESNEW) "tx2" 트랜잭션 관리자를, 전파 방식을 REQUIRES_NEW로 사용한다.

 

 

설정 클래스

설정 클래스의 포인트는 @EnableTransactionManagement 어노테이션을 Configuration Class에 부여하는 것이다. 이렇게 하면 @Transactional 어노테이션을 사용한 트랜잭션 제어가 가능해진다.

@Configuration
@EnableTransactionManagement
public class TransactionManagerConfig {
    @Autowired // DI 컨테이녀가 의존성 주입을 할 수 있도록 설정
    DataSource dataSource;
    
    @Bean // 클래스 빈 정의
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource);
    }
}

 

 

명시적 트랜잭션

  • 명시적 트랜잭션 : 커밋이나 롤백과 같은 트랜잭션 처리를 소스코드에 직접 명시적으로 기술하는 방법

메서드 단위보다도 더 작은 단위로 트랜잭션을 제어하고 싶거나 선언적 트랜잭션으로는 표현하가 이려운 섬세한 트랜잭션 제어가 필요할때 이 방법을 사용한다. 

 

스프링 프레임워크에서는 명시적 트랜잭션을 이용하는 방법으로 아래 2가지 방법을 제공한다.

  • PlatformTransactionManager
  • TransactionTemplate

 

 

PlatformTransactionManager를 이용한 명시적 트랜잭션 제어

@Service
public class RoomServiceImpl implements RoomService {
  @Autowired
  PlatformTransactionManager txManager;
  
  @Autowired
  JdbcRoomDao roomDao;

  @Override
  public void insertRoom(Room room) {
    DefaultTransactionDefinition def = new DefaultTransactionDefinition();
    def.setName("InsertRoomWitEquipmentTx");
    def.setReadOnly(false);
    def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
    
    TransactionStatus status = txManager.getTransaction(def);
    
    try {
      roomDao.insertRoom(room);
      List<Equipment> equipmentList = room.getEquipmentList();
      for(Equipment item : equipmentList) {
          roomDao.insertEquipment(item);
      }
    } catch(Exception e) {
      txManager.rollback(status);
      throw new DataAccessException("error occurred by insert room" e) {};
    }
    
    txManager.commit(status);
  }
}

 

1) DefaultTransactionDefinition 객체를 설정한다.

DefaultTransactionDefinition def = new DefaultTransactionDefinition();

 

2) 트랜잭션에 이름을 설정한다.

def.setName("InsertRoomWitEquipmentTx");

 

3) DefaultTransactionDefinition객체의 인수로 TransactionManager의 메서드를 수행한다.

이 이후의 처리가 트랜잭션 범위가 된다.

TransactionStatus status = txManager.getTransaction(def);

 

4) 트랜잭션 롤백이 수행된다.

txManager.rollback(status);

 

5) 트랜잭션 커밋이 수행된다.

txManager.commit(status);

 

 

 

TransactionTemplate을 활용한 명시적 트랜잭션 제어

트랜잭션 제어 작업을 TransactionCallback 인터페이스가 제공하는 메서드에 구현하고 TransactionTemplate의 execute 메서드에 인수로 전달한다. TrnasactionTemplate은 jdbcTemplate과 같은 방식을 사용하고 있기 때문에 애플리케이션 개발자는 필수 처리 로직만 구현하면 된다.

@Service
public class RoomServiceImpl implements RoomService {
  @Autowired
  TransactionTemplate transactionTemplate;

  @Autowired
  JdbcRoomDao roomDao;

  @Override
  public void insertRoom(final Room room) {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
      @Override
      protected void doInTransactionWithoutResult(TransactionStatus status) {
        roomDao.insertRoom(room);
        
        List<Equipment> equipmentList = room.getEquipmentList();
        
        for(Equipment item : equipmentList)
          roomDao.insertEquipment(item);
      }
    })
  }
}

 

doInTransactionWithoutResult 메서드가 정상적으로 종료되면, transactionTemplate이 자동으로 트랜잭션을 커밋한다. 또한 데이터 등록 과정에서 예외가 발생하는 경우에는 TransactionTemplate이 트랜잭션을 롤백한다.

 

 

빈 정의

아래와 같이 TransactionTemplate의 자바 기반 설정 방식을 이용한 빈 정의를 할 수 있다.

@Configuration
public class AppConfig {

  @Bean
  public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
    TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
    transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITED);
    transactionTemplate.setTimeout(30);
    return transactionTemplate;
  }
}

 

 

트랜잭션 격리 수준

  • 트랜잭션 격리 수준 : 참조하는 데이터나 변경한 데이터를 다른 트랜잭션으로부터 어떻게 격리할 것인지를 결정한다.

 

격리 수준은 여러 트랜잭션의 동시 실행과 데이터의 일관성과 깊이 연관되어있다. 

  • 더티 리드(Dirty Read) : 하나의 트랜잭션에서 작업이 완료되지 않았음에도 다른 트랜잭션에서 볼수 있는 현상
  • 반복되지 않은 읽기(Unrepeatable Read) : 하나의 트랜잭션에서 같은 쿼리를 두번 이상 수행할때, 똑같은 쿼리임에도 다른 결과를 볼 수 있게 되는 현상
  • 팬텀 읽기 (Phantom Read) : 다른 트랜젹션에서 수행한 작업에 의해 레코드가 안보였다 보였다 하는 현상
트랜잭션 격리 수준 설명
DEFAULT 사용하는 데이터베이스의 기본 격리 수준을 이용한다.
READ_UNCOMMITTED 더티 리드(Dirty Read), 반복되지않은 읽기(Unrepeatable REad), 팬텀 읽기가 발생한다.
이 격리 수준은 커밋되지 않은 변경 데이터를 다른 트랜잭션에서 참조하는 것을 허용한다. 
만약 변경 데이터가 롤백된 경우 다음 트랜잭션에서 무효한 데이터를 조회하게된다.
READ_COMMITTED 더티 리드를 방지하지만 반복되지 않은 읽기, 팬텀 읽기는 발생한다.
이 격리 수준은 커밋되지 않은 변경 데이터를 다른 트랜잭션에서 참조하는 것을 금지한다.
REPEATABLE_READ 더티 리드, 반복되지 않은 읽기를 방지하지만 팬텀 읽기는 발생한다.
SERIALIZABLE 더티 리드, 반복되지 않은 읽기, 팬텀 읽기를 방지한다.

 

 

트랜잭션 전파 방식

  • 트랜잭션 전파 방식 : 트랜잭션 경계에서 트랜잭션에 참여하는 방법을 결정한다.

 

트랜잭션 경계와 전파 방식

트랜잭션 전파 방식을 의식해야하는 경우는 트랜잭션 경계가 중첩될때다. 트랜잭션 관리 대상이 되는 메서드 안에서 또 다른 트랜잭션 관리 대상이 되는 메서드를 호출할 경우에는 트랜잭션의 전파 방식을 고려해야한다.

 

https://velog.io/@minwoorich/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EC%A0%84%ED%8C%8C-%EC%9D%B4%EB%A1%A0

 

트랜잭션 전파방식의 기본값은 'REQUIRED'이다.

만약 전파방식을 변경하고 싶다면, @Transactional의 propagation 속성을 변경하면 된다.

전파 방식 설명
REQUIRED 이미 만들어진 트랜잭션이 존재한다면 해당 트랜잭션 관리 범위 안에 함께 들어간다.
만약 이미 만들어진 트랜잭션이 존재하지 않는다면 새로운 트랜잭션을 만든다.
REQUIRES_NEW 이미 만들어진 트랜잭션 범위안에 들어가지 않고 반드시 새로운 트랜잭션을 만든다.
만약 이미 만들어진 트랜잭션이 아직 종료되지 않았다면 새로운 트랜잭션은 보류 상태가 되어 이전 트랜잭션이 끝나는 것을 기다려야한다.
MANDATORY 이미 만들어진 트랜잭션 범위 안에 들어가야한다.
만약 기존에 만들어진 트랜잭션이 없다면 예외가 발생한다.
NEVER 트랜잭션 관리를 하지 않는다.
만약 이미 만들어진 트랜잭션이 있다면 예외가 발생한다.
NOT_SUPPORTED 트랜잭션을 관리하지 않는다.
만약 이미 만들어진 트랜잭션이 있다면 이전 트랜잭션이 끝나는 것을 기다려야한다.
SUPPORTS 이미 만들어진 트랜잭션이 있다면 그 범위 안에 들어가고, 만약 트랜잭션이 없다면 트랜잭션 관리를 하지 않는다.
NESTED REQUIRED와 마찬가지로 현재 트랜잭션이 존재하지 않으면 새로운 트랜잭션을 만들고 이미 존재하는 경우에는 이미 만들어진 것을 계속 이용하지만, NESTED가 적용된 구간은 중첩된 트랜잭션처럼 취급한다.
NESTED 구간 안에서 롤백이 발생한 경우 NESTED 구간 안의 처리 내용은 모두 롤백되지만 NESTED 구간 밖에서 실행된 처리 내용은 롤백되지 않는다. 
단, 부모 트랜잭션에서 롤백되면 NESTED 구간의 트랜잭션은 모두 롤백된다.

 

 

 

AOP

@Transactional의 동작 원리는 Spring AOP와 관련이 있으므로, 이전에 공부했던 인프런의 스프링 핵심 원리 - 고급편의 동적 프록시 내용을 정리해본다. 해당 강의에서 AOP, 동적 프록시 등 전반적인 내용을 학습할 수 있으므로 수강을 추천한다.

 

 

ProxyFactory

스프링은 유사한 구체적인 기술들이 있을때, 그것들을 통합해서 일관성있게 접근할 수 있고, 더욱 편리하게 사용할 수 있는 추상화된 기술을 제공한다.

스프링은 동적 프록시를 통합해서 편리하게 만들어주는 ProxyFactory라는 기능을 제공한다. 이 프록시 팩토리로 동적 프록시를 생성할 수 있다.

동적 프록시

동적 프록시 기술을 사용하면 개발자가 직접 프록시 클래스를 만들지 않아도 되며, 프록시 객체를 동적으로 런타임에 개발자 대신 만들어준다. 동적 프록시에 원하는 실행 로직도 지정할 수 있다.

 

 

 

JDK 동적 프록시

인터페이스를 기반으로 프록시를 동적으로 만들어준다. 따라서, 인터페이스가 필수로 존재해야한다.

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

 

AInterface.java
public interface AInterface {
    String call();
}

 

AImpl
@Slf4j
public class AImpl implements AInterface {
    @Override
    public String call() {
        log.info("AImpl call");
        return "a";
    }
}

 

BInterface.java
public interface BInterface {
    String call();
}

 

BInpl.java
@Slf4j
public class BImpl implements BInterface {
    @Override
    public String call() {
        log.info("BImpl call");
        return "b";
    }
}

 

 

JDK 동적 프록시 InvocationHandler 

InvocationHandler 인터페이스를 구현한 클래스를 생성한다. 해당 클래스를 통해서 JDK 동적 프록시에 적용할 공통 로직을 추가한 것이다. 프록시는 동적으로 만들어주지만, 프록시가 실행할 로직은 InvocationHandler 인터페이스를 구현해서 별도로 작성해야한다.

 

TimeInvocationHandler.java
@Slf4j
public class TimeInvocationHandler implements InvocationHandler {
    private final Object target;

    public TimeInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        log.info("TimeProxy 실행");

        long startTime = System.currentTimeMillis();

        // proxy -> proxy
        // target : AImpl 또는 BImpl,
        Object result = method.invoke(target, args);

        long endTime = System.currentTimeMillis();

        long resultTime = endTime -  startTime;
        log.info("TimeProxy 종료 resultTime={}", resultTime);

        return result;
    }
}

 

1) 제공되는 파라미터는 다음과 같다.

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
parameter 설명
Object proxy 프록시 자신
Method method 호출한 메서드
Object[] args 메서드를 호출할 때 전달한 인수

 

2) invoke()는 리플렉션을 사용해서 target 인스턴스의 메서드를 실행한다.

// proxy -> proxy
// target : AImpl 또는 BImpl,
Object result = method.invoke(target, args);

 

이제, 테스트 코드를 사용하여 JDK 동적 프록시를 사용해보자.

JdkDynamicProxyTest.java
@Slf4j
public class JdkDynamicProxyTest {

    @Test
    void dynamicA() {
        AInterface target = new AImpl();
        TimeInvocationHandler handler = new TimeInvocationHandler(target);

        // proxy
        // 반환타입 Object
        AInterface proxy = (AInterface) Proxy.newProxyInstance(AInterface.class.getClassLoader() // 어느 클래스로더에 할지
                , new Class[]{AInterface.class}, handler); // handler : 프록시가 사용해야할 로직

        proxy.call();

        log.info("targetClass={}", target.getClass()); // hello.proxy.jdkdynamic.code.AImpl
        log.info("proxyClass={}", proxy.getClass()); // com.sun.proxy.$Proxy12
    }

    @Test
    void dynamicB() {
        BInterface target = new BImpl();
        TimeInvocationHandler timeInvocationHandler = new TimeInvocationHandler(target);

        // proxy 객체를 여기서 만들어주니깐 개발자는 TimeInvocationHandler 만 개발하면 된다.
        // proxy
        // 반환타입 Object
        BInterface proxy = (BInterface) Proxy.newProxyInstance(BInterface.class.getClassLoader() // 어느 클래스로더에 할지
                , new Class[]{BInterface.class}, timeInvocationHandler); // timeInvocationHandler : 프록시가 사용해야할 로직

        proxy.call();

        log.info("targetClass={}", target.getClass()); // hello.proxy.jdkdynamic.code.AImpl
        log.info("proxyClass={}", proxy.getClass()); // com.sun.proxy.$Proxy12
    }
}

 

1) 우리가 위에서 생성한, 동적 프록시에 적용할 핸들러 로직이다.

TimeInvocationHandler handler = new TimeInvocationHandler(target);

 

2) 동적 프록시는 Proxy를 통해서 생성할 수 있다.

해당 인터페이스를 기반으로 동적 프록시를 생성하고 그 결과를 반환한다.

AInterface proxy = (AInterface) Proxy.newProxyInstance(AInterface.class.getClassLoader() // 어느 클래스로더에 할지
                , new Class[]{AInterface.class}, handler); // handler : 프록시가 사용해야할 로직

 

실행결과
TimeInvocationHandler - TimeProxy 실행 
AImpl - A 호출 
TimeInvocationHandler - TimeProxy 종료 resultTime=0 
JdkDynamicProxyTest - targetClass=class hello.proxy.jdkdynamic.code.AImpl 
JdkDynamicProxyTest - proxyClass=class com.sun.proxy.$Proxy1

 

위 결과를 보면, 동적으로 생성된 프록시 클래스 정보를 확인할 수 있다.

JdkDynamicProxyTest - proxyClass=class com.sun.proxy.$Proxy1

 

위 클래스가 바로, JDK 동적 프록시가 동적으로 만들어준 프록시다. 이 프록시는 이제 우리가 지정했던 TimeInvocationHandler 로직을 수행하게된다.

 

실행순서

1. 클라이언트는 JDK 동적 프록시의 call() 을 실행한다

2. JDK 동적 프록시는 InvocationHandler.invoke() 를 호출한다.

  • TimeInvocationHandler 가 구현체로 있으로 TimeInvocationHandler.invoke() 가 호출된다. 

3. TimeInvocationHandler 가 내부 로직을 수행하고method.invoke(target, args) 를 호출해서 target 인 실제 객체AImpl )를 호출한다

4. AImpl 인스턴스의 call() 이 실행된다

5. AImpl 인스턴스의 call() 의 실행이 끝나면 TimeInvocationHandler 로 응답이 돌아온다시간 로그를 출력하고 결과를 반환한다.

 

 

CGLIB: Code Generator Library

CGLIB는 바이트코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리이다.

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

 

CGLIB를 사용하면 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다.

CGLIB는 원래는 외부 라이브러리인데스프링 프레임워크가 스프링 내부 소스 코드에 포함했다. 따라서 스프링을 사용한다면 별도의 외부 라이브러리를 추가하지 않아도 사용할 수 있다

 

ServiceInterface.java
public interface ServiceInterface {
    void save();
    void find();
}

 

ServiceImpl.java
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ServiceImpl implements ServiceInterface {
    @Override
    public void save() {
        log.info("save 호출");
    }

    @Override
    public void find() {
        log.info("find 호출");
    }
}

 

ConcreteService.java

ConcreteService 는 인터페이스가 없는 구체 클래스이다.

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ConcreteService {
    public void call() {
        log.info("ConcreteService 호출");
    }
}

 

CGLIB 동적 프록시 MethodInterceptor 

TimeMethodInterceptor.java
public class TImeMethodInterceptor implements MethodInterceptor {
    // 항상 프록시는 호출할 대상이 필요하다.
    private final Object target;

    public TImeMethodInterceptor(Object target) {
        this.target = target;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        log.info("TimeProxy 실행");

        long startTime = System.currentTimeMillis();

        // Object result = method.invoke(target, args); // 이렇게 해도 되긴됨
        Object result = methodProxy.invoke(target, args);// 이게 더 빠르다. (매뉴얼에 그렇게 되어있음)

        long endTime = System.currentTimeMillis();

        long resultTime = endTime -  startTime;
        log.info("TimeProxy 종료 resultTime={}", resultTime);

        return result;
    }
}

 

1) 제공되는 파라미터는 다음과 같다.

package org.springframework.cglib.proxy; 

public interface MethodInterceptor extends Callback { 
    Object intercept(Object obj, Method method, Object[] args, MethodProxy  proxy) throws Throwable; 
}
parameter 설명
Object obj CGLIB가 적용된 객체 
Method method 호출한 메서드
Object[] args 메서드를 호출할 때 전달한 인수
MethodProxy proxy 메서드 호출에 사용 

 

2) 실제 대상을 동적으로 호출한다.

Object result = methodProxy.invoke(target, args);

 

이제, 테스트 코드를 사용하여 CGLIB 동적 프록시를 사용해보자.

CglibTest.java
@Slf4j
public class CglibTest {

    /**
     * CGLIB 는 상속을 사용한다.
     * 부모 클래스의 생성자를 체크해야한다. 자식 클래스를 동적으로 생성하므로 기본 생성자가 필요하다.
     * final 키워드가 붙으면 상속이 불가능하므로 CGLIB 에서는 예외가 발생한다.
     * final 키워드가 붙으면 메서드 오버라이딩이 불가능하다. CGLIB에서는 프록시 로직이 동작하지 않는다.
     */
    @Test
    void cglib() {
        ConcreteService target = new ConcreteService();

        // CGLIB는 Enhancer 를 사용해서 프록시를 생성한다.
        Enhancer enhancer = new Enhancer();

        // CGLIB 는 구체 클래스를 상속받아서 프록시를 생성할 수 있다.
        // 어떤 구체 클래스를 상속 받을지 지정한다.
        enhancer.setSuperclass(ConcreteService.class);

        // 프록시에 적용할 실행 로직을 할당
        enhancer.setCallback(new TImeMethodInterceptor(target));

        // enhancer.setSuperclass(ConcreteService.class); 에서 지정한 클래스를 상속받아서 프록시를 만든다.
        ConcreteService proxy = (ConcreteService) enhancer.create();

        // hello.proxy.common.service.ConcreteService
        log.info("targetClass={}", target.getClass());

        // hello.proxy.common.service.ConcreteService$$EnhancerByCGLIB$$25d6b0e3
        log.info("proxyClass={}", proxy.getClass());

        proxy.call();
    }
}

 

1) Enhancer를 사용해서 프록시를 생성한다.

// CGLIB는 Enhancer 를 사용해서 프록시를 생성한다.
Enhancer enhancer = new Enhancer();

 

2) CGLIB는 구체 클래스를 상속받아서 프록시를 생성할 수 있으므로, 어떤 구체 클래스를 상속 받을지 지정한다.

// CGLIB 는 구체 클래스를 상속받아서 프록시를 생성할 수 있다.
// 어떤 구체 클래스를 상속 받을지 지정한다.
enhancer.setSuperclass(ConcreteService.class);

 

3) 프록시에 적용할 실행 로직을 할당한다.

// 프록시에 적용할 실행 로직을 할당
enhancer.setCallback(new TImeMethodInterceptor(target));

 

4) 프록시를 생성한다.

CGLIB는 구체 클래스를 상속 (extends)해서 프록시를 만든다

// enhancer.setSuperclass(ConcreteService.class); 에서 지정한 클래스를 상속받아서 프록시를 만든다.
ConcreteService proxy = (ConcreteService) enhancer.create();

 

실행결과
CglibTest - targetClass=class hello.proxy.common.service.ConcreteService
CglibTest - proxyClass=class hello.proxy.common.service.ConcreteService$ $EnhancerByCGLIB$$25d6b0e3 
TimeMethodInterceptor - TimeProxy 실행 
ConcreteService - ConcreteService 호출 
TimeMethodInterceptor - TimeProxy 종료 resultTime=9

 

위 결과를 보면, 동적으로 생성된 프록시 클래스 정보를 알 수 있다.

CglibTest - proxyClass=class hello.proxy.common.service.ConcreteService$ $EnhancerByCGLIB$$25d6b0e3

 

 

CGLIB 제약과 Spring Boot가 선택한 CGLib

1) 부모 클래스의 생성자를 체크해야 한다. CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요하다

→ [해결] Objensis 라이브러리의 도움을 받아 [2] default 생성자 없이도 Proxy를 생성할 수 있게 되었다.

2) 클래스에 final 키워드가 붙으면 상속이 불가능하다. CGLIB에서는 예외가 발생한다.

→ 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다. CGLIB에서는 프록시 로직이 동작하지 않는다

3) net.sf.cglib.proxy.Enhancer 의존성이 필요하다.

→ [해결] Spring 3.2 버전부터 CGLib을 Spring Core 패키지에 포함시켜 더이상 의존성을 추가하지 않아도 개발할 수 있게 되었다.

 

 

AOP와의 관계 정리

AOP 용어 정리

https://yeoncoding.tistory.com/175

용어 설명
JoinPoint advice가 적용될 수 있는 모든 위치를 말한다.
PointCut advice가 적용될 위치를 선별하는 기능을 말한다. (적용할 타겟의 메서드를 선별하는 정규표현식)
Target advice의 대상이 되는 객체
advice 실질적인 부가 기능 로직을 정의하는 곳
Aspect Advice(부가기능) + PointCut(advice를 어디에 적용시킬 것인지 결정)를 모듈화한 것
Advisor 스프링 AOP에서만 사용되는 용어로, advice + pointcut
Weaving pointcut으로 결장한 타겟의 join point에 advice를 적용하는 것

 

동적 프록시와 관계

https://jjeongil.tistory.com/1160

 

AOP JDK Dynamic Proxy CGLIB
JoinPoint InvocationHandler MethodInterceptor
PointCut MethodMatcher MethodMatcher
Advice invoke() intercept()

 

 

 

@Transactional의 동작 원리 (AOP)

@Transactional을 메서드 또는 클래스에 명시하면 AOP를 통해 타겟이 상속하고 있는 인터페이스 또는 타겟을 상속한 프록시 객체가 생성된다. 이때 프록시 객체의 메서드를 호출하면 타겟 메서드 전 후로 트랜잭션 처리를 수행한다.

 

https://velog.io/@ann0905/AOP%EC%99%80-Transactional%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC

 

1) target에 대한 호출이 들어오면 AOP proxy가 이를 가로채서(intercept) 가져온다.
2) AOP proxy에서 Transaction Advisor가 commit 또는 rollback 등의 트랜잭션 처리를 한다.
3) 트랜잭션 처리 외에 다른 부가 기능이 있을 경우 해당 Custom Advisor에서 그 처리를 한다.
4) 각 Advisor에서 부가 기능 처리를 마치면 Target Method를 수행한다.
5) interceptor chain을 따라 caller에게 결과를 다시 전달한다.

 

 

ThreadLocal과 스프링 트랜잭션

스프링은 TransactionManager 안에 있는 TransactionSynchronizationManager의 ThreadLocal을 사용해 커넥션을 동기화한다. 

리소스 동기화를 위해 스프링에서 제공하는 것이 TransactionSynchronizationManager 이고, 위에서 설명한 PlatformTransactionManager가 TransactionSynchronizationManager에서 보관하는 커넥션을 가져와서 사용하는 방식이다.

https://escapefromcoding.tistory.com/810?category=1246236

 

트랜잭션의 진행 흐름은 아래와 같다.

1. transactionManager.getTransaction()으로 트랜잭션 시작

2. 트랜잭션 매니저가 데이터 소스에서 커넥션 생성

3. con.setAutoCommit(false)로 변경

4. 트랜잭션 매니저는 트랜잭션을 시작한 커넥션을 TransactionSynchronizationManager에 보관

5. TransactionSynchronizationManager는 쓰레드로컬(ThreadLocal) 덕분에 멀티 쓰레드 환경에서 쓰레드를 안전하게 보관 가능

 

https://escapefromcoding.tistory.com/810?category=1246236

 

6. 비즈니스 로직에서 데이터 접근 로직을 호출

7. DataSourceUtils.getConnection(dataSource)로 TransactionSynchronizationManager에 있는 커넥션 조회

8. SQL 실행

 

https://escapefromcoding.tistory.com/810?category=1246236

 

9. 비지니스 로직이 끝나고 transactionManager.commit() 혹은 rollback()으로 트랜잭션 종료

10. TransactionSynchronizationManager에 있는 커넥션을 획득

11. 획득한 커넥션으로 con.commit() 혹은 con.rollback() 수행

12. con.setAutoCommit(true), con.close() 수행하여 전체 리소스 정리

 

TransactionSynchronizationManager는 아래와 같이 쓰레드 로컬(ThreadLocal)을 사용하여 쓰레드를 독립적으로 커넥션을 동기화시킨다. 따라서 TransactionSynchronizationManager는 내부적으로 멀티쓰레드 상황에 안전하게 Connection을 동기화할 수 있다. 

 

 

트랜잭션 AOP 적용 전체 흐름

https://hoonsmemory.tistory.com/24

 

1. 클라이언트 요청으로 AOP 프록시 호출

2. 스프링 먼테이너를 통해 트랜잭션 매니저 획득

3. getTransaction() 메서드로 트랜잭션 시작

4. 트랜잭션이 시작되면 가장 먼저 데이터소스로 데이터베이스 커넥션 획득

5. 커넥션을 수동 커밋 모드로 변경 (con.setAutoCommit(false))

6. 트랜잭션 동기화를 위한 TransactionSynchronizationManager에 트랜잭션을 보관

7. AOP 프록시가 실제 서비스를 호출

8. 데이터 접근 계층에서 DatasourceUtils.getConnection()을 호출하여 TransactionSynchronizationManager에 보관된 커넥션을 다시 꺼내서 사용

9. 획득한 커넥션을 사용해서 SQL 실행

10. 실행 결과에 따라 TransactionManager는 commit 또는 rollback 수행

11. 전체 리소스 정리

 

 

@Transactional 사용시 주의할 점

1) private 메서드는 @Transactional 사용 불가능

@Transactional이 적용된 메서드는 프록시 패턴이 적용되어, 프록시 객체가 생성되고, 이 프록시 객체는 실제 target 객체나 그것의 인터페이스를 상속받아 생성되어 원본 서비스 객체의 메서드를 대신 호출하면서 트랜잭션 관련 로직을 수행한다. private 접근 제한자를 가진 메서드는 프록시 객체가 접근 불가능하다.

 

2) @Transactional이 없는 메서드(outer method)에서 @Transactional이 있는 메서드(inner method)를 호출할 경우

두 메서드 모두 트랜잭션이 적용되지 않는다. 

outerMethod()에는 @Trnasactional이 붙어있지 않으므로, 특별 추가 로직 없이 원본 outerMethod가 실행된다. 여기서 innerMethod()를 호출하면 이 경우에도 프록시 객체에서 트랜잭션 로직을 거치지 않고 원본 메서드를 수행한다. 

 

3) @Transactional(readOnly=true) 

스프링에서 해당 옵션으로 '읽기 전용 모드'로 변경할 수 있다. 말 그대로, DB에서 데이터를 읽기만 하는 서비스 메서드에 적용해야한다. 

  • 성능 최적화 : 해당 메서드가 데이터를 읽기만 한다는 것을 DB에 알려줌으로써 쿼리 및 캐싱을 최적화할 수 있다.
  • 가독성 향상 : 위 어노테이션이 설정된 메서드가 DB에서 데이터 읽기만 한다는 사실을 명확하게 확인할 수 있다.

 

 

Reference

1) 도서 - 스프링 철저 입문

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

3) https://velog.io/@ann0905/AOP%EC%99%80-Transactional%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC

4) https://bimmm.tistory.com/51

5) https://escapefromcoding.tistory.com/810?category=1246236

6) https://wookjongbackend.tistory.com/45

7) https://velog.io/@jhbae0420/TransactionalreadOnly-true%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0%EC%99%80-%EC%A3%BC%EC%9D%98%ED%95%A0%EC%A0%90

8) 강의 - https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8

반응형

Designed by JB FACTORY