Development/Design Pattern

[Design Pattern] 싱글톤 패턴

우봉수 2023. 8. 29. 13:22

싱글턴 패턴

정의

하나의 인스턴스만을 가지는 class의 집합

static 키워드를 사용하여 구현 가능

메서드 실행 주체

클래스가 수행하는 메서드 → static

인스터스가 실행 → 일반

예제: 로그 출력기 만들기

의도: 모든 회원들의 입출금 내역을 파일에 저장

  • 계좌 클래스
public class Account {
    private int balance;
    private String owner;
    private Logger myLogger;
    public Account(String owner, int balance){
        this.balance = balance;
        this.owner = owner;
        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;
        }
    }
}
  • 로그 출력기 클래스
import java.util.Date;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;

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 data = new Date(System.currentTimeMillis());
        writer.println(formatter.format(data)+ " : " + message);
    }
}

<문제점>

public class Main {
    public static void main(String[] args) {
        Account acct1 = new Account("Wuhan",1000000);
        acct1.deposit(20000);
        Account acct2 = new Account("YongGang",20000);
        acct2.withdraw(20000);
    }
}

해당 코드 실행시 뒤에 결과로인해 앞에 결과가 덮어 씌어짐으로 사용자의 모든 입출력이 찍히는 의도가 작동되지 않음 즉 본래의 의도와 벗어남

(해결책) 싱글톤 패턴으로 수정한 출력기 클래스들

Eager Initialization

이른 초기화: 인스턴스를 프로그램이 시작되자 마자 미리 생성

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

import java.util.Date;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;

public class Logger{
    private static Logger instance = new Logger();
    private final String LOGFILE = "log.txt";
    private static PrintWriter writer;
    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 data = new Date(System.currentTimeMillis());
        writer.println(formatter.format(data)+ " : " + message);
    }
}

Lazy Initialization

늦은 초기화: Eager Initialization의 문제점을 해결하기 위해 인스턴스가 필요할 때 생성하는 방식

문제점: 다중 스레드 환경에서는 단일 인스턴스 생성이라는 싱글톤의 목적과는 다르게 여러 인스턴스가 생성될 위험이 있음

import java.util.Date;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;

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

<문제점 예시>

import java.util.Random;

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("suhan"+i);
            users[i].start();
        }
    }
}

코드 실행 결과

Logger@6ba60d46
Logger@3b1b95ed
Logger@1e628db7
Logger@70282c99
Logger@6a07b062
Logger@148cd591
Logger@7f20d3a2
Logger@71eac517
Logger@f37a491
Logger@33306c50

 

해결책: synchronized 키워드

synchronized 키워드 사용: instance를 임계구역으로 설정 한다면 다중 스레드 환경에서의 문제점을 해결 할 수 있다.

문제점: 비용이 비싸다 제일 처음에만 synchronized로 임계구역으로 설정한게 필요하지 그 이후 부터는 공유자원으로 보호 하지 않아도 되기 때문에 비효율적인 방식이 되어 버린다.

public class Logger{
    private static Logger instance;
    private final String LOGFILE = "log.txt";
    private static PrintWriter writer;
    private Logger(){
        try{
            FileWriter fw = new FileWriter(LOGFILE);
            writer = new PrintWriter(fw, true);
        }catch (IOException e){}
    }
    synchronized 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 data = new Date(System.currentTimeMillis());
        writer.println(formatter.format(data)+ " : " + message);
    }
}

 

해결책: DCL(Double Check Locking)

임계구역(Critical Section)에 진입하기 전에 두 번의 검사를 수행하여 해당 객체가 이미 생성되었는지 확인하는 방법 synchronized 키워드 문장을 실행사기 전에 조건문을 사용하여 특정 조건이 수행되지 않았는지 체크하고 임계구역으로 설정한다.

문제점: DCL 방식을 사용하더라도 생성자를 통한 초기화를 하기 전 인스턴스에 접근 하는 문제가 발생 할 수 있다

+ 단 Logger instance 를 volatile 키워드를 사용하여 순서 변경을 막는다면 문제점이 없다.

 

해결책: Initialization on demand holder idiom

내부 클래스로 만들어 해당 클래스를 통해 인스턴스를 반환함으로서 위에서 발생 할 수 있는 생성자를 통한 초기화를 하기 전 인스턴스에 접근 하는 문제 해결

참조 하기 전 (메모리 적재 x) -> 참조 후 (메모리 적재) 내부 클래스임으로 바로 생성 (원자적 행위)임으로 끼어들기 불가능

import java.util.Date;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;

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

    private static final class LoggerHolder {
        private static final Logger instance = new Logger();
    }

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

 

내용 요약

  • when: 단일 인스턴스로만 특정 클래스가 필요한 상황
  • how: static 키워드를 통해 정적 요소 생성 반환, private 접근 지정자로 생성자 외부 호출로 부터 보호
  • solve: 늦은 초기화(lazy initialization)을 활용하여 정적으로 클래스 본인 자체를 내부에 생성하고 호출시 처음 딱 한번 생성시키고 반환, 내부 클래스를 통해 로딩시 바로 해당 인스턴스가 생성되도록 구현
class SingleThon {
	// 생성자 private 접근 지정자로 외부 호출 제어
    private SingleThon(){}
    /* 
    
    그 밖에 맴버 변수 함수들..
    
    */
    private static final class InstanceHolder{
    	private static final SingleThon instance = new SingleThon();
    }
    // 처음 호출 시 딱 한번만 생성되고 그 이후는 기존에 있던 인스턴스 반환
    public static SingleThon getInstance(){
        return InstanceHolder.instance;
    }
}