7 분 소요

 인프런 스프링 핵심 원리 - 고급편을 학습하고 정리한 내용 입니다.

프로젝트 생성

프로젝트를 강의에서 직접 제공해서 프로젝트 깃허브 링크를 올린다.

나는 스프링 부트 3.x , 자바 21을 사용할 것이기 때문에 살짝 바꿔줘야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
plugins {  
    id 'java'  
    id 'org.springframework.boot' version '3.3.1'  
    id 'io.spring.dependency-management' version '1.1.5'  
}  
  
group = 'hello'  
version = '0.0.1-SNAPSHOT'  
java {  
    toolchain {  
       languageVersion = JavaLanguageVersion.of(21)  
    }  
}  
  
configurations {  
    compileOnly {  
       extendsFrom annotationProcessor  
    }  
}  
  
repositories {  
    mavenCentral()  
}  
  
dependencies {  
    implementation 'org.springframework.boot:spring-boot-starter-web'  
    compileOnly 'org.projectlombok:lombok'  
    annotationProcessor 'org.projectlombok:lombok'  
    testImplementation 'org.springframework.boot:spring-boot-starter-test'  
    //테스트에서 lombok 사용  
    testCompileOnly 'org.projectlombok:lombok'  
    testAnnotationProcessor 'org.projectlombok:lombok'  
}  
  
test {  
    useJUnitPlatform()  
}

이런 식으로 11 -> 21, 2.x.x -> 3.3.1 로 바꾸기 때문에 gradle 수정했고

혹시 javax 가 있으면 jakarta 로 패키지를 바꿔줘야 한다.

다음과 같이 나오면 완료

예제 프로젝트 만들기 v1

다양한 상황에서 프록시 사용법을 이해하기 위해 다음과 같은 기준으로 기본 예제 프로젝트를 만들어보자.

  • v1 - 인터페이스와 구현 클래스 - 스프링 빈으로 수동 등록
  • v2 - 인터페이스 없는 구체 클래스 - 스프링 빈으로 수동 등록
  • v3 - 컴포넌트 스캔으로 스프링 빈 자동 등록

실무에서는 스프링 빈으로 등록할 클래스는 인터페이스가 있는 경우도 있고 없는 경우도 있다. 그리고 스프링 빈을 수동으로 직접 등록하는 경우도 있고, 컴포넌트 스캔으로 자동으로 등록하는 경우도 있다.

이런 다양한 케이스에 프록시를 어떻게 적용하는지 알기 위해 다양한 예제를 준비해보자.

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

지금까지 보아왔던 Controller, Service, Repository에 인터페이스를 도입하고, 스프링 빈으로 수동 등록해 보자

hello.proxy.app.v1.OrderRepositoryV1

1
2
3
4
5
package hello.proxy.app.v1;  
  
public interface OrderRepositoryV1 {  
    void save(String itemId);  
}

hello.proxy.app.v1.OrderRepositoryV1Impl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class OrderRepositoryV1Impl implements OrderRepositoryV1{  
    @Override  
    public void save(String itemId) {  
        // 저장 로직  
        if (itemId.equals("ex"))  
            throw new IllegalStateException("예외 발생!!");  
  
        sleep(1000);  
    }  
  
    private void sleep(long millis) {  
        try {  
            Thread.sleep(millis);  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
}

hello.proxy.app.v1.OrderServiceV1

1
2
3
public interface OrderServiceV1 {  
    void orderItem(String itemId);  
}

hello.proxy.app.v1.OrderControllerV1

1
2
3
4
5
6
7
8
9
@RestController  
public interface OrderControllerV1 {  
  
    @GetMapping("/v1/request")  
    String request(@RequestParam("itemId") String itemId);  
  
    @GetMapping("/v1/no-log")  
    String noLog();  
}

주의 !
인터페이스이다. 스프링 부트 3.0 이상에서는 클래스 레벨에 @RequestMapping이 있어도 스프링 컨트롤러로 인식하지 않는다. 오직 @Controller가 있어야 스프링 컨트롤러로 인식한다. 참고로 @RestController는 해당 애노테이션 내부에 @Controller를 포함하고 있으므로 인식 된다.

hello.proxy.app.v1.OrderControllerV1Impl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 관련 애노테이션이 정의되어 있다.

AppV1Config

이제 스프링 빈으로 수동 등록해보자.

hello.proxy.config.AppV1Config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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();  
    }  
}

빈으로 클래스들을 등록하는 코드다.

ProxyApplication - 코드 추가

1
2
3
4
5
6
7
8
9
@Import(AppV1Config.class)  
@SpringBootApplication(scanBasePackages = "hello.proxy.app.v3") //주의  
public class ProxyApplication {  
  
    public static void main(String[] args) {  
       SpringApplication.run(ProxyApplication.class, args);  
    }  
  
}

이제 여기다가 등록했는데, 여기가 설명이 필요하다.

  • @Import(AppV1Config.class) : 클래스를 스프링 빈으로 등록한다. 여기 서는 AppV1Config.class를 스프링 빈으로 등록한다. 일반적으로 @Configuration같은 설정 파일을 등록할 때 사용하지만, 스프링 빈을 등록할 때도 사용할 수 있다.
  • @SpringBootApplication(scanBasePackages = "hello.proxy.app") : @ComponentScan의 기능과 같다. 컴포넌트 스캔을 시작할 위치를 지정한다. 이 값을 설정하면 해당 패키지와 그 하위 패키지를 컴포넌트 스캔한다. 이 값을 사용하지 않으면 ProxyApplication이 있는 패키지와 그 하위 패키지를 스캔한다. 참고로 v3에서 지금 설정한 컴포넌트 스캔 기능을 사용한다.

주의
강의에서는 @Configuration을 사용한 수동 빈 등록 설정을 hello.proxy.config위치에 두고 점진적으로 변경할 예정이다. 지금은 AppConfigV1.class@Import를 사용해서 설정하지만 이후에 다른 것을 설정할 거다.

@Configuration은 내부에 @Component애노테이션을 포함하고 있어서 컴포넌트 스캔의 대상이 된다. 따라서 컴포넌트 스캔에 의해 hello.proxy.config위치의 설정 파일들이 스프링 빈으로 자동 등록 되지 않도록 컴포넌트 스캔의 시작 위치를 scanBasePackages = "hello.proxy.app.v3"로 설정해야 한다. (스프링 부트 3.0 때문에 컨트롤러 애노테이션을 사용해서 자동 컴포넌트 스캔을 하기 때문에 v3로 디테일게 지정해 줬다.)

다음과 같이 나오면 성공이다.

예제 프로젝트 만들기 v2

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

hello.proxy.app.v2.OrderRepositoryV2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class OrderRepositoryV2 {  
    public void save(String itemId) {  
        // 저장 로직  
        if (itemId.equals("ex"))  
            throw new IllegalStateException("예외 발생!!");  
  
        sleep(1000);  
    }  
  
    private void sleep(long millis) {  
        try {  
            Thread.sleep(millis);  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
}

hello.proxy.app.v2.OrderServiceV2

1
2
3
4
5
6
7
8
9
10
11
12
public class OrderServiceV2 {  
  
    private final OrderRepositoryV2 orderRepository;  
  
    public OrderServiceV2(OrderRepositoryV2 orderRepository) {  
        this.orderRepository = orderRepository;  
    }  
  
    public void orderItem(String itemId) {  
        orderRepository.save(itemId);  
    }  
}

hello.proxy.app.v2.OrderControllerV2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Slf4j  
@RestController  
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 "ok";  
    }  
}

AppV2Config

hello.proxy.config.AppV2Config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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();  
    }  
}

hello.proxy.ProxyApplication

1
2
3
4
5
6
7
8
9
10
//@Import(AppV1Config.class)  
@Import({AppV1Config.class, AppV2Config.class})  
@SpringBootApplication(scanBasePackages = "hello.proxy.app.v3") //주의  
public class ProxyApplication {  
  
    public static void main(String[] args) {  
       SpringApplication.run(ProxyApplication.class, args);  
    }  
  
}

크게 다른 건 없고,

1
@Import({AppV1Config.class, AppV2Config.class})  

다음과 같이 배열로 등록을 두 개 했다.

v2 로 호출 했을 때도 잘 나온다.

예제 프로젝트 만들기 v3

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

hello.proxy.app.v3.OrderRepositoryV3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Repository  
public class OrderRepositoryV3 {  
  
    public void save(String itemId) {  
        // 저장 로직  
        if (itemId.equals("ex"))  
            throw new IllegalStateException("예외 발생!!");  
  
        sleep(1000);  
    }  
  
    private void sleep(long millis) {  
        try {  
            Thread.sleep(millis);  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
}

hello.proxy.app.v3.OrderServiceV3

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service  
public class OrderServiceV3 {  
  
    private final OrderRepositoryV3 orderRepository;  
  
    public OrderServiceV3(OrderRepositoryV3 orderRepository) {  
        this.orderRepository = orderRepository;  
    }  
  
    public void orderItem(String itemId) {  
        orderRepository.save(itemId);  
    }  
}

hello.proxy.app.v3.OrderControllerV3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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";  
    }  
}

자 이제 @Repository, @Service, @Controller 애노테이션을 사용해서 컴포넌트 스캔의 대상이 되게 했다.

1
@SpringBootApplication(scanBasePackages = "hello.proxy.app.v3")

해당 v3패키지부터 컴포넌트 스캔의 위치이기 때문에 자동으로 스캔된다.

v3도 정상 작동한다.

요구 사항 추가

지금까지 로그 추적기를 만들어서 기존 요구사항을 모두 만족했다.

기존 요구 사항

  • 모든 PUBLIC 메서드의 호출과 응답 정보를 로그로 출력
  • 애플리케이션의 흐름을 변경하면 안됨
    • 로그를 남긴다고 해서 비즈니스 로직의 동작에 영향을 주면 안됨
  • 메서드 호출에 걸린 시간
  • 정상 흐름과 예외 흐름 구분
    • 예외 발생 시 예외 정보가 남아야 함
  • 메서드 호출의 깊이 표현
  • HTTP 요청을 구분
    • HTTP 요청 단위로 특정 ID를 남겨서 어떤 HTTP 요청에서 시작된 것인지 명확하게 구분이 가능해야 함.
    • 트랜잭션 ID (DB 트랜잭션X)

예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
정상 요청
[796bccd9] OrderController.request()
[796bccd9] |-->OrderService.orderItem()
[796bccd9] | |-->OrderRepository.save()
[796bccd9] | |<--OrderRepository.save() time=1004ms
[796bccd9] |<--OrderService.orderItem() time=1014ms
[796bccd9] OrderController.request() time=1016ms

예외 발생
[b7119f27] OrderController.request()
[b7119f27] |-->OrderService.orderItem()
[b7119f27] | |-->OrderRepository.save()
[b7119f27] | |<X-OrderRepository.save() time=0ms
ex=java.lang.IllegalStateException: 예외 발생!
[b7119f27] |<X-OrderService.orderItem() time=10ms
ex=java.lang.IllegalStateException: 예외 발생!
[b7119f27] OrderController.request() time=11ms
ex=java.lang.IllegalStateException: 예외 발생!

하지만

하지만 이 요구사항을 만족하기 위해서 기존 코드를 많이 수정해야 한다. 코드 수정을 최소화 하기 위해 템플릿 메서드 패턴과 콜백 패턴도 사용했지만, 결과적으로 로그를 남기고 싶은 클래스가 수백 개라면 수백 개의 클래스를 모두 고쳐야 한다.

로그를 남길 때 기존 원본 코드를 변경해야 한다는 사실 그 자체가 개발자에게는 가장 큰 문제로 남는다.

기존 요구 사항에 다음 요구 사항이 추가되었다.

추가 요구 사항

  • 원본 코드를 전혀 수정하지 않고, 로그 추적기를 적용해라.
  • 특정 메서드는 로그를 출력하지 않는 기능
    • 보안 상 일부는 로그를 출력하면 안된다.
  • 다음과 같은 다양한 케이스에 적용할 수 있어야 한다.
    • v1 - 인터페이스가 있는 구현 클래스에 적용
    • v2 - 인터페이스가 없는 구체 클래스에 적용
    • v3 - 컴포넌트 스캔 대상에 기능 적용

가장 어려운 문제는 원본 코드를 전혀 수정하지 않고, 로그 추적기를 도입하는 것이다. 이 문제를 해결하려면 프록시 (Proxy)의 개념을 먼저 이해해야 한다

댓글남기기