오늘보다 발전된 내일의 나를 위해…












Design Pattern-Wrapper Pattern


해당 내용은 POCU 아카데미 COMP_2500에서 배운 내용을 공부하기 위해 작성된 글입니다


:pushpin: Wrapper Pattern


  • 주로 업계에서는 래퍼(wrapper) 패턴이라 함
  • GoF 책에서는 어댑터(adapter) 패턴이란 이름을 사용
  • 어떤 클래스의 메서드 시그내처가 맘에 안 들 때 다른 걸로 바꾸는 방법
  • 단, 그 클래스의 메서드 시그내처를 직접 변경하지 않음
    • 그 클래스의 소스코드가 없을 수도 있음
    • 그 클래스에 의존하는 다른 코드가 있을 수도 있음
  • 그 대신 새로운 클래스를 만들어 기존 클래스를 감쌈



Wrapper란 포장지를 의미한다. A 클래스의 어떤 getA()라는 메서드 시그니처가 있는데 마음에 안들어서 바꾼다고 해보자. 그러면 클래스 B를 만들고 클래스 B안에 클래스 A를 포함하는 것이다. 정확히 말하면 클래스 A로부터 만든 개체를 포함하는 것이다. 그리고 앞으로 클래스 B에 있는 getB() 메서드를 호출할 건데 이게 알아서 A의 getA()를 호출해주는 것이다. 즉 내가 호출할 때는 B만 사용하고 내부적으로는 getA()를 어떻게서든 사용하는 것이다. 그래서 기존의 클래스를 감싼다고 해서 Wrapper Pattern 이라고 부른다.



위의 사진과 같이 어댑터와 같이 이해할 수도 있다. 원래 A의 getA()라는 메서드가 있었으면 A를 그대로 사용하지 않고 앞에 B라는 어댑터를 꽂는 거이다. 그럼 A에 있는 뭔가에 어댑터를 꽂았으니까 바로 접근할 수 있는 방법이 없다. 그래서 B에서 어떻게든 접속을 해서 사용을 하라는 것이다.


:pushpin: 메서드 시그니처를 바꾸려는 다양한 이유


  • 추후 외부 라이브러리를 바꿀 때 클라이언트 코드를 변경하지 않기 위해
  • 그냥 사용 중인 메서드가 코딩 표준에 맞지 않아서
  • 기존 클래스에 없는 기능을 추가하기 위해
  • 확장된 용도: 내부 개체를 클라이언트에게 노출시키지 않기 위해
    • DTO(data transfer object) 만들기


코드로 예를 보자. 윈도우에서 사용 가능한 3D 그래픽 API는 대표적으로 두 개가 있다.

  • Microsoft DirectX
  • OpenGL


두 API 모두 컴퓨터에 설치된 그래픽 카드를 사용한다. 따라서 두 API에서 지원하는 기능이 매우 비슷하다. 아래와 같은 두 클래스가 있다고 하자.



clear()와 clearScreen() 메서드는 둘 다 화면을 어떤 색상으로 지우는 메서드이다. 다른 점은 메서드 이름과 r, g, b, a 매개변수의 형과 유효한 범위이다. float[0.0. 1.0]이고 int[0, 255]이다.


원래 프로그램에서 OpenGL을 사용했다면(즉, 내가 클라이언트) 내 소스파일 곳곳에 이런 코드가 있을 것이다.


this.graphics.clearScreen(1.f, 0.f, 0.f, 0.f);


그리고 추후 DirectX로 바꾸기로 결정하면 위의 코드를 찾아 고쳐야 한다.


this.graphics.clear(0,0,0,255);


즉 수정할 코드가 많을수록 실수할 가능성이 높아진다.

이럴 때 래퍼 클래스를 만들어 사용하면 한 곳에서만 바꾸면 된다.


Graphics Wrapper Class

public final class Graphics{
  private OpenGL gl;

  ...

  public void clear(float r, float g, float b, float a){
    this.gl.clearScreen(a, r, g, b);
  }
}


내부에 OpenGL 개체를 들고 있고 Graphics 메서드는 OpenGL의 메서드를 호출한다.


Main Class

Graphics graphics;
this.graphics.clear(0.f, 0.f, 0.f, 1.f);


Graphics 개체만 만들고 속에 OpenGL 개체가 들어있다. Graphics 개체의 메서드만 호출한다. 이제 여기서 OpenGL 대신 DirectX를 사용하려면 어떻게 해야 할까? Graphics 클래스 안에서 OpenGL 개체를 DirectX 개체로 변경하면 된다.

Graphics Class

public final class Graphics{
  private OpenGL gl;

  ...

  public void clear(float r, float g, float b, float a){
    this.gl.clearScreen(a, r, g, b);
  }
}


위 코드를 아래와 같이 바꾼다.


Graphics Class

public final class Graphics{
  private DirectX dx;

  ...

  public void clear(float r, float g, float b, float a){
    this.dx.clear((int)(r*255),(int)(g*255),(int)(b*255), (int)(a*255));
  }
}


즉, Graphics 메서드들이 DirectX 메서드를 호출하게 변경한다.


그리고 추가적으로 살펴볼 것이 DTO이다. 엄밀히 말하면 어댑터 패턴은 아니지만 궁극적인 목표가 같은 DTO 개념에 대해서 살펴본다. 어댑터 패턴은 타 클래스의 메서드 시그내처를 내 필요에 맞게 바꾸는 것이다. DTO는 타 클래스의 데이터를 내 필요에 맞게 바꾸는 것이다.


이제 DTO 변환하기의 간단한 예를 살펴보자. 시스템 규모가 크면 종종 이런 문제들을 겪는다.



DB에 저장된 데이터를 읽어와서 웹페이지에 보여준다고 가정했을 때 PersonEntitiy의 모든 정보를 반환하면 필요 이상의 데이터를 반환하게 되는 것이다. 따라서 정말 클라이언트가 필요로 하는 정보만 반환하는 게 더 좋다 이때 데이터 전송에만 사용하는 개체를 데이터 전공 개체(DTO)라 한다.



PersonEntity Class

public final class PersonEntity{
  public UUID id;
  public String fullName;
  public String email;
  public String passwordHash;
  public String phoneNumber;
  public int balance;
  public Date createDateTime;
  public Date modifiedDateTime;

  public PeronDto toDto(){
    return new PersonDto(this.fullName, this.email, this.createdDateTime);
  }
}


위와 같이 구성하게 되면 웹에서 필요한 데이터만 DTO로 변환해 전달해줄 수 있다. 즉 필요없는 정보를 안 보내게 됨으로 메모리를 아끼고 보안성을 갖출 수 있는 것이다.




:pushpin: 적응자(Adapter) 패턴


적응자 패턴은 다른 이름으로 래퍼(Wrapper)라고 불리우는 패턴이다.

클래스의 인터페이스를 사용자가 원하는 형태로 변환(적응) 시킨다.

이렇게 변환(적응)을 통해서 일치하지 않는 인터페이스를 갖는 클래스들이 함께 동작할 수 있도록 한다.


시나리오


그림 편집기가 있다.

그림판의 주요한 추상적 개념은 그래픽 객체들이다.

이런 공통 그래픽 요소에 대한 인터페이스는 추상 클래스인 Shape에 정의되어 있다.

그리고 각 그래픽 요소인 다각형은 각각 LineShape, PolygonShape과 같은 클래스로 개발해야 한다.


위와 같이 간단한 도형도 있겠지만 TextShape는 텍스트 처리시 버퍼 관리와 같이 다른 그래픽 요소에 비해 특별하게 고려해야할 점이 있을 수 있다.


이때 재사용할 수 있는 라이브러리나 자원이 없을까 조사를 하게 된다.

다행히 사용자 인터페이스 툴킷에서 복잡한 TextView를 처리하는 클래스를 제공하고 있다고 가정하자.

당연히 재사용하는 것이 바람직하긴 하지만 TextViewShape를 고려해서 설계한 것이 아니라서 곧바로 TextShape 클래스로 대체하여 사용할 수 없다.


적응자 패턴의 구조


적응자 패턴에는 2 가지 구현 방식이 있다.

  • TextShape(Adapter)Shape의 인터페이스TextView의 구현을 모두 상속
  • TextShape(Adapter)TextView의 인스턴스를 포함하고, TextView의 인터페이스를 사용


아래 다이어그램은 위의 방법 중 인스턴스를 포함한 방식으로 적응자 패턴을 구현한 다이어그램이다.



위에서 TextShapeTextView 클래스에 정의된 인터페이스를 바꾸어 Shape 클래스에 정의된 인터페이스와 잘 부합되게 한다.

이로써 TextView 클래스를 TextShape 적응자를 통해 재사용할 수 있게 되었다.


적응자 패턴참여자는 아래와 같다.


Client(DrawingEditor): Target 인터페이스를 만족하는 객체와 동작할 대상

Target(Shape): 사용자(Client)가 사용하는데 필요한 인터페이스를 정의한 클래스가

Adaptee(TextView): 인터페이스에 적응이 필요한 기존 인터페이스, 적응 대상자

Adapter(TextShape): Target 인터페이스에 Adaptee의 인터페이스를 적응 시키는 클래스


Java Example


DrawingEditor.java(Client)

public class DrawingEditor {

	public void useShape(Shape shape) {

		shape.boundingBox();
		shape.createManipulator();
	}
}


Shape Interface(Target)

public interface Shape {

	void boundingBox();

	void createManipulator();
}


Line.java(평범한 클래스)

public class Line implements Shape {

	@Override
	public void boundingBox() {
		System.out.println("Line bounding box");
	}

	@Override
	public void createManipulator() {
		System.out.println("Line create manipulator");
	}
}


TextView Interface(Adaptee)

public interface TextView {

	void getExtent();
}


TextShape.java(Adapter)

public class TextShape implements Shape{

	private TextView text;

	public TextShape(TextView text) {
		super();
		this.text = text;
	}

	@Override
	public void boundingBox() {

		text.getExtent();
		System.out.println("TextShape bounding box.");
	}

	@Override
	public void createManipulator() {
		System.out.println("TextShape create manipulator.");
	}
}


TextShape 클래스는 AdapteeTextView를 생성자의 인자를 통해 받아서 속성에 설정하였다.

boundingBox() 메서드에서 AdapteeTextView의 메서드를 이용하여 기능을 구현하고 있다.

이 패턴의 참여자들을 이용하여 작성한 Main 코드는 아래와 같다.


Main

public static void main(String[] args) {

		DrawingEditor drawingEitor = new DrawingEditor();

		Shape lineShape = new Line();

		TextView text = () -> {
			System.out.println("getExtent called in Text View");
		};
		Shape textShape = new TextShape(text);

		System.out.println("====DrawingEditor use line shape.====");
		drawingEitor.useShape(lineShape);
		System.out.println();

		System.out.println("====DrawingEditor use text shape.====");
		drawingEitor.useShape(textShape);
	}


lineShape는 특별한 것이 없다.

우리가 작성한 코드에서 adapteeTextView인터페이스만 정의하였고, 구현체는 없었다.

TextView는 추상 method가 1개인 FunctionalInterface 이기 때문에 익명함수를 이용하여 구현체 text를 생성하였다.

그리고 adapterTextShape에서 TextView 를 생성자 인자로 받으므로, text를 인자로 넘겨주었다.


====DrawingEditor use line shape.====
Line bounding box
Line create manipulator

====DrawingEditor use text shape.====
getExtent called in Text View
TextShape bounding box.
TextShape create manipulator.

YoungKyonYou

Integration of Knowledge