Decorator 패턴은 동일한 타입의 객체를 품고 있는 패턴이다.
Decorator 패턴은 기본적인 기능을 구현한 클래스를 인자로 받아서 추가된 기능을 구현한 객체가 이용함으로써 기능의 확장이나 변경을 수행하는 패턴이다. 이 패턴의 장점은 동적으로 기능의 추가 제거가 가능하고, 기능을 구현하는 클래스들을 분리함으로써 수정이 용이해진다는 점이다. 마치 기본 제품에 포장지나 외부 디자인을 살짝 변경해 줌으로써 새로운 기능을 부여하는 것과 같다고 해서 이 명칭이 붙었다.
해결하고자 하는 문제
일단 다음과 같은 데이터가 있다고 가정하자.
int data;
개발자들에게 주어진 미션은 다음과 같다.
1. data를 멀티쓰레드 환경에서 사용할 수 있도록 구현할 것. 즉 동시성을 만족할 것.
2. data의 사용 환경이 싱글쓰레드일 경우 성능을 우선시 할 것.(즉 동시성 만족을 위한 코드를 제거할 것)
이런 문제를 처음 접한 사람들은 상당한 난감함에 빠질 것이다. 데이터의 동시성 코드를 구현함과 동시에 동시성을 구현하지 않은 코드도 구현하라니. 특히나 C언어를 중심으로 공부한 사람들은 사실 이 문제가 더더욱 어렵게 느껴질 것이다. 전역 변수 문제로 데이터를 다루는 함수를 특정짓기가 어렵기 때문이다.
하지만 일단 객체지향으로 넘어오고 나면 개념적으로 이 문제를 해결할 수 있는 실마리가 있다. 클래스의 정의로부터 클래스는 속성(데이터)과 행위의 집합임을 알 수 있고, 속성은 정보 은닉(infomation hiding)을 통해서 외부에 노출시키지 않을 수 있다. 그리고 데이터를 다루는 것은 데이터를 포함하고 있는 클래스의 메소드들로 한정 지을 수 있다.
먼저 해결의 단계를 밟기 전에 Decorator 패턴을 적용하기 용이하도록 interface를 하나 정의하도록 하겠다. 물론 int data를 보호하기 위해 만들어질 클래스를 위한 인터페이스이다. 단순히 데이터를 꺼내 가고 집어 넣는 get/set 함수들이다.
interface IData{
public void setData(int data);
public int getData();
}
이제 위의 인터페이스를 구현하면서 int data를 선언하고 그에 필요한 행위를 정의하는 클래스를 선언한다. 이 클래스를 선언하는 순간 해결의 첫 단계를 밟게 된다.
class Data implements IData{
private int data;
public void setData(int data){
this.data = data;
}
public int getData(){
return data;
}
}
위의 클래스를 선언함으로써 얻은 효과는 다음과 같다.
1. data를 private으로 선언함으로써 외부에서 임의로 접근하여 생기는 동시성 문제를 차단하였다.
2. data를 다루는 행위들을 모두 한 클래스 안에 모아 둠으로써 동시성 문제가 확산되는 것을 방지하였다.
이제 적극적으로 동시성 문제를 해결해 볼 차례다. Java 뿐 만 아니라 다른 객체지향 언어를 사용하는 사람들도 익숙한 형태로 먼저 문제를 해결해 보도록 하겠다. 현재 data를 다루는 메소드는 getData() 함수와 setData() 함수 둘 뿐이다. 그리고 여러 쓰레드에서 이들 함수를 호출할 때 동시성 문제가 발생하게 된다. 이를 방지하려면 데이터를 다루는 코드 영역을 동시에 여러 쓰레드에서 접근하지 못하도록 제한하면 된다. 즉, Lock 또는 Mutex를 이용하는 것이다.
동시 접근을 제한해야 할 영역은 이미 위의 두 메소드로 한정되어 있으니 적용도 역시 간단하다.
class Data implements IData{
private int data;
private Lock mutex = new ReentrantLock();
public void setData(int data){
mutex.lock();
this.data = data;
mutex.unlock();
}
public int getData(){
mutex.lock();
int backup = data;
mutex.unlock();
return backup;
}
}
ReentrantLock 클래스는 Java에서 사용하는 Lock의 구체 클래스이다. 다른 언어에서 사용하는 Mutex나 Lock이라고 생각하고 보면 된다. setData()와 getData()를 보면 data를 다루는 영역이 Lock으로 잠겨 있는 것을 알 수 있다. data를 사용하는 영역은 저 두 영역 뿐이고, 두 영역이 같은 Lock 객체를 통해 잠겨 있으므로 동시에 저 영역이 접근되는 것은 차단되어 있다.
자 이제 Java 사용자들이 익숙한 synchronized 키워드를 이용한 접근 제한을 구현해 보도록 하겠다. 다른 언어 개발자들은 그냥 위의 코드와 아래 코드가 동일한 기능을 한다고 이해하면 되겠다.
class Data implements IData{
private int data;
public void setData(int data){
synchronized(this){
this.data = data;
}
}
public int getData(){
synchronized(this){
return data;
}
}
}
바뀐 부분을 보면, 기존에 Lock 객체에 의해 상하로 막혀 있던 것이 synchronized 키워드를 통해 감싸져 있고, this 객체, 즉 자기 자신에 의해 동기화 되어 있음을 알 수 있다. this 객체에 의해 동기화 되어 있으므로 같은 객체 안에서 synchonized 키워드로 둘러 싸인 영역은 중복 접근이 불가능하다.
이것으로 일단 1번 요구사항을 만족시켰다. 하지만 2번 요구사항을 만족시키는 것은 간단해 보이지 않는다. 2번 요구사항을 좀 해석해 보자면, 동시성을 만족시키는 구현과 동시성을 만족시키지 않는 구현을 바꿔치기 하기 용이하도록 구현하라는 것이다.
이제 위에서 선언한 interface가 사용될 때가 되었다. 일단 동시성을 만족시키지 않는 클래스는 그대로 사용하도록 한다. 그러면 동시성을 만족하는 클래스를 만들어야 한다. 하지만 이미 구현된 동시성 코드와 그렇지 않는 코드는 중복이다. 이러면 한 클래스가 수정되었을 때 다른 클래스는 수정되지 않을 수 있다. 따라서 중복 코드는 제거 되어야 한다. 이 때 사용하는 것이 Decorator 패턴이다. 아래는 Decorator 패턴을 통해 구현한 동시성 만족 클래스이다.
class SynchronizedData implements IData{
private IData data;
public SynchronizedData(IData data){
this.data = data;
}
public void setData(int data){
synchronized(this){
this.data.setData(data);
}
}
public synchronized int getData(){
synchronized(this){
return data.getData();
}
}
}
위의 코드는 우선 IData 인터페이스를 구현하고 있다. 따라서 Data 클래스와 외부적 관점에서는 동일한 타입이 된다. 내부에는 첫째로 IData 객체에 대한 레퍼런스를 선언해 두고 있다. 그리고 생성자를 통해서 IData 타입의 객체를 입력 받도록 되어 있다. 우리가 선언한 Data 클래스가 외부적 관점에서는 IData 타입이다. 이것을 객체로 생성하여 집어 넣을 것이다. 그리고 data를 다루는 메소드들에서는 생성자를 통해 들어온 참조 객체를 그냥 이용하기만 한다. 대신 synchronized 블럭을 이용함으로써 동시성을 만족하도록 구현 되어 있다. 이렇게 하면 동시성을 만족하면서도 중복 코드가 없어서 수정에 닫혀 있는 형태의 구현이 완성된다.
그러면 이제 결과물을 종합해 보자.
클래스 다이어그램
최종 결과물
interface IData{ // 동일한 타입으로 만들어 주기 위한 인터페이스
public void setData(int data);
public int getData();
}
class Data implements IData{ // 동시성을 구현하지 않은 Data 클래스
private int data;
public void setData(int data){
this.data = data;
}
public int getData(){
return data;
}
}
class SynchronizedData implements IData{ // 동시성을 구현한 클래스(Decorator 패턴)
private IData data;
public SynchronizedData(IData data){
this.data = data;
}
public void setData(int data){
synchronized(this){
this.data.setData(data);
}
}
public synchronized int getData(){
synchronized(this){
return data.getData();
}
}
}
이 패턴을 사용하는 코드를 보자.
public static void main(String[] args) {
IData data = new Data(); // 동시성이 필요없을 때
IData data = new SynchronizedData(new Data()); // 동시성이 필요할 때
}
동시성이 필요하지 않은 경우에는 그냥 new Data()를 호출해서 바로 Data 클래스를 사용하면 된다. 동시성이 필요한 경우에는 같은 코드에서 new SynchronizedData()를 호출한 후 Data 클래스를 생성해서 넣어 주기만 하면 된다. 즉, 동시성을 만족하고 안하고는 저 코드 한 줄만 수정하면 되는 문제이다.
사실 이 구현 방식은 특별한 것이 아니다. 이미 Java 라이브러리에서는 동시성을 이런 방식으로 구현해 두었다. 다음의 코드를 보자.
사용 방법
List<String> list = new ArrayList<String>();
List<String> list = Collections.synchronizedList(new ArrayList<String>());
Java에서는 Collection을 자주 사용하게 되는데, 사용 빈도가 높은 만큼 Collection의 동시성 문제는 상당히 해결하기 어려워 질 수 있다. 하지만 두번째 줄에 보면 우리가 구현한 내용과 유사한 코드가 보인다. 저것이 Decorator 패턴을 이용해 구현한 동시성 지원 Collection 클래스 사용 방법이다. Collections.synchronizedList와 우리의 SynchronizedData가 얼마나 유사한지 알아보기 위해 라이브러리 소스를 살펴 보도록 하겠다.
Collections.synchronizedList 내부 소스 코드
static class SynchronizedList<E> implements List<E> {
final List<E> list;
SynchronizedList(List<E> list) {
super(list);
this.list = list;
}
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized (mutex) {return list.remove(index);}
}
......
}
일단 잡다한 코드들은 일부 지웠다. 눈여겨 볼 것은 데이터를 다루는 메소드들이다. 보면 mutex 객체를 통해 모두 synchronized 블럭으로 막혀 있고, 그 안에서 일반 List 객체를 다루고 있는 것을 볼 수 있다. 그리고 생성자에서는 당연히 일반 List 객체를 인자로 받도록 되어 있다. 구조적으로 위에서 구현 내용과 완전히 같음을 알 수 있다.
'5.디자인패턴' 카테고리의 다른 글
Holder 패턴 (0) | 2016.08.28 |
---|---|
Adapter 패턴 (0) | 2016.08.23 |
Pluggable Selector 패턴 (1) | 2016.08.21 |
State 패턴 (0) | 2016.08.15 |
Strategy 패턴 (0) | 2016.08.15 |