23가지 디자인 패턴 찍먹하기

이상민·2021년 8월 5일
0
post-thumbnail

계속 업데이트 중입니다

Design Pattern

  • SOLID 원칙을 정확하게 지키며 설계하기란 쉬운일이 아니다

  • 원칙들로만 이뤄져 있기 때문에 틀리게 해석할 위험성이 있고, 객체 작성마다 각 원칙을 고려하기는 어렵다

  • 여러 SW 엔지니어들이 SOLID 원칙에 따라 OOP를 설계 했고, 이런 설계들에서 나타나는 공통점들을 정리한 것이 바로 디자인 패턴이다

  • SOLID 원칙이 추상체라면, 디자인 패턴은 구상체라고 생각할 수 있다


1. Creational Patterns

객체 생성을 위한 메커니즘을 제공하는 패턴들

1-1. Factory Method

객체 생성을 위한 인터페이스를 제공하는 패턴

문제

  • 프로그램이 특정 클래스에 tight coupling 되어 있어 새로운 기능을 추가하려면 기존 코드를 많이 수정해야한다

해결

  • 객체 생성을 직접 호출하는 것이 아니라, 팩토리 메소드를 호출해 팩토리 내부에서 객체를 생성한다

  • 단순히 생성 위치를 옮긴 것으로 보일 수도 있지만, 팩토리 메소드를 통하게 하면서 자식 클래스에서 팩토리 메소드를 오버라이딩해 생성되는 객체를 바꿀 수 있다

  • 객체의 팩토리 메소드를 호출하는 호스트 코드는 모든 프로덕트(팩토리 메소드가 반환한 객체)를 추상체인 상위 클래스로 취급하기 때문에 실제 어떤 구현체가 프로덕트인지는 호스트 코드에게 중요하지 않다

예시

  • 프로덕트 인터페이스와 구현체
interface Button {
    void render();
    void click();
}


class LinuxButton implements Button {
    @Override
    public void render() {
        /* 리눅스 버튼 렌더 구현 */
    }
    
    @Override
    public void click() {
        /* 리눅스 버튼 클릭 구현 */
    }
}


class WindowsButton implements Button {
    @Override
    public void render() {
        /* 윈도우 버튼 렌더 구현 */
    }
    
    @Override
    public void click() {
        /* 윈도우 버튼 클릭 구현 */
    }
}
  • 팩토리 베이스 클래스와 구현체
import Button;
import LinuxButton;
import WindowsButton;

abstract class Dialog {
    public void renderWindow() {
        Button yesButton = createButton();
        yesButton.render();
    }
    
    // 팩토리 메소드는 하위 클래스에서 구현
    public abstract Button createButton();
}


class LinuxDialog extends Dialog {
    @Override
    public Button createButton() {
        return new LinuxButton();
    }
}


class WindowsDialog extends Dialog {
    @Override
    public Button createButton() {
        return new WindowsButton();
    }
}
  • 호스트 코드
// 호스트 코드는 프로덕트를 처음 생성할때만 어떤 팩토리를 사용하는지 선택하고
// 이후로는 베이스 클래스로 프로덕트를 다룬다 
Dialog dia = new WindwosDialog();
dia.renderWindow();

1-2. Abstract Factory

연관된 객체의 그룹을 생산하기 위한 패턴

문제

  • 다양한 종류의 프로덕트가 있고 각 프로덕트들은 하나의 그룹에 속할 때

  • 새로운 프로덕트 또는 그룹의 프로덕트를 추가할 때 기존 코드를 수정해야한다

해결

  • 각 프로덕트의 종류 별로 인터페이스를 만들고 모든 프로덕트를 생성할 수 있는 베이스 팩토리를 생성한 뒤 그룹별로 팩토리를 구체화한다

  • 호스트 코드는 모든 프로덕트를 각 프로덕트의 종류로 대한다

예시

  • 프로덕트 종류별 인터페이스와 구현체들
interface ProductA {
    void someMethod();
}

interface ProductB {
    void otherMethod();
}

class ProductA1 implements ProductA {
    @Override
    void someMethod() {
        /* 구현 */
    }
}

class ProductA2 implements ProductA {
    @Override
    void someMethod() {
        /* 구현 */
    }
}

class ProductB1 implements ProductB {
    @Override
    void otherMethod() {
        /* 구현 */
    }
}

class ProductB2 implements ProductB {
    @Override
    void otherMethod() {
        /* 구현 */
    }
}
  • 팩토리

public interface AbstractFactory {
    ProductA createProductA();
    ProductB createProductB();
}


class ConcreteFactory1 impelemnts AbtractFactory {
    @Override
    public ProductA createProductA() {
        return new ConcreteProductA1;
    }
    
    @Override
    public ProductB createProductB() {
        return new ConcreteProductB1;
    }
}


class ConcreteFactory2 impelemnts AbtractFactory {
    @Override
    public ProductA createProductA() {
        return new ConcreteProductA2;
    }
    
    @Override
    public ProductB createProductB() {
        return new ConcreteProductB2;
    }
}
  • 호스트 코드
AbstractFactory factory = new ConcreteFactory1();
ProductA pa = factory.createProductA();
  • 구현부가 없는 팩토리 인터페이스로도 프로덕트 생산이 가능해진다

1-3. Builder

동일한 생성코드로 다양한 객체를 생산 가능하게 하는 패턴

문제

  • 하나의 추상체에서 매우 다양한 하위 구상체가 나오고, 계속 추가해야한다면 어떻게 관리해야 효율적일까?

  • 모든 것이 가능하도록 추상체의 생성자에 많은 데이터를 넣을 수도 있지만, 그렇게 하면 말도 안되게 거대한 생성자가 생길 것이다

해결

  • 객체 생성코드를 클래스 밖으로 빼내어 별도 클래스인 builder를 만든다

  • 각 빌더는 객체를 생성하기 위한 다양한 메소드들을 가지고, 필요한 메소드만 호출해 객체를 생성할 수 있다

  • director 클래스를 통해 호스트 코드에서 일일히 메소드를 호출하지 않고 관리할 수도 있다

예시

  • builder
class Car {}
class Manual {}


interface Builder {
    void reset();
    void setSeats(int n);
    void setEngine(Engine e);
    void setTripComputer();
    void setGPS();
}


class CarBuilder implements Builder {
    private Car car;
    
    CarBuilder() {
        this.reset();
    }
    
    @Override
    public void reset() {
        this.car = new Car();
    }
    
    @Override
    public void setSeats(int n) {}
    
    @Override
    public void setEngine(Engine e) {}
    
    @Override
    public void setTripComputer() {}
    
    @Override
    public void setGPS() {}
    
    // 빌더는 다양한 프로덕트를 생산할 수 있기 때문에
    // 빌더 인터페이스에서 getResult()를 정의하지 않고
    // 각 구상체에서 정의한다
    // 빌더는 프로덕트 반환 후 생산 준비가 되어야하기 때문에 
    // reset 해줘야한다
    public Car getResult() {
        Car product = this.car;
        this.reset();
        return product;
    }
}


class CarManualBuilder implements Builder {
    private Manual man;
    
    CarManualBuilder() {
        this.reset();
    }
    
    @Override
    public void reset() {
        this.man = new Manual();
    }
    
    @Override
    public void setSeats(int n) {}
    
    @Override
    public void setEngine(Engine e) {}
    
    @Override
    public void setTripComputer() {}
    
    @Override
    public void setGPS() {}
    
    public Manual getResult() {
        Manual product = this.man;
        this.reset();
        return product;
    }
}
  • Director
public class Director {
    private Builder builder;
    
    public void setBuilder (Builder builder) {
        this.builder = builder;
    }
    
    public void makeSportsCar(Builder builder) {
        builder.reset();
        builder.setSeats(2);
        builder.setEngine(new SportEngine());
        builder.setTripComputer(true);
        builder.setGPS(true);
    }

}
  • 호스트 코드
Director director = new Director();
CarBuilder builder = new CarBuilder();
director.makeSportsCar(builder);
Car car = builder.getResult();

1-4. Prototype

클래스에 대한 의존 없이 기존의 객체를 복사하기 위한 패턴

문제

  • 인스턴스를 복제하고 싶을때, 인스턴스의 클래스를 찾아 동일한 속성값을 넣어주면 똑같이 작동하는 인스턴스를 만들 수 있지만, 클래스를 정확히 모르거나 클래스 멤버가 private일때 어려움이 있고, 인스턴스가 클래스에 의존하게 된다

해결

  • 복사 과정을 실제 복사되는 객체에 위임한다

  • 복사가 가능한 객체에 클래스에 coupling될 필요 없이 복사할 수 있는 인터페이스를 clone()을 선언한다

  • clone()은 보통 동일한 클래스의 인스턴스를 생성하고 복사하는 객체의 정보를 새로운 객체에 옮겨 담아 구현한다

  • clone() 인터페이스를 구현한 객체를 프로토타입이라 부른다

예시

public abstract class Shape {
    int x;
    int y;
    String color;
    
    public Shape() {}
    
    // 프로토타입 생성자 
    public Shape(Shape source) {
        this.x = source.x
        this.y = source.y
        this.color = source.color
    }
    
    abstract Shape clone();
}
class Rectangle extends Shape {
    int width;
    int height;
    
    public Rectangle(Rectangle source) {
        super(source);
        this.width = source.width;
        this.height = source.height;
    }
    
    @Override
    Shape clone() {
        return new Rectangle(this);
    }
}
  • 호스트 코드
Shape shape = new Rectangle();
Shape shapeCopy = shape.clone();

1-5. Singleton

클래스의 인스턴스가 오직 하나만 존재하는 것을 보장하고 글로벌 접근을 제공하기 위한 패턴

문제

  1. 클래스가 하나의 인스턴스만 가지도록 보장하기 어렵다. 인스턴스의 수를 관리하는 가장 기본적인 예시는 공유 자원에 대해 접근을 제어하는 경우이다. RDB의 경우 어디에서 접근하던 해당 객체는 하나여야만 한다. 일반적인 생성자는 항상 새로운 인스턴스를 반환하기 때문에 이를 보장하기는 어렵다.

  2. 전역에서 인스턴스를 안전하게 접근 가능하게 하기 어렵다. 전역변수가 위험성이 많은 것처럼 전역 인스턴스는 어디에서도 바꿀 위험이 있어 조심해야한다.

해결

  • 기본 생성자를 private으로 만들어 새로운 인스턴스 생성을 막는다

  • static한 생성 메소드를 만든다. 내부적으로 이 생성 메소드를 호출 시 private 생성자를 호출하고 이를 static 필드에 저장한다. 이후 이 생성자로의 접근은 static 필드를 반환한다

  • 사실 요즘에는 둘 중 하나만 해결해도 싱글톤 패턴이라고 한다

예시

  • Singleton
public final class Singleton {
    // double check lock이 정상적으로 작동하도록 volatile로 선언
    private static volatile Singleton instance;
    public String value;

    private Singleton(String value) {
        this.value = value;
    }

    public static Singleton getInstance(String value) {
        Singleton result = instance;
        
        if (result != null) {
            return result;
        }
        
        synchronized(Singleton.class) {
            if (instance == null) {
                instance = new Singleton(value);
            }
            return instance;
        }
    }
}
  • 호스트 코드
Singleton instance = Singleton.getInstance("싱글턴");
var value = instance.value;

안티 패턴


2. Structural Patterns

객체들를 합쳐 구조를 이루면서, 유연하고 효율적인 구조를 유지하기 위한 패턴들

2-1. Adapter

호환되지 않는 인터페이스를 가진 객체끼리 협력할 수 있게 하기 위한 패턴

문제

  • 사용자 요청은 xml으로 들어오는데 사용하려는 외부 라이브러리는 json만 받는다. 라이브러리가 xml을 받도록 수정하는 것을 불가능하다

해결

  • 어플리케이션에서 xml을 json로 변환해 라이브러리에 전달하도록한다

  • 이런 한 객체의 인터페이스를 다른 객체가 이해할 수 있도록 해주는 객체(위 경우 변환기)를 어답터라고 한다

예시

@AllArgsConstructor
@Getter
public class RoundHole {
    
    private int radius 
    
    public boolean fits(RoundPeg peg) {
        return this.getRadius() >= peg.getRadius();
    }
    
}


@AllArgsConstructor
@Getter
public class RoundPeg {

    private int radius;

}


@AllArgsConstructor
@Getter
public class SquarePeg {

    private int width;

}


@AllArgsConstructor
public class SquarePegAdapter extends RoundPeg {
    
    private SquarePeg peg;
    
    public int getRadius() {
        return this.peg.getWidth() * Math.sqrt(2) / 2
    }
}


// 클라이언트 코드

// 컴파일 불가한 코드 
var hole = new RoundHole(5);
var squarePeg = new SquarePeg(5);
hole.fits(squarePeg);

// 어답터 사용
var hole = new RoundHole(5);
var squarePegAdapter = new SquarePegAdapter(new SquarePeg(5));
hole.fits(squarePegAdapter);

2-2. Bridge

하나의 거대한 클래스 또는 깊게 연관된 클래스들을 두개의 별도 계층으로 분리하기 위한 패턴

2-3. Composite

객체들을 트리 구조들로 구성하고 마치 트리 구조의 부분부분이 각각의 객체인것처럼 처리하기 위한 패턴

문제

  • 여러 제품이 담긴 주문품의 총 가격을 어떻게 알 수 있을까?

  • 위 트리 구조처럼 주문품을 나타낸다면 루프를 돌며 상자인지, 제품인지 확인하고 제품인 경우에 대해서만 가격을 구하고, 모든 제품의 가격을 더할 방법이 있어야하고 등등 복잡하다

해결

  • 컴포지트 패턴은 제품과 상자를 동일한 인터페이스를 통해 총 가격을 계산하도록 한다

  • 만약 제품이라면 그냥 가격을 반환하면 되고, 상자라면 하위의 모든 제품을 찾아 값을 더해 가격을 반환한다

  • 이때 만약 상자 아래에 또 다른 상자가 있다면, 마찬가지로 모든 제품을 찾게 될것이다

  • 클라이언트는 트리의 구성을 명확히 알 필요 없이, 똑같이 인터페이스를 통해 사용한다

예시

  • 컴퓨터의 파일 구조를 생각해볼 수 있다

abstract public class Component {

    private String name;
    
    public Component(String name) {
        this.name = name;
    }
    
    abstract public int getSize();
    
}


// 리프 
public class File extends Component {

    private Object data;
    
    private int size;
    
    public File(String name) {
        super(name);
    }
    
    @Override
    public int getSize() {
        return this.size;
    }

}


// 컴포지트
public class Folder extends Component {

    List<Component> children = new ArrayList<>();
    
    public Folder(String name) {
        super(name);
    }

    public boolean addComponent(Component component) {
        return children.add(component);
    }
    
    public boolean removeComponent(Component component) {
        return children.remove(component);
    }
    
    @Override
    public int getSize() {
        return children.stream()
            .mapToInt(Component::getSize)
            .sum();
    }

}

2-4. Decorator

객체를 새로운 행동을 지닌 wrapper 객체로 감싸므로서 행동을 추가하기 위한 패턴.

문제

  • 어플리케이션에서 사용할 수 있는 알림 라이브러리를 만든다고 하자. 다양한 채널로 알림을 보낼 수 있게 하려면 다음처럼 상속을 통해 구현할 수 있다

  • 여기서 채널이 더 추가되거나, 또는 복수 채널에 알림을 보낼 수 있게 하려면 다음처럼 복잡해질 것이다

해결

  • wrapper 객체는 타겟 객체와 동일한 인터페이스를 가진다. 메소드에 요청이 들어올 시 타겟 객체에 위임한다. 이때 wrapper 객체는 타겟 객체에게 전달하기전, 또는 이후 다른 작업을 해서 결과를 바꿀 수 있다

  • 즉 클라이언트 입장에서 wrapper 객체와 타겟 객체는 동일하다

  • wrapper 객체가 특정 인터페이스를 구현한 객체를 필드로 가질 수 있게 한다면, wrapper를 wrapper로 감싸며 모든 wrapper들의 행동을 추가하는 것이 가능해진다

예시

  • 데이터를 사용하는 코드와 별개로 데이터를 압축하거나 암호화할 수 있다

// DataSource.java
public interface DataSource {
    void writeData(Data data);
    Data readData();
}


// FileDataSource.java
public class FileDataSource implements DataSource {

    private final String filename;
    
    public FileDataSource(String filename) {
        this.filename = filename;
    }
    
    @Override
    public void writeData(Data data) {
        // 파일에 데이터 작성
    }
    
    @Override
    public Data readData() {
        // 파일에서 데이터를 읽는다 
        return data;
    }
}


// DataSourceDecorator.java
public class DataSourceDecorator implements DataSource {
    
    private final DataSource wrappee;
    
    public DataSourceDecorator(DataSource dataSource) {
        this.wrappee = dataSource;
    }
    
    @Override
    public void writeData(Data data) {
        this.wrappee.writeData(data);
    }
    
    @Override
    public Data readData() {
        return this.wrappee.readData();
    }
}


// EncryptionDecorator.java
public class EncryptionDecorator extends DataSourceDecorator {

    public EncryptionDecorator(DataSource dataSource) {
        super(dataSource);
    }

    @Override
    public void writeData(Data data) {
        super.writeData(encrypt(data));
    }
    
    @Override
    public Data readData() {
        Data data = super.readData();
        
        if (data.isEncrpyted)
            return decrypt(data);
            
        return data;
    }

}


// CompressionDecorator.java
public class CompressionDecorator extends DataSourceDecorator {

    public CompressionDecorator(DataSource dataSource) {
        super(dataSource);
    }

    @Override
    public void writeData(Data data) {
        super.writeData(compress(data));
    }
    
    @Override
    public Data readData() {
        Data data = super.readData();
        
        if (data.isCompressed)
            return decompress(data);
            
        return data;
    }

}


// 클라이언트 코드
// file.dat에 평문 데이터 추가
DataSource dataSource = new FileDataSource("file.dat");
dataSource.writeData(someData);

// file.dat에 압축된 데이터 추가
dataSource = new CompressionDecorator(dataSource);
dataSource.writeData(someData);

// file.dat에 암호화되고 압축된 데이터 추가 
dataSource = new EncryptionDecorator(dataSource);
source.writeData(someData);

2-5. Facade

라이브러리, 프레임워크 등 복잡한 클래스들의 집합에 단순화된 인터페이스를 제공하기 위한 패턴

문제

  • 복잡한 라이브러리와 동작하는 코드를 작성해야한다면, 객체 생성, 의존석 관리, 메소드 실행을 올바른 순서로 하려고 해야하고 복잡하다

  • 코드의 비즈니스 로직이 라이브러리와 결합도가 높아지게 된다. 코드를 이해, 유지 보수하기 어려워진다

해결

  • 퍼사트 패턴을 사용한다. 라이브러리를 직접 사용하는 것보다 기능은 제한되지만, 클라이언트가 필요로하는 기능만 포함시켜 복잡도를 낮출 수 있다

예시


public class VideoFile{}

public class OggCompressionCodec{}

public class MPEG4CompressionCodec{}

public class CodecFactory{}

public class BitrateReader{}

public class AudioMixer{}

public class VideoConverter {
    public File convert(String filename, Format format) {
        File file = new VideoFile(filename);
        srcCodec == new CodecFactory.extract(file);
        ...
        if (format.equals("mp4")
            destCodec = new MPEG4CompressionCodec();
        else 
        ...
        return new File(...);
    }
}

// 클라이언트 코드
var converter = new VideoConverter();
var mp4File = converter.convert("/file/dir/namme", Format.MP4);

2-6. Flyweight

객체간 공통 부분을 공유해 더 많은 객체를 RAM에 담기 위한 패턴

2-7. Proxy

객체의 대체로 다른 객체를 두어 프록시 객체가 원본 객체로의 접근을 관리하게 하는 패턴

문제

  • 어떻게 해야 객체로의 접근을 제어 할 수 있을까? lazy initialization을 구현해 실제로 필요한 시점에만 호출하도록 할 수도 있지만, 그러면 클라이언트들에서 코드 중복이 많을 것이다

해결

  • 객체와 동일한 인터페이스를 가진 프록시를 만든다. 클라이언트들은 프록시에 요청을 하고, 프록시는 이를 처리하여 실제 객체에 요청한다.

예시

public interface ThridPartyYoutubeLib {

    List<Video> listVideos();
    Video getVideoInfo(Long id);
    void downloadVideo(Long id);

}


public class ThridPartyYouTubeClass implements ThridPartyYoutubeLib {

    @Override
    public List<Video> listVideos() {}
    
    @Override
    public Video getVideoInfo(Long id) {}
    
    @Override
    public video downloadVideo(Long id) {}

}


public class CachedYoutubeClass implements ThirdPartyYoutubeLib {

    private ThridPartyYouTubeLib service;

    private List<Video> listCache;

    private Video videoCache;

    boolean needReset;

    Public CachedYoutubeClass(ThridPartyYouTubeLib service) {
    this.service = service
    }
    
    @Override
    public List<Video> listVideos() {
        if (listCache == null || needReset)
            listCache = service.listVideos();
        return listCache;
    }
    
    @Override
    public Video getVideoInfo(Long id) {
        if (videoCache == null || needReset)
            videoCache = service.getVideoInfo(id);
        return videoCache;
    }
    
    @Override
    public void downloadVideo(Long id) {
        
    }

}

3. Behavioral Patterns

객체간 알고리즘과 책임의 할당에 대한 패턴들

3-1. Chain of Responsibility

여러 handler가 이루는 체인에 요청을 보내고, 각 handler가 요청을 처리할지 또는 다음 handler로 전달할지로 구성된 패턴

3-2. Command

요청을 독립적인 객체로 만들어 요청을 메소드의 인자로 사용할 수도 있고, 요청을 처리를 지연시킬 수도 있게 하는 패턴

문제

  • 프로그램의 다양한 버튼을 위해 버튼 클래스를 만들었다. 버튼마다 각기 다른 작업을 할텐데 이를 어떻게 코딩할 수 있을까? 가장 간단하게는 버튼을 상속해 추가해줄 수 있다

  • 하지만 이렇게 구현할 경우 1) 수많은 서브 클래스가 생기고 2) 버튼 클래스 수정 시 서브 클래스가 고장날 수 있고 3) 여러 버튼에서 동일한 기능을 사용할 시 코드가 중복된다

해결

  • 비즈니스 레이어와 GUI 레이어를 분리해 해결할 수 있다. GUI 레이어에서 비즈니스 로직을 실행하는 것이 아니라 분리해 여러 GUI 레이어 객체에서 동일한 비즈니즈 로직을 호출하도록 할 수 있다

  • GUI와 비즈니스의 중간 레이어로 인페이스를 두어 GUI와 비즈니스 레이어의 결합도를 낮출 수 있다

예시

public abstract class Command {
    protected Application app;
    protected Editor editor;
    protected String backup;
    
    public Command(Application app, Editor editor) {
        this.app = app;
        this.editor = editor;
    }
    
    public void saveBackup() {
        backup = editor.getText();
    }
    
    public void undo() {
        editor.setText() = backup;
    }

    public abstract boolean execute();
}
public class CopyCommand extends Command {
    
    @Override
    public boolean execute() {
        app.clipboard = editor.getSelection()
        return false;
    }
    
}
public class CutCommand extends Command {
    
    @Override
    public boolean execute() {
        saveBackup();
        app.clipboard = editor.getSelection();
        editor.deleteSelection();
        
        return true;
    }
    
}
public class PasteCommand extends Command {
    
    @Override
    public boolean execute() {
        saveBackup();
        editor.replaceSelection(app.clipboard);
        
        return true;
    }
    
}
public class PasteCommand extends Command {
    
    @Override
    public boolean execute() {
        saveBackup();
        editor.replaceSelection(app.clipboard);
        
        return true;
    }
    
}
public class UndoCommand extends Command {
    
    @Override
    public boolean execute() {
        app.undo()
        return false;
    }
    
}
public class CommandHistory {
    
    private Stack<Command> history;
    
    public void push(Command command) {
        history.push(command);
    }
    
    public Command pop() {
        return history.pop();
    }
}
public class Editor {

    private String text;
    
    public String getSelection() {}
    
    public void deleteSelection() {}
    
    public void replaceSelection() {}

}

3-3. Iterator

컬랙션의 요소를 자료구조의 노출 없이 탐색하기 위한 패턴

문제

  • 콜렉션이란 객체들의 집합의 컨테이너이다. 이 객체들은 단순한 리스트 형태로 저장될 수도 있지만, 스택, 트리, 그래프 등 다양한 자료 구조 형태로 저장될 수 있다. 콜렉션은 클라이언트에게 쉽게 객체들에 순차적으로 접근할 수 있는 방법을 제공해야한다.

  • 같은 트리라도 DFS, BFS로 탐색 방법이 다를 수 있기 때문에 이를 모드 포함하도록 한다면 효율적인 데이터 저장소인 콜렉션의 의미가 퇴색된다. 또한 클라이언트가 신경써야하는 부분이 추가로 늘어나게 된다

해결

  • 이터레이터 패턴을 통해 콜렉션의 탐색 방법을 별도 객체로 분리한다

  • 이터레이터 객체는 알고리즘의 구현 외에도 현재 위치 정보, 남은 객체 수 등을 관리한다. 콜렉션이 관리하지 않기 때문에, 여러 이터레이터가 하나의 콜렉션에 대해 동시에 동작할 수 있다

예시

public interface SocialNetwork {
    ProfileIterator createFriendsIterator(Long profileId);
    ProfileIterator createCoworkersIterator(Long profileId);
}


public class Facebook implements SocialNetwork {
    
    @Override
    ProfileIterator createFriendsIterator(Long profileId) {
      return new FacebookIterator(this, profileId, "friends");
    }
    
    @Override
    ProfileIterator createCoworkersIterator(Long profileId) {
        return new FacebookIterator(this, profileId, "coworkers);
    }
    
}


public interface ProfileIterator {
    Profile getNext();
    boolean hasMore();
}


public class FacebookIterator implements ProfileIterator {

    private Facebook facebook;
    private Long profileId;
    private String type;
    private Long currentPosition;
    private Profile[] cache;
    
    public FacebookIterator(Facebook facebook, Long profileId, String type) {
        this.facebook = facebook;
        this.profileId = profileId;
        this.type = type;
    }
    
    private void lazyInit() {
        if (cache == null) {
            cache = facebook.socialGraphRequest(profileId, type);
        }
    }
    
    public Profile getNext() {
        if (hasMore()) {
            currentPositon++;
            return cache[currentPosition];
        }
    }
    
    public hasMore() {
        lazyInit()
        return currentPositon < cache.length;
    }

}


public class SocialSpammer {
    public void sned(ProfileIterator iterator, String message) {
        while (iterator.hasMore()) {
            profile = iterator.getNext();
            System.sendEmail(profile.getEmail(), message);
        }
    }
}


public class Application {
    private SocialNetwork network;
    private SocialSpammer spammer;
    
    public void config() {
        if (working with Facebook)
            this.network = new Facebook();
        if (working with LinkedIn)
            this.network = new LinkedIn();
        this.spammer = new SocialSpammer();
    }
    
    public void sendSpamToFriends(Profile profile) {
        iterator = network.createFirendsIterator(profile.getId());
        spammer.send(iterator, "hi");
    }
    
    public void snedSpamToCoworkers(Profile profile) {
        iterator = network.createCoworkersIterator(profile.getId());
        spammer.send(iterator, "hello");
    }
}

3-4. Mediator

객체들이 mediator 객체를 통해 협동하도록 강제하여 의존성을 줄이는 패턴

3-5. Memento

객체 구현에 대한 상세 정보 없이 객체의 상태를 저장하고 이전 상태로 복구 시키기 위한 패턴

3-6. Observer

= Listener
객체가 다른 객체를 구독하는 장치를 정의해 객체에서 발생하는 이벤트를 구독하는 객체에게 알리는 패턴

문제

  • 상태를 주기적으로 확인하는 폴링 방식으로 다른 객체의 변화를 감지하려고 한다면, 아직 변화가 일어나지 않았음에도 확인하는 것은 자원의 낭비이다

  • 반대로 모든 변화에 대해 전달해도 필요없는 데이터의 전달이 될것이다. 어떻게 하면 원하는 변화에 대해서만 알 수 있을까?

해결

  • 상태를 가지고, 이를 다른 객체에게 알리는 객체를 publisher라고 한다. 알림을 받는 객체를 subscriber라고 한다

  • 퍼블리셔는 구독자의 배열을 필드로 가지고, 배열에 구독자를 추가하거나 제거할 수 있는 인터페이스를 제공한다

  • 퍼블리셔는 어떤 상태가 변화하거나 무엇인가 실행했을때 구독자들에게 알림을 보낸다

예시

// EventManager.java
public class EventManager {
    
    private Map<String, List<Listener>> listeners = new HashMap<>();
    
    public EventManager(String... operations) {
        for (String operation : operations)
            this.listeners.put(operation, new ArrayList<>());
    }
    
    public void subscribe(String eventType, Listener listener) {
        List<Listener> users = listeners.get(eventType);
        users.add(listener);
    }
    
    public void unsubscribe(String eventType, Listener listener) {
        List<Listener> users = listeners.get(eventType);
        users.remove(listener);
    }
    
    public void notify(String eventType, File file) {
    	List<Listener> users = listeners.get(eventType);
        for (Listener listener : users) {
            listener.update(eventType, file);
        }
    }
}


// Editor.java
public class Editor {

    public EventManager events = new EventManager("open", "save");
    
    private File file;
    
    public void openFile(String filePath) {
        this.file = new File(filePath);
        events.notify("open", file);
    }
    
    public void saveFile() throws Exception {
        if (this.file == null)
            throw new Exception("Must open file before save");
        
        events.notify("save", file);
    }

}


// Listener.java
public interface Listener {

    void update(String eventType, File file);

}


// EmailAlertsListener.java
// 파일이 저장되는 이벤트 구독
public class EmailAlertsListener implements Listener {
    
    private String email;
    
    public EmailAlertsListener(String email) {
        this.email = email;
    }

    @Override
    public void update(String eventType, File file) {
        System.out.println("Email to " + email + ": Someone has performed " + eventType + " operation with the following file: " + file.getName());
    }
}


// LoggingListener.java
// 파일이 열리는 이벤트 구독 
public class LoggingListener implements Listener {

    private File log;
    
    public LoggingListener(String fileName) {
        this.log = new File(fileName);
    }
    
    @Override
    public void update(String eventType, File file) {
        System.out.println("Save to log " + log + ": Someone has performed " + eventType + " operation with the following file: " + file.getName());
    }

}


// 클라이언트 코드
Editor editor = new Editor();
editor.events.subscribe("open", new LoggingListener("some.log"));
editor.events.subscribe("save", new LoggingListener("some.log"));

try {
    editor.openFile("test.txt");
    editor.saveFile();
} catch (Exception e) {
    e.printStackTrace();
}

3-7. State

객체 내부의 상태가 변하면 객체의 행동을 변경하게해 마치 클래스가 변한것처럼 작동하는 패턴

문제

  • 객체에는 다양한 상태가 있을 수 있다. 이때 각 상태에 따라 다른 상태로 변화가 가능할 수도 가능하지 않을 수도 있다

  • 예를 들어 위 그림에서 객체가 A 상태에 있다면 B 상태로는 변할 수 있지만, B 상태를 거치지 않고 다른 상태가 될 수는 없다

  • 이런 상태의 제약을 어떻게 코드로 표현할 수 있을까?

해결

  • 상태의 인터페이스를 두고, 모든 상태를 해당 인터페이스를 상속하는 클래스로 만든다. 객체(컨텍스트)는 상태 인터페이스를 상속하면서 상태 객체를 필드로 가진다. 컨택스트는 상태 인터페이스의 메소드 실행을 상태 객체에 위임한다

예시

public enum PaymentStatus {

    WAITING {

        @Override
        public PaymentStatus cancel() {
            return PaymentStatus.CANCELED;
        }

        @Override
        public PaymentStatus complete() {
            return PaymentStatus.COMPLETED;
        }

    },

    COMPLETED {

        @Override
        public PaymentStatus cancel() {
            return PaymentStatus.CANCELED;
        }

        @Override
        public PaymentStatus complete() {
            throw new StatusConflictException("payment status is already competed");
        }

    },

    CANCELED {
        
        @Override
        public PaymentStatus cancel() {
            throw new StatusConflictException("payment status is already canceled");
        }

        @Override
        public PaymentStatus complete() {
            throw new StatusConflictException("canceled payment cannot be completed");
        }

    };

    abstract public PaymentStatus cancel();

    abstract public PaymentStatus complete();

}

@Entity
@Getter
@NoArgsConstructor
public class Ticket {

    ...
    
    public void cancel() {
        LocalDateTime cancelableUntil = round.getTicketCancelableUntil();

        if (LocalDateTime.now().isAfter(cancelableUntil)) {
            throw new TimeExceedException(
                MessageFormat.format("ticket was cancelable until {0}", cancelableUntil)
            );
        }

        paymentStatus = paymentStatus.cancel();
    }

    public void complete() {
        paymentStatus = paymentStatus.complete();
    }

}

3-8. Strategy

일련의 알고리즘들을 정의하고, 이 알고리즘들을 하나의 클래스로 담아 이런 클래스들이 상호 교환 가능하게 만드는 패턴

문제

하나의 동작을 여러가지 방식으로 실행할 수 있는 경우를 생각하자. 예를들어 출발 지점부터 도착 지점까지 경로를 추천해주는 네비게이션 앱을 만든다고 하자. 처음에는 자동차를 위한 경로 추천 기능만 추가했지만, 이후에는 자전거, 보행자, 여행객 등 다양한 경로 추천 기능을 추가하고 싶다. 이를 하나의 객체 내에서 구현한다면 다음과 같은 문제들이 발생한다

  1. 객체가 너무 커진다. 하나의 경로 추천 알고리즘의 수정이 객체 전체에 영향을 끼친다. 에러가 발생할 확률이 높아진다

  2. 협업이 어려워진다. 형상 관리 툴을 통해 협업한다면, 동일한 객체를 수정하다보니 팀원들이 계속 머지를 하고 충돌을 해결해야한다.

해결

  • 하나의 동작을 여러가지 방식으로 할 수 있는 객체에 알고리즘을 별도 객체로 분리한다. 이때 동작을 가진 객체를 context라 하고, 분리된 알고리즘을 strategy라고 한다

  • contextstrategy 객체를 필드로 가진다. 하지만 context가 전략을 선택하지는 않는다. 전략을 클라이언트에게 주입 받는 것으로 컨택스트는 단순히 특정 동작에 대한 인터페이스만 제공한다

  • 컨택스트 객체의 수정 없이 전략을 추가할 수 있게 된다. 또한 런타임에 동작의 실행 방식을 바꿀 수 있다

예시

// Strategy.java
public interface Strategy {

    int execute(int a, int b);
    
}


// ConcreteStrategyAdd.java
public class ConcreteStrategyAdd implements Strategy {

    @Override
    int execute(int a, int b) {
        return a + b;
    }
    
}


// ConcreteStrategySubtract.java
public class ConcreteStrategySubtract implements Strategy {

    @Override
    int execute(int a, int b) {
        return a - b;
    }
    
}


// Context.java
public class Context {
    
    private Strategy strategy;
        
    public Context(Strategy strategy) {
        this.strategy = strategy;
    }
        
    void changeStrategy(Strategy strategy) {
        this.strategy = strategy;
    }
    
    int executeStrategy(int a, int b) {
        return this.strategy.execute(a, b);
    }
    
}


// 클라이언트 코드
int num1 = 10;
int num2 = 5;
Strategy strategy = new ConcreteStrategyAdd();

Context context = new Context(strategy);
context.executeStrategy(num1, num2);

주의 사항

  • 알고리즘이 몇개 없고, 자주 바뀌지 않는다면 전략 패턴을 적용하는 것은 오히려 프로그램을 복잡하게 한다

  • 전략이 간단하다면 그냥 익명 함수를 전달하는 것이 더 깔끔할 수 있다


3-9. Template Method

알고리즘의 기본 구조를 부모 클래스에 정의하고 자세한 실행은 자식 클래스에서 구현하도록하는 패턴

문제

  • 문서 파일에서 정보를 읽어와 저장하는 프로그램을 만들고 싶다. 처음에는 Doc 파일만 지원했지만, 나중에는 CSV, PDF 파일까지 지원하게 된다. 여러 파일을 지원하고 보니 파일을 읽어오는 객체 간에 중복된 코드가 많다는 것을 알게 된다.

  • 클라이언트 코드에서 위 객체들을 사용하려고 보니, 어떤 포멧의 파일인지에 따라 조건문을 통해 실행해야한다. 세 객체가 동일한 인터페이스를 가졌다면 조건문에 의한 분기 없이 다형성으로 해결할 수 있지 않을까?

해결

  • 템플릿 메소드 패턴은 알고리즘의 단계들을 메소드로 나누고 템플릿 메소드에 이들을 호출하는 방식으로 작성한다. 각 단계는 추상 메소드 일 수도 있고, 기본 구현이 있을 수도 있다. 알고리즘을 사용하기 위해서, 클라이언트는 추상 메소드를 구현한 자식 클래스를 만든다. 이때 일부 기본 구현도 오버라이딩이 필요하다면 한다.

예시

public abstract class GameAI {

    ...
    
    public void takeTurn() {
        collectResources();
        buildStructures();
        buildUnits();
        attack();
    }
    
    protected void collectResources() {
        for (Structure s : this.builtStructures) {
            s.collect();
        }
    }
    
    protected abstract void buildStructures();
    
    protected abstract void buildUnits();
    
    protected void attack() {
        enemy = closestEnemy();
        if (enemy == null)
            sendScouts(map.center);
        else
            sendWarrirors(enemy.position)
    }
    
    protected abstract void sendScouts(Position position);
    
    protected abstract void sendWarrirors(Positon position);
}
public class OrcsAI extends GameAI {

    @Override
    protected void buildStrctures() {
        if (자원이 있으면)
            // 건물들을 지음
    }        
    
    @Override
    protected void buildUnits() {
        if (자원이 충분하면)
            // 피온을 만들고 정찰 그룹에 포함
        else
            // 그런트를 만들고 전사 그룹에 포함 
    }
    
    @Overide
    protected void sendScouts(Position positon) {
        if (scouts.length > 0)
            // 위치로 정찰을 보냄 
    }
    
    @Override
    protected void sendWarriors(Position position) {
        if (warriors.length > 5) 
            // 위치로 전사를 보냄 
    }
    
}
public class extends GameAI {
  
    @Override
    protected void collectResources {}
    
    @Override
    protected void buildStructures() {}
    
    @Override
    protected void buildUnits() {}
    
}

3-10. Visitor

알고리즘이 작동하는 객체에서 분리할 수 있게 하는 패턴

3-11. Interpreter

문법 규칙을 클래스화 한 구조로, 일련의 규칙으로 정의된 문법적 언어를 해석하는 패턴


출처

https://refactoring.guru/

profile
편하게 읽기 좋은 단위의 포스트를 추구하는 개발자입니다

0개의 댓글

관련 채용 정보