Repeat is the best medicine for memory
스프링의 컨테이너에 대한 이해와 Bean 등록 방법들에 대해서 알아보았다.
이제 의존성 주입과 관련한 스프링의 원리와 활용법들에 대해서 파악해보겠다.
실제 개발 환경에서 하나의 계층에 있는 객체는 다른 계층의 객체를 필요로 하는 경우가 대부분이다.
( 예를 들어, 서비스 계층의 객체는 레파지토리 계층의 객체를 필요로한다. )
그리고 이와 관련하여, 의존성 주입 시 @Configuration을 활용한 CGLIB 기술로 싱글톤을 지키는 방법에 대해서도 알아보았다.
더 나아가, 의존관계 주입의 다양한 방법과 원리 및 활용법들에 대해서 살펴보자.
의존관계 주입 방법의 종류
의존관계 주입에는 크게 4가지의 방법이 있다. 각 주입 방법에 대해 파악해보자.
1. 생성자 주입
2. 수정자 주입(setter 주입)
3. 필드 주입
4. 일반 메서드 주입
생정자 주입
이름 그대로 객체의 생성자를 통해서 의존 관계를 바로 주입받는 것이다.
@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가 붙은 객체가 컨테이너에 등록되는 시점에, 관련 의존 객체들이 주입된다.
따라서 생성자 호출 시점에 의존관계 형성이 1번만 이뤄지는 것이 가능하고 이는 불변성을 보장하며, 필수적 의존관계를 지켜준다.
( private 키워드가 불변성을 만들며, final 키워드가 필수성을 형성한다. )
( 그리고 만약 생성자가 1개뿐이라면 @Autowired를 생략해도 좋다. )
수정자 주입
대게 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;
}
생성자 주입과 달리 컨테이너에 등록된 이후( @Componenet가 붙은 객체 먼저 Bean 등록하는 동작이 발생 - 생성자 호출 ),
@Autowired가 붙은 setter 메서드를 호출하여 의존관계를 주입하는 방법이다.
이를 사용할 경우, 당연히 해당 메서드를 사용해서 필드에 접근하고 의존대상을 변경할 수 있다.
필드 주입
@Autowired 키워드를 붙이고 필드에 직접 주입하는 방법이다.
@Component
public class OrderServiceImpl implements OrderService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
}
위의 방법들과 비교하여 코드가 간결해진다.
하지만, 요즘은 많이 사용하지 않는데 일단 테스트하기 어렵고 final 키워드 사용을 할 수 없어 불변 객체로 지정할 수 없다.
또한 순환 참조 문제의 발생 가능성이 생기기 때문이다.
에를 들어, 아래와 같은 Java 코드로 테스트를 작성하는 경우 null 예외가 발생한다.
필드에 직접 주입하는 것은 스프링이 지원하는 기술이기에 실제 컨테이너 기반 실행이 아니라면, 각 필드는 null이기 때문이다.
일반 메서드 방법
@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;
}
}
별도의 메서드를 통한 의존 관계를 설정하는 방법이며, 일반적으론 사용하지 않는다.
생성자 주입을 선택하자
이처럼 다양한 의존 관계 설정 방법들이 존재하는 데, 그 중 가장 권장되는 주입 방법은 생성자 주입이다.
그 이유는 크게 ‘ 불변과 누락 ‘에 있다.
불변
생각해보면, 대부분의 어플리케이션 코드에서 의존관계 주입은 한번 발생하고 수정되는 경우가 없다. 오히려 불변성이 지켜져야 하는 경우가 더 많다.
따라서 수정자 주입(Setter)와 같이 변경에 대해서 열어두는 방법은 그 목적이 뚜렷한 것이 아니라면, 누군가의 실수로 변경되는 상황을 사전에 막는 것이 좋다.
누락
Java 코드 수준에서 생성자를 사용하는 이유에 대해서 말해보자면, 강제성에 있다.
생성자는 객체를 인스턴스로 생성함에 필요한 요소들을 지정하는 것을 강제한다.
이는 테스트 코드 작성에서 의존 객체를 잊는 상황과 다른 의존 객체들에 대한 누락을 컴파일 단계에서 방지한다.
또한 final 키워드를 사용할 수 있기에 혹시라도 생성자에서 설정되지 않은 값들의 에러를 컴파일 단계로 막을 수 있다.
( 컴파일 에러가 가장 바람직한 에러이다..! )
한편, 의존 관계에 있어 특정 객체가 필수적이기 않은 상황이 있을 수 있다.
예를 들어, 해당 의존 객체가 Bean으로 등록되지 않은 경우 기본 로직이 발생하도록 하는 경우이다.
관련하여 처리하는 방법들을 살펴보자.
옵션 처리
의존 객체들을 주입하기 위해서 @Autowired를 사용하면 required의 옵션이 기본적으로 true로 설정된다.
이는 자동 주입 대상이 없다면, null을 반환하여 에러를 발생시킨다.
별도로 자동 주입 대상으로 옵션으로 처리하는 방법들은 아래와 같은 것들이 존재한다.
// 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨
@Autowired(required=false)
// 자동 주입할 대상이 없으면 null이 입력된다.
org.springframework.lang.@Nullable
// 자동 주입할 대상이 없으면 Optional.empty 가 입력된다.
Optional<>
아래는 위의 방법들을 적용한 예시와 실행 결과이다.
//호출 안됨
@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);
}
Lombok 활용하여 편리하게 개발하기
여기까지 의존 주입의 다양한 방법과 그 특징들을 살펴보았고 각 의존 관계 설정에 있어 고려사항을 파악하였다.
이제, Lombok을 활용하여 의존관계 설정 및 주입을 보다 편리하게 사용하는 방법에 대해서 적어보겠다.
Lombok이 제공하는 어노테이션을 활용하면, 보다 쉽게 의존 관계들을 설정할 수 있다.
제공해주는 어노테이션에는 아래와 같은 것들이 있다.
@Getter
@Setter
@AllArgsConstructor // 전체 필드값을 전달받는 생성자 생성
@ToString
@RequiredArgsConstructor // final이 붙거나, @Nonnull이 붙은 변수들을 받는 생성자 생성.
@Builder // 생성자에 빌더패턴 적용
그 중에서 @RequiredArgsConstructor을 적용하면 아래와 같은 사용이 가능하다.
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
조회 대상 Bean이 2개 이상인 경우
의존관계 설정에 활용되는 @Autowired는 Bean 타입을 조회하여 주입한다.
따라서 DIP, OCP를 지키며 필요한 객체들을 @Autowired로 주입 받으면, 동일 타입의 Bean이 조회될 상황이 나타날 수 있다.
아래의 예시를 보며 이해하고 해결 방법들을 알아보자.
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;
}
이와 같은 클래스가 있고, MemberRepository와 DiscountPolicy 타입의 객체를 주입받는다.
그리고 @Autowired은 아래 코드처럼 getBean()로 필요한 객체를 컨테이너 내부에서 찾는다.
ac.getBean(MemberRepository.class);
ac.getBean(DiscountPolicy.class);
그런데, DiscountPolicy의 경우 FixDiscountPolicy와 RateDiscountPolicy가 있었다.
두 구현체 모두 DiscountPolicy을 상속한 것이며 일반적으로 컨테이너의 관리를 받을 것이다.
( 필요한 구현체가 변경될 때마다 @Component를 쓰고 지우는 행위는 적절하지 않다. 그럴거면, 그냥 OCP, DIP를 위반하고 구현체를 직접 쓰는 것이 더 수월할 것 같다. )
따라서 DiscountPolicy 타입의 Bean이 두 개 이상 존재하는 상황이며, 이는 조회가 발생함에 에러가 발생한다.
스프링은 이러한 상황을 해결하기 위한 방법을 크게 3가지 제공해준다.
@Autowired 필드 및 파리미터 명 매칭
@Autowired는 먼저 타입 매칭을 시도하고, 만약 2개 이상의 Bean이 있다면 필드 이름, 파라미터 이름으로 Bean 이름을 매칭하도록 작동한다.
@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;
}
해당 코드에서 DiscountPolicy 타입의 변수명이 discountPolicy으로 적혀있는 데,
이를 직접 사용할 구현체의 Bean 이름으로 변수명을 변경한다는 것이다.
예를 들어, RateDiscountPolicy 구현체를 사용할 것이라면, 다음과 같이 코드를 변경하면 된다.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy rateDiscountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
this.memberRepository = memberRepository;
this.rateDiscountPolicy = rateDiscountPolicy;
}
이제 타입 매칭 결과 Bean이 2개 이상이라면, 해당 변수의 이름과 Bean 이름을 매칭하기 위한 시도를 동작한다.
그 결과, 파라미터 or 필드 변수의 이름과 Bean 이름이 일치하면 해당 구현체로 의존 주입을 수행한다.
@Qulifier → @Qulifier끼리 매칭 → 빈 이름 매칭
@Qulifier는 Bean에 추가적인 구분자를 붙여주는 방법이다. ( Bean 이름은 그대로 )
아래와 같이 사용할 수 있다.
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {
각 Bean으로 등록되는 객체에 해당 어노테이션으로 추가 구분자를 지정한다.
이어서 의존 주입이 필요한 객체에서 해당 구분자를 적어 필요한 구현체를 주입한다.
생성자 주입
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
수정자 주입
@Autowired
public DiscountPolicy setDiscountPolicy(@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
그리고 만약 의존 주입이 이뤄지지 않으면 @Autowired와 마찬가지로 Bean 이름을 통해 매칭을 시도하고 계속 실패하면, NoSuchBeanDefinitionException 예외를 발생시킨다.
@Primary 사용
@Primary는 우선순위를 부여하는 방법이다.
만약 Bean이 여러 개 조회된다면, @Primary가 붙은 Bean이 우선적으로 사용된다.
예시로, rateDiscountPolicy가 우선권을 가지고 동작하도록 만들어보자.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
//생성자
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
//수정자
@Autowired
public DiscountPolicy setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
위의 코드를 실행하면, @Primary가 붙은 RateDiscountPolicy가 주입된다.
@Primary, @Qualifier 활용 고려사항
코드에서 자주 사용하는 메인 데이터베이스의 커넥션을 획득하는 스프링 빈이 있고, 코드에서 특별한 기능으로 가끔 사용하는 서브 데이터베이스의 커넥션을 획득하는 스프링 빈이 있다고 생각해보자. 메인 데이터베이스의 커넥션을 획득하는 스프링 빈은 @Primary 를 적용해서 조회하는 곳에서 @Qualifier 지정 없이 편리하게 조회하고, 서브 데이터베이스 커넥션 빈을 획득할 때는 @Qualifier 를 지정해서 명시적으로 획득 하는 방식으로 사용하면 코드를 깔끔하게유지할 수 있다. 물론 이때 메인 데이터베이스의 스프링 빈을 등록할 때 @Qualifier 를 지정해주는 것은 상관없다.
우선순위
@Primary 는 기본값 처럼 동작하는 것이고, @Qualifier 는 매우 상세하게 동작한다. 이런 경우 어떤 것이 우선권을 가져갈까? 스프링은 자동보다는 수동이, 넒은 범위의 선택권 보다는 좁은 범위의 선택권이 우선 순위가 높다. 따라서 여기서도 @Qualifier 가 우선권이 높다.
@ Qualifier 커스텀 어노테이션 만들기
@Qualifier()를 사용하는 경우, @Qualifier("mainDiscountPolicy")처럼 문자열을 직접 입력하여 사용하기에 컴파일시 타입 체크가 안된다.
이를 해결하기 위해, 직접 어노테이션을 만들고 활용하는 방법에 대해 적어보겠다.
즉, 커스텀 @Qualifier() 어노테이션을 만드는 것이다.
이를 위해서 @Qualifier()의 소스코드에 들어가서 설정 정보를 그대로 가져와 MainDiscountPolicy 어노테이션을 만들면 아래와 같은 코드가 구성된다.
@Target({
ElementType.FIELD,
ElementType.METHOD,
ElementType.PARAMETER,
ElementType.TYPE,
ElementType.ANNOTATION_TYPE
})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy{
}
단 @Qualifier("mainDiscountPolicy") 추가적으로 붙여주어서, MainDiscountPolicy 어노테이션을 사용하면 @Qualifier("mainDiscountPolicy") 작동하도록 해준다.
이제 이를 설정하려면 @Qualifier처럼 @MainDiscountPolicy을 붙여주면 된다.
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {}
그리고 의존 관계 주입이 필요한 객체 내부에서도 @Qualifier를 사용하듯 @MainDiscountPolicy 붙여준다.
@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;
}
이처럼 스프링이 관리하는 Bean 중에서 중복 타입을 지닌 Bean들 중 특정 Bean을 사용하기 위한 방법들이 있다.
그런데, 만약 조회한 모든 Bean이 전부 필요하다면 그때는 어떻게 해야할까?
조회한 모든 Bean 활용법( 가변적 활용 )
대게, 어플리케이션을 구현함에 불변성을 가지지만, 그렇지 않은 경우도 있다.
예를 들어, 할인 서비스를 제공하는 데 클라이언트의 요구에 따라 할인 정책을 선택할 수 있다고 해보자.
그럼 모든 특정 정책을 사용한다고 확고하게 단정지을 수 없다.
( 디자인 패턴으로 생각하면, 전략패턴이 필요한 것이다. )
이를 위해 스프링은 Bean들을 자료구조에 담고 동적으로 구현체를 할당할 수 있도록 제공한다.
아래의 코드는 Bean들을 Map과 List에 가져오고 사용하는 방법이다.
public class AllBeanTest {
@Test
void findAllBean() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
DiscountService discountService = ac.getBean(DiscountService.class);
Member userA = new Member(1L, "userA", Grade.VIP);
int discountPrice = discountService.discount(userA, 10000, "fixDiscountPolicy");
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);
}
int discount(Member member, int price, String discountCode) {
// 할인 빈 이름과 매칭
DiscountPolicy discountPolicy = policyMap.get(discountCode); // 빈 이름 필요한 객체 탐색 및 할당
return discountPolicy.discount(member, price); //가져온 DiscountPolicy타입의 객체를 이용해서 할인액 리턴
}
}
}
이는 아래와 같은 원리에 의하여 동작한다.
Map, List에 Bean 주입 원리
동적 할당 원리
'Spring > SpringCore - basic' 카테고리의 다른 글
스프링 핵심 원리 - 빈 스코프와 웹 스코프( Provider와 Proxy 활용 지연처리 ) (1) | 2025.01.14 |
---|---|
스프링 핵심 원리 - Bean 생명주기 콜백 (1) | 2025.01.04 |
스프링 핵심 원리 - ComponentScan의 자동 빈 등록과 빈 탐색 관련 옵션, 빈 중복에 의한 충돌에 대한 고려 (0) | 2024.12.30 |
스프링 핵심 원리 - 싱글톤 컨테이너(stateless 코드 작성의 필요성과 @Configuration의 바이트 조작(CGLIB)을 통한 싱글톤 작동원리) (0) | 2024.12.28 |
스프링 핵심원리 - 스프링 컨테이너 설계 이해, 스프링의 다양한 설정 형식 지원(JAVA/Xml), 스프링 빈 메타 정보(BeanDefinition) (1) | 2024.12.28 |
댓글