다양한 의존관계 주입 방법
생성자 주입
생성자를 통해서 의존관계를 주입받는다.
- 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다.
- 불변, 필수 의존관계에 사용된다. (private 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;
}
}
생성자가 딱 하나일 때에는 @Autowired를 생략할 수 있다.(만약 다른 생성자가 있다면 생략하면 안됨!!)
수정자 주입(setter 주입)
setter이라고 불리는 수정자 메서드를 통해 의존관계를 주입한다.
- 선택, 변경 가능성이 있는 의존관계에 사용한다.
- 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다.
@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;
}
}
@Autowired에서 주입할 대상이 없으면 오류가 발생한다. 주입할 대상이 없어도 동작하게 하기 위해서는 @Autowired(required = false)로 지정하면 된다.
참고로, 자바빈 프로퍼티와 자바에서는 필드 값을 직접 변경하도록 하지 않고, getter, setter을 사용한다.
필드주입
필드에 바로 주입하는 방법이다.
- 외부에서 변경이 불가능해서 테스트하기 힘들다는 치명적인 단점이 있다.(목 객체를 넣지 못함)
- DI 프레임워크가 없으면 아무것도 할 수 없다. (순수한 자바코드로 실행 불가)
-> 권장되지 않는 방법이다
실행해도 되는 경우
- 애플리케이션의 실제 코드와 관계 없는 테스트코드'
- 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 사용
일반 메서드 주입
일반 메서드를 통해서 주입받을 수 있다.
- 한번에 여러 필드를 주입받을 수 있다.
- 일반적으로 잘 사용하지는 않는다.
@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를 사용하면 옵션의 기본값이 true이기 때문에 오류가 발생한다.
자동주입대상을 옵션으로(빈이 없어도 오류가 나지 않게)처리하는 방법은 다음과 같다.
- @Autowired(required=false) : 자동주입할 빈이 없으면 수정자 메서드 자체가 호출되지 않는다
- @Nullable : 대상이 없으면 null이 입력된다.
- Optional : 자동 주입할 대상이 없으면 Optional.empty가 입력된다
@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);
}
해당 부분에 대한 예제이다
생성자 주입을 선택해라
최근에는 생성자 주입이 권장된다.
불변
- 대부분의 의존관계 주입은 끝날때까지 변하면 안된다.
- 수정자 주입을 사용하면 setter을 public로 열어두어야한다. -> 누군가 실수로 변경할 수 있다
- 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로, 불변하게 설계가 가능하다
누락
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;
}
//...
}
만약 위와 같은 클래스가 있고, 이를 사용한 테스트를 하고자 할 때
@Test
void createOrder() {
OrderServiceImpl orderService = new OrderServiceImpl();
orderService.createOrder(1L, "itemA", 10000);
}
이런식으로 순수 자바 코드로 테스트하면 실행은 되지만, null point exception이 발생한다.
하지만, 만약 생성자 주입방식으로 위 클래스를 설계했다면, 컴파일 오류가 발생하여 누락을 보다 쉽게 잡을 수 있다.
final 키워드
생성자 주입을 사용하면 final 키워드를 사용할 수 있다. final을 설정해주면 불변성을 보장할 수 있다.
또한 final 키워드를 사용하면 생성자에서 실수로 특정 필드를 주입하는 것을 누락했을 때 컴파일 시점에 오류를 발생시킨다.
컴파일 오류는 세상에서 가장 빠르고, 좋은 오류다!
롬복과 최신 트랜드
개발을 하다보면 거의 다 불변이라 final 키워드를 사용하게 된다. 이럴 때 생성자를 하나하나 만드는 것은 귀찮다
롬복을 사용하면 필드를 정의하는 것만으로도 모든 것을 해결할 수 있다.
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
이렇게 @RequiredArgsConstructor을 사용하면 final이 들어간 필드에 대해서 자동으로 의존성 주입을 해준다.
조회한 빈이 2개 이상일 때
@Autowired는 type로 조회한다.
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
@Component
public class RateDiscountPolicy implements DiscountPolicy {}
따라서 위와 같은 빈 두개가 있는 상태에서
@Autowired
private DiscountPolicy discountPolicy
위와 같은 코드를 실행하면 NoUniqueBeanDefinitionException 에러가 발생한다.
@Autowired 필드명, @Qualifier, @Primary
조회 대상 빈이 2개 이상일 때 해결방법
- Autowired 필드명 매칭
- @Qualifier끼리 매칭
- @Primary 사용
@Autowired 필드명 매칭
@Autowired
private DiscountPolicy discountPolicy
를
@Autowired
private DiscountPolicy rateDiscountPolicy
로 바꾸면 문제 없이 주입된다.
@Qualifier 사용
@Qualifier은 추가 구분자를 붙여주는 방법이다. 빈 이름을 변경하는 것은 아니다.
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}
이런 식으로 빈 등록시 붙여주면 된다.
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy") DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
주입할 때에도 위와 같이 어노테이션을 붙여준다.
@Bean
@Qualifier("mainDiscountPolicy")
public DiscountPolicy discountPolicy() {
return new ...
}
이렇게 빈을 직접 등록할 때에도 사용할 수 있다.
@Primary사용
@Primary는 우선순위를 정하는 방법이다. @Autowired 때 여러 빈이 매칭되면 @Primary가 우선권을 가진다.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
이런식으로 빈을 생성하고, DiscountPolicy를 주입하면 RateDiscountPolicy가 우선 주입된다.
@Primary보다 @Qualifier가 우선순위가 높다.
스프링은 기본적으로 자동보다 수동이, 넓은 범위의 선택권 보다는 좁은 범위의 선택권이 우선순위가 높다.
어노테이션 직접 만들기
@Qulifier("mainDiscountPolicy") 이렇게 문자를 적으면 컴파일 시 타입 체크가 안된다.
-> 정의할때랑 주입할 때 문자가 달라도 오류가 발생하지 않는다.
이럴 때를 대비해서 어노테이션을 새로 만들 수 있다.
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
위와 같이 인터페이스를 만들고 위에는 Qulifier에 붙일 수 있는 곳에 모두 붙일 수 있게 하기 위해서 @Qulifier에 있는 어노테이션들 모두 가져와서 붙여넣기 한다.
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@MainDiscountPolicy DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
//수정자 자동 주입
@Autowired
public DiscountPolicy setDiscountPolicy(@MainDiscountPolicy DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
어노테이션을 만들면 이렇게 간단하게 적용할 수 있다.
이런 방식으로 어노테이션을 커스텀하고 만들 수 있지만, 스프링에 있는 기본 어노테이션만 사용해도 충분하고, 잘못건들이면 유지보수에 혼란을 줄 수 있다.
여기서 궁금한 점이 생겼다.
그냥 @Qualifier있으니까 굳이 @Qualifier안에 있는 코드까지 다 넣을 필요가 있나?
찾아보니까 스프링은 어노테이션의 어노테이션까지는 참조하지만
어노테이션의 어노테이션의 어노테이션까지는 참조하지 못한다고 한다.
조회한 빈이 모두 필요할 때, List, Map
할인 서비스를 제공하는데 클라이언트가 할인 종류(rate, fix)를 선택할 수 있다고 하자.
스프링을 이용하면 이를 아주 편리하게 구현할 수 있다.
우선
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {} ```
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}
위와 같이 같은 타입으로 구현된 두가지 클래스가 있다.
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);
}
이런식으로 의존관계주입을 받는다(생성자 하나라 @Autowired 안해도 괜차늠)
Map는 해당 클래스 명(키)와 객체가 나오고, List는 객체들이 나온다.
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를 주입한 클래스에서
discountService.discount(member,1000,"fixDiscountPolicy");
이런식으로 호출하면 쉽게 할인정책을 설정할 수 있다.
자동, 수동의 올바른 실무 운영 기준
편리한 자동 기능을 기본으로 사용하는 것이 좋다.
최근 스프링부트는 컴포넌트 스캔을 기본으로 사용한다.
결정적으로 자동 빈 등록을 사용해도 OCP와 DIP를 지킬 수 있다.
수동빈은 언제 사용?
애플리케이션은 크게 업무로직과 기술지원로직으로 나눌 수 있다.
업무로직 : 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포 지토리등이 모두 업무 로직이다. 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.
기술지원로직 : 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다. 데이터베이스 연결이나, 공통 로그 처리 처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.
주로 기술지원로직에 수동빈을 사용한다. 왜냐하면 적용이 잘 되고있는지 아닌지 파악하기 어렵기 때문이다.
또한 다형성을 적극 활용할 때 좋다
@Configuration
public class DiscountPolicyConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}
```
앞의 예시처럼 다형성을 활용하여 다양한 유형의 같은 타입의 빈을 관리할 때 딱 보고 이해가 될 수 있도록 할 수 있다.
출처
인프런 김영한 스프링 핵심원리 - 기본편
스프링 핵심 원리 - 기본편 강의 | 김영한 - 인프런
김영한 | , 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보세요! 📢 수강 전 확인해주세요! 본 강의는 자바 스프링 완전 정복 시리즈의 두 번째 강의입니다. 우아한형제들 최연
www.inflearn.com
'Back-end > Spring' 카테고리의 다른 글
[Spring] 빈 스코프 (1) | 2025.03.11 |
---|---|
[Spring] 빈 생명주기 콜백 (1) | 2025.02.27 |
[Spring] 컴포넌트 스캔 (2) | 2024.11.29 |
[Spring] 싱글톤 컨테이너 (3) | 2024.11.14 |
[spring] 스프링 컨테이너와 스프링 빈 (1) | 2024.10.04 |