[Spring] 의존성 주입 - 생성자 주입을 권장하는 이유

2021. 5. 8. 22:17·Web Backend/Spring
반응형

스프링과 JPA를 개념 순서대로 정리하는 시리즈입니다. 1부 스프링 컨테이너 (3/5)

스프링의 의존성 주입 방법

이번에는 스프링이 의존성을 주입하는 세 가지 방법을 비교하고, 그중 생성자 주입을 권장하는 이유에 대해 알아보겠습니다.

앞에서 살펴본 것처럼 객체를 연결하는 일은 스프링 컨테이너가 담당합니다.

그런데 컨테이너가 객체를 넣어주는 통로는 하나가 아닙니다.

생성자로 넣을 수도 있고, 세터 메서드로 넣을 수도 있고, 필드에 직접 꽂을 수도 있습니다.

세 가지 모두 동작하지만 스프링은 그중 하나만 권장합니다.

이번에는 나머지 두 방법의 문제가 무엇인지 살펴보고, 순환 참조를 직접 만들어 어떤 차이가 생기는지 확인해 보겠습니다.

예제 코드는 Java 21, Spring Boot 3.5, Hibernate 6 환경에서 작성했습니다.


1. 세 가지 주입 방법

생성자 주입

생성자의 파라미터로 받는 방식입니다.

@Service
public class WorkoutService {

    private final WorkoutSessionRepository repository;

    public WorkoutService(WorkoutSessionRepository repository) {
        this.repository = repository;
    }
}

세터 주입

세터 메서드에 @Autowired를 붙이는 방식입니다.

@Service
public class WorkoutService {

    private WorkoutSessionRepository repository;

    @Autowired
    public void setRepository(WorkoutSessionRepository repository) {
        this.repository = repository;
    }
}

필드 주입

필드에 @Autowired를 직접 붙이는 방식입니다.

@Service
public class WorkoutService {

    @Autowired
    private WorkoutSessionRepository repository;
}

코드가 가장 짧아 보입니다.

그래서 예전 자료에서 자주 보이지만, 실제로는 가장 권장되지 않는 방식입니다.


2. 필드 주입의 문제

final을 붙일 수 없습니다

final 필드는 생성자가 끝나기 전에 값이 정해져야 합니다.

필드 주입은 객체가 만들어진 뒤에 값을 넣으므로 final을 쓸 수 없습니다.

따라서 주입된 의존이 나중에 바뀔 수 있는 상태로 남습니다.

스프링 없이는 객체를 만들 수 없습니다

다음과 같은 테스트를 생각해 보겠습니다.

@Test
void 세션을_저장한다() {
    WorkoutService service = new WorkoutService();
    // repository는 어떻게 넣을까?
}

필드가 private이므로 밖에서 값을 넣을 방법이 없습니다.

리플렉션을 쓰거나 스프링 컨테이너를 통째로 띄워야 합니다.

간단한 단위 테스트 하나가 무거워집니다.

의존이 몇 개인지 드러나지 않습니다

생성자 주입은 파라미터가 늘어나면 눈에 띕니다.

파라미터가 다섯 개, 여섯 개로 불어나면 이 클래스가 너무 많은 일을 하고 있다는 신호가 됩니다.

반면 필드 주입은 @Autowired를 한 줄씩 추가하면 그만이라 이런 신호가 보이지 않습니다.


3. 세터 주입의 문제

세터 주입은 필드 주입보다는 낫습니다.

메서드가 공개되어 있으므로 테스트에서 값을 넣을 수 있습니다.

하지만 여전히 문제가 있습니다.

세터가 공개되어 있다는 것은 누구나 언제든 바꿀 수 있다는 뜻입니다.

service.setRepository(다른저장소);   // 실행 중에 바꿀 수 있다

의존 관계는 애플리케이션이 시작될 때 정해지고 그 뒤로 바뀌지 않는 것이 정상입니다.

그런데 세터 주입은 바뀔 수 있는 통로를 계속 열어 둡니다.

또한 세터가 호출되지 않은 상태로 객체가 사용될 수 있습니다.

이 경우 NullPointerException이 실행 중에 발생하며, 그때가 되어서야 문제를 알게 됩니다.


4. 생성자 주입의 장점

final로 불변을 보장할 수 있습니다

private final WorkoutSessionRepository repository;

생성자에서 한 번 정해지고 그 뒤로 바뀌지 않습니다.

컴파일러가 이를 보장하므로 실수로 바꾸는 코드를 애초에 작성할 수 없습니다.

누락하면 컴파일 단계에서 걸립니다

생성자에 파라미터가 있으면 값을 넘기지 않고는 객체를 만들 수 없습니다.

의존을 빠뜨린 코드는 컴파일이 되지 않습니다.

실행해 보기 전에 잡힌다는 뜻입니다.

순수 자바로 테스트할 수 있습니다

@Test
void 세션을_저장한다() {
    WorkoutSessionRepository fake = new FakeWorkoutSessionRepository();
    WorkoutService service = new WorkoutService(fake);

    Long id = service.record(member, LocalDate.now(), "가슴 운동");

    assertThat(id).isNotNull();
}

스프링 컨테이너를 띄우지 않고 new로 만들어 테스트할 수 있습니다.

가짜 구현을 넣기도 쉽습니다.

순환 참조를 시작 시점에 발견할 수 있습니다

이 부분은 직접 확인해 보는 편이 빠릅니다.


5. 순환 참조를 직접 만들어보기

서로를 필요로 하는 두 클래스를 만들어 보겠습니다.

@Service
public class CircularA {
    private final CircularB b;
    public CircularA(CircularB b) { this.b = b; }
}

@Service
public class CircularB {
    private final CircularA a;
    public CircularB(CircularA a) { this.a = a; }
}

CircularA를 만들려면 CircularB가 필요하고, CircularB를 만들려면 CircularA가 필요합니다.

어느 쪽도 먼저 만들 수 없습니다.

실행하면 애플리케이션이 아예 시작되지 않습니다.

APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  circularA defined in file [.../demo/CircularA.class]
↑     ↓
|  circularB defined in file [.../demo/CircularB.class]
└─────┘

어느 빈들이 고리를 이루고 있는지까지 그림으로 알려줍니다.

필드 주입으로 바꾸면 어떻게 될까?

같은 두 클래스를 필드 주입으로 바꿔 보겠습니다.

@Service
public class CircularA {
    @Autowired
    private CircularB b;
}

@Service
public class CircularB {
    @Autowired
    private CircularA a;
}

이번에도 시작에 실패합니다.

하지만 메시지가 조금 다릅니다.

Action:
Relying upon circular references is discouraged and they are prohibited by default.
Update your application to remove the dependency cycle between beans.
As a last resort, it may be possible to break the cycle automatically by
setting spring.main.allow-circular-references to true.

설정 하나로 통과시킬 수 있다고 안내합니다.

실제로 application.yml에 다음과 같이 설정하면 애플리케이션이 정상적으로 시작됩니다.

spring:
  main:
    allow-circular-references: true

그런데 생성자 주입에 같은 설정을 적용하면 여전히 시작에 실패합니다.

정리하면 다음과 같습니다.

주입 방식 기본 설정 allow-circular-references: true
생성자 주입시작 실패여전히 시작 실패
필드 / 세터 주입시작 실패정상 시작 (문제가 감춰짐)

생성자 주입은 순환 참조를 우회할 방법 자체가 없습니다.

설계가 잘못되었다는 사실을 반드시 마주하게 됩니다.

반면 필드 주입은 설정 한 줄로 넘어갈 수 있고, 그 순간 문제는 코드 안에 남습니다.

오래된 자료를 볼 때 주의할 점

예전 글에서는 필드 주입을 쓰면 순환 참조가 그냥 통과된다고 설명하는 경우가 있습니다.

Spring Boot 2.6부터 순환 참조는 기본적으로 금지되었습니다.

따라서 지금은 어떤 주입 방식이든 기본 설정에서는 시작에 실패합니다.

차이는 우회할 수 있는지 여부에 있습니다.


6. @Autowired는 언제 생략할 수 있나?

앞의 예제에서 생성자에 @Autowired가 붙어 있지 않았습니다.

생성자가 하나뿐이면 생략할 수 있습니다.

Spring 4.3부터 적용된 규칙입니다.

생성자가 둘 이상이면 어느 것을 사용할지 알 수 없으므로 명시해야 합니다.

@Service
public class WorkoutService {

    private final WorkoutSessionRepository repository;

    @Autowired   // 생성자가 여러 개면 필요하다
    public WorkoutService(WorkoutSessionRepository repository) {
        this.repository = repository;
    }

    public WorkoutService() {
        this.repository = null;
    }
}

실무에서는 생성자를 하나만 두는 경우가 대부분이므로 대체로 생략합니다.


7. 같은 타입의 빈이 둘 이상일 때

주입할 타입의 빈이 여러 개면 컨테이너는 무엇을 넣을지 결정할 수 없습니다.

public interface NotificationSender { }

@Component
public class EmailSender implements NotificationSender { }

@Component
public class PushSender implements NotificationSender { }

이 상태에서 NotificationSender를 주입받으면 시작에 실패합니다.

해결 방법은 두 가지입니다.

@Primary

기본으로 사용할 빈을 지정합니다.

@Primary
@Component
public class EmailSender implements NotificationSender { }

@Qualifier

주입받는 쪽에서 이름으로 지정합니다.

public AlarmService(@Qualifier("pushSender") NotificationSender sender) {
    this.sender = sender;
}

대부분의 경우에 쓰는 구현이 정해져 있다면 @Primary가 간결합니다.

호출하는 쪽마다 다른 구현이 필요하다면 @Qualifier를 사용합니다.


마무리

이번에는 스프링의 세 가지 의존성 주입 방법을 비교하고 생성자 주입을 권장하는 이유에 대해 알아봤습니다.

생성자 주입은 final로 불변을 보장하고, 의존을 빠뜨리면 컴파일 단계에서 걸리며, 스프링 없이도 객체를 만들 수 있어 테스트가 가벼워집니다.

무엇보다 순환 참조를 우회할 수 없기 때문에 설계 문제를 반드시 드러냅니다.

순환 참조를 직접 만들어 확인해 보니, 필드 주입은 설정 한 줄로 통과시킬 수 있었지만 생성자 주입은 어떤 설정으로도 통과되지 않았습니다.

또한 Spring Boot 2.6부터는 순환 참조가 기본적으로 금지되므로, 오래된 자료의 설명과 지금의 동작이 다르다는 점도 함께 확인했습니다.

다음 글에서는 AOP에 대해 살펴보겠습니다.

앞선 글에서 확인했던 프록시가 실제로 어떤 일을 하는지, 그리고 횡단 관심사를 어떻게 분리하는지 정리하겠습니다.

반응형
'Web Backend/Spring' 카테고리의 다른 글
  • [Spring] @Transactional이 동작하지 않는 이유 - 프록시 자기 호출
  • [Spring] AOP - 관점 지향 프로그래밍과 프록시
  • [Spring] 빈 등록과 생명주기 - @Component, @Bean, @Configuration
  • [Spring] IoC와 DI - 제어의 역전이란 무엇인가
seunghwaan
seunghwaan
공부한 내용을 정리하는 개발 기록 블로그
    반응형
  • seunghwaan
    SH's Devlog
    seunghwaan
  • 전체
    오늘
    어제
    • 분류 전체보기 (164)
      • Android (62)
        • Basic (17)
        • Kotlin(Java) (14)
        • UI & Animation (1)
        • Compose (2)
        • Coroutines (1)
        • Dependency Injection (6)
        • RxJava (8)
        • BLE (3)
        • TDD (2)
        • JetPack (1)
        • NextStep (4)
        • Error Log (3)
      • Flutter (14)
        • Basic (5)
        • Dart (1)
        • State Management (2)
        • Widgets (4)
        • Error and Tips (2)
      • iOS (8)
        • Basic (0)
        • Swift (8)
      • Web Frontend (16)
        • Basic (0)
        • JavaScript (5)
        • TypeScript (0)
        • React (11)
      • Web Backend (5)
        • Spring (5)
      • CS(Computer Science) (18)
        • Network (4)
        • Database (10)
        • Design Pattern (1)
        • Computer Architecture (3)
        • Operating System (0)
      • Cloud (6)
        • AWS (6)
      • DevOps (25)
        • GIT (4)
        • CI CD (8)
        • Linux (4)
        • Docker (9)
        • Error Log (0)
      • 코딩테스트 (10)
        • DB (6)
        • 알고리즘 (4)
      • Mac Tip (0)
      • Language (0)
        • English (0)
        • Japanese (0)
      • Temporary (0)
  • 블로그 메뉴

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

  • 공지사항

  • 인기 글

  • 태그

    컴퓨터공학
    spring
    JavaScript
    Algorithm
    Dependency Injection
    FLUTTER
    Android
    Web Frontend
    Dagger
    error
    gradle
    di
    IOS
    Network
    Kotlin
    Computer Science
    Web Backend
    MySQL
    BLE
    Linux
    docker
    Jenkins
    AWS
    Swift
    cs
    RxJava
    CICD
    시작하세요! 도커
    React
    database
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
seunghwaan
[Spring] 의존성 주입 - 생성자 주입을 권장하는 이유
상단으로

티스토리툴바