[Spring 기본] 의존 관계 주입 전략 (DI Strategy)

2022. 10. 17. 17:24·Spring/Spring 기본
728x90
반응형

이전 포스트에서 잠시 언급된 적이 있지만, Spring에서는 App 을 탐색하며 Bean을 등록하는 과정은 크게 두 가지 절차를 거치게 됩니다. (당장 지난 포스트에서 그림들. 1번, 2번, 3번 그림) 

 

1. Bean 생성
2. 의존 관계 주입 (DI)

 

지금까지는 간단히 생성자를 통해서 DI를 진행하였지만 (제일 일반적인 방법입니다), 생성자를 통한 주입 말고도 DI 에는 크게 4가지 전략이 있습니다 (Autowire Strategy).

 

 

 

(1) 생성자 주입

 

@Component
public class MemberServiceImpl implements MemberService{

    private final MemberRepository memberRepository;

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

 

객체를 만들 때는 일반적으로 생성자 호출을 하게 되므로, 생성자 호출시 Autowired 가 있고, 가장 일반적인 방법

입니다. Autowired가 있는것을 Spring 이 확인하면 Spring Container 에서 MemberRepository 로 등록된 구현

객체를 꺼내서 주입시켜줍니다. 

 

생성자 주입 같은 경우 싱글톤으로 보관된다는 특성상 (개발 방향이 이와 같을 경우) 앱 내에서 딱 한 번만 호출되는

것이 보장됩니다. 따라서, 변하지 않는 의존관계에서 자주 사용됩니다. 그리고 사실 변하지 않아야 할 관계에서는

생성자로 인해서 주입되는 것이 맞기 때문에, 개발시 임의로 set method 생성 등을 통해 함부로 변경 통로를 열어

두면 안됩니다. 만약 해당 역할을 여러 구현체들이 번갈아가며 수행시켜줘야 할 경우, 애초에 Map 등을 통해서 둘

다 주입받아서 내부에서 case 를 나눠서 사용해야 합니다 (지난 포스트 참조).

 

(참고로, 개발 협업시 누군가가 한번 DI시 불변해야하는 객체를 설계했는데 setter를 같이 열어줬다면, 그리고 다른

개발자가 바꿔도 되나보다 하고 setter를 사용해서 서버 오류가 발생했다면, 그건 설계한 사람의 잘못이 더 크다고

합니다. 함부로 setter를 열어두면 안되는 점 꼭 알고 갑시다!) 

 

근데 경험상 setter 없으면... 왜 setter 없어 ㅋㅋ 하면서 @Setter 바로 박고 사용하는 사람들도 많은 것 같다는... 

 

 

 

(2) 수정자 주입

 

@Component
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

 

Setter 에 @Autowired 를 달아놓게 되어도 Bean 등록 후 자동으로 의존관계를 주입시키게 됩니다. 다만, setter

을 열어두는 것이기 때문에 "필요에 따라 의존관계를 바꿀 수 있고, 바꾸는 것을 허락한다"라는 의미를 내포하게 되

니, 주의하면 될 것 같습니다. 참고로 이 방식을 택하더라도 어차피 기본 생성자는 호출되어 만들어지기 때문에, 엔

간하면 생성자 주입을 하고 추후 수정을 가능하게 하려면 수정자를 열어두는 방식을 택하는 것이 일반적인 방식입

니다. 하지만 변하지 않는 관계에서는, 수정자를 열어두지 않는 것이 훨~~씬 안정적입니다. 

 

 

 

(3) 필드 주입

 

@Component
public class OrderServiceImpl implements OrderService {

    @Autowired 
    private MemberRepository memberRepository;
    
    @Autowired 
    private DiscountPolicy discountPolicy;
    
    ...
}

 

필드에 있는채로 바로 DI를 자동으로 해주는 방법입니다. 제일 간단하죠? Spring Boot 로 Data JPA 등 빠르게 스프링 배우신 분들은 이 방법에 익숙하실 것이라 생각합니다. 하지만 IntelliJ 에서도 "Field injection is not recommended"이라고 나오듯이, 필드 주입은 권장되는 방법이 아닌, 안티 패턴입니다. 대표적인 이유는 바로 외부 변경이 불가능해서 테스트 하기어렵기 때문입니다. 

 

여기서 변경한다는 것은 수정자를 통해서 바꾸는 것과 다른 개념입니다. 수정자 주입은 setter 호출로, 생성자 주입은 생성자 호출로 사용하는 구현 객체를 갈아 끼워줄 수 있습니다. 하지만 필드를 통한 DI는 구현 객체를 갈아 끼울 방법이 없습니다 public 으로 선언하여 외부에서 바꿀 수 있지 않냐 할 수도 있지만 그 방식은 기본적인 Java 캡슐화 방식과 맞지 않는 패턴입니다. 즉, 기본적인 setting 을 못해주고 DI에만 의존하기 때문에, 가장 기본적인 Java 테스트를 못하게 됩니다. 다음과 같은 Test를 예로 들어보겠습니다. 

 

@Test
void test1(){
    OrderService orderService = new OrderServiceImpl();
    orderService.createdOrder(1L, "memberA", 10000);
}

 

실제 App을 만들면서 여러 객체를 만들게 되는데 일반적으로 각 객체마다 Test 를 진행하게 됩니다. 담당 로직 Test, 생성 Test 등 다양한 Test 를 거쳐서 안정적인 App을 만들어야 하므로, DI가 없더라도 내부 Test 정도는 필수적으로 진행이 되어야 합니다. 하지만 필드 주입을 사용하면, DI 없이는 구현체가 지정되지 않아서 아무 수행을 하지 못하며, 위 Test Code 역시 NullPointerException 이 발생하게 됩니다. 

 

"???? 왜요??? OrderService 내부에 있는 MemberRepository 를 IOC 컨테이너에서 가져와서 주입해주기 위해서 @Autowired 를 넣어주는 것 아닌가요???" 네 맞습니다. 하지만 위 코드를 잘 보시기 바랍니다. 위 OrderService 는 지금 IOC Container 에서 가져온게 아니라, 순수 Java 로 객체를 생성하였습니다. IOC 컨테이너에서 가져와야 필요 객체들이 주입된 싱글톤을 가지고 올 수 있는 것은 @Config 의 동작 방식 포스트에서 충분히 이해할 수 있었습니다. 따라서, 위 처럼 Java 테스트를 할 시에, memberRepository 를 지정해줄 방법이 전혀 없어서 필드 주입에 문제가 되는 것입니다.

 

따라서 필드 주입도 제대로 동작을 시키려면 Setter 메소드가 필수적이지만, 그럴바엔 그냥 Setter 에 DI를 거는것이 훨씬 낫습니다. (하지만 실제 앱이 아니라 Test Code 내 필드에서만 빠르게 주입시켜서 쓸 때에는 필드 주입 법이 매우 간편 합니다. 그 테스트 클래스를 어디서 가져다 쓸 용도가 아니기 때문에, 이런 경우에는 필드를 통해 빠르게 주입하는 방법이 훨씬 합리적입니다)

 

 

 

(4) 일반 메소드 주입

 

@Component
public class OrderServiceImpl implements OrderService {

    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

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

 

일반 메소드를 활용해서도 의존 관계를 자동주입 설정을 해줄 수 있습니다. 하지만 setter 방식과 큰 차이 없기 때문에 잘 사용되지는 않는 방식입니다. 

 

 

 

왜 생성자 주입?

 

 

최근에는 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장한다고 합니다. 대표적인 이유는 위에서 언급하였듯이, 대부분 DI 는 App 종료까지 변경할 일이 거의 없기 때문입니다. Setter을 열어두는 것은 누군가가 실수로 변경할 수 있는 여지를 남기는 것인데, DI는 App 의 방향 변경 외에는 불변하는 것이 맞기 때문에 불변하도록 설계가 되어야 하므로, 불변성이 보장된 생성자 주입을 대부분 권장한다고 합니다. 

 

또한, 필드 주입방법과 같이, 일반 Java Test 시 Setter 함수를 통해 필요한 구현체들을 넣어줘야 하기 때문입니다. 만약 Bean 등록이 이루어지지 않은채로 다음과 같이 실행된다고 가정해봅시다.

 

@Test
void test(){
    MemberService memberService = new MemberServiceImpl();
    memberService.join("1L", "memberA");
}

 

MemberService에는 내부적으로 MemberRepository 의 의존관계가 필요하여 Setter로 주입시키도록 설계된 상태입니다. 따라서 첫 줄 생성자는 아무 오류가 없습니다. 하지만 해당 테스트를 돌릴시에는, join 함수에 대한 NullPointerException 이 발생하게 됩니다. MemberRepository 에 대한 의존관계 주입이 이루어지지 않았기 때문입니다 (Spring Container에 등록하지 않음). 따라서, Java 에선 어차피 객체를 생성할 때에는 생성자를 통해 생성될 수밖에 없으므로, 생성자를 통해 불변이 보장된 주입을 하는 것이 가장 권장되는 방식입니다. (위에서 set 안해서 뜨는 겁니다. 필드 주입에서 든 예시와 동일)

 

마지막으로, final 키워드를 붙일 수 있습니다. Java를 공부하셔서 아시겠지만 final 은 불변의 변수이기 때문에, 필드 지정을 먼저 해주거나, 생성자를 통해서 지정을 해줄 수 있습니다. 초기화 및 불변을 보장해주기 위해서 DI 대상 필드에 final 을 지정해 주는 습관을 가지는 것이 좋습니다.

 

 

 

* 그래서 최종 모델링:  Lombok 활용

 

 

Spring 에서 제공되는 Lombok Library 를 통해서 한다면 생성자 주입 코드를 보다 깔끔하게 작성할 수 있습니다.  @RequiredArgsConstructor Annotation 을 사용하면 final 로 지정된 필요한 변수들을 가지는 생성자를 기본으로 생성해줍니다. 생성자가 1개만 있으면 @Autowired 없어도 한 개의 생성자를 DI 취급하기 때문에 생략할 수 있다는 특성을 감안하면, 다음과 같이 작성해줄 수 있습니다.

 

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
}

 

 

IOC 를 활용하는 것이 이토록 어렵고 복잡합니다. 하지만 이렇게 오랫동안 사용한 분들의 경험을 배울 수 있고, 추천 방식을 추천받을 수 있으니 참 공부하기 편한 것 같고 감사하다는 생각이 드네요.. :] 이번 포스트는 이렇게 마치겠습니다!

 

 

 

 

 

 

 

포스트 요약

 

  • Spring Container 에서 Bean 을 등록할 때는 Bean 생성 + 의존관계 주입 두 절차로 이루어진다. 
  • DI 4가지 전략에는 각각 생성자, 수정자, 필드, 일반 메소드를 활용하는 전략들이 있다. 
  • 안정성과 일회성이 보장된 생성자를 통한 전략을 많이 활용하고, 수정 가능성 여부에 따라 수정자를 활용하기도 한다.
  • 수정자의 여부는 매우 유심히 두어야 한다. 
  • 필드 DI 전략은 매우 간편하나, Test Code 필드 내에서만 주입할 때 사용하고, App Code 를 작성할 때는 권장하지 않는다. 

 

 

 

출처

 

 

[스프링 기본]으로 엮인 모든 포스트들은 교육 사이트 인프런의 지식공유자이신 김영한님의 [스프링 핵심 원리] 강의를 기반으로 작성되었습니다. 열심히 정리하고 스스로 공부하기 위해 만든 포스트이지만, 제대로 공부하고 싶으시면 해당 강의를 꼭 들으시는 것을 추천드립니다. 

 

 

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

 

728x90
반응형

'Spring > Spring 기본' 카테고리의 다른 글

[Spring 기본] Bean Scope  (0) 2022.10.19
[Spring 기본] Bean 생명주기 콜백  (0) 2022.10.18
[Spring 기본] Component Scan을 통한 Bean 자동화 관리  (0) 2022.10.17
[Spring 기본] Spring Container의 Singleton 전략  (0) 2022.10.14
[Spring 기본] Container와 Bean과 조금 더 친해져보자  (0) 2022.10.13
'Spring/Spring 기본' 카테고리의 다른 글
  • [Spring 기본] Bean Scope
  • [Spring 기본] Bean 생명주기 콜백
  • [Spring 기본] Component Scan을 통한 Bean 자동화 관리
  • [Spring 기본] Spring Container의 Singleton 전략
문케이크
문케이크
    반응형
  • 문케이크
    누구나 개발할 수 있다
    문케이크
  • 전체
    오늘
    어제
    • 전체 보기 (122)
      • CS 이론 (13)
        • 운영체제 (8)
        • 네트워크 (2)
        • 알고리즘 (0)
        • Storage (3)
      • Spring (26)
        • Spring 기본 (12)
        • Spring 심화 (0)
        • JPA (11)
        • Spring Security (3)
      • 리액티브 (0)
        • RxJava (0)
      • SW 설계 (14)
        • OOP (0)
        • UML (3)
        • OOAD (0)
        • Design Pattern (11)
      • Java (8)
      • 웹 운영 (17)
        • AWS (15)
        • 운영 구축 (2)
      • Testing (3)
        • Unit (3)
      • Extra (3)
        • API 적용 (1)
      • 인프라 기술 (5)
        • Kubernetes (2)
        • Elasticsearch (3)
      • Logging (7)
        • Spring (5)
        • 인프라 (2)
      • 일상 (2)
        • 음식점 리뷰 (2)
        • Extra (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • 문케이크의 블로그
  • 인기 글

  • 태그

    JPA
    elasticsearch
    n+1
    spring boot
    단위테스트
    mockito
    Java
    디자인 패턴
    composition
    Setter
    Composite
    spring container
    lazy loading
    decorator
    SRP
    OOP
    Configuration
    GoF
    김영한
    runtime exception
    analyzer
    OOAD
    junit
    객체지향
    lombok
    BEAN
    Design Pattern
    di
    k8s
    Spring
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
문케이크
[Spring 기본] 의존 관계 주입 전략 (DI Strategy)
상단으로

티스토리툴바