개발 학습 실습실

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

기초개념 › Spring

빈

빈은 무엇이고, 그냥 만든 객체와 무엇이 다른가

빈은 스프링이 만들어서 보관하는 객체입니다. 코드로는 평범한 클래스이고, 클래스 위에 @Component를 적어 두면 스프링이 시작할 때 app 패키지를 훑어 그 클래스를 찾아 객체를 하나 만들고, 컨테이너라는 보관하는 곳에 넣어 둡니다. 우리는 new를 쓰지 않고 필요한 곳에서 context.getBean(Greeter.class)로 스프링에게 객체를 달라고 하며, 몇 번을 달라고 해도 스프링은 보관하던 같은 객체 하나를 줍니다. 이 실습은 @Component를 지우거나 getBean 대신 new Greeter()로 만들어 가며 같은 코드를 실행해, 스프링이 만든 객체와 우리 코드가 new로 만든 객체가 각각 몇 개인지, a == b가 true인지 false인지를 봅니다.

실행으로 대조 · 스프링 프레임워크 7.0.9에서 네 가지 조합을 실제 컴포넌트 스캔으로 실행하고 객체 수를 재어 대조

이 실습에 나오는 개념

스프링 빈(Bean)
스프링 IoC 컨테이너가 생성하고 관리하는 객체다. 개발자가 new로 직접 만든 객체와 달리 컨테이너가 의존성을 주입하고 초기화와 소멸을 관리하며, 프록시를 통해 트랜잭션이나 AOP 같은 부가 기능을 적용할 수 있다.
IoC 컨테이너(ApplicationContext)
빈의 생성, 의존성 연결, 생명주기 관리를 담당하는 스프링의 핵심 구성 요소다. BeanFactory가 가장 기본적인 컨테이너 인터페이스이고, ApplicationContext는 이를 확장해 이벤트 발행, 메시지 국제화, 환경 설정 등의 기능을 더한 것이다. 보통 컨테이너라고 하면 ApplicationContext를 가리킨다.
@Component
@Component는 클래스 위에 붙여, 스프링이 그 클래스를 알고 객체를 만들어 관리하는 대상(빈)으로 삼게 하는 애너테이션이다. 스프링은 컴포넌트 스캔으로 지정한 패키지 아래의 클래스를 훑어 @Component가 붙은 클래스를 찾고, 그 클래스의 객체를 만들어 컨테이너에 보관한다. 빈의 이름을 따로 정하지 않으면 클래스 이름의 첫 글자를 소문자로 바꾼 것이 쓰인다(Greeter는 greeter). @Repository, @Service, @Controller는 @Component를 특수화한 것이어서 같은 방식으로 찾아진다. 이 애너테이션이 없는 클래스는 같은 패키지에 있어도 찾아지지 않으므로, 스프링에게 그 클래스의 객체를 달라고 하면 그런 빈이 없다는 예외가 난다.
컴포넌트 스캔
지정한 패키지 아래에서 @Component 계열 애너테이션(@Service, @Repository, @Controller 등)이 붙은 클래스를 찾아 자동으로 빈으로 등록하는 기능이다. Spring Boot에서는 @SpringBootApplication이 붙은 클래스의 패키지와 그 하위 패키지가 기본 대상이다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. main 메서드가 실행되고, new AnnotationConfigApplicationContext("app") 줄이 스프링을 시작합니다. 시작할 때 컨테이너가 만들어집니다. 컨테이너는 스프링이 만든 객체를 보관하는 곳이고, 지금은 비어 있습니다.
  2. 스프링이 app 패키지의 클래스를 훑어 @Component가 붙은 클래스를 찾습니다. Greeter 클래스에 붙어 있습니다.
  3. 스프링이 Greeter 클래스의 객체를 하나 만들어 컨테이너에 보관합니다. 이렇게 스프링이 만들어 보관하는 객체가 빈입니다. 이 객체는 우리 코드가 달라고 하기 전에, 스프링을 시작할 때 이미 만들어집니다. 우리가 쓴 코드에는 new Greeter()라는 줄이 없습니다.
  4. Greeter a = context.getBean(Greeter.class); 줄이 실행됩니다. 스프링에게 Greeter 객체를 달라고 하는 줄입니다. 스프링은 새로 만들지 않고, 컨테이너에 보관하던 그 객체를 줍니다. 변수 a가 그 객체를 가리킵니다. 스프링이 만든 객체는 그대로 1개입니다.
  5. Greeter b = context.getBean(Greeter.class); 줄이 실행됩니다. 한 번 더 달라고 해도 스프링은 새로 만들지 않고 같은 객체를 줍니다. 스프링은 빈을 하나만 만들어 두고, 달라고 할 때마다 그 하나를 주기 때문입니다. 변수 b도 a와 같은 객체를 가리킵니다.
  6. System.out.println(a.hello()); 줄이 실행됩니다. a가 가리키는 객체의 hello() 메서드가 "안녕하세요"를 돌려주고, 이 글이 출력됩니다.
  7. System.out.println(a == b); 줄이 실행됩니다. a == b는 두 변수가 같은 객체를 가리키는지 비교하는 식입니다. a와 b는 컨테이너에 보관된 같은 객체를 가리키므로 true가 출력됩니다.

실행 결과

코드

// 표시: 스프링이 이 클래스의 객체를 만들어 보관한다
@Component
public class Greeter {
    public String hello() {
        return "안녕하세요";
    }
}

public class Main {
    public static void main(String[] args) {
        // 스프링을 시작한다. app 패키지를 훑어 @Component를 찾는다
        ApplicationContext context =
                new AnnotationConfigApplicationContext("app");
        // 스프링이 보관하고 있는 Greeter 객체를 달라고 한다
        Greeter a = context.getBean(Greeter.class);
        Greeter b = context.getBean(Greeter.class);
        System.out.println(a.hello());
        System.out.println(a == b);
    }
}

이 모형의 전제

스프링 프레임워크 7.0.9를 JDK 21.0.12.1에서 실행한 결과입니다. 스프링 부트는 쓰지 않았고, 화면의 코드대로 AnnotationConfigApplicationContext("app")만 썼습니다. 네 가지 조합을 모두, 화면의 코드와 같은 구조의 소스(패키지 app의 Greeter와 Main)를 컴파일해 자바 프로세스를 새로 띄워 그대로 실행했습니다. Greeter를 손으로 등록하지 않았고, 스프링이 app 패키지를 훑어 @Component를 찾게 했습니다. 대조한 것은 프로그램이 출력한 글(안녕하세요와 a == b의 값), 예외로 멈췄을 때 그 예외의 종류, 스프링이 만든 Greeter 객체의 수, 우리 코드가 new로 만든 객체의 수입니다. 객체 수는 짐작이 아니라 실측했습니다. 대조용 소스에만 Greeter의 생성자에 객체가 만들어질 때마다 숫자를 올리는 줄을 더했고, 스프링을 시작한 직후의 수를 스프링이 만든 수로, 끝난 뒤의 수에서 그 수를 뺀 것을 우리 코드가 new로 만든 수로 적었습니다. 시작한 직후의 수는 getBean을 부르기 전에 이미 1개(@Component가 있을 때)였으므로, 스프링이 객체를 만드는 때는 getBean을 부를 때가 아니라 스프링을 시작할 때입니다. getBean을 두 번 불러도 수는 늘지 않았고 a == b는 true였으며, new를 쓴 조합에서는 컨테이너에 들어 있는 Greeter 빈이 끝난 뒤에도 시작 직후와 같은 수여서 new로 만든 객체는 컨테이너에 들어가지 않았습니다. @Component가 없을 때는 시작한 직후의 수가 0이었고 컨테이너에 등록된 이름도 없었으며(스프링 내부의 것은 뺐습니다), getBean 줄에서 org.springframework.beans.factory.NoSuchBeanDefinitionException(메시지는 app.Greeter 타입의 빈이 없다는 내용)이 나와 프로세스가 오류 코드로 끝났고 출력은 하나도 없었습니다. @Component가 있을 때 컨테이너에 등록된 이름은 greeter 하나였고 그 빈은 싱글톤이었습니다. 문서가 정한 것은 스프링 프레임워크 문서 Core의 다음 절입니다. Introduction to the Spring IoC Container and Beans(빈은 스프링 IoC 컨테이너가 만들고 조립하고 관리하는 객체이고, 그렇지 않은 객체는 그냥 애플리케이션의 여러 객체 가운데 하나라는 것), Classpath Scanning and Managed Components(@Component가 스프링이 관리하는 컴포넌트를 뜻하는 일반적인 애너테이션이고 @Service, @Controller 등이 그것을 특수화한 것이라는 것, 스프링이 애너테이션이 붙은 클래스를 찾아 빈 정의를 등록한다는 것, 이름을 정하지 않으면 클래스 이름의 첫 글자를 소문자로 바꾼 이름이 쓰인다는 것), Bean Scopes(싱글톤은 기본 범위이고 컨테이너가 그 빈 정의로 객체를 정확히 하나만 만들어 두고 요청이 올 때마다 그 객체를 돌려준다는 것), 그리고 AnnotationConfigApplicationContext(String... basePackages)의 API 문서(주어진 패키지에서 컴포넌트를 찾아 빈 정의를 등록하고 컨텍스트를 자동으로 새로 고친다는 것)입니다. 문서가 정한 것과 달리, 객체가 시작할 때 만들어지고 @Component가 없는 클래스는 getBean을 불러도 만들어지지 않는다는 점과 예외의 이름은 이 버전에서 관찰한 동작입니다. 화면의 코드는 package와 import 줄을 줄였고, Greeter와 Main이 둘 다 app 패키지에 있다고 가정했습니다. 컨테이너를 닫는 줄은 쓰지 않았습니다. 빈을 등록하는 방법은 @Component 말고도 @Bean 메서드 등이 있고, @Component 계열인 @Service나 @Controller, 빈의 범위가 싱글톤이 아닌 경우, 빈끼리 서로 연결하는 의존성 주입은 다루지 않았습니다. 이것들은 "세부 동작 실습: 스프링 컨테이너와 빈"에서 다룹니다.

앞 실습: 라이브러리와 의존성 · 다음 실습: 의존성 주입

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요