클라이언트가 서버에 요청을 보낼 때, 서버에서 각 요청마다 객체를 생성해서 처리한다면 고객 트래픽 만큼의 객체가 생성되고 소멸되기를 반복하며 메모리를 낭비 할 것입니다.
이에 대한 해결 방법으로 디자인 패턴중 생성 패턴에 속하는 Singleton 패턴을 통해 해당 객체가 딱 1개만 생성되고, 공유하도록 설계하면 됩니다. 그리고 스프링은 싱글톤을 기본으로 보장해 줍니다.
싱글톤 패턴은 클래스의 인스턴스가 딱 1개만 존재하도록 보장해주는 디자인 패턴입니다.
public class SingletonService {
//1. static 영역에 객체를 딱 1개만 생성해둔다.
private static final SingletonService instance = new SingletonService();
//2. 객체 인스턴스가 필요하면 이 static 메소드를 통해서만 조회하도록 허용한다.
public static SingletonService getInstance() {
return instance;
}
//3. private 생성자를 통해 외부에서의 new 키워드를 통한 객체 생성을 막는다.
private SingletonService() {
}
public void login() {
System.out.println("싱글톤 객체 로직 호출");
}
}
싱글톤 객체는 static final로 인스턴스를 생성하여 자바가 올라갈 때 하나가 올라가고 그 이후로는 static 메소드인 getInstance() 를 호출하여 해당 객체를 참조할 수 있습니다.
또한 생성자의 접근 제어 지시자도 private으로 설정해 실수로라도 new 키워드를 통해 해당 객체가 생성되는것을 방지하고 있습니다.
요청에 의해 객체를 생성해서 제공하는 비용이 1000 이라면, 싱글톤으로 참조하는 비용이 1정도라고 하니 이렇게만 보면 아주 좋은 해결책 인것으로 보입니다.
이러한 싱글톤 패턴에도 문제점들이 다수 존재하는데,
싱글톤 패턴을 구현하는 코드 자체가 많이 들어갑니다.
-> 모든 객체에 싱글톤을 위한 코드를 작성해야 한다고 생각하면 쉽지 않은 작업입니다.
의존관계상 클라이언트가 구체 클래스에 의존합니다.
-> DIP를 위반하게 되는데, 이는 추후에 코드를 수정할 일이 있을 때 OCP를 위반하게 될 것입니다.
예를들어 SingletonServiceImpl 객체를 불러오고 싶으면
SingletonServiceImpl.getInstance()를 호출해야 하기에 구체 클래스에 의존할 수 밖에 없게 됩니다.
내부 속성을 변경하거나 초기화 하기 어렵습니다.
private 생성자로 자식 클래스를 만들기 어렵습니다.
결론적으로 유연성이 떨어지게 됩니다.
스프링 컨테이너는 싱글톤 컨테이너의 역할을 하는데, 이러한 문제들을 해결하면서 객체들을 싱글톤으로 관리합니다.
우선 스프링 컨테이너는 객체에 지저분한 코드를 추가하지 않더라도 객체 인스턴스를 싱글톤으로 관리합니다.
또한 DIP,OCP,테스트 private 생성자로 부터 자유롭게 싱글톤을 사용 가능합니다.
이러한 마법같은 일이 가능한 이유는, 스프링 컨테이너가 객체 빈들을 생성, 관리 해주고 우리는 컨테이너로부터 빈을 조회해서 사용하기만 하면 되기 때문에 추가적인 코드 필요없이 싱글톤으로 객체를 이용할 수 있습니다.
싱글톤 패턴은 항상 무상태(stateless)로 설계 해야합니다!!
만약 상태를 유지하게(stateful)하게 설계한다면 다음과 같은 문제를 만날 수 있습니다.
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;
}
}
//테스트 부분
@Test
public void statefulServiceSingleton() throws Exception {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
//ThreadA: A사용자 10000원 주문
statefulService1.order("userA",10000);
//ThreadB: B사용자 20000원 주문
statefulService2.order("userB",20000);
//ThreadA: 사용자A 주문 금액 조회
int price = statefulService1.getPrice();
System.out.println("price = " + price);
}
아래와 같은 테스트가 이루어질 때 userA가 주문을 한 이후 자신의 주문 가격을 조회하기 이전에 다른 userB가 주문을 하게된다면 userA는 userB의 주문가격을 조회하게 됩니다.
이러한 현상이 나타나게 되면 다른경우에는 로그인을 했는데 다른사람의 아이디가 보인다던지, 결제시에 다른 사람의 결제내용을 결제하게 된다던지 하는 심각한 문제로 이어질 수 있습니다.
결론
개발자로서 성장하는 데 큰 도움이 된 글이었습니다. 감사합니다.