지난번 포스팅에서 static 키워드를 다뤘는데, 그 이유는 이번 시간 싱글톤 패턴에 대해서 설명하기 이전에 static 키워드에 대해서 어느정도 이해하고 있어야 된다는 생각 때문이었다. 그리고 싱글톤 패턴은 꼭 Java가 아니라도, 개인적으로는 C#으로 프로젝트를 진행할 때도 활용한 경험이 있고, 중요하지만 핵심 원리만 이해하기까지는 그렇게 어려운 내용까지는 아니라고 생각해서 포스팅하게 되었다. (물론 깊게 들어가면 어렵다..)
싱글톤 패턴이란, Java에서 많이 쓰이는 디자인 패턴 중 하나로 클래스의 인스턴스가 딱 1개만 생성되는 것이다. 코드로 확인해보자.
public class SingletonPattern {
private static SingletonPattern instance = new SingletonPattern();
public static SingletonPattern getInstance() {
return instance;
}
private SingletonPattern() {
}
}
위의 코드는 싱글톤 패턴 적용을 위해 미리 인스턴스를 1개 선언(private static SingletonPattern instance = new SingletonPattern();
)하고, 오직 getInstance()
함수를 통해서만 미리 생성된 인스턴스를 가져올 수 있도록 설계되어있다. 생성자를 private
으로 선언하여 new 키워드를 이용한 객체 생성을 막아놨다.
public class Main {
public static void main(String[] args) {
SingletonPattern singletonPattern1 = SingletonPattern.getInstance();
SingletonPattern singletonPattern2 = SingletonPattern.getInstance();
System.out.println("singletonPattern1 = " + singletonPattern1);
System.out.println("singletonPattern2 = " + singletonPattern2);
}
}
SingletonPattern
클래스를 위와 같이 getInstance()
함수를 통해서 가져오고, 출력하는 코드이다. 결과는 다음과 같다.
singletonPattern1 = springstudy.core.exercise.SingletonPattern@68837a77
singletonPattern2 = springstudy.core.exercise.SingletonPattern@68837a77
위와 같이 결과는 singletonPattern1
과 singletonPattern2
는 같은 인스턴스를 반환한 것을 알 수 있다.
이와 같은 방법은 Eager Initialization로 싱글톤 패턴을 구현하는 가장 간단한 방법이다. static을 통해 해당 클래스를 Class Loader가 로딩할 때 객체를 생성하는 방식이다. 이 방식은 자세히 보면, 객체를 사용하지 않는 경우라도 무조건 하나의 객체를 생성해야만 한다. 그로 인해 자원 낭비가 발생할 수 있다는 단점이 있다. 다른 몇가지 싱글톤 패턴 구현 방법이 존재하나, 이번 포스팅에선 여기까지만 확인하도록 하겠다.
위의 예제 코드를 보면서도 알 수 있듯이 인스턴스를 미리 1개만 선언한다는 걸 알 수 있다. 그리하여 발생하는 몇가지 이점들이 있다.
이처럼 싱글톤 패턴은 객체지향 원리를 잘 이용한 패턴이고, 충분히 프로젝트에서 활용할 만한 부분이다. 그러나 싱글톤 패턴에는 몇가지 문제점이 존재한다.
인터넷에 싱글톤 패턴에 관하여 검색하다보면 발견할 수 있는 문제점들이 있다. 그것도 좋은 자료들이지만 내가 개발하면서 느꼈던 문제점들에 중점을 두고 포스팅해보겠다.
1. 생성자를 private으로 제한하여 상속이 불가능하다.
싱글톤 패턴은 오직 자신만이 객체를 생성할 수 있도록 기본적인 생성자를 private으로 설정해둔다. 그로 인해 생성자를 생성할 수 없으며 객체지향의 장점인 상속을 통한 다형성을 구현할 수가 없다.
2. 클래스 간의 데이터 공유가 쉽다.
위에서 분명 해당 부분은 장점이다 라고 설명했는데, 알고 보면 이것은 엄청난 단점일 수도 있다. 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 혹시! 각각의 다른 쓰레드에서 마구잡이로 해당 인스턴스에 접근하면 정말 큰 일이 일어날 수도 있다. 예를 들어 보자.
public class Singleton {
private static final Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
private Singleton() {
}
private String id;
public void save(String name, String id) {
System.out.println("name = " + name + ", id = " + id);
this.id = id;
}
public String getId() {
return id;
}
}
public class Main {
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
instance1.save("john", "230122");
instance2.save("amy", "230129");
String id = instance1.getId();
System.out.println("id = " + id);
}
}
위의 코드처럼 싱글톤을 설계했을 경우, 실행 결과는 다음과 같다.
name = john, id = 230122
name = amy, id = 230129
id = 230129
Singleton 클래스의 id
값이 공유되어버렸다. Singleton 클래스에서 save
메소드가 선언된 부분에서 this.id = id;
이 부분에서 발생한 문제이다. 상태를 유지하도록 설계해서 발생한 문제이다. 해결방안은 다음과 같다.
public class Singleton {
...
public String save(String name, String id) {
System.out.println("name = " + name + ", id = " + id);
return id;
}
...
}
위처럼 save
메소드를 String
타입으로 반환시키고, 실행 코드에서 리턴받은 id
값을 지역변수화 시키는 방법이 있다. id
값을 지역변수로 만들면서 Singleton 클래스에선 더이상 id
값의 상태를 관리하지 않도록 설계한 것이다. 그래서 결론은 무상태(Stateless)로 설계해야 한다는 것이다!
3. SOLID 원칙을 지키기가 어렵다.
SOLID 원칙 중 하나인 DIP(Dependency Inversion Principle)
인 의존관계 역전 원칙은 "추상화에 의존해야지, 구체화에 의존하면 안된다" 는 것이다. 인터페이스와 구현체를 분리시키고, 개발자는 인터페이스에만 의존하여 좀더 유연하게 구현체를 변경할 수 있도록 해야한다는 것이다. 그러나 싱글톤 패턴에서는 이 부분을 구현하기가 어렵다. 의존관계상 클라이언트가 구체 클래스에 의존하게 되고 DIP를 위반하게 된다..
[참조]
https://jeong-pro.tistory.com/86
https://velog.io/@sms8377/Structure-%EC%8B%B1%EA%B8%80%ED%86%A4-%ED%8C%A8%ED%84%B4%EA%B3%BC-%EB%AC%B8%EC%A0%9C%EC%A0%90