군만두의 IT 공부 일지

[스터디7] 03. 스프링 컨텍스트: 빈의 스코프 및 수명 주기 본문

프로그래밍/Java

[스터디7] 03. 스프링 컨텍스트: 빈의 스코프 및 수명 주기

mandus 2025. 4. 29. 16:25

목차

    5장. 스프링 컨텍스트: 빈의 스코프 및 수명 주기

    이 장에서 다룰 내용
    - 싱글톤 빈 스코프 사용하기
    - 싱글톤 빈을 위한 즉시 및 지연 인스턴스 생성하기
    - 프로토타입 빈 스코프
    • 스코프(scope): 스프링에서 빈을 생성하고 수명 주기를 관리하는 방식
      1. 싱글톤(singleton)
      2. 프로토타입(prototype)

    5.1 싱글톤 빈 스코프 사용

    5.1.1 싱글톤 빈의 작동 방식

    • 싱글톤은 스프링에서 가장 많이 사용되는 기본 빈 스코프이다.
    • 스프링은 컨텍스트를 로드할 때 싱글톤 빈을 생성하고 빈에 이름(빈 ID)을 할당한다. 특정 빈을 참조할 때 항상 동일한 인스턴스를 얻기 때문에 이 스코프를 싱글톤이라고 한다.
    • 스프링 컨텍스트에서 이름이 다른 경우 동일한 타입의 인스턴스를 더 많이 가질 수 있다. 스프링에서 싱글톤은 동일한 타입의 여러 인스턴스를 허용하며, 싱글톤은 이름별로 고유하지만 앱 단위로 고유하지 않다는 것을 의미한다.

    @Bean으로 싱글톤 스코프의 빈 선언하기

    그림에서 원두는 스프링이 컨텍스트에 추가한 인스턴스를 나타낸다. 컨텍스트에는 관련된 이름을 가진 인스턴스(빈)가 하나만 포함되어 있다는 것을 알 수 있다.

    • @Bean 애너테이션 접근 방식을 사용하여 컨텍스트에 빈을 추가할 때 @Bean으로 애너테이션된 메서드 이름이 빈 이름이 된다.
    • 스프링 컨텍스트에서 빈을 가져올 때, 해당 종류의 빈이 하나만 있을 때는 이름을 사용할 필요 없이 빈의 타입을 사용하여 해당 빈을 가져올 수 있다.
    package config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import services.CommentService;
    
    @Configuration
    public class ProjectConfig {
    
      @Bean	// 스프링 컨텍스트에 CommentService 빈을 추가함.
      public CommentService commentService() {
        return new CommentService();
      }
    }

    스테레오타입 애너테이션으로 싱글톤 빈 선언하기

    서비스 클래스 두 개(CommentService, UserService)가 리포지토리(CommentRepository)에 종속된다고 했을 때, 빈 간 관계와 스프링이 컨텍스트에서 링크를 설정하는 방법에 중점을 둔다.

    package repositories;
    
    import org.springframework.stereotype.Repository;
    
    @Repository
    public class CommentRepository {
    }
    package services;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import repositories.CommentRepository;
    
    @Service
    public class CommentService {
    
      @Autowired
      private CommentRepository commentRepository;
    
      public CommentRepository getCommentRepository() {
        return commentRepository;
      }
    }

    CommentService 클래스는 @Autowired를 사용하여 스프링이 클래스에서 선언된 속성에 CommentRepository 타입의 인스턴스를 주시하도록 지시한 것을 볼 수 있다.

    package services;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import repositories.CommentRepository;
    
    @Service
    public class UserService {
    
      @Autowired
      private CommentRepository commentRepository;
    
      public CommentRepository getCommentRepository() {
        return commentRepository;
      }
    }

    UserService 클래스도 CommentService 클래스와 같은 로직으로 정의되어 있다. 이 프로젝트에서는 구성 클래스가 비어 있다.

    5.1.2 실제 시나리오의 싱글톤 빈

    싱글톤 빈의 스코프는 여러 컴포넌트가 하나의 객체 인스턴스를 공유할 수 있다고 가정하기 때문에 불변(immutable)이어야 한다. 이런 스레드가 인스턴스를 변경하면 경쟁 상태(race condition) 시나리오가 발생한다.

    • 경쟁 상태: 멀티스레드 아키텍처에서 여러 공유 자원을 변경하려고 할 때 발생할 수 있는 상황
      1. 속성이 변경되는 가변(mutable) 싱글톤 빈을 변경하려면 주로 스레드 동기화를 사용하여 동시성이 있는(concurrent) 빈이 되도록 직접 만들어야 한다. 하지만 싱글톤 빈은 동기화되도록 설계되지 않았기 때문에, 동일한 문제를 해결할 다른 방법을 찾는 것이 좋다.
      2. 생성자 주입의 장점 중 하나는 인스턴스를 불변으로 만들 수 있다는 것이다. 필드 주입을 생성자 주입으로 대체하여 CommentService 클래스 정의를 개선할 수 있다.
    @Service
    public class CommentService {
    
      @Autowired
      private final CommentRepository commentRepository;	// 필드를 final로 정의하면 이 필드는 변경될 수 없다는 것을 강조할 수 있음.
    
      public CommentService(CommentRepository commentRepository) {
        this.commentRepository = commentRepository;
      }
      
      public CommentRepository getCommentRepository() {
        return commentRepository;
      }
    }

    5.1.3 즉시 및 지연 인스턴스 생성 방식

    • 스프링은 컨텍스트를 초기화할 때 모든 싱글톤 빈을 생성하는 기본 동작인 즉시 인스턴스 생성(eager instantiation) 방식을 사용한다.
    • 프레임워크의 다른 방식인 지연 인스턴스 생성(lazy instantiation) 방식은 스프링 컨텍스트를 생성할 때 싱글톤 인스턴스를 생성하지 않는다. 대신 빈을 처음 참조할 때 각 인스턴스를  생성한다.

    예제 1)

    기본(즉시) 초기화를 테스트하는 데 빈만 필요할 때, 아래 코드처럼 @Bean 애너테이션 방식이나 스테레오타입 애너테이션을 사용하여 이 클래스를 빈으로 만든다.

    @Service
    public class CommentService {
        public CommentService() {
            System.out.println("CommentService instance created!");
        }
    }
    @Configuration
    @ComponentScan(basePackages = {"services"})
    public class ProjectConfig {
    }
    public class Main {
        public static void main(String[] args) {	// 이 앱은 스프링 컨텍스트를 생성하지만, 어디에서도 CommentService 빈을 사용하지 않음.
            var c = new AnnotationConfigApplicationContext(ProjectConfig.class);
        }
    }

    앱이 어디에도 빈을 사용하지 않더라도, 앱을 실행하면 콘솔에서 다음 출력문을 확인할 수 있다.

    CommentService instance created!
    

    예제 2)

    클래스(스테레오타입 애너테이션 방식의 경우) 또는 @Bean 메서드(@Bean 메서드 접근 방식의 경우)에 @Lazy 애너테이션을 추가해서 변경한다. 누군가 빈을 사용할 때만 생성하도록 스프링에 지시했기 때문에 앱을 실행할 때 콘솔에 앞의 메시지가 더 이상 출력되지 않는 것을 볼 수 있다.

    @Service
    @Lazy	// @Lazy 애너테이션은 누군가 처음 이 빈을 참조할 때만 스프링에 빈을 생성하도록 지시함.
    public class CommentService {
        public CommentService() {
            System.out.println("commentService instance created!");
        }
    }

    다음 코드처럼 Main 클래스를 변경하고 CommentService 빈에 대한 참조를 추가한다.

    public class Main {
        public static void main(String[] args) {
            var c = new AnnotationConfigApplicationContext(ProjectConfig.class);
    
            System.out.println("Before retrieving the CommentService");
            var service = c.getBean(CommentService.class); // 이 코드 줄에서 스프링은 CommentService 빈의 참조 값을 제공해야 함. 이때 빈 인스턴스도 생성함.
            System.out.println("After retrieving the CommentService");
        }
    }

     

    앱을 다시 실행하면 콘솔에서 출력을 확인할 수 있다.

    Before retrieving the CommentService  
    CommentService instance created!  
    After retrieving the CommentService
    

    지연 인스턴스 생성 방식에서는 프레임워크가 먼저 인스턴스가 있는지 확인한 후 없다면 결국 인스턴스를 생성해야 한다.

    • 무언가 잘못되어 프레임워크가 빈을 생성할 수 없다면, 앱을 시작할 때 이 문제를 확인할 수 있다.
    • 지연 인스턴스화를 사용하면 앱이 이미 실행 중이고 빈을 생성해야 하는 시점에 문제를 확인할 수 있다.

    5.2 프로토타입 빈 스코프 사용

    5.2.1 프로토타입 빈의 동작 방식

    • 프로토타입 스코프의 빈에 대한 참조를 요청할 때마다 스프링은 새로운 객체 인스턴스를 생성한다.
    • 프로토타입 빈의 경우 스프링은 객체 인스턴스를 직접 생성하고 관리하지 않는다.
    • 프레임워크는 객체의 타입을 관리하고 빈에 대한 참조를 요청받을 때마다 새로운 인스턴스를 생성한다.
    • 빈의 스코프를 변경하려면 @Scope라는 새 애너테이션을 사용해야 한다.
      • @Bean 애너테이션 방식을 사용하여 빈을 생성할 때, @Scope는 빈을 선언하는 메서드에 대해 @Bean과 함께 사용된다.
      • 스테레오타입 애너테이션을 사용하여 빈을 선언할 때는 빈을 선언하는 클래스 위에 @Scope 애너테이션과 스테레오타입 애너테이션을 사용한다.

    @Bean으로 프로토타입 스코프의 빈 선언하기

    package services;
    
    public class CommentService {
    }
    package config;
    
    import org.springframework.beans.factory.config.BeanDefinition;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Scope;
    import services.CommentService;
    
    @Configuration
    public class ProjectConfig {
    
      @Bean
      @Scope(BeanDefinition.SCOPE_PROTOTYPE)	// 이 빈을 프로토타입 스코프로 만듦.
      public CommentService commentService() {
        return new CommentService();
      }
    }
    package main;
    
    import config.ProjectConfig;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    import services.CommentService;
    
    public class Main {
    
      public static void main(String[] args) {
        var c = new AnnotationConfigApplicationContext(ProjectConfig.class);
    
        var cs1 = c.getBean("commentService", CommentService.class);
        var cs2 = c.getBean("commentService", CommentService.class);
    
        boolean b1 = cs1 == cs2;	// 두 변수 cs1과 cs2가 서로 다른 인스턴스를 참조함.
        System.out.println(b1);	// 항상 'false'가 콘솔에 출력됨.
      }
    }

    스테레오타입 애너테이션으로 프로토타입 스코프 빈 선언하기

    package services;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import repositories.CommentRepository;
    
    @Service
    public class CommentService {
    
      @Autowired
      private CommentRepository commentRepository;
    
      public CommentRepository getCommentRepository() {
        return commentRepository;
      }
    }
    package config;
    
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    @ComponentScan(basePackages = {"services", "repositories"})
    public class ProjectConfig {
    
    }
    package main;
    
    import config.ProjectConfig;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    import services.CommentService;
    import services.UserService;
    
    public class Main {
    
      public static void main(String[] args) {
        var c = new AnnotationConfigApplicationContext(ProjectConfig.class);
    
        var s1 = c.getBean(CommentService.class);
        var s2 = c.getBean(UserService.class);
    
        boolean b = s1.getCommentRepository() == s2.getCommentRepository();
    
        System.out.println(b);
      }
    }

    5.2.2 실제 시나리오에서 프로토타입 빈 관리

    댓글을 처리하고 유효성을 검사하는 CommentProcessor라는 객체를 설계할 때, 서비스에서는 사용 사례를 구현하려고 CommentProcessor 객체를 사용하지만 CommentProcessor 객체는 처리할 댓글을 속성으로 저장하고 그 메서드는 그 속성을 변경한다.

    package processors;
    
    import model.Comment;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.config.BeanDefinition;
    import org.springframework.context.annotation.Scope;
    import org.springframework.stereotype.Component;
    import repositories.CommentRepository;
    
    @Component
    @Scope(BeanDefinition.SCOPE_PROTOTYPE)
    public class CommentProcessor {
    
      @Autowired
      private CommentRepository commentRepository;
    
      private Comment comment;
    
      public void setComment(Comment comment) {
        this.comment = comment;
      }
    
      public Comment getComment() {
        return this.comment;
      }
    
      public void processComment(Comment comment) {	// 두 메서드가 Comment 속성 값을 변경함.
      	// comment 속성을 변경함.
      }
    
      public void validateComment(Comment comment) { // 두 메서드가 Comment 속성 값을 변경함.
      	// comment 속성을 검사하고 변경함.
      }
    }
    @Service
    public class CommentService {
    
      @Autowired
      private ApplicationContext context;
    
      public void sendComment(Comment c) {
        CommentProcessor p = context.getBean(CommentProcessor.class);	// 이 메서드를 호출하면 항상 새로운 CommentProcessor 인스턴스가 제공됨.
    
        p.setComment(c);
        p.processComment(c);
        p.validateComment(c);
    
        c = p.getComment();
        // 추가 작업 수행
      }
    
    }
    싱글톤 프로토타입
    • 프레임워크는 이름을 실제 객체 인스턴스와 연관시킨다.
    • 빈을 참조할 때마다 동일한 객체 인스턴스를 얻게 된다.
    • 컨텍스트가 로드될 때 또는 처음 참조될 때 인스턴스를 생성하도록 스프링을 구성할 수 있다.
    • 싱글톤은 스프링의 기본 빈 스코프다.
    • 이름은 타입과 연관되어 있다.
    • 빈 이름을 참조할 때마다 새로운 인스턴스가 생성된다.
    • 프레임워크는 빈을 참조할 때 항상 프로토타입 스코프에 대한 객체를 생성한다.
    • 빈을 명시적으로 프로토타입으로 표시해야 한다.
    • 프로토타입 빈은 변경 가능한 속성을 포함시킬 수 있다.

     

    이 글은 『스프링 교과서』 책을 학습한 내용을 정리한 것입니다.
    Comments