[Spring] 빈 스코프

2025. 2. 27. 02:16·Spring

빈 스코프란? 빈 유지 범위

 

스프링 빈의 스코프 종류


1. 싱글톤 -기본 스코프

스프링 컨테이너가 시작될 때 생성되고, 컨테이너 종료 시까지 유지됨.

@Scope("singleton") (기본값이라 명시 안 해도 됨)

@Component
@Scope("singleton")
public class SingletonBean {
    public SingletonBean() {
        System.out.println("싱글톤 빈 생성");
    }
}
더보기
public class SingletonTest {
    @Test
    public void singletonBeanFind() {
    	//컨테이너, SingletonBean 생성
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
        SingletonBean singletonBean1 = ac.getBean(SingletonBean.class); //호출전 이미 빈 생성되있음
        SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
        System.out.println("singletonBean1 = " + singletonBean1);
        System.out.println("singletonBean2 = " + singletonBean2);
        assertThat(singletonBean1).isSameAs(singletonBean2);

        ac.close();
    }

    @Scope("singleton")
    static class SingletonBean {

        @PostConstruct
        public void init(){
            System.out.println("SingletonBean.init");
        }
        @PreDestroy
        public void destroy(){
            System.out.println("SingletonBean.destroy");
        }
    }
}

 -결과

SingletonBean.init
singletonBean1 = hello.core.scope.SingletonTest$SingletonBean@651aed93
singletonBean2 = hello.core.scope.SingletonTest$SingletonBean@651aed93
SingletonBean.destroy

 

2. 프로토타입 (Prototype)

요청할 때마다 새로운 빈을 생성함. 매우 짧은 스코프

 

스프링 컨테이너가 생성과 의존관계 주입까지만 관리하고, 이후에는 컨테이너가 관리하지 않음.(종료메서드 호출x)

@Scope("prototype")

@Component
@Scope("prototype")
public class PrototypeBean {
    public PrototypeBean() {
        System.out.println("프로토타입 빈 생성");
    }
}

 

싱글톤 스코프 빈 조회 시 항상 같은 인스턴스의 스프링 빈 반환. 반면

프로토타입 스코프 조회 시  스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환함.

 

1. 스프링 컨테이너에 빈 요청 

2. 이 시점에 프로토타입 빈을 생성, 필요 의존관계 주입

3. 생성한 프로토타입 빈 클라이언트에 반환, 관리x (@PreDestroy 같은 종료 메서드가 호출x )

4. 이후에 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로토타입 빈을 생성해서 반환함

 

핵심은 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리한다는 것

 

이후엔 사용자가 직접 관리해야함.

 

public class PrototypeTest {
    @Test
    public void prototypeBeanFind() {
    	//컨테이너 생성, 프로토타입이라 요청x, 빈 생성x
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        //프로토타입. 요청시점 새 prototypeBean빈 생성
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
       	//프로토타입. 요청시점 새 prototypeBean빈 생성
        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
        System.out.println("prototypeBean1 = " + prototypeBean1);
        System.out.println("prototypeBean2 = " + prototypeBean2);
        assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
		
        prototypeBean1.destroy() //수동 호출해줘야함
        ac.close();
    }

    @Scope("prototype")
    static class PrototypeBean {

        @PostConstruct
        public void init(){
            System.out.println("prototype.init");
        }
        @PreDestroy //호출x
        public void destroy(){
            System.out.println("prototype.destroy");
        }
    }
}
prototype.init  <-find prototypeBean1
prototype.init  <-find prototypeBean2
prototypeBean1 = hello.core.scope.PrototypeTest$PrototypeBean@651aed93
prototypeBean2 = hello.core.scope.PrototypeTest$PrototypeBean@4dd6fd0a

 

매번 새로운 인스턴스가 필요할 때 사용(사용할 일 드뭄)

 

  2-1. 싱글톤 빈과 함께 사용시 문제점

1. 클라이언트 A,B가 프로토타입 빈 요청

2. 컨테이너 프로토타입 빈 생성(x01,x02), 반환 . 해당 빈들의 count필드 값 0

3. 프로토타입 빈에 addCount()호출 count 필드 +1

 

여기서 clientBean이라는 싱글톤 빈에 프로토타입 빈을 주입받아 사용한다면?

public class SingletonWithPrototypeTest1 {
    @Test
    void prototypeFind() {
        //1. 컨테이너 생성, ClientBean 빈생성
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);

        ClientBean clientBean1 = ac.getBean(ClientBean.class);
        //3. logic()실행. count =1
        int count1 = clientBean1.logic();
        assertThat(count1).isEqualTo(1);

        ClientBean clientBean2 = ac.getBean(ClientBean.class); //4. 싱글톤 동일객체 반환
        //5. logic()실행. 같은 PrototypeBean(x01) 사용 count = 2
        int count2 = clientBean2.logic();
        assertThat(count2).isEqualTo(2);
    }

    static class ClientBean {
        private final PrototypeBean prototypeBean;

        //2. PrototypeBean(x01) 한 번 생성되어 주입됨, init() 실행
        @Autowired
        public ClientBean(PrototypeBean prototypeBean) {
            this.prototypeBean = prototypeBean;
        }

        public int logic() {
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }

    @Scope("prototype")
    static class PrototypeBean {
        private int count = 0;
        public void addCount() {
            count++;
        }
        public int getCount() {
            return count;
        }
        @PostConstruct
        public void init() {
            System.out.println("PrototypeBean.init " + this);
        }
        @PreDestroy
        public void destroy() {
            System.out.println("PrototypeBean.destroy");
        }
    }
}

 

❗여기서의 문제점

 

- 프로토타입 빈을 주입받았지만, 싱글톤 빈이 생성될 때 한 번만 생성되고 이후에는 계속 같은 객체를 사용

- 원래 기대했던 "매번 새로운 프로토타입 빈이 생성되는 것"과 다름

 

   2-2. 싱글톤 빈과 함께 사용시 Provider로 문제 해결

어떻게 하면 사용할 때마다 항상 새로운 프로토타입 빈 생성할 수 있을까?

 

  • ObjectProvider 사용
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;//PrototypeBean를 찾을수있는 lookup주입됨

public int logic() {
    PrototypeBean prototypeBean = prototypeBeanProvider.getObject();//컨테이너 없을때 새 프로토타입 빈 생성
    prototypeBean.addCount();
    int count = prototypeBean.getCount();
    return count;
}

1. 스프링 컨테이너 실행 (ClientBean 싱글톤 생성)
2.  clientBean1.logic() 실행

   PrototypeBean(x01) 생성

   x01.addCount() → count = 1

   return 1

3. clientBean1.logic() 다시 실행

   PrototypeBean(x02) 새로 생성

   x02.addCount() → count = 1

   return 1

 

  • Provider API 사용
dependencies {
    implementation 'jakarta.inject:jakarta.inject-api:2.0.1'
}
@Autowired
private Provider<PrototypeBean> provider; 

public int logic() {
    PrototypeBean prototypeBean = provider.get(); //항상 새 프로토타입 빈 생성
    prototypeBean.addCount();
    int count = prototypeBean.getCount();
    return count;
}

 

 

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

웹 애플리케이션에서만 사용되며, 특정 웹 요청 및 세션과 함께 빈의 생명주기가 결정됨.

해당 스코프의 종료시점까지 관리한다. 따라서 종료 메서드가 호출됨

스코프 이름 설명
request HTTP 요청이 들어오고 나갈 때까지 유지. (@Scope("request"))
각각의 HTTP 요청마다 별도 빈 인스턴스 생성,관리됨
session HTTP 세션이 생성되고 종료될 때까지 유지됨. (@Scope("session"))
application 웹 애플리케이션이 실행되는 동안 유지됨. (@Scope("application"))

 

예제

HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 UUID를 사용해서 HTTP 요청을 구분해보자.

 

웹환경에 동작하기 떄문에 라이브러리 추가

implementation 'org.springframework.boot:spring-boot-starter-web'

 

-MyLogger

@Component
@Scope("request") //HTTP 요청 당 하나의 빈 생성되고, HTTP 요청이 끝나는 시점에 소멸
public class MyLogger {
    private String uuid;
    private String requestURL;

    public void setRequestURL(String requestURL) {
        this.requestURL = requestURL;
    }

    public void log(String message){
        System.out.println("[" + uuid + "]" + "[" + requestURL + "] " + message);
    }

    @PostConstruct
    public void init(){
        uuid = UUID.randomUUID().toString();
        System.out.println("[" + uuid + "] request scope bean create:" + this);

    }
    @PreDestroy
    public void destroy(){
        System.out.println("[" + uuid + "] request scope bean destroy:" + this);
    }
}

 

@Service
@RequiredArgsConstructor
public class LogDemoService {
    private final MyLogger myLogger;

    public  void logic(String id) {
        myLogger.log("service id = " + id);
    }
}

 

@Controller
@RequiredArgsConstructor
public class LogDemoController {
    private final LogDemoService logDemoService;
    private final MyLogger myLogger; //오류. request요청 없어 빈 생성 아직 안됨

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) { //request정보
        String requestURI = request.getRequestURI().toString();//어떤 url로 요청했는지
        myLogger.setRequestURL(requestURI);

        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}

 

실행시키 오류 발생

Error creating bean with name 'myLogger': Scope 'request' is not active for the
 current thread; consider defining a scoped proxy for this bean if you intend to
 refer to it from a singleton;

 

싱글톤 빈은 생성해서 주입이 가능하지만, request 스코프 빈은 요청전이라 아직 생성되지 않음!

Provider, 프록시를 사용해보자


스코프와 Provider


@Controller
@RequiredArgsConstructor
public class LogDemoController {
    private final LogDemoService logDemoService;
    private final ObjectProvider<MyLogger> myLoggerProvider;
    //요청전 MyLogger빈없으니 lookup주입해 필요할 때까지 빈 생성을 미룸

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        String requestURI = request.getRequestURI().toString();

        MyLogger myLogger = myLoggerProvider.getObject(); //추가.(컨테이너에 없을 때만 새로 생성)
        myLogger.setRequestURL(requestURI);

        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}

 

@Service
@RequiredArgsConstructor
public class LogDemoService {
    private final ObjectProvider<MyLogger> myLoggerProvider;

    public  void logic(String id) {
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.log("service id = " + id);
    }
}

 

결과

[e00a37a1-95f6-4c17-bb28-6da6b1382b6c] request scope bean create:hello.core.common.MyLogger@3b58d3ed
[e00a37a1-95f6-4c17-bb28-6da6b1382b6c][/log-demo] controller test
[e00a37a1-95f6-4c17-bb28-6da6b1382b6c][/log-demo] service id = testId
[e00a37a1-95f6-4c17-bb28-6da6b1382b6c] request scope bean destroy:hello.core.common.MyLogger@3b58d3ed

더 줄여보자,,

 

스코프와 프록시


Provider 사용 이전 코드에서 이것만 수정

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) 
public class MyLogger {

*적용 대상이  클래스 → TARGET_CLASS, 인터페이스  → INTERFACES 선택

MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있음!

 

동작원리


System.out.println("myLogger = " + myLogger.getClass());
myLogger = class hello.core.common.MyLogger$$SpringCGLIB$$0

 

1. 컨테이너는 CGLIB 라는 바이트코드를 조작하는 라이브러리를 사용해서,

MyLogger를 상속받은 가짜 프록시 객체를 생성,주입(실제 요청들어오면 진짜 빈을 가져와 호출하는 위임 로직 포함됨)

2. 요청이 들어오면 가짜 프록시 객체가 내부적으로 컨테이너에서 진짜 MyLogger 빈을 가져와 실제 객체처럼 동작하면서 내부 요청을 위임

3. 요청이 끝나면 @PreDestroy로 빈 소멸

 

*싱글톤 빈을 사용하듯이 편리하게 request scope를 사용할 수 있지만 다르게 동작하니 주의, 최소화 사용할것

 

 

'Spring' 카테고리의 다른 글

[Spring] HTTP (2)  (0) 2025.03.03
[Spring] HTTP(1)  (0) 2025.02.28
[Spring] 빈 생명주기 콜백  (0) 2025.02.26
[Spring] 의존관계 자동 주입  (0) 2025.02.24
[Spring] 컴포넌트 스캔  (0) 2025.02.21
'Spring' 카테고리의 다른 글
  • [Spring] HTTP (2)
  • [Spring] HTTP(1)
  • [Spring] 빈 생명주기 콜백
  • [Spring] 의존관계 자동 주입
Naah
Naah
  • Naah
    blueprint
    Naah
  • 전체
    오늘
    어제
    • 분류 전체보기 (106)
      • Java (28)
      • Kotlin (0)
      • TypeScript (7)
      • React (22)
      • Next.js (1)
      • Spring (22)
      • JPA (12)
      • Spring Data JPA (6)
      • Querydsl (1)
      • Error (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 글쓰기
    • manage
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Naah
[Spring] 빈 스코프
상단으로

티스토리툴바