본문 바로가기
Web/spring study

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

by 장인이 2023. 1. 10.

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

댓글