[Spring] 핵심원리 기본편 - 의존관계 자동 주입
Java & Spring

[Spring] 핵심원리 기본편 - 의존관계 자동 주입

컴포넌트 스캔에 대해 배운 저번 챕터에 이어 이번 챕터는 의존관계에 대한 내용이다.

 

다양한 의존관계 주입 방법

의존관계 주입에는 크게 4가지 방법이 있다. 하나하나씩 살펴보자.

(생성자 주입. 수정자 주입 (setter 주입). 필드 주입. 일반 메서드 주입)

 

생성자 주입

우리가 지금까지 사용했던 방법이다.

말 그대로 생성자를 통해서 의존 관계를 주입 받는 방법이다.

@Component
public class MemberServiceImpl implements MemberService {

	private final MemberRepository memberRepository;

	@Autowired
	public MemberServiceImpl(MemberRepository memberRepository) {
		this.memberRepository = memberRepository;
	}

    ...
}

 

컴포넌트 스캔을 할 때 @Component를 보고 MemberServiceImpl 이 스프링 빈에 등록이 된다. 이때, 생성자가 호출되면서 @Autowired가 생성자에 붙어져 있는 것을 보고, MemberRepository이라는 스프링 빈이 스프링 컨테이너에서 꺼내와져서 의존성 주입이 된다.

 

생성자 주입을 통해 의존성 주입을 하면 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다. 그래서 불변, 필수 의존관계에 사용한다. (구현체가 클래스 내에서 바뀌지 않는) 위에서 보면 memberRepository는 생성자 외에 다른 곳에서 수정되거나 추가될 여지가 없어보인다. 이렇게 불변하는 의존관계에 사용하면 좋다.

 

참고) 생성자가 1개만 있다면, @Autowired를 생략해도 된다. 스프링 빈인 경우에만!

 

수정자 주입 (setter 주입)

아래와 같이 setxxx 메서드를 통해 받는 방식이다.

수정자를 통해 주입받는 방식은 생성자 주입 방식과 다르게 '선택, 변경'이 가능한 의존관계에 사용한다.

자바빈 프로퍼티(set, get으로 메서드 수정하거나 읽는 규칙) 규약의 수정자 메서드 방식을 사용하는 방법이다.

@Component
public class MemberServiceImpl implements MemberService {

	private MemberRepository memberRepository;

        @Autowired
	public void setMemberRepository(MemberRepository memberRepository) {
		this.memberRepository = memberRepository;
    }
    
    ...
}

 

필드 주입

필드에 바로 값을 넣어버리는 방식이다.

코드가 간결해서 좋아보이지만 외부에서 변경이 어려워 테스트가 힘들어서 권장되지 않는 방식이다.

@Component
public class MemberServiceImpl implements MemberService {

	@Autowired private MemberRepository memberRepository;
    
	...
}

 

일반 메서드 주입

일반 메서드를 통해서 주입받는 방식이다.

한번에 여러 필드를 받을 수 있지만 권장되지 않는 방법이다.

@Component
public class MemberServiceImpl implements MemberService {

	private MemberRepository memberRepository;
	
	@Autowired
	public void init(MemberRepository memberRepository) {
		this.memberRepository = memberRepository;
        
        ...
}

옵션 처리

이번 파트에서는 주입할 스프링 빈이 없을 경우 주는 옵션에 대해 설명한다.

우리가 위에서처럼 @Autowired 만 사용해서 의존성을 주입하면 required = true가 디폴트로 들어가기 때문에 자동 주입 대상이 없으면 오류가 발생한다.

 

처리 방법은 다음과 같다.

  • @Autowired(required=true): 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출이 안됨
  • org.springframework.lang.@Nullable: 자동 주입할 대상이 없으면 null이 입력됨
  • Optional<>: 자동 주입할 대상이 없으면 Optional.empty가 입력됨
public class AutowiredTest {

	@Test
	void AutowiredOption() {
		AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
	}

	static class TestBean {
            // 첫번째 케이스: 호출이 안됨
		@Autowired(required = false)
		public void setNoBean1(Member noBean1) {
			System.out.println("noBean1 = " + noBean1);
		}
        
            // 두번째 케이스: null로 출력됨
		@Autowired
		public void setNoBean2(@Nullable Member noBean2) {
			System.out.println("noBean2 = " + noBean2);
		}

            // 세번째 케이스: Optional.empty 출력됨
		@Autowired
		public void setNoBean3(Optional<Member> noBean3) {
			System.out.println("noBean3 = " + noBean3);
		}
	}
}

출력결과

위의 예제에서 보면 Member를 @Autowired로 의존성 주입을 해주는 메서드들인데, Member라는 빈은 존재하지 않는다. 따라서 각 옵션에 따라 맞는 값이 출력되는 것을 알 수 있다.


생성자 주입을 선택해라!

생성자 주입을 왜 선택해야할까? 왜 생성자 주입을 권장할까? 이번 파트에서 알아보자.

 

1. 불변

위에서 생성자 주입의 특징이 불편, 필수 의존관계에 주로 사용한다고 했다. 근데 사실 대부분의 의존관계 주입은 한번 일어나면 앱 종료시점까지 의존관계를 변경할 일이 없고 없는게 좋다. 그래서 생성자 주입이 권장된다.

 

2. 누락 방지

순수 자바코드로 테스트를 할 때 생성자가 아닌 수정자 주입 방식으로 하면 의존관계 주입을 누락시킬 가능성이 있다. 생성자 주입 방식으로 하면 컴파일 오류가 미리 발생하기 때문에 오류를 미리 알 수 있다.

 

3. final 키워드

생성자 주입을 사용하면 private final MemberInterface; 와 같이 final 키워드를 넣어서 혹시라도 값이 설정되지 않는 오류를컴파일 시점에 막아준다.

 

결론

여러가지 이유로 생성자 주입이 권장되는 이유를 알아봤다.

좋은 방식은 생성자 주입을 주로 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하는 것이다.

(+ 필드 주입은 사용하지 않는게 좋다.)

 


롬복과 최신 트랜드

롬복을 사용하면 생성자 주입을 좀더 편리하게 할 수 있다.

우선 gradle 파일에 롬복 라이브러리 추가 설정을 해준다.

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

dependencies {
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
}

+ Lombok 플러그인도 설치해준다.

+ preference에 annotation processors 검색해서 Enable annotation processing 체크 버튼을 활성화해준다.

이제 intellij에서 롬복을 쓸수 있게 되었다.

롬복은 간단하게 말하자면 @Getter, @Setter 등을 annotation 한줄만으로 간단하게 생성해주는 편리한 라이브러리이다.

 

한번 사용해보자.

@RequiredArgsConstructor는 final이 붙은 필수적인 필드가 인자로 들어간 생성자를 만들어주는 annotation이다.

아래와 같이 @RequiredArgsConstructor만 붙이면 주석처리된 생성자를 자동으로 만들어준다.

@RequiredArgsConstructor
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;
	// }

윈도우는 ctrl + f12, 맥은 cmd + f12를 눌러보면 롬복 annotation에 의해 생성된 생성자가 들어가있다는 것을 확인할 수 있다.


조회 빈이 2개 이상 - 문제

@Autowired를 하는데 빈이 2개인 경우를 살펴보자.

 

@Autowired는 ac.getBean(DiscountPolicy.class)와 유사하게 동작한다. 그럼 만약에 DiscountPolicy의 하위 타입(구현체)가 FixDiscountPolicy, RateDiscountPolicy 이렇게 두개고 둘다 스프링 빈에 등록이 되어있다면 어떻게 될까?

 

NoUniqueBeanDefinitionException 오류가 발생한다. 하나의 빈을 기대했는데 2개가 발견되었다는 의미이다.

이제 다음 파트에서 의존관계 자동 주입에서 이를 어떻게 해결하는지 알아보자.

 


@Autowired 필드 명, @Qualifier, @Primary

NoUniqueBeanDefinitionException 오류가 발생했을 때 해결할 수 있는 방법 3가지를 알아보자.

 

@Autowired 필드 명 매칭

@Autowired에는 ac.getBean처럼 처음에는 타입 매칭으로 스프링 빈을 찾기를 시도한다. 그런데 만약 2개 이상이 존재한다면, 필드 이름, 파라미터 이름으로 추가 매칭한다.

예를 들어서

@Autowired
private DiscountPolicy discountPolicy;

가 아니라

@Autowired
private DiscountPolicy rateDiscountPolicy;

라고 작성하는 것이다. 그러면 처음에 DiscountPolicy로 빈을 찾고 -> 중복된 빈을 발견하고 -> rateDiscountPolicy라는 이름으로 알맞은 빈을 찾아나가게 된다.

 

@Qulifier끼리 매칭 -> 빈 이름 매칭

@Qulifier는 추가 구분자를 붙여주는 방법이다. 즉, 부가적인 이름을 붙여준다고 생각하면 된다.

@Component
@Qualifier("mainDiscountPolicy") // 이렇게 정의해주고
public class RateDiscountPolicy implements DiscountPolicy {
...
}

@Autowired
public OrderServiceImpl(@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) { // 이렇게 씀
...
}

 

@Primary 사용

@Primary는 우선순위를 정하는 방법이다. 의존성 주입시에 여러 빈이 존재한다면 @Primary가 붙은 스프링 빈이 우선권을 가진다. 자주 사용하는 방법이다.


애노테이션 직접 만들기

위처럼 @Qualifier("mainDiscountPolicy") 이렇게 문자로 적으면 컴파일시 타입 체크가 안된다. 실행해야 결과를 알 수 있다. 그래서 애노테이션을 만들어서 이를 해결한다.

 

먼저 아래와 같이 annotation을 만들어주고

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}

아래와 같이 annotation을 붙여주면 오타가 나도 컴파일 단계에서 오류가 발생해 문제를 사전에 방지할 수 있다.

@Component
@MainDiscountPolicy // 이렇게 정의해주고
public class RateDiscountPolicy implements DiscountPolicy {
...
}

@Autowired
public OrderServiceImpl(@MainDiscountPolicy DiscountPolicy discountPolicy) { // 이렇게 씀
...
}

 


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

만약 위에서 선택지에 있었던 rateDiscountPolicy, fixDiscountPolicy 모두를 필요로 한다면 어떻게 될까? 만약 클라이언트가 둘 중 하나를 선택할 수 있게 하려면?

여기서 스프링을 사용하면 전략 패턴을 간단하게 구현할 수 있다.

 

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);

		int rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
		assertThat(rateDiscountPrice).isEqualTo(2000);
	}

	static class DiscountService {
		private final Map<String, DiscountPolicy> policeMap;
		private final List<DiscountPolicy> policies;

		@Autowired
		public DiscountService(Map<String, DiscountPolicy> policeMap, List<DiscountPolicy> policies) {
			this.policeMap = policeMap;
			this.policies = policies;
			System.out.println("policyMap = " + policeMap);
			System.out.println("policies = " + policies);
		}

		public int discount(Member member, int price, String discountCode) {
			DiscountPolicy discountPolicy = policeMap.get(discountCode);
			return discountPolicy.discount(member, price);
		}
	}
}

위의 코드를 보자.

우선 DiscountService라는 클래스에서는 생성자 주입을 통해 DiscountPolicy의 모든 하위 빈들(fixDiscountPolicy, rateDiscountPolicy)을 policeMap에 주입받는다. 그리고 List로 정의된 policies에도 동일하게 모든 빈들이 들어간다. 아래 출력을 통해 알 수 있다.

그리고 DiscountService의 discount 메서드를 호출할 때는 discountCode로 어떤 빈을 선택해서 사용할지 입력을 받아서 그 빈을 선택하고 있는 것을 policeMap.get(discountCode)를 통해 알 수 있다.


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

자동 빈 등록과 수동 빈 등록 중 어떤걸 사용하는게 좋을까?

 

애플리케이션은 크게 업무 로직과 기술 지원 로직으로 나눌 수 있다.

업무 로직은 컨트롤러, 서비스, 레포지토리 등이 포함되고, 기술 지원 로직은 기술적인 문제가 공통 관심사를 처리할 때 주로 사용된다. DB연결이나 공통 로그 처리가 포함된다.

 

여기서 업무 로직은 유사한 패턴이 많고 정형화된 곳이 많기 때문에 문제가 발생하더라도 원인 파악이 쉬워 자동 빈 등록을 사용하는게 좋다.

 

반면에 기술 지원 로직은 업무 로직에 비해 수가 적고, 광범위하게 영향을 미쳐 문제가 발생했을 때 파악이 어려워 수동 빈 등록을 사용하는게 좋다.

 

다만 업무 로직에서도 수동 빈 등록을 사용하는게 나은 경우가 있다. 바로 다형성을 활용할 때이다. 위에서 설명했던 List, Map을 사용해서 여러 빈들을 주입받을 경우를 생각해보자. 이렇게 코드가 짜여져 있다면 한눈에 어떤 빈이 주입될지 알기 쉽지 않다. 따라서 이런 경우는 수동 빈으로 등록하거나 자동으로 하면서 특정 패키지에 같이 묶어 두는 게 좋다.


정리

의존성 주입에 대해 알아봤다. 의존성 주입의 종류와 어떤 걸 사용하면 좋은지, 그리고 의존성 주입을 사용했을 때 나타나는 문제상황과 해결법들을 배웠다.

728x90
반응형