해당 글은 인프런의 김영한님의 강의인 '스프링 핵심 원리 - 기본편'을 공부하며 작성한 글입니다.

컴포넌트 스캔

이제 스프링 컨테이너의 컴포넌트 스캔을 활용해보자.

새로운 설정 정보를 나타낼 클래스를 프로젝트 상단의 디렉토리에 생성해서 @ComponentScan을 붙이면 스프링이 컴포넌트를 찾아서 빈으로 등록한다.

package hello.core;

...

@Configuration
@ComponentScan(
	excludeFilters = ... // 컴포넌트 스캔에 제외할 목록
)
public class AutoAppConfig {
...

그리고 빈으로 등록할 클래스마다 상단에 @Component을 붙이면 해당 클래스를 스프링 빈으로 등록한다.

@Component
public class MemberServiceImpl implements MemberService {
...

그렇다면 이제 의존관계 주입은 어떻게 할까?
기존에는 AppConfig 클래스에서 수동으로 직접 지정해줬지만, 이제는 스프링 컨테이너가 자동으로 주입하도록 해야 한다.

의존관계 자동 주입

의존관계 자동 주입 4가지 방법

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

생성자 주입 👍

  • 생성자를 통하여 의존 관계를 주입받는 방법
  • 특징
    • 생성자 호출 시점에 딱 1번만 호출되는것이 보장됨.
    • 불변, 필수 의존관계에 사용.
    • 더 이상 수정할 수 없게 하여 버그가 줄어들 수 있도록 한다.
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;

@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
	this.memberRepository = memberRepository;
	this.discountPolicy = discountPolicy;
}
  • 생성자가 한개일때는 @Autowired가 자동 주입된다.

수정자 주입

  • setter 메서드를 사용하여 의존관계 주입
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
	this.memberRepository = memberRepository;
}

필드 주입

  • 필드에 바로 주입
  • 코드가 간결함
  • 권장하지 않는 방법
    • 외부에서 의존관계 변경이 불가능하여 순수한 자바코드로 테스트하는데 제약이 있음.
    • 해당 이슈를 해결하기 위해서 결국에는 수정자 주입을 하게됨.
    • 즉 DI 프레임워크가 없으면 아무것도 할 수 없음
@Autowired private MemberRepository memberRepository;
@Autowired private DiscountPolicy discountPolicy;

일반 메서드 주입

  • 아무 메서드에다가 @Autowired 를 붙여서 의존관계를 주입시킨다
  • 한번에 여러 필드를 주입 받을 수 있지만 일반적으로 잘 사용하지 않음
@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
	this.memberRepository = memberRepository;
	this.discountPolicy = discountPolicy;
}

어떤 의존관계를 사용할 것인가

  • 대부분의 의존관계 주입은 한번 일어나면 어플리케이션 종료 전까지 의존관계를 변경할 일이 없다.
    오히려 대부분의 의존관계는 종료 전까지 불변해야 하는 경우가 대부분이다.
  • 수정자 주입을 사용하면 setter 메서드를 public 으로 열어두어야하기 때문에 불변성이 보장되지 않는다.
  • 생성자 주입은 객체를 생성할 때 딱 한번 호출되므로 불변성이 보장된다.
  • 생성자 주입 만이 필드 상에서 final 키워드를 사용할 수 있으므로, 혹시라도 생성자에서 값이 설정되지 않는 오류를 컴파일 시점에서 막아줄 수 있다.

생성자 주입을 선택하자!

Lombok

@RequiredArgsConstructor 를 사용하여 필수값(final)이 붙은 객체를 생성자로 만들어 준다.
즉, 코드가 아래와 같이 더 간결해진다.

@RequiredArgsConstructor
public class ... {

private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;

public void customMethod() {
...
...

최근에는 생성자를 1개 두면 @Autowired 가 자동으로 붙기 때문에 생성자를 하나만 두고 lombok을 사용하는 방법이 주를 이루고 있다.


조회 Bean이 2개 이상

조회 대상 bean이 2개 이상일때 해결 방법

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

@Autowired

타입이 똑같은 빈을 가져오지만 필드명을 다르게 설정하면, 해당 필드명으로 빈 이름을 매칭한다

@Qualifier

추가 구분자를 붙여주는 방법.

@Qualifier("구분명")
public class BeanName {
...
@Autowired
public ServiceImpl(@Qualifier("구분명") BeanName beanName) {
...

@Primary

우선 순위를 지정해 준다.

  • Comment : 만약 자동으로 의존관계를 주입시키는 상황에서 할인 방식을 정률 할인 방식에서 정액 할인 방식으로 변경하고자 한다면 아래와 같이 사용할 수 있을 것 같다.
public class 결제서비스 {
	private final DiscountPolicy discountPolicy;
    
    public int getPrice() {
	...
@Component
public class RateDiscountPolicy implements DiscountPolicy {
...

@Primary
@Component
public class FixDiscountPolicy implements DiscountPolicy {
...

조회한 빈이 모두 필요할 때

의도적으로 해당 타입의 모든 스프링 빈이 다 필요한 경우에는?
예를 들어서 할인 서비스를 제공하는데 클라이언트가 할인의 종류를 선택할 수 있을 경우.

Map 혹은 List 를 통하여 동일한 타입의 빈을 모두 의존성을 주입받을 수 있다.

자동, 수동의 올바른 실무 운영 기준

편리한 자동 기능을 기본으로 사용

  • 수동으로 스프링 빈을 일일히 등록해야 하는 일이 상당히 번거로워지며, 관리할 빈이 많아져서 설정정보가 커지면 설정정보를 관리하는것 자체가 부담이 됨.
  • 자동 빈 등록 기능을 사용해도 OCP, DIP 를 지킬 수 있다.

OCP (Open Close Principle) : 개방 폐쇄의 원칙

  • 확장에는 열려있고, 변경에는 닫혀 있다.
  • 기능을 추가하거나 변경하면서 그것을 사용하는 코드는 수정하지 않는다.

수동 빈 등록은 언제 사용하면 좋을까?

어플리케이션은 크게 업무 로직기술 지원 로직으로 나눌 수 있다.

  • 업무 로직 빈 : 웹을 지원하는 Controller, 핵심 비즈니스 로직이 있는 Service, 데이터 계층의 로직을 처리하는 Repository 등... 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.
  • 기술 지원 빈 : 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용. 데이터베이스 연결이나, 공통 로그 처리 처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술.
  • 업무 로직은 숫자도 매우 많고 한번 개발하면 controller, service, repository처럼 유사한 패턴이 있다. 이런 경우 자동 기능을 적극 사용하는 것이 좋다. 보통 문제가 발생해도 어떤 곳에서 문제가 발생했는지 명확하게 파악하기가 쉽다.
  • 기술 지원 로직은 업무 로직과 비교해서 숫자도 적고 보통 어플리케이션 전반에 걸쳐서 광범위하게 영향을 미친다. 그리고 업무로직은 문제가 발생했을 때, 어디가 문제인지 명확하게 잘 들어나지만, 기술 지원 로직은 적용이 잘되고 있는지 아닌지 조차 파악하기가 어려운 경우가 많다. 그래서 이런 기술 지원 로직들은 가급적 수동 빈을 사용해서 명확하게 들어내는 것이 좋다.

어플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 딱 설정 정보에 명확하게 나타내는 것이 유지보수 관점에서 이점이 있다.

비즈니스 로직 중에서 다형성을 적극 활용할 때, 여기에 어떤 빈이 주입될지, 각 빈들의 이름은 무엇일지 자동 등록을 사용한다면 한눈에 보고 파악하기가 어렵다. 이런 경우 수동 빈으로 등록하거나 자동으로 하면 특정 패키지에 같이 묶어두는것이 좋다.

@Configuration
public class DiscountPolicyConfig {

	@Bean
	public DiscountPolicy rateDiscountPolicy() {
		return new RateDiscountPolicy();
	}
	
	@Bean
	public DiscountPolicy fixDiscountPolicy() {
		return new fixDiscountPolicy();
	}
}

어노테이션 차이

  • @Controller : 스프링 MVC 컨트롤러로 인식.
  • @Service : 특별한 처리는 하지 않으나, 보통 핵심 비즈니스 로직이 여기에 있음을 예상할 수 있다.
  • @Repository : 스프링 데이터 접근 계층으로 인식하고 데이터 계층의 예외를 스프링 예외로 변환.
  • @Configuration : 스프링 설정 정보에서 사용.

Bean 생명주기 콜백

DBMS와 미리 연결을 해놓을 때 네트워크 소켓 처럼 어플리케이션 시작 지점에 필요한 연결을 미리 해두고, 어플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면 객체의 초기화와 종료 작업이 필요하다.

스프링 빈은 객체를 생성하고 의존관계 주입이 다 끝난 다음에 데이터를 사용할 수 있는 준비가 완료된다.
따라서 초기화 작업은 의존관계 주입이 모두 완료되고 난 다음에 호출해야 한다.

그럼 의존관계 주입이 모두 완료된 시점을 어떻게 알 수 있나?
스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 기능을 제공한다. 또한 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다.

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

객체의 생성과 초기화를 분리하자

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

따라서 생성자 안에서 무거운 초기화 작업을 함께 하는것 보다는 객체를 생성하는 부분과 초기화 하는 부분을 명확하게 나누는 것이 유지보수 관점에서 좋다.

물론 초기화 작업이 내부 값들만 약간 변경하는 정도로 단순한 경우에는 생성자에서 한번에 다 처리하는 것이 나을 수 있다.

빈 생명주기 콜백 방법

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

인터페이스

초기화, 소멸 인터페이스 단점

  • 스프링 전용 인터페이스, 해당 코드가 스프링에 의존한다.
  • 초기화, 소멸 메서드의 이름 변경 불가.
  • 외부 라이브러리에 적용할 수 없다.

인터페이스 방법은 요즘은 거의 사용하지 않는다.

설정 정보 사용

  • 메서드 이름을 자유롭게 사용 가능
  • 스프링 빈이 스프링 코드에 의존하지 않음.
  • 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화 종료 메서드 적용 가능.

종료 메서드 추론

  • @BeandestroyMethod 는 default 값이 "(inferred)" (추론)으로 등록되어 있다.
  • 라이브러리는 대부분 close, shutdown 이라는 이름의 종료 메서드를 사용한다.
  • 이 추론 기능은 close, shutdown 라는 이름의 메서드를 자동으로 호출해준다.
  • 따라서 스프링 빈으로 등록하면 종료 메서드는 따로 적어주지 않아도 잘 동작한다.
  • 추론 기능을 사용하지 않을 경우 destroyMethod="" 으로 지정하면 된다.

어노테이션

  • @PostConstruct, @PreDestroy 어노테이션 하나만 붙이면 됨.
  • 자바 표준에서 지원하는 기능이므로 스프링에 종속적이지 않음.
  • 외부 라이브러리에는 적용하지 못하기 때문에, 외부 라이브러리를 초기화 종료해야 한다면 @Bean의 기능을 사용하면 된다.

최신 스프링에서 해당 방법 권장.


빈 스코프

빈 스코프란?
스프링 빈이 존재할 수 있는 범위를 뜻한다.

기본적으로 스프링 빈은 스프링 컨테이너의 시작과 함께 생성되어서 스프링 컨테이너가 종료될 때 까지 유지된다. 이것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다.

  • 싱글톤
    • 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
  • 프로토타입
    • 스프링 컨테이너가 빈의 생성과 의존관계 주입까지만 관여하고, 더는 관리하지 않는 매우 짧은 범위의 스코프
    • 클라이언트가 빈을 요청하면 스프링 컨테이너가 생성만 하고 클라이언트에게 던져주고 끝난다. 이후에는 클라이언트가 해당 빈을 관리할 책임이 생긴다. 그러므로 @PreDestroy 같은 종료 메서드가 호출되지 않는다.
  • 웹 관련 스코프
    • request : 웹 요청이 들어오고 나갈 때 까지 유지
    • session : 웹 세션이 생성되고 종료될 때 까지 유지
    • application : 웹의 서블릿 컨텍스트와 같은 범위로 유지

Comment : 어디에서 사용될 수 있을까 ...?

프로토타입 스코프와 싱글톤 빈을 함께 사용했을 시 문제점

프로토타입 스코프를 사용하려고 하면 아마 문제가 생길 가능성이 높다. 어떤 문제일까?

일반적으로 스프링 빈은 싱글톤이다. 이 싱글톤 빈에서 프로토타입 스코프 빈을 주입하여 사용하려고 하는 상황이라면
싱글톤 빈은 생성 시점에 생성되고 의존관계 주입도 이때 발생한다. 이때 프로토타입 빈을 요청하게 되고 스프링 컨테이너는 프로토타입 빈을 생성해서 싱글톤 빈에 반환한다.
그러면 싱글톤 빈은 내부에 프로토타입 빈의 참조값을 가지고있게 된다.

...? 프로토타입은 클라이언트가 빈을 요청하면 생성해야하는데 어플리케이션을 띄우니까 생성되어서 유지되어버린다.
이 문제를 어떻게 해결할까?

가장 간단한 방법은 싱글톤 빈 내부 로직 실행 시에 프로토타입 빈을 생성하는것이다.

  • 이것은 좋은 방법이 아니다.
  • 이것은 DI가 아니라 의존관계를 조회(DL : Dependency Lookup)하는것이다.
  • 스프링 어플리케이션 컨텍스트 전체를 주입받아서 사용하게 되면 스프링 컨테이너에 종속적이게 되며, 단위 테스트도 어려워진다.

ObjectProvider

지정한 빈을 스프링 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는것이 ObjectProvider.
이전에 있었던 ObjectFactory를 상속받으며 편의기능을 추가한 메서드.

  • 기능이 단순하며 스프링에 의존.
  • 라이브러리를 추가할 필요가 없음.

JSR-330 Provider

  • implementation 'javax.inject:javax.inject:1' 라이브러리를 추가하여 사용.
  • 스프링에 의존적이지 않음.
  • ObjectProvider와 동일한 단순한 기능을 제공.

프로토타입 빈은 매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사용한다.
하지만 실무에서 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 직접적으로 사용하는 일은 매우 드물다.

그렇다면 실무에서 JSR-330 Provider를 사용할 것인지 스프링이 제공하는 ObjectProvider를 사용할 것인가.
코드를 스프링이 아닌 다른 컨테이너에서 사용할 수 있어야하는것이 아니라면 스프링이 제공하는 ObjectProvider를 사용하면 된다.

웹 스코프

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

웹 스코프 종류

  • request : 웹(HTTP) 요청이 들어오고 나갈 때 까지 유지. 각 HTTP 요청마다 별도의 빈 인스턴스가 생성된다.
  • session : 웹 세션이 생성되고 종료될 때 까지 유지
  • application : 웹의 ServletContext와 같은 범위로 유지
  • websocket : 웹 소켓과 동일한 생명주기

스코프와 프록시

ObjectProvider를 일일히 써야하고, 대상 Object를 또 찾아야 하는 번거로움을 줄이기 위하여 프록시 모드를 사용할 수 있다.

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

@Scope 에서 proxyMode를 사용하면 가짜 프록시 클래스를 만들어 두고, HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있다.

  • 적용 대상이 클래스이면 TARGET_CLASS
  • 적용 대상이 인터페이스라면 INTERFACES

CGLIB 라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입.
이 가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 로직이 들어있다.

프록시(Proxy)란? 대리(행위)나 대리권, 대리 투표, 대리인 등을 뜻한다

주의점

싱글톤 처럼 보이기 때문에 주의해서 사용해야 하며, 이러한 싱글톤이 아닌 스코프를 가지는 빈은 최소화 하여 사용하는것이 좋다. 무분별하게 사용하면 유지보수하기가 어려워 진다.

profile
NO EFFORT, NO RESULTS

0개의 댓글

Powered by GraphCDN, the GraphQL CDN