개발 이야기/Spring

[Spring] 컴포넌트 스캔과 의존 관계 자동 주입

제이온 (J.ON) 2021. 6. 17.

안녕하세요? 제이온입니다.

 

 오늘은 컴포넌트 스캔과 의존 관계 자동 주입에 대해서 알아 보겠습니다.

 

 

컴포넌트 스캔

지금까지는 @Configuration이 붙은 설정 파일을 이용하여 빈을 수동 주입하였습니다. 이렇게 개발자가 수동으로 빈을 주입하고 의존 관계를 정해줄 수도 있지만, 만약 등록할 빈이 많다면 일일이 모두 등록해야해서 상당히 귀찮습니다. 그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공합니다.

 

 

@Component
public class OrderServiceImpl implements OrderService {

    private final DiscountPolicy discountPolicy;

    public OrderServiceImpl(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }

    @Override
    public Order createOrder(int age, String itemName, int itemPrice) {
        int discountPrice = discountPolicy.discount(age, itemPrice);
        return new Order(itemName, itemPrice, discountPrice);
    }
}

@Component
public class RateDiscountPolicy implements DiscountPolicy {

    private static final int ADULT = 20;
    private static final int DISCOUNT_PERCENT = 10;

    @Override
    public int discount(int age, int price) {
        if (age < ADULT) {
            return price * DISCOUNT_PERCENT / 100;
        }
        return 0;
    }
}

@Configuration
@ComponentScan
public class AppConfig {

}

 

 

먼저, 위와 같이 빈으로 등록할 클래스에 @Component를 붙여 주고 설정 파일에 @Configuration과 @ComponentScan을 붙여 줍니다. 그러면 끝납니다. @ComponentScan이 @Component가 붙은 모든 객체를 찾아서 빈으로 등록하기 때문이죠.

 

 

public class Main {

    public static void main(String[] args) {
        final ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        final OrderService orderService = context.getBean(OrderService.class);
        final Order order = orderService.createOrder(15, "연필", 3000);
        System.out.println(order.getDiscountPrice());
    }
}

// 실행 결과
21:05:00.141 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@462d5aee
21:05:00.196 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalConfigurationAnnotationProcessor'
21:05:00.382 [main] DEBUG org.springframework.context.annotation.ClassPathBeanDefinitionScanner - Identified candidate component class: file [C:\Users\hikar\Desktop\pjy\programming\ps\out\production\classes\blog\discount\RateDiscountPolicy.class]
21:05:00.390 [main] DEBUG org.springframework.context.annotation.ClassPathBeanDefinitionScanner - Identified candidate component class: file [C:\Users\hikar\Desktop\pjy\programming\ps\out\production\classes\blog\order\OrderServiceImpl.class]
21:05:00.716 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerProcessor'
21:05:00.722 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerFactory'
21:05:00.725 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalAutowiredAnnotationProcessor'
21:05:00.729 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalCommonAnnotationProcessor'
21:05:00.758 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'appConfig'
21:05:00.769 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'rateDiscountPolicy'
21:05:00.770 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'orderServiceImpl'
21:05:00.823 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Autowiring by type from bean name 'orderServiceImpl' via constructor to bean named 'rateDiscountPolicy'
300

 

 

사용자 입장에서는 동일하게 설정 파일을 이용하여 스프링 컨테이너를 정의하고 비즈니스 로직을 작성하면 됩니다. 실행 결과를 보면 원하는 빈들이 잘 등록되어 있고 할인 금액도 잘 출력되는 것을 알 수 있습니다. 다만, 맨 마지막 줄을 보면 Autowiring이라는 처음 보는 명령어가 보입니다. 이것이 바로 의존 관계 자동 주입입니다.

 

의존 관계 자동 주입을 설명하기 전에 왜 getBean()에서 이름을 붙이지 않고 OrderService.class만 붙였는지 이유를 말씀드리겠습니다. 이유는 간단합니다. 빈을 자동 등록할 때 빈의 이름은 등록하려는 객체의 맨앞 글자를 소문자로 바꾸고 나머지는 그대로 쓰기 때문입니다. 가령, OrderServiceImpl의 경우 스프링 컨테이너에 orderServiceImpl이라는 이름으로 등록이 됩니다. 어차피 OrderService 타입의 빈은 하나만 있으므로 이름은 생략했습니다.

 

 

의존 관계 자동 주입

자, 아까 언급했던 의존 관계 자동 주입에 대해서 알아 보겠습니다. 컴포넌트 스캔으로 빈을 등록하고 나면 의존 관계를 만들어 줘야 합니다. 설정 파일에서 수동으로 의존 관계를 주입할 때는 직접 주입할 코드를 적어 주었지만, 자동으로 빈 등록할 때는 @Autowired라는 어노테이션을 사용해야 합니다. 이때, @Autowired를 사용하여 의존 관계를 주입하는 방법은 총 4가지가 있습니다.

 

 

(1) 생성자 주입

이름 그대로 생성자를 통해서 의존 관계를 주입 받는 방법입니다. 최근 스프링 버전부터는 생성자가 1개만 존재한다면 @Autowired를 생략해도 무방하게끔 업데이트 되었습니다.

 

 

@Component
public class OrderServiceImpl implements OrderService {

    private final DiscountPolicy discountPolicy;
	
    @Autowired // 생략 가능
    public OrderServiceImpl(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }

    @Override
    public Order createOrder(int age, String itemName, int itemPrice) {
        int discountPrice = discountPolicy.discount(age, itemPrice);
        return new Order(itemName, itemPrice, discountPrice);
    }
}

 

 

위와 같이 일반적인 자바 코드에서 사용하는 것과 똑같이 사용합니다. 생성자 주입은 생성자 호출 시점에 딱 1번만 호출된다는 것이 보장되기 때문에 불변 또는 필수 의존 관계에 사용됩니다. 참고로 당연하겠지만 @Autowired는 스프링 빈이 아니면 작동하지 않습니다.

 

 

(2) 수정자 주입

setter를 통해 의존 관계를 주입하는 방법입니다. 주로 선택이나 변경 가능성이 있는 의존 관계에 사용됩니다.

 

 

@Component
public class OrderServiceImpl implements OrderService {

    private DiscountPolicy discountPolicy;

    @Override
    public Order createOrder(int age, String itemName, int itemPrice) {
        int discountPrice = discountPolicy.discount(age, itemPrice);
        return new Order(itemName, itemPrice, discountPrice);
    }

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

 

 

위와 같이 setter 위의 @Autowired를 붙여줌으로써 의존 관계를 주입할 수 있습니다.

 

 

(3) 필드 주입

이름 그대로 필드에 바로 주입하는 방법입니다.

 

 

@Component
public class OrderServiceImpl implements OrderService {

    @Autowired
    private DiscountPolicy discountPolicy;

    @Override
    public Order createOrder(int age, String itemName, int itemPrice) {
        int discountPrice = discountPolicy.discount(age, itemPrice);
        return new Order(itemName, itemPrice, discountPrice);
    }
}

 

 

생성자도 setter 코드 없이 상당히 간결하게 의존 관계를 주입할 수 있다는 단점이 있습니다. 하지만, 위 방법은 수많은 문제점이 존재합니다.

 

먼저, 외부에서 변경이 불가능하므로 테스트를 하기 힘듭니다. 그리고 @Autowired라는 DI 프레임워크가 없다면 어떠한 것도 할 수 없습니다. 예를 들어 해당 코드에서 스프링 프레임워크를 제거하면 동작하지 않게 됩니다. 반면, 생성자나 setter는 @Autowired가 없다고 해서 코드가 먹통이 되는 일은 발생하지 않습니다.

 

따라서 해당 방법은 프로덕션 코드에서는 사용하지 마시고 테스트 코드에서만 사용하시길 바랍니다.

 

 

(4) 일반 메소드 주입

setter가 아닌 일반 메소드를 통해서 의존 관계를 주입하는 방법입니다. setter와 달리 한 번에 여러 필드를 주입받을 수 있다는 장점이 있으나 일반적으로 잘 사용하지는 않습니다.

 

 

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

 

 

컴포넌트 스캔의 대상

제가 위에서 @Component가 붙은 대상은 자동으로 스프링 컨테이너에 등록된다고 이야기하였습니다. 이외에도 @Controller, @Service, @Repository, @Configuration도 등록 대상이 됩니다.

 

@Controller은 스프링 MVC 컨트롤러로 인식되며 @Repository는 스프링 데이터 접근 계층으로 인식하고 해당 계층에서 발생하는 예외는 모두 DataAccessException으로 변환합니다. @Configuration은 스프링 설정 정보로 인식하고 스프링 빈이 싱글톤을 유지하도록 추가 처리를 합니다. 마지막으로 @Service는 특별한 처리는 하지 않으나, 개발자들이 핵심 비즈니스 계층을 인식하는데 도움을 둡니다.

 

따라서 상황에 맞게 단순히 @Component를 박기 보다는 적절히 컴포넌트 스캔 대상이 되는 어노테이션을 붙여주는 것이 좋겠습니다.

 

 

정리

지금까지 컴포넌트 스캔과 의존 관계 자동 주입에 대해서 알아 보겠습니다. 다음 시간에는 빈의 생명 주기를 설명하겠습니다.

 

 

출처

 

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

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 개발자가 되어보세요! 📣 확인해주

www.inflearn.com

 

추천 글