예제 프로젝트 만들기
- 예제 프로젝트 종류
- v1 - 인터페이스와 구현 클래스 (스프링 빈 수동 등록)
- v2 - 인터페이스 없는 구체 클래스 (스프링 빈 수동 등록)
- v3 - 컴포넌트 스캔 (스프링 빈 자동 등록)
- 실무에서 스프링 빈으로 등록할 클래스는 인터페이스가 있는 경우도 없는 경우도 있다.
- 스프링 빈으로 직접 등록하는 경우도, 컴포넌트 스캔으로 자동 등록하는 경우도 있다.
예제 프로젝트 만들기 - v1
public interface OrderRepositoryV1 {
void save(String itemId);
}
public class OrderRepositoryV1Impl implements OrderRepositoryV1 {
@Override
public void save(String itemId) {
//저장 로직
if (itemId.equals("ex")) {
throw new IllegalStateException("예외 발생!");
}
sleep(1000);
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- OrderRepositoryV1 인터페이스 & 구현체 예제
public interface OrderServiceV1 {
void orderItem(String itemId);
}
public class OrderServiceV1Impl implements OrderServiceV1 {
private final OrderRepositoryV1 orderRepository;
public OrderServiceV1Impl(OrderRepositoryV1 orderRepository) {
this.orderRepository = orderRepository;
}
@Override
public void orderItem(String itemId) {
orderRepository.save(itemId);
}
}
- OrderServiceV1 인터페이스 & 구현체 예제
@RequestMapping
@ResponseBody
public interface OrderControllerV1 {
@GetMapping("/v1/request")
String request(@RequestParam("itemId") String itemId);
@GetMapping("/v1/no-log")
String noLog();
}
public class OrderControllerV1Impl implements OrderControllerV1 {
private final OrderServiceV1 orderService;
public OrderControllerV1Impl(OrderServiceV1 orderService) {
this.orderService = orderService;
}
@Override
public String request(String itemId) {
orderService.orderItem(itemId);
return "ok";
}
@Override
public String noLog() {
return "ok";
}
}
- OrderControllerV1 인터페이스 & 구현체 예제
- 스프링 MVC는 @Controller 또는 @RequestMapping 어노테이션이 있어야 스프링 컨트롤러로 인식한다.
- 스프링 컨트롤러로 인식해야 HTTP URL이 매핑되고 동작하며 인터페이스에도 해당 어노테이션들을 사용 가능하다.
- 스프링 부트 3.0 이상부터는 @RequestMapping이 있어도 스프링 컨트롤러로 인식하지 않기에 @Controller / @RestController가 필요하다.
- @ResponseBody : HTTP 메시지 컨버터를 사용해서 응답한다.
- @RequestParam("itemId") String itemId
- 인터페이스에서 @RequestParam("itemId") 값을 생략하면 itemId 단어를 컴파일 이후 자바 버전에 따라 인식하지 못할 수 있다.
- 스프링부트 3.2 이상부터 클래스에서 생략하는 경우 파라미터 인식 문제가 발생
- 자바 컴파일러에 -parameters 옵션을 넣어야 생략할 수 있다.
- 스프링 MVC는 @Controller 또는 @RequestMapping 어노테이션이 있어야 스프링 컨트롤러로 인식한다.
@Configuration
public class AppV1Config {
@Bean
public OrderControllerV1 orderControllerV1() {
return new OrderControllerV1Impl(orderServiceV1());
}
@Bean
public OrderServiceV1 orderServiceV1() {
return new OrderServiceV1Impl(orderRepositoryV1());
}
@Bean
public OrderRepositoryV1 orderRepositoryV1() {
return new OrderRepositoryV1Impl();
}
}
- 스프링 빈 설정 파일V1 예제
@Import(AppV1Config.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app")
public class ProxyApplication {
public static void main(String[] args) {
SpringApplication.run(ProxyApplication.class, args);
}
}
- 메인 애플리케이션 클래스
- @SpringBootApplication 어노테이션의 scanBasePackages : 컴포넌트 스캔을 시작할 위치를 지정하는 속성
- 기본 값은 메인 애플리케이션이 있는 패키지와 하위 패키지를 스캔한다.
- scanBasePackages 속성에 의해서 스프링 설정 파일 AppV1Config는 컴포넌트 스캔이 되지 않는다.
- 원래는 AppV1Config는 @Configuration을 사용했기에 별도로 스프링 빈을 등록하지 않아도 된다.
- @Configuration 내부에는 @Component가 있기에 기본적으로 컴포넌트 스캔의 대상이 된다.
- @Import 어노테이션을 사용해서 별도로 스프링 빈으로 등록
- 원래는 AppV1Config는 @Configuration을 사용했기에 별도로 스프링 빈을 등록하지 않아도 된다.
- @Import
- @Configuration으로 설정한 파일을 두개 이상 사용하는 경우 설정 파일을 등록할 때 사용한다.
- @Import는 일반적으로 스프링 설정 파일을 등록할 때 사용하지만 스프링 빈을 등록할 때 사용할 수도 있다.
- @SpringBootApplication 어노테이션의 scanBasePackages : 컴포넌트 스캔을 시작할 위치를 지정하는 속성
- @Bean vs @Import
- 공통점 : 둘 다 직접 스프링 빈을 등록할 때 사용
- @Bean
- 메소드 레벨에 사용하며 해당 메소드의 반환 객체를 스프링 빈으로 등록할 때 사용한다.
- 개별 객체 등록
- 메소드 레벨에 사용하며 해당 메소드의 반환 객체를 스프링 빈으로 등록할 때 사용한다.
- @Import
- 주로 하나의 설정 클래스에서 다른 설정 클래스를 가져와 통합해서 스프링 빈으로 등록할 때 사용한다.
예제 프로젝트 만들기 - v2
- OrderControllerV2, OrderServiceV2, OrderRepositoryV2, 설정 파일 코드는 V1 구현체와 유사하므로 생략
@Import({AppV1Config.class, AppV2Config.class})
@SpringBootApplication(scanBasePackages = "hello.proxy.app") //주의
public class ProxyApplication {
public static void main(String[] args) {
SpringApplication.run(ProxyApplication.class, args);
}
}
- 메인 애플리케이션 클래스
- @Import({~.class, ~.class})처럼 {}안에 ,로 구분하여 여러 클래스를 등록할 수도 있다.
예제 프로젝트 만들기 - v3
- V2 예제와 차이점
- OrderRepositoryV3 코드는 V2 코드에서 컴포넌트 스캔을 위해 @Repository 추가
- OrderServiceV3 코드는 V2 코드에서 컴포넌트 스캔을 위해 @Service 추가
- OrderControllerV3 코드는 V2 코드에서 컴포넌트 스캔을 위해 @RequestMapping, @ResponseBody 대신 @RestController 추가
- @Repository, @Service, @Controller / @RestController는 내부에 @Component가 있기에 컴포넌트 스캔의 대상이 된다.
요구사항 추가
- 기존 요구사항
- 모든 PUBLIC 메서드의 호출과 응답 정보를 로그로 출력
- 애플리케이션의 흐름을 변경하면 안됨
- 로그를 남긴다고 해서 비즈니스 로직의 동작에 영향을 주면 안됨
- 메서드 호출에 걸린 시간
- 정상 흐름과 예외 흐름 구분
- 예외 발생시 예외 정보가 남아야 함
- 메서드 호출의 깊이 표현
- HTTP 요청을 구분
- HTTP 요청 단위로 특정 ID를 남겨서 어떤 HTTP 요청에서 시작된 것인지 명확하게 구분이 가능해야 함
- 트랜잭션 ID (DB 트랜잭션X)
- 기존 로그 추적기 예제는 템플릿 메소드 패턴 / 콜백 패턴을 사용하더라도 로그를 남기고 싶은 클래스가 있으면 로그 추적기를 적용하기 위해서 해당 클래스 모두를 고쳐야 한다는 문제점이 있다.
- 요구사항 추가
- 원본 코드를 전혀 수정하지 않고, 로그 추적기를 적용해라.
- 특정 메서드는 로그를 출력하지 않는 기능
- 보안상 일부는 로그를 출력하면 안된다.
- 다음과 같은 다양한 케이스에 적용할 수 있어야 한다.
- v1 - 인터페이스가 있는 구현 클래스에 적용
- v2 - 인터페이스가 없는 구체 클래스에 적용
- v3 - 컴포넌트 스캔 대상에 기능 적용
- 추가 요구사항을 통해 원본 코드를 전혀 수정하지 않고 로그 추적기를 도입하는 것이 목표
- 추가 요구사항을 해결하기 위해 프록시가 필요하다.
프록시
- 클라이언트 - 서버 개념은 넓게 사용된다.
- 서버 : 서비스나 상품을 제공하는 사람이나 물건
- 클라이언트 : 의뢰인
- 클라이언트는 서버에 필요한 것을 요청하고 서버는 클라이언트의 요청을 처리한다.
- 해당 개념을 컴퓨터 네트워크에 도입하면 클라이언트는 웹 브라우저, 서버는 요청을 처리하는 웹 서버
- 해당 개념을 객체에 도입하면 요청하는 객체는 클라이언트, 요청을 처리하는 객체는 서버
- 직접 호출 vs 간접 호출
- 직접 호출 : 클라이언트가 서버를 직접 호출하고 처리 결과를 직접 받는 것
- 간접 호출 : 클라이언트가 대리자(Proxy)를 통해서 간접적으로 서버에 요청하는 것
- 대리자를 영어로 프록시라고 한다.
- 직접 호출과 달리 간접 호출을 하면 대리자가 중간에서 여러가지 일을 할 수 있다.
- 접근 제어, 캐싱, 부가 기능 추가, 프록시 체인... 등
- 객체에서 프록시가 되려면 클라이언트는 서버에게 요청한 것인지, 프록시에게 요청한 것인지 조차 몰라야한다.
- 서버와 프록시는 같은 인터페이스를 사용해야 한다.
- 클라이언트가 사용하는 서버 객체를 프록시 객체로 변경해도 클라이언트 코드를 변경하지 않고 동작할 수 있어야한다.
- 클라이언트는 서버 인터페이스에만 의존하고 서버와 프록시는 같은 인터페이스를 사용하며 DI를 사용해서 대체가능하다.
- 런타임 객체 의존 관계
- 런타임에 클라이언트 객체에 DI를 사용해서 Client -> Server에서 Client -> Proxy로 객체 의존관계를 변경해도 클라이언트 코드를 전혀 변경하지 않아도 되며 변경 사실 조차 모른다.
- 프록시 주요 기능
- 접근 제어
- 권한에 따른 접근 차단
- 캐싱
- 지연 로딩
- 부가 기능 추가
- 원래 서버가 제공하는 기능에 추가로 부가 기능을 수행한다.
- ex) 요청 값이나 응답 값을 중간에 변형, 실행 시간을 측정해서 추가 로그를 남기는 것
- 접근 제어
- 프록시 패턴과 데코레이터 패턴은 둘 다 프록시를 사용하는 GOF 디자인 패턴이지만 의도에 따라서 구분한다.
- 프록시 패턴 : 접근 제어가 목적인 프록시를 사용하는 GOF 디자인 패턴 중 하나
- 데코레이터 패턴 : 새로운 기능 추가가 목적인 프록시를 사용하는 GOF 디자인 패턴 중 하나
프록시 패턴 - 예제 (적용x)
public interface Subject {
String operation();
}
@Slf4j
public class RealSubject implements Subject {
@Override
public String operation() {
log.info("실제 객체 호출");
sleep(1000);
return "data";
}
private void sleep(int millis) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 프록시 패턴 적용을 위한 서버 예제 코드
- RealSubject는 Subject 인터페이스를 구현
- operation() : 데이터 조회가 1초 걸린다고 가정
public class ProxyPatternClient {
private Subject subject;
public ProxyPatternClient(Subject subject) {
this.subject = subject;
}
public void execute() {
subject.operation();
}
}
- 클라이언트 예제 코드
@Test
public void noProxyTest() {
RealSubject realSubject = new RealSubject();
ProxyPatternClient client = new ProxyPatternClient(realSubject);
client.execute();
client.execute();
client.execute();
}
- 테스트 예제 코드
- noProxyTest()
- client.execute()
- client -> realSubject를 호출해서 1초가 걸리는 데이터 조회를 3번 반복
- 총 3초의 시간 소요됨
- 만약 데이터가 한번 조회 후 변하지 않는다면 어딘가에 보관해두고 이미 조회한 데이터를 사용하는 것이 성능상 좋다 => 캐시
- client.execute()
- noProxyTest()
- 프록시 패턴의 주요 목적은 접근 제어이며 캐시는 접근 제어 기능 중 하나다.
프록시 패턴 - 예제 (적용O)
- 프록시 객체를 적용해서 기존 로직을 수정하지 않고 캐시를 적용
@Slf4j
public class CacheProxy implements Subject {
private Subject target;
private String cacheValue;
public CacheProxy(Subject target) {
this.target = target;
}
@Override
public String operation() {
log.info("프록시 호출");
if (cacheValue == null) {
cacheValue = target.operation();
}
return cacheValue;
}
}
- 프록시 패턴을 적용한 캐시 예제
- 프록시 객체 CacheProxy도 실제 객체 RealSubject와 모양이 같아야하기에 Subject 인터페이스를 구현
- private Subject target : 클라이언트가 프록시를 호출하면 프록시가 최종적으로 실제 객체를 호출해야하기 때문에 내부에 실제 객체의 참조를 가지고 있어야 한다.
- 프록시가 호출하는 대상을 target이라 한다.
- operation() : cacheValue에 값이 없으면 실제 객체(target)을 호출해서 값을 구하고 저장하고 반환한다. 값이 있다면 실제 객체를 호출하지 않고 cacheValue(캐시 값)를 그대로 반환한다.
- 처음 조회 이후에는 cacheValue를 통해 매우 빠르게 데이터를 조회할 수 있다.
@Test
public void cacheProxyTest() {
RealSubject realSubject = new RealSubject();
CacheProxy cacheProxy = new CacheProxy(realSubject);
ProxyPatternClient client = new ProxyPatternClient(cacheProxy);
client.execute();
client.execute();
client.execute();
}
- 테스트 예제 코드
- cacheProxyTest()
- Client에 RealSubject가 아닌 CacheProxy를 주입, CacheProxy에는 RealSubject를 주입
- Client -> CacheProxy -> RealSubject 런타임 객체 의존 관계가 완성된다.
- client.execute()
- 총 3번 호출
- RealSubject가 아닌 CacheProxy를 호출
- client.execute() 3번 호출 처리 과정
- execute() 호출 -> CacheProxy에 캐시 값이 없음 -> RealSubject 호출, 결과를 캐시에 저장
(1초 소요) - execute() 호출 -> CacheProxy 캐시 값이 있음 -> CacheProxy에서 즉시 반환 (0초)
- execute() 호출 -> CacheProxy 캐시 값이 있음 -> CacheProxy에서 즉시 반환 (0초)
=> 이전에는 총 3초의 시간이 걸렸지만 캐시 프록시를 도입함으로 총 1초의 시간이 소요된다.
- execute() 호출 -> CacheProxy에 캐시 값이 없음 -> RealSubject 호출, 결과를 캐시에 저장
- 총 3번 호출
- Client에 RealSubject가 아닌 CacheProxy를 주입, CacheProxy에는 RealSubject를 주입
- cacheProxyTest()
- 프록시 패턴의 핵심은 RealSubject (서버)코드와 ProxyPatternClient(클라이언트) 코드를 전혀 변경하지 않고 프록시를 도입함으로 접근 제어를 한 것이다.
- 클라이언트 코드 변경 없이 자유롭게 프록시를 넣고 뺄 수 있다.
- 실제 클라이언트 입장에서는 프록시 객체가 주입되었는지 실제 객체가 주입되었는지 알 수 없다.
데코레이터 패턴 - 예제
public interface Component {
String operation();
}
@Slf4j
public class RealComponent implements Component {
@Override
public String operation() {
log.info("RealComponent 실행");
return "data";
}
}
- 데코레이터 패턴 서버 예제 코드
@Slf4j
public class DecoratorPatternClient {
private Component component;
public DecoratorPatternClient(Component component) {
this.component = component;
}
public void execute() {
String result = component.operation();
log.info("result={}", result);
}
}
- 데코레이터 패턴 클라이언트 예제 코드
- 데코레이터 패턴을 적용해서 프록시로 부가 기능을 추가
- ex) 요청, 응답 값을 중간에 변형
실행 시간을 측정해서 추가 로그를 남기는 것
- ex) 요청, 응답 값을 중간에 변형
@Slf4j
public class MessageDecorator implements Component {
private Component component;
public MessageDecorator(Component component) {
this.component = component;
}
@Override
public String operation() {
log.info("MessageDecorator 실행");
String result = component.operation();
String decoResult = "*****" + result + "*****";
log.info("MessageDecorator 꾸미기 전={}, 적용 후={}", result, decoResult);
return decoResult;
}
}
- 데코레이터 패턴을 적용해서 응답 값을 꾸며주는 예제
- 프록시 객체 MessageDecorator는 실제 객체인 RealComponent와 같이 Component를 구현
- Component component : 프록시가 호출해야하는 대상(실제 객체)을 저장한다.
- operation() : 프록시와 연결된 대상을 호출 (component.operation())하고 그 응답 값에 *****을 앞뒤로 더해 꾸며준 다음 반환한다.
- 기존 실제 객체의 응답 값 data -> 꾸민 후 *****data*****
- 기존 예제 (데코레이터 패턴으로 응답 값 꾸미기) + 실행 시간 측정 데코레이터 패턴 적용
@Slf4j
public class TimeDecorator implements Component {
private Component component;
public TimeDecorator(Component component) {
this.component = component;
}
@Override
public String operation() {
log.info("TimeDecorator 실행");
long startTime = System.currentTimeMillis();
String result = component.operation();
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeDecorator 종료, resultTIme={}ms", resultTime);
return result;
}
}
- 데코레이터 패턴을 적용해 시간 측정하는 예제 코드
- client -> timeDecorator -> messageDecorator -> realComponent 순서로 호출 (반환은 역순으로)
- 프록시 체인 방식으로 한 프록시의 응답 값을 다른 프록시가 꾸며주며 계속 체인 형식으로 이어질 수 있다.
- 데코레이터 패턴을 적용하려면 항상 꾸며줄 대상이 있어야한다.
- 꾸며줄 대상(component)을 내부에 필드로 가지고 있어야하며 항상 호출해줘야 한다.
- 이 부분이 중복되기에 component를 필드로 가지고 있는 Decorator 추상 클래스를 만드는 방법도 있다.
=> 추상 클래스로 만들면 클래스 다이어그램에서 어떤 것이 실제 컴포넌트인지 데코레이터인지 명확하게 구분할 수 있게 된다.
- 이 부분이 중복되기에 component를 필드로 가지고 있는 Decorator 추상 클래스를 만드는 방법도 있다.
- 꾸며줄 대상(component)을 내부에 필드로 가지고 있어야하며 항상 호출해줘야 한다.
- 프록시 패턴 vs 데코레이터 패턴
- 둘 다 모양이 거의 같지만 디자인 패턴에서 중요한 것은 겉모양이 아닌 패턴을 만든 의도가 중요하다.
- 프록시 패턴의 의도 : 다른 개체에 대한 접근을 제어하기 위해 대리자(proxy)를 제공
- 데코레이터 패턴의 의도 : 객체에 추가 책임(기능)을 동적으로 추가하고 기능 확장을 위한 유연한 제공
- 프록시를 사용할 때 프록시의 목적이 접근 제어라면 프록시 패턴, 새로운 기능 추가가 목적이라면 데코레이터 패턴이 된다.
인터페이스 기반 프록시 - 적용
- 인터페이스와 구현체가 있는 V1 예제에 프록시를 도입함으로 기존 코드를 수정하지 않고 로그 추적기능 추가
@RequiredArgsConstructor
public class OrderControllerInterfaceProxy implements OrderControllerV1 {
private final OrderControllerV1 target;
private final LogTrace logTrace;
@Override
public String request(String itemId) {
TraceStatus status = null;
try {
status = logTrace.begin("OrderController.request()");
//target 호출
String result = target.request(itemId);
logTrace.end(status);
return result;
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
@Override
public String noLog() {
return target.noLog();
}
}
- ControllerV1에 프록시 패턴 적용한 예제
- 프록시에서 실제 객체(ControllerV1)을 호출하기 위해 내부에 target을 가지고 있다
@RequiredArgsConstructor
public class OrderServiceInterfaceProxy implements OrderServiceV1 {
private final LogTrace logTrace;
private final OrderServiceV1 target;
@Override
public void orderItem(String itemId) {
TraceStatus status = null;
try {
status = logTrace.begin("OrderService.orderItem()");
//target 호출
target.orderItem(itemId);
logTrace.end(status);
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
- ServiceV1에 프록시 패턴 적용한 예제
- 프록시에서 실제 객체(ServiceV1)을 호출하기 위해 내부에 target을 가지고 있다
@RequiredArgsConstructor
public class OrderRepositoryInterfaceProxy implements OrderRepositoryV1 {
private final LogTrace logTrace;
private final OrderRepositoryV1 target;
@Override
public void save(String itemId) {
TraceStatus status = null;
try {
status = logTrace.begin("OrderRepository.save()");
//target 호출
target.save(itemId);
logTrace.end(status);
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
- RepositoryV1에 프록시 패턴 적용한 예제
- 프록시에서 실제 객체(RepositoryV1)을 호출하기 위해 내부에 target을 가지고 있다
- Controller, Service, Repository 모두 프록시를 사용함으로 기존 코드를 변경하지 않고 로그 추적 기능을 추가
@Configuration
public class InterfaceProxyConfig {
@Bean
public OrderControllerV1 orderController(LogTrace logTrace) {
OrderControllerV1Impl controllerImpl = new OrderControllerV1Impl(orderService(logTrace));
new OrderControllerInterfaceProxy(controllerImpl, logTrace);
}
@Bean
public OrderServiceV1 orderService(LogTrace logTrace) {
OrderServiceV1Impl serviceImpl = new OrderServiceV1Impl(orderRepository(logTrace));
return new OrderServiceInterfaceProxy(logTrace, serviceImpl);
}
@Bean
public OrderRepositoryV1 orderRepository(LogTrace logTrace) {
OrderRepositoryV1Impl repositoryImpl = new OrderRepositoryV1Impl();
return new OrderRepositoryInterfaceProxy(logTrace, repositoryImpl);
}
}
- 의존관계 설정 클래스
- 기존 V1 예제에서는 스프링 빈이 ~Impl과 같은 실제 객체를 반환
- 프록시를 사용함으로 프록시를 실제 객체 대신 스프링 빈으로 등록하고 실제 객체는 등록하지 않는다.
- 프록시 객체는 내부에서 실제 객체를 참조해야 한다.
- 스프링 빈을 주입 받으면 실제 객체 대신 프록시 객체가 주입되고 실제 객체는 프록시 객체에 의해 호출된다.
- 실제 객체는 프록시 객체에 의해 참조되고 있기에 가비지 컬렉션되지 않는다.
- 프록시를 적용함으로 스프링 컨테이너에는 프록시 객체가 등록되고 스프링 빈으로 관리한다.
- 프록시 객체는 스프링 컨테이너가 관리하며 자바 힙 메모리에 올라간다.
- 실제 객체는 자바 힙 메모리에는 올라가지만 스프링 컨테이너가 관리하지는 않는다.
- 런타임 호출 과정
- client가 OrderControllerV1 호출
- OrderControllerV1 프록시가 호출되고 로직 수행
- OrderControllerV1 프록시가 실제 객체 OrderControllerV1Impl 호출되고 로직 수행
- 실제 객체 OrderControllerV1Impl가 OrderServiceV1 호출
- OrderServiceV1 프록시가 호출되고 로직 수행
이하 반복(Repository까지)
구체 클래스 기반 프록시 - 예제
@Slf4j
public class ConcreteLogic {
public String operation() {
log.info("ConcreteLogic 실행");
return "data";
}
}
- 인터페이스가 없는 구체 클래스 (서버) 예제
@Slf4j
public class TimeProxy extends ConcreteLogic {
private ConcreteLogic concreteLogic;
public TimeProxy(ConcreteLogic concreteLogic) {
this.concreteLogic = concreteLogic;
}
@Override
public String operation() {
log.info("TimeDecorator 실행");
long startTime = System.currentTimeMillis();
String result = concreteLogic.operation();
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeDecorator 종료, resultTIme={}ms", resultTime);
return result;
}
}
- 구체 클래스에 프록시 적용 예제
- 프록시 TimeProxy는 시간 측정하는 부가 기능을 제공
- 프록시를 사용하기 위해서 인터페이스를 구현해서 만든 것이 아닌 ConcreteLogic클래스를 상속받아서 만든다.
public class ConcreteClient {
private ConcreteLogic concreteLogic;
public ConcreteClient(ConcreteLogic concreteLogic) {
this.concreteLogic = concreteLogic;
}
public void execute() {
concreteLogic.operation();
}
}
- 인터페이스가 없는 구체 클래스 (클라이언트) 예제
- 생성자에는 ConcreteLogic을 주입 받지만 ConcreteLogic 뿐만 아니라 TimeProxy도 주입받을 수 있다.
- ConcreteClient는 ConcreteLogic을 의존하는데 다형성에 의해 ConcreteLogic에는 ConcreteLogic과 TimeProxy 모두 들어갈 수 있다.
- ex) ConcreteLogic = concreteLogic (본인의 타입을 할당)
ConcreteLogic = timeProxy (자식 타입을 할당)
- 생성자에는 ConcreteLogic을 주입 받지만 ConcreteLogic 뿐만 아니라 TimeProxy도 주입받을 수 있다.
- 자바에서 다형성은 인터페이스나 클래스를 구분하지 않고 모두 적용된다.
- 해당 타입과 하위 타입 모두 다형성의 대상이 된다.
구체 클래스 기반 프록시 - 적용
@RequiredArgsConstructor
public class OrderRepositoryConcreteProxy extends OrderRepositoryV2 {
private final LogTrace logTrace;
private final OrderRepositoryV2 target;
@Override
public void save(String itemId) {
TraceStatus status = null;
try {
status = logTrace.begin("OrderRepository.save()");
target.save(itemId);
logTrace.end(status);
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
- OrderRepositoryV2 프록시 예제
- 인터페이스가 아닌 OrderRepositoryV2 클래스를 상속받아서 프록시를 만든다.
- 프록시 내부에는 내부에서 호출할 실제 객체(target)를 가지고 있다.
public class OrderServiceConcreteProxy extends OrderServiceV2 {
private final LogTrace logTrace;
private final OrderServiceV2 target;
public OrderServiceConcreteProxy(LogTrace logTrace, OrderServiceV2 target) {
super(null);
this.logTrace = logTrace;
this.target = target;
}
@Override
public void orderItem(String itemId) {
TraceStatus status = null;
try {
status = logTrace.begin("OrderService.orderItem()");
target.orderItem(itemId);
logTrace.end(status);
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
- OrderServiceV2 프록시 예제
- 인터페이스가 아닌 OrderServiceV2 클래스를 상속받아서 프록시를 만든다.
- 프록시 내부에는 내부에서 호출할 실제 객체(target)를 가지고 있다.
- 클래스 기반 프록시의 단점
- 자바 기본 문법에 의해 자식 클래스를 생성할 때는 항상 super()로 부모 클래스의 생성자를 호출해야 한다.
- super(...)를 생략하면 기본 생성자를 호출하는 super()가 자동으로 생성된다.
- 예제에서 부모 클래스 OrderServiceV2는 기본 생성자가 없고 파라미터 1개를 필수로 받는 생성자만 있기에 파라미터를 넣어서 super(...)를 호출해야 한다.
- 인터페이스 기반 프록시는 이런 고민을 하지 않아도 된다.
- 자바 기본 문법에 의해 자식 클래스를 생성할 때는 항상 super()로 부모 클래스의 생성자를 호출해야 한다.
- super(null) : 클래스 기반 프록시는 부모 객체의 기능을 사용하지 않기 때문에 super(null)로 입력해도 된다.
public class OrderControllerConcreteProxy extends OrderControllerV2 {
private final LogTrace logTrace;
private final OrderControllerV2 target;
public OrderControllerConcreteProxy(LogTrace logTrace, OrderControllerV2 target) {
super(null);
this.logTrace = logTrace;
this.target = target;
}
@Override
public String request(String itemId) {
TraceStatus status = null;
try {
status = logTrace.begin("OrderController.request()");
String result = target.request(itemId);
logTrace.end(status);
return result;
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
@Override
public String noLog() {
return target.noLog();
}
}
- OrderControllerV2 프록시 예제
- 인터페이스가 아닌 OrderControllerV2 클래스를 상속받아서 프록시를 만든다.
- 프록시 내부에는 내부에서 호출할 실제 객체(target)를 가지고 있다.
- noLog()
- target은 결국 상속받은 부모 클래스와 같기에 target 대신 super를 사용해도 되지 않을까?
- target.noLog() 대신 super.noLog()로 호출할 수 있는 것처럼 보이지만 super.noLog()를 사용하면 오버라이딩한 noLog()도 호출돼서 의도한대로 결과가 나오지 않기에 프록시 객체와 원본 객체를 명확하게 분리하기 위해서 target으로 별도로 나눴다.
인터페이스 기반 프록시와 클래스 기반 프록시
- 인터페이스 기반 프록시 vs 클래스 기반 프록시
- 인터페이스가 없어도 클래스 기반으로 프록시를 생성할 수 있다.
- 인터페이스 기반 프록시
- 인터페이스만 같으면 모든 곳에 적용할 수 있다.
- 클래스 기반 프록시
- 해당 클래스에만 적용할 수 있다.
- 상속을 사용함으로 인해 생기는 제약이 있다.
- 부모 클래스의 생성자를 호출해야 한다.
- 클래스에 final 키워드가 붙으면 상속이 불가능하다.
- 메소드에 final 키워드가 붙으면 해당 메소드를 오버라이딩 할 수 없다.
- 인터페이스 기반 프록시가 상속에서 자유롭고 프로그래밍 관점에서도 역할과 구현을 명확하게 나누기에 더 좋다.
- 인터페이스가 없으면 안된다는 것과 캐스팅 관련해서 단점이 있다.
- 인터페이스는 구현을 변경할 가능성이 있을 때 효과적이지만 가능성이 거의 없는 경우 인터페이스를 사용하는 것은 번거롭고 실용적이지 못하기에 인터페이스 없이 구체 클래스를 사용하는 것이 더 좋은 경우도 있다.
- 프록시 클래스를 사용하면 기존 코드를 변경하지 않아도 부가 기능을 추가할 수 있다.
- 문제점 : 프록시를 적용해야 하는 클래스가 많을수록 만들어야하는 프록시 클래스가 많아진다.
- 해결 방법 : 동적 프록시 기술을 사용하면 된다.
정리
- 프록시 : 클라이언트와 서버 사이에서 요청을 처리하고 응답해주는 역할
- 객체에서 프록시가 되려면 클라이언트는 서버에게 요청한 것인지, 프록시에게 요청한 것인지 조차 몰라야한다.
- 서버와 프록시는 같은 인터페이스를 사용해야 한다.
- 클라이언트가 사용하는 서버 객체를 프록시 객체로 변경해도 클라이언트 코드를 변경하지 않고 동작할 수 있어야한다.
- 주요 기능
- 접근 제어
- 권한에 따른 접근 차단
- 캐싱
- 지연 로딩
- 부가 기능 추가
- 원래 서버가 제공하는 기능에 추가로 부가 기능을 수행한다.
- ex) 요청 값이나 응답 값을 중간에 변형, 실행 시간을 측정해서 추가 로그를 남기는 것
- 접근 제어
- 객체에서 프록시가 되려면 클라이언트는 서버에게 요청한 것인지, 프록시에게 요청한 것인지 조차 몰라야한다.
- 프록시 패턴 vs 데코레이터 패턴
- 둘 다 모양이 거의 같고 프록시를 사용하지만 디자인 패턴에서 중요한 것은 겉모양이 아닌 패턴을 만든 의도가 중요하다.
- 프록시 패턴의 의도 : 다른 개체에 대한 접근을 제어하기 위해 대리자(proxy)를 제공
- 데코레이터 패턴의 의도 : 객체에 추가 책임(기능)을 동적으로 추가하고 기능 확장을 위한 유연한 제공
- 프록시를 사용할 때 프록시의 목적이 접근 제어라면 프록시 패턴, 새로운 기능 추가가 목적이라면 데코레이터 패턴이 된다.
- 프록시를 사용하면 프록시 객체를 실제 객체 대신 스프링 빈으로 등록하고 실제 객체는 등록하지 않는다.
- 프록시 객체는 내부에서 실제 객체를 참조해야 한다.
- 스프링 빈을 주입 받으면 실제 객체 대신 프록시 객체가 주입되고 실제 객체는 프록시 객체에 의해 호출된다.
- 실제 객체는 프록시 객체에 의해 참조되고 있기에 가비지 컬렉션되지 않는다.
- 인터페이스 기반 프록시 vs 클래스 기반 프록시
- 인터페이스가 없어도 클래스 기반으로 프록시를 생성할 수 있다.
- 인터페이스 기반 프록시
- 인터페이스만 같으면 모든 곳에 적용할 수 있다.
- 클래스 기반 프록시
- 해당 클래스에만 적용할 수 있다.
- 상속을 사용함으로 인해 생기는 제약
- 부모 클래스의 생성자를 호출해야 한다.
- 클래스에 final 키워드가 붙으면 상속이 불가능하다.
- 메소드에 final 키워드가 붙으면 해당 메소드를 오버라이딩 할 수 없다.
- 인터페이스 기반 프록시가 상속에서 자유롭고 프로그래밍 관점에서도 역할과 구현을 명확하게 나누기에 더 좋다.
출처 : [인프런 김영한 스프링 핵심 원리 - 고급편]
스프링 핵심 원리 - 고급편 강의 | 김영한 - 인프런
김영한 | 스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., 핵심 디자인 패턴, 쓰레드 로컬, 스프링 AOP스프링의 3가지 핵심 고급 개념 이해하기
www.inflearn.com
'Spring > [인프런 김영한 스프링 핵심 원리 - 고급편]' 카테고리의 다른 글
[인프런 김영한 스프링 핵심 원리 - 고급편] 스프링이 지원하는 프록시 (0) | 2024.12.10 |
---|---|
[인프런 김영한 스프링 핵심 원리 - 고급편] 동적 프록시 기술 (2) | 2024.12.08 |
[인프런 김영한 스프링 핵심 원리 - 고급편] 템플릿 메소드 패턴과 콜백 패턴 (1) | 2024.12.05 |
[인프런 김영한 스프링 핵심 원리 - 고급편] 쓰레드 로컬 - ThreadLocal (0) | 2024.12.03 |
[인프런 김영한 스프링 핵심 원리 - 고급편] 예제 만들기 (1) | 2024.12.01 |