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

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


팩토리 메서드 패턴

: 조건에 따른 객체 생성을 팩토리 클래스로 위임하여, 팩토르 클래스에서 객체를 생성하는 패턴 ( 링크 )


추상 팩토리 패턴

: 서로 관련이 있는 객체들을 통째로 묶어서 팩토리 클래스로 만들고, 이들 팩토리를 조건에 따라 생성하도록 다시 팩토리를 만들어서 객체를 생성하는 패턴


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


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

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

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


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

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

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


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

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



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

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


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

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





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

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

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



1)

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

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

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

public class LGKeyboard extends Keyboard {
public LGKeyboard(){
System.out.println("LG 키보드 생성");
}

}

public class SamsungKeyboard extends Keyboard {
public SamsungKeyboard(){
System.out.println("Samsung 키보드 생성");
}
}
public interface class 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 extends Mouse {
public LGMouse(){
System.out.println("LG 마우스 생성");
}
}
public class SamsungMouse extends Mouse {
public SamsungMouse(){
System.out.println("Samsung 마우스 생성");
}
}
public interface class 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)



팩토리 메서드 패턴 ( 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. 팩토리 메서드 패턴 적용

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

1) 팩토리 클래스를 정의

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)



템플릿 메서드 패턴 ( 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)




데커레이터 패턴 ( 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)




옵저버 패턴 ( Observer Pattern )

옵저버 패턴은 어떤 객체에 이벤트가 발생했을 때, 이 객체와 관련된 객체들( 옵저버들 )에게 통지하도록 하는 디자인 패턴을 말합니다.

즉, 객체의 상태가 변경되었을 때, 특정 객체에 의존하지 않으면서 상태의 변경을 관련된 객체들에게 통지하는 것이 가능해집니다.

이 패턴은 PUB/SUB( 발행/구독 ) 모델으로 불리기도 합니다.


예를 들어, 유튜브를 생각해보겠습니다.

PUB/SUB 모델에 따르면, 유튜브 채널은 발행자가 되고 구독자들은 구독자( 옵저버 )가 되는 구조입니다.

즉 유튜버가 영상을 올리면, 구독자들은 영상이 올라왔다는 알림을 받을 수 있습니다.


이렇게 각각의 유저들을 유튜브 채널을 구독하고 있는 옵저버가 됩니다.





1. 옵저버 패턴 사용 이유

예를 들어, 어떤 유저와 채팅 방이 있다고 가정하겠습니다.

유저는 채팅방에 말할 수 있고,

채팅방은 이를 들을 수 있습니다.


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

public class User {
private Room room;

public void setRoom(Room room){
this.room = room;
}

public void talk(String msg){
room.receive(msg);
}
}
public class Room {
public void receive(String msg){
System.out.println(msg);
}
}
public class Client {
public static void main(String args[]){
User user = new User();
Room room = new Room();

user.setRoom(room);

String msg = "안녕하세요~~";
user.talk(msg);
}
}



그런데 유저가 여러 채팅 방(채팅방, 게임방, 개발방)에 입장을 하게되었습니다.

그리고 유저가 채팅을 입력하면 모든 채팅방에 메시지가 전달되어야 하는 상황입니다.

( 예를 들면, 공지사항을 전달하는 개념 )


이렇게 여러 채팅 방에 참여하게 될 경우, 코드는 다음과 같습니다.


public class Room {
publicString roomName;

public void receive(String msg){
System.out.println(this.roomName + "에서 메시지를 받음 : " + msg);
}
}

public class ChatRoom extends Room{
public ChatRoom(String roomName){
this.roomName = roomName;
}
}
public class GameRoom extends Room{
public GameRoom(String roomName){
this.roomName = roomName;
}
}
public class DevRoom extends Room{
public DevRoom(String roomName){
this.roomName = roomName;
}
}

채팅방, 게임방, 개발방 클래스를 정의하고 이를 Room 클래스로 캡슐화합니다.


import java.util.List;

public class User {
private List<Room> room;

public void setRoom(List<Room> room){
this.room = room;
}

public void talk(String msg){
for(Room r:room){
r.receive(msg);
}
}
}
import java.util.ArrayList;
import java.util.List;

public class Client {
public static void main(String args[]){
User user = new User();
List<Room> rooms = new ArrayList<Room>();
rooms.add(new ChatRoom("채팅방"));
rooms.add(new GameRoom("게임방"));
rooms.add(new DevRoom("개발방"));

user.setRoom(rooms);

String msg = "안녕하세요~";
user.talk(msg);

}
}

Client 클래스의 코드를 보시면, User와 Room은 매우 강하게 연결되어 있습니다.

이런 상황에서 다른 방이 생성되거나, 특정 방이 제거되어 메시지를 보내야 한다면 어떻게 해야 할까요?

물론 List이기 때문에 추가 제거가 가능하지만, 옵저버 패턴을 이용하면 객체지향적으로 구현이 가능해집니다.

즉, 옵저버 패턴을 사용함으로써 시스템이 유연해지고, 객체간의 의존성도 제거됩니다.





2. 옵저버 패턴 적용

이제 옵저버 패턴을 적용해서 개선을 하도록 하겠습니다.


1)

먼저 채팅방, 게임방, 개발방을 옵저버로 둡니다.

그러기 위해서 옵저버 클래스를 정의하여 캡슐화 하도록 하겠습니다.

( 상황에 따라서 인터페이스 또는 추상클래스로 정의해도 상관은 없는데, 지금은 클래스가 더 어울려 보입니다. )

public class Observer {
public String roomName;

public void receive(String msg){
System.out.println(this.roomName + "에서 메시지를 받음 : " + msg);
}
}

public class ChatRoom extends Observer{
public ChatRoom(String roomName){
this.roomName = roomName;
}
}

public class GameRoom extends Observer{
public GameRoom(String roomName) {
this.roomName = roomName;
}
}
public class DevRoom extends Observer{
public DevRoom(String roomName){
this.roomName = roomName;
}
}


2)

다음으로 옵저버를 추가하고 제거하고 메시지를 알리는 기능들을 정의하는 Subject 클래스를 정의합니다.

그리고 User 클래스에서 Subject 클래스를 상속받도록 합니다.

즉, User 클래스에서 옵저버들을 관리할 수 있는 것이죠.

import java.util.ArrayList;
import java.util.List;

public class Subject {
private List<Observer> observers = new ArrayList<Observer>();

// 옵저버에 추가
public void attach(Observer observer){
observers.add(observer);
}

// 옵저버에서 제거
public void detach(Observer observer){
observers.remove(observer);
}

// 옵저버들에게 알림
public void notifyObservers(String msg){
for (Observer o:observers) {
o.receive(msg);
}
}
}
public class User extends Subject{

}


3)

마지막으로 Client에서 코드가 어떻게 바뀌었는지 보겠습니다.

의존성이 많이 제거된 것을 확인할 수 있습니다.

public class Client {
public static void main(String args[]){
User user = new User();
ChatRoom chatRoom = new ChatRoom("채팅방");
GameRoom gameRoom = new GameRoom("게임방");
DevRoom devRoom = new DevRoom("개발방");
user.attach(chatRoom);
user.attach(gameRoom);
user.attach(devRoom);

String msg = "안녕하세요~";
user.notifyObservers(msg);

user.detach(chatRoom);
msg = "Hi~";
user.notifyObservers(msg);
}
}





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


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

Observer (or Publish/Subscribe)

(recognizeable by behavioral methods which invokes a method on an instance of another abstract/interface type, depending on own state)




커맨드 패턴( 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 )



스테이트 패턴 ( State Pattern )

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

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

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


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

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

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





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

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

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

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


이런 행위를 할 수 있는 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으로 변경하는 간단한 코드입니다.



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

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

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

2) 노트북 전원이 꺼져 있는 상태에서 전원 버튼을 누르면, 절전모드가 됩니다.

3) 노트북 절전모드 상태에서 전원 버튼을 누르면, 전원을 켤 수 있습니다.


이렇게 절전모드가 추가된 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 )




싱글톤 패턴 ( 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



전략 패턴 ( 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() 메서드를 일일이 수정해야 할 뿐더러, 같은 메서드를 여러 클래스에서 똑같이 정의하고 있으므로 메서드의 중복이 발생하고 있습니다.


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

1) OCP 위배

2) 시스템이 커져서 확장이 될 경우 메서드의 중복문제 발생


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





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




SOLID 원칙

SOLID 원칙이란 객체지향 설계에서 지켜줘야 할 5개의 원칙( SRP, OCP, LSP, DIP, ISP )을 말합니다.

하지만... 개념을 알아도 실현하기는 어려운 원칙들입니다.


그럼에도 설계원칙을 알아야 하는 이유는 시스템에 예상하지 못한 변경사항이 발생하더라도, 유연하게 대처하고 이후에 확장성이 있는 시스템 구조를 설계하기 위해서입니다.

좋은 설계란 시스템에 새로운 요구사항이나 변경사항이 있을 때, 영향을 받는 범위가 적은 구조를 말합니다.


앞으로 앞아볼 여러 디자인 패턴들은 아래의 SOLID 원칙에 입각해서 만들어진 것이므로, SOLID 원칙이 무엇인지 알아보도록 하겠습니다.





1. SRP( Single Responsibility Principle ), 단일 책임 원칙

객체는 단 하나의 책임만 가져야 한다는 원칙을 말합니다.


객체지향적으로 설계할 때는 응집도를 높게, 결합도는 낮게 설계하는 것이 좋습니다.

응집도란, 한 프로그램의 요소가 얼마나 뭉쳐있는지, 즉 구성 요소들 사이의 응집력을 말하며,

결합도란 프로그램 구성 요소들 사이가 얼마나 의존적인지를 말합니다.

SRP에 따른 설계를 하면 응집도는 높게, 결합도는 낮게 설계할 수 있게 됩니다.



흔히 함수는 하나의 기능만 수행하도록 구현되어야 하는 것을 알고 있습니다.

calculator() 함수가 덧셈, 뺼셈, 곱셈, 나눗셈을 모두 한다면 이는 좋은 설계가 아닙니다.

덧셈, 뺼셈, 곱셈, 나눗셈이 각각의 함수로 정의되어 있어야 합니다.


마찬가지로 Calculator 객체가 있을 때, Calculator 객체는 덧셈, 뺄셈, 곱셈, 나눗셈만 할 수 있어야 합니다.

즉, 사칙연산에 대한 책임만 갖고 있어야 합니다.

이후에 계산기에 알람 기능을 추가한다고 해서, alarm() 함수를 Calculator의 기능으로 추가하면 SRP에 위배됩니다.



한 객체에 책임이 많아질수록, 클래스 내부에서 서로 다른 역할을 수행하는 코드끼리 강하게 결합될 가능성이 높아집니다.

즉, 객체마다 책임을 제대로 나누지 않는다면 시스템은 매우 복잡해집니다.

왜냐하면 그 객체가 하는 일( 함수 )에 변경사항이 생기면 이 기능을 사용하는 부분의 코드를 모두 다시 테스트를 해야 하기 때문입니다.


따라서 여러 객체들이 하나의 책임만 갖도록 잘 분배한다면, 시스템에 변화가 생기더라도 그 영향을 최소화 할 수 있기 때문에 SRP 원칙을 따르는 것이 좋습니다.





2. OCP ( Open-Closed Principle ), 개방-폐쇄 원칙

기존의 코드를 변경하지 않으면서( closed ), 기능을 추가할 수 있도록( open ) 설계가 되어야 한다는 원칙을 말합니다.

즉, 확장에 대해서는 개방적이고 수정에 대해서는 폐쇄적이어야 한다는 의미를 갖습니다.


이를 만족하는 설계가 되려면, 캡슐화를 통해 여러 객체에서 사용하는 같은 기능을 인터페이스에 정의하는 방법이 있습니다.



Animal 인터페이스를 구현한 각 클래스들은 울음소리 crying() 함수를 재정의합니다.
울음소리를 호출하는 클라이언트는 다음과 같이 인터페이스에서 정의한 crying() 함수만 호출하면 됩니다.
public class Client {
public static void main(String args[]){
Animal cat = new Cat();
Animal dog = new Dog();

cat.crying();
dog.crying();
}
}
이렇게 캡슐화를 하면, 동물이 추가되었을 때 cyring() 함수를 호출하는 부분은 건드릴 필요가 없으면서 쉽게 확장할 수 있게됩니다.




3. LSP ( Liskov Substitution Principle ), 리스코프 치환 원칙

자식 클래스는 최소한 자신의 부모 클래스에서 가능한 행위는 수행할 수 있어야 한다는 설계 원칙입니다.

즉, 자식 클래스는 언제나 부모 클래스의 역할을 대체할 수 있어야 한다는 것을 말하며, 부모 클래스와 자식 클래스의 행위가 일관됨을 의미합니다.


자식 클래스가 부모 클래스를 대체하기 위해서는 부모의 기능에 대해 오버라이드 되지 않도록 하면 됩니다.

즉, 자식 클래스는 부모 클래스의 책임을 무시하거나 재정의하지 않고 확장만 수행하도록 해야 LSP를 만족하게 됩니다.


처음 OOP를 공부할 때 오버라이드가 OOP의 특징이라 배웠고 이를 잘 활용해야 할 것만 같았는데,

LSP에 따르면 객체지향적으로 설계를 하기 위해서는 오버라이드는 가급적 피하는 것이 좋다고 합니다.





4. ISP ( Interface Segregation Principle ), 인터페이스 분리 원칙

자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다는 설계 원칙입니다.

즉, 하나의 거대한 인터페이스 보다는 여러 개의 구체적인 인터페이스가 낫다는 것을 의미합니다.

SRP는 객체의 단일 책임을 뜻한다면, ISP는 인터페이스의 단일 책임을 의미한다고 보면 됩니다.


예를 들어, 핸드폰( Phone )에는 전화( call ), 문자( sms ), 알람( alarm ), 계산기( calculator ) 등의 기능이 있습니다.

옛날 3G폰과 현재 스마트폰은 Phone의 기능들을 사용하므로, call, sms, alarm, calculator 기능이 정의된 Phone 인터페이스를 정의하려고 합니다.


그러나 ISP를 만족하려면 Phone 인터페이스에 call(), sms(), alarm(), calculator() 함수를 모두 정의하는 것보다,

Call, Sms, Alarm, Calculator 인터페이스를 각각 정의하여, 3G폰과 스마트폰 클래스에서 4개의 인터페이스를 구현하도록 설계되어야 합니다.


이렇게 설계를 하면, 각 인터페이스의 메서드들이 서로 영향을 미치지 않게 됩니다.

즉, 자신이 사용하지 않는 메서드에 대해서 영향력이 줄어들게 됩니다.





5. DIP ( Dependency Inversion Principle ), 의존 역전 원칙

객체들이 서로 정보를 주고 받을 때 의존 관계가 형성되는데, 이 때 객체들은 나름대로의 원칙을 갖고 정보를 주고 받아야 한다는 설계 원칙입니다.

여기서 나름대로의 원칙이란, 추상성이 낮은 클래스보다 추상성이 높은 클래스와 의존 관계를 맺어야 한다는 것을 의미합니다.

일반적으로 인터페이스를 활용하면 이 원칙을 준수할 수 있게 됩니다. ( 캡슐화 )



Client 객체는 Cat, Dog, Bird의 crying() 메서드에 직접 접근하지 않고, Animal 인터페이스의 crying() 메서드를 호출함으로써 DIP를 만족할 수 있습니다.





이상으로 객체 지향 프로그래밍의 5가지 원칙 SOLID에 대해 알아보았습니다.


+ Recent posts

티스토리 툴바