이 글은 Java 환경에서 Dynamic Proxy 기반의 기능들을 사용할 때 주로 발생하는 문제인 Self-Invocation을 Lambda를 통해 어떻게 회피할 수 있는지에 대한 내용을 담고 있는 글입니다.

Dynamic Proxy, AOP에 대한 이론적인 부분(발생 원인, 호출 흐름 등)들은 다루지 않을 것이기 때문에 다른 글들을 참고하시길 바랍니다.


Lambda와 관련해서는 학습을 진행하며 작성한 글이기 때문에 잘못된 내용이 있을 수도 있습니다. 틀린 내용이 있다면 채널톡 혹은 댓글을 통해 피드백해주시길 바랍니다. ㅎㅎ


Self Invocation

Self Invocation은 Dynamic Proxy 기반의 기능들을 사용할 때 사소한 실수로 인하여 자주 발생하는 문제입니다. 쉽게 설명하자면, 객체 외부에서 보내는 메시지(요청)에 대해서만 반응하도록 설계되어 있기에 내부의 요청에 대해서는 반응하지 못하기 때문입니다.

JVM 생태계에서 많은 사랑을 받는 Spring Framework는 다양한 기능들을 Dynamic Proxy 방식으로 제공하고 있습니다.

  • @Transcational, @Async, @Cacheable, AOP(Before, Around) 등의 Aspect 기능들이 속합니다.


이러한 기능들에 대한 보편적인 해결 방법으로는

  • AspectJ으로 전환 혹은 부분 적용 (Weaving 방식 전환)
    • @EnableTransactionManagement(proxyTargetClass = true, mode = AdviceMode.ASPECTJ)
  • AopContext의 currentProxy() 메서드를 통해 해당 객체를 감싸고 있는 Proxy 객체를 반환
    • ((Type) AopContext.currentProxy()). someMethod();
  • 상태 변수를 통한 자기 참조 (@Autowired나 ApplicationContext.getBean() 활용)
  • 객체 외부에서 호출하는 메서드에 Dynamic Proxy가 반응하도록 설정하기

등이 있지만, 이것들을 적용하기에는 과한 상황이거나 Spring Container에 종속되는 좋지 않은 코드를 작성하게 될 수 있습니다.


Lambda를 이용하여 Self-Invocation 회피

Lambda로 어떻게 Self-Invocation을 회피할 수 있을까요?

결론만 이야기하자면 Lambda를 통해 실행되는 메서드를 접근하기 위해서 현재 호출 객체 외부로 메시지가 나가고, 최종적으로 호출해야 되는 메서드를 찾아 요청하였을 때, 외부에서 전달되기 때문에 감싸고 있는 Proxy가 해당 요청을 인지할 수 있기 때문입니다.

Java Lambda는 Reflection API, MethodHandle, LambdaMetaFactory 인터페이스를 이용하여 기능을 제공합니다.


Lambda Method를 호출하는 흐름

  1. Reflection API를 통해 실행 대상이 되는 메서드 정보를 가져옵니다.
  2. MethodHandle Lookup API에 정의된 Factory 메서드를 통해 Lookup 객체를 가져옵니다.
  3. 1번에서 가져온 정보를 Lookup.unreflect() 메서드에 전달함으로써 해당 메서드의 구현, 수행 정보를 알고 있는 MethodHandle 객체를 가져옵니다. (실제 메서드를 바라보고 있는 일종의 포인터)
  4. LambdaMetafactory.metafactory() 메서드에 필요한 인자를 넘겨 CallSite 객체를 반환받습니다. 해당 객체는 Functional Interface를 객체로 다룰 수 있으며, 매개 변수를 설정하고 응답을 반환합니다. 인자 목록은 밑에 나열하였습니다.
    1. 접근 권한을 가지고 있는 Lookup 객체
    2. 구현할 메서드 이름(Supplier Interface를 사용했을 경우 get이라는 문자열을 넘긴다.)
    3. 메서드의 매개 변수와 응답 값의 Class 정보. methodType(Supplier.class, {Type}. class)
    4. 함수 객체(Lambda)에 의해 반환될 응답 값의 유형. methodType(Object.class)
    5. 메서드의 구현 및 수행 방식을 알고 있는 MethodHandle 객체
    6. 호출 시 동적으로 적용되어야 할 응답 값의 세부 정보. methodType({Type}. class)
  5. callSite.getTarget()을 통해 호출할 메서드 정보를 가져오고 bindTo({Type}. class)를 통해 메서드에 넘길 인자 값을 지정한 뒤 Invoke를 통해 메서드를 호출합니다.

(사실상 그 호출하는 형태는 Dynamic Proxy와 유사한 것 같습니다)

꽤 복잡하지만, 결론적으로는 Lambda를 통해 호출되는 인터페이스를 인스턴스 화 하고, 메서드를 호출하기 때문에 객체 외부 요청으로 다시 돌아오는 것입니다.

  • 이 흐름은 Bytecode의 invokedynamic이 호출된 경우에 수행 흐름을 나타냅니다. Lambda에서 Function Interface를 사용하지 않고 단순한 로직을 사용하는 경우 static 메서드를 생성하여 활용하기도 합니다.
    • invokedynamic은 Bootstrap 메서드라는 특정한 메서드를 호출하고, 이를 통해 위의 호출 프로세스를 초기화 및 실행하여 CallSite 객체를 반환받습니다. (InnerClassLambdaMetafactory를 활용하여 내부 클래스 생성 후 반환) 
  • 한번 Bootstrap 메서드가 실행된다면 이후에는 기존의 CallSite와 MethodHandle를 재사용하여 요청을 처리합니다.

 

 

 

구현 예시

Self-Invocation을 회피하기 위해 구현한 TransactionalHandler입니다.

@Service
public class TransactionHandler {

    @Transactional(propagation = Propagation.REQUIRED)
    public <T> T runInTransaction(Supplier<T> supplier) {
        return supplier.get();
    }
}


Sample Service.

@Service
@RequiredArgsConstructor
public class SampleService {

    private final SampleRepository someRepository;
    private final TransactionHandler transactionHandler;

    // 특정 객체에서 호출하는 Method
    public void addNewSamples(List<Sample> samples) {
        return samples.forEach(sample ->
            transactionHandler.runInTransaction(() -> addSample(sample.toEntity()))
        )
    }

    // 외부에서 호출되는 Method
    @Transcational
    public SomeEntity addSample(SampleEntity newSample) {
        return someRepository.insertSample(newSample);
    }
}


단순한 예시여서 실제 효용성과 조금 동떨어진 감은 있지만, 실제 업무 중 활용할 수 있을만한 부분을 특정하실 수 있을 것이라고 생각합니다. ㅎㅎ 이 글은 여기까지입니다. 감사합니다.


참고 자료

+ Recent posts