[기본] 빈 스코프

이전 포스팅

스프링빈 중 싱글톤빈은 스프링 컨테이너의 시작과 함께 생성되며 스프링 컨테이너가 종료될 때 까지 유지된다. 또한 싱글톤빈은 모든 사용자가 컨테이너 내내 존재하는 하나의 빈을 공유한다.

사용자 마다 다른 빈을 만들어 사용하고 싶은데 이런 경우 어떻게 하면 될까?


빈 스코프

빈 스코프란 빈이 존재할 수 있는 범위를 말한다. 기본적인 스프링빈은 싱글톤 스코프로 컨테이너 종료시 까지 컨테이너 안에 존재한다. 스프링은 필요에 따라 사용하도록 다양한 스코프를 지원한다.

스코프 이름생성 시점소멸 시점
singleton컨테이너 시작컨테이너 종료
prototype빈 요청빈 반환
request클라이언트 요청클라이언트에게 응답
session웹 세션 생성웹 세션 종료
application컨테이너 시작컨테이너 종료


프로토타입 빈

프로토타입 빈은 싱글톤 빈과 다르게 빈 조회(요청)시에 생성되며. 생성한 빈에 의존성주입하여 반환해준 뒤 컨테이너는 해당 프로토타입 빈을 더이상 관리하지 않는다.

싱글톤 빈에 프로토타입 빈 주입

프로토타입 빈은 사용할 때 마다 새로운 객체를 할당받고 싶을 때 사용한다. 예를 들어 각 클라이언트의 count를 측정하는 객체를 프로토타입 빈으로 등록하고. 클라이언트가 사용할 객체를 싱글톤 빈으로 등록한다고 가정하자.

@Scope("singleton")
    static class ClientBean {
        private final PrototypeBeanProvider;
		
        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++;
        }
    }
}

즉 요청시마다 새로운 프로토타입 빈을 싱글톤 빈에 주입하여 사용하고 싶었던 것이다. 하지만 실제 구동해보면 클라이언트들은 프로토타입 빈 까지도 공유한다.

왜 그럴까?
프로토타입 빈은 조회시 생성된다. 싱글톤 빈은 프로토타입 빈을 주입받기에 프로토타입 빈을 조회할 것이고. 이때 프로토타입 빈이 생성되어 싱글톤 빈에 주입된다. 그 후에는 이미 싱글톤 빈에 주입되어 있으므로 새로 주입될 필요가 없어 하나의 싱글톤 빈+프로토타입 빈을 계속해서 사용하게 되는 것이다.

해결책

해결책은 logic 호출시 마다 프로토타입 빈을 조회하면 새로운 프로토타입 빈을 생성할 것이고. 거기서 count를 증가시키면 된다.


@Scope("singleton")
    static class ClientBean {
        private ObjectProvider<PrototypeBean> prototypeBeanProvider;
        
        public int logic() {
            PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }
}

즉 의존관계주입(DI)가 아닌 의존관계탐색(DL)이 필요하다. DL은 지정한 빈을 컨테이너에서 찾아주는 것이며 스프링은 ObjectProvider 로 이를 제공한다. ObjectProvider를 멤버변수로 선언하고 필요시 getObject 하여 사용하면 된다.

ObjectProvider.getObect() = ac.getBean(Object.class) 와 동일하게 동작한다.


사용할 때마다 새로운 객체가 필요할 때 사용하는 프로토타입 빈은 조회시에만 새로 생성된다. 싱글톤빈에 프로토타입 빈을 주입해 사용하면 처음 의존관계주입시에만 조회하므로 새로 생성되지 않는다. 컨테이너에서 빈 조회시에는 ObjectProvider를 사용하여 빈의 주입시점을 마음대로 정할 수 있다.


Request 스코프

request 스코프의 빈은 웹에서 사용자의 요청시 새로 생성되며, 응답 시 컨테이너는 관리를 멈춘다.

웹 라이브러리 추가

  1. build.gradle의 dependencies에
    implementation 'org.springframework.boot:spring-boot-starter-web' //웹 라이브러리 추가

  2. AnnotationConfigApplicationContext 대신 웹 기능을 추가한 AnnotationConfigServletWebServerApplicationContext로 컨테이너 구동


Request 스코프 활용

사용자의 request 마다 유일한 id를 부여하고 이를 확인해보고자 한다.
request 스코프 빈 MyLogger의 필드로 id를 갖도록 등록하고.
Controller에서 request 빈을 주입받아 출력하면 되지 않을까?

-> 안된다 Controller는 싱글톤 스코프로. 컨테이너 시작시 생성 및 주입을 수행한다. 따라서 주입받을 request 빈을 조회하는데 요청이 들어올 때 생성되므로 컨테이너 생성 시점에 없다. 따라서 에러가 발생한다. 필요할 때 빈을 가져오게 하던가. 시작 시에는 가짜 빈을 넣도록 해야한다.

1. Provider 사용 (필요할 때 빈 가져오도록 하기)

@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);
    }
}
@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final ObjectProvider<MyLogger> myLoggerProvider;
    //myLogger와 Service 주입받음

    @RequestMapping("log-demo")
    @ResponseBody
    public String lodDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURL().toString();
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.setRequestURL(requestURL);

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

        return "OK";
    }
}

Provider를 통해 Controller 생성 시 myLogger를 요청하는 것이 아닌. 필요할 때 조회하여 사용하도록 하여 에러를 방지했다.

2. 가짜 빈 넣어두기

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
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);
    }
}

위와 같이 @Scope에 proxyMode 속성을 추가하면 CGLIB이 가짜 프록시 클래스를 만들어 다른 빈에 주입할 수 있다. 가짜 빈은 진짜 빈의 위치를 알고 있어 실제 사용시에는 진짜 빈을 호출해 사용한다. 프록시 객체는 진짜 객체를 상속하였기에 사용자는 동일하게 사용 가능(다형성)


요청마다 새로운 객체를 원할 때는 request 스코프 사용. Controller에 request빈을 주입해 사용하면 생성시점에 request빈이 없어 에러 발생. Provide나 프록시 방식으로 이를 해결가능. request빈과 HttpRequest는 다르다. HttpRequest는 클라이언트 요청 자체. request 빈은 요청마다 처리를 위해 컨테이너에 등록한 객체.



본 글은 김영한님의 "스프링 핵심 원리 - 기본편" 강의내용 및 이해한 내용을 정리한 것입니다.

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8

좋은 웹페이지 즐겨찾기