Notice
Recent Posts
Recent Comments
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

DeFacto-Standard IT

Adapter Pattern 본문

Design Pattern/Structural Pattern

Adapter Pattern

defacto standard 2018. 2. 16. 23:52

1. 가정


 - TV와 TV 콘센트를 만든다.

 - TV에는 해외 직구용 TV와, 국내용 TV가 존재한다.

 - 해외직구TV는 110V를 사용하며, 국내용 TV는 220V를 사용한다.


2. Naive Code


 -  DomesticTV / OverseasTV

public class DomesticTV {
private final int acceptableVoltage = 220;

public void on(int electricCurrent) {
if ( this.isAcceptableVoltage(electricCurrent) )
System.out.println("Domestic TV is Turned On");
else
System.out.println("Warning : Not Acceptable Voltage");
}

private boolean isAcceptableVoltage(int supplyVoltage) {
return this.acceptableVoltage == supplyVoltage;
}
}

public class OverseasTV {
private final int acceptableVoltage = 110;

public void on(int electricCurrent) {
if ( this.isAcceptableVoltage(electricCurrent) )
System.out.println("Overseas TV is Turned On");
else
System.out.println("Warning : Not Acceptable Voltage");
}

private boolean isAcceptableVoltage(int supplyVoltage) {
return this.acceptableVoltage == supplyVoltage;
}
}


전원을 공급받아, 해당 TV에서 필요로하는 전압과 일치한다면 켜지는 TV이다.

DomesticTV는 국내TV로서 220V를 필요로 한다.

OverseasTV는 해외TV로서 110V를 필요로 한다.


 - DomesticTVConsent / OverseasTVConsent

public class DomesticTVConsent {
public int getDomesticVoltage() {
return 220;
}
}

public class OverseasTVConsent {
public int getOverseasVoltage() {
return 110;
}
}


콘센트에 해당하는 클래스이다.

DomesticTVConsent는 220V를 지원하고,

OverseasTVConsent는 110V를 지원한다.


 - NaiveClient

public class NaiveClient {
public static void main(String args[]) {
DomesticTV domesticTV = new DomesticTV();
DomesticTVConsent domesticTVConsent = new DomesticTVConsent();
OverseasTVConsent overseasTVConsent = new OverseasTVConsent();

domesticTV.on( domesticTVConsent.getDomesticVoltage() );
domesticTV.on( overseasTVConsent.getOverseasVoltage() );
}
}


실행 결과는 다음과 같다.



3. 문제점


위와 같은 소스코드의 문제는, 제공해야 하는 Voltage의 값이 다른 제품이 많아질수록, 계속해서 새로운 Consent클래스를 만들어야 한다.


위의 경우 클래스마다 공통적인 소스코드가 많기 때문에, Template Method Pattern과 Strategy 패턴을 이용한다면 소스코드를 최적화시킬 수 있다.


하지만 간단하지 않고 복잡하고 거대한 시스템이나, 클래스마다 조금씩 다르게 동작할 여지가 생기는 경우에는 Template Method Pattern은 사용하기 조심스러워진다.


만약 Consent처럼 각 TV클래스에 무언가를 제공하는 클래스가 엄청나게 복잡하다는 가정을 해보자. 이를 만드는데는 시간이 오래걸릴 것이다. 즉, 공수가 많아 새로 만들기가 굉장히 부담스럽다.


만약 특정 Consent를 복사하여 새로운 클래스를 만든 후 이를 수정한다고 가정해보자. 아주 현실적이며, 실제로 비슷한 클래스를 찍어내는데 유용하다.


하지만 기존의 클래스가 소스코드가 굉장히 복잡하고, 객체지향적이지 못한 레거시라면 ,수정보다는 새로 만드는게 더 나을 수가 있다.



4. 해결방안


TV를 켜는데 필요한 Voltage가 TV마다 다르기 때문에, 이에 대한 것은 어찌할 방도가 없다. 하지만, 콘센트의 경우는 말이 다르다.


'클래스를 새로 만든다'라는 작업은 얼핏 보면 굉장히 기본적이고 흠이 없어보인다.


하지만 새로 만들 클래스가 굉장히 복잡하다면, 새로운 방법을 고안해볼 필요가 있다.


즉, DomesticTVConsent를 활용하여 OverseasTV를 충전할 수 있게끔 구현한다면 많은 시간을 아낄 수 있다.


DomesticTVConsent는 제공하는 볼트가 DomesticTV에 맞추어져있기 때문에, OverseasTV에 강제로 꽂는 것이 불가능하다.


현실에서 강제로 꽂으면 꽂을 수도 있지만, 고장나거나 충전이 되지 않거나 폭발할 가능성도 있을 수도 있다.


이는 원래 Consent의 구현목적이 아니다.


따라서, 기존 DomesticTVConsent를 이용하려면 바로 객체를 생성하여 사용하는 것은 불가능하다.



'기존의 것을 이용한다'는 것은 Delegation을 의미한다.


즉, 사용하는 클래스와 사용되는 클래스를 직접 연결하는 것이 아니라, 중간에서 어떠한 역할을 해주는 클래스를 사용하는 것을 의미한다.


사용되는 클래스, 즉 기존의 시스템에 해당하는 클래스는 Adaptee 라고 한다.


사용하는 클래스, 즉 새롭게 구현될 시스템의 추상적인 개념을 Target이라고 한다.


구체적으로 구현될 클래스는 Adapter라고 한다. 추상적인 개념을 놓는 이유는, 여러 개의 레거시 시스템을 활용할 여지가 존재할 수 있기 때문이다.


그리고 Target와 Adaptee를 이어주어 Target이 Adaptee를 사용할 수 있도록 해주는 것을 Adapter라고 한다.


이미 구현된 기존 시스템인 Adaptee와 새롭게 구현될 시스템인 Target은, 이 둘을 연결시키는 Adapter에 의해 연결된다.




NaiveCode에 이를 적용해보자.


DomesticTVConsent는 220V를 지원하여 DomesticTV를 켤 수 있는 Voltage값을 제공한다. 

게다가, OverseasTVConsent와 같은 추가적인 Consent없이도, OverseasTV를 켤 수 있는 Voltage 역시 제공되어야 한다.



1. Voltage값에 대한 소스코드가 바뀐다면?

둘 중 하나는 지원할 수 없게 되어버린다. 따라서 이 방법은 불가능하다.


2. Consent를 추가

위에서 말한 것 처럼 공수가 크다면 비효율적이다. 


3. 위 모든 경우 테스트 코드 역시 다시 작성되어야 한다.


따라서, 기존의 소스코드인 DomesticTVConsent를 활용하여 어떠한 연산을 추가적으로 할 필요가 있다.


이를 DomesticOverseasAdapter라고 하겠다. 


이 AdapterClass는 220V를 110V로 전환하는 역할을 한다. 내부적으로는 DomesticTVConsent에 대한 레퍼런스가 있다.


즉, DomesticOverseasAdapter는 전원을 공급하는 메서드가 있고, DomesticTVConsent의 메서드로부터 220V를 제공받으므로, 110V만큼의 값을 뺀 다음 제공하면 된다.


이렇게 한다면 기존의 DomesticTVConsent는 전혀 변경되지 않아 220V를 사용하는 기존의 DomesticTV는 바로 DomesticTVConsent를 사용하여 전원을 켜는 것이 가능하다.


만약 110V를 사용하는 OverseasTV를 사용해야할 필요가 있다고 하더라도, DomesticOverseasAdapter를 사용하면 되므로 추가적인 Consent는 발생하지 않는다.


물론 DomesticOverseasAdapter라는 클래스를 만드는 것은 마찬가지다. 

다만, 복잡한 구현내용을 구현할지, 이미 구현된 내용을 사용하되 조금 수정하여 제공할지의 차이라는 것이다.


단순히 어댑터를 통해서 전압을 변경하면 된다.


그리고 DomesticTVConsent에 대한 테스트 코드 역시 작성되어 있다면 추가적으로 작성할 필요가 없다.

아무리 복잡하더라도 메서드 하나를 호출하고 적절한 출력을 조정하기만 하면 된다.


현재는 220V에서 110V로 전환하지만, 반대의 경우를 고려하여 Domestic, OverseasTV에 상관없이 동작하게하는 콘센트를 TargetConsent라고 가정하고 이 추상화된 개념을 사용한다면 좀 더 유연한 소스코드가 제공될 것이다.



5. Solution Code


 - Adaptee

// Legacy Code
public class Adaptee {
public void doLegacyCode1() {
System.out.println("Adaptee's Legacy Code1");
}

public void doLegacyCode2() {
System.out.println("Adaptee's Legacy Code2");
}
}

기존 시스템에 해당하는 Adaptee 클래스이다. 새로 구현할 시스템에서 상당부분의 기능을 제공한다.


 - Target

// Adapter에 대한 추상적인 개념
// 실질적으로 Client에서 사용하기 위해 필요한 클래스의 형태
public interface Target {
public void adaptiveOperation1();
public void adaptiveOperation2();
}

새롭게 구현할 시스템이다. 위 2가지 메서드의 제공이 보장되어야 한다.


 - Adapter

// Adaptee를 Target이라는 추상적 개념으로 사용할 수 있게 Taget의 구조에 맞게 개조하는 클래스
// Inheritance(Class Adapter) 보단 Composition(Object Adapter)을 사용하는 것이 좋다.
// 아예 Adapter 내부에서 new 연산자를 사용해 구현해도 된다.
public class Adapter implements Target {
Adaptee adaptee;

public Target setAdaptee(Adaptee adaptee) {
this.adaptee = adaptee;
return this;
}

@Override
public void adaptiveOperation1() {
adaptee.doLegacyCode1();
adapterOperation();
}

@Override
public void adaptiveOperation2() {
adaptee.doLegacyCode2();
adapterOperation();
}

private void adapterOperation() {
System.out.println("Adapter's Operation");
}
}

기존의 시스템인 Adaptee를 이용하여, 새로운 시스템인 Target을 사용할 수 있도록 연결해주는 Adapter 클래스이다.


실행 결과는 다음과 같다.



6. Adapter Pattern


기존의 시스템을 활용하여 새로운 시스템에 적용할 수 있는 패턴을 Adapter Pattern이라고 한다.


기업과 프로그래머의 입장에서, 그 클래스를 새로 제작하는데 엄청난 시간이 소요된다면, 조금 더 효율적인 방법으로 계획했던 바와 동일하게 동작하게 할 수 있는 여지가 존재하는지 생각해볼 필요가 있다.


이러한 여지가 있고, 실질적으로 가능하다면, 엄청나게 많은 공수를 해결할 수 있게 된다.


단순히 구현비용 측면뿐만 아니다. 기존 시스템에 대한 테스트코드가 존재한다면 상당 부분 테스트가 보장된 클래스를 이용함으로써 테스트에 대한 공수와 신뢰도 역시 보장된다.


이는 다음과 같은 상황에서 특히나 효율적이다.

1. 새로운 시스템을 새로 만드는 데 드는 공수가 굉장히 크면서, 기존의 시스템이 새로운 시스템의 많은 부분을 해결할 수 있을 때

2. 1번과 더불어 신뢰성 있는 테스트 코드를 원할 때

3. 기존의 시스템이 레거시로 이루어져 있어 수정하기가 불가능에 가까울 때


Adapter Pattern에는 2가지 방법이 존재한다.


1. Class Adapter Pattern ( Inheritance )

Adapter Class에서 기존의 시스템인 Adaptee를 상속받아 상위 클래스의 메서드를 사용할 수 있다.

자바에서는 다중상속이 불가능하기 때문에, 단 하나의 Adaptee만을 상속받아 새로운 시스템을 구현할 수 있다.


2. Object Adapter Pattern ( Composition )

Adapter Class에서 기존의 시스템인 Adaptee의 레퍼런스를 가지고 있어, 메서드를 사용할 수 있다.

상속은 단 하나의 Adaptee만을 구현하지만, 여러 개의 레퍼런스를 선언할 수 있어 다수의 Adaptee를 통해 새로운 시스템을 구현할 수 있다



- UML & Sequence Diagram




7. 장단점


장점 : 기존의 시스템을 활용하여 새로운 시스템을 구현할 수 있다.

이에 따라 새로운 시스템 구현의 공수를 줄일 수 있다.

기존의 소스코드를 변경하지 않으므로 기존 시스템에도 영향이 가지 않는다.

테스트 코드가 있는 경우 신뢰도가 보장되고, 재작성할 필요가 없다.



단점 :

'Design Pattern > Structural Pattern' 카테고리의 다른 글

Facade Pattern (기본)  (4) 2018.02.16
Bridge Pattern (기본)  (3) 2018.02.16
Proxy Pattern  (0) 2018.02.16
Composite Pattern  (0) 2017.09.26
Decorator Pattern  (0) 2017.09.26
Comments