본문 바로가기

공부/Spring 핵심 원리

빈 스코프의 종류와 사용법 알아 보기

빈 스코프가 뭐에요? 

 👧 빈이 존재할 수 있는 범위

 

✔ 스프링이 지원하는 다양한 스코프

싱글톤 기본 스코프로 스프링 컨테이너의 시작과 종료까지 유지되는 넓은 범위의 스코프
프로토타입 매우 짧은 범위의 스코프, 스프링 컨테이너가 프로토타입 빈의 생성과 의존관계 주입가지만 관여
웹 관련 스코프 - request : 웹 요청이 들어오고 나갈 때 까지 유지
- session : 웹 세션이 생성되고 종료될 때 까지 유지
- application : ;웹의 서블릿 컨텍스트와 같은 범위로 유지

 

✔ 싱글톤 스코프

기본인 싱글톤 스코프의 경우 스프링 조회 시 항상 같은 인스턴스를 공유해서 사용한다.

 

더보기
package com.example.core.scope;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import static org.assertj.core.api.Assertions.*;

public class SingletonTest {

    @Test
    void singletonBeanFind(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
        SingletonBean singletonBean = ac.getBean(SingletonBean.class);
        SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);

        System.out.println("singletonBean = " + singletonBean);
        System.out.println("singletonBean2 = " + singletonBean2);
        assertThat(singletonBean).isSameAs(singletonBean2);

        ac.close();
    }

    @Scope("singleton")
    static class SingletonBean{
        @PostConstruct
        public void init(){
            System.out.println("SingletonBean.init");
        }

        @PreDestroy
        public void destory(){
            System.out.println("SingletonBean.destory");
        }
    }
}

- singletonBean, singletonBean2는 같은 객체다.

- init과 distory를 한번 실행한 것을 확인할 수 있다.

 

 

✔ 프로토타입 스코프

- 프로토타입 스코프는 스프링을 조회하면 항상 새로운 인스턴스를 생성해서 반환한다.

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

- 프로토타입 빈 관리는 클라이언트가 해야 하기 때문에, @PreDestory 같은 종료 메서드가 호출되지 않는다.

더보기
package com.example.core.scope;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import static org.assertj.core.api.Assertions.*;

public class PrototypeTest {

    @Test
    void prototypeBeanFind(){

        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);

        System.out.println("find prototypeBean1");
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);

        System.out.println("find prototypeBean2");
        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);

        System.out.println("prototypeBean1 = " + prototypeBean1);
        System.out.println("prototypeBean2 = " + prototypeBean2);

        assertThat(prototypeBean1).isNotSameAs(prototypeBean2);

        ac.close();
    }

    @Scope("prototype")
    static class PrototypeBean{
        @PostConstruct
        public void init(){
            System.out.println("prototypeBean.init");
        }

        @PreDestroy
        public void destory(){
            System.out.println("prototypeBean.destory");
        }
    }
}

- prototypeBean1과 prototypeBean2는 다른 객체다

- close()를 했음에도 destory()가 실행되지 않는다.

 

 

✔ 프로토타입 스코프 - 싱글톤 빈과 함께 사용 시 문제점

스프링 컨테이너에서 프로토타입의 스프링 빈과, 싱글톤 빈을 함께 사용하면 의도대로 동작되지 않는다

 

더보기
package com.example.core.scope;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

import javax.annotation.PostConstruct;

import static org.assertj.core.api.Assertions.*;

public class SingletonWithPrototypeTest1 {


    @Test
    void singletonClientusePrototype(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class,PrototypeBean.class);

        ClientBean clientBean1 = ac.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        assertThat(count1).isEqualTo(1);

        ClientBean clientBean2 = ac.getBean(ClientBean.class);
        int count2 = clientBean2.logic();
        assertThat(count2).isEqualTo(2);
    }

    @Scope("singleton")
    static class ClientBean{
        private final PrototypeBean prototypeBean;


        @Autowired
       public ClientBean(PrototypeBean prototypeBean) {
            this.prototypeBean = prototypeBean;
        }

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

    @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);
        }

        public void destroy(){
            System.out.println("PrototypeBean.destroy"+this);
        }
    }
}

 

- 싱글톤 빈인 ClientBean에서 프로토타입의 빈을 생성했다.

- 프로토타입의 빈이면 새로운 객체를 계속해서 만들기 때문에 assertThat(count2).isEqualTo(2); 에서 오류가 발생해야하지만, 해당 코드에서 오류가 발생하지 않는다.

 

싱글톤 빈은 생성 시점에서 의존관계를 주입 받고, 주입 시점에 프로토 타입이 빈이 새로 생성되긴 하지만 싱글톤 빈과 함께 계속 유지된다. 

 

 

📌 프로토타입 스코프 + 싱글톤 빈 Provider로 문제 해결하자

 

✔ ObjectFactory,ObjectProvider

- 지정한 빈을 컨테이너에서 대신 찾아 주는 기능을 제공한다.

- ObjectProvider가 편의 기능이 추가돼서 제공해줌 

- 싱글톤 빈에서 프로토타입의 빈을 생성할 때 ObjectProvider를 사용하면

- ObjectProvider에서 프로토타입의 빈을 찾아줘서 프로토타입의 기능을 수행한다.

 

 

✔ JSR-330 Provider

Gradle 추가 :  implementation 'javax.inject:javax.inject:1'

 

- provider의 get()을 호출하면 내부에서 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.

 

 

📌 웹 스코프

- 웹 환경에서만 동작하며, 프로토타입과는 다른게 종료 시점까지 관리된다.

- request : HTTP 요청 하나가 들어오고 나갈때까지 유지, 각각 HTTP 요청 마다 빈이 별도로 관리됌
- session : HTTP Session과 동일한 생명주기를 가지는 스코프
- application : 서블릿 컨텍스트와 동일한 생명주기를 가지는 스코프

- websocket : 웹 소켓과 동일한 생명주기를 가지는 스코프

 

 

✔ request 스코프 예제

Gradle 추가 (내장된 톰캣 서버 사용)

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

 

✔ request 스코프 Logger을 만들어 보자

- UUID를 사용해 HTTP 요청 구분

- requestURL 추가로 어떤 URL 요청인지 로그 확인

 

Logger

더보기
package com.example.core.common;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.UUID;

@Component
@Scope(value = "request")
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 close(){
        System.out.println("[" + uuid+"]  request scope bean close : "+this);
    }
}

- request 스코프로 지정, 해당 빈은 HTTP 요청 당 하나 씩 생성된다.

- @PostConstruct 초기화 메서드를 사용해 uuid를 저장해 놓았다가 다른 HTTP 요청과 구분한다.

 

 

Controller

더보기
package com.example.core.web;


import com.example.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;

    private final ObjectProvider<MyLogger> myLoggerProvider;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request){


        String reuquestURL = request.getRequestURI().toString();
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.setRequestURL(reuquestURL);

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

        return "OK";

    }
}

- 요청 URL을 받는다.

- request 스코프는 스프링 애플리케이션 실행 시점에 생기지않는다.(실제 고객이 요청이 와야 생성)

- ObjectProvider를 사용해 HTTP 요청이 왔을 때 request 스코프 빈을 생성한다.

 

Service

더보기
package com.example.core.web;


import com.example.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final ObjectProvider<MyLogger> myLoggerProvider;

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

- 파라미터로 모든 정보를 서비스 계층에 넘길 수 있지만, 지저분해 진다. requestURL 과 같은 웹 관련 정보는 서비스 계층까지 안넘가는게 좋다.

 

 

✔ 스코프와 프록시

-ObjectProvider를 사용하지 않고 프록시를 사용해서 프로바이더처럼 사용할 수 있다.

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

    private String uuid;
    private String requestURL;
}

 

- CGLIB 라이브러리로 내 클래스를 상속받은 가짜 프록시 객체를 만들어서 주입한다.

- 스프링 실행 시점에 가짜 플록시 객체를 만들어 주입만들어 문제가 발생하지 않는다.

- 실제 요청이 오는 시점에 내부에서 진짜 빈을 요청한다.

 

 

REFERENCE


https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의

웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 원

www.inflearn.com

 

300x250