
의존관계를 주입하는 방법에는 크게 4가지가 있다.
Setter 메서드를 통해서 의존관계를 주입
이름 그대로 생성자를 통해 의존관계를 주입 받는 방법이다. 지금까지 진행했던 방법이 바로 생성자 주입이다. 여기서 특징이 중요한데, 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다는 점이다. 그래서 불변, 필수 의존관계에 사용된다.
@Component
public class OrderServiceImpl implements OrderService {
// private final는 무조건 값이 있어야 한다는 뜻
private final MemberRepository memberRespository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
좋은 아키텍처, 좋은 개발 습관은 뭐냐 하면 바로 한계점, 즉 제약이 있는 것이다. 알다시피 다~ 열어두면? 뭘 어디서 수정했는지 알 방도가 없다. 값을 세팅하고 나서 이건 더 이상 값을 바꾸면 안 되겠다 싶으면, 생성자에 값을 넣고 Setter 메서드를 안 만들면 된다.
여기서 더 중요한 것은, 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입이 된다는 점이다. 물론 스프링 빈에만 해당한다.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRespository memberRepository;
private final DiscountPolicy discountPolicy;
// 생성자가 1개밖에 없으므로 @Autowired 생략 가능
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
public OrderServiceImpl() {
// 만약 이렇게 생성자가 2개 있다면 @Autowired를 지정해줘야 한다.
}
}
필드의 값을 변경하는 Setter 메서드를 통해서 의존관계를 주입하는 방법이다.
@Component
public class OrderServiceImpl implements OrderService {
// 값을 수정할 수 있게 하기 위해 final 생략
private MemberRespository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
// 수정자로 의존관계를 주입했기 때문에 굳이 생성자를 작성하지 않아도 된다.
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
일단 final을 빼고, 필드명 앞에 “set” 을 붙인다. 필드값을 직접 수정하기 좀 그러니까 메서드를 통해서 이런 식으로 수정한다. 이걸 수정자라고 한다. 그리고 거기에 @Autowired를 붙이면 주입이 되는 것이다.
생성자를 사용할 때는, OrderServiceImpl을 스프링이 생성해서 빈 등록을 해야 한다. 결국 new OrderServiceImpl()처럼 객체 인스턴스를 만들어야 하는데 이때 생성자가 발동돼서 컨테이너에서 스프링 빈 등록된 memberRepository와 discountPolicy를 찾아와서 인스턴스를 생성하는 거다. 그래서 생성자는 어쩔 수 없이 빈 등록할 때 그냥 자동 주입이 일어나는 것이다. 반면, 수정자는 의존관계 주입이 두 번째 단계에서 일어난다.
만약 수정자를 호출하는데 memberRepository나 discountPolicy가 스프링 빈으로 등록되어 있지 않다면 어떻게 될까?? 놀랍게도 이럴 때도 사용이 가능하다.
@Component
public class OrderServiceImpl implements OrderService {
private MemberRespository memberRepository;
private DiscountPolicy discountPolicy;
// 이런 식으로 선택적으로 설정할 수도 있다.
@Autowired(required = false)
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
참고) 자바 빈 프로퍼티, 자바에서는 과거부터 필드의 값을 직접 변경하지 않고, setXXX, getXXX 라는 메서드를 통해서 값을 읽거나 수정하는 규칙을 만들었는데, 그것이 자바 빈 프로퍼티 규약이다. 아래 예시를 보자.
class Data {
private int age;
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
}
위처럼 age 필드가 있는데, 이 필드의 데이터 값을 바꿀 때는 setAge()처럼, 값을 조회해야 할 때는 getAge()처럼 메서드를 만들면 된다.
말 그대로 그냥 필드에 바로 때려 박는 방법이다.
// OrderServiceImpl.java
@Component
public class OrderServiceImpl implements OrderService {
@Autowired private MemberRepository memberRepository;
@Autowired private DiscountPolicy discountPolicy;
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
// AutoAppConfigTest.java
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.getMembmerRepository();
System.out.println("memberRepository = " + memberRepository);
}
}
// 출력
// memberRepository = hello.core.member.MemoryMemberRepository@5cde6747
코드도 간결하고 매우 좋다. 근데… @Autowired에서 “Field injection is not recommended” 라는 메시지는 뭐지…? 필드에 의존관계를 바로 주입하는 방법은 외부에서 변경이 불가능하기 때문에 테스트 하기 힘들다는 치명적인 단점이 있다. "그냥 사용하지 말자." 다만, 애플리케이션의 실제 코드와 관계없는 테스트 코드는 테스트를 스프링 컨테이너에서 할 수 있다. 이럴 때 그냥 바로 @Autowired 박아서 사용하면 편리하다.
하지만, 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만 사용하면 required 옵션의 default 값이 true로 설정되어 있기 때문에 자동 주입의 대상이 없으면 오류가 발생한다. 그렇다면 어떻게 옵션으로 처리하냐?
@Autowured(required = false): 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안 됨org.springframework.lang.@Nullable: 자동 주입할 대상이 없으면 Null이 입력된다.Optional<>: 자동 주입할 대상이 없으면 Optional.empty가 입력된다.
현재 보다시피 Member는 스프링 빈이 아니다. 따라서 @Autowired(required = false)를 붙여서 setNoBean1()은 호출 자체가 되지 않는다.
참고) @Nullable, Optioanl은 스프링 전반에 걸쳐서 지원된다. 예를 들어, 생성자 자동 주입에서 특정 필드에만 사용해도 된다.
과거에는 수정자 주입과 필드 주입을 많이 사용했지만, 최근에는 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장한다. 왜 그럴까?
대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료 시점까지 의존관계를 변경할 일이 없다. 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안 된다.
수정자 주입을 사용하면, setXxx 메서드를 public으로 열어 두어야 한다.
생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다. 따라서 불변하게 설계할 수 있다.
추가로, OrderServiceImpl을 수정자를 통해 생성하도록 하고 테스트를 하나 진행해봤다.
@Component
public class OrderServiceImpl implements OrderService {
private MemberRespository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
// OrderServiceImplTest
class OrderServiceImplTest {
@Test
void createOrder() {
OrderServiceImpl orderService = new OrderServiceImpl();
orderService.createOrder(1L, "itemA", 10000);
}
}
결과는 NullPointerException이 터진다. 왜냐하면 memberRepository와 discountPolicy의 값을 세팅해줘야 하기 때문이다. 다시 말해, memberRepository, discountPolicy 모두 의존관계 주입이 누락됐다는 말이다. 하지만 생성자 주입은 컴파일 오류 메시지로 바로 알아차릴 수 있다.
생성자 주입을 사용하면 필드에 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;
}
}
위처럼 만약 필수 필드인 discountPolicy에 값을 설정해야 하는데 누락되었다면? 그럼 자바는 컴파일 시점에 아래와 같은 오류를 발생시킨다. 기억하자. 컴파일 오류는 세상에서 가장 빠르고, 좋은 오류다.
“java: variable discountPolicy might not have been initialized”
참고) 수정자 주입을 포함한 나머지 주입 방식은 모두 생성자 이후에 호출되므로, 필드에 final 키워드를 사용할 수 없다. 오직 생성자 주입 방식만 final 키워드를 사용할 수 있다.
정리하자면,
생성자 주입 방식을 선택하는 여러가지 이유가 있지만, 프레임워크에 의존하지 않고, 순수 자바 언어의 특징을 가장 잘 살리는 방법이기도 하다.
기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면 된다. 생성자 주입과 수정자 주입을 동시에 사용할 수 있다.
항상 생성자 주입을 선택하자. 그리고 가끔 옵션이 필요하면 수정자 주입을 선택하자. 필드 주입은 사용하지 않는 것이 좋다.
의존 관계 주입을 자동으로 해줄 때 생성자 주입이 좋긴 한데, 코드가 많다는 단점이 있다. 그래서 Lombok이라는 라이브러리를 사용하는 기가 막힌 방법이 생겨났다.
막상 개발을 하면, 99%가 불변이고, 그래서 일단 생성자에 final 키워드를 사용하게 된다. 그런데 생성자도 만들어야 하고, 주입 받을 값을 대입하는 코드도 만들어야 한다.
// 이 코드 쓰는거 이제 좀 귀찮다...
@Autowired
public OrderServiceImpl(MmeberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
필드 주입처럼 편리하게 사용하는 방법은 없을까?
package hello.core.order;
import hello.core.member.Member;
import hello.core.discount.DiscountPolicy;
import hello.core.member.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
위의 기본 코드에서 Lombok이라는 라이브러리를 사용해서 코드를 간소화 해보자.
// HelloLombok.java
package hello.core;
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("rockernun");
String name = helloLombok.getName();
System.out.println("name = " + name);
}
}
// 출력
// name = rockernun
이처럼 Getter, Setter를 자동으로 만들어 준다. 이제 롬복 라이브러리를 이용해서 OrderServiceImpl을 리팩토링 해보자.
package hello.core.order;
import hello.core.member.Member;
import hello.core.discount.DiscountPolicy;
import hello.core.member.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
롬복 라이브러리에서 제공하는 @RequiredArgsConstructor 기능을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어 준다. 완전 간결하다. 심지어 필드에 직접 때려 박는 것보다 더 간단하다.
리팩토링 코드와 이전 코드는 완전히 동일하다. 롬복이 자바의 Annotation Procedure라는 기능을 이용해서 컴파일 시점에 생성자 코드를 자동으로 생성해준다.
@Autowired는 타입으로 조회한다.
package hello.core.order;
import hello.core.member.Member;
import hello.core.discount.DiscountPolicy;
import hello.core.member.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@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;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
여기서 의존관계를 주입하는데, discountPolicy를 타입으로 조회하게 된다. 그렇기 때문에 ac.getBean(DiscountPolicy.class)와 유사하게 동작한다. 알다시피 타입으로 조회하면 선택된 빈이 2개 이상일 때 문제가 발생한다. 어떤 빈을 주입해줘야 하는 거지? 진짜 궁금했던 부분이다...
DiscountPolicy의 하위 타입인 FixDiscountPolicy, RateDiscountPolicy 둘 다 스프링 빈으로 선언해보자.
// FixDiscountPolicy.java
@Component
public class FixDiscountPolicy implements DiscountPolicy {
private int discountFixAmount = 1000;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return discountFixAmount;
} else {
return 0;
}
}
}
// RateDiscountPolicy.java
@Component
public class RateDiscountPolicy implements DiscountPolicy {
private int discountPercent = 10;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
} else {
return 0;
}
}
}
그리고 OrderServiceImpl에서 discountPolicy 의존관계 자동 주입을 실행하면
NoUniqueBeanDefinitionException 오류가 발생한다. 1개의 빈을 기대했는데, fixDiscountPolicy, rateDiscountPolicy 2개가 발견되었다는 말이다.
하위 타입으로 지정할 수도 있지만, 이 방법은 DIP를 위배하고 유연성이 떨어진다. 물론 스프링 빈을 수동으로 등록해서 해결할 수는 있지만, 다행스럽게도 자동 의존관계 주입에서 해결하는 여러 방법이 있다.
여러 개의 빈이 선택 될 때 어떻게 해결하는지 하나씩 알아보자.
@Autowired에 필드명을 매칭시키는 방법@Qualifier끼리 매칭시키는 방법@Primary를 사용하는 방법
@Autowired는 좀 특이한 기능이 있다. @Autowired는 처음에 타입 매칭을 시도한다. 근데 만약 빈이 2개다? 그럼 필드 이름이나 파라미터 이름으로 빈 이름을 추가 매핑한다. 뭔 말인지 모르겠으니, 코드를 비교해보자.
// 기존 코드
@Autowired
private DiscountPolicy discountPolicy
...
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
...
위의 코드들처럼 일단 필드명(discountPolicy)나 생성자 주입의 경우, 파라미터(discountPolicy) 이름을 쓱 훑어본다. 이 이름을 가지고 2개의 빈 중에서 빈 이름이 똑같은 것이 있다면 그 녀석을 찾아온다.
그래서 그냥 아래처럼 주입받고 싶은 빈 이름으로 필드명과 파라미터 이름을 바꿔놓으면...
@Autowired
private DiscountPolicy rateDiscountPolicy
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = rateDiscountPolicy;
}
필드 주입 방식도 rateDiscountPolicy가 주입되고, 생성자 주입 방식으로도 rateDiscountPolicy가 주입된다.
@Qualifier라는 추가 구분자를 붙여 주는 방법도 있다. 주입 시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것이 아니다. 아래 코드를 보자.
// RateDiscountPolicy.java
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {
private int discountPercent = 10;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
} else {
return 0;
}
}
}
// FixDiscountPolicy.java
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {
private int discountFixAmount = 1000;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return discountFixAmount;
} else {
return 0;
}
}
}
이런 식으로 특별한 이름을 부여하고, OrderServiceImpl 코드를 수정하자.
@Component
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
위 코드처럼 @Qualifier는 필드 주입에도 붙일 수 있고, 생성자는 물론, 수정자에도 가능하다. @Qualifier로 설정한 이름의 빈을 찾아서 의존관계를 주입시켜 주는 것이다. 근데 만약 @Qualifier로 주입할 때 mainDiscountPolicy(RateDiscountPolicy)를 찾지 못한다면 어떻게 될까?
얘도 진짜 이름이 mainDiscountPolicy인 스프링 빈이 있는지 추가로 뒤져본다. 그래서 만약에 mainDiscountPolicy라는 스프링 빈이 있으면 그 스프링 빈을 찾아온다. 하지만 그냥 @Qualifier는 딱 @Qualifier를 찾는 용도로만 사용하는 것이 명확하다.
그리고 컴포넌트 스캔 할 때 말고 스프링 빈을 직접 등록할 때도 @Qualifier를 넣을 수도 있다.
정리하자면,
@Qualifier끼리 매칭한다.@Qualifier에서 정한 이름과 같은 빈이 있는지 찾는다.NoSuchBeanDefinitionException 예외가 터진다.
얘는 우선순위를 지정한다. 만약 @Autowired 했는데 여러 개가 매칭된다? 그러면 @Primary라는 애노테이션이 지정되어 있으면 그게 선택된다. 만약 discountPolicy 중 rateDiscountPolicy가 먼저 선택되도록 하고 싶다면? 아래와 같이 @Primary만 넣어주면 된다는 말이다.
// RateDiscountPolicy.java
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {
private int discountPercent = 10;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
} else {
return 0;
}
}
}
그럼 @Qualifier와 @Primary 중에 어떤 것을 사용하면 좋을까? @Qualifier의 단점은 주입 받을 때 모든 코드에 @Qualifier를 붙여줘야 한다는 점이다.
만약 자주 사용하는 메인 데이터베이스의 커넥션을 획득하는 스프링 빈이 있고, 특별한 기능으로 가끔 사용하는 보조 데이터베이스의 커넥션을 획득하는 스프링 빈이 있다고 가정해보자. 메인 데이터베이스의 커넥션을 획득하는 스프링 빈은 @Primary를 적용해서 조회하는 곳에서 @Qualifier 지정 없이 편리하고 조회하고, 보조 데이터베이스 커넥션 빈을 획득할 때는 @Qualifier를 지정해서 명시적으로 획득하는 방식으로 사용하면 코드를 깔끔하게 유지할 수 있다. 물론 메인 데이터베이스의 스프링 빈을 등록할 때 @Qualifier를 지정해주는 것은 상관없다.
<참고>
@Primary는 기본값처럼 동작하고, @Qualifier는 매우 상세하게 동작한다. 이런 경우 어떤 것이 우선권을 가져갈까? 스프링은 자동보다는 수동이, 넓은 범위의 선택권보다는 좁은 범위의 선택권이 우선 순위가 높다. 따라서 @Qualifier가 우선권이 높다.
@Qualifier는 다 좋은데 @Qualifier("mainDiscountPolicy)처럼 문자를 적어 넣으면 컴파일 시 타입 체크가 안 된다. 그래서 애노테이션을 만들어서 깔끔하게 문제를 해결할 수 있다.
// MainDiscountPolicy.java
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)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {}
일단 위와 같이 애노테이션을 만들어주자. 이제 @MainDiscountPolicy 애노테이션을 붙이면...
// RateDiscountPolicy.java
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {
private int discountPercent = 10;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
} else {
return 0;
}
}
}
이제 실수로 @MainDiscountPolicy에 오타를 내도 컴파일 오류가 발생한다.
// OrderServiceImpl.java (생성자 자동 주입)
@Component
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
이제 RateDiscountPolicy 의존관계 주입이 필요한 구현체에도 @MainDiscountPolicy 애노테이션을 붙여주면 성공적으로 의존관계를 주입받을 수 있다.
의도적으로 해당 타입의 스프링 빈이 정말 다 필요한 경우도 있다. 예를 들어, 할인과 관련된 서비스를 제공하는데, 클라이언트가 할인의 종류를 선택할 수 있다고 가정해보자. 스프링을 사용하면 소위 말하는 전략 패턴을 매우 간단하게 구현할 수 있다.
package hello.core.autowired;
import hello.core.AutoAppConfig;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
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("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);
}
}
}
위의 코드를 살펴보면, DiscountService는 Map을 이용해서 모든 DiscountPolicy를 주입받고 있다. 이때 fixDiscountPolicy와 rateDiscountPolicy가 주입된다. 그리고 discount() 메서드를 살펴보면, discountCode 파라미터로 넘어온 DiscountPolicy(fixDiscountPolicy 혹은 rateDiscountPolicy) 이름으로 Map 자료구조에 저장되어 있는 스프링 빈을 찾아서 실행한다.
이렇게 스프링은 빈에 등록된 애들을 다형성을 활용해서 유연한 전략 패턴을 실현할 수 있는 것이다.
어떤 경우에 컴포넌트 스캔과 자동 주입을 사용하고, 어떤 경우에 설정 정보를 통해서 수동으로 빈을 등록하고, 의존관계도 수동으로 주입해야 할까?
이제 스프링인 컴포넌트 뿐만 아니라 컨트롤러, 서비스, 레포지토리처럼 계층에 맞춰서 일반적인 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원한다. 그리고 스프링 부트의 다양한 스프링 빈들도 조건이 맞으면 자동으로 등록되도록 설계되어 있다.
그래서 설정 정보를 기반으로 애플리케이션을 구성하는 부분과 실제 동작하는 부분을 명확하게 나누는 것이 이상적이지만, 개발자 입장에서는 이게 만만치 않다. 스프링 빈 하나 등록할 때 @Component만 넣어주면 끝나는 일을, @Configuration 설정 정보에 가서 @Bean을 적고, 객체 생성하고, 주입할 대상을 일일이 적어주는 과정을 매번 할 수 있을까? 생각만 해도 하기 싫다… 만약 관리할 빈이 많아서 설정 정보가 커지면 설정 정보를 관리하는 것 자체가 부담된다.
"그럼 수동 빈 등록은 언제 사용하면 좋을까?"
애플리케이션 개발은 크게 비즈니스 업무 로직과 기술 지원 로직으로 나눌 수 있다.
업무 로직 빈 : 웹을 지원하는 “컨트롤러”, 핵심 비즈니스 로직이 있는 “서비스”, 데이터 계층 의 로직을 처리하는 “레포지토리” 들이 모두 업무 로직이다. 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.
기술 지원 빈 : 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다. 데이터베이스 연결이나 공통 로그 처리처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.
업무 로직 같은 경우, 그 숫자가 너~무 많다. 한번 개발해야 하면 컨트롤러, 서비스, 레포지토리 이런 거 다 만들어야 한다. 근데 얘들은 유사한 패턴이 있다. 이런 경우에는 자동 기능을 적극 사용해야 할 것이다. 만약 문제가 발생해도 어느 곳에서 발생할지 너무 뻔하기 때문이다.
그에 반해, 기술 지원 로직은 그 숫자가 매우 적다. 기술 지원하는 빈들은 보통 애플리케이션 전반에 걸쳐서 광범위하게 해당 빈 하나가 영향을 주게 된다. 그리고 기술 지원 로직은 적용이 잘 되고 있는지 아닌지 조차 파악하기 어려운 경우가 다반사다. 이런 애들은 가급적 수동 빈 등록을 사용해서 명확하게 밖으로 드러내는 것이 유지보수 하기에 좋다.
근데 비즈니스 로직 중에서도 수동 빈 등록을 하는 것이 더 유리한 경우도 있다. “비즈니스 로직 중에서 다형성을 적극 활용할 때...”
아까 살펴봤던 DiscountService가 의존관계 자동 주입으로 Map<String, DiscountPolicy>에 주입 받는 상황을 생각해보자. 여기에 어떤 빈들이 주입될지, 각 빈들의 이름은 무엇이고 코드만 보고 한번에 파악할 수 있을까? 지금이야 내가 개발했으니 문제가 없지만, 이 코드를 다른 개발자가 나에게 넘겨줬다고 생각한다면… 자동 등록을 사용하고 있기 때문에 파악하려면 여러 코드를 찾아봐야 한다.
이런 경우에 수동 빈으로 등록하거나, 자동으로 할 거면 특정 패키지에 같이 묶어 두는 것이 좋다.
해당 부분을 별도의 설정 정보로 만들고 수동으로 등록하면 아래와 같다.
@Configuration
public class DiscountPolicyConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}
...
이런 식으로 작업이 되어 있으면, “아, 얘들은 이 DiscountPolicyConfig라는 데서만 무조건 등록을 하고 쓰나 보네… 그리고 rateDiscountPolicy, fixDiscountPolicy 이 2개가 등록돼 있구나…” 라고 한눈에 알 수 있다.
정리하자면, 위처럼 설정 정보만 봐도 한눈에 빈의 이름은 물론이고, 어떤 빈들이 주입될지 잘 파악되도록 하든가, 자동 등록을 사용하고 싶으면 최소한 DiscountPolicy의 구현 빈들만 따로 모아서 특정 패키지에 모아두자는 말이다.
참고로, 스프링과 스프링 부트가 자동으로 등록하는 수 많은 빈들은 예외다. 예를 들어, 스프링 부트 같은 경우에는 데이터베이스에 연결하는 DataSource, 이런 건 어떻게 보면 기술 지원과 관련된 스프링 빈인데 이런 것들까지 스프링 부트가 자동으로 등록을 해준다. 이런 부분은 매뉴얼을 잘 참고해서 스프링 부트가 의도한 대로 편리하게 사용하면 된다.
<최종 정리>
편리한 자동 기능을 기본으로 사용하자.
직접 등록하는 기술 지원 객체는 수동 등록하자.
다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해보자.