Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- 모던 자바스크립트
- 유효시간 설정 url
- 딥다이브
- TypeORM
- 스프링부트
- 우아한 테크코스
- oauth
- Dev-Matching
- 음악 url 파일 다운로드
- 프론트엔드 과제
- 파일 url
- invalid_grant
- 코멘토 #코멘토실무PT #실무PT후기 #실무강의 #리액트강의 #웹프로그래밍 #react #웹개발실무
- api 요청 수 제한
- Deep Dive
- this
- 프리코스
- 검색
- 프로그래머스
- NestJS
- api 비동기처리
- redis
- AWS
- 우아한테크코스
- 타입스크립트
- bucket4j
- concurrency limit
- 프론트엔드
- 자바스크립트
- compateto
Archives
- Today
- Total
개발 알다가도 모르겠네요
Singleton Pattern 을 알아보자 본문
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를 실행중인 쓰레드가 이미 있다면 실행하지 않게함.
문제점 - 인스턴스가 여러 개 생긴다.
시나리오 (쓰레드 경합)
- Logger 인스턴스가 아직 생성되지 않았을 때 스레드 1이 getInstance 메서드의 if문을 실행해 이미 인스턴스가 생성되었는지 확인한다. 현재 instance 변수는 null인 상태다.
- 만약 스레드 1이 생성자를 호출해 인스턴스를 만들기 전 스레드 2가 if문을 실행 해 instance 변수가 null인지 확인한다. 현재 null이므로 인스턴스를 생성하는 코드, 즉 생성자를 호출하는 코드를 실행하 게 된다.
- 스레드 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;
}
...
}
- T1이 sync으로 들어갔을 때 T2도 instancerk null이기 때문에 T1 sync 끝날때 까지 기다렸다가 진입.
- T2 sync 진입 후 한번 더 instance null checking.
- 이외 instance 생겼으므로 바로 return.
실행결과
BUT,
public static Logger getInstance() {
if (instance == null) {
synchronized(Logger.class) {
if (instance == null)
instance = new Logger();
}
}
return instance;
}
- Logger 인스턴스를 위한 메모리 할당
- 생성자를 통한 초기화
- 할당된 메모리를 instance 변수에 할당
명령어 reorder를 통한 최적화 수행
public static Logger getInstance() {
if (instance == null) {
synchronized(Logger.class) {
if (instance == null)
instance = new Logger();
}
}
}
return instance;
}
- Logger 인스턴스를 위한 메모리 할당
- 할당된 메모리를 instance 변수에 할당
- 생성자를 통한 초기화
T1이 2까지 끝낸 후 T2가 실행됨.
Instance가 null이 아니므로 T2는 초기화 X인 instance 반환.
then, 잘못된 결과 반환 가능.
문제점 - 미완성 인스턴스를 사용
시나리오
- Logger 인스턴스가 아직 생성되지 않았을 때 쓰레드 1이 getInstance 메서드의 첫번째 if문을 실행해 이미 인스턴스가 생성되었는지 확인. 현재 instance 변수는 null인 상태.
- Logger 인스턴스를 위한 메모리 할당
- 할당된 메모리를 instance 변수에 할당
- 쓰레드 2가 getInstance 메소드의 첫번째 if문 실행하여 instance 변수가 성정 된 것을 확인하고 생성된(그러나 아직 초기화 되지 않은) 인스턴스를 반환 받음
- 쓰레드 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 키워드 없이도 동시성 문제를 해결.
- something 클래스가 로딩돼도 여전히 Lazyholder의 instatnce는 설정X.
- 누군가가 getInstance()를 호출하면 그때 Lazyholder 참조되면서 instance 생성.
- 효율적인 작업이 가능.
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 |