Web/spring study

[Spring] 컴포넌트 스캔(@ComponentScan), 의존관계 자동 주입, 롬복(lombok)

장인이 2023. 1. 10. 01:51

0. 개요

 이번 게시물에서는 스프링 컨테이너에 스프링 빈을 컴포넌트 스캔으로 추가하는 방법에 대해 작성할 것이다. 해당 게시물을 보기 전, 아래 링크의 글을 보고 오는 것을 추천한다.

(스프링 빈과 스프링 컨테이너)

(스프링 빈 자바코드로 수동 등록)

 

1. 자바 코드로 수동 등록의 한계

모든 설명에 나오는 예시 프로젝트의 설계 방식은 아래 링크를 참고
https://imgzon.tistory.com/114

 스프링 컨테이너를 @Bean을 통해서 수동 등록할 경우, 예제와 같이 3~4개정도면 충분히 작성 할 수 있다. 하지만 빈이 수백개가 된다면? 설정 정보 클래스도 커지고, 누락하는 문제도 발생할 수 있다.

 

 그래서 스프링은 @ComponentScan을 통해 자동으로 스프링 빈을 등록하고, @Autowired를 통해 의존관계도 자동으로 주입해준다.

 

 

2. @ComponentScan

@Configuration
@ComponentScan
public class AppConfig {
}

 이러면 끝이다! 하지만 @ComponentScan을 붙인 AppConfig을 만들기만 하는 것으로는 아무일도 일어나지 않는다. 스프링도 어떤 클래스를 빈으로 추가해야 할지 모르기 때문이다.

 

 컴포넌트 스캔은 애노테이션 이름 그대로 @Component 애노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록해준다. 따라서 스프링 빈에 등록하고 싶은 클래스에 @Component 애노테이션을 붙여주자.

 

 

3. @Component 추가

@Component
public class MemoryMemberRepository implements MemberRepository {}

@Component
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
public class MemberServiceImpl implements MemberService {
    private final MemberRepository memberRepository;
    
    @Autowired
    public MemberServiceImpl(MemberRepository memberRepository) {
    	this.memberRepository = memberRepository;
    }
}

@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;
    }
}

 이전에 @Bean으로 직접 스프링 빈을 등록할 때에는 의존관계도 직접 명시했다. 하지만 이런 설정정보가 없기 때문에, @Autowired를 통해 의존관계를 자동으로 주입해준다.

 

 

4. 스프링 컨테이너 생성 과정

1) @ComponentScan

@ComponentScan으로 등록할 경우, 스프링 빈의 이름은 클래스 명을 사용하되, 맨 앞글자만 소문자를 사용한다.

 

2) @Autowired 의존관계 자동 주입

 생성자에 @Autowired 애노테이션을 지정해주면, 스프링 컨테이너가 파라미터에 맞는 스프링 빈을 찾아서 주입한다. 우선 타입이 같은 빈을 찾아서 주입하며, 복잡한 상황에 대해서는 뒤에서 설명할 예정이다.

 

 

5. @ComponentScan 탐색할 패키지 위치

@ComponentScan(
    basePackages = "hello.core",
    basePackageClasses = AppConfig.class
)

- basePackages: 어느 패키지부터 탐색할지 시작 위치를 지정하며, 이 패키지를 포함한 하위 패키지를 모두 탐색한다.

   - basePackages = {"hello.core", "hello.service"} 이렇게 여러 시작 위치를 정할수도 있다.

 

- basePackageClasses: 지정한 클래스의 패키지를 탐색 시작 위치로 지정한다.

   - 만약 지정하지 않으면 @ComponentScan이 붙은 클래스의 패키지가 시작 위치가 된다.

 

 원하는 위치에서 탐색을 시작하고 싶은 경우, 위 두 방식중 하나를 채택해서 사용하면 된다. 하지만 실제로 권장하는 방법설정 정보 클래스(AppConfig)을 프로젝트 최상단에 두고, 패키지 위치를 지정하지 않는 것이다.

(스프링 부트도 이 방법을 기본으로 제공한다.)

 

 

6. 컴포넌트 스캔 대상

@ComponentScan은 @Component 뿐만 아니라 아래 애노테이션이 붙은 클래스도 대상에 포함한다.

 

- @Controller

   - 스프링 MVC 컨트롤러로 인식

- @Service

   - 특별한 처리를 하지는 않지만, 개발자들이 비즈니스 계층을 인식하는데 도움을 줌

- @Repository

   - 스프링 데이터 접근 계층으로 인식

   - 데이터 계층의 예외를 스프링 예외(DataAccessException)으로 변환해줌

- @Configuration

   - 스프링 설정 정보로 인식, 스프링 빈이 싱글톤을 유지하도록 처리해줌

 

위 애노테이션들도 인식할 수 있는 이유해당 애노테이션을 찾아보면 알 수 있다.

//실제로는 더 많은 애노테이션이 있지만, 생략
@Component
public @interface Controller {
}

@Component
public @interface Service {
}

@Component
public @interface Repository {
}

@Component
public @interface Configuration {
}

 이렇게 @Component를 포함하고 있는 것을 알 수 있다.

 사실 애노테이션에는 상속관계가 없지만, 스프링이 애노테이션이 특정 애노테이션을 들고 있는 것을 인식하게 도와준다.

 

 

7. @SpringBootApplication

 하지만 스프링 부트로 프로젝트를 구성해본 사람들은 알것이다. 굳이 @ComponentScan을 하지 않아도 알아서 잘 작동하는데, 그 이유는 스프링 부트를 시작시켜주는 애노테이션인 @SpringBootApplication에 @ComponentScan이 숨겨져 있기 때문이다!

//실제로는 더 많은 애노테이션이 있지만, 생략
@ComponentScan()
public @interface SpringBootApplication {
}

 

 또한, @Controller, @Service, @Repository 클래스의 정보를 스프링 빈으로 자동 등록해주기 때문에, 원할하게 실행되었던 것이다!

 

 

8. 의존관계 자동 주입 방법

@Component 애노테이션이 달린 클래스가 의존관계를 주입받는 방법은 크게 4가지로 나뉘어진다.

 

- 생성자 주입

- 수정자(setter) 주입

- 필드 주입

- 일반 메서드 주입

 

1) 생성자 주입

 이름 그대로 생성자를 통해서 주입 받는 방법으로, 위의 모든 예시가 이 방법을 사용했다. 생성자 주입의 특징으로는

- 인스턴스가 생성될때만 실행되므로, 딱 1번만 호출되는 것이 보장

- 따라서 불변, 필수 의존관계에 사용

@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를 생략해도 의존관계가 자동 주입된다!!

 

2) 수정자(setter) 주입

 수정자 메서드를 통해서 주입받는 방법이다. 수정자 주입의 특징으로는

- 선택, 변경 가능성이 있는 의존관계에 사용 가능하다.

@Component
public class OrderServiceImpl implements OrderService {
    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;
    
    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
    	this.memberRepository = memberRepository;
    }
    
    @Autowired
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
    	this.discountPolicy = discountPolicy;
    }
}

 

3) 필드 주입

 

 필드에 바로 주입하는 방법이다. 필드 주입의 특징으로는

- 간결해서 편해보이지만, 테스트하기 어렵다는 단점이 있다.

  - (순수 자바 코드로 단위테스트하기 어려움)

- DI 프레임워크가 없으면 아무것도 할 수 없으므로, 사용하지 말자!

- 실제 코드와 관계없는 테스트 코드에서 임시로 @Configuration할 시 사용함

@Component
public class OrderServiceImpl implements OrderService {

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

 

4) 일반 메서드 주입

 일반 메서드를 통해서 주입하는 방법이다. 일반 메서드 주입의 특징으로는

- 한번에 여러 필드를 받을 수 있다.(수정자 주입과 다르게)

- 일반적으로 잘 사용하지 않는다.

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

 

5) 그래서 뭘 써야하지? -> 생성자 주입을 선택해라!

 최근에는 스프링을 포함한 대부분 DI 프레임워크들이 생성자 주입을 권장한다! 그 이유는 불변하고, 누락할 경우가 없기 때문이다. 또한, 필드에 final 키워드를 사용할 수 있다는 장점이 있다.(값이 설정 안되면 컴파일 오류 발생시킬 수 있음)

 

 가끔씩 스프링 빈이 필수값이 아닌 경우, 수정자 주입 방식을 선택하자.

- @Autowired(required = false) 애노테이션을 활용하면 된다.

 

 그게 아니라면, 왠만하면 생성자 주입을 선택하자!

 

 

9. 롬복(lombok)

 이제 위의 생성자 주입 방식에 롬복을 적용시켜 보자. 롬복에서 제공하는 애노테이션인 @RequiredArgsConstructor를 활용하면 된다.

 

@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;
    }
}

<기존 코드>

 

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

<롬복 적용 코드>

 

훨씬 간단해졌다! @RequiredArgsConstructor를 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.

 

- 참고로 롬복을 라이브러리에 사용하려면,

  1) build.grade에 라이브러리 및 환경 추가

  2) intellij 사용중이라면 인식을 위해 settings -> plugins -> lombok 검색후 다운

  3) intellij 사용중이라면 settings -> annotation process 검색 -> Enable annotation processing 체크

위 세팅을 해주어야 한다.

 

 

10. 조회되는 빈이 2개 이상이라면?

 @Autowired 애노테이션은 타입으로 주입할 의존관계를 찾는데, 만일 여러개의 하위 타입들이 스프링 빈으로 등록되어있다면 어떻게 될까? DiscoountPolicy와 FixDiscountPolicy, RateDiscountPolicy로 예시를 들어보자

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

 

이후 DiscountPolicy로 의존관계 자동 주입을 하게 되면, NoUniqueBeanDefinitionException 오류가 발생한다.

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

 

물론 하위 타입으로 의존관계 자동 주입을 하면 해결되는 문제이지만, 이는 DIP를 위배하게 된다.

(DIP란?)

 

따라서 이런 문제를 해결하기 위한 방법으로는

 

- @Autowired 필드 명 매칭

- @Qualifier 사용

- @Primary 사용

 

이 있다.

 

 

10-1. @Autowired 필드 명 매칭

 위에서 @Autowired는 타입을 통해서 의존관계를 주입받는다고 했다. 이때, 하위 타입 빈이 여러개 있으면 필드 이름, 파라미터 이름으로 추가적인 확인을 한다.

@Autowired
private DiscountPolicy discountPolicy

<기존 코드>

 

@Autowired
private DiscountPolicy rateDiscountPolicy

<필드 명을 빈 이름으로 변경>

 

따라서 정리해보면

1) 타입 매칭 시도

2) 타입 매칭 시 결과가 2개 이상이면, 파라미터 명으로 매칭

 

 

10-2. @Qualifier 사용

 빈 등록 시, @Qualifier라는 구분할 수 있는 기능을 추가로 등록하는 방법이다. 단, 빈 이름을 변경하는 것은 아니다.

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}

<빈 등록시 @Qualifier 붙여주자>

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

<생성자 자동 주입 시 원하는 @Qualifier 적기>

 

 위의 예시와 같이 @Qualifier로 이름을 부여하고, 의존관계 주입 시 어떤 빈을 주입받을지 @Qualifier로 지정해 주는 방법이다.

 

 

10-3. @Primary 사용

 @Primary 어노테이션을 사용하고 싶은 @Component 위에 추가해주는 방식이다.

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
public class FixDiscountPolicy implements DiscountPolicy {}

<rateDiscountPolicy가 우선권을 가지도록 세팅>

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

<생성자 자동 주입은 수정하지 않음>

 

 코드를 실행해보면 문제 없이 @Primary 애노테이션이 붙은 rateDiscounPolicy 빈이 주입되는 것을 확인 가능하다.

 

 

10-4. 그래서 어떤 방식을 채용해야 하지?

 @Primary와 @Qualifier 둘중 우선순위를 활용하면 된다. @Qualifier가 우선순위가 높으므로

 

1) 자주 사용하는 메인 데이터베이스를 @Primary로 적용시켜 사용한다.

2) 가끔씩 서브 데이터베이스로 변경해야 하는 경우, @Qualifier를 지정해서 휙득하는 방식으로 사용

 

이렇게 운용하면 깔끔하게 유지할 수 있다.

 

 

11. 직접, 자동 등록, 무엇을 선택해야 할까?

 그러면 우리는 직접 애노테이션 자바코드로 등록해야할까, 자동 스캔을 통해 등록해야 할까? 사실 @ComponentScan을 통한 자동 스캔 등록이 훨씬 편리하고, 스프링부트도 이를 기본으로 사용한다.

 

따라서 아래와 같은 규칙으로 선택하면 좋을 것 같다.

 

- 우선 자동 스캔 등록을 기본으로 한다.

- 애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체들은 수동 빈으로 등록해서 설정정보에 바로 나타나게 하자

  - 유지보수 하기 좋다.

 

 


위 내용은 김영한 님의 인프런 강의 "스프링 핵심 원리 - 기본편"의 내용과 강의자료를 토대로 작성된 게시글입니다.

강의 링크:

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8