Published:
Updated:

망나니 개발자님의 블로그를 보고 제 생각을 첨가하여 정리한 글입니다.

Why

스크린샷 2023-04-02 오후 5 37 49

  • 코드스쿼드 리뷰어 kyu의 말씀으로 내가 그동안 “왜”를 너무 등한시했던 경향이 있었다는 것을 깨달았다.
    • 그 무엇하나 허투로 넘어가지 말자.
    • 도저히 모르겠는 것들은 기록으로 남겨두더라도 절대 그냥 넘어가지는 말자.
  • 의존성 주입 안에 내가 가장 궁금했던 생성자 주입이 있으니 단계적으로 정리해 보려 한다.


DI (Dependency Injection)

  • Sprin 프레임워크는 3가지 핵심 프로그래밍 모델을 지원하고 있는데 그 중 하나가 DI다.
    • DI를 의존성 주입이라 함
      • Inversion of Control이라고도 함
    • DI란 외부에서 두 객체간의 관계를 결정해주는 디자인 패턴
      • 인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않도록 하고
      • 런타임 시에 관계를 동적으로 주입하여 유연성을 확보하고 결합도를 낮출 수 있게 해줌
  • 어떤 객체가 사용하는 의존 객체를 직접 만들어 사용하는 게 아닌 주입 받아 사용하는 방법을 말함
  • 의존성이란 한 객체가 다른 객체를 사용할 때 의존성이 있다고 함
    • 아래와 같이 Store 객체가 Pencil 객체를 사용하고 있는경우 Store 객체가 Pencil 객체에 의존성이 있다 말함
public class Store {
    private Pencil pencil;
}
  • 두 객체간의 관계를 맺어주는 것을 의존성 주입이라 함
    • 생성자 주입, 필드 주입, 수정자 주입 등 다양한 주입 방법이 있음
  • Spring 4부터 생성자 주입을 강력 권장


의존성 주입 방법

생성자 주입

  • 생성자 주입 (Constructor Injection)
    • 생성자를 통해 의존 관계를 주입하는 방법
@Service
public class UserSerice {
    private UserRepository userRepository;
    private MemberService memberService;

    @Autowired
    public UserService(UserRepository userRepository, MemberService memberService) {
        this.userRepository = userRepository;
        this.memberService = memberService;
    }
}
  • 생성자 주입은 생성자의 호출 시점에 1회 호출이 보장
    • 주입받은 객체가 변하지 않거나, 반드시 객체의 주입이 필요한 경우
      • 이를 강제하기 위해 사용할 수 있음
  • Spring 프레임워크에서는 생성자 주입을 적극 지원하고 있음
    • 그렇기 때문에 아래처럼 생성자가 1개만 있을 경우에 @Autowired를 생략해도 주입이 가능
      • 내가 미션하면서 이해했던 거로는 생성자가 아니라 필드가 하나일 때 @AutoWired를 쓰지 않는 거였는데
      • 사실은 생성자가 1개일 때는 생략해도 주입이 가능했던 거였고, 필드가 아니라 생성자 얘기였다.
@Service
public class UserSerice {
    private UserRepository userRepository;
    private MemberService memberService;

    public UserService(UserRepository userRepository, MemberService memberService) {
        this.userRepository = userRepository;
        this.memberService = memberService;
    }
}

수성자 주입 (Setter 주입)

  • 수성자 주입 (Setter Injection)
    • 필드 값을 변경하는 Setter를 통해서 의존 관계를 주입하는 방법
  • Setter 주입은 생성자 주입과는 다르게 주입받는 객체가 변경될 가능성이 있는 경우 사용
    • 하지만, 실제로 변경이 필요한 경우는 극히 드묾
@Service
public class UserService {
    private UserRepository userRepository;
    private MemberService memberSerive;

    @Autowired
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}
  • @Autowired로 주입할 대상이 없는 경우에는 오류 발생
    • 주입할 대상이 없어도 동작하도록 하려면
      • @Autowired(requied = false)를 통해 설정 가능
  • 스프링 초기에는 수정자 주입이 자주 사용됨
    • 그 이유는 게터, 세터 등의 프로퍼티를 기반으로 하는 자바의 기본 스펙 때문
    • 시간이 지나면서 점차 수정자 주입이 아닌 다른 방식이 주목받게 됨
      • 이는 그동안 내가 수십번 들어왔던 게터와 세터를 쓰지 말라는 것과 일치한다.
      • 이번 미션에서는 실력이 부족하지만 어쩔 수 없이 게터와 세터로 수정자 주입을 이용했지만
      • 어느 정도 익숙해지면 최대한 쓰지 않도록 노력해보자!

필드 주입

  • 필드 주입 (Field Injection)
    • 필드에 의존 관계를 주입하는 방법
    • 인텔리제이에서 Field Injection을 사용하면 Field injection is not recommended이라는 경고 문구 발생
      • 이것이 여기에 첨부한 사진에 나와 있는 것처럼 내가 의존성 주입에 대해서 공부하게 된 계기다.
      • 결국 이것이 궁금해서 여기까지 왔다.
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private MemberService memberService;
}
  • 필드 주입을 이용하면 코드가 간결해져서 과거에 상당히 이용되었던 주입 방법
    • 하지만 필드 주입은 외부에서 접근이 불가능하다는 단점이 존재
      • 테스트 코드의 중요성이 부각됨에 따라 필드의 객체를 수정할 수 없는 필드 주입은 거의 사용되지 않게 됨
    • 또한 필드 주입은 반드시 DI 프레임워크가 존재해야 하므로, 사용을 지양해야 함
  • 애플리케이션의 실제 코드와 무관한 테스트 코드나 설정을 위해
    • 불가피한 경우에만 이용

일반 메서드 주입

  • 일반 메서드를 통해 의존 관계를 주입하는 방법
    • 수정자 주입과 동일하며 마찬가지로 거의 사용할 필요가 없는 주입 방법
      • 수정자 주입을 사용하면 한 번에 여러 필드를 주입받을 수 있도록 메서드를 작성할 수도 있음


Why 생성자 주입을 사용해야 하는가

객체의 불변성 확보

  • 개발을 하다 보면 의존 관계의 변경이 필요한 상황은 거의 없음
    • 하지만 수정자 주입이나 일반 메서드 주입을 이용하면 불필요하게 수정의 가능성을 열어두어
      • 유지보수성을 떨어뜨림
  • 그러므로 생성자 주입을 통해 변경의 가능성을 배제하고 불변성을 보장하는 것이 좋음

테스트 코드의 작성

  • 테스트가 특정 프레임워크에 의존하는 것은 침투적이므로 좋지 않음
    • 그러므로 순수 자바로 테스트를 작성하는 것이 가장 좋음
    • 생성자 주입이 아닌 다른 주입으로 작성된 코드는 순수한 자바 코드로 단위 테스트를 작성하는 것이 어려움
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private MemberService memberService;

    public void register(String name) {
        userRepository.add(name);
    }
}
  • 예를 들어 위 코드에 대해 순수 자바 테스트 코드를 작성하면 아래와 같이 작성할 수 있음
public class UserServiceTest {
    @Test
    public void addTest() {
        UserService.userService = new UserService();
        userService.register("Sully");
    }
}
  • 이 테스트 코드는 Spring 위에서 동작하지 않음
    • 따라서 의존 관계 주입이 되지 않을 것이고
    • userRepository가 null이 되어 add 호출 시 널 포인트 익셉션이 발생
    • 이를 해결하기 위해 Setter를 사용하면 변경 가능성을 열어두게 되는 단점을 갖게 됨
  • 반대로 테스트 코드에서 @Autowired를 사용하기 위해 스프링을 사용하면
    • 단위 테스트가 아닐 뿐만 아니라
      • 컨포넌트들을 등록하고 초기화하는 시간 때문에 비용이 증가하게 됨
    • 대안으로 Reflection을 사용하면 깨지기 쉬운 테스트가 됨
      • 이것도 몇 주 전 미션인 사다리 게임에서 내가 직접 맞닥뜨린 문제였다.
      • 객체지향적으로 설계를 해본 적이 없어서 public 메서드 안에 private 메서드가 주렁주렁 달린 형태였는데
      • 이 private 메서드를 테스트하기 위해 Reflection을 사용했었다.. 다신 사용하지 말아야지
  • 반면에 생성자 주입을 사용하면
    • 컴파일 시점에 객체를 주입받아 테스트 코드를 작성할 수 있고
    • 주입하는 객체가 누락된 경우 컴파일 시점에 오류를 발견할 수 있음
    • 심지어 테스를 위해 만든 Test 객체를 생성자에 넣어 편리함을 얻을 수도 있음

final 키워드 작성 및 Lombok과의 결합

  • 생성자 주입을 사용하면 필드 객체에 final 키워드를 사용할 수 있으며
    • 컴파일 시점에 누락됨 의존성을 확인할 수 있음
  • 반면에 다른 주입 방법들은 객체의 생성(생성자 호출) 이후에 호출되므로
    • final 키워드를 사용할 수 있음
  • 또한 final 키워드를 붙이면 Lombok과 결합되어 코드를 간결하게 작성할 수 있음
    • Lombok에는 final 변수를 위한 생성자를 대신 생성해주는 @RequiredArgsConstructor가 있음
      • 이번 미션에서는 호눅스가 Lombok을 사용하지 말라고 하셨기 때문에 나중에 이 내용에 대해서 읽어보자
  • Spring과 같은 DI 프레임워크는 Lombok과의 환상적인 궁합을 보여줌
    • 에서 작성했던 생성자 주입 코드를 Lombok과 결합시키면 아래와 같이 간편하게 작성할 수 있음
@Service
@RequiredArgsConstructor
public class UserService {
    private UserRepository userRepository;
    private MemberService memberService;

    public void register(String name) {
        userRepository.add(name);
    }
}
  • 이러한 코드가 가능한 이유는
    • 생성자가 1개인 경우 @Autowired를 생략할 수 있도록 하고
    • 해당 생성자를 Lombok으로 구현했기 때문
      • 이 부분도 Lombok을 사용할 수있을 때 여기에서 읽어 보자!

스프링에 비침투적인 코드 작성

  • 필드 주입을 용하려면 @Autowired를 이용해야 함
    • 이것은 스프링이 제공하는 어노테이션임
      • 그러므로 @Autowired를 사용하면 아래와 같이 UserService에 스프링 의존성이 침투하게 됨
import org.springframework.beans.factory.annotation.Autowired;
// 스프링 의존성이 UserService에 import되어 자동 추가됨

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private MemberService memberService;
}
  • 프레임워크는 언제 바뀔지도 모를 뿐만 아니라
    • 사용자와 관련된 책임을 지는 UserService에 스프링 코드가 박혀버리는 것은 바람직하지 않음
  • 프레임워크는 비즈니스 로직을 작성하는 서비스 계층에서 알아야 할 대상이 아님
    • 물론 이는 필요한 자바 파일을 import 해야 하는 정적 언어인 자바의 한계이기도 함
    • 그래도 가능하다면 스프링을 사용하지 말고 코드를 작성해보자.
      • 그럼 더 유연한 코드가 확보되게 된다고 하니까 👀
  • 프레임워크가 자주 바뀌는 것도 아니므로 비록 스프링 코드가 침투하는 게 치명적인 문제가 아니긴 함
    • 그래도 생성자 주입 같은 더 좋은 방법이 있으니까 굳이 사용하지 말자!

순환 참조 에러 방지

  • 생성자 주입을 사용하면
    • 애플리케이션 구동 시점(객체의 생성 시점)에 순환 참조 에러를 예방할 수 있음
  • 예를 들어 아래와 같이 필드를 사용하여 서로 호출하는 코드를 보자
@Service
public class UserService {
    @Autowired
    private MemberService memberService;

    @Override
    public void register(String name) {
        userRepository.add(name);
    }
}
@Service
public class MemberService {
    @Autowired
    private UserService memberService;

    @Override
    public void add(String name) {
        userRepository.register(name);
    }
}
  • UserService가 이미 MemberService에 의존하고 있고
    • MemberService 역시 UserService에 의존하고 있음
  • 이 두 메서드는 서로를 계속 호출할 것이고
    • 메모리에 함수의 CallStack이 계속 쌓여 StackOverFlow 애러가 발생하게 됨
  • 만약 이러한 문제를 발견하지 못하고 서버가 운영된다면
    • 해당 메서드 호출 시에 StackOverFlow 애러에 의해 서버가 죽게 됨
  • 하지만 생성자 주입을 이용하면 순환 참조 문제를 방지할 수 있음
    • 애플리케이션 구동 시점(객체의 생성 시점)에 에러가 발생하는 것인데,
    • Bean에 등록하기 위해 객체를 생성하는 과정에서 아래와 같은 순환 참조 문제가 발생하는 것
new UserService(new MemberService(new UserService(new MemberService()...)))
  • @Autowired를 이용한 필드 주입에서 위와 같은 문제가 애플리케이션 구동 시점에 발생하지 않는 이유는
    • Bean의 생성과 조립(@Autowired) 시점이 분리되어 있기 때문
  • 생성자 주입은 객체의 생성과 조립(의존관계 주입)이 동시에 실행되다 보니
    • 위와 같은 에러를 사전에 잡을 수 있음
  • 하지만 @Autowired는 모든 객체 생성이 완료된 이후에 조립(의존관계 주입)이 처리됨
    • 즉, 위와 같은 호출이 되고 나서 순환 이슈를 확인할 수 있는 것
  • 참고로 SpringBoot 2.6 버전부터 순환 참조가 기본적으로 허용되지 않도록 변경됨
    • 필드 주입을 받아도 순환 참조가 발생한다면 애플리케이션 로딩 시점에 에러가 발생하므로
      • 위와 같은 에러는 SpringBoot 2.6 이전의 버전을 사용하는 경우에만 발생할 것임


생성자 주입 요약 및 정리

  • 객체의 불변성을 확보할 수 있음
  • 테스트 코드의 작성이 용이해짐
  • final 키워드를 사용할 수 있고
    • Lombok과의 결합을 통해 코드를 간결하게 작성할 수 있음
  • 스프링에 침투적이기 않은 코드를 작성할 수 있음
  • 순환 참조 에러를 애플리케이션 구동(객체의 생성) 시점에 파악하여 방지할 수 있음
  • kyu의 단 한줄의 질문으로 여기까지 공부할 수 있었다.
    • 이 내용 때문에 생성자 주입에 대해서 완벽하게는 아니어도 어느정도는 이해할 수 있었던 것 같다.
    • 생성자 주입이라는 키워드와 그 의미를 계속 상기시키면서 내 프로젝트에 적용할 수 있도록 노력해보자 💪🏻


Reference

  • https://mangkyu.tistory.com/150
  • https://mangkyu.tistory.com/125
  • https://devlog-wjdrbs96.tistory.com/165

Tags:

Categories:

Published:
Updated:

Leave a comment