본문 바로가기
Design Pattern/구조 패턴(Structural Patterns)

프록시(Proxy) 패턴 - 구조 패턴 (Structural Patterns)

by 김 민 준 2024. 5. 23.

 

프록시 패턴은 다른 개체에 대한 접근을 제어하기 위한 디자인 패턴이다. 프록시 개체는 실제 개체에 대한 인터페이스를 제공하면서, 접근을 제어하거나 추가 기능을 수행할 수 있다. 실제 개체에 대한 접근을 통제하고, 로깅, 캐싱, 권한 확인 등 다양한 추가 기능을 제공할 수 있다.

 

프록시 패턴에는 종류가 있다. 

 

1. 가상 프록시 (Virtual Proxy) : 실제 개체의 생성을 지연하여, 필요한 시점에만 생성한다. 예로 이미지 로딩을 지연시킬 때 사용할 수 있다. 

2. 보호 프록시 (Protection Proxy) : 접근 제어를 위해 사용된다. 예로  권한 접근을 제한할때 사용할 수 있다.

3. 원격 프록시 (Remote Proxy) :  원격 개체에 대한 접근을 제어한다. 예로 서버의 개체를 로컬에 접근할 때 사용한다. 

4. 스마트 프록시 (Smart Proxy) : 접근 이외의 추가 기능을 제공한다. 예로 참조 횟수를 계산하거나 로깅 기능을 추가할 때 사용할 수 있다. 

 

간단한 예시 코드를 살펴보자

 

아래 코드는 이미지 로딩을 지연시키는 가상 프록시 예제 소스코드이다. 

 

// Image 인터페이스: 공통 인터페이스 정의
interface Image {
    void display();
}
// RealImage 클래스: 실제 객체
class RealImage implements Image {
    private String filename;

    public RealImage(String filename) {
        this.filename = filename;
        loadFromDisk();
    }

    private void loadFromDisk() {
        System.out.println("Loading " + filename);
    }

    @Override
    public void display() {
        System.out.println("Displaying " + filename);
    }
}

 

// ProxyImage 클래스: 프록시 객체
class ProxyImage implements Image {
    private String filename;
    private RealImage realImage;

    public ProxyImage(String filename) {
        this.filename = filename;
    }

    @Override
    public void display() {
        if (realImage == null) {
            realImage = new RealImage(filename);
        }
        realImage.display();
    }
}

 

// 사용 예시
public class ProxyPatternDemo {
    public static void main(String[] args) {
        Image image = new ProxyImage("test_image.jpg");

        // 처음에는 실제 이미지 로딩
        image.display();
        System.out.println("");

        // 두 번째 호출부터는 캐시된 이미지를 사용
        image.display();
    }
}

//출력 

//Loading test_image.jpg
//Displaying test_image.jpg

//Displaying test_image.jpg

 

 

소스코드를 살펴보면 프록시 개체에서 길제 개체에 대한 참조를 관리하는것을 확인할 수 있다. 생성을 지연시키고, 실제 개체가 필요할때만 생성한다. 그리고 생성 된 후에 실제 개체의 메서드를 호출한다.

 

이번에는 보호 프록시를 살펴보자 

 

// Database 인터페이스: 공통 인터페이스 정의
interface Database {
    void query(String sql);
}
// RealDatabase 클래스: 실제 데이터베이스 객체
class RealDatabase implements Database {
    @Override
    public void query(String sql) {
        System.out.println("Executing query: " + sql);
    }
}

 

// DatabaseProxy 클래스: 보호 프록시 객체
class DatabaseProxy implements Database {
    private RealDatabase realDatabase;
    private String userRole;

    public DatabaseProxy(String userRole) {
        this.realDatabase = new RealDatabase();
        this.userRole = userRole;
    }

    @Override
    public void query(String sql) {
        if ("ADMIN".equals(userRole)) {
            realDatabase.query(sql);
        } else {
            System.out.println("Access denied for user role: " + userRole);
        }
    }
}
// 사용 예시
public class ProxyPatternDemo {
    public static void main(String[] args) {
        Database adminDatabase = new DatabaseProxy("ADMIN");
        adminDatabase.query("SELECT * FROM users");

        Database userDatabase = new DatabaseProxy("USER");
        userDatabase.query("SELECT * FROM users");
    }
}

//출력 

//Executing query: SELECT * FROM users
//Access denied for user role: USER

 

데이터 베이스 접근에 보호 프록시 패턴을 적용한 소스코드이다. 소스코드에서 확인할 수 있듯이 권한이 없는 사용자가 민감한 데이터에 접근하지 못하도록 할 수 있다.

 

 

이번엔 원격 프록시 예제 소스코드를 살펴보자.

 

// FileServer 인터페이스: 공통 인터페이스 정의
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface FileServer extends Remote {
    void uploadFile(String fileName, byte[] data) throws RemoteException;
    byte[] downloadFile(String fileName) throws RemoteException;
}

 

// RealFileServer 클래스: 실제 원격 객체 구현
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import java.util.Map;

public class RealFileServer extends UnicastRemoteObject implements FileServer {
    private Map<String, byte[]> fileStorage;

    protected RealFileServer() throws RemoteException {
        fileStorage = new HashMap<>();
    }

    @Override
    public void uploadFile(String fileName, byte[] data) throws RemoteException {
        fileStorage.put(fileName, data);
        System.out.println("File uploaded: " + fileName);
    }

    @Override
    public byte[] downloadFile(String fileName) throws RemoteException {
        System.out.println("File downloaded: " + fileName);
        return fileStorage.get(fileName);
    }
}
// ClientFileServerProxy 클래스: 원격 프록시 구현
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class ClientFileServerProxy implements FileServer {
    private FileServer remoteFileServer;

    public ClientFileServerProxy(String host, int port) {
        try {
            Registry registry = LocateRegistry.getRegistry(host, port);
            remoteFileServer = (FileServer) registry.lookup("FileServer");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void uploadFile(String fileName, byte[] data) {
        try {
            remoteFileServer.uploadFile(fileName, data);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public byte[] downloadFile(String fileName) {
        try {
            return remoteFileServer.downloadFile(fileName);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}
// 사용 예시
public class RemoteProxyDemo {
    public static void main(String[] args) {
        // RMI 서버에 FileServer 객체 등록 (서버 측 코드)
        // ...
        
        // 클라이언트 측 코드
        ClientFileServerProxy fileServerProxy = new ClientFileServerProxy("localhost", 1099);
        fileServerProxy.uploadFile("example.txt", "Hello, world!".getBytes());
        byte[] data = fileServerProxy.downloadFile("example.txt");
        System.out.println(new String(data));
    }
}

 

파일 서버 등 원격 개체에 대한 접근을 제어하는 소스코드를 확인 할 수 있다.

 

그럼 마지막으로 스마트 프록시에 대해서 예시 소스코드를 살펴보자.

 

// Database 인터페이스: 공통 인터페이스 정의
interface Database {
    void query(String sql);
}
// RealDatabase 클래스: 실제 데이터베이스 객체
class RealDatabase implements Database {
    @Override
    public void query(String sql) {
        System.out.println("Executing query: " + sql);
    }
}

 

// LoggingDatabaseProxy 클래스: 로깅 스마트 프록시 구현
class LoggingDatabaseProxy implements Database {
    private RealDatabase realDatabase;

    public LoggingDatabaseProxy(RealDatabase realDatabase) {
        this.realDatabase = realDatabase;
    }

    @Override
    public void query(String sql) {
        System.out.println("Logging: About to execute query: " + sql);
        realDatabase.query(sql);
        System.out.println("Logging: Finished executing query: " + sql);
    }
}

 

// 사용 예시
public class SmartProxyDemo {
    public static void main(String[] args) {
        RealDatabase realDatabase = new RealDatabase();
        Database loggingProxy = new LoggingDatabaseProxy(realDatabase);

        loggingProxy.query("SELECT * FROM users");
    }
}

//출력 

//Logging: About to execute query: SELECT * FROM users
//Executing query: SELECT * FROM users
//Logging: Finished executing query: SELECT * FROM users

 

스마트 프록시를 소스코드를 보면 데이터 베이스 접근에 대한 로깅 기능을 추가한 소스코드라는것을 확인 할 수 있다.

 

무에서 프록시를 다룰일이 엄청 자주 발생하지 않는다. 이는 대부분의 이런 기능들은 라이브러리나 프레임워크에 종속되어 있고 우리는 이를 이용하여 개발을 진행하는경우가 많다. 

 

예를 들어 스프링의 @Transactional 어노테이션은 내부적으로 프록시를 사용하여 메서드 실행 전후에 트랜젝션을 관리한다. 그리고 로깅이나 모니터링도 AOP를 통해 구현하는 경우가 많으며, 보안은 Spring Security를 이용하는 경우가 많고 이때 메서드 실행 전후에 보안검사를 수행하기 위해 프록시 패턴을 사용하는 경우가 있다. 캐싱 역시 Spring Cache를 사용하는 등 일반적으로 실무에서 개발자가 직접 구현하는 경우는 많이 드물기는 하다. 하지만 해당 라이브러리나 프레임워크를 사용함에 있어 내부 동작에 대한 원리를 100% 이해하지 않더라도 어떤 프록시를 사용하고 있겠구나 정도는 이해하는게 문제가 발생했을때 대응하기 조금 더 좋지 않을까?