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

업데이트(2020.07.18): 예제 및 코드 변경

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


환경

  • Java


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

싱글턴 패턴(Singleton Pattern)

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


구현

일반적인 구현

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

Bed

public class Bed {
    private static Bed bed = null;

    private Bed() {
    }

    public static Bed getBed() {
        if (bed == null) {
            bed = new Bed();
        }
        return bed;
    }

    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() {
        Bed.getBed().print(this.name + " " + "is in" + " " + Bed.getBed().toString() + " " + "bed");
    }
}

Main

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

Result

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


문제점

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

Bed

public class Bed {
    private static Bed bed = null;

    private Bed() {
    }

    public static Bed getBed() {
        if (bed == null) {
            try {
                Thread.sleep(10);
            } catch (Exception e) {
                System.out.println(e.toString());
            }
            bed = new Bed();
        }
        return bed;
    }

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

User

public class User extends Thread {
    private String name;

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

    public void run() {
        Bed.getBed().print(this.name + " " + "is in" + " " + Bed.getBed().toString() + " " + "bed");
    }
}

Main

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

Result

0-user is in com.company.Bed@4d0c434e bed
2-user is in com.company.Bed@4d0c434e bed
5-user is in com.company.Bed@135b88ad bed
1-user is in com.company.Bed@17733f02 bed
3-user is in com.company.Bed@4d0c434e bed
4-user is in com.company.Bed@2c79921e bed
  • 이처럼 스레드가 동시에 접근하게 되면 하나의 객체만을 만들어서 사용하려는 싱글턴 패턴의 구현에 실패하게 된다.


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

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

Bed

public class Bed {
    private static final Bed bed = new Bed();

    private Bed() {
    }

    public static Bed getBed() {
        if (bed == null) {
            try {
                Thread.sleep(10);
            } catch (Exception e) {
                System.out.println(e.toString());
            }
        }
        return bed;
    }

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

User

public class User extends Thread {
    private String name;

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

    public void run() {
        Bed.getBed().print(this.name + " " + "is in" + " " + Bed.getBed().toString() + " " + "bed");
    }
}

Main

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

Result

1-user is in com.company.Bed@6fa76e7 bed
3-user is in com.company.Bed@6fa76e7 bed
2-user is in com.company.Bed@6fa76e7 bed
0-user is in com.company.Bed@6fa76e7 bed
4-user is in com.company.Bed@6fa76e7 bed
5-user is in com.company.Bed@6fa76e7 bed
  • 모두 똑같이 하나의 객체에 접근했음을 알 수 있다.


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

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

Bed

public class Bed {
    private static Bed bed = null;

    private Bed() {
    }

    public synchronized static Bed getBed() {
        if (bed == null) {
            try {
                Thread.sleep(10);
            } catch (Exception e) {
                System.out.println(e.toString());
            }
            bed = new Bed();
        }
        return bed;
    }

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

User

public class User extends Thread {
    private String name;

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

    public void run() {
        Bed.getBed().print(this.name + " " + "is in" + " " + Bed.getBed().toString() + " " + "bed");
    }
}

Main

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

Result

5-user is in com.company.Bed@6fa76e7 bed
2-user is in com.company.Bed@6fa76e7 bed
0-user is in com.company.Bed@6fa76e7 bed
4-user is in com.company.Bed@6fa76e7 bed
3-user is in com.company.Bed@6fa76e7 bed
1-user is in com.company.Bed@6fa76e7 bed
  • 모두 똑같이 하나의 객체에 접근했음을 알 수 있다.


문제점

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

Bed

public class Bed {
    private static Bed bed = null;
    private static int cnt = 0;

    private Bed() {
    }

    public synchronized static Bed getBed() {
        if (bed == null) {
            bed = new Bed();
        }
        return bed;
    }

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

User

public class User extends Thread {
    private String name;

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

    public void run() {
        Bed.getBed().print(this.name + " " + "is in" + " " + Bed.getBed().toString() + " " + "bed");
    }
}

Main

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

Result

1-user is in com.company.Bed@1f904935 bed cnt: 2
4-user is in com.company.Bed@1f904935 bed cnt: 2
0-user is in com.company.Bed@1f904935 bed cnt: 2
3-user is in com.company.Bed@1f904935 bed cnt: 3
2-user is in com.company.Bed@1f904935 bed cnt: 3
5-user is in com.company.Bed@1f904935 bed cnt: 4


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

  • synchronized()를 이용해 스레드의 동시 접근을 막을 부분을 감싸주면 된다.
  • synchronized(this)는 해당 객체에 대한 동기화를 의미합니다.

Bed

public class Bed {
    private static Bed bed = null;
    private static int cnt = 0;

    private Bed() {
    }

    public synchronized static Bed getBed() {
        if (bed == null) {
            bed = new Bed();
        }
        return bed;
    }

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

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

User

public class User extends Thread {
    private String name;

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

    public void run() {
        Bed.getBed().print(this.name + " " + "is in" + " " + Bed.getBed().toString() + " " + "bed");
    }
}

Main

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

Result

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


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


참고자료