이번에는 스프링을 이루는 핵심 디자인 패턴의 일부로 프록시 패턴에 대해서 알아보자.
앞서 다뤘던, 템플릿 콜백 패턴을 비롯하여 공통 로직을 최대한 애플리케이션의 전반적으로 걸쳐 재사용할 수 있도록 하는 방법에 대해서 알아봤었다.
그러나, 여전히 로그 기능 추가로 인한 코드의 수정이 불가피하였다. 그리고 이는 개발자에게 가장 큰 문제로 남게 된다.
따라서 원본 코드의 수정을 필요로하지 않으며, 로그 추적기를 적용할 수 있는 방법에 대해 알아보기 위한 방법을 목표로 내용을 정리해보겠다.
기존 코드의 작동에서 아래와 같은 요구사항이 추가되었다.
요구사항 추가
- 원본 코드 수정없이 로그 추적기를 적용하라.
- 특정 메소드는 로그를 출력하지 않도록 하라.
→ 보안상 일부는 로그를 출력하지 않는 기능
- 다음과 같은 다양한 케이스에 적용할 수 있도록 구현하라.
→ v1 : 인터페이스가 있는 구현 클래스에 적용
→ v2 : 인터페이스가 없는 구현 클래스에 적용
→ v3 : 컴포넌트 스캔 대상에 기능 적용
이러한 과정을 통해, 해결하고자 하는 바는 "원본 코드를 전혀 수정하지 않고, 로그 추적기를 도입" 하는 것이다.
이를 달성하기 위해서는 프록시( Proxy )에 대한 개념 이해가 먼저 필요하다.
먼저, 요구사항에 맞춰 v1, v2, v3 각 version의 예제 프로젝트를 만들어보자.
예제 프로젝트 만들기 v1
< 인터페이스와 구현 클래스 - 스프링 빈으로 수동 등록 >
Controller, Service, Repository에 인터페이스를 도입하고 스프링 빈으로 수동 등록하는 과정을 거쳐보자.
OrderRepositoryV1 (Interface)
public interface OrderRepositoryV1 {
void save(String itemId);
}
OrderRepositoryV1Impl
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();
}
}
}
OrderServiceV1 (Interface)
public interface OrderServiceV1 {
void orderItem(String itemId);
}
OrderServiceV1Imple
public class OrderServiceV1Impl implements OrderServiceV1 {
private final OrderRepositoryV1 orderRepositoryV1;
public OrderServiceV1Impl(OrderRepositoryV1 orderRepositoryV1) {
this.orderRepositoryV1 = orderRepositoryV1;
}
@Override
public void orderItem(String itemId) {
orderRepositoryV1.save(itemId);
}
}
OrderControllerV1 (Interface)
@RequestMapping
@ResponseBody
public interface OrderControllerV1 {
@RequestMapping(path = "/v1/request", method = RequestMethod.GET)
String request(@RequestParam("itemId") String itemId);
@RequestMapping(path = "/v1/no-log", method = RequestMethod.GET)
String noLog();
}
OrderControllerV2Impl
public class OrderControllerV1Impl implements OrderControllerV1 {
private final OrderServiceV1 orderServiceV1;
public OrderControllerV1Impl(OrderServiceV1 orderServiceV1) {
this.orderServiceV1 = orderServiceV1;
}
@Override
public String request(String itemId) {
orderServiceV1.orderItem(itemId);
return "OK";
}
@Override
public String noLog() {
return "";
}
}
→ @RequestMapping : 스프링 MVC는 타입에 @Controller 또는 @RequestMapping 어노테이션을 붙여야 스프링 컨트롤러로 인식하고, HTTP URL이 매핑되며 동작한다.
여기서 @Controller 어노테이션은 구현체인 Class 에만 사용가능하지만, @RequestMapping 어노테이션은 인터페이스에도 적용 가능하다.
그 이유는 @Controller에는 @Component가 적용되었기 때문이다.
@Component는 구현체에만 붙인다? - 인프런 | 커뮤니티 질문&답변
누구나 함께하는 인프런 커뮤니티. 모르면 묻고, 해답을 찾아보세요.
www.inflearn.com
AppV1Config ( 수동 Bean 등록 )
@Configuration
public class AppV1Config {
@Bean
public OrderControllerV1 orderControllerV1() {
return new OrderControllerV1Impl(orderServiceV1());
}
@Bean
public OrderServiceV1 orderServiceV1() {
return new OrderServiceV1Impl(orderRepositoryV1());
}
@Bean
OrderRepositoryV1 orderRepositoryV1() {
return new OrderRepositoryV1Impl();
}
}
어플리케이션에 코드 추가
@Import(AppV1Config.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app") //주의
public class ProxyApplication {
public static void main(String[] args) {
SpringApplication.run(ProxyApplication.class, args);
}
}
→ @ Import(AppV1Config.class) - 클래스를 스프링 빈으로 등록한다. 여기서는 AppV1Config.class을 스프링 빈으로 등록하며, 일반적으로 @Configuration 같은 설정 파일을 등록할 때, 사용하지만, 스프링 Bean을 등록할 때도 사용할 수 있다.
→ @SpringBootApplication(scanBasePackages = "hello.proxy.app") : @ComponentScan 의 기능을 시작할 패키지의 위치를 저장한다. 이 값을 설정하면 해당 패키지와 그 하위 패키지를 컴포넌트 스캔한다.
만약, 해당 값을 설정하지 않으면, 해당 패키지와 하위 패키지를 컴포넌트 스캔한다.
예제 프로젝트 만들기 v2
< 인터페이스 없는 구체 클래스 - 스프링 빈으로 수동 등록 >
인터페이스가 없는 Controller, Service, Repository를 스프링 빈으로 수동 등록해보자.
OrderRepositoryV2
public class OrderRepositoryV2 {
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();
}
}
}
OrderServiceV2
public class OrderServiceV2 {
private final OrderRepositoryV2 orderRepositoryV2;
public OrderServiceV2(OrderRepositoryV2 orderRepositoryV2) {
this.orderRepositoryV2 = orderRepositoryV2;
}
public void orderItem(String itemId) {
orderRepositoryV2.save(itemId);
}
}
OrderControllerV2
@Slf4j
@RequestMapping
@ResponseBody
public class OrderControllerV2 {
private final OrderServiceV2 orderServiceV2;
public OrderControllerV2(OrderServiceV2 orderServiceV2) {
this.orderServiceV2 = orderServiceV2;
}
@RequestMapping(path ="/v2/request", method = RequestMethod.GET)
public String request(String itemId) {
orderServiceV2.orderItem(itemId);
return "OK";
}
@RequestMapping(path="/v2/no-log", method = RequestMethod.GET)
public String noLog() {
return "OK";
}
}
AppV2Config ( 수동 Bean 등록 )
@Configuration
public class AppV2Config {
@Bean
public OrderControllerV2 orderControllerV2() {
return new OrderControllerV2(orderServiceV2());
}
@Bean
public OrderServiceV2 orderServiceV2() {
return new OrderServiceV2(orderRepositoryV2());
}
@Bean
public OrderRepositoryV2 orderRepositoryV2() {
return new OrderRepositoryV2();
}
}
어플리케이션에 코드 추가
@Import({AppV1Config.class, AppV2Config.class })
@SpringBootApplication(scanBasePackages = "hello.proxy.app")
public class ProxyApplication {
public static void main(String[] args) {
SpringApplication.run(ProxyApplication.class, args);
}
}
예제 프로젝트 만들기 v3
< 컴포넌트 스캔으로 스프링 빈 자동 등록 >
컴포넌트 스캔을 이용하여 스프링 빈 자동 등록을 활용한 코드를 만들어 보자.
OrderRepositoryV3
@Repository
public class OrderRepositoryV3 {
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();
}
}
}
OrderServiceV3
@Service
public class OrderServiceV3 {
private OrderRepositoryV3 orderRepositoryV3;
public OrderServiceV3(OrderRepositoryV3 orderRepositoryV3) {
this.orderRepositoryV3 = orderRepositoryV3;
}
public void orderItem(String itemId) {
orderRepositoryV3.save(itemId);
}
}
OrderControllerV3
@RestController
public class OrderControllerV3 {
private final OrderServiceV3 orderService;
public OrderControllerV3(OrderServiceV3 orderService) {
this.orderService = orderService;
}
@GetMapping("/v3/request")
public String request(String itemId) {
orderService.orderItem(itemId);
return "ok";
}
@GetMapping("/v3/no-log")
public String noLog() {
return "ok";
}
}
→ V3 프로젝트의 경우에는 @Componenet 와 @ComponentScan을 사용하기에 별도로 빈 등록할 필요가 없다.
지금까지 스프링 어플리케이션을 개발함에 있어서, 각 케이스를 나눠 아래와 같은 형식의 개발을 진행하였다.
→ v1 : 인터페이스가 있는 구현 클래스에 적용
→ v2 : 인터페이스가 없는 구현 클래스에 적용
→ v3 : 컴포넌트 스캔 대상에 기능 적용
이어지는 내용부터는 각 케이스에 따라 로그 추적기와 같은 부가기능을 추가함에 있어 코드의 변화를 최소화하면서,
개발할 수 있는 방법에 대해서 학습을 이어가보겠다.
'Spring > SpringCore - advance' 카테고리의 다른 글
| 스프링 핵심원리( 심화 ) - 인터페이스 기반 프록시 (0) | 2025.11.07 |
|---|---|
| 스프링 핵심원리( 심화 ) - 프록시 패턴과 데코레이터 패턴(2) (0) | 2025.08.23 |
| 스프링 핵심원리( 심화 ) - 로그 추적기와 디자인패턴( 템플릿 콜백 패턴 ) (0) | 2025.05.30 |
| 스프링 핵심원리( 심화 ) - 로그 추적기와 디자인패턴( 전략 패턴 ) (0) | 2025.03.16 |
| 스프링 핵심원리( 심화 ) - 로그 추적기와 디자인패턴( 템플릿 메서드 패턴 ) (0) | 2025.02.19 |
댓글