[Design Pattern] 싱글턴 패턴(Singleton Pattern) 개념과 예제

싱글턴 패턴(Singleton Pattern)에 대해서 알아보자

해당 내용은 책 ‘JAVA 객체지향 디자인 패턴’을 참고해서 작성하였습니다.


환경

  • Java


싱글턴 패턴(Singleton Pattern)이란?

싱글턴 패턴(Singleton Pattern)

  • 싱글턴 패턴(Singleton Pattern) : 클래스가 오직 하나의 인스턴스만을 유지하도록 하는 패턴이며 한번 만들어지면 getInstance()와 같은 함수 또는 메소드 호출을 통해서 객체에 접근한다.
  • 이미지 출처 : https://en.wikipedia.org/wiki/Singleton_pattern


구현

일반적인 구현

  • 프린터를 만든다고 가정해보자.
  • 프린터는 하나의 객체만 만들어져야한다고 생각하면 싱글턴 패턴으로 아래처럼 구현이 가능하다.

Printer

public class Printer {
    private static Printer printer = null;
    private Printer() {}

    public static Printer getPrinter(){
        if(printer == null){
            printer = new Printer();
        }
        return printer;
    }

    public void print(String str){
        System.out.println(str);
    }
}

User

public class User {
    private String name;

    public User(String name){
        this.name = name;
    }

    public void print() {
        Printer.getPrinter().print(this.name + " " + "print using" + " " + Printer.getPrinter().toString());
    }
}

Main

public class Main {
    public static void main(String[] args) {
        final int CNT = 6;
        User[] user = new User[CNT];
        for (int i = 0; i < CNT; i++){
            user[i] = new User(i + "-user");
            user[i].print();
        }
    }
}

Result

0-user print using com.company.Printer@60e53b93
1-user print using com.company.Printer@60e53b93
2-user print using com.company.Printer@60e53b93
3-user print using com.company.Printer@60e53b93
4-user print using com.company.Printer@60e53b93
5-user print using com.company.Printer@60e53b93
  • 위 클래스에서는 객체가 있는지 확인하고 있으면 반환하고 없다면 만들어서 반환해주는 구조
  • static를 사용해서 printer 변수와 getPrinter() 메소드를 만들었으며 이렇게 했기에 이 둘은 클래스 자체에 속하며 인스턴스 없이도 호출이 및 사용이 가능하다.
  • 이러한 방식을 통해 단 하나의 객체만 만들어서 사용할 수 있다.
  • 결과를 보면 모두 동일한 프린터를 사용했다.


문제점

  • 만약 여러 스레드가 이 객체가 생성되기 전에 동시에 접근한다면 경합 조건(Race Condition)이 발생할 수 있다.
  • 객체를 만들기전에 스레드가 동시에 접근하면 객체를 여러개 만들게되는 상황에 직면할 수 있다.
  • 하단의 예제는 Thread.sleep(1)을 통해 객체를 여러개 생성하는 경우를 만들어본 결과이다.

Printer

public class Printer {
    private static Printer printer = null;
    private Printer() {}

    public static Printer getPrinter(){
        if(printer == null){
            try{
                Thread.sleep(1);
            }
            catch (InterruptedException e) { }
            printer = new Printer();
        }
        return printer;
    }

    public void print(String str){
        System.out.println(str);
    }
}

UserThread

public class UserThread extends Thread {
    public UserThread(String name){
        super(name);
    }

    public void run(){
        Printer.getPrinter().print(Thread.currentThread().getName()
                + " " + "print using" +
                " " + Printer.getPrinter().toString());
    }
}

Main

public class Main {
    public static void main(String[] args) {
        final int CNT = 6;
        UserThread[] user = new UserThread[CNT];
        for (int i = 0; i < CNT; i++){
            user[i] = new UserThread(i + "-user");
            user[i].start();
        }
    }
}

Result

0-user print using com.company.Printer@4f90c612
5-user print using com.company.Printer@17a724
4-user print using com.company.Printer@d156cd4
3-user print using com.company.Printer@4f90c612
1-user print using com.company.Printer@6fa76e7
2-user print using com.company.Printer@4f90c612
  • 이처럼 스레드가 동시에 접근하게 되면 하나의 객체만을 만들어서 사용하려는 싱글턴 패턴의 구현에 실패하게 된다.


문제 해결 구현법 - 정적 변수에 바로 초기화

  • 클래스에서 변수를 생성함과 동시에 객체를 만든다.
  • private static Printer printer = new Printer();처럼 생성과 동시에 객체를 만들어준다.

Printer

public class Printer {
    private static Printer printer = new Printer();
    private Printer() {}

    public static Printer getPrinter(){
        return printer;
    }

    public void print(String str){
        System.out.println(str);
    }
}

UserThread

public class UserThread extends Thread {
    public UserThread(String name){
        super(name);
    }

    public void run(){
        Printer.getPrinter().print(Thread.currentThread().getName()
                + " " + "print using" +
                " " + Printer.getPrinter().toString());
    }
}

Main

public class Main {
    public static void main(String[] args) {
        final int CNT = 6;
        UserThread[] user = new UserThread[CNT];
        for (int i = 0; i < CNT; i++){
            user[i] = new UserThread(i + "-user");
            user[i].start();
        }
    }
}

Result

3-user print using com.company.Printer@651f2f3d
1-user print using com.company.Printer@651f2f3d
2-user print using com.company.Printer@651f2f3d
0-user print using com.company.Printer@651f2f3d
4-user print using com.company.Printer@651f2f3d
5-user print using com.company.Printer@651f2f3d
  • 모두 똑같이 하나의 객체에 접근했음을 알 수 있다.


문제 해결 구현법 - 인스턴스 생성 메소드 동기화

  • synchronized를 이용해 여러 스레드가 동시에 메소드에 접근하는걸 방지한다.

Printer

public class Printer {
    private static Printer printer = null;
    private Printer() {}

    public synchronized static Printer getPrinter(){
        if(printer == null){
            printer = new Printer();
        }
        return printer;
    }

    public void print(String str){
        System.out.println(str);
    }
}

UserThread

public class UserThread extends Thread {
    public UserThread(String name){
        super(name);
    }

    public void run(){
        Printer.getPrinter().print(Thread.currentThread().getName()
                + " " + "print using" +
                " " + Printer.getPrinter().toString());
    }
}

Main

public class Main {
    public static void main(String[] args) {
        final int CNT = 6;
        UserThread[] user = new UserThread[CNT];
        for (int i = 0; i < CNT; i++){
            user[i] = new UserThread(i + "-user");
            user[i].start();
        }
    }
}

Result

3-user print using com.company.Printer@8f62e66
4-user print using com.company.Printer@8f62e66
0-user print using com.company.Printer@8f62e66
1-user print using com.company.Printer@8f62e66
2-user print using com.company.Printer@8f62e66
5-user print using com.company.Printer@8f62e66
  • 모두 똑같이 하나의 객체에 접근했음을 알 수 있다.


문제점

  • 스레드를 2가지 방식으로 해결했어도 아래처럼 클래스에서 상태를 유지해야하는 변수가 있다면 이야기가 달라진다.
  • cnt의 값이 일관적이지 않다.

Printer

public class Printer {
    private static Printer printer = null;
    private int cnt = 0;
    private Printer() {}

    public synchronized static Printer getPrinter(){
        if(printer == null){
            printer = new Printer();
        }
        return printer;
    }

    public void print(String str){
        cnt++;
        System.out.println(str + " " + "cnt : " + cnt);
    }
}

UserThread

public class UserThread extends Thread {
    public UserThread(String name){
        super(name);
    }

    public void run(){
        Printer.getPrinter().print(Thread.currentThread().getName()
                + " " + "print using" +
                " " + Printer.getPrinter().toString());
    }
}

Main

public class Main {
    public static void main(String[] args) {
        final int CNT = 6;
        UserThread[] user = new UserThread[CNT];
        for (int i = 0; i < CNT; i++){
            user[i] = new UserThread(i + "-user");
            user[i].start();
        }
    }
}

Result

2-user print using com.company.Printer@6fa76e7 cnt : 2
1-user print using com.company.Printer@6fa76e7 cnt : 4
3-user print using com.company.Printer@6fa76e7 cnt : 3
0-user print using com.company.Printer@6fa76e7 cnt : 2
4-user print using com.company.Printer@6fa76e7 cnt : 5
5-user print using com.company.Printer@6fa76e7 cnt : 6


문제 해결 구현법 - 메소드 안에 동기화 부분 추가

  • synchronized()를 이용해 스레드의 동시 접근을 막을 부분을 감싸주면 된다.

Printer

public class Printer {
    private static Printer printer = null;
    private int cnt = 0;
    private Printer() {}

    public synchronized static Printer getPrinter(){
        if(printer == null){
            printer = new Printer();
        }
        return printer;
    }

    public void print(String str){
        synchronized (this){
            cnt++;
            System.out.println(str + " " + "cnt : " + cnt);
        }
    }
}

UserThread

public class UserThread extends Thread {
    public UserThread(String name){
        super(name);
    }

    public void run(){
        Printer.getPrinter().print(Thread.currentThread().getName()
                + " " + "print using" +
                " " + Printer.getPrinter().toString());
    }
}

Main

public class Main {
    public static void main(String[] args) {
        final int CNT = 6;
        UserThread[] user = new UserThread[CNT];
        for (int i = 0; i < CNT; i++){
            user[i] = new UserThread(i + "-user");
            user[i].start();
        }
    }
}

Result

0-user print using com.company.Printer@4f90c612 cnt : 1
5-user print using com.company.Printer@4f90c612 cnt : 2
4-user print using com.company.Printer@4f90c612 cnt : 3
2-user print using com.company.Printer@4f90c612 cnt : 4
1-user print using com.company.Printer@4f90c612 cnt : 5
3-user print using com.company.Printer@4f90c612 cnt : 6
  • 이제 cnt 값도 정상적으로 나오며 객체도 하나만 생성된다.


싱글턴 패턴과 정적 클래스의 비교


참고자료