구조적 디자인 패턴
디자인패턴의 아름다움에 나오는 여러 구조적 디자인 패턴들을 쉬운 예시들과 함께 정리했습니다.
Proxy 패턴
원본 클래스를 변경하지 않는 상태로 Proxy Class 도입으로 새로운 기능을 추가하는 것
인터페이스 기반의 프록시 패턴
from abc import ABC, abstractmethod
class DatabaseAccessInterface(ABC):
@abstractmethod
def get_data(self):
pass
@abstractmethod
def set_data(self, data):
pass
class DatabaseAccess(DatabaseAccessInterface):
def get_data(self):
return "Some data from the database"
def set_data(self, data):
print(f"Data {data} set in the database")
이 때, DB 에 접근할 때마다 로그를 추가하려고 할 때 프록시 패턴 사용할 수 있습니다.
class DatabaseAccessProxy(DatabaseAccessInterface):
def __init__(self, database_access):
self.database_access = database_access
def get_data(self):
print("Logging: Data retrieval has been started.")
data = self.database_access.get_data()
print("Logging: Data retrieval has been finished.")
return data
def set_data(self, data):
print(f"Logging: Setting data {data} has been started.")
self.database_access.set_data(data)
print("Logging: Setting data has been finished.")
DatabaseAccessProxy
는 DatabaseAccess
의 기능을 그대로 사용하면서, 데이터 접근 전후에 로그를 남기는 추가 기능을 제공합니다.
상속 기반의 프록시 패턴
상속을 사용하면 기존 클래스의 구조를 변경하지 않고도 새로운 기능을 추가하거나 기존 기능을 확장할 수 있습니다.
class DatabaseAccessProxy(DatabaseAccess):
def get_data(self):
print("Logging: Data retrieval has been started.")
data = super().get_data() # 원래 클래스의 메소드 호출
print("Logging: Data retrieval has been finished.")
return data
def set_data(self, data):
print(f"Logging: Setting data {data} has been started.")
super().set_data(data) # 원래 클래스의 메소드 호출
print("Logging: Setting data has been finished.")
리플렉션 기반의 동적 프록시
프록시 클래스를 정의해야하는 원본 클래스 개수가 50개다! → 도저히 모든 클래스를 새로 정의할 수 없을 경우, 리플렉션을 사용하여 동적으로 프록시 클래스를 생성할 수 있습니다.
→ 동적 프록시 사용
프록시 패턴의 활용 방법
- 주요 비즈니스와 관련 없는 요구사항의 개발에 활용 가능
- RPC에서 프록시 패턴 적용 가능
- 서버의 RPC 를 호출하는 클라이언트는 서버의 세부 정보를 알 수 없음
- 서버는 클라이언트와의 상호 작용 신경 쓰지 않고 비즈니스 논리만 개발
- 캐시를 활용하기 위한 프록시 패턴
- 인터페이스 기반의 프록시 패턴을 제외하고 권장 하고 싶지 않습니다. 왜 일까요?
Decorator 패턴
객체에 동적으로 새로운 기능을 추가할 수 있게 해주는 구조적 디자인 패턴
예시: Decorator 패턴으로 로깅 기능을 추가
class ComponentInterface:
def operation(self):
pass
class ConcreteComponent(ComponentInterface):
def operation(self):
print("기본 기능")
class Decorator(ComponentInterface):
def __init__(self, component):
self.component = component
def operation(self):
self.component.operation()
self.added_functionality()
def added_functionality(self):
print("추가 기능")
# 사용 예제
component = ConcreteComponent()
decorated = Decorator(component)
decorated.operation()
Adapter 패턴
호환되지 않는 인터페이스를 호환 가능한 인터페이스로 변환해서 두 클래스가 함께 작동하도록 함
eg. USB 인터페이스
예시: ListView 용 Adapter 패턴들
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
private List<String> mData;
public static class ViewHolder extends RecyclerView.ViewHolder {
private final TextView textView;
public ViewHolder(View view) {
super(view);
textView = view.findViewById(R.id.textView);
}
public void setText(String text) {
textView.setText(text);
}
}
public MyAdapter(List<String> data) {
mData = data;
}
@Override
public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.my_text_view, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.setText(mData.get(position));
}
@Override
public int getItemCount() {
return mData.size();
}
}
public class MainActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private MyAdapter adapter;
private List<String> data;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = findViewById(R.id.recyclerView);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
// 데이터 생성
data = Arrays.asList("Item 1", "Item 2", "Item 3");
// 어댑터 설정
adapter = new MyAdapter(data);
recyclerView.setAdapter(adapter);
}
}
ListAdapter 는 백그라운드 스레드에서 데이터 세트의 변화를 계산해서 성능을 최적화합니다.
public class MyListAdapter extends ListAdapter<String, MyListAdapter.ViewHolder> {
public MyListAdapter() {
super(DIFF_CALLBACK);
}
@Override
public MyListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.my_text_view, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
String item = getItem(position);
holder.setText(item);
}
private static final DiffUtil.ItemCallback<String> DIFF_CALLBACK =
new DiffUtil.ItemCallback<String>() {
@Override
public boolean areItemsTheSame(String oldItem, String newItem) {
// 여기에 비교 로직 구현
}
@Override
public boolean areContentsTheSame(String oldItem, String newItem) {
// 여기에 내용 비교 로직 구현
}
};
// ViewHolder 클래스는 위 예시와 동일
}
public class MainActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private MyListAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = findViewById(R.id.recyclerView);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
// 어댑터 설정
adapter = new MyListAdapter();
recyclerView.setAdapter(adapter);
// 데이터 설정
List<String> data = Arrays.asList("Item 1", "Item 2", "Item 3");
adapter.submitList(data);
}
}
Adapter 를 사용하는 쪽에서는 Adapter 내부의 로직을 신경 쓰지 않아도 됩니다.
Adapter 패턴의 특징
- Adapter 패턴은 원본 클래스와 다른 인터페이스를 제공 (cf. Proxy / Decorator 는 같은 인터페이스)
Bridge 패턴
🌉 추상화와 구현을 디커플링해서 두가지가 서로 독립적으로 변화할 수 있도록 함
Bridge 패턴으로 폭발적인 상속 해결
class Device:
def turn_on(self):
raise NotImplementedError
def turn_off(self):
raise NotImplementedError
class TV(Device):
def turn_on(self):
print("TV를 켭니다.")
def turn_off(self):
print("TV를 끕니다.")
class Radio(Device):
def turn_on(self):
print("라디오를 켭니다.")
def turn_off(self):
print("라디오를 끕니다.")
리모콘의 추상화 클래스 정의
class RemoteControl:
def __init__(self, device):
self.device = device
def toggle_power(self):
if self.is_powered:
self.device.turn_off()
self.is_powered = False
else:
self.device.turn_on()
self.is_powered = True
사용 예시
# 기기 인스턴스 생성
tv = TV()
radio = Radio()
# 각 기기에 대한 리모콘 인스턴스 생성
tv_remote = RemoteControl(tv)
radio_remote = RemoteControl(radio)
# 리모콘을 사용하여 기기 제어
tv_remote.toggle_power() # TV를 켭니다.
radio_remote.toggle_power() # 라디오를 켭니다.
- 추상화 -
RemoteControl
클래스 - 구현 -
Device
클래스와 그 서브클래스들(TV
,Radio
)
Facade 패턴
복잡한 시스템에 대한 단순한 인터페이스를 제공해서 복잡한 시스템의 사용을 단순화하고, 클라이언트와 시스템 사이의 의존성을 줄이는 것
class Screen:
def down(self):
print("스크린을 내립니다.")
def up(self):
print("스크린을 올립니다.")
class Projector:
def on(self):
print("프로젝터를 켭니다.")
def off(self):
print("프로젝터를 끕니다.")
class AudioSystem:
def on(self):
print("오디오 시스템을 켭니다.")
def off(self):
print("오디오 시스템을 끕니다.")
class HomeTheaterFacade:
def __init__(self):
self.screen = Screen()
self.projector = Projector()
self.audio_system = AudioSystem()
def watch_movie(self):
print("영화 보기를 준비합니다.")
self.screen.down()
self.projector.on()
self.audio_system.on()
def end_movie(self):
print("영화 보기를 종료합니다.")
self.screen.up()
self.projector.off()
self.audio_system.off()
Fasade 인터페이스를 통해 홈시어터 시스템을 간단하게 제어할 수 있습니다.
# 홈 시어터 퍼사드 인스턴스 생성
home_theater = HomeTheaterFacade()
# 영화 보기 시작
home_theater.watch_movie()
# 영화 보기 종료
home_theater.end_movie()
Adapter 패턴과의 차이
- 목적의 차이: 어댑터는 두 호환되지 않는 인터페이스를 연결하는 데 중점을 두고, 퍼사드는 복잡한 시스템을 단순화하는 데 중점
- 적용 범위: 어댑터는 주로 두 클래스나 컴포넌트 간의 호환성 문제를 해결하는 데 사용되며, 퍼사드는 하나의 복잡한 시스템에 대한 단순한 인터페이스를 제공하는 데 사용
- 구현 방식: 어댑터는 기존 인터페이스를 새로운 인터페이스로 변환하는 데 중점을 두고, 퍼사드는 복잡한 시스템의 내부 작업을 감추고 단순화된 접근 방법을 제공
Composite 패턴
객체들을 트리 구조로 구성하여 개별 객체와 복합 객체를 클라이언트가 동일하게 다룰 수 있도록 해서 개별 객체와 복합 객체를 구별하지 않고 동일한 방식으로 처리하는 디자인 패턴
주로 Tree 구조의 데이터(단순한 객체의 모음)를 처리하는데 사용합니다.
예시: 파일 시스템
파일은 Leaf, 디렉토리는 Composite
class FileSystemComponent:
def __init__(self, name):
self.name = name
def display(self):
raise NotImplementedError
class File(FileSystemComponent):
def display(self):
print(f"파일: {self.name}")
class Directory(FileSystemComponent):
def __init__(self, name):
super().__init__(name)
self.children = []
def add(self, component):
self.children.append(component)
def remove(self, component):
self.children.remove(component)
def display(self):
print(f"디렉토리: {self.name}")
for child in self.children:
child.display()
클래스 사용 예시
# 디렉토리 생성
root_dir = Directory("Root")
docs_dir = Directory("Documents")
pics_dir = Directory("Pictures")
# 파일 생성
file1 = File("File1.txt")
file2 = File("File2.jpg")
file3 = File("File3.txt")
# 디렉토리에 파일 추가
docs_dir.add(file1)
pics_dir.add(file2)
root_dir.add(docs_dir)
root_dir.add(pics_dir)
root_dir.add(file3)
# 파일 시스템 구조 표시
root_dir.display()
컴포지트 패턴을 사용하면 클라이언트는 복합 객체와 개별 객체를 동일한 방식으로 다룰 수 있으며, 트리 구조의 재귀적인 구성을 쉽게 관리할 수 있습니다.
Flyweight 패턴
공유를 위해 객체를 재사용하여 메모리를 절약. 이때 공유하는 객체는 Immutable 해야 함
예제: 체스 게임
많은 수의 체스 말이 사용되지만, 각 타입의 체스 말은 외형이 동일. (eg. 비숍들 끼리)
class ChessPieceFlyweight:
def __init__(self, name, color):
self.name = name # 예: 'Pawn', 'Knight', 'Bishop' 등
self.color = color # 예: 'Black', 'White'
def display(self, position):
print(f"{self.color} {self.name} at {position}")
팩토리에서 체스말 객체의 생성과 관리를 담당
class ChessPieceFactory:
_flyweights = {}
@classmethod
def get_flyweight(cls, name, color):
key = (name, color)
if not cls._flyweights.get(key):
cls._flyweights[key] = ChessPieceFlyweight(name, color)
return cls._flyweights[key]
체스 말의 위치를 관리하는 컨텍스트 클래스 정의
class ChessPiece:
def __init__(self, name, color, position):
self.flyweight = ChessPieceFactory.get_flyweight(name, color)
self.position = position
def move(self, new_position):
self.position = new_position
def display(self):
self.flyweight.display(self.position)
사용 예시
# 체스말 생성
pawn_black_1 = ChessPiece("Pawn", "Black", "A2")
pawn_black_2 = ChessPiece("Pawn", "Black", "B2")
# 체스말 표시
pawn_black_1.display()
pawn_black_2.display()
# 체스말 이동
pawn_black_1.move("A4")
pawn_black_1.display()
ChessPieceFlyweight
- 체스말의 공통 상태(이름과 색상)ChessPiece
- 각 체스말의 개별 상태(위치)ChessPieceFactory
- 필요한 플라이웨이트 객체를 생성하고 관리하여 중복 생성을 방지
플라이웨이트 패턴을 사용하면 체스 게임에서 같은 타입의 체스말들을 대량으로 사용해도, 각 체스말의 공통 상태는 오직 한 번만 생성되어 메모리 사용을 크게 절약할 수 있습니다.
Flyweight 패턴, Singleton 패턴, Cache, Object Pool의 차이
- Singleton: 하나의 객체 생성 vs Flyweight: 여러가지 생성
- 차라리 Singleton 패턴의 변형인 다중 인스턴스 패턴과 유사함
- 단, Flyweight 패턴은 메모리 재사용이 목적
- Cache: 보통 액서스를 빠르게 하기 위한 것 vs Flyweight: 저장소로의 의미
- Object Pool: 반복 사용(사용한 후 Pool 에 돌려줌. 한번에 하나만 사용) vs Flyweight: 공동사용
댓글남기기