Proxy Parttern이란?
특정 객체에 대한 접근을 제어하거나 기능을 추가할 수 있는 구조 패턴이다.
다이어그램 설명
- Client:
- 클라이언트 객체는 주체(Subject) 인터페이스를 통해 프록시 객체에 접근함.
- Subject:
- 주체 인터페이스는 실제 객체와 프록시 객체가 구현해야 하는 인터페이스를 정의함. (클래스보다는 인터페이스)
- RealSubject:
- 실제 주체는 주체 인터페이스를 구현하는 실제 객체.
- 실제 작업을 수행하는 클래스.
- Proxy:
- 프록시 클래스도 실제 객체처럼 주체 인터페이스를 구현함.
- 실제 주체 객체에 대한 참조를 가지고 있으며, 필요에 따라 실제 객체를 생성하고 작업을 위임함.
- 접근 제어, 지연 초기화, 로깅 등의 추가 기능을 제공함.
전체 동작 흐름
- 클라이언트(Client)가 Proxy 객체를 생성
- 클라이언트는 Proxy 객체를 생성하여 Subject 인터페이스를 통해 접근함.
- 클라이언트가 Proxy 객체의 메서드를 호출
- 클라이언트는 Proxy 객체의 doAction() 메서드를 호출함.
- Proxy 객체가 실제 객체 생성 및 작업 위임
- Proxy 객체는 실제 객체(RealSubject)가 생성되지 않았으면 생성하고, 그 후에 doAction() 메서드를 호출하여 실제 작업을 위임함.
- RealSubject가 실제 작업 수행
- RealSubject는 실제 작업을 수행하고 결과를 반환함.
Proxy Pattern을 쓰는 이유(==기존 개발 형태에 대한 문제점)?
기존 개발의 문제점
1. 객체의 무거운 초기화 비용
2. 객체의 메모리 사용 최적화
3. 접근 제어(권한 관리나 보안 문제)
4. 기존 서비스에 추가나 수정 없이 추가적인 기능(로깅, 모니터링)등을 넣고 싶을 때가 생김.
5. 원격 객체에 대한 접근
우선 1번~4번까지는 이해했으나 아직 원격 객체에 대한 접근 관련한 문제점은 필자가 이해하지 못했으므로 생략하겠습니다..!
1. 객체의 무거운 초기화 비용
public class RealImage {
private String filename;
public RealImage(String filename) {
this.filename = filename;
loadFromDisk(filename);
}
private void loadFromDisk(String filename) {
System.out.println("Loading " + filename);
}
public void display() {
System.out.println("Displaying " + filename);
}
}
public class ImageViewer {
private RealImage realImage;
public ImageViewer(String filename) {
this.realImage = new RealImage(filename);
}
public void displayImage() {
realImage.display();
}
public static void main(String[] args) {
ImageViewer viewer = new ImageViewer("test_image.jpg");
viewer.displayImage(); // Loading test_image.jpg \n Displaying test_image.jpg
}
}
어떤 객체의 초기화가 매우 비용이 많이 드는 경우,
해당 객체를 사용하지 않을 때에도 미리 초기화하는 것은 성능 저하를 일으킬 수 있음.
2. 객체의 메모리 사용 최적화 필요
1. 객체의 무거운 초기화 비용과 비슷한 문제점이다.
불필요한 객체를 미리 생성해두면 메모리 사용에 낭비가 될수 있다.
- 자주 사용되지 않는 객체를 미리 생성하면 메모리가 낭비될 수 있음.
- 객체에 대한 참조를 직접 관리하면, 참조 카운팅과 같은 메모리 관리 로직이 복잡해질 수 있음.
3. 접근 제어 (권한 관리나 보안 문제)
- 특정 객체에 대한 접근 권한을 관리하고 제어해야 할 때, 이를 직접 구현하면 코드가 복잡해질 수 있음.
- 중요한 데이터나 기능에 대한 접근을 제한하지 않으면 보안 취약점이 발생할 수 있음.
public class RealDocument {
private String filename;
public RealDocument(String filename) {
this.filename = filename;
loadFromDisk(filename);
}
private void loadFromDisk(String filename) {
System.out.println("Loading " + filename);
}
public void display() {
System.out.println("Displaying " + filename);
}
}
public class DocumentViewer {
private RealDocument document;
public DocumentViewer(String filename) {
this.document = new RealDocument(filename);
}
public void displayDocument() {
document.display();
}
public static void main(String[] args) {
DocumentViewer viewer = new DocumentViewer("secure_document.pdf");
viewer.displayDocument(); // Loading secure_document.pdf \n Displaying secure_document.pdf
}
}
위와 같이 접근 권한 없이도 모든 사용자가 문서를 볼수 있게 되는데
이때 권한관리를 추가한다고 해보자.
그럼 RealDocument 클래스에 필드를 사용자 값도 추가하고 loadFromDisk와 display 메소드에서 String으로 들어오는 사용자 값을 일일히 if문으로 체크해서 보여주거나 막아야한다.
4. 기존 서비스에 추가나 수정 없이 추가적인 기능(로깅, 모니터링)등을 넣고 싶을 때가 생김.
- 메서드 호출 전후로 로깅이나 모니터링 코드가 산재하면, 코드가 복잡해지고 유지보수가 어려워짐.
- 비즈니스 로직과 로깅/모니터링 로직이 뒤섞여 있으면, 코드의 가독성과 유지보수성이 떨어짐.
public class RealService {
public void performOperation() {
System.out.println("Performing operation in RealService");
}
}
public class ServiceClient {
private RealService service;
public ServiceClient() {
this.service = new RealService();
}
public void useService() {
System.out.println("Logging: Before operation");
service.performOperation();
System.out.println("Logging: After operation");
}
public static void main(String[] args) {
ServiceClient client = new ServiceClient();
client.useService();
}
}
useService 메서드에 로깅 관련 코드를 추가해놓으면 책임도 분리되어있지 않고 수정에도 열려있지 않다.
5. 원격 객체에 대한 접근
(해당 부분은 추후 학습을 추가하여 다시 작성해 놓겠습니다.)
Proxy Pattern( 패턴) 구현 방법
다이어그램처럼 구현하면 된다.
1. 우리는 실제 객체를 바로 구현하기 보다 인터페이스로 먼저 구현해놓는다.
2. 실제 사용할 객체는 구현해놓은 인터페이스를 implements하여 구현한다.
3. 프록시 객체(대리 객체)도 실제 객체와 같이 인터페이스를 implements하여 구현하고, 실제 객체를 참조 변수로 둔다.
4. 클라이언트에서는 인터페이스를 통해 프록시 객체에 접근한다.
6. 생성된 프록시 객체가 실제 객체에 대한 참조로 필요할때 실제 객체를 생성하고 작업을 위임한다.
프록시 패턴 적용 예시: 접근 권한에 따라 문서 표시
1. Document 인터페이스 구현
public interface Document {
void display();
}
2. 실제 객체인 RealDocument
public class RealDocument implements Document {
private String filename;
public RealDocument(String filename) {
this.filename = filename;
loadFromDisk(filename);
}
private void loadFromDisk(String filename) {
System.out.println("Loading " + filename);
}
@Override
public void display() {
System.out.println("Displaying " + filename);
}
}
3. 대리 객체인 프록시 객체, DocumentProxy
public class DocumentProxy implements Document {
private RealDocument realDocument;
private String filename;
private boolean isAdmin;
public DocumentProxy(String filename, boolean isAdmin) {
this.filename = filename;
this.isAdmin = isAdmin;
}
@Override
public void display() {
if (isAdmin) {
if (realDocument == null) {
realDocument = new RealDocument(filename);
}
realDocument.display();
} else {
System.out.println("Access Denied: You do not have permission to view this document.");
}
}
}
4. 실제 클라이언트에서 인터페이스를 통해 프록시 객체에 접근
public class DocumentViewer {
private Document document;
public DocumentViewer(String filename, boolean isAdmin) {
this.document = new DocumentProxy(filename, isAdmin);
}
public void displayDocument() {
document.display();
}
public static void main(String[] args) {
DocumentViewer viewer = new DocumentViewer("secure_document.pdf", false);
viewer.displayDocument(); // Access Denied: You do not have permission to view this document.
DocumentViewer adminViewer = new DocumentViewer("secure_document.pdf", true);
adminViewer.displayDocument(); // Loading secure_document.pdf \n Displaying secure_document.pdf
}
}
Proxy Pattern의 장단점
장점
기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있음 (OCP 충족)
기존 코드가 해야하는 일만 유지할 수 있음. (SRP 충족)
기능 추가 및 초기화 지연 등 다양하게 활용 가능함(캐싱 관련해서 메모리도 절약됨)
단점
코드의 복잡도가 증가함.
프록시 패턴이 쓰이는 곳
자바 > 다이나믹 프록시, java.lang.reflect.Proxy
Java의 다이나믹 프록시는 런타임에 동적으로 프록시 객체를 생성할 수 있는 기능을 제공한다.
장점
1. 유연성: 런타임에 프록시 객체를 동적으로 생성하므로 컴파일 타임에 프록시 클래스를 정의할 필요가 없다.
2. 반복 코드 감소: 또한 공통된 기능을 프록시 객체에서 처리해서 실제 비즈니스 로직 코드가 중복되는 것을 막을 수 있다.
간단히 구현한 다이나믹 프록시 핸들러
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class DynamicProxyHandler implements InvocationHandler {
private Object realObject;
public DynamicProxyHandler(Object realObject) {
this.realObject = realObject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Proxy: Before method call");
Object result = method.invoke(realObject, args);
System.out.println("Proxy: After method call");
return result;
}
}
public class Client {
public static void main(String[] args) {
RealSubject realSubject = new RealSubject();
Subject proxyInstance = (Subject) Proxy.newProxyInstance(
realSubject.getClass().getClassLoader(),
realSubject.getClass().getInterfaces(),
new DynamicProxyHandler(realSubject)
);
proxyInstance.doAction();
// Proxy: Before method call
// RealSubject: Performing action
// Proxy: After method call
}
}
- InvocationHandler: 다이나믹 프록시가 호출될 때 실행되는 메서드 호출을 가로채는 핸들러.
- Proxy.newProxyInstance: 런타임에 프록시 객체를 생성하는 메서드. 원본 객체와 동일한 인터페이스를 구현하는 프록시 객체를 생성함.
스프링 > 스프링 AOP
앞서 본
<4. 기존 서비스에 추가나 수정 없이 추가적인 기능(로깅, 모니터링)등을 넣고 싶을 때가 생김. > 과 같은 경우처럼
추가적인 기능을 넣고 싶을 때 스프링에서는 스프링 AOP를 사용할 수 있다.
간단한 AOP 예제
public interface MyService {
void performAction();
}
public class MyServiceImpl implements MyService {
@Override
public void performAction() {
System.out.println("Performing action in MyService");
}
}
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class LoggingAspect {
@Before("execution(* MyService.performAction(..))")
public void logBefore() {
System.out.println("LoggingAspect: Before performing action");
}
}
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Client {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
MyService myService = (MyService) context.getBean("myService");
myService.performAction();
// LoggingAspect: Before performing action
// Performing action in MyService
}
}
- Aspect: AOP에서 관심사(Concern)를 모듈화한 것. 여기서는 로깅을 위한 LoggingAspect가 Aspect 역할을 함.
- Advice: 특정 조언(Advice)을 실행할 시점을 정의. 여기서는 메서드 실행 전(@Before)에 로깅을 수행함..
- Pointcut: Advice가 적용될 지점을 정의. 여기서는 MyService.performAction 메서드에 적용됨.
- Spring AOP: 스프링 AOP는 프록시를 사용하여 메서드 호출을 가로채고, 설정된 Aspect를 적용함.
참조
코딩으로 학습하는 GoF의 디자인 패턴 | 백기선 - 인프런
백기선 | 디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를
www.inflearn.com
'CS > 디자인패턴' 카테고리의 다른 글
[Design Pattern] Facade Pattern(퍼사드 패턴) (0) | 2024.06.05 |
---|---|
[Design Pattern] Decorator Pattern(데코레이터 패턴) (0) | 2024.06.05 |
[Design Pattern] Strategy Pattern(전략 패턴) (0) | 2024.06.02 |
[Design Pattern] Factory Pattern(팩토리 패턴) (0) | 2024.06.01 |
[Design Pattern] Singleton Pattern(싱글톤 패턴) (0) | 2024.05.31 |