스프링은 컨테이너에 스프링 빈을 등록할 때 싱글톤으로 등록한다.
이 방법은 @ComponentScan 어노테이션을 이용해서 스프링 컨테이너에 의해 @Component 및 @Service, @Repository, @Controller 등 부여된 Class 들을 스캔해서 자동으로 생성되어 스프링 Bean으로 등록합니다.
위 그림과 같이 @Service도 내부적으론 @Component 어노테이션을 사용합니다.
그래서 빈으로 등록이 되는것입니다.
@Configuration
public class SampleConfiguration {
@Bean
public SampleController sampleController() {
return new SampleController;
}
}
**proxyBeanMethods**
빈에 대한 프록시 객체를 생성할 지 여부를 결정한다.
디폴트값은 true, : 빈에 대한 프록시 객체가 생성된다.
proxyBeanMethods = true일때 config 빈의 상태
proxyBeanMethods = false일때 config 빈의 상태
프록시 객체로 생성된 빈의 클래스 이름 보면 $$EnhancerBySpringCGLIB&& 라는게 추가된걸 볼수 있다.
❓ 참고로 CGLIB는 바이트 코드를 가지고 프록시 객체를 만들어주는 라이브러리다. 런타임 시에 자바 클래스를 상속하고 인터페이스를 구현해 동적 프록시 객체를 만든다.즉 디폴트 상태의 config bean은 우리가 직접 생성한 객체가 아니라 CGLIB 라이브러리에서 생성해준 프록시 객체임을 알수 있다.
프록시 객체를 생성하는 이유가 뭘까?
public class Resource{
}
위의 클래스를 스프링 빈으로 등록하고자 할때 @Component를 이용해 자동으로 빈 등록 하면 스프링이 알아서 객체를 제어하는데,
@Bean을 이용해 직접 빈으로 등록 해준다면
public class Resource{
@Bean
public Resource Resource() {
return new Resource();
}
@Bean
public MyFirstBean myFirstBean() {
return new MyFirstBean(mangKyuResource());
}
@Bean
public MySecondBean mySecondBean() {
return new MySecondBean(mangKyuResource());
}
}
실수로 빈을 생성하는 메소드를 여러 번 호출 했을때 여러개의 빈이 생성이 된다. 그래서 스프링은 이런 문제를 방지하고자 @Configuration이 있는 클래스를 객체로 생성할 때 CGLib 라이브러리를 사용해 프록시 패턴을 적용해 싱글톤을 보장하는것입니다.
Bean Lite Mode는 CGLIB를 이용해 바이트 코드 조작을 하지 않는 방식을 의미합니다. 즉 스프링의 싱글톤을 보장하지 않는겁니다.
@Component //@Comfiguration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
설정하는 방법은 @Configuration이 아닌 @Conponent로 변경하면 됩니다. 이렇게 lite mode로 spring bean을 생성합니다. → 정확하게 표현하면 objectMapperLiteBean메소드가 lite mode로 작동한다고 해야합니다.
원래 코드
@Configuration
public class BeanConfig1 {
@Bean
public ObjectMapper objectMapperBean() {
return new ObjectMapper();
}
@Bean
public ObjectMapper anyObjectMapperBean() {
return objectMapperBean();
}
}
밑에 anyObjectMapperBean 메소드를 작동하면 내부에 만들어 놓은 spring bean을 리턴 하기에 ObjectMapper 객체를 생성하는 objectMapperBean() 메소드는 1번만 실행한다.
여기서 Lite mocde
@Component
public class BeanConfig1 {
@Bean
public ObjectMapper objectMapperBean() {
return new ObjectMapper();
}
@Bean
public ObjectMapper anyObjectMapperBean() {
return objectMapperBean();
}
}
이렇게 되면 anyobjectMapperLiteBean() 호출 해서 objectMapperLiteBean() 를 호출하면 진짜 메서드를 호출하여 ObjectMapper 객체를 하나 더 만들게 된다. 즉 ObjectMapper 객체가 2개가 된다.
LiteMode를 쓰는 이유는
프록시 객체를 동적으로 생성하게 되고 이 객체는 메소드에 대한 요청을 가로채게 된다. 그래서 @Component로 설정 클래스를 만드면 이 클래스는 순수 객체로 만들어져서 메소드를 호출하게 됐을때 가로채지 않고 메소드가 처리 되는 것입니다.
먼저 콜백이란것은, 주로 콜백 함수를 부를때 사용되는 용어이며, 콜백 함수를 등록하면 특정 이벤트가 발생했을때 해당 메소드가 호출되는것입니다.
즉, 조건에 따라 실행 유무 개념이라고 보면 됩니다.
데이터베이스 커넥션 풀(DB 연결)이나, 네트워크 소켓 연결 처럼 애플리케이션 시작 시점에 필요한 연결을 미리 한뒤 , 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면, 객체의 초기화 및 종료 작업이 필요합니다.
(커넥션 풀의 connect,disconnect)
❓ 데이터 베이스 커넥션 풀이란? - 데이터 베이스와 연결된 커넥션을 미리 만들어 놓고 이를 pool로 관리하는것이다. 장점으로는 Connection에 필요한 비용을 줄여 DB에 빠르게 접속할 수 있습니다, 또한 커넥션 수를 제한할 수 있어서 과도한 접속으로 인한 서버 자원 고갈을 방지할 수 있으며 DB 접속 모듈을 공통화해 DB 서버의 환경이 바뀔 경우 유지보수를 쉽게 할 수 있다. - 커넥션 비용이란? 서버와 DB 사이의 연결하는 비용.스프링 빈도 위와 같은 원리로 초기화 작업과 종료 작업을 나눠서 진행합니다.
간단하게 객체 생성 → 의존관계 주입 이라는 라이프 사이클을 가집니다.
즉, 스프링 빈은 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 준비가 완료됩니다.
가장 먼저 Spring IoC 컨테이너가 만들어지고, 빈들이 등록되는 과정입니다.
@Configuration 방법으로 Bean으로 등록할 수 있는 어노테이션들과 설정파일들을 읽어 IoC컨테이너에 Bean들을 등록시킨다.
의존 관계 주입 하기전에 준비 단계가 있습니다.
이 단계에서 객체 생성이 일어나는데,주입 종류에 따라 다른 부분이 있습니다.
생성자 주입은 객체 생성과 의존관계 주입이 동시에 일어나고,
Setter주입, 필드 주입은 객체 생성되고 그다음 의존관계 주입으로 라이프 사이클이 나누어져 있습니다.
왜? 생성자 주입은 동시에 일어날까?
예를 들어 MemberController가 있으면
@Controller
public class MemberController {
private final CocoService cocoService;
public MemberController(CocoService cocoService) {
this.cocoService = cocoService;
}
}
public class Main {
public static void main(String[] args) {
// MemberControllercontroller = new MemberController(); // 컴파일 에러
MemberController controller1 = new MemberController(new CocoService());
}
}
자바에선 new 연산자를 호출하면 생성자가 호출됩니다.
의존관계가 존재 하지 않는다면 Controller 클래스는 객체 생성이 불가능 하기 때문에 생성자 주입에서는 객체 생성, 의존관계 주입이 하나의 단계에서 일어나는것입니다.
스프링 컨테이너는 설정 정보를 참고해 의존관계를 주입한다.
먼저 스프링 Bean의 LifeCycle을 보면
스프링 IoC 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 메소드 호출 → 사용 → 소멸전 콜백 메소드 호출 → 스프링 종료
이렇게 스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메소드를 통해 초기화 시점을 알려주며,
스프링 컨테이너가 종료되기 직전에도 소멸 콜백 메소드를 통해 소멸 시점을 알려준다.
여기서 왜 객체 생성과 초기화를 분리하는 이유는
생성자는 파라미터를 받고, 메모리를 할당 해서 객체를 생성하는 역할이고,
초기화는 이렇게 생성된 값들을 활용해 외부 커넥션을 연결하는등 무거운 동작을 수행한다.
그렇기 때문에 생성자 안에서 무거운 동작들을 같이 하는것 보다 명확하게 나누는것이 유지보수 관점에서 좋다. 물론 단순한 초기화 작업(내부 값들을 살짝 변경하는 작업)은 한번에 처리하는게 더 낫다.
public class ExampleBean implements InitializingBean, DisposableBean {
@Override
public void afterPropertiesSet() throws Exception {
// 초기화 콜백 (의존관계 주입이 끝나면 호출)
System.out.println("시작한다~");
}
@Override
public void destroy() throws Exception {
// 소멸 전 콜백 (메모리 반납, 연결 종료와 같은 과정)
System.out.println("끝났다!~");
}
}
단점으로는
public class ExampleBean {
public void initialize() throws Exception {
// 초기화 콜백 (의존관계 주입이 끝나면 호출)
System.out.println("시작한다~");
}
public void close() throws Exception {
// 소멸 전 콜백 (메모리 반납, 연결 종료와 같은 과정)
System.out.println("끝~");
}
}
@Configuration
class LifeCycleConfig {
@Bean(initMethod = "initialize", destroyMethod = "close")
public ExampleBean exampleBean() {
// ~~~~~~
}
}
이 방식의 장단점
@Bean의 destoryMethod 속성의 특징
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
public class ExampleBean {
@PostConstruct
public void initialize() throws Exception {
// 초기화 콜백 (의존관계 주입이 끝나면 호출)
}
@PreDestroy
public void close() throws Exception {
// 소멸 전 콜백 (메모리 반납, 연결 종료와 같은 과정)
}
}
이렇게 편리하게 초기화와 종료를 실행할 수 있다.
장단점
@PostConstruct, @PreDestroy 어노테이션 쓰자
외부 라이브러리를 초기화, 종료 해야하면 @Bean의 initMethod,destroyMethod를 사용하자.
스코프 지정하는 방법은 @Scope(”종류”)로 지정한다
@Scope("prototype")
@Component
public class Bean {}//자동 등록
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {//수동 등록
return new HelloBean();
}
이렇게 clientBean안에 prototypeBean을 포함하면 스프링 컨테이너 생성시점에 함께 생성되고, DI도 발생한다.
클라이언트 A가 clientBean.logic()을 호출 하면 clientBean은 prototypeBean의 addCount()를 호출해서 프로토타입 빈의 count를 증가시킨다. count값이 1이된다.
클라이언트 B도 logic()을 호출하면 프로토타입 빈이 컨테이너에서 소멸되지 않고, 해당 프로토 타입 인스턴스를 가지고 있게 된다. 싱글톤 빈안에서는 다른 결과를 가져온다.
javax.inject.Provider이라는 JSR-330 자바 표준을 사용하는 방법.
이 방법을 사용할려면 스프링 부트 3.0 미만 'javax.inject:javax.inject:1' 라이브러리를 스프링 부트 3.0 이상은 jakarta.inject:jakarta.inject-api:2.0.1 를gradle에 추가하면된다.
dependencies {
implementation 'javax.inject:javax.inject:1'
...
}
//스프링 부트 3.0 미만
public interface Provider<T> {
T get();
}
//스프링 부트 3.0
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
logic() 메소드를 호출할 때 마다 다른 PrototypeBean 인스턴스가 호출된다.
Provider는 자바 표준이라서 스프링에 독립적이라는 장점이있다.
@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ProtoType {
}
@Component
@AllArgsConstructor
public class ScopeWrapper {
...
@Getter
ProtoProxy protoProxy;
}
Protytpe에 proxyMode 설정을 추가한다. 프록시 적용 대상이 클래스면 TARGET_CLASS, 인터페이스면 INTERFACE를 선택한다.
웹 환경에서만 동작하는 스코프이며, 프로토 타입과 달리 특정 주기가 끝날 때까지 관리 해준다. 그래서 소멸 콜백 메소드가 호출된다.
종류는
이 나머지도 범위만 다르지 동작 방식은 비슷하다고 합니다.
동작하는 방식
웹 스코프를 사용을 위한 라이브러리 추가
implementation : 'org.springframework.boot:spring-boot-starter-web'
예를 들어 우리가 MyLogger 이라는 로그 찍는 클래스를 Request Scope로 등록했고 , A가 요청을 보냈다고 가정을 하면
컨트롤러에서 myLogger 객체를 요청 받았다면, 스프링 컨테이너는 A 전용으로 사용할 수 있는 빈을 생성하여 컨트롤러에 주입해 준다.
로직이 진행되면서 서비스에서 다시 myLogger 객체가 필요해서 요청을 하게 되면 방금 A 전용으로 생성했던 빈을 그대로 활용해서 주입받을 수 있다. 이후 요청이 끝나면 Request 빈은 소멸된다.
만약 다른 클라이언트 B가 A와 동시에 요청을 보낸다면,
클라이언트 B도 역시 컨트롤러와 서비스에서 각각 myLogger 객체가 필요한데, 이 때는 클라이언트 A에게 주입해 주었던 빈이 아닌 새로 생성해서 주게 된다. 따라서 Request Scope를 활용하면 디버깅하기 쉬운 로그 환경을 만들 수 있다.
싱글 thread에서 한개의 thread가 객체를 쓰지만 멀티 Thread 환경에서는 Thread들이 객체를 공유해서 작업해야 하는 경우가 있다. 이렇게 공유 자원으로 쓰이는 영역을 쓰레드가 동시에 접근하면 안되는 영역을 임계 영역 이라고 한다. 이문제를 해결 하기 위해 나온 개념이 세마포어(Semaphore), 상호 배제(Mutex)등이 있습니다.
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
싱글톤 패턴은 인스턴스가 한번 초기화 하면 애플리케이션이 종료될때 까지 메모리에 있다. 만약 싱글톤이 상태를 갖게 되면 멀티 스레드 환경에서 동기화 문제가 발생한다.
그런데 우리가 쓰는 싱글톤 빈은 사용할때 static변수, private 생성자, static 메소드를 정의하지 않고 싱글톤으로 쓴다.
그래서 싱글톤 빈은 상태를 가져도 Thread-safe(동기화)할것이라고 착각을 하는 경우가 있는데
정리 하자면 스프링은 싱글톤 레지스트리(자세한건 밑에서 )를 통해 private 생성자, static 변수 등의 코드 없이 비즈니스 로직에 집중하고 테스트 코드에 용이한 싱글톤 객체를 제공해 주는 것 뿐이지, 동기화 문제는 개발자가 처리해야 한다. 그래서 여러 쓰레드가 동시에 이 인스턴스를 접근할 경우(멀티 쓰레드 환경) 이슈가 발생합니다.
이 객체로 멀티 쓰레드 환경 이슈를 해결 할수 있습니다.
ExampleService.class
@Slf4j
public class ExampleService {
private Integer numberStorage;
public Integer storeNumber(Integer number) {
log.info("저장할 번호: {}, 기존에 저장된 번호: {}", number, numberStorage);
numberStorage = number;
sleep(1000); // 1초 대기
log.info("저장된 번호 조회: {}", numberStorage);
return numberStorage;
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
ExampleServiceTest.class
@Slf4j
public class ExampleServiceTest {
private ExampleService exampleService = new ExampleService();
@Test
void field() {
log.info("main start");
Runnable storeOne = () -> {
exampleService.storeNumber(1);
};
Runnable storeTwo = () -> {
exampleService.storeNumber(2);
};
Thread threadA = new Thread(storeOne);
threadA.setName("thread-1");
Thread threadB = new Thread(storeTwo);
threadB.setName("thread-2");
threadA.start();
sleep(100); // 동시성 문제 발생
threadB.start();
sleep(3000); // 메인 쓰레드 종료 대기
log.info("main exit");
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
실행 했을때
ThreadLocalExampleService.class
@Slf4j
public class ThreadLocalExampleService {
private ThreadLocal<Integer> numberStorage = new ThreadLocal<>();
public Integer storeNumber(Integer number) {
log.info("저장할 번호: {}, 기존에 저장된 번호: {}", number, numberStorage.get());
numberStorage.set(number);
sleep(1000); // 1초 대기
log.info("저장된 번호 조회: {}", numberStorage.get());
return numberStorage.get();
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
ThreadLocalExampleServiceTest.class
@Slf4j
public class ThreadLocalExampleServiceTest {
private ThreadLocalExampleService exampleService = new ThreadLocalExampleService();
@Test
void field() {
log.info("main start");
Runnable storeOne = () -> {
exampleService.storeNumber(1);
};
Runnable storeTwo = () -> {
exampleService.storeNumber(2);
};
Thread threadA = new Thread(storeOne);
threadA.setName("thread-1");
Thread threadB = new Thread(storeTwo);
threadB.setName("thread-2");
threadA.start();
sleep(100); // 동시성 문제 발생
threadB.start();
sleep(3000); // 메인 쓰레드 종료 대기
log.info("main exit");
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
실행했을때
동시성 이슈를 해결 완료
ex)@applicationContext
스프링 싱글톤 레지스트리에서 다루는 싱글톤은 일반적인 싱글톤 디자인 패턴과 다른 방법으로 구현된다. 일반 싱글톤 디자인 패턴에서의 많은 단점을 해소한 버전이다.
스프링 프레임워크가 동작하는 환경이 대부분 서버환경인데,
서버는 수많은 오브젝트를 이용해서 사용자의 요청을 처리해줍니다. 요청을 초당 수백번 씩 받아야하는 경우도 있고, 대규모 시스템은 더 심한 경우도 있다. 이럴때 마다 매번 오브젝트를 새로 만들면서 사용하면 비용이 너무 많이 들기때문에,
서블릿(서비스 오브젝트)를 사용하는데 이게 대부분 멀티스레드 환경에서 싱글톤으로 동작합니다. 그래서 서버환경에선 싱글톤 사용이 권장됩니다.
싱글톤 패턴을 구현하는 방법은
public class UserSingleton {
private static UserSingleton INSTANCE;
private UserSingleton () {
}
public static synchronized UserSingleton getInstance() {
if(INSTANCE == null) INSTANCE = new UserSingleton ();
return INSTANCE;
}
}
위 코드는 싱글톤 구현의 예입니다.
싱글톤의 단점은
private 생성자로 인해 상속이 불가능합니다.
싱글톤 패턴은 테스트 하기 힘듭니다.
서버환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못한다.
싱글톤의 사용은 전역 상태를 만들 수 있기 때문에 바람직하지 못하다.
→ 객체지향 프로그래밍에서는 권장되지 않는 프로그래밍 모델이다.
이러한 문제점 때문에 스프링은 싱글톤 레지스트리를 제공해서 직접 싱글톤 형태의 오브젝트를 만들고 관리할 수 있게 되었습니다.
private 생성자를 사용해야 하는 방법이 아닌 평범한 자바 클래스를 싱글톤으로 활용할 수 있게 해준다.
스프링이 지지하는 객체지향적인 설계 방식의 원칙, 디자인 패턴 등을 적용하는데
아무런 제약이 없게 만들어줬습니다.
https://atoz-develop.tistory.com/entry/Spring-스프링-빈Bean의-개념과-생성-원리
https://okimaru.tistory.com/114
https://mimah.tistory.com/entry/Spring-스프링-빈을-등록하는-두-가지-방법
데이터베이스 커넥션 풀 https://code-lab1.tistory.com/209
https://tecoble.techcourse.co.kr/post/2023-05-22-configuration/
https://mangkyu.tistory.com/234
https://multifrontgarden.tistory.com/253
https://docs.spring.io/spring-framework/reference/core/beans/factory-scopes.html
https://steady-coding.tistory.com/594
https://velog.io/@probsno/Bean-스코프란
https://spongeb0b.tistory.com/513
https://yeonbot.github.io/java/ThreadLocal/
인프런 스프링 핵심 원리 - 고급편 (김영한 강사님)
https://velog.io/@jakeseo_me/토비의-스프링-정리-프로젝트-1.6-싱글톤-레지스트리와-오브젝트-스코프