디자인 패턴 - 프록시 패턴(Proxy Pattern)

박진형·2022년 3월 24일
1
post-thumbnail

프록시 패턴이란?

프록시 패턴의 프록시(Proxy)는 '대리' 라는 뜻을 가졌다. 이름 그대로 무엇인가를 대신 처리한다는 의미다. 프록시 서버, 리버스 프록시, 프록시 패턴 등에서의 프록시는 그런 의미에서 사용된다.

그림에서 알 수 있듯이 RealSubject의 메소드를 Proxy 객체를 통해 대신 처리하는 것이다.

그렇다면 프록시 패턴으로 실제 객체의 메소드 사용 이전에 로직을 추가해 메소드의 사용 권한을 제한할 수도 있을것이고. 메소드 사용 이후에 별도의 후처리 로직을 추가할 수도 있을 것이다. 또한 실제 객체의 생성을 지연시켜 메모리의 낭비를 줄일 수 있는 역할을 할 수도 있을 것이다.

프록시 패턴의 활용

프록시 패턴은 아래와 같이 다양한 방법으로 활용될 수 있다.

가상 프록시 - 객체 생성을 필요한 시점까지 지연시켜 메모리의 낭비를 줄여준다.

보호 프록시 - 특정 클라이언트만 접근할 수 있도록 액세스 제어를 할 수 있다.

원격 프록시 - 원격 객체에 대한 접근을 마치 로컬 객체를 통해서 접근하는 것처럼 할 수 있다.

로깅 프록시 - 실제 객체의 요청 이력을 프록시 객체를 통해 로그를 남길 수 있다.

캐싱 프록시 - 요청에 대한 결과를 캐싱하고 캐시의 라이프 사이클을 관리할 수 있다.

스마트 참조 - 실제 객체에 액세스할 때 추가적인 작업을 수행할 수 있다. 실제 객체에 대한 포인터 수를 저장해 참조 목록이 비어 있다면 리소스를 해제할 수 있다.

프록시 패턴 구현

다양한 프록시 패턴의 활용이 있지만 나는 가상 프록시와 보호 프록시를 직접 구현 해봤다. 다른 프록시 패턴의 활용도 전체적인 틀은 비슷하고 그 용도가 다를 뿐인 것 같다.

가상 프록시

가상 프록시 패턴을 구현하며 설명하기에 앞서 특정 상황을 제시하고 설명하는 것이 좋을 것 같다.

시나리오

암호화된 이름을 인자로 받아 복호화해 저장하는 객체가 있고 이는 객체 생성시 바로 복호화 된다.
하지만 이 객체 생성과 복호화 과정은 높은 비용을 요구한다. 하나의 거대한 데이터 리스트를 열람한다고 가정하자 하지만 나는 당장은 이 중 1개의 객체의 이름만 가져오고 싶다. 일반적인 방법으로는 데이터 리스트를 유지하는 동안 모든 객체가 즉시 로딩이 되어 상당한 비용을 요구한다. 이럴 때 프록시 패턴을 이용해 접근하는 객체만 로딩하기로 한다.

구현

  • SecretProvider class
    암호화, 복호화 기능을 제공하는 클래스
public class SecretProvider {
  public static String decryptAes(String str, String key) throws Exception {
    System.out.println("decrypting...");

    Cipher cipher = Cipher.getInstance("AES");
    SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "AES");
    cipher.init(Cipher.DECRYPT_MODE, secretKey);
    byte[] decPassword = cipher.doFinal(Base64.getDecoder().decode(str));
    String result = new String(decPassword, "UTF-8");
    Thread.sleep(3000);
    System.out.println("decrypt complete!!!");
    return result;
  }

  public static String encryptAes(String str, String key) throws Exception {
    Cipher cipher = Cipher.getInstance("AES");
    SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "AES");
    cipher.init(Cipher.ENCRYPT_MODE, secretKey);
    byte[] encPassword = cipher.doFinal(str.getBytes("UTF-8"));
    String result = Base64.getEncoder().encodeToString(encPassword);
    return result;
  }
  • Subject interface
public interface Subject {
  String fetch() throws Exception;
}
  • HighCostSubject class
    이름을 저장하는 클래스, Subject를 implement 받는다
public class HighCostSubject implements Subject {

  private String plainData;

  public HighCostSubject(String secretData) throws Exception {
    this.plainData = SecretProvider.decryptAes(secretData, "oingisprettyintheworld1234567890");
  }

  public String fetch() {
    return plainData;
  }
}
  • ProxySubject class
    HighCostSubject를 포함하고 이것을 대신해서 fetch를 진행해준다.
public class ProxySubject implements Subject{

  private final String secureData;
  HighCostSubject subject;

  public ProxySubject(String secureData) {
    this.secureData = secureData;
  }

  @Override
  public String fetch() throws Exception {
    if (subject == null)
    {
      subject = new HighCostSubject(secureData);
    }
    return "proxy :" + subject.fetch();
  }
}
  • VirtualProxyTest class
    일반 방식으로 데이터를 생성하고, 프록시 패턴을 사용하여 데이터를 생성해봤다.
public class VirtualProxyTest {

  public static void main(String[] args) throws Exception {
    List<Subject> normalList = new ArrayList<>();
    List<Subject> proxyList = new ArrayList<>();

    System.out.println("=== 일반 객체로 추가 ===");
    long sTime =System.currentTimeMillis();
    normalList.add(new HighCostSubject(SecretProvider.encryptAes("park","oingisprettyintheworld1234567890")));
    normalList.add(new HighCostSubject(SecretProvider.encryptAes("lee","oingisprettyintheworld1234567890")));
    System.out.println("객체 생성까지 "+ (System.currentTimeMillis() - sTime)+"millis 소요");
    System.out.println("=== fetch 사용 시점 ===");
    for (Subject subject : normalList) {
      subject.fetch();
    }

    System.out.println("=== 프록시 객체로 추가 ===");
    sTime = System.currentTimeMillis();
    proxyList.add(new ProxySubject(SecretProvider.encryptAes("park","oingisprettyintheworld1234567890")));
    proxyList.add(new ProxySubject(SecretProvider.encryptAes("lee","oingisprettyintheworld1234567890")));
    System.out.println("객체 생성까지 "+ (System.currentTimeMillis() - sTime)+"millis 소요");
    System.out.println("=== proxy 통해 fetch 사용 시점 ===");
    for (Subject subject : proxyList) {
      subject.fetch();
    }
  }
}

테스트 결과

테스트 결과 분석

일반적인 방법으로는 데이터를 생성할 시에 바로 복호화가 진행된다. 이 복호화는 시간이 오래걸릴 뿐더러 객체를 유지하는데 메모리를 계속 차지한다.

프록시 패턴을 적용했을 때는 프록시 객체 생성까지는 많은 비용이 요구되지 않는다. 프록시 객체를 통해 마치 데이터가 존재하는 것 처럼 보이게하고, 필요 시에 실제 객체를 생성한다.
이것을 지연 초기화(lazy initialization)이라고 한다.

보호 프록시

이번에는 보호 프록시를 구현 해본다. 가상 프록시와 마찬가지로 가상의 시나리오를 제시한다.

시나리오

Senior 등급과 Junior 등급의 직원이 있다. 기존의 인사 정보 시스템은 인사과에서 사용하며 등급과는 상관없이 모든 인사 정보를 열람할 수 있다. 하지만 이 인사 정보 시스템을 전산과 직원들에게도 제공하고자 한다.
1.Senior 직원은 Junior 등급의 직원의 인사정보를 열람할 수 있지만 Junior 등급은 Senior 등급의 직원의 인사정보를 열람할 수 없다.
2.Junior는 Junior의 등급의 직원의 인사정보를 열람할 수 있다.
그런데 전산과 간부들은 전산과 직원들이 이 프로그램을 사용할 때는 다음과 같은 열람 제한 옵션을 걸고자 한다. 하지만 전산과 간부들은 이 프로그램을 마음대로 변경하지 못한다.
이 경우에는 보호 프록시 패턴을 적용할 수 있다.

구현

  • Grade enum
    등급을 분류 한다.
public enum Grade {
  Junior, Senior
}
public interface PersonBean {

  String getName();
  String readInfo(PersonBean person);
  Grade getGrade();

}
  • PersonBeanImpl class
    PersonBean을 implement 받는다. 직원의 이름, 성별, 입사일, 등급의 정보를 저장하고, 다른 직원의 정보를 열람하는 readInfo 메소드를 제공한다.
public class PersonBeanImpl implements PersonBean {

  String name;
  String gender;
  LocalDate hiredDate;
  Grade grade;

  @Override
  public String toString() {
    return "PersonBeanImpl{" +
        "name='" + name + '\'' +
        ", gender='" + gender + '\'' +
        ", hiredDate=" + hiredDate +
        ", grade=" + grade +
        '}';
  }

  @Override
  public String readInfo(PersonBean person) {
   return person.toString();
  }

  @Override
  public Grade getGrade() {
   return this.grade;
  }

  public PersonBeanImpl(String name, String gender, LocalDate hiredDate, Grade grade) {
    this.name = name;
    this.gender = gender;
    this.hiredDate = hiredDate;
    this.grade = grade;
  }

  @Override
  public String getName() {
    return name;
  }
}
  • PersonInvocationHandler class
    다이나믹 프록시를 구현하기 위해 InvocationHandler를 구현한다.
    readInfo 메소드 사용 시 권한을 확인하고 권한에 따라 제한을 건다.
public class PersonInvocationHandler implements InvocationHandler {

  PersonBean person;

  public PersonInvocationHandler(PersonBean person) {
    this.person = person;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (method.getName().equals("readInfo")) {
        if (((PersonBean) proxy).getGrade() == Grade.Junior && ((PersonBean) args[0]).getGrade() == Grade.Senior) {
          System.out.println("접근 권한이 없습니다.");
          throw new IllegalAccessException();
        } else {
          return method.invoke(person, args);
        }
      } else if (method.getName().startsWith("get")) {
        return method.invoke(person, args);
      }
    } catch (InvocationTargetException e) {
      throw e.getCause();
    }
    return null;
  }
}
  • ProtectionProxyTest class
    가상의 각각 시니어, 주니어, 주니어 등급의 직원 세명을 데이터 베이스에 넣고 인사 정보를 불러온다.
public class ProtectionProxyTest {

  HashMap<String, PersonBean> db = new HashMap<>();

  @BeforeEach
  public void before() {
    initDummy();
  }

  void initDummy() {
    PersonBean park = new PersonBeanImpl("jinhyung Park", "man", LocalDate.now(), Grade.Senior);
    PersonBean lee = new PersonBeanImpl("joongddak Lee","man",LocalDate.now(), Grade.Junior);
    PersonBean kim = new PersonBeanImpl("soohan Kim","man",LocalDate.now(), Grade.Junior);
    db.put(park.getName(), park);
    db.put(lee.getName(), lee);
    db.put(kim.getName(), kim);
  }

  PersonBean getProxy(PersonBean person) {
    return (PersonBean) Proxy.newProxyInstance(person.getClass().getClassLoader(),
        person.getClass().getInterfaces(),
        new PersonInvocationHandler(person));
  }

  PersonBean getPersonFromDb(String name) {
    return db.get(name);
  }

  @Test
  public void test() {
    PersonBean park = getProxy(getPersonFromDb("jinhyung Park"));
    PersonBean lee = getProxy(getPersonFromDb("joongddak Lee"));
    PersonBean kim = getProxy(getPersonFromDb("soohan Kim"));

    //시니어가 주니어의 인사 정보를 열람한다.
    Assertions.assertEquals(lee.toString(), park.readInfo(lee));
    Assertions.assertEquals(kim.toString(), park.readInfo(kim));

    //주니어가 시니어 인사 정보를 열람한다.
    try {
      lee.readInfo(park);
    } catch (Exception e) {
      Assertions.assertEquals(IllegalAccessException.class, e.getCause().getClass());
    }
    try {
      kim.readInfo(park);
    } catch (Exception e) {
      Assertions.assertEquals(IllegalAccessException.class, e.getCause().getClass());
    }

    //주니어가 주니어 인사 정보를 열람한다.
    Assertions.assertEquals(kim.toString(),lee.readInfo(kim));

  }
}

테스트 결과

테스트 결과 분석

테스트가 문제없이 통과 했다.
시니어 직원인 park이 lee와 kim의 정보를 열람하고자 했을 때는 Senior가 Junior의 인사정보를 열람하는 것이므로 IllegalAccessException이 터지지 않았다.
하지만 lee와 kim이 park의 인사정보를 열람하고자 했을 때는 Junior가 Senior의 인사정보를 열람하는 것이므로 IllegalAccessException이 터졌다.
그리고 lee가 kim의 인사정보를 열람하고자 했을 때는 Junior와 Junior 관계이므로 열람이 가능했다.

0개의 댓글