Gof 디자인패턴

Gof 디자인패턴

디자인패턴의 종류

1. 생성패턴

–Abstract Factory (추상 팩토리)   -동일한 주제의 다른 팩토리를 묶어 준다.

추상 팩토리 패턴 ( Abstract Factory Pattern )

추상 팩토리 패턴이라는 이름만 봐서는 팩토리 메서드 패턴과 비슷해보이지만, 명확한 차이점이 있습니다.

  • 팩토리 메서드 패턴
    • 조건에 따른 객체 생성을 팩토리 클래스로 위임하여, 팩토르 클래스에서 객체를 생성하는 패턴 ( 링크 )
  • 추상 팩토리 패턴
    • 서로 관련이 있는 객체들을 통째로 묶어서 팩토리 클래스로 만들고, 이들 팩토리를 조건에 따라 생성하도록 다시 팩토리를 만들어서 객체를 생성하는 패턴

추상 팩토리 패턴은 어떻게 보면, 팩토리 메서드 패턴을 좀 더 캡슐화한 방식이라고 볼 수 있습니다.

예를 들어, 컴퓨터를 생산하는 공장이 있을 때, 마우스, 키보드, 모니터의 제조사로 Samsung과 LG가 있다고 가정하겠습니다.

컴퓨터를 생산할 때 구성품은 전부 삼성으로 만들거나, 전부 LG로 만들어야겠죠?

키보드, 모니터는 Samsung인데, 마우스만 LG면 안되겠죠….

이렇게 컴퓨터는 같은 제조사인 구성품들로 생산되어야 합니다.

다시 말하면, SamsungComputer 객체는 항상 삼성 마우스, 키보드, 모니터 객체들이 묶여서 생산되어야 합니다.

즉, 객체를 일관적으로 생산해야 할 필요가 있습니다.

또한 코드 레벨에서 보면, SamsungComputer인지 LGComputer인지는 조건에 따라 분기될 것이기 때문에

팩토리 메서드 패턴과 같이, 조건에 따라 객체를 생성하는 부분을 Factory 클래스로 정의할 것입니다.

대충 이 정도의 감을 잡고 코드를 보도록 하겠습니다.

먼저, 팩토리 메서드 패턴을 사용하여 컴퓨터를 생산하는 로직을 구현해보도록 하겠습니다.

주의할 것은 추상 팩토리 패턴이 팩토리 메서드 패턴의 상위호환이 아니라는 것입니다.

두 패턴의 차이는 명확하기 때문에 상황에 따라 적절한 선택을 해야할 것입니다.

1. 추상 팩토리 패턴 사용 이유 – 팩토리 메서드 패턴을 사용할 경우의 문제

팩토리 메서드 패턴을 사용하여, 컴퓨터를 생산하는 로직을 구현해보도록 하겠습니다.

클래스가 많아서 조금 복잡해보일 수 있는데, 로직은 동일하니 크게 어려운건 없습니다.

1)먼저 키보드 관련 클래스들을 정의하겠습니다.

LGKeyboard와 SamsugKeyboard 클래스를 정의하고, 이를 캡슐화하는 Keyboard 인터페이스를 정의합니다.

그리고 KeyboardFactory 클래스에서 입력 값에 따라 LGKeyboard 객체를 생성할지, SamsungKeyboard를 생성할지 결정합니다.

public class LGKeyboard implements Keyboard {
public LGKeyboard(){
System.out.println("LG 키보드 생성");
}
}
public class SamsungKeyboard implements Keyboard {
public SamsungKeyboard(){
System.out.println("Samsung 키보드 생성");
}
}
public interface Keyboard {
}
public class KeyboardFactory {
public Keyboard createKeyboard(String type){
Keyboard keyboard = null;
switch (type){
case "LG":
keyboard = new LGKeyboard();
break;

case "Samsung":
keyboard = new SamsungKeyboard();
break;
}

return keyboard;
}
}

2)

Keyboard와 동일하게 Mouse 관련 클래스들을 정의합니다.

public class LGMouse implements Mouse {
public LGMouse(){
System.out.println("LG 마우스 생성");
}
}
public class SamsungMouse implements Mouse {
public SamsungMouse(){
System.out.println("Samsung 마우스 생성");
}
}
public interface Mouse {
}
public class MouseFactory {
public Mouse createMouse(String type){
Mouse mouse = null;
switch (type){
case "LG":
mouse = new LGMouse();
break;

case "Samsung":
mouse = new SamsungMouse();
break;
}

return mouse;
}
}

3)

다음으로 ComputerFactory 클래스를 구현합니다.

ComputerFactory 클래스는 KeyboardFactory와 MouseFactory 객체를 생성해서 어떤 제조사의 키보드와 마우스를 생산할 것인지 결정합니다.

public class ComputerFactory {
public void createComputer(String type){
KeyboardFactory keyboardFactory = new KeyboardFactory();
MouseFactory mouseFactory = new MouseFactory();

keyboardFactory.createKeyboard(type);
mouseFactory.createMouse(type);
System.out.println("--- " + type + " 컴퓨터 완성 ---");
}
}

4)

마지막으로 컴퓨터를 생산하기 위한 Client 클래스를 구현합니다.

public class Client {
public static void main(String args[]){
ComputerFactory computerFactory = new ComputerFactory();
computerFactory.createComputer("LG");
}
}

팩토리 메서드 패턴을 사용하여, 컴퓨터를 생산해보았습니다.

그런데 컴퓨터의 구성품은 키보드, 마우스 뿐만 아니라 본체 구성품들, 모니터, 스피커, 프린터 등등 여러가지가 있죠.

위의 코드를 그대로 사용하고자 한다면, 본체팩토리, 모니터팩토리, 스피커팩토리, 프린터팩토리 클래스를 정의해야 하고,

CoumputerFactory에서는 예를 들어, 다음과 같이 각 팩토리 클래스 객체들을 생성해서 컴퓨터가 완성이 될 것입니다.

public class ComputerFactory {
public void createComputer(String type){
KeyboardFactory keyboardFactory = new KeyboardFactory();
MouseFactory mouseFactory = new MouseFactory();
BodyFactory bodyFactory = new BodyFactory();
MonitorFactory monitorFactory = new MonitorFactory();
SpeakerFactory speakerFactory = new SpeakerFactory();
PrinterFactory printerFactory = new PrinterFactory();

keyboardFactory.createKeyboard(type);
mouseFactory.createMouse(type);
bodyFactory.createBody(type);
monitorFactory.createMonitor(type);
speakerFactory.createSpeaker(type);
printerFactory.createPrinter(type);
System.out.println("--- " + type + " 컴퓨터 완성 ---");
}
}

그런데 사실 Samsung 컴퓨터라면 구성품이 모두 Samsung이어야 하고, LG 컴퓨터라면 구성품이 모두 LG인 것이 맞습니다.

즉, 각각의 컴퓨터 구성품들을 Samsung이냐 LG냐 구분할 필요가 없이,

Samsung 컴퓨터를 만들고자 한다면 구성품이 모두 Samsung이 되도록, 일관된 방식으로 객체를 생성할 필요가 있습니다.

또한 구성품이 늘어날수록 팩토리 객체를 생성하는 부분이 더욱 길어지겠죠.

따라서 추상 팩토리 패턴을 적용하여 구성품이 모두 동일한 제조사가 되도록 개선해보겠습니다.

2. 추상 팩토리 패턴 적용

복잡한걸 싫어하지만, 어쩔수 없었습니다…

패턴 적용 전과 비교했을 때의 차이점은 다음과 같습니다.

  • 어떤 제조사의 부품을 선택할지 결정하는 팩토리 클래스( KeyboardFactory, MouseFactory )가 제거되고, Computer Factory 클래스가 추가되었습니다. ( SamsungComputerFactory, LGComputerFactory )
  • SamsungComputerFactory, LGComputerFactory는 ComputerFactory 인터페이스로 캡슐화하고, 어떤 제조사의 부품을 생성할 것인지 명확하므로, 각각의 제조사의 부품을 생성합니다. ( 일관된 객체 생성 )
  • FactoryOfComputerFactory 클래스에서 컴퓨터를 생산하는 createComputer() 메서드를 호출합니다.

이제 코드를 살펴보겠습니다.

Keyboard, LGKeyboard, SamsungKeyboard, Mouse, LGMouse, SamsungMouse 클래스는 이전과 코드가 동일합니다.

1)

먼저 SamsungComputerFactory, LGComputerFactory 클래스를 정의하고, 이들을 캡슐화하는 ComputerFactory 인터페이스를 정의합니다.

각 클래스는 자신의 제조사 부품 객체를 생성합니다.

예를 들어, SamsungComputerFactory 클래스는 삼성 키보드, 마우스를 가지므로 SamsungKeyboard, SamsungMouse 객체를 생성합니다.

public class SamsungComputerFactory implements ComputerFactory {
public SamsungKeyboard createKeyboard() {
return new SamsungKeyboard();
}

public SamsungMouse createMouse() {
return new SamsungMouse();
}
}
public class LGComputerFactory implements ComputerFactory {
public LGKeyboard createKeyboard() {
return new LGKeyboard();
}

public LGMouse createMouse() {
return new LGMouse();
}
}
public interface ComputerFactory {
public Keyboard createKeyboard();
public Mouse createMouse();
}

2)

다음으로 FactoryOfComputerFactory 클래스를 정의합니다.

이 클래스는 패턴 적용 전 ComputerFactory 클래스와 하는일이 같습니다.

입력값에 따라 객체 생성을 분기하는데요, 이 때 어떤 제조사 컴퓨터 객체를 생성할지 결정합니다.

즉, 부품이 아니라 컴퓨터 객체를 생성한다는 점에서 차이점이 있습니다.

public class FactoryOfComputerFactory {
public void createComputer(String type){
ComputerFactory computerFactory= null;
switch (type){
case "LG":
computerFactory = new LGComputerFactory();
break;

case "Samsung":
computerFactory = new SamsungComputerFactory();
break;
}

computerFactory.createKeyboard();
computerFactory.createMouse();
}
}

3)

마지막으로 컴퓨터를 생산하기 위한 Client 클래스를 정의합니다.

public class Client {
public static void main(String args[]){
FactoryOfComputerFactory factoryOfComputerFactory = new FactoryOfComputerFactory();
factoryOfComputerFactory.createComputer("LG");
}
}

결과는 이전과 동일합니다.

이상으로 추상 팩토리 패턴이 무엇인지에 대해 알아보았습니다.

정리 하면, 패턴 적용 전 ( 팩토리 메서드 패턴 )에서는 구성품 마다 팩토리를 만들어서 어떤 객체를 형성했는데 그 객체의 구성품은 일정하므로, 

추상 팩토리 패턴을 적용하여 관련된 객체들을 한꺼번에 캡슐화 하여 팩토리로 만들어서 일관되게 객체를 생성하도록 했습니다.

Java에서도 여러 디자인 패턴을 사용하고 있는데 추상 팩토리 패턴을 사용하고 있는 API는 다음과 같으며, 참고하시면 좋을 것 같습니다. ( 참고링크 )

Abstract factory

(recognizeable by creational methods returning the factory itself which in turn can be used to create another abstract/interface type)

–Builder -생성(construction)과 표기(representation)를 분리해 복잡한 객체를 생성한다

JAVA/Design Pattern

[생성 패턴] 빌더 패턴(Builder pattern) 이해 및 예제

빌더 패턴은 복잡한 객체를 생성하는 방법을 정의하는 클래스와 표현하는 방법을 정의하는 클래스를 별도로 분리하여, 서로 다른 표현이라도 이를 생성할 수 있는 동일한 절차를 제공하는 패턴입니다. 빌더 패턴은 생성해야 되는 객체가 Optional한 속성을 많이 가질 때 빛을 발휘합니다.

빌더 패턴은 앞서 살펴봤던 싱글톤 패턴팩토리 패턴추상 팩토리 패턴과 마찬가지로 생성 패턴에 속합니다.

빌더 패턴은 생성 패턴(Creational Pattern) 중 하나이다.

생성 패턴은 인스턴스를 만드는 절차를 추상화하는 패턴입니다.

생성 패턴에 속하는 패턴들은 객체를 생성, 합성하는 방법이나 객체의 표현 방법을 시스템과 분리해줍니다.

생성 패턴은 시스템이 상속(inheritance) 보다 복합(composite) 방법을 사용하는 방향으로 진화되어 가면서 더 중요해지고 있습니다.

생성 패턴에서는 중요한 이슈가 두 가지 있습니다.

  1. 생성 패턴은 시스템이 어떤 Concrete Class를 사용하는지에 대한 정보를 캡슐화합니다.
  2. 생성 패턴은 이들 클래스의 인스턴스들이 어떻게 만들고 어떻게 결합하는지에 대한 부분을 완전히 가려줍니다.

쉬운 말로 정리하자면, 생성 패턴을 이용하면 무엇이 생성되고, 누가 이것을 생성하며, 이것이 어떻게 생성되는지, 언제 생성할 것인지 결정하는 데 유연성을 확보할 수 있게 됩니다.

생성 패턴에 어떤 패턴들이 있는지 궁금하신 분들은 이전 글을 참고하시기 바랍니다.

빌더 패턴(Builder Pattern)의 개념 및 예제

빌더 패턴은 많은 Optional한 멤버 변수(혹은 파라미터)나 지속성 없는 상태 값들에 대해 처리해야 하는 문제들을 해결합니다.

예를 들어, 팩토리 패턴이나 추상 팩토리 패턴에서는 생성해야하는 클래스에 대한 속성 값이 많을 때 아래와 같은 이슈들이 있습니다.

  1.  클라이언트 프로그램으로부터 팩토리 클래스로 많은 파라미터를 넘겨줄 때 타입, 순서 등에 대한 관리가 어려워져 에러가 발생할 확률이 높아집니다.
  2. 경우에 따라 필요 없는 파라미터들에 대해서 팩토리 클래스에 일일이 null 값을 넘겨줘야 합니다.
  3. 생성해야 하는 sub class가 무거워지고 복잡해짐에 따라 팩토리 클래스 또한 복잡해집니다. 

빌더 패턴은 이러한 문제들을 해결하기 위해 별도의 Builder 클래스를 만들어 필수 값에 대해서는 생성자를 통해, 선택적인 값들에 대해서는 메소드를 통해 step-by-step으로 값을 입력받은 후에 build() 메소드를 통해 최종적으로 하나의 인스턴스를 리턴하는 방식입니다.

빌더 패턴은 굉장히 자주 사용되는 생성 패턴 중 하나로, Retrofit이나 Okhttp 등 유명 오픈소스에서도 이 빌더 패턴을 사용하고 있습니다.

빌더 패턴을 구현하는 방법은 아래와 같습니다.

  1. 빌더 클래스를 Static Nested Class로 생성합니다. 이때, 관례적으로 생성하고자 하는 클래스 이름 뒤에 Builder를 붙입니다. 예를 들어, Computer 클래스에 대한 빌더 클래스의 이름은 ComputerBuilder 라고 정의합니다.
  2. 빌더 클래스의 생성자는 public으로 하며, 필수 값들에 대해 생성자의 파라미터로 받습니다.
  3. 옵셔널한 값들에 대해서는 각각의 속성마다 메소드로 제공하며, 이때 중요한 것은 메소드의 리턴 값이 빌더 객체 자신이어야 합니다.
  4. 마지막 단계로, 빌더 클래스 내에 build() 메소드를 정의하여 클라이언트 프로그램에게 최종 생성된 결과물을 제공합니다. 이렇듯 build()를 통해서만 객체 생성을 제공하기 때문에 생성 대상이 되는 클래스의 생성자는 private으로 정의해야 합니다.

구현 방법만 봐서는 잘 이해가 가지 않을 수 있으니, 간단한 예제를 통해 이해를 돕도록 하겠습니다.

예제는 ComputerBuilder 클래스를 통해 Computer 클래스 객체를 생성하는 샘플 코드입니다. 

public class Computer {	    //required parameters    private String HDD;    private String RAM;	    //optional parameters    private boolean isGraphicsCardEnabled;    private boolean isBluetoothEnabled;	     public String getHDD() {        return HDD;    }     public String getRAM() {        return RAM;    }     public boolean isGraphicsCardEnabled() {        return isGraphicsCardEnabled;    }     public boolean isBluetoothEnabled() {        return isBluetoothEnabled;    }	    private Computer(ComputerBuilder builder) {        this.HDD=builder.HDD;        this.RAM=builder.RAM;        this.isGraphicsCardEnabled=builder.isGraphicsCardEnabled;        this.isBluetoothEnabled=builder.isBluetoothEnabled;    }	    //Builder Class    public static class ComputerBuilder{         // required parameters        private String HDD;        private String RAM;         // optional parameters        private boolean isGraphicsCardEnabled;        private boolean isBluetoothEnabled;		        public ComputerBuilder(String hdd, String ram){            this.HDD=hdd;            this.RAM=ram;        }         public ComputerBuilder setGraphicsCardEnabled(boolean isGraphicsCardEnabled) {            this.isGraphicsCardEnabled = isGraphicsCardEnabled;            return this;        }         public ComputerBuilder setBluetoothEnabled(boolean isBluetoothEnabled) {            this.isBluetoothEnabled = isBluetoothEnabled;            return this;        }		        public Computer build(){            return new Computer(this);        }     } }

여기서 살펴볼 것은 Computer 클래스가 setter 메소드 없이 getter 메소드만 가진다는 것과 public 생성자가 없다는 것입니다. 그렇기 때문에 Computer 객체를 얻기 위해서는 오직 ComputerBuilder 클래스를 통해서만 가능합니다.

이제 이렇게 작성한 예제를 클라이언트에서 사용해보도록 하겠습니다.

public class TestBuilderPattern {     public static void main(String[] args) {        Computer comp = new Computer.ComputerBuilder("500 GB", "2 GB")                .setBluetoothEnabled(true)                .setGraphicsCardEnabled(true)                .build();    } }

보시는 것처럼 Computer 객체를 얻기 위해 ComputerBuilder 클래스를 사용하고 있으며 필수 값인 HDD와 RAM 속성에 대해서는 생성자로 받고 Optional한 값인 BluetoothEnabled와 GraphicsCardEnabled에 대해서는 메소드를 통해 선택적으로 입력 받고 있습니다.

즉, BluetoothEnabled 값이 필요 없는 객체라면 setBluetoothEnabled() 메소드를 사용하지 않으면 됩니다.

–Factory Method -생성할 객체의 클래스를 국한하지 않고 객체를 생성한다.

팩토리 메서드 패턴 ( Factory Method Pattern )

어떤 상황에서 조건에 따라 객체를 다르게 생성해야 할 때가 있습니다.

예를 들면, 사용자의 입력값에 따라 하는 일이 달라질 경우, 분기를 통해 특정 객체를 생성해야 합니다.

객체마다 하는 일이 다르기 때문에 조건문에 따라 객체를 다르게 생성하는 것은 이상한 일이 아닙니다.

팩토리 메서드 패턴은 이렇게 분기에 따른 객체의 생성( new 연산자로 객체를 생성하는 부분 )을 직접하지 않고,

팩토리라는 클래스에 위임하여 팩토리 클래스가 객체를 생성하도록 하는 방식을 말합니다.

팩토리는 말 그대로 객체를 찍어내는 공장을 의미합니다.

1. 팩토리 메서드 패턴 사용이유

위의 예를 그대로 적용해보겠습니다.

이를 코드로 표현하면 다음과 같습니다.

public abstract class Type {
}
public class TypeA extends Type{
public TypeA(){
System.out.println("Type A 생성");
}
}
public class TypeB extends Type{
public TypeB(){
System.out.println("Type B 생성");
}
}
public class TypeC extends Type{
public TypeC(){
System.out.println("Type C 생성");
}
}
public class ClassA {
public Type createType(String type){
Type returnType = null;
switch (type){
case "A":
returnType = new TypeA();
break;

case "B":
returnType = new TypeB();
break;

case "C":
returnType = new TypeC();
break;
}

return returnType;
}
}
public class Client {
public static void main(String args[]){
ClassA classA = new ClassA();
classA.createType("A");
classA.createType("C");
}
}

TypeA, TypeB, TypeC 클래스를 정의했고, Type 추상 클래스를 정의하여 캡슐화 했습니다.

ClassA의 createType() 메서드에서 문자열 타입 type에따라 Type클래스 생성을 분기처리하고 있습니다.

그런데 이렇게 분기처리하여 객체를 생성하는 코드가 여러 클래스에서 사용하는 경우라면 어떻게 될까요?

위와 같이 중복된 코드가 발생합니다.

또한 객체를 생성하는 일은 객체간의 결합도를 강하게 만드는 일이고, 객체간 결합도가 강하면 유지보수가 어려워집니다.

따라서 팩토리 메서드 패턴을 사용하여, 다른 객체 생성하는 부분을 자신이 하지 않고 팩토리 클래스를 만들어서 팩토리 클래스에서 하도록 할 것입니다.

2. 팩토리 메서드 패턴 적용

팩토리 메서드 패턴을 적용하는 방법은 다음과 같습니다.

  • 팩토리 클래스를 정의
  • 객체 생성이 필요한 클래스( ClassA )에서 팩토리 객체를 생성하여 분기에 따른 객체 생성 메서드를 호출

따라서 Type, TypeA, TypeB, TypeC, Client 클래스의 코드는 동일하고,

팩토리 클래스인 TypeFactory 클래스와 ClassA 클래스를 구현해보겠습니다.

public class TypeFactory {
public Type createType(String type){
Type returnType = null;
switch (type){
case "A":
returnType = new TypeA();
break;

case "B":
returnType = new TypeB();
break;

case "C":
returnType = new TypeC();
break;
}

return returnType;
}
}
public class ClassA {
public Type createType(String type){
TypeFactory factory = new TypeFactory();
Type returnType = factory.createType(type);

return returnType;
}
}

패턴을 적용하기 전 ClassA가 할 일을 TypeFactory 클래스에서 하고 있습니다.

ClassA는 TypeFactory 클래스를 사용하여 객체를 생성하고 있습니다.

즉, 조건에 따른 객체 생성 부분을 자신이 직접하지 않고 팩토리 클래스에 위임하여 객체를 생성하도록 하는 방법이 팩토리 메서드 패턴입니다.

따라서 팩토리 메서드 패턴을 적용함으로써, 객체간의 결합도가 낮아지고 유지보수에 용이해집니다.

이상으로 팩토리 메서드 패턴이 무엇인지에 대해 알아보았습니다.

Java에서도 여러 디자인 패턴을 사용하고 있는데 팩토리 메서드 패턴을 사용하고 있는 API는 다음과 같으며, 참고하시면 좋을 것 같습니다. ( 참고링크 )

Factory method 

(recognizeable by creational methods returning an implementation of an abstract/interface type)

–Prototype (원형) -기존 객체를 복제함으로써 객체를 생성한다.

프로토타입 패턴은 생성 패턴(Creational Pattern) 중 하나이다.

생성 패턴은 인스턴스를 만드는 절차를 추상화하는 패턴입니다.

생성 패턴에 속하는 패턴들은 객체를 생성, 합성하는 방법이나 객체의 표현 방법을 시스템과 분리해줍니다.

생성 패턴은 시스템이 상속(inheritance) 보다 복합(composite) 방법을 사용하는 방향으로 진화되어 가면서 더 중요해지고 있습니다.

생성 패턴에서는 중요한 이슈가 두 가지 있습니다.

  1. 생성 패턴은 시스템이 어떤 Concrete Class를 사용하는지에 대한 정보를 캡슐화합니다.
  2. 생성 패턴은 이들 클래스의 인스턴스들이 어떻게 만들고 어떻게 결합하는지에 대한 부분을 완전히 가려줍니다.

쉬운 말로 정리하자면, 생성 패턴을 이용하면 무엇이 생성되고, 누가 이것을 생성하며, 이것이 어떻게 생성되는지, 언제 생성할 것인지 결정하는 데 유연성을 확보할 수 있게 됩니다.

생성 패턴에 어떤 패턴들이 있는지 궁금하신 분들은 이전 글을 참고하시기 바랍니다.

프로토타입 패턴(Prototype Pattern)의 이해 및 예제

앞서 말씀드린 것처럼 프로토타입 패턴은 객체를 생성하는 데 시간과 노력이 많이 들고, 이미 유사한 객체가 존재하는 경우에 사용됩니다. 그리고 java의 clone()을 이용하기 때문에 생성하고자 하는 객체에 clone에 대한 Override를 요구합니다. 이때 주의할 점은 반드시 생성하고자 하는 객체의 클래스에서 clone()이 정의되어야 한다는 것입니다.

예를 들어 DB로부터 데이터를 가져오는 객체가 존재한다고 가정해보겠습니다.

만약 DB로부터 가져온 데이터를 우리의 프로그램에서 수차례 수정을 해야하는 요구사항이 있는 경우, 매번 new 라는 키워드를 통해 객체를 생성하여 DB로부터 항상 모든 데이터를 가져오는 것은 좋은 아이디어가 아닙니다.

왜냐하면 DB로 접근해서 데이터를 가져오는 행위는 비용이 크기 때문입니다.

따라서 한 번 DB에 접근하여 데이터를 가져온 객체를 필요에 따라 새로운 객체에 복사하여 데이터 수정 작업을 하는 것이 더 좋은 방법입니다.

이때 객체의 복사를 얕은 복사(shallow copy)로 할 지, 깊은 복사(deep copy)로 할 지에 대해서는 선택적으로 행하시면 됩니다.

샘플 코드를 통해 이해를 돕도록 하겠습니다.

실제 DB와 연동되는 샘플 코드를 작성하는 것은 다소 복잡할 수 있으니 쉽게 직원의 명단을 갖고 있는 Employees 클래스를 통해 살펴보겠습니다.

public class Employees implements Cloneable{     private List<String> empList;	    public Employees(){        empList = new ArrayList<String>();    }	    public Employees(List<String> list){        this.empList=list;    }        public void loadData(){        //read all employees from database and put into the list        empList.add("Pankaj");        empList.add("Raj");        empList.add("David");        empList.add("Lisa");    }	    public List<String> getEmpList() {        return empList;    }     @Override    public Object clone() throws CloneNotSupportedException{        List<String> temp = new ArrayList<String>();        for(String s : this.empList){            temp.add(s);        }        return new Employees(temp);    }	}

위 코드를 보시면 clone() 메소드를 재정의하기 위해 Cloneable 인터페이스를 구현한 것을 확인할 수 있습니다. 여기서 사용되는 clone()은 empList에 대하여 깊은 복사(deep copy)를 실시합니다.

이번에는 위에서 작성한 코드를 메인에서 테스트해보도록 하겠습니다.

public class PrototypePatternTest {     public static void main(String[] args) throws CloneNotSupportedException {        Employees emps = new Employees();        emps.loadData();		        //Use the clone method to get the Employee object        Employees empsNew = (Employees) emps.clone();        Employees empsNew1 = (Employees) emps.clone();        List<String> list = empsNew.getEmpList();        list.add("John");        List<String> list1 = empsNew1.getEmpList();        list1.remove("Pankaj");		        System.out.println("emps List: "+emps.getEmpList());        System.out.println("empsNew List: "+list);        System.out.println("empsNew1 List: "+list1);    } }
emps List: [Pankaj, Raj, David, Lisa]empsNew List: [Pankaj, Raj, David, Lisa, John]empsNew1 List: [Raj, David, Lisa]

만약 Employees 클래스에서 clone()을 제공하지 않았다면, DB로부터 매번 employee 리스트를 직접 가져와야 했을 것이고, 그로 인해 상당히 큰 비용이 발생했을 것입니다.

하지만 프로토타입을 사용한다면 1회의 DB 접근을 통해 가져온 데이터를 복사하여 사용한다면 이를 해결할 수 있습니다. (객체를 복사하는 것이 네트워크 접근이나 DB 접근보다 훨씬 비용이 적습니다.)

–Singleton (단일체) -한 클래스에 한 객체만 존재하도록 제한한다.

싱글톤 패턴 ( Singleton Pattern )

싱글톤 패턴이란, 인스턴스가 프로그램 내에서 오직 하나만 생성되는 것을 보장하고, 프로그램 어디에서든 이 인스턴스에 접근할 수 있도록 하는 패턴입니다.

즉, 인스턴스가 사용될 때 똑같은 인스턴스를 여러 개 만드는 것이 아니라, 기존에 생성했던 동일한 인스턴스를 사용하게끔 하는 것입니다.

싱글톤 패턴은 개념도 간단하고 구현도 간단한 편입니다.

public class SingleObj {
private static SingleObj singleObj = null;

// 외부에서 직접 생성하지 못하도록 private 선언
private SingleObj(){ }

// 오직 1개의 객체만 생성
public static SingleObj getInstance(){
if( singleObj == null ){
singleObj = new SingleObj();
}

return singleObj;
}
}

먼저 외부에서 객체를 생성할 수 없도록 생성자를 private으로 선언합니다.

즉, 객체 생성에 대한 관리를 내부적으로 하겠다는 의미이죠.

그러면 외부에서 SingleObj 객체를 생성할 수 없으므로, 미리 생성된 자신을 반환할 수 있도록 getInstance() 메서드를 정의합니다.

주의해야 할 것은 static으로 정의가 되었다는 점입니다.

생성자를 private으로 선언했기 때문에 객체를 생성할 수 없으므로, getInstacne() 메서드가 클래스에 정의되도록 static 제어자를 사용했습니다.

getInstance() 메서드를 호출했을 때,

  • singleObj 변수에 객체가 할당되지 않으면( == null ) 새로운 객체를 생성하고,
  • singleObj 변수에 객체가 이미 있으면 그것을 그대로 반환합니다.

정말로 하나의 인스턴스를 호출하는지 확인해보도록 하겠습니다.

public class Client {
public static void main(String args[]){
for( int i = 0; i < 5; i++ ){
SingleObj obj = SingleObj.getInstance();
System.out.println(obj.toString());
}
}
}

반복문을 돌면서 객체를 가져오도록 했는데, 모두 같은 객체임을 확인했습니다.

이상으로 싱글톤 패턴에 대해 알아보았습니다.

싱글톤 패턴을 검색해보면 많은 포스트에서 JVM을 특성을 이용하는 방식 또는 Java 언어에 특화된 방식으로 싱글톤을 구현하는 방법을 소개하고 있고,

또 다중 쓰레드 환경에서 싱글톤을 처리하는 방법 등을 소개하고 있습니다.

이 글에서는 싱글톤 자체에 대한 이해를 목표로 했기 때문에, 간단하게 글을 마쳤습니다.

그리고 다른 언어에서는 싱글톤을 어떻게 구현하고 있는지 참고하면 좋을 것 같아 링크를 남깁니다.

1) Python ( 링크 )

2) PHP ( 링크 )

3) JS ( 링크 )

또 Java에서도 여러 디자인 패턴을 사용하고 있는데 Singleton 패턴을 사용하고 있는 API는 다음과 같으며, 참고하시면 좋을 것 같습니다. ( 참고링크 )

Singleton

recognizeable by creational methods returning the same instance (usually of itself) everytime

2. 구조패턴

–Adapter (적응자) -인터페이스가 호환되지 않는 클래스들을 함께 이용할 수 있도록, 타 클래스의 인터페이스를 기존 인터페이스에 덧씌운다.

어댑터 패턴 (adapter pattern)

_JSPark 2016. 6. 13. 23:02

어댑터 패턴 (adapter pattern)

한 클래스의 인터페이스를 클라이언트에서 사용하고자하는 다른 인터페이스로 변환한다.

어댑터를 이용하면 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있다.

호환되지 않는 인터페이스를 사용하는 클라이언트를 그대로 활용할수 있다.

이렇게 함으로써 클라이언트와 구현된 인터페이스를 분리시킬수 있으며, 향후 인터페이스가 바뀌더라도 그 변경 내역

은 어댑터에 캡슐화 되기 때문에 클라이언트는 바뀔 필요가 없어진다.

어댑터 패턴 클래스 다이어그램

전기 콘센트를 보면 이해가 쉽다.

한국의 표준 플러그를 일본에 전원 소켓에 바로끼워줄수 없어 동그랑 모양을 일자로 바꿔주는 어댑터를 끼워주어야 한다.

이와같이 어댑터는 소켓의 인터페이스를 플러그에서 필요로 하는 인터페이스로 바꿔준다고 할 수 있다.

객체지향 어댑터는 ???

일상 생활에서와 동일하게 어떤 인터페이스를 클라이언트에서 요구하는 형태의 인터페이스에 적응시켜주는 역할을 한다.

 public interface Duck {

          public void quack();

          public void fly();

 }

 public class MallardDuck implements Duck {

          @Override

          public void quack() {

                   System.out.println(“Quack”);

          }

          @Override

          public void fly() {

                   System.out.println(“I’m flying”);

          }

 }

public interface Turkey {

          public void gobble();

          public void fly();

 }

public class WildTurkey implements Turkey{

          @Override

          public void gobble() {

                   System.out.println(“Gobble gobble”);

          }

          @Override

          public void fly() {

                   System.out.println(“I’m flying a short distance”);

          }

 }

Duck 객체가 모자라서 Turkey 객체를 대신 사용해양 하는 상황이라고 해보자.

인터페이스가 다르기 때문에 Turkey객체를 바로 사용할 수는 없다.

어댑터를 만들어 보자.

 public class TurkeyAdapter implements Duck {

          Turkey turkey;

          public TurkeyAdapter(Turkey turkey) {

                   this.turkey = turkey;

          }

          @Override

          public void quack(){ 

                   turkey.gobble();

          }

          @Override

          public void fly() {

                   turkey.fly();

          }

 }

 public class DuckTestDrive {

          public static void main(String[] args) {

                  MallardDuck duck = new MallardDuck();

                  WildTurkey turkey = new WildTurkey();

                  Duck turkeyAdapter = new TurkeyAdapter(turkey);

                  System.out.println(“The turkey says…”);

                  turkey.gobble();

                  turkey.fly();

                  System.out.println(“The Duck says…”);

                  testDuck(duck);

                  System.out.println(“The TurkeyAdapter says…”);

                  testDuck(turkeyAdapter);

          }

          public static void testDuck(Duck duck)

                   duck.quack();

                   duck.fly();

          }

 }

클라이언트 -> request() -> 어댑터 – translatedRequest() -> 어댑티.

클라이언트는 타겟 인터페이스에 맞게 구현, 어댑터는 타겟 인터페이스를 구현하며, 어댑티 인스턴스가 들어있음.

클라이언트에서 어댑터를 사용하는 방법.

1. 클라이언트에서 타겟 인터페이스를 사용하여 메소드를 호출함으로써 어댑터에 요청을 한다.

2. 어댑터에서는 어댑티 인터페이스를 사용하여 그 요청을 어댑티 에 대한 하나 이상의 메소드를 호출로 변환한다.

3. 클라이언트에서는 호출 결과를 받긴 하지만 중간에 어댑터가 껴 있는지는 전혀 알지 못한다.

어댑터에는 두종류가 있다.

하나는. 객체 어댑터

다른 하나는. 클래스 어댑터

클래스 어댑터 패턴을 쓰려면 다중 상속이 필요한데, 자바에서는 다중 상속이 불가능하다.

두 어댑터의 클래스 다이어 그램을 보면 이해가 수월하다.

클래스 어댑터

객체 어댑터

두 클래스 다이어 그램에서 Target은 오리, Adaptee는 칠면조라 볼 수 있다.

클래스 어댑터에서는 어댑터를 만들 때 타겟과 어댑티 모두의 서브 클래스로 만들고,

객체 어댑터 에서는 구성을 통해서 어댑티에 요청을 전달한다는 점을 제외하면 별다른 차이점이 없다.

구식 Enumeration.

Enumeration을 리턴하는 elements() 메소드가 구현되어 있었던, 초기 컬렉션 형식(Vector, Stack, Hashtable 등)

Enumeration 인터페이스를 이용하면 컬렉션 내에서 각 항목이 관리되는 방식에는 신경 쓸 필요 없이 컬렉션의 모든 항목에 접근이 가능하다.

신형 Iterator.

썬에서 새로운 컬렉션 클래스를 출시하면서, Enumeration과 마찬가지로 컬렉션에 있는 일련의 항목들에 접근할 수 있게 해 주면서 항목을 제거할 수 도 있게 해 주는 iterator라는 인터페이스를 이용하기 시작했다.

개발을 하다보면, Enumerator 인터페이스를 사용하는 구형 코드를 사용해야 하는 경우가 종종 있지만, 새로 만드를 코드에서는 Iterator만 사용할 계획이다.

이런 경우에 어댑터 패턴을 적용하면 좋다.

 public class EnumerationIterator implements Iterator{

          Enumeration enumeration;

          public EnumerationIterator(Enumeration enumeration) {

                   this.enumeration= enumeration;

          }

          @Override

          public boolean hasNext(){ 

                   return enumeration.hasMoreElements();

          }

          @Override

          public Object next() {

                   return enumeration.nextElement();

          }

          @Override

          public void remove() {

                   throw new UnsupportedOperationException(); //예외 던짐 UnsupportedOperationException 지원

          }

 }

Enumeration에서는 remove()에 해당하는 기능을 제공하지 않는다. (읽기 전용 인터페이스)

어댑터 차원에서 완벽하게 작동하는 remove() 메소드를 구현할 수 있는 방법은 없다.

그나마 가장 좋은 방법으로 런타임 예외를 던지는 방법, Iterator 인터페이스를 디자인한 사람들은 이런 필요성을 미리 예견하고

remove() 메소드를 구현할 때 UnsupportedOperationException을 지원하도록 만들었다.

이런경우는 어댑터가 완벽하게 정용될 수 없는 경우라고 할수있다.

하지만 클라이 언트에서 충분히 주의를 기울이고 어댑터 문서를 잘 만들어 두면 상당히 쓸만한 해결책이 될 수 있다.

–Bridge (가교) -추상화와 구현을 분리해 둘을 각각 따로 발전시킬 수 있다.

브릿지 패턴 (Bridge Pattern)

구현부에서 추상층을 분리하여 각자 독립적으로 변형이 가능하고 확장이 가능하도록 합니다. 즉 기능과 구현에 대해서 두 개를 별도의 클래스로 구현을 합니다.

■ 브릿지 패턴의 구조

● Abstraction : 기능 계층의 최상위 클래스. 구현 부분에 해당하는 클래스를 인스턴스를 가지고 해당 인스턴스를 통해 구현부분의 메서드를 호출합니다.

● RefindAbstraction : 기능 계층에서 새로운 부분을 확장한 클래스

● Implementor : Abstraction의 기능을 구현하기 위한 인터페이스 정의

● ConcreteImplementor : 실제 기능을 구현합니다.

■ 브릿지 패턴 예제

각 ‘동물’이라는 클래스와 이 동물 클래스가 가질 수 있는 ‘사냥방법’을 Bridge 패턴을 적용하여 각각 분리하여 설계를 해보겠습니다.

먼저 기능부에 해당하는 최상위 클래스 Animal이 존재하고 그 하위클래스로 Bird와 Tiger 클래스가 존재합니다. ‘동물’이라는 추상 객체의 기능 구현 부분을 Hunting_Handler와 분리하여 구조를 설계 하였습니다.

한 클래스씩 살펴보겠습니다.

Hunting_Handler.interface

12345public interface Hunting_Handler {    public void Find_Quarry();    public void Detected_Quarry();    public void attack();}

동물이 가질 수 있는 ‘사냥 방식’들이 가져야 할 공통 인터페이스를 정의하고 있습니다.

Huntig_Method1.java , Hunting_Method2.java

1234567891011121314public class Hunting_Method1 implements Hunting_Handler {    public void Find_Quarry()    {        System.out.println(“물 위에서 찾는다”);    }    public void Detected_Quarry()    {        System.out.println(“물고기 발견!”);    }    public void attack()    {        System.out.println(“낚아챈다.”);    }}
123456789101112131415public class Hunting_Method2 implements Hunting_Handler {    public void Find_Quarry()    {        System.out.println(“지상에서 찾는다”);    }    public void Detected_Quarry()    {        System.out.println(“노루 발견”);    }    public void attack()    {        System.out.println(“물어뜯는다.”);    }} 

Hunting_Handler 인터페이스를 상속받아 실제 기능에 해당하는 부분을 구현합니다.

Animal.class

123456789101112131415161718192021222324252627public class Animal {        private Hunting_Handler hunting;        public Animal(Hunting_Handler hunting)    {        this.hunting=hunting;    }    public void Find_Quarry()    {        hunting.Find_Quarry();    }    public void Detected_Quarry()    {        hunting.Detected_Quarry();    }    public void attack()    {        hunting.attack();    }    public void hunt()    {        Find_Quarry();        Detected_Quarry();        attack();    }}

기능 부분에 해당되는 최상위 클래스입니다. Hunting_Handler의 인스턴스를 가지고 각각의 Hunting_Handler를 상속받아 구현하고 있는 메서드들을 호출하고 있습니다.

Tiger.java , Bird.java

1234567891011121314public class Tiger extends Animal{    public Tiger(Hunting_Handler hunting)    {        super(hunting);    }    public void hunt()    {        System.out.println(“호랑이의 사냥방식”);        Find_Quarry();        Detected_Quarry();        attack();    }}

1234567891011121314public class Bird extends Animal{    public Bird(Hunting_Handler hunting)    {        super(hunting);    }    public void hunt()    {        System.out.println(“새의 사냥방식”);        Find_Quarry();        Detected_Quarry();        attack();    }}

Animal를 확장한 클래스입니다. 패턴을 보여주기 위해 억지적인 면은 있지만 실제로 확장의 의미로 Tiger 와 Bird만의 추가적인 기능을 가질 수도 있습니다.

(예제에서 확장의 의미로는 어느 동물의 사냥방식인지 화면에 출력해주는 정도..)

Main.java

12345678910111213public class Main {        public static void main(String argsp[])    {            Animal tiger = new Tiger(new Hunting_Method2());        Animal bird = new Bird(new Hunting_Method1());                tiger.hunt();        System.out.println(“————–“);        bird.hunt();    }} 

실제 메인에서 동물 클래스를 생성하고 각각 다른 사냥 방식을 채택하는 모습입니다.

–Composite (복합체) – 0개, 1개 혹은 그 이상의 객체를 묶어 하나의 객체로 이용할 수 있다.

컴포지트(Composite) 패턴

OOP 에서 컴포지트(Composite) 는 하나 이상의 유사한 객체를 구성으로 설계된 객체로 모두 유사한 기능을 나타낸다.

이를 통해 객체 그룹을 조작하는 것처럼, 단일 객체를 조작할 수 있다.

컴포지트 패턴은 무엇인가?

컴포지트 패턴은 클라이언트가 복합 객체(group of object) 나 단일 객체를 동일하게 취급하는 것을 목적으로 한다.

여기서 컴포지트의 의도는 트리 구조로 작성하여, 전체-부분(whole-part) 관계를 표현하는 것이다.

트리 구조를 다룰 때, 프로그래머는 리프 노드와 브랜치를 구별해야한다.

여기서 코드는 많은 복잡성을 만들어 많은 에러를 초래한다.

이를 해결하기 위해, 복잡하고 원시적인 객체를 동일하게 취급하기 위한 인터페이스를 작성할 수 있다.

결과적으로 컴포지트 패턴은 인터페이스와 본연의 컴포지트의 개념을 활용한다.

컴포지트 패턴은 언제 사용하는가?

복합 객체와 단일 객체의 처리 방법이 다르지 않을 경우, 전체-부분 관계로 정의할 수 있다.

전체-부분 관계의 대표적인 예는 Directory-File 이 존재한다.

이러한 전체-부분 관계를 효율적으로 정의할 때 유용하다.

  • 전체-부분 관계를 트리 구조로 표현하고 싶을 경우.
  • 전체-부분 관계를 클라이언트에서 부분, 관계 객체를 균일하게 처리하고 싶을 경우.

컴포지트 패턴 uml

UML 다이어그램이 뜻하는 의미는 다음과 같다.

“Client” 클래스는 “Leaf” 와 “Composite” 클래스를 직접 참조하지 않고, 공통 인터페이스 “Component” 를 참조하는 것을 볼 수 있다.

“Leaf” 클래스는 “Component” 인터페이스를 구현한다.

“Composite” 클래스는 “Component” 객체 자식들을 유지하고, operation() 과 같은 요청을 통해 자식들에게 전달한다.

각각을 조금 더 코드 관점에서 보면 다음과 같다.

Component

모든 component 들을 위한 추상화된 개념으로써, “Leaf” 와 “Composite” 클래스의 인터페이스이다.

Leaf

“Component” 인터페이스를 구현하고, 구체 클래스를 나타낸다.

Composite

“Component”  인터페이스를 구현하고, 구현되는 자식(Leaf or Composite) 들을 가지고, 이러한 자식들을 관리하기 위한 메소드(addChild, removeChild…)를 구현한다.

또한, 일반적으로 인터페이스에 작성된 메소드는 자식에게 위임하는 처리를 한다. 

* Composite.operation() => Leaf.operation(), 자세한 이해는 아래 예제를 통해 할 수 있다.

이를 기반으로, 객체 다이어그램을 보면 이해하기 쉽다.

컴포지트 패턴 객체 다이어그램

“Client” 에서 트리 구조에서의 top-level 에 존재하는 “Composite1” 에 요청을 보낸다.

그러면 “Component” 인터페이스를 구현한 객체들은 트리 구조를 토대로 위에서 아래 방향으로 모든 자식 요소에게 전달하게 된다.

이것은 실제로 런타임에서 일어나는 행위라고 보면 된다.

예제 코드를 접목해보자.

예제 코드는 원, 삼각형, 사각형 등과 같은 형태의 그래픽을 주제로 한다.

/** "Component" */
interface Graphic {

    //Prints the graphic.
    public void print();
}

“Component” 에 해당하는 인터페이스 “Graphic” 를 나타낸다.

이를 구현하는 클래스들이 동일한 처리를 하는 메소드로 “무엇인가를 출력하는 행위” 를 작성했다.

/** "Leaf" */
class Ellipse implements Graphic {

    //Prints the graphic.
    public void print() {
        System.out.println("Ellipse");
    }
}

“Leaf” 에 해당하는 구체 클래스 중 하나로 표현할 수 있는 “Ellipse” 라는 클래스를 만들었다.

그리고 인터페이스에 작성된 메소드인 print() 를 오버라이딩했다.

/** "Composite" */
class CompositeGraphic implements Graphic {

    //Collection of child graphics.
    private List<Graphic> childGraphics = new ArrayList<Graphic>();

    //Prints the graphic.
    public void print() {
        for (Graphic graphic : childGraphics) {
            graphic.print();  //Delegation
        }
    }

    //Adds the graphic to the composition.
    public void add(Graphic graphic) {
        childGraphics.add(graphic);
    }

    //Removes the graphic from the composition.
    public void remove(Graphic graphic) {
        childGraphics.remove(graphic);
    }
}

“Composite” 에 해당하는 클래스로, “Component” 에 해당하는 인터페이스 “Graphic” 를 구현하는 요소들을 관리하기 위한 리스트가 존재한다.

그리고 이를 위한 추가적인 메소드 add, remove 가 존재하고, 인터페이스에서 작성된 메소드인 print() 를 오버라이딩 해주었다.

여기서 print() 는 위에서 언급한 “Composite” 가 일반적으로 하는 작성되는 형태로써, 자식들에게 요청을 위임하는 처리를 하게 된다.

/** Client */
public class Program {

    public static void main(String[] args) {
        //Initialize four ellipses
        Ellipse ellipse1 = new Ellipse();
        Ellipse ellipse2 = new Ellipse();
        Ellipse ellipse3 = new Ellipse();
        Ellipse ellipse4 = new Ellipse();

        //Initialize three composite graphics
        CompositeGraphic graphic = new CompositeGraphic();
        CompositeGraphic graphic1 = new CompositeGraphic();
        CompositeGraphic graphic2 = new CompositeGraphic();

        //Composes the graphics
        graphic1.add(ellipse1); // children - leaf
        graphic1.add(ellipse2); // children - leaf
        graphic1.add(ellipse3); // children - leaf

        graphic2.add(ellipse4); // children - leaf

        graphic.add(graphic1); // children - composite
        graphic.add(graphic2); // children - composite

        //Prints the complete graphic (Four times the string "Ellipse").
        graphic.print();
    }
}

“Client” 에서는 “Composite” 는 자식을 관리하기 위한 추가적인 메소드인 add() 를 통해 자식으로 여러 개의 “Leaf” 를 가질 수 있다.

또한, “Composite” 에 해당하는 또 다른 인스턴스를 자식으로 가질 수 있는 모습을 볼 수 있다.

결과적으로, 트리 구조가 만들어지면서, print() 와 같이 단일 객체와 복합 객체가 같은 방법으로 처리되는 형태가 만들어진다.

이것이 일반적으로 알려진 컴포지트 패턴에 대한 이야기이다.

더 나아가, 2가지 형태의 방식으로 나눌 수 있다.

컴포지트 패턴에서 “Composite” 클래스는 자식들을 관리하기 위해 추가적인 메소드가 필요하다고 언급했다.

이러한 메소드들이 어떻게 작성되느냐에 따라, 컴포지트 패턴은 다른 목적을 추구할 수 있다.

컴포지트 패턴

지금까지 다룬 방식은 타입의 안정성을 추구하는 방식이다.

이것은 자식을 다루는 add(), remove() 와 같은 메소드들은 오직 “Composite” 만 정의되었다.

그로 인해, “Client” 는 “Leaf” 와 “Composite” 를 다르게 취급하고 있다.

하지만 “Client” 에서 “Leaf” 객체가 자식을 다루는 메소드를 호출할 수 없기 때문에, 타입에 대한 안정성을 얻게 된다.

Ellipse ellipse = new Ellipse();CompositeGraphic graphic = new CompositeGraphic();

다른 방식으로 일관성을 추구하는 방식은, 자식을 다루는 메소드들을 “Composite” 가 아닌 “Component” 에 정의한다.

그로 인해, “Client” 는 “Leaf” 와 “Composite” 를 일관되게 취급할 수 있다.

하지만 “Client” 는 “Leaf” 객체가 자식을 다루는 메소드를 호출할 수 있기 때문에, 타입의 안정성을 잃게 된다.

Graphic ellipse = new Ellipse();Graphic graphic = new CompositeGraphic();

어떤 방식이 더 좋냐를 따지기에는 너무 많은 것이 고려된다.

위키에서의 이론은 컴포지트 패턴은 타입의 안정성보다는 일관성을 더 강조한다고 한다.

–Decorator (장식자) -기존 객체의 매서드에 새로운 행동을 추가하거나 오버라이드 할 수 있다.

데커레이터 패턴 ( Decorator Pattern )

데커레이터는 어떤 기능에 추가적으로 기능을 덧붙이고 싶은 경우, 그 기능들을 Decorator로 만들어서 덧붙이는 방식입니다.

예를 들어, 서브웨이 샌드위치를 생각해보겠습니다.

서브웨이를 주문하면 고객의 기호에 따라 채소를 선택할 수 있습니다.

즉, 기본 빵 위에 채소와 토핑을 추가하여 샌드위치가 완성됩니다.

여기서 채소와 토핑( 양상추, 피클, 양파, 치즈 … )들 각각이 데커레이터가 됩니다.

데커레이터 패턴을 사용하면 기능이 딱 정해져있는 객체가 아닌,

동적으로 기능을 조합하여 객체를 만드는 것이 가능해집니다.

이제 서브웨이 주문 방식을 예로 데커레이터 패턴이 필요한 이유에 대해서 알아보겠습니다.

1. 데커레이터 패턴 사용 이유

샌드위치를 만들기 위해서는 기본적으로 빵( Bread )이 필요합니다.

그리고 토핑으로 양상추( lettuce ), 피클( pickle )이 있을 수 있겠죠.

이런 재료를 갖고 샌드위치를 만들어 보겠습니다.

1) 그냥 빵,

2) 양상추가 있는 빵,

3) 피클이 있는 빵

public class Sandwich {
public void make(){
System.out.println("빵 추가");
}
}
public class SandwichWithLettuce extends Sandwich{
public void make(){
super.make();
addLettuce();
}

private void addLettuce(){
System.out.println(" + 양상추");
}
}
public class SandwichWithPickle extends Sandwich{
public void make(){
super.make();
addPickle();
}

private void addPickle(){
System.out.println(" + 피클");
}
}
public class Client {
public static void main(String args[]){
Sandwich sandwich = new Sandwich();
sandwich.make();
System.out.println("-------");

SandwichWithLettuce sandwichWithLettuce = new SandwichWithLettuce();
sandwichWithLettuce.make();
System.out.println("-------");

SandwichWithPickle sandwichWithPickle = new SandwichWithPickle();
sandwichWithPickle.make();
}
}

그런데 양상추와 피클이 모두 들어가 있는 샌드위치를 만드려면 어떻게 해야할까요?

아마도 SandwichWithLettuceAndPickle 클래스를 만들어야 할 것입니다. 

public class SandwichWithLettuceAndPickle extends Sandwich{
public void make(){
super.make();
addLettuce();
addPickle();
}

private void addLettuce(){
System.out.println(" + 양상추");
}

private void addPickle(){
System.out.println(" + 피클");
}
}

또한 치즈같은 토핑을 추가하고자 한다면, SandwichWithCheese 클래스를 만들어야 할 것이고,

여러 토핑을 조합해야 한다면, SandwichWithLettuceAndCheese , SandwichWithPickleAndCheese , SandwichWithLettuceAndPickleAndCheese 같은 클래스가 추가될 수 있습니다.

야채가 10개가 넘어가면…. 조합을 따졌을 때 총 1024개의 클래스가 있어야하네요.

이는 좋은 방법 같진 않습니다.

따라서 서브클래스를 만드는 방식이 아닌, 데커레이터 패턴을 적용하여 이를 해결해보도록 하겠습니다.

2. 데커레이터 패턴 적용

1)

먼저 Sandwich 추상클래스를 정의합니다.

양상추 샌드위치, 피클 샌드위치 등 여러 샌드위치 만드는 것을 캡슐화 하기 위해서입니다.

public abstract class Sandwich {
public abstract void make();
}

2)

다음으로 토핑을 추가하는 ToppingDecorator 클래스를 정의합니다.

ToppingDecorator 클래스는 샌드위치를 토핑하는 것이므로, Sandwich 클래스를 상속받습니다.

public class ToppingDecorator extends Sandwich{
private Sandwich sandwich;

public ToppingDecorator(Sandwich sandwich){
this.sandwich = sandwich;
}

public void make(){
sandwich.make();
}
}

2)

다음으로 빵을 추가하기 위해 Bread 클래스를 정의합니다.

빵은 데코레이터가 아닌, 기본적으로 있어야 하는 것이므로 데커레이터로 정의하지 않았습니다.

public class Bread extends Sandwich {
public void make(){
System.out.println("빵추가");
}
}

3)

다음으로 양상추, 피클을 토핑으로 추가하기 위해 LettuceDecorator , PickleDecorator 클래스를 정의합니다.

public class LettuceDecorator extends ToppingDecorator {
public LettuceDecorator(Sandwich sandwich){
super(sandwich);
}

public void make(){
super.make();
addLettuce();
}

private void addLettuce(){
System.out.println(" + 양상추");
}
}
public class PickleDecorator extends ToppingDecorator {
public PickleDecorator(Sandwich sandwich) {
super(sandwich);
}

public void make() {
super.make();
addPickle();
}

private void addPickle() {
System.out.println(" + 피클");
}
}

4)

마지막으로 샌드위치를 만드는 Client 클래스를 작성합니다.

public class Client {
public static void main(String args[]){
// 양상추 샌드위치
Sandwich sandwichWithLettuce = new LettuceDecorator(new Bread());
sandwichWithLettuce.make();
System.out.println("-------");

// 양상추+피클 샌드위치
Sandwich sandwichWithLettuceAndPickle = new PickleDecorator(new LettuceDecorator(new Bread()));
sandwichWithLettuceAndPickle.make();
}
}

데커레이터 객체를 생성할 때, 생성자로 다시 데커레이터를 생성하고, 최종적으로 Bread 객체를 생성합니다.

토핑이 더 늘어나도 이와 같이 계속 데커레이터 객체를 생성함으로써 샌드위치를 만들 수 있습니다.

이상으로 데커레이터 패턴이 무엇인지에 대해 알아보았습니다.

Java에서도 여러 디자인 패턴을 사용하고 있는데 데커레이터 패턴을 사용하고 있는 API는 다음과 같으며, 참고하시면 좋을 것 같습니다. ( 참고링크 )

Decorator 

(recognizeable by creational methods taking an instance of sameabstract/interface type which adds additional behaviour)

–Façade (퍼사드) -많은 분량의 코드에 접근할 수 있는 단순한 인터페이스를 제공한다.

퍼사드 패턴 (facade pattern)

퍼사드패턴 (facade pattern)

어떤 서브시스템의 일련의 인터페이스에 대한 통합된 인터페이스를 제공한다.

퍼사드에서 고수준 인터페이스를 정의하기 때문에 서브시스템을 더 쉽게 사용할수 있다.

패턴을 사용할때는 항상 패턴이 어떤 용도로 쓰이는지를 잘 알아둬야 한다.

퍼사드 패턴은 단순화된 인터페이스를 통해서 서브시스템을 더 쉽게 사용할 수 있도록 하기위한 용도로 쓰인다.

홈씨어터로 퍼사드 패턴을 구현해보자.

전선과 프로젝터를 설치하고, 각 장치들을 케이블로 연결하고 등등 여러 인터페이스들이 나열되어 있다.

DVD영화를 보려고하면..

1. 팝콘 기계를켠다.

2. 팝콘 튀기기 시작.

3. 전등을 어둡게 조절

4. 스크린을 내린다.

..

..

12. DVD 플레이어를 켠다

13. DVD를 재생한다.

poper.on();

poper.pop();

light.dim(10)

screen.down();

…..

dvd.on();

dvd.play(movie);

이런식으로 된다면..

영화를 끄려면 방금 했던 일을 전부 역순으로 해야하는가?

CD나 라디오를 들을 때도 복잡한가?

시스템이 업그레이드 되면 또 다른 작동 방법을 배워야 하는가?

같은 홈 씨어터 사용법이 너무 복잡해진다.

이런 경우에 퍼사드를 사용하면 된다.

퍼사드 패턴은 인터페이스를 단순화시키기 위해서 인터페이스를 변경한다.

때문에 훨씬 쓰기 쉬운 인터페이스를 제공하는 퍼사드 클래스를 구현함으로써 복잡한 시스템을 훨씬 쉽게 사용할 수 있다.

HomeTheaterFacade를 만들어 보자

 public class HomeTheaterFacade {

           Amplifier amp;

           Tuner tuner;

           Dvdplayer dvd;

           CdPlayer cd;

           Projector projector;

           TheaterLights lights;

           Screen screen;

           PopcornPopper popper;

           public HomeTheaterFacade( Amplifier amp,

                                                Tuner tuner, 

                                                DvdPlayer dvd,

                                                CdPlayer cd,

                                                Projector projector,

                                                Screen screen,

                                                TheaterLights lights,

                                                PopcornPopper popper) {

                    this.amp = amp;

                    this.tunner = tuner;

                    this.dvd = dvd;

                    this.cd = cd;

                    this.projector = projector;

                    this.screen = screen;

                    this.lights = lights;

                    this.popper = popper;

          }

          public void watchMovie (String movie) {

                    System.out.println(“Get ready to watch a movie…”);

                    popper.on();

                    popper.pop();

                    lights.dim(10);

                    screen.down();

                    projector.on();

                    projector.wideScreenMode();

                    amp.on();

                    amp.setDvd(dvd);

                    amp.setsurroundSound();

                    amp.setVolume(5);

                    dvd.on();

                    dvd.play(movie);

          }

          public void endMovie() {

                    System.out.println(“Shutting movie theater down…”);

                    popper.off();

                    lights.on();

                    screen.up();

                    projector.off();

                    amp.off();

                    dvd.stop();

                    dvd.eject();

                    dvd.off();

          }

 }

public class HomeTheaterTestDrive {

         public static void main(String[] args) {

                    // instantiate components here

                    HomeTheaterFacade homeTheater  =

                               new HomeTheaterFacade(amp, tuner, dvd, cd, projector, screen, lights, popper);

                    homeTheater.watchMovie(“타짜”);

                    homeTheater.endMovie();

          }

}

어떤 서브시스템에 속한 일련의 복잡한 클래스들을 단순화 하고 통합한 클래스를 만들어 퍼사드 패턴을 완성하였다.

이제 클라이언트와 서브시스템이 서로 긴밀하게 연결되지 않아도 되고. 최소 지식 원칙 을 준수하는데도 도움을 준다.

최소 지식 원칙.

정말 친한 친구하고만 얘기하라.

어떤 객체든 그 객체와 상호작용을 하는 클래ㅡ의 개수에 주의해야 하며,

그런 객체들과 어떤 식으로 상호작용을 하는지에도 주의를 기울여야 한다는 뜻이다.

어떻게 하면 여러 객체하고 인연을 맺는 것을 피할 수 있을까??

어떤 메소드에서든지 아래와 같은 네 종류의 객체의 메소드만을 호출하면 된다.

1. 객체 자체

2. 메소드에 매개변수로 전달된 객체

3. 그 메소드에서 생성하거나 인스턴스를 만든 객체

4. 그 객체에 속하는 구성요소

원칙을 따르지 않은 경우

public float getTemp() {

          Thermometer thermometer = station.getThermometer(); // station 오로부터 thermometer라는 객체를 받은다음

          return thermometer.getTemperature();                           그 갹체의 getTemperature()메소드를 직접 호출.

}

원칙을 따르는 경우

public float getTemp() {

         return station.getTemperature(); // Station 클래스에 thermometer에 요청을 해주는 메소드를 추가

                                                            이렇게 하면 의존해야 하는 클래스의 개수를 줄일수 있다.

}

예를들면.

public class Car {

           Engine engine; //이 클래스의 구성요소. 이 구성요소의 메소드는 호출해도 된다.

           public Car() { }

           public void start(Key key) { // 매개변수로 전달된 객체의 메소드는 호출해도 된다.

                     Doors doors = new Doors(); //새로운 객체 생성. 이 객체의 메소드는 호출해도 된다.

                     boolean authorized = key.turns(); //매개변수로 전달된 객체의 메소드는 호출해도 된다.

                     if ( authorized ) { 

                               engine.start(); // 이 객체의 구성요소의 메소드는 호출해도 된다.

                               updateDashboardDisplay(); // 객체 내에 있는 메소드는 호출해도 된다.

                               doors.lock(); //직접 생성하거나 인스턴스를 만든 객체의 메소드는 호출해도 된다.

                     }

           }

          public void updateDashboardDisplay() { }

 }

–Flyweight (플라이급) -다수의 유사한 객체를 생성·조작하는 비용을 절감할 수 있다.

–Proxy (프록시) -접근 조절, 비용 절감, 복잡도 감소를 위해 접근이 힘든 객체에 대한 대역을 제공한다.

3. 행위패턴

–Chain of Responsibility (책임연쇄) -책임들이 연결되어 있어 내가 책임을 못 질 것 같으면 다음 책임자에게 자동으로 넘어가는 구조

–Command (명령) -위의 명령어를 각각 구현하는 것보다는 위 그림처럼 하나의 추상 클래스에 메서드를 하나 만들고 각 명령이 들어오면 그에 맞는   서브 클래스가 선택되어 실행하는 것

커맨드 패턴( Command Pattern )

커맨드 패턴은 객체의 행위( 메서드 )를 클래스로 만들어 캡슐화 하는 패턴입니다.

즉, 어떤 객체(A)에서 다른 객체(B)의 메서드를 실행하려면 그 객체(B)를 참조하고 있어야 하는 의존성이 발생합니다.

그러나 커맨드 패턴을 적용하면 의존성을 제거할 수 있습니다.

또한 기능이 수정되거나 변경이 일어날 때 A 클래스 코드를 수정없이 기능에 대한 클래스를 정의하면 되므로 시스템이 확장성이 있으면서 유연해집니다.

1. 커맨드 패턴 사용이유

구글홈이라고 “OK Google 히터 틀어줘” 라고 하면, 히터를 틀어주는 실제 구글 서비스가 있습니다.

구글홈을 사용하는 사용자를 Client 클래스

구글홈을 OKGoogle 클래스,

히터를 Heater 클래스로 정의하도록 하겠습니다.

그러면 OKGoogle은 히터를 켜기 위해서 Heater 객체를 참조해야 합니다.

이를 코드로 표현하면 다음과 같습니다.

public class Heater {
public void powerOn(){
System.out.println("Heater on");
}
}
public class OKGoogle {
private Heater heater;

public OKGoogle(Heater heater){
this.heater = heater;
}

public void talk(){
heater.powerOn();
}
}
public class Client {
public static void main(String args[]){
Heater heater = new Heater();
OKGoogle okGoogle = new OKGoogle(heater);
okGoogle.talk();
}
}

크게 어려운 코드는 없습니다.

그런데 OKGoogle에서 히터를 켜는 기능 말고, 램프를 켜는 기능을 추가하고 싶다면 어떻게 해야 할까요?

위와 같이 Lamp 클래스를 정의하고, OKGoogle 클래스에서 Lamp 객체를 참조하도록 해야 합니다.

물론 기존의 Heater 기능도 있어야 하구요.

이를 적용하여 코드로 표현하면 다음과 같습니다.

public class Heater {
public void powerOn(){
System.out.println("Heater on");
}
}
public class Lamp {
public void turnOn(){
System.out.println("Lamp on");
}
}
public class OKGoogle {
private static String[] modes = {"heater", "lamp"};

private Heater heater;
private Lamp lamp;
private String mode;

OKGoogle(Heater heater, Lamp lamp){
this.heater = heater;
this.lamp = lamp;
}

public void setMode(int idx){
this.mode = modes[idx];
}

public void talk(){
switch(this.mode){
case "heater":
this.heater.powerOn();
break;
case "lamp":
this.lamp.turnOn();
break;
}

}
}
public class Client {
public static void main(String args[]){
Heater heater = new Heater();
Lamp lamp = new Lamp();
OKGoogle okGoogle = new OKGoogle(heater, lamp);

// 램프 켜짐
okGoogle.setMode(0);
okGoogle.talk();

// 알람 울림
okGoogle.setMode(1);
okGoogle.talk();
}
}

OKGoogle에게 mode 설정을 통해, 모드가 0이면 히터를 틀고, 1이면 램프를 켜도록 가정했습니다.

OKGoogle은 히터를 틀고, 램프를 켜기 위해서 Heater, Lamp 객체를 참조해야 하기 때문에,

OKGoogle의 기능이 많아질수록 객체 프로퍼티는 더욱 늘어날 것이고,

기존의 talk() 메서드에서 분기가 늘어날 것입니다.

OCP에도 위배되죠.

2. 커맨드 패턴 적용

문제점을 해결하기 위해 커맨드 패턴을 적용해보겠습니다.

먼저 OKGoogle이 할 수 있는 기능들(Heater를 튼다, Lamp를 킨다.) 을 클래스로 만들어서( HeaterOnCommand, LampOnCommand ) 각 기능들을 캡슐화 합니다.

그리고 OKGoogle 클래스의 talk() 메서드에서 heater.powerOn() , lamp.turnOn()과 같이 기능들을 직접 호출하지 않고,

캡슐화한 Command 인터페이스의 메서드를 호출하도록 합니다.

이를 코드로 표현하면 다음과 같습니다.

1)

먼저 인터페이스를 정의합니다.

public interface Command {
public void run();
}

2)

Heater를 켜는 명령을 클래스화 하여, HeaterOnCommand 클래스를 정의하고,

Heater 클래스는 그대로 히터를 켜는 powerOn() 메서드를 정의합니다.

public class HeaterOnCommand implements Command{
private Heater heater;

public HeaterOnCommand(Heater heater){
this.heater = heater;
}

public void run(){
heater.powerOn();
}
}
public class Heater {
public void powerOn(){
System.out.println("Heater on");
}
}

3)

마찬가지로 Lamp를 켜는 명령을 클래스화 하여, LampOnCommand 클래스를 정의하고

Lamp 클래스는 그대로 램프를 켜는 turnOn() 메서드를 정의합니다.

public class LampOnCommand implements Command{
private Lamp lamp;

public LampOnCommand(Lamp lamp){
this.lamp = lamp;
}

public void run(){
lamp.turnOn();
}
}
public class Lamp {
public void turnOn(){
System.out.println("Lamp on");
}
}

4)

OKGoogle 클래스의 talk() 메서드에서는 Command 인터페이스의 run() 메서드를 하여 명령을 실행합니다.

public class OKGoogle {
private Command command;

public void setCommand(Command command){
this.command = command;
}

public void talk(){
command.run();
}
}

5)

마지막으로 OKGoogle을 사용하는 Client 클래스를 정의합니다.

public class Client {
public static void main(String args[]){
Heater heater = new Heater();
Lamp lamp = new Lamp();

Command heaterOnCommand = new HeaterOnCommand(heater);
Command lampOnCommand = new LampOnCommand(lamp);
OKGoogle okGoogle = new OKGoogle();

// 히터를 켠다
okGoogle.setCommand(heaterOnCommand);
okGoogle.talk();

// 램프를 켠다
okGoogle.setCommand(lampOnCommand);
okGoogle.talk();

}
}

만약 OKGoogle에 TV를 틀어줘 기능이 추가된다면,

TVOnCommand 클래스를 추가하면 되므로, OCP에 위배되지 않으면서 기능을 추가할 수 있습니다.

이상으로 커맨드 패턴에 대해 알아보았습니다.

참고로 Java에서도 여러 디자인 패턴을 사용하고 있는데, 커맨드 패턴을 사용하고 있는 API는 다음과 같으며, 참고하시면 좋을 것 같습니다. ( 참고링크 )

Command

( recognizeable by behavioral methods in an abstract/interface type which invokes a method in an implementation of a different abstract/interface type which has been encapsulated by the command implementation during its creation )

–Interpreter (해석자) -문법 규칙을 클래스화한 구조를 갖는SQL 언어나 통신 프로토콜 같은 것을 개발할 때 사용

Interpreter Pattern 인터프리터패턴

문장을 해석할 때 사용하는 패턴. 해석기, 즉 간이언어를 만들기 위한 패턴. 언어 문법이나 표현을 평가할 수 있는 방법을 제공.

(행동패턴)

특정 컨텍스트를 해석하도록 지시하는 표현 인터페이스를 구현하는 것도 포함. 이 패턴은 SQL구문분석, 기호처리엔진 등에 사용됨.


-> 쉽게 말해, 사용자가 원하는 다양한 명령을 쉽게 표현할 수 있게 구문 약속을 해야함. 그리고, 해석자에서는 이와 같이 약속된 구문을 입력 인자로 전달되었을 때 이를 해석을 할 수 있어야 합니다. 

ex) “2 add 3” 과 같은 표현은 피연산자:2, 연산자: +, 피연산자:3 으로 해석될 수 있다는 것.

 사용자가 다양한 명령을 쉬운 표현 방법으로 전달할 수 있다. 하지만, 너무 많은 명령에 대한 조합에 대해 해석자 패턴을 적용한다면 정규화 과정에 들어가는 비용이 기하급수적으로 커질 수 있으므로 그렇다면 다른 방법을 사용하는게 좋음!!! 

ex) 정규표현식

문자열에서 어떤 패턴을 찾는 알고리즘을 매번 작성하는 것보다는 발견할 문자열을 정의하는 정규 표현식을 해석하는 알고리즘을 만드는 것이 더 낫다!! -> 정규표현식


언제 사용하는 것이 좋을까?

– 정의할 언어의 문법이 간단할 때 

( 문법이 복잡하면 관리할 클래스가 많아져 복잡해짐)

– 성능이 중요한 문제가 되지 않을 때

* 추상구문트리 (Abstarct Sytax Tree)

고급 언어를 기계어로 번역할 때 트리 구조를 통해 표현

‘ a+b*c+d’를 표현할 때 AST라는 트리구조를 사용해 문장정보를 나누고, 평가의 순서를 정할 수 있다.

결과

– 문법의 변경과 확장이 쉽다 : 상속 이용

– 문법 구현이 용이 : 노드에 해당되는 클래스들은 비슷한 구현방법을 갖기 때문에 이들 클래스를 작성하는 것이 쉬움

– 복잡한 문법은 관리하기 어려움 : 많은 규칙을 포함한 문법은 관리, 유지 어려움 -> 컴파일러 생성기나 파서 생성기 이용하는 것이 좋음

예제) 

package interpreter;

public interface Expression {
	public boolean interpret(String context);
}
package interpreter;

public class TerminalExpression implements Expression {
	private String data;
	public  TerminalExpression(String data) {
		this.data=data;
	}
	@Override
	public boolean interpret(String context) {
		if(context.contains(data)) return true;
		  return false;
	}

}
package interpreter;

public class OrExpression implements Expression {
	private Expression exp1 =null;
	private Expression exp2 = null;
	
	public OrExpression(Expression exp1, Expression exp2) {
		this.exp1 = exp1;
		this.exp2 = exp2;
	}

	@Override
	public boolean interpret(String context) {
		return exp1.interpret(context)||exp2.interpret(context);
	}

}
package interpreter;

public class AndExpression implements Expression {
	private Expression exp1 =null;
	private Expression exp2 = null;
	
	public AndExpression(Expression exp1, Expression exp2) {
		this.exp1 = exp1;
		this.exp2 = exp2;
	}

	@Override
	public boolean interpret(String context) {
		return exp1.interpret(context)&&exp2.interpret(context);
	}

}
package interpreter;

public class InterpreterPatternDemo {
	//규칙을 생성하고 그것을 파싱하기 위해 Expression클래스를 사용한다
	
	//Rule : Robert and John are male
	public static Expression getMaleExpression() {
		Expression robert=new TerminalExpression("Robert");
		Expression john = new TerminalExpression("John");
		return new OrExpression(robert, john);
	}
	
	//Rule : Julie is a married women
	public static Expression getMarriedWomanExpression() {
		Expression Julie =new TerminalExpression("Julie");
		Expression married =new TerminalExpression("married");
		return new AndExpression(Julie,married);
	}
	
	public static void main(String[] args) {
		Expression isMale=getMaleExpression();
		Expression isMarriedWoman=getMarriedWomanExpression();
		System.out.println("John is male? "+isMale.interpret("John"));
		System.out.println("Julie is a married women? "+isMarriedWoman.interpret("Julie married"));
		
	}
}

>> Client는 문장해석의 주체이면서 인터프리터를 Context에 대해 적용

Context는 interpreter의 전역정보를 가지고 있다.

아래 그림을 보면 TerminalExpression과 NonterminalExpression이 존재한다. 트리구조를 생각하면 편하다.

표현식안에 다른 표현식(자식 노드가 있다면)에는 다시 평가를 수행해야 한다.

–Iterator (반복자) -반복이 필요한 자료구조를 모두 동일한 인터페이스를 통해 접근할 수 있도록 메서드를 이용해 자료구조를 활용할 수 있도록 해준다.

이터레이터 패턴 (iterator pattern)

컬렉션 구현 방법을 노출시키지 않으면서도 그 집합체 안에 들어있는 모든 항목에 접근할 수 있는 방법을 제공한다.

컬렉션 객체 안에 들어있는 모든 항목에 접근하는 방식이 통일되어 있으면 어떤 종류의 집합체에 대해서도 사용할 수 있는 다형적인 코드를 만들수 있다.

이터레이터 패턴을 사용하면 모든 항목에 일일이 접근하는 작업을 컬렉션 객체가 아닌 반복자 객체에서 맡게 된다. 이렇게 하면 집합체의 인터페이스 및 구현이 간단해질 뿐 아니라, 집합체에서는 반복작업에서 손을 떼고 원래 자신이 할 일(객체 컬렉션 관리)에만 전념할 수 있다.

이터레이터 패턴 클래스 다이어그램

두개의 서로다른 식당이있고 각각의 식당에서 메뉴를 구현한다고 가정해보자.

public class MenuItem {

String name;

String description;

String vegetarian;

double price;

public MenuItem(String name, String description, boolean vegetarian, double price){

this.nae = name;

this.description = description;

this.vegetarian = vegetarian;

this.price = price;

}

public String getName() {

return name;

}

public String getDescription() {

return description;

}

public double getPrice() {

return price;

}

public boolean isVegetarian() {

return vegetarian;

}

}

public class PancakeHouseMenu {

ArrayList<MenuItem> menuItems;

public PancakeHouseMenu() {

this.menuItems = new ArrayList();

additem(“K&B 팬케이크 세트”,”스크램블드 에그와 토스트가 곁들여진 펜케이크”,true,2.99);

additem(“레귤러 팬케이크 세트”,”달걀 후라이와 소시지가 곁들여진 펜케이크”,false,2.99);

additem(“블루베리 펜케이크”,”신선한 블루베리와 블루베리 시럽으로 만든 펜케이크”,true,3.49);

additem(“와플”,”와플, 취향에 따라 블루베리나 딸기를 얹을 수 있습니다.”,true,3.59);

}

public void additem(string name, String description, boolean vegetarian, double price) {

MenuItem menuItem = new MenuItem(name, description, vegetarian, price);

menuItem.add(menuItem);

}

public ArrayList<MenuItem> getMenuItems() {

return menuItems;

}

//기타 메소드

}

public class DinerMenu {

static final int MAX_ITEMS = 6;

int numberOfItems = 0;

MenuItem[] menuItems;

public DinerMenu() {

this.menuItems = new MenuItem[MAX_ITEMS];

additem(“채식주의자용 BLT”,”통밀 위에 (식물성)베이컨, 상추, 토마토를 얹은 메뉴”,true,2.99);

additem(“BLT”,”통밀 위에 베이컨, 상추, 토마토를 얹은 메뉴”,false,2.99);

additem(“오늘의 스프”,”감자 샐러드를 곁들인 오늘의 스프”,false,3.29);

additem(“핫도그”,”사워크라우트, 갖은 양념, 양파, 치즈가 곁들여진 핫도그”,false,3.05);

}

public void additem(string name, String description, boolean vegetarian, double price) {

MenuItem menuItem = new MenuItem(name, description, vegetarian, price);

if(nemberOfItems >= MAX_ITEMS){

System.err.println(“죄송합니다, 메뉴가 꽉 찼습니다. 더 이상 추가할 수 없습니다.”);

} else {

menuItems[numberOfItems] = menuItem;

numberOfItems = numberOfItems+1;

}

}

public MenuItem[] getMenuItems() {

return menuItems;

}

//기타 메소드

}

위와 같이 두가지 서로 다은 메뉴 표현 방식이 있을 때 어떤문제가 생길 수 있을까.

두 메뉴를 사용하는 클라이언트를 만들어보자.

클라이언트 기능은 5가지로 정해보자

1. printMenu() – 메뉴에 있는 모든 항목을 출력

2. printBreakfastMenu() – 아침 식사 항목만 출력

3. printLunchMenu() – 점심 식사 항목만 출력

4. printVegetarianMenu() – 채식주의자용 메뉴 항목만 출력

5. isItemVegetarian(name) – name 항목이 채식주의자용이면 true 그렇지 않으면 false를 리턴.

각메뉴에 들어있는 모든항목 출력하려면..

PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();

ArrayList<MenuItem> breakfastItems = pancakeHouseMenu.getMenuItems();

DinerMenu dinerMenu = new DinerMenu();

MenuItem[] lunchItems = dinerMenu.getMenuItems();

for ( int i=0; i < breakfaseItems.size(); i++) {

MenuItem menuItem = breakfastItems.get(i);

System.out.println(menuItem.getName());

System.out.println(menuItem.getPrice());

System.out.println(menuItem.getDescription());

}

for ( int i=0; i < lunchItems.length; i++) {

MenuItem menuItem = lunchItems[i];

System.out.println(menuItem.getName());

System.out.println(menuItem.getPrice());

System.out.println(menuItem.getDescription());

}

다른 모든 메소드들도 결국 위에 있는 코드랑 비슷한 식으로 작성해야 한다.. 항상 두 메뉴를 이용하고, 각아이템에 대해서 반복적인 작업을 수행하기 위해 두 개의 순환문을 써야 한다. 이후에 식당이 더 추가된다면 이런상황이 게속 반복된다.

디자인 원칙중 바뀌는 부분을 캡슐화 하라! 라는 내용이 있었다.

반복을 캡슐화 할 수 있을까??

아래와 같이 반복작업을 캡슐화한 Iterator 라는 객체를 만들면 된다.

Iterator<MenuItem> iterator = breakfastMenu.createIterator();

while(iterator.hasNext()){

MenuItem menuItem = iterator.next();

}

Iterator<MenuItem> iterator = lunchMenu.createIterator();

while(iterator.hasNext()){

MenuItem menuItem = iterator.next();

}

하나의 새로운 Iterator 인터페이스를 만들어도 되지만.

java.util.Iterator 인터페이스를 사용해서 Iterator를 적용시켜보자.

public interface Menu {

public Iterator<MenuItem> createIterator();

|

public class PancakeHouseMenu implements Menu {

ArrayList<MenuItem> menuItems;

public PancakeHouseMenu() {

this.menuItems = new ArrayList();

additem(“K&B 팬케이크 세트”,”스크램블드 에그와 토스트가 곁들여진 펜케이크”,true,2.99);

additem(“레귤러 팬케이크 세트”,”달걀 후라이와 소시지가 곁들여진 펜케이크”,false,2.99);

additem(“블루베리 펜케이크”,”신선한 블루베리와 블루베리 시럽으로 만든 펜케이크”,true,3.49);

additem(“와플”,”와플, 취향에 따라 블루베리나 딸기를 얹을 수 있습니다.”,true,3.59);

}

public void additem(string name, String description, boolean vegetarian, double price) {

MenuItem menuItem = new MenuItem(name, description, vegetarian, price);

menuItem.add(menuItem);

}

public ArrayList<MenuItem> getMenuItems() {

return menuItems;

}

@Override

public Iterator<MenuItem> createIterator() {

return menuItems.iterator(); //ArrayList 컬렉션은 반복자를 리턴하는 iterator() 라는 메소드가 있음.

}

//기타 메소드

}

public class DinerMenu implements Menu {

static final int MAX_ITEMS = 6;

int numberOfItems = 0;

MenuItem[] menuItems;

public DinerMenu() {

this.menuItems = new MenuItem[MAX_ITEMS];

additem(“채식주의자용 BLT”,”통밀 위에 (식물성)베이컨, 상추, 토마토를 얹은 메뉴”,true,2.99);

additem(“BLT”,”통밀 위에 베이컨, 상추, 토마토를 얹은 메뉴”,false,2.99);

additem(“오늘의 스프”,”감자 샐러드를 곁들인 오늘의 스프”,false,3.29);

additem(“핫도그”,”사워크라우트, 갖은 양념, 양파, 치즈가 곁들여진 핫도그”,false,3.05);

}

public void additem(string name, String description, boolean vegetarian, double price) {

MenuItem menuItem = new MenuItem(name, description, vegetarian, price);

if(nemberOfItems >= MAX_ITEMS){

System.err.println(“죄송합니다, 메뉴가 꽉 찼습니다. 더 이상 추가할 수 없습니다.”);

} else {

menuItems[numberOfItems] = menuItem;

numberOfItems = numberOfItems+1;

}

}

public MenuItem[] getMenuItems() {

return menuItems;

}

@Override

public Iterator<MenuItem> createIterator() {

return new DinerMenujIterator(menuItems);

}

//기타 메소드

}

public class DinerMenuIterator implements Iterator<MenuItem> {

Menuitem[] list;

int position = 0;

public DinerMenuIterator(MenuItem[] list) {

this.list = list;

}

@Override

public MenuItem next() {

MenuItem menuItem = list[position];

position += 1;

return menuItem;

}

@Override

public boolean hasNext() {

if(position >= list.length || list[position] == null) return false;

else return true;

}

@Override

public void remove() { // 반드시 기능을 제공하지 않아도됨 그렇다면 java.lang.UnsupportedOperationException을 던지도록 하면됨

if(position <= 0) Throw new IllegalStateException(“next()가 한번도 호출되지 않음.”);

if(list[position-1] != null){

for(int i=position-1; i<(list.length-1); i++){

list[i] = list[i+1];

}

list[list.length-1] = null;

}

}

}

public class Waitress {

ArrayList<Menu> menus;

public Waitress(ArrayList<Menu> menus) {

this.menus = menus;

}

public void printMenu() {

Iterator menuIterator = menus.iterator();

while(menuIterator.hasNext()){

Menu menu = menuIterator.next();

printMenu(menu.createIterator());

}

}

private void printMenu(Iterator<MenuItem> iterator) {

while(iterator.hasNext()) {

MenuItem menuItem = iterator.next();

System.out.println(menuItem.getName());

System.out.println(menuItem.getPrice());

System.out.println(menuItem.getDescription());

}

}

}

public class MenuTestDrive {

public static void main(String args[]) {

ArrayList<Menu> menuList = new ArrayList();

menuList.add(new PancakeHouseMenu());

menuList.add(new DinerMenu());

Waitress waitress = new Waitress(menuList);

waitress.printMenu();

}

}

이제 집합체 내에서 어떤 식으로 일이 처리되는 지에 대해서 전혀 모르는 상태에서도 그 안에 들어있는 모든 항목들에 대해서 반복작업을 수행할수 있게 되었다.

집합체에서 내부 컬랙션과 관련된 기능과 반복자용 메소드 관련기능을 전부 구현하도록 했다면 어떨까?

그렇게 하면 집합체에 들어가는 메소드 개수가 늘어나지.. 그게 나쁜건가?

우선 클래스에서 원래 그 클래스의 역할(집합체 관리) 외에 다른 역할(반복자 메소드)을 처리하 도록 하면, 두 가지 이유로 인해 그 클래스가 바뀔 수 있게 된다.

하나는 컬렉션이 어떤 이유로 인해 바뀌게 되면 그 클래스가 바뀌어야 하고, 반복자 관련 기능이 바뀌었을 때도 클래스가 바뀌여야 된다.

이런 이유로 인해 “변경” 이라는 주제와 관련된 디자인 원칙이 있다.

디자인 원칙

클래스를 바꾸는 이유는 한 가지 뿐이어야 한다.

클래스를 고치는 것은 최대한 피해야 한다. 코드를 변경하다 보면 온갖 문제가 생겨날수 있으니까..

때문에 코드를 변경할 만한 이유가 두가지가 되면 그만큼 그 클래스를 나중에 고쳐야 할 가능성이 커지게 될 뿐 아니라, 디자인에 있어서 두 가지 부분이 동시에 영향이 미치게 된다.

이 원칙에 따르면 한 역할은 한 클래스에서만 맡게 해야 한다.

–Mediator (중재자) -클래스간의 복잡한 상호작용을 캡슐화하여 한 클래스에 위임해서 처리 하는 디자인 패턴

중재자 패턴

Purpose

    – 서로 상호작용하는 object들을 캡슐화함으로써 loose coupling을 유지하기 위해 사용한다.

Use When

    – 객체들 사이에 너무 많은 관계가 얽혀있을때

    – 객체들 사이의 상호작용 관계가 복잡할때


  • 중재자 패턴이란
Mediator Pattern Diagram

M개의 object 사이에 N개의 관계가 형성되어 있어 아주 복잡하게 얽혀있을때 이를 M:1 관계로 바꿔주기 위해 중재자 패턴을 사용한다. M개의 object 사이에 이들의 관계를 control 하는 Mediator를 하나 넣어서 Mediator가 모든 object들의 communication을 관리하도록 한다.

  • objects들 사이에 Mediator를 넣어 연결관계를 캡슐화한다.
  • class들을 loose coupling 상태로 유지할 수 있다. (서로 알 필요 없고 Mediator가 모두 관리하므로)
  • 장점 : 전체적인 연결관계를 이해하기 쉽다 (communication의 흐름을 이해하기 쉽다)
  • 단점 : 특정 application 로직에 맞춰져있기 때문에 다른 application에 재사용하기 힘들다 (옵저버 패턴의 경우 반대이다. 재사용성은 좋지만 연결관계가 복잡해지면 이해하기 어렵다)

  • 중재자 패턴 vs 옵저버 패턴
Observer Pattern
Mediator Pattern

 옵저버 패턴은 1개의 Publisher에 대해 N개의 Subscriber가 존재한다. 즉 복수의 Subscriber가 Publisher의 상태만 관찰하는 셈이다. 그러나 Mediator의 경우 M개의 Publisher와 n개의 Subscriber가 존재한다. 즉 M개의 Publisher가 서로서로 상태를 관찰하기 때문에 Publisher가 Subscriber가 될 수도, Subscriber가 Publisher가 될 수도 있다.


  • 중재자 패턴의 예시
Font Dialog boxe

 다음과 같이 Font box가 있을때 Font가 선택됨에 따라 지원되는 size, style, 등.. 이 다르다. 따라서 각 object들이 서로를 관찰하고 있어야 한다.

Mediator Pattern Diagram for Font Dialog

 따라서 위의 다이어그램과 같이 Font box의 Mediator인 FontDialogDirector가 복수개의 Widget object들을 관리해줘야 한다. 

Sequence Diagram for Font Dialog

1. ListBox가 Director에게 변화를 알려준다.

2. Director가 ListBox에서 어떤 selection이 선택된건지 요청해서 받아온다.

3. EntryField에게 그 selection을 전달한다.

4. 이제 EntryField는 그 selection에 맞는 style을 제공한다.


  • Related Patterns and Summary
  • Observer : 재사용성이 좋다. 복잡한 communication에서는 이해하기 힘들다. 1개의 Publisher와 N개의 Subscriber로 이루어져 있다.
  • Mediator : 재사용성이 안좋다. 복잡한 communication에서 이해하기 쉽다. M개의 Publisher, N개의 Subscriber 사이의 communication을 1개의 Mediator를 이용해 캡슐화하고 있다.

–Memento (메멘토) -Ctrl + z 와 같은 undo 기능 개발할 때 유용한 디자인패턴. 클래스 설계 관점에서 객체의 정보를 저장

메멘토 패턴(Memento Pattern)

1) 개요

메맨토 패턴은 객체의 상태 정보를 가지는 클래스를 따로 생성하여, 객체의 상태를 저장하거나 이전 상태로 복원할 수 있게 해주는 패턴입니다. 메멘토 패턴은 바둑, 오목, 체스 등의 보드게임 등에서 ‘무르기’ 기능을 구현할 때 사용되기도 합니다.

단, 이전 상태의 객체를 저장하기 위한 Originator가 클 경우 많은 메모리가 필요합니다.

2) UML

– Originator : 객체의 상태를 저장합니다. Memento 객체를 생성하며 후에 Memento를 사용하여 실행 취소(undo)를 할 수 있습니다.

– Memento : Originator의 상태를 유지하는 객체입니다.(POJO) 

– Caretaker : 마치 게임의 세이브포인트처럼 복수의 Memento의 상태를 유지해주는 객체입니다.

–Observer (감시자) -어떤 클래스에 변화가 일어났을 때, 이를 감지하여 다른 클래스에 통보해주는 것

옵저버 패턴 (observer pattern)

한객체의 상태가바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로

일대다(one-to-many) 의존성을 정의한다.

옵저버 패턴을 구현하는 방법에는 여러가지가 있지만 대부분 상태를 저장하고있는 주제 인터페이스를 구현한 하나의 주제객체와 주제객체에 의존하고있는 옵저버 인터페이스를 구현한 여러개의 옵저버객체 가 있는 디자인을 바탕으로 한다.

데이터 전달방식은 2가지가 있다.

주제객체에서 옵저버로 데이터를 보내는 방식(푸시 방식)

옵저버에서 주제객체의 데이터를 가져가는 방식 (풀 방식)

옵저버 패턴 클래스 다이어그램

디자인 원칙.

서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.

옵저버 패턴은 주제와 옵저버가 느슨하게 결합되어있는 객체 디자인을 제공.

주제가 옵저버에 대해서 아는 것은 옵저버가 특정 인터페이스(Observer 인터페이스)를 구현 한다는 것 뿐.

옵저버는 언제든지 새로 추가할 수 있음. (주제는 Observer인터페이스 구현하는 객체 목록에만 의존하기때문)

새로운 형식의 옵저버를 추가하려해도 주제를 전혀 변경할 필요가 없음. (새로운 클래스에서 Observer 인터페이스만 구현해주면됨)

주제나 옵저버가 바뀌더라도 서로에게 전혀 영향을 주지않음. 그래서 주제와 옵저버는 서로 독립적으로 재 사용할수 있음.

느슨하게 결합하는 디자인을 사용하면 변경 사항이 생겨도 무난히 처리할 수 있는 유연한 객체지향 시스템을 구축할수 있다. (객체 사이의 상호 의존성을 최소화 할 수 있기 때문)

문제의 시작

날씨 데이터를 가지고 있는 회사와 데이터를 연동하여 여러종류의 각각의 디스플레이에 날씨데이터를 출력해줘야 하는 업무가 생겼다고 가정했을때.

제공받은 객체와 각 메소드의 역할.

getTemperature() : 온도

getHumidity() : 습도

getPressure() : 기압

measurementsChanged() : 새로운 기상 측정 데이터가 나올때마다 자동으로 호출되는 부분.

대강구현해 본다면..

measurementsChanged 메소드 안 Display update 메소드들이 구체적인 구현에 맞춰서 코딩이 되었기 때문에, 프로그램을 고치지 않고는 다른 디스플레이 항목을 추가/제거할수 없다. 향후에 바뀔수있는 부분은 캡슐화해서 분리하여야 한다.

효과적으로 모든 디스플레이들에게 Weather의 상태를 알려줄수 있는 방법이 필요.

public class WeatherData{

     // 인스턴스 변수들

     public void measurementsChanged(){ //새로운 데이터 세팅시 갱신되는 메소드

         float temp = getTemperature();

         float humidity = getHumidity();

         float pressure = getPressure();

         currentCondirionsDisplay.update(temp, humidity, pressure); //디스플레이 갱신

         statisticsDisplay.update(temp, humidity, pressure); //디스플레이 갱신

         forecastDisplay.update(temp, humidity, pressure); //디스플레이 갱신

     }

     //기타 메소드

 }

해결해보자.

1.옵저버 패턴 구현해보기 (푸시 방식 사용)

 public interface Subject {

      void registerobserver(Observer observer);

      void removeObserver(Observer observer);

      void notifyObservers();

 } 

 public interface Observer {

      void update(float temp, float humidity, float pressure);

 }

 public interface DisplayElement {

      void display();

 }

 public class WeatherData implements Subject{

private List<Observer> observers;

private float temperature;

private float humidity;

private float pressure;

{

this.observers = new ArrayList<>();

}

public void measurementsChanged(){ this.notifyObservers(); }

public void setMeasurementsChanged(float t, float h, float p){ //값이 세팅된다고 가정.

this.temperature = t;

this.humidity = h;

this.pressure = p;

this.measurementsChanged();

}

@Override

public void notifyObservers() {

for (Observer observer : observers) {

observer.update(this.temperature, this.humidity, this.pressure);

}

}

@Override

public void registerobserver(Observer observer) {

this.observers.add(observer);

}

@Override

public void removeObserver(Observer observer) {

if(observers.contains(observer)) observers.remove(observer);

}

 }

public class CurrentConditions implements Observer, DisplayElement{

private float temperature;

private float humidity;

private Subject weatherData;

public CurrentConditions(Subject weatherData) {

this.weatherData = weatherData;

this.weatherData.registerobserver(this); //옵저버 등록

}

@Override

public void display() {

System.out.println(“Current conditions : “+temperature+” , “+humidity);

}

@Override

public void update(float temp, float humidity, float pressure) {

this.temperature = temp;

this.humidity = humidity;

this.display();

}

 }

 public class WeatherStation {

public static void main(String[] args) {

WeatherData weatherData = new WeatherData();

CurrentConditions currentConditions = new CurrentConditions(weatherData);

StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);

ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);

weatherData.setMeasurementsChanged(85, 62, 36.7f);

}

 }

2.자바 내장 옵저버 패턴 사용

java.util.Observer 인터페이스와 java.util.Observable 클래스를 사용할수 있음.

자바 내장 옵저버 패턴은 푸시 방식, 풀 방식 모두 사용가능.

자바 내장 옵저버 패턴 클래스 다이어그램

이전에 구현 했던것과 마찬가지로 java.util.Observer 인터페이스를 구현하고 java.util.Observable 객체의 addObserver() 메소드를 호출하면 옵저버 목록에 추가가되고 deleteObserver()를 호출하면 옵저버 목록에서 탈퇴가 된다.

연락을 돌리는 방법은 java.util.Observable를 상속받는 주제 클래스에서 setChanged() 메소드를 호출해서 객체의 상태가 바뀌었다는 것을 알린후 notifyObservers() 또는 notifyObserver(Object arg) 메소드를 호출하면 된다. (인자값을 넣어주는 메소드는 푸시방식으로 쓰임.)

옵저버 객체가 연락을 받는 방법은 update(Observable o, Object arg) 메소드를 구현하기만 하면된다. Observable o 에는 연락을 보내는 주제 객체가 인자로 전달이 되고 Object arg 에는 notifyObservers(Object arg) 메소드에서 인자로 전달된 데이터 객체가 넘어온다. (데이터 객체가 지정되지 않은경우 null)

 public class WeatherData extends Observable{

private float temperature;

private float humidity;

private float pressure;

public void measurementsChanged(){ 

this.setChanged(); //상태가 바뀌었다는 플래그값을 바꿔줌.

this.notifyObservers(); //풀 방식을 사용해서 알림

}

public void setMeasurementsChanged(float t, float h, float p){ //값이 세팅된다고 가정.

this.temperature = t;

this.humidity = h;

this.pressure = p;

this.measurementsChanged();

}

public float getTemperature() {

return temperature;

}

public float getHumidity() {

return humidity;

}

public float getPressure() {

return pressure;

}

 }

public class CurrentConditions implements Observer, DisplayElement{

        private Observable observable;

private float temperature;

private float humidity;

public CurrentConditions(Observable observable) {

this.observable = observable;

this.observable.addObserver(this);

}

@Override

public void display() {

System.out.println(“Current conditions : “+temperature+” , “+humidity);

}

@Override

public void update(Observable o, Object arg) {

if(o instanceof WeatherData){

WeatherData weatherData = (WeatherData) o;

this.temperature = weatherData.getTemperature();

this.humidity = weatherData.getHumidity();

this.display();

}

}

 }

java.util.Observable 의 단점.

첫째. Observable 은 클래스이기 때문에 서브클래스를 만들어야 한다. 이미 다른 수퍼클래스를 확장하고 있는 클래스에 Observable의 기능을 추가할수가 없어서 재사용성에 제약이 생긴다.

둘째. Observable 인터페이스라는 것이 없기 때문에 자바에 내장된 Observer API 하고 잘 맞는 클래스를 직접 구현하는 것이 불가능하다.

java.util.Observable을 확장한 클래스를 쓸 수 있는 상황이면 Observable API를 쓰는 것도 괜찮지만 상황에 따라 직접 구현해야 할수도 있다.

둘중 어떤방법을 쓰든 옵저버 패턴만 제대로 알고 있다면 그 패턴을 활용하는 API는 어떤 것이든 잘 활용할 수 있다.

–State (상태) -동일한 동작을 객체의 상태에 따라 다르게 처리해야 할 때 사용하는 디자인 패턴

스테이트 패턴 ( State Pattern )

스테이트 패턴은 객체가 특정 상태에 따라 행위를 달리하는 상황에서,

자신이 직접 상태를 체크하여 상태에 따라 행위를 호출하지 않고,

상태를 객체화 하여 상태가 행동을 할 수 있도록 위임하는 패턴을 말합니다.

즉, 객체의 특정 상태를 클래스로 선언하고, 클래스에서는 해당 상태에서 할 수 있는 행위들을 메서드로 정의합니다.

그리고 이러한 각 상태 클래스들을 인터페이스로 캡슐화 하여, 클라이언트에서 인터페이스를 호출하는 방식을 말합니다.

여기서 상태란, 객체가 가질 수 있는 어떤 조건이나 상황을 의미합니다.

1. 스테이트 패턴 사용 이유

예를 들어, 노트북을 켜고 끄는 상황을 생각해보겠습니다.

  • 노트북 전원이 켜져 있는 상태에서 전원 버튼을 누르면, 전원을 끌 수 있습니다.
  • 노트북 전원이 꺼져 있는 상태에서 전원 버튼을 누르면, 전원을 켤 수 있습니다.

이런 행위를 할 수 있는 Laptop 클래스는 다음과 같이 정의할 수 있습니다.

public class Laptop {
public static String ON = "on";
public static String OFF = "off";
private String powerState = "";

public Laptop(){
setPowerState(Laptop.OFF);
}

public void setPowerState(String powerState){
this.powerState = powerState;
}

public void powerPush(){
if ("on".equals(this.powerState)) {
System.out.println("전원 off");
}
else {
System.out.println("전원 on");
}
}
}
public class Client {
public static void main(String args[]){
Laptop laptop = new Laptop();
laptop.powerPush();
laptop.setPowerState(Laptop.ON);
laptop.powerPush();
}
}

노트북이 on 상태이면 노트북 상태를 off로 변경하고, 상태가 off이면, on으로 변경하는 간단한 코드입니다.

그런데 간단하게 켜고, 끄는 노트북에 절전모드를 추가한다고 해보겠습니다.

상태에 따른 동작은 다음과 같다고 가정합니다.

  • 노트북 전원이 켜져 있는 상태에서 전원 버튼을 누르면, 전원을 끌 수 있습니다.
  • 노트북 전원이 꺼져 있는 상태에서 전원 버튼을 누르면, 절전모드가 됩니다.
  • 노트북 절전모드 상태에서 전원 버튼을 누르면, 전원을 켤 수 있습니다.

이렇게 절전모드가 추가된 Laptop 클래스는 다음과 같이 조건문이 하나 더 추가됩니다.

public class Laptop {
public static String ON = "on";
public static String OFF = "off";
public static String SAVING = "saving";
private String powerState = "";

public Laptop(){
setPowerState(Laptop.OFF);
}

public void setPowerState(String powerState){
this.powerState = powerState;
}

public void powerPush(){
if ("on".equals(this.powerState)) {
System.out.println("전원 off");
}
else if ("saving".equals(this.powerState)){
System.out.println("전원 on");
}
else {
System.out.println("절전 모드");
}
}
}
public class Client {
public static void main(String args[]){
Laptop laptop = new Laptop();
laptop.powerPush();
laptop.setPowerState(Laptop.ON);
laptop.powerPush();
laptop.setPowerState(Laptop.SAVING);
laptop.powerPush();
laptop.setPowerState(Laptop.OFF);
laptop.powerPush();
laptop.setPowerState(Laptop.ON);
laptop.powerPush();
}
}

조건문이 하나 추가 된다고 해서 크게 불편한 것은 없는데 뭐가 문제일까요?

그런데 상태가 여러개 있다면 분기하는 코드는 굉장히 길어질 것이기 때문에, 상태에 따라 하고자 하는 행위를 파악하기가 쉽지 않을 것입니다.

또한 Laptop의 powerPush() 메서드 뿐만 아니라, 예를 들면 TV의 powerPush() , SmartPhone의 powerPush() 에서 같은 분기문이 사용된다면, 기능이 변경될 때 마다 일일이 다 찾아가서 수정을 해야 합니다.

따라서 이렇게 상태에 따라 행위를 달리해야 하는 경우에 사용하는 패턴이 스테이트 패턴입니다.

2. 스테이트 패턴 적용

스테이트 패턴을 적용하면 각 상태들, 즉 On, Off, Saving 상태를 클래스로 정의하고, 이들을 하나의 인터페이스로 묶습니다.

그리고나서 Laptop이 상태 인터페이스의 메서드를 호출하면, 각 상태 클래스에서 정의된 행위가 수행되는 방식입니다.

이를 코드로 표현하면 다음과 같습니다.

1)

먼저 전원 상태를 캡슐화한 인터페이스를 선언합니다.

public interface PowerState {
public void powerPush();
}

2)

다음으로 PowerState 인터페이스를 구현한 각 상태 클래스를 정의합니다.

public class On implements PowerState{
public void powerPush(){
System.out.println("전원 off");
}
}
public class Off implements PowerState {
public void powerPush(){
System.out.println("절전 모드");
}
}
public class Saving implements PowerState {
public void powerPush(){
System.out.println("전원 on");
}
}

3)

이어서 Laptop 클래스를 수정합니다.

public class Laptop {
private PowerState powerState;

public Laptop(){
this.powerState = new Off();
}

public void setPowerState(PowerState powerState){
this.powerState = powerState;
}

public void powerPush(){
powerState.powerPush();
}
}

Laptop 클래스는 이제 상태를 분기하는 코드가 사라지고, 인터페이스의 powerPush() 메서드를 호출하기만 합니다.

4)

마지막으로 Laptop 객체를 사용하는 Client 클래스를 정의합니다. 

public class Client {
public static void main(String args[]){
Laptop laptop = new Laptop();
On on = new On();
Off off = new Off();
Saving saving = new Saving();

laptop.powerPush();
laptop.setPowerState(on);
laptop.powerPush();
laptop.setPowerState(saving);
laptop.powerPush();
laptop.setPowerState(off);
laptop.powerPush();
laptop.setPowerState(on);
laptop.powerPush();
}
}

이전과 결과는 동일합니다.

이상으로 스테이트 패턴에 대해 알아보았습니다.

스테이트 구현 과정을 보면, 전략 패턴과 상당히 유사합니다.

거의 동일한 구조이죠.

굳이 사용을 구분 하자면, 전략 패턴은 상속을 대체하려는 목적으로, 스테이트 패턴은 코드내의 조건문들을 대체하려는 목적으로 사용됩니다. ( 참고 )

참고로 Java에서도 여러 디자인 패턴을 사용하고 있는데 State 패턴을 사용하고 있는 API는 다음과 같으며, 참고하시면 좋을 것 같습니다. ( 참고링크 )

State

( recognizeable by behavioral methods which changes its behaviour depending on the instance’s state which can be controlled externally )

–Strategy (전략) -알고리즘 군을 정의하고 각각 하나의 클래스로 캡슐화한 다음, 필요할 때 서로 교환해서 사용할 수 있게 해준다.

전략 패턴 ( Strategy Pattern )

객체들이 할 수 있는 행위 각각에 대해 전략 클래스를 생성하고, 유사한 행위들을 캡슐화 하는 인터페이스를 정의하여,

객체의 행위를 동적으로 바꾸고 싶은 경우 직접 행위를 수정하지 않고 전략을 바꿔주기만 함으로써 행위를 유연하게 확장하는 방법을 말합니다.

간단히 말해서 객체가 할 수 있는 행위들 각각을 전략으로 만들어 놓고, 동적으로 행위의 수정이 필요한 경우 전략을 바꾸는 것만으로 행위의 수정이 가능하도록 만든 패턴입니다.

1. 전략 패턴 사용 이유

예를 들어, 기차( Train )와 버스( Bus ) 클래스가 있고, 이 두 클래스는 Movable 인터페이스를 구현했다고 가정하겠습니다.

그리고 Train과 Bus 객체를 사용하는 Client도 있습니다.

이 구조를 코드로 표현하면 다음과 같습니다.

public interface Movable {
public void move();
}
public class Train implements Movable{
public void move(){
System.out.println("선로를 통해 이동");
}
}
public class Bus implements Movable{
public void move(){
System.out.println("도로를 통해 이동");
}
}
public class Client {
public static void main(String args[]){
Movable train = new Train();
Movable bus = new Bus();

train.move();
bus.move();
}
}

기차는 선로를 따라 이동하고, 버스는 도로를 따라 이동합니다.

그러다 시간이 흘러 선로를 따라 움직이는 버스가 개발되었다고 가정해봅시다.

그러면 Bus의 move() 메서드를 다음과 같이 바꿔주기면 하면 끝납니다.

public void move(){
System.out.println("선로를 따라 이동");
}

그런데 이렇게 수정하는 방식은 SOLID의 원칙 중 OCP( Open-Closed Principle )에 위배됩니다.

OCP에 의하면 기존의 move()를 수정하지 않으면서 행위가 수정되어야 하지만, 지금은 Bus의 move() 메서드를 직접 수정했지요.

또한 지금과 같은 방식의 변경은 시스템이 확장이 되었을 때 유지보수를 어렵게 합니다.

예를 들어, 버스와 같이 도로를 따라 움직이는 택시, 자가용, 고속버스, 오토바이 등이 추가된다고 할 때, 모두 버스와 같이 move() 메서드를 사용합니다.

만약에 새로 개발된 선로를 따라 움직이는 버스와 같이, 선로를 따라 움직이는 택시, 자가용, 고속버스 … 등이 생긴다면,

택시, 자가용, 고속버스의 move() 메서드를 일일이 수정해야 할 뿐더러, 같은 메서드를 여러 클래스에서 똑같이 정의하고 있으므로 메서드의 중복이 발생하고 있습니다.

즉, 지금과 같은 수정 방식의 문제점은 다음과 같습니다.

  • OCP 위배
  • 시스템이 커져서 확장이 될 경우 메서드의 중복문제 발생

따라서 이를 해결하고자 전략 패턴을 사용하려고 합니다.

2. 전략 패턴 구현

이번에는 위와 같이 선로를 따라 이동하는 버스가 개발된 상황에서 시스템이 유연하게 변경되고 확장될 수 있도록 전략 패턴을 사용해보도록 하겠습니다.

1)

먼저 전략을 생성하는 방법입니다.

현재 운송수단은 선로를 따라 움직이든지, 도로를 따라 움직이든지 두 가지 방식이 있습니다.

즉, 움직이는 두 방식에 대해 Strategy 클래스를 생성하도록 합니다. ( RailLoadStrategy, LoadStrategy )

그리고 두 클래스는 move() 메서드를 구현하여, 어떤 경로로 움직이는지에 대해 구현합니다.

또한 두 전략 클래스를 캡슐화 하기 위해 MovableStrategy 인터페이스를 생성합니다.

이렇게 캡슐화를 하는 이유는 운송수단에 대한 전략 뿐만 아니라,

다른 전략들( 예를 들어, 주유방식에 대한 전략 등)이 추가적으로 확장되는 경우를 고려한 설계입니다.

이를 코드로 표현하면 다음과 같습니다.

public interface MovableStrategy {
public void move();
}
public class RailLoadStrategy implements MovableStrategy{
public void move(){
System.out.println("선로를 통해 이동");
}
}
public class LoadStrategy implements MovableStrategy{
public void move() {
System.out.println("도로를 통해 이동");
}
}

2)

다음으로 운송 수단에 대한 클래스를 정의할 차례입니다.

기차와 버스 같은 운송 수단은 move() 메서드를 통해 움직일 수 있습니다.

그런데 이동 방식을 직접 메서드로 구현하지 않고, 어떻게 움직일 것인지에 대한 전략을 설정하여, 그 전략의 움직임 방식을 사용하여 움직이도록 합니다.

그래서 전략을 설정하는 메서드인 setMovableStrategy()가 존재합니다.

이를 코드로 표현하면 다음과 같습니다.

public class Moving {
private MovableStrategy movableStrategy;

public void move(){
movableStrategy.move();
}

public void setMovableStrategy(MovableStrategy movableStrategy){
this.movableStrategy = movableStrategy;
}
}
public class Bus extends Moving{

}
public class Train extends Moving{

}

3)

이제 Train과 Bus 객체를 사용하는 Client를 구현할 차례입니다.

Train과 Bus 객체를 생성한 후에, 각 운송 수단이 어떤 방식으로 움직이는지 설정하기 위해 setMovableStrategy() 메서드를 호출합니다.

그리고 전략 패턴을 사용하면 프로그램 상으로 로직이 변경 되었을 때, 얼마나 유연하게 수정을 할 수 있는지 살펴보기 위해

선로를 따라 움직이는 버스가 개발되었다는 상황을 만들어 버스의 이동 방식 전략을 수정했습니다.

public class Client {
public static void main(String args[]){
Moving train = new Train();
Moving bus = new Bus();

/*
기존의 기차와 버스의 이동 방식
1) 기차 - 선로
2) 버스 - 도로
*/
train.setMovableStrategy(new RailLoadStrategy());
bus.setMovableStrategy(new LoadStrategy());

train.move();
bus.move();

/*
선로를 따라 움직이는 버스가 개발
*/
bus.setMovableStrategy(new RailLoadStrategy());
bus.move();
}
}

이상으로 전략 패턴이 무엇인지에 대해 알아보았습니다.

Java에서도 여러 디자인 패턴을 사용하고 있는데 Strategy 패턴을 사용하고 있는 API는 다음과 같으며, 참고하시면 좋을 것 같습니다. ( 참고링크 )

Strategy

recognizeable by behavioral methods in an abstract/interface type which invokes a method in an implementation of a different abstract/interface type which has been passed-in as method argument into the strategy implementation

–Template Method -상위 클래스에서는 추상적으로 표현하고 그 구체적인 내용은 하위 클래스에서 결정되는 디자인 패턴

템플릿 메서드 패턴 ( Template Method Pattern )

템플릿 메서드 패턴은 여러 클래스에서 공통으로 사용하는 메서드를 상위 클래스에서 정의하고,

하위 클래스마다 다르게 구현해야 하는 세부적인 사항을 하위 클래스에서 구현하는 패턴을 말합니다.

코드의 중복 제거를 위해 흔히 사용하는 리팩토링 기법이죠.

상위 클래스에서 정의하는 부분은 템플릿 메서드라 하고,

템플릿 메서드에서 하위 클래스마다 다르게 작성되야 하는 일부분을 훅이라 합니다.

( 용어가 중요한 것은 아니지만…. )

웹 개발을 하다보면 HTML 문서에다가 서버로부터 받은 값을 변수에 할당하곤 하는데,

HTML을 템플릿 메서드, 템플릿 변수를 훅으로 대응할 수 있을 것 같습니다.

코드를 보면 더 쉽게 이해가 될 것습니다.

public class Parent {
// 자식에서 공통적으로 사용하는 부분( someMethod )를 템플릿 메서드라 한다.
public void someMethod(){
System.out.println("부모에서 실행되는 부분 - 상");

// 자식에서 구현해야 할 부분을 훅 메서드라 한다.
hook();

System.out.println("부모에서 실행되는 부분 - 하");
}

public void hook(){};
}
public class ChildA extends Parent {
@Override
public void hook(){
System.out.println("Child A 에서 hook 구현");
}
}
public class ChildB extends Parent{
@Override
public void hook(){
System.out.println("Child B 에서 hook 구현");
}
}
public class Client {
public static void main(String args[]){
ChildA childA = new ChildA();
childA.someMethod();

System.out.println("--------");

ChildB childB = new ChildB();
childB.someMethod();
}
}

Parent 클래스에 정의된 someMethod()는 ChildA, ChildB에서 공통으로 사용하는 부분이고,

hook() 메서드만 ChildA, ChildB에서 따로 구현해줘야 하는 부분입니다.

이상으로 템플릿 메서드 패턴이 무엇인지에 대해 알아보았습니다.

Java에서도 여러 디자인 패턴을 사용하고 있는데 템플릿 메서드 패턴을 사용하고 있는 API는 다음과 같으며, 참고하시면 좋을 것 같습니다. ( 참고링크 )

Template method

(recognizeable by behavioral methods which already have a “default” behaviour defined by an abstract type)

–Visitor (방문자) -각 클래스의 데이터 구조로부터 처리 기능을 분리하여 별도의 visitor 클래스로 만들어놓고 해당 클래스의 메서드가   각 클래스를 돌아다니며 특정 작업을 수행하도록 하는 것

방문자 (Visitor) 패턴

1. 개념

비지터 패턴은 방문자와 방문 공간을 분리하여,
방문 공간이 방문자를 맞이할 때, 이후에 대한 행동을 방문자에게 위임하는 패턴이다.

간단하게 설명하기 참 어려운 패턴이다.
보통 OOP에서, 객체는 그 객체가 하는 행동을 메쏘드로 가지고 있다.
그리고 행동의 대상이 되는 객체가 있을 경우, 메쏘드의 파라미터로 입력받는다.
그런데, 비지터 패턴은 행동의 대상이 되는 객체가 행동을 일으키는 객체를 입력으로 받는다.
설명이 좀 어려운데, 간단히 예로 설명하면 다음과 같다.

“나는 상점에 방문한다. 나는 ~를 한다.”
‘나’라는 객체가 ‘상점’이라는 객체를 입력받은 후, 이 상점에 대해서 뭔가를 한다.
이게 일반적인 OOP 추상화다.
반면 비지터 패턴은 다음과 같다.

“상점에 내가 방문을 했다. 내가 ~를 하게 한다.”
‘상점’ 이라는 객체가 ‘나’라는 객체를 입력받은 후, ‘나’ 라는 객체의 행동을 호출하는 것이다.
이 때, 상점에 대한 정보를 파라미터로 넘겨준다.
즉, 사용자는 방문자의 입장이 아니라, 방문 공간의 입장에서 먼저 생각해보게 된다.

1.1. 구조

출처 : https://www.codeproject.com/Articles/186185/Visitor-Design-Pattern-2
  • Visitor
    • 방문자 클래스의 인터페이스
    • visit(Element) 을 공용 인터페이스로 쓴다. Element 는 방문 공간이다.
  • Element
    • 방문 공간 클래스의 인터페이스
    • accept(Vistor) 를 공용 인터페이스로 쓴다. Visitor 는 방문자다.
      • 내부적으로 Vistor.visit(this) 를 호출한다.
  • ConcreteVisitor
    • Visitor 를 구체적으로 구현한 클래스
  • ConcreteElement
    • Element 를 구체적으로 구현한 클래스

1.2. 장점

  • 작업 대상(방문 공간) 과 작업 항목(방문 공간을 가지고 하는 일)을 분리시킨다.
    • 작업 대상(방문 공간) 은 단지 데이터를 담고있는 자료구조로 만들고,
    • 작업 주체(방문자) 는 visit() 안에 이 작업 대상을 입력받아 작업 항목을 처리하면 된다.
    • 즉, 데이터와 알고리즘이 분리되어, 데이터의 독립성을 높여준다.
  • 작업 대상의 입장에서는 accept() 로 인터페이스를 통일시켜, 사용자에게 동일한 인터페이스를 제공한다.

1.3. 단점

  • 새로운 작업 대상(방문 공간)이 추가될 때마다 작업 주체(방문자) 도 이에 대한 로직을 추가해야 한다.
  • 두 객체 (방문자와 방문 공간)의 결합도가 높아진다.
    • 서로 visit() 과 accept() 에 의존하므로.

1.4. 활용 상황

  • 자료 구조(데이터)와 자료 구조를 처리하는 로직(알고리즘)을 분리해야할 경우
  • 데이터 구조보다 알고리즘이 더 자주 바뀌는 경우
    • 즉, 방문공간은 어느정도 정해져있고 방문자가 더 자주 바뀌는 경우

2. 구현

클라이언트는 다음과 같이 사용할 수 있다.

concrete_visitor_1 = ConcreteVisitor1()

for concrete_element in [ConcreteElementA(), ConcreteElementB()]:
    concrete_element.accept(concrete_visitor_1)

방문자 개념인 Visitor의 추상클래스와 구체적인 클래스는 다음과 같다.

class Visitor(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def visit_concrete_element_a(self, concrete_element_a: ConcreteElementA):
        # concrete_element_a 를 방문했을 때, 처리할 로직
        # 하위 클래스에서 구현한다.

    @abc.abstractmethod
    def visit_concrete_element_b(self, concrete_element_b: ConcreteElementB):
        # concrete_element_b 를 방문했을 때, 처리할 로직
        # 하위 클래스에서 구현한다.


class ConcreteVisitor1(Visitor):
    def visit_concrete_element_a(self, concrete_element_a: ConcreteElementA):
        pass

    def visit_concrete_element_b(self, concrete_element_b: ConcreteElementB):
        pass


class ConcreteVisitor2(Visitor):
    def visit_concrete_element_a(self, concrete_element_a: ConcreteElementA):
        pass

    def visit_concrete_element_b(self, concrete_element_b: ConcreteElementB):
        pass

방문 공간의 개념인 Component의 추상클래스와 구체적인 클래스는 다음과 같다.

class Element(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def accept(self, visitor: Visitor):
        pass

class ConcreteElementA(Element):
    def accept(self, visitor: Visitor):
        visitor.visit_concrete_element_a(self)


class ConcreteElementB(Element):
    def accept(self, visitor: Visitor):
        visitor.visit_concrete_element_b(self)

3. 정리

구현만 보면 별로 어렵지는 않은데, 정확히 어떤 경우에 사용하는지가 좀 의문이긴 하다.
결합도가 높아진다는 측면에서, 그렇게 매우 좋아보이지는 않는데…
찾아보고 찾아봐도, 뭔가 뚜렷하게 사용해야겠다는 느낌이 잘 안온다.

객체에 대한 행위의 내용을 외부 클래스로 빼서 객체의 행위를 위임하기도 한다. 이런 타입의 패턴으로 전략패턴, 커맨드 패턴, 비지터 패턴등이 있다. 셋 모두 객체의 행위를 바깥으로 위임하는 것이지만, 전략패턴이 하나의 객체에 대해 여러 동작을 하게 하거나(1:N), 커맨드 패턴이 하나의 객체에 대하 하나의 동작(+보조동작)에 대한 설계방식(1:1)인 반면에, 방문자 패턴은 여러 객체들에 대해 객체의 동작들을 지정하는 방식(N:N) 이다.

Start typing and press Enter to search

Shopping Cart