본문으로 바로가기
TaeyoungKim.dev

Spring @Autowired가 null인 이유: new 객체와 스프링 빈의 차이

자바/스프링작성 약 4분 읽기TaeyoungKim
LinkedInX

@Autowired를 붙였는데 service가 null이다. 분명 같은 클래스를 쓰는데, 애플리케이션에서는 되고 직접 만든 테스트 객체에서는 안 된다. 애노테이션이 변덕을 부린 건 아니다. 객체를 누가 만들었는지부터 보면 답이 나온다.

@Autowired 필드가 null인 객체는 누가 만들었을까?

아래 MessagePrinter는 MessageService를 필요로 한다. @Component와 @Service가 스캔 범위에 들어 있다는 전제다.

java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

@Service
class MessageService {
    String text() { return "ready"; }
}

@Component
class MessagePrinter {
    @Autowired
    MessageService service;

    String print() { return service.text(); }
}

new MessagePrinter()는 Java가 객체 하나를 직접 만든다. 그 객체의 service에는 아무 값도 넣지 않았으므로 null이고, print()를 부르면 NullPointerException이 난다. 반면 스프링 컨테이너가 만든 MessagePrinter 빈은 등록된 MessageService를 필드에 연결한다. Spring의 @Autowired 처리 설명에서 필드 주입이 빈 생성 뒤 이뤄진다는 경계를 확인할 수 있다.

그림의 왼쪽은 직접 생성한 객체, 오른쪽은 컨테이너가 만든 빈이다. 같은 클래스여도 인스턴스와 생성 경로가 다르다. 그림은 실행 화면이 아니라 이 차이를 설명하는 도식이다.

new로 만들면 의존성 주입이 전부 안 될까?

그렇지는 않다. new MessagePrinter(service)처럼 필요한 객체를 생성자 인자로 직접 전달하는 것도 의존성 주입이다. 다만 그 인자를 넣은 주체는 개발자 코드이지 스프링 컨테이너가 아니다. 직접 만든 인스턴스가 자동으로 스프링 빈이 되지도 않는다.

필수 의존성을 필드에 숨겨 두기보다 생성자에 드러내면 실수를 빨리 발견할 수 있다. 앞의 MessagePrinter를 다음처럼 바꿔 보자.

java
@Component
class MessagePrinter {
    private final MessageService service;

    MessagePrinter(MessageService service) {
        this.service = java.util.Objects.requireNonNull(service, "service");
    }

    String print() { return service.text(); }
}

스프링이 관리하는 빈이고 생성자가 하나라면 @Autowired를 따로 적지 않아도 스프링이 필요한 빈을 생성자에 전달한다. 직접 테스트할 때도 new MessagePrinter(new MessageService())처럼 인자를 명시하면 된다. 반대로 null을 전달하면 print()까지 기다리지 않고 생성 시점에 service 오류가 난다.

Java 객체만 놓고 보면, 값을 넣지 않은 필드는 null이고 그 필드를 사용한 호출은 NullPointerException이다. 생성자에 MessageService를 전달한 객체는 ready를 반환한다. 두 생성 경로를 비교할 때는 주입이 됐는지와 스프링이 관리하는 객체인지를 별개로 보자.

Spring 빈을 쓰려는데 계속 null이라면 무엇을 확인할까?

먼저 new MessagePrinter()를 호출한 곳을 찾는다. 테스트·유틸리티·직접 만든 설정 코드에서 나온 인스턴스라면 주입받은 빈과 다른 객체다. 이미 컨테이너에서 받은 객체라면 다음을 보자.

  1. MessagePrinter가 실제로 빈으로 등록됐는가? @Component를 붙여도 컴포넌트 스캔 범위 밖이면 탐색되지 않는다.
  2. 필요한 MessageService도 빈인가? 클래스만 만들어 놓거나 @Service가 스캔되지 않으면 주입할 대상이 없다.
  3. 보고 있는 객체가 컨테이너에서 꺼낸 바로 그 인스턴스인가? 같은 클래스의 다른 new 객체를 로그나 테스트에서 섞지 않았는가?

@Component, @Service 등은 스캔 대상으로 선택될 때 등록된다. 애노테이션 하나가 모든 패키지의 객체를 자동으로 찾아 주지는 않는다. Spring의 컴포넌트 스캔 설명

예외처럼 보이는 @Bean 메서드도 있다. 설정 메서드 안에서 new MessageService()를 반환할 수 있지만, 그 반환 객체를 컨테이너가 빈으로 등록하기 때문에 의미가 다르다. 애플리케이션 곳곳의 임의 new까지 빈으로 만드는 규칙은 아니다.

단위 테스트에서는 생성자에 대역 객체를 직접 전달해 로직을 확인하고, 스프링 연결 자체는 컨텍스트를 띄운 테스트에서 확인하자. 전자가 통과해도 빈 등록·스캔 설정까지 검증된 것은 아니다.

핵심 요약

@Autowired 필드가 null이라면 애노테이션을 더 붙이기 전에 그 인스턴스를 누가 만들었는지 확인하자. 직접 new한 객체는 일반적인 생성 경로에서 스프링의 필드 주입을 거치지 않는다. 생성자 주입은 필요한 객체를 드러내고 누락을 일찍 잡는다. 스프링 빈을 기대했다면 객체 생성 경로, 컴포넌트 스캔 범위, 의존 대상의 빈 등록을 순서대로 살펴보면 된다.

작성자

TaeyoungKim

기초 개념을 구현과 검증, 실제 운영 판단까지 연결해 기록합니다.

#Spring#@Autowired#스프링 빈#의존성 주입#생성자 주입

함께 읽으면 좋은 글