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 에 붙이면 동작할지 잘 생각해 보시면 좋을 것 같습니다.
'Spring' 카테고리의 다른 글
Hibernate 1 차 Cache 에 대해서 알아보자 (0) | 2022.03.10 |
---|---|
MySQL 으로 형태소 분석기 없는 자동완성만들기 (0) | 2022.02.24 |
Redisson 으로 분산 Lock 구현하기 (0) | 2022.02.10 |
[Spring Batch] Data 를 CSV 파일로 만들기 (0) | 2022.01.23 |
[Spring] MultiModule 의존성 순서 설정으로 인한 오류 (0) | 2022.01.17 |