오늘은 싱글톤에 대해서 알아보겠습니다.
우선 싱글톤은 흔히 싱글톤 패턴이라고 알려진 디자인 패턴에서 나왔습니다.
싱글톤 패턴이란, 자바에서 어떤 클래스에 대한 인스턴스를 하나만 유지하도록 도와주는 디자인 패턴을 의미합니다.
(디자인 패턴은 자세히 설명할 수는 없지만, 어떠한 용도나 문제점을 해결하는데 좋은 구조...?? 정도로 저는 이해했습니다)
1. 싱글톤 패턴
그럼 우리는 왜 싱글톤 패턴을 사용할까요???
우리는 프로젝트를 통해서 많은 클라이언트의 요청을 받고 처리합니다.
그러면 이러한 상황을 가정해봅시다.
만약 1,000,000명의 클라이언트가 1초에 한번씩 요청을 하는 상황을 가정합니다. 이 클라이언트는 주문을 요청하며 주문을 요청할 때마다 주문 객체를 생성하여 관리합니다.
그렇다면 우리는 10초에 몇 개의 인스턴스가 생기는걸까요?? 10,000,000개일겁니다. 만약 이 상황이 1분만 지속되도 엄청난 인스턴스들이 생성되게 됩니다. 물론 gc(garbage collector)가 잘 관리하겠지만서도 우리는 효율을 더 따져볼 필요가 있습니다.
이러한 이유로 우리는 싱글톤 패턴을 사용해왔습니다. 아래에는 간단하게 순수 자바로 싱글톤 패턴을 구현하는 코드를 작성해보았습니다.
public class SingletonService {
private static final SingletonService instance = new SingletonService();
public static SingletonService getInstance(){
return instance;
}
private SingletonService() { }
public void logic() {
System.out.println(“싱글톤 객체 로직 호출”);
}
}
여기서의 포인트는 static final로 필드를 변할 수 없게 한 것과, 그 필드에서 선언한 new 메서드가 private이라는 점입니다.
이에 따라서 SingletonService 인스턴스는 필드의 static 때문에 최초 실행되고, 이후에는 실행되지 않습니다. 또한 생성자가 private이므로 외부에서 접근이 불가능하여, .getInstance()를 통해 인스턴스를 받습니다.
2. 싱글톤 컨테이너
싱글톤 컨테이너는 스프링에서 등장하는 개념으로, 싱글톤으로 관리하는 컨테이너(흔히 AppConfig로 사용)를 일컫습니다.
우리는 @Configuration으로 싱글톤 컨테이너를 만들고, @Bean으로 싱글톤 컨테이너에 빈을 등록합니다.
이제 싱글톤 컨테이너 내에 있는 빈은 싱글톤 패턴으로 관리됩니다.
아래에는 간단한 예제 코드를 보여줍니다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
이렇게 되면 우리는 MemberService의 객체를 하나만 갖는 싱글톤을 유지할 수 있게 됩니다.
3. 싱글톤 컨테이너. 그럼 왜 쓰는거지??
그럼 우리는 굳이 싱글톤 패턴으로 싱글톤의 기능을 구현할 수 있는데, 왜 무거운 스프링을 이용하여 싱글톤을 구현하는 걸까요??
우리는 싱글톤 컨테이너를 사용하면 기존의 싱글톤 패턴보다 더 많은 이점을 취하게 됩니다.
1. 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
2. 의존관계상 클라이언트가 구체 클래스에 의존한다. DIP를 위반한다.
3. 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
4. 테스트하기 어렵다.
5. 내부 속성을 변경하거나 초기화 하기 어렵다.
6. private 생성자로 자식 클래스를 만들기 어렵다.
7. 결론적으로 유연성이 떨어진다.
8. 안티패턴으로 불리기도 한다.
이러한 여러가지 이유가 있다고 하는데, 여기서 2번에 집중해봅시다.
왜 싱글톤 패턴을 이용함으로써 DIP를 위반하는거지?? 의문이 생겼습니다.
이 의문을 그럼, 테스트 코드를 통해서 해결해봅시다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
2번 싱글톤 컨테이너에서 사용했던 코드입니다. 그럼 만약 우리가 싱글톤 패턴으로 구현한다면 어떻게 바뀔까요??
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl.getInstance(memberRepository());
}
이러한 코드로 구현이 될 것입니다. 그럼 이게 왜 DIP 위반인지 아시겠나요???
DIP를 따르게 되면, 우리는 구체 클래스에 의존하지 않고 추상 클래스에 의존해야합니다. 그런데, MemberServiceImpl.getInstance를 봅시다.
우리는 MemberService(인터페이스)의 구현체인 MemberServiceImple 내부의 메소드인 getInstance를 알게된 셈이죠. 따라서 DIP를 위반하게 되는 것입니다.
4. 싱글톤?? 그거 좋은거 맞아??
지금 이 글을 다 읽고 조금만 고민해본다면, 몇가지 고민점이 들게 됩니다. 그 중 가장 큰 문제가 있습니다.
바로 하나의 객체만을 갖게 되면 필드를 공유하므로, 클라이언트마다의 처리가 불가능하다. 라는 단점이 있습니다.
너무 많은 서비스에서, 개인으로 가지고 있는 정보가 많을텐데, 싱글톤으로 보장되면 이러한 부분이 모두 성립될 수 없습니다.
실제로 아래와 같은 테스트 코드에서만 봐도 알 수 있습니다.
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;
}
}
이러한 클래스가 AppConfig.class에서 스프링 빈으로 등록되고 싱글톤을 보장한다고 해봅시다.
void statefulServiceSingleton(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
// ThreadA : A사용자 10,000원 주문
statefulService1.order("userA", 10000);
// ThreadB : B사용자 20,000원 주문
statefulService2.order("userB", 20000);
int price = statefulService1.getPrice();
System.out.println(price);
}
그럼 이 메서드에서의 출력값이 뭐가 될까요?? 아마 우리는 10,000이 나오길 기대할 것입니다. 하지만 20,000이 나옵니다. 그 이유는 싱글톤이므로 인스턴스가 하나이기 때문에 필드를 공유하기 때문이죠.
이러한 너무 크나큰 단점을 어떻게 보완할까요?? 그리고 이런 단점이 있는데도 불구하고 도대체 왜 쓰는걸까요??
저희는 아래의 몇가지 원칙을 준수하면서 설계하게 되면, 싱글톤을 유지하면서 문제 없이 서비스를 제공할 수 있습니다.
- 특정 클라이언트에 의존적인 필드가 있으면 안된다.
- 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다!
- 가급적 읽기만 가능해야 한다.
- 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
따라서 위에서 정의된 클래스인 StatefulService 클래스를 다음과 같이 바꿀 수 있습니다.
public class StatefulService {
public int order(String name, int price){ // void 대신 int로!!
System.out.println("name = " + name+" price = "+price);
return price;
}
}
필드를 바로 파라미터로 바꿔서 사용합니다!! 이렇게 된다면 테스트 코드는 아래와 같이 변경하여 성공할 수 있습니다.
void statefulServiceSingleton(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
// ThreadA : A사용자 10,000원 주문
int userAPrice = statefulService1.order("userA", 10000);
// ThreadB : B사용자 20,000원 주문
int userBPrice = statefulService2.order("userB", 20000);
System.out.println("userAPrice = " + userAPrice);
System.out.println("userBPrice = " + userBPrice);
}
지금까지 스프링에서 사용되는 개념 중 중요한 싱글톤에 대해서 알아봤습니다.
'Java' 카테고리의 다른 글
[이펙티브 자바] 매개변수가 유효한지 검사하라. (Item 49) (0) | 2024.02.27 |
---|---|
[Java/자바] 자바 프로젝트와 mysql을 docker-compose로 묶어보자 (0) | 2023.08.30 |
[Java/자바] 문자열을 "+"로 split 해보자(BOJ 1541 잃어버린 괄호) (0) | 2023.07.26 |
[자바/Java] 추상 클래스(Abstract Class) (0) | 2023.07.10 |
[자바/Java] 오버라이딩과 오버로딩(OverRiding and OverLoading) (0) | 2023.07.04 |