[Spring] IoC, 컴포넌트 스캔, 의존관계 주입

Ioc (Inversion of Control)

 

 간단하게 말하면, 사용자의 new 선언을 방지하고, 모든 레퍼런스 변수, 관계설정, 제거, 사용까지 오브젝트(스프링 빈) 전반에 걸친 모든 과정을 프레임워크의 컨테이너(스프링 컨테이너)에게 위임하는 것. 이를 통해서 객체는 프레임워크가 관리하고, 사용자가 구현부를 관리함으로서 응집도를 높이고 결합도를 낮추며, 싱글톤 패턴을 유지할 수도 있다.

 

스프링은 패키지에서 스프링 빈으로 등록된 모든 메소드의 리턴값을 스캔(컴포넌트 스캔)하여 객체들을 관리하고, 의존관계를 주입해준다. 즉 스프링에서 말하는 DI는 객체 합성+객체를 관리할 컨테이너 패턴+IoC를 사용하는 스프링 프레임워크의 특징이라고 할 수 있다.

 

스프링에서 사용하는 IoC를 DI라고 하며, DI 말고도 IoC에는 여러 종류가 있다 (IoC의 일부가 DI이다.)

 

 

 

컴포넌트 스캔

 

원래는 설정 정보를 @Bean 어노테이션을 통해서 일일히 등록해야 했다. 그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 기능을 제공하는데, 이 기능을 컴포넌트 스캔이라고 한다.

컴포넌트 스캔을 사용하기 위해서는 AppConfig에서 일일히 @Bean으로 등록하는 대신 @ComponentScan 어노테이션을 통해서 설정정보를 한번에 등록할 수 있다.

@Configuration
@ComponentScan
public class AutoAppConfig {
}

 

컴포넌트 스캔은 이름 그대로 @Component 어노테이션이 붙은 클래스들을 스캔해서 스프링 빈으로 자동 등록해준다.

(@Configuration/@Controller/@Service/@Repository의 경우, 해당 어노테이션 내부에 @Component 애노테이션이 붙어있기 때문에 별도로 @Component 어노테이션을 선언해주지 않아도 빈으로 등록된다.)

간단하게 스프링 빈으로 등록하고 싶은 구현체 클래스들에 @Component 애노테이션을 붙여주자.

@Controller: 스프링 MVC 컨트롤러로 인식
@Repository : 스프링 데이터 접근 계층으로 인식. CRUD기능별 관리
@Configuration : 스프링 설정정보로 인식
@Service : 핵심 비지니스 계층 명시. 여러 DB접근기능 모듈화, 트랜잭션 단위 관리

 

 

의존관계 주입 (DI, Dependency Injection)

 

 그런데 기존에는 AppConfig.class에 구현체들의 의존관계를 일일히 설정정보 코드로 설정해주었는데, @ComponentScan을 사용하게 되면 @Component로 등록한 클래스들의 생성자에 @Autowired 애노테이션을 설정해주면 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 의존관계를 주입해준다.

@Autowired //ac.getBean(MemberRepository.class)와 비슷하게 동작한다.
public MemberServiceImpl(MemberRepository memberRepository) {
    ...

 

 

생성자 주입

 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다. 지금까지 사용한, @Autowired를 생성자에 설정해준것이 생성자 주입이다. 생성자를 제외하고는 수정할 수 있는 방법이 존재하지 않기 때문에 불변성, 필수성을 보장해준다.

+ 생성자가 단 1개만 존재한다면 @Autowired가 자동으로 적용된다.

+ 순환 참조 문제를 컴파일 타임에 잡을 수 있다.

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

 

Setter(수정자) 주입

 setter를 설정해주고 @Autowired를 걸어주면 자동으로 의존관계를 주입해준다. 생성자 주입과 달리 변경의 가능성이 있는 의존관계에 사용한다. 생성자와 수정자 모두에 @Autowired가 있다면 생성자를 통해 의존관계가 주입된 후 수정자를 통한 주입이 이루어진다.

 @Autowired
 public void setMemberRepository(MemberRepository memberRepository) {
     this.memberRepository = memberRepository;
 }
 
 @Autowired
 public void setDiscountPolicy(DiscountPolicy discountPolicy) {
     this.discountPolicy = discountPolicy;
 }

 

필드 주입

 필드에 @Autowired를 직접 걸어주는 방법이다. 코드가 매우 간결하지만 외부에서 변경이 불가능해서 테스트가 불가능하다. 되도록이면 사용하지 말자.

 @Autowired
 private MemberRepository memberRepository;
 
 @Autowired
 private DiscountPolicy discountPolicy;

 

일반 메서드 주입

 setter와 다르게 한번에 여러 필드를 주입할 수 있기는 하지만, 범용성을 위해 되도록이면 사용하지 말자

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

 

 

정리

 사실상 의존관계 주입은 애플리케이션 실행전에 결정되며, 실행 이후에는 종료시점까지 의존관계를 변경할 일이 없기 때문에 변경할 일이 없다. 그러므로 실수로라도 누락되거나 변경될 일이 없게 final로 선언된 필드에 생성자 주입을 사용하는 것이 바람직하다. 반면 수정자 주입과 같은 나머지 방법의 경우 필요한 의존관계를 한눈에 알 수 없기 때문에 누락해서 NullPointerException과 같은 오류들이 발생할 수 있다.