오늘은 싱글톤 패턴과 스프링 컨테이너의 상관관계에 대해서 알아보고자 한다.
싱글톤 패턴
디자인패턴 중 하나로, 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴을 의미한다. 아래는 싱글톤 패턴을 구현하는 방법을 코드로 작성한 모습이다.
- SingletonPatterns.java
public class SingletonPatterns {
// 1. static 영역에 객체를 딱 1개만 생성한다.
private static final SingletonPatterns instance = new SingletonPatterns();
// 2. public으로 열어 객체 인스턴스가 필요하면 이 static 메서드를 통해서만 조회하도록 허용한다.
public static SingletonPatterns getInstance(){
return instance;
}
// 3. 생성자를 private로 선언하여 new 키워드를 사용한 객체 생성을 못하게 막는다.
private SingletonPatterns(){
}
public void logic(){
System.out.println("싱글톤 객체 로직 호출");
}
}
싱글톤 패턴을 사용하면, 불 필요한 객체 생성을 남용하지 않아서 좋은 점이 있지만, 그에 따른 단점도 존재한다.
- 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다. (예 : getInstance(), private으로 객체 생성 방지 등..)
- 의존관계상 클라이언트가 구체 클래스에 의존한다. (DIP 위반)
- 클라이언트가 구체 클래스에 의존해서 OCP 위반 가능성이 높다.
- private 생성자로 인한 유언성이 떨어진다.
싱글톤 방식에서 제일주의할 점이 있는데, 이는 싱글톤 객체는 상태를 유지하게 설계하면 안 된다라는 점이다. 예를 들어 다음과 같인 서비스를 싱글톤 방식을 이용하여 만든다고 가정한다.
- StatefulService
public class StatefulService {
private int price; //상태를 유지하는 필드
public void order(String name, int price) {
System.out.println("name = " + name + " price = " + price);
this.price = price; //여기가 문제!
}
public int getPrice() {
return price;
}
}
해당 클래스는 price라는 상태를 유지하는 필드를 가지고 있는 클래스이다. 이 클래스를 싱글톤으로 만들어서 아래와 같이 2명의 유저의 주문을 받았다고 가정한다.
- StatefulServiceTest.java
public class StatefulServiceTest {
@Test
void statefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
//ThreadA: A사용자 10000원 주문
statefulService1.order("userA", 10000);
//ThreadB: B사용자 20000원 주문
statefulService2.order("userB", 20000);
//ThreadA: 사용자A 주문 금액 조회
int price = statefulService1.getPrice();
//ThreadA: 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력 System.out.println("price = " + price);
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
이럴 경우 userA가 주문했던 10000과는 달리 userB가 주문했던 20000원이 마지막으로 price에 저장되게 되면서, userA는 얼떨결에 20000이라는 price값을 받아버리는 결과가 발생한다.
따라서 싱글톤 패턴으로 작성할 때에는 공유필드를 항상 조심하여야 한다.
Spring과 싱글톤 패턴
스프링은 해당 디자인 패턴을 사용하고 있는데, 그 이유는 아래와 같다.
- 웹 애플리케이션에서 여러 고객이 동시에 요청이 들어올 때, AppConfig에서는 각 요청마다 memberService 객체를 생성해야 한다.
- 이는 고객 트래픽이 초당 100이 나오면 초당 100개의 객체가 생성되고 소멸됨을 의미하며 즉 메모리 낭비로 이어진다.
- 따라서 해당 객체를 1개만 생성하고 이를 공유하도록 설계하여야 한다.
따라서 스프링 컨테이너는 위의 싱글톤 패턴의 일반적인 문제점을 해결하면서 객체 인스턴스를 싱글톤(1개만 생성)으로 관리한다.
@Configuration과 싱글톤
AppConfig 코드를 보면 이상한 점이 있다.
@Configuration
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();
}
... }
바로 Bean으로 등록된 객체를 생성할 때, memberService 객체를 생성할 때, new MemoryMemberRepository()가 호출되고 orderService객체가 생성될 때, new MemoryMemberRepository()가 호출된다.
결과론적으로 각각 다른 2개의 new MemoryMemberRepository()가 생성되면서 싱글톤이 깨져 보이는 것처럼 보이는데, 스프링에서는 해당 문제를 바이트코드 조작 라이브러리를 이용하여 해결하였다.
쉽게 이야기하자면, @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다. 때문에 스프링에서 싱글톤을 보장받고 있다.
- @Bean만 사용해도 스프링 빈으로 등록되지만, @Configuration이 없다면, 싱글톤을 보장받지 못한다.
- 따라서 스프링 설정 정보는 항상 @Configuration을 사용하는 것을 추천한다.
'FrameWork > Spring' 카테고리의 다른 글
RestTemplate (0) | 2024.06.03 |
---|---|
ComponentScan (0) | 2024.05.30 |
컨테이너에 빈 등록 및 조회 (0) | 2024.05.27 |
IoC, DI 컨테이너 (0) | 2024.05.27 |
Thymeleaf (0) | 2024.05.21 |