본문 바로가기

Spring/스프링 핵심 원리 - 기본

07. 의존관계 자동 주입

💡 본 게시글은 김영한님의 인프런(Inflearn) 강의 스프링 핵심 원리 - 기본편에 대해 공부하고, 정리한 내용입니다.

 


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

  • 의존관계 주입은 주로 4가지 방법을 통해 이루어집니다. 의존 관계 주입은 생성자 주입, 수정자 주입, 필드 주입, 일반 메서드 주입이 있습니다. 

1) 생성자 주입(Constructor Injection)

(1) 정의

  • 생성자 주입은 클래스의 생성자를 통해 해당 클래스가 의존하는 다른 객체들을 주입받는 방식입니다.
  • 이 방식은 객체 생성 시 필요한 의존성을 모두 받아오므로, 객체가 생성되고 나서는 변경되지 않는 불변 상태를 유지합니다.

(2) 특징

  • 단 한 번의 호출 보장: 생성자는 객체 생성 시 단 한 번만 호출되므로, 의존 관계도 한 번만 설정됩니다.
  • 불변성과 필수 의존성: 생성자를 통해 의존 관계를 주입받기 때문에, 이후에는 해당 의존성이 변경될 수 없습다. 이는 객체의 불변성을 보장합니다. 또한, 필수적으로 필요한 의존성들만을 관리할 수 있습니다.

(3) 사용 예시

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

(4) 주의사항

  • `@Autowired` 생략 가능: 생성자가 클래스 내에 단 하나만 있을 경우, 스프링은 `@Autowired` 어노테이션을 생략해도 자동으로 의존성을 주입합니다. 이는 해당 클래스가 스프링 빈으로 등록되어 있을 때에만 적용됩니다.
@Component
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

(5) 장점

  • 명시적인 의존성 전달: 생성자를 통해 필요한 의존성이 무엇인지 명확하게 드러낸다.
  • 불변성 보장: 생성 후 의존성이 변경되지 않아, 객체가 안정적인 상태를 유지한다.
  • 테스트 용이성: 생성자 주입을 사용하면, 테스트 코드 작성 시 필요한 의존성을 쉽게 주입할 수 있다.

(6) 단점

  • 의존성이 많을 경우 복잡성 증가: 생성자에 많은 매개변수가 필요할 경우, 코드가 복잡해지고 관리하기 어려워질 수 있다.

2) 수정자 주입(Setter Injection)

(1) 정의

  • 수정자 주입은 setter 메서드를 이용하여 클래스의 의존성을 주입하는 방법입니다.
  • 이 방식은 클래스 내부에 setter 메서드를 정의하고, 스프링이 이 메서드를 호출하여 의존성을 주입합니다.

(2) 특징

  • 선택적 의존성 주입: 수정자 주입은 선택적이거나 변경 가능성이 있는 의존 관계에 적합합니다. 즉, 필수적이지 않은 의존성에 주로 사용됩니다.
  • 자바빈 프로퍼티 규약: 수정자 메서드는 자바빈 프로퍼티 규약에 따라 'set'이라는 접두어를 사용합니다. 이 규약은 필드 값을 직접 변경하지 않고, 메서드를 통해 값을 읽거나 수정하는 방식을 의미합니다.

(3) 사용 예시

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


(4) 주의사항

  • @Autowired`의 필수 여부: `@Autowired`는 기본적으로 주입할 대상이 없으면 오류를 발생시킵니다. 필수적이지 않은 의존성에 대해서는 `@Autowired(required = false)`로 설정하여, 해당 의존성이 없어도 오류 없이 동작하도록 할 수 있습니다.

(5) 장점

  • 유연한 의존성 관리: 수정자 주입을 사용하면, 의존성을 선택적으로 주입할 수 있으며, 런타임 시에도 의존성을 변경할 수 있습니다.
  • 낮은 결합도: 필수적이지 않은 의존성을 관리할 때 유용하며, 클래스 간의 결합도를 낮출 수 있습니다.

(6) 단점

  • 변경 가능한 상태: 객체가 생성된 후에도 의존성이 변경될 수 있어, 객체의 상태가 변경될 수 있습니다. 이는 불변성을 보장하지 않는다는 단점이 될 수 있습니다.
  • 누락된 의존성: 필수 의존성이 누락되었을 때, 오류가 발생하지 않거나 런타임 시점까지 문제가 드러나지 않을 수 있습니다.

(7) 자바빈 프로퍼티 규약 예시

class Data {
    private int age;

    public void setAge(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }
}

3) 필드 주입(Field Injection)

(1) 정의

  • 필드 주입은 클래스의 필드에 직접 의존성을 주입하는 방식입니다.
  • 스프링의 `@Autowired` 어노테이션을 필드에 적용하여 의존성을 자동으로 주입받습니다.

(2) 기본적인 특징

  • 간결성: 코드가 매우 간결하며, 의존성 주입을 위한 별도의 설정이 필요 없습니다.
  • 테스트의 어려움: 필드 주입은 외부에서 변경이 불가능하기 때문에, 테스트가 어렵습니다. 특히, 순수 자바 환경에서는 `@Autowired`가 작동하지 않아 테스트에 제약이 있습니다.
  • DI 프레임워크 의존성: 필드 주입은 DI(Dependency Injection) 프레임워크 없이는 사용할 수 없습니다. 즉, 스프링과 같은 컨테이너가 필요합니다.

(3) 사용 예시

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

(4) 주의사항

  • 필드 주입은 매우 간편하지만, 그로 인해 발생하는 단점이 있습니다. 특히, 애플리케이션 코드와는 무관한 테스트 코드 작성이 어렵습니다.
  • 스프링 설정을 목적으로 하는 `@Configuration`과 같은 특수한 경우에 한하여 사용하는 것이 좋습니다.

(5) 스프링 테스트와의 관계

  • 순수한 자바 테스트 코드에서는 `@Autowired`가 동작하지 않습니다.
  • `@SpringBootTest`와 같이 스프링 컨테이너를 테스트에 통합한 경우에만 필드 주입이 가능합니다.

(6) 수동 빈 등록과의 관계

  • `@Bean` 어노테이션을 사용한 수동 빈 등록에서는 파라미터에 대한 의존성이 자동으로 주입됩니다.
  • 이 방식을 사용하면 수동으로 등록한 빈에서도 필요한 의존성을 주입받을 수 있습니다.

(7) 수동 빈 등록 예시

@Bean
OrderService orderService(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
    return new OrderServiceImpl(memberRepository, discountPolicy);
}

4) 일반 메서드 주입(Method Injection)

(1) 정의

  • 일반 메서드 주입은 클래스 내의 일반 메서드를 사용하여 의존성을 주입하는 방식입니다.
  • 스프링의 @Autowired 어노테이션을 특정 메서드에 적용하여, 해당 메서드를 통해 필요한 의존성들을 주입받습니다.

(2) 특징

  • 여러 필드 동시 주입: 한 메서드에서 여러 개의 파라미터를 받을 수 있기 때문에, 여러 필드에 대한 의존성을 동시에 주입할 수 있습니다.
  • 사용 빈도: 일반적으로 많이 사용되지 않는 방법입니다. 다른 주입 방법(예: 생성자 주입, 수정자 주입)이 더 선호됩니다.

(3) 사용 예시

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

(4) 스프링 컨테이너와의 관계

  • 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈에만 적용됩니다.
  • 스프링 빈이 아닌 일반 클래스에서는 @Autowired 어노테이션을 사용해도 아무런 기능이 동작하지 않습니다.

(5) 장점

  • 유연한 의존성 관리: 하나의 메서드에서 여러 의존성을 주입받을 수 있어 유연한 설정이 가능합니다.
  • 명시적인 의존성 설정: 특정 메서드에서 필요한 의존성을 명시적으로 설정할 수 있습니다.

(6) 단점

  • 사용 빈도 제한: 일반 메서드 주입은 다른 주입 방식에 비해 덜 직관적이고 덜 일반적으로 사용되어, 특정 상황에서만 사용됩니다.
  • 코드 복잡성 증가: 여러 의존성을 한 메서드에 주입하면 메서드가 복잡해지고 의존성 관리가 어려워질 수 있습니다.

2. 옵션 처리 

  • 스프링 프레임워크에서는 의존성 주입 시 주입할 대상이 없는 경우에 대한 처리를 위해 다양한 옵션을 제공합니다. 이러한 옵션들은 의존성이 필수적이지 않은 상황에서 유용하게 사용됩니다.

1) 옵션 처리리의 필요성

  • 때때로, 주입할 스프링 빈이 존재하지 않아도 애플리케이션이 정상적으로 동작해야 할 필요가 있습니다.
  • `@Autowired`의 `required` 속성의 기본값은 `true`이며, 이 경우 자동 주입 대상이 없으면 오류가 발생합니다.

2) 옵션 처리 방법

  • @Autowired(required = false): 이 옵션을 사용하면 자동 주입할 대상이 없는 경우, 수정자 메서드가 호출되지 않습니다.
  • @Nullable 어노테이션: 이 어노테이션을 사용하면 자동 주입할 대상이 없을 경우, null 값이 주입됩니다.
  • Optional<>: 이 옵션을 사용하면 자동 주입할 대상이 없는 경우, Optional.empty가 주입됩니다.

3) 예제 코드

// 자동 주입 대상이 없을 때 호출되지 않음
@Autowired(required = false)
public void setNoBean1(Member member) {
    System.out.println("setNoBean1 = " + member);
}

// 자동 주입 대상이 없을 때 null 호출
@Autowired
public void setNoBean2(@Nullable Member member) {
    System.out.println("setNoBean2 = " + member);
}

// 자동 주입 대상이 없을 때 Optional.empty 호출
@Autowired(required = false)
public void setNoBean3(Optional<Member> member) {
    System.out.println("setNoBean3 = " + member);
}

4) 실제 사용 예

  • 이 예제에서 Member 클래스는 스프링 빈이 아닙니다.
  • setNoBean1()은 @Autowired(required = false)가 설정되어 있으므로, 주입 대상이 없을 때 메서드 호출 자체가 이루어지지 않습니다.
  • 출력 결과는 다음과 같습니다:
setNoBean2 = null
setNoBean3 = Optional.empty

5)`@Nullable`과 `Optional`의 범용성

  • @Nullable과 Optional은 스프링 프레임워크 전반에 걸쳐 지원됩니다.
  • 이들은 생성자 자동 주입에서도 특정 필드에만 사용될 수 있으며, 유연한 의존성 관리를 가능하게 합니다.

3. 생성자 주입을 선택하라! 

  • 과거에는 수정자 주입과 필드 주입을 많이 사용했지만, 최근에는 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장합니다다. 그 이유는 다음과 같습니다.

1) 생성자 주입(Constructor Injection)을 선택하는 이유

(1) 불변성(Immutability)

  • 애플리케이션의 실행 동안 대부분의 의존관계는 변경되지 않아야 합니다. 생성자 주입을 사용하면 한 번 설정된 의존관계는 변경될 수 없어 불변성을 유지할 수 있습니다.
  • 수정자 주입(setter injection)을 사용하면 setXxx 메서드가 public으로 공개되어야 하며, 이는 실수로 변경될 수 있는 위험이 있습니다.
@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;
    }
}

(2) 의존성 누락 방지

  • 순수한 자바 코드에서 단위 테스트를 수행할 때, 생성자 주입을 사용하면 의존성 누락 시 컴파일 오류를 통해 즉시 알 수 있습니다.
  • 반면, 수정자 주입을 사용하면 의존관계가 누락되었을 때 Null Pointer Exception(NPE)과 같은 런타임 오류가 발생할 위험이 있습니다.
@Test
void createOrder() {
    // 컴파일 오류 발생
    OrderServiceImpl orderService = new OrderServiceImpl();
    orderService.createOrder(1L, "itemA", 10000);
}

(3) final 키워드 사용

  • 생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있습니다. 이는 필드가 초기화되지 않는 오류를 컴파일 시점에 잡아낼 수 있게 해줍니다.
  • 수정자 주입이나 필드 주입을 사용할 경우, 생성자 이후에 호출되므로 final 키워드 사용이 불가능합니다.
@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;
    }
}

(4) 프레임워크 의존성 감소

  • 생성자 주입은 프레임워크에 의존하지 않고 순수한 자바 언어의 특성을 활용하는 방법입니다.
  • 프레임워크 없이도 의존성 주입이 필요한 컴포넌트를 쉽게 테스트할 수 있습니다.

(5) 사용 방법의 유연성

  • 기본적으로는 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 사용할 수 있습니다.
  • 생성자 주입과 수정자 주입을 동시에 사용하는 것도 가능합니다.

2)  결론 및 권장 사항

  • 생성자 주입은 불변성, 의존성 누락 방지, final 키워드 사용의 이점을 제공합니다.
  • 프레임워크에 대한 의존성을 줄이고, 순수 자바의 특성을 잘 활용하는 가장 좋은 방법입니다.
  • 항상 생성자 주입을 기본으로 사용하고, 필요에 따라 수정자 주입을 선택하세요. 필드 주입은 가급적 피하는 것이 좋습니다.

4. 롬복(Lombok)과 최신 개발 트렌드

1) 기본적인 필요성

  • 대부분의 개발 과정에서는 객체의 불변성을 유지하기 위해 final 키워드를 많이 사용합니다.
  • 이렇게 final 필드를 사용하면 생성자를 명시적으로 작성해야 하며, 필드에 값을 할당하는 코드도 필요합니다.

2) 롬복(Lombok) 라이브러리의 도입

  • 롬복은 자바의 애노테이션 프로세서를 활용하여 반복적인 코드 작성을 줄여줍니다.
  • 롬복의 @RequiredArgsConstructor 애노테이션을 사용하면 final이나 @NonNull 필드에 대한 생성자를 자동으로 생성해줍니다.

3) 롬복 적용 전후의 비교

  • 적용 전 코드:
@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;
    }
}

 

  • 롬복 적용 후 코드:
     
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
}

=>  이 코드에서 롬복은 컴파일 시점에 필요한 생성자를 자동으로 생성해줍니다.


4) 롬복의 편리함

  • 롬복을 사용하면 생성자 코드를 작성할 필요가 없어집니다. 이는 코드를 더욱 깔끔하고 관리하기 쉽게 만들어줍니다.
  • 실제 클래스 파일을 열어보면 롬복이 생성해준 생성자를 확인할 수 있습니다.

5) 최신 개발 트렌드

  • 최근 개발 트렌드는 @Autowired를 생략하고 생성자를 하나만 두는 방식을 선호합니다.
  • 롬복의 @RequiredArgsConstructor를 사용하면 이 방식을 더욱 효율적으로 구현할 수 있습니다.

6) 롬복 라이브러리 적용 방법

(1) build.gradle 설정

  • 먼저, 프로젝트의 build.gradle 파일에 롬복 라이브러리와 관련 설정을 추가해야 합니다.
  • 다음은 build.gradle 파일에 롬복을 설정하는 예시입니다:
plugins {
    id 'org.springframework.boot' version '2.3.2.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id 'java'
}
group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

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

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    // 롬복 라이브러리 추가 시작
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testCompileOnly 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
    // 롬복 라이브러리 추가 끝

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

test {
    useJUnitPlatform()
}

 


(2) IDE 설정

  • 롬복은 IDE의 플러그인을 통해 작동합니다. 사용하는 IDE에 따라 롬복 플러그인을 설치해야 합니다.
  • 예를 들어, IntelliJ IDEA에서는 다음과 같이 설치합니다:
    1. Preferences (Windows에서는 File > Settings)에서 Plugins를 선택합니다.
    2. 플러그인 마켓에서 Lombok을 검색하여 설치합니다.
    3. IDE를 재시작합니다.

(3) 애노테이션 프로세서 활성화

  • 롬복은 애노테이션 프로세서를 사용하므로, IDE에서 이 기능을 활성화해야 합니다.
  • IntelliJ IDEA의 경우:
    1. Preferences에서 Annotation Processors를 검색합니다.
    2. Enable annotation processing 옵션을 체크합니다.
    3. IDE를 재시작합니다.

(4) 롬복 기능 테스트

  • 롬복 설치가 완료된 후, @Getter, @Setter와 같은 롬복 애노테이션을 테스트 클래스에 적용하여 테스트합니다.
  • 예를 들어, 클래스에 @Getter와 @Setter를 추가하고, 해당 메서드들이 정상적으로 작동하는지 확인합니다.

5. 조회 빈이 2개 이상일 때의 문제

1) 문제 상황

(1) 기본적인 동작 원리

  • @Autowired는 타입(Type)으로 빈을 조회합니다.
  • 예를 들어, DiscountPolicy 타입으로 주입받으려고 하면, 스프링은 DiscountPolicy 타입의 빈을 컨테이너에서 찾습니다.
  • 이는 ac.getBean(DiscountPolicy.class)와 유사한 방식으로 작동합니다.

(2) 문제 발생 상황

  • 만약 DiscountPolicy 타입의 빈이 두 개 이상 존재하는 경우 (FixDiscountPolicy와 RateDiscountPolicy), 스프링은 어떤 빈을 주입해야 할지 결정할 수 없습니다.
  • 예시 코드:
@Component
public class FixDiscountPolicy implements DiscountPolicy {}

@Component
public class RateDiscountPolicy implements DiscountPolicy {}

 


(3) 결과적인 오류

  • DiscountPolicy 타입의 빈을 자동 주입하려고 할 때, 스프링은 NoUniqueBeanDefinitionException 오류를 발생시킵니다.
  • 오류 메시지는 fixDiscountPolicy, rateDiscountPolicy 두 개의 빈을 찾았다고 알려줍니다.

2) 해결 방법

(1) 하위 타입 지정

  • 하위 타입을 직접 지정하여 의존성을 주입받을 수 있지만, 이는 DIP(의존관계 역전 원칙)를 위배하고 유연성을 저하시킵니다.

(2) 주요 해결 방법

  • 스프링은 이러한 문제를 해결하기 위해 몇 가지 방법을 제공합니다:
    • @Primary 사용: 가장 우선순위를 가지는 빈을 지정할 수 있습니다.
    • @Qualifier 사용: 특정 빈을 명시적으로 지정할 수 있습니다.
    • 리스트, 맵 사용: 해당 타입의 모든 빈을 주입받을 수 있습니다.
    • 자체 정의 어노테이션 사용: @Qualifier의 보다 명시적인 대체 방법입니다.
    • 수동 빈 등록: 필요한 경우, 특정 빈을 수동으로 등록하여 문제를 해결할 수 있습니다.

3) 정리

  • @Autowired는 타입으로 스프링 빈을 조회합니다. 동일한 타입의 빈이 여러 개 있을 경우 NoUniqueBeanDefinitionException 오류가 발생할 수 있습니다.
  • 스프링은 @Primary, @Qualifier, 리스트/맵 주입, 자체 정의 어노테이션, 수동 빈 등록 등 여러 해결 방법을 제공합니다.
  • 이러한 방법들을 통해 개발자는 다양한 상황에서 유연하게 의존성 주입을 처리할 수 있습니다.

6. @Autowired 필드 명, @Qualifier, @Primary

  • @Autowired를 사용할 때 동일한 타입의 스프링 빈이 여러 개 있을 경우 발생하는 문제를 해결하는 세 가지 주요 방법에 대해 설명드리겠습니다. 이들은 @Autowired 필드 명 매칭, @Qualifier 사용, 그리고 @Primary 어노테이션 사용 방법입니다.

1) @Autowired 필드 명 매칭

(1) 타입 매칭 기본 원칙

  • @Autowired는 먼저 타입으로 스프링 컨테이너에 등록된 빈을 찾습니다.
  • 예를 들어, DiscountPolicy 타입의 빈을 찾기 위해 타입 매칭을 시도합니다.

(2) 필드 명을 이용한 추가 매칭

  • 타입 매칭 결과가 두 개 이상일 경우 (즉, 동일 타입의 빈이 여러 개 존재할 경우), @Autowired는 필드 이름이나 파라미터 이름을 사용하여 추가적인 매칭을 시도합니다.
  • 이는 빈의 이름과 필드의 이름이 일치하는 경우 해당 빈을 주입하게 됩니다.

(3) 필드 명 매칭 예시

  • 기존 코드:
@Autowired
private DiscountPolicy discountPolicy;
  • 필드 명을 빈 이름으로 변경: 
  •  
@Autowired
private DiscountPolicy rateDiscountPolicy;
  • 이 경우, 필드 명 rateDiscountPolicy가 스프링 컨테이너에 등록된 RateDiscountPolicy 빈의 이름과 일치하므로, RateDiscountPolicy 빈이 정상적으로 주입됩니다.

(4) 매칭 순서 정리

  • 타입 매칭: 먼저 타입에 맞는 빈을 찾습니다.
  • 빈 이름 매칭: 타입 매칭 결과가 여러 개일 경우, 필드 또는 파라미터의 이름을 사용해 빈 이름과 매칭을 시도합니다.

2) @Qualifier 사용 방법

(1) 빈 등록 시 @Qualifier 사용

  • @Qualifier 애노테이션을 사용하여 스프링 빈에 추가 구분자를 제공합니다.
  • 이 구분자는 빈의 이름을 변경하는 것이 아니라, 주입할 빈을 구별하는 데 사용됩니다.
  • 예시:
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}

(2) 의존성 주입 시 @Qualifier 사용

  • 주입받을 대상에 @Qualifier를 붙이고, 등록했던 구분자를 명시합니다.
  • 생성자 또는 수정자(Setter) 방식에 모두 적용할 수 있습니다.
  • 예시:
// 생성자 자동 주입
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
                        @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}

// 수정자 자동 주입
@Autowired
public DiscountPolicy setDiscountPolicy(@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
    this.discountPolicy = discountPolicy;
}

 


(3) @Qualifier 미매칭 시 동작

  • @Qualifier("mainDiscountPolicy")와 같은 구분자가 없을 경우, 스프링은 mainDiscountPolicy라는 이름의 빈을 찾습니다.
  • 경험상, @Qualifier는 명시된 @Qualifier로 매칭하는 용도로만 사용하는 것이 더 명확하고 바람직합니다.

(4) 수동 빈 등록 시 @Qualifier 사용

  • @Bean 애노테이션을 사용하여 수동으로 빈을 등록할 때도 @Qualifier를 사용할 수 있습니다.
  • 예시:
@Bean
@Qualifier("mainDiscountPolicy")
public DiscountPolicy discountPolicy() {
    return new ...
}

(5) @Qualifier 정리

  1. @Qualifier끼리 매칭: @Qualifier 애노테이션이 붙은 빈을 우선적으로 매칭합니다.
  2. 빈 이름 매칭: @Qualifier로 지정된 이름의 빈을 찾습니다.
  3. 예외 발생: 적합한 @Qualifier 또는 빈 이름이 없으면 NoSuchBeanDefinitionException 예외가 발생합니다.

(6) 결론

  • @Qualifier는 스프링에서 같은 타입의 여러 빈 중에서 특정 빈을 주입받고 싶을 때 명확한 지시를 제공합니다.
  • 이를 통해 개발자는 의도한 대로 특정 빈을 주입받을 수 있으며, 의존성 주입 과정을 더욱 유연하고 정확하게 관리할 수 있습니다.

3) @Primary 사용

(1) 기본 원리

  • @Primary 애노테이션은 @Autowired와 같은 자동 주입 상황에서 여러 매칭 빈 중에서 우선적으로 사용될 빈을 지정합니다.
  • 이를 통해 개발자는 기본적으로 사용할 구현체를 명확하게 지정할 수 있습니다.
    1.  

(2) 사용 예시

  • RateDiscountPolicy를 기본 구현체로 지정하고자 할 때, @Primary 애노테이션을 사용합니다:
// 생성자 주입
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}
  • 이렇게 설정하면, DiscountPolicy 타입의 의존성 주입이 필요할 때 RateDiscountPolicy가 기본적으로 선택됩니다.

(3) 주입 코드

  • 생성자 또는 수정자 주입 방식에서 @Primary로 지정된 빈이 자동으로 주입됩니다:
// 생성자 주입
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}

(4) @Primary와 @Qualifier의 결합 사용

  • @Primary는 일반적인 경우에 사용하고, 특별한 경우에는 @Qualifier를 사용하여 빈을 명시적으로 지정할 수 있습니다.
  • 예를 들어, 메인 데이터베이스의 커넥션을 가져오는 빈에 @Primary를 사용하고, 서브 데이터베이스 커넥션 빈을 가져올 때는 @Qualifier를 명시적으로 지정하는 방식입니다.

(5) 우선순위

  • @Primary는 일종의 "기본값"으로 작동하며, @Qualifier는 더 구체적인 지정을 제공합니다.
  • @Qualifier가 지정된 빈은 @Primary보다 우선순위가 높습니다.

(6) 정리

  • @Primary는 자동 주입 시 여러 빈 중에서 기본적으로 선택되어야 할 빈을 지정하는 데 사용됩니다.
  • @Qualifier는 특정 상황에서 특별히 다른 빈을 선택하고자 할 때 사용되며, @Primary보다 우선순위가 높습니다.
  • 이 두 가지 방법을 적절히 결합하여 사용함으로써, 의존성 주입 과정을 보다 유연하고 명확하게 관리할 수 있습니다.

6. 애노테이션 직접 만들기

1) 문제점

(1) 타입 매칭 기본 원칙

  • @Qualifier("mainDiscountPolicy")처럼 문자열을 사용하면 컴파일 시 타입 체크가 이루어지지 않습니다. 이로 인해 오타나 잘못된 문자열 사용이 컴파일 시점에 발견되지 않을 수 있습니다.

(2) 사용자 정의 애노테이션 생성

  • 사용자 정의 애노테이션을 만들어 이 문제를 해결할 수 있습니다. 이를 통해 문자열 대신 애노테이션을 사용하여 타입 안전성을 확보하고, 코드의 명확성을 높일 수 있습니다.
  • 예시:
package hello.core.annotation;

import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}

(3) 사용자 정의 애노테이션 적용

  • 이 사용자 정의 애노테이션은 @Component, @Autowired 등과 함께 사용될 수 있습니다.
  • 예시:
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {}

// 생성자 자동 주입
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}

// 수정자 자동 주입
@Autowired
public DiscountPolicy setDiscountPolicy(@MainDiscountPolicy DiscountPolicy discountPolicy) {
    this.discountPolicy = discountPolicy;
}

(4) 애노테이션 조합 기능

  • 스프링은 여러 애노테이션을 조합하여 사용하는 기능을 지원합니다. 이는 @Qualifier뿐만 아니라 다른 애노테이션들과도 함께 사용될 수 있습니다.
  • 예를 들어, @Autowired와 같은 스프링 기본 애노테이션도 사용자 정의 애노테이션으로 재정의할 수 있습니다.

(5) 정리

  • 사용자 정의 애노테이션을 만들어 사용하면 타입 안전성을 확보하고, 코드의 가독성을 향상시킬 수 있습니다.
  • 이 방법은 스프링의 기능을 활용하여, @Qualifier와 같은 애노테이션의 사용성을 향상시키는 데 도움이 됩니다.
  • 하지만 스프링 기본 기능을 불필요하게 재정의하는 것은 유지보수에 혼란을 가중시킬 수 있으므로 신중하게 사용해야 합니다.

7. 조회한 빈이 모두 필요할 때: List, Map 사용

  • 조회한 빈이 모두 필요한 상황, 특히 전략 패턴을 구현할 때 스프링에서는 ListMap을 사용하여 여러 빈을 편리하게 관리할 수 있는 방법을 제공합니다. 이를 통해 특정 타입의 모든 스프링 빈을 주입받고, 상황에 맞게 적절한 빈을 사용할 수 있습니다.
 

1) 전략 패턴의 구현

  • 할인 정책(DiscountPolicy)과 같은 여러 전략이 존재하고, 이를 동적으로 선택하여 사용해야 하는 경우가 있습니다.
  • 예를 들어, 클라이언트가 'rate' 또는 'fix' 할인 정책을 선택할 수 있다고 가정하면, 각각에 해당하는 빈(RateDiscountPolicy, FixDiscountPolicy)을 사용해야 합니다.

 

2) DiscountService 예제 코드

  • DiscountService 클래스는 모든 DiscountPolicy를 Map과 List로 주입받습니다.
  • Map에서는 빈의 이름을 키로 하고, 해당하는 DiscountPolicy 객체를 값으로 가집니다.
  • List에는 해당 타입의 모든 빈이 담깁니다.
  • 주입된 DiscountPolicy 중 하나를 선택하여 discount() 메서드에서 사용합니다.
  • 예제 코드:
public 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;
    }

    public int discount(Member member, int price, String discountCode) {
        DiscountPolicy discountPolicy = policyMap.get(discountCode);
        return discountPolicy.discount(member, price);
    }
}

3) 주입 분석

  • Map<String, DiscountPolicy>: 키는 스프링 빈의 이름, 값은 해당 타입의 모든 스프링 빈을 담습니다.
  • List<DiscountPolicy>: 해당 타입의 모든 스프링 빈을 리스트로 담습니다.
  • 해당 타입의 스프링 빈이 없으면 비어 있는 컬렉션이나 맵을 주입합니다.

4) 스프링 컨테이너와 빈 등록

  • 스프링 컨테이너는 생성 시 클래스 정보를 받아 해당 클래스를 스프링 빈으로 자동 등록합니다.
  • new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class)을 통해 AutoAppConfig와 DiscountService를 스프링 빈으로 등록할 수 있습니다.

5) 정리

  • 스프링에서는 List나 Map을 사용하여 특정 타입의 모든 빈을 주입받고 관리할 수 있는 기능을 제공합니다.
  • 이 방식은 전략 패턴 구현과 같이 동적으로 빈을 선택해야 하는 상황에서 매우 유용합니다.
  • 스프링 컨테이너는 생성 시점에 클래스를 스프링 빈으로 자동 등록하는 기능을 제공하여, 여러 클래스를 편리하게 관리할 수 있게 해줍니다.

8. 자동, 수동의 올바른 실무 운영 기준 (다시 정리하기!)

  • 그러면 어떤 경우에 컴포넌트 스캔과 자동 주입을 사용하고, 어떤 경우에 설정 정보를 통해서 수동으로 빈을 등록하고, 의존관계도 수동으로 주입해야 할까?
  • 자동과 수동 빈 등록의 올바른 실무 운영 기준에 대해서 설명드리겠습니다. 스프링과 스프링 부트는 개발자에게 편리한 자동 빈 등록 기능을 제공하지만, 특정 상황에서는 수동으로 빈을 등록하는 것이 더 적합할 수 있습니다.

1) 편리한 자동 기능을 기본으로 사용하자

 

(1) 자동 기능의 선호 추세

  • 시간이 지남에 따라 스프링은 점점 더 자동 기능을 선호하는 추세입니다.
  • @Component, @Controller, @Service, @Repository 등을 사용하여 애플리케이션 로직을 자동으로 스캔하고 빈으로 등록할 수 있습니다.
  • 스프링 부트는 기본적으로 컴포넌트 스캔을 사용하며, 조건에 맞는 스프링 빈을 자동으로 등록합니다.

 

(2) 자동 빈 등록의 장점

  • 간편함: @Component 애노테이션만 추가하면 빈 등록이 간단히 처리됩니다.
  • 관리의 용이성: 설정 정보가 간결해지고, 빈을 관리하기가 더 쉬워집니다.
  • OCP, DIP 준수: 자동 빈 등록을 사용해도 개방-폐쇄 원칙(OCP)과 의존관계 역전 원칙(DIP)을 준수할 수 있습니다.

2) 수동 빈 등록의 사용

(1) 수동 빈 등록의 적용 상황

  • 업무 로직과 기술 지원 로직: 업무 로직(컨트롤러, 서비스, 리포지토리)은 자동 등록을 사용하며, 기술 지원 로직(데이터베이스 연결, 로그 처리 등)은 수동 등록을 고려합니다.
  • 명확성과 제어의 필요성: 애플리케이션 전반에 영향을 미치는 기술 지원 로직은 명확성을 위해 수동으로 등록하는 것이 좋습니다.

 

(2) 수동 빈 등록의 장점

  • 명확한 구성: 애플리케이션의 구성이 명확해지며, 중요한 로직이나 인프라 설정이 한 눈에 드러납니다.
  • 제어와 관리: 복잡한 환경이나 특수한 상황에서 빈의 생명주기나 의존성을 더 세밀하게 제어하고 관리할 수 있습니다.

3) 정리

  • 자동 빈 등록은 편리함과 간결함을 제공하지만, 모든 상황에 적합한 것은 아닙니다.
  • 수동 빈 등록은 애플리케이션의 중요한 부분이나 복잡한 설정을 명확하게 관리하는 데 유리합니다.
  • 적절한 상황에 맞게 자동과 수동 빈 등록을 혼합하여 사용하는 것이 바람직합니다.

'Spring > 스프링 핵심 원리 - 기본' 카테고리의 다른 글

09. 빈 스코프 (작성 중!)  (1) 2024.02.07
08. 빈 생명주기 Call Back  (1) 2024.01.30
06. 컴포넌트 스캔  (0) 2024.01.26
05. 싱글톤 컨테이너  (0) 2024.01.20
04. 스프링 컨테이너와 스프링 빈  (0) 2024.01.17