[Spring] 빈 등록과 생명주기 - @Component, @Bean, @Configuration

2021. 5. 7. 21:01·Web Backend/Spring
반응형

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

스프링 빈 등록과 생명주기

이번에는 스프링 컨테이너에 빈(Bean)을 등록하는 방법과 등록된 빈이 거치는 생명주기에 대해 알아보겠습니다.

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

그렇다면 컨테이너는 어떤 객체를 관리할지 어떻게 알 수 있을까요.

등록 방법은 크게 두 가지입니다.

하나는 클래스에 애노테이션을 붙여 자동으로 등록하는 방법이고, 다른 하나는 설정 클래스에서 직접 반환하는 방법입니다.

이번에는 두 방법의 차이와 각각을 언제 사용해야 하는지 정리하겠습니다.

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


1. 스프링 빈이란?

스프링 빈(Spring Bean)은 스프링 컨테이너가 생성하고 관리하는 객체를 말합니다.

일반적인 자바 객체와 클래스 자체는 다르지 않습니다.

차이는 그 객체를 누가 만들고 누가 수명을 관리하느냐에 있습니다.

컨테이너가 관리하는 객체가 되면 다른 빈에 주입될 수 있고, 트랜잭션이나 AOP 같은 기능도 적용받을 수 있습니다.


2. @Component로 등록하기

가장 많이 쓰는 방법입니다.

클래스에 애노테이션을 붙여두면 컨테이너가 알아서 찾아 등록합니다.

@Service
public class WorkoutService {

    private final WorkoutSessionRepository repository;

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

컴포넌트 스캔

이렇게 붙여둔 애노테이션을 찾아내는 과정을 컴포넌트 스캔(Component Scan)이라고 합니다.

스프링 부트에서는 @SpringBootApplication이 @ComponentScan을 포함하고 있습니다.

따라서 별도 설정 없이도 애플리케이션 클래스가 위치한 패키지와 그 하위 패키지가 자동으로 스캔됩니다.

com.example.workoutlog
├── WorkoutLogApplication.java   ← 여기를 기준으로
├── domain                        ← 스캔된다
├── repository                    ← 스캔된다
└── service                       ← 스캔된다

애플리케이션 클래스를 패키지 최상단에 두는 관례는 이 때문입니다.

만약 상위 패키지나 다른 패키지에 두면 스캔 범위에서 빠져 빈이 등록되지 않습니다.

@Service와 @Repository는 무엇이 다른가?

@Service, @Repository, @Controller는 모두 내부에 @Component를 가지고 있습니다.

즉 등록되는 방식 자체는 동일합니다.

그런데도 이름을 나눠 둔 데에는 이유가 있습니다.

첫째, 클래스의 역할을 이름만으로 드러낼 수 있습니다.

둘째, AOP에서 특정 계층만 대상으로 지정할 때 구분자로 쓸 수 있습니다.

셋째, @Repository는 실제 기능이 하나 더 있습니다.

JPA나 JDBC가 던지는 데이터 접근 예외를 스프링의 DataAccessException으로 변환해 줍니다.

따라서 저장소 구현체에는 @Component 대신 @Repository를 붙이는 편이 좋습니다.


3. @Bean으로 등록하기

컴포넌트 스캔은 내가 작성한 클래스에만 쓸 수 있습니다.

애노테이션을 붙이려면 소스 코드를 수정할 수 있어야 하기 때문입니다.

외부 라이브러리의 클래스는 수정할 수 없으므로 다른 방법이 필요합니다.

이때 사용하는 것이 @Bean입니다.

@Configuration
public class AppConfig {

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        return mapper;
    }
}

메서드가 반환하는 객체가 그대로 빈이 됩니다.

그리고 빈 이름은 메서드 이름이 됩니다.

위 예제에서는 objectMapper라는 이름으로 등록됩니다.

@Bean(name = "customMapper")처럼 이름을 직접 지정할 수도 있습니다.


4. @Configuration은 무슨 일을 하는가?

@Bean 메서드를 담는 클래스에는 보통 @Configuration을 붙입니다.

그런데 @Configuration 역시 내부에 @Component를 가지고 있으므로, @Component만 붙여도 @Bean 메서드는 동작합니다.

그렇다면 둘의 차이는 무엇일까요.

다음과 같이 빈이 서로를 참조하는 상황을 보겠습니다.

@Configuration
public class AppConfig {

    @Bean
    public VolumeCalculator volumeCalculator() {
        return new VolumeCalculator(unitConverter());
    }

    @Bean
    public PaceCalculator paceCalculator() {
        return new PaceCalculator(unitConverter());
    }

    @Bean
    public UnitConverter unitConverter() {
        System.out.println("[@Bean] unitConverter() 호출됨");
        return new UnitConverter();
    }
}

unitConverter()가 두 번 호출되고 있습니다.

일반적인 자바 코드라면 UnitConverter 객체가 두 개 생성됩니다.

하지만 @Configuration이 붙어 있으면 한 번만 생성되고 같은 객체가 재사용됩니다.

실제로 실행해 보면 출력이 다음과 같이 한 번만 찍힙니다.

[@Bean] unitConverter() 호출됨

어떻게 그것이 가능한가?

@Configuration이 붙은 클래스는 컨테이너에 등록될 때 그대로 등록되지 않습니다.

스프링이 CGLIB으로 상속받은 프록시 클래스를 만들어 대신 등록합니다.

이 프록시는 @Bean 메서드 호출을 가로챕니다.

그리고 해당 이름의 빈이 컨테이너에 이미 있으면 새로 만들지 않고 기존 것을 돌려줍니다.

직접 확인해 보겠습니다.

@Bean
ApplicationRunner configInspector(AppConfig appConfig,
                                  VolumeCalculator volume,
                                  PaceCalculator pace) {
    return args -> {
        System.out.println("AppConfig 클래스 : " + appConfig.getClass().getName());
        System.out.println("같은 객체인가   : " + (volume.converter() == pace.converter()));
    };
}

실행하면 다음과 같이 출력됩니다.

AppConfig 클래스 : com.example.workoutlog.config.AppConfig$$SpringCGLIB$$0
같은 객체인가   : true

클래스 이름 뒤에 $$SpringCGLIB$$0이 붙어 있습니다.

그리고 두 빈이 주입받은 UnitConverter가 같은 객체임을 확인할 수 있습니다.

앞선 글에서 빈 목록을 출력했을 때 보였던 $$SpringCGLIB$$가 바로 이것입니다.

@Component만 붙였다면 이 프록시가 만들어지지 않습니다.

그러면 workoutSessionRepository()는 호출할 때마다 새 객체를 반환하고, 싱글톤이 깨집니다.

따라서 @Bean 메서드를 담는 클래스에는 @Configuration을 붙이는 것이 안전합니다.


5. @Component와 @Bean 중 무엇을 쓸까?

상황 등록 방법
내가 작성한 클래스@Component 계열 (@Service, @Repository 등)
외부 라이브러리 클래스@Bean
생성 과정에 설정이 필요한 객체@Bean
조건에 따라 다른 구현을 등록@Bean

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

수정할 수 있는 내 코드라면 @Component를 붙이는 편이 간결합니다.

수정할 수 없거나 생성 과정을 직접 제어해야 한다면 @Bean을 사용합니다.


6. 빈 스코프

스코프(Scope)는 빈이 존재하는 범위를 뜻합니다.

기본값은 싱글톤(Singleton)입니다.

컨테이너당 하나만 생성해서 모든 요청이 공유합니다.

@Service
@Scope("singleton")   // 생략해도 동일하다
public class WorkoutService { }

싱글톤이 기본이므로 빈에는 상태를 두면 안 됩니다.

여러 요청이 같은 객체를 동시에 사용하기 때문입니다.

다음과 같은 코드는 문제가 됩니다.

@Service
public class WorkoutService {

    private int lastSessionId;   // 모든 요청이 공유한다

    public void record(...) {
        this.lastSessionId = ...;
    }
}

값을 유지해야 한다면 필드가 아니라 메서드의 지역 변수나 파라미터로 다루어야 합니다.

싱글톤 외에 prototype, request, session 스코프도 있지만 실제로 쓰는 경우는 드뭅니다.


7. 빈 생명주기

빈은 다음과 같은 순서로 관리됩니다.

객체 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 소멸 전 콜백 → 종료

생성자 주입을 사용하면 객체 생성과 의존관계 주입이 동시에 이루어집니다.

따라서 생성자가 끝난 시점에 이미 필요한 빈은 모두 들어와 있습니다.

그런데도 초기화 콜백이 따로 필요한 경우가 있습니다.

주입받은 객체를 이용해 무언가를 준비해야 할 때입니다.

@Service
public class ExerciseCatalog {

    private final ExerciseRepository repository;
    private Map<String, Exercise> cache;

    public ExerciseCatalog(ExerciseRepository repository) {
        this.repository = repository;
    }

    @PostConstruct
    public void init() {
        this.cache = repository.findAll().stream()
                .collect(toMap(Exercise::getName, it -> it));
    }

    @PreDestroy
    public void close() {
        cache.clear();
    }
}

@PostConstruct는 의존관계 주입이 끝난 뒤에 한 번 호출됩니다.

@PreDestroy는 컨테이너가 종료되기 직전에 호출됩니다.

생성자에서 이런 작업을 하지 않는 이유는 책임이 다르기 때문입니다.

생성자는 객체를 사용할 수 있는 상태로 만드는 데까지만 관여하는 것이 좋습니다.

데이터베이스 조회처럼 시간이 걸리고 실패할 수 있는 작업은 초기화 콜백으로 분리하면 문제가 생겼을 때 원인을 찾기 쉽습니다.


마무리

이번에는 스프링 빈을 등록하는 방법과 생명주기에 대해 알아봤습니다.

등록 방법은 두 가지이며, 내가 작성한 클래스에는 @Component 계열을, 외부 라이브러리나 생성 과정을 제어해야 하는 객체에는 @Bean을 사용합니다.

@Configuration은 CGLIB 프록시를 만들어 @Bean 메서드가 여러 번 호출되어도 같은 객체를 반환하도록 보장합니다.

앞선 글에서 확인했던 $$SpringCGLIB$$의 정체가 이것이었습니다.

또한 빈의 기본 스코프가 싱글톤이므로 빈에 상태를 두면 안 된다는 점과, 의존관계 주입 이후의 준비 작업은 @PostConstruct로 분리한다는 점도 함께 살펴봤습니다.

다음 글에서는 의존성을 주입하는 세 가지 방법을 비교하겠습니다.

생성자 주입, 세터 주입, 필드 주입 중에서 스프링이 생성자 주입을 권장하는 이유를 정리하겠습니다.

반응형
'Web Backend/Spring' 카테고리의 다른 글
  • [Spring] @Transactional이 동작하지 않는 이유 - 프록시 자기 호출
  • [Spring] AOP - 관점 지향 프로그래밍과 프록시
  • [Spring] 의존성 주입 - 생성자 주입을 권장하는 이유
  • [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)
  • 블로그 메뉴

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

  • 공지사항

  • 인기 글

  • 태그

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

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
seunghwaan
[Spring] 빈 등록과 생명주기 - @Component, @Bean, @Configuration
상단으로

티스토리툴바