1. 플라이웨이트 패턴 Flyweight Pattern
반복자 패턴이란?
- 공유해서 쓰는 패턴
- 인스턴스가 구별되는 이유는 멤버 변수 값이 다르기 때문이다.
- 규모가 작고 인스턴스마다 특성이 따로 없는 경우 공유해서 사용한다.
의도와 동기
- 매번 new를 하는 것이 아니라 효율성을 위해 낭비를 절감한다.
- 예를 들어, 각 단어를 각각 표현하기보다는 문자를 공유하여 표현하면 비용이 훨씬적게 소요됨
- 각 객체가 부가적인 상태 (글꼴등...)이 있다면 따로 관리해야 하는 경우가 있음
- 작은 여러개의 객체를 관리해야 할때 주로 사용
-> 따로 관리해야 하는 것들이 많다면 이 패턴은 좋지 않다.
클래스 다이어그램
- Flyweight
각 객체가 사용할 인터페이스를 정의한다.
- CocreteFlyweight
공유될 수 있는 실제적 객체를 구현
UnSharedCocreteFlyweight
- 각 인스턴스마다 가지게 되는 부가적인 특성이 있다면 구현한다.
FlyweightFactory
- Flyweight에 pool을 관리한다. 각 Flyweight 객체는 Singleton으로 생성한다.
예시
점선으로 된 글자를 표기하는 예제를 살펴보자.
예를 들어 다음과 같이 숫자를 출력해야 할 때, 이 점숫자를 매번 새롭게 만들지 않아도 된다.
....######......
..##......##....
..##......##....
..##......##....
..##......##....
..##......##....
....######......
................
팩토리에서 인스턴스를 출력할 수 있도록 해준다.
public class BigString {
// "큰 문자"의 배열
private BigChar[] bigchars;
// 생성자
public BigString(String string) {
bigchars = new BigChar[string.length()];
BigCharFactory factory = BigCharFactory.getInstance();
for (int i = 0; i < bigchars.length; i++) {
bigchars[i] = factory.getBigChar(string.charAt(i));
}
}
// 표시
public void print() {
for (int i = 0; i < bigchars.length; i++) {
bigchars[i].print();
}
}
}
메인에서 호출이 될 때 어떻게 되는지를 살펴보자.
public static void main(String[] args) {
BigString bs = new BigString("123abc123");
bs.print();
}
호출 될 때마다 해당 숫자를 매번 새롭게 만드는 것이 아니다.
public class BigString {
// "큰 문자"의 배열
private BigChar[] bigchars;
// 생성자
public BigString(String string) {
bigchars = new BigChar[string.length()];
BigCharFactory factory = BigCharFactory.getInstance();
for (int i = 0; i < bigchars.length; i++) {
bigchars[i] = factory.getBigChar(string.charAt(i));
}
}
// 표시
public void print() {
for (int i = 0; i < bigchars.length; i++) {
bigchars[i].print();
}
}
}
결론
- 공유를 통해서 인스턴스의 수를 절약
2. 프록시 패턴 Proxy Pattern
프록시 패턴이란?
- 객체에 대한 접근을 제어하기 위한 대리자이다.
의도와 동기
- 어떤 객체가 생성 초기에 초기화 비용이 많이 들어간 복잡한 경우, 대리자를 통해 간단하게 한다.
- 실제적으로 비용이 많이 들어가는 처리일 때 객체를 만들도록 한다.
종류
- 원격지 프록시: 서로 다른 주소 공간에 존재하는 객체를 대리하는 로컬 객체
- 가상 프록시: 고비용의 객체는 필요한 경우만 생성
- 보호용 프록시: 실제 객체에 대한 접근 권한을 제어하기 위한 경우
-> 프록시는 기본적으로 Real Object와 동일한 기능을 제공하고, 클라이언트 입장에서 동일한 객체라고 생각하고 사용하게 된다.
클래스 다이어그램
- Proxy
실제 참조할 대상을 관리
실제 객체와 동일한 메서드 제공
클라이언트에게 먼저 노출되는 객체
- RealSubject
프록시가 대리하는 실제 객체
- Subject
Proxy와 Real의 공통 메서드 선언
예시
간단한 프린트 예제를 살펴보자.
실제 프린트 행위 이외에 다른 정보들을 리턴하는 등의 기능들은 프록시 객체가 해도 무방하다. 이후 실제 프린팅을 하는 기능을 할 때는 실제 객체를 가져와서 하도록 한다.
먼저 printable이라는 인터페이스를 다음과 같이 정의한다.
public interface Printable {
void setPrinterName(String name); // 이름의 설정
String getPrinterName(); // 이름의 취득
void print(String string); // 문자열 표시(프린트아웃)
}
해당 인터페이스를 구현하는 구현체는 다음과 같다.
public class Printer implements Printable {
private String name;
public Printer() {
heavyJob("Printer의 인스턴스를 생성중");
}
public Printer(String name) { // 생성자
this.name = name;
heavyJob("Printer의 인스턴스(" + name + ")를 생성중");
}
public void setPrinterName(String name) { // 이름의 설정
System.out.println("real : setPrinterName()");
this.name = name;
}
public String getPrinterName() { // 이름의 취득
System.out.println("real : getPrinterName()");
return name;
}
public void print(String string) { // 이름을 붙여서 표시
System.out.println("=== " + name + " ===");
System.out.println(string);
}
private void heavyJob(String msg) { // 무거운 작업
System.out.print(msg);
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.print(".");
}
System.out.println("완료");
}
}
이때 이 프린터에 대한 프록시를 다음과 같이 작성할 수 있다.
public class PrinterProxy implements Printable {
private String name; // 이름
private Printer real; // "본인"
public PrinterProxy() {
}
public PrinterProxy(String name) { // 생성자
this.name = name;
}
public synchronized void setPrinterName(String name) { // 이름의 설정
if (real != null) {
real.setPrinterName(name); // "본인"에게도 설정한다.
}
System.out.println("proxy : setPrinterName()");
this.name = name;
}
public String getPrinterName() { // 이름의 취득
System.out.println("proxy : getPrinterName()");
return name;
}
public void print(String string) { // 표시
realize();
real.print(string);
}
private synchronized void realize() { // "본인"을 생성
if (real == null) {
real = new Printer(name);
}
}
}
여기서 주목할 점은 다른 메서드에 대해서는 프록시 객체가 가진 메서드로 처리해서 응답을 하지만, 실제 print를 하는 행위에 대해서는 실제 Print객체를 만들어서 작동하도록 한다는 것이다.
public void print(String string) { // 표시
realize();
real.print(string);
}
private synchronized void realize() { // "본인"을 생성
if (real == null) {
real = new Printer(name);
}
}
실제 print가 만들어지는 동안에는 lock이 필요하다. 따라서 동기화 메서드로 생성해주고 프린트 작업을 하게 한다.
다음과 같은 예제를 돌려보자.
public static void main(String[] args) {
Printable print = new PrinterProxy("Alice");
System.out.println("현재의 이름은" + print.getPrinterName() + "입니다.");
print.setPrinterName("Bob");
System.out.println("현재의 이름은" + print.getPrinterName() + "입니다.");
print.print("Hello, world.");
print.print("test");
print.setPrinterName("Tomas");
System.out.println("현재의 이름은" + print.getPrinterName() + "입니다.");
}
결론
- 프록시 패턴은 객체에 접근하는데 보호, 가상등의 역할을 한다.
3. 명령 패턴 Command Pattern
명령 패턴이란?
- 명령을 클래스로 만들어서 사용하는 패턴
- 요청에 대한 프로토콜을 객체로 보낼 수 있는데 이를 Command로 한다.
- > 항상 동일한 방식으로 오기 때문에 동일한 메서드로 처리할 수 있다.
의도와 동기
- 요청을 객체로 만들어 전달한다.
- 요청을 히스토리로 관리할 수 있어 롤백도 할 수 있고, 한 꺼번에 실행하게 할 수도 있다.
- 클라이언트 서버간의 프로토콜로 사용할 수 있다.
- e.g. 메뉴, 프로토콜
클래스 다이어그램
- Command
각 명령이 수행할 메서드 선언
- CocreteCommand
실제 명령이 호출되도록 execute 구현
- Client
ConcreteCommand 객체를 생성하고 처리 객체로 정의
- Invoker
Command 처리를 수행할 것을 요청
- Receiver
Command를 처리함
이를 테면 메서드로서 execute를 갖고 모든 커맨드마다 해당 메서드를 실행할 수 있도록 하여 일관성 있게 관리할 수 있다.
결론
- 명령 자체를 객체화 하여 여러 다른 객체에 명령이 전달되거나 명령이 조합될 수도 있다.
- 새로운 프로토콜이 추가되기 쉽다.
- 부가적인 정보가 많은 경우는 비효율적일 수 있다.
4. 해석 패턴 Interpreter Pattern
해석 패턴이란?
- 문법 규칙을 클래스로 표현
- 간단한 프로그램을 해석하기 위한 패턴
- 게임 같은 데서 용어를 사용하는 문법을 만들려고 하는 경우 사용해볼 수 있다.
-> 일종의 해석기다.
의도와 동기
- 간단한 언어에 대한 해석기 패턴
- 각 문법에 대한 해석을 클래스로 표현
- 미니언어나 게임에서 사용하는 간단한 언어에 대한 문법 해석기
클래스 다이어그램
- AbstractExpress
추상 구문 트리에 속한 모든 노드가 가져야할 공통의 메서드 interpte() 오퍼레이션 선언
- TerminalExpression
터미널 기호에 대한 해석방법을 구현
- NonterminalExpression
재귀적으로 호출되어 다른 AbstractExpression으로 구문을 패쓰하게 된다.
- Context
번역기에 대한 포괄적인 정보 포함
- Client
Syntax를 실제로 정의하고 추상 구문 트리를 생성하고 각 문법 표현 클래스의 interpret() 메서드를 호출한다.
예시
특정한 문법이 주어졌을 때 그 언어를 해석하는 해석기를 만들어보자.
자동차를 움직이는 간단한 언어
- 프로그램의 시작: program
- 프로그램의 끝 : end
- 자동차를 움직이게 하는 명령들
go : 앞으로 1미터 전진 right : 오른쪽으로 향함 left : 왼쪽으로 향함 repeat : 반복명령
어떤 명령을 받게 되면 valid를 체크해서 node로 변환
public abstract class Node {
public abstract void parse(Context context) throws ParseException;
}
예를 들어 아래와 같은 명령에 대해서
// <command> ::= <repeat command> | <primitive command>
public class CommandNode extends Node {
private Node node;
public void parse(Context context) throws ParseException {
if (context.currentToken().equals("repeat")) {
node = new RepeatCommandNode();
node.parse(context);
} else {
node = new PrimitiveCommandNode();
node.parse(context);
}
}
public String toString() {
return node.toString();
}
}
repeat로 시작하면 RepeatCommandNode에게 던진다.
// <primitive command> ::= go | right | left
public class PrimitiveCommandNode extends Node {
private String name;
public void parse(Context context) throws ParseException {
name = context.currentToken();
context.skipToken(name);
if (!name.equals("go") && !name.equals("right") && !name.equals("left")) {
throw new ParseException(name + " is undefined");
}
}
public String toString() {
return name;
}
}
위와 같은 방식으로 명령이 들어올 때 각각의 Node로 객체를 던져서 해석을 하게 되는 개념이라고 보면 된다.
Chain Of Respoinsibility와 유사하지만 현재는 문법에 의해서 메시지를 전달한다.
결론
- 문법을 고안하고 어떻게 해석기를 만드느냐의 문제
참고자료
- 패스트 캠퍼스 (박은종의 객체지향 설계를 위한 디자인패턴 with 자바)
'Lecture' 카테고리의 다른 글
자바 코드 리팩토링: 생성자 대신 팩토리 메서드, 전략 패턴/상태 패턴 적용, 상속 대신 위임, 에러 예외 처리 (0) | 2023.07.07 |
---|---|
자바 코드 리팩토링: 매직넘버, 제어 플래그, 널 문제, 분류 코드 문제, 분기문 문제 (0) | 2023.07.06 |
자바 디자인 패턴: 반복자, 방문자, 연쇄책임 패턴 (0) | 2023.07.06 |
자바 디자인 패턴: 상태 패턴, 옵저버 패턴, 메멘토 패턴, 파사드 패턴, 중재자 패턴 (0) | 2023.07.06 |
자바 디자인 패턴: 데코레이터 패턴, 콤포지트 패턴, 어댑터 패턴 (0) | 2023.07.05 |