프로그래밍Java 아키텍트

프록시란 무엇이며, Java에서의 프록시 유형과 이를 통해 객체의 동적 동작을 어떻게 구현하는가?

Hintsage AI 어시스턴트로 면접 통과

답변.

질문 역사:

프록시 객체와 Proxy 패턴은 Java에서 실제 객체에 대한 접근을 제어하거나 행동을 변경하거나 보안, 로깅, 메트릭과 같은 교차 기능을 주입할 수 있는 대체 객체를 생성하기 위해 등장하였습니다. Java 1.3부터 java.lang.reflect 패키지에서 표준 동적 프록시에 대한 지원이 나타났고, 이후 CGLIB 및 ByteBuddy와 같은 인기 라이브러리가 등장했습니다.

문제:

기존 클래스의 로직을 직접적으로 변경하는 것이 항상 가능하거나 편리하지 않습니다. 종종 원래 객체의 코드를 변경하지 않고도 행동(예: 로깅, 캐싱, 트랜잭션)을 투명하게 추가해야 할 필요가 있어, 이는 표준 상속 방법으로는 불가능합니다.

해결책:

프록시 객체는 정적으로(수동 서브클래싱을 통해) 및 동적으로(리플렉션 또는 바이트코드 메커니즘을 통해) 구현할 수 있습니다. 동적 프록시는 인터페이스를 구현하고 invoke() 메서드 내에서 실제 객체에 호출을 위임하는 객체를 생성하여 외부 행동을 주입하는 유연성을 제공합니다.

코드 예제:

import java.lang.reflect.*; interface Service { void doWork(); } class RealService implements Service { public void doWork() { System.out.println("Doing work!"); } } class LoggingInvocationHandler implements InvocationHandler { private final Object target; public LoggingInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Calling: " + method.getName()); return method.invoke(target, args); } } Service real = new RealService(); Service proxy = (Service) Proxy.newProxyInstance( real.getClass().getClassLoader(), new Class[] {Service.class}, new LoggingInvocationHandler(real)); proxy.doWork(); // 호출 로그 기록됨

주요 특징:

  • 정적 프록시는 각 함수에 대한 코드를 사전 정의해야 함
  • 동적 프록시는 런타임에 인터페이스에 대해 동적으로 생성될 수 있음
  • 인터페이스가 없는 클래스의 경우 제 3자 라이브러리가 필요함 (예: CGLIB)

함정 질문.

Java의 표준 동적 프록시가 인터페이스를 구현하지 않는 클래스에서 작동할 수 있는가?

아니요. Java의 표준 프록시는 오직 인터페이스와만 작동합니다. 인터페이스가 없는 클래스(예를 들어 일반 클래스를 프록시하려는 경우)에는 제3자 도구가 필요합니다 (예: CGLIB, ByteBuddy).

프라이빗 메서드를 가진 인터페이스의 프록시를 생성할 수 있는가?

아니요. Java의 인터페이스는 프록시해야 하는 프라이빗 메서드를 포함할 수 없습니다. 동적 프록시는 인터페이스의 공적 메서드만 구현합니다. Java 9부터 private default 메서드가 등장했지만, 이들은 프록시를 통해 접근할 수 없습니다.

InvocationHandler와 MethodInterceptor(CGLIB)의 차이점은 무엇인가?

InvocationHandler는 동적 프록시를 위해 사용되는 JDK의 표준 인터페이스로, 인터페이스 메서드의 호출을 수신합니다. MethodInterceptor는 동적 상속을 통해 모든 클래스의 메서드 호출을 가로채는 CGLIB의 인터페이스입니다. 적용 가능성과 수준의 차이가 있습니다: JDK는 오직 인터페이스와 작업하고, CGLIB는 모든 클래스와 작업합니다.

일반적인 오류 및 안티 패턴

  • 오류 유형: 인터페이스를 구현하지 않는 클래스에 Proxy를 사용하려는 시도
  • 명시적 순환 호출로 인한 StackOverflow
  • 프록시화에 적합하지 않은 클래스의 프록시화, 혹은 final 클래스를 프록시하려는 시도

실제 사례

부정적 케이스

엔지니어가 모든 서비스의 각 메서드에 로그 코드 반복을 수동으로 작성합니다. 새로운 메서드를 추가할 때마다 로직이 중복되고, 종종 필요한 호출을 추가하는 것을 잊어버립니다.

장점:

  • 투명하여 실행 흐름을 쉽게 이해할 수 있음

단점:

  • 막대한 시간 소모, 중복된 코드가 많아 유지 보수가 어려움

긍정적 케이스

프로젝트에 AOP가 동적 프록시를 통해 도입되었습니다: 로깅과 트랜잭션 관리가 중앙에서 구현된 대체 래퍼를 통해 처리됩니다.

장점:

  • 교차 기능성을 신속하게 도입하고 변경할 수 있으며 중복이 없음

단점:

  • 프록시의 작동 방식을 이해할 필요가 있으며, 디버깅에서 잠재적인 복잡성이 발생할 수 있음