반응형

# 의존관계 자동 주입

## 롬북과 최신 트랜드

  • 개발을 해보면 대부분 불변, 그래서 final 키워드 사용하게 된다. 그런데 생성자도 만들어야하고 주입받는 값을 대입하는 코드도 만들어야 하는 귀찮음이 존재한다. 필드 주입처럼 편리하게 사용하는 방법은 없을까 라는 고민에서 시작.
  • 생성자가 1개만 있을 경우 아래와 같이 @Autowired 생략 가능.
package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

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

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

롬북 라이브러리 적용. (build.gradle 에 라이브러리 및 환경 추가)

plugins {
   id 'org.springframework.boot' version '2.6.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'
   testImplementation 'org.springframework.boot:spring-boot-starter-test'
   //lombok 라이브러리 추가 시작
   compileOnly 'org.projectlombok:lombok'
   annotationProcessor 'org.projectlombok:lombok'
   testCompileOnly 'org.projectlombok:lombok'
   testAnnotationProcessor 'org.projectlombok:lombok'
   //lombok 라이브러리 추가 끝
}

tasks.named('test') {
   useJUnitPlatform()
}
  • 1. File Settings(맥은 Preferences) > plugin lombok 검색 설치 실행 (재시작)
  • 2. File Settings > Annotation Processors 검색 Enable annotation processing 체크 (재시작)
  • 3. 임의의 테스트 클래스를 만들고 @Getter, @Setter 확인
package hello.core;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class HelloLombok {

    private String name;
    private int age;

    public static void main (String[] args) {
        HelloLombok helloLombok = new HelloLombok();
        helloLombok.setName("abc");

        //String name = helloLombok.getName();
        //System.out.println("name = " + name);
        System.out.println("helloLombok = " + helloLombok);
    }
}
  • @RequiredArgsConstructor : final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다. (코드가 간결해진다.)
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.stereotype.Component;

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final 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;
    }
}
  • 최근에는 생성자를 1개만 두고, @Autowired 를 생략하는 방법을 주로 사용한다. Lombok 라이브러리의 @RequiredArgsConstructor 함께 사용하면 기능은 다 제공하면서, 코드는 깔끔하게 사용할 수 있다.

## 조회 빈이 2개 이상인 경우.

  • @Autowired 는 타입(Type)으로 조회한다.
@Autowired
private final MemberRepository memberRepository;

@Autowired
private final DiscountPolicy discountPolicy;
  • 타입으로 조회하기 때문에, 마치 다음 코드와 유사하게 동작. (실제로는 더 많은 기능을 제공한다.) 
ac.getBean(DiscountPolicy.class)

## @Autowired 필드명, @Qualifier, @Primary

  • 조회 대상 빈이 2개 이상인 경우 해결 방법 : @Autowired 필드명, @Qualifier, @Primary 등의 방법 사용.

1. @Autowired 필드 명 매칭

  • @Autowired 는 타입 매칭을 시도, 이때 여러 빈이 있는 경우 필드명, 파라미터 이름으로 빈 이름을 추가 매칭한다.
  • 기존코드 가 아래와 같을 때.
@Autowired
private DiscountPolicy discountPolicy
  • 필드명을 빈 이름으로 변경.
@Autowired
private DiscountPolicy rateDiscountPolicy
  • 필드 명이 rateDiscountPolicy 이므로 정상 주입.
  • 필드명 매칭은 먼저 타입 매칭을 시도, 그 결과에 여러 빈이 있을 때 추가로 동작하는 기능.
  • @Autowired 매칭 정리
1. 타입 매칭 

2. 타입 매칭의 결과가 2개 이상일 때 필드 명, 파라미터 명으로 빈 이름 매칭

2. @Qualifier @Qualifier끼리 매칭 빈 이름 매칭

  • 추가 구분자를 붙여주는 방법. (주입 시 추가적인 방법을 제공할 뿐, 빈 이름을 변경하는게 아님)
  • 빈 등록 시 @Qualifier 를 붙여서 사용.

생정자 자동 주입 예시.

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; // 할인율 10%

    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP) {
            return price * discountPercent / 100;
        } else {
            return 0;
        }
    }
}
package hello.core.discount;

import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.beans.factory.annotation.Qualifier;

@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {

    private int discountFixAmount = 1000; // 1,000원 할인

    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP) {
            return discountFixAmount;
        } else {
            return 0;
        }
    }
}

수정자 자동 주입 예시.

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.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") 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;
    }
}
  • @Qualifier 로 주입 시 @Qualifier("mainDiscountPolicy") 를 못찾게 될 경우 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다.
  • @Qualifier 는 @Qualifier 를 찾는 용도로만 사용하는게 명확하고 좋다.
  • @Qualifier 정리
1. @Qualifier끼리 매칭

2. 빈 이름 매칭

3. NoSuchBeanDefinitionException 예외 발생

3 . @Primary 사용 (자주 사용!!)

  • @Primary 는 우선순위를 정하는 방법으로, @Autowired 시 여러 빈이 매칭되면 @Primary 가 우선권을 가진다.
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; // 할인율 10%

    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP) {
            return price * discountPercent / 100;
        } else {
            return 0;
        }
    }
}
  • @Qualifier 의 단점 : 주입 받을 때 모든 코드에 @Qualifier 를 붙여주어야 한다는 점. (@Primary 를 사용하면 @Qualifier 를 붙일 필요가 없다.)
  • 우선순위 : 자세한것(좁은선택범위) 이 우선순위가 높다. (스프링은 자동보다 수동, 넒은 범위의 선택권 보다 좁은 범위의 선택권이 우선 순위가 높다. @Qualifier 가 우선권이 높다.)

## 애노테이션 직접 만들기

  • @Qualifier("mainDiscountPolicy") 와 같이 적게되면 컴파일시 타입 체크가 되지 않는 문제가 발생하는데, 애노테이션을 직접 만들어서 해결할 수 있다.
  • 생성한 애노테이션에 Ctrl + B (인텔리제이, 윈도우 기준) 입력 시, 해당 애노테이션을 사용하는 곳 추적 가능하다.
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 {
}
  • 생성한 애노테이션 추가.
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; // 할인율 10%

    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP) {
            return price * discountPercent / 100;
        } else {
            return 0;
        }
    }
}
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy 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;
    }
}
  • 애노테이션에는 상속이라는 개념이 없다.
  • 여러 애노테이션을 모아 사용하는 기능은 스프링에서 지원해주는 기능이다.
  • @Qulifier 뿐만 아니라 다른 애노테이션들도 조합해서 사용 가능하다. 
  • @Autowired 도 재정의 할 수 있지만, 뚜렷한 목적 없이 무분별하게 재정의 하는것은 유지보수에 혼란을 줄 수 있다.

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

  • 의도적으로 해당 타입의 스프링 빈이 다 필요한 경우 사용.
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 rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
        assertThat(rateDiscountPrice).isEqualTo(2000);
    }

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

            System.out.println("discountCode = " + discountCode);
            System.out.println("discountPolicy = " + discountPolicy);

            return discountPolicy.discount(member, price);
        }
    }
}
  • DIscountService는 Map으로 모든 DiscountPolicy를 주입 받음. 이때 fixDiscountPolicy, rateDiscountPolicy 주입.
  • discount() 멧드는discountCode로 fixDiscountPolicy(or rateDiscountPolicy)가 오는경우 map에서 fixDiscountPolicy(or rateDiscountPolicy) 빈을 찾아 실행한다.

## 자동과 수동의 올바른 실무 운영 기준.

편리한 자동 기능을 기본으로 사용.

  • 스프링이 나오고 시간이 갈수록 자동을 선호하는 추세.
  • 스프링은 계층에 맞춰 일반적인 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원한다.
  • 최근 스프링 부트는 컴포넌트 스캔을 기본으로 사용하고, 스프링 부트의 다양한 스프링 빈들도 조건만 맞으면 자동으로 등록되도록 설계함.
  • 자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다.

수동 빈 등록은 언제 하면 좋을지.

  • 애플리케이션은 크게 업무 로직과 기술지원 로직으로 나뉨.
  • 업무로직 빈 : 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리 등. 보통 비즈니스 요구사항을 개발할 때 추가 or 변경된다.
  • 기술지원 빈 : 기술적 문제나 공통 관심사(AOP)를 처리할 때 사용. 데이터베이스 연결, 공통 로그 처리처럼 업무를 지원하기 위한 하부 기술 또는 공통 기술. (수동 빈 등록하여 설정 정보에 바로 나타나도록 하는게 유지보수 에 좋음)
  • 업무 로직은 숫자가 많고, 한번 개발해야 하면 컨트롤러, 서비스, 리포지토리 등 어느정도 유사한 패턴이 존재. 이런 경우 자동 기능을 적극 사용하는 것이 좋다. (문제 발생 시 어떤 곳에서 문제가 발생했는지 명확하게 파악하기 쉬움)
  • 기술 지원 로직은 업무 로직과 비교 시 수가 매우 적고, 보통 애플리케이션 전반에 걸쳐 광범위하게 영향을 미친다. 기술 지원 로직은 적용이 잘 되고 있는지 아닌지 조차 파악하기 어려운 경우가 많다. 그래서 기술 지원 로직들은 가급적 수동 빈 등록을 사용해서 명확하게 들어내는 것이 좋다.
  • 비즈니스 로직 중 다형성을 적극 활용할 때에는 수동 빈 등록하는 것이 좋다.
  • 수동, 자동 등록의 핵심은 빈의 이름 및 어떤 빈들이 주입될지 한 눈에 파악 가능하도록 하는 것.
  • 정리.
편리한 자동 기능을 기본으로 사용.

직접 등록하는 기술 지원 객체는 수동 등록.

다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민.
반응형

+ Recent posts