[Spring] 스프링 컨테이너

kai6666·2022년 6월 25일
0

TIL. Spring

목록 보기
3/11


1️⃣ Spring 컨테이너와 빈

Spring 컨테이너는 IoC 컨테이너, 빈 컨테이너 등 여러 가지 말로 불린다. 핵심은 빈의 생성, 관리, 제거 등 생명 주기 전반을 관리해준다는 점이다.

빈은 인스턴스화된 객체이자 재사용 가능한 소프트웨어 컴포넌트이다. 이 경우 컴포넌트는 앱내 독립적인 실행 단위이며, 클래스를 지칭한다. 복잡하게 생각할 필요없이 빈이라는 객체가 있고, 그것을 개발자가 일일이 관리하는 게 아니라 컨테이너라는 녀석이 중간에서 관리해준다고 이해하면 된다.

컨테이너를 쓰는 이유는 인터페이스에 의존함으로써 객체 간 의존성을 낮추기 위함이다. 객체 간 참조가 난무하고 이로 인해 하나의 클래스가 두 가지 이상의 역할을 수행해야 하면, 그건 좋은 객체 지향 애플리케이션이 아니다.

(좋은 객체 지향 애플리케이션이란 클라이언트에 영향을 주지 않으면서 새로운 기능을 제공할 수 있는 애플리케이션이다. 클라이언트의 코드를 건드리지 않으려면 객체를 설계할 때 다형성을 적용해 역할과 구현을 분리해야 한다. 그래야 변경이 용이해 유연성과 확장가능성을 가진다. 역할과 구현을 분리하면 클라이언트는 내부 구조를 알 필요도 없고, 구현 대상이 바뀌어도 괜찮다.)

스프링 컨테이너의 종류

👉 BeanFactory

  • 스프링 컨테이너의 최상위 인터페이스
  • 빈 관리, 조회, getBean 등 스프링 컨테이너의 대부분 기능 제공
  • 직접 사용하는 일은 거의 없다고 한다

👉 ApplicationContext

  • BeanFactory의 기능을 모두 상속 받아 제공 (마찬가지로 인터페이스)
  • 부가 기능 제공 (메시지 소스 국제화 기능, 환경 변수, 애플리케이션 이벤트, 편리한 리소스 조회 등)

👉 컨테이너 인스턴스화

ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

보통 "스프링 컨테이너라 함은 위 두 가지를 지칭하는 말이다. 개발자는 AnnotationConfig를 설정 정보로 쓰는 ApplicationContext, 붙여서 AnnotationConfigApplicationContext 생성자에 AppConfig.class와 같은 설정 메타데이터를 넣어서 컨테이너를 인스턴스화해서 사용한다.

빈은 스프링 컨테이너가 관리하는 객체로, 컨테이너에 등록되면 스프링 빈이라고 칭한다. 컨테이너를 인스턴스화할 때 컨테이너가 주어진 설정 메타데이터를 전부 읽고, 그 속에 @Bean이 붙은 메서드 등 빈이라고 표시된 것들을 빈으로 등록한다. (설정 정보는 XML, 자바 애너테이션, 자바 코드 등 다양한 형식이 될 수 있다.)

BeanDefinition

빈 데피니션은 빈 설정 메타정보라는 의미로, 컨테이너가 빈을 어떻게 생성하고 관리할지 정해준다. 따라서 스프링 컨테이너가 설정 메타데이터를 읽고 빈을 임의로 등록하는 것이 아니라, BeanDefinition이라는 인터페이스를 토대로 빈을 생성한다.

이렇듯 스프링은 많은 역할을 추상화에 의존한다는 점을 알 수 있다. 추상화 덕분에 개발자는 객체를 일일이 관리할 필요가 없고, 추상화 덕분에 다양한 형식의 설정 정보를 받아들여 객체로 등록할 수 있다.

2️⃣ 자바 기반 컨테이너 설정 & 애너테이션

👉 @Configuration@Bean

자바 기반으로 컨테이너를 설정할 때 가장 중요한 키워드는 @Configuration@Bean이라는 애너테이션이다.

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl();
    }
}

설정 메타데이터 클래스에 @Configuration이라는 클래스 레벨의 애너테이션을 붙임으로써, 컨테이너에게 "이 클래스에 등록할 빈 있다" 즉 "여기 BeanDefinition의 소스있다"고 알려주는 것이다. 그리고 객체로 등록할 주요 기능에 메서드 레벨의 애너테이션인 @Bean을 붙여 "이 메서드를 빈으로 등록해줘"라고 알려주면 컨테이너가 데이터를 읽으며 빈을 생성한다.

@Configuration
public class AppConfig {

    @Bean
    public OrderService orderService(MemberRepository memberRepository) {
        return new OrderServiceImpl(memberRepository);
    }
}

@Bean 애너테이션이 추가된 메서드는 빈을 구축하는데 필요한 의존성을 나타내는데 매개 변수를 사용할 수 있다.

✨ @Configuration과 싱글톤

@Configuration 없이 @Bean만 붙여도 빈은 등록된다. 그렇지만 @Configuration을 썼을 때 함께 사용되는 @CGLIB라는 바이트코드 조작 라이브러리가 동작하지 않는다.
@CGLIB는 빈 등록할 클래스를 상속받아 임의의 클래스를 만들어 빈으로 등록해주는데, 이를 통해 스프링이 싱글톤을 보장해주기 때문에 중요한 기능이다. @Bean 애너테이션만 사용하면 new 생성자로 객체를 만드는 것이랑 똑같다. 따라서 객체가 스프링 빈으로서 컨테이너의 관리를 받게 하고 싶다면 두 애너테이션은 무조건 함께 사용해야 한다.

👉 @ComponentScan@Component

설정 정보 없이 자동으로 스프링 빈을 등록하는 @ComponentScan을 사용할 수도 있다.

@Configuration
@ComponentScan
public class AutoAppConfig {
    // AppConfig와 다르게 @Bean으로 등록한 클래스가 없다.
}

기능 위에 직접 @Bean 애너테이션을 넣는 대신, 설정 메타데이터에는 @Configuration 애너테이션과 함께 @ComponentScan이라는 애너테이션을 붙이면 된다. 그럼 @Component 애너테이션이 붙은 클래스들이 자동으로 스프링 빈으로 등록된다.

@Component
public class MemberServiceImpl implements MemberService{

    private final MemberRepository memberRepository;

    @Autowired
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

이때 의존관계는 자동으로 의존 관계를 주입해주는 @Autowired 애너테이션을 통해 만드면 된다. 의존관계를 주입하고자 하는 클래스의 생성자에 붙이면 된다.

한 가지 주의할 점은, @ComponentScan 애너테이션이 스캔하는 대상은 Controller, Service, Repository, Configuration, Component로 매우 광범위하다는 점이다. 때문에 @Configuration이 붙은 모든 클래스를 자동으로 등록해버리기 때문에 이를 막고자 한다면 아래와 같은 필터를 @ComponentScan 애너테이션 사용시 덧붙여줘야 한다.

@Configuration
@ComponentScan(
        excludeFilters =  @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Configuration.class))

3️⃣ Spring DI

스프링에서 의존관계를 주입하는 방법은 4가지가 있다.

  • 생성자 주입
@Component
public class MemberServiceImpl implements MemberService{

    private final MemberRepository memberRepository;

    @Autowired
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

위에서 살펴본 @Component@Autowired 애너테이션을 사용한 방법이다. 이 방법은 불변과 필수 의존 관계에 사용되고, NullPointerException을 방지할 수 있다. 장점은 주입 받을 필드를 final로 선언할 수 있다는 점이고, 생성자가 1개만 있는 경우 @Autowired 애너테이션을 생략할 수 있다.

  • 수정자 주입 (setter)
@Component
public class MemberServiceImpl implements MemberService{

    private MemberRepository memberRepository;

    @Autowired
    public void setMemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

생성자 대신 set 메서드를 생성해서 의존관계를 주입하는 방식이다. 이 경우 @Autowired 애너테이션은 생략이 불가능하다.

  • 필드 주입
@Component
public class MemberServiceImpl implements MemberService{

	@Autowired
    private MemberRepository memberRepository;
    }

필드에 @Autowired를 붙여 바로 주입하는 방식이라 코드가 간결하다. 그치만 외부에서 변경이 불가능해 테스트가 어렵고, 정상적으로 작동하게 하려면 결국 setter가 필요해 수정자 주입을 쓰는 게 낫다. (실제 코드와 무관한 특정 테스트를 하고 싶을 때는 사용할 수 있다.)

  • 일반 메서드 주입
    한 번에 여러 필드를 주입 받을 수 있는 방식인데, 일반적으로 사용되지 않는다.

👉 생성자 주입 장점

과거에는 이 방식들이 두루 쓰였으나, 현재로서 가장 권장되는 스프링 DI 방법은 생성자 주입이다.

  • 의존성 주입이 필요한 필드를 final로 선언 가능
  • 순환 참조 방지 가능
  • 테스트 코드 작성 용이
  • 의존관계 설정을 하지 않으면 객체 생성 불가 == 컴파일 타임에 인지 및 수정 가능, 널포인터익셉션 방지 가능
profile
성장 아카이브

0개의 댓글