본문 바로가기
3학년 2학기 학사/소프트웨어 분석 및 설계

[소프트웨어 분석 및 설계] # L14. 디자인 패턴, 싱글톤 패턴

by whiteTommy 2024. 12. 11.
반응형
더보기

목차

  • Introduction to Design Pattern
  • Singleton Pattern
  • Implementation
  • Discussion

 

예상문제

  • 코드 주어지고, context, problem, solution 서술
  • Singleton 흐름

 

Introduction to Design Pattern

  • 디자인 패턴이란?
    • SW 를 설계할 때, 특정 맥락/ 상황에서 자주 발생하는 문제들의 해결 방법을 반복적으로 재사용할 수 있도록 패턴화한 것
    • 다양한 설계 분야에서도 디자인 패턴의 개념이 사용됨
      • 건축 분야에서 패턴에 대한 논의가 처음으로 시작됨
        • Christopher Alexander– A pattern language, 1977
        • 도시 환경을 설계하기 위한 ‘언어’ 를 설명하며 언어의 단위를 패턴으로 정리
      • "각 패턴은 우리 주변에서 자주 반복해서 발생하는 문제와 그 문제를 해결하는 핵심을 기술해 동일한 일을 두 번 다시 하지 않고 해결할 수 있도록 한다."
        • 바퀴를 다시 발명하지 마라 (Don't reinvent the wheel)
  • 역사
    • 1987, Ward Cunningham과 Kent Beck가 Alexander의 패턴 아이디어를 객체 지향 설계로 확장
      • Smalltalk 언어를 기반으로 패턴을 설명
      • “Using pattern languages for object-oriented programs”, OOPSLA-87
    • 1987, Erich Gamma가 “important of patterns and how to capture them”에 대한 주제로 학위논문출간
    • 1992, Jim Coplien이 C++언어 기반의 몇몇패턴을설명한 Erich “Advanced C++ Programming Styles and Idioms” 출간
  • Design Patterns of GoF
    • 1993-1994, design pattern에 대한 workshops이 진행됨
      • Erich Gamma, Richard Hem, Ralph Johnson, John Vlissides에 의해 1994년 “Design Patterns: Elements of Reusable Object-Oriented Software”라는 책이 출간됨
        • 위 저자를 Gang of Four (GoF)라고부르며,책에서는 23개의 패턴을 소개
  • Why Patterns?
    • 검증된 해결책
      • 디자인 패턴은 SW 디자인의 일반적인 문제들에 대해 저명한 개발자에 의해 여러 시도되고 검증된 해결책을 모은 것
        • 특정 문제에 대한 giant 들의 생각을 볼 수 있음 (lesson)
        • 더 좋은 설계를 기반으로 견고하고 재사용성이 높은 코드를 만들 수 있음
    • 팀원 간의 효율적인 의사소통
      • 팀원들이 더 효율적으로 의사소통 하는데 사용할 수 있는 공통의 언어를 정의
      • 객체를 하나만 생성하도록 제약하는 상황일 때
        1. 클래스의 생성자를 public 이 아닌 private 로 해서 외부에서 생성할 수 없도록 제약하고, 하나의 공유할 객체를 내부에서 ...., v.s.,
        2. Singleton 패턴을 쓰자!
  • Warning about Design Patterns
    • 디자인 패턴은 만병통치약이 아님
      • 검증된 해결책이지만, 패턴이 미칠 영향과 결과를 주의 깊게 생각하지 않고 무작정 남용하는 것은 결과적으로 악영향
        • 망치를 든 사람에겐 모든 게 못으로 보인다 (man-with-a-hammer syndrome)
      • 패턴을 쓰지 않고 간단하게 해결할 수 있다면, 항상 간단한 것을 선택!
        • 다른 모든 요소가 동일할 때 가장 단순한 설명이 최선 (Ockham's razor)
      • 설계상의 문제에 적합하다는 확신이 들 때 패턴을 도입
        • 이를 위해서는 맥락과 패턴을 정확하게 이해하고, 해당 맥락에 맞추어 사용해야 함
  • Categories of Design Patterns
    • 생성 패턴 (creational design patterns)
      • 객체 생성에 관련된 패턴
      • 객체의 생성과 조합을 캡슐화해 특정 객체가 생성되거나 변경되어도 프로그램 구조에 영향을 크게 받지 않도록 유연성을 제공
    • 구조 패턴 (structural design patterns)
      • 클래스나 객체를 조합해 더 큰 구조를 만드는 패턴
        ex) 서로 다른 인터페이스를 지닌 2개의 객체를 묶어 단일 인터페이스를 제공
    • 행위 패턴 (behavior design patterns)
      • 객체나 클래스 사이의 알고리즘이나 책임 분배에 관련된 패턴
      • 한 객체가 수행할 수 없는 작업을 여러 개의 객체로 어떻게 분배할지, 그렇게 하면서도 객체 사이의 결합도를 최소화하는 것에 중점
  • 구조
    • 맥락 (Context)
      • 문제가 발생하는 여러 상황을 기술
      • 패턴이 적용될 수 있는 상황 (또는 패턴이 유용하지 못한 상황)
    • 문제 (Problem)
      • 패턴이 적용되어 해결될 필요가 있는 여러 디자인 이슈
      • 여러 제약 사항과 영향력도 문제 해결을 위해 고려해야 함
    • 해결 (Solution)
      • 문제를 해결하도록 설계를 구성하는 요소들과 요소들 사이의 관계, 책임, 협력 관계를 기술 (필요하면 코드도 같이 살펴봄)
  • 디자인 패턴의 표현 (class diagram, sequential diagram, etc.)

 

Singleton Pattern

lazy initialization

  • 싱글톤 패턴이란?
    • 하나의 클래스가 오직 하나의 인스턴스만 가질 수 있도록 하는 패턴
      • singleton = 단 하나의 원소만 가지는 칩함 (e.g., {0})
      • 생성 패턴에 속함
    • 싱글톤 패턴이 사용되는 맥락
      • 한 객체가 리소스를 많이 사용하는 경우 (생성 시 시간이 오래 걸리거나 메모리를 많이 쓰는 경우)
      • 객체를 만들 때 마다 리소스를 객체 수에 비례해서 사용하게 됨
      • 이런 경우 리소스 절약을 위해 하나만 만들고 (마치 전역변수 처럼) 공유해서 쓸 수 있을까?
    • 싱글톤 패턴이 사용되는 예시
      • DB 연결 (커넥션 풀), 파일, 스레드풀, 로깅 등등
  • 싱글톤 패턴의 표현
    • 싱글톤 패턴은 단일 클래스에 대한 내용이기 때문에 클래스 다이어그램으로는 클래스 하나로 간단하게 표현됨
      • 오직 하나의 인스턴스만 가져야하기 때문에 객체는 클래스 내부에서 만들어 공유해 사용하고 생성자는 클라이언트가 볼 수 없게 함
        • 생성자가 private 로 지정해서 생성 제한
        • static 으로 공유
      • 클라이언트는 getInstance 메소드를 통해 인스턴스에 접근할 수 있음
    • 표현은 간단하나 여러가지 구현 방법이 있음
  • 싱글톤 패턴의 구현 방법
    • 아래 방식은 모두 싱글톤 패턴을 지향하나 각각 장단점이 존재
      • Lazy initialization
      • Thread-safe initialization
      • Eager initialization
      • Double-checked locking
      • Lazy holder (Bill Pugh's solution)
      • Enum method

 

Implementation

1. lazy initailization

public class Settings {
    private static Settings instance;
    
    private Settings() { }
    
    public static Settings getInstance() {
    	if (instance == null) {
        	instance = new Settings();
        }
        return instance;
    }
}

Settings settings = Settings.getInstance();
  • 설명
    • 멤버 변수 private static 선언
      • 하나의 instance 로 공유, 외부에서 접근 X
    • 생성자 private 선언
      • 외부에서 인스턴스를 생성할 수 없어야 함
    • getInstance 메소드 public static 선언
      • global 하게 접근할 수 있어야 함
    • if (instance == null) { instance = new Setting(); }
      • 실제로 인스턴스를 써야하는 시점에 생성 (lazy) 
      • 객체가 사용되지 않고 있는 상황이라면 불필요하게 메모리를 차지하지 않음
  • 문제점
    • 멀티 쓰레드 환경에서 인스턴스가 여러 존재 할 수 있음 (no thread-safe)
    • 쓰레드는 리소스를 공유하고, 실행단위를 기억하면서 순차적으로 수행
    • 예시 상황
      public class Settings {
          private static Settings instance;
          
          private Settings() {}
          
          public static Settings getInstance() {
          	if (instance == null) {
              	instance = new Settings();
              }
              return instance;
          }
      }
      • Thread A 와 Thread B 가 수행
      • A 가 if 문을 평가하고 내부로 진입 (아직 new 가 수행된 것은 아님)
      • 그때 B 가 if 문을 평가함, 아직 A 가 생성하지 않았기 때문에 if 문이 true (내부 진입)
      • 결과적으로 A와 B 둘다 if 문 내부로 들어왔기에 각자 객체를 생성
        • 싱글톤 개념에 위배

 

2. Thread-safe initialization

  • getInstance 메소드에 synchronized 키워드를 사용해서 한 번에 하나의 thread 만 들어오게 제약
    • 장점 : thread-safe
    • 단점 : synchronization 으로 인해 overhead 가 발생 (추가적인 성능 부하)
public static synchronized Setting getInstance() {
    if(instance == null) {
        instance = new Settings();
    }
    return instance;
}

 

 

3. Eager initialization

  • 한 번 미리 상수 (constant) 로 만들어 두고 사용하는 방법
  • static final 은 프로그램 로딩 시점에 만들어지기 때문에 thread-safe
  • 객체 생성 비용이 적으면 괜찮지만, 크다면 당장 사용하지 않을 때는 공간을 차지하는 문제가 있음 (공간 자원 낭비 가능성)
public class Settings{
    private static final Settings INSTANCE = new Settings();
    
    private Settings(){}
    
    public static Settings getInstance(){
    	return INSTANCE;
    }
}

 

 

4. Double-checked locking

  • Lazy 방식으로 생성하고 싶은데, 매번 동기화를 하지 않기 위해서, 최초 초기화 할 때만 동기화를 수행
    • 장점 : lazy initialization 이 가능하고 동기화 부담이 적음
    • 단점 : 코드 구성과 이해가 어려움 (JVM 1.5 이상부터 동작)
public class Settings{
    private static volatile Settings instance;
    
    private Settings() {}
    
    public static Settings getInstance(){
    	if(instance == null) {
            synchronized (Setting.class){
            	instance = new Settings();
            }
        }
        return instance;
}
  • 설명
    • volatile 은 변수를 캐시가 아니라 메모리에서 읽어오도록 지정 
      • 이런게 있다고만 알고 있으면 된다.
      • keyword 를 붙여줘야 정확하게 동작한다.

 

5. Lazy holder (Bill Pugh's solution)

  • 클래스 안에 내부 static 클래스 (holder) 를 두는 방식
    • 장점 : lazy initialization, thread-safe, 간결한 코드
    • 단점 : 클라이언트가 임의로 싱글톤 패턴을 파괴할 수도 있음
      • Reflection API, 직렬화/역직렬화 등
public class Settings{
    private Settings() {}
    
    public static class SettingsHolder{
    	private static final Settings INSTANCE = new Settings();
    }
    
    public static Settings getInstnace(){
    	return SettingsHolder.INSTANCE;
    }
}
  • 설명
    • 내부 클래스 holder 는 static 이기에, Settings 가 초기화되어도 내부 클래스는 메모리에 로딩되지 않음
    • final 로 지정함으로서 다시 값이 할당되지 않도록 방지
    • getInstance() 를 호출하면, 이때 내부 클래스가 한번 초기화 되면서 객체를 최초 생성

 

6. Enum method

public enum Settings{
    INSTANCE;
    
    private Settings() {}
    
    private static Settings getInstance(){
    	return INSTANCE;
    }
    
    public int getNumber(){
    	return number;
    }
}
  • 실제로 class 는 아니지만, class 처럼 구현
  • 열거 타입에 활용되는 키워드지만, enum 의 특성을 응용한 사례
    • 멤버를 만들 때 private 으로 만들고, 한번만 초기화 해 thread-safe
    • 상수 뿐 아니라 변수/메소드 선언해 사용 가능 (싱글톤 클래스 처럼 응용)
  • 코드가 간단하고 클라이언트가 임의 변경해 싱글톤을 깰 수 없음
  • 단점
    • 선언하는 순간 만들기 때문에 lazy 하지 않음
    • 클래스 상속이 필요할 때 enum 은 상속 불가능

 

일반적으로 권장되는 싱글톤 구현 방법

  • Lazy holder : 성능이 중요시 되는 환경
    • lazy initialization 이 가능
    • 그러나 ,클라이언트의 공격에 싱글톤이 깨질 여지가 있음
  • Enum : 안전성이 중요시 되는 환경
    • (살펴보지는 않았지만) reflection 및 직렬화 이슈에 자연스럽게 대응 가능
    • 그러나 eager initialization 와 비슷하기 때문에 불필요하게 메모리를 차지할 수도 있음

 

Singleton 사용 사례

  • Runtime (java.lang.Runtime)
    • JAVA 프로그램이 실행 되고 있는 환경에 대한 객체
      • 실행 환경 정보는 애플리케이션 전체에서 일관되게 유지되어야 하므로, 하나의 인스턴스만 존재해야 한다.
      • 다수의 Runtime 인스턴스가 존재하면 JVM 상태를 중복 관리하거나 혼란을 초래할 수 있다.
        public class RuntimeExample{
            public static void main(String[] args){
            	Runtime runtime = Runtime.getRuntime();
                System.out.println(runtime.maxMemory());
                System.out.println(runtime.freeMemory());
            }
        }
  • Logging (java.util.logging)
    • 프로그램 로그를 남기기 위한 라이브러리
      • 애플리케이션의 로그는 시스템 전반에서 수집되며, 이를 통합적으로 관리할 필요가 있다.
      • 다수의 Logger 인스턴스를 생성하면 로그 메시지의 흐름이 분산되거나 일관성이 깨질 수 있다.
      • 싱글톤 패턴을 사용하면 애플리케이션의 모든 로그를 중앙화된 하나의 Logger 인스턴스에서 처리할 수 있다.
        import java.util.logging.Level;
        import java.util.logging.Logger;
        
        public class HelloWorld{
            private static Logger logger = Logger.getLogger(HelloWorld.class.getName());
            
            public static void main(String[] args){
            	logger.info("This is level info logging");
                logger.log(Level.WARNING, "This is level warning logging");
                logger.log(Level.SEVERE, "This is level severe logging");
                System.out.println("Hello Java Logging APIs.");
            }
        }



Discussion

  • 싱글톤 패턴의 문제점
    • 모듈간 의존성 증가
      • 여러 모듈들이 하나의 싱글톤 객체를 사용하니 싱글톤 객체 변경시 이를 참조하는 다른 모듈에도 영향이 감
    • SOLID 원칙에 위배되는 사례가 많음
      • SRP 위배 : 클래스 본연의 작업 책임 + 인스턴스 접근 관리 역할 책임
      • OCP 위배 : 싱글톤은 무조건 단일 객체만을 생성하고 상속도 불가능
      • DIP 위배 : 인터페이스가 아니라 싱글톤 객체와 의존 관계가 설정
    • 단위 테스트가 어려움
      • 단위 테스트는 서로 독립적이어야 하는데, 싱글톤은 마치 전역 변수와 같이 공유 되기 때문에 테스트 순서에 따라 테스트 결과에 종속이 생길 수 있음
      • 많은 테스트 프레임워크들이 상속에 의존하는데 싱글톤은 상속을 할 수가 없음
  • 싱글톤 패턴으로 얻을 수 있는 이득
    • 클래스가 하나의 인스턴스만 가짐 (공유를 통해 리소스 절약)
    • 싱글턴 객체는 처음 요청될 때만 초기화 (객체 생성 비용 절약)
    • 해당 인스턴스에 대한 전역 접근을 할 수 있음 (전역 변수 처럼 사용)
  • 정리
    • 싱글톤 패턴은 한 개의 인스턴스를 보증하여 효율성을 확보할 수 있지만, 그에 따라 수반되는 여러 문제점이 있을 수 있음
      • 이러한 문제점으로 인해 싱글톤 패턴은 유연성이 많이 떨어지는 패턴이라고 함 (안티 패턴)
      • 해당 클래스의 객체가 무조건 한 개만 있어야 하는지, 여러 객체를 생성하면 효율에 악영향이 생기는지 등 꼭 필요한 상황인지 검토해야 함

 

 

 

 

 

 

 

반응형