디자인 패턴 공부 - 메멘토 패턴

이혁진·2023년 1월 28일
0

메멘토 패턴

메멘토 패턴은 캡슐화를 유지한 상태로 객체의 상태를 외부에 저장할 때 사용할 수 있다. 원래라면 어떤 객체(Originator)를 쓰는 Client가 상태 정보를 외부에 저장할라면

<Client>
// save
상태1 = originator.getState1();
상태2 = originator.getState2();
상태3 = originator.getState3();

... update originator states ...

// restore
originator.setState1(상태1);
originator.setState2(상태2);
originator.setState3(상태3);

이런 식이다. 여기서 문제는 클라이언트가 상태 정보를 알고 있다는 것이다. 즉, 상태 정보가 추가되거나 하면 엉뚱하게도 Client가 변하게 된다. 이것을 해결하기 위해서 저장하고 복구하는 로직을 Originator에 캡슐화한다.

너무 당연하게도, 이렇게 하면 클라이언트는 상태 정보를 모르게 되고, 상태가 추가되거나 없어져도 변경이 일어나지 않게 된다.(응집도, SRP) 상태 정보의 추가와 변경이 Originator에서만 일어나게 된다고도 할 수 있다.

구현

Memento 클래스로 상태 정보를 감싼다. 래퍼 클래스 같은 느낌이다. 이때 중요한 것은 언제나 그렇지만 불변 객체로 만들어야 한다.

  • final class 를 통해 상속을 금지, 캐스팅을 통한 필드 조작을 방어
  • 모든 필드에 final을 붙이고, setter를 다 제거
  • 참조형 필드인 경우에는 getter 및 생성자에서 방어적 복사를 수행, 배열 같은 복합 타입? 일수도 있으니 깊은 복사를 하자.

이렇게 하면 된다.

public final class Memento {
	private final int state1;
    private final Object state2;
    
    public Memento(int state1, Object state2) {
    	this.state1 = state1;
        this.state2 = state2.deepcopy(); // ?
    }
    
    public int getState1() {
    	return this.state1;
    }
    
    public Object getState2() {
    	return state2.deepcopy();
    }
}

이렇게 만들고 Originator 에는 save()와 restore()을 각각 구현하면 된다. 캡슐화.

public class Originator {
	...
    
	public Memento save() {
		return new Memento(this.state1, this.state2);
	}
    
    public void restore(Memento memento) {
    	this.state1 = memento.getState1();
        this.state2 = memento.getState2();
    }
}

이게 끝이긴 한데, 여기에서 Originator를 인터페이스로 둘 수도 있고, Memento를 Originator의 이너 클래스로 넣어도 된다.

이거를 실제로 적용해보자. 간단하니까 설명하지 않겠다.

public class Game {
	private int blueTeamScore;
	private int redTeamScore;

	public Game() {
		this.blueTeamScore = 0;
		this.redTeamScore = 0;
	}

	public int getBlueTeamScore() {
		return blueTeamScore;
	}

	public void setBlueTeamScore(int blueTeamScore) {
		this.blueTeamScore = blueTeamScore;
	}

	public int getRedTeamScore() {
		return redTeamScore;
	}

	public void setRedTeamScore(int redTeamScore) {
		this.redTeamScore = redTeamScore;
	}
}

public class Main {
	public static void main(String[] args) {
		Game game = new Game();
		game.setBlueTeamScore(10);
		game.setRedTeamScore(20);

		int blueTeamScore = game.getBlueTeamScore();
		int redTeamScore = game.getRedTeamScore();

		game.setBlueTeamScore(100);
		game.setRedTeamScore(50);

		game.setBlueTeamScore(blueTeamScore);
		game.setRedTeamScore(redTeamScore);
	}
}

메멘토 패턴을 적용하면

public class Game {
	private int blueTeamScore;
	private int redTeamScore;

	public Game() {
		this.blueTeamScore = 0;
		this.redTeamScore = 0;
	}

	public int getBlueTeamScore() {
		return blueTeamScore;
	}

	public void setBlueTeamScore(int blueTeamScore) {
		this.blueTeamScore = blueTeamScore;
	}

	public int getRedTeamScore() {
		return redTeamScore;
	}

	public void setRedTeamScore(int redTeamScore) {
		this.redTeamScore = redTeamScore;
	}

	// create memento
	public GameSave save() {
		return new GameSave(blueTeamScore, redTeamScore);
	}

	// restore
	public void restore(GameSave gameSave) {
		this.redTeamScore = gameSave.getRedTeamScore();
		this.blueTeamScore = gameSave.getBlueTeamScore();
	}
}

public class Main {
	public static void main(String[] args) {
		Game game = new Game();
		game.setBlueTeamScore(10);
		game.setRedTeamScore(20);

		GameSave gameSave = game.save();

		game.setBlueTeamScore(40);
		game.setRedTeamScore(50);

		game.restore(gameSave);
	}
}

메멘토는 이렇게 생겼다.

public final class GameSave {
	private final int blueTeamScore;
	private final int redTeamScore;

	public GameSave(int blueTeamScore, int redTeamScore) {
		this.blueTeamScore = blueTeamScore;
		this.redTeamScore = redTeamScore;
	}

	public int getBlueTeamScore() {
		return this.blueTeamScore;
	}

	public int getRedTeamScore() {
		return this.redTeamScore;
	}
}

장점

응집도의 상승.

예시1 - 객체 직렬화해서 입출력하기

스트림

스트림이란 데이터가 이동하는 통로이다. 데이터가 메모리 - 디스크, 메모리 - 콘솔 간 이동하는 그 통로를 말한다. 여기에다가 read 혹은 write 메시지(메소드)를 보내서 이동을 수행할 수 있다.

이러한 스트림은 두 가지 종류가 있다. 기반스트림과 보조스트림이 그것이고, 이들은 자바 내에 데코레이터 패턴으로써 동적으로 조합하여 사용할 수 있다.

일단 기반스트림이란 실제로 프로그램 밖에서 데이터를 받아오거나 내보내는 스트림이다. FileInput/OutputStream은 파일과 프로그램 간 통신을 담당하고, System.in/out은 키보드나 콘솔에서 프로그램 간 통신을 담당한다. 그 외에도 메모리 간, 프로세스 간, 오디오에서의 통신을 담당하는 스트림도 있다.

보조스트림이란 기반스트림이 읽은 데이터를 활용한다. BufferedInput/OutputStream은 입출력 시 버퍼를 활용한다. 아마 입출력 한번 할 때 블록 단위로 가져오니까 버퍼에다가 캐싱하는 듯? 아무튼 이렇게 입출력을 가속한다. ObjectInput/OutputStream은 객체의 직렬화와 역직렬화를 담당한다. 객체를 바이트 단위로 쓰고 읽을 수 있다. 파일에 저장할 때나 콘솔에 출력할 때 등 직렬화를 쓸 수 있다.

직렬화

직렬화란 데이터(객체 등)를 파일에 저장하거나, 네트워크 통신으로 보내고 받기 위해 특정 형식으로 변환하는 과정을 말한다. 직렬화된 데이터를 원래 데이터로 변환하는 것을 역직렬화라고 한다.

직렬화의 방법에는 자바 직렬화 뿐만 아니라 CSV, JSON, XML형식으로 직렬화하는 것도 있다. 표 형태의 다량의 데이터 저장 혹은 전송 시 CSV를 활용하고(opencsv..) 네트워크에서 구조화된(객체같은)데이터를 송수신 할 때 JSON을 많이 쓴다. java backend, javascript frontend 이런식으로 되어있는 API 시스템에서 JSON으로 통신을 할 수 있다.(jackson...)

자바 직렬화는 이들보다 자바 시스템 간의 데이터 전송과 저장에 최적화되어있다. 특히, 데이터 타입이 자동으로 맞추어지기에 타입 안맞춰줘도 바로 기존 객체처럼 쓸 수 있다. 아무튼 이러한 장점으로 자바 직렬화는 Redis로 저장하여 캐싱을 하거나, 서블릿에서 세션 공유를 저장. RMI를 통한 원격 메소드 호출에서 잘 쓰인다고 한다.

근데, 직렬화하여 파일에 저장했는데, 해당 클래스가 바뀌면 문제가 생길 수 있다. 값이 삭제되거나 null로 들어올 수 있다. 그리고 직렬화된 파일이 리버싱 처럼 조작될 가능성이 있다. 그리고 용량도 엄청 늘어난다고 한다.(2배 이상) 그래서 데이터 크기가 중요하면 자바 직렬화 말고 JSON으로 직렬화하는 하는 것이 좋다. 특히 스프링에서 타입 처리의 편리로 인해서 Spring 내부 세션이나 Redis를 자바 직렬화를 기본으로 쓰는데, 알아두면 좋을 것 같다.

아무튼 자바 직렬화도 메멘토의 일종?이라고 할 수도 있겠다. 아까 거기에 구현하면 이렇게 할 수 있다.

public class Game implements Serializable {
	...
    
	public void save() {
		try(ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("GameSave.hex"));) {
			objectOutputStream.writeObject(this);
		} catch (IOException e) {
			e.printStackTrace();
			System.out.println(e.getMessage());
		}
	}

	public void restore() {
	// 괄호 안에 넣으면 try 끝나고 인스턴스 자동 close
		try(ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("GameSave.hex"));) {
			Game game = (Game) objectInputStream.readObject();
			this.blueTeamScore = game.blueTeamScore;
			this.redTeamScore = game.redTeamScore;
		} catch (IOException | ClassNotFoundException e) {
			e.printStackTrace();
		}
	}
}

메멘토 대신 스트림으로 대체된 느낌. 신기한 게 있는데, try안에 중괄호 말고 그냥 괄호를 쓸 수 있다. 여기에다가 close할 수 있는 객체를 넣어주면 try-catch가 끝나고 그걸 자동으로 close 해준다.

그리고, 저 스트림이 데코레이터 패턴이 적용된 것 같아서 코드를 까보니까, 스트림은 가장 마지막 것만 close하면 되는 듯 하다.

데코레이터로 받은 입력 스트림을 bin의 생성자로 다시 주입해주는데, 이 bin은 BlockDataInputStream이라는 내부 클래스의 객체이다.

아무튼 이렇게 내부클래스에서 in을 또 다른 내부클래스인 PeekInputStream의 객체에 또 주입해주는데,

드디어 필드로 in을 넣어줬다. 이러면 어떻게 되냐면, InputStream에서 close가 호출되었을 때, 인자로 주입해준 스트림의 close도 연쇄적으로 호출된다.

ObjectInputStream의 close()가 호출되면 그 내부에서 bin(BlockDataInputStream)의 close()가 호출되고,

그러면 그 안에서 in으로 선언되었던 PeekInputStream의 객체에서 close()를 다시 호출.

그 내부에서 비로소 ObjectInputStream에 주입되었던 in 객체의 close가 호출된다.

별것 아닌 것 같지만, 디자인 패턴 공부하니까 코드 까보는게 훨씬 쉬워진 것 같아 뿌듯하다.

profile
한양대학교 정보시스템학과 22학번 이혁진 입니다

0개의 댓글