[김영한 스프링 review] 스프링 핵심 원리 - 기본편 (3)

조갱·2023년 10월 22일
1

스프링 강의

목록 보기
4/16

[김영한 스프링 review] 스프링 핵심 원리 - 기본편 (2)에서 이어지는 내용입니다.

의존관계 자동 주입

다양한 의존관계 주입 방법

의존 관계 주입 방법은 이전 포스팅에서도 한번 소개한 적이 있다.
한번 더 리마인드 하며 복습해본다.

  • 생성자 주입 (추천)

    • 생성자를 통해 의존관계를 주입한다.
    • 불편, 필수적인 의존관계에 사용한다.
    • 의존관계는 한번 주입되면 바뀔 일이 없기 때문에 생성자 주입을 사용하는게 좋다.
    • 필드를 final 로 사용할 수 있기 때문에 외부에서 변경될 일이 없다.
    • final 필드를 사용하면 반드시 초기화가 되어야 하기 때문에, 혹시라도 생성자에서 주입하는 로직이 누락되면 컴파일에러로 인지할 수 있다.
    • 생성자는 1번만 호출되기 때문에, 최초 1회만 실행됨이 보장된다.
    • 생성자가 하나만 존재하는 경우 @Autowired 생략 가능
    • 테스트코드 작성시에도 용이하다.
  • Setter 주입

    • setXXX 메소드를 통해 의존관계를 주입한다.
    • 선택, 변경이 가능한 의존관계에 사용한다.
    • setter 메소드가 public으로 열리기 때문에 외부에서 수정이 가능해진다.
  • 필드 주입 (가급적 지양)

    • 필드에 @Autowired를 사용하여 의존관계를 주입한다.
    • 코드가 비교적 깔끔하다.
    • DI 프레임워크가 없으면 사용할 수 없다.
    • 어플리케이션의 비즈니스 로직과 크게 관련이 없는 테스트코드나, 구성정보 (Configuration) 에 사용하자.
    • 단위 테스트 코드 작성이 어렵다. (DI 프레임워크가 필요하므로, @SpringBootTest 를 통한 통합테스트만 가능하다.)
  • 메소드 주입

    • 일반적인 메소드에 @Autowired를 사용하여, 파라미터에 의존관계를 주입받을 수 있다.
    • 여러개의 파라미터도 주입받을 수 있다.
    • 일반적으로 사용되지 않는다.
      (의존성은 생성자 주입으로 받고, 메소드 내에서는 필드를 호출하면 되기 때문)
  • 메소드 주입 예제

    @Component
    public class OrderServiceImpl implements OrderService {
    	private MemberRepository memberRepository;
    	private DiscountPolicy discountPolicy;
    
    	@Autowired // 아래 init 이라는 메소드의 MemberRepository, DiscountPolicy 의존성을 주입
    	public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
    		this.memberRepository = memberRepository;
    		this.discountPolicy = discountPolicy;
    	}
    }

옵션 처리

@Autowired 를 통해 의존성을 주입받을 때 옵션을 지정할 수 있다.

  • @Autowired(required=true) > 기본값 (=@Autowired)
    • 기본적으로 @Autowired를 사용하면 required=true 옵션이 들어간다.
    • 자동 주입 대상이 없으면 오류를 발생시킨다.
  • @Autowired(required=false)
    • 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨
  • org.springframework.lang.@Nullable
    • 자동 주입할 대상이 없으면 null이 입력된다.
  • Optional<>
    • 자동 주입할 대상이 없으면 Optional.empty 가 입력된다.

아래 예제코드는 Spring Bean으로 관리되지 않는 클래스인 NoSpringBean의 의존성을 주입받으려는 코드이다.
당연하게도, 의존성 자동 주입은 주입 대상 객체와 주입 받는 객체 둘 다 스프링이 관리하는 빈이어야 한다.
아래 예제에서 메소드가 위치한 클래스는 스프링 빈으로 관리되고, NoSpringBean은 (스프링 빈이 아닌) 일반 클래스라고 가정한다.

//호출 안됨
@Autowired(required = false)
public void setNoBean1(NoSpringBean bean) {
	System.out.println("setNoBean1 = " + bean); // 호출 자체가 안됨
}

//null 호출
@Autowired
public void setNoBean2(@Nullable NoSpringBean bean) {
	System.out.println("setNoBean2 = " + bean); // setNoBean2 = null
}

//Optional.empty 호출
@Autowired(required = false)
public void setNoBean3(Optional<NoSpringBean> bean) {
	System.out.println("setNoBean3 = " + bean); // setNoBean3 = Optional.empty
}

롬복과 최신 트랜드

롬복에서 지원하는 @RequiredArgsConstructor어노테이션을 클래스에 붙여주면
컴파일 단계에서 final 필드들을 모아서 1개의 primary constructor를 생성한다.

이전에 기본 생성자 1개만 존재하면 @Autowired를 생략해도 된다고 작성했는데,
여기서도 그 원리가 적용된다. 즉,

@Component
public class OrderServiceImpl implements OrderService {
	private final MemberRepository memberRepository;
	private final DiscountPolicy discountPolicy;

	@Autowired
	public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
		this.memberRepository = memberRepository;
		this.discountPolicy = discountPolicy;
	}
    
    ...
}

에서 기본 생성자가 1개이기 때문에 @Autowired를 제거할 수 있고

@Component
public class OrderServiceImpl implements OrderService {
	private final MemberRepository memberRepository;
	private final DiscountPolicy discountPolicy;
	
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
		this.memberRepository = memberRepository;
		this.discountPolicy = discountPolicy;
	}
    
    ...
}

@RequiredArgsConstructor어노테이션을 통해 final 이 붙은 필드를 기본 생성자로 만들어주면

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
	private final MemberRepository memberRepository;
	private final DiscountPolicy discountPolicy;
    
    ...
}

처럼 쓸 수 있다.

참고로, 본인은 kotlin으로 개발하기 때문에 롬복은 안쓴다.
(kotlin은 class를 자바 바이트코드로 변환될 때 기본적으로 primary constructor를 1개 생성해주기 때문에..)
코틀린 코드는 이렇다. (코틀린 쓰세요, 두번 쓰세요.)

@Component
class OrderServiceImpl(
	val memberRepository: MemberRepository,
    val discountPolicy: DiscountPolicy,
) : OrderService {
	...
}

조회 빈이 2개 이상 - 문제

@Autowired 는 타입(Type)으로 조회한다.
스프링은 빈 주입 시, 동일한 타입이 2개 이상 존재하면 NoUniqueBeanDefinitionException 예외를 발생시킨다.

@Component
public class FixDiscountPolicy implements DiscountPolicy {}
@Component
public class RateDiscountPolicy implements DiscountPolicy {}
@Autowired
private DiscountPolicy discountPolicy

// NoUniqueBeanDefinitionException: No qualifying bean of type 'hello.core.discount.DiscountPolicy' available: expected single matching bean but found 2: fixDiscountPolicy,rateDiscountPolicy

에러메시지를 자세히 읽어보면, 친절하게 1개의 빈을 기대했으나, fixDiscountPolicy, rateDiscountPolicy 2개를 발견했다고 알려준다.

@Autowired 필드 명, @Qualifier, @Primary

이렇게 2개 이상의 Bean이 조회되는 경우 해결 방법을 알아보자.

  • @Autowired 필드 명 매칭
  • @Qualifier끼리 매칭 빈 이름 매칭
  • @Primary 사용

@Autowired 필드 명 매칭

@Autowired는 아래와 같은 순서로 Unique한 Bean을 찾아 주입한다.
타입 매칭 > 필드 이름 매칭 > 파라미터 이름 매칭

따라서, 타입 조회 시 2개 이상의 Bean이 조회됐다면, 이후에 필드 이름을 조회한다.

@Autowired
private DiscountPolicy rateDiscountPolicy

위와 같이 코드를 수정하면
DiscountPolicy 타입으로 조회된 FixDiscountPolicy, RateDiscountPolicy 중에
필드명이 일치한 RateDiscountPolicy 1개를 unique하게 선택한다.

@Qualifier끼리 매칭 빈 이름 매칭

@Qualifier는 추가적인 구분자를 명시하는 방법이다. (bean 이름을 지정하는것과는 다르다.)
아래와 같은 순서로 Unique한 Bean을 찾아 주입한다.
@Qualifier끼리 매칭 > 빈 이름 매칭 > NoSuchBeanDefinitionException 예외 발생

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(
	MemberRepository memberRepository,
	@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy
) {
	this.memberRepository = memberRepository;
	this.discountPolicy = discountPolicy;
}

만약 로직에서 mainDiscountPolicy라는 Qualifier를 찾지 못하면
Bean 이름이 mainDiscountPolicy인것을 찾아 매핑하며,
그것도 없다면 NoSuchBeanDefinitionException 예외를 발생시킨다.

@Primary 사용

@Primary 는 우선순위를 정하는 방법이다.
@Autowired 시에 여러 빈이 매칭되면 @Primary 가 우선권을 가진다.

@Autowired
private DiscountPolicy discountPolicy

위 코드에서 rateDiscountPolicy 가 우선적으로 선택되길 희망한다면,
아래와 같이 RateDiscountPolicy에 @Primary를 사용할 수 있다.

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}

Primary와 Qualifier 동시 사용

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
@Qualifier("mainDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(
	MemberRepository memberRepository,
	@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy
) {
	this.memberRepository = memberRepository;
	this.discountPolicy = discountPolicy;
}

위와 같이 RateDiscountPolicy가 Primary이고,
FixDiscountPolicy 클래스가 mainDiscountPolicy로 Qualifier가 지정되고, 로직에서도 사용한다면 어떻게 될까?

Primary는 단순히 우선권을 부여하는 것이고
Qualifier는 상세하게 추가 구분자를 부여한다.

스프링은 항상 구체적인것을 우선적으로 사용하기 때문에, Qualifier를 우선적으로 매칭하여
FixDiscountPolicy가 주입된다.

조회한 빈이 모두 필요할 때, List, Map

분기에 따라 Bean을 다르게 사용해야할 수 있다.
(할인 정책을 선택적으로 사용할 수 있어야 하는 경우)

이 때는 List와 Map으로 중복되는 Bean들을 모두 받을 수 있다.
하지만 여러명이 협업하여 개발하는 특성 상, List 혹은 Map으로 받으면
해당 타입의 Bean들이 어떤게 있는지 구분하기가 어렵기 때문에 사용을 지양한다.

@Service
class DiscountService {
	private final Map<String, DiscountPolicy> policyMap;
	private final List<DiscountPolicy> policies;
	
    // 생성자가 1개이기 때문에 Autowired 생략 가능
    public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
		this.policyMap = policyMap;
		this.policies = policies;
		
        System.out.println("policyMap = " + policyMap);
		System.out.println("policies = " + policies);
	}

	public int discount(Member member, int price, String discountCode) {
		// 분기에 따라 Bean을 선택적으로 사용
	}
}

빈 생명주기 콜백

빈 생명주기 콜백 시작

네트워크 IO 등을 지원하는 빈들은 초기화 시점에 커넥션풀을 미리 생성해두어야 한다.
또한, 사용 후 스프링이 종료되기 전에 메모리를 해제하는것 또한 잊지 않아야 한다.

스프링은 위와 같은 기능들을 제공하는데, 대표적으로 아래와 같다.

  • 인터페이스(InitializingBean, DisposableBean)
  • 설정 정보에 초기화 메서드, 종료 메서드 지정
  • @PostConstruct, @PreDestroy 어노테이션

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

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

InitializingBean, DisposableBean 인터페이스

InitializingBean : afterPropertiesSet() 메서드로 초기화를 지원한다.
DisposableBean : destroy() 메서드로 소멸을 지원한다.

public class NetworkClient implements InitializingBean, DisposableBean {
	
    ...
    
	@Override
	public void afterPropertiesSet() throws Exception {
		// 빈이 초기화될 때 수행할 작업
	}

	@Override
	public void destroy() throws Exception {
		// 빈이 소멸될 때 수행할 작업
	}
}

인터페이스의 단점

  • 위 인터페이스는 스프링 전용 인터페이스로, 스프링에 의존성을 가진다.
  • 초기화, 소멸 메서드의 이름을 변경할 수 없다.
  • 내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.

인터페이스를 사용하는 초기화, 종료 방법은 스프링 초창기에 나온 방법들이고, 지금은 다음의 더 나은 방법 들이 있어서 거의 사용하지 않는다.

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

구성 정보(configuration) 에 스프링 빈을 등록할 때, 초기화/소멸 메소드를 지정할 수 있다.
구성 정보에 등록하는 정보이기 때문에, 외부 라이브러리에도 초기화/소멸 기능을 적용할 수 있다.

구성 정보

@Configuration
static class LifeCycleConfig {
	@Bean(initMethod = "init", destroyMethod = "close")
	public NetworkClient networkClient() {
		return new NetworkClient();
	}
}

빈 등록 대상 코드

public class NetworkClient {

	... // 비즈니스 로직
    
	public void init() {
		// 초기화 로직
	}
    
    public void close() {
		// 소멸 로직
	}
}

구성 정보에 init메소드를 초기화 대상으로, destroyMethod를 소멸 대상 메소드로 지정했다.
근데 여기서 이상한 점이 있다.

외부 라이브러리에도 초기화/소멸 로직을 적용할 수 있다며?
구성정보에서 호출하는 초기화/소멸 메소드명은 빈에서 설정하는게 아니라
빈 등록 대상 코드에 존재하는데?

관련해서 찾아보니, 인프런에 동일한 질답이 있었다.
결론을 먼저 얘기하자면, 외부 라이브러리에 초기화/소멸 메소드가 정의되어있어야 한다는건데
외부 라이브러리에 지정이 안돼있으면 못쓰는건가?
외부 라이브러리에서는 걔네를 메소드로만 지정을 해놓나? 애초에 초기화/소멸 로직을 구현하지 않고?
라는 의문점이 든다,, 요건 공부좀 더하고 예제랑 같이 글을 보충해두겠다.

설정 정보 사용 특징

  • 메서드 이름을 자유롭게 줄 수 있다.
  • 빈 등록 대상이 스프링 코드에 의존하지 않는다. (참고하면 좋은 인프런 질답)
  • 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있다.

종료 메서드 추론

@Bean의 destroyMethod 는 기본값이 (inferred) (추론)으로 등록되어 있다.
이 추론 기능은 빈 등록 대상 내에 close, shutdown 라는 이름의 메서드가 있으면 자동으로 호출해준다.
추론 기능을 사용하기 싫으면 destroyMethod="" 처럼 빈 공백을 지정하면 된다.

@PostConstruct, @PreDestroy 어노테이션

어노테이션을 사용하여 초기화/소멸 메소드를 지정할 수도 있다.

@PostConstruct: 초기화 메소드를 지정
@PreDestroy: 소멸 메소드를 지정

* 참고로, 이 어노테이션들은 스프링의 어노테이션이 아닌
javax.annotation 패키지에 존재하는 어노테이션이다.
따라서 스프링으로 관리되는 객체가 아니더라도 초기화/소멸을 위해 사용할 수 있다.

public class NetworkClient {
	... // 비즈니스 로직

	@PostConstruct
	public void init() {
		// 초기화 시 호출
	}

	@PreDestroy
    public void close() {
		// 소멸시 호출
	}
}

어노테이션 방법의 특징

  • 최신 스프링에서 가장 권장하는 방법이다.
  • 어노테이션 하나만 붙이면 되므로 매우 편리하다.
  • javax.annotation.PostConstruct 패키지에 존재하는 어노테이션이다.
    -> 스프링에 종속적인 기술이 아닌 JSR-250라는 자바 표준이다.
    -> 따라서 스프링이 아닌 다른 컨테이너에서도 동작한다.
  • 컴포넌트 스캔과 잘 어울린다.
  • 유일한 단점은 외부 라이브러리에는 적용하지 못한다는 것이다.
    외부 라이브러리를 초기화, 종료 해야 하면 @Bean의 기능을 사용해야 한다.
  • Reactive Programming에서는 의도한대로 동작하지 않을 수 있다.
    관련 포스팅을 참고하자.

빈 스코프

빈 스코프란?

빈 스코프란, 빈의 생명주기를 의미한다.
즉, 빈이 생성되어서부터 소멸되기까지의 범위를 말하는데,
우리가 지금까지 알아본 것은 Singleton Scope이다.

  • singleton (기본 스코프)
    • 스프링 컨테이너의 시작부터 종료까지 유지되는 가장 넓은 범위의 스코프이다.
  • prototype
    • 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다.
  • 웹 관련 스코프
    • request: HTTP 요청이 들어오고 나갈때 까지 유지되는 스코프
    • session: HTTP 세션이 생성되고 종료될 때 까지 유지되는 스코프
    • application: 서블릿 컨텍스트와 동일한 생명주기를 가지는 스코프
    • websocket : 웹소켓과 동일한 생명주기를 가지는 스코프

스프링 공식 문서에서 예제와 자세한 내용을 확인할 수 있다.

아래와 같이 스코프를 지정할 수 있다.

@Component
@Scope("prototype")
public class MyBean {}
@Bean
@Scope("prototype")
MyBean myBean() {
	return new MyBean();
}

프로토타입 스코프

프로토타입 스코프는 일반적으로 객체 인스턴스를 생성하는것 처럼, 매번 새로운 객체가 생성된다.

그렇다면, 그냥 로직에서 new MemberService();로 인스턴스를 생성하는 것과 어떤 차이가 있을까?

  1. 스프링 컨테이너에서 관리되기 때문에, DIP, OCP를 유지할 수 있다.
  2. 스프링의 다른 부가기능을 사용할 수 있다.
  3. AOP, 트랜잭션 관리 등과 함께 사용할 수 있다.
  4. 스프링의 라이프사이클과 콜백, 초기화작업을 사용할 수 있다.

싱글톤 빈의 요청 과정 그림은 이전 포스팅에서 확인할 수 있다.

프로토타입 빈은 스프링에서 생성과 의존관계 주입까지만 관여하므로, @PreDestroy같은 소멸 기능은 수행되지 않는다.

싱글톤 빈과 함께 사용시 문제점

@Component
@RequiredArgsConstructor
class SingletonBean {
	private final PrototypeBean prototypeBean;
	...
}

과 같이 싱글톤 내에서 프로토타입 빈이 사용되는 경우,
SingletonBean 내의 PrototypeBean은 SingletonBean이 생성되는 최초 1회에만 초기화가 된다. (사용 할 때마다 새로 생성되는 것이 아니다!)

즉, 싱글톤 내에 있는 프로토타입 빈은 싱글톤처럼 사용된다.

싱글톤 빈과 함께 사용시 Provider로 문제 해결

싱글톤 빈 내에부에 있는 프로토타입 빈을 호출할 때마다 새로운 객체를 얻기 위해서는, 간단하게도 호출하는 시점에 객체를 새로 만들면 된다.

@Autowired
private ApplicationContext ac;

public void logic() {
	PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
	prototypeBean.doSomething();
}

하지만, 이 코드는 스프링 컨테이너를 주입받기 때문에 스프링 컨테이너에 종속적이게 된다.

위와 같이 의존성을 외부에서 주입(DI) 받는게 아니라, 의존성을 찾는것을 의존관계 조회, 의존관계 탐색 (DL: Dependency LookUp) 이라고 한다.
그리고, Provider는 이러한 DL 기능을 제공한다.

ObjectFactory, ObjectProvider

ObjectFactory는 의존관계를 검색하는 getObject() 메소드만 존재하며
ObjectProvider는 이러한 ObjectFactory를 상속받아 다양한 기능을 제공한다.

@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;

public void logic() {
	PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
	prototypeBean.doSomething();
}

이제는 ApplicationContext를 직접 주입받아 스프링 컨테이너에 종속적인 코드가 아니다.

JSR-330 Provider

JSR-330 Provider는 스프링의 기능이 아닌 자바의 기능이다.
그래서 이 기능을 사용하기 위해서는 build.gradle(.kts) 에 의존성을 추가해줘야한다.

*스프링부트 3.0 미만
build.gradle : implementation 'javax.inject:javax.inject:1'
build.gradle.kts : implementation("javax.inject:javax.inject:1")

*스프링부트 3.0 이상
build.gradle : implementation 'jakarta.inject:jakarta.inject-api:2.0.1'
build.gradle.kts : implementation("jakarta.inject:jakarta.inject-api:2.0.1")

@Autowired
private Provider<PrototypeBean> provider;

public void logic() {
	PrototypeBean prototypeBean = provider.get();
	prototypeBean.doSomething();
}

웹 스코프

웹 스코프는 웹 환경에서만 동작한다.
웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다.
따라서 @PostConstruct 종료 메서드가 호출된다.

웹 스코프에는 위에서 기술한것 처럼
request, session, application, websocket
이 존재한다. 각각의 작동원리는 비슷하기 때문에, request만 다루도록 한다.

request 스코프 예제 만들기

웹 환경을 만들기 위해서는 의존성을 추가해야한다.

  • build.gradle
    implementation 'org.springframework.boot:spring-boot-starter-web'
  • build.gradle.kts
    implementation("org.springframework.boot:spring-boot-starter-web")

을 추가하고, gradle reload 후에 프로젝트를 실행하면 내장 톰캣이 8080프토로 뜨는것을 확인할 수 있다.
포트를 변경하고 싶다면 application.properties 파일에서 server.port=포트로 변경하면 된다.

@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);
	}
}
@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";
	}
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
	private final MyLogger myLogger;
	public void logic(String id) {
		myLogger.log("service id = " + id);
	}
}

각 사용자별로 UUID, Request URL과 함께 로깅하는 기능이다.
스프링부트를 실행하고 GET /log-demo를 요청하면 원하는 결과를 얻을 수 있을거라 생각했는데, 의외로 어플리케이션 실행 단계부터 예외가 발생한다.

Error creating bean with name 'myLogger': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton;

request 스코프는 사용자의 요청이 들어와야 초기화가 되는데, 스프링부트가 실행되는 시점에는 초기화가 되어있지 않기 때문이다.

스코프와 Provider

앞에서 알아봤던 ObjectProvider 를 사용하면 쉽게 해결할 수 있다.

@Controller
@RequiredArgsConstructor
public class LogDemoController {
	private final ObjectProvider<MyLogger> myLoggerProvider;
	
    @RequestMapping("log-demo")
	@ResponseBody
	public String logDemo(HttpServletRequest request) {
		...
        MyLogger myLogger = myLoggerProvider.getObject();
		myLogger.setRequestURL(requestURL);
		...
	}
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
	private final ObjectProvider<MyLogger> myLoggerProvider;
	public void logic(String id) {
		MyLogger myLogger = myLoggerProvider.getObject();
		myLogger.log("service id = " + id);
	}
}

스코프와 프록시

해결은 됐지만, 코드도 지저분하고, 매번 .getObject()를 사용하는것도 번거롭다.
이것은 프록시를 사용하여 해결할 수 있다.

Provider를 사용하기 이전 코드 기준으로, MyLogger의 Scope에 proxyMode 옵션을 부여하자.

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
	...
}

ScopedProxyMode.TARGET_CLASS -> 클래스에 사용하는 경우
ScopedProxyMode.INTERFACE -> 인터페이스에 사용하는 경우

위 코드로 실행하면 Provider를 사용한것 처럼 잘 동작될 것이다.

동작 원리

먼저 주입된 myLogger를 확인해보자.

System.out.println("myLogger = " + myLogger.getClass());

출력결과

myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$b68b726d

MyLogger에 CGLIB 프록시가 적용된 것을 확인할 수 있다.
스프링이 실행되면서 빈이 주입될 때, 우리가 만든 MyLogger가 아닌 프록시 객체가 주입되며
실제 호출 시점에서는 프록시 객체가 실제 MyLogger를 반환하여 지연생성이 가능해진다.

profile
A fast learner.

0개의 댓글