[스프링 핵심 원리] - 의존관계 자동 주입

peeerr·2023년 1월 31일
0

Spring

목록 보기
6/10
post-thumbnail

이 글은 김영한님의 스프링 핵심 원리 - 기본편 강의를 수강하고 정리한 내용입니다.
강의 보러가기


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


의존관계 주입은 크게 4가지가 방법이 있다.

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

1) 생성자 주입

  • 생성자를 통해서 주입 받는 방법이다.

    • 우리가 이제까지 써온 방법이다.
  • 생성자 호출 시점에 딱 한번만 호출되는 것이 보장된다.

  • 불변, 필수 의존관계에 주로 사용된다.

예시

private final MemberRepository memberRepository;

//@Autowired
public MemberServiceImpl(MemberRepository memberRepository) {
    this.memberRepository = memberRepository;
}
  • memberRepository는 생성자 호출할 때 값이 정해지고 값이 바뀌지 않는 불변 객체이다.

  • 또, final 키워드가 붙어 있기 때문에 값이 무조건 존재해야 하는 필수 객체이다.

  • 생성자가 하나만 있을 경우 @Autowired를 생략할 수 있다.

2) 수정자 주입

  • 스프링 컨테이너 생성과정은 빈 등록 단계의존관계 주입 단계로 나뉜다.

    • 생성자 주입은 빈을 등록할 때 같이 일어난다. (빈 등록 시 생성자 호출이 필요하기 때문)

    • 수정자 주입은 의존관계 주입 단계에서 일어난다.

  • 선택, 변경 가능성이 있는 의존관계에 사용한다.

예시

private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;

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

@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
    this.discountPolicy = discountPolicy;
}

변경 예시

orderService.setMemberRepository(new MemoryMemberRepository());
  • 관례상 메서드명은 setXxx 형식으로 정한다.

  • 의존관계가 주입되어 있다가 다른 구현체로 바꾸고 싶을 때 바꿀 수 있다.
    -> OCP 위반 가능성이 높다.

  • 잘 사용하지 않는다.

3) 필드 주입

바로 예시부터 보자

@Component
public class OrderServiceImpl implements OrderService {
    
    @Autowired private MemberRepository memberRepository;
    @Autowired private DiscountPolicy discountPolicy;
    
}
}
  • 이름 그대로 필드에 바로 주입하는 방법이다.

  • 예를 들어 OrderServiceImplmemberRepository 구현체를 다른 클래스로 바꾸고 싶어도 바꿀 수 있는 방법이 없다.

    • 테스트하기 어렵다.
  • 테스트 코드에서는 아무도 가져다 쓸일이 없으니 문제없이 사용해도 되지만,
    아닌 경우엔 사용을 지양하도록 하자..


4) 일반 메서드 주입

  • 일반 메서드를 통해 주입받을 수 있다.
@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;
    }
    
}
  • 한번에 여러 필드를 주입받을 수 있다.

  • 사실 수정자 주입이랑 비슷하다.

  • 잘 사용하지 않는다.


📌 2. 옵션 처리


주입할 스프링 빈이 없어도 동작해야 할 때가 있다.

자동 주입 대상을 옵션으로 처리하는 방법 3가지를 알아보자.

1. @Autowired(required = false)

// 호출 안됨
@Autowired(required = false)
public void setNoBean1(Member member) {
    System.out.println("setNoBean1 = " + member);
}
  • 여기서 Member는 스프링 빈이 아니다.

  • 자동 주입할 대상이 없으므로 호출되지 않는다.

    @Autowired만 사용하면 required 옵션 기본값이 true 이기 때문에 오류가 발생한다


2. @Nullable

// null 호출
@Autowired
public void setNoBean2(@Nullable Member member) {
    System.out.println("setNoBean2 = " + member);
}
  • 자동 주입할 대상이 없어도 호출되길 원하면 @Nullable
    • null이 입력된다.

3. Optional<>

// Optional.empty 호출
@Autowired(required = false)
public void setNoBean3(Optional<Member> member) {
   System.out.println("setNoBean3 = " + member);
}
  • 자동 주입할 대상이 없으면 Optional.empty가, 있으면 Optinal로 감싸져서 입력된다.

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


스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장한다.

1) 불변

  • 대부분의 의존관계 주입은 한번 일어나면 변경할 일이 없다. 오히려 변경이 되면 안된다.

  • 수정자 주입을 사용하면 setXxx 메서드를 public으로 열어두어야 한다.

    • 누군가 실수로 변경할 수 있고, 변경하면 안되는 메서드를 열어두는 것은 좋지 않다.
  • 생성자 주입은 객체를 생성할 때 딱 1번만 호출이 되므로 이후에 호출되는 일이 없다. 따라서 불변하게 설계할 수 있다.

2) 누락

예제 - 스프링 컨테이너 사용X, 순수 자바 코드

@Test
void createOrder() {
    OrderServiceImpl orderService = new OrderServiceImpl();
    orderService.createOrder(1L, "itemA", 10000);
}
  • OrderServiceImpl에 수정자 주입 사용 시

    • 실행은 되는데 막상 실행하면 NPE이 발생한다.
      -> memberRepository , discountPolicy 모두 의존관계 주입이 누락되었기 때문이다.
  • OrderServiceImpl에 생성자 주입 사용 시

    • 굳이 실행해보지 않아도 컴파일 오류가 발생한다.
    • IDE에서 바로 어떤 값을 필수로 주입해야 하는지 알 수 있다.

3) final 키워드

생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다.

  • final=으로 초기 값을 바로 넣어주거나 생성자에서만 값을 세팅할 수 있다.
    -> 생성자에서 혹시라도 값을 이상하게 세팅했을 때, 오류를 컴파일 시점에서 잡아준다.

📌 4. 롬복과 최신 트렌드


  • 생성자 주입은 생성자도 만들어야 하고, @Autowired도 붙여주어야 하고, final도 붙여주어야 하기 때문에 귀찮다..

기존 코드

@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
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
    
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
}
  • 이 코드는 기존 코드와 완전히 동일하다.

  • @RequiredArgsConstructor가 컴파일 시점에 final이 붙은 필드들을 파라미터로 사용하는 생성자 코드를 자동생성 해준다.

    • `@Autow
  • 이 방법은 생성자가 1개일 때만 적용이 가능하다.


📌 5. @Autowired - 조회 빈이 2개 이상


1) 문제점

@Autowired 아래 코드처럼 타입으로 조회해서 주입한다.
ac.getBean(DiscountPolicy.class);   (실제로는 이 코드보다 더 많은 기능을 제공한다.)

FixDiscountPolicy , RateDiscountPolicy@Component를 붙여 모두 스프링 빈으로 등록한다면 ❓

@Autowired
private DiscountPolicy discountPolicy

위 코드 @Autowired에서 빈이 두 개 조회가 되어 오류가 난다.  (하위타입 모두 조회)


2) 해결방법

1) @Autowired 필드명 매칭

  • 아래 코드처럼 필드명을 빈 이름과 매칭 시켜주는 방법이 있다.
    • 이는 타입 조회부터 하고 그 결과에 여러 빈이 있을 때 추가로 동작하는 기능이다.
@Autowired
private DiscountPolicy rateDiscountPolicy  // 정상 주입!

2) @Quilifier

  • 먼저 @Qualifier끼리 매칭하는데 찾지 못하면 구분자 이름과 빈 이름으로 매칭한다.

    • @Component를 붙여 빈으로 등록한 RateDiscountPolicy , FixDiscountPolicy 클래스
      각각에 Qualifier("mainDiscountPolicy") , Qualifier("fixDiscountPolicy") 를 추가로 붙이고 아래와 같이 사용 가능하다.
@Autowired
@Qualifier("mainDiscountPolicy")  // 또는 @Qualifier("fixDiscountPolicy")
private DiscountPolicy discountPolicy
  • @Qualifier@Qualifier를 찾는 용도로만 사용하는게 명확하고 좋다.
    -> 빈 이름으로 찾는 단계까지 X

3) @Primary

  • 우선순위를 정하는 방법이다.

    • @Autowired시에 여러 빈이 매칭되면 @Primary가 우선권을 가진다.

    • 위 예제에서는 RateDiscountPolicy 또는 FixDiscountPolicy 클래스에 @Primary를 붙여 우선권을 부여해줄 수 있다.

  • 이 방식을 자주 사용한다.

@Quilifier, @Primary를 같이 사용할 때 @Quilifier의 우선순위가 더 높다


📌 6. 어노테이션 직접 만들기


1) @Qualifier의 문제점

이전 예제처럼 @Qualifier("mainDiscountPolicy") 이렇게 문자를 적으면 컴파일 시 타입체크가 안된다.
(오타 발생 시 체크가 어려움)

2) 어노테이션을 만들어 문제해결

-> 다음과 같은 어노테이션을 만들어 해결할 수 있다.

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
{

}

만든 MainDiscountPolicy 어노테이션 적용하기!

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

}

RateDiscountPolicy를 가져다 쓸 orderServiceImpl에도 적용!

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

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


의도적으로 정말 해당 타입의 스프링 빈이 다 필요한 경우도 있다.
-> 예를 들어 할인 서비스를 제공하는데, 클라이언트가 할인의 종류(rate, fix)를 선택할 수 있다고 가정해보자.

코드로 이해해보자

public class AllBeanTest {

    @Test
    void findAllBean() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);


    }

    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("policyList = " + policies);
        }
    }

}
  • 우선 생성자 파라미터를 보면 각각 Map<String, DiscountPolicy>, List<DiscountPolicy> 타입이다.

    • Map의 key에는 빈 이름(String), value에는 빈 객체(DiscountPolicy)가 모두(rate, fix) 주입된다.

    • List에는 빈 객체(DiscountPolicy)만 모두(rate, fix) 주입된다.

    • 만약 해당하는 스프링 빈이 없으면, 빈 자료구조를 주입한다

출력 결과

policyMap = {fixDiscountPolicy=hello.core.discount.FixDiscountPolicy@278bb07e, rateDiscountPolicy=hello.core.discount.RateDiscountPolicy@4351c8c3}
policyList = [hello.core.discount.FixDiscountPolicy@278bb07e, hello.core.discount.RateDiscountPolicy@4351c8c3]

마저 작성해보자

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

    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("policyList = " + policies);
        }

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

}
  • discount 메서드로 얼마나 할인되는지 값을 반환 받는다.

    • 메서드 파라미터로 discountCode(빈 이름)을 넘겨준다.
      -> 이걸 key로 사용해 Map에 등록된 객체를 찾는다. == 할인 종류를 선택한다.

-> 이 방법은 다형성을 유지하면서 동적으로 빈을 선택할 수 있다.


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


1) 빈 등록 - 자동 VS 수동

스프링 빈 자동 등록, 수동 등록 방법 중 어떤걸 선택해서 개발해야 할까?

  • @Component만 넣어주면 끝나는 일을 @Configuration 설정 정보로 가서 @Bean 적고 ...
    -> 매우 번거롭다

  • 설정 정보가 커지면 그걸 관리하는 것 자체가 부담된다.

  • 어차피 자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다.

-> 편리한 자동 기능을 기본으로 사용하자

2) 수동 빈 등록은 언제 사용할까?

그렇다면 수동 빈 등록은 언제 사용하면 좋을까?

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

  • 업무 로직 빈 : 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리 등이 모두 업무 로직이다.
    보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.

  • 기술 지원 빈 : 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다.
    데이터베이스 연결이나, 공통 로그처리 업무 로직을 지원하기 위한 하부 기술이나 공통 기술이다.

정리

  • 업무 로직은 숫자도 매우 많고, 컨트롤러, 서비스, 리포지토리처럼 유사한 패턴이 존재한다.

       -> 대부분 자동 빈 등록 사용한다.

  • 기술 지원 로직은 상대적으로 수가 매우 적고, 광범위하게 영향을 미친다. 또, 문제 발생 시 어디가 문제인지 파악하기는 커녕, 적용이 잘 되고 있는지조차 파악하기 어렵다.

       -> 가급적 수동 빈 등록을 사용해서 설정 정보에 한 눈에 보이도록 하는 것이 유지보수에 좋다.

profile
개발 공부

0개의 댓글