개발 알다가도 모르겠네요

Singleton Pattern 을 알아보자 본문

디자인패턴

Singleton Pattern 을 알아보자

이재빵 2021. 12. 10. 16:13
728x90

객체의 인스턴스가 오직 1개만 생성되는 패턴

애플리케이션이 시작될 때 어떤 클래스가 최초 한번만 메모리를 할당하고(Static) 그 메모리에 인스턴스를 만들어 사용.

싱글턴은 하나의 원소만을 갖는 집합을 말합니다.

인스턴스: 클래스 특성O

객체: 클래스 특정X

 

Example - Logger 만들기

공통 로그 파일에 모든 사용자 계좌의 입금 / 출금의 발생 내역을 기록

 

public class Logger {
private final String LOGFILE = "log.txt"; private PrintWriter writer;
public Logger() {
        try {
            FileWriter fw = new FileWriter(LOGFILE);
            writer = new PrintWriter(fw, true);
        } catch (IOException e) {}
    }
public void log (String message) {
SimpleDateFormat formatter= new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z"); 
Date date = new Date(System.currentTimeMillis()); 
writer.println(formatter.format(date) + ":" + message);
	} 
}
public class Account { 
private String owner; 
private int balance; 
private Logger myLogger;
    public Account(String owner, int balance) {
        this.owner = owner;
        this.balance = balance;
        this.myLogger = new Logger();
}
public String getOwner() { return owner; }
    public int getBalance() { return balance; }
public void deposit(int money) {
myLogger.log("owner" + " : " +this.getOwner() + " deposit " + money); 
balance += money;
    }
    public void withdraw(int money) {
if (balance >= money) {
myLogger.log("owner" + " : " +this.getOwner() + " withdraw " + money); 
balance -= money;
		} 
	}
}
public class Main {
    public static void main(String[] args) {
    Account acct1 = new Account("insang1", 1000000); 
    acct1.deposit(20000);
	Account acct2 = new Account("insang2", 2000000); 
    acct2.withdraw(5000);
 	} 
 }

 

실행 결과: log.txt

 

 

문제점:  Account 인스턴스가 생성될 때 마다 새로운 Logger 인스턴스 생성

해결책: 모든 Account 인스턴스가 하나의 Logger 인스턴스를 공유하도록 설계

 

public class Account {
    private String owner;
    private int balance;
    private Logger myLogger;
    public Account(String owner, int balance) {
        this.owner = owner;
        this.balance = balance;
    }
    
    public String getOwner() { return owner; }
    public int getBalance() { return balance; }
    
public void deposit(int money) {
myLogger.log("owner" + " : " +this.getOwner() + " deposit " + money); 
balance += money;
}

public void withdraw(int money) {
if (balance >= money) {
myLogger.log("owner" + " : " +this.getOwner() + " withdraw " + money); 
balance -= money;
	} 
}

public void setMyLogger(Logger myLogger) { 
this.myLogger = myLogger;
	} 
}
public class Main {
public static void main(String[] args) {
	Logger logger = new Logger();
	Account acct1 = new Account("insang1", 1000000); acct1.setMyLogger(logger);
	acct1.deposit(20000);
	Account acct2 = new Account("insang2", 2000000); acct2.setMyLogger(logger);
	acct2.withdraw(5000);
 	} 
 }

 

문제점: Logger 인스턴스를 생성하지 못하게 하는 수단을 제공하지 못 하고, 외부에서 제공받아야 함.

현재는 여러개가 생성 가능하며 원할 때 쓰지 못함.

public class Main {
    public static void main(String[] args) {
		Logger logger1 = new Logger();
		Account acct1 = new Account("insang1", 1000000); 
        acct1.setMyLogger(logger1); 
        acct1.deposit(20000);
		Account acct2 = new Account("insang2", 2000000); 
        Logger logger2 = new Logger(); 
        acct2.setMyLogger(logger2); 
        acct2.withdraw(5000);
        }
}

logger2가 logger1 덮어씌움.

 

해결책: 클래스가 하나의 인스턴스만을 가지도록 설계

  • static 변수 instance 선언
  • 생성자를 private으로
  • Logger의 (유일한) 인스턴스를 생성 및 반환하는 getInstance() 메소드 정의

 

 

Eager Initialization: Logger 클래스

public class Logger {
	private final String LOGFILE = "log.txt"; private PrintWriter writer;
	private static Logger instance = new Logger();  //Logger클래스 만들어질때 바로 생성됨.
    private Logger() {
		try {
			FileWriter fw = new FileWriter(LOGFILE); 
            writer = new PrintWriter(fw, true);
        } catch (IOException e) {}
    }
    
    public static Logger getInstance() { return instance; } 
    public void log (String message) {
 	SimpleDateFormat formatter= new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z"); 
    Date date = new Date(System.currentTimeMillis()); 
    writer.println(formatter.format(date) + " : " + message);
    } 
 }

외부에서 필요할 때 마다 -> Logger.getInstance();

동일한 인스턴스 얻을 수 있음.

 

public class Account { 
	private String owner; 
    private int balance; 
    private Logger myLogger;
    
 	public Account(String owner, int balance) { 
    	this.owner = owner;
		this.balance = balance;
		this.myLogger = Logger.getInstance();
    }
    
    public String getOwner() { return owner; }
    public int getBalance() { return balance; }
    
	public void deposit(int money) {
		myLogger.log("owner" + " : " +this.getOwner() + " deposit " + money); 
        balance += money;
	}
	public void withdraw(int money) {
		if (balance >= money) {
			myLogger.log("owner" + " : " +this.getOwner() + " withdraw " + money); 
            balance -= money;
	} 
}
}
public class Main {
	public static void main(String[] args) {
	Account acct1 = new Account("insang1", 1000000); 
    acct1.deposit(20000);
	Account acct2 = new Account("insang2", 2000000); 
    acct2.withdraw(5000);
	} 
}

 

실행결과

 

public class Main {
	public static void main(String[] args) {
	Account acct1 = new Account("insang1", 1000000); 
    acct1.deposit(20000);
	Account acct2 = new Account("insang2", 2000000); 
    acct2.withdraw(5000);
    acct2.withdraw(3000);
	acct1.withdraw(5000);
	} 
}

실행결과

 

 

문제점  - 클래스 로딩 시점에 초기화되어 인스턴스가 필요하지 않는 경우에도 생성

해결책

  • Lazy Initialization
  • 인스턴스가 필요할 때 생성

 

Lazy Initialization

public class Logger {
	private final String LOGFILE = "log.txt"; 
    private PrintWriter writer;
	private static Logger instance;
	
    private Logger() {
		try {
			FileWriter fw = new FileWriter(LOGFILE); 
            writer = new PrintWriter(fw, true);
        } catch (IOException e) {}
    }
	public static Logger getInstance() {
		if (instance == null) instance = new Logger(); return instance;
	}
	public void log (String message) {
		SimpleDateFormat formatter= new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z");
        Date date = new Date(System.currentTimeMillis()); 
        writer.println(formatter.format(date) + " : " + message);
    } 
 }

 

다중 쓰레드 환경

public class User extends Thread {
	public User(String name) { super(name); } 
	public void run() {
		Random r = new Random();
		Account acct = new Account(Thread.currentThread().getName(), r.nextInt(1000000));
        if (r.nextBoolean()) acct.withdraw(r.nextInt(acct.getBalance()));
		else acct.deposit(r.nextInt(acct.getBalance()));
     } 
}
public class Main {
    public static void main(String[] args) {
	User[] users = new User[10]; 
    for (int i = 0; i < 10; i++) {
    	users[i] = new User("insang"+i);
    	users[i].start();
		}
	} 
}

 

실행결과

 

public class Logger {
	private final String LOGFILE = "log.txt"; 
    private PrintWriter writer;
	private static Logger instance;
	private Logger() {
		try {
		FileWriter fw = new FileWriter(LOGFILE); 
        writer = new PrintWriter(fw, true);
        	} catch (IOException e) {}
    	}
 
public static Logger getInstance() {
	if (instance == null) instance = new Logger(); 
    return instance;
}

public void log (String message) {
	System.out.println(this.toString());
	SimpleDateFormat formatter= new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z"); 
    Date date = new Date(System.currentTimeMillis()); 
    writer.println(formatter.format(date) + " : " + message);
   } 
}

따라서 임계구역을 만들어 getInstance를 실행중인 쓰레드가 이미 있다면 실행하지 않게함.

 

 

문제점 - 인스턴스가 여러 개 생긴다.

시나리오 (쓰레드 경합) 

  1. Logger 인스턴스가 아직 생성되지 않았을 때 스레드 1 getInstance 메서드의 if문을 실행해 이미 인스턴스가 생성되었는지 확인한다. 현재 instance 변수는 null인 상태다.
  2. 만약 스레드 1이 생성자를 호출해 인스턴스를 만들기 전 스레드 2 if문을 실행 해 instance 변수가 null인지 확인한다. 현재 null이므로 인스턴스를 생성하는 코드, 즉 생성자를 호출하는 코드를 실행하 게 된다.
  3. 스레드 1도 스레드 2와 마찬가지로 인스턴스를 생성하는 코드를 실행하게 되면 결과적으로 instance 클래스의 인스턴스가 2개 생성된다.

 

해결책: 동기화

Synchronized로 동기화하는 방법

public class Logger {
	private final String LOGFILE = "log.txt"; 
    private PrintWriter writer;
	private static Logger instance;
	private Logger() {
		try {
			FileWriter fw = new FileWriter(LOGFILE); 
            writer = new PrintWriter(fw, true);
            } catch (IOException e) {}
         }
	public synchronized static Logger getInstance() { 
    	if (instance == null) instance = new Logger(); 
        	return instance;
         }
    public void log (String message) {
		System.out.println(this.toString());
		SimpleDateFormat formatter= new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z");
        Date date = new Date(System.currentTimeMillis()); 
        writer.println(formatter.format(date) + " : " + message);
     } 
}

 

실행결과

 

 

DCL(Double Checked Locking)

Synchronized를 이용하는 방식은 성능적으로 효율적이지 않음.

계속 instance null checking하고, 다른 쓰레드들은 기다려야 하기 때문.

public class Logger { 
	...
	public static Logger getInstance() { 
    	if (instance == null) {
			synchronized(Logger.class) { 
            if (instance == null) {
				instance = new Logger(); return instance;
		}
     }
     return instance;
     }
  ...
}
  1. T1이 sync으로 들어갔을 때 T2도 instancerk null이기 때문에 T1 sync 끝날때 까지 기다렸다가 진입.
  2. T2 sync 진입 후 한번 더 instance null checking.
  3. 이외 instance 생겼으므로 바로 return.

 

 

실행결과

 

BUT,

public static Logger getInstance() { 
	if (instance == null) {
		synchronized(Logger.class) { 
        if (instance == null)
			instance = new Logger();
		} 
	}
	return instance;
 }
  1. Logger 인스턴스를 위한 메모리 할당
  2. 생성자를 통한 초기화
  3. 할당된 메모리를 instance 변수에 할당

 

명령어 reorder를 통한 최적화 수행

public static Logger getInstance() { 
	if (instance == null) {
		synchronized(Logger.class) { 
        	if (instance == null)
  				instance = new Logger();
			}
		} 
	}
	return instance;
}
  1. Logger 인스턴스를 위한 메모리 할당
  2. 할당된 메모리를 instance 변수에 할당
  3. 생성자를 통한 초기화

T1이 2까지 끝낸 후 T2가 실행됨.

Instance가 null이 아니므로 T2는 초기화 X인 instance 반환.

then, 잘못된 결과 반환 가능.

 

문제점 - 미완성 인스턴스를 사용

시나리오

  1. Logger 인스턴스가 아직 생성되지 않았을 때 쓰레드 1 getInstance 메서드의 첫번째 if문을 실행해 이미 인스턴스가 생성되었는지 확인. 현재 instance 변수는 null인 상태.
  2. Logger 인스턴스를 위한 메모리 할당
  3. 할당된 메모리를 instance 변수에 할당
  4. 쓰레드 2 getInstance 메소드의 첫번째 if문 실행하여 instance 변수가 성정 된 것을 확인하고 생성된(그러나 아직 초기화 되지 않은) 인스턴스를 반환 받음
  5. 쓰레드 2가 로깅하려고 하면 문제가 발생.

 

해결책 - Volatile를 사용하여 명령어 reorder 금지

public class Logger {
	private final String LOGFILE = "log.txt"; 
    private PrintWriter writer;
	private volatile static Logger instance; 
    private Logger() {
		try {
			FileWriter fw = new FileWriter(LOGFILE); 
            writer = new PrintWriter(fw, true);
        } catch (IOException e) {}
    }
... 
}

 

 

Initialization on demand holder idiom

Demand(또는 Lazy) Holder 방식은 가장 많이 사용되는 싱글턴 구현 방식.

Lazy -> 우리가 원할 때 인스턴스 생성.

volatile 이나 synchronized 키워드 없이도 동시성 문제를 해결.

 

 

  1. something 클래스가 로딩돼도 여전히 Lazyholder의 instatnce는 설정X.
  2. 누군가가 getInstance()를 호출하면 그때 Lazyholder 참조되면서 instance 생성.
  3. 효율적인 작업이 가능. 
public class Logger {
	private final String LOGFILE = "log.txt"; 
    private PrintWriter writer;
	private static Logger instance;
	private Logger() {
		try {
			FileWriter fw = new FileWriter(LOGFILE); 
            writer = new PrintWriter(fw, true);
		} catch (IOException e) {} 
     }
     
	private static class LazyHolder {
		public static final Logger INSTANCE = new Logger();
	}
    
	public static Logger getInstance() { return LazyHolder.INSTANCE; }
		public void log (String message) {
			System.out.println(this.toString());
			SimpleDateFormat formatter= new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z"); 
            Date date = new Date(System.currentTimeMillis()); 
            writer.println(formatter.format(date) + " : " + message);
            }
} }

 



'디자인패턴' 카테고리의 다른 글

Command Pattern 을 알아보자  (0) 2021.12.11
Strategy Pattern 을 알아보자  (0) 2021.12.11
Builder Pattern 을 알아보자  (0) 2021.12.10
SOLID 설계원칙  (0) 2021.12.09
객체지향의 원리  (0) 2021.12.09