CodeGym /Java Blog /무작위의 /프록시 디자인 패턴
John Squirrels
레벨 41
San Francisco

프록시 디자인 패턴

무작위의 그룹에 게시되었습니다
프로그래밍에서는 응용 프로그램의 아키텍처를 올바르게 계획하는 것이 중요합니다. 디자인 패턴은 이를 달성하는 데 없어서는 안 될 방법입니다. 오늘은 프록시에 대해 알아보겠습니다.

프록시가 필요한 이유는 무엇입니까?

이 패턴은 개체에 대한 제어된 액세스와 관련된 문제를 해결하는 데 도움이 됩니다. "왜 통제된 액세스가 필요한가요?"라고 물을 수 있습니다. 무엇이 무엇인지 파악하는 데 도움이 되는 몇 가지 상황을 살펴보겠습니다.

예 1

데이터베이스에서 보고서 내보내기를 담당하는 클래스가 있는 오래된 코드가 많은 대규모 프로젝트가 있다고 상상해 보십시오. 클래스는 동 기적으로 작동합니다. 즉, 데이터베이스가 요청을 처리하는 동안 전체 시스템이 유휴 상태입니다. 평균적으로 보고서를 생성하는 데 30분이 걸립니다. 따라서 수출 프로세스는 오전 12시 30분에 시작되며 경영진은 오전에 보고서를 받습니다. 감사 결과 정상 업무 시간에 바로 신고를 받을 수 있으면 더 좋겠다는 결론이 나왔습니다. 시작 시간을 연기할 수 없으며 데이터베이스에서 응답을 기다리는 동안 시스템을 차단할 수 없습니다. 해결책은 시스템 작동 방식을 변경하여 별도의 스레드에서 보고서를 생성하고 내보내는 것입니다. 이 솔루션을 사용하면 시스템이 정상적으로 작동하고 경영진은 새로운 보고서를 받을 수 있습니다. 하지만, 문제가 있습니다. 시스템의 다른 부분이 해당 기능을 사용하기 때문에 현재 코드를 다시 작성할 수 없습니다. 이 경우 프록시 패턴을 사용하여 보고서 내보내기 요청을 수신하고 시작 시간을 기록하고 별도의 스레드를 시작하는 중간 프록시 클래스를 도입할 수 있습니다. 보고서가 생성되면 스레드가 종료되고 모두가 만족합니다.

예 2

개발팀이 이벤트 웹사이트를 만들고 있습니다. 새 이벤트에 대한 데이터를 얻기 위해 팀은 타사 서비스를 쿼리합니다. 특수 개인 라이브러리는 서비스와의 상호 작용을 용이하게 합니다. 개발 중에 문제가 발견되었습니다. 타사 시스템은 하루에 한 번 데이터를 업데이트하지만 사용자가 페이지를 새로 고칠 때마다 요청이 전송됩니다. 이로 인해 많은 수의 요청이 생성되고 서비스가 응답을 중지합니다. 해결책은 서비스의 응답을 캐시하고 페이지가 다시 로드될 때 캐시된 결과를 방문자에게 반환하여 필요에 따라 캐시를 업데이트하는 것입니다. 이 경우 프록시 디자인 패턴은 기존 기능을 변경하지 않는 탁월한 솔루션입니다.

디자인 패턴의 원리

이 패턴을 구현하려면 프록시 클래스를 만들어야 합니다. 클라이언트 코드에 대한 동작을 모방하여 서비스 클래스의 인터페이스를 구현합니다. 이러한 방식으로 클라이언트는 실제 개체 대신 프록시와 상호 작용합니다. 일반적으로 모든 요청은 서비스 클래스로 전달되지만 전후에 추가 작업이 있습니다. 간단히 말해서 프록시는 클라이언트 코드와 대상 개체 사이의 계층입니다. 오래되고 매우 느린 하드 디스크에서 쿼리 결과를 캐싱하는 예를 고려하십시오. 논리를 변경할 수 없는 일부 고대 앱의 전기 열차 시간표에 대해 이야기하고 있다고 가정합니다. 업데이트된 시간표가 있는 디스크는 매일 정해진 시간에 삽입됩니다. 그래서, 우리는:
  1. TrainTimetable상호 작용.
  2. ElectricTrainTimetable, 이 인터페이스를 구현합니다.
  3. 클라이언트 코드는 이 클래스를 통해 파일 시스템과 상호 작용합니다.
  4. TimetableDisplay클라이언트 클래스. 그 printTimetable()방법은 클래스의 방법을 사용합니다 ElectricTrainTimetable.
다이어그램은 간단합니다. 프록시 디자인 패턴: - 2현재 메서드를 호출할 때마다 printTimetable()클래스 ElectricTrainTimetable는 디스크에 액세스하고 데이터를 로드하여 클라이언트에 제공합니다. 시스템은 정상적으로 작동하지만 매우 느립니다. 결과적으로 캐싱 메커니즘을 추가하여 시스템 성능을 향상시키기로 결정했습니다. 이는 프록시 패턴을 사용하여 수행할 수 있습니다. 프록시 디자인 패턴: - 3따라서 클래스는 이전 클래스 대신 클래스 TimetableDisplay와 상호 작용하고 있다는 사실조차 인식하지 못합니다 . ElectricTrainTimetableProxy새로운 구현은 하루에 한 번 시간표를 로드합니다. 반복 요청의 경우 메모리에서 이전에 로드된 개체를 반환합니다.

프록시에 가장 적합한 작업은 무엇입니까?

다음은 이 패턴이 확실히 유용한 몇 가지 상황입니다.
  1. 캐싱
  2. 지연 또는 게으른 초기화 필요에 따라 개체를 로드할 수 있는데 왜 바로 개체를 로드합니까?
  3. 로깅 요청
  4. 데이터 및 액세스의 중간 검증
  5. 작업자 스레드 시작
  6. 개체에 대한 액세스 기록
그리고 다른 사용 사례도 있습니다. 이 패턴의 원리를 이해하면 성공적으로 적용할 수 있는 상황을 식별할 수 있습니다. 언뜻 보기에 프록시는 파사드 같은 역할을 하지만 그렇지 않습니다. 프록시 에는 서비스 개체와 동일한 인터페이스가 있습니다. 또한 이 패턴을 데코레이터 또는 어댑터 패턴 과 혼동하지 마십시오 . 데코레이터 확장 인터페이스를 제공하고 어댑터는 대체 인터페이스를 제공합니다.

장점과 단점

  • + 서비스 개체에 대한 액세스를 원하는 대로 제어할 수 있습니다.
  • + 서비스 개체의 수명 주기 관리와 관련된 추가 기능
  • + 서비스 개체 없이 작동
  • + 성능 및 코드 보안을 향상시킵니다.
  • - 추가 요청으로 인해 성능이 저하될 위험이 있습니다.
  • - 클래스 계층 구조를 더 복잡하게 만듭니다.

실제 프록시 패턴

하드 디스크에서 열차 시간표를 읽는 시스템을 구현해 보겠습니다.

public interface TrainTimetable {
   String[] getTimetable();
   String getTrainDepartureTime();
}
기본 인터페이스를 구현하는 클래스는 다음과 같습니다.

public class ElectricTrainTimetable implements TrainTimetable {

   @Override
   public String[] getTimetable() {
       ArrayList<String> list = new ArrayList<>();
       try {
           Scanner scanner = new Scanner(new FileReader(new File("/tmp/electric_trains.csv")));
           while (scanner.hasNextLine()) {
               String line = scanner.nextLine();
               list.add(line);
           }
       } catch (IOException e) {
           System.err.println("Error:  " + e);
       }
       return list.toArray(new String[list.size()]);
   }

   @Override
   public String getTrainDepartureTime(String trainId) {
       String[] timetable = getTimetable();
       for (int i = 0; i < timetable.length; i++) {
           if (timetable[i].startsWith(trainId+";")) return timetable[i];
       }
       return "";
   }
}
기차 시간표를 얻을 때마다 프로그램은 디스크에서 파일을 읽습니다. 그러나 그것은 우리 문제의 시작일뿐입니다. 단일 열차의 시간표를 얻을 때마다 전체 파일을 읽습니다! 그런 코드는 하지 말아야 할 예제에만 존재하는 것이 좋습니다 :) 클라이언트 클래스:

public class TimetableDisplay {
   private TrainTimetable trainTimetable = new ElectricTrainTimetable();

   public void printTimetable() {
       String[] timetable = trainTimetable.getTimetable();
       String[] tmpArr;
       System.out.println("Train\\tFrom\\tTo\\t\\tDeparture time\\tArrival time\\tTravel time");
       for (int i = 0; i < timetable.length; i++) {
           tmpArr = timetable[i].split(";");
           System.out.printf("%s\t%s\t%s\t\t%s\t\t\t\t%s\t\t\t%s\n", tmpArr[0], tmpArr[1], tmpArr[2], tmpArr[3], tmpArr[4], tmpArr[5]);
       }
   }
}
예제 파일:

9B-6854;London;Prague;13:43;21:15;07:32
BA-1404;Paris;Graz;14:25;21:25;07:00
9B-8710;Prague;Vienna;04:48;08:49;04:01;
9B-8122;Prague;Graz;04:48;08:49;04:01
테스트해 봅시다:

public static void main(String[] args) {
   TimetableDisplay timetableDisplay = new timetableDisplay();
   timetableDisplay.printTimetable();
}
산출:

Train  From  To  Departure time  Arrival time  Travel time
9B-6854  London  Prague  13:43  21:15  07:32
BA-1404  Paris  Graz  14:25  21:25  07:00
9B-8710  Prague  Vienna  04:48  08:49  04:01
9B-8122  Prague  Graz  04:48  08:49  04:01
이제 패턴을 도입하는 데 필요한 단계를 살펴보겠습니다.
  1. 원래 개체 대신 프록시를 사용할 수 있도록 인터페이스를 정의합니다. 이 예에서 이것은 TrainTimetable.

  2. 프록시 클래스를 만듭니다. 서비스 개체에 대한 참조가 있어야 합니다(클래스에서 만들거나 생성자에 전달).

    프록시 클래스는 다음과 같습니다.

    
    public class ElectricTrainTimetableProxy implements TrainTimetable {
       // Reference to the original object
       private TrainTimetable trainTimetable = new ElectricTrainTimetable();
      
       private String[] timetableCache = null
    
       @Override
       public String[] getTimetable() {
           return trainTimetable.getTimetable();
       }
    
       @Override
       public String getTrainDepartureTime(String trainId) {
           return trainTimetable.getTrainDepartureTime(trainId);
       }
      
       public void clearCache() {
           trainTimetable = null;
       }
    }
    

    이 단계에서 우리는 단순히 원본 개체에 대한 참조가 있는 클래스를 만들고 모든 호출을 전달합니다.

  3. 프록시 클래스의 논리를 구현해 봅시다. 기본적으로 호출은 항상 원래 개체로 리디렉션됩니다.

    
    public class ElectricTrainTimetableProxy implements TrainTimetable {
       // Reference to the original object
       private TrainTimetable trainTimetable = new ElectricTrainTimetable();
    
       private String[] timetableCache = null
    
       @Override
       public String[] getTimetable() {
           if (timetableCache == null) {
               timetableCache = trainTimetable.getTimetable();
           }
           return timetableCache;
       }
    
       @Override
       public String getTrainDepartureTime(String trainId) {
           if (timetableCache == null) {
               timetableCache = trainTimetable.getTimetable();
           }
           for (int i = 0; i < timetableCache.length; i++) {
               if (timetableCache[i].startsWith(trainId+";")) return timetableCache[i];
           }
           return "";
       }
    
       public void clearCache() {
           trainTimetable = null;
       }
    }
    

    getTimetable()시간표 배열이 메모리에 캐시되었는지 여부를 확인합니다 . 그렇지 않은 경우 디스크에서 데이터를 로드하라는 요청을 보내고 결과를 저장합니다. 시간표가 이미 요청된 경우 메모리에서 객체를 빠르게 반환합니다.

    간단한 기능 덕분에 getTrainDepartureTime() 메서드는 원래 개체로 리디렉션할 필요가 없었습니다. 우리는 단순히 그 기능을 새로운 방법으로 복제했습니다.

    이러지 마. 코드를 복제하거나 유사한 작업을 수행해야 하는 경우 문제가 발생한 것이며 다른 각도에서 문제를 다시 살펴봐야 합니다. 우리의 간단한 예에서는 다른 옵션이 없었습니다. 그러나 실제 프로젝트에서는 코드가 더 정확하게 작성될 가능성이 높습니다.

  4. 클라이언트 코드에서 원본 개체 대신 프록시 개체를 만듭니다.

    
    public class TimetableDisplay {
       // Changed reference
       private TrainTimetable trainTimetable = new ElectricTrainTimetableProxy();
    
       public void printTimetable() {
           String[] timetable = trainTimetable.getTimetable();
           String[] tmpArr;
           System.out.println("Train\\tFrom\\tTo\\t\\tDeparture time\\tArrival time\\tTravel time");
           for (int i = 0; i < timetable.length; i++) {
               tmpArr = timetable[i].split(";");
               System.out.printf("%s\t%s\t%s\t\t%s\t\t\t\t%s\t\t\t%s\n", tmpArr[0], tmpArr[1], tmpArr[2], tmpArr[3], tmpArr[4], tmpArr[5]);
           }
       }
    }
    

    확인하다

    
    Train  From  To  Departure time  Arrival time  Travel time
    9B-6854  London  Prague  13:43  21:15  07:32
    BA-1404  Paris  Graz  14:25  21:25  07:00
    9B-8710  Prague  Vienna  04:48  08:49  04:01
    9B-8122  Prague  Graz  04:48  08:49  04:01
    

    좋습니다. 올바르게 작동합니다.

    특정 조건에 따라 원본 개체와 프록시 개체를 모두 생성하는 팩터리 옵션을 고려할 수도 있습니다.

작별 인사를 하기 전에 여기 유용한 링크가 있습니다.

오늘은 그게 다야! 수업으로 돌아가서 실제로 새로운 지식을 시험해 보는 것도 나쁘지 않을 것입니다 :)
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION