[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
값도 정상적으로 나오며 객체도 하나만 생성된다.
싱글턴 패턴과 정적 클래스의 비교
- 정적 클래스도 싱글턴처럼 클래스를 이용해 메소드 사용이 가능하다는 점과 전역에 걸쳐서 하나만 사용한다는 공통점이 있지만 정적 클래스는 인터페이스를 구현할 수 없는 반면 싱글턴은 인터페이스를 구현할 수 있다는 장점이 있다.
- Stack Overflow 링크: https://stackoverflow.com/questions/519520/difference-between-static-class-and-singleton-pattern