개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

기초개념 › Spring

의존성 주입

의존성 주입은 무엇을 대신해 주는가

의존성 주입은 클래스가 일을 하려고 필요로 하는 객체를 스스로 만들지 않고 밖에서 넣어 받는 것입니다. 여기서 OrderService는 결제를 하려고 PayService 객체가 필요하고, 이것을 "OrderService가 PayService에 의존한다"고 합니다. 스프링에서는 OrderService의 생성자 매개변수에 PayService를 적어 두는 것이 전부이고, 클래스 위의 @Component는 스프링이 그 클래스의 객체(빈)를 만들어 보관하라는 표시입니다. 스프링이 OrderService 객체를 만들 때 보관하던 PayService 빈을 그 생성자에 넣어 줍니다. 이 실습은 PayService를 생성자로 받는 대신 new PayService()로 직접 만들거나 아예 받지도 만들지도 않고, PayService 위의 @Component를 지워 가며 같은 main을 실행해, OrderService가 쓰는 PayService가 어디서 왔는지와 PayService 객체가 몇 개 만들어지는지를 봅니다.

실행으로 대조 · 스프링 프레임워크 7.0.9에서 여섯 가지 조합을 실제 컴포넌트 스캔으로 컴파일해 실행하고, 필드의 객체까지 리플렉션으로 재어 대조

이 실습에 나오는 개념

DI(의존성 주입)
객체가 사용하는 다른 객체(의존성)를 스스로 만들지 않고 외부에서 전달받는 방식이다. 스프링에서는 컨테이너가 빈을 생성하면서 생성자, 세터, 필드를 통해 의존성을 넣어 준다. 구현 클래스를 바꾸거나 테스트에서 가짜 객체를 넣기 쉬워지고 클래스 사이의 결합도가 낮아진다.
생성자 주입, 세터 주입, 필드 주입
의존성을 넣는 세 가지 방법이다. 생성자 주입은 객체 생성 시 의존성을 받으므로 필드를 final로 둘 수 있고 의존성이 빠진 채 객체가 만들어지는 일이 없어 스프링이 권장한다. 세터 주입은 선택적인 의존성에 쓴다. 필드 주입은 필드에 @Autowired를 붙이는 방식으로 코드는 짧지만 스프링 없이 객체를 만들어 테스트하기 어렵다.
의존성
의존성은 어떤 클래스가 일을 하려고 필요로 하는 다른 클래스의 객체다. 예를 들어 OrderService가 결제를 하려고 PayService 객체의 메서드를 부른다면, OrderService는 PayService에 의존한다고 말하고 PayService 객체가 OrderService의 의존성이다. 스프링 문서는 의존성을 객체가 함께 일하는 다른 객체로 설명한다. 필요한 객체를 new로 직접 만들 수도 있고 생성자의 매개변수로 받을 수도 있는데, 스프링이 만들어 넣어 주는 것을 의존성 주입이라고 한다.
@Component
@Component는 클래스 위에 붙여, 스프링이 그 클래스를 알고 객체를 만들어 관리하는 대상(빈)으로 삼게 하는 애너테이션이다. 스프링은 컴포넌트 스캔으로 지정한 패키지 아래의 클래스를 훑어 @Component가 붙은 클래스를 찾고, 그 클래스의 객체를 만들어 컨테이너에 보관한다. 빈의 이름을 따로 정하지 않으면 클래스 이름의 첫 글자를 소문자로 바꾼 것이 쓰인다(Greeter는 greeter). @Repository, @Service, @Controller는 @Component를 특수화한 것이어서 같은 방식으로 찾아진다. 이 애너테이션이 없는 클래스는 같은 패키지에 있어도 찾아지지 않으므로, 스프링에게 그 클래스의 객체를 달라고 하면 그런 빈이 없다는 예외가 난다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. main 메서드의 첫 줄이 실행됩니다. new AnnotationConfigApplicationContext("app")이 스프링을 시작합니다. 괄호 안의 "app"은 스프링이 클래스를 찾아볼 패키지의 이름이고, 스프링은 자기가 만든 객체를 보관해 둘 곳을 준비합니다. 스프링이 만들어 보관하는 객체를 빈이라고 부릅니다.
  2. 스프링이 app 패키지의 클래스를 훑어 @Component가 붙은 클래스를 찾습니다. PayService와 OrderService에 붙어 있으므로, 이 두 클래스의 객체를 만들어 보관할 대상으로 적어 둡니다.
  3. 스프링이 OrderService 객체를 만들려고 합니다. 생성자 OrderService(PayService payService)를 보니 매개변수로 PayService를 받습니다. 그래서 OrderService가 일을 하려면 PayService 객체가 필요하다는 것을 스프링이 알아냅니다. 이것을 "OrderService가 PayService에 의존한다"고 합니다.
  4. OrderService를 만들려면 PayService 객체가 먼저 있어야 하므로, 스프링이 PayService 객체를 먼저 만들어 보관합니다. 이 객체가 PayService 빈입니다. 만들어진 PayService 객체는 1개입니다.
  5. 스프링이 OrderService 객체를 만듭니다. 보관하고 있던 PayService 빈을 OrderService 생성자의 payService 매개변수에 넣어 주고, 생성자의 this.payService = payService; 줄이 필드가 그 객체를 가리키게 합니다. 우리가 쓴 코드에는 new OrderService(...)도 new PayService()도 없습니다. 이렇게 필요한 객체를 스프링이 넣어 주는 것이 의존성 주입입니다.
  6. 컨텍스트를 만드는 줄이 오류 없이 끝나고, main의 다음 줄 context.getBean(OrderService.class)가 실행됩니다. 스프링이 보관하던 OrderService 빈을 main에 줍니다. 객체는 앞 줄에서 이미 만들어졌고, 이 줄에서 새로 만들어진 객체는 없습니다.
  7. orderService.order() 메서드가 실행됩니다. order()는 payService.pay()를 부르는데, 이 payService는 스프링이 넣어 준 PayService 빈입니다. pay() 메서드가 "결제 완료"를 돌려줍니다.
  8. System.out.println이 돌려받은 글 "결제 완료"를 출력합니다.

실행 결과

코드

@Component
public class PayService {
    public String pay() {
        return "결제 완료";
    }
}

@Component
public class OrderService {
    private final PayService payService;

    // 주입: 필요한 객체를 생성자의 매개변수로 적어 두면 스프링이 넣어 준다
    public OrderService(PayService payService) {
        this.payService = payService;
    }

    public String order() {
        return payService.pay();
    }
}

public class Main {
    public static void main(String[] args) {
        ApplicationContext context =
                new AnnotationConfigApplicationContext("app");
        OrderService orderService = context.getBean(OrderService.class);
        System.out.println(orderService.order());
    }
}

이 모형의 전제

스프링 프레임워크 7.0.9를 JDK 21.0.12.1에서 실행한 결과입니다. 스프링 부트는 쓰지 않고, 화면의 main이 그대로 쓰는 AnnotationConfigApplicationContext("app")의 컴포넌트 스캔만 썼습니다. 여섯 가지 조합을 모두, 화면의 코드와 같은 소스(패키지 app의 PayService, OrderService, Main)를 javac(-parameters)로 컴파일하고 자식 자바 프로세스로 실행해 확인했습니다. 컴포넌트를 손으로 등록하지 않고 스프링이 클래스를 훑어 찾게 했습니다. 대조용 소스에만 Probe 클래스를 더하고 네 줄을 끼웠습니다. 두 클래스에는 객체가 만들어질 때 그 생성자를 부른 쪽(스프링인지 OrderService의 어느 생성자인지)을 스택에서 읽어 적는 인스턴스 초기화 블록 한 줄씩을, main에는 컨텍스트가 만들어진 뒤와 getBean 뒤에 측정하는 줄을 한 줄씩 더했고, 대조 스크립트는 화면의 코드와 견줄 때 이 줄들을 뺍니다. 대조한 것은 실행 결과(출력, 시작 실패한 예외의 종류와 원인, 멈춘 예외와 그 자리), OrderService가 쓰는 PayService, 만들어진 PayService 객체의 수입니다. "OrderService가 쓰는 PayService"는 짐작이 아니라 실측했습니다. 실행 중에 리플렉션으로 OrderService 객체의 payService 필드를 읽어, 컨텍스트에 있는 PayService 빈과 같은 객체인지(==)와 비어 있는지를 견주었습니다. 객체의 수도 생성자가 불린 횟수를 센 것입니다. 단계 문장이 기대는 사실도 따로 재서 확인했습니다. 컴포넌트 스캔이 찾은 클래스, 객체가 만들어진 순서(생성자 주입에서는 PayService가 먼저, 직접 만들 때는 OrderService가 만들어지는 중에 PayService가 만들어짐), 모든 객체가 컨텍스트를 만드는 줄에서 만들어지고 getBean 줄에서는 새로 만들어지지 않는다는 것, 스프링이 기록한 OrderService의 의존 대상(생성자로 받을 때만 payService), PayService 빈이 없을 때 OrderService가 만들어지지 않고 main이 첫 줄에서 멈춘다는 것, 비어 있는 필드를 쓰다 멈추는 자리가 OrderService의 order() 메서드라는 것, 우리 코드에 new OrderService(...)라는 줄이 없다는 것입니다. 시작에 실패한 조합의 실제 예외는 UnsatisfiedDependencyException이고 원인은 NoSuchBeanDefinitionException(PayService 타입의 빈이 없음)이며, 원문은 Probe 출력에 남겼습니다. 문서가 정한 것은 스프링 프레임워크 문서 Core의 Dependency Injection 절(의존성 주입이 객체가 의존하는 다른 객체를 생성자 인자 등으로만 정하고 컨테이너가 빈을 만들 때 그것을 넣어 주는 과정이라는 것, 생성자 기반 주입이 컨테이너가 각각 의존성을 뜻하는 인자를 가진 생성자를 부르는 것이라는 것), Using @Autowired 절(대상 빈의 생성자가 하나뿐이면 그 생성자에 @Autowired를 붙이지 않아도 된다는 것), Classpath Scanning and Managed Components 절과 @Component, AnnotationConfigApplicationContext의 API 문서(@Component가 붙은 클래스가 자동 탐지의 후보이고, 지정한 패키지를 스캔해 빈 정의를 등록한다는 것)입니다. 같은 실행에서 본 다음 동작은 문서가 정한 것이 아니라 이 버전에서 관찰한 것입니다. 클래스 파일을 훑는 순서에서 OrderService가 PayService보다 먼저 나왔고, 생성자 주입에서는 그 때문에 OrderService를 만들다가 PayService를 먼저 만들었다는 것, new로 직접 만들 때 스프링이 OrderService를 먼저 만들고 PayService 빈을 그다음에 만든다는 것(스캔 순서에 달린 것이어서 클래스 이름이 바뀌면 달라질 수 있음), 비어 있는 필드를 스프링이 오류로 알리지 않는다는 것입니다. 화면의 코드는 package와 import 줄을 줄였습니다. 이 실습은 생성자 주입만 다룹니다. 세터 주입, 필드 주입(@Autowired), 같은 타입의 빈이 여러 개일 때, 순환 의존, 빈의 범위와 생명주기, 인터페이스로 받는 설계는 다루지 않았고, "세부 동작 실습: 빈 생성과 주입"에서 다룹니다.

앞 실습: 빈 · 다음 실습: AOP와 프록시

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요