Spring Boot 빈(Bean) 생명주기와 스코프(Scope) 총정리: 싱글톤부터 프로토타입까지

앞선 포스팅에서 스프링의 핵심 엔진인 DI(의존성 주입)IoC(제어의 역전)를 통해 객체 관리를 프레임워크에 위임한다는 사실을 배웠습니다. 그렇다면 스프링 컨테이너에 맡겨진 이 객체(Bean)들은 도대체 언제 태어나서, 언제 죽는 것일까요? 또, 요청할 때마다 매번 새로운 객체가 만들어지는 걸까요, 아니면 하나를 계속 돌려 쓰는 걸까요?

이 질문에 대한 답이 바로 오늘 다룰 빈 생명주기(Bean Lifecycle)빈 스코프(Bean Scope)입니다. 이 두 가지 개념은 메모리 효율성과 데이터의 무결성을 결정짓는 매우 중요한 요소입니다.


Spring Boot 빈(Bean) 생명주기와 스코프(Scope) 총정리: 싱글톤부터 프로토타입까지

1. 서론

스프링 부트로 개발을 하다 보면, 별다른 설정 없이 만든 서비스(Service)나 리포지토리(Repository) 객체들이 애플리케이션이 실행되는 내내 살아있다가 종료될 때 함께 사라지는 것을 경험하게 됩니다. 반면에 어떤 객체는 사용자의 HTTP 요청이 들어올 때마다 새롭게 생성되어야 할 필요도 있습니다. 개발자가 new 키워드로 직접 객체를 생성하던 시절에는 개발자가 모든 시점을 통제했지만, 제어의 역전(IoC) 환경에서는 스프링 컨테이너가 이 막중한 임무를 대신 수행합니다.

하지만 “알아서 해주겠지”라고 방관해서는 안 됩니다. 빈이 언제 생성되고 의존성이 주입되는지, 언제 소멸되는지 정확한 타이밍(Lifecycle)을 모르면, 데이터베이스 연결이 끊기지 않아 리소스가 낭비되거나 초기화되지 않은 객체를 참조하여 NullPointerException을 만나게 될 것입니다. 또한, 상황에 맞는 스코프(Scope)를 설정하지 않으면 전역 변수처럼 데이터가 공유되어 심각한 보안 이슈를 초래할 수도 있습니다. 오늘은 스프링 빈의 탄생부터 죽음까지의 과정인 생명주기와, 빈의 존재 범위를 결정하는 스코프에 대해 완벽하게 정리해 보겠습니다.


2. 본론

1: 스프링 빈의 생명주기 (Bean Lifecycle)

스프링 컨테이너는 매우 체계적인 순서로 빈을 관리합니다. 단순히 객체 생성(new)만 하는 것이 아니라, 의존성을 주입하고 초기화 작업을 수행한 뒤 사용할 수 있는 상태로 만들어줍니다.

[빈 생명주기 전체 흐름]

  1. 스프링 컨테이너 생성: 애플리케이션이 시작되면 컨테이너가 구동됩니다.
  2. 스프링 빈 생성: 컴포넌트 스캔 등을 통해 등록된 클래스들의 객체(인스턴스)를 생성합니다. (생성자 호출 시점)
  3. 의존관계 주입 (DI): 생성된 객체에 필요한 의존성을 주입합니다. (Setter, Field 주입의 경우)
  4. 초기화 콜백: 의존성 주입이 완료되면, 필요한 초기화 로직을 수행합니다. (예: DB 커넥션 연결)
  5. 사용: 실제 애플리케이션 로직에서 빈이 동작합니다.
  6. 소멸 전 콜백: 애플리케이션 종료 직전, 리소스 해제 등의 정리 작업을 수행합니다.
  7. 스프링 종료: 컨테이너가 완전히 종료됩니다.

여기서 개발자가 가장 중요하게 봐야 할 부분은 ‘초기화 콜백‘과 ‘소멸 전 콜백‘입니다.

[실무 필수 어노테이션: @PostConstruct, @PreDestroy]

과거에는 인터페이스를 구현하는 방식을 썼지만, 최신 스프링에서는 자바 표준 어노테이션(JSR-250)을 사용하는 것이 권장됩니다.

  • @PostConstruct: 의존성 주입이 끝난 직후 실행됩니다. 생성자만으로는 할 수 없는(필드에 값이 주입된 이후에야 가능한) 초기화 작업에 필수적입니다.
  • @PreDestroy: 빈이 소멸되기 직전에 실행됩니다. 파일 스트림을 닫거나 DB 연결을 끊는 등 안전한 종료 작업을 위해 사용합니다.

Java

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Component;

@Component
public class DatabaseConnector {

    public DatabaseConnector() {
        System.out.println("1. 생성자 호출");
    }

    @PostConstruct
    public void init() {
        System.out.println("2. 초기화 콜백: DB 연결 설정 및 데이터 로딩 완료");
        // 의존성 주입이 확실하게 완료된 시점
    }

    public void connect() {
        System.out.println("3. 빈 사용: 쿼리 수행");
    }

    @PreDestroy
    public void close() {
        System.out.println("4. 소멸 전 콜백: DB 연결 안전하게 종료");
    }
}

2: 빈 스코프 (Bean Scope) – 빈의 존재 범위

스코프는 번역하면 ‘범위’입니다. 즉, “이 빈이 언제까지 존재하고, 어디서 공유되는가?“를 결정하는 설정입니다. 스프링은 다양한 스코프를 지원합니다.

1. 싱글톤 (Singleton) – 기본값

  • 특징: 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프입니다. 컨테이너 내에 딱 하나의 인스턴스만 생성되어 모든 요청에서 공유됩니다.
  • 장점: 객체를 매번 생성하지 않으므로 메모리 낭비가 없고 성능이 우수합니다.
  • 주의사항: 여러 스레드가 동시에 접근하므로 상태를 유지하는 필드(Stateful)를 가급적 만들지 않아야 합니다. (무상태 설계 필수)

2. 프로토타입 (Prototype)

  • 특징: 스프링 컨테이너는 프로토타입 빈의 생성과 의존성 주입, 초기화까지만 관여하고 더 이상 관리하지 않습니다. 요청할 때마다 항상 새로운 인스턴스를 생성해서 반환합니다.
  • 용도: 인스턴스마다 고유한 상태 값을 가져야 할 때 사용하지만, 실무에서는 싱글톤 빈 내부에서 상태를 관리하지 않는 방식으로 설계하므로 자주 사용되지는 않습니다.
  • 주의사항: 컨테이너가 종료될 때 @PreDestroy 같은 소멸 메서드가 호출되지 않습니다. 클라이언트가 직접 자원 해제를 해야 합니다.

Java

@Scope("prototype")
@Component
public class PrototypeBean { ... }

3. 웹 관련 스코프 (Web Scope)

웹 환경(Spring MVC)에서만 동작하는 특별한 스코프들입니다.

  • request: HTTP 요청 하나가 들어오고 나갈 때까지 유지됩니다. 각 요청마다 별도의 빈 인스턴스가 생성되므로, 사용자별 로그를 남기거나 요청 정보를 추적할 때 유용합니다.
  • session: HTTP Session과 동일한 생명주기를 가집니다. 사용자 로그인 정보나 장바구니처럼 사용자가 브라우저를 닫을 때까지 유지되어야 하는 정보 관리에 쓰입니다.
  • application: 서블릿 컨텍스트(ServletContext)와 같은 범위로 유지됩니다.

3: 주의사항 – 싱글톤 빈과 프로토타입 빈을 함께 사용할 때

면접에서도 자주 나오는 고난도 문제입니다. “싱글톤 빈 내부에 프로토타입 빈을 주입받아 사용하면 어떻게 될까요?”

개발자의 의도는 싱글톤 빈을 사용할 때마다 내부의 프로토타입 빈이 새로 생성되기를 기대하겠지만, 실제로는 그렇지 않습니다. 싱글톤 빈은 생성 시점에 딱 한 번 의존성 주입을 받습니다. 이때 주입된 프로토타입 빈도 딱 한 번 생성되어 싱글톤 빈 내부에 영원히 갇히게 됩니다. 즉, 프로토타입의 의미가 사라지고 계속 같은 객체를 쓰게 되는 것입니다.

[해결 방법: DL(Dependency Lookup)]

이를 해결하기 위해 ObjectProvider를 사용해야 합니다.

Java

@Service
public class SingletonService {

    // 직접 주입받지 않고, 빈을 찾아주는 Provider를 주입받음
    private final ObjectProvider<PrototypeBean> prototypeBeanProvider;

    public SingletonService(ObjectProvider<PrototypeBean> prototypeBeanProvider) {
        this.prototypeBeanProvider = prototypeBeanProvider;
    }

    public void logic() {
        // getObject()를 호출할 때마다 스프링 컨테이너에서 새로운 프로토타입 빈을 생성해서 반환
        PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
        prototypeBean.use();
    }
}

이렇게 하면 싱글톤 빈 안에서도 필요할 때마다 새로운 프로토타입 빈을 꺼내서(Lookup) 사용할 수 있습니다.


3. 결론

오늘은 스프링 부트 애플리케이션을 안정적으로 운영하기 위해 반드시 알아야 할 빈의 생명주기와 스코프에 대해 자세히 알아보았습니다.

요약하자면, 스프링 빈은 생성 -> 의존성 주입 -> 초기화(@PostConstruct) -> 사용 -> 소멸(@PreDestroy)의 과정을 거치며, 개발자는 콜백 메서드를 통해 특정 시점에 필요한 작업을 수행할 수 있습니다. 또한, 기본적으로 싱글톤 스코프로 관리되어 성능을 최적화하지만, 상황에 따라 프로토타입이나 웹 스코프(Request, Session)를 적절히 활용하여 데이터의 격리 수준을 조절해야 합니다.

특히 실무에서는 99% 이상 싱글톤 스코프를 사용하되, 동시성 이슈를 피하기 위해 객체를 ‘무상태(Stateless)’로 설계하는 것이 무엇보다 중요합니다. 오늘 배운 내용을 바탕으로 여러분의 애플리케이션이 메모리는 적게 쓰면서도 기능은 완벽하게 수행하는 효율적인 시스템이 되기를 바랍니다.


“Spring Boot 빈(Bean) 생명주기와 스코프(Scope) 총정리: 싱글톤부터 프로토타입까지”에 대한 1개의 생각

댓글 남기기