📂 목차
📚 본문
GoF 디자인 패턴
정처기에서 정리를 다 했지만, 다시 본다. 글 보다는 이번에는 코드 위주로 설명한다.
생성 패턴
객체를 생성할 때 마주할 수 있는 프로그래밍 문제를 해결할 솔루션이며
객체 생성 과정을 캡슐화해서 유연하고 재사용 가능한 객체 생성을 돕는 패턴이다.
Singleton
public class Example {
private static Example instance;
private Example() { /* 생성은 감추고 */ }
// synchronized 를 쓰는건 때에 따라서
public synchronized static Example getInstance() {
instance = Objects.requiredNonNullElseGet(instance, () ->
new Example()
);
}
}
Factory
interface Phone {
void call();
}
class IPhone implements Phone {
void call() {
System.out.println("아이폰 짱");
}
}
class Galaxy implements Phone {
void call() {
System.out.println("갤럭시 짱");
}
}
public interface PhoneFactory {
// 어떤 제품이 나올지 모르니 공통적인 제품에 대해
// 추상화를 반환하도록
Phone createPhone();
}
// 서브 클래스가 이를 구현하여 다양한 Factory 구현
public GalaxyFactory implements PhoneFactory {
public Phone createPhone() {
return new Galaxy();
}
}
public IPhoneFactory implements IPhoneFactory{
public Phone createPhone() {
return new IPhone();
}
}
Builder
public class Person {
// 전부 private final 하게
private final long id;
private final String name;
private final int height;
private final int weight;
private final String phoneNumber;
// 생성자를 encapsulation
private Person(long id, String name, int height, int weight, String phoneNumber) {
this.id = id;
this.name = name;
this.height = height;
this.weight = weight;
this.phoneNumber = phoneNumber;
}
// Getter 만 구현
public static class Builder {
private long id;
private String name;
private int height;
private int weight;
private String phoneNumber;
// 기본 생성자로
// Lombok 에서는 set 없이 id() 로 가져가기는 한다.
public Builder id(long id) {
this.id = id;
}
// ... 이하 setter 생략
public Person build() {
return new Person (id, name, height, weight, phoneNumber);
}
}
}
Prototype
public class Rectangle implements Cloneable {
private int height;
private int weight;
public Rectangle(int height, int weight) {
this.height = height;
this.weight = weight;
}
// 생성자로 복제하기
public Rectangle(Rectangle rectangle) {
this.height = rectangle.height;
this.weight = rectangle.weight;
}
// 메서드로 복제하기 (Clonable 구현)
@Override
public Rectangle clone() {
// 깊은 복사 로직도 필요 시 추가
// 상위 구현체에 cloneable 하다면 해당 메서드를 써도 됨
// 단, 캐스팅을 할지 말지는 선택
return new Rectangle(this);
}
}
Abstract Factory
다수의 부품들을 유연하게 제작하고 싶을때 가져가는 패턴이다.
// 1️⃣ Product 인터페이스
interface Chair {
void sitOn();
}
interface Sofa {
void lieOn();
}
// 2️⃣ ConcreteProduct 클래스
class ModernChair implements Chair {
@Override
public void sitOn() {
System.out.println("앉는 중: 모던 체어");
}
}
class ModernSofa implements Sofa {
@Override
public void lieOn() {
System.out.println("누워보는 중: 모던 소파");
}
}
class VictorianChair implements Chair {
@Override
public void sitOn() {
System.out.println("앉는 중: 빅토리안 체어");
}
}
class VictorianSofa implements Sofa {
@Override
public void lieOn() {
System.out.println("누워보는 중: 빅토리안 소파");
}
}
// 3️⃣ AbstractFactory
interface FurnitureFactory {
Chair createChair();
Sofa createSofa();
}
위처럼 쓰면 FurnitureFactory
의 하위로 다수의 다양한 가구들을 만들어내는
팩토리들을 생성시킬 수 있을 것이다.
구조 패턴
클래스와 객체를 조합하여 더 큰 구조를 만들 때 사용하는 패턴이다.
Adapter
호환되지 않는 인터페이스를 맞춰주고 싶을 때 사용하는 패턴이다.
// 기존 클래스 1
class KakaoRemote {
public void turnOn() {
System.out.println("카카오 TV ON");
}
public void turnOff() {
System.out.println("카카오 TV OFF");
}
}
// 기존 클래스 2
class NaverRemote {
public void on() {
System.out.println("네이버 TV ON");
}
public void off() {
System.out.println("네이버 TV OFF");
}
}
위 두 인터페이스는 기능은 동일하되 인터페이스는 맞지 않으므로 인터페이스를 하나 더 추가하여 위 API를 통일시켜준다.
interface Remote {
void on();
void off();
}
// Adapter: KakaoRemote를 Remote 인터페이스로 변환
// has-a 로 느슨하게 연결
class KakaoRemoteAdapter implements Remote {
private final KakaoRemote kakaoRemote;
public KakaoRemoteAdapter(KakaoRemote kakaoRemote) {
this.kakaoRemote = kakaoRemote;
}
@Override
public void on() {
kakaoRemote.turnOn();
}
@Override
public void off() {
kakaoRemote.turnOff();
}
}
// Adapter: NaverRemote를 Remote 인터페이스로 변환
// has-a 로 느슨하게 연결
class NaverRemoteAdapter implements Remote {
private final NaverRemote naverRemote;
public NaverRemoteAdapter(NaverRemote naverRemote) {
this.naverRemote = naverRemote;
}
@Override
public void on() {
naverRemote.on();
}
@Override
public void off() {
naverRemote.off();
}
}
// Client
public class Main {
public static void main(String[] args) {
Remote kakao = new KakaoRemoteAdapter(new KakaoRemote());
Remote naver = new NaverRemoteAdapter(new NaverRemote());
kakao.on(); // "카카오 TV ON"
kakao.off(); // "카카오 TV OFF"
naver.on(); // "네이버 TV ON"
naver.off(); // "네이버 TV OFF"
}
}
Bridge
실체과 실행을 분리시켜 독립적으로 확장시키도록 하는 패턴이다.
여기서 실행은 어떤 것이든 될 수 있다. 어쨋든 분리시키는게 중요한 것이다.
interface Renderer {
void renderCircle(float x, float y, float radius);
}
class VectorRenderer implements Renderer {
@Override
public void renderCircle(float x, float y, float radius) {
System.out.println("Vector 방식으로 원 그리기 at (" + x + "," + y + ") radius " + radius);
}
}
class RasterRenderer implements Renderer {
@Override
public void renderCircle(float x, float y, float radius) {
System.out.println("Raster 방식으로 원 그리기 at (" + x + "," + y + ") radius " + radius);
}
}
Renderer
는 실행하는 인터페이스 이다. 즉 특정 행위를 하는 인터페이스를 정의하고,
특정 행위를 하는 구현체를 VectorRenderer
, RasterRenderer
로 가져가고 있다.
여기서 이 행위의 중심에 있는, 행위를 하는 주체가 없다. 즉, 실체가 없다는 뜻이다.
해당 실체를 Renderer 을 has-a 로 가지게 하여서 구현하면 되겠다.
abstract class Shape {
protected Renderer renderer;
protected Shape(Renderer renderer) {
this.renderer = renderer;
}
abstract void draw();
abstract void resize(float factor);
}
class Circle extends Shape {
private float x, y, radius;
public Circle(Renderer renderer, float x, float y, float radius) {
super(renderer);
this.x = x;
this.y = y;
this.radius = radius;
}
@Override
void draw() {
renderer.renderCircle(x, y, radius);
}
@Override
void resize(float factor) {
radius *= factor;
}
}
public class Main {
public static void main(String[] args) {
Renderer vector = new VectorRenderer();
Renderer raster = new RasterRenderer();
Circle circle1 = new Circle(vector, 5, 5, 10);
Circle circle2 = new Circle(raster, 10, 10, 20);
circle1.draw(); // Vector 방식
circle2.draw(); // Raster 방식
circle1.resize(2);
circle1.draw(); // Vector 방식, radius 20
}
}
Composite
말 그대로 그냥 객체를 조립하여서 부분-전체 관계로 표현하는 것이다.
Application에 DI 를 할 때 해당 instance 들을 다 들고 있는 클래스를 생각해보자.
이는 쉬우니 그냥 넘어간다.
Decorator
이는 Java IO 로 많이 보았을 것이다.
객체에 새로운 책임을 추가할 수 있는 패턴이며, 상속 대신 구성(has-a 관계) 를 사용하여 기능을 확장한다.
- Interface Component
- Concrete Component
- Decorator Class
- Decorator Concrete Class
Interface Component
// Component
interface Coffee {
String getDescription();
int cost();
}
기본 뼈대를 정의하고, Concrete Component 실체를 구현한다.
Concrete Component
class BasicCoffee implements Coffee {
@Override
public String getDescription() {
return "기본 커피";
}
@Override
public int cost() {
return 2000;
}
}
여기까지는 일반적인 클래스 설계 및 구현과 다를 바가 없다. 하지만 그 이후 Decorator(추상 클래스) 를 통해
- Component 를 상속하여 내부에 Concrete Component 를 두고,
- 해당 기능들을 전부 유지시키며 Overriding 을 한다.
Decorator
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
protected CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public String getDescription() {
return coffee.getDescription();
}
@Override
public int cost() {
return coffee.cost();
}
}
여기서 이 추상 클래스의 하위 클래스도 base object(Concrete component) 를 사용할 수 있도록 protected 로 구성해야 한다.
그 이후는 추가하고 싶은 기능들을 Decorator 를 통해 다양한 기능을 가진 클래스들로 확장해나가면 된다.
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", 우유 추가";
}
@Override
public int cost() {
return coffee.cost() + 500;
}
}
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", 설탕 추가";
}
@Override
public int cost() {
return coffee.cost() + 200;
}
}
Facade
복잡한 서브시스템을 단순화된 인터페이스로 제공하기 위해(여러 라이브러리를 하나의 API로 통합하기 위해) 사용된다.
Adapter 랑 다른 점은 Adapter 는 유사한 기능을 가진 클래스 끼리 메서드 시그니처가 다를 때 사용하고,
Facade 는 다수의 다양한 기능들과 복합적이고 복잡한 인터페이스들을 제공하는 시스템에 대해 여러 객체를 단일 접근점을 통해
단순하게 접근할 수 있도록 사용하는 것이다. 즉 Facade 는 종합 리모컨 느낌이면 Adapter 는 연결만 시켜주는 애다.
class TV {
public void on() { System.out.println("TV 켜기"); }
public void off() { System.out.println("TV 끄기"); }
}
class SoundSystem {
public void on() { System.out.println("사운드 시스템 켜기"); }
public void off() { System.out.println("사운드 시스템 끄기"); }
}
class DVDPlayer {
public void on() { System.out.println("DVD 플레이어 켜기"); }
public void off() { System.out.println("DVD 플레이어 끄기"); }
public void play(String movie) { System.out.println(movie + " 재생"); }
}
다수의 기기들이 다양한 기능들을 제공함을 볼 수 있지만 각각 메서드 시그니처가 다를 수도, 같을 수도 있다.
Facade
class HomeTheaterFacade {
private final TV tv;
private final SoundSystem sound;
private final DVDPlayer dvd;
public HomeTheaterFacade(TV tv, SoundSystem sound, DVDPlayer dvd) {
this.tv = tv;
this.sound = sound;
this.dvd = dvd;
}
public void watchMovie(String movie) {
tv.on();
sound.on();
dvd.on();
dvd.play(movie);
System.out.println("영화 시작 준비 완료!");
}
public void endMovie() {
dvd.off();
sound.off();
tv.off();
System.out.println("영화 종료!");
}
}
Facade 를 두어 모든 기능들을 조합하여 다양한 기능들을 생성해낼 수 있다.
Proxy
실체 객체에 접근하기 전에 대리 객체를 두어 제어한다. 목표하는 기능으로는 다음과 같다:
- 접근 제어
- 지연 로딩
- 로깅, 캐싱
구조는 Subject(inteface), RealSubject, Proxy(RealSubject 를 감싸고 접근 제어) 로 가져간다.
Lazy Proxy
필요할 때만 초기화를 수행하도록 하여 메모리를 최적화 할 수 있다.
// Subject
interface Image {
void display();
}
// RealSubject
class RealImage implements Image {
private String filename;
public RealImage(String filename) {
this.filename = filename;
loadFromDisk();
}
private void loadFromDisk() {
System.out.println("이미지 로딩: " + filename);
}
@Override
public void display() {
System.out.println("이미지 표시: " + filename);
}
}
// 아직 생성되지 않은 상태
// Proxy 를 통해 has-a 관계로 RealImage 를 부르기 전까지는 초기화 X
class ProxyImage implements Image {
private RealImage realImage;
private String filename;
public ProxyImage(String filename) {
this.filename = filename;
}
@Override
public void display() {
if (realImage == null) { // Lazy initialization
realImage = new RealImage(filename);
}
System.out.println("프록시를 통해 접근 중...");
realImage.display();
}
}
Protection Proxy
사용자의 권한에 따라 접근을 제어한다.
class ProtectedImageProxy implements Image {
private RealImage realImage;
private String filename;
private String userRole;
public ProtectedImageProxy(String filename, String userRole) {
this.filename = filename;
this.userRole = userRole;
}
@Override
public void display() {
if ("ADMIN".equals(userRole)) {
if (realImage == null) realImage = new RealImage(filename);
realImage.display();
} else {
System.out.println("권한 없음: 이미지 접근 불가");
}
}
}
Virtual Proxy
필요한 기능만을 가져오고 싶을 때 사용한다. 이때는 Subject 가 분리가 잘 되어 있어야 한다.
// Subject
interface Image {
void display();
}
// RealSubject
class RealImage implements Image {
private String filename;
public RealImage(String filename) {
this.filename = filename;
loadFromDisk();
}
private void loadFromDisk() {
System.out.println("원본 이미지 로딩: " + filename);
}
@Override
public void display() {
System.out.println("이미지 표시: " + filename);
}
}
// Virtual Proxy
class VirtualImageProxy implements Image {
private RealImage realImage;
private String filename;
private boolean showThumbnailOnly;
public VirtualImageProxy(String filename, boolean showThumbnailOnly) {
this.filename = filename;
this.showThumbnailOnly = showThumbnailOnly;
}
@Override
public void display() {
if (showThumbnailOnly) {
System.out.println("썸네일 표시: " + filename);
} else {
if (realImage == null) {
realImage = new RealImage(filename);
}
realImage.display();
}
}
}
Smart Proxy
실 객체에 접근할 때 추가 기능을 제공하는 프록시이며, 캐싱 기능, 로깅 기능 등등을 예로 들 수 있겠다.
interface Image {
void display();
}
// 실제 객체
class RealImage implements Image {
private final String filename;
public RealImage(String filename) {
this.filename = filename;
loadFromDisk();
}
private void loadFromDisk() {
System.out.println("이미지 로딩: " + filename);
}
@Override
public void display() {
System.out.println("이미지 표시: " + filename);
}
}
// Smart Proxy
class SmartImageProxy implements Image {
private RealImage realImage;
private final String filename;
private boolean cached = false;
public SmartImageProxy(String filename) {
this.filename = filename;
}
@Override
public void display() {
System.out.println("[Smart Proxy] display 호출 기록");
if (!cached) {
realImage = new RealImage(filename);
cached = true;
} else {
System.out.println("[Smart Proxy] 캐시된 이미지 사용");
}
realImage.display();
}
}
행위 패턴은 다음 장에서..