객체지향 디자인패턴(싱글톤, 전략, 상태)

JP·2022년 1월 21일
0

자바

목록 보기
3/10

싱글톤 패턴

  • 싱글톤 패턴은 클래스의 인스턴스화를 제한하고 단 하나의 인스턴스만 존재하도록 하는 것이다.
  • 싱글톤 클래스는 클래스의 인스턴스를 가져오기 위한 전역적으로 접근 가능한 포인트가 있어야 한다.
  • 로깅, 드라이버 개체, 캐싱 및 스레드 풀에서 사용 된다.
  • Abstract Factory, Builder, Prototype, Facade 등의 디자인 패턴에서도 사용된다.
  • java.lang.Runtime, java.awt.Desktop 등의 자바 코어클래스에도 사용된다.

싱글톤 패턴을 사용하기 위한 방법은 여러가지가 있지만 공통적으로

  • 다른 클래스에서 인스턴스화(new)를 제한하는 private 생성자
  • 클래스의 인스턴스를 반환하는 공용 정적 메서드
  • 싱글톤 클래스의 인스턴스를 가져오기 위한 외부 클래스에서의 전역 액세스 포인트
    가 있어야 한다.

유튜브 얄팍한 코딩사전
에서 싱글톤 패턴에 대한 소스코드 예제를 참조하였다.

ex) 어플리케이션 내의 세팅에서 다크모드가 있다.
이 다크모드는 어떤 페이지에 있던 그대로 유지되어야 하고 어떤 페이지에 있던 이 세팅을 관리하는 객체는 같은 것이어야 한다.

before

public class Settings {
	
    private boolean darkMode = false;
    private int fontSize = 13;

    public boolean getDarkMode(){ return darkMode;}
    public int getFontSize(){ return fontSize;}

    public void setDarkMode (boolean _darkMode) {
        darkMode = _darkMode;
    }
    public void setFontSize (int _fontSize) {
        fontSize = _fontSize;
    }
}

위와 같이 클래스 선언을 싱글톤으로 하지 않는다면 세팅을 불러오는 페이지마다 아래와 같이

public class FirstPage {
    private Settings settings = new Seetings();
    public void setSettings() {
        settings.setDarkMode(true);
        settings.setFontSize(15);
        System.out.println(settings.getDarkMode()+","+settings.getFontSize());
    }
}

public class SecondPage {
    private Settings settings = new Settings();
    public void setSettings () {
        System.out.println(settings.getDarkMode()+","+settings.getFontSize());
    }
}

public class MyProgram {
    public static void main(String[] args) {
        new FirstPage().setSettings();//true,15
        new SecondPage().setSettings();//false,13
    }
}

서로 다른 settings 를 만들어 관리하기가 어려울뿐더러 다른 상태값을 가질 수 있을수도 있게 된다.

이를 해결하기 위해 싱글톤 패턴을 사용하는데 우선적으로 시행해야 할 일로

  • private 생성자를 만들어 new를 못하게 한다.(인스턴스화 방지)
  • static으로 클래스 자기 자신을 정적 변수로 만든다.
    - 싱글톤 클래스의 인스턴스를 가져오기 위한 외부 클래스에서의 전역 액세스 포인트
    가 있어야 한다.
    - private static Settings settings = null;
    - 클래스 안의 static이 아닌 변수나 메소드들은 객체가 생성될 때마다 메모리의 공간을 차지하지만
    static으로 선언된 것들은 객체가 얼마나 만들어지든 메모리의 지정된 공간에 딱 하나씩만 존재하게 된다.
  • 정적 메서드 getSettings를 만든다. (클래스의 인스턴스를 반환하는 공용 정적 메서드)
    public static Settings getSettings () {
      if(settings==null) {
          settings = new Settings();
      }
      return settings;
    }

이렇게 클래스를 만들어주면 FirstPage와 SecondPage에서 settings 값을 넣는 부분을

private Settings settings = new Settings();

에서

private Settings settings = Settings.getSettings();

로 변경해준다.

이렇게 되면 getSettings() 이미 static 공간에 자리를 차지하고 있기 때문에 해당 객체를 new로 생성하지 않아도 이처럼 클래스에서 바로 불러낼 수 있다.

public class FirstPage {
    private Settings settings = new Seetings();
    public void setSettings() {
        settings.setDarkMode(true);
        settings.setFontSize(15);
        System.out.println(settings.getDarkMode()+","+settings.getFontSize());
    }
}

public class SecondPage {
    private Settings settings = new Settings();
    public void setSettings () {
        System.out.println(settings.getDarkMode()+","+settings.getFontSize());
    }
}

public class MyProgram {
    public static void main(String[] args) {
        new FirstPage().setSettings();//true,15
        new SecondPage().setSettings();//true,15
    }
}

이제 한 곳에서 settings의 상태값이 변하면 다른 페이지에서의 상태값 또한 같은 객체를 참조하게 되기 때문에 항상 똑같은 상태값을 유지할 수 있게 된다.

그렇다면 정적 변수를 쓰는 것과 싱글톤 패턴의 차이는 무엇일까?
싱글톤 패턴을 쓰면서 얻는 이점으로는 , interface의 사용이나 lazy loading 등 싱글톤으로 할 수 있는 것들이 더 많기 때문이다.

-> 멀티 쓰레드 환경에서는?
thread-safe 한 싱글톤 클래스를 만들기 위해서는 전역 메서드를 synchronized하게 만들어서 오직 한 쓰레드에서만 한 번씩 그 메서드를 읽을 수 있게 만들면 된다.

public static synchronized Settings getSettings () {
  if(settings==null) {
      settings = new Settings();
  }
  return settings;
}

하지만 위와 같은 사용은 속도 이슈를 가져올 수 있기에 synchronized 블락을 if문 안에다 두는 것이 더 좋다.

public static Settings getSettings () {
    synchronized (Settings.class) {
        if(settings==null) {
            settings = new Settings();
        }  
    }
    return settings;
}

쓰레드 세이프 싱글톤


Strategy 패턴(전략 패턴)

옵션들마다의 행동들을 모듈화해서 독립적이고 상호 교체 가능하게 만드는 것이 전략 패턴이다.
예를 들어 온라인 결제 시스템에서 돈을 지불하는 방법이 카드와 무통장 입금이 있다고 하자.

public void pay(String payment){
    if("card".equals(payment)){
        ...
    }else if("cash".equals(payment)){
        ...
    }
}

이런식의 코드는 아름답지 못하고 수정사항이나 새로운 기능이 추가 될 때마다 메서드들을 그 때 그 때 수정해줘야 하는 번거로움과
소프트웨어가 커지고 복잡해질수록 코드를 분석하고 관리하기 어려워지고 클래스마다 역할지정을 뚜렷히 해서 모듈화된 소프트웨어를 구축해가는 객체지향의 철학에도 어긋나게 된다.

여기서 Strategy 패턴을 적용하여 새롭게 코드를 짜보면

public interface PaymentStrategy {
	public void pay(int amount);
}

우선 위와 같이 돈을 지불하는 것에 대한 인터페이스가 있을 것이다.

그리고 cash와 card들 모두 PaymentStrategy를 implements 하여 pay를 오버라이딩 하게 만드는 식으로 클래스를 짠다

public class CardStrategy implements PaymentStrategy {

	private String name;
	private String cardNumber;
	private String cvv;
	private String dateOfExpiry;
	
	public CreditCardStrategy(String nm, String ccNum, String cvv, String expiryDate){
		this.name=nm;
		this.cardNumber=ccNum;
		this.cvv=cvv;
		this.dateOfExpiry=expiryDate;
	}
	@Override
	public void pay(int amount) {
		System.out.println(amount +" paid with credit/debit card");
	}

}
public class CashStrategy implements PaymentStrategy {

	private String accountNumber;
	private String password;
	
	public PaypalStrategy(int num, String pwd){
		this.accountNumber=num;
		this.password=pwd;
	}
	
	@Override
	public void pay(int amount) {
		System.out.println(amount + " paid using Cash.");
	}

}

위와 같이 지불 방식에 대한 전략이 세워졌다면 우리는 이제 상품을 장바구니에 담고 실제로 제품을 구매할 때 다음과 같이 코드를 작성할 수 있다.

public class Item {

	private String upcCode;
	private int price;
	
	public Item(String upc, int cost){
		this.upcCode=upc;
		this.price=cost;
	}

	public String getUpcCode() {
		return upcCode;
	}

	public int getPrice() {
		return price;
	}
	
}
public class ShoppingCart {

	//List of items
	List<Item> items;
	
	public ShoppingCart(){
		this.items=new ArrayList<Item>();
	}
	
	public void addItem(Item item){
		this.items.add(item);
	}
	
	public void removeItem(Item item){
		this.items.remove(item);
	}
	
	public int calculateTotal(){
		int sum = 0;
		for(Item item : items){
			sum += item.getPrice();
		}
		return sum;
	}
	
	public void pay(PaymentStrategy paymentMethod){
		int amount = calculateTotal();
		paymentMethod.pay(amount);
	}
}

위의 코드는 journaldev 에서 가져왔다.
작성자의 말로는 특정 작업에서 개개의 전략이 사용되어야 한다.
Collections.sort() 와 Arrays.sort()가 다른것처럼 이 둘을 합성해서 array가 들어올때의 sort, collection일때의 sort 이런 식의 전략패턴은 옳지 않다는 의미인 것 같다.

또한, 전략패턴의 사용은 특정 작업에 대해 여러 알고리즘이 있고 애플리케이션이 특정 작업에 대해 런타임 시 알고리즘을 유연하게 선택할 수 있기를 원할 때 유용하게 사용된다고 더했다.

이처럼 옵션들마다의 행동들을 모듈화해서 독립적이고 상호교체 가능하게 만드는것이 전략패턴이다.

State Pattern

전략패턴은 state pattern(상태 패턴)과 매우 유사하다고 한다.
이 둘의 차이점은 state pattern의 경우 컨텍스트가 state에 인스턴스 변수로 포함되어 있고 구현이 상태에 종속될 수 있지만,
strategy pattern의 경우 전략이 메서드에 인자값으로 전달되고 컨텍스트 객체에서는 저장할 변수가 없다는 것이다.

우리가 TV를 리모컨으로 On/Off 할 때를 생각해보자.
티비를 껏다 켜는 동작을 코드로 구현해보자면 아래와 같이

public class TVRemoteBasic {

	private String state="";
	
	public void setState(String state){
		this.state=state;
	}
	
	public void doAction(){
		if(state.equalsIgnoreCase("ON")){
			System.out.println("TV is turned ON");
		}else if(state.equalsIgnoreCase("OFF")){
			System.out.println("TV is turned OFF");
		}
	}

	public static void main(String args[]){
		TVRemoteBasic remote = new TVRemoteBasic();
		
		remote.setState("ON");
		remote.doAction();
		
		remote.setState("OFF");
		remote.doAction();
	}

}

로 될 것이다.
이를 다시 state - pattern으로 구현해본다면,

public interface State {

	public void doAction();
}
public class TVStartState implements State {

	@Override
	public void doAction() {
		System.out.println("TV is turned ON");
	}

}
public class TVStopState implements State {

	@Override
	public void doAction() {
		System.out.println("TV is turned OFF");
	}

}

TV를 끄는것과 켜는것에 대한 action을 담은 두 state가 만들어졌다.
이제 이 state를 imple한 context를 만들어보자.

public class TVContext implements State {

	private State tvState;

	public void setState(State state) {
		this.tvState=state;
	}

	public State getState() {
		return this.tvState;
	}

	@Override
	public void doAction() {
		this.tvState.doAction();
	}

}

context는 state를 구현하였고, 현재 상태에 대한 참조를 유지하고 요청을 구현에 전달하고 있다.
이제 다시 TVRemote 클래스를 설계해보면


public class TVRemote {

	public static void main(String[] args) {
		TVContext context = new TVContext();
		State tvStartState = new TVStartState();
		State tvStopState = new TVStopState();
		
		context.setState(tvStartState);
		context.doAction();
		
		
		context.setState(tvStopState);
		context.doAction();
		
	}

}

와 같이 state pattern을 사용하여 클래스를 작성할 수 있게 되었다.

위의 코드는 journaldev 에서 가져왔다.
state-pattern을 사용하는것의 이점으로는 동작을 구현하는 것을 명확하게 볼 수 있다는 것이다.
따라서 오류 가능성이 적고 추가 동작을 위해 더 많은 상태를 추가하는 것이 매우 쉽다.
이러한 패턴은 if-else, switch-case 조건부 논리를 피하는 데 도움이 된다.


singleton-pattern 소스코드
singleton-pattern
strategy-pattern
state-pattern

profile
to Infinity and b

0개의 댓글