상세 컨텐츠

본문 제목

[스프링1] 컴포넌트 스캔, 의존관계 자동 주입

22-23/22-23 Spring 1

by YUZ 유즈 2022. 10. 13. 10:00

본문

728x90

해당 포스트는 스프링 핵심 원리 - 기본편 강의 내용을 참고하였습니다.

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com


❗컴포넌트 스캔

📌컴포넌트 스캔과 의존관계 자동 주입 방법

1. @ComponentScan 를 설정 정보에 추가

@Configuration
@ComponentScan(
        excludeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION, classes=Configuration.class)
)
public class AutoAppConfig {
		//이전과는 달리 @Bean 등록 없음!
}

 

2. @Component 를 컴포넌트 스캔의 대상인 클래스들에 추가

@Component
public class MemberServiceImpl implements MemberService{
	  ...
}

3. @Autowired 로 의존관계 자동 주입하기

@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 MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

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

 

 

📌탐색 위치 지정

  • basePackages: 탐색할 패키지의 시작 위치 지정, 하위 패키지 모두 탐색
  • basePackageClasses: 지정한 클래스의 패키지를 탐색 시작 위치로 지정

지정하지 않을 시엔, @ComponentScan 이 붙은 설정 정보 클래스의 패키지가 시작 위치로 지정된다.

설정 정보 클래스의 위치를 프로젝트 최상단에 두고, 패키지 위치 지정 코드를 생략하는 것이 스프링에서 권장하는 방법이다(@SpringBootApplication).

 

📌컴포넌트 스캔 기본 대상

  • @Component : 컴포넌트 스캔에서 사용(기본)
  • @Controller : 스프링 MVC 컨트롤러에서 사용
  • @Service : 스프링 비즈니스 로직에서 사용
  • @Repository : 스프링 데이터 접근 계층에서 사용
  • @Configuration : 스프링 설정 정보에서 사용, 싱글톤 처리

📌필터

  • includeFilters : 컴포넌트 스캔 대상을 추가로 지정
  • excludeFilters : 컴포넌트 스캔에서 제외할 대상 지정
@Configuration
 @ComponentScan(
 includeFilters = @Filter(type = FilterType.ANNOTATION, classes =MyIncludeComponent.class),
 excludeFilters = { @Filter(type = FilterType.ANNOTATION, classes =MyExcludeComponent.class), }
 )
 static class ComponentFilterAppConfig {
 }

[FilterType.]

  • ANNOTATION : 기본값, 애노테이션
  • ASSIGNABLE : 지정한 타입과 자식 타입
  • ASPECTJ : AspectJ 패턴 사용
  • REGEX : 정규 표현식
  • CUSTOM : TypeFilter 인터페이스를 구현하여 처리

 

📌중복 등록과 충돌

1. 자동 빈 등록 vs 자동 빈 등록

    ConflictingBeanDefinitionException 스프링 오류가 발생한다.

2. 수동 빈 등록 vs 자동 빈 등록 : 

    수동 빈 등록이 우선권을 가진다.

    순수 자바 코드로 실행 시에는 오류가 발생하지 않고 아래 로그를 확인할 수 있다.

    Overriding bean definition for bean 'memoryMemberRepository' with a different definition: replacing

   스프링 부트를 통해 실행하면(CoreApplication) 오류가 발생한다.

Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

  오버라이딩 하게 두려고 한다면, resource/application.propperties 파일에 위 코드를 추가한다.

 

 

❗의존관계 자동 주입

📌다양한 의존관계 주입 방법

  1. 생성자 주입
    • 불변, 필수 의존관계에 사용한다.
    • 생성자가 1개만 있는 경우엔 @Autowired 생략해도 자동 주입된다.
  2. 수정자(setter) 주입
    • 선택, 변경 가능성이 있는 의존관계에 사용된다.
    @Autowired
     public void setMemberRepository(MemberRepository memberRepository) {
       this.memberRepository = memberRepository;
     }
    
  3. 필드 주입
    • 코드가 간결하지만 외부에서 변경 불가능하여 테스트하기 어렵다.
    • DI 프레임워크(@SpringBootTest)가 없으면 실행할 수 없다.
    @Component
    public class OrderServiceImpl implements OrderService {
      @Autowired
      private MemberRepository memberRepository;
    
      @Autowired
      private DiscountPolicy discountPolicy;
    }
    
  4. 일반 메서드 주입
    • 한번에 여러 필드 주입할 수 있다.
    • 일반적으로 잘 사용하지 않는다.
    @Autowired
     public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
       this.memberRepository = memberRepository;
       this.discountPolicy = discountPolicy;
     }
    

 

📌옵션 처리

만약 주입할 스프링 빈이 없는 상태에서 @Autowired 를 사용하고 싶다면?

  • @Autowired(required = false) : 자동 주입할 대상이 없으면 메서드 호출X
@Autowired(required=false)
public void setNoBean1(Member nobean1) {
	System.out.println("setNoBean1 = " + nobean1);
}
  • org.springframeword.lang.@Nullable : 자동 주입할 대상이 없으면 null 입력
@Autowired
public void setNoBean2(@Nullable Member nobean2) {
  System.out.println("setNoBean2 = " + nobean2);
}
  • Optional<> : 자동 주입할 대상이 없으면 Optional.empty(java8)
@Autowired
public void setNoBean3(Optional<Member> nobean3) {
  System.out.println("setNoBean3 = " + nobean3);
}

 

📌생성자 주입을 1순위로 선택하자!

  • 불변
    • 대부분의 의존관계는 변경되지 않는다(변경하지 않는 편이 좋다).
    • 수정자 주입 사용 시, setter 메서드를 public으로 변경에 열려있게 해야하기 때문에 밖에서 수정할 수 있게된다.
  • 누락
    • 생성자 주입 시, 의존관계가 누락된 경우 컴파일 오류가 발생한다.
  • final 키워드
    • 필드 값 설정이 누락된 경우, 컴파일 오류가 발생한다.
    • 나머지 주입 방식들은 생성 이후 호출되어 final 키워드를 사용할 수 없다.

위 이유들로, 기본적으로는 항상 생성자 주입을 선택하자. 필수 값이 아닌 경우엔 수정자 주입 방식을 추가하자.

 

📌롬복과 최신 트랜드

생성자 주입 방식을 사용하면 결국 생성자 코드를 작성해야 하는데 코드 작성이 귀찮다면? Lombok을 써보자!

생성자 주입 방식을 사용할 때 final 키워드를 필드에 붙였는데, lombok은 이 final 키워드가 붙은 필드들을 인식하여 자동으로 생성자를 만들어준다.

변경 전 코드

@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 //lombok
public class OrderServiceImpl implements OrderService {
  private final MemberRepository memberRepository;
  private final DiscountPolicy discountPolicy;
}

 

📌조회 빈이 2개 이상

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

@Autowired로 연결 가능한 빈이 2개 이상인 경우, 오류가 발생한다.

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

 

  • @Autowired 필드 명 매칭
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = rateDiscountPolicy;
    }
  • @Qualifier 매칭 : 추가 구분자
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}

 

@Autowired
public OrderServiceImpl(MemberRepository memberRepository,@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
 this.memberRepository = memberRepository;
 this.discountPolicy = discountPolicy;
}
  • @Primary 사용 : 우선순위 지정
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

주의: @Primary보다 @Qualifier의 우선순위가 더 높음!

 

📌애노테이션 직접 만들기

1. 애노테이션 생성

package hello.core.annotataion;
import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.*;
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}

2. 애노테이션 지정

@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {
....
}

 

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

 

📌조회한 빈 모두 필요할 때, List&Map

public class AllBeanTest {
	 @Test
	 void findAllBean() {
		 ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
		 DiscountService discountService = ac.getBean(DiscountService.class);

		 Member member = new Member(1L, "userA", Grade.VIP);
		 int discountPrice = discountService.discount(member, 10000,"fixDiscountPolicy");
		 
		 assertThat(discountService).isInstanceOf(DiscountService.class);
		 assertThat(discountPrice).isEqualTo(1000);
	 }

	 static class DiscountService {
		 private final Map<String, DiscountPolicy> policyMap;
		 private final List<DiscountPolicy> policies;

		 //생성자 하나여서 @Autowired생략, 의존관계 자동 주입
		 public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
			 this.policyMap = policyMap;
			 this.policies = policies;

			 System.out.println("policyMap = " + policyMap);
			 System.out.println("policies = " + policies);
		 }

         //스프링 빈 실행
		 public int discount(Member member, int price, String discountCode) {
			 DiscountPolicy discountPolicy = policyMap.get(discountCode);

			 System.out.println("discountCode = " + discountCode);
			 System.out.println("discountPolicy = " + discountPolicy);
			 return discountPolicy.discount(member, price);
		 }
 }
}
  • Map<String, DiscountPolicy> : key는 스프링 빈 이름, value는 해당 타입(DiscountPolicy)으로 조회한 스프링 빈
  • List<DiscountPolicy> : 해당 타입(DiscountPolicy)로 조회한 모든 스프링 빈 

 

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

  1. 편리한 자동 기능을 기본으로 사용하자.
  2. 기본적으론 자동 기능을 사용하되, 기술 지원 객체는 수동 빈으로 등록하여 설정 정보에서 바로 확인할 수 있도록 하자.
    • 업무 로직 빈 : 컨트롤러, 서비스, 레포지토리 등 비즈니스 요구사항을 개발할 때 추가되거나 변경 됨 → 자동 기능 적극 사용
    • 기술 지원 빈 : 기술적인 문제나 공통 관심사(AOP) 처리에 주로 사용, 업무로직 지원하는 수준의 기술 → 가급적 수동 빈 등록
  3. 다형성을 적극 활용하는 비즈니스 로직은 수동 등록도 염두하자.
    • 혹은, 특정 패키지에 묶어서 담기
  1.  

 


Spring 1

EDITOR: 로자

728x90

관련글 더보기