[Spring] 의존관계 자동 주입
다양한 의존관계 주입 방법
- 생성자 주입
- 수정자 주입(setter)
- 필드 주입
- 일반 메서드 주입
생성자 주입
- 생성자를 통해서 의존 관계를 주입 받는 방법 (지금까지 진행한 방법)
- 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다.
- [불변, 필수] 의존관계에 사용
- 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
this.memberRepository = memberRepository;
}
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
위와같이 setDiscountPolicy로 값을 변경할 수 있으면 안됨.
수정자 주입 (setter주입)
- setter (필드의 값을 변경하는 수정자 메서드)를 통해서 의존관계 주입
- [선택, 변경] 가능성이 있는 의존관계에 사용
private DiscountPolicy discountPolicy;
private MemberRepository memberRepository;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
위 생성자 주입과 같은 코드. (위의 생성자 주입 코드를 제거해도 된다.)
필드 주입
- 외부에서 변경이 불가능해서 테스트하기 힘들다는 치명적인 단점
- DI 프레임워크가 없으면 아무것도 할 수 없다.
- 간결하지만 쓰지말자 ㅇㅅㅇ;
@Autowired private MemberRepository memberRepository;
@Autowired private DiscountPolicy discountPolicy;
일반 메서드 주입
- 일반 메서드를 통해서 주입 받을 수 있다.
- 한번에 여러 필드를 주입 받을 수 있다.
- 일반적으로 잘 사용하지 않는다.
private DiscountPolicy discountPolicy;
private MemberRepository memberRepository;
@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
초반에 자바 코드로 작성할 때의 코드가 이런 모습이었던 거 같다!
옵션 처리
주입할 스프링 빈이 없어도 동작해야 할 때가 있다.
그런데 @Autowired만 사용하면 'required' 옵션의 기본값이 true로 설정되어 있어서 자동 주입 대상이 없으면 오류가 발생한다.
자동 주입 대상을 옵션으로 처리하는 방법
- @Autowired(required=false): 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨.
- org.springframework.lang.@Nullable: 자동 주입할 대상이 없으면 null이 입력된다.
- Optimal<>: 자동 주입할 대상이 없으면 Optimal.empty가 입력된다.
static class TestBean {
@Autowired(required = false)
public void setNoBean1(Member noBean1) {
System.out.println("noBean1 = " + noBean1);
}
@Autowired
public void setNoBean2(@Nullable Member noBean2) {
System.out.println("noBean2 = " + noBean2);
}
@Autowired
public void setNobean3(Optional<Member> nobean3) {
System.out.println("nobean3 = " + nobean3);
}
}
실행결과
롬복
막상 개발해보면 대부분이 다 불변이라서 생성자에 final 키워드를 사용하게 된다.
그런데 생성자도 만들어야 하고, 주입 받은 값을 대입하는 코드도 만들어야 하기 때문에 번거롭다.
필드 주입처럼 편리하게 사용하는 방법!
원래 코드를 바꿔보자.
public class OrderServiceImpl implements OrderService {
private final DiscountPolicy discountPolicy;
private final MemberRepository memberRepository;
// @Autowired 생성자가 1개이므로 생략 가능
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
this.memberRepository = memberRepository;
}
lombok을 사용하기 위해 build.gradle에 코드 추가
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
dependencies에도 코드 추가
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// lombok 라이브러리 추가
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
}
Settings-Plugins에서 lombok 설치
이후 Settings-Compiler의 Annotation Processors의 Enable annotation processing을 킨다.
lombok 사용을 위해 HelloLombok 클래스 생성
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class HelloLombok {
private String name;
private int age;
public static void main(String[] args) {
HelloLombok helloLombok = new HelloLombok();
helloLombok.setName("choikorea");
String name = helloLombok.getName();
System.out.println("name = " + name);
}
}
편리하게 getter와 setter를 제공해준다.
다시 OrderServiceImpl로 돌아와서
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final DiscountPolicy discountPolicy;
private final MemberRepository memberRepository;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
this.memberRepository = memberRepository;
}
@RequiredArgsConstructor를 추가해주면 필수값인 final이 붙은 것을 찾아 자동으로 생성자 (아래 코드)를 만들어준다.
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
this.memberRepository = memberRepository;
}
@RequiredArgsConstructor를 사용하면 코드가 아래와 같이 줄어든다!!!
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final DiscountPolicy discountPolicy;
private final MemberRepository memberRepository;
조회 빈이 2개 이상 -문제
@Autowired는 타입으로 조회
@Component
public class FixDiscountPolicy implements DiscountPolicy {
@Component
public class RateDiscountPolicy implements DiscountPolicy {
RateDiscountPolicy와 FixDiscountPolicy 둘 다 @Component를 달아준다.
public class AutoAppConfigTest {
@Test
void basicScan() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
MemberService memberService = ac.getBean(MemberService.class);
Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
OrderServiceImpl bean = ac.getBean(OrderServiceImpl.class);
MemberRepository memberRepository = bean.getMemberRepository();
System.out.println("memberRepository = " + memberRepository);
}
}
테스트 코드를 실행하면 오류 발생
하나의 빈을 기대했는데 fixDiscountPolicy, rateDiscountPolicy 2개가 발견됐다.
이때 하위 타입으로 지정할 수도 있지만, 이는 DIP를 위배하고 유연성이 떨어진다.
또, 이름만 다르고, 같은 타입의 스프링 빈이 2개 이상 있을 때 해결이 안된다.
@Autowired 필드 명, @Qualifier, @Primary
@Autowired 필드 명
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
this.discountPolicy = rateDiscountPolicy;
this.memberRepository = memberRepository;
}
DiscountPolicy discountPolicy 에서 DiscountPolicy rateDiscountPolicy로 변경
@Autowired 매칭 정리
- 타입 매칭
- 타입 매칭의 결과가 2개 이상일 때 필드 명, 파라미터 명으로 빈 이름 매칭
@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) {
파라미터 DiscountPolicy 앞에 @Qualifier("이름")을 추가해준다.
@Qualifier 정리
- @Qualifier끼리 매칭
- 그 다음, 빈 이름 매칭
- 그래도 안되면 예외 발생 (NoSuchBeanDefinitionException)
@Primary 사용
우선순위를 정하는 방법. @Autowired 시에 여러 빈이 매칭되면 @Primary가 우선권을 가진다.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {
RateDiscountPolicy에 @Primary를 붙여주면, 최우선으로 찾는다. 따라서 @Primary를 사용하면 @Qualifier를 붙일 필요가 없다.
우선순위
@Primary는 기본값 처럼 동작, @Qualifier는 상세하게 동작. 스프링은 자동보다는 수동, 넓은 범위의 선택권 보다는 좁은 범위의 선택권이 우선 순위가 높다. 따라서 @Qualifier가 우선권이 높다
애노테이션 직접 만들기
hello.core.annotation.MainDiscountPolicy 인터페이스 생성
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
@Qualifier를 "mainDiscountPolicy"로 지정해주었다.
RateDiscountPolicy에 아래와 같이 직접 만든 애노테이션 추가
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {
이후 OrderServiceImpl에서 다음과 같이 사용이 가능하다!
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
조회한 빈이 모두 필요할 때 List / Map
static class DiscountService {
private final Map<String , DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
@Autowired
public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
System.out.println("policyMap = " + policyMap);
System.out.println("policyMap = " + policies);
}
- Map<String, DiscountPolicy>: map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
- List<DiscountPolicy>: DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
- 만약 해당하는 타입의 스프링 빈이 없으면, 빈 컬렉션이나 Map을 주입한다.
자동 / 수동 올바른 실무 운영 기준
자동 빈 등록을 기본으로 사용하되, 수동 빈 등록은 언제 사용할까?
애플리케이션은 크게 업무 로직과 기술 지원 로직으로 나눌 수 있다.
- 업무 로직 빈: 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리 등이 모두 업무 로직이다. 보통 요구사항을 개발할 때 추가되거나 변경된다.
- 기술 지원 빈: 기술적인 문제나 공통 관심사 (AOP)를 처리할 때 주로 사용된다. 데이터 베이스 연결이나, 공통 로그 처리 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.
- 업무 로직은 숫자도 매우 많고 한번 개발해야 하면 컨트롤러, 서비스, 리포즈토리 처럼 어느정도 유사한 패턴이 있다.
- 기술 지원 로직은 업무 로직에 비해 그 수가 매우 적고, 보통 애플리케이션 전반에 걸쳐서 광범위하게 영향을 미친다. 업무 로직은 문제가 발생했을 때 어디가 문제인지 명확하게 잘 들어나지만, 기술 지원 로직은 적용이 잘 되고 있는지 아닌지조차 파악하기 어려운 경우가 많다
애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 설정 정보에 바로 나타나게 하는 것이 유지보수하기 좋다.
[출처] 스프링 핵심 원리 - 기본편, 김영한님