싱글톤 패턴

임준성·2023년 3월 28일
1

CS

목록 보기
4/9




디자인 패턴(Design Pattern)

  • 객체 지향 프로그래밍 설계를 할 때 자주 발생하는 문제들을 피하기 위해 사용되는 패턴

    "바퀴를 다시 발명하지 마라" 라는 말이 있다. 잘 작동되고 있는 것을 처음부터 다시 개발할 필요는 없다는 것을 의미한다.



패턴(Pattern)

  • 공통의 언어를 만들어 의사 소통을 원활하게 한다.

  • 각기 다른 소프트웨어 모듈이나 기능을 가진 다양한 응용 소프트웨어 시스템들을 개발할 때도 서로 공통되는 설계 문제가 존재하며, 이를 처리하는 해결책 사이에도 공통점이 있다. 이러한 유사점을 패턴이라고 한다.



디자인 패턴의 구조

  • 콘텍스트(Context)
    문제가 발생하는 여러 상황을 기술
    패턴이 적용될 수 있는 상황을 나타내며,
    경우에 따라서는 이러한 패턴이 유용하지 못한 상황도 존재함.


  • 문제(problem))
    패턴이 적용되어 해결될 필요가 있는 여러 디자인 이슈들을 기술하며, 여러 제약 사항과 영향력도 고려해야 한다.


  • 해결(solution))
    문제를 해결하도록 설계를 구성하는 요소들과 그 요소들 사이의 관계, 책임, 협력 관계를 기술한다.



싱글톤 패턴(Singleton Pattern)

Instance가 오직 한개만 생성되는 것을 보장하고, 어디서든 동일한 Instance에 접근할 수 있도록 하는 ( 전역적으로 접근할 수 있도록 제공하는 ) 디자인 패턴.

생성 패턴의 한 종류이며, 생성 패턴은 객체 생성에 관련된 패턴으로 객체의 생성과 조합을 캡슐화하여 특정 객체가 생성되거나 변경돼도 프로그램 구조에 영향을 크게 받지 않는 유연성을 제공한다.



구현방법

  1. 생성자를 private으로 선언하여 외부에서 인스턴스를 직접 생성할 수 없도록 한다.

  2. 클래스 내부에서 유일한 인스턴스를 생성한다.

  3. 외부에서 해당 클래스의 인스턴스를 요청할 경우, 이미 생성된 인스턴스를 반환하는 getInstance() 메서드를 제공한다.



예시

회사에서 프린터기가 1대밖에 없어서 모든 직원이 한 대의 프린터를 사용해야 할 때의 상황이 현실에서 싱글톤 패턴이 적용된 것과 같은 예시다.

public class Printer {
// 싱글톤 패턴을 위한 printer 인스턴스를 생성한다.
private static Printer printer;

// 외부에서 인스턴스화를 막기 위한 private 생성자를 만든다.
private Printer() { }

// 인스턴스를 얻는 메소드를 생성한다.
public static Printer getInstance() {
    // 인스턴스가 없다면 새로 생성한다.
    if (printer == null) {
        printer = new Printer();
    }
    // 인스턴스를 반환한다.
    return printer;
}

// 프린트 메소드를 생성한다.
public void print(final String message) {
    System.out.println(message);
	}
}


  // User 클래스를 생성한다.
public class User {
// 이름을 저장하기 위한 변수를 생성한다.
private final String name;

// 생성자를 생성하여 이름을 저장한다.
public User(String name) {
    this.name = name;
}
  

// 프린터를 이용해 출력하는 메소드를 생성한다.
public void print() {
    // 싱글톤 패턴으로 만들어진 인스턴스를 얻는다.
    Printer printer = Printer.getInstance();
    // 이름과 사용한 프린터의 정보를 함께 출력한다.
    printer.print(this.name + " print using " + printer);
	}
}
  
  
  
// JUnit5를 이용하여 Printer 클래스를 테스트하는 클래스를 생성한다.
import org.junit.jupiter.api.Test;

class PrinterTest {
// 단일 스레드로 프린터를 테스트하는 메소드를 생성한다.
@Test
void testSingleThreadPrinter() {
    // 사용자 수를 설정한다.
    final int userNumber = 5;
    // 사용자를 저장할 배열을 생성한다.
    User[] users = new User[userNumber];

    // 사용자를 생성하고 각각 프린트한다.
    for (int i = 0; i < userNumber; i++) {
        users[i] = new User((i + 1) + "-user");
        users[i].print();
    	}
	}
}
  • 출력결과

Print Instance가 똑같이 출력되는 것을 확인할 수 있다.




싱글톤패턴의 장점

  1. 메모리 측면에서 유리하다.
    한번의 new로 인스턴스를 사용하기 때문에 메모리 낭비를 방지할 수 있다.
    -> 두 번째 이용시 부터는 객체 로딩 시간이 현저하게 줄어들어 성능이 좋아진다.


  2. 다른 클래스의 인스턴스들이 데이터를 공유하기 쉽다.
    싱글톤으로 만들어진 클래스의 인스턴스는 전역 인스턴스이기 때문이다.




싱글톤패턴의 문제점

  1. 테스트가 어렵다.
    싱글톤 패턴은 전역 상태를 유지하므로, 테스트 코드 작성 시에 매우 불편하다. 특히 다른 객체와의 의존성이 높은 경우에는 테스트를 작성하기 어렵다.


  2. 다중 스레드 환경에서 안전하지 않다.
    싱글톤 인스턴스는 하나만 존재해야 하므로, 여러 스레드에서 동시에 접근하면 인스턴스가 중복 생성될 수 있다. 이러한 문제를 해결하기 위해서는 동기화 처리가 필요한데, 동기화 처리로 인해 성능 저하가 발생할 수 있다.



  3. 유연성이 떨어진다.
    싱글톤 인스턴스는 생성 시점에 초기화되기 때문에, 런타임에 다른 객체로 변경할 수 없다. 이로 인해 어플리케이션의 기능을 확장하거나 변화시키기가 어렵다.


  4. 객체 간 결합도가 높아진다.
    싱글톤 인스턴스는 전역적으로 참조될 수 있기 때문에, 다른 객체들과의 결합도가 높아진다. 이는 유지보수성을 저하시키고, 어플리케이션의 모듈성을 낮추는 요인이 된다.


  5. 메모리 누수 가능성이 존재한다.
    싱글톤 인스턴스는 어플리케이션이 종료될 때까지 메모리에 유지된다. 이는 메모리 누수가 발생할 가능성을 높이는 요인이 된다.


해결방법

  1. 객체의 이른 초기화(eager initialization) 사용


    이른 초기화 방법은 클래스 로딩 시점에서 인스턴스를 생성한다. 이로 인해 다른 메서드에서 해당 인스턴스를 처음으로 사용할 때 초기화 작업이 필요하지 않아 초기화 시간을 절약할 수 있다. 하지만, 클래스 로딩 시점에서 인스턴스가 생성되므로, 해당 클래스가 매우 크거나 생성과 초기화 비용이 많이 드는 경우, 초기화되지 않은 객체도 메모리에 존재하게 되므로 메모리 낭비가 발생할 수 있습니다.


  2. synchronized 키워드 사용

    public static synchronized Printer getInstance() 

    메소드에 synchronized 키워드를 사용할 수 있다.

    synchronized를 걸면, 해당 함수가 포함된 해당 객체(this)에 lock을 거는 것과 같으며,
    두 개의 thread가 각기 다른 함수를 synchronized 함수를 호출하지만, 객체에 lock이 걸리기 때문에 동시에 호출할 수 없다.

    그러나 단지 메소드가 중요하다는 이유로 synchronized를 호출하면, 자바 내부적으로 메서드나 변수에 동기화를 하기 위해 block과 unblock을 처리하게 되어 성능 저하를 일으킬 수 있다.

    메소드 블록에 synchronized를 통해 동기화를 하면 극단적인 경우에서는 성능이 100배 이상 저하될 수 있다고 함.




  3. double check locking


    메소드 블록에서 synchronized를 통한 동기화는 해당 메소드를 호출할 때 마다 동기화를 하지만,
    위의 코드와 같이 메소드 내부에서 인스턴스가 null 일 때만 동기화를 설정하게 하면 성능성에서 이점을 가져갈 수 있다.







출처
https://zzang9ha.tistory.com/392
https://tourspace.tistory.com/54
https://coding-restaurant.tistory.com/144 https://jeonyoungho.github.io/posts/%EB%94%94%EC%9E%90%EC%9D%B8%ED%8C%A8%ED%84%B4/

profile
아무띵크 있이

0개의 댓글