728x90

인프런 김영한 님의 강의를 참고했습니다.

 

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

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

www.inflearn.com

 

지금까지는 스프링 빈을 등록할 때 자바의 @Bean을 사용하였다.

등록할 스프링 빈의 수가 많아 질수록 등록하는 데에 걸리는 시간일 길어진다.

그렇기 때문에 스프링은 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다.

의존관계도 자동으로 주입해주는 @Autowired도 제공을 해준다.

 

기존에 만들었던 AppConfig.java에 추가로 AutoAppConfig.java를 만들자.

package hello.core;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

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

기존 AppConfig는

package hello.core;

import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.FixDiscountPolicy;
import hello.core.Discount.RateDiscountPolicy;
import hello.core.customer.CustomerRepository;
import hello.core.customer.CustomerService;
import hello.core.customer.CustomerServiceImpl;
import hello.core.customer.MemoryCustomerRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public CustomerService customerService(){
        System.out.println("call AppConfig.customerService");
        return new CustomerServiceImpl(customerRepository());
    }


    @Bean
    public OrderService orderService(){
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(customerRepository(), discountPolicy());
    }

    @Bean
    public CustomerRepository customerRepository(){
        System.out.println("call AppConfig.customerRepository");
        return new MemoryCustomerRepository();
    }

    @Bean
    public DiscountPolicy discountPolicy(){
        //return new FixDiscountPolicy();
        return  new RateDiscountPolicy();
    }
}

안에 @Bean을 나열한 것에 비해 코드가 상당히 짧을 것을 볼 수 있다.

 

이제 @ComponentScan에서 자동으로 읽을 대상이 될 수 있도록 @Component annotation을 붙여주자.

MemoryCustomerRepository

package hello.core.customer;

import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Component
public class MemoryCustomerRepository implements CustomerRepository{

    private static Map<Long, Customer> store = new HashMap<>();

    @Override
    public void save(Customer customer) {
        store.put(customer.getId(), customer);
    }

    @Override
    public Customer findById(Long id) {
        return store.get(id);
    }
}

 

RateDiscountPolicy

package hello.core.Discount;

import hello.core.customer.Customer;
import hello.core.customer.Grade;
import org.springframework.stereotype.Component;

@Component
public class RateDiscountPolicy implements DiscountPolicy{

    private int VIPDiscountPercent = 5; // 할인되는 비율
    private int VVIPDiscountPercent = 10; //할인되는 비율

    @Override
    public int discount(Customer customer, int price) {
        if(customer.getGrade() == Grade.VIP){
            return price * VIPDiscountPercent / 100;
        }
        else if (customer.getGrade() == Grade.VVIP){
            return price * VVIPDiscountPercent / 100;
        }
        else{
            return 0;
        }
    }
}

 

CustomerServiceImpl

package hello.core.customer;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class CustomerServiceImpl implements CustomerService{

    private final CustomerRepository customerRepository;

    @Autowired
    public CustomerServiceImpl(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    @Override
    public void join(Customer customer) {
        customerRepository.save(customer);
    }

    @Override
    public Customer findCustomer(Long id) {
        return customerRepository.findById(id);
    }

    public CustomerRepository customerRepository(){
        return customerRepository;
    }
}

 

OrderServiceImpl

package hello.core.order;

import hello.core.Discount.DiscountPolicy;
import hello.core.customer.Customer;
import hello.core.customer.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements OrderService{

    private final CustomerRepository customerRepository;
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(CustomerRepository customerRepository, DiscountPolicy discountPolicy) {
        this.customerRepository = customerRepository;
        this.discountPolicy = discountPolicy;
    }

    @Override
    public Order createOrder(Long id, String itemName, int itemPrice) {
        Customer customer = customerRepository.findById(id);
        int discountPrice = discountPolicy.discount(customer, itemPrice);

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

    public CustomerRepository customerRepository(){
        return customerRepository;
    }
}

생성자에 @Autowired를 사용하면 의존관계를 주입받을 수 있다.

 

이렇게 고치고 AutoAppConfigTest.java를 만들어보자.

package hello.core.scan;

import hello.core.AutoAppConfig;
import hello.core.customer.CustomerService;
import hello.core.customer.CustomerServiceImpl;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AutoAppConfigTest {

    @Test
    void basicScan(){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AutoAppConfig.class);

        CustomerService customerService = applicationContext.getBean(CustomerServiceImpl.class);
        Assertions.assertInstanceOf(CustomerService.class, customerService);
    }
}

테스트를 해보면 AppConfig와 동일하게 작동하는 것을 볼 수 있다.

 

ComponentScan과 Autowired의 기본 전략

@ComponentScan은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록한다.

그 때 빈 이름은 맨 앞글자만 소문자로 바꾼 후 사용한다.

바꿀 수는 있지만 보통은 그냥 사용한다.

 

@Autowired를 사용하면 의존관계를 자동으로 주입해주는데, 기본적으로 타입이 같은 빈을 찾아서 주입하게 된다.

 

탐색 위치와 기본 스캔 대상

모든 코드들을 찾으면서 @Component를 찾다보면 시간이 오래 걸릴 것이다.

그래서 탐색 범위를 지정해 줄 수 있다.

 

basePackages: 탐색할 패키지의 시작 위치를 지정한다. 이 패키지를 포함해서 해당 패키지의 하위 패키지까지 모두 탐색한다.

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

 

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

 

요즘은 이 설정 정보를 프로젝트 최상단에 올린 후 스캔 하는 것이 권장된다고 한다.

스프링의 시작 정보인 @SpringBootApplication 안에도 @ComponentScan이 들어있어 프로젝트 시작 루트 위치에 두는 것이 관례이다.

 

컴포넌트 스캔의 대상에는

@Component 뿐만 아니라 다음과 내용도 추가로 대상에 포함된다.

  • @Controller: 스프링 MVC 컨트롤러에서 사용, 스프링 MVC 컨트롤러로 인식.
  • @Service: 스프링 비즈니스 로직에서 사용, 특별한 처리는 하지 않는다.
  • @Repository: 스프링 데이터 접근 계층에서 사용, 스프링 데이터 접근 계층으로 인식.
  • @Configuration: 스프링 설정 정보에서 사용, 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다.

해당 annotation들을 보면 @Component를 포함하고 있다.

 

필터

필터를 사용해서 스캔 대상을 추가로 지정하거나 스캔 대상에서 재외 할 수 있다.

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

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

 

예제 Component들을 만들고 실습해보록 하자.

우선 annotation들을 만들고

package hello.core.scan.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TmpExcludeComponent {

}
package hello.core.scan.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TmpIncludeComponent {
    
}

우리가 스캔할 class들에 달아준다.

package hello.core.scan.filter;

@TmpIncludeComponent
public class MyBeanA {

}
package hello.core.scan.filter;

@TmpExcludeComponent
public class MyBeanB {

}

이 class들을 스캔해보자

package hello.core.scan.filter;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

public class ComponentFilterAppConfigTest {

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

    }

    @Test
    void filterScan() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);

        MyBeanA myBeanA = applicationContext.getBean("myBeanA", MyBeanA.class);
        Assertions.assertNotNull(myBeanA);

        Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> applicationContext.getBean("myBeanB", MyBeanB.class));
    }
}

이렇게 테스트를 해보면 includeFilter에 들어간 Component들만 스캔이 되고 exclydeFilter에 들어간 Component는 스캔이 되지 않는 것을 볼 수 있다.

 

중복 등록과 충돌

스프링에서 이름이 같은 경우에 충돌하는 경우가 생긴다.

 

1. 자동 빈 등록 VS 자동 빈 등록

이 경우에는

이렇게 ConflictionBeanDefinitionException이 발생하게 된다.

그럴 때에는 이름을 바꿔주도록 하자.

 

2. 수동 빈 등록 VS 자동 빈 등록

이 경우에는 수동 빈이 자동 빈을 오버라이딩 해버린다.

 

그리고 그런 경우에는 이런 로그가 남는다.

하지만 이런 경우에 의도하지 않는 버그가 만들어 질 수도 있으니, 피하도록 하고 최근에는 스프링 부트에서 오류가 발생하도록 해주고 있다.

최대한 명확하게 사용하도록 하지.

'백엔드 > 스프링' 카테고리의 다른 글

스프링 11일차  (0) 2023.02.12
스프링 10일차  (0) 2023.02.11
스프링 8일차  (0) 2023.02.07
스프링 7일차  (0) 2023.02.07
스프링 6일차  (0) 2023.02.06

+ Recent posts