이 글은 김영한님의
스프링 핵심 원리 - 기본편
강의를 수강하고 정리한 내용입니다.
강의 보러가기
의존관계 주입은 크게 4가지가 방법이 있다.
생성자를 통해서 주입 받는 방법이다.
생성자 호출 시점에 딱 한번만 호출되는 것이 보장된다.
불변, 필수 의존관계에 주로 사용된다.
예시
private final MemberRepository memberRepository;
//@Autowired
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
memberRepository
는 생성자 호출할 때 값이 정해지고 값이 바뀌지 않는 불변 객체이다.
또, final
키워드가 붙어 있기 때문에 값이 무조건 존재해야 하는 필수 객체이다.
생성자가 하나만 있을 경우 @Autowired
를 생략할 수 있다.
스프링 컨테이너 생성과정은 빈 등록 단계와 의존관계 주입 단계로 나뉜다.
생성자 주입은 빈을 등록할 때 같이 일어난다. (빈 등록 시 생성자 호출이 필요하기 때문)
수정자 주입은 의존관계 주입 단계에서 일어난다.
선택, 변경 가능성이 있는 의존관계에 사용한다.
예시
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
변경 예시
orderService.setMemberRepository(new MemoryMemberRepository());
관례상 메서드명은 setXxx
형식으로 정한다.
의존관계가 주입되어 있다가 다른 구현체로 바꾸고 싶을 때 바꿀 수 있다.
-> OCP 위반 가능성이 높다.
잘 사용하지 않는다.
바로 예시부터 보자
@Component
public class OrderServiceImpl implements OrderService {
@Autowired private MemberRepository memberRepository;
@Autowired private DiscountPolicy discountPolicy;
}
}
이름 그대로 필드에 바로 주입하는 방법이다.
예를 들어 OrderServiceImpl
의 memberRepository
구현체를 다른 클래스로 바꾸고 싶어도 바꿀 수 있는 방법이 없다.
테스트 코드에서는 아무도 가져다 쓸일이 없으니 문제없이 사용해도 되지만,
아닌 경우엔 사용을 지양하도록 하자..
@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;
}
}
한번에 여러 필드를 주입받을 수 있다.
사실 수정자 주입이랑 비슷하다.
잘 사용하지 않는다.
주입할 스프링 빈이 없어도 동작해야 할 때가 있다.
자동 주입 대상을 옵션으로 처리하는 방법 3가지를 알아보자.
1. @Autowired(required = false)
// 호출 안됨
@Autowired(required = false)
public void setNoBean1(Member member) {
System.out.println("setNoBean1 = " + member);
}
여기서 Member
는 스프링 빈이 아니다.
자동 주입할 대상이 없으므로 호출되지 않는다.
@Autowired
만 사용하면required
옵션 기본값이true
이기 때문에 오류가 발생한다
2. @Nullable
// null 호출
@Autowired
public void setNoBean2(@Nullable Member member) {
System.out.println("setNoBean2 = " + member);
}
@Nullable
null
이 입력된다.3. Optional<>
// Optional.empty 호출
@Autowired(required = false)
public void setNoBean3(Optional<Member> member) {
System.out.println("setNoBean3 = " + member);
}
Optional.empty
가, 있으면 Optinal로 감싸져서 입력된다.스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장한다.
대부분의 의존관계 주입은 한번 일어나면 변경할 일이 없다. 오히려 변경이 되면 안된다.
수정자 주입을 사용하면 setXxx
메서드를 public
으로 열어두어야 한다.
생성자 주입은 객체를 생성할 때 딱 1번만 호출이 되므로 이후에 호출되는 일이 없다. 따라서 불변하게 설계할 수 있다.
예제 - 스프링 컨테이너 사용X, 순수 자바 코드
@Test
void createOrder() {
OrderServiceImpl orderService = new OrderServiceImpl();
orderService.createOrder(1L, "itemA", 10000);
}
OrderServiceImpl
에 수정자 주입 사용 시
NPE
이 발생한다.memberRepository
, discountPolicy
모두 의존관계 주입이 누락되었기 때문이다.OrderServiceImpl
에 생성자 주입 사용 시
생성자 주입을 사용하면 필드에 final
키워드를 사용할 수 있다.
final
은 =
으로 초기 값을 바로 넣어주거나 생성자에서만 값을 세팅할 수 있다.@Autowired
도 붙여주어야 하고, 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;
}
}
롬복을 적용한 코드
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
이 코드는 기존 코드와 완전히 동일하다.
@RequiredArgsConstructor
가 컴파일 시점에 final
이 붙은 필드들을 파라미터로 사용하는 생성자 코드를 자동생성 해준다.
이 방법은 생성자가 1개일 때만 적용이 가능하다.
@Autowired
아래 코드처럼 타입으로 조회해서 주입한다.
ac.getBean(DiscountPolicy.class);
(실제로는 이 코드보다 더 많은 기능을 제공한다.)
❓ FixDiscountPolicy
, RateDiscountPolicy
에 @Component
를 붙여 모두 스프링 빈으로 등록한다면 ❓
@Autowired
private DiscountPolicy discountPolicy
위 코드 @Autowired
에서 빈이 두 개 조회가 되어 오류가 난다. (하위타입 모두 조회)
@Autowired
private DiscountPolicy rateDiscountPolicy // 정상 주입!
먼저 @Qualifier끼리 매칭하는데 찾지 못하면 구분자 이름과 빈 이름으로 매칭한다.
@Component
를 붙여 빈으로 등록한 RateDiscountPolicy
, FixDiscountPolicy
클래스Qualifier("mainDiscountPolicy")
, Qualifier("fixDiscountPolicy")
를 추가로 붙이고 아래와 같이 사용 가능하다.@Autowired
@Qualifier("mainDiscountPolicy") // 또는 @Qualifier("fixDiscountPolicy")
private DiscountPolicy discountPolicy
@Qualifier
는@Qualifier
를 찾는 용도로만 사용하는게 명확하고 좋다.
-> 빈 이름으로 찾는 단계까지 X
우선순위를 정하는 방법이다.
@Autowired
시에 여러 빈이 매칭되면 @Primary
가 우선권을 가진다.
위 예제에서는 RateDiscountPolicy
또는 FixDiscountPolicy
클래스에 @Primary
를 붙여 우선권을 부여해줄 수 있다.
이 방식을 자주 사용한다.
@Quilifier
,@Primary
를 같이 사용할 때@Quilifier
의 우선순위가 더 높다
이전 예제처럼 @Qualifier("mainDiscountPolicy")
이렇게 문자를 적으면 컴파일 시 타입체크가 안된다.
(오타 발생 시 체크가 어려움)
-> 다음과 같은 어노테이션을 만들어 해결할 수 있다.
package hello.core.annotation;
import org.springframework.beans.factory.annotation.Qualifier;
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 {
private int discountPercent = 10; //10% 할인
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
} else {
return 0;
}
}
}
RateDiscountPolicy
를 가져다 쓸 orderServiceImpl
에도 적용!
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
의도적으로 정말 해당 타입의 스프링 빈이 다 필요한 경우도 있다.
-> 예를 들어 할인 서비스를 제공하는데, 클라이언트가 할인의 종류(rate, fix)를 선택할 수 있다고 가정해보자.
코드로 이해해보자
public class AllBeanTest {
@Test
void findAllBean() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
}
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("policyList = " + policies);
}
}
}
우선 생성자 파라미터를 보면 각각 Map<String, DiscountPolicy>
, List<DiscountPolicy>
타입이다.
Map의 key에는 빈 이름(String), value에는 빈 객체(DiscountPolicy)가 모두(rate, fix) 주입된다.
List에는 빈 객체(DiscountPolicy)만 모두(rate, fix) 주입된다.
만약 해당하는 스프링 빈이 없으면, 빈 자료구조를 주입한다
출력 결과
policyMap = {fixDiscountPolicy=hello.core.discount.FixDiscountPolicy@278bb07e, rateDiscountPolicy=hello.core.discount.RateDiscountPolicy@4351c8c3}
policyList = [hello.core.discount.FixDiscountPolicy@278bb07e, hello.core.discount.RateDiscountPolicy@4351c8c3]
마저 작성해보자
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, "userA", Grade.VIP);
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;
@Autowired
public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
System.out.println("policyMap = " + policyMap);
System.out.println("policyList = " + 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);
}
}
}
discount
메서드로 얼마나 할인되는지 값을 반환 받는다.
discountCode
(빈 이름)을 넘겨준다.-> 이 방법은 다형성을 유지하면서 동적으로 빈을 선택할 수 있다.
스프링 빈 자동 등록, 수동 등록 방법 중 어떤걸 선택해서 개발해야 할까?
@Component
만 넣어주면 끝나는 일을 @Configuration
설정 정보로 가서 @Bean
적고 ...
-> 매우 번거롭다
설정 정보가 커지면 그걸 관리하는 것 자체가 부담된다.
어차피 자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다.
-> 편리한 자동 기능을 기본으로 사용하자
그렇다면 수동 빈 등록은 언제 사용하면 좋을까?
애플리케이션은 크게 업무 로직과 기술 지원 로직으로 나눌 수 있다.
업무 로직 빈 : 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리 등이 모두 업무 로직이다.
보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.
기술 지원 빈 : 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다.
데이터베이스 연결이나, 공통 로그처리 업무 로직을 지원하기 위한 하부 기술이나 공통 기술이다.
-> 대부분 자동 빈 등록 사용한다.
-> 가급적 수동 빈 등록을 사용해서 설정 정보에 한 눈에 보이도록 하는 것이 유지보수에 좋다.