Clean Code that Works.


http://nurkiewicz.blogspot.com/2011/10/spring-pitfalls-proxying.html

취미 삼아 번역...... 발 번역 '-'

Bean Proxying은 매우 중요하고 스프링에서 제공하는 가장 중요한 기반 기술중에 하나이다. 이것은 매우 중요하고 low-level 수준이지만 우리는 이것들이 분명이 존재하고 있다는 것을 쉽게 인식하지 않는다. 하지만 트랜잭션, AOP, advanced scoping, @Async 들과 다양한 스프링의 사용 방법들은 이 기술 없이는 가능 할 수 없다. 그럼 프록싱이란 무엇인가?

예를 들어 : 만약 DAO를 service에 inject할 때, 스프링은 DAO의 instances를 가지고 바로 inject 한다. 이게 다다. 하지만 때때로 t스프링이 DAO에 대한 서비스에 의해 각각의 호출(다른 빈들에서의)에 대한 인지가 필요 한다. 예를 들어 만약 DAO에 트랜잭션이 마크 되어 있으면 이것들은 호출 되기 전과 커밋 되기전이나 롤백 되기전에 트랜잭션이 시작될 필요가 있다. 물론 수동적으로 이 작업을 할수 있지만 지루한 작업이고 오류가 발생하기 쉽다. 이게 바로 왜 우리가 선언적인 트랜잭션을 먼저 사용하는 이유이다.

그래서 스프링은 어떻게 인터셉션 메카니즘을 구현했는가? 가장 간단한것에서 진보된 3가지 메서드가 있다. 아직 이것들에 대한 장점과 단점을 논의 하지는 않고 곧 구체적인 예제에서 볼 수 있을 것이다.

Java dynamic proxies
가장 쉬운 해결책이다. 만약 DAO가 어떤 인터페이스를 구현하고 있다면 스프링은 인터페이스를 구현하는 Java dynamic proxy를 사용 하여  실제 클래스에 inject 해준다. 실제 구현 클래스는 계속 존재하고 있고 프록시는 그것들을 참조 하고 있다. 하지만 바깥쪽에서는 프록시는 빈이다. DAO에서 계속 메소드를 호출 하면 스프링은 이것들을 가로체서 몇가지 AOP 매직 과 함께 진짜 메서드를 호출 한다.

CGLIB generated classed
자바 동적 프록시의 단점은 빈은 적어도 하나의 인터페이스를 구현해야 한다는 것이다. CGLIB 이런 제한된 상황에서 동적으로 실제 bean에 대한 자식 클래스를 만들고 오버라이딩 가능한 모든 메서드에 인터셉터 로직을 직접적으로 추가한다. 원본 클래스에 대한 호출은 자식클래스를 통해서 이루어 진다.


class DAO {
  def findBy(id: Int) = //...
}
 
class DAO$EnhancerByCGLIB extends DAO {
  override def findBy(id: Int) = {
    startTransaction
    try {
      val result = super.findBy(id)
      commitTransaction()
      result
    } catch {
      case e =>
        rollbackTransaction()
        throw e
    }
  }
}

하지만 이 슈도코드는 이것이 어떻게 실제로 동작하는지 표현 하지는 못한다.

AspectJ weaving
이것은 가장 침투적이지만 개발자의 관점에서는 가장 신뢰할 수 있고 직관적인 방법이다. 이 상태에서의 가로채는 것은 클래스의 바이트코드에 직접적으로 적용되고 이 의미는 JVM의 클래스는 당신이 작성한 것과 일치하지 않다는 의미이다. AspectJ
weaver는 가로채는 로직을 클래스 내부의 바이트코드를 빌드하는 순간(compile time weaving-CTW, load time weaving-LTW) 직접적으로 수정해서 집어 넣는다.

AspectJ의 마법이 어떻게 동작하는지 이상하다고 느낀다면 여기 AspectJ에 의해 컴파일 된 후 디컴파일되고 간단한 .class 파일이 있다.
public void inInterfaceTransactional()
{
  try
  {
    AnnotationTransactionAspect.aspectOf().ajc$before$1$2a73e96c(this, ajc$tjp_2);
    throwIfNotInTransaction();
  }
  catch(Throwable throwable)
  {
    AnnotationTransactionAspect.aspectOf().ajc$afterThrowing$2$2a73e96c(this, throwable);
    throw throwable;
  }
  AnnotationTransactionAspect.aspectOf().ajc$afterReturning$3$2a73e96c(this);
}


로드타임위빙도 런타임시 클래스가 로드될 때 같은 방식으로 변환되어 호출된다. 보시다시피 불안하게 만드는 요소는 없다. 사실 수동 트랙잭션을 프로그램상에서 정확히 구현한 것이다.

프록시 기술을 아는 것은 그 코드에 미치는 영향과 프록시의 동작 방식을 이해하는 것이 중요하다. 우리는 선언적 트랜잭션 경계의 예라고 해두고, 여기가 바로 우리의 전쟁터다.

trait FooService {
  def inInterfaceTransactional()
  def inInterfaceNotTransactional();
}
 
@Service
class DefaultFooService extends FooService {
 
  private def throwIfNotInTransaction() {
    assume(TransactionSynchronizationManager.isActualTransactionActive)
  }
 
  def publicNotInInterfaceAndNotTransactional() {
    inInterfaceTransactional()
    publicNotInInterfaceButTransactional()
    privateMethod();
  }
 
  @Transactional
  def publicNotInInterfaceButTransactional() {
    throwIfNotInTransaction()
  }
 
  @Transactional
  private def privateMethod() {
    throwIfNotInTransaction()
  }
 
  @Transactional
  override def inInterfaceTransactional() {
    throwIfNotInTransaction()
  }
 
  override def inInterfaceNotTransactional() {
    inInterfaceTransactional()
    publicNotInInterfaceButTransactional()
    privateMethod();
  }
}


throwIfNotInTransaction 은 트랜잭션과 함께 메소드가 시작되지 않으면 오류를 던지게 된다. 누가 생각이나 했을까? 이 메서드는 알수없는 위치와 다른 설정에서 호출 된다.