스프링은 스프링 컨테이너 ( Ioc 컨테이너 , DI 컨테이너라고도 불린다. ) 를 통해 객체를 빈에 등록하고 생명주기를 관리한다.
때문에 비즈니스 로직에 맞게 알맞은 생명주기를 선택해 적용하는 것은 매우 중요하다.
어떻게 빈을 생성하고 관리하고 소멸할지에 대해서는 백엔드 개발자에게 매우 민감하고 중요한 문제이기 때문에 스프링의 빈 관리 특징들을 이해해보자.
싱글톤
스프링의 기본 디자인패턴은 싱글톤이다.
싱글톤 디자인 패턴은 스프링 애플리케이션의 시작부터 종료까지로 빈의 생명주기를 관리한다.
즉 애플리케이션이 실행될 때 스프링 컨테이너에서 해당 빈 객체들을 생성하고 의존관계를 맺어준다. 또한 애플리케이션이 종료될 때 빈 객체의 소멸 메소드를 실행해 빈 객체의 소멸까지 보장해준다.
또한 싱글톤 디자인 패턴은 클라이언트의 요청이 올 때마다 매번 빈 객체를 생성하고 의존관계를 맺어주는 것이 아닌, 기존의 컨테이너 내에 있는 해당 빈 객체를 가져다가 사용한다.
때문에 이전 포스팅에서도 설명했지만, 멀티쓰레드 환경에서 동기화를 신경써주어야 한다.
가장 손쉽고 효과가 좋은 방법은 빈 객체에서 멤버변수를 사용하지 않는 것이다.
멤버변수를 사용하지 않고, 지역변수 혹은 매개변수를 사용하여 비즈니스 로직을 태우게 되면 해당 변수는 다른 요청과 공유되지 않기 때문에 데이터 일관성이 지켜질 수 있을 것이다.
프로토타입
프로토타입 빈 스코프란 애플리케이션을 실행하는 시기가 아닌 해당 객체가 호출될 때마다 매번 생성하고 의존관계 주입하는 스코프를 의미한다.
스프링 컨테이너에서는 빈 객체를 생성해줌과 동시에 의존관계를 맺는다. 그 후 해당 객체를 반환한 후 더이상 컨테이너에서 관리하지 않는다. 때문에 빈 객체에 대해서 소멸을 보장해주지 않고, 클라이언트가 직접 소멸 메소드를 호출해야 하는 번거로움이 있다.
비즈니스 로직상 반드시 필요한 경우가 아니면 많이 사용하지 않는다.
또한 요청마다 독립적인 빈 객체가 필요하다고 한다면 프로토타입 스코프가 아닌 request 스코프를 많이 사용한다.
문제점
여기서 중요한 점은, 싱글톤 빈과 프로토타입 빈을 함께 사용할 때이다.
비즈니스 로직상 반드시 프로토타입 스코프를 사용해야할 때가 올 수 있고, 모든 빈 객체를 프로토타입으로 관리하지 않기 때문에 싱글톤 패턴과 함께 사용할 경우가 생길 수 있다. 이 때 의도와는 다르게 프로토타입 스코프로 지정한 빈 객체가 마치 싱글톤 스코프처럼 동작하는 경우가 생긴다.
처음 스프링 애플리케이션을 실행하게되면 스프링 컨테이너가 싱글톤 빈을 생성하고 관리한다.
싱글톤 빈을 생성할 때 의존관계 주입도 함께 해주기 때문에, 프로로타입 스포크 빈과 의존관계를 맺어야 한다면 스프링 컨테이너는 프로토타입 빈을 생성한 후, 싱글톤 빈에게 반환해 의존관계를 맺어준다.
싱글톤 패턴은 모든 요청에 같은 객체를 반환하기 때문에 싱글톤 객체가 다루는 프로토타입 빈 또한 같은 객체가 반환하게 된다.
즉 다른 요청이 들어왔다고 해서, 프로토타입 빈을 두번 생성하는것이 아닌, 기존에 의존관계 맺어있던 싱글톤 빈의 프로토타입 빈을 반환하기 때문에 의도대로 동작하지 않는다.
그럼 생성하고 싶은 순간에 스프링 컨테이너에게 프로로타입 빈을 요청해 사용하는 방법은 뭘까 ?
ObjectProvider 를 사용하는 것이다. ObjectProvider 는 DL 서비스를 제공한다.
DL 서비스란 의존관계를 주입하는 방식이 아닌 필요한 빈 객체를 스프링 컨테이너에서 탐색하는 서비스이다.
ObjectProvider<PrototypeBean> provider 키워드를 통해 ObjectProvider 객체를 생성한 후 provider.getObject() 메소드를 호출하면 호출 할 때 스프링컨테이너에게 해당 프로토타입 빈을 요청할 수 있다.
때문에 싱글톤 객체를 생성할 때 자동으로 프로토타입 빈 객체를 생성해 주입하는 방식이 아닌 필요할 때마다 직접 컨테이너에서 프로토타입 빈 객체를 찾기 때문에 프로토타입 스코프를 유지할 수 있다.
ObjectProvider은 단순히 프로토타입 빈을 새로 생성하고 싶을 때 사용되는 건 아니고, 내부에서 스프링 컨테니어를 통해 해당 빈을 찾고 싶을 때 사용한다.
Request
웹관련 스코프이다. 때문에 springBoot web starter 패키지를 함께 설치해야지 사용가능하다.
Request 스코프는 말 그대로 하나의 http 요청이 들어올 때, 스프링 컨테이너에서 해당 스코프 객체를 생성해서 관리한다.
한 요청 아래 여러번 호출해도, 하나의 싱글톤 객체로 관리한다.
요청이 끝나 클라이언트에게 응답값을 반환하게 되면 컨테이너가 destroy 메소드를 호출하여 객체를 소멸한다.
보편적으로 하나의 요청에서 함께 관리하는 Log 객체에서 활용한다.
로그는 클라이언트마다 관리하는게 편하다. 각각의 요청마다 요청 URL, UserId 등이 포함되는데 request 스코프가 요청에 대한 생성주기를 가지기 때문에 유용하다.
그러나 요청이 들어올 때 생성되기 때문에, 싱글톤 패턴 객체가 애플리케이션을 실행해 생성됨과 동시에 request 스코프로 정의한 빈 객체와 의존관계를 주입하려고 하면 에러가 발생한다.
요청이 들어오지 않았기 때문에 request 스코프 빈은 생성되지 않았고, 해당하는 빈을 컨테이너에서 찾을 수 없기 때문이다.
이를 해결하기 위해 역시 ObjectProvider 를 사용한다.
ObjectProvider 를 통해 생성하게 되면, getObject() 메소드를 통해 필요할 때 컨테이너에서 찾아서 주입할 수 있다.
추가로 ObjectProvider를 사용하게 되면 서비스 로직과, 요청에 대한 로그 설정 로직을 구분할 수 있어서 좋다. 역할을 분리하기 떄문에 유지보수하기 편하다.
추가로 ObjectProvider가 아닌 프록시 객체를 통해서도 주입할 수 있다.
싱글톤 빈이 생성되고 의존관계를 맺을 때, 어노테이션을 사용해 프록시 설정하게 되면 실제 request 스코프 객체가 아닌 바이트코드 CGLIB 라이브러리를 통해 프록시 객체를 만든 후 빈으로 등록한다.
이 때 프록시 객체는 싱글톤으로 관리되기 때문에, 애플리케이션을 실행할 때 생성한다. 또한 그저 껍데기 객체이기 때문에 멤버 변수로 선언해도 된다. 생성하는 시기가 같기 때문에 의존관계 주입도 문제없이 이뤄진다.
그럼 프록시 객체는 어떤 원리로 사용될까 ?
실제 요청이 들어오면, 스프링 컨테이너는 요청마다 프록시 객체를 상속 받은 request scope 객체를 생성해 관리한다.
이는 프록시 객체가 실제 객체의 메소드를 실행하는 역할 하기 때문에 가능하다.
때문에 외부적으로 봤을 때는 마치 request scope 객체가 싱글톤처럼 보일 수 있지만, 각 요청마다 생성된다는 것을 확인해야한다.
특별한 케이스기도 하고, 디버깅이 어렵기 때문에 이 역시 반드시 필요할 때에만 사용해야한다.
웹관련 스코프는 request 이외에도 session(보통 로그인), application 이 존재한다.
'개인 공부 > Spring' 카테고리의 다른 글
[Spring] Redis Cache 사용하기 (0) | 2023.03.10 |
---|---|
[Spring] AWS S3 Bucket 에 이미지 등록하기 (0) | 2023.02.28 |
[Spring] Multi Thread (2) | 2022.11.21 |
[Spring] HashMap 으로 Cache 구현하기 (0) | 2022.11.14 |
[Spring] Redis vs EHcache vs HashMap (0) | 2022.11.09 |