싱글턴 패턴이란?
인스턴스를 하나만 만들어서 사용하기 위한 패턴
시스템 상 하나만 존재해야 하는 객체, 예를 들어, 커넥션 풀, 스레드 풀, 디바이스 설정 객체 등의 경우 인스턴스가 여러개 존재하게 되면 불필요한 자원을 사용하게 되고, 시스템 전체 설정에 관여하는 객체 인스턴스가 여러개 존재할 경우 객체 별로 다른 변수를 갖게 되는 경우가 생길 수 있다.
싱글턴 패턴 사용시 장점
유일 인스턴스 및 전역 접근 : 싱글턴 패턴을 사용하면 클래스의 인스턴스가 오직 하나만 생성되고, 어디에서든 접근 가능하게 됩니다. 이는 고유한 객체를 생성하고 관리하는 데 유용합니다.
자원 공유: 여러 부분에서 공유 자원을 사용해야 할 때 유용합니다. 예를 들어, 로그 생성, 데이터베이스 연결 풀, 캐시, 설정 관리 등과 같은 공유 자원을 한 번만 생성하고 여러 곳에서 이용할 수 있습니다.
레이지 로딩: 싱글턴 패턴을 사용하면 인스턴스를 처음 사용할 때까지 생성을 미룰 수 있습니다. 이를 통해 애플리케이션 시작 시 초기화 시간을 줄이고 메모리를 절약할 수 있습니다.
메모리 관리: 싱글턴 패턴은 애플리케이션에서 하나의 인스턴스만 유지하기 때문에 메모리 관리에 도움이 됩니다. 다수의 객체가 생성되는 것을 방지하여 메모리 사용을 줄일 수 있습니다.
싱글턴 패턴 사용시 단점
- 멀티 쓰레드 환경의 경우 싱글톤을 구현하는 코드가 많이 들어간다.
- 처음 객체가 할당될 때, 정적 클래스 내에서 직접 전역적으로 접근 가능한 객체를 생성하기 때문에 SOLID 원칙의 SRP를 위반한다.
SOLID 관점에서 본 싱글톤 패턴
- 단일 책임 원칙 : 전역적으로 접근 가능한 객체의 생성과 사용(환경 변수 읽기 등)의 두개의 책임이 존재한다. 엄밀히 따지면 단일 책임 원칙을 위배하고 있다.
- 개방 폐쇄 원칙 : 클래스 내부에서 객체를 직접 생성하고 사용하고 있다. 싱글톤 기능의 확장으로 다른 객체를 사용할 경우 코드가 변경되어야하므로 개방 폐쇄 원칙도 위배된다.
- 의존성 역전 원칙 : 추상화에 의존하지 않고 직접 구현체와 결합하고 있으므로 의존성 역전 원칙도 따르지 않고 있다.
싱글턴 패턴을 구현하려면 아래 3개를 기억하고 있으면 다른 디자인 패턴에 비해 쉽게 사용할 수 있다.
- 새로 동적 할당할 수 없도록 private 생성자 사용
- 유일한 단일 객체를 반환할 수 있는 정적 메서드 구현
- 정적 참조변수에 객체 할당
전역적으로 쉽게 접근할 수 있는 만큼, 전역적으로 사용될 싱글턴 객체 내의 변수 값을 쉽게 변경할 수 없도록 해야 하기 때문에 싱글턴 객체는 쓰기 가능한 속성을 갖지 않는 것이 정석이다.
예제로 알아보는 싱글턴 패턴
싱글턴 클래스
package singleton;
public class Singleton {
static Singleton Instance;
private Singleton(){};
public static Singleton getInstance(){
if(Instance == null)
Instance = new Singleton();
return Instance;
}
}
싱글턴 클래스 사용 예제
package singleton;
public class Client {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance(); // 처음으로 getInstance를 실행한 시점에 유일 객체가 동적할당된다.
Singleton s2 = Singleton.getInstance(); // 이후 getInstance 실행시 최초 동적 할당 된 객체의 인스턴스를 반환
Singleton s3 = Singleton.getInstance();
System.out.println(s1);
System.out.println(s2);
System.out.println(s3);
s1 = null; // s1 객체를 더이상 사용하지 않겠다는 뜻이지, 이런 방식으로 Singleton 객체를 없앨 수 없음
System.out.println(s2);
}
}
코드 하단에 s1 = null 로 s1이 더 이상 싱글톤 객체를 가리키지 않도록 했는데,
당연하게도 s2는 여전히 싱글톤 객체를 가리키고 있다.
결과는 아래와 같다.
심지어 아래 사진처럼 s1, s2, s3에 모두 null값을 대입해도 정적 변수인 Singleton.Instance 는 여전히 그대로 할당 되어있다.
디버깅 중인 현재 s1 = null; 코드까진 실행되었으므로 null값을 가지며
debug watch 기능으로 Singleton.Instance를 확인한 결과
s2,s3,Singleton.Instance 모두 같은 참조 식별자 @716을 갖는다.
다음 중단점 까지 진행해보자
s1~3 변수들이 모두 null값을 가지게 되어도 Singleton.Instance 변수는 당연히 할당되어있다.
싱글턴 객체를 소멸시키려면?
먼저, 싱글턴 객체를 소멸시키는 행위, 소멸시킬 수 있도록 인터페이스가 존재하는 것이 권장되지 않는다. 왜 권장되지 않는지는 예제로 알아보고, 먼저 직접 싱글턴 객체를 소멸시켜 보자.
아까 s1,s2,s3 변수에 null값을 대입한다고 한들 정적 클래스의 정적 변수인 싱글턴 객체는 소멸되지 않은 것을 확인했다.
그럼 싱글턴 객체를 완전히 소멸시키고 싶다면 어떻게 해야할까?
싱글턴 클래스에 삭제 기능을 하는 함수를 추가해주면 된다.
public static void destroyInstance() {
Instance = null;
}
위의 코드를 추가한 싱글턴 클래스
package singleton;
public class Singleton {
static Singleton Instance;
private Singleton(){};
public static Singleton getInstance(){
if(Instance == null)
Instance = new Singleton();
return Instance;
}
public static void destroyInstance() {
Instance = null;
}
}
이렇게 싱글턴 클래스를 변경하고 main 함수에서 아래 코드처럼 destroyInstance 함수 실행 전과 후를 비교해보자
package singleton;
public class Client {
public static void main(String[] args) {
Singleton s = Singleton.getInstance(); // 처음으로 getInstance를 실행한 시점에 유일 객체가 동적할당된다.
System.out.println(s.Instance.toString()); // 싱글턴 객체 파괴 전
Singleton.destroyInstance();
System.out.println(s.Instance.toString()); // 싱글턴 객체 파괴 후
}
}
당연한 결과이지만, Singeton.destroyInstance() 함수가 실행된 이후에 싱글턴 객체를 참조하려고 하면,
NullPointerException이 발생한다.
그러니까 Singleton s 객체를 사용하는 입장에서 다른 로직의 코드에서 싱글턴 객체를 파괴한다면,
영문도 모른채 NPE가 발생할 수도 있게 된다.
물론 소멸된 것을 확인했다면, 그 때 다시 생성하면 되지 않느냐고 생각할 수 있지만, 소멸 전의 상태와 재 생성한 후의 상태가 같음을 보장할 수 없다.
또한 멀티 쓰레드 환경에서는 동기화 문제가 있을 수 있고,
이 때문에 싱글턴 객체를 소멸시키는 행위가 권장되지 않는다고 한다.
결론
싱글톤 패턴은 애플리케이션 실행 중 인스턴스를 한 개만 생성하고 싶을 때 사용하는 디자인 패턴이다. 싱글톤 패턴은 장단점이 명확히 구분되기도 하지만, 싱글턴 패턴을 스프링 프레임워크 도움없이 사용하게 될 경우
SOLID 관점을 지키지 못한 단점들 때문에 오히려 안티패턴으로 간주되기도 한다. SOLID 관점에서 보더라도 여러 원칙들을 위배하기 때문에 객체지향스럽지 못한 패턴이기도 한다.
스프링 같은 프레임워크의 도움을 받는다면, 객체를 직접 생성할 필요없이 프레임워크가 단점을 커버해주는 부분이 많으므로 싱글톤 패턴의 장점을 누릴 수 있다. 그래서 스프링이 관리하는 빈들은 기본 스코프가 싱글톤으로 되어있다.