[Spring] 의존관계 자동 주입, 빈 생명주기, 빈 스코프, 프록시 빈

[Spring] 의존관계 자동 주입, 빈 생명주기, 빈 스코프, 프록시 빈

이전 글 이어서 진행합니다.

컴포넌트 스캔을 이용한 자동 등록

자바 빈을 등록할 때는 어노테이션 기반으로 자동 설정을 사용한다. @ComponentScan 어노테이션을 설정 파일에 추가해주고, 자바 파일로 작성한 클래스에 @Component 어노테이션을 추가하면, 스프링이 구동할 때, @Component 어노테이션을 스캔해서 빈 으로 등록을 해준다.( @Component 어노테이션 이외에도 빈으로 등록하는 모든 어노테이션( @Controller , @Service 등)을 빈으로 만들어서 등록해준다)

@Component( basePackages = " package path ", includeFilters = @Filter(type = FilterType.ANNOTATION, classes = annotation class) excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = annotation class) )

@Component 어노테이션의 부가 설정으로 컴포넌트 스캔 탐색 위치를 지정할 수 있다. 탐색 위치를 지정하면, 해당 패키지 및 하위 패키지의 컴포넌트를 스캔한다. 기본적으로는 설정 파일이 있는 패키지를 기준으로 하위 패키지 전부를 탐색한다.

커스텀한 어노테이션을 포함 또는 제외할 수 있는 옵션이나, 특정 클래스 빈을 제외하는 것도 "FilterType 설정과 해당 클래스 명시"를 통해 가능하다.

일반적으로는 프로젝트 시작 루트에 메인 설정 정보를 작성한 파일을 두고, basePackages 지정을 생략한다.

빈 자동 주입

등록된 빈을 자동 주입하기 위해 @Autowired 라는 어노테이션을 사용한다. @Autowired 어노테이션으로 의존관계 주입이 필요하다고 알려주면, 스프링 컨테이너는 자동으로 빈을 찾아서 주입을 시켜준다.

빈 자동 주입으로는 크게 4가지 방법이 있다.

생성자 주입 수정자(Setter) 주입 필드 주입 일반 메서드 주입

public class OrderServiceImpl implemenst OrderSerivce { private final MemberRepository memberRepository; private final DiscountPolicy discountPolicy; // 3. @Autowired private MemberRepository memberRepository; // 3. @Autowried private DiscountPolicy discountPolicy; 1. @Autowired public OrderServiceImpl(MemberRepository memberRepositroy, DiscountPolicy discountPolicy) { this.memberRepository = memberRepository; this.discountPolicy = discountPolicy; } 2. @Autowried public void setMemberRepository(MemberRepository memberRepository) { this.memberRepository = memberRepository } 2. @Autowired public void setDiscountPolicy(DiscountPolicy discountPolicy) { this.discountPolicy = discountPolicy } 4. @Autowired public void init(MemberRepository memberRepositroy, DiscountPolicy discountPolicy) { this.memberRepository = memberRepository; this.discountPolicy = discountPolicy; } }

1.생성자 주입

위 코드의 1번에 해당하는 방법이 생성자 주입이다. 객체를 생성하는 생성자 위에 @Autowired 를 붙여주면 된다. 생성자가 하나뿐이라면 Autowired 어노테이션을 생략해도 자동으로 주입된다.

2.수정자 주입

자바는 필드의 직접적인 접근보다 getXXX, setXXX, getter/setter 메서드를 통해서 필드의 접근 및 수정을하는 규칙을 만들었다. 이것을 자바빈 프로퍼티 규약이라고 하는데, 여기서 말하는 setter 메서드에 @Autowired 어노테이션을 붙여서 의존 주입을 설정한다.

3.필드 주입

필드 주입은 필드명에 @Autowired 어노테이션을 붙여, 해당 필드가 의존성 주입이 필요한 필드임을 명시하는 것이다.

코드가 매우 깔끔해지지만, 의존 주입을 해주는 프레임워크가 없으면 해당 의존성 주입을 사용할 수가 없다는 것이다.

예를 들어, 간단한 유닛테스트를 실행하고 싶어도, 의존 주입 프레임워크가 없다면 테스트가 힘들다. 가급적 사용 안하는 편이 좋다.

4.일반 메서드 주입

일반 메서드를 통해서 주입을 하는 방법이다. 한번에 여러 필드를 주입받을 수 있지만 일반적으로 사용하지 않는다.

4가지 방법중 보통은 1번 생성자 주입을 Lombok 라이브러리와 함께 사용하는 것이 일반적이다.

@RequiredArgsConstructor public class OrderServiceImpl implemenst OrderSerivce { private final MemberRepository memberRepository; private final DiscountPolicy discountPolicy; }

@RequiredArgsConstructor 어노테이션은 Lombok 라이브러리를 설치하면 사용할 수 있다. Lombok 라이브러리는 반복적인 코드들을 단순한 어노테이션 설정으로 대체할 수 있도록 해준다. 예를 들어, @Getter , @Setter 를 통해서 getter/setter 코드 생성을 대신 할 수 있다.

위 코드에서 사용한 @RequiredArgsConstructor 는 해당 객체에 final 과 함께 선언된 필드들의 생성자 코드를 생성해준다. 다시 말해

//@RequiredArgsConstructor public class OrderServiceImpl implemenst OrderSerivce { ... final 필드 선언 ... @Autowired public OrderServiceImpl(MemberRepository memberRepositroy, DiscountPolicy discountPolicy) { this.memberRepository = memberRepository; this.discountPolicy = discountPolicy; } ... }

위 코드의 자동 생성을 어노테이션 한가지로 가능하다. 위 방식을 주로 권장하는데, 그 이유는 의존 관계 주입은 한번 일어나면 종료 전까지 변하면 안되기 떄문이다. 또한 해당 빈이 없으면 스프링에서 null point exception을 발생 시킨다. 기본적으로 required 옵션이 true로 설정되어 있기 때문이다. 그리고 생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있기 때문에, 값이 설정되지 않는다면 컴파일 시점에 오류를 내보내기 때문에 불상사가 일어나기 전에 미리 막을 수 있다.

만약 @Autowired 의 해당 옵션을 false로 지정해주면, 해당 빈이 없으면 호출 자체가 되지 않는다. 반면, @Nullable이 설정되어 있거나 Optional<>이 되어 있으면 null 또는 Optional.emtpy가 주입된다.

의존 주입 중복 빈 문제

DI 컨테이너를 통해 의존 주입이 자동으로 이루어지는데, 빈 이름이 중복되면 어떤 빈을 주입해야 하는가.

@Autowired 는 빈의 타입으로 의존성 주입을 한다. 이 때 주입해야 될 타입의 하위 타입이 2개 이상일 경우, 스프링은 NoUniqueBeanDefinitionException 을 발생시킨다. 구체적으로 하위 타입을 지정할 수 있지만, 이는 DIP를 위배하기 때문에 지양해야 한다.

중복 빈 문제를 해결하기 위한 방법은 다음과 같다.

@Autowired 는 우선 타입명이 일치하면, 다음 조건으로 해당 필드의 이름, 파라미터 이름으로 빈이 매칭하는지 검사를 한다. @Qualifier 어노테이션으로 추가 구분자를 지정한다.(빈 이름을 지정하 것이 아니라 추가 정보를 일치시켜서 매칭한다.) // 생성자 메서드에서 Qualifier 사용 public A a(@Qualifier(aaa) T t) { .... } // 빈 설정에서 Qualifier 등록 @Bean @Qualifier("aaa") public T t() { return new ... } @Primary 어노테이션으로 빈 등록의 우선순위를 매겨 의존 주입을 한다. (@Qualifier 설정이 우선순위가 높다)

메인이 되는 스프링빈은 @Primary로, 서브가 되는 빈은 @Qualifier로 지정하면 코드를 깔끔하게 유지할 수 있다.

조회한 빈이 모두 필요할 때

조회한 빈을 모두 주입받아서 선택적으로 사용해야 할 경우, Map 또는 List< T> 타입으로 조회한 모든 스프링 빈을 담아준다.

(스프링 컨테이너를 생성할 때, 빈으로 생성하고 싶은 클래스 정보를 넘기면 자동으로 빈으로 등록된다 - 수동등록)

Map으로 등록된 모든 빈들의 경우, 해당 빈의 이름이 key로 설정되어 있기 때문에 해당 빈의 이름으로 조회해서 선택적으로 사용할 수 있다.

자동 주입, 수동 주입

자동 주입 의 경우, 스프링 컨테이너에서 자동으로 등록 및 관리를 해주기 때문에 실제 코드로 한 눈에 알아보기 쉽지가 않다.

반면 수동 주입 의 경우, 설정 정보 파일에 명확하게 빈을 명시하기 때문에 설정 정보 파일을 보고 한 눈에 파악할 수 있다.

일반적으로 유사한 디자인 패턴을 적용하고, 발생해도 문제가 있는 부분을 쉽게 특정 지을 수 있는 부분에 대해서는 자동주입이 유리하다. 실제로 점점 컴포넌트 스캔을 활용한 자동 주입을 선호하는 추세이며, 스프링 부트는 컴포넌트 스캔을 기본으로 사용해서 조건에 맞는 빈을 자동으로 주입한다. 이는 보통 업무 로직 빈에 해당한다.

수동 주입의 경우는 설정 정보를 수동으로 등록하기 때문에, 해당 구성 정보에 대한 성격을 명확하게 드러내고 있다. 그렇기 때문에 해당 빈을 사용하는 로직이 정확하게 드러나지 않는 부분에 대해서는 수동 주입을 통해 명확하게 들어내는 것이 좋다. 이는 보통 기술 지원 빈 에 해당한다.

업무 로직 빈: 컨트롤러, 비지니스 로직, 데이터 계층 등 업무의 특정 부분을 담당하기 때문에 오류 발생 시 문제 있는 부분에 대한 특정을 하기 쉽다.

기술 지원 빈: 기술적인 문제나 AOP를 처리할 떄 주로 사용된다. 데이터베이스 연결 또는 공통 로그 처리 같이 광범위하게 적용되어 오류가 발생할 때 특정하기 어렵다. (물론, 스프링에서 기술 지원하는 것들은 스프링의 의도를 파악하고 자동으로 사용하는 편이 좋다.)

조회한 빈을 Map에 담아서 사용하는, 다형성을 활용하는 경우 수동으로 빈을 등록하거나, 특정 패키지에 같이 묶어두어 다른 개발자가 보더라도 알기 쉽게 구성해야 한다.

빈 생명 주기 및 콜백

객체 지향 설계를 하다보면, 해당 객체의 라이프사이클을 이해하는 것은 매우 중요하다고 생각한다.

스프링 빈의 라이프사이클

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

객체의 생성과 초기화는 분리하는 것이 좋다.

생성자는 메모리를 할당해서 객체를 생성하는 책임을 가지고 있다. 반면 초기화는 외부 커넥션을 연결하는 등의 무거운 작업을 수행하기 때문에 생성 과 초기화는 분리하는 것이 유지보수 관점에서 좋다.

스프링 빈 생명주기 콜백 지원 방법 3가지

인터페이스(InitializingBean, DisposableBean) 인터페이스 InitializingBean 과 DisposableBean 을 상속해서 필수 메서드를 오버라이딩을 하면, afterPropertiesSet()와 destroy() 함수를 통해서 초기화 콜백과 소멸전 콜백을 받아 원하는 로직을 실행할 수 있다. 설정 정보의 초기화 및 종료 메서드 지정 @Bean 을 설정할 때 옵션으로 initMethod = "함수명", destroyMethod = "함수명" 을 통해서 초기화 및 종료 메서드를 지정할 수 있다. 해당 함수는 빈(Bean) 클래스에 작성하면 된다. @PostConstruct , @PreDestroy 어노테이션 활용 위 어노테이션을 통해 빈(Bean) 클래스에 콜백 함수를 작성하고, 붙여주기만 하면 된다. 위 어노테이션은 스프링에 종속적인 기술이 아니라 자바 표준에서 지원하는 기능이기 때문에 다른 컨테이너에서도 동작한다.

위 3가지 방법 중 1번은 해당 인터페이스에 의존적이기도 하고, 외부 라이브러리에 적용하기 힘든 단점 때문에 사용하지 않는다.

보통 3번 방법이 간편하고 어노테이션을 활용하기 때문에 컴포넌트 스캔과 잘 어울린다. 그래서 대부분의 경우 3번을 권장하지만, 코드를 고칠 수 없는 외부 라이브러리를 초기화 및 종료해야 하면 2번의 방법도 같이 사용하는 것이 좋다.

빈 스코프

지금까지 빈은 싱글톤으로 관리되어 스프링 컨테이너의 시작과 종료까지 유지되는 넓은 범위 동안 라이프사이클이 유지되는 빈을 설명했지만, 특정 범위 내에서 잠깐 사용하고 종료되어지는 빈들도 있다.

프로토 타입: 스프링 컨테이너가 빈의 생성과 의존관계 주입까지만 관리하는 짧은 주기의 빈

웹 관련: 웹에서 사용되는 request, session, application 관련 된 빈으로써, 각 이름에서 유추할 수 있다시피 해당 생명주기와 동일하게 유지된다.

프로토타입 빈

프로토타입 빈은 스프링 컨테이너에 빈을 요청하면, 해당 시점에 컨테이너가 빈을 생성하고 의존관계를 주입한 후, 클라이언트에 반환한다. 컨테이너에서 관리하지 않기 때문에 해당 빈은 요청 시마다 새로 생성해서 반환한다. 반환 하고 나서는, 컨테이너가 관리하지 않기 때문에 소멸전 콜백 메서드 호출은 되지 않는다.

프로토타입을 선언하기 위해서는 해당 빈에 @Scope('prototype) ' 어노테이션을 설정하면 된다.

싱글톤 빈이 프로토타입 빈을 주입받는 경우 주의해야 할 점이 있다.

우선 싱글톤 빈이 생성되고 의존 주입을 받을 때, 프로토타입 빈이 생성되어 의존 주입이 이뤄진다.

이후 클라이언트가 싱글톤 빈의 내부 로직을 호출해서 프로토타입 빈을 사용할 경우, 요청마다 동일한 프로토타입 빈이 유지되어 사용된다. 왜냐하면, 프로토타입 빈이 생성되는 시점은 컨테이너에 요청을 할 때 생성되고 컨테이너에서 관리가 끝난다. 반면 싱글톤 빈에서는 주입 받은 프로토타입 빈을 자신의 생명주기가 마칠때까지 계속 사용한다. 그래서 프로토타입의 내부 변수를 사용할 때 공유가 되므로 사용에 주의해야 한다.

(그러나 여러 빈에서 같은 프로토타입 빈을 요청할 경우, 컨테이너는 각각 프로토타입을 생성해서 보내준다)

빈 Provider 사용

위에서 생긴 문제점은 싱글톤 빈 내부에서 의존 주입을 받을 때만 생성되어 넘겨받는 것이 원인이다. 개발자의 의도가 클라이언트의 요청마다 새로운 객체를 생성되어 빈을 주입받아 사용받길 원한다면, 클라이언트 요청마다 컨테이너에 프로토타입 빈 생성 요청 및 주입을 받는 것이다.

Dependency Lookup(DL), 의존관계 조회(탐색)

Dependency Injection(DI)는 컨텍스트 전체를 주입 받아서 생성되는 반면, Dependency Lookup(DL)은 직접 필요한 의존관계를 찾는 것을 말한다. DI보다 가볍다고 보면 된다.

ObjectFactory, ObjectProvider

DL 서비스를 제공하는 것이 ObjectProvider 이다. 과거에는 ObjectFactory 를 사용했지만, 현재는 ObjectProvider 로 여러 편의기능을 더해져서 사용된다.

프로토타입 빈을 필요로 한 클래스에 ObjectProvider 을 주입 받으면 된다.

@Autowired private ObjectProvider provider; public void logic() { PrototypeBean prototypeBean = provider.getObject(); prototypeBean.method(); }

위 코드처럼, 해당 타입을 지정한 Provider를 주입 받고, 필요할 때 해당 Provider에서 getObject() 메서드를 통해 받아서 쓰면 된다.

ObjectProvider는 정확하게 DL의 기능만 수행하며, 별도의 라이브러리 없이 스프링에만 의존한다.

JSR-330 Provider

같은 Provider 이지만 javax.inject.Provider 라는 자바 표준을 사용하는 방법이다. 이 방법을 사용하기 위해서는 gradle 또는 maven에 라이브러리를 추가해야 한다.

@Autowired private Provider provider; public void logic() { PrototypeBean prototypeBean = provider.get(); prototypeBean.method(); }

비슷하지만, 조금 심플해진 것을 알 수 있다. 이 Provider는 자바 표준이기 때문에 스프링 이외 다른 컨테이너에서도 사용이 가능하다.

(별도의 다른 컨테이너를 사용하지 않는다면 ObjectProvider를 사용하는 것이 편하다)

웹 스코프

웹 환경에서만 동작하는 빈 스코프. 프로토타입과는 다르게 해당 빈의 스코프의 종료시점까지 관리하기 때문에 종료 콜백 메서드를 호출한다.

Request: HTTP 요청이 들어오고 응답이 나갈 때까지 유지된다. 각 HTTP 요청마다 별도의 빈 인스턴스가 생성된다.

Session: HTTP Session과 동일한 생명주기를 가지는 스코프

application: 서블릿 컨텍스트와 동일한 생명주기를 가지는 스코프

websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프

웹 스코프 또한 똑같이 @Scope(value = "request") 같이 스코프에 지정을 해주면 된다.

그러나 해당 스코프를 지정해도, 스프링 컨테이너가 빈을 생성 및 주입하는 단계에서는 해당 스코프가 적용이 되지 않기 때문에 오류를 내보낸다. 그래서 해당 스코프에 해당 되는 시점에 빈이 생성되서 사용되야 한다. 이를 위해서 앞서 배웠던 Provider 를 통해서, 해당 스코프 시작 시점에 해당 빈을 요청해서 받으면 된다.

프록시 방식의 스코프

프록시는 대리, 대신 이라는 뜻을 가진다. 프록시를 사용하면, 가짜 객체를 세워놓고 클라이언트는 마치 있는 듯이 동일한 로직 실행하면, 실행 요청 시점에 진짜 빈을 요청하는 위임 로직을 실행시킨다.

(흔히 나루토의 그림자 분신술 같다고 보면 된다. 본체는 아니지만 할수 있는건 똑같다.)

프록시의 동작은 이전 글에서 본것과 같이 CGLIB 이라는 바이트조작 코드를 통해서 위임 로직을 생성한다.

프록시 방식의 사용법은 아래와 같이 스코프에 설정 옵션을 넣는다.

@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) // 클래스인 경우 @Scope(value = "request", proxyMode = ScopedProxyMode.INTERFACES) // 인터페이스인 경우

프록시 방식의 가장 큰 특징은 클라이언트 측에서는 프록시 객체인지, 원본 객체인지 모르지만 동일하게 사용할 수 있다는 점이다.

from http://devcabinet.tistory.com/39 by ccl(A) rewrite - 2021-12-05 18:27:17