Post

스프링 기본편

싱글톤(Singleton)

스프링은 기본적으로 객체를 싱글톤으로 관리한다.

스프링 컨테이너가 없다면 요청 당 하나의 객체를 새로 생성해야되기 때문에 메모리 낭비가 심하다.

그렇기에 스프링은 객체를 딱 1개만 생성되고, 그 객체를 공유하도록 설계되어있다. 이것을 싱글톤이라고 한다.

하지만 일반 싱글톤 패턴은 여러 문제가 있다.

싱글톤 패턴 문제점

  • 구현 코드가 김
  • 의존관계상 클라이언트가 구체 클래스에 의존할 수도 있음 -> DIP를 위반
  • 구체 클래스에 의존해서 OCP 원칙 위반할 가능성 있음
  • 테스트가 어려움
  • 유연성이 떨어짐
  • 안티 패턴으로 불림

이런 문제를 스프링에서는 사용자가 직접 싱글톤 패턴을 적용하지 않아도 싱글톤으로 관리해준다.

스프링 컨터이너는 싱글톤 컨테이너로써 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.

위의 싱글톤 패턴을 해결하면서 싱글톤으로 유지할 수 있다.

image

싱글톤 방식의 주의점

  • 싱글톤 방식으로 객체를 관리할 땐 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 stateful하게 설계해서는 안된다. stateless로 설계해야된다.

  • stateless

    • 특정 클라이언트에 의존적인 필드가 있으면 안됨
    • 특정 클라이언트가 값을 변결할 수 있는 필드가 있으면 안됨
    • 가급적 읽기만 가능해야 함
    • 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 됨

CGLIB

  • 코드 생성 라이브러리로서 런타임에 동적으로 자바 클래스의 프록시를 생성해주는 기능을 제공한다. 인터페이스가 아닌 클래스에 대해서 동적 프록시를 생성할 수 있다.

  • 간단히 말해, 스프링이 CGLIB이라는 바이트코드 조작 라이브러리를 사용해 임의의 다른 클래스를 만들고, 그 클래스를 빈에 등록한다. 이를 통해 싱글톤이 보장되도록 해준다.

컴포넌트 스캔

  • 스프링 빈에 등록하는 방법
  • config 파일에서 @Bean으로 등록하는 방법도 있지만 등록해야 될 빈이 많아지면 많이 번거로워진다.
  • 그때 사용할 수 있는 것이 컴포넌트 스캔이다. 추가로 의존관계도 자동으로 주입하는 @Autowired라는 기능도 제공한다.

  • 컴포넌트 스캔은 @Componet 애노테이션이 붙은 클래스들을 스캔해서 스프링 빈으로 등록한다.

image

image

  • 모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸린다. 그래서 필요한 위치부터 탐색하도록 위치를 지정할 수 있다.

    하지만 패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것을 권장한다. (참고로 스프링 부트의 대표 시작 정보인 @SpringBootApplication를 이 프로젝트 시작 루트 위치에 두는 것이 관례 -> 이 설정 안에 @ComponentScan이 들어있음)

  • 컴포넌트 스캔 대상

    • @Component : 컴포넌트 스캔에서 사용
    • @Controller : 스프링 MVC 컨트롤러에서 사용, 스프링 MVC 컨트롤러로 인식
    • @Service : 스프링 비즈니스 로직에서 사용
    • @Repository : 스프링 데이터 접근 계층에서 사용, 데이터 접근 계층으로 인식, 데이터 계층의 예외를 스프링 예외로 변환
    • @Configuration : 스프링 설정 정보에서 사용, 스프링 설정 정보로 인식, 스프링 빈이 싱글톤을 유지하도록 추가 처리

컴포넌트 스캔 필터

  • includeFilters : 컴포넌트 스캔 대상을 추가로 지정
  • excludeFilters : 컴포넌트 스캔에서 제외할 대상 지정
  • 보통 @Component로 충분해 잘 사용 X

수동 빈 등록 vs 자동 빈 등록

  • config 파일을 통해 수동으로 등록한 빈 이름과 컴포넌트 스캔으로 등록한 빈 이름이 같으면 수동 빈 등록이 우선권을 가져 덮어쓴다.
  • 이런 결과를 의도한 것이라면 문제는 없지만 만약 아니라면 이런 버그들이 잡기 힘들다. 이런 부분은 항상 조심하자.
  • 하지만 최근 스프링 부트에서는 위와 같은 상황이 발생할 때 오류가 발생하도록 바꿈

의존 관계 주입

  • 의존 관계 주입은 크게 4가지
    • 생성자 주입
    • setter 주입
    • 필드 주입
    • 일반 메서드 주입
  • 위 4가지 중 생성자 주입만 사용한다고 알면 된다.

생성자 주입

  • 생성자 호출 시점에 딱 1번만 호출되는 것이 보장
  • 불변, 필수 의존 관계에 사용

  • 생성자가 1개면 @Autowired 생략 가능

옵션 처리

  • 주입할 스프링 빈이 없어도 동작해야 할 때가 있다.
  • @Autowired만 사용시 required 옵션이 기본값인 true로 되어있어 자동 주입 대상이 없으면 오류가 발생
  • 처리하는 방법
    • @Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨
    • @Nullable : 자동 주입할 대상이 없으면 null
    • Optional<> : 자동 주입할 대상이 없으면 Optional.empty가 입력

생성자 주입을 해야하는 이유

  • 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경하는 일 없음(불변해야함)
  • 수정자 주입 시, setter가 public으로 열려있어 다른 누군가 실수로 변경할 수 있는 위험이 있음
  • 생성자 주입은 1번만 호출되므로 불변함을 보장할 수 있음, final 키워드도 사용 가능하다.
  • 대부분의 경우 생성자 주입을 선택하는 것이 옳다. 가끔 옵션이 필요하면 setter 주입을 선택

룸북을 사용한 주입

  • @RequiredArgsConstructor는 final이 붙은 필드를 모아 자동으로 생성자를 만들어줌
  • 막상 개발해보면 대부분의 필드에 final 키워드를 붙인다. 그렇기 때문에 룸북을 사용하면 편하다.

조회 빈이 2개 이상일 때

  • @Autowired는 클래스 Type으로 조회한다.

  • 만약 Type으로 조회 시 같은 타입의 클래스가 2개라면 문제가 발생한다. 아래와 같이 말이다.

    image

해결법

  • @Autowired 필드 명 매칭
    • @Autowired는 타입 매칭을 시도하고 만약 여러 빈이 있다면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.
  • @Qualifier
    • @Qualifier(“빈 이름”) 으로 등록할 이름을 지정해준 뒤 주입할 생성자에 똑같이 @Qualifier(“빈 이름”)을 붙여주면 스프링이 알아서 매칭해준다.
  • @Primary
    • @Primary는 우선수위를 정하는 방법이다. @Autowired 시에 여러 빈이 매칭될 시 @Primary가 우선권을 가진다.
  • @Primary VS @Qualifier
    • @Primary는 기본값처럼 동작하고, @Qualifier는 매우 상세하게 동작

조회한 빈이 모두 필요할 때 (List, Map 사용)

  • 의도적으로 해당 타입의 스프링 빈이 모두 필요한 경우도 있다.

    image

  • DiscountService는 Map으로 모든 DiscountPolicy를 주입 받는다. (fixDiscountPolicym, rateDiscountPolicy)
  • discount() 는 discountCode로 fixDiscountPolicy가 넘어오면 map에서 fixDiscountPolicy를 찾아서 실행된다. rateDiscountPolicy도 마찬가지

자동 주입 VS 수동 주입

  • 자동 기능을 기본으로 사용

  • 스프링은 @Component 뿐만 아니라 @Service, @Controller 등 계층에 맞춰 지원하기 때문에 매우 편리

  • 애플리케이션은 크게 업무로직과 기술 지원 로직으로 나뉨

    • 업무 로직 빈 : 컨트롤러, 서비스, 리포지토리 등이 업무 로직, 보통 비즈니스 요구사항을 개발할 때 추가, 변경
    • 기술 지원 빈 : 기술적인 문제나 AOP를 처리할 때 주로 사용, db 연결, 공통 로그 처리와 같은 업무 로적을 지원하기 위한 기술들
    • 업무 로직 : 보통 컨트롤러, 서비스, 리포지토리 처럼 어느정도 유사한 패턴이 있다. 이런 곳에는 자동 주입을 사용
    • 기술 지원 로직 : 업무 로직과 비교해 그 수가 적고, 보통 애플리케이션 전반에 걸쳐서 영향을 미침. 업무로직은 문제가 발생했을 때 어디가 문제인지 명확히 들어나지만, 기술 지원 로직은 적용이 잘 되는지, 잘 작동 되는지 파악하기 힘듬. 그렇기 때문에 가급적 수동 빈 등록을 사용해서 명확하게 드러내는 것이 좋다.
  • 비즈니스 로직 중에서 다형성을 적극 활용할 때는 수동 주입 고려

    • 예를 들어 위의 Map<String, DiscountPolicy>에 주입 받는 상황에서 어떤 빈들이 DiscountPolicy로 Map에 들어올지 파악하기가 힘들다.

    • 이런 경우 다름 사람이 볼 때 한 눈에 파악하기 힘들어 유지 보수에 안 좋다.

    • 그렇기 때문에 아래와 같이 특정 패키지에 같이 묶는 것이 좋다.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      
      @Configuration
      public class DiscountPolicyConfig { 
          @Bean
      	public DiscountPolicy rateDiscountPolicy() { 
      		return new RateDiscountPolicy();
          } 
          @Bean
      	public DiscountPolicy fixDiscountPolicy() { 
      		return new FixDiscountPolicy();
          } 
      }
      

정리

  • 편리한 자동 기능을 기본으로 사용하자
  • 직접 등록하는 기술 지원 객체는 수동 등록
  • 다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해보자

빈 생명주기 콜백

  • db 커넥션 풀이나, 네트워크 소켓 등과 같이 애플리케이션 시작 지점에 연결하고 종료 시점에 연결을 종료하는 작업이 필요할 때가 있다.
  • 하지만 스프링 빈의 라이프 사이클은 객체 생성 -> 의존관계 주입이다. 초기화 작업을 하려면 의존 관계가 주입이 끝난 후에 가능한데 이 의존 관계 주입이 끝나는 시점을 개발자는 알 수가 없다.
  • 위와 같은 문제를 해결하기 위해 스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해 초기화 시점을 알려주는 다양한 기능을 제공한다. 또 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다. 따라서 안전하게 종료 작업을 진행할 수 있다.

스프링 빈 이벤트 라이프 사이클

스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸 전 콜백 -> 종료

  • 초기화 콜백: 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
  • 소멸전 콜백 : 빈이 소멸되기 직전에 호출

위의 설명을 보고 ‘생성자에서 생성할 때 작업을 하면 되는거 아니야?’ 라는 의문이 들 것이다.

하지만 객체의 생성과 초기화는 분리해야한다.

그 이유는 보통 생성자는 필수 정보를 받고, 메모리를 할당해서 객체를 생성하는 책임을 가진다. 반면 초기화는 이렇게 생성된 값들을 활용해 외부 커넥션과 연결하는 등 무거운 동작을 수행한다.

따라서 생성자 안에서 무거운 초기화 작업을 하는 것보다 둘을 명확히 나누는 것이 유지보수 관점에서 좋다. 물론 가벼운 초기화 작업의 경우 생성자에서 처리하는 것이 나을 수도 있다.

스프링이 지원하는 빈 생명주기 콜백

  • 인터페이스
  • 설정 정보에 초기화 메서드, 종료 메서드 지정
  • @PostConstruch, @PreDestroy 애노테이션 지줜

인터페이스 InitializingBean, DisposableBean

  • InitializingBean : afterPropertiesSet() 메서드로 초기화를 지원한다.
  • DisposableBean : destroy() 메서드로 소멸을 지원한다.
  • 단점
    • 스프링 전용 인터페이스로 스프링에 의존함
    • 초기화, 소멸 메서드의 이름 변경 불가
    • 외부 라이브러리에 적용 불가
  • 이런 단점 때문에 거의 사용 X

빈 등록 초기화, 소멸 메서드 지정

  • 설정 정보에 @Bean(initMethod = "init", destroyMethod = "close") 처럼 초기화, 소멸 메서드를 지정할 수 있다.
  • 특징
    • 메서드 이름 지정 가능
    • 스프링 코드에 의존 X
    • 코드가 아니라 설정 정보에 사용해 코드를 고칠 수 없는 외부 라이브러리에도 적용 가능
  • 종료 메서드 추론
    • @Bean의 destroyMethod는 기본값이 (inferred) (추론)으로 등록되어있음
    • 이 추론 기능은 close, shutdown라는 이름의 메소드를 자동을 호출. 따라서 따로 destroyMethod를 지정하지 않아도 작동
    • 사용하기 싫다면 destroyMethod=”“처럼 빈 공백으로 지정하면 됨

애노테이션 @PostConstruct, @PreDestroy

  • 원하는 메서드에 @PostConstruct, @PreDestroy 를 붙이면 초기화와 종료를 실행할 수 있음
  • 특징
    • 스프링에서 권장하는 방법, 편리함
    • javax 패키지로 스프링에 종속적인 기술이 아니라 자바 표준. 스프링 종속 X (javax.~로 시작하는 패키지는 자바 표준이라는 의미)
    • 유일한 단점은 외부 라이브러리에 적용 못함

정리

  • 보통 애노테이션을 사용하자
  • 만약 외부 라이브러리에 초기화, 종료를 적용해야 하면 @Bean의 initMethod, destroyMethod를 사용

빈 스코프

  • 스코프란 빈이 존재할 수 있는 범위를 뜻한다.
  • 스프링 컨테이너의 경우 싱글톤으로 컨테이너가 종료될 때 까지 유지된다.
  • 스프링의 다양한 스코프
    • 싱글톤 : 기본 스코프. 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
    • 프로토타입 : 컨테이너가 빈의 생성과 의존관계 주입까지만 관여하고 그 이후로는 관리하지 않는 스코프
    • 웹 관련 스코프
      • request : 웹 요청이 들어오고 나갈때까지 유지되는 스코프
      • session : 웹 세션이 생성되고 종료될 때까지 유지되는 스코프
      • application : 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프
  • @Scope(“스코프 명”)으로 지정할 수 있음

프로토타입 스코프

아래 그림을 통해 살펴보자

image

image

  • 여기서 핵심은 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리한다는 것이다. 그 이후로는 관리하지 않는다. @PreDestroy 같은 종료 메소드는 호출하지 않는다. 필요하다면 직접 호출해야된다.

  • 싱글톤 + 프로토타입 함께 사용 시 발생하는 문제

    • 싱글톤 빈에 프로토타입 빈을 주입 받아서 사용 시 원하는 결과는 프로토타입 빈을 사용할 때마다 새로운 빈을 생성하는 것을 원할 것이다.

    • 하지만 싱글톤 빈 안에 프로토타입 빈을 주입 받는다면 주입 시 처음 생성된 빈이 싱글톤 빈과 같은 생명주기를 가지고 유지된다.

    • Provider로 문제를 해결할 수 있다.

      1
      2
      3
      4
      5
      6
      7
      
      @Autowired
      private ApplicationContext ac;
          
      public int logic() {
          PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class); // prototypeBean이 필요할 때 빈을 생성
          // ... 비즈니스 로직
      }
      
    • 이와 같이 의존관계를 외부에서 주입(DI) 받는 것이 아니라 직접 필요한 의존관계를 찾는 것을 Dependency Lookup(DL) 의존관계 조회(탐색) 이라고 한다. 이 또한 스프링에서 제공해준다.

ObjectFactory, ObjectProvider

  • 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공해주는 것이 ObjectProvider이다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    @Autowired
    private ObjectProvider<PrototypeBean> prototypeBeanProvider; // 제네릭으로 지정한 타입의 빈을 적용
      
    public int logic() {
    	PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
        // 제네릭으로 지정한 타입의 빈을 찾아서 반환함 (DL)
          
        prototypeBean.addCount();
    	int count = prototypeBean.getCount(); 
    	return count;
    }
    
  • 특징 : ObjectFactory 상속, 옵션, 스트림 처리 등 편의기능이 많고, 별도의 라이브러리 필요 없음, 스프링에 의존

  • 스프링에서 제공하는 기능으로 기능이 단순해 단위테스트나 mock 코드 만들기가 쉬워짐

JSR-330 Provider

  • javax.inject.Provider(자바 표준)을 사용하는 방법, 스프링 부트 3.0dms jakarta.inject.Provider를 사용
1
2
3
4
5
6
7
8
9
10
11
@Autowired
private Provider<PrototypeBean> Provider; // 제네릭으로 지정한 타입의 빈을 적용

public int logic() {
	PrototypeBean prototypeBean = provider.get();
    // 제네릭으로 지정한 타입의 빈을 찾아서 반환함 (DL)
    
    prototypeBean.addCount();
	int count = prototypeBean.getCount(); 
	return count;
}
  • 특징
    • get() 메서드 하나로 기능이 매우 단순
    • 별도의 라이브러리가 필요
    • 자바 표준

정리

  • 프로토타입은 매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요하다면 사용하면 된다.

  • 하지만 대부분의 문제는 싱글톤 빈으로 해결이 가능해 직접 사용하는 일은 드물다.

  • ObjectProvider와 JSR-330 Provider 둘 중 무엇을 쓸지 정할 때는 ObjectProvider는 편의 기능을 많이 제공해 편리하기 장점이 있다. 하지만 스프링이 아닌 다른 컨테이너에서도 사용해야된다면 자바 표준인 JSR-330 Provider를 선택하자.

    보통 우리는 스프링 컨테이너를 사용하기 때문에 스프링에서 제공하는 기능을 사용하자.

웹 스코프

  • 웹 스코프는 웹 환경에서만 동작
  • 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다. 종료 메서드 또한 호출 됨
  • 종류
    • request : HTTP 요청 하나가 들어오고 나갈 때까지 유지되는 스코프. 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고 관리됨
    • session : HTTP Session과 동일한 생명주기를 가지는 스코프
    • application : 서블릿 컨텍스트와 동일한 생명주기를 가지는 스코프
    • websocket : 웹 소켓과 동일한 생명주기를 가지는 스코프

image

  • 그림과 같이 request당 하나의 빈 인스턴스가 생성된다.

request 스코프 예제

  • 동시에 여러 HTTP 요청이 들어오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다. 이럴 때 request 스코프가 사용된다.

  • 다음과 같이 로그를 남기는 기능을 request 스코프로 개발해보자

    1
    2
    3
    4
    
    [d06b992f...] request scope bean create
    [d06b992f...][http://localhost:8080/log-demo] controller test
    [d06b992f...][http://localhost:8080/log-demo] service id = testId 
    [d06b992f...] request scope bean close
    
  • MyLogger

    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
    
    @Component
    @Scope(value = "request")
    public class MyLogger {
          
      private String uuid;
      private String requestURL;
          
      public void setRequestURL(String requestURL) {
        this.requestURL = requestURL;
      }
          
      public void log(String message) {
        System.out.println("[" + uuid + "]" + "[" + requestURL + "] " +message);
      }
          
      @PostConstruct
      public void init() {
        uuid = UUID.randomUUID().toString();
        System.out.println("[" + uuid + "] request scope bean create:" + this);
      }
          
      @PreDestroy
      public void close() {
        System.out.println("[" + uuid + "] request scope bean close:" + this);
      }
    }
    
    • 로그를 출력하기 위한 클래스이다.
    • @Scope에 (value = “request”)를 사용해 request 스코프로 지정했다. 이로써 HTTP 요청 당 하나씩 생성되고, 요청이 끝나는 시점에 소멸한다.
    • requestURL은 빈이 생성되는 시점에서 알 수 없으므로, 외부에서 setter로 입력 받는다.
  • Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    @Controller
    @RequiredArgsConstructor
    public class LogDemoController {
          
        private final LogDemoService logDemoService; 
    	private final MyLogger myLogger;
                                      
        @RequestMapping("log-demo") 
        @ResponseBody
    	public String logDemo(HttpServletRequest request) {
    		String requestURL = request.getRequestURL().toString();
        	myLogger.setRequestURL(requestURL); 
            myLogger.log("controller test");
            logDemoService.logic("testId");
    		return "OK";
        } 
    }
    
    • requestURL을 MyLogger에 저장하는 부부은 컨트롤러보다 공통 처리가 가능한 스프링 인터셉터나 서블릿 필터 같은 곳을 활용하는 것이 좋다.
  • Service

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    @Service
    @RequiredArgsConstructor 
    public class LogDemoService {
          
        private final MyLogger myLogger; 
                                   
    	public void logic(String id) {
            myLogger.log("service id = " + id); 
        }
    }
    
    • 여기서 만약 request scope를 사용하지 않고 파라미터로 이 모든 정보를 서비스 계층에 넘긴다면 파라미터가 많아 지저분해진다.
    • 더 큰 문제는 requestURL 같은 웹과 관련된 정보가 웹과 관련없는 서비스 계층까지 넘어가게된다. 웹 관련된 부분은 컨트롤러에서 처리하고 서비스 계층은 웹 기술에 종속되지 않고, 가급적 순수하게 유지하는 것이 좋다.

위의 코드는 실행 시 오류가 난다. 그 이유는 request scope 빈은 실제 요청이 들어와야 생성할 수 있기 때문이다. 이럴 때 쓸 수 있는 것이 Provider이다.

스코프와 Provider

  • Provider 활용 Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    @Controller
    @RequiredArgsConstructor
    public class LogDemoController {
          
    	private final LogDemoService logDemoService;
    	private final ObjectProvider<MyLogger> myLoggerProvider;
          
        @RequestMapping("log-demo")
        @ResponseBody
    	public String logDemo(HttpServletRequest request) {
    		String requestURL = request.getRequestURL().toString(); 
    		MyLogger myLogger = myLoggerProvider.getObject();
            myLogger.setRequestURL(requestURL); 
            myLogger.log("controller test");
            logDemoService.logic("testId");
    		return "OK";
        } 
    }
    
  • Provider 활용 Service

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    @Service
    @RequiredArgsConstructor 
    public class LogDemoService {
          
    	private final ObjectProvider<MyLogger> myLoggerProvider; 
          
    	public void logic(String id) {
    		MyLogger myLogger = myLoggerProvider.getObject();
            myLogger.log("service id = " + id); 
        }
    }
    
  • ObjectProvider로 request scope 빈의 생성을 지연할 수 있다.
  • ObjectProvider.getObject() 를 호출하는 시점까지 지연시켜 request scope 빈의 생성을 요청이 진행 중일 때만 처리하도록 했다.

스코프와 프록시

  • Provider 대신 프록시 방식을 이용해도 된다.

    1
    2
    3
    4
    
    @Component
    @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) 
    public class MyLogger {
    }
    
    • proxyMode를 사용하면 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있다.
  • 이렇게 하면 Provider를 사용하지 않아도 된다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    @Controller
    @RequiredArgsConstructor
    public class LogDemoController {
          
    	private final LogDemoService logDemoService; 
    	private final MyLogger myLogger;
          
        @RequestMapping("log-demo") 
        @ResponseBody
    	public String logDemo(HttpServletRequest request) {
    		String requestURL = request.getRequestURL().toString();
            myLogger.setRequestURL(requestURL); 
            myLogger.log("controller test");
            logDemoService.logic("testId");
    		return "OK";
        } 
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    @Service
    @RequiredArgsConstructor
    public class LogDemoService { 
          
    	private final MyLogger myLogger; 
          
    	public void logic(String id) {
            myLogger.log("service id = " + id); 
        }
    }
    

웹 스코프와 프록시 동작 원리

  • 주입된 myLogger를 확인해보자

    1
    
    myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$b68b726d
    
  • 위와 같이 CGLIB라는 라이브러리를 사용해 내 클래스를 상속 받은 가짜 프록시 객체를 만들어 주입한다.

    image

  • 가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.

  • 프록시 객체를 이용하면 마치 싱글톤 빈을 사용하듯 편리하게 request scope을 사용 가능하다.

정리

  • 사실 Provider를 사용하든, 프록시를 사용하듯 핵심은 진짜 객체 조회를 필요한 시점까지 지연처리한다는 점이다.
  • 이렇듯 애노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체했다. 이것이 바로 다형성과 DI 컨테이너가 가진 큰 강점이다.

  • 주의점
    • 마치 싱글톤을 사용하는 것 같지만 다르게 동작하므로 주의해서 사용
    • 이런 특별한 scope는 꼭 필요한 곳에서 최소화해서 사용하자. 무분별하게 사용하면 유지보수가 어렵다.

This post is licensed under CC BY 4.0 by the author.