[스프링 핵심 원리] 빈 스코프

JUJU·2024년 3월 4일
1

Spring

목록 보기
9/21
본 포스트는 김영한 개발자님의 스프링 핵심 원리 강의를 듣고 정리한 것입니다.
※ 코드는 강의에서 사용된 것과 다릅니다.
jaewon-ju Github Address

✏️ 빈 스코프란?

빈 스코프는 빈이 존재할 수 있는 범위를 말한다.

스프링은 다양한 스코프를 지원한다.

스코프 이름존재 범위
싱글톤컨테이너 시작 ~ 종료
프로토타입컨테이너는 프로토타입 빈의 생성만 관여하고, 그 이후는 관리하지 않음
웹(request)웹 요청 받음 ~ 웹 요청 나감
웹(session)웹 세션 생성 ~ 종료

빈 스코프는 다음과 같이 지정할 수 있다.

// 컴포넌트 스캔
@Scope("prototype")
@Componenet
public class AutoAppConfig {}
// 수동
@Scope("prototype")
@Bean
public BoardPolicy boardPolicy(){
	return new ReadOnlyPolicy();
}



✏️ 프로토타입 스코프

싱글톤 스코프의 빈을 조회하면, 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다.
하지만, 프로토타입 스코프의 빈을 조회하면 항상 새로운 인스턴스의 빈을 반환한다.

  • 싱글톤 스코프의 빈은 컨테이너가 생성될 때 같이 생성되지만, 프로토타입 스코프의 빈은 요청을 받을 때마다 생성된다.
  • 컨테이너는 생성된 빈에 의존관계를 주입한 후 반환한다.
  • 반환한 빈은 더이상 관리하지 않는다.
  • 프로토타입 빈을 관리할 책임은 클라이언트에 있다.

컨테이너: 프로토타입 빈의 생성 및 초기화 담당
클라이언트: 관리 및 종료 담당


⚠️ 싱글톤과 함께 사용할 때 문제점

싱글톤 빈이 프로토타입 빈에 의존한다면 문제가 발생할 수 있다.
다음과 같은 상황을 보자.

프로토타입 빈은 addOne()이라는 메소드를 가지고 있다.

@Scope("prototype")
static class PrototypeBean {
    private static int num = 0;

    public static int getNum() {
        return num;
    }

    public static void addOne() {
        num++;
    }
}

클라이언트 A가 프로토타입 빈을 컨테이너로부터 받은 뒤, addOne()을 호출한다.
결과는 1이다.


그 다음에 클라이언트 B가 프로토타입 빈을 컨테이너로부터 받은 뒤, addOne()을 호출한다.
프로토타입 빈은 요청받을 때마다 새롭게 생성되기 때문에 결과는 1이다.


문제는 싱글톤 빈이 의존관계 주입을 통해서 프로토타입 빈을 주입받을 때 발생한다.

SingletonBean 은 PrototypeBean을 주입받는다.

@Scope("singleton") // 생략가능
static class SingletonBean {
    private final PrototypeBean prototypeBean;

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

    public int logic() {
        PrototypeBean.addOne();
        return PrototypeBean.getNum();
    }
}

SingletonBean은 컨테이너가 생성될 때 함께 생성되고 의존관계 주입도 발생한다.
스프링 컨테이너는 프로토타입 빈을 생성하여 SingletonBean에 주입한다.

클라이언트 A가 SingletonBean을 받아서 logic() 메소드를 호출한다.
1이 리턴된다.


그 다음에 클라이언트 B가 SingletonBean을 받아서 logic() 메소드를 호출한다.
2가 리턴된다.

SingletonBean 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다.
따라서, 클라이언트 A와 B는 동일한 프로토타입 빈의 메소드를 사용하게 된다!

개발자가 의도한 것은 이것이 아닐것이다.
해결방법은 여러가지가 있다.


1. logic() 메소드 안에서 프로토타입 빈 주입
2. ObjectFactory, ObjectProvider
3. JSR-330 Provider




✏️ 가장 간단한 방법

가장 간단한 방법은 싱글톤 빈이 프로토타입 빈을 사용할 때 마다 스프링 컨테이너에 새로 요청하는 것이다.

@Autowired
private ApplicationContext ac;
// 컨테이너 자체를 주입받음

public int logic(){
	PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
	// logic 메소드를 실행할 때마다 새로운 빈을 가져옴
    
	prototypeBean.addOne();
   	return prototypeBean.getNum();
}

의존관계를 외부에서 주입(Dependency Injection) 받는게 아니라, 필요한 의존관계를 직접 찾는 것을 의존관계 조회(Dependency Lookup) 라고 한다.

위의 방법은 간단하지만, 단점도 존재한다.
컨테이너 자체를 DI 하면 스프링 컨테이너에 종속적인 코드가 되어버린다.
DL 정도의 기능만 제공하는 무언가가 필요하다.




✏️ ObjectFactory, ObjectProvider

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

ObjectFactory는 ObjectProvider에서 편의 기능을 추가한 것이다.

@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
// 스프링이 자동으로 ObjectProvider를 주입해줌

public int logic() {
     PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
     // DI
     
     prototypeBean.addOne();
   	 return prototypeBean.getNum();
}

  • ObjectProvider에서 PrototypeBean 타입의 빈을 찾을 때마다 새로운 빈을 생성해서 반환해준다.
  • 별도의 라이브러리가 필요없지만 스프링에 의존한다는 단점이 있다.



✏️ JSR-330 Provider

마지막 방법은 JSR-330 자바 표준을 사용하는 방법이다.

JSR-330 Provider는 자바 표준이기 때문에 스프링에 의존적이지 않고, 아주 단순한 기능(DL)만 제공한다.

@Autowired
private Provider<PrototypeBean> provider;

public int logic() {
     PrototypeBean prototypeBean = provider.get();
     prototypeBean.addOne();
   	 return prototypeBean.getNum();
}

  • Provider는 get() 메소드 하나만 가지고 있기 때문에 매우 단순하다.
  • 별도의 라이브러리 javax.inject 패키지가 필요하다.
  • 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.



✏️ 웹 스코프

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

웹 스코프의 종류는 다음과 같다.

웹 스코프존재 범위
requestHTTP 요청 받음 ~ HTTP 요청 나감
sessionHTTP session와 동일한 생명 주기
applicationServletContext와 동일한 생명 주기
websocket웹 소켓과 동일한 생명 주기

대표적인 웹 스코프인 request에 대해서 알아보도록 하자.




✏️ request

클라이언트가 특정 url을 요청하면 LogController 클래스가 해당 요청을 받는다.
LogController 클래스는 MyLog 객체를 활용해서 클라이언트의 로그를 출력하고, LogService 객체를 활용해서 서비스 계층에서 로그를 출력한다.

다음의 로직을 한번 살펴보자.

  1. 클라이언트 A가 http://localhost:8080/server 를 요청한다.
  2. LogController 가 해당 요청을 받아서 URL을 MyLog 객체에 저장해둔다.
  3. LogController 가 로그를 남긴다.
  4. LogService 의 로직을 호출해서, 서비스 계층에서도 로그를 출력한다.

요청된 URL 저장 및 로그 출력을 하는 MyLog 클래스

@Component
@Scope(value = "request")
public class MyLog {
    private String uuid;
    private String request;

    public void setRequest(String request) {
        this.request = request;
    }

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

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

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

비즈니스 로직을 실행하는 LogService 클래스

@Service
@RequiredArgsConstructor
public class LogService {
    private final MyLog myLog;
    // MyLog 타입의 빈이 여러개 있어도, 스프링이 각 클라이언트에 맞는 MyLog 빈을 넣어줌

    public void logic(String testId) {
        myLog.printLog(testId);
    }
}

클라이언트로부터 받은 요청을 처리하는 LogController 클래스

@Controller
@RequiredArgsConstructor
public class LogController {
    private final MyLog myLog;
    private final LogService logService;

    @RequestMapping("server") // server라는 요청이 오면 실행되는 내용
    @ResponseBody
    public String requestLogic(HttpServletRequest request) {
        String requestURL = request.getRequestURL().toString();
        myLog.setRequest(requestURL);

        myLog.printLog("log from LogController class");
        logService.logic("testId");
        return "Test Pass";
    }
}

⚠️ 문제점 발생

위의 코드를 실행하면, 오류가 발생한다. 그 이유는 무엇일까?

애플리케이션이 실행되면, 스프링은 컨테이너를 생성하고 스프링 빈을 등록한다.
각 스프링 빈을 등록한 후에는 의존관계 주입을 한다.

LogController 클래스의 의존관계를 한번 보자.

public class LogController {
    private final MyLog myLog;
    private final LogService logService;
    ...
}

LogController 클래스는 MyLog 클래스에 의존한다.
하지만, MyLog 클래스는 request 스코프를 가지고 있다.
따라서, 클라이언트의 요청이 오기전까지 빈은 생성되지 않는다!!

해결방안은 2가지가 존재한다.

  1. Provider
  2. 프록시



✏️ Provider

이전에 배운 ObjectProvider, JSR-330 Provider 등은, 프로토타입 스코프 뿐만 아니라 DL이 필요한 경우는 언제든지 사용할 수 있다.

ObjectProvider를 사용해서 request 스코프에서 발생한 문제를 해결해보자.
앞선 문제점은 생성되지 않은 빈을 주입하려 했기 때문에 발생했다.

따라서, 빈 대신에 Provider를 주입한 뒤 클라이언트의 요청이 오면 Provider에서 DL을 통해 request 스코프의 빈을 사용하면 된다.

@Controller
@RequiredArgsConstructor
public class LogController {
    private final ObjectProvider<MyLog> objectProvider; // 변경된 부분
    private final LogService logService;

    @RequestMapping("server")
    @ResponseBody
    public String requestLogic(HttpServletRequest request) {
        String requestURL = request.getRequestURL().toString();
        MyLog myLog = objectProvider.getObject(); // 변경된 부분

        myLog.printLog("log from LogController class");
        logService.logic("testId");
        return "Test Pass";
    }
}
@Service
@RequiredArgsConstructor
public class LogService {
    private final ObjectProvider<MyLog> objectProvider; // 변경된 부분

    public void logic(String testId) {
        MyLog myLog = objectProvider.getObject(); // 변경된 부분
        myLog.printLog(testId);
    }
}

코드를 작성하다가 getObject() 메소드의 파라미터가 없다는게 이상해 보였다.
MyLog 타입의 빈이 여러개가 있다면 getObject()로 클라이언트에 맞는 빈을 꺼내야 할텐데...


이것에 대한 답은 다음 게시물에서 확인할 수 있었다.
인프런 게시물


Request 스코프의 빈은 클라이언트의 요청이 발생할 때 생성된다.
각 요청은 Request 스코프의 빈을 가지고 다닌다고 보면 된다.
해당 요청에서 getObject() 메소드를 호출하면 클라이언트 간의 요청을 구분할 필요 없이, 자신의 요청의 Attributes 중에서 MyLog 타입의 빈을 꺼내는 것이다.

위의 코드는 오류없이 잘 동작하는 것을 확인할 수 있다.

  • ObjectProvider는 getObject() 메소드를 호출하는 시점까지 request scope 빈의 생성을 지연한다.



✏️ 프록시

이전의 문제점을 다시 한번 생각해보자.
앞선 문제점은 생성되지 않은 빈을 주입하려 했기 때문에 발생했다.

그럼 가짜 객체를 주입한 후에 요청이 오면 그때 내부에서 진짜 빈을 요청하도록 만들면 되지 않을까?

이 가짜 객체가 바로 프록시이다.

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
// 프록시 객체의 사용 방법
public class MyLog { ... }

// 원본 코드
@Controller
@RequiredArgsConstructor
public class LogController {
    private final MyLog myLog;
    private final LogService logService;

    @RequestMapping("server")
    @ResponseBody
    public String requestLogic(HttpServletRequest request) {
        String requestURL = request.getRequestURL().toString();
        myLog.setRequest(requestURL);

        myLog.printLog("log from LogController class");
        logService.logic("testId");
        return "Test Pass";
    }
}

  • 클라이언트가 myLog.printLog() 메소드를 호출하면 가짜 프록시 객체인 myLog는 request 스코프의 진짜 메소드를 호출한다.
  • 프록시 빈은 싱글톤처럼 동작한다.

Provider나 프록시의 핵심은 객체 조회의 "지연처리"이다.




REFERENCE

스프링 핵심 원리 - 김영한 개발자님

스프링 사용이유 -> DI(의존성 주입)
빈을 싱글톤으로 유지시켜준다.

profile
개발자 지망생

0개의 댓글

관련 채용 정보