[Spring] 스프링 빈 등록

olsohee·2023년 3월 20일
0

Spring

목록 보기
2/12

1. 수동 빈 등록

수동 빈 등록은 자바 코드의 @Bean이나 XML의 <bean> 등을 통해서 등록할 스프링 빈을 직접 설정 정보에 적어주는 방식이다.

1-1. @Configuration

  • @Configuration이 달린 클래스는 빈 설정을 담당하는 설정 정보 클래스가 된다.
    스프링 컨테이너는 @Configuration이 붙은 클래스를 설정 정보로 사용한다.

1-2. @Bean

  • @Configuration이 달린 클래스 내에서, 메소드에 @Bean을 적용하면 메소드가 반환하는 객체가 스프링 빈으로 등록된다.

2. 자동 빈 등록

스프링은 설정 정보가 없어도 자동으로 스프링 빈으로 등록해주는 @ComponentScan 기능과 의존관계도 자동으로 주입해주는 @Autowired 기능을 제공한다.

2-1. @ComponentScan, @Component

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

    • @Configuration이 붙은 설정 정보가 스프링 빈으로 등록되었던 이유도, @Configuration 소스코드 내부에 @Component가 붙어있기 때문이다.

    • 이때 클래스 명의 앞글자를 소문자로 바꾼 이름이 빈 이름으로 등록된다.
      (ex, MemberService -> memberService)

    • @Component("memberService2")와 같이 스프링 빈 이름을 직접 지정할 수도 있다.

  • 탐색 위치

    • @ComponentScan이 적용된 클래스의 패키지와 그 하위 패키지에 있는 @Component가 적용된 클래스들이 빈으로 등록된다.

    • 다음과 같이 탐색 위치를 지정할 수 있다.

      • basePackages : 해당 패키지를 포함해서 그 하위 패키지를 모두 탐색한다.
        ex, @ComponentScan(basePackages = "hello.core")
        ex, @ComponentScan(basePackages = "hello.core", "hello.service")

      • basePackagesClasses : 지정한 클래스의 패키지를 탐색 시작 위치로 지정한다.
        ex, @ComponentScan(basePackagesClasses = AppConfig.class)

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

권장: 패키지 위치를 지정하지 않고, 설정 정보 클래스를 프로젝트의 최상단에 두는 방식

예를 들어서 프로젝트가 다음과 같은 구조로 되어 있으면

  • com.hello
  • com.hello.service
  • com.hello.repository

com.hello 위치에 AppConfig 같은 설정 정보 클래스를 두고, @ComponentScan 애노테이션을 붙이고, basePackages 지정은 생략한다. 그러면 프로젝트의 최상단부터 그 하위 패키지까지 모두 컴포넌트 스캔의 대상이 된다.

스프링 부트 사용 시

스프링 부트를 사용하면 스프링 부트의 대표 시작 정보인 @SpringBootApplication을 프로젝트의 시작 위치에 두는 것이 관례이다.
그리고 @SpringBootApplication 안에 @ComponentScan이 들어있다. 따라서 별도의 설정 정보를 만들지 않아도 스프링 부트가 알아서 컴포넌트 스캔을 통해 스프링 빈을 등록한다.

다음 애노테이션은 @Component를 가지고 있으면서 추가로 부가 기능을 수행한다.

  • @Controller
    : 스프링 MVC 컨트롤러로 인식
  • @Repository
    : 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해준다.
  • @Configuration
    : 스프링 설정 정보로 인식하고, 스프링 빈을 싱글톤으로 관리해준다.
  • @Service
    : 특별한 처리를 하지는 않지만, 비즈니스 계층을 인식하는데 도움이 된다.

2-2. @Autowired

  • 생성자를 통한 의존관계 주입 시 사용하는 애노테이션이다.
    생성자에 @Autowired를 붙이면, 스프링 컨테이너가 등록된 빈 중에 해당 빈을 찾아서 자동으로 의존관계를 주입한다.

  • 이때 조회 전략은 타입이 같은 빈을 찾아서 주입한다.
    예를 들어, public MemberServiceImpl(MemberRepository memberRepository) {} 생성자에 @Autowired가 붙어 있는 경우, getBean(MemberRepository.class)로 동작한다.

  • 생성자가 한 개인 경우 생략 가능하다.

  • 롬복의 @RequiredArgsConstructor를 적용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다. 따라서 생성자를 한 개만 두어 @Autowired를 생략하고, @RequiredArgsConstructor까지 적용하면 다음과 같이 코드를 깔끔하게 사용할 수 있다.

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

2-2-1. 옵션 처리

  • @Autowired의 기본 값이 required = true이기 때문에 자동 주입할 대상이 없으면 오류가 발생한다. 이때 오류가 발생하지 않고 제대로 동작하기 위해서는 다음과 같은 자동 주입 대상을 옵션을 사용하면 된다.

  • @Autowired(required = false)
    : 자동 주입할 대상이 없으면 해당 메소드 자체가 호출되지 않는다.

    @Autowired(required = false)
    public void NoBean(Member member) {...} //Member는 스프링 빈이 아니다. 
    //위 메소드는 호출되지 않는다. 
  • @Nullable
    : 자동 주입할 대상이 없으면 null이 입력된다.

    @Autowired
    public void NoBean(@Nullable Member member) {...} 
    //member에는 null이 입력된다.
  • Optional<>
    : 자동 주입할 대상이 없으면 Optional.empty가 입력된다.

    @Autowired
    public void NoBean(Optional<Member> Member member) {...} 
    //member에는 Optional.empty가 입력된다. 

3. 다양한 의존관계 주입 방법

3-1. 생성자 주입

@Component
public class OrderServiceImpl implements OrderService {
	
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
    @Autowired //생성자가 1개만 있는 경우 생략 가능
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
    	this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}
  • 생성자를 통해 의존관계를 주입하는 방식이다.

  • 생성자 호출 시점에 딱 1번만 호출되는 것이 보장된다. 따라서 의존관계가 불변하며 필수인 경우 사용된다.

  • 가장 권장되는 의존관계 주입 방식이다. 그 이유는 다음과 같다.

    • 대부분의 의존관계는 불변해야 한다.
      만약 수정자 주입을 사용하면, setter 메소드를 public으로 열어두기 때문에 의존관계가 변경될 수 있다. 반면 생성자 주입을 사용하면, 객체를 생성할 때 생성자가 딱 1번만 호출되므로 한 번 설정된 의존관계는 변하지 않는다.

    • NullPointerException이 발생하지 않는다.
      생성자를 통해 초기에 의존관계가 할당되기 때문에, NullPointerException이 발생하지 않는다. 그리고 만약 new OrderServiceImpl()와 같이 객체 생성 시점에 주입 데이터를 누락한다면, 이때 컴파일 오류가 발생하기 때문에 NullPointerException이 발생하지 않도록 데이터 누락을 바로 잡을 수 있다.

    • final 키워드를 사용할 수 있다
      생성자 주입 방식을 사용하면 final 키워드를 사용할 수 있다. 따라서 생성자를 정의할 때 의존관계 주입 코드가 빠진 경우, 컴파일 오류가 발생하여 바로 잡을 수 있다.
      * 생성자 주입만 final 키워드를 사용할 수 있다. 수정자 주입과 필드 주입은 모두 생성자 이후에 호출되기 때문이다.

3-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;
    }
}
  • setter 메소드를 통해 의존관계를 주입하는 방식이다.

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

  • final 키워드를 사용할 수 없으며, 의존관계가 변경될 수 있다.

  • 실수로 해당 메소드 호출을 빼먹어 의존관계 주입이 누락된 경우, NullPointerException이 발생한다.

3-3. 필드 주입

@Component
public class OrderServiceImpl implements OrderService {
	
	@Autowired private MemberRepository memberRepository;
    @Autowired private DiscountPolicy discountPolicy;
}
  • 빈으로 등록된 객체를 사용하고자 하는 클래스에 필드로 선언한 뒤 @Autowired를 붙여주면 자동으로 의존관계가 주입된다.

  • 코드가 간결하다는 장점이 있다.

  • final 키워드를 사용할 수 없으며, 의존관계가 변경될 수 있다.

  • 참조 관계를 눈으로 확인하기 어렵다.


4. 스프링 빈 중복 등록

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

  • 컴포넌트 스캔에 의해 자동으로 빈이 등록되었는데 이름이 같은 경우, ConflictingBeanDefinitionException 예외가 발생한다.

4-2. 수동 빈 등록 vs 자동 빈 등록

  • 수동 빈이 자동 빈을 오버라이딩 하며, 수동 빈 등록이 우선권을 갖는다.
    ex, Overriding bean definition for bean 'memoryMemberRepository' with a different definition: replacing

  • 만약 의도적인 설정이 아니라 여러 설정들이 꼬여서 이런 결과가 나온 경우에는 잡기 어려운 버그가 만들어진다. 그래서 스프링 부트는 수동 빈 등록과 자동 빈 등록이 충돌되는 경우에 오류가 발생하도록 기본 값을 설정해두었다.
    ex, Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true


5. 조회 빈이 2개 이상인 경우

  • @Autowired는 타입으로 조회한다.
    따라서 아래 코드의 경우 ac.getBean(DiscountPolicy.class)와 유사하게 동작한다.

    @Autowired
    public OrderServiceImpl(DiscountPolicy discountPolicy) {
    	this.discountPolicy = discountPolicy;
    }
  • 따라서 만약 DiscountPolicy의 하위 타입인 FixDiscountPolicy, RateDiscountPolicy 둘 다 스프링 빈으로 등록되어 있다면,
    위 코드 실행 결과, NoUniqueBeanDefinitionException 오류가 발생한다.

  • 해결 방법은 다음과 같다.

    • @Autowired 필드명 매칭

    • @Qualifier

    • @Primary

5-1. @Autowired 필드 명 매칭

  • @Autowired는 타입 매칭을 시도하고, 이때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.

    1. 타입 매칭 시도

    2. 타입 매칭 결과가 2개 이상인 경우 필드명, 파라미터 명으로 빈 이름 매칭

      //필드 이름을 빈 이름으로 변경
      @Autowired
      private DiscountPolicy rateDiscountPolicy;
      }
      //파라미터 이름을 빈 이름으로 변경
      @Autowired
      public OrderServiceImpl(DiscountPolicy rateDiscountPolicy) {
      	discountPolicy = rateDiscountPolicy;
      }

5-2. @Qualifier

  • @Qualifier는 추가 구분자를 붙여주는 방법이다.
    * 빈 이름 자체를 변경하는 것은 아니다.

    //추가 구분자 붙여주기
    @Component
    @Qualifier("mainDiscountPolicy")
    public class RateDiscountPolicy implements DiscountPolicy{}
    //추가 구분자 붙여주기
    @Component
    @Qualifier("fixDiscountPolicy")
    public class FixDiscountPolicy implements DiscountPolicy {}
    //주입 시에 등록한 이름을 적어주기
    @Component
    public class OrderServiceImpl implements OrderService {
    
       private final DiscountPolicy discountPolicy;
    
       @Autowired
       public OrderServiceImpl(@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
           this.discountPolicy = discountPolicy;
       }
  • @Qualifier("mainDiscountPolicy") 이렇게 문자를 적으면, 오타가 있어도 컴파일 시 체크가 안된다. 따라서 애노테이션을 만들어서 문제를 해결할 수 있다.

    //애노테이션 생성
    @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @Documented
    @Qualifier("mainDiscountPolicy") //이때 여기서도 추가 구분자를 지정한다.
    public @interface MainDiscountPolicy {
    }
    //애노테이션 적용
    @Component
    @MainDiscountPolicy
    public class RateDiscountPolicy implements DiscountPolicy{}
     //주입 시에 등록한 애노테이션 적어주기
    @Component
    public class OrderServiceImpl implements OrderService {
    
       private final DiscountPolicy rateDiscountPolicy;
    
       @Autowired
       public OrderServiceImpl(@MainDiscountPolicy DiscountPolicy discountPolicy) {
           this.discountPolicy = discountPolicy;
       }
    }

5-3. @Primary

  • @Primary는 우선순위를 정하는 방법이다. 의존관계 주입시에 여러 빈이 매칭되면 @Primary가 우선권을 갖는다.

    @Component
    @Primary
    public class RateDiscountPolicy implements DiscountPolicy{}
    @Component
    public class FixDiscountPolicy implements DiscountPolicy {}
    @Component
    public class OrderServiceImpl implements OrderService {
    
       private final DiscountPolicy discountPolicy;
    
       @Autowired
       public OrderServiceImpl(DiscountPolicy discountPolicy) {
           this.discountPolicy = discountPolicy; //@Primary가 붙은 RateDiscountPolicy가 주입된다.
       }

@Primary, @Qualifier

  • 활용
    자주 사용하는 메인 데이터베이스의 커넥션을 획득하는 스프링 빈이 있고,
    특별한 기능으로 가끔 사용하는 서브 데이터베이스의 커넥션을 획득하는 스프링 빈이 있는 경우, 메인 데이터베이스의 커넥션을 획득하는 스프링 빈은 @Primary를 적용해서 조회하는 곳에 @Qualifier 지정 없이 편리하게 조회하고, 서브 데이터베이스의 커넥션을 획득할 때는 @Qualifier를 지정해서 명시적으로 획득하는 방식으로 사용하면 코드를 깔끔하게 유지할 수 있다.
  • 우선순위
    @Primary는 기본값처럼 동작하고 @Qualifier는 매우 상세하게 동작한다.
    스프링은 자동보다 수동이, 넓은 범위의 선택권 보다는 좁은 범위의 선택권이 우선순위가 높다. 따라서 여기서도 @Qualifier가 우선순위가 높다.

6. 조회한 빈이 모두 필요할 때

  • 의도적으로 해당 타입의 스프링 빈이 다 필요한 경우도 있다.
    예를 들어 할인 서비스를 제공하는데, 클라이어트가 할인 정책(rate, fix)을 선택할 수 있는 경우, RateDiscountPolicyFixDiscountPolict 둘 다 스프링 빈에 등록되어야 한다.
public class AllBeanTest {

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

        DiscountService discountService = ac.getBean(DiscountService.class);

        Member member = new Member(1L, "memberA", Grade.VIP);
        
        int fixDiscountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");
        Assertions.assertThat(fixDiscountPrice).isEqualTo(1000);
        
        int rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
        Assertions.assertThat(rateDiscountPrice).isEqualTo(2000);
    }

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

        public int discount(Member member, int price, String discountCode) {
            DiscountPolicy discountPolicy = policyMap.get(discountCode);
            return discountPolicy.discount(member, price);
        }
    }
}
  • DiscountService가 스프링 빈으로 등록될 때 생성자가 호출되고,
    이때 DiscountPolicy가 주입되면서 Map과 List에 모든 할인 정책(FixDiscountPolicy, RateDiscountPolicy)이 주입된다.

  • discount 메소드는 discountCode로 "fixDiscountPolicy" 또는 "rateDiscountPolicy"가 넘어오면 map에서 해당 이름의 스프링 빈을 찾아서 실행한다.

  • 이렇게 동적으로 빈을 선택해야 하는 경우, Map 또는 List를 통해 스프링 빈을 받으면, 상황에 따라 빈을 찾아 사용하기 좋다.


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

  • 편리한 자동 등록을 기본으로 사용하자
    스프링은 @Component 뿐만 아니라 @Controller, @Service, @Repository처럼 계층에 맞춰 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원한다. 게다가 스프링 부트는 컴포넌트 스캔을 기본으로 사용하고, 스프링 부트의 다양한 스프링 빈들도 조건이 맞으면 자동으로 등록하도록 설계되어 있다.

    설정 정보를 기반으로 애플리케이션을 구성하는 부분과 동작하는 부분을 명확하게 나누는 것이 이상적이지만 일일이 설정 정보 클래스를 정의하고, @Bean을 적고, 객체를 생성하고, 주입할 대상을 적어주는 과정은 상당히 번거롭다. 그리고 관리할 빈이 많아지면 설정 정보를 관리하는 것 자체가 부담이 된다.

    따라서 편리한 자동 등록을 기본으로 사용하자. 자동 등록을 사용해도 OCP, DIP를 지킬 수 있다.

  • 직접 등록하는 기술 지원 객체는 수동 등록
    애플리케이션은 크게 업무 로직과 기술 지원 로직으로 나눌 수 있다.

    업무 로직 빈: 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리 등이 모두 업무 로직이다.

    기술 지원 빈: 기술적인 문제나 공통 관시사(AOP)를 처리할 때 주로 사용된다. 데이터베이스 연결이나, 공통 로그 처리처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.

    기술 지원 로직은 업무 로직에 비해 그 수가 매우 적고, 보통 애플리케이션 전반에 걸쳐서 광범위하게 영향을 미친다. 그리고 기술 지원 로직이 적용이 잘 되고 있는지 아닌지 파악하기 어려운 경우가 많다. 따라서 이런 기술 지원 로직들은 가급적 수동 빈 등록을 사용해서 명확하게 들어내는 게 좋다.

  • 다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해보자
    DiscountService가 자동 의존관계 주입으로 Map<String, DiscountPolicy>에 주입 받는 상황을 생각해보자. 이때 여기에 어떤 빈들이 주입될지 코드만 보고 한 번에 파악하기 힘들다. 자동 등록을 사용하고 있기 때문에 파악하려면 여러 코드를 찾아봐야 한다.

    이런 경우 설정 정보를 만들고 수동으로 빈을 등록하면 한 눈에 파악하기 좋다.

    @Configuration
    public class DiscountPolicyConfig {
    	
       @Bean
       public DiscountPolicy rateDiscountPolicy() {
       	return new RateDiscountPolicy();
       }
       
       @Bean
       public DiscountPolicy fixDiscountPolicy() {
       	return new FixDiscountPolicy();
       }
    }

    또는 자동 등록을 할 경우에는, DiscountPolicy의 구현 빈들만 따로 모아서 특정 패키지에 함께 묶어두는 것이 좋다.

profile
공부한 것들을 기록합니다.

0개의 댓글