[Spring] 스프링 기본4 - 의존관계 자동 주입

easyone·2026년 9월 17일

Spring

목록 보기
28/29

다양한 의존관계 주입 방법

  • 생성자 주입
  • 수정자 주입(setter 주입)
  • 필드 주입
  • 일반 메서드 주입

생성자 주입

생성자를 통해서만, 의존관계가 주입되며 외부에서 수정할 수 있는 방법이 없다.

  • 생성자 호출시점에 딱 한번만 호출되는 것이 보장된다.
  • 불변, 필수 의존관계에 사용된다.

생성자가 한개만 있으면 @Autowired를 생략해도 자동 주입되는데 스프링 빈에만 해당한다.
생성자 두개 이상이면 무조건 명시해야 한다.

최근에는 Lombok의 @RequiredArgsConstructor를 쓰면 final 필드 기준으로 생성자를 자동 생성해줘서, 이 규칙이랑 합쳐서 생성자 코드 자체를 안 써도 되게 하는 게 요즘 사용 방법이다.

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    // 생성자 코드 자체가 안 보임 (Lombok이 컴파일 시점에 만들어줌)
}

이렇게 하면 생성자 코드 자체가 안보이게 된다.

수정자 주입(setter 주입)

@Component
public class OrderServiceImpl implements OrderService {

    private MemberRepository memberRepository;  // final 없음!
    private DiscountPolicy discountPolicy;

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

    @Autowired
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
}
  • setter라고 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법이다.
  • 선택, 변경 가능성이 있는 의존관계에 사용한다.
  • 스프링이 객체를 먼저 기본 생성자로 만들고 나서 setXxx() 메서드들을 호출해서 나중에 값을 채워넣는다. AutoWired가 있으니까 주입, 이런 식이다.
  • 나중에 값을 바꿀 수도 있으므로 final을 못 붙인다.
  • 즉 setter를 호출 한다음에, 의존관계가 완성된다.

-> AutoWired는 주입할 대상이 없으면 오류가 발생한다. 주입할 대상이 없어도 동작하게 하려면 @Autowired(required=false)로 지정하면 된다.

-> 실무에서는 잘 안 쓴다. 필수값인데 final이 안되기 때문에 실수로 인해 채우지 않아서 사용하게 되면 NPE 위험이 있다.

필드 주입

@Autowired private MemberRepository memberRepository;
  • 필드에 바로 주입하는 방법이다.
  • 코드가 간결하지만, 외부에서 변경이 불가능해서 테스트하기 힘들다는 단점이 있다.
    -> 생성자가 없어서 순수 자바 코드로는 테스트가 절대 안되고, 순수 자바로 테스트하려면 스프링 없이 new로 값을 채울 방법이 없다. 그러면 어떻게 해야 하냐, @SpringBootTest를 써서 스프링 컨테이너를 통째로 띄워야만 한다.
  • DI 프레임워크(Autowired)가 없으면 아무것도 할 수 없다.
  • 인텔리제이에서 사용하게 되면 경고가 뜨면서 추천되지 않는 방법이라고 뜨기도 한다.

-> 사용 안하는게 좋다.

일반 메서드 주입

  • 일반 메서드를 통해서 주입 받을 수 있다.
  • 한번에 여러 필드를 주입 받을 수 있다.
  • 일반적으로 잘 사용하지 않는다. 생성자나 수정자 주입으로 대부분 커버된다.
  • 자바 규약 이름이 아니어도 된다.
@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;
    }
}

옵션 처리

주입할 스프링 빈이 없어도 동작해야 할 때가 있다. Autowired는 자동 주입 대상이 없으면 오류가 발생한다.
자동 주입 대상을 옵션으로 처리하는 방법은 다음과 같다.

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

Member라는 타입의 빈이 스프링 컨테이너에 없다면..

setNoBean1: 주입할 빈이 없으니까, 메서드 자체가 호출이 안된다.
setNoBean2: 빈이 없으면 파라미터에 null이 들어간 채로 호출된다.
setNoBean3: 빈이 없으면 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);
}

생성자 주입을 선택하기

결론적으로 생성자 주입이 권장된다.

불변성
대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다.
오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안된다.
수정자 주입을 사용하면, setXxx 메서드를 public으로 열어두어야 한다.
누군가 실수로 변경할 수 도 있고, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아므로, private으로 하는 것이 좋다.

생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다.
따라서 불변하게 설계할 수 있다는 장점이 있다.

누락
프레임워크 없이 순수한 자바 코드를 단위 테스트 하는 경우에, 수정자 의존관계라면..
Autowired가 프레임워크 안에서 동작할 때는 의존관계가 없으면 오류가 발생하지만, 프레임워크 없이 순수 자바 코드로만 단위 테스트를 수행한다면 일단 실행은 된다.
그런데 의존관계 주입이 누락되었다면 NPE가 발생한다.
반명 생성자 주입을 사용하면? 누락이 발생하면 NPE가 아니라 컴파일 오류가 발생해서, 어떤 값을 필수로 주입해야 하는지 바로 알 수가 있다.

final 키워드 사용

생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다.
그래서 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아준다.

조회 빈이 2개 이상일 경우의 문제

@Component
public class FixDiscountPolicy implements DiscountPolicy {}

@Component
public class RateDiscountPolicy implements DiscountPolicy {}
@Autowired
private DiscountPolicy discountPolicy;

@Autowired는 타입으로 찾는데, DiscountPolicy 타입이 2개(Fix, Rate)라서 스프링이 뭘 넣을지 모른다.
NoUniqueBeanDefinitionException이 터지게 된다.

필드명을 빈 이름이랑 똑같이 맞추기

필드명을 빈 이름이랑 똑같이 맞추면 된다. 타입으로 여러 개 나오면 필트,파라미터 이름으로 추가 매칭한다. 근데 실무에서 잘 안쓴다.

Qualifier

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

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
public OrderServiceImpl(MemberRepository memberRepository,
                         @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {

주입받을 때 지정해주면 된다. 주입받는 모든 곳에 @Qualifer를 붙여줘야 한다.
Qualifier는 Qualifier를 쓰는 것들끼리만 사용하는 방법이 권장된다. 헷갈리기 때문에..

Primary

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}  // 이게 기본적으로 선택된다

@Component
public class FixDiscountPolicy implements DiscountPolicy {}

@Primary vs @Qualifier 언제 쓰는지?
자주 쓰는 메인 → @Primary (기본값처럼 편하게)
가끔 쓰는 특수 케이스 → @Qualifier (명시적으로 콕 찍어서)

둘이 충돌나면 @Qualifier가 우선순위다.

조회한 빈이 모두 필요한 경우 List,Map

하나만 딱 찍는게 아니라, DiscountPolicy 타입 전부를 받고 싶을 때, Map이나 List로 받아서 사용할 때는 코드로 원하는 것을 골라서 쓸 수가 있다.

public DiscountService(Map<String, DiscountPolicy> policyMap,
                        List<DiscountPolicy> policies) {
    this.policyMap = policyMap;  // {"fixDiscountPolicy": Fix객체, "rateDiscountPolicy": Rate객체}
    this.policies = policies;    // [Fix객체, Rate객체]
}
public int discount(Member member, int price, String discountCode) {
    DiscountPolicy discountPolicy = policyMap.get(discountCode); // "fixDiscountPolicy" 넘기면 Fix 꺼내옴
    return discountPolicy.discount(member, price);
}

전략 패턴을 구현할 때 유용하다. 사용자가 특정 방식을 선택하게 하고 싶을 때 등등..

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

자동 등록 vs 수동 등록

기본은 @Component같이 자동을 사용한다.
요즘 스프링 트렌드가 자동을 선호한다.
@Component 하나만 붙이면 끝나고 설정 파일 관리 안 해도 되기 때문이다.!
자동으로 등록해도 OCP, DIP 지킬 수 있다.

수동을 고려해야 할 때는 다음과 같다.

  1. 기술 지원 로직 (업무 로직 아닌 것 — DB 연결, 로깅, 공통 기술)

개수 적고, 애플리케이션 전체에 광범위하게 영향 줌
문제 생겨도 어디가 문제인지 파악하기 어려움
→ 수동 등록해서 설정 파일에 딱 드러나게 하는 게 유지보수에 좋음

  1. 다형성을 적극 활용하는 비즈니스 로직 (위의 Map<String, DiscountPolicy> 같은 경우)

코드만 봐서는 어떤 빈들이 들어올지 한눈에 보이지 않는다.
다른 개발자가 코드 받으면 파악하려고 여러 파일 뒤져야 한다.
→ 수동 등록하거나, 자동으로 하더라도 관련 클래스들을 한 패키지에 모아두기

profile
백엔드 개발자 지망 대학생

1개의 댓글

comment-user-thumbnail
2026년 10월 5일

띠아이 >.<

답글 달기