728x90

인프런 김영한 님의 강의를 참고했습니다.

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

빈 스코프에 대해서 배워보자.

 

11일차에 우리는 스프링 빈이 컨테이너가 시작될 때 생성이되어 컨테이너가 종료될 때까지 유지된다고 배웠다.

싱글톤 스코프로 생성이 되기 때문인데, 스코프는 말 그대로 빈이 존재할 수 있는 범위이다.

 

스프링은 다음과 같은 스코프를 지원한다.

  • 싱글톤

기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.

  • 프로토타입

스프링 컨테이너는 프로토타입 빈의 생성과 의존관계까지만 관여하고 더는 관리하지 않는 짧은 범위의 스코프이다.

  • 웹 관련 스코프

웹 요청에 관한 스코프들로 request, session, application이 있다.

 

 

빈 스코프는

@Scope("prototype")로 설정할 수 있다.

 

프로토타입 스코프

기존의 싱글톤을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환했다.

하지만 프로토타입 스코프를 조회하면 항상 새로운 인스턴스를 생성해서 반환해준다.

 

 

프로토타입 스코프의 빈을 스프링 컨테이너에 요청하면

스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한다.

그 빈을 스프링 컨테이너는 반환한다.

 

스프링은 딱 이 과정에만 관여하고, 그 후에는 관리를 하지 않는다.

따라서 @PreDestroy와 같은 종료 메서드도 호출되지 않는다.

 

늘 하던대로 싱글톤 스코프 빈을 먼저 테스트 해보자

package hello.core.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

public class SingletonTest {
    
    @Scope("singleton")
    static class SingletonBean{
        @PostConstruct
        public void init(){
            System.out.println("SingletonBean.init");
        }
        
        @PreDestroy
        public void destroy(){
            System.out.println("SingletonBean.destroy");
        }
    }
    
    @Test
    public void singletonBeanFind(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(SingletonBean.class);
        
        SingletonBean singletonBean1 = applicationContext.getBean(SingletonBean.class);
        SingletonBean singletonBean2 = applicationContext.getBean(SingletonBean.class);

        System.out.println("singletonBean1 = " + singletonBean1);
        System.out.println("singletonBean2 = " + singletonBean2);

        Assertions.assertSame(singletonBean1, singletonBean2);
        
        applicationContext.close();
    }
}

이렇게 같은 인스턴스가 출력이 되는 것을 볼 수 있다.

 

이번에는 싱글톤이 아닌 프로토타입을 테스트해보자.

package hello.core.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

public class PrototypeTest {

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

        @PreDestroy
        public void destroy(){
            System.out.println("PrototypeBean.destroy");
        }
    }

    @Test
    public void prototypeBeanFind(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(PrototypeBean.class);
        System.out.println("find prototypeBean1");
        PrototypeBean prototypeBean1 = applicationContext.getBean(PrototypeBean.class);

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

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

        Assertions.assertNotSame(prototypeBean1, prototypeBean2);
        applicationContext.close();
    }
}

이렇게 프로토타입을 테스트해보면

다른 인스턴스가 생성이 되는 것을 확인할 수 있다.

그리고 초기화도 2번이 실행된 것을 확인할 수 있다.

또한 프로토타입은 생성, 의존관계주입, 초기화까지만 관여하기에 @PreDestroy의 메서드가 출력되지 않는 것을 볼 수 있다.

 

프로토타입과 싱글톤을 함께 사용시의 문제점

프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 반환한다.

하지만 싱글톤 빈에 들어간 프로토타입 빈은 어떻게 될까?

 

싱글톤 빈이 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈일 것이다.

그렇기 때문에 사용할 때마다 생성되는 것이 아니며, 안에 프로토타입 빈이 있더라도 프로토타입 빈으로의 작동을 못하게 될 것이다.

이렇게 사용할 수도 있지만, 아마 이것은 프로토타입을 사용하는 취지에 어긋날 것이다.

그렇기에 항상 새로운 프로토타입 빈을 생성하는 방법에 대해 알아보자.

 

Provider로 프로토타입 스코프를 사용

프로토타입 빈을 요청받는 방법은 사용할 때마다 스프링 컨테이너에 새로 요청하는 것이다.

package hello.core.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

public class PrototypeProviderTest {

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

    static class ClientBean{
        @Autowired
        private ApplicationContext applicationContext;

        public int logic(){
            PrototypeBean prototypeBean = applicationContext.getBean(PrototypeBean.class);
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }

    @Test
    void providerTest(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);

        ClientBean clientBean1 = applicationContext.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        Assertions.assertEquals(1, count1);

        ClientBean clientBean2 = applicationContext.getBean(ClientBean.class);
        int count2 = clientBean1.logic();
        Assertions.assertEquals(1, count2);
    }
}

이렇게 의존관계를 찾는 방법을 Dependency Lookup이라고 하는데, 스프링 컨테이너 종속적인 코드가 되고 좋은 코드가 아니다.

 

따라서 다른 방법들을 사용한다.

 

ObjectFactory, ObjectProvider

Dependency Lookup 서비스를 제공하는 것이 바로 ObjectProvider이고, 이걸 상속받아 기능을 추가한 것이 ObjectProvider이다.

package hello.core.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

public class PrototypeProviderTest {

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

    static class ClientBean{
        @Autowired
        private ObjectProvider<PrototypeBean> prototypeBeanObjectProvider;
        
        public int logic(){
            PrototypeBean prototypeBean = prototypeBeanObjectProvider.getObject();
            prototypeBean.addCount();
            return prototypeBean.getCount();
        }
    }

    @Test
    void providerTest(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);

        ClientBean clientBean1 = applicationContext.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        Assertions.assertEquals(1, count1);

        ClientBean clientBean2 = applicationContext.getBean(ClientBean.class);
        int count2 = clientBean1.logic();
        Assertions.assertEquals(1, count2);
    }
}

ClientBean 클래스의 내용을 이렇게 변경하면 된다.

이렇게 다른 인스턴스를 가지는 것을 볼 수 있다.

ObjectProvider의 getObject()를 호출하면 스프링 컨테이너에서 해당 빈을 찾아서 반환한다.

 

여기에 추가로 자바 표준을 사용하는 방법이 있다.

 

JSP-330 Provider

스프링 부트 3.0.1을 사용하는 방법이다.

우선 gradle에

implementation 'jakarta.inject:jakarta.inject-api:2.0.1'

을 추가해준다.

 

그리고 Provider에 맞게 ClientBean의 코드를 수정해준다.

package hello.core.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.inject.Provider;
import org.junit.jupiter.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;

public class SingletonWithPrototypeTest1 {

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

    static class ClientBean{
        
        @Autowired
        private Provider<PrototypeBean> provider;
        
        public int logic(){
            PrototypeBean prototypeBean = provider.get();
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }

    @Test
    void prototypeFind(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(PrototypeBean.class);
        PrototypeBean prototypeBean1 = applicationContext.getBean(PrototypeBean.class);
        prototypeBean1.addCount();
        Assertions.assertEquals(1, prototypeBean1.getCount());

        PrototypeBean prototypeBean2 = applicationContext.getBean(PrototypeBean.class);
        prototypeBean2.addCount();
        Assertions.assertEquals(1, prototypeBean2.getCount());
    }

    @Test
    void singletonClientUsePrototype(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);

        ClientBean clientBean1 = applicationContext.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        Assertions.assertEquals(1, count1);

        ClientBean clientBean2 = applicationContext.getBean(ClientBean.class);
        int count2 = clientBean2.logic();
        Assertions.assertEquals(2, count2);
    }
}

둘 중에 상황에 맞는 DL을 사용하면 된다.

 

웹 스코프

지금까지는 싱글톤과 프로토타입 스코프를 학습했다.

 

이번에는 웹 스코프에 대해서도 알아보자.

웹 스코프는 웹 환경에서만 동작한다.

그리고 프로토타입과는 다르게 해당 스코프의 종료시점까지 관리해준다.

 

종류로는

request: HTTP 요청 하나가 들어오고 나갈 때까지 유지되는 스코프, 요청마다 별도의 인스턴스가 생성된다.

session: HTTP Session과 동일한 생명주기를 가지는 스코프

application: ServletContext와 동일한 생명주기를 가지는 스코프

 

바로 request 스코프 예제를 만들어보자.

웹 스코프는 웹 환경에서만 동작하기 때문에 해당 라이브러리를 추가해줘야 한다.

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

해당 라이브러리를 추가하면

http://localhost:8080/ 해당 페이지를 통해 접속할 수 있게 된다.

 

request 요청이 오면 로그를 남기는 예제를 만들어보자.

우선 로그를 출력하기 위한 클래스를 만든다.

package hello.core.web;

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

import java.util.UUID;

@Component
@Scope(value = "request")
public class MyLog {
    private String uuid;
    private String requestURL;
    
    public void setRequestURL(String requestURL){
        this.requestURL = requestURL;
    }
    
    public void log(String message){
        System.out.println("UUID: " + uuid + ", requestURL: " + requestURL + ", message: " + message);
    }
    
    @PostConstruct
    public void init(){
        uuid = UUID.randomUUID().toString();
        System.out.println("UUID: " + uuid + ", request scope bean create: " + this);
    }
    
    @PreDestroy
    public void close(){
        System.out.println("UUID: " + uuid + ", request scope bean close: " + this);
    }
}

Scope를 request로 지정해서, HTTP 요청 당 하나씩 생성이 되고, 요청이 끝나는 시점에 소멸이 된다.

 

이제 Controller를 만든다.

package hello.core.web;

import jakarta.servlet.http.HttpServletRequest;
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;

@Controller
@RequiredArgsConstructor
public class LogController {

    private final LogService logService;
    private final ObjectProvider<MyLog> myLogObjectProvider;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request){
        String requestURL = request.getRequestURL().toString();
        MyLog myLog = myLogObjectProvider.getObject();
        myLog.setRequestURL(requestURL);

        myLog.log("controller");
        logService.logic("testId");

        return "OK";
    }
}

request를 받아야 URL을 알 수 있기에 이 단계에서 URL을 넣어준다.

 

package hello.core.web;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogService {

    private final ObjectProvider<MyLog> myLogObjectProvider;

    public void logic(String id){
        MyLog myLog = myLogObjectProvider.getObject();
        myLog.log("service id = " + id);
    }
}

여기에서 ObjectProvider로 MyLog를 두개 생성하지만, 같은 HTTP 요청이기 때문에 동일한 빈이 들어간다.

ObjectProvider 덕분에 Object.getObject() 호출 전까지, request scope빈의 생성을 지연할 수 있다.

 

하지만 여기에서 더 나아가

 

스코프와 프록시

프록시 방식을 사용한다.

프록시 방식은

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

로 proxyMode를 추가하는게 핵심이다.

적용 대상이 class면 TARGET_CLASS, 적용 대상이 인터페이스면 INTERFACES를 선택한다.

이렇게 하면 MyLog의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 주입해 둘 수 있다.

 

 

이렇게 myLog를 출력해보면, 이 객체를 상속받은 다른 객체가 출력되는 것을 볼 수 있다.

'백엔드 > 스프링' 카테고리의 다른 글

스프링 14일차  (0) 2023.03.26
스프링 13일차  (0) 2023.03.25
스프링 11일차  (0) 2023.02.12
스프링 10일차  (0) 2023.02.11
스프링 9일차  (0) 2023.02.10

+ Recent posts