자바의 디자인패턴은 상당히 다양한데 전부 다 꼼꼼하게 공부한 적은 없었다. 그래서 이번 부트캠프를 기회로 전부 상세하게 공부하고 내것으로 만들어보려고 한다.


📌 싱글톤 패턴 (Singleton Pattern)

1. 개념

싱글톤 패턴은 하나의 클래스에 오직 하나의 인스턴스만 존재하도록 보장하고, 이 인스턴스에 전역적으로 접근할 수 있는 방법을 제공하는 디자인 패턴이다.
즉, 프로그램 전체에서 단 하나의 객체만 필요할 때 사용하는 패턴이다.

2. 특징

  • 인스턴스가 하나만 생성됨 (메모리 절약 가능).
  • 전역적으로 접근할 수 있어 공용 자원 관리에 용이.
  • 객체 생성을 제어하므로 불필요한 인스턴스 생성을 방지.
  • 다만, 전역 상태를 갖기 때문에 테스트가 어려워질 수 있고, 너무 많이 사용하면 객체 간 결합도가 높아질 수 있음.

3. 적절한 사용 예

  • 공유 자원이 필요한 경우

    • 예: DB 연결 객체, 로그 기록 객체
  • 설정이나 환경을 관리하는 객체

    • 예: 애플리케이션 설정(Configuration)
  • 공용 캐시, 스레드 풀, 윈도우 관리자프로그램 전체에서 일관성을 유지해야 하는 객체

4. 용례 예시

  • Java

    • java.lang.Runtime (JVM 런타임 환경)

    • java.awt.Desktop (데스크탑 연동)

    • Logger 라이브러리들 (ex. Log4j, java.util.logging.Logger)

  • Spring Framework

    • 스프링 빈(bean)은 기본적으로 싱글톤 스코프를 가짐

5. 예시 코드

(1) 기본 싱글톤 구현

(2) 사용 예시

(3) 멀티스레드 환경에서의 안전한 싱글톤 (Lazy Initialization + Double-Checked Locking)

(자세한 내용은 뒤에서 정리)


6. 싱글톤 패턴 구현 방법 7가지

1️⃣ Eager Initialization (이른 초기화)

  • 특징: 클래스가 로딩될 때 인스턴스를 미리 생성.
  • 장점: 구현이 가장 간단, 멀티스레드 안전.
  • 단점: 실제로 사용하지 않더라도 인스턴스가 생성되어 메모리 낭비 가능.

2️⃣ Static Block Initialization (정적 블록 초기화)

  • 특징: static 블록을 사용하여 인스턴스를 초기화.
  • 장점: 예외 처리를 넣을 수 있어 유연함.
  • 단점: 이른 초기화와 동일하게 불필요한 인스턴스 생성 가능.(그냥 예외 처리만 추가한 것)

3️⃣ Lazy Initialization (지연 초기화, 기본형)

  • 특징: 요청이 있을 때만 인스턴스를 생성.
  • 장점: 메모리 효율적.
  • 단점: 멀티스레드 환경에서는 안전하지 않음.

4️⃣ Thread Safe Singleton (synchronized)

  • 특징: synchronized 키워드로 동기화.
  • 장점: 멀티스레드 환경에서도 안전.
  • 단점: 매번 동기화로 성능 저하 가능.

5️⃣ Double-Checked Locking (DCL, 이중 체크 락킹)

  • 특징: 동기화 비용을 최소화하기 위해 2번 체크.
  • 장점: 효율적이고 멀티스레드 안전.
  • 단점: 코드가 다소 복잡.

🤔 volatile이란❓

volatile자바 키워드로, 멀티스레드 환경에서 변수의 값을 메인 메모리(Main Memory)에 항상 직접 읽고 쓰도록 강제하는 역할을 한다.


📌 volatile의 의미

  • 자바에서 변수는 기본적으로 스레드마다 CPU 캐시에 복사되어 사용.
  • 이렇게 되면 어떤 스레드가 값을 변경해도 다른 스레드에서는 변경된 값을 즉시 보지 못하는 문제(가시성 문제, visibility issue) 가 발생.
  • volatile 키워드를 붙이면, 그 변수는 항상 메인 메모리에서 읽고 쓰기 때문에 모든 스레드가 동일한 최신 값을 보장.

📌 싱글톤에서 volatile이 필요한 이유

Double-Checked Locking(DCL) 방식에서 instance를 생성할 때는 new Singleton()이라는 명령이 실행됨
이 과정은 단순히 "메모리 할당 → 초기화 → 참조 변수 연결" 이 아니라, 컴파일러/CPU 최적화 때문에 순서가 바뀔 수 있음.
(사람이 코드를 작성한 순서와 다르게 동작할 수 있다는 의미. 컴파일러는 코드를 더 효율적으로 만들기 위해 메모리 접근 순서를 재배치하거나, CPU는 여러 명령어를 동시에 처리하면서 실제 실행 순서를 변경할 수 있기 때문.)

1. 메모리 할당
2. (최적화 때문에) 참조 변수 연결
3. 객체 초기화
이렇게 객체를 초기화 하기 전에 참조 변수를 연결하게 된다면, 다른 스레드가 instance를 읽을 때 아직 초기화되지 않은 객체를 참조할 수 있는 문제가 발생.

volatile을 붙이면 이 명령어 재정렬(reordering)을 막아주어, 안전하게 객체가 초기화된 이후에 참조할 수 있도록 보장.


📌 정리

  • volatile = 모든 스레드가 변수의 최신 값을 볼 수 있도록 보장 + 명령어 재정렬 방지.
  • 싱글톤의 Double-Checked Locking 방식에서는 volatile이 없으면 잘못된 객체를 참조할 수 있으므로 필수.

6️⃣ Bill Pugh Singleton (정적 내부 클래스 이용) - 권장

  • 특징: 정적 내부 클래스를 사용해 클래스 로딩 시점에 초기화. 내부클래스를 static으로 선언하였기 때문에, Singleton 클래스가 초기화되어도 SingleInstanceHolder 내부 클래스는 메모리에 로드되지 않음
  • 장점: Lazy Initialization + 멀티스레드 안전 + 성능 우수.
  • 단점: 약간의 문법 이해 필요. (실무에서 가장 많이 사용되는 방식) 다만 클라이언트가 임의로 싱글톤을 파괴할 수 있다는 단점을 지님 (Reflection API, 직렬화/역직렬화를 통해)

    🤔 메서드로 정의하는거랑 클래스로 정의하는거랑 뭐가 다르냐❓
  • 메서드 방식은 JMM(Java Memory Model)의 가시성/재배치 문제를 개발자가 직접 제어해서 안전성을 보장해야 함.
  • 반면, Bill Pugh(정적 내부 클래스)는 JVM (Java Virtual Machine)의 클래스 로딩 & 초기화 메커니즘이 이미 원자적이고 스레드 안전하게 보장해줌. 따라서 개발자가 동기화를 구현해줄 필요가 없음.
  • Bill Pugh 방식 = JVM 차원에서 안전 보장 → 가장 권장되는 구현

7️⃣ Enum Singleton - 권장

  • 특징: Enum을 사용하여 싱글톤을 보장. enum은 애초에 멤버를 만들때 private로 만들고 한번만 초기화 하기 때문에 thread safe.
  • 장점: 직렬화/역직렬화, 리플렉션(Reflection) 공격에도 안전.
  • 단점: 싱글톤 클래스를 멀티톤(일반적인 클래스)로 마이그레이션 해야할때 처음부터 코드를 다시 짜야 되는 단점이 존재. (개발 스펙은 언제어디서 변경 될수 있기 때문에) 클래스 상속이 필요할때, enum 외의 클래스 상속은 불가능.

🤔 Reflection 공격이란❓

  • 일반 싱클톤 클래스의 경우
  • 위와 같이 강제로 생성자를 가져다가 접근 권한을 무시해서 새로운 인스턴스를 생성할 수 있다
  • 코드 실행 결과 서로 다른 생성자가 생성된 것을 확인할 수 있다

    반면,
  • enum클래스의 경우
  • 리플렉션을 시도하면,
  • JVM이 아예 막아버려서 이렇게 에러가 나게 된다.

✅ 구현 방식 요약

방식멀티스레드 안전성초기화 시점장점단점
Eager Initialization안전클래스 로딩 시구현 간단메모리 낭비
Static Block안전클래스 로딩 시예외 처리 가능메모리 낭비
Lazy Initialization불안전필요 시메모리 절약스레드 불안전
Thread Safe (synchronized)안전필요 시구현 쉬움성능 저하
Double-Checked Locking안전필요 시효율적코드 복잡
Bill Pugh (Inner Class)안전필요 시가장 권장문법 이해 필요
Enum안전Enum 로딩 시직렬화/리플렉션 안전Enum 문법 제약

✅ 정리

  • 싱글톤은 객체가 하나만 필요할 때 쓰는 패턴
  • 장점: 메모리 절약, 전역 접근 가능, 일관성 유지
  • 단점: 전역 상태 관리의 어려움, 테스트 어려움
  • 활용: DB 연결, 로그, 설정 객체, 스프링 빈 등

0개의 댓글