의존 관계 주입 II
개발을 하다보면 대부분 불변으로 설계를 하고, 생성자 주입으로 final 키워드를 사용하게 된다.
이 부분을 필드 주입처럼 편하게 사용할 수 있는 방법이 있다. 롬복에 대해서 간단하게 정리하고 이어서 의존 관계 주입에 대해 알아보자.
Lombok
@Getter/@Setter
: 게터 세터 기능을 제공한다.@NoArgsConstructor
: 빈 생성자(기본 생성자)를 생성해준다.@AllArgsConstructor
: 필드 값을 모두 포함한 생성자를 생성해준다.@RequiredArgsConstructor
:final 필드와 @NonNull이 붙은 필드에 대한 생성자를 생성해준다.@ToString
: toString 기능을 제공하며exclude
를 사용하여 제외할 필드를 선택할 수 있다.@EqualsAndHashCode
: Equals와 HashCode 메서드를 자동으로 생성해준다.@Data
: @Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor를 한 번에 생성해준다.
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("sasca");
System.out.println(helloLombok.getName());
}
}
- 롬복 설정은 프로젝트 생성 시에 추가할 수 있으며 다음과 같이
Getter/Setter
를 애노테이션 하나로 설정할 수 있다.
롬복 적용 전
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
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);
}
}
롬복 적용 후
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import lombok.RequiredArgsConstructor;
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);
}
}
@Autowired
는 생성자가 1개일 경우 생략이 가능하다. 여기에 롬복의@RequiredArgsConstructor
를 사용하면 코드를 정말 깔끔하게 사용할 수 있다.
조회 빈이 2개 이상일 경우
@Autowired
는 기본적으로 타입으로 조회한다. 즉, ac.getBean(인터페이스명)
과 같이 인터페이스에서 구현체를 찾아 해당 빈을 가져온다. 만약 인터페이스를 가진 여러 구현체가 존재하고 모두 빈에 관리 대상이면 어떻게 될까? 다음과 같은 오류가 발생한다.
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'hello.core.discount.DiscountPolicy' available: expected single matching bean but found 2: fixDiscountPolicy,rateDiscountPolicy
- 같은 인터페이스에 여러 구현체가
@Component
로 등록 되어 있을 경우 해당 오류 발생한다.
@Autowired 필드 명, @Qualifier, @Primary 와 같은 기능을 사용하여 해당 오류를 해결해보자.
@Autowired 필드명 매칭
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = rateDiscountPolicy; // rateDisCountPolicy로 설정
}
- DiscountPolicy 에 RateDiscountPolicy, FixDiscountPolicy 2개 모두 빈 등록 되어 있는 경우
- 필드명, 파라미터명을 rateDiscountPolicy로 지정해주면 @Autowired가 해당 빈으로 인식 한다.
- 클래스 레벨이 빈 등록될 때 클래스명에 첫글자는 소문자로 등록이 된다.
- ex ) RateDiscountPolicy 는 rateDiscountPolicy라는 빈이름으로 등록
@Qualifier 사용
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy{
...
@Component // 빈 등록시 네이밍 설정
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy{
...
@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;
}
...
- 빈 이름을 변경하는 것이 아닌, @Qualifier는 추가 구분자를 붙여주는 방법이다.
- @Qualifier를 못찾으면 명시한 이름으로 스프링 빈으로 찾는다. (그래도 없으면 예외 발생)
- @Qualifier는 찾는 용도로만 사용하자. (헷갈림 방지)
@Primary 사용
@Primary
public class RateDiscountPolicy implements DiscountPolicy{
...
@Primary
가 등록되면 여러 빈중에 최상위 우선순위를 갖게 된다.- 우선 순위는 항상 자동 < 수동 , 넓은 선택 < 좁은 선택이다.
- @Primary 와 @Qualifier가 모두 적용 되어있을 경우 @Qualifier가 우선 순위가 높다.
- 보통 2개의 빈을 모두 사용할 때 메인 구현체에 @Primary, 서브에 @Qualifier를 지정하여 사용한다.
- @Qualifier 보다 간결하여 주로 많이 사용하는 방식이다.
메인 데이터베이스의 커넥션을 획득하는 스프링 빈은
@Primary
를 적용해서 조회하고, 서브 데이터베이스 커넥션 빈을 획득할 때는@Qualifier
를 지정해서 명시적으로 획득 하는 방식으로 사용하면 코드를 깔끔하게 유지할 수 있다.
커스텀 애노테이션
@Qualifier
에 지정한 문자는 컴파일 환경에서 알 수 없으므로 타입 체크가 안된다. 그래서 커스텀 어노테이션을 사용하여 오타를 검증하는 경우가 많다.
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 {
}
- 다음과 같이 커스텀 애노테이션을 생성한다. ElementType을 통해 애노테이션 사용 범위를 지정하고, @Qualifier를 통해 네이밍을 지정해주자. 이렇게 생성하면 외부에서 해당 애노테이션 접근만으로 오타를 방지하고 사용할 수 있다.
package hello.core.discount;
import hello.core.annotation.MainDiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.stereotype.Component;
@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;
}
}
- 다음과 같이 만든 커스텀 애노테이션을 통해 우선 순위를 지정할 수 있다.
조회한 빈이 모두 필요할때 (List, Map)
개발을 하다보면 의도적으로 스프링 빈이 다 필요한 경우도 있다. 예를 들어서 할인 서비스를 제공하는 클라이언트가 할인의 종류를 선택할 수 있을 때와 같다. 스프링을 사용하면 소위 말하는 전략 패턴을 매우 간단하게 구현할 수 있다.
package hello.core.autowired;
import hello.core.AutoAppConfig;
import hello.core.discount.DiscountPolicy;
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;
public class AllBeanTest {
@Test
void findAllBean() {
ApplicationContext ac = new AnnotationConfigApplicationContext(DiscountService.class);
}
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);
}
}
}
DiscountPolicy
가 빈 등록이 되지 않았기 때문에 policyMap과 policies 는 빈 객체를 받아오지 못한다. 설정 클래스에 등록된 설정 파일을 같이 실행해야 빈 객체가 생성된다.
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.*;
public class AllBeanTest {
@Test
void findAllBean() {
//AutoAppConfig : 컴포넌트 스캔을 위한 Configuration, DiscountService 빈 등록
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
// 등록된 DiscountService 빈을 꺼내옴
DiscountService discountService = ac.getBean(DiscountService.class);
// 테스트 더미 데이터
Member member = new Member(1L, "userA", Grade.VIP);
// discount 메서드를 통해 fix 정책을 적용
int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");
assertThat(discountService).isInstanceOf(DiscountService.class);
assertThat(discountPrice).isEqualTo(1000);
// discount 메서드를 통해 rate 정책을 적용
int rateDiscountPrice = discountService.discount(member, 10000, "rateDiscountPolicy");
assertThat(rateDiscountPrice).isEqualTo(2000);
}
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);
return discountPolicy.discount(member, price);
}
}
}
- DiscountService는 Map으로 모든 DiscountPolicy 를 주입받는다. 이때 fixDiscountPolicy , rateDiscountPolicy 가 주입된다. 다음 예시를 보자.
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);
for (String x : policyMap.keySet()) {
System.out.println(x);
System.out.println(policyMap.get(x));
}
}
- 인터페이스가 갖고있는 구현체의 빈 네임, 객체정보를 맵에 자동으로 담아준다. 그 원리는
Map<String,DiscountPolicy> policyMap = ac.getBeansOfType(DiscountPolicy.class);
다음과 같이 동작하여 담아진기 때문이다. - discount로 넘어온 값으로 스프링 빈을 찾아서 실행한다.
- 해당 타입의 빈이 없으면 빈컬렉션이나 Map을 주입한다.
마법과 같은 일이 일어났다. 해당 동작 과정에 대해서 다시 정리해보자. 스프링 컨테이너에 클래스 정보를 넘기면 스프링 빈으로 등록된다. 이때 인터페이스인 DiscountPolicy 주입이 담긴 AutoAppConfig.class와 추가로 생성한 DiscountService를 넣어주면서 빈 등록이 되며, 여러가지 구현체들을 입맛대로 사용할 수 있게 된다.
자동, 수동 의존 관계의 기준
편리한 자동 기능을 기본으로 사용하자. 최근 스프링 부트는 컴포넌트 스캔을 기본으로 사용하고, 다양한 스프링 빈들도 조건이 맞으면 자동으로 등록하도록 설계했다. 또한, 자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다.
수동 빈 등록을 사용할 때
애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 설정 정보에서 바로 볼 수 있도록 하는 것이 유지보수하기 좋다. 예를 들어서 방금 전에 했던 예제인 DiscountPolicy
를 가진 구현체들을 별도의 설정 정보에 등록해두는 것이 협업 과정에서 효율적이라고 볼 수 있다.
- 어플리케이션 : 업무로직빈, 기술 지원 빈으로 크게 나눌 수 있다.
- 업무로직 빈 (비즈니스 요구사항을 개발 할 때) : 자동 의존 관계 사용
- 자동 기능을 기본으로 사용하자 (최근 스프링부트는 @Component를 기본으로 갖추고 있음)
- 자동 빈 등록을 해도 OCP, DIP를 지킬 수 있다.
- 기술 지원 빈 (기술적인 문제나 , 공통 관심사(AOP)) : 수동 의존 관계 사용
- 애플리케이션에 광범위하게 영향을 미치는 경우
- 로직 적용 확인 파악하기 어려운 경우
- 가급적 수동 빈 등록을 사용하여 명확하게 들어내는 것이 좋다. (주로 Root 계층에 지정)
- 다형성을 활용하여 타인이 파악하기 어려운 경우
본 포스팅은 인프런 김영한님 강의(스프링 핵심원리 - 기본편)를 토대로 정리한 내용입니다.