6 분 소요

스프링 복습 및 정리 3P

컴포넌트 스캔과 의존성 자동 주입

@Configuration과 @Bean을 사용한 수동 주입 방식

@Configuration
public class AppConfigurer {

    @Bean
    public Menu menu() {
        return new Menu(productRepository());
    }

    @Bean
    public ProductRepository productRepository() {
        return new ProductRepository();
    }

    @Bean
    public Cart cart() {
        return new Cart(productRepository(), menu());
    }

    @Bean
    public Order order() {
        return new Order(cart(), discount());
    }

    @Bean
    public Discount discount() {
        return new Discount(new DiscountCondition[] {
                new CozDiscountCondition(new FixedRateDiscountPolicy()),
                new KidDiscountCondition(new FixedAmountDiscountPolicy())
        });
    }
}

위 코드처럼 수동으로 개발자가 직접 의존 관계를 설정해 주는 일은 분명 직관적이고 유용하지만,

또 다른 한편에서 굉장히 번거롭기도 하다.

이러한 번거로움을 해결하기 위해, 스프링 프레임워크는 수동으로 클래스 구성 정보를 일일이 작성하지 않고,

자동으로 스프링 빈을 등록하는 컴포넌트 스캔(Component Scan) 기능을 지원한다.

컴포넌트 스캔만으로는 앞에서 봤던 것과 같은 구체적인 의존 관계 설정이 불가능하기 때문에

@Autowired 어노테이션을 통해 빈을 자동으로 등록함과 동시에 의존 관계가 설정된다.

사용 예시

package com.codestates.burgerqueenspring;

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

@Configuration
@ComponentScan
public class TestConfigurer {

}

@ComponentScan 이라는 이름의 새로운 애너테이션이 클래스 레벨에 붙여져 있다.

ComponentScan은 이름처럼 @Component 어노테이션이 붙은 클래스를 모두 스캔하여 자동으로 스프링 빈으로 등록한다.

위 예시에서 스캔 범위는 burgerqueenspring 디렉토리 전체이다.

만약 범위를 변경하고 싶다면, @ComponentScan(basePackages = “스캔을 할 패키지”) 방법으로 스캔의 범위 지정을 바꿀 수 있다.

@Component@Autowired 어노테이션을 사용하여 자동으로 스프링 빈 등록 및 의존 관계 설정이 되도록 작업해 보겠다.

프로젝트 구조

com.example.myapp
|-- MyApplication.java
|-- controllers
|   |-- MyController.java
|-- services
|   |-- MyService.java

컨트롤러 클래스

package com.example.myapp.controllers;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import com.example.myapp.services.MyService;

@Controller
public class MyController {

    private final MyService myService;

    @Autowired
    public MyController(MyService myService) {
        this.myService = myService;
    }

    public void doSomething() {
        myService.doServiceLogic();
    }
}

서비스 클래스

package com.example.myapp.services;

import org.springframework.stereotype.Service;

@Service
public class MyService {

    public void doServiceLogic() {
        System.out.println("Service logic executed.");
    }
}

애플리케이션 메인 클래스

package com.example.myapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan(basePackages = "com.example.myapp") // 컴포넌트 스캔을 할 패키지 설정
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);

        // 컨트롤러 실행
        MyController myController = new MyController(null);
        myController.doSomething();
    }
}

위 코드에서 @ComponentScan 어노테이션은 "com.example.myapp" 패키지와

하위 패키지에서 @Component 어노테이션이 붙은 클래스들을 스캔하여 스프링 빈으로 등록한다.

MyController 클래스에서는 @Autowired 어노테이션을 사용하여 MyService 빈을 주입받고, doSomething 메서드를 실행할 때 MyService의 로직을 호출한다.

이렇게 설정하면 MyControllerMyService가 스프링 컨테이너에서 관리되며, 의존성 주입이 자동으로 이루어진다.

참고로 생성자가 단 하나만 존재하는 경우에는 @Autowired 어노테이션을 붙이지 않아도 자동으로 의존 관계가 연결된다.

@Service
public class DiscountService {

    private final DiscountPolicy discountPolicy;

    // 생성자가 하나만 존재하는 경우, @Autowired 어노테이션을 사용하지 않아도 됨
    public DiscountService(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
    
    // ...
}

마지막으로, @Component 어노테이션만 컴포넌트 스캔의 기본 타깃에 들어가는 것은 아니다.

@Configuration, @Controller, @Service, @Repository 등의 어노테이션도 컴포넌트 스캔의 대상에 포함된다.

@Autowired

@Component
public class Discount {
    private DiscountCondition[] discountConditions;

    public Discount(DiscountCondition[] discountConditions) {
        this.discountConditions = discountConditions;
    }

    --- 생략 ---
}
@Component
public class CozDiscountCondition implements DiscountCondition {

    --- 생략 ---
}
@Component
public class KidDiscountCondition implements DiscountCondition {
    
	--- 생략 ---
}
@Component
public class FixedAmountDiscountPolicy implements DiscountPolicy {

  --- 생략 ---
}
@Component
public class FixedRateDiscountPolicy implements DiscountPolicy {

    --- 생략 ---
}

위 클래스들에 @Component 어노테이션을 붙이고 프로그램을 실행해 보면 에러 메시지가 뜬다.

NoUniqueBeanDefinitionException 에러

No qualifying bean of type 'com.codestates.burgerqueenspring.discount.discountPolicy.DiscountPolicy' available: expected single matching bean but found 2: fixedAmountDiscountPolicy,fixedRateDiscountPolicy

하나의 빈이 매칭될 것이 예상되었는데 두 개의 빈이 발견되었다고 한다.

CozDiscountCondition 또는 KidDiscountCondition 클래스의 입장에서 보면, DiscountPolicy 타입의 객체만 주입되면 아무런 문제가 없는데,

들어올 수 있는 선택지가 두 가지가 되어 어떤 구현 객체가 들어와야 할지 스프링에 입장에서는 알 방도가 전혀 없기 때문이다.

만약 FixedAmountDiscountPolicy 또는 FixedRateDiscountPolicy 클래스 중에 하나만 @Component를 붙인다면,

프로그램은 잘 실행되겠지만 모든 할인에 둘 중에 하나의 정책이 적용되는 문제가 발생한다.

이 문제를 해결할 수 있는 방법으로 스프링은 크게 3가지의 해결 방법을 제공한다.

  1. @Autowired 필드명 매칭
  2. @Qualifier 사용
  3. @Primary 사용

@Autowired 필드명 매칭

@Autowired는 먼저 타입으로 빈을 조회하고, 만약 2개 이상의 여러 개의 빈이 있는 경우에 필드명 또는 매개변수명으로 빈을 매칭한다.

@Component
public class CozDiscountCondition implements DiscountCondition {

    private boolean isSatisfied;

    @Autowired
    private DiscountPolicy fixedRateDiscountPolicy;

//    public CozDiscountCondition(DiscountPolicy discountPolicy) {
//        this.fixedRatediscountPolicy = discountPolicy;
//    }

    --- 생략 --- 

    // 필드명 변경 
    public int applyDiscount(int price) {
        return fixedRateDiscountPolicy.calculateDiscountedPrice(price);
    }
}

먼저 기존의 생성자를 주석처리 한 후, 필드(참조변수)의 이름을 discountPolicyfixedRateDiscountPolicy로 바꾸었다.

KidDiscountPolicy에도 동일하게 필드명을 fixedAmountDiscountPolicy로 변경 후에 프로그램을 동작시켜 보면,

이전과 같이 프로그램이 잘 작동하는 모습을 확인할 수 있다.

@Qualifier 사용

추가적인 구분자를 통해 의존 관계를 연결하는 방식이다.

@Component
@Qualifier("fixedAmount")
public class FixedAmountDiscountPolicy implements DiscountPolicy {
	
	--- 생략 ---

}
@Component
@Qualifier("fixedRate")
public class FixedRateDiscountPolicy implements DiscountPolicy {

    --- 생략 ---

}
@Component
public class CozDiscountCondition implements DiscountCondition {

    private boolean isSatisfied;
    private DiscountPolicy discountPolicy;

    public CozDiscountCondition(@Qualifier("fixedRate") DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }

    --- 생략 ---
}
@Component
public class KidDiscountCondition implements DiscountCondition {
    private boolean isSatisfied;

    private DiscountPolicy discountPolicy;

    public KidDiscountCondition(@Qualifier("fixedAmount") DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }

    --- 생략 ---
}

@Qualifier 애너테이션은, 먼저 @Qualifier이 붙여진 추가 구분자를 통해 매칭되는 빈이 있는지 탐색하고, 매칭되는 빈이 없다면 빈의 이름으로 조회를 진행한다.

참고로 어노테이션을 직접 커스터마이징하여 사용할 수 있는 방법도 존재한다.

@Primary 사용

가장 빈번하게 사용되는 방식인 @Primary 애너테이션을 사용하여 여러 개의 빈이 들어올 수 있는 경우 빈 객체들 간 우선순위를 설정해 줄 수 있다.

@Component
@Primary
public class FixedRateDiscountPolicy implements DiscountPolicy {

    private int discountRate = 10;

    public int calculateDiscountedPrice(int price) {
        return price - (price * discountRate / 100);
    }
}

CozDiscountCondition와 KidDiscountCondition 클래스의 입장에서 같은 타입의 여러 개의 빈이 조회되는 경우 우선순위를 가지는 FixedRateDiscountPolicy가 우선적으로 의존성 주입이 된다.

출력 화면

새우버거를 하나 주문한 후에, 두 가지 할인 정책이 모두 적용되도록 값을 입력해 보면, 그 결과 값이 모두 정률할인으로 적용되어 나오고 있음을 확인할 수 있다.

이런 경우 @Qualifier를 함께 사용하여 원하는 결과를 얻어낼 수 있다.

@Component
public class KidDiscountCondition implements DiscountCondition {
    private boolean isSatisfied;

    private DiscountPolicy discountPolicy;

    public KidDiscountCondition(@Qualifier("fixedAmount") DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }

    --- 생략 ---
}
@Component
@Qualifier("fixedAmount")
public class FixedAmountDiscountPolicy implements DiscountPolicy {

    private int discountAmount = 500;

    --- 생략 ---
}

이제 다시 프로그램을 동작시켜 보면 이전과 같이 잘 작동하는 모습을 확인하실 수 있다.

이처럼 빈번하게 사용되는 인스턴스를 @Primary로 해두고 상대적으로 사용 빈도가 적은 인스턴스를 @Qualifier로 지정하여 상황에 맞게 변용하여 사용할 수 있다.

Spring Boot 핵심 정리 모음

1P - POJO, IoC, DI, AOP, PSA >

1.1P - DI / 의존성 주입은 왜 필요한가? >

2P - 빈(Bean) 생명주기와 범위 >

3P - 컴포넌트 스캔과 의존성 자동 주입, @Autowired >

4P - AOP (관심 지향 프로그래밍) >

5P - Spring MVC / MVC의 동작 방식과 구성 요소 >

6P - REST, REST API >

7P - HTTP Header, Rest Client >

8P - DTO, DTO 유효성 검증 >

9P - Service / Entity 클래스, Mapper / MapStruct >

10P - 예외 처리 (@ExceptionHandler, @RestControllerAdvice) >

10.1P - 비즈니스 로직에 대한 예외 처리 >

11P - JDBC 데이터 액세스 계층 >

11.1P - DDD, 도메인 엔티티 및 테이블 설계 >

11.2P - 데이터 액세스 계층 구현 (도메인 엔티티 클래스 정의) >

11.3P - 데이터 액세스 계층 구현 (서비스, 리포지토리 구현) >

12P - JPA(Java Persistence API) >

12.1P - JPA 엔티티 매핑 >

12.2P - JPA 엔티티 간의 연관 관계 매핑 >

12.3P - Spring Data JPA 데이터 액세스 계층 구현 >

13P - Spring MVC 트랜잭션 >

13.1P - 선언형 방식의 트랜잭션 적용 >

13.2P - JTA를 이용한 분산 트랜잭션 적용 >

14P - Spring MVC Testing 단위 테스트 >

14.1P - JUnit 단위 테스트 >

14.2P - Hamcrest Assertion >

14.3P - 슬라이스 테스트 (API, 데이터 액세스 계층) >

14.4P - Mockito >

14.5P - TDD (Test Driven Development) >

15P - API 문서화 (Documentation) >

15.1P - API 문서 생성, Asciidoc, Asciidoctor >

16P - 애플리케이션 빌드 / 실행 / 배포 >

17P - Spring Security >

17.1P - Spring Security 2 >

18P - JWT (JSON Web Token) >

댓글남기기