프록시(Proxy) 패턴
프록시 패턴은 실제 기능을 수행하는 객체 대신에 가상의 객체를 사용해 로직의 흐름을 제어(접근 제어가 목적)하는 디자인 패턴 입니다.
클라이언트는 서버에 필요한 것을 요청하고, 서버는 클라이언트의 요청을 처리하는 일을 합니다.
프록시는 클라이언트가 요청한 결과를 서버에 직접 요청하는것이 아니라 어떤 대리자를 통해 간접적으로 서버에 요청하는것을 말합니다.
프록시의 주요 기능
- 접근제어
- 권한에 따른 접근 차단
- 캐싱
- 지연로딩
- 부가 기능추가
- 원래 서버가 제공하는 기능에 더해서 부가 기능을 수행한다.
- 예) 요청 값이나, 응답 값을 중간에 변경한다.
- 예) 실행 시간을 측정해서 추가 로그를 남긴다.
프록시(Proxy) 패턴 예시
SNS 서비스가 있습니다. 이 SNS의 글의 목록을 조회하여 화면에 보여주고 스크롤 할 때 나머지 목록을 화면에 보여준다.
인스타그램을 예를 들어 모든 페이지를 가져오는것이 아니라 특정 스크롤을 내리면 프록시를 통해 가져오게 됩니다.
FeedList는 Feed라는 상위 클래스에 의존합니다. 여기서 RealFeed는 실제 피드를 로딩하는 구현 클래스이고 ProxyFeed가 프록시 패턴에서 프록시 역할을 한다.
public class ProxyFeed implements Feed {
private String name;
private Feed feed;
public ProxyFeed(Feed feed, String name) {
this.feed = feed;
this.name = name;
}
@Override
public String load() {
System.out.println("프록시로 사용합니다.");
feed.load();
return "data";
}
}
public class RealFeed implements Feed {
private final String name;
public RealFeed(String name) {
this.name = name;
}
@Override
public void load() {
System.out.println("피드를 로딩합니다.");
}
}
RealFeed는 실제 피드를 로딩하는 역할을 한다.
public class FeedList {
private List<Feed> feeds;
public FeedList(List<Feed> feeds) {
this.feeds = feeds;
}
public void onScroll(final int start, final int end) {
for (int i = start; i <= end; i++) {
final Feed feed = feeds.get(i);
feed.load();
}
}
}
FeedList는 Feed 인터페이스를 의존하고 있습니다. 하지만 이게 실제 피드인지 프록시 피드인지 알지는 못한다.
단지 Feed 타입의 load() 메소드를 이용해 피드를 로딩해달라고 합니다.
FeedList는 피드 리스트를 가지고 있고, 스크롤을 내렸을때 특정 피드를 로딩하는 책임을 지니고 있습니다.
public class ProxyTest {
@Test
void proxyPattern() {
final List<String> names = Arrays.asList("feed1", "feed2", "feed3", "feed4", "feed5", "feed6");
final List<Feed> feeds = new ArrayList<>(names.size());
for (int i = 0; i < names.size(); i++) {
if (i <= 3) {
feeds.add(new RealFeed(names.get(i)));
continue;
}
feeds.add(new ProxyFeed(new RealFeed(names.get(i)), names.get(i)));
}
final FeedList feedList = new FeedList(feeds);
feedList.onScroll(0, 3);
System.out.println();
feedList.onScroll(4, 5);
}
}
코드를 실행해 피드를 불러오도록 하겠습니다. 상위 4개에 대해서는 바로 로딩이 되었다는 메시지가 출력되고, 나머지는 프록시를 사용한다는 메시지와 피드를 로딩한다는 메시지가 출력 됩니다.
데코레이터(Decorator) 패턴
데코레이터 패턴은 프록시 패턴과 비슷하게 생겼지만 새로운 기능을 동적으로 추가한다는것이 목적이다.
여기서 동적으로 추가할때는 보통 특정 객체를 결합하는 방식을 사용한다.
프록시 패턴과 데코레이터 패턴 둘다 프록시를 사용하지만, 의도가 다르다는 점이 핵심이다.
데코레이터 패턴 예제
위의 프록시 예제에서 프록시를 통해 피드를 로딩할때 시간 측정과 피드의 이름을 출력하는 기능을 추가해야 한다는 요구사항이 들어왔다.
이때 그냥 시간 측정과 피드이름을 출력할 수 있는 데코레이터를 만들어 주면 된다.
public class TimeDecorator implements Feed {
private Feed feed;
public TimeDecorator(Feed feed) {
this.feed = feed;
}
@Override
public String load() {
long startTime = System.currentTimeMillis();
String result = feed.load();
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
System.out.println("피드 로딩 종료 resultTime=" + resultTime + "ms, feedName=" + result );
return result;
}
}
public class ProxyTest {
@Test
void proxyPattern() {
final List<String> names = Arrays.asList("feed1", "feed2", "feed3", "feed4", "feed5", "feed6");
final List<Feed> feeds = new ArrayList<>(names.size());
for (int i = 0; i < names.size(); i++) {
if (i <= 3) {
feeds.add(new RealFeed(names.get(i)));
continue;
}
feeds.add(new TimeDecorator(new ProxyFeed(new RealFeed(names.get(i)), names.get(i))));
}
final FeedList feedList = new FeedList(feeds);
feedList.onScroll(0, 3);
System.out.println();
feedList.onScroll(4, 5);
}
}
위와 같이 만들고 대신 클라이언트에서 기존의 Proxy대신 TimeDecorator를 호출해주면 된다.
데코레이터 패턴의 최대 장점은 이미 있는 피드를 변경하지 않은 채 데코레이터를 이어 붙여서 정의할 수 있다는 장점이 있다.
Spring에서 프록시 패턴을 적용한 사례
스프링에서는 스프링 AOP는 여러 코드에 흩어질 수 있는 다양한 코드를 한곳에 Aspect라는 개념으로 모아서 코딩할 수 있게하는 기법입니다.
@Aspect
@Component
public class Aspect {
@Around("bean(service)")
public void timestamp(ProceedingJoinpoint point) throws Throwable {
long startTime = System.currentTimeMillis();
point.proceed();
System.out.println(System.currentTimeMillis() - startTime);
}
}
위와 같이 service 빈을 찾아 시간측정을하게 되는데 이렇게 빈을 등록해주면 service 빈 타입의 또 다른 프록시 빈을 생성만들어 줍니다. 그리고 이 service를 사용하는곳에서는 프록시 빈을 주입 받아 사용하게 됩니다.
'Spring Boot' 카테고리의 다른 글
제어할수 없는 것에 의존하지 않기(테스트하기 좋은 코드) (1) | 2023.11.05 |
---|---|
QueryDSL 다중 DB 설정하기 (0) | 2023.06.27 |
[Git] Permission denied (publickey). fatal- Could not read from remote repository (0) | 2022.10.09 |
Spring WebSocket (1) | 2022.09.09 |
[Spring] DTO는 왜 써야 하나요? (0) | 2022.08.18 |
댓글