[TIL] _ 260314 _ 의존관계 주입을 깊게 학습하다

호두·2026년 3월 16일

✔️ Java / Spring

목록 보기
5/25

오늘 학습 내용

오늘은 의존관계 주입 강의 학습을 완료했다. 스프링 입문편을 공부하면서 알게된 DI 를 더 자세한 내용으로 진행한다. 의존관계 주입 방법 4가지와 관련해서 편리하게 할 수 있는 애노테이션들, 전략패턴을 만들어서 동적으로 스프링 빈 변경하는 방법 등을 배웠다.


✅ 의존관계 주입 4가지 방법

DI 하는 방법에는 4가지가 있다. 이 중에서 생성자를 사용하는 것을 보통 추천한다. 스프링의 2가지 Life Cycle이 존재하는데 1단계는 스프링 빈을 생성하고, 2단계는 의존관계를 주입하는 것이다. 다른 방법들은 객체를 생성한 후에 주입을 실행하는데 비해 생성자 주입은 스프링 빈을 생성함과 동시에 DI를 해서 주입이 확실히 보장된다는 장점이 있다. 또 컴파일로 체크도 가능하고 final을 사용할 수 있어서 불변성도 보장할 수 있다.


생성자 주입

@Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
  • 생성 시점에 딱 1번만 호출되는 것이 보장됨 (불변성)
  • private final 로 무조건 생성시점에 값이 있어야 하는게 보장됨 (컴파일 오류로 체크)
  • 생성자가 1개면 @Autowired 생략 가능


필드 주입

// @Autowired
private final MemberRepository memberRepository;

스프링 : @Autowired 붙여서 자동으로 스프링이 해당 빈을 주입하게 한다.
순수 자바 : private 으로 의존관계 주입이 불가능하다.

순수 자바 코드는 단위테스트에 사용되는데 필드 주입으로 하면 단위테스트가 불가능하다. 스프링으로 주입이 가능하다고 해도 final 을 사용할 수 없어 불변성 보장이 안된다.


@Bean 에서 파라미터에 의존관계 주입

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

파라미터 이름을 discountPolicy ➡️ rateDiscountPolicy 로 변경해두면 스프링이 이 매개변수 이름을 보고 해당하는 구현체를 매칭해서 주입해준다.



이 부분을 배우다가 든 의문점💡


이렇게 코드를 바꾸면 OCP 원칙에 어긋나지 않나?


정확하게 말하면 OCP 원칙에서 변경되지 않아야하는 코드는 비지니스 로직이다. 중요한건 무엇을 수정하는지 다. 기존의 비지니스 로직을 건들이지 말자는 원칙이지 의존관계를 이름으로 수정하는 부분은 관대하게 허용해준다. 하지만 OCP 원칙을 완전히 지킨 것은 아니다.

OCP 원칙 준수 정도

@Qualifier < @Primary < @Configuration



Setter 주입

@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;

Setter 주입은 객체를 생성하고 그 후에 주입한다. 이때 개발자가 실수로 @Autowired (스프링) 을 붙이는 걸 까먹거나, 설정파일에서 setter 를 호출하는 걸 까먹으면 의존관계가 주입되지 않은 채로 실행되서 NPE 가 발생한다.


일반 메서드 주입

@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy
 discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
  }
}

대신 스프링 컨테이너가 가지고 있는 스프링 빈의 메서드여야 한다.




✅ 옵션

주입할 빈이 없어도 동작해야하는 경우
: 추가적인 기능 (있으면 좋지만 없어도 기존 요청사항 실행에는 문제 없음)

package hello.core.autowired;


import hello.core.member.Member;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.lang.Nullable;

import java.util.Optional;

public class AutowiredTest {

    @Test
    void AutowiredOption() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class); // 스프링 컨테이너에 해당 설정파일이 스프링빈으로 등록

    }

    static class TestBean { // 테스트 하려는 클래스

        @Autowired(required = false)
        public void setNoBean1(Member noBean1) {
            System.out.println("noBean1 = " + noBean1);
        }

        @Autowired
        public void setNoBean2(@Nullable Member noBean2) { // @Nullable 패키지 잘 보고 가져오기 (springframework 꺼로)
            System.out.println("noBean2 = " + noBean2);
        }

        @Autowired
        public void setNoBean3(Optional<Member> noBean3) {
            System.out.println("noBean3 = " + noBean3);
        }
    }
}
  • required = false 는 원래 메서드 호출 시 기본값이 true 인데 false 로 하면 호출 가능성을 따져서 불가능하면 호출을 하지 않는다.

  • org.springframework.lang.Nullable 의 @Nullable 은 호출 시 반환값이 null 이면 메서드를 호출하지 않는다.

  • Optional<> 는 null 일 경우 Optional.empty() 형식으로 반환해서 예외 발생을 방지해준다.



✅ Rombok

필드 주입처럼 아주 간단하게 애노테이션만 붙이면 스프링이 알아서 다 해주도록 기능을 제공하는 라이브러리.

생성자 애노테이션

@Component
@RequiredArgsConstructor // ✅
public class OrderServiceImpl implements OrderService {

   private final MemberRepository memberRepository;
   private final DiscountPolicy discountPolicy;
}

생성자가 1개일 경우 @Autowired 를 붙이지 않아도 된다. 여기서 추가로 @RequiredArgsConstructor 을 붙여 롬복을 사용하면 생성자를 직접 추가하지 않아도 호출할 수 있다. 스프링이 해당 클래스를 보고 final 이 붙은 필드를 가지고 생성자를 속에 만들어둔다.

생성자 뿐만 아니라 @Getter, @Setter, @ToString 등도 있다.




✅ 스프링 빈을 타입으로 조회할 때 2개 이상일 경우

@Autowired 는 타입으로 조회한다. 할인 정책 구현체가 2개고 둘 다 스프링 빈으로 컨테이너에 등록이 되어있을 경우에는 어떻게 특정할 수 있을까?


컴포넌트 자동 등록한 할인 정책 스프링 빈 2개

@Component
public class FixDiscountPolicy implements DiscountPolicy {}

@Component
public class RateDiscountPolicy implements DiscountPolicy {}

NoUniqueBeanDefinitionException

스프링 빈에서 배웠듯이 타입으로 조회했을 때 스프링 빈이 2개면 예외가 발생한다. 하위타입으로 지정할 수도 있지만 DIP 원칙을 위반한다.



@Autowired 필드명, 파라미터 이름 매칭

@Autowired 는 먼저 타입을 매칭 시도하고 여러개 빈일 경우! 필드명, 파라미터 이름을 보면서 추가로 매칭한다.

필드명 매칭

public class OrderServiceIml {

@Autowired
private DiscountPolicy rateDiscountPolicy

...

}

의존관계가 주입되는 구현체에 있는 해당 필드명을 discountPolicrateDiscountPolicy 로 변경한다. 이 필드명을 보고 매칭되는 스프링 빈을 찾아서 주입해준다.


생성자의 매개변수 이름 매칭

public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {

  this.memberRepository = memberRepository;
  this.discountPolicy = rateDiscountPolicy;

}

생성자의 할인 정책 매개변수 이름을 사용하려는 스프링 빈 이름으로 변경한다. 안의 바디도 맞춰서 변경해준다.



💡 공부하다 생긴 의문

생성자 매개변수로 매칭하려는데 저 바디에도 바꿔주면 OCP 위반 아닌가?

➡️ 매개변수 이름을 바꾸면 바디에서 참조하는 이름도 같이 바꿔야 컴파일 에러가 안난다. 이때 바디에 있는 이름도 바꿔야하는데, 생성되는 스프링 빈인 OrderServiceImpl 클래스에서는 DiscountPolicy 인터페이스에 의존하고 있고 코드가 전혀 변경되지 않는다. 지금 코드를 변경하는 곳은 AppConfig 설정파일이다. OrderServiceImpl 자체는 닫혀있고 외부 설정 AppConfig 에서 수정하기 때문에 OCP 원칙 위배가 아니다.



하나의 스프링 빈에 @Autowired 를 2개 적어두면 안되지 않나?

➡️ 이건 오류가 아니라 중복 지시에 해당한다. 생성자 주입이 스프링 빈 생성과 동시에 실행되고 그 후에 필드에 해당 스프링 빈을 또 주입해 덮어쓰게 된다. @Autowired 는 비지니스 로직과는 관련이 없고, DI 에서는 해당 스프링 빈을 사용할 수 있느냐가 중요하기 때문에 크게 문제되지 않는다.

(대신, 앞에서 말했다시피 필드 주입은 불변성을 보장 못하고 final 이 아니라서 누군가 값을 변경할 수 있다.)



@Qualifier 사용

스프링이 구분할 수 있도록 추가적인 이름을 붙여주는 기능이다. 스프링 빈 이름이 변경되는 것은 아니다.

생성자 자동 주입 시

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

  this.memberRepository = memberRepository;
  this.discountPolicy = discountPolicy;
}
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}

모든 코드에 @Qualifier 를 붙여줘야 한다. 같은 이름의 @Qualifier 를 가지고 있는 스프링 빈을 주입시켜준다. 단점은 문자라서 컴파일 체크가 되지 않는다는 점인데 이 부분은 이걸 애노테이션으로 만들어서 사용하면 해결이 가능하다.


@Primary 사용

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
public class FixDiscountPolicy implements DiscountPolicy {}

스프링 빈에 우선순위를 매기는 기능이다. 스프링이 같은 타입인 스프링 빈을 2개 이상 발견했을 때 @Primary 가 붙은 스프링 빈을 가져와서 주입해준다.


실무에서 보통 메인 데이터베이스 연결할 때 @Primary 를 사용해서 편리하게 사용하고, 가끔 사용하는 서브 데이터베이스를 @Qualifier 를 붙여서 명시적으로 사용하는 방식을 주로 한다. 더 세세하게 명시적으로 연결하는 @Qualifier 가 @Primary 보다 우선순위가 높다.



✅ 조회한 빈이 여러개 필요한 경우

의도적으로 해당 타입의 여러 개의 빈이 전부 필요한 경우가 있다. 클라이언트가 사용하려는 기능을 그 중에 골라서 요청한다면? 이 부분을 스프링을 사용해 전략패턴으로 간단히 구현할 수 있다.

package hello.core.autowired;

import hello.core.AutoAppConfig;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.*;

public class AllBeanTest {

    @Test
    void findAllBean() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
        DiscountService discountService = ac.getBean(DiscountService.class);
        Member member = new Member(1L, Grade.VIP,"userA");
        int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");

        assertThat(discountService).isInstanceOf(DiscountService.class); // 찾은 거랑 만든 게 같은 빈인지
        assertThat(discountPrice).isEqualTo(1000); // 지정한 할인 정책이 잘 사용됐는지
    }

    static 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;
            System.out.println("policyMap = " + policyMap);
            System.out.println("policies = " + policies);
        }

        public int discount(Member member, int price, String discountCode) {

            DiscountPolicy discountPolicy = policyMap.get(discountCode);

            System.out.println("discountCode = " + discountCode);
            System.out.println("discountPolicy = " + discountPolicy);

            return discountPolicy.discount(member,price);
        }
    }
}

스프링 컨테이너를 AutoAppConfig 와 해당 클래스 안에 생성한 DiscountService.class 를 기준으로 생성했다. 설정파일에서 컴포넌트로 자동 생성된 스프링 빈들을 넣어두고 밑의 DiscountService 스프링 빈도 생성해서 넣어둔다.

할인 정책을 적용하기 위해서 회원 정보를 하나 생성하고 DiscountService 의 discount() 메서드를 호출한다. 이 메서드는 회원 정보, 상품 금액, 사용하려는 할인 정책 이름을 요구한다.

DiscountService 에는 할인 정책 관련 빈들을 List 와 Map 에 다 저장해뒀다. 이렇게 컬렉션에 스프링 빈을 저장해서 메서드 호출을 받으면 매칭되는 할인을 골라서 동시에 적용이 가능해진다.


✅ 왜 List 와 Map 일까?

먼저 스프링은 해당하는 스프링 빈을 가져올 뿐만 아니라 원하는 형태로 만들어서 줄 수도 있다. 스프링 빈 생성자의 매개변수에 원하는 타입을 지정해두면 스프링이 스프링 컨테이너에 있는 스프링 빈들을 해당하는 타입으로 만들어서 DI 해준다.

Map 을 사용하면 편리하게 동적으로 다형성을 이용해 여러 같은 타입의 스프링 빈을 클라이언트가 변경하며 사용할 수 있다. ( = 전략패턴 )
List 는 연속적으로 등록된 모든 스프링 빈들을 다 돌아봐야 할 때 사용한다. 보안 정책 시스템에서 로그인 -> 권한 체크 -> IP체크 처럼 모두 통과해야지만 되는 부분을 구현해준다.


해당 스프링 빈 이름이 key 로 설정되고, 스프링 빈 객체가 value 로 들어간다. 이 중에 문자열, 즉 DiscountCode 를 가지고 Map 의 key 와 매칭되는 스프링 빈을 찾아서 그 할인 정책을 적용한 할인 금액을 반환한다.

0개의 댓글