5 분 소요

Spring에서는 트랜잭션을 적용하고 싶은 메서드에 @Transactional을 적용하면 구체적으로 어떤 동작이 이뤄지는지 몰라도 무척 간편하게 적용할 수 있다. (TransactionTemplate을 적용하는 방법도 있지만, 여기서는 @Transactional만 다룬다)

어떻게 적용되는지 알아보자.

템플릿 콜백 패턴

템플릿 콜백 패턴은 전략 패턴의 일종이다. 그니까 전략 패턴부터 알아보자.

전략 패턴

작성하려는 로직에서 고정된 부분변하는 부분이 있다고 해보자.
전략 패턴에서 고정된 부분은 Context, 변하는 부분은 Strategy 라고 부른다.

계산기를 예로 들어보자.
파라미터를 a, b로 받고 있다. 반드시 양수로 들어와야 해서 검증을 하고 있고, 계산 로직은 CalculatorStrategy 에게 맡기고 있다. 이때 양수로 들어도록 검증하는 부분은 수정되지 않으므로 context라고 할 수 있고, 계산 로직은 CalculatorStrategy가 operate()를 어떻게 구현하는지에 따라 다르므로 strategy라고 할 수 있겠다.

class CalculatorContext(private val strategy: CalculatorStrategy) {

    fun calculate(a: Int, b: Int): Int {
        // 변하지 않는 부분 - context
        if (a < 0 || b < 0) {
            throw IllegalArgumentException("파라미터는 반드시 양수여야 합니다.")
        }
        // 변하는 부분 - strategy
        return strategy.operate(a, b)
    }
}


여기서 CalculatorStrategy는 여러 방식으로 구현될 수 있으므로 인터페이스로 정의한다.

interface CalculatorStrategy {
    fun operate(a: Int, b: Int): Int
}

// Strategy 구현체로 더하기, 빼기를 지원한다.
class PlusStrategy : CalculatorStrategy {
    override fun operate(a: Int, b: Int): Int {
        return a + b
    }
}

class MinusStrategy : CalculatorStrategy {
    override fun operate(a: Int, b: Int): Int {
        return a - b
    }
}

CalculatorContext 클래스는 calculate()에서 operate()의 동작을 철저히 CalculatorStrategy에게 위임하고 있다. 어떤 전략(Strategy)를 쓸지 모르니까 context에서 분리한 모습이다. 그래서 전략 패턴이라 부른다.

템플릿 콜백 패턴

템플릿 콜백 패턴은 전략 패턴의 일종이다. context는 템플릿으로, strategy는 콜백으로 생각하면 된다.
콜백은 함수 인자로 넘겨주는 실행 가능한 로직을 말한다. 이 로직은 익명 클래스일 수도 있고, 람다식일 수도 있다.
CalculatorTemplate 클래스와 CalculatorCallback 인터페이스는 아래처럼 작성할 수 있다.

class CalculatorTemplate {

    fun calculate(callback: CalculatorCallback, a: Int, b: Int): Int {
        // 변하지 않는 부분 - template
        if (a < 0 || b < 0) {
            throw IllegalArgumentException("파라미터는 반드시 양수여야 합니다.")
        }
        // 변하는 부분 - callback
        return callback.operate(a, b)
    }
}

interface CalculatorCallback {
    fun operate(a: Int, b: Int): Int
}
  • 전략 패턴 예시에서는 strategy를 생성자로 주입받았었다.
  • 템플릿 콜백 패턴 예시에서는 callback을 파라미터로 받는다. (그래도 전략 패턴의 일종이다)

익명 클래스로 콜백을 만들어 넘기면 아래처럼 작성해줄 수 있다.

val calculator = CalculatorTemplate()

val plusCallback = object : CalculatorCallback {
    override fun operate(a: Int, b: Int): Int {
        return a + b
    }
}
val minusCallback = object : CalculatorCallback {
    override fun operate(a: Int, b: Int): Int {
        return a - b
    }
}

calculator.calculate(plusCallback, 10, 5)
calculator.calculate(minusCallback, 10, 5)

전략 패턴은 context 인스턴스가 만들어질 때 strategy도 같이 초기화된다는 특징이 있어 그 이후엔 수정하기가 어렵다.
하지만 템플릿 콜백 패턴은 필요할 때마다 요구사항에 맞는 callback을 넘겨주면 되므로 변화에 강하다.

프록시 패턴

프록시 패턴은 작성된 클래스의 객체를 직접 사용하는게 이나라 이를 wrapping한 프록시 객체를 참조하는 방식을 말한다.
기존 로직의 변경없이 앞뒤로 로직을 삽입하고 싶을 때 사용한다.
예를 들면, 비즈니스 로직이 트랜잭션 안에서 돌아가게 하려면 아래처럼 동작해야 한다.

val con = dataSource.connection
try {
    con.autoCommit = false
    businessLogic(...) // 비즈니스 로직 수행
    con.commit()
} catch (e: Exception) {
    con.rollback()
    throw e
} finally {
    release(con)
}
  • dataSource.connection 에서 커넥션을 얻는다 -> con
  • 오토커밋 설정을 false로 바꾼다 -> con.setAutoCommit(false)
  • 비즈니스 로직을 수행한다 -> businessLogic()
  • 문제가 없었다면 커밋해서 DB에 쿼리를 날린다 -> con.commit()
  • 그 외에는 롤백한다 -> rollback()


근데 우리는 @Transactional을 붙인 메서드에서 비즈니스 로직을 작성하기만 했지 커넥션을 얻고, 오토커밋을 false로 바꾸고, 커밋하거나 롤백하는 코드는 따로 적지 않았다. 이런 작업들은 프록시 객체에 삽입된 코드에서 이뤄진다고 생각하면 된다.

@Transactional

위 로직을 다시 참고해보면, @Transactional템플릿 콜백 패턴도 사용하고 프록시 패턴도 사용한다!
우리는 바뀔 수 있는 비즈니스 로직(callback)만 열심히 작성할 뿐, 다른 작업들은 항상 변하지 않는 로직(template)에서 동작한다.
template은 프록시 객체에서 추가되는 파란 부분이 될거고, callback은 빨간 부분이 될 것이다.

?


이제 템플릿과 콜백에 대해서 끄덕일 수 있을 것이다. 그런데 콜백은 누가 넘겨주는 걸까? 프록시 객체는 누가 만들어주는 걸까?

그것은 바로바로 Spring AOP가 해준다! @Transactional이 적용된 메서드를 가진 클래스가 있다면, Spring은 해당 클래스를 프록시 객체로 관리한다. 즉, 원본 메서드(또는 클래스)에 @Transactional이 있으면 Spring AOP가 적용되는 대상이므로, 호출될 때 전처리(connection 획득, 오토커밋 false) + 콜백으로 비즈니스 로직 호출 + 후처리(커밋 or 롤백) 로직을 프록시 객체에 등록해준다.

즉, 트랜잭션 관리 로직(template)과 비즈니스 로직(callback)을 합쳐서 프록시 객체에 등록해준다.

  • Spring AOP에 대한 내용은 Haon님의 글을 참조하자.
  • 기존 로직 변경없이 부가 기능을 적용하기 위해 프록시 객체를 쓰는데, 여러 클래스에 같은 로직을 적용하면 중복이 많아 관리포인트가 많아진다. 그래서 Spring AOP로 관리포인트를 한 곳으로 줄이면서 기존 로직 변경없이 부가 기능을 적용하는 것이다.

아주 간단한 TestService가 있다고 해보자. businessLogic()에는 @Transactional이 적용돼 있다.

@Service
class TestService {

    @Transactional
    fun businessLogic() {
        // 비즈니스 로직
    }
}


Spring은 컴포넌트 스캔을 하면서 TestService를 스프링 빈으로 등록하려고 한다.
이때 TestService.businessLogic()은 @Transactional이 붙어 있어서, TestService의 Proxy를 스프링 빈으로 관리한다.
다른 클래스에서 TestService와 연관돼 있는 경우 Spring은 이 프록시 객체 ProxyTestService를 주입시킨다. (DI)

class ProxyTestService(private val testService: TestService) {

    fun businessLogic() {
        val con = dataSource.connection
        try {
            con.autoCommit = false
            testService.businessLogic() // 비즈니스 로직 수행
            con.commit();
        } catch (e: Exception) {
            con.rollback()
            throw e
        } finally {
            release(con)
        }
    }
}
  • TestService.businessLogic()를 호출하도록 했어도 런타임엔 ProxyTestService.businessLogic()를 호출한다.

실제 코드에서는

@Transactional이 붙은 메서드(또는 클래스)에 부가 기능을 적용하는 로직은 TransactionAspectSupport 클래스에 있다.
TransactionAspectSupport 클래스에서 invokeWithinTransaction()를 호출해서 처리한다. 위에서 본 모양과 닮았다!

// TransactionAspectSupport.java
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
        final InvocationCallback invocation) throws Throwable {
    // 트랜잭션이 적용되는 메서드(또는 클래스)인지 확인한다. (@Transactional이 적용됐는지)
    final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);

    // 트랜잭션 시작
    TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

    Object retVal;
    try {
        // 비즈니스 로직 수행
        retVal = invocation.proceedWithInvocation();
    }
    catch (Throwable ex) {
        // Exception 발생 시 트랜잭션 롤백
        completeTransactionAfterThrowing(txInfo, ex);
        throw ex;
    }

    // ...

    // 성공 시 트랜잭션 커밋
    commitTransactionAfterReturning(txInfo);
    // 비즈니스 로직 결과값 반환
    return retVal;
}
  • 파라미터로 invocation이 콜백이다! (비즈니스 로직)

@Transactional이 적용됐는지 확인하려면 getTransactionAttribute()를 확인해보면 된다.

Return the transaction attribute for the given method, or null if the method is non-transactional.

TransactionAttributeSource.getTransactionAttribute() 설명이다. 트랜잭션 적용됐으면 transaction attribute를 내려줄 거고, 아니면 null을 내려준다고 써있다.

구현 메서드를 찾아 더 내려가면 추상 클래스 AbstractFallbackTransactionAttributeSource가 있다.
여기서는 fallback 이라는 이름처럼 여러 케이스에 대해 처리한다.

getTransactionAttribute()에서 computeTransactionAttribute()를 호출하고 있으니 더 따라가보자.

// AbstractFallbackTransactionAttributeSource.java
@Override
public TransactionAttribute getTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
  // ...
  TransactionAttribute txAttr = computeTransactionAttribute(method, targetClass);
}   
  • 캐시에 없으면 computeTransactionAttribute()를 호출하는데, 중요하지 않아서 생략했다.


// AbstractFallbackTransactionAttributeSource.java
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
  // method는 인터페이스 메서드일 수 있어서, 구현체의 메서드를 찾아줌
  Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);

  // 메서드에 트랜잭션이 걸려있나?
  TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
  if (txAttr != null) {
    return txAttr;
  }

  // 메서드의 클래스에 트랜잭션이 걸려있나?
  txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
  if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
    return txAttr;
  }

  if (specificMethod != method) {
    // 원본 메서드에 트랜잭션이 걸려있나?
    txAttr = findTransactionAttribute(method);
    if (txAttr != null) {
      return txAttr;
    }
    // 원본 메서드의 클래스에 트랜잭션이 걸려있나?
    txAttr = findTransactionAttribute(method.getDeclaringClass());
    if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
      return txAttr;
    }
  }

  // 트랜잭션이 안걸려있다.
  return null;
}
  • 여러 단계를 통해서 트랜잭션 적용 여부를 확인하고, 있으면 TransactionAttribute 없으면 null을 반환한다.

References

카테고리:

업데이트:

댓글남기기