framework/spring

스프링 핵심 원리 이해 6 - 의존관계 자동 주입

wooweee 2023. 10. 29. 13:33
728x90

1. 의존관계 주입 방법 - 4가지

  • 크게 4가지 존재
    1. 생성자 주입 - 권장
    2. 수정자 주입(setter 주입) - 정말 필요시

      -- 3, 4 번은 사용하지 말 것 --

    3. 필드 주입
    4. 일반 메서드 주입
  • 복습
    • spring container 설정 과정
      1. 모든 @Component 찾아서 등록 (의존관계 주입을 바로 수행하지 않는다.)
      2. 의존 관계 주입을 수행 (@Component가 다 등록 된 이후에 수행)
    • 하지만 예외적으로 생성자 주입이나 수동 bean 설정 같은 경우에는 java code 특성상 1,2, 단계가 동시에 진행

 

1.1. 생성자 주입

  • 생성자 호출시점에 딱 1번만 호출되는 것이 보장
  • 불변, 필수 의존관계에 사용
    • 불변 : (외부에서 생성자 값 변경 못하게)  필수(private final를 이용해서 생성자의 params를 무조건 다 쓰도록) 컴파일 에러에서 해결
    • 필수 : final 사용 가능 - 생성자에 DI 하지 않으면 compile 오류 나타남 - 예외를 사전에 잡음
  • @Autowired : 생성자가 1개만 있으면 생략 가능

 

  • 복습
    자동, 수동 모두 빈 등록과 수정이 동시에 일어난다.
    만약 springBean에 등록 되지 않은 DI가 bean 존재시, 해당 class를 bean으로 등록 후 생성자 주입 한다.
    1. 자동 생성자 주입
      • 장점: 자동화
      • 단점: 변경시 실제 코드 건들여야 한다.
    2. 수동 생성자 주입 (appConfig)
      • 장점: 변경에 용의
      • 단점: 모든 걸 수동 작성해야 한다.

 

@Component
public class OrderServiceImpl implements OrderService {

    // final 사용 가능 - 생성자에 DI 하지 않으면 compile 오류 나타남 - 예외를 사전에 잡음
    private final MemberRepository memberRepository; 
    private final DiscountPolicy discountPolicy;
    
    @AutoWired // 생성자가 1개 일 때 생략가능
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy){
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}

 

1.2. 수정자 주입 (setter 주입)

  • 생성자 주입과 차이점
    • Spring Container에 스프링 bean이 다 등록된 후에 자동관계 주입을 setter를 통해서 들어가도록 한다.
    • 등록, DI 가 분리되어서 진행
  • 사용
    • 선택, 변경 가능성이 있는 의존관계에 사용
      • 선택적으로 사용하고 싶을 때 @Autowired(required = false)하면 된다. - 2. 옵션처리 설명
      • required =  false 하는게 필수는 아니다.
    • 주입할 class 들에 final을 붙일 수 없다 == 선택적 의존관계 주입
  • 특징
    • 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다.
      • 자바빈 프로퍼티 규약: 자바에서는 과거부터 필드의 값을 직접 변경하지 않고, setXxx, getXxx 라는 메서드를 통해서 값을 읽거나 수정하는 규칙
@Component public class OrderServiceImpl implements OrderService {

    private MemberRepository memberRepository; // 필수가 아니여서 final 빠짐
    private DiscountPolicy discountPolicy;
    
    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
    	this.memberRepository = memberRepository;
    }
    @Autowired(required = false) // 선택 가능하게 값 안들어와도 되도록 함.
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
    	this.discountPolicy = discountPolicy;
    }
}

 

1.3. 필드 주입

  •  코드가 간결해서 좋지만 외부에서 변경이 불가능해서 테스트가 힘들다는 단점, DI 프레임워크가 없으면 아무것도 할 수 없다.
  • 사용하지 말 것
@Component
public class OrderServiceImpl implements OrderService {
    @Autowired
    private MemberRepository memberRepository;
    @Autowired
    private DiscountPolicy discountPolicy;
}

 

1.3. 일반 메서드 주입

  • 일반적으로 잘 사용하지 않는다.
@Component 
public class OrderServiceImpl implements OrderService {

    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;
    
    @Autowired // setter 주입과 유사
    public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
    	this.memberRepository = memberRepository;
    	this.discountPolicy = discountPolicy;
    }
}

 

2. 옵션처리

  • 자동 의존관계 주입을 하려하는데 주입할 스프링 빈이 없을 때, 동작시키려고 하는 옵션 3가지
    DI 안해도 동작하도록 하고 싶을 때

  • 생성자 주입, setter 주입, 필드 주입에도 다 사용 가능
    특히, @Nullable, Optional은 스프링 전반에 걸쳐서 지원 된다. - 생성자 자동 주입에서 특정 필드에만 사용해도 된다.
    1. @Autowired(required=false) : 자동 주입할 대상이 없으면수정자 메서드 자체가 호출 안됨
      @Autowired는 default가 true 이다.
    2. @Nullable : 자동 주입할 대상이 없으면 null이 입력
    3. Optional<> : 자동 주입할 대상이 없으면  Optional.empty 가 입력
  • 참고: @Nullable, Optional은 스프링 전반에 걸쳐서 지원된다. ex) 생성자 자동 주입에서 특정 필드에만 사용해도 적용 된다.

  • Member.class는 spring 관련 bean이 아니다. 
    == spring Container에 관리되는 것이 아닌 경우 autowired 수행 예시
package hello.core.autowired;

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

    static class TestBean {
    // Member.class는 spring 관련 bean이 아니다. 
    // == spring Container에 관리되는 것이 아닌 경우 autowired 수행 예시
        @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);
        }
    }
}

 

3. 생성자 주입 권장 이유 3가지

3.1. 불변

  • 대부분 의존관계 주입은 한번 일어나면 app 종료까지 변경할 필요가 없고 대부분 변경되어선 안된다.
  • -> 수정자 주입 사용시 setter를 public으로 열어둬야한다. - 누군가 실수로 변경할 수 있어서 위험하다.

    

3.2. 누락

  • 가정: TEST 단계에서 java 코드로만 단위테스트를 수행하려고 한다. - 실제로도 많이 쓰이고 제일 좋은 test 방법
  • 앞서 배운 생성자 주입 방법, 수정자 주입 방법을 비교한다.
  • 컴파일 오류가 발생해야지 바로바로 해결이 가능하다.
// 수정자 주입시
@Test
void createOrder(){
	OrderServiceImpl orderService = new OrderServiceImpl();// compile 오류 안뜸
    orderService.createOrder(1L, "item", 1900);
}
// compile 오류 발생 안함. runtime때 null point Exception 뜬다. 의존관계 주입이 모두 누락되었기 때문
// 생성자 주입시
@Test void createOrder() {
OrderServiceImpl orderService = new OrderServiceImpl(); // compile 오류 발생
orderService.createOrder(1L, "itemA", 10000);
}

// compile 오류 발생, 여기에 임의이 memberRepository, discountPolicy 넣어서 사용하면 된다.

 

3.3. final keyword

  • 설정 파일 입장에서 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아준다. 
  • 수정자 주입때 사용 못함
@Component public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    '''
}

 

3.4. 정리

  • 생성자 주입 방식을 선택하는 이유는 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살리는 방법이다.
  • 기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면 된다.
  • 항상 생성자 주입을 선택, 그리고 가끔 옵션 필요시 수정자 주입 선택.
  • 필드 주입은 사용하지 말 것.

 

4. Lombok

  • 코드 간략화 최신 트랜드
  • 개발을 해보면, 대부분이 다 불변이고, 다음과 같이 생성자에 final 키워드를 사용하게 된다.
  • 그런데 생성자도 만들어야하고 주입 받은 값을 대입하는 코드도 만을어야한다. 이러한 반복작업을 해결할 준다.

  • 사용예시
    1. getter
    2. setter
    3. 생성자
    4. toString (이건 권장 안함)
    5. requiredArgsConstructor
    6. Data

 

4.1. Lombok library 설치

  • spring.start.io 생성 때 lombok 추가 - 권장

  • 만약 직접 추가할 경우

    4.1.1. build.gradle - library 및 환경 추가

'''
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'

    //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'
	}
}
...

    4.1.2. 설정 수정

  1. Preferences -> plugin ->  lombok 검색 설치 실행 -> (재시작)
  2. Preferences  -> Annotation Processors ->   Enable annotation processing 체크 (재시작)

 

4.2.  @RequiredArgsConstructor

  • 기능: 생성자 주입이 필요한 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.
  • 조건
    • 생성자가 1개 일 때
    • 필드에 final 붙었을 때
// lombok 사용 전
@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;
    }
}

// lombok 사용
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
}

 

5. 조회 bean이 2개 이상 - 문제와 해결

  • 문제
    • @Autowired는 type으로 조회
      - ac.getBean(DiscountPolicy.class) 형식으로 동작

    • 아래 2개 동일 type bean 등록 경우
      1. @Component public class FixDiscountPolicy implements DiscountPolicy {}
      2. @Component public class RateDiscountPolicy implements DiscountPolicy {}
    • 예외 발생
      • @Autowired private DiscountPolicy discountPolicy
      • NoUniqueBeanDefinitionException

 

 

  • 해결 - 3가지 방법
    1. @Autowired 필드 명
    2. @Qualifier
    3. @Primary 

 

5.1. @Autowired 필드 명

  • @Autowired 매칭 정리
    1.  타입 매칭 을 우선 수행
    2. 동일 타입 빈이 2개 이상일 때, 필드명, 파라미터 명과 동일 한 빈을 매칭
@Autowired
private DisountPolicy discountPolicy;

//@Autowired 필드 명
@Autowired
private DiscountPolciy rateDiscountPolicy;

//@Autowired 파라미터 명
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy){
    this.memberRepository = memberRepository;
    this.discoutnPolicy = ratediscountPolicy;
}

 

5.2. @Qualifier

  • 추가 구분자를 붙여주는 방법
  • 등록위치
    1. @Component - @Autowired 자동 주입
      1. 생성자 주입
      2. 수장자 주입
    2. AppConfig - @Bean에 주입

 

  • 작동 방식
    1. 번 방식에서 모든 것이 종료 되야지, 2번으로 넘어간다는 것은 보통 실수로 인한 잡기 힘든 에러가 될 가능성이 높다.
    1. 타입으로 빈을 찾는 것이 아니라 해당 @Qualifier("이름")의 이름과 동일 이름을 가지고 있는 class을 찾는다.
    2. 만약  해당 @Qualifier를 가지고 있는 Bean이 없을 시, Bean의 이름이 @Qualifier의 이름 (현재 예시에선 mainDiscountPolicy)과 동일 이름의 Bean 한 것을 찾아서 등록한다.
    3. 아무 것도 없으면 예외 발생

 

  • 1.1. 생성자 주입
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}

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

@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
 }
  • 1.2. 수정자 주입
@Autowired
public DiscountPolicy setDiscountPolicy(@Qualifier("mainDiscountPolicy")
                                        DiscountPolicy discountPolicy){
    this.discountPolicy = discountPolicy;
}
  • 2. AppConfig file
@Bean
@Qualifier("mainDiscountPolicy")
public DiscountPolicy discountPolicy(){
    return new ...
}

 

5.3. @Primary 

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

  • @Primary vs @Qualifier
    • @Qualifier: 관련 class 하위 모든 Component에는 @Qualifier 다붙여줘야함.
    • @Primary: @Component에만 붙이면 된다.

    • @Qualifier 가 @Primary보다 구체적이므로 서로 중복 될때, @Qualifier가 우선권을 가진다.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
public class FixDiscountPolicy implements DiscountPolicy {}

// 아래는 원래 주입코드와 동일함
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
	this.memberRepository = memberRepository;
	this.discountPolicy = discountPolicy;

 

  • @Primary와 @Qualifier 사용 선택
    • 메인 데이터베이스의 커넥션을 획득하는 스프링 빈은  @Primary를 적용해서 조회하는 곳에서  @Qualifier 지정 없이 편리하게 조회
    • 서브 데이터베이스 커넥션 빈을 획득할 때는  @Qualifier를 지정해서 명시적으로 획득 하는 방식으로 사용하면 코드를 깔끔하게 유지

 

6.  어노테이션 직접 만들기 _ @Qualifier

  • @Qualifier를 사용할 때 string을 사용 ->  오타가 있을 때 compiler가 잡지를 못한다.
  • 예외 방지 위해서 @Qualifier 정보를 포함한 임의의 어노테이션을 만든다.

 

  • 어노테이션 만들기
//@Qualifier에서 가져온 정보
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) 
@Retention(RetentionPolicy.RUNTIME) 
@Documented 
// 앞으로 사용할 임의의 애노테이션
@Qualifier("mainDiscountPolicy") 
public @interface MainDiscountPolicy { }
  • 적용
    1. @Component
    2. 생성자 자동 주입의 params
    3. 수정자 주입
// 1.Component
@Component 
@MainDiscountPolicy 
public class RateDiscountPolicy implements DiscountPolicy {}


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

// 3. 수정자 주입
@Autowired
public DiscountPolicy setDiscountPolicy (@MainDiscountPolicy DiscountPolicy discountPolicy){
    this.discountPolicy = discountPolicy;
}

 

  • 참고
    • 어노테이션에는 상속이라는 개념이 없다.
    • 여러 애노테이션을 모아서 사용하는 기능은 스프링이 지원해주는 기능
    • @Qulifier 뿐만 아니라 다른 애노테이션들도 함께 조합해서 사용할 수 있다.
    • 하지만 무분별하게 사용하지 말 것
    • 가능하면 원래 제공해주는 것 그대로 사용할 것

 

7.  조회한 빈이 모두 필요할 때 - List, Map  <중요>

  • 의도적으로 해당 타입의 스프링 빈이 다 필요한 경우가 존재한다. 
    ex) 할인 서비스를 제공하는데 client가 할인의 종류를 선택할 수 있도록 하는 경우

  • 모든 빈 조회 방법 == 전략패턴
  • List와 Map을 이용하면 다 받을 수 있다. 
    1. Map<String, BeanType>
      • Map key = spring bean name
      • Map values = bean 객체
    2. List<DiscountPolicy>
      • bean 객체 나열

 

  • 사용 로직 예시
    1. 스프링은 BeanType에 해당하는 빈들을 찾아서 Map으로 의존관계를 주입
    2. controller getMapping에서 원하는 할인 선택 - 내부적으로 data 넘어 갈때 string은 bean 이름으로 넘어감
    3. controller postMapping에서 DiscoutService 에 getMapping으로 부터 받은 data 넘김
    4. 해당 data는 int discount() 의 params로 들어가게 된다.
      1. int discount내부에서 Map의 빈들 중에 1가지를 호출
      2. 해당 discount를 적용한 int를 반환

 

  • 코드 
package hello.core.autowired;

import static org.assertj.core.api.Assertions.*;

public class AllBeanTest {
    @Test
    void findAllBean() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class, AutoAppConfig.class, DiscountService.class);
        /* CGLIB 기준
        appconfig는 CGLIB가 적용되지만 자동 주입 받는 녀석들은 적용 안됨. appconfig 내부 녀석들도 cglib가 없다.
        아마도 자동 주입 받는 녀석들은 singleton 문제를 해결된 상태에 ac에 객체로 들어가진거고
        appconfig 같은 경우는 한번 cglib로 singleton 문제를 해결한 후에 그 내부의 값을 빈으로 등록하는거여서 내부 값들은 CGLIB가 없는 듯 하다.
        hello.core.AppConfig$$SpringCGLIB$$0@79c7532f
        hello.core.AutoAppConfig@2a448449
        hello.core.autowired.AllBeanTest$DiscountService@32f232a5
        hello.core.discount.FixDiscountPolicy@6bffbc6d
        hello.core.discount.RateDiscountPolicy@1b84f475
        hello.core.member.MemberServiceImpl@43f82e78
        hello.core.member.MemoryMemberRepository@e54303
        hello.core.order.OrderServiceImpl@e8df99a
        hello.core.order.OrderServiceImplLombok@2dc995f4
        hello.core.order.OrderServiceImplSetter@2f40e5db
        hello.core.member.MemoryMemberRepository@517566b
        hello.core.discount.RateDiscountPolicy@7749bf93
        hello.core.member.MemberServiceImpl@64b73e7a
        hello.core.order.OrderServiceImpl@530712d
        */
        for (String beanDefinitionName : ac.getBeanDefinitionNames()) {
            System.out.println(ac.getBean(beanDefinitionName));
        }

        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
            // 경고 문구. 정상동작함
            // ide 입장에서는 @Component가 아닌 것이 @Autowired 사용해서 경고 문구 발생.
            // 직접 springContext에 넣기 때문에 @Autowired 있어도 됨. 생략도 가능
        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);
        }
    }
}

 

 

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

  • 편리한 자동 기능을 기본으로 상요
  • 최근 스프링부트는 컴포넌트 스캔을 기본으로 사용

  • 업무로직 - 자동 기능
    • 컨트롤러, 서비스, 리포지토리
    • 문제 발생해도 문제 발생 위치 파악이 쉽다

  • 기술로직 - 수동 기능
    • AOP(공통 관심사), 기술적인 문제 
    • db 연결, 공통 로그 처리
    • 광범위하게 영향을 미쳐서 문제 발생시 파악이 힘들다.
    • 수동 빈 등록을 사용해서 명확하게 하는 것이 좋다.

  • 예외 사항
    • spring과 springboot가 자동으로 등록하는 수 많은 빈들은 이들이 지원해주는 기술을 따르는 것을 권장
    • 내가 직접 기술 지원 객체를 만들어서 빈으로 등록시 수동으로 하여 명확히 하는 것이 좋다.

  • 업무로직 중 다형성을 활용할 때 - 자동, 수동 고민
    • Map으로 쌓인 bean이 어떤게 얼마나있는지 한 눈에 파악하기 위해서 수동 빈으로 등록
    • 자동으로 하고 싶으면 같은 패키지에 묶어둔다.

 

//업무로직 중 다형성
@Configuration 
public class DiscountPolicyConfig {
    @Bean     
    public DiscountPolicy rateDiscountPolicy() {
        return new RateDiscountPolicy();
    }
    @Bean
    public DiscountPolicy fixDiscountPolicy() {
        return new FixDiscountPolicy();
    }
}

 

 

 

이전 발행글 : 스프링 핵심 원리 이해 5 - 컴포넌트 스캔

 

다음 발행글 : 스프링 핵심 원리 이해 7 - bean 생명주기 callback

 


출처: 인프런 스프링 핵심 원리 - 기본편