JAVA 언어로 배우는 디자인 패턴 입문, 헤드퍼스트 디자인 패턴을 통해 학습 중이며 두 책의 예제 위주로 정리.
생성패턴(Creational Pattern)
1. 싱글톤(Singleton)
2. 빌더(Builder)
3. 팩토리 메소드(Factory Method)
4. 추상 팩토리(Abstract Factory)
5. 프로토타입(Prototype)
구조패턴(Structural Pattern)
1. 어댑터(Adapter)
2. 브릿지(Bridge)
3. 컴포지트(Composite)
4. 데코레이터(Decorator)
5. 퍼사드(Facade)
6. 플라이웨이트(flyweight)
7. 프록시(Proxy)
행동(행위) 패턴(Behavioral Pattern)
1. 책임 연쇄(Chain of Responsibility)
2. 커맨드(Command)
3. 인터프리터(Interpreter)
5. 중재자(Mediator)
6. 메멘토(Memento)
7. 옵저버(Observer)
8. 상태(State)
11. 방문자(Visitor)
어댑터 패턴(Adapter Pattern)은 구조패턴(Structural Pattern)에 속하는 패턴이다.
현실에서 어댑터라는 것은 보통 전류 변환을 해주는 용도로 사용이 된다.
제공되는 것과 필요한 것 사이에서 그 사이를 채워주는 역할을 하는 것이 어댑터이다.
책에서는 이렇게 예시를 들고 있다.
노트북에 필요한 전류는 12볼트이지만 콘센트에서는 100볼트로 들어온다면, 그것을 변환해줘야 하는 것이 필요하다.
100볼트로 들어온 전류를 12볼트로 변환해주는 역할을 어댑터가 담당한다.
프로그램에서도 이미 제공된 코드를 그대로 사용할 수 없을 때, 필요한 형태로 변환 후 이용하는 경우가 발생했을 때 어댑터 패턴을 사용한다.
어댑터 패턴은 Wrapper 패턴이라고 불리기도 한다.
감싼다는 의미인데 무엇인가를 포장해서 다른 용도로 사용할 수 있도록 변환해주는것이 Wrapper이자 어댑터이다.
어댑터 패턴에는 두가지 종류가 있다.
1. 클래스에 의한 어댑터(상속을 사용한 패턴)
2. 인스턴스에 의한 어댑터(위임을 사용한 패턴)
예제
예제 프로그램은 주어진 문자열을 ( ) 안에 넣어 출력하거나 * * 안에 넣어 출력하도록 하는 프로그램이다.
1. 클래스에 의한 어댑터
메인 클래스를 제외하고 2개의 클래스와 1개의 인터페이스로 작성한다.
//예제 출처 - JAVA 언어로 배우는 디자인 패턴 입문
public class Banner {
private String string;
public Banner(String string) {
this.string = string;
}
public void showWithParen() {
System.out.println("(" + string + ")");
}
public void showWithAster() {
System.out.println("*" + string + "*");
}
}
public interface Print {
public abstract void printWeak();
public abstract void printStrong();
}
public class PrintBanner extends Banner implements Print {
public PrintBanner(String string) {
super(string);
}
@Override
public void printWeak() {
showWithParen();
}
@Override
public void printStrong() {
showWithAster();
}
}
public class AdapterMain {
public static void main(String[] args) {
Print p = new PrintBanner("Hello");
p.printWeak();
p.printStrong();
}
}
처리 과정을 먼저 보면 PrintBanner 생성자의 super를 통해 Banner 의 생성자에 접근하게 된다.
그리고 생성된 인스턴스는 Print를 구현하고 있는 구현체이기 때문에 Print 타입의 p 변수에 담을 수 있게 된다.
그리고 여기서 printWeak과 printStrong을 호출하면 PrintBanner 클래스에서는 Banner를 상속받고 있기 때문에
Banner의 showWithParen()과 showWithAster()를 바로 호출해 사용할 수 있게 된다.
그래서 (Hello)와 *Hello*가 출력되게 된다.
여기서 Banner 클래스는 문자열을 괄호로 묶는 메소드와 * * 로 묶는 메소드가 준비되어 있다.
이 Banner 클래스를 위 전류 예시 중 콘센트에서 제공되는 100볼트라고 가정한다.
Print 인터페이스에서는 printWeak과 printStrong이 선언되어 있다.
이 인터페이스를 12볼트가 필요한 노트북이라고 가정한다.
마지막으로 PrintBanner 클래스는 Banner 클래스를 상속받으며 Print 인터페이스를 구현하고 있다.
PrintBanner에서는 부모클래스인 Banner의 showWithParen()으로 printWeak을 구현하고 있고, showWithAster()로 printStrong을 구현하고 있다.
이 PrintBanner 클래스가 어댑터의 역할을 한다.
정리해보자면 아래와 같다.
- 제공되는 것(100볼트) - Banner(showWithParen, showWithAster)
- 전류 변환장치(어댑터) - PrintBanner
- 필요한 것(12볼트) - Print 인터페이스(printWeak, printStrong)
그럼 메인메소드 코드를 이렇게 볼 수 있다.
PrintBanner 인스턴스를 Print 타입으로 생성해준다.
Print에서 사용하고자 한 printWeak, printStrong을 호출하게 되면
어댑터인 PrintBanner는 제공되는 Banner의 showWithParen, showWithAster를 Print()가 사용할 수 있게 해준다.
그럼 메인 메소드에서는 Print 인터페이스를 통해 요청을 처리하고 있게 되는 것이기 때문에 Print 인터페이스에만 관심이 있을 뿐, Banner클래스와 PrintBanner 클래스가 어떻게 구현되어있는지는 관심이 없게 된다.
즉, Banner와 PrintBanner에서 어떻게 처리하는지는 알 수 없고, Print 인터페이스에 printWeak(), printStrong()이 선언되어 있다는것만 알고 있는 것이다.
마치 노트북에서 12볼트로 동작을 하지만 어댑터에 들어오는 전류가 100볼트이던 200볼트이던 상관이 없다는 것이다.
이렇게 구현하게 되면 수정이 필요하게 되더라도 메인메소드의 수정이 발생하지 않고 Banner 클래스나 PrintBanner 클래스에서의 수정만 발생하게 된다.
2. 인스턴스에 의한 어댑터
클래스에 의한 어댑터와 다른 점으로는 상속이 아닌 위임을 사용한다는 점이다.
자바에서 위임은 어떤 메소드의 실제 처리를 다른 인스턴스의 메소드에 맡기는 것을 말한다.
예제는 Banner 클래스는 수정이 없고, Print 인터페이스를 추상클래스로, 그리고 PrintBanner 클래스에서 Banner 클래스를 상속받지 않도록 수정한다.
public abstract class Print2 {
public abstract void printWeak();
public abstract void printStrong();
}
public class PrintBanner2 extends Print2 {
private Banner banner;
public PrintBanner2(String string) {
this.banner = new Banner(string);
}
@Override
public void printWeak() {
banner.showWithParen();
}
@Override
public void printStrong() {
banner.showWithAster();
}
}
public class AdapterMain {
public static void main(String[] args) {
Print2 p2 = new PrintBanner2("Hi");
p2.printWeak();
p2.printStrong();
}
}
1번과 다른점을 보자면 Print 인터페이스가 추상클래스인 Print2로 바뀌었다는 점.
PrintBanner2에서는 Banner를 필드로 갖고 생성자에서는 상위 클래스가 아니기 때문에 new 키워드를 통한 인스턴스를 생성해 필드에 담아주게 된다.
그리고 구현메소드 역시 상속에서 벗어났기 때문에 그냥 사용할 수 없어 banner 필드를 통해 호출한다.
자바에서는 다중상속이 불가능하기 때문에 Print2 클래스를 상속받은 구현체라면 Banner 클래스를 또 상속받을 수 없다.
그래서 Banner 필드를 갖게 되는 것이고 이 필드를 통한 호출로 인해 위임이 발생하게 된다.
즉, PrintBanner2에서 printWeak() 메소드가 호출되었을 때 자신이 처리하지 않고, 다른 인스턴스인 banner의 showWithParen()으로 위임하게 되는 것이다.
처음에는 이게 너무 이해가 어려웠다.
PrintBanner 클래스가 어댑터라는데 이게 왜 어댑터인지, 중간에서 뭘 변환해준다는 것인지 이해하기가 어려웠다.
그래서 헤드퍼스트 디자인 패턴에서의 예제를 봤는데 이게 좀 더 이해가 쉬웠다.
예제 프로그램은 아래와 같다.
Duck, Terkey 라는 객체가 존재하고,
Duck은 꽥! 이라는 소리를 내고 날아간다.
Terkey는 골골 이라는 소리를 내고 5번을 짧게 날아간다.
위 예제에서와는 다르게 다른 처리과정이 존재하는 예제라고 볼 수 있다.
구조는 인터페이스로 Duck, Terkey를 갖게 되고, 클래스로 MallardDuck, WildTurkey, TurkeyAdapter가 존재한다.
그리고 메인 클래스로 DuckMain 클래스가 있다.
//예제 출처 - 헤드퍼스트 디자인 패턴
public interface Duck {
public void quack();
public void fly();
}
public class MallardDuck implements Duck {
@Override
public void quack() {
System.out.println("꽥!");
}
@Override
public void fly() {
System.out.println("날고 있어요!");
}
}
public interface Turkey {
public void gobble();
public void fly();
}
public class WildTurkey implements Turkey {
@Override
public void gobble() {
System.out.println("골골");
}
@Overrdie
public void fly() {
System.out.println("짧게 나는중!");
}
}
public class TurkeyAdapter implements Duck {
private Turkey turkey;
public TurkeyAdapter(Turkey turkey) {
this.turkey = turkey;
}
@Override
public void quack() {
turkey.gobble();
}
@Override
public void fly() {
for(int i = 0; i < 5; i++)
turkey.fly();
}
}
public class DuckMain {
public static void main(String[] args) {
Turkey turkey = new WildTurkey();
System.out.println("칠면조가");
turkey.gobble();
turkey.fly();
Duck duck = new MallardDuck();
System.out.println();
System.out.println("오리가");
testDuci(duck);
Duck turkeyAdapter = new TurkeyAdapter(turkey);
System.out.println();
System.out.println("칠면조 어댑터가");
testDuck(turkeyAdapter);
}
static void testDuck(Duck duck) {
duck.quack();
duck.fly();
}
}
실행해보면
칠면조가 한번 울고 한번 날고, 오리가 한번 울고 한번 날고, 칠면조 어댑터가 한번 울고 5번을 나는것을 볼 수 있다.
Duck은 quack()과 fly()메소드를, Turkey는 gobble(), fly() 메소드를 갖고 있고
각각 MallardDuck과 WildTurkey라는 구현체를 갖고 있다.
각 구현체에서는 울음소리와 날아가는 것을 한번씩 출력하도록 되어있다.
그리고 칠면조 어댑터가 존재한다.
이 어댑터의 역할은 칠면조를 Duck과 같이 사용하도록 하는 것이다.
메인 클래스의 testDuck 메소드에서는 Duck 타입의 매개변수를 받아 quack()과 fly()를 호출하고 있다.
이 testDuck 메소드는 들어오는 매개변수가 오리인지 칠면조인지 전혀 구분하지 못하지만 다른 결과를 출력하게 된다.
그럼 이 결과로 gobble()과 fly()를 갖고 있는 Turkey 인터페이스가 어댑터를 통해 Duck과 같은 quack(), fly()로 변환되어 동작하고 있다고 볼 수 있고, testDuck 메소드는 타깃 인터페이스인 Duck를 통해 어댑터에게 요청을 보낼 뿐 어댑터에 연결된 반대쪽 객체가 무엇인지에 대해서는 알 수 없다.
정리.
어댑터 패턴은 새로운 객체를 추가하여 연결해주는 목적이라기 보다 호환이 되지 않는 서로 다른 인터페이스(오리와 칠면조)를 어댑터를 통해 연결하여 사용할 수 있도록 하는 패턴이다.
'JAVA > Design Pattern' 카테고리의 다른 글
디자인패턴(Java) - 템플릿 메소드(Template Method) (0) | 2023.12.27 |
---|---|
디자인패턴(Java) - 전략패턴(Strategy Pattern) (1) | 2023.12.19 |
디자인패턴(Java) - 반복자(Iterator) (1) | 2023.12.15 |
디자인패턴(Design Pattern)이란 (0) | 2023.12.15 |