Rlog

Spring 에서 왜 Private Method 는 Cglib Proxy 에 포함이 되지않을까? 본문

Spring

Spring 에서 왜 Private Method 는 Cglib Proxy 에 포함이 되지않을까?

dev_roach 2022. 2. 22. 22:56
728x90

Spring 면접 하면 잘 등장하는 단골 질문 중 하나이다.

이 개념을 이해하기 위해서는 일단 Proxy 디자인 패턴에 대해서 간단하게 짚고 넘어가야 한다.

우리가 흔히 볼 수 있는 Server-Client 형태는 아래와 같다.

Client 는 Server 에 요청을 보내고, Server 는 요청을 처리한다.

만약 객체 세상으로 가져온다고 했을때는 아래 그림처럼 A 객체가 B 객체에게 협력을 요청하는 것일 수 있다.

근데 만약 B 객체는 너무나 은밀한 친구라 중간에 누군가 자신이 한것처럼 대신 행동을 하는 척 해줬으면 좋겠다 라고 해보자.

그렇다면 A 객체는 B 객체와 이야기 하는것 같지만 사실은 대리자가 대신 해주고 있는 것으로 객체를 설계 해주어야 할 것이다.

위와 같은 설계가 될 것 이다. 하지만 결국 A 가 받게 되는 값은 동일할 것이다. 왜냐하면 대리자를 통해 B 를 단순히 호출하기 만 때문이다. 여기서 이런 의문점이 들 수 있다. 굳이 이렇게 해야 하는 이유가 있나요? 라고 질문 할 수 있다.

이렇게 해서 얻는 이점은 무엇일까? 아래 코드를 한번 보자.

@Slf4j
public class AClass {

    public void add() {
        BClass bClass = new BClass();

        log.info("result = {}", bClass.add(1,2));
    }

}

AClass 는 BClass 에게 덧셈을 요청하고 있다.

지금 처럼 똑같이 동작하지만 만약 대리자를 놓고 B 를 호출한다고 했을때 어떤 장점을 활용할 수 있을지 생각해보자.

 

일단 첫번째로, 동일한 값에 대한 빠른 처리가 가능하다.

아래 코드를 한번 보자.

@Slf4j
public class BProxyClass {

    private final BClass bClass = new BClass();
    private Integer beforeResult = null;
    private Integer[] beforeParams = {null, null};

    public int add(int num1, int num2) {

        if (isSameParams(num1, num2)) {
            log.info("동일한 Parameter 이므로 캐싱되어 있는 이전값 리턴 = {}", beforeResult);
            return beforeResult;
        }

        int result = bClass.add(num1, num2);
        cacheBeforeValueAndParams(num1, num2, result);

        return result;
    }

    private boolean isSameParams(int num1, int num2) {
        if (beforeParams[0] == null) {
            return false;
        }
        return beforeParams[0] == num1 && beforeParams[1] == num2;
    }

    private void cacheBeforeValueAndParams(int param1, int param2, int result) {
        this.beforeParams[0] = param1;
        this.beforeParams[1] = param2;
        this.beforeResult = result;
    }
    
}

예시 코드를 생각나는 대로 적다보니 퀄리티는 좋진 않지만 예시를 들기 위함에는 충분하다고 생각했습니다.

BProxyClass 는 이전 Parameter 를 Caching 하여 동일한 Parameter 가 들어온다면 Caching 한 값을 리턴하여 add 연산을 수행하지 않으므로 좀 더 빠르게 요청을 처리할 수 있습니다. 아래 테스트 결과를 보시죠 :)

일단 Proxy 에 대해서 이정도로 설명을 마치고 데코레이터 패턴에 대한 간단한 설명이 필요합니다.

데코레이터 패턴이란 기능을 추가하는 것이라고 생각하면 편합니다.

예를 들면 "덧셈을 해주는 add()" 에 로그를 앞뒤로 출력하고 싶다면 "LogDecorator" 를 덮어 씌워서 사용하면 되는데요.

일단 이 부분까지 설명하면 너무 길어지니 여기서 넘어가겠습니다.

 

위의 설명이 필요했던 이유는 Spring 에서는 위와 같이 부가기능을 탑재한 Proxy 를 많이 이용하는데요.

예를 들면 우리가 사용하는 @Transactional 어노테이션을 붙여준 Class 는 Spring 이 자체적으로 Proxy 객체로 만들어 사용합니다.

덕분에 우리의 대부분의 비즈니스 로직에는 tx.commit 과 같은 코드가 존재하지 않습니다.

 

본론으로 들어가서 왜 Private Method 는 Proxy 객체에 포함되지 않을까를 생각해본다면 Spring 에서 Proxy 를 만드는 방식을 알아야 하는데요. Spring 은 기존에는 ProxyFactory 를 이용하여 JDKDynamic Proxy 또는 Cglib 을 이용하여 Proxy 를 구현하였습니다. 하지만 Spring Boot 2.6 버전 부터는 Cglib 을 이용해서 Proxy 객체를 만드는 것으로 알고 있습니다.

 

따라서 Cglib 이 Proxy 를 만드는 방식을 안다면 private method 가 왜 포함이 될수 없는지를 알 수 있을 것입니다.

Cglib 을 통해 아까의 코드대신 BClass 가 실행되기 전 그리고 종료되고 나서 "실행시작" / "실행 종료" 를 남기는 기능을 추가해봅시다.

@Slf4j
public class LoggerInterceptor implements MethodInterceptor {

    private final Object target;

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

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        log.info("BClass Add method 시작");
        Object result = methodProxy.invoke(target, args);
        log.info("BClass Add method 종료");
        return result;
    }
}

Cglib 은 기본적으로 target 을 받아온 뒤에 해당 target 의 메소드를 invoke 시킬 수 있습니다.

따라서 invoke 되는 시점 전 / 후로 위와 같이 로깅을 찍어주는 코드를 남겨줍니다.

    @Test
    void CGLIB_테스트() {
        BClass bClass = new BClass();

        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(bClass.getClass());
        enhancer.setCallback(new LoggerInterceptor(bClass));
        BClass proxy = (BClass) enhancer.create();

        proxy.add(1, 2);
    }

위 코드에서 결정적으로 Cglib 을 사용했을때 private 을 입력하지 못하는 이유를 알 수 있는데 보시다시피 setSuperClass 를 이용해 현재클래스는 부모클래스로 생성합니다. Cglib 내부에서 부모 클래스를 상속하여 진행한다는 걸 아래 결과를 보면 알 수 있습니다.

따라서 상속을 이용하기 때문에 private method 는 Proxy 안에 존재할 수 없습니다.

@Transactional 을 이용할때도 그렇다면 private method 에 붙이면 동작할지 잘 생각해 보시면 좋을 것 같습니다.