공부해봅시당
[Spring] 프록시와 디자인패턴 본문
1. 프록시(Proxy)란?
용어
프록시(Proxy)
정의
대리자 라는 뜻으로,
클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 역할
특징
- 실제 대상인 것처럼 위장함으로서 이를 사용하는 클라이언트는 구체 클래스를 알 필요가 없어짐
- 클라이언트의 요청을 받아서 원래 요청 대상에게 바로 넘겨주는 게 아닌, 다양한 부가기능을 지원할 수 있음
타깃
여기서 원래 요청하려는 대상, 즉 최종적으로 요청을 위임받아 처리하는 실제 오브젝트
프록시의 조건
클라이언트의 요청을 대리로 수행해주는 모든 객체가 프록시 인것은 아님
객체가 프록시가 되려면 클라이언트는 요청을 보낸 대상이 타깃인지 프록시인지 구분을 할 수 없어야 함
즉, 타깃과 프록시는 같은 인터페이스를 확장해야 함
(CGLib처럼 구현 클래스를 상속받는 방법도 있음)
이로써 느슨한 연결을 유지하며, OCP(개방-폐쇄원칙)를 통해 좋은 코드를 작성할 수 있음
2. 디자인 패턴
프록시는 사용목적(intent)에 따라 두 가지로 구분할 수 있음
부가적인 기능 부여 → 데코레이터(Decorator) 패턴
접근제어 → 프록시(Proxy) 패턴
2-1. 데코레이터 패턴
런타임 시, 타깃에 부가적인 기능을 다이내믹하게 부여하기 위해 프록시를 사용하는 패턴
특징
- 타깃에 부가적인 기능을 부여해줄 수 있음
ex. 선물 상자를 포장지로 꾸미는 것
- 하나의 타깃에도 다양한 부가기능을 추가하기 위해 한 개 이상의 프록시를 사용할 수 있음
ex. 선물 상자를 꾸미는데 포장지 제한이 없는 것
예시
- TargetInterface.java
타깃 구현 클래스와 프록시의 부모 클래스로 run() 메서드를 선언
- TargetImpl.java
타깃 구현 클래스로 run() 메서드는 간단한 로그를 남김
- Proxy.java
TargetInterface를 선언하며 타깃의 핵심 로직 전후에 로그를 남김
- Client.java
TargetInterface를 의존하며 execute() 메서드로 요청을 보내는 사용자
TargetInterface.java
public interface TargetInterface {
void run();
}
TargetImpl.java
@Slf4j
public class TargetImpl implements TargetInterface{
@Override
public void run() {
log.info("TargetImpl.run() 실행"); //이 부분이 핵심(비즈니스)로직이라고 가정합니다.
}
}
Proxy.java
데코레이터의 역할을 수행하는 클래스
run() 메서드의 target.run()을 제외한 나머지는 모두 부가적인 기능을 부여하는 역할을 수행하게 됨
@Slf4j
public class Proxy implements TargetInterface{
private final TargetInterface target; //타깃 오브젝트를 멤버 변수로 유지해야 합니다.
public Proxy(TargetInterface target) { //생성자 주입
this.target = target;
}
@Override
public void run() {
log.info("DecoratorProxy 시작"); //부가적인 기능을 부여하는 것이라 가정합니다.
target.run(); //실제 타깃의 핵심로직 실행
log.info("DecoratorProxy 종료"); //부가적인 기능을 부여하는 것이라 가정합니다.
}
}
Client.java
public class Client {
private TargetInterface targetInterface; //구현 클래스가 아닌 인터페이스에 의존
//런타임 시 구체적 의존관계가 설정됨
public Client(TargetInterface targetInterface) { //생성자 주입
this.targetInterface = targetInterface;
}
public void execute(){
targetInterface.run();
}
}
DecoratorPattern.java
public class DecoratorPatternTest {
@Test
void noDecoratorPattern(){
TargetInterface target = new TargetImpl();
Client client = new Client(target);
client.execute();
}
@Test
void decoratorPattern(){
TargetInterface target = new TargetImpl();
TargetInterface proxy = new Proxy(target);
Client client = new Client(proxy);
client.execute();
}
}
결과
2-2. 프록시 패턴
타깃에 대한 접근 방법을 제어하려는 목적을 가지고 프록시를 사용하는 패턴
특징
- 프로젝트에서 객체를 생성하는 일은 언제나 비용이 소모됨
따라서 객체를 최소한으로 생성할수록, 필요 시점까지 생성을 미룰수록 좋음
- 프록시 메서드를 통해 실사용 시 타깃 오브젝트를 생성할 수 있음
프록시를 이용하면 타깃 오브젝트에 대한 레퍼런스가 미리 필요할 때,
객체를 생성해서 넘겨주지 않고 프록시를 먼저 넘겨준 후
프록시의 메서드를 통해 실제로 사용될 때 타깃 오브젝트를 생성할 수 있음
- 특정 상황에서 타깃에 대한 접근권한을 제어할 수 있음
특정 조건이 만족되면 타깃의 핵심 로직을 호출하기 전에 예외를 던져서 접근을 불가능하게 만들 수 있음
- 캐싱(cache) 가능
타깃으로부터 응답으로 받은 데이터가 메모리에 존재할 때,
프록시는 타깃으로 요청을 보내지 않고, 기존 응답의 데이터를 클라이언트에게 전달할 수 있음
예시
(대부분의 코드가 데코레이터 패턴 예시와 동일하므로 변경된 클래스들만 설명하겠음)
- Proxy1.java : 캐싱 기능
private String data를 사용하여 null이라면 타깃 메서드를 호출하고, null이 아니라면 해당 값을 반환
- Proxy2.java : 접근 제한 기능
private int count를 사용하여 3보다 작다면 타깃에 접근 가능하지만, 크다면 예외가 던져져 접근을 제한
Proxy1.java (캐싱 기능)
public class Proxy1 implements TargetInterface{
private TargetInterface target; //타깃 오브젝트를 멤버 변수로 유지해야 합니다.
private String data; //메모리에 저장된 캐싱용 데이터라고 가정합니다.(다른 방법으로 구현가능)
public Proxy1(TargetInterface target) {
this.target = target;
}
@Override
public String run() {
if(data==null){ //메모리에 저장된 데이터가 없다면
data = target.run(); //타겟의 비즈니스 로직을 호출합니다.
}
return data; //메모리에 데이터가 있다면 타깃에 접근하지 않으며 비용을 절약합니다.
}
}
Proxy2.java (접근 제한 기능)
public class Proxy2 implements TargetInterface{
private TargetInterface target; //타깃 오브젝트를 멤버 변수로 유지해야 합니다.
private int count; //접근 횟수를 멤벼 변수로 저장합니다.
public Proxy2(TargetInterface target) {
this.target = target;
}
@Override
public String run() {
if(++count>3){ //접근 횟수가 3회 이상이라면 예외를 던져 접근을 제한합니다.
throw new RuntimeException("접근 제한!!");
}
return target.run();
}
}
ProxyPatternTest.java
import blog.proxy.proxy.*;
import org.junit.jupiter.api.Test;
public class ProxyPatternTest {
@Test
void noProxyPattern(){
TargetInterface target = new TargetImpl();
Client client = new Client(target);
client.execute();
}
@Test
void proxyPattern1(){
TargetInterface target = new TargetImpl();
TargetInterface proxy = new Proxy1(target);
Client client = new Client(proxy);
for(int i=0;i<4;i++){
client.execute();
}
}
@Test
void proxyPattern2(){
TargetInterface target = new TargetImpl();
TargetInterface proxy = new Proxy2(target);
Client client = new Client(proxy);
for(int i=0;i<4;i++){
client.execute();
}
}
}
결과 화면
proxyPattern1.test는 client.execute()가 4번 호출되었지만,
프록시에서 첫번째 호출 외에는 캐싱된 데이터를 반환했기 때문에 타깃에는 한번만 접근함
proxyPattern2.test도 마찬가지로 client.execute()가 4번 호출되었지만,
프록시에서 3번째 접근후에는 예외를 던져 접근을 제한했기 때문에 3번째 로그를 남긴 후 예외가 발생함
참조
https://yejun-the-developer.tistory.com/5
[Spring] 프록시와 디자인패턴
프록시와 디자인 패턴 스프링의 3대 기반기술 중 AOP를 공부하던 중 관심사 분리를 위한 다이내믹 프록시와 팩토리 빈이라는 개념의 등장에 당황했습니다. 평소 객체지향과 디자인 패턴을 공부
yejun-the-developer.tistory.com
'STUDY > Spring' 카테고리의 다른 글
[Spring] WebServer에서 CGI, Servlet, Spring Web MVC의 DispatcherServlet까지 (1) | 2024.04.20 |
---|---|
[Spring] 흐름으로 이해해보는 AOP와 프록시 (0) | 2024.03.02 |
[Spring] 스프링빈(Spring Bean) (0) | 2024.02.28 |
[Spring] 필터(Filter) vs 인터셉터(Intercepter) (2) | 2024.02.28 |
[Spring] @SpringBootTest와 @DataJpaTest (0) | 2024.02.23 |