Singleton Pattern, Command Pattern

‍이시현·2023년 11월 14일
0

Design Pattern

목록 보기
4/6

Singleton Pattern

싱글톤 패턴은 하나의 클래스의 대한 객체를 오직 하나만 만들도록 코드로 강제시키고 그 객체(인스턴스)의 대한 전역 접근을 허용하는 패턴이다. 이렇게 만들어진 객체를 다른 클래스가 사용하고 싶다면 해당 클래스에게 요청해 객체를 전달 받고 그 객체를 사용하는 방식으로 구현된다. 이런 패턴은 보통 관리자의 역할을 하는 클래스에게 쓰인다. 관리자는 보통 2명이 있으면 이상한 상황이 발생하기 때문이다. 여러 스레드나 특정 연결을 관리하는 클래스가 그 예이다.

이를 구현하는 방법은 아래와 같다.

public class Singleton {
	private static Singleton uniqueInstance;
    private Singleton() {}
    
    public static Singleton getInstance() {
    	if (uniqueInstance == null) {
        	uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

생각보다 간단하지만 방법이 참신했다. 우선 클래스 안에 자신과 같은 타입의 객체를 static으로 가진다. 자신과 같은 타입의 멤버변수를 가질 수 있는 이유는 static 덕분이다. static 변수는 컴파일 때 클래스 크기 계산 과정에서 제외되기 때문이다.
그 뒤에 생성자를 private로 만들어준다. 이렇게하면 이 클래스 내부를 제외하고 바깥에선 해당 클래스를 new를 통해 만들수가 없다. 그 뒤 public static method로 인스턴스를 만드는 함수를 만들어주면 바깥에서 객체 없이 클래스.method이름 (Singleton.getInstance())의 방식으로 인스턴스를 만들 수 있다. 바깥에서 이렇게 만들면 내가 해당 클래스를 사용하고 싶을 때 객체가 생성되므로 해당 클래스가 규모가 크다면 메모리 크기를 효율적으로 가져갈 수 있다.

하지만 위의 코드는 멀티 스레드를 쓰는 경우 문제가 될 수 있다. 두개의 스레드가 같이 if문 안으로 들어가면 new가 두번 불리고 각각의 스레드는 서로 다른 객체를 가지게 된다.
이를 해결하는 방법은 3가지가 있다.
1. public static synchronized를 통해 메소드 자체를 동기화 시킨다.
2. 시작부터 인스턴스를 만들고 시작한다.
3. DCL(Double-Checked Locking)을 사용한다.

1이 가장 쉬운 방법으로 메소드 전체를 Critical Section으로 만들어 다른 스레드가 해당 메소드를 실행하고 있다면 다른 스레드는 대기하는 방식이다. 다만 메소드 전체에 걸기 때문에 함수가 길고 여러 스레드가 이 메소드를 필요로 한다면 병목현상이 일어날 수 있다.

2는 인스턴스를 처음부터 만드는 방식으로 클래스가 로딩될 때 JVM에서 알아서 하나만 만들어준다. 하지만 이 방법은 위에서 언급한 객체를 늦게 만들어 메모리를 효율적으로 가져가는 이점이 사라진다.

  1. DCL이 가장 현실적인 해결책이다. synchronized 키워드를 일정 코드 범위에만 적용하는 것이다. java에서 이런 것이 있다는 걸 처음 알았다.

synchronized (Singleton.class){} 이런식으로 하면 해당 블록 안에 있는 코드는 동기화되어 실행된다.

if(uniqueInstance == null) {
	synchronized (Singleton.class){
    	if (uniqueInstance == null) {
        	uniqueInstance = new Singleton():
        }
    }
}

이런식으로 만들면 된다. 다만 이런식으로 어떻게 해야 문제가 해결될지 코드 구성을 잘 생각해야한다.

싱글톤은 느슨한 결합 법칙을 위배하는 패턴이라서 잘 생각해서 쓰는 것이 좋다.
enum으로도 싱글톤을 구현할 수 있다는데 뭔소린지 모르겠다.

커맨드 패턴

커맨드 패턴은 요청 내욕을 객체로 캡슐화해서 객체를 서로 다른 요청내역에 따라 매개변수화 하는 패턴이다. 이렇게하면 요청을 큐에 저장하거나 로그기록, 작업취소기능 등을 사용할 수 있다고한다. 솔직히 읽어봐도 잘 모르겠다.
실제 코드를 보면 이해가 되는데 이걸 general한 다이어그램으로 보면 이해가 힘들다. 바텀 업 방식으로 설명하자면 우선 거실 조명이라는 클래스가 있고 조명을 키는 일을 가지고 있다고 해보자. 그리고 여러 종류의 조명에게 명령을 내릴 수 있는 LightOnCommand 클래스가 있고 이 클래스는 Command라는 인터페이스를 상속 받고 있다. 이 Command 클래스는 execute라는 추상 메소드를 가지고 있어서 LightOnCommand 클래스가 이 함수를 구현해야하고 이 함수 안에서 여러 종류의 Light들을 다형성을 이용해 조명을 키고 있다. 그리고 여기서 이런 명령을 내릴 수 있는 Command를 상속 받아 구현된 클래스의 execute 명령을 내리는 또 다른 클래스가 있다. 이 클래스를 invoke 클래스라고 부르고 이 예제에선 SimpleRemoteControl이란 이름으로 불린다. 이 클래스는 setCommand라는 메소드를 통해 Command 객체를 주입받는다.

이렇게 구조가 완성되면 main 함수에선 Light 객체 LightOnCommand 객체에 주입해주고 또 다시 객체를 SimpleRemoteControl 객체에 주입해준다. 그러면 이 객체는 command 객체를 command.excute()를 통해 실행시킬 수 있다.

여기서 중요한 점은 command 객체를 언제든지 다른 command 객체로 바꿔 줄 수 있다는 점이다.

위에서 Receiver가 Light 클래스 ConcreteCommand가 LightOnCommand, Invoker가 SimpleRemoteConrol 클래스이다. Client는 main함수인것같다.

여러 커맨드를 한번에 Invoker에서 관리하고 싶다면 command 배열을 만들어 관리할수도 있다.

public class RemoteControl {
	Command[] onCommands;
	Command[] offCommands;
 
	public RemoteControl() {
		onCommands = new Command[7];
		offCommands = new Command[7];
 
		Command noCommand = new NoCommand();
		for (int i = 0; i < 7; i++) {
			onCommands[i] = noCommand;
			offCommands[i] = noCommand;
		}
	}
  
	public void setCommand(int slot, Command onCommand, Command offCommand) {
		onCommands[slot] = onCommand;
		offCommands[slot] = offCommand;
	}
 
	public void onButtonWasPushed(int slot) {
		onCommands[slot].execute();
	}
 
	public void offButtonWasPushed(int slot) {
		offCommands[slot].execute();
	}
  
	public String toString() {
		StringBuffer stringBuff = new StringBuffer();
		stringBuff.append("\n------ Remote Control -------\n");
		for (int i = 0; i < onCommands.length; i++) {
			stringBuff.append("[slot " + i + "] " + onCommands[i].getClass().getName()
				+ "    " + offCommands[i].getClass().getName() + "\n");
		}
		return stringBuff.toString();
	}
}

위 코드는 책에서 예시로 나온 7개의 장치의 on off를 컨트롤하는 리모컨을 만든 클래스이다. 주목할 점은 NoCommand부분이다. 해당 클래스는 execute라는 함수는 선언되어있지만 그 안에서 아무것도 하지 않는 클래스이다. 이런 클래스를 디폴트로 배열에 넣어놓으면 굳이 배열의 인스턴스가 null인지 확인하지 않고 일관성있게 execute를 호출해도 프로그램이 정상적으로 실행될 수 있게 할 수 있다.

책에서 undo 기능도 구현하였는데 딱히 특별한거 없이 Command 인터페이스에 undo 추상메소드를 추가하고 이를 상속하는 모드 클래스가 이를 구현하는 방식이다. 그리고 위의 클래스에서 undo 멤버변수를 만들어주고 마지막에 실행됐던 인스턴스를 거기에 넣는 방식으로 구현하면 끝이다. 하지만 이 방법은 undo가 단 한번만 가능하다는 것이 단점!

MacroCommand
리모컨 예제랑 비슷한데 여러 커맨드를 한번에 실행하는 것이라고 한다.

public class MacroCommand implements Command {
	Command[] commands;
    
    public MacroCommand(Command[] commands) {
    	this.commands = commands;
    }
    
    public void execute() {
    	for(int i=0; i<commands.lengths(); i++){
        	commands[i].execute();
        }
    }
}

그냥 받아서 실행하는게 다이다.

profile
알고리즘과 머신러닝에 관심이 있는 평범한 공대생입니다!

0개의 댓글