객체를 생성할 때, 즉 인스턴스를 생성할 때 메모리를 소모한다.
하지만 굳이 매번 인스턴스를 만들 필요 없이 보통 인스턴스 하나만 필요한 경우가 있다.
예를들면 데이터베이스 접근 객체(DAO), 콘솔 입출력을 위한 객체(View)등
이런 객체들은 굳이 인스턴스를 만들 필요가 있을까? 애초에 상태를 가지는 클래스가 아니라면 굳이 인스턴스를 매번 만들어 불필요하게 메모리를 할당할 필요성을 못느낀다.
그래서 보통 메소드를 정적으로 설계하여 인스턴스를 만들지 않고 메서드를 사용해본 경험이 있을 것 이다.
하지만 정적으로 메소드를 만들지 않고 다른 방법은 없을까?
바로 싱글턴 패턴
을 사용하면 된다.
싱글턴 패턴
이란 GoF에서 소개된 디자인 패턴 중 하나로 생성자를 여러번 호출하더라도 같은 인스턴스, 즉 참조하는 주소가 같은 인스턴스를 반환한다.
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
와 같은 Java 코드를 예시로 싱글턴 패턴
을 구현할 수 있다.
이렇게 된다면 생성자의 접근자가 private라 인스턴스 생성을 하지 않고 정적 메서드인 getInstance
를 호출하여 인스턴스를 가져와야 한다.
자, 우리에게 선택지가 2가지가 있다.
그렇다면 어떤 선택을 해야할까?
우선 정적 메서드를 사용할 때의 이점을 알아보자,
인스턴스를 만들 필요가 없다. 즉 메모리 낭비가 적다.
언제 어디서나 필요한 메서드를 호출해서 사용할 수 있다.
하지만 그 외엔 어떤 단점이 있을까?
정적 메서드를 사용한다면 Lazy Loading
이 불가능하다. 이 부분은 나중에 알아보겠다.
그리고 Java에서 정적 초기화를 처리하는 방법 때문에 예기치 못한 버그가 생길 수 있다. (static 변수 → 필드 변수 → 생성자 block)
때문에 간단한 경우 정적 메서드를 사용하면 좋고, 그 이상의 경우엔 싱글턴 패턴
을 사용하는 것이 바람직 하겠다.
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
우선 위의 방식을 바로 Lazy Loading
이라고 한다.
getInstance
메서드를 보면 instance
가 null 일 때, 즉 getInstance
를 처음 호출할 때 인스턴스를 생성한다.
이렇게 Lazy Loading
을 사용한다면 인스턴스가 굳이 필요하지 않은 상황에서는 인스턴스 생성을 하지 않게 되어 불필요한 메모리 소모를 줄일 수 있다.
자 그렇다면 싱글턴 패턴
은 단순히 위의 코드처럼 작성하면 끝일까?
아니다, 고려해야할 상황들이 여러가지 있다.
우선 정말 싱글턴 패턴
을 사용했다고 해서 인스턴스가 하나임을 보장 할 수 있을까?
싱글 스레드 환경에서는 큰 문제가 없겠지만, 멀티 스레드 환경에서는 이 부분을 보장할 수 없다.
void singleton_multiThread() {
Thread thread1 = new Thread(this::singletonTest);
Thread thread2 = new Thread(this::singletonTest);
thread1.start();
thread2.start();
}
private void singletonTest() {
Singleton instance = Singleton.getInstance();
System.out.println(instance);
}
실행결과
main.Singleton@61cae6e9
main.Singleton@f33eca9
이 경우 싱글턴 패턴
이 깨지게 된다.
이런 멀티 스레딩 환경에서 싱글턴을 보장하기 위해 제일 간단한 방법은 synchronized
키워드를 사용하는 것이다.
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
키워드 하나만 추가하면 문제가 해결이 되겠지만, synchronized
의 비용은 비싸다.
또 하나의 해결법은 애초에 처음부터 생성자로 인스턴스를 초기화 하는 것이다.
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
하지만 이렇게 된다면 결국 Lazy Loading
을 사용하는 이점이 사라진다.
또 다른 해결법은 DCL(Double Checking Locking)
을 사용하여 getInstance
에서 동기화되는 부분을 줄이는 것이다.
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
위와 같이 volatile
키워드로 필드를 선언하여 성능 문제를 줄일 수도 있다.
하지만 volatile
키워드는 기초 자료형 외엔 사용하는 것을 권장하지 않는다고 한다.
그렇다면 또 다른 해결법인 Holder Class
를 만들어 해결할 수 있다.
private static final class InstanceHolder {
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return InstanceHolder.instance;
}
그런데 이렇게 된다면 Lazy Loading
이 될까?
물론 Lazy Loading
을 사용할 수 있다.
해당 내용은 이펙티브 자바
아이템 83에 소개되어 있는데, [JSL, 12.4.1] 클래스는 클래스가 처음 쓰일 때 초기화 된다는 특성을 이용한다.
그 외 열거형을 사용하는 방법도 있다. 참조)https://blog.hexabrain.net/394
하지만 싱글턴 패턴
의 단점도 있는데, 우선 테스트가 힘들다.
@BeforeEach
void setUp() {
playerRepository = PlayerRepository.getInstance();
}
@Test
@DisplayName("사용자 리스트가 정상적으로 저장되어야 한다.")
void saveAll_success() {
// given
List<Player> players = createPlayers(5);
// when
playerRepository.saveAll(players);
// then
assertThat(playerRepository.findAll())
.hasSize(5);
}
@Test
@DisplayName("사용자 인원 수가 정상적으로 반환되어야 한다.")
void countBy_success() {
// given
List<Player> players = createPlayers(5);
// when
playerRepository.saveAll(players);
// then
assertThat(playerRepository.countBy())
.isEqualTo(5);
}
위의 두 테스트를 실행할 때 두 테스트가 통과할까?
getInstance
를 호출하여도 새로운 인스턴스를 생성하는 것이 아닌 기존 인스턴스를 사용하기에 테스트 마다 clearAll()
같은 메서드를 매번 호출해야한다.
그리고 생성자의 접근자가 private
이라서 상속도 불가능하다.
이렇기에 여러 장점이 있는데 불구하고 싱글턴 패턴
은 안티 패턴
이라고 불린다.
이러한 단점을 해결하기 위해 여러 프레임워크에서는 의존성 주입
기능을 제공하며 주요 프레임워크 중 하나인 Spring
은 Bean, Component
를 사용하여 싱글턴을 보장하면서 이러한 단점들을 해결한다.
싱글턴
을 잘 사용한다면 불필요한 메모리 낭비를 줄이며, Lazy Loading
을 사용하여 원할때 초기화를 할 수 있다는 장점이 있지만, 정적 메서드 처럼 언제 어디서나 인스턴스를 가져와 사용이 가능하니 의존성이 증가하여 유지보수가 힘들 수 있다.
이렇게 싱글턴 패턴
에 대해 알아봤다.