본문 바로가기

WINK-(Web & App)/Spring Boot 스터디

[2024 Spring Boot 스터디] 남윤찬#2 주차 - 6~7 섹션

반응형

컴포넌트 스캔과 의존관계 자동 주입 시작하기

Configuration, 즉 빈 정보를 직접 작성해주었지만, 프로젝트가 방대해지면 불가능에 가까워진다. 그래서 스프링은 Configuration 없이 스프링 빈을 등록하는 컴포넌트 스캔을 제공한다.

컴포넌트 스캔은 @ComponentScan이라는 어노테이션 하나를 붙여주면 된다. 그러면 이름 그대로 @Component 어노테이션이 붙은 클래스를 스캔해 스프링 빈으로 등록한다.

package hello.core;

...

@Configuration
@ComponentScan(
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {
}

이전의 AppCofig에서와 달리 컴포넌트 스캔에서는 의존관계를 설정하지 않았기에 @Autowired를 각 클래스에 붙여 의존관계를 자동으로 주입하여 해결해준다.

@Component
public class MemberServiceImpl implements MemberService{

    private final MemberRepository memberRepository;

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

컴포넌트 스캔은 아래 그림처럼 진행한다.

@ComponentScan이 @Component가 붙은 모든 클래스를 스프링 빈으로 등록한다. 스프링 빈의 이름은 클래스명의 맨 앞글자만 소문자로 하여 사용한다.

의존관계 주입도 스프링이 자동으로 한다.

생성자에 @Autowired를 지정하면 스프링 컨테이너가 타입이 같은 해당 스프링 빈을 찾아서 주입한다.

탐색 위치와 기본 스캔 대상

탐색 패키지의 시작 위치

컴포넌트 스캔을 할 때 모든 컴포넌트를 스캔하면 스프링이 가진 기본 컴포넌트까지 모두 스캔하기 때문에 오랜 시간이 걸린다. 그렇기에 basePackages를 지정해 탐색할 패키지의 시작 위치를 지정한다.

@ComponentScan(
	basePackages = "hello.core",
)

하지만 권장하는 방법은 프로젝트 메인 설정 정보는 프로젝트의 루트 위치에 두고 basePackages 지정을 생략하는 것이다. 이 대표적인 예시가 @SpringBootApplication 어노테이션이다. 이것이 @ComponentScan을 포함하고 있다.

컴포넌트 스캔 기본 대상

컴포넌트 스캔을 할 때는 아래 어노테이션들을 모두 스캔한다. @Component를 제외한 다른 어노테이션들은 모두 @Component를 포함하고 있다.

  • @Component
    • 컴포넌트 스캔에 사용
  • @Controller
    • 스프링 MVC 컨트롤러로 인식되어 사용됨
  • @Service
    • 스프링 비즈니스 로직에서 사용
  • @Repository
    • 스프링 데이터 접근 계층에서 사용
    • 데이터 계층의 예외를 스프링 예외로 변환
  • @Configuration
    • 스프링 설정 정보에서 사용
    • 스프링 빈이 싱글톤을 유지하도록 추가 처리

필터

includeFilters: 컴포넌트 스캔 대상을 추가로 지정

excludeFilters: 컴포넌트 스캔에서 제외할 대상을 지정

스캔 대상에 추가/제외할 어노테이션을 생성한다.

package hello.core.scan.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
///같은 방식으로 MyExcludeComponent를 생성한다
public @interface MyIncludeComponent {
}

스캔 대상에 추가/제외할 클래스에 어노테이션을 붙여준다.

@MyIncludeComponent
public class BeanA {
}

@MyExcludeComponent
public class BeanB {
}

테스트 코드

**package hello.core.scan.filter;

...

public class ComponentFilterAppConfigTest {

    @Test
    void filterScan() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
        BeanA beanA = ac.getBean("beanA", BeanA.class);
        Assertions.assertThat(beanA).isNotNull();

        assertThrows(
                NoSuchBeanDefinitionException.class,
                () -> ac.getBean("beanB", BeanB.class)
        );
    }

    @Configuration
    @ComponentScan(
            includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
            excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
    )
    static class ComponentFilterAppConfig {

    }
}**

이처럼 @ComponentScans을 달아줄 때 포함/제외할 어노테이션을 미리 지정해준다.

중복 등록과 충돌

자동 빈 등록 vs 자동 빈 등록 → 스프링이 ConflictingBeanDefinitionException 발생시킴

수동 빈 등록 vs 자동 빈 등록 → 수동 빈이 우선권을 가지기 때문에 자동 빈을 오버라이딩 해버린다. 하지만 스프링부트를 실행시키면 오류를 발생시킨다.

하지만 아래 것을 명심하여 이런 오류가 생기지 않도록 설계를 하도록 한다.

    명확하지 않은 건 하지 말자.

    애매한 상황(어설픈 추상화, 우선순위)은 만들지 말자.


다양한 의존관계 주입 방법

생성자 주입

그동안 해왔던 대로 생성자 호출 시점에 딱 한 번 호출하여 DI 해주는 것이다.

불변, 필수 의존관계에 사용

스프링 빈에서는 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입된다.

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

수정자 주입(setter 주입)

필드의 값을 변경하는 수정자 메서드(setter)로 DI 한다

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

@Component
public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

필드 주입

필드에 바로 주입한다

코드가 간결하지만 DI 프레임워크 없이는 사용 불가

테스트나 @Configuration 외에 사용하지 않기(말고도 굳이 쓰지 않기)(인텔리제이도 말린다)

@Component
public class OrderServiceImpl implements OrderService {

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

일반 메서드 주입

일반 메서드를 통해 주입한다

한 번에 여러 필드를 주입 받을 수 있지만 일반적으로 사용하지 않는다

@Component
public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

옵션 처리

@Autowired에는 자동 주입 대상을 처리하는 옵션이 있다

자동 주입 대상이 없으면..

@Autowired(required = false): 호출 자체를 안함

org.springfframework.lang.@Nullable: null이 입력됨

Optional<>: Optional.empty가 입력됨

  	//호출X
  	@Autowired(required = false)
    public void setNoBean1(Member noBean1) {
        System.out.println("noBean1 = " + noBean1);
    }

    //null 호출
    @Autowired
    public void setNoBean2(@Nullable Member noBean2) {
        System.out.println("noBean2 = " + noBean2);
    }

    //Optional.empty 호출
    @Autowired
    public void setNoBean3(Optional<Member> noBean3) {
        System.out.println("noBean3 = " + noBean3);
    }

생성자 주입을 선택해라!

최근에는 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장한다. 이유는 다음과 같다.

불변

대부분의 DI는 애플리케이션 종료 시점까지 의존관계를 변경할 일이 없다.

수정자 주입을 사용하면 메서드를 public으로 열어둬야하고, 누군가 변경할 수 있어서 좋지 않다.

생서자 주입은 객체가 생성될 때 딱 한 번 호출 되므로 불변하게 설계할 수 있다.

누락

순수한 자바 코드로만 unit test를 할 때 DI가 누락되면 컴파일 오류(NullPointerException)가 발생하여 어떤 값이 누락 되었는지 바로 알 수 있다.

final 키워드

필드에 final 키워드를 붙여줌으로써 생성자에 값 설정에 오류/누락이 있는지 확인할 수 있다.

롬복과 최신 트렌드

롬복 라이브러리를 사용해 기존의 번잡한 생성자 주입 방식을 최적화할 수 있게 도와준다.

최근에는 생성자를 딱 한 개만 두고 @Autowired를 생략하고 @RequiredArgsConstructor 사용하는 식으로 한다.

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

+라이브러리 등록방법

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.2.5'
	id 'io.spring.dependency-management' version '1.1.4'
}

group = 'hello'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '17'
}

//lombok 설정 추가 시작
configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}
//lombok 설정 추가 끝

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'

	//lombok 라이브러리 추가 시작
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'

	testCompileOnly 'org.projectlombok:lombok'
	testAnnotationProcessor 'org.projectlombok:lombok'
	//lombok 라이브러리 추가 끝

	testImplementation ('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}

}

tasks.named('test') {
	useJUnitPlatform()
}

조회 빈이 2개 이상 - 문제

@Autowired는 타입으로 조회한다. 타입으로 조회하면 타입이 두 개 이상 인식되면 오류가 발생한다.

@Autowired 필드 명, @Qualifier, @Primary

조회 빈이 2개 이상일 때는 세 가지 방법을 통해 해결할 수 있다.

@Autowired 필드 명 매칭

@Autowired //필드 명이 rateDiscountPolicy인 빈을 매칭
private DiscountPolicy rateDiscountPolicy
  1. 타입 매칭을 하고
  2. 여러 개의 빈이 매칭 되면, 필드명과 파라미터 명으로 빈 이름 매칭을 한다

@Qualifier → @Qualifier끼리 매칭 → 빈 이름 매칭

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
}

@Qualifier는 추가 구분자를 붙여주는 방법이다.

  1. @Qualifier끼리 매칭하고
  2. 찾지 못하면 빈 이름 매칭을 한다.

@Primary 사용

@Primary는 우선순위를 정하는 방법이다.

@Autowired시에 여러 빈이 매칭 되면 @Primary가 우선권을 가진다j

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
public class FixDiscountPolicy implements DiscountPolicy {}
@Autowired //@Primary가 붙은 빈을 매칭시킨다
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
}

@Primary와@Qualifier사용

예를 들어 메인 db와 서브 db가 있다면, 메인 db에는 @Primary로 지정해주고, 필요할 때만 사용하도록 서브 db에는 @Qualifier를 지정해주는 것이다. 스프링은 자동 설정보다 수동 설정, 넓은 범위보다 좁은 범위에 우선권이 있어 @Qualifier가 우선순위가 높다.

애노테이션 직접 만들기

@Qualifier(”mainDiscountPolicy”)처럼 안에 문자를 쓸 때 컴파일을 할 때 타입 체크가 되지 않는다. 이럴 땐 어노테이션을 만들어 해결할 수 있다

package hello.core.annotation;

import org.springframework.beans.factory.annotation.Qualifier;

import java.lang.annotation.*;

@Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.ANNOTATION_TYPE})
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}

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

의도적으로 해당 타입의 스프링 빈이 모두 필요한 경우도 있는데, Spring은 List와 Map으로 해결할 수 있다.

package hello.core.autowired;

...

import static org.assertj.core.api.Assertions.*;

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);

        int rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
        assertThat(rateDiscountPrice).isEqualTo(2000);
    }

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

        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);
            return discountPolicy.discount(member, price);
        }
    }
}

DiscountService는 DiscountPolicy를 주입받는데, fixDiscountPolicy와 rateDiscountPolicy가 주입된다.

discount()는 discountCode에 맞추어 map에서 스프링 빈을 찾아 실행한다.

탐지된 여러 스프링 빈은 Map<String, DiscountPolicy>와 List<DiscountPolicy>에 맞추어 담기게 된다.

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

자동 기능을 기본으로 사용하자

이제는 @Controller, @Service, @Repository처럼 계층에 맞추어 구현 하기도 하고 관리할 빈도 많고 직접 등록하는 과정은 번거롭기 때문에 자동 기능을 기본으로 사용하는 편이 좋다.

수동 빈 등록은 직접 등록하는 기술 지원 객체에 사용하자

기술 지원 로직은 업무 로직에 비해 수도 적고 애플리케이션 전체적으로 끼치는 영향도 적어 에러 파악도 쉽지 않기에 수동으로 등록하여 잘 작동하고 있는지 명확하게 드러내는 것이 좋다.

다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해보자

DiscountPolicy 같이 구현체를 자주 바꿀 일이 있는 경우라면 수동 등록을 사용해볼법하다.

반응형