yja
8. 의존관계 자동 주입 본문
1. 다양한 의존관계 자동 주입 방법
A. 생성자 주입
: 생성자를 통해 의존관계를 주입받는다.
생성자가 1개만 있으면 @Autowired 를 생략해도 된다.
호출 시점에 1번만 호출되고, 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;
}
}
B. setter(수정자) 주입
이 방식은 의존관계가 설정되었더라도 나중에 수정할 수 있는 방식이다. setter가 public으로 열려 있어서 외부에서 수정할 수 있다.
@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;
}
}
C. 필드 주입
필드에 @Autowired를 바로 지정해서 코드가 깔끔하다.
하지만 테스트할 때 의존관계를 정해줄 수 있는 방법이 없어서 권장되지 않는다.
@Component
public class OrderServiceImpl implements OrderService {
@Autowired private MemberRepository memberRepository;
@Autowired private DiscountPolicy discountPolicy;
}
D. 일반 메서드 주입
한번에 여러 필드를 주입받을 수 있지만 잘 쓰이지 않는다.
@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;
}
}
2. 옵션 처리
스프링 빈이 없어도 동작해야하는 경우가 있다.
@Autowired는 기본값이 `required=true` 여서 의존성 주입을 해주지 않으면 UnsatisfiedDependencyException 오류가 발생한다.
자동 주입 관계를 옵션으로 처리해줄 수 있다.
1. @Autowired(required=false)
자동 주입할 대상이 없으면 호출 자체가 안된다.
// 메서드 호출 자체가 안된다.
@Autowired(required = false) // false 지정 안하면 UnsatisfiedDependencyException 오류난다.
public void setNoBean1(Member noBean1) { // 스프링 빈이 아닌 Member
System.out.println("noBean1 = " + noBean1);
}
2. org.springframework.lang.@Nullable
자동 주입할 대상이 없으면 호출은 되지만 객체가 null이 된다.
// 호출은 되는데 null
@Autowired
public void setNoBean2(@Nullable Member noBean2) {
System.out.println("noBean2 = " + noBean2);
}
3. Optional<>
자동 주입할 대상이 없으면 호출은 되지만 객체가 Optional.empty가 된다.
// 호출은 되는데 Optional.empty
@Autowired
public void setNoBean3(Optional<Member> noBean3) {
System.out.println("noBean3 = " + noBean3);
}
3. 생성자 주입을 선택해라!
대부분의 의존 관계 주입은 한번 주입하고 나서 바뀌지 않아야 한다.
1. 컴파일 오류에서 의존 관계가 빠진 것을 확인할 수 있다.
생성자를 통한 의존 관계 주입을 선언해놓으면, 의존 관계 주입을 까먹으면 테스트에서 컴파일 오류가 먼저 뜬다.
memberRepository와 discountPolicy 관련 의존관계가 필요하다고 한다. 만약 setter 주입이었다면 컴파일 오류가 뜨지 않는다.

2. `final` 키워드를 사용할 수 있다.
즉, 생성자에서만 값을 세팅할 수 있고 세팅 후엔 변경할 수 없다. 다른 나머지 방식은 생성자 이후에 호출되므로 final 키워드를 사용하지 못한다.
또한 생성자에서 값을 주입해주는 코드를 빠뜨리면 컴파일 오류를 내준다.

4. 롬복과 최신 트랜드
기본적인 생성자 주입 코드는 일일이 써주기 귀찮으므로 이런 반복되는 코드를 롬복을 통해 간단히 만들 수 있다.
1. 롬복 설정하는 방법
build.gradle에 롬복 설정 추가하기
//lombok 설정 추가 시작
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
//lombok 설정 추가 끝
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
//lombok 라이브러리 추가 시작
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
//lombok 라이브러리 추가 끝
}
인텔리제이 설정 > 플러그인에서 lombok 추가

설정 > 어노테이션 프로세서 에 들어가서 '어노테이션 처리 활성화'를 눌러준다.

2. 롬복 활용하기
- `@Getter`: getXxx() 코드를 만들어준다.
- `@Setter`: setXxx() 코드를 만들어준다.
- `@RequiredArgsConstructor`: final이 붙은 필드에 대해 생성자 주입 코드를 만들어준다.
@Component
@RequiredArgsConstructor // 생성자 코드를 만들어준다.
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;
// }
}
5. 조회 빈이 2개 이상 - 문제
RateDiscountPolicy와 FixDiscountPolicy 둘다 스프링 빈으로 등록시켜보자.
@Component
public class FixDiscountPolicy implements DiscountPolicy{}
@Component
public class RateDiscountPolicy implements DiscountPolicy{}
@Autowired는 타입으로 조회한다.
@Autowired
private DiscountPolicy discountPolicy
그러면 DiscountPolicy 타입 하나를 기대했는데, 2개가 발견되었다고 뜨면서 UnsatisfiedDependencyException 오류가 발생한다.

6. @Autowired 필드 명, @Qualifier, @Primary
조회되는 빈이 여러 개일 때 해결 방법 3가지
1. @Autowired 필드명 매칭
필드명을 빈 이름으로 바꾼다. 그러면 그 빈 이름의 빈이 주입된다.
@Autowired
private DiscountPolicy rateDiscountPolicy
2. @Qualifier -> @Qualifier끼리 매칭 -> 빈 이름 매칭
빈을 등록할 때 `@Qualifier`라는 추가 구분자를 붙여준다.
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy{}
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy{}
생성자 주입 코드에서 어떤 `@Qualifier` 을 사용할지 지정해준다.
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
만약 "mainDiscountPolicy"라는 이름을 찾지 못하면 그 이름의 스프링 빈을 찾게 된다.
3. @Primary 사용
`@Primary`가 붙은 RateDiscountPolicy 클래스가 우선순위를 갖는다.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
@Qualifier가 @Primary보다 더 상세하게 이름을 지정하므로 둘중에서는 @Qualifier가 더 우선순위를 갖는다.
7. 애노테이션 직접 만들기
@Qualifier("mainDiscountPolicy") 처럼 직접 문자열로 적으면 실수를 할 수 있다. 컴파일 오류가 잡히지 않는다.
따라서 이 실수를 방지하기 위해 어노테이션으로 만들어놓고 쓸 수 있다.
import java.lang.annotation.*;
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy") // <- 이름 지정
public @interface MainDiscountPolicy {
}
mainDiscountPolicy로 지정하고 싶은 할인 정책 클래스 위에 붙인다.
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy{}
그리고 사용하는 쪽(생성자)에서도 마찬가지로 붙인다.
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@MainDiscountPolicy DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
8. 조회한 빈이 모두 필요할 때 - List, Map
스프링에서는 전략 패턴을 쉽게 적용할 수 있다.
Map과 List에 스프링 빈으로 등록된 DiscountPolicy 타입인 빈을 넣을 수 있다.
public class AllBeanTest {
@Test
void findAllBean() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
}
@Component
static class DiscountService {
// fixDiscountPolicy와 rateDiscountPolicy가 policyMap에 주입된다.
private final Map<String, DiscountPolicy> policyMap; // key, value
private final List<DiscountPolicy> policies; // value
@Autowired
DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
System.out.println("policyMap = " + policyMap);
System.out.println("policies = " + policies);
}
}
}

`discount()` 메서드에서는 discountCode가 "fixDiscountPolicy"인지 "rateDiscountPolicy"인지에 따라 적용되는 할인 정책이 달라진다.
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);
}
`discountService.discount(member, 10000, "fixDiscountPolicy");` 로 호출한 경우 아래와 같이 적용된다.

9. 자동, 수동의 올바른 실무 운영 기준
기본적으로는 자동 등록 빈을 사용하자!
어플리케이션은 업무 로직 빈과 기술 지원 빈이 있는데,업무 로직 빈은 매우 많고 공통 부분이 많으므로 자동 등록 빈을 사용하면 좋고, 기술 지원 빈(예: AOP 공통 관심사)은 적고 어플리케이션 전체적으로 적용되기 때문에 수동 등록 빈을 사용하는 것이 좋다.
수동 등록을 하면 어떤 객체가 주입될 지 한눈에 파악할 수 있다는 장점이 있다. 자동 등록을 쓴다면 적어도 같은 패키지 안에 두어서 한눈에 파악할 수 있도록 해야한다.
'[Web-Back] Spring > 스프링 핵심 원리 - 기본편 (강의)' 카테고리의 다른 글
| 10. 빈 스코프 (0) | 2026.05.04 |
|---|---|
| 9. 빈 생명주기 콜백 (0) | 2026.04.29 |
| 7. 컴포넌트 스캔 (0) | 2026.04.06 |
| 6. 싱글톤 컨테이너 (1) | 2025.08.25 |
| 5. 스프링 컨테이너와 스프링 빈 (0) | 2025.07.26 |