Search
Duplicate

의존관계 자동 주입

목차

다양한 의존관계 주입 방법

1. 생성자 주입

: 생성자를 통해 의존관계를 주입받는 방식으로 생성자 호출시점에 딱 1번 호출되는것이 보장된다.
불변, 필수 의존관계에 사용된다.
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; } }
Java
복사
DI된 스프링 빈들이 final 키워드를 붙혀 필수값인경우 생성자 주입을 통해 의존관계주입.
생성자가 단 하나인경우 @Autowired 어노테이션이 없어도 된다.(생략가능)

2. 수정자 주입(Setter 주입)

: Setter를 통해 필드의 값을 수정할 수 있는데(자바 관례인 getter/setter) 이런 수정자 메서드를 통해 의존관계를 주입하는 방법.
선택, 변경가능성이 있는 의존관계에 사용된다.
자바빈 프로퍼티 규약의 수정자 메서드 방식(setter)을 사용하는 방법이다.
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; } }
Java
복사
각각의 수정자 메서드에 @Autowired 애노테이션을 부여해서 의존관계 주입을 한다.
애노테이션이 부여되지않으면 Null 상태.
스프링 컨테이너의 생성과정에서 스프링 빈 등록 이후 스프링 빈 의존관계 설정 준비단계에서 설정하는 단계와 동일하다.
생성자 주입은 스프링 빈에서 해당 클래스를 생성하는 시점에서 생성자를 호출할수밖에없는데 해당 생성자에 파라미더를 전달하기위해서 의존관계를 주입해줘야 한다.
수정자 주입은 아직 의존관계로 주입되야할 스프링 빈이 등록이 되지않아 Null 상태일때도 사용이 가능하다. 그런데 @Autowired 애너테이션은 주입할 대상이 없으면 오류가 발생하는데, 오류가 발생하지 않게하려면 속성으로 (required = false) 지정하면 된다.

3. 필드 주입

: 필드에 바로 주입하는 방식.
@Component public class OrderServiceImpl implements OrderService{ @Autowired private MemberRepository memberRepository; @Autowired private DiscountPolicy discountPolicy; }
Java
복사
접근제어자가 private여도 문제없이 의존관계 주입이 된다.
사용이 몹시 직관적이고 간결해서 좋은 것 같지만, 외부에서의 변경 및 테스트의 어려움이라는 큰 단점이 있다.
DI 프레임워크가 없으면 아무것도 할 수 없다.
고립테스트를 할 때 해당 어노테이션이 있으면 할 수 가 없다.
외부에서의 변경및 테스트가 어째서 어려운가?
⇒ 내가 테스트를 위해 테스트 클래스를 만들어 해당 스프링 빈들(memberRepository, discountPolicy)을 FakeObject를 만들어 고립테스트를 하고싶다고 할 때 @Autowired 어노테이션이 있으면 못하거나 몹시 번거로워진다. 스프링 빈 등록시점에서 FakeObject를 주입할수 없고 그렇기에 따로 setter를 만들어 주입을 해줘야하는데, 그렇게 setter가 만들어지면 그냥 수정자 주입을 하면 된다. 즉, DI 프레임워크가 없으면 사용할 수 없다는 의미다.
써도 되는곳은 없을까?
⇒재활용 가능성이 없거나 적고, 한정적인 테스트 케이스 작성하는 부분에서는 사용해도 문제가 없다. 어짜피 해당 어노테이션은 테스트코드 내에서 해당 테스트의 라이프사이클동안만 사용되니 때문. 그외에 구성영역(AppConfig)에서 생성자 주입시점에서 사용하는 것도 있긴하지만, 굳이 추천하지 않는다. (다른 좋은 방법들이 많기 때문)

4. 일반 메서드 주입

: 일반 메서드를 통해서 주입받을 수도 있다.
생성자처럼 한번에 여러 필드를 주입받을수도 있는데, 일반적으로 사용되지는 않는다.
(생성자 주입이나 수정자주입으로 다 해결되기 때문이다.)
더하여, 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 한다. 스프링 빈으로 등록되지 않은 클래스(ex: Member와 같은 Domain ) 는 주입받을 수 없다.

옵션 처리

지금까지 기본적으로 스프링 빈이 없을 경우 에러가 발생했다.
하지만, 주입할 빈이 없어도 넘어가야 할 시점도 분명히 있는데, 이를 해결하기 위해서 옵션처리를 해줘야한다.
@Autowired(required=false)
: 자동 주입할 대상이 없으면 수정자 메서드 호출이 되지 않는다.
org.springframework.lang.@Nullable
: 자동 주입할 대상이 없으면 Null이 입력된다.
Optional<>
: 자동 주입할 대상이 없으면 Optional.empty가 입력된다.
package hello.core.autowired; import hello.core.member.Member; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.lang.Nullable; import java.util.Optional; public class AutowiredTest { @Test void autowiredOption() { ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class); } static class TestConfig { @Autowired(required = false) public void setBean1(Member member) { System.out.println("setBean1 = " + member); } @Autowired public void setBean2(@Nullable Member member) { System.out.println("setBean2 = " + member); } @Autowired public void setBean3(Optional<Member> member) { System.out.println("setBean3 = " + member); } } }
Java
복사
실행 결과

생성자 주입을 선택하라

많은 의존관계 주입방법에 대해 알아봤다. 그럼 일반적으로 이 많은 방법중 무엇을 써야할까?
요즘 스프링뿐아니라 DI 프레임워크 대부분이 생성자 주입을 권장하고 있는데 그 이유를 알아보면

불변

대부분의 의존관계 주입은 최초 1회 주입후 해당 의존관계가 변경될 일이 없다. (불변)
그런데 수정자주입을 할 경우 setter(setXxx)를 public으로 열어야해서 변경 가능성이 생긴다.
객체 생성시점에서 최초 1회 생성자가 반드시 호출되며 이 상황에서 의존관계까지 주입하기에 불변 설계가 가능하다.

누락

프레임워크에 의존하지 않고 순수 자바로 고립 테스트를 하는 경우 수정자 의존관계 혹은 필드 의존관계인 경우 @Autowired 애노테이션이 스프링 프레임워크 내에서는 문제가 있을경우 오류를 발생하지만, 순수 자바로 짤 경우 실행은 되지만, NullPointException이 발생한다.
아래에 주문서비스를 수정자 주입으로 의존관계를 주입하도록 한 뒤 테스트를 작성해보자.
OrderServiceImpl
@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; } }
Java
복사
OrderServiceImplTest
package hello.core.order; import org.junit.jupiter.api.Test; public class OrderServiceImplTest { @Test void createOrder() { OrderServiceImpl orderService = new OrderServiceImpl(); orderService.createOrder(1L, "itemA", 10000); } }
Java
복사
실행하면 NPE 가 발생한다. 그 이유는 memberRepository와 discountPolicy 의존관계 주입이 누락되었기 때문이다. 해당 에러를 해결하기 위해서는 테스트코드에서 orderService에 직접 의존관계를 다음과 같이 주입해줘야 한다.
public class OrderServiceImplTest { @Test void createOrder() { MemberRepository memberRepository = new MemoryMemberRepository(); memberRepository.save(new Member(1L, "catsbi", Grade.VIP)); OrderServiceImpl orderService = new OrderServiceImpl(); orderService.setMemberRepository(memberRepository); orderService.setDiscountPolicy(new FixDiscountPolicy()); Order order = orderService.createOrder(1L, "itemA", 10000); assertThat(order.getDiscountPrice()).isEqualTo(1000); } }
Java
복사
이제 정상적으로 동작한다. 그런데 이런 테스트 코드를 다시 작성해주기위해서는 주문서비스에 필요한 의존관계와 여기에 관련된 다른 의존관계까지 다 알아야하는데, 생성자가 아닌 수정자이기 때문에 직접 코드를 다 살펴보기전까지는 무슨 의존관계가 필요한지 알 수 없다.
하지만, 생성자 주입을 선택한다면 모든 주입이 생성자 호출시점에 이뤄지고 그렇기에 필드에 final
키워드를 사용해서 무조건 의존관계가 주입되도록 할 수 있다.

롬복과 최신 트랜드

의존관계주입을 일반적으로 생성자 주입 방식을 사용하기로 결정했다.
그럼 이제 매번 필요한 부분에 대해서 다 생성자를 만들어주고 주입받은 값을 대입도 해줘야 한다.
이런 번거로운 작업을 롬복(Lombok)라이브러리를 사용해서 어노테이션으로 해결할 수 있다.

1. 롬복 설정

프로젝트 최초 구성시 의존성 설정에서 롬복을 설정했다면 자동으로 설정되있으니 바로사용하면 된다. 아래 과정은 프로젝트 생성당시에 롬복 설정을 안 한 경우이다.
build.gradle 설정
plugins { id 'org.springframework.boot' version '2.4.3' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'java' } group = 'hello' version = '0.0.1-SNAPSHOT' sourceCompatibility = '11' //lombok 설정 추가 시작 configurations { compileOnly { extendsFrom annotationProcessor } } //lombok 설정 추가 끝 repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter' //lombok 라이브러리 추가 시작 compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testCompileOnly 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok' //lombok 라이브러리 추가 끝 testImplementation 'org.springframework.boot:spring-boot-starter-test' } test { useJUnitPlatform() }
Groovy
복사
Preferences→plugin→lombok 검색&설치
Preferences→Annotatio Processors 검색 → Enable annotation processing 체크
사용 가능
/**Before Case**/ 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; } } /**After Case**/ @Component @RequiredArgsConstructor @Getter public class OrderServiceImpl implements OrderService{ private final MemberRepository memberRepository; private final DiscountPolicy discountPolicy; }
Java
복사
만약 롬복(Lombok)에 대해 더 알고싶다면

조회 빈이 2개 이상 - 문제

의존관계를 자동주입하는 방법에서 자동, 수동 그리고 롬복을 이용한 방법까지 알아봤다.
이런 자동주입에서 주의할 점이 있는데, 같은 타입의 빈이 둘 이상 등록될 경우 문제가 생길수 있는데, @Autowired 애노테이션은 빈을 타입(type)으로 조회한다. 이는 이전에쓰던 ac.getBean(xxxx.class)와 유사한데, 스프링 빈 조회할 때 발생했던 문제와 동일하게 타입으로 조회시 선택된 빈이 둘 이상이면 문제가 발생한다.
예제프로젝트에서 할인정책(DiscountPolicy)의 구현체인 FixDiscountPolicty, RateDiscountPolicy가 둘 다 스프링 빈으로 등록할 경우 해당 문제를 만날 수 있다. 코드를 작성해보자.
/**FixDiscountPolicy.class**/ @Component public class FixDiscountPolicy implements DiscountPolicy{ ... } /**RateDiscountPolicy.class**/ @Component public class RateDiscountPolicy implements DiscountPolicy{ ... } public class AutoAppConfigTest { @Autowired private DiscountPolicy discountPolicy; ... }
Java
복사
위와같이 각 할인정책을 모두 스프링 빈으로 등록한 후 @Autowired로 자동주입을 받으려고 시도하면 다음과 같은 결과가 나온다.
NoUniqueBeanDefinitionException: No qualifying bean of type 'hello.core.discount.DiscountPolicy' available: expected single matching bean but found 2: fixDiscountPolicy,rateDiscountPolicy
Java
복사
해석하면 하나의 빈을 기대했는데, fixDiscountPolicy,rateDiscountPolicy 두 개의 빈이 발견되었다는 에러다. 하지만, 그렇다고 매번 사용할 하나의 정책만 스프링 빈으로 등록해서 사용하는것은 안되고, 어떤식으로 해결해야 할까? 그 방법으로는 @Autowired가 부여된 필드의 이름, @Qualifer 애노테이션, @Primary 애노테이션을 사용해 해결할 수 있다.

@Autowired 필드 명, @Qualifer, @Primary

@Autowired 필드명

@Autowired 애노테이션은 의존관계 자동주입을 타입 매칭을 통해 주입한다. 그런데 이 때 빈이 여러개가 검색된다면 필드명으로 빈 이름을 추가적으로 매칭한다. 즉 할인정책(DiscountPolicy) 타입으로 스프링 빈을 검색하였을 때 fixDiscountPolicy,rateDiscountPolicy 두 개의 빈이 검색된다면 그 다음으로 필드명으로 빈을 추가적으로 매칭한다.
public class AutoAppConfigTest { //@Autowired //private DiscountPolicy discountPolicy; @Autowired private DiscountPolicy rateDiscountPolicy; ... }
Java
복사
위와같이 필드명을 rateDiscountPolicy로 바꾼뒤 테스트를 실행해보면, 에러없이 정상 작동하는 것을 볼 수 있다. 여기서 필드 명 매칭은 타입 매칭을 먼저 시도하고 그 이후 빈이 2개 이상 있을때 추가로 동작하는 기능이다.
정리하면 @Autowired 매칭은 우선, 타입으로 매칭을 시도하고 매칭결과가 2개 이상인경우 빈 이름으로 매칭을 시도한다.

@Qualifer

다음으로는 @Qualifer 애노테이션을 붙혀 추가 구분자를 붙혀주는 방식이다.
이때, 빈 이름이 변경되는 것이아닌 주입시 추가적인 방법으로 매칭시켜주는 것이지 빈 이름이 바뀌거나 하는 것은 아니다.
@Component @Qualifier("myDiscountPolicy") public class FixDiscountPolicy implements DiscountPolicy{ ... } @Component @RequiredArgsConstructor public class OrderServiceImpl implements OrderService{ private final MemberRepository memberRepository; @Qualifier("myDiscountPolicy") private final DiscountPolicy discountPolicy; ... }
Java
복사
이제 OrderServiceImpl 빈에 의존관계 주입을 할 때 할인 정책은 Qualifier로 등록된 이름을 통해 매칭한다.
필드뿐아니라 생성자및 수정자에도 파라미터에 해당 애노테이션을 부여해서 매칭 할 수 있다.
Component뿐 아니라 @Bean을 직접 등록하는 경우에도 동일하게 사용 가능하다.
매칭되는 빈을 찾지 못한다면 NoSuchBeanDefinitionException 예외가 발생한다.
해당 기능을 좀 사용자 애노테이션을 만들어 더 편리하게 관리할 수 있다 다음 섹션에서 그 방법을 학습한다.

@Primary 사용

애노테이션의 이름 뜻 그대로 우선순위를 정하는 방법이다. @Autowired를 통해 여러 빈이 매칭될 경우 해당 @Primary 애노테이션을 가지고 있는 빈이 우선권을 가진다.
/**FixDiscountPolicy.class**/ @Component public class FixDiscountPolicy implements DiscountPolicy{ ... } /**RateDiscountPolicy.class**/ @Component @Primary public class RateDiscountPolicy implements DiscountPolicy{ ... } public class AutoAppConfigTest { @Autowired private DiscountPolicy discountPolicy; ... }
Java
복사
이렇게 @Primary 애노테이션이 붙으면 테스트 클래스에서 DiscountPolicy 타입으로 빈을 조회할때 검색되는 FixDiscountPolicy, RateDiscountPolicy 두 개의 빈중 @Primary 애노테이션을 가지고있는 RateDiscountPolicy가 우선권을 가지게 되어 해당 빈이 자동주입된다.
@Primary는 @Qualifier와는 다르게 양쪽에 모두 애노테이션을 붙혀주지 않아도 된다는 점에서 장점을 가지고 있다.
그럼 @Primary, @Qualifer 두 애노테이션간의 우선순위는 어느것이 더 높을까?
일반적으로 자동보다는 수동, 넓은범위보다는 좁은범위, 자유보다는 제약조건이 더 강한 힘을가지고 우선순위를 가진다. 그렇기에 직접 이름을 지정해 매칭하는 @Qualifer가 우선순위를 가진다.

애노테이션 직접 만들기

@Qualifier 애노테이션의 이름을 부여하여 매칭을 하는데 이 이름은 문자열이기에 컴파일시 타입 체크가 안된다.
그래서 한쪽에서는 @Qualifier("myService") 다른 한쪽에서는 @Qualifier("mySrvice") 이런식으로 실수를 할 수가 있는 것이다. 그래서 이런 경우 애노테이션을 직접 만들어서 해결해보자.

1. 애노테이션 정의

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 { }
Java
복사
이 애노테이션은 필드, 메서드, 파라미터, 타입, 애노테이션타입에 전부 사용 가능하다.
런타임시에 동작한다.
상속이 된다.
@Qualifer동작을 한다.

2. 적용

@Component @MainDiscountPolicy public class RateDiscountPolicy implements DiscountPolicy { ... } @Component @RequiredArgsConstructor public class OrderServiceImpl implements OrderService{ private final MemberRepository memberRepository; @MainDiscountPolicy private final DiscountPolicy discountPolicy; ... }
Java
복사

But,

이대로 동작해보면 분명 에러가 날 것이다. 애노테이션 생성자주입방식을 쓴다면 발생하지 않을 에러인데, 여기서는 롬복을 사용해 (@RequiredArgsConstructor) 작동하기 때문이다. 이는 해당 롬복 애노테이션이 애노테이션을 포함해서 생성자를 만들지 않기 때문이라서 사용자 정의 애노테이션 뿐아니라 @Qualifer를 사용했어도 동일하게 안되었을 것이다. 그렇기 때문에 롬복 설정을 만져줘야 한다.
1.
src/main/java/lombok.config 파일 생성
2.
파일 내 다음 설정 적용
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier lombok.copyableannotations += hello.core.annotation.MainDiscountPolicy
YAML
복사
해당 설정에서는 Qualifier와 MainDiscountPolicy 애노테이션이 포함되도록 설정.
필요에따라 기호에 맞게 설정하면 된다.
3.
IntelliJ인 경우 out폴더 삭제/ gradlew 인 경우 clean 실행
4.
다시 컴파일

조회한 빈이 모두 필요할 때, List, Map

지금까지는 자동주입하기위해 스프링 빈 검색해서 2개 이상 나오는 경우 하나를 어떻게 골라서 주입할지에 대해 알아보았다. 하지만, 반대로 2개이상 나올경우 모든 스프링 빈을 조회해야하는 경우도 있다.
(Ex: 할인정책에 동적으로 변경)
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); int discount = discountService.discount(new Member(1L, "catsbi", Grade.VIP), 10000, "fixDiscountPolicy"); assertThat(discount).isEqualTo(1000); } 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("policyMap = " + policyMap); } 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); } } }
Java
복사
DiscountService에서 생성자가 Map, List 타입이니 해당 콜렉션의 지네릭 타입에 매칭되는 모든 빈들이 주입된다.
discount 메서드에서 인자로 받은 discountCode를 스프링 빈 이름으로 policyMap에서 꺼내 걸맞는 할인 정책을 반환하고 이 정책의 discount 메서드를 호출한다.
만약, 해당 타입에 맞는 빈이 없다면 빈 컬렉션이나 Map을 주입한다.

자동, 수동의 올바른 실무 운영 기준

결론부터 말하면 자동기능을 최대한 활용하자.
스프링 프로젝트 생성시 자동생성되는 루트 클래스부터 @SpringBootApplication 애노테이션이 있는데, 이 안에는 컴포넌트 스캔이 붙어있고 @Controller, @Service, @Repository 와 같이 계층별로 맞는 애플리케이션 로직을 자동으로 스캔할수 있다.
그리고 @Component면 자동으로 스프링 빈으로 등록되는 상황에서 직접 @Configuration 설정정보에 가 빈을 직접 생성해서 객체를 생성후 의존관계 주입까지 다 수동으로 하는것은 비용 소모가 상당하다.
그렇기에 자동 기능을 최대한 활용하고 그렇게 하 더라도 OCP, DIP 원칙은 거진 다 지켜진다.

그럼에도 불구하고 수동 빈 등록을 사용하는 시점.

패턴의 유사성이 있는 업무 로직이 아닌 기술 지원 로직은 업무 로직과 비교할 때 많지도 않고, 애플리케이션 전반에 영향을 미친다. 그리고 기술 지원 로직은 문제의 노출이 제대로 안되기때문에 이런 기술 지원 로직의 경우 가급적 수동 빈 등록을 사용해 명시적으로 드러내는 것이 좋다.
즉, 애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록하는게 유지보수에 좋다.
또 하나, 비즈니스 로직 중 다형성의 활용이 많은 경우
할인정책이나 기타 특정 인터페이스의 구현체로써 많은 다형성을 활용하는 경우 자동으로 할 경우 같은 패키지에 묶어놓거나 수동 빈으로 등록을 해야 가독성및 인수인계에 유리하다.
자동으로 할 경우
특정 패키지에 묶어서 한곳에 모아둔다.
수동으로 할 경우
@Configuration public class DiscountPolicyConfig { @Bean public DiscountPolicy rateDiscountPolicy(){ return new RateDiscountPolicy(); } @Bean public DiscountPolicy fixDiscountPolicy(){ return new FixDiscountPolicy(); } }
Java
복사

이전 챕터로

다음 챕터로