[스프링 핵심 원리] 6

smj_716·2025년 5월 3일

스프링 완전 정복

목록 보기
9/16

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

➡️ 1) 생성자 주입

  • 생성자 호출시점에서 딱 1번만 호출되는 것이 보장된다.
  • 불변, 필수 의존관계에 사용한다.
 @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;
    }
 }

⚠️ 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입이 된다.

➡️ 2) 수정자 주입

  • setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법이다.
  • 자바빈 프로퍼티의 수정자 메서드 방식이다. (=setter)
  • 선택, 변경 가능성이 있는 의존관계에 사용한다.
...
@Autowired
 public void setMemberRepository(MemberRepository memberRepository) {
     this.memberRepository = memberRepository;
 }
   
 @Autowired
 public void setDiscountPolicy(DiscountPolicy discountPolicy) {
     this.discountPolicy = discountPolicy;
 }
 ...

⚠️ @Autowired 는 주입할 대상이 없으면 오류가 발생한다. 오류가 발생하지 않게 하려면 @Autowired(required = false)로 지정해야한다.

➡️ 3) 필드 주입

  • 코드가 간결하고 쉽다.
  • But, 외부에서 값을 주입하거나 변경할 수 없어서(setter도 없고 생성자를 통한 주입도 아니니) 테스트에서 객체를 바꿔 넣기 어렵다는 단점이 있다.
  • DI 프레임워크(스프링) 없이는 객체를 만들 수 없다.
    • ⚠️ 스프링 컨테이너는 @Conponent 등이 붙은 클래스들을 자동으로 스프링 빈에 등록하여 관리해주는 통이다. 이 통 안에 들어간 객체만 스프링이 주입(@Autowired)도 관리도 해주기 때문에 new로 직접 객체를 생성하면 스프링은 몰라서 null이 될 수도 있다.
...
 @Autowired
 private MemberRepository memberRepository;
 @Autowired
 private DiscountPolicy discountPolicy;
 ...
  • 즉, 사용하지말자 ‼️
  • 아래의 경우는 고려해보자
    • 애플리케이션의 실제 코드와 관계 없는 테스트 코드
    • 스프링 설정을 목적으로 하는 @Configuration 같은 곳

➡️ 4) 일반 메서드 주입

  • 한번에 여러 필드를 주입 받을 수 있다.
  • 잘 사용하진 않는다.
...
 @Autowired
 public void init(MemberRepository memberRepository, DiscountPolicy 
discountPolicy) {
 this.memberRepository = memberRepository;
 this.discountPolicy = discountPolicy;
    }

2. 옵션 처리

주입할 스프링 빈이 없어도 동작해야 할 때가 있다.
그런데 @Autowired만 사용하면 required 옵션의 기본값이 true로 되어 있어서 자동 주입 대상이 없으면 오류가 발생한다.

해결방법은 다음 3가지이다.

  • @Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨
  • org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입력됨
  • Optional<> : 자동 주입 대상이 없으면 Optional.empty가 입력됨

(@Nullable, Optional은 스프링 전반에 걸쳐서 지원된다. 예를 들어서 생성자 자동 주입에서 특정 필드에만 사용해도 된다.)

//호출 안됨
@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);
 }

Member는 스프링 빈이 아니기 때문에 아래와 같이 setNoBean1 자체는 호출이 되지 않는 결과가 나온다.


3. 왜 생성자 주입을 권장할까?

💡 불변성 보장

  • 대부분의 의존 객체는 한 번 주입되면 변경되면 안된다.
  • 생성자 주입은 객체 생성 시점에만 호출되기 때문에, 객체가 생성된 후에는 절대 바뀌지 않는다.
  • final 키워드를 사용할 수 있어서 컴파일 타임에 안정성을 확보한다.

💡 의존성 누락 방지 (컴파일 오류)

  • java: variable discountPolicy might not have been initialized
  • 수정자 주입이나 필드 주입은 의존성 누락이 있어도 컴파일 오류 없이 실행되어 NPE 가능성이 있다.
  • 생성자 주입은 생성자 파라미터가 필수이므로 누락되면 컴파일 에러로 알려줘서 안전하다.

💡 순수 자바 코드로 테스트 가능

// 생성자 주입은 테스트에서도 명확하게 어떤 의존 객체가 필요한지 알 수 있음
OrderServiceImpl service = new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
  • DI 프레임워크 없이도 new를 이용하여 객체를 만들 수 있어서 스프링 없이도 테스트가 가능하다.

항상 생성자 주입을 선택하자! 그리고 가끔 옵션이 필요하면 수정자 주입을 선택하도록 하자.

🛠 룸복으로 더 간단하게!

롬복 라이브러리가 제공하는@RequiredArgsConstructor 기능을 사용하면 final이 붙은 필드를 모아서 컴파일 시점에 생성자를 자동으로 만들어준다. (자바의 애노테이션 프로세서라는 기능을 이용)
➕ 생성자가 하나일 경우 @Autowired 생략이 가능하기 때문에 아래와 같이 코드가 훨씬 간단해진다.

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

4. 의존성 주입 충돌 (빈이 2개 이상일 때)

@Autowired타입 기준으로 주입을 시도하여 같은 타입에 등록된 빈이 2개 이상일 경우 NoUniqueBeanDefinitionException 오류가 발생한다.

@Autowired
private DiscountPolicy discountPolicy; 
// DiscountPolicy 빈이 2개라면?  (FixDiscountPolicy, RateDiscountPolicy)

이 문제를 해결하는 방법 3가지는 아래와 같다.

👉 필드명 매칭

@Autowired는 먼저 타입 매칭을 시도하고 여러 빈이 있을 때 추가로 동작하는 기능이다. 아래와 같이 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.

@Autowired
private DiscountPolicy rateDiscountPolicy; // 빈 이름과 일치

👉 @Qualifier 사용

추가 구분자를 붙여주는 방법이다.
주입시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것이 아니다! 주의하자!

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}

@Autowired
public OrderServiceImpl(
    MemberRepository memberRepository,
    @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
    ...
}

만약 @Qualifier("mainDiscountPolicy")을 찾지 못한다면 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다.
추가로 찾을 시에도 없다면 NoUniqueBeanDefinitionException 오류가 발생한다.

👉 @Primary 사용

우선 순위를 정하는 방법이다. @Autowired 시에 여러 빈이 매칭되면 @Primary가 우선권을 가진다.

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

✅ @Qualifier vs @Primary

  • @Primary는 기본(default) 빈으로 지정하는 것이고 별다른 지정이 없을 때 주입된다.
  • @Qualifier는 명시적으로 특정 빈을 지정하는 것이고 @Primary보다 우선순위가 높다.
  • 자주 사용하는 빈은 @Primary, 가끔 사용하는 빈은 @Qualifier로 지정하면 깔끔하게 관리할 수 있다.

커스텀 애노테이션으로 간결하게!

@Qualifier("mainDiscountPolicy") 이렇게 문자를 적으면 컴파일시 타입 체크가 안된다.

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

자바 애노테이션에는 상속이 없고, 여러 애노테이션을 모아서 사용하는 기능은 스프링이 지원해주는 기능이다.

//빈 정의 시
 @Component
 @MainDiscountPolicy
 public class RateDiscountPolicy implements DiscountPolicy {}
//생성자 자동 주입시
@Autowired
 public OrderServiceImpl(MemberRepository memberRepository,
                         @MainDiscountPolicy DiscountPolicy discountPolicy) {
      this.memberRepository = memberRepository;
      this.discountPolicy = discountPolicy;
}

But, 명확한 목적 없이 무분별하게 사용하면 유지보수에 혼란이 생기므로 주의하자!


5. List, Map으로 전체 빈 주입받기

할인 서비스를 제공하는데 클라이언트가 할인의 종류(rate, fix)를 선택할 수 있다고 가정해보자!
이럴 땐 모든 DiscountPolicy 구현체를 한번에 주입받고 클라이언트 선택에 따라 전략적으로 실행하는 방법이 있다.

@Component
public class DiscountService {

    private final Map<String, DiscountPolicy> policyMap;
    private final List<DiscountPolicy> policies;

    // 모든 DiscountPolicy 빈이 Map과 List로 자동 주입됨
    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 policy = policyMap.get(discountCode);
        // 클라이언트가 고른 빈의 discount 메서드 실행 
        return policy.discount(member, price);
    }
}
  • Map : map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
  • List : DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.

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

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

  • 컴포넌트 스캔과 자동 주입을 사용하는 선호하는 추세이다.
  • 자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다.

2. 그럼 언제 빈 등록을 사용할까 ❓

  • 애플리케이션은 크게 아래와 같이 나눌 수 있다.
    • 업무 로직 빈 : 보통 비지니스 요구사항 개발할 때 추가되거나 변경되며 컨트롤러, 서비스, 레포지토리 등이 해당된다.
    • 기술 지원 빈 : 데이터 베이스 연결이나 공통 로그 처리처럼 업무 로직을 지원하기 위한 공통 기술들이다. (ex : AOP 처리 시 사용)
  • 숫자도 많고 한번 개발하면 어느정도 유사한 패턴이 있는 업무 로직자동 기능을 활용하고, 수가 적고 광범위하게 영향을 미치며 문제 발생 시 찾기 어려운 기술 지원수동으로 등록하는 것이 좋다.

3. 비지니스 로직 중 다형성을 적극 활용할 때는 ❓

  • 자동으로 등록된다면, 다른 개발자가 구현한Map<String, DiscountPolicy> 에 어떤 빈들이 주입될지 파악하기 어렵다.
  • 이런 경우 수동 빈으로 등록하거나 자동으로하면 특정 패키지에 같이 묶어두는게 좋다!

0개의 댓글