프록시 패턴은 객체 지향 디자인 패턴 중 하나로, 다른 객체에 대한 접근을 제어하는 대리인(프록시) 객체를 제공합니다. 프록시는 원래 객체와 동일한 인터페이스를 구현하며, 클라이언트의 요청을 원래 객체에 전달하기 전이나 후에 추가 기능을 수행할 수 있습니다.
-
Subject (주체) 인터페이스:
- RealSubject와 Proxy가 구현하는 공통 인터페이스
- 클라이언트는 이 인터페이스만 알면 됨
-
RealSubject (실제 주체):
- 실제 비즈니스 로직을 처리하는 클래스
- 주요 기능을 담당
-
Proxy (프록시):
- RealSubject와 동일한 인터페이스 구현
- RealSubject에 대한 참조를 가지고 있음
- 클라이언트의 요청을 RealSubject에 위임하기 전후에 추가 기능 수행
public interface UserService {
User getUserById(Long id);
List<User> getAllUsers();
User createUser(User user);
User updateUser(User user);
void deleteUser(Long id);
}@Service
public class RealUserService implements UserService {
// 실제 비즈니스 로직 구현
// ...
}@Service
@Primary
@RequiredArgsConstructor
@Slf4j
public class UserServiceProxy implements UserService {
private final UserService userService; // RealUserService 주입
@Override
public User getUserById(Long id) {
log.info("Proxying getUserById request for user ID: {}", id);
long startTime = System.currentTimeMillis();
try {
// 실제 서비스 메서드 호출
return userService.getUserById(id);
} finally {
long endTime = System.currentTimeMillis();
log.info("getUserById completed in {} ms", (endTime - startTime));
}
}
// 다른 메소드도 비슷한 패턴으로 구현
}A: Spring의 의존성 주입 메커니즘을 통해 가능합니다. UserServiceProxy는 UserService 타입의 빈을 주입받을 때, 자기 자신이 아닌 다른 UserService 구현체(RealUserService)를 주입받습니다. Spring은 순환 참조를 방지하기 위해 이런 방식으로 동작합니다.
A: @Primary 어노테이션으로 인해 UserServiceProxy가 우선적으로 주입됩니다. 따라서 컨트롤러는 UserServiceProxy를 사용하게 되고, 실제 비즈니스 로직은 내부적으로 RealUserService에 위임됩니다.
A: UserServiceProxy의 각 메소드 내부에서 userService.메소드명(파라미터) 형태로 실제 서비스에 작업을 위임합니다. 이 전후로 로깅이나 시간 측정 같은 부가 기능을 수행할 수 있습니다.
-
관심사의 분리:
- 실제 비즈니스 로직(RealUserService)과 부가 기능(UserServiceProxy)이 분리됨
-
코드 변경 없이 기능 추가:
- 기존 코드를 수정하지 않고 프록시를 통해 새로운 기능을 추가할 수 있음
-
클라이언트 코드 영향 최소화:
- 클라이언트는 인터페이스만 알면 되므로, 프록시 도입에 따른 변경이 필요 없음
-
로깅 및 감사:
- 모든 메소드 호출에 대한 로그를 일관성 있게 남김
-
성능 모니터링:
- 각 메소드의 실행 시간을 측정하여 성능 병목 식별
-
캐싱:
- 자주 조회되는 데이터를 캐시에 저장하여 성능 향상
-
접근 제어/보안:
- 메소드 호출 전에 사용자 권한 확인
-
지연 초기화:
- 리소스가 많이 소모되는 객체의 생성을 실제로 필요할 때까지 지연
이 프로젝트에서는 프록시 패턴을 사용하여 다음 두 가지 핵심 기능을 구현했습니다:
프록시 클래스에서는 각 서비스 메소드 호출 시 로깅을 수행합니다:
log.info("Proxying getUserById request for user ID: {}", id);이 로그를 통해 어떤 메소드가 어떤 파라미터와 함께 호출되었는지 추적할 수 있습니다. 실무에서 이러한 로깅은 다음과 같은 이점을 제공합니다:
- 시스템 활동 감사 추적
- 문제 발생 시 원인 분석 지원
- 사용 패턴 분석을 위한 데이터 제공
각 메소드 호출의 시작과 종료 시간을 측정하여 실행 시간을 로깅합니다:
long startTime = System.currentTimeMillis();
try {
return userService.getUserById(id);
} finally {
long endTime = System.currentTimeMillis();
log.info("getUserById completed in {} ms", (endTime - startTime));
}이 기능은 다음과 같은 장점을 제공합니다:
- 성능 병목 지점 파악
- 느린 메소드 식별 및 최적화 대상 선정
- 시간 경과에 따른 성능 변화 모니터링
실제 개발 환경에서는 이런 성능 측정 데이터를 수집하여 대시보드를 구성하거나, 임계값을 초과하는 경우 알림을 발송하는 등의 모니터링 시스템을 구축할 수 있습니다.
Spring에서는 AOP(Aspect-Oriented Programming)를 통해 더 선언적인 방식으로 프록시 패턴을 활용할 수 있습니다:
@Aspect
@Component
@Slf4j
public class PerformanceAspect {
@Around("execution(* com.designpattern.sample.service.impl.*.*(..))")
public Object logPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
// 부가 기능 및 위임 처리
// ...
}
}이 방식을 사용하면 프록시 클래스를 직접 작성하지 않아도 Spring AOP가 자동으로 프록시를 생성합니다.
- 인터페이스(UserService)를 정의합니다.
- 실제 구현체(RealUserService)를 만듭니다.
- 프록시 구현체(UserServiceProxy)를 만들고, @Primary 어노테이션을 붙입니다.
- 프록시 구현체는 실제 구현체를 주입받고, 추가 기능 수행 후 실제 작업을 위임합니다.
- 클라이언트(Controller)는 인터페이스만 주입받으면, 자동으로 프록시를 통해 서비스를 이용하게 됩니다.
이 모든 과정이 Spring의 의존성 주입 메커니즘을 통해 우아하게 이루어집니다.
./gradlew bootRun또는 IntelliJ IDEA에서 SampleApplication.java 파일을 실행합니다.
- URL:
GET http://localhost:8080/api/users - 응답: 모든 사용자 목록 JSON 배열
- URL:
GET http://localhost:8080/api/users/1 - 응답: ID가 1인 사용자의 JSON 객체
- URL:
POST http://localhost:8080/api/users - Headers:
Content-Type: application/json - Body:
{ "username": "newuser", "email": "[email protected]", "role": "USER" } - 응답: 생성된 사용자 JSON 객체
- URL:
PUT http://localhost:8080/api/users - Headers:
Content-Type: application/json - Body:
{ "id": 1, "username": "updateduser", "email": "[email protected]", "role": "ADMIN" } - 응답: 업데이트된 사용자 JSON 객체
- URL:
DELETE http://localhost:8080/api/users/1 - 응답: 204 No Content
애플리케이션 로그를 확인하여 다음과 같은 로그가 출력되는지 확인합니다:
INFO com.designpattern.sample.service.proxy.UserServiceProxy - Proxying getUserById request for user ID: 1
INFO com.designpattern.sample.service.proxy.UserServiceProxy - getUserById completed in 23 ms
이러한 로그는 프록시 패턴이 제대로 작동하고 있음을 확인할 수 있는 증거입니다. 로그에 표시된 실행 시간을 통해 각 메소드의 성능을 모니터링할 수 있습니다.
- 브라우저에서 http://localhost:8080/h2-console 접속
- 다음 정보로 로그인:
- JDBC URL:
jdbc:h2:mem:userdb - Username:
sa - Password: (비워두기)
- JDBC URL:
USERS테이블을 조회하여 데이터 확인