이 실습에서는 Virtual, Protection, Remote, Caching 등 다양한 Proxy 유형을 직접 구현하며 성능 최적화 기법을 익힙니다.
네 가지 Proxy 유형은 겉보기엔 모두 “대상 객체를 감싸는 클래스"라는 같은 모양이지만, 각각 해결하는 문제가 다릅니다. Virtual Proxy는 “생성 비용이 큰 객체를 언제 만들 것인가”, Protection Proxy는 “누가 접근할 수 있는가”, Remote Proxy는 “물리적으로 떨어진 객체를 어떻게 로컬처럼 다룰 것인가"라는 서로 다른 질문에 답합니다. 이 차이 때문에 실무에서 “Proxy를 쓸까"보다 “어떤 문제 때문에 Proxy 유형을 쓸까"를 먼저 판단하는 것이 중요하며, 아래 실습들은 이 네 가지 문제 상황을 각각 별도로 다룹니다.
GoF는 Proxy를 “다른 객체에 대한 접근을 제어하기 위해 그 객체를 대리하는 자리표시자를 제공하는” 패턴으로 정의한다(Gamma, Helm, Johnson, Vlissides, Design Patterns, 1994). 아래 네 실습은 이 정의에서 “접근을 제어"하는 방식(생성 시점 제어, 권한 제어, 위치 제어, 반복 호출 제어)이 유형마다 어떻게 달라지는지를 코드로 확인합니다.
흔한 오개념
**“Proxy와 Decorator는 구조가 같으니 같은 패턴이다”**는 가장 흔한 오해입니다. 두 패턴 모두 “Subject 인터페이스를 구현한 래퍼가 다른 구현체를 감싼다"는 클래스 구조만 보면 실제로 구분이 안 됩니다. 그러나 GoF는 이 둘을 의도(intent)로 구분합니다. Decorator는 원 객체의 호출을 항상 그대로 수행하면서 그 앞뒤에 책임을 얹는 것이 목적이고, Proxy는 위임 자체를 조건부로 만드는 것이 목적입니다. 실습 1의 ImageProxy는 getWidth() 호출은 RealImage에 위임하지 않고 메타데이터로 즉시 답하며, 실습 2의 SecurityFileProxy는 권한이 없으면 위임 자체를 거부합니다. 이렇게 “위임할지, 언제 위임할지, 무엇으로 대신 답할지"를 프록시가 스스로 결정한다는 점이 Decorator와의 실질적 차이입니다.
**“동적 프록시는 아무 클래스나 프록시로 만들 수 있다”**도 실습 4에서 자주 나오는 오해입니다. 실습 4의 Proxy.newProxyInstance()가 사용하는 java.lang.reflect.Proxy는 인터페이스만 프록시할 수 있고, 인터페이스가 없는 구체 클래스는 이 방식으로 프록시할 수 없습니다. Spring AOP가 인터페이스를 구현하지 않은 빈에 대해 CGLIB 기반 프록시로 자동 전환하는 이유가 바로 이 제약 때문입니다. ProxyFactory.createLoggingProxy()에 인터페이스가 아닌 클래스를 전달하면 컴파일은 되어도 런타임에 IllegalArgumentException이 발생합니다.
**“프록시는 항상 성능을 떨어뜨린다”**는 오해는 유형을 뭉뚱그려 생각할 때 생깁니다. 실습 1의 Virtual Proxy와 체크리스트의 Caching Proxy는 오히려 불필요한 작업을 없애 성능을 개선하려고 존재합니다. 성능 저하는 실습 2의 매 호출 권한 검사, 실습 4의 리플렉션 기반 호출처럼 “매번 고정 비용이 붙는” 특정 유형에서만 나타나는 특성이며, 이는 아래 “선택 기준과 오버헤드 트레이드오프"에서 유형별로 구분해 다룹니다.
**“Protection Proxy를 앞에 두면 인증(authentication)과 인가(authorization)를 모두 대체한다”**는 오해도 실습 2의 SecurityFileProxy를 처음 접할 때 자주 생깁니다. SecurityFileProxy가 실제로 검사하는 것은 AccessController.canRead/canWrite/canDelete뿐이며, 이 검사는 모두 “이미 신원이 확인된 User가 어떤 작업을 할 권한이 있는가"라는 인가 질문입니다. “이 User가 실제로 본인이 맞는가"라는 인증 질문은 getCurrentUser()가 대신 답해주지 않습니다 — 이 실습의 CurrentUserHolder는 로그인 절차 없이 ThreadLocal.withInitial(() -> new User("guest"))로 항상 기본값을 채워 넣을 뿐이고, 실제 서비스에서는 이 자리를 스프링 시큐리티의 SecurityContextHolder나 JWT 검증 필터처럼 별도의 인증 계층이 채워야 합니다. Protection Proxy 하나로 인증과 인가를 모두 해결하려 하면, 인증되지 않은 요청조차 기본 User("guest")로 통과시켜 놓고 인가 검사만 통과하면 접근을 허용하는 구멍이 생길 수 있습니다. 이 구분은 테스트 설계에도 그대로 이어져, “신원이 위조되거나 세션이 탈취된 요청을 어떻게 막는가"라는 인증 테스트와 “정상적으로 로그인한 사용자가 자신의 권한을 넘어선 작업을 시도할 때 이를 막는가"라는 인가 테스트를 서로 다른 시나리오로 분리해서 작성해야, SecurityFileProxy가 실제로 책임지는 범위와 그렇지 않은 범위가 코드에서도 명확해집니다.
실습 목표
- 실습 1에서
ImageProxy가getWidth()같은 메타데이터 조회 시점과display()같은 실제 데이터 조회 시점을 구분해, 후자에서만RealImage가 생성됨을 로그로 확인할 수 있다. - 실습 2에서 권한이 없는
User로SecurityFileProxy.writeFile()을 호출했을 때SecurityException이 발생하고RealFileService의 상태가 변경되지 않음을 테스트로 검증할 수 있다. - 실습 3에서
RemoteUserServiceProxy와LocalUserService를 동일한UserService타입으로 다루면서, 호출부 코드를 전혀 수정하지 않고 두 구현을 교체할 수 있음을 보일 수 있다. - 실습 4에서
LoggingInvocationHandler하나로 서로 다른 두 인터페이스에 대한 프록시를 생성해, 정적 Proxy 클래스를 인터페이스 수만큼 만들지 않아도 됨을 확인할 수 있다.
실습 1: 이미지 로딩 Virtual Proxy
왜 Virtual Proxy인가
수백 장의 대용량 이미지를 목록에 나열할 때, 화면에 보이지도 않는 이미지까지 전부 디스크에서 읽어 메모리에 올리면 초기 로딩이 매우 느려집니다. ImageProxy는 파일명과 메타데이터만 먼저 들고 있다가, display()처럼 실제 픽셀 데이터가 필요한 시점에만 RealImage를 생성합니다. 클라이언트는 Image 인터페이스만 보고 있으므로 지금 다루는 것이 프록시인지 실제 이미지인지 신경 쓸 필요가 없습니다.
요구사항
대용량 이미지의 지연 로딩 시스템
코드 템플릿
Virtual Proxy를 구현하려면 먼저 클라이언트가 실제 이미지인지 프록시인지 구분하지 않고 동일하게 다룰 수 있는 공통 인터페이스가 필요하다. 아래 Image 인터페이스는 픽셀 데이터에 접근하는 display()와, 메타데이터만 필요한 getWidth()/getHeight()/getFileSize()/getFilename()을 함께 선언해, 뒤에 나올 프록시가 후자만으로 즉시 응답할 여지를 남겨 둔다.
| |
RealImage는 이 인터페이스를 그대로 구현하되, 생성자에서는 파일명만 기억해 두고 실제 픽셀 로딩은 뒤로 미룬다. loadImageIfNeeded()가 loaded 플래그로 최초 1회만 무거운 작업을 수행하도록 막아, display()가 여러 번 호출되어도 로딩이 중복되지 않게 한다.
| |
ImageProxy가 즉시 반환해야 하는 메타데이터(너비, 높이, 파일 크기)를 담을 값 객체가 별도로 필요하다. RealImage가 픽셀 전체를 읽어야 채울 수 있는 필드들과 달리, ImageMetadata는 파일 헤더만 읽어도 채울 수 있는 가벼운 정보만 갖는다.
| |
이제 ImageProxy가 앞의 두 타입을 조합한다. 생성자에서 loadMetadata()로 가벼운 정보만 미리 채워 두면, getWidth() 같은 메서드는 이 메타데이터만 보고 즉시 답할 수 있다. 반면 display()처럼 실제 픽셀 데이터가 필요한 메서드만 getRealImage()를 거쳐 RealImage를 생성한다.
| |
ImageProxy.getRealImage()의 널 검사 방식은 언뜻 사소해 보이지만, 실무에서 실제로 겪는 버그 유형입니다. 단일 스레드로 실행되는 CLI 도구나 배치 작업에서는 문제가 드러나지 않다가, 같은 ImageProxy 인스턴스를 여러 요청 스레드가 공유하는 웹 서버 환경으로 옮기는 순간 RealImage가 중복 생성되는 사례로 재현됩니다. Hibernate 같은 ORM의 지연 로딩 프록시가 세션(영속성 컨텍스트)마다 별도 인스턴스를 만들어 이 문제를 원천적으로 피해가는 것과 달리, 이 실습의 ImageProxy는 인스턴스를 여러 스레드가 공유할 수 있다고 가정하면 동기화가 필요합니다. 이 트레이드오프(동기화 비용 vs 중복 생성 위험)를 먼저 판단한 뒤에만 실습 목표의 “로그로 확인” 검증이 신뢰할 수 있는 결과를 냅니다.
Virtual Proxy가 지연시킨 로딩 비용은 같은 이미지를 반복 요청할 때마다 다시 소모됩니다. CachingImageProxy는 이미 만든 RealImage를 Map에 담아 재사용해 이 반복 비용을 없앱니다. 손수 캐시 정책을 구현하기 전에, JDK 표준 LinkedHashMap의 accessOrder 모드로 동일한 LRU 축출을 구현할 수 있다는 점부터 검토해야 합니다(아래 주석 참고).
| |
이 cache 필드는 정적(static)이라 클래스 전체가 하나의 캐시를 공유하므로, 여러 CachingImageProxy 인스턴스가 동시에 cache에 쓰기 작업을 수행하면 LRUCache 구현이 스레드 안전하지 않은 한 경쟁 조건이 발생할 수 있습니다. 앞서 ImageProxy에서 다룬 동기화 문제와 같은 종류이므로, LRUCache를 직접 구현할 때도 synchronized 또는 ConcurrentHashMap 기반 접근으로 동시성을 보장해야 합니다.
실습 2: 보안 Protection Proxy
왜 Protection Proxy인가
파일 읽기/쓰기/삭제마다 권한 검사 코드를 RealFileService 내부에 심으면, 서비스 로직과 보안 로직이 뒤섞여 둘 다 테스트하기 어려워집니다. SecurityFileProxy는 실제 파일 접근 로직은 RealFileService에 그대로 두고, 그 앞단에서 AccessController로 권한을 검증한 뒤에만 위임합니다. 권한 정책이 바뀌어도 RealFileService는 전혀 건드릴 필요가 없습니다.
요구사항
사용자 권한에 따른 파일 접근 제어
코드 템플릿
FileService는 SecurityFileProxy와 RealFileService가 함께 구현할 공통 계약이다. 목록 조회가 List<String>을 반환하므로, 이 인터페이스를 담는 코드 블록에는 java.util.List import가 필요하다.
| |
RealFileService는 실제 저장소 역할만 맡고 권한 검사는 전혀 하지 않는다. 여러 스레드가 동시에 파일을 읽고 쓸 수 있다고 가정해 내부 저장소로 ConcurrentHashMap을 쓰고, 존재하지 않는 파일을 읽으면 NoSuchElementException을 던진다.
| |
SecurityFileProxy가 이 실습의 핵심이다. FileService의 네 메서드 모두에서 실제 위임(fileService.xxx(...)) 앞에 AccessController 검사를 두어, 권한이 없으면 위임 자체가 일어나지 않고 SecurityException으로 즉시 실패하게 만든다.
| |
SecurityFileProxy가 위임하는 실제 판단은 AccessController에 있다. 사용자별 권한 집합(userPermissions)과 파일별 허용 권한(filePermissions)을 각각 Map으로 받아, hasPermission()이 이 중 사용자 권한만으로 1차 판단을 내린다(파일별 세부 권한 반영은 TODO로 남겨 확장 여지를 열어 둔다).
| |
나머지는 앞의 두 클래스가 참조하는 최소 지원 타입이다. Permission은 세 가지 권한을 나타내는 열거형이고, FilePermission은 파일별 허용 권한을 담는 값 객체이며, User와 CurrentUserHolder는 각각 사용자 식별자와 “현재 요청의 사용자가 누구인가"를 답하는 자리다.
| |
CurrentUserHolder가 ThreadLocal로 사용자 정보를 보관하는 방식은 Tomcat·Netty 같은 스레드 풀 기반 서버에서 추가 주의가 필요합니다. 스레드 풀은 요청이 끝나도 스레드를 종료하지 않고 다음 요청에 재사용하므로, set()만 호출하고 요청 종료 시점에 clear()(ThreadLocal.remove())를 호출하지 않으면 이전 요청의 User가 스레드에 남아 다음 요청이 우연히 그 값을 이어받는 사고가 생길 수 있습니다. SecurityFileProxy처럼 인가 여부를 좌우하는 값을 ThreadLocal로 다룰 때는 이 누수가 단순 버그가 아니라 “다른 사용자의 권한으로 파일에 접근하는” 보안 사고로 이어질 수 있다는 점에서, 필터·인터셉터의 finally 블록에서 clear()를 호출하는 정리 코드가 getCurrentUser() 자체보다 오히려 더 중요합니다.
실습 3: 원격 서비스 Remote Proxy
왜 Remote Proxy인가
클라이언트 코드가 “이 호출이 네트워크를 타는지"를 매번 의식해야 한다면, HTTP 요청 구성·직렬화·예외 처리 코드가 비즈니스 로직 곳곳에 흩어집니다. RemoteUserServiceProxy는 UserService 인터페이스 뒤에서 HTTP 통신을 캡슐화하여, 클라이언트가 LocalUserService를 쓰든 원격 프록시를 쓰든 동일한 코드로 호출할 수 있게 합니다.
요구사항
원격 서버의 서비스를 로컬에서 사용하는 것처럼 처리
코드 템플릿
UserService의 쓰기 메서드가 참조하는 요청 값 객체부터 정의한다. CreateUserRequest와 UpdateUserRequest는 이 실습이 다루는 범위(이름·이메일)만 최소로 담는 불변 값 객체다.
| |
UserService 인터페이스가 실습 목표 3이 말하는 “동일한 타입"이다. LocalUserService와 RemoteUserServiceProxy가 이 인터페이스 하나만 구현하면, 호출부는 어떤 구현을 쓰는지 알 필요 없이 동일한 코드로 호출할 수 있다. searchUsers()의 반환 타입이 List<User>이므로 이 블록에는 java.util.List import가 필요하다.
| |
RemoteUserServiceProxy가 이 실습의 핵심이다. httpClient와 objectMapper 필드로 HTTP 통신과 JSON 역직렬화를 캡슐화해, 클라이언트는 이 프록시가 내부적으로 네트워크를 타는지 신경 쓸 필요가 없다. 아래 예제는 자바 표준 java.net.http.HttpClient(Java 11+)와 Jackson의 ObjectMapper(외부 라이브러리, com.fasterxml.jackson.core:jackson-databind 의존성 필요)를 그대로 사용한다고 가정한다.
| |
RemoteUserServiceProxy.getUserById()가 모든 예외를 catch (Exception e)로 붙잡아 RuntimeException("Remote call failed", e)로 감싸는 방식은 실습 목표가 요구하는 “호출부 코드를 전혀 수정하지 않고 두 구현을 교체"를 예외 상황에서는 지키지 못합니다. LocalUserService가 예를 들어 NoSuchElementException을 던지도록 구현했다면, 호출부는 UserService 인터페이스만 보고 NoSuchElementException을 잡아 “사용자 없음” 화면을 보여주는 코드를 작성했을 텐데, 같은 상황에서 RemoteUserServiceProxy는 항상 RuntimeException만 던지므로 그 catch 블록이 더 이상 동작하지 않습니다. 정상 흐름에서는 두 구현이 UserService라는 동일한 계약을 지키지만, 예외 흐름에서는 계약이 깨지는 이 간극이 리스코프 치환 원칙(LSP)이 요구하는 “부모 타입을 대체해도 프로그램의 정확성이 유지되어야 한다"는 조건을 예외 타입 수준에서 위반하는 사례이며, 원격 프록시를 설계할 때는 예외 매핑 정책(어떤 원격 오류를 어떤 도메인 예외로 변환할지)을 먼저 정해야 이 간극을 없앨 수 있습니다.
네트워크 장애가 실제로 발생했을 때, RemoteUserServiceProxy를 매번 호출하며 재시도만 반복하면 장애가 클라이언트 전체로 전파될 수 있다. CircuitBreakerProxy는 이 위험을 실패율 기준으로 호출 자체를 일시 차단해 줄인다. 아래 예제는 JDK 표준 타입이 아니라 Resilience4j(io.github.resilience4j.circuitbreaker.CircuitBreaker) 같은 외부 회복탄력성 라이브러리의 타입을 그대로 사용한다고 가정한다.
| |
실습 4: 동적 프록시 구현
왜 동적 프록시인가
로깅, 캐싱, 재시도 같은 부가 기능을 인터페이스마다 별도의 정적 Proxy 클래스로 만들면 대상 인터페이스 수만큼 클래스가 늘어납니다. LoggingInvocationHandler처럼 InvocationHandler를 구현하면, Proxy.newProxyInstance()가 런타임에 임의의 인터페이스에 대한 프록시 인스턴스를 만들어주므로 하나의 핸들러로 여러 인터페이스에 동일한 횡단 관심사를 적용할 수 있습니다.
코드 템플릿
LoggingInvocationHandler가 이 실습의 핵심이다. InvocationHandler.invoke() 하나가 대상 객체의 모든 메서드 호출을 가로채므로, 이 한 클래스로 인터페이스 수와 무관하게 동일한 로깅 로직을 적용할 수 있다. 아래 예제는 JDK 표준 타입이 아니라 SLF4J(org.slf4j.Logger, org.slf4j.LoggerFactory)의 타입을 그대로 사용한다고 가정한다.
| |
ProxyFactory는 Proxy.newProxyInstance()를 감싸, 클라이언트가 리플렉션 API를 직접 다루지 않고도 로깅·캐싱·재시도 프록시를 인터페이스 타입만으로 만들 수 있게 한다. createLoggingProxy()에 인터페이스가 아닌 구체 클래스를 넘기면, 위 오개념 절에서 다룬 대로 컴파일은 되어도 런타임에 IllegalArgumentException이 발생한다.
| |
ProxyFactory가 프로그래밍 방식으로 프록시를 만든다면, 어노테이션은 “이 메서드에 어떤 횡단 관심사를 적용할지"를 메서드 선언 자리에서 선언적으로 표시하는 방식이다. @Cacheable과 @Retry는 각각 캐싱 TTL과 재시도 횟수를 메서드 단위로 지정할 수 있게 한다.
| |
AnnotationProxyHandler는 LoggingInvocationHandler와 같은 InvocationHandler 구조를 재사용하되, invoke() 내부에서 method.getAnnotation(Cacheable.class) 같은 리플렉션 조회로 어노테이션 유무에 따라 동작을 분기해야 한다(구체 구현은 TODO로 남겨 둔다).
| |
method.invoke(target, args)는 대상 메서드가 던진 예외를 그대로 전달하지 않고 InvocationTargetException으로 한 번 감싸서 던집니다(JDK Method.invoke 명세에 정의된 동작). 따라서 catch (Exception e) { throw e; }처럼 감싸는 대로 다시 던지면, 클라이언트는 원래 호출하려던 인터페이스가 선언한 예외가 아니라 항상 InvocationTargetException을 받게 되어 “정적 Proxy 클래스와 동일하게 동작한다"는 동적 프록시의 투명성 목표가 예외 흐름에서 깨집니다. 이 저장소의 09. 프록시 패턴의 다면성 “Dynamic Proxy 구현” 절의 예제도 같은 이유로 catch (InvocationTargetException e) { throw e.getCause(); } 형태를 쓰고 있으며, 위 LoggingInvocationHandler도 이를 따라 원인 예외(getCause())를 다시 던지도록 맞췄습니다. 실습 2의 SecurityException처럼 클라이언트가 특정 예외 타입으로 분기하는 코드를 이미 작성했다면, 이 원인 예외 언랩 없이는 그 분기가 동적 프록시를 통과하는 순간 전부 무력화됩니다.
체크리스트
기본 Proxy 유형
- Virtual Proxy (지연 로딩) —
RealImage생성 시점을display()호출까지 미뤄야 대용량 이미지 목록의 초기 로딩 비용을 없앨 수 있다. - Protection Proxy (접근 제어) — 권한 검사를
SecurityFileProxy에 모아두지 않으면RealFileService내부 곳곳에 보안 로직이 흩어져 테스트하기 어려워진다. - Remote Proxy (원격 접근) — HTTP 통신 세부사항을
RemoteUserServiceProxy뒤에 숨겨야 클라이언트 코드가 로컬/원격 구현을 동일하게 다룰 수 있다. - Caching Proxy (결과 캐싱) — 동일 이미지를 반복 요청할 때마다 디스크를 다시 읽으면
Virtual Proxy의 지연 로딩 이득이 캐싱 없이는 매번 소모된다.
고급 기능
- 동적 프록시 구현 —
LoggingInvocationHandler하나로 여러 인터페이스를 처리해야 인터페이스 수만큼 정적 Proxy 클래스를 만드는 비용을 피할 수 있다. - 어노테이션 기반 AOP —
@Cacheable,@Retry같은 선언적 표시가 없으면 캐싱·재시도 로직을 메서드마다 수동으로 반복 작성해야 한다. - 회로 차단기 패턴 — 원격 서비스가 장애 상태일 때도 계속 호출을 시도하면 장애가 클라이언트 전체로 전파되므로
CircuitBreakerProxy로 차단해야 한다. - 성능 모니터링 — 프록시 계층에서 실행 시간을 기록해야 어떤 호출이 오버헤드의 주된 원인인지 코드 수정 없이 파악할 수 있다.
최적화 및 확장
- LRU 캐시 구현 — 캐시 크기를 제한하지 않으면
CachingImageProxy가 메모리를 무한정 소비할 수 있어 오래된 항목을 밀어낼 정책이 필요하다. - 비동기 프록시 — 호출 스레드를 블로킹하면 원격 호출 지연이 그대로 클라이언트 응답 시간에 더해지므로 비동기 처리로 분리할 필요가 있다.
- 프록시 체이닝 — 캐싱과 보안 검사를 동시에 적용하려면 여러 Proxy를 겹쳐 감싸야 하며, 이때 감싸는 순서가 동작 순서를 결정한다.
- 메트릭 수집 — 프록시 계층에서 호출 횟수와 실패율을 집계해야 어떤 유형의 오버헤드가 실제로 문제인지 데이터로 판단할 수 있다.
추가 도전
- Smart Proxy: 참조 카운팅과 자동 정리
- Copy-on-Write Proxy: 쓰기 시점 복사
- Adaptive Proxy: 상황에 따른 전략 변경
- Distributed Proxy: 분산 환경 투명 접근
선택 기준과 오버헤드 트레이드오프
네 가지 Proxy 유형 중 무엇을 선택할지는 “무엇을 지연시키거나 통제하고 싶은가"로 결정됩니다. 객체 생성 비용 자체가 문제라면 Virtual Proxy, 호출 주체의 권한이 문제라면 Protection Proxy, 물리적 위치가 문제라면 Remote Proxy, 동일 연산의 반복 호출이 문제라면 Caching Proxy가 대상입니다. 여러 문제가 겹치면(예: 원격 호출 결과를 캐싱하고 싶다) 프록시를 체이닝해서 조합할 수 있지만, 체이닝이 길어질수록 각 계층이 어디서 예외를 던지는지 추적하기 어려워지므로 실무에서는 꼭 필요한 조합만 남기는 것이 안전합니다.
아래 표는 이 선택 기준과 오버헤드 성격을 한눈에 정리한 것이고, 뒤이은 다이어그램은 같은 판단 과정을 순서도로 보여줍니다.
| 유형 | 제어 대상 | 제어 시점 | 오버헤드 성격 |
|---|---|---|---|
| Virtual Proxy (실습 1) | 객체 생성 비용 | 최초 실제 접근 시점 | 최초 1회 지연, 이후 없음 |
| Protection Proxy (실습 2) | 호출 주체 권한 | 매 호출 | 모든 호출에 고정 검사 비용 |
| Remote Proxy (실습 3) | 물리적 위치 | 네트워크 호출 시 | 네트워크 지연·직렬화 비용 |
| Caching Proxy (체크리스트) | 반복 호출 결과 | 캐시 조회 시 | 캐시 적중 시 거의 없음, 미스 시 원본 비용 |
flowchart TD
Start["무엇을 통제하고 싶은가?"] --> Q1{"객체 생성 비용이 문제인가?"}
Q1 -->|"그렇다"| Virtual["Virtual Proxy생성 시점을 최초 접근까지 지연"]
Q1 -->|"아니다"| Q2{"호출 주체 권한이 문제인가?"}
Q2 -->|"그렇다"| Protection["Protection Proxy매 호출마다 접근 권한 검사"]
Q2 -->|"아니다"| Q3{"물리적 위치가 문제인가?"}
Q3 -->|"그렇다"| Remote["Remote Proxy네트워크 호출을 인터페이스 뒤로 캡슐화"]
Q3 -->|"아니다"| Q4{"동일 연산의 반복 호출이 문제인가?"}
Q4 -->|"그렇다"| Caching["Caching Proxy이전 결과를 재사용"]
오버헤드 트레이드오프도 유형마다 다릅니다. ImageProxy의 지연 로딩은 최초 접근 시점의 지연을 감수하는 대신 불필요한 로딩을 통째로 없애므로 대체로 이득이 명확합니다. 반면 SecurityFileProxy의 권한 검사나 LoggingInvocationHandler의 리플렉션 기반 호출은 모든 호출마다 고정 비용이 붙습니다. I/O가 지배적인 작업(파일, 네트워크, DB)에서는 이 고정 비용이 상대적으로 무시할 수준이지만, 초당 수백만 번 호출되는 순수 연산 경로에 동적 프록시를 무분별하게 씌우면 그 오버헤드가 누적되어 눈에 띄는 성능 저하로 이어질 수 있으므로, 적용 전에 대상 경로의 호출 빈도를 먼저 확인해야 합니다.
핵심 포인트: Proxy 패턴은 다양한 형태로 진화하여 현대 소프트웨어의 핵심 인프라가 되었습니다. 지연 로딩, 보안, 캐싱, 모니터링 등 횡단 관심사를 우아하게 처리하는 강력한 도구입니다.
![Featured image of post [Design Patterns] 09. 프록시 패턴의 다면성 — 실습](/post/design-patterns/proxy-pattern-multifaceted-practice/wordcloud_hu_a061a2f855769805.webp)
![[Design Patterns] 08. 데코레이터와 컴포지트: 재귀적 아름다움 — 실습](/post/design-patterns/decorator-composite-recursive-beauty-practice/wordcloud_hu_ab530bac78fdaae6.webp)
![[Design Patterns] 09. 프록시 패턴의 다면성](/post/design-patterns/proxy-pattern-multifaceted/wordcloud_hu_91ce4c2edf87ada0.webp)
![[Design Patterns] 09. 프록시 패턴의 다면성 — 실습](/post/design-patterns/proxy-pattern-multifaceted-practice/wordcloud_hu_e0281f1105f51649.webp)
![[Design Patterns] 10. 브릿지와 플라이웨이트: 분리와 효율성](/post/design-patterns/bridge-flyweight-separation-efficiency/wordcloud_hu_6f69a5eba4ba5040.webp)
![[Design Patterns] 10. 브릿지와 플라이웨이트: 분리와 효율성 — 실습](/post/design-patterns/bridge-flyweight-separation-efficiency-practice/wordcloud_hu_9df51d59e6d2dd65.webp)
![[Design Patterns] 07. 어댑터와 파사드: 인터페이스의 철학 — 실습](/post/design-patterns/adapter-facade-interface-philosophy-practice/wordcloud_hu_a75d6edaac27ca9b.webp)
![[Hardware] LattePanda Alpha에 Ubuntu 16.04 LTS 설치 가이드](/post/2018-12-06-install-ubuntu-16.04-on-lattepanda/wordcloud_hu_fc536f8de2cbd4bf.webp)