BackEnd/Spring

[Spring] 프록시 패턴과 데코레이터 패턴 예제

샤아이인 2022. 8. 9.

내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.

이번 시간에는 우선 예제 프로젝트부터 만들어보자!

디자인 패턴에 대한 글을 다음 게시물 부터 시작할 예정이다.

 

1. 예제 프로젝트 만들기 v1

이번 글 에서는 다음 글에서 설명할 데코레이터, 프록시 패턴을 위한 준비 예제 작업 시간이다.

설명보다는 단순 코드만 많은 점 양해 부탁 드립니다.

 

예제는 크게 3가지 상황으로 만든다.

v1 - 인터페이스와 구현 클래스 - 스프링 빈으로 수동 등록

v2 - 인터페이스 없는 구체 클래스 - 스프링 빈으로 수동 등록

v3 - 컴포넌트 스캔으로 스프링 빈 자동 등록

 

V1 - 인터페이스와 구현 클래스 - 스프링 빈으로 수동 등록

우선 코드는 다음과 같다.

 

▶ OrderRepositoryV1

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();
        }
    }
}

 

▶ OrderServiceV1

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);
    }
}

 

▶ OrderContrllerV1

Spring은 @RequestMapping만 추가해도 컨트롤러로 인식해준다!

@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 null;
    }
}

Controller는 조금 설명이 필요하다.

 

@RequestMapping

스프링MVC는 타입에 @Controller 또는 @RequestMapping 애노테이션이 있어야 스프링 컨트롤러로 인식한다. 그리고 스프링 컨트롤러로 인식해야, HTTP URL이 매핑되고 동작한다. 이 애노테이션은 인터페이스에 사용해도 된다.

 

@ResponseBody

HTTP 메시지 컨버터를 사용해서 응답한다. 이 애노테이션은 인터페이스에 사용해도 된다.

 

@RequestParam("itemId") String itemId

인터페이스에는 @RequestParam("itemId") 의 값을 생략하면 itemId 단어를 컴파일 이후 자바 버전에 따라 인식하지 못할 수 있다.

인터페이스에서는 꼭 넣어주자. 클래스에는 생략해도 대부분 잘 지원된다.


코드를 보면 request() , noLog() 두 가지 메서드가 있다. request() LogTrace 를 적용할 대상이고, noLog() 는 단순히 LogTrace 를 적용하지 않을 대상이다.

 

이제 Bean을 등록하기 위한 Config 파일을 만들어보자!

@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();
    }
}

Bean을 등록시키는 간단한 코드이다.

 

이후 @Import(AppV1Config.class)를 사용하여 config 빈을 등록시켜 줍니다.

@Import(AppV1Config.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app") //주의
public class ProxyApplication {

	public static void main(String[] args) {
		SpringApplication.run(ProxyApplication.class, args);
	}

}

원래였다면 ProxyApplication 이하의 모든 페키지는 자동 스캔 되기 때문에 따로 @Import로 AppV1Config를 Import할 필요가 없다.

하지만 위에서는 이를 사용하고 있다.

 

다음 페키지 구조를 살펴보자.

ProxyApplication 에서 @SpringBootApplication(scanBasePackages = "hello.proxy.app") 로 설정 되어있기 때문에 빨간박스 안의 v1을 전부 등록한다.

하지만 이때 AppV1Config는 자동으로 등록되지 않는다!

scanBasePackages=hello.proxy.app 로 설정했기 때문이다!

 

원래였다면 @Configuration 은 내부에 @Component 애노테이션을 포함하고 있어서 컴포넌트 스캔의 대상이 된다.

base scan 범위가 달라져서 자동 스캔되지 않는 것 이다.

따라서 @Import 를 사용해서 AppV1Config 파일을 추가로 등록시켜주는 것 이다.

 

이후 config파일을 v2, v3로 점진적으로 발전시키면서 원하는 config만 등록하려는 목적이다.

지금은 AppV1Config.class @Import 를 사용해서 설정하지만 이후에 다른 것을 설정한다는 이야기이다.

 

2. 예제 프로젝트 만들기 v2

 

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 orderRepository;

    public OrderServiceV2(OrderRepositoryV2 orderRepository) {
        this.orderRepository = orderRepository;
    }

    public void orderItem(String itemId) {
        orderRepository.save(itemId);
    }
}

 

▶ OrderControllerV2

@Slf4j
@RequestMapping
@ResponseBody
public class OrderControllerV2 {

    private final OrderServiceV2 orderService;

    public OrderControllerV2(OrderServiceV2 orderService) {
        this.orderService = orderService;
    }

    @GetMapping("/v2/request")
    public String request(String itemId) {
        orderService.orderItem(itemId);
        return "ok";
    }

    @GetMapping("/v2/no-log")
    public String noLog() {
        return "no log";
    }
}

@RequestMapping : Spring은 @Controller 또는 @RequestMapping 애노테이션이 있어야 스프링 컨트롤러로 인식한다.

그리고 스프링 컨트롤러로 인식해야, HTTP URL이 매핑되고 동작한다.

 

그런데 여기서는 @Controller 를 사용하지 않고, @RequestMapping 애노테이션을 사용했다.

그 이유는 @Controller 를 사용하면 자동 컴포넌트 스캔의 대상이 되기 때문이다.

우리는 수동 빈 등록을 하는 것이 목표다. 따라서 컴포넌트 스캔과 관계 없는 @RequestMapping 를 타입에 사용했다.

 

▶ AppV2Config

@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();
    }
}

 

▶ ProxyApplication

@Import({AppV1Config.class, AppV2Config.class}) // 추가된 config
@SpringBootApplication(scanBasePackages = "hello.proxy.app")
public class ProxyApplication {

	public static void main(String[] args) {
		SpringApplication.run(ProxyApplication.class, args);
	}

}

@Import를 통해서 AppV2Config가 추가되었다.

실행시 이전 v1과 동일하게 정상작동 한다.

 

3. 예제 프로젝트 만들기 v3

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 final OrderRepositoryV3 orderRepository;

    public OrderServiceV3(OrderRepositoryV3 orderRepository) {
        this.orderRepository = orderRepository;
    }

    public void orderItem(String itemId) {
        orderRepository.save(itemId);
    }
}

 

▶ OrderControllerV3

@Slf4j
@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 "no log";
    }
}

 

다음 게시물 부터 이를 활용하여 학습해 보자!


우리의 기존 로그 추적기에 다음 요구사항이 추가되었다.

 

원본 코드를 전혀 수정하지 않고, 로그 추적기를 적용해라.

특정 메서드는 로그를 출력하지 않는 기능 -> 보안상 일부는 로그를 출력하면 안된다.

 

  • 다음과 같은 다양한 케이스에 적용할 수 있어야 한다.
    • v1 - 인터페이스가 있는 구현 클래스에 적용
    • v2 - 인터페이스가 없는 구체 클래스에 적용
    • v3 - 컴포넌트 스캔 대상에 기능 적용

 

가장 어려문 문제는 원본 코드를 전혀 수정하지 않고, 로그 추적기를 도입하는 것이다.

이 문제를 해결하려면 프록시(Proxy)의 개념을 먼저 이해해야 한다.

 

'BackEnd > Spring' 카테고리의 다른 글

[Spring] 데코레이터 패턴  (0) 2022.08.10
[Spring] 프록시 패턴  (0) 2022.08.10
[Spring] 템플릿 콜백 패턴  (0) 2022.08.07
[Spring] 전략 패턴  (0) 2022.08.07
[Spring] 템플릿 메서드 패턴  (0) 2022.08.06

댓글