Spring Framework/[인프런] Spring 핵심 원리

Chapter 07. 의존 관계 자동 주입

계란💕 2022. 8. 15. 16:32

7.1 다양한 의존 관계 주입(Dependency Injection) 방법

 

  Def) 의존 관계 주입(DI): 하나의 객체가 다른 객체의 의존성을 제공하는 테크닉이다. 

    - @Autowired: 의존 관계 주입할 때, 사용하는 애너테이션이며 IoC컨테이너에 존재하는 빈을 찾아서 주입하는 역할을 한다.

    - 객체의 의존성을 가지는 부분에 애너테이션을 사용해서 의존성을 주입할 수 있다.

 

 

의존 관계 주입

 

  • 생성자 주입: 생성자를 통해 의존 관계를 주입한다. 생성자 호출 시점에 딱 1번만 호출되는 것이 보장된다.
    • 불변, 필수 의존관계에서 사용된다.
    • 생성자가 1개만 있으면 @Autowired를 생략해도 자동으로 주입된다. (스프링 빈에만 해당)
  • 수정자 주입(setter): setter를 통해서 의존 관계를 주입하는 방법이다.
    • 선택이나 변경 가능성이 있는 의존 관계에서 사용한다.
    • setter의 매개변수가 스프링 빈에 등록되지 않은 경우에도 사용이 가능하다. 선택적으로 의존 관계 주입
    • 선택적으로 하려면 @Autowired(required = false) 라고 주면 된다. 즉, 주입한 대상이 없어도 동작하게 하려면 @Autowired(required = false)
    • @Autowired(required = true)는 주입 대상이 없으면 기본적으로 오류가 발생한다. true가 기본값이다.
    • 앞에 set을 붙이는 것이 자바 프로퍼티 규약
  • 필드 주입: 필드에 의존 관계를 주입한다. 필드 주입은 사용하지 않는다.
    • 코드가 간결하지만 외부에서 변경이 불가능해서 테스트하기 힘들다는 치명적인 단점이 있다.
    • DI 프레임워크가 없으면 아무것도 할 수 없다.
    • 스프링 설정을 목적으로 하는 @Configuration같은 곳 또는 애플리케이션 실제 코드와 관계없는 테스트 코드에만 특별한 용도로 필드 주입을 사용한다.
    • @SpringBootTest처럼 스프링 컨테이너를테스트에 통합한 경우에만 가능하다. (순수한 자바 테스트 코드에는 @Autowired가 동작하지 않는다.)
    • 필드 주입은 setter를 만들어야한다. 그럴바에는 setter에 주입하는 것이 낫다.
  • 일반 메서드 주입: 일반 메서드를 통해 주입 받을 수 있다.
    • 한 번에 여러 필드를 주입 받을 수 있다. 일반적으로 잘 사용하지는 않는다.
    • 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야만 동작한다.
    • 보통 생성자 주입, 수정자 주입 안에서 해결하므로 보통 일반 메서드 주입은 사용하지 않는다.

 

 

  Ex 1) 생성자 주입 - 빈 등록하면서 의존 관계 주입도 함께 일어난다.

<hide/>
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements  OrderService{

    private final MemberRepository memberRepository;    // = new MemoryMemberRepository();
    private final DiscountPolicy discountPolicy;    // = new RateDiscountPolicy();

    public MemberRepository getMemberRepository(){
        return memberRepository;
    }

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

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
            Member member = memberRepository.findById(memberId);    // 회원정보를 조회한다.
            int discountPrice = discountPolicy.discount(member, itemPrice); // 최종적으로 할인된 금액
            return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}

    - OrderServiceImpl에 @Component가 있으면  빈에 등록하기 위해 스프링이 생성자를 호출한다.

    - 그런데, 생성자에 @Autowired가 있으면? 스프링 컨테이너에서 스프링 빈(MemberRepository, DiscountPolicy)을 꺼낸다.

      -> MemberRepository, DiscountPolicy를 꺼내서 주입해준다.

 

  Note) 실행 결과

 

 

  Ex) OrderServiceImpl의 생성자에 Autowired를 붙이기 전 

<hide/>
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements  OrderService{

    private final MemberRepository memberRepository;    // = new MemoryMemberRepository();
    private final DiscountPolicy discountPolicy;    // = new RateDiscountPolicy();


    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {

        System.out.println("memberRepository = " + memberRepository);
        System.out.println("discountPolicy = " + discountPolicy);
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
            Member member = memberRepository.findById(memberId);    // 회원정보를 조회한다.
            int discountPrice = discountPolicy.discount(member, itemPrice); // 최종적으로 할인된 금액
            return new Order(memberId, itemName, itemPrice, discountPrice);
    }

    public MemberRepository getMemberRepository(){
        return memberRepository;
    }
}

  Note) 실행 결과

 

 

  Ex) Autowired 후 (OrderServiceImpl의 생성자에 Autowired를 붙인 경우)

<hide/>
package hello.core.singletone;
import hello.core.AppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberServiceImpl;
import hello.core.order.OrderServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
public class ConfigurationSingletonTest {

    @Test
    void configurationTest(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

        // 모두 같은 인스턴스를 참조하고 있다.
        System.out.println("memberService -> memberRepository = " + memberService.getMemberRepository());
        System.out.println("orderService -> memberRepository = " + orderService.getMemberRepository());
        System.out.println("memberRepository = " + memberRepository);

        assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
    }

    @Test
    void configurationDeep() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        AppConfig bean = ac.getBean(AppConfig.class);
        System.out.println("bean = " + bean.getClass());

    }
}
<hide/>
package hello.core.scan;
import hello.core.AutoAppConfig;
import hello.core.member.MemberService;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
public class AutoAppConfigTest {
    @Test
    void basicScan(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

  Note) basicScan 실행 결과

    - 붙이기 전이나 후나 결과가 똑같다

    - 생성자가 딱 하나기 때문에 자동으로 주입되기 때문이다.

 

 

  Ex 2) 수정자(setter) 주입

    - OrderServiceImpl클래스를 다음과 같이 수정한다.

<hide/>
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements  OrderService{

    private MemberRepository memberRepository;    // = new MemoryMemberRepository();
    private DiscountPolicy discountPolicy;    // = new RateDiscountPolicy();

    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
        System.out.println("memberRepository = " + memberRepository);
        this.memberRepository = memberRepository;
    }
    
    @Autowired
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
        System.out.println("discountPolicy = " + discountPolicy);
        this.discountPolicy = discountPolicy;
    }
		
    // 생성자 주입
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
            Member member = memberRepository.findById(memberId);    // 회원정보를 조회한다.
            int discountPrice = discountPolicy.discount(member, itemPrice); // 최종적으로 할인된 금액
            return new Order(memberId, itemName, itemPrice, discountPrice);
    }

    public MemberRepository getMemberRepository(){
        return memberRepository;
    }
}
<hide/>
<hide/>
package hello.core.scan;
import hello.core.AutoAppConfig;
import hello.core.member.MemberService;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
public class AutoAppConfigTest {
    @Test
    void basicScan(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

  Note) 실행 결과 - basic

    1) OrderServiceImpl을 스프링 컨테이너에 등록한다. 

    2) 그 다음 스프링 컨테이너는 스프링 빈을 모두 등록하고 나서 연관 관계를 자동으로 주입한다. (생성자 먼저하고 그 다음에 수정자 setter)

    - Autowired가 없으면 아래처럼 출력이 안된다.

 

 

  Ex 3) 필드 주입 

    - OrderServiceImpl은 Autowired가 있기 때문에 생성자가 필요없다.

<hide/>
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements  OrderService{

    @Autowired private MemberRepository memberRepository;    // = new MemoryMemberRepository();
    @Autowired private DiscountPolicy discountPolicy;    // = new RateDiscountPolicy();


//    @Autowired
//    public void setMemberRepository(MemberRepository memberRepository) {
//        System.out.println("memberRepository = " + memberRepository);
//        this.memberRepository = memberRepository;
//    }
//
//    @Autowired
//    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
//        System.out.println("discountPolicy = " + discountPolicy);
//        this.discountPolicy = discountPolicy;
//    }

//    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
//        System.out.println("1. OrderServiceImpl.OrderServiceImpl");
//        this.memberRepository = memberRepository;
//        this.discountPolicy = discountPolicy;
//    }

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
            Member member = memberRepository.findById(memberId);    // 회원정보를 조회한다.
            int discountPrice = discountPolicy.discount(member, itemPrice); // 최종적으로 할인된 금액
            return new Order(memberId, itemName, itemPrice, discountPrice);
    }

    public MemberRepository getMemberRepository(){
        return memberRepository;
    }
}

 

<hide/>
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService(){
        System.out.println("call AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }
    @Bean
    public OrderService orderService(){ // 누군가 오더서비스를 조회하면?
        System.out.println("call AppConfig.orderService");
//        return new OrderServiceImpl(memberRepository(), discountPolicy());
        return null;
    }

    @Bean
    public DiscountPolicy discountPolicy(){
        return  new RateDiscountPolicy();
    }
}
<hide/>
<hide/>
package hello.core.scan;
import hello.core.AutoAppConfig;
import hello.core.member.MemberService;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
public class AutoAppConfigTest {
    @Test
    void basicScan(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

Note) basicScan 실행 결과 - 성공

 

    - basicScan에 내용을 추가해서 실행한다.

<hide/>
package hello.core.scan;
import hello.core.AutoAppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.order.OrderServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
public class AutoAppConfigTest {

    @Test
    void basicScan(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);

        OrderServiceImpl bean = ac.getBean(OrderServiceImpl.class);
        MemberRepository memberRepository = bean.getMemberRepository();
        System.out.println("memberRepository = " + memberRepository);
    }
}

  Note) basicScan 실행 결과 - 성공

    - private이더라도 의존 관계를 필드에 직접 넣을 수 있다.

 

    - 그런데, 필드 삽입은 권장되지 않는다는 메시지가 뜬다.

      -> 코드는 간단하지만 외부에서 변경이 불가능하다는 단점이 있기 때문이다.

 

 

  Ex 4) 일반 메서드 주입 -  한 번에 여러 필드를 주입받을 수 있다.

<hide/>
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements  OrderService{

    private MemberRepository memberRepository;    // = new MemoryMemberRepository();
    private DiscountPolicy discountPolicy;    // = new RateDiscountPolicy();

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

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

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
            Member member = memberRepository.findById(memberId);    // 회원정보를 조회한다.
            int discountPrice = discountPolicy.discount(member, itemPrice); // 최종적으로 할인된 금액
            return new Order(memberId, itemName, itemPrice, discountPrice);
    }

    public MemberRepository getMemberRepository(){
        return memberRepository;
    }
}

    - 위와 같이 init 메서드를 추가하고 애너테이션을 붙인다.

    - 자동으로 의존 관계가 주입된다.

 

 

  Ex)  테스트 오류 수정하기

    - ConfigurationTest()에서 ac.getBean("orderService", ..) => 이 부분에서 getBean()할 때 문제가 생긴다.

      -> NullBean

      -> 설정 정보인 AppConfig클래스로 가서 확인해본다.

    - AppConfig에서 orderService()의 반환부에 return null 이었던 부분을 다시 원상 복구한다.

<hide/>
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService(){
        System.out.println("call AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }
    @Bean
    public OrderService orderService(){ // 누군가 오더서비스를 조회하면?
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    @Bean
    public DiscountPolicy discountPolicy(){
        return  new RateDiscountPolicy();
    }
}

  Note) 실행 결과 - 모든 테스트 완료

 

 

 

7.2 옵션 처리

 

옵션 처리

  • 주입할 스프링 빈이 없어도 동작해야할 때가 있다.
  • 그런데, @Autowired만 사용하면 'required'의 기본값이 true로 되어 있어서 자동 주입 대상이 없으면 오류가 발생한다.

 

 

  Ex) 자동 주입 대상을 옵션 처리하는 방법

<hide/>
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(TestBean.class);
    }

    static class TestBean{

        @Autowired(required = false)
        public void setNoBean1(Member noBean1){
            System.out.println("noBean1 = " + noBean1);
        }

        @Autowired
        public void setNoBean2(@Nullable Member noBean2){
            System.out.println("noBean2 = " + noBean2);
        }

        @Autowired
        public void setNoBean3(Optional<Member> noBean3){
            System.out.println("noBean3 = " + noBean3);
        }
    }
}

  Note) 실행 결과

    - 1번은 Member가 스프링 빈으로 등록되지 않았기 때문에 (required = false)를 해주지 않으면 예외가 발생한다.

      -> 의존 관계가 없으면 수정자 메서드 자체가 호출되지 않는다.

    - @Nullable은 자동 주입할 대상이 없는 경우, null이 입력된다.

      -> 2번은 의존 관계가 없더라도 호출은 되지만 null

    - Optional<>: 자동 주입할 대상이 없으면 Optional.empty가 입력된다.

    - @Nullable과 Optional운 스프링 전반에 걸쳐 지원된다. 생성자 자동 주입에서 특정 필드에만 사용해도 된다.

 

 

 

7.3 생성자 주입을 선택하라

 

생성자 주입을 선택하는 이유 (1) - 불변

  • 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료 시점까지 의존 관계를 변경할 일이 없다. 오히려 대부분의 의존 관계는 변경하면 안된다. 
  • 수정자 주입을 사용하면, setXX 메서드를 public으로 열어둬야한다.
  • 누군가 실수로 변경할 수 있고 변경하면 안 되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다.
  • 생성자 주입은 객체를 생성할 때 한 번만 호출되므로 이후에 노출되는 일이 없다. 따라서, 변하지 않도록 설계할 수 있다.

 

생성자 주입을 선택하는 이유 (2) - 누락

  • 생성자 주입을 사용하면  데이터를 누락했을 때, 컴파일 오류가 난다. 그리고 IDE에서 바로 어떤 값을 필수로 주입해야하는지 알 수 있다.

 

생성자 주입을 선택하는 이유 (3) - final 키워드

  • 생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다. 컴파일 시점에 오류 수정 가능하다
  • 다른 주입 방법 세 가지는  final 사용 불가능하다. 생성자 이후에 호출되기 때문이다.

 

Note)

  • 생성자 주입 방식을 선택하는 이유는 여러가지가 있지만 순수한 자바 언어의 특징을 잘 살리는 방법이기도 하다.
  • 기본으로 생성자 주입을 사용하고 필수값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면된다. 생성자 주입과 수정자 주입을 동시에 사용할 수 있다.
  • 따라서 항상 생성자 주입을 사용하고 옵션이 필요한 경우는 수정자 주입을 선택하면 된다. 필드 주입은 사용하지 않는게 좋다.

 

  Ex)

    - OrderServiceImpl 코드에  생성자를 지우고 setter를 추가한다.

<hide/>
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements  OrderService{

    private MemberRepository memberRepository;    // = new MemoryMemberRepository();
    private DiscountPolicy discountPolicy;    // = new RateDiscountPolicy();

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

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

//    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
//        this.memberRepository = memberRepository;
//        this.discountPolicy = discountPolicy;
//    }

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

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
            Member member = memberRepository.findById(memberId);    // 회원정보를 조회한다.
            int discountPrice = discountPolicy.discount(member, itemPrice); // 최종적으로 할인된 금액
            return new Order(memberId, itemName, itemPrice, discountPrice);
    }

    public MemberRepository getMemberRepository(){
        return memberRepository;
    }
}

 

    - OrderServiceImpl테스트를 만든다. 순수한 자바 코드로 만든다.

<hide/>
package hello.core.order;
import org.junit.jupiter.api.Test;

class OrderServiceImplTest {

    @Test
    void createOrder(){
        OrderServiceImpl orderService = new OrderServiceImpl();
        orderService.createOrder(1L, "itemA", 10000);
    }
}

 

  Note) 실행 결과

    - NullPointerException이 난다.

    - 아무리 createOrder()만 테스트하더라도 OrderServiceImp의 두 멤버변수의 값을 세팅해줘야한다.

      -> 따라서, 가짜 멤버리포지토리라도 만들어서 넣어줘야한다.

 

    - 다시 OrderServiceImp의 setter를 지우고 생성자를 살린다.

<hide/>
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements  OrderService{

    private MemberRepository memberRepository;    // = new MemoryMemberRepository();
    private DiscountPolicy discountPolicy;    // = new RateDiscountPolicy();


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

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

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
            Member member = memberRepository.findById(memberId);    // 회원정보를 조회한다.
            int discountPrice = discountPolicy.discount(member, itemPrice); // 최종적으로 할인된 금액
            return new Order(memberId, itemName, itemPrice, discountPrice);
    }

    public MemberRepository getMemberRepository(){
        return memberRepository;
    }
}

 

  - 그리고 한 명을 만들어준다.

<hide/>
package hello.core.order;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemoryMemberRepository;
import org.junit.jupiter.api.Test;

class OrderServiceImplTest {

    @Test
    void createOrder(){
        MemoryMemberRepository memberRepository = new MemoryMemberRepository();
        memberRepository.save(new Member(1L, "name",  Grade.VIP));
        OrderServiceImpl orderService = new OrderServiceImpl(new MemoryMemberRepository(), new  FixDiscountPolicy());
        orderService.createOrder(1L, "itemA", 10000);
    }
}

  Note) 실행 결과 - 성공

 

 

 

7.4 롬복(lombok)과 최신 트렌드

 

롬복과 최신 트렌드

  • @getter, @setter, @toString
  • @RequiredArgsConstructor: final이 붙은 필드에 대한 생성자를 만들어준다.
    • 기능은 모두 사용하면서 코드를 깔끔하게 만들 수 있다.

 

  Ex)  @RequiredArgsConstructor

    - build.gradle 파일을 다음과 같이 수정한다.

<hide/>
plugins {
	id 'org.springframework.boot' version '2.7.2'
	id 'io.spring.dependency-management' version '1.0.12.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'
//	implementation 'org.projectlombok:lombok:1.18.22'
//    implementation 'org.springframework.boot:spring-boot-starter-test'
//	implementation 'junit:junit:4.13.1'
//	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	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') {         exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
}

tasks.named('test') {
	useJUnitPlatform()
}

 

    - 다음과 같이 애너테이션 처리 활성화를 체크해줘야 롬복을 쓸 수 있다.

<hide/>
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements  OrderService{

    private final MemberRepository memberRepository;    // = new MemoryMemberRepository();
    private final DiscountPolicy discountPolicy;    // = new RateDiscountPolicy();

//    RequiredArgsConstructor가 붙었으니 제거 가능
/*    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
*/

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
            Member member = memberRepository.findById(memberId);    // 회원정보를 조회한다.
            int discountPrice = discountPolicy.discount(member, itemPrice); // 최종적으로 할인된 금액
            return new Order(memberId, itemName, itemPrice, discountPrice);
    }

    public MemberRepository getMemberRepository(){
        return memberRepository;
    }
}

 

 

 

7.5 조회된 빈이 2개 이상인 경우 - 문제

  • @ Autowired는 타입으로 조회한다.
  • 그래서 마치, ac.getBean(DiscountPolicy.class)와 같이 동작한다.
  • 스프링 빈 조회에서 본 것처럼 선택된 빈이 2개 이상일 때 문제가 발생한다.

  Ex) DiscountPolicy의 하위 타입인 FixDiscountPolicy, RaterDiscountPolicy 둘다 스프링 빈으로 선언

<hide/>
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.stereotype.Component;
@Component
public class FixDiscountPolicy implements DiscountPolicy {

    private int discountFixAmount = 1000;
    @Override
    public int discount(Member member, int price) {

        if(member.getGrade() == Grade.VIP){
            return discountFixAmount;
        }
        return 0;
    }
}

    - FixDiscountPolicy, RateDiscountPolicy 클래스에 각각 @Component를 붙여서 스프링 빈으로 등록한다.

 

  Note) 테스트 실행 결과  

    - basicScan 테스트 실패한다. (NoUniqueBeanDefinitionException)

      -> 빈이 유니크하지 않아서 오류가 발생

    - 이 때, 하위 타입으로 지정할 수도 있지만 하위 타입으로 지정하는 것은 DIP를 위배하고 유연성이 떨어진다.

    - 스프링 빈을 수동 등록해서 문제를 해결해도 되지만 의존 관계 자동 주입에서 해결하는 여러 방법이 있다.

 

 

 

7.6 여러 개의 빈이 선택될 때 해결방법 -  @Autowired 필드명, @Qualifier, @Primary

 

@Autowired 필드명 매칭

  • @Autowired는 먼저 타입 매칭을 시도한다. 타입이 하나면 바로 주입한다.
  • 그런데, 타입 매칭의 결과가 2개 이상이면 필드 이름이나 파라미터 이름으로 빈 이름을 추가 매칭한다.

 

  Ex) Autowired의 특별한 기능

    - 타입이 여러 개가 똑같으면 

    - OrderServiceImpl의 생성자 부분에 내용을 수정한다.

    - Autowired가 필드인 경우와 Autowired가 파라미터인 경우이다.

<hide/>
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements  OrderService{

    private final MemberRepository memberRepository;    // = new MemoryMemberRepository();
    private final DiscountPolicy discountPolicy;    // = new RateDiscountPolicy();

    // Autowired가 필드인 경우
    @Autowired
    private DiscountPolicy rateDiscountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) { // Autowired가 파라미터 일 때
        this.memberRepository = memberRepository;
        this.discountPolicy = rateDiscountPolicy;
    }

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
            Member member = memberRepository.findById(memberId);    // 회원정보를 조회한다.
            int discountPrice = discountPolicy.discount(member, itemPrice); // 최종적으로 할인된 금액
            return new Order(memberId, itemName, itemPrice, discountPrice);
    }

    public MemberRepository getMemberRepository(){
        return memberRepository;
    }
}

  Note) basicScan 실행 결과 - 성공

 

 

 

@Qualifier 사용

  • 추가 구분자를 붙여주는 방법이다.
  • 주입 시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아니다.
  • @Qualifier를 주입할 때, @Qualifier("mainDiscountPolicy")를 못 찾으면 어떻게 될까?
    • 그러면 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다. 그래도 없으면 NoSuchBeanException발생한다.
    • 하지만 @Qualifier는 @Qualifier를 찾는 용도로만 사용하는 게 명확하고 좋다. 

 

 

  Ex) qualifier

    - RateDiscountPolicy에 @Qualifier를 붙인다.

<hide/>
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements  DiscountPolicy {
    private int discountPercent = 10;
    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){
            return price * discountPercent / 100;
        }
        return 0;
    }
}

    - 클래스 앞에 @Qualifier 붙이는 경우 

<hide/>
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {

    private int discountFixAmount = 1000;

    @Override
    public int discount(Member member, int price) {

        if(member.getGrade() == Grade.VIP){
            return discountFixAmount;
        }
        return 0;
    }
}

 

    - @Qualifier는 필드 인젝션에도 붙일 수 있고 매개변수에도 붙일 수 있다.

<hide/>
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements  OrderService{

    private final MemberRepository memberRepository;    // = new MemoryMemberRepository();
    private final DiscountPolicy discountPolicy;    // = new RateDiscountPolicy();

    // Autowired가 필드인 경우
    @Autowired
    private DiscountPolicy rateDiscountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository,
    				@Qualifier("mainDiscountPolicy")DiscountPolicy discountPolicy) { // Autowired가 파라미터 일 때
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
            Member member = memberRepository.findById(memberId);    // 회원정보를 조회한다.
            int discountPrice = discountPolicy.discount(member, itemPrice); // 최종적으로 할인된 금액
            return new Order(memberId, itemName, itemPrice, discountPrice);
    }

    public MemberRepository getMemberRepository(){
        return memberRepository;
    }
}

  Note) 실행 결과

 

 

@Primary

  • 우선순위를 지정하는 방법이다.
  • @Autowired 시에 여러 번 매칭되면 '@Primary'가 우선권을 가진다.

 

 

  Ex) @Primary

    - RateDiscountPolicy클래스에 애너테이션을 추가한다.

<hide/>
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Component
@Primary
public class RateDiscountPolicy implements  DiscountPolicy {
    private int discountPercent = 10;
    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){
            return price * discountPercent / 100;
        }
        return 0;
    }
}

  Note) 실행 결과 - 성공

 

 

 

@Primary와  @Qualifier 활용

  • @Qualifier의 단점은 주입 받을 때 모든 코드에 @Qualifier를 붙여야한다는 단점이 있다.
  • 코드에서 자주 사용하는 메인 데이터베이스의 커넥션을 획득하는 스프링 빈이 있고 코드에서 특별한 기능으로 가끔 사용하는 서브 데이터베이스의 커넥션을 획득하는 스프링 빈이 있다고 할 때, 메인 데이터베이스의 커넥션을 획득하는 스프링 빈은 @Primary를 적용해서 조회하는 곳에서 @Qualifier 지정없이 편리하게 조회하는 것이 좋다.
  • 서브 데이터베이스 커넥션 빈을 획득할 때는 @Qualifier를 지정해서 명시적으로 획득하는 방식으로 사용하면 코드를 깔끔하게 유지 가능하다. 
  • 이 때, 메인 데이터베이스 스프링 빈을 등록할 때 @Qualifier를 지정해주는 것을 상관없다.
  • @Primary는 기본값처럼 동작하고 @Qualifier는 매우 상세하게 동작한다.
    • 스프링은 항상 자세한 것이 우선순위가 높다.
    • 따라서, @Qualifier가 우선권이 높다.

 

 

 

7.7 애너테이션 직접 만들기

 

  Ex) 애너테이션 만들기

    - @MainDiscountPolicy 애너테이션을  만든다. 아래과 같이 애너테이션을 모두 붙이면 MainDiscountPolicy를 쓸 때, 스프링 컨테이너 안에서  @Target 부터 5가지 기능이 모두 동작한다.

<hide/>
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 {
}

 

     -  아래 클래스에 애너테이션 추가

<hide/>
package hello.core.discount;
import hello.core.annotation.MainDiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements  DiscountPolicy {
    private int discountPercent = 10;
    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){
            return price * discountPercent / 100;
        }
        return 0;
    }
}

 

    - OrderServiceImpl에도 애너테이션을 추가한다.

<hide/>
package hello.core.order;
import hello.core.annotation.MainDiscountPolicy;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements  OrderService{

    private final MemberRepository memberRepository;    // = new MemoryMemberRepository();
    private final DiscountPolicy discountPolicy;    // = new RateDiscountPolicy();

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) { // Autowired가 파라미터 일 때
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
            Member member = memberRepository.findById(memberId);    // 회원정보를 조회한다.
            int discountPrice = discountPolicy.discount(member, itemPrice); // 최종적으로 할인된 금액
            return new Order(memberId, itemName, itemPrice, discountPrice);
    }

    public MemberRepository getMemberRepository(){
        return memberRepository;
    }
}

  Note) 실행 결과 - 아직도 테스트 3개가 오류 난다.

 

====================================오류====================================

 

 

 

7.8 조회한 빈이 모두 필요할 때 List, Map으로 한 번에 조회하는 방법

 

  • 의도적으로 해당 타입의 스프링 빈이 다 필요한 경우가 있다.
  • 예를 들어, 클라이언트가 할인 종류(rate, fix)를 선택 가능한 경우가 있다고 할 때, 스프링을 사용해서 전략 패턴을 간단히 구현할 수 있다.

 

  Ex)

<hide/>
package hello.core.autowired;
import hello.core.discount.DiscountPolicy;
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;
public class AllBeanTest {
    @Test
    void findAllBean(){
        ApplicationContext ac =  new AnnotationConfigApplicationContext(DiscountService.class);
    }
    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("policies = " + policies);

        }
    }
}

    - AllBeanTest 클래스를 만든다.

 

  Note) 실행 결과

    - 아무 값도 나오지 않는다.

    - DiscountService만 스프링 빈으로 등록한 상태이기 때문이다.

 

 

    - 다음과 같이 매개변수에  AutoAppConfig를 등록하고 DiscountService도 등록한다

ApplicationContext ac =  new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);

    - DiscouintService의 생성자 앞에 @Autowired도 추가한다.

 

 

  Note) 실행 결과

 

 

  Ex)

    - DiscountService는 map으로 모든 DiscountPolicy를 주입 받는다.

    - 그리고  map에 fixDiscountPolicy와 rateDiscountPolicy 주입이 된다. 

<hide/>
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.assertj.core.api.Assertions;
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 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);
        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 rageDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
        assertThat(rageDiscountPrice).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);
        }

        public int discount(Member member, int price, String discountCode) {

           DiscountPolicy discountPolicy = policyMap.get(discountCode);
           return discountPolicy.discount(member, price);
        }
    }
}

  Note) 실행 결과 - 성공

 

 

 

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

 

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

  • 편리한 자동 빈 등록 기능을 기본으로 사용한다. 자동 빈 등록을 사용해도 DIP, OCP를 지킬 수 있다. 
  • 수동 빈 등록은 언제 사용하는 게 좋을까?
    • 업무 로직 빈: 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리 등이 모두 업무 로직이다. 
    • 기술 지원 빈: 기술적 문제나 공통 관심사(AOP)를 처리할 때, 주로 사용된다. 데이터베이스 연결이나 공통 로그 처리처럼 업무로직을 지원하기 위한 하부 기술이나 공통 기술들이다. 
    • 애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 딱 설정 정보에 바로 나타나게 하는 것이 유지보수 하기에 좋다.
    • 다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해본다.

 

 

 

출처 https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com