본문 바로가기
개발자 일지/Spring

[인프런 김영한 로드맵2]스프링 핵심 원리 정리(5)

by 네빌링 2022. 3. 5.
반응형

-인프런 김영한 강사님의 스프링 핵심 원리를 정리한다.

-의존관계 주입의 개념, 방법 등에 대해 알아본다.

-모든 소스는 깃허브에서 관리한다.(https://github.com/coderahn/Spring-Lecture2)


7.의존관계 자동 주입

 

1.다양한 의존관계 주입 방법

 

크게 4가지 방법이 있다.

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

 

생성자 주입이 가장 추천된다. 생성자 호출 시점에 1번만 호출되며 불변객체로 유지된다. 또한 생성자가 1개인 경우 @Autowired를 생략할 수 있다. 생성자 주입 코드는 다음과 같다.

 

OrderServiceImpl.java의 생성자 주입

 

수정자 주입은 setter를 통해 주입한다. 컴파일 이후에도 변경 가능성이 있기 때문에 선택, 변경 가능성이 있는 의존관계에 사용된다.

 

필드 주입은 필드에 바로 주입하는 방법이다. 실무에서 이렇게 많이 쓰는데 실제로 좋은 방법은 아니라고 한다. 코드가 간결해지지만 외부에서 변경이 불가능해 테스트가 힘들다. DI프레임워크가 없으면 아무 것도 할 수 없다. 

 

수정자 주입과 필드 주입은 다음의 예제 코드 스샷과 같다.

 

OrderServiceImpl.java의 수정자 주입과 필드 주입

 

일반 메서드 주입은 한번에 여러 필드를 주입받을 수 있지만 잘 사용하지 않는다.

 

 

2.옵션 처리

 

스프링 빈이 없어도 동작이 필요할 때가 있다. 이럴 때 그냥 컴파일을 하면 빈이 null로 들어와서 오류가 발생한다. 이 때 @Autowired의 옵션과 다른 옵션 처리 방법을 사용한다. 다음과 같이 3가지 방법이 있다.

 

@Autowired(required=false)는 호출 자체가 안 된다.

 

 

3.생성자 주입을 선택해라!

 

생성자 주입이 가장 권장된다. 다음과 같이 3가지 이유가 있다.

 

1)불변 

  • 의존관계 주입이 한 번 일어나면 애플리케이션 종료 시점까지 의존관계를 변경할 일이 없다. 오히려 의존관계는 애플리케이션 종료 전까지 변하면 안 된다.(불변해야 함)
  • 수정자 주입 사용시 setXXX등으로 메서드를 public으로 열여둬야 한다. 누군가 실수로 변경할 수 도 있고 의도적으로 변경할 수 있기 때문에 좋은 설계가 아니다.
  • 생성자 주입은 객체 생성시 1번만 호출되기 때문에 이후 호출할 일이 없다. 불변 설계가 가능하다.

 

2)생성자 주입은 누락의 문제를 막을 수 있다.

  • 생성자 주입은 주입 데이터를 누락했을 때 컴파일 오류를 발생시켜서 에러를 미리 예방할 수 있다.
  • 생성자 주입이 아닌 setter 주입의 경우 다음과 같이 자바테스트코드를 작성하면 실행은 되지만 널포인터가 발생할 것이다.

 

//실행은 되지만 createOrder 내부에서 memoryMemberRepository 등의 메소드를 호출할 때 NPE가 발생
@Test
void createOrder() {
	OrderServiceImpl orderService = new OrderServiceImpl();
    orderService.createOrder(1L, "itemA", 10000);
}

 

 

3)final 키워드를 사용할 수 있다.

  • 생성자 주입 사용시 필드에 final 키워드를 사용할 수 있다. 생성자에서 값 설정이 안 되어 있을 때 컴파일 오류를 발생시킬 수 있어 좋다,
  • 수정자 주입을 포함한 나머지 방식은 생성자 이후 호출되기 때문에 final 키워드를 사용할 수 없다.

 

4.롬복과 최신 트랜드

 

의존관계 주입시 생성자 주입 방식이 선호되지만 코드가 길어지고 귀찮아진다. 롬복 라이브러리를 사용하면 생성자 코드 없이 컴파일 시점에 생성자 호출이 가능하다.

 

다음과 같이 @RequiredArgsConstructor을 붙여주면 된다.

 

memberRepository, discountPolicy에 인스턴스 참조값이 자동으로 주입된다.

 

5.조회 빈이 2개 이상 - 문제

 

@Autowired는 타입으로 조회한다. 타입이 DiscountPolicy인 경우, ac.getBean(DiscountPolicy.class)처럼 동작한다. 그러나 자식타입으로도 인식을 하기 때문에 FixDiscountPolicy, RateDiscountPolicy 중 어떤 것으로 가져와야 할지 모른다. 이런 경우 자식타입으로 선언하면 DIP를 위배하기 때문에 좋지 않다. 이를 해결하는 여러 방법이 존재한다.

 

6.@Autowired 필드명, @Qualifier, @Primary

 

조회 대상 빈이 2개 이상인 경우 해결할 수 있는 3가지 방법이 있다.

 

첫번째는 @Autowired 필드 명을 매칭하는 것이다. 필드명 매칭은 먼저 타입 매칭을 시도하고 여러 조회 빈이 있는 경우 필드명으로 매칭한다. 다음과 같다.

//@Autowired
//private DiscountPolicy discountPolicy;

@Autowired
private DiscountPolicy rateDiscountPolicy;

 

두번째는 @Qualifier를 사용하는 것이다. 빈 이름을 변경하는 것이 아니라 주입시 추가적인 방법을 제공하는 것이다.

 

//RateDiscountPolicy.java

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {
//...
}

//FixDiscountPolicy.java

@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {
//...
}

 

그리고 주입시 생성자에 주입받을 @Qualifier를 다음과 같이 써준다.

 

//OrderServiceImpl.java

@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy 
discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}

 

주입시에 @Qualifier("mainDiscountPolicy)를 못찾으면 mainDiscountPolicy라는 이름의 스프링 빈을 찾는다. 그러나 추천하지 않는다.

 

마지막으로 @Primary를 사용하는 방법이 있다. @Autowired에 여러 빈이 매칭되면 @Primary가 붙은 스프링 빈이 우선권을 갖는다.

 

//RateDiscountPolicy.java
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

//FixDiscountPolicy.java
@Component
public class FixDiscountPolicy implements DiscountPolicy {}

 

주입시에 생성자 등에 아무런 처리가 필요 없다.

 

//OrderServiceImpl.java

//생성자
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}

 

@Primary와 @Qualifier 중 @Qualifier가 우선권을 갖는다. 스프링은 자동보다 수동이, 넓은 범위 선택권 보다 좁은 범위 선택권이 우선권이 높다.

 

7.애노테이션 직접 만들기

 

@Qualifier("mainDiscountPolicy")와 같이 문자로 적으면 컴파일 때 타입 체크가 안 된다. 다음과 같이 어노테이션을 만들어 해결 가능하다.

 

우선 어노테이션을 만든다.

MainDiscountPolicy.java / 내부에 @Qualifier("mainDiscountPolicy")를 상속하고 있어서 @Qualifier 기능 사용 가능

 

어노테이션을 적용할 컴포넌트에 어노테이션을 붙여준다. 컴파일시 체크 가능하다.

RateDiscountPolicy.java / @MainDiscountPolicy는 컴파일 때 체크가 가능하게 변경 되었다.

 

주입 받을 생성자에도 어노테이션을 붙여준다.

 

OrderServiceImpl.java

 

어노테이션은 상속 개념이 없다. 스프링이 제공하는 기능이다.

 

 

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

 

조회한 빈이 2개 이상인 경우 구분하기 위한 방법들을 위에서 살펴 봤는데, 모든 조회한 빈이 필요할 수도 있다.

사용자가 할인정책을 선택하는 경우(rate, fix)를 예시로 들 수 있겠다.

 

우선 테스트하기 전에 Map과 List로 받는 static 설정정보를 다음과 같이 만든다.

 

AllBeanTest.java

 

DiscountPolicy 타입은 RateDiscountPolicy, FixDiscountPolicy 2개이다. policyMap, policies에는 2개가 주입될 것이다.

discount()는 할인정책을 사용하기 위한 컨텍스트 메소드이다.

 

이제 테스트 코드를 다음과 같이 작성한다.

 

AllBeanTest.java

 

 AutoAppConfig.class와 DiscountService.class 둘다 설정정보로 등록하여 DiscountService가 빈생성시 생성자에서 DiscountPolicy 빈을 조회할 수 있도록한다. 그리고 DiscountService를 getBean으로 가져와 discount를 호출할 수 있게 한다. 3번째 인자로 "fixDiscountPolicy"와 "rateDiscountPolicy"의 이름을 넣어서 호출하면 discount 메소드가 이름에 맞게 값을 가져온다. 전략패턴의 방식을 사용한 것이다.

 

테스트코드 실행시 DiscountService가 빈등록되면 다음과 같이 콘솔로그를 확인할 수 있다.

policyMap = {fixDiscountPolicy=hello.core.discount.FixDiscountPolicy@61a5b4ae, rateDiscountPolicy=hello.core.discount.RateDiscountPolicy@3a71c100}
polices = [hello.core.discount.FixDiscountPolicy@61a5b4ae, hello.core.discount.RateDiscountPolicy@3a71c100]

 

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

 

빈등록은 자동등록방식과 수동등록방식으로 나눌 수 있다. 지금까지 살펴본 @ComponentScan을 통해 @Component를 전체조회하여 빈등록하는 간편한 방식이 자동등록방식이다. @Configuration을 사용하여 @Bean을 개별적으로 등록하는 것은 수동등록방식이다.

 

최근에는 자동등록방식이 선호된다. 간편하고 스프링부트도 컴포넌트 스캔을 기본으로 사용한다. 업무로직빈들은 패턴이 동일하기 때문에 자동등록방식을 사용하는 게 선호된다. 반면 AOP등의 기술로직빈은 애플리케이션 전반에 걸쳐 광범위하기 적용되기 때문에 가급적 수동등록방식으로 명확하게 드러내는 것이 좋다.

 

다만 업무로직빈들 중 다형성을 활용해야 하는 경우는 수동등록방식이 추천된다. 위의 List, Map을 활용하여 조회된 빈들을 받는 곳에서 자동등록을 사용하면 어떤 빈들이 주입되는지 알기 쉽지 않다. 이런 경우 명시적으로 수동등록방식을 사용하면 다른 개발자들이 파악하기에 좋다.

 

 

반응형