디자인 패턴

YH·2023년 4월 5일
0

✅ 디자인 패턴이란?

  • 자주 사용하는 설계 패턴을 정형화해서 이를 유형별로 가장 최적의 방법으로 개발할 수 있도록 정해둔 설계
  • 알고리즘과 유사하지만, 명확하게 정답이 있는 형태가 아니며, 프로젝트의 상황 및 구성에 맞추어 적용 가능

✅ GOF 디자인 패턴

  • 소프트웨어를 설계할 때는 기존의 경험이 매우 중요한데, 이러한 지식을 공유하기 위해서 나온 것이 GOF(Gong of Four)의 디자인 패턴
  • 객체지향 개념에 따른 설계 중 재사용할 경우 유용한 설계를 디자인 패턴으로 정리해 둔 것
  • GOF의 디자인 패턴은 총 23개

✅ 디자인 패턴의 장/단점

장점

✔️ 개발자(설계자) 간의 원할한 소통
✔️ 소프트웨어 구조 파악에 용이
✔️ 재사용이 가능하여 개발 시간 단축 가능
✔️ 설계 변경 요청에 대한 유연한 대처 가능

단점

✔️ 객체지향적 설계와 구현을 해야하므로 객체지향에 대해 잘 알고 있어야 함
✔️ 초기 설계 및 구현하는데 투자 비용이 많이 들어갈 수 있음

✅ 디자인 패턴의 종류

생성 패턴

✔️ 객체를 생성하는 것과 관련된 패턴으로, 객체의 생성과 변경이 전체 시스템에 미치는 영향을 최소화하고 코드의 유연성을 높여줌

  • Factory Method
  • Singleton
  • Prototype
  • Builder
  • Abstract Factory
  • Chaning

구조 패턴

✔️ 프로그램 내의 자료구조나 인터페이스 구조 등 프로그램 구조를 설계하는데 활용될 수 있는 패턴
✔️ 클래스, 객체들의 구성을 통해서 더 큰 구조를 만들 수 있게 해줌
✔️ 큰 규모의 시스템에서는 많은 클래스들이 서로 의존성을 가지게 되는데, 이런 복잡한 구조를 개발하기 쉽게 만들어주고, 유지보수 하기 쉽게 만들어 줌

  • Adapter
  • Composite
  • Bridge
  • Decorator
  • Facade
  • Flyweight
  • Proxy

행위 패턴

✔️ 반복적으로 사용되는 객체들의 상호작용을 패턴화한 것
✔️ 클래스나 객체들이 상호작용하는 방법과 책임을 분산하는 방법을 제공
✔️ 행위 관련 패턴을 사용하여 독립적으로 일을 처리하고자 할 때 사용

  • Template Method
  • Interpreter
  • Iterator
  • Observer
  • Strategy
  • Visitor
  • Chain of resposibility
  • Command
  • Mediator
  • State
  • Memento

✅ 싱글톤 패턴(Singleton)

✔️ 어떤 클래스(객체)가 유일하게 1개만 존재할 때 사용
✔️ 서로 자원을 공유할 때 주로 사용 되며, 실물 세계에서의 대표적인 예는 프린터가 있음
✔️ 실제 프로그래밍에서는 TCP Socket 통신에서 서버와 연결된 connect 객체에 주로 사용됨
✔️ 스프링에서는 Spring Bean이 싱글톤으로 관리됨
✔️ 기본적으로 default 생성자는 Private이며, get method를 통해 생성된 인스턴스를 가져다 씀

✔️ 예제 코드

public class SocketClient {
    private static SocketClient socketClient = null;

    //기본 생성자를 private로 막음
    private SocketClient() {}

    //static 메소드를 통해서 인스턴스를 가져오도록 함
    public static SocketClient getInstance() {
        //객체가 null인 경우 새로 생성
        if(socketClient == null) {
            socketClient = new SocketClient();
        }
        return socketClient;
    }

    public void connect() {
        System.out.println("connect");
    }
}

싱글톤 패턴의 장/단점

  • 장점
    ✔️ 고정된 메모리 영역을 얻으면서 하나의 객체만 사용하므로, 메모리 낭비를 방지
    ✔️ 싱글톤 객체의 인스턴스는 전역(Static)이므로 다른 인스턴스와 데이터를 공유할 수 있음
    ✔️ 처음 하나의 객체 생성 이후에는 객체를 그대로 가져다 사용하므로 성능이 좋아짐

  • 단점
    ✔️ 싱글톤 패턴을 구현하는 코드 작성이 많아짐
    ✔️ 의존관계상 클라이언트가 구체 클래스에 의존함 -> DIP 위반
    ✔️ 클라이언트가 구체 클래스에 의존함으로 OCP 위반할 가능성이 높음
    ✔️ 테스트 하기 어려움 - 싱글톤은 생성 방식이 제한적이기 때문에 Mock 객체로 대체하기가 어려우며, 동적으로 객체를 주입하기도 힘듦
    ✔️ 내부 속성을 변경하거나 초기화 하기 어려움
    ✔️ Private 생성자 이므로 자식 클래스를 만들기 어려움
    ✔️ 유연성이 떨어짐

💡 스프링에서는 스프링 컨테이너에서 객체 인스턴스를 싱글톤으로 관리하는데 위의 기존 자바에서의 싱글톤 문제점들을 해결하여 사용한다. -> 스프링 컨테이너(싱글톤 컨테이너)는 따로 글 작성(링크)

✅ 어댑터 패턴(Adapter)

✔️ 호환성이 없는 기존 클래스의 인터페이스를 변환하여 재사용 할 수 있도록 함
✔️ 실생활의 예시로는, 110v를 220v로 변환해주거나 또는 그 반대로 변환해주는 돼지코로 불리는 변환기가 있음
✔️ SOLID 원칙에서 개방폐쇄 원칙 (OCP)를 따름
✔️ 어댑터 패턴의 구성 요소

  • Target - 클라이언트가 사용하길 원하는 인터페이스
  • Adaptee - 클라이언트가 갖고 있는 인터페이스
  • Adapter - Target 인터페이스를 구현하는 클래스, Adaptee의 메소드 사용
  • Client - Target 인터페이스를 사용하는 주체

어댑터 패턴 장/단점

  • 장점
    ✔️ 기존 코드를 변경하지 않고 원하는 인터페이스 구현체를 만들어 사용할 수 있음 - 개방 폐쇄 원칙
    ✔️ 기존코드가 하던 일과 특정 인터페이스 구현체로 변환하는 작업을 각기 다른 클래스로 분리하여 관리할 수 있음 - 단일 책임 원칙에 가까움

  • 단점
    ✔️ 다수의 새로운 인터페이스와 클래스를 생성해야 하므로 추가 코드가 많아지고 복잡해질 수 있음
    ✔️ 코드의 성능이 개선되지는 않는다. 오히려 어댑터를 통해야 하므로 속도가 저하

✔️ 예제 코드

//Electronic110v - Target
//Electronic220v - Adaptee
//SocketAdapter - Adapter
public class SocketAdapter implements Electronic110v{
    private Electronic220v electronic220v;

    public SocketAdapter(Electronic220v electronic220v) {
        this.electronic220v = electronic220v;
    }

    @Override
    public void powerOn() {
        electronic220v.connect();
    }
}

//cleaner - Client
Cleaner cleaner = new Cleaner();
Electronic110v adapter = new SocketAdapter(cleaner);

✅ 프록시 패턴(Proxy)

✔️ Proxy란, 대리인 이라는 뜻으로 뭔가를 대신해서 처리하는 것
✔️ Proxy Class를 통해서 대신 전달하는 형태로 설계되며, 실제 Client는 Proxy로부터 결과를 받는 형식
✔️ Cache의 기능으로도 활용 가능함
✔️ SOLID 원칙 중에서 개방폐쇄 원칙(OCP)의존 역전 원칙(DIP)를 따름
✔️ 스프링에서는 AOP에서 프록시 패턴을 사용

✔️ 예제 코드

public class BrowserProxy implements IBrowser{

    private String url;
    private Html html;

    public BrowserProxy(String url) {
        this.url = url;
    }

    /**
     * 처음 객체가 없으면 생성하고, 이후부터는 기존 객체를 가져와 Cache처럼 사용
     * @return Html {@link Html}
     */
    @Override
    public Html show() {
        if(html == null) {
            html = new Html(url);
            System.out.println("BrowserProxy loading from : " + url);
        }

        System.out.println("BrowserProxy use cache html : " + url);
        return html;
    }
}

프록시 패턴 장/단점

  • 장점
    ✔️ 기존 객체를 수정하지 않고 일련의 로직을 프록시 패턴을 통해 추가 가능
    ✔️ 실제 객체를 수행하기 이전에 전처리를 하거나, 기존 객체를 캐싱할 수 있음
    ✔️ 클라이언트는 객체를 신경쓰지 않고, 서비스 객체를 제어하거나 생명 주기를 관리

  • 단점
    ✔️ 많은 프록시 클래스를 도입해야 하므로 코드의 복잡도가 증가
    ✔️ 프록시 클래스 자체에 들어가는 자원이 많다면 서비스로부터의 응답이 늦어질 수 있음
    ✔️ 객체 생성 시 프록시 클래스를 거치므로 객체를 빈번하게 생성 해야하는 경우 성능이 저하될 수 있음

✅ 데코레이터 패턴(Decorator)

✔️ 객체의 결합을 통해 기능을 동적으로 유연하게 확장 할 수 있게 해주는 패턴
✔️ 기존 뼈대 클래스는 유지하되, 이후 필요한 형태로 꾸밀 때 사용
✔️ 확장이 필요한 경우 상속 대신의 대안으로도 활용함
✔️ SOLID 원칙 중에서 개방폐쇄 원칙(OCP)의존 역전 원칙(DIP)를 따름
✔️ 실생활의 예시로는, 커피를 들 수 있다. 커피 원액(에소프레소)이 있고 여기서 물을 타면 아메리카노, 우유를 타면 카페라떼가 되는 것처럼 기본에서 다른 것들을 추가하여 확장하는 형식

✔️ 예제 코드

//Decorator 정의
public class AudiDecorator implements ICar{

    protected ICar audi;
    protected String modelName;
    protected int modelPrice;

    public AudiDecorator(ICar audi, String modelName, int modelPrice) {
        this.audi = audi;
        this.modelName = modelName;
        this.modelPrice = modelPrice;
    }

    @Override
    public int getPrice() {
        return audi.getPrice() + modelPrice;
    }

    @Override
    public void showPrice() {
        System.out.println(modelName + "의 가격은 " + getPrice() + "원 입니다.");
    }
}

//AudiDecorator를 확장하여 사용
public class A3 extends AudiDecorator{
    public A3(ICar audi, String modelName) {
        super(audi, modelName, 1000);
    }
}

데코레이터 패턴 장/단점

  • 장점
    ✔️ 데코레이터를 사용하면 서브클래스를 만들때보다 훨씬 더 유연하게 기능을 확장할 수 있다.
    ✔️ 객체를 여러 데코레이터로 래핑하여 여러 동작을 결합할 수 있다.
    ✔️ 컴파일 타임이 아닌 런타임에 동적으로 기능을 변경할 수 있다.
    ✔️ 각 장식자 클래스마다 고유의 책임을 가져 단일 책임 원칙(SRP)을 준수
    ✔️ 클라이언트 코드 수정없이 기능 확장이 필요하면 장식자 클래스를 추가하면 되니 개방 폐쇄 원칙(OCP) 준수
    ✔️구현체가 아닌 인터페이스를 바라봄으로써 의존 역전 원칙(DIP) 준수

  • 단점
    ✔️ 데코레이터 패턴을 사용하면 자잘한 객체들이 많이 추가될 수 있고, 데코레이터를 너무 많이 사용하면 코드가 필요 이상으로 복잡해 질 수 있음 - 가동성이 떨어짐

✅ 옵저버 패턴(Observer)

✔️ 변화가 일어났을 때, 미리 등록된 다른 클래스에 통보해주는 패턴
✔️ 보통 event listner가 옵저버 패턴을 사용함

✔️ 예제 코드

//Listener
public interface IButtonListener {
    void clickEvent(String event);
}

//Listener 등록
public class Button {
    private String name;
    private IButtonListener buttonListener;

    public Button(String name) {
        this.name = name;
    }

    public void click(String message) {
        buttonListener.clickEvent(message);
    }

    public void addListener(IButtonListener iButtonListener) {
        this.buttonListener = iButtonListener;
    }
}

//이벤트 발생 시 이벤트 전달
Button button = new Button("버튼");
        button.addListener(new IButtonListener() {
            @Override
            public void clickEvent(String event) {
                System.out.println(event);
            }
        });

        button.click("메시지 전달 : click 1");
        button.click("메시지 전달 : click 2");
        button.click("메시지 전달 : click 3");
        button.click("메시지 전달 : click 4");

옵저버 패턴 장/단점

  • 장점
    ✔️ Subject의 상태 변경을 주기적으로 조회하지 않고 자동으로 감지 가능
    ✔️ 발행자의 코드를 변경하지 않고도 새 구독자 클래스를 도입할 수 있어 개방 폐쇄 원칙(OCP) 준수
    ✔️ 상태를 변경하는 객체(Subject)와 변경을 감지하는 객체(Observer)의 결합도를 낮출 수 있음
  • 단점
    ✔️ 구독자는 알림 순서를 제어할수 없고, 무작위 순서로 알림을 받음
    ✔️ 옵저버 패턴을 자주 구성하면 구조와 동작을 알아보기 힘들어져 코드 복잡도가 증가
    ✔️ 다수의 옵저버 객체를 등록 이후 해지하지 않는다면 메모리 누수 발생 가능

✅ 퍼사드 패턴(Facade)

✔️ Facade는 건물의 정면이라는 뜻으로, 여러 개의 객체와 실제 사용하는 서브 객체 사이에 복잡한 의존관계가 있을 때, 중간에 Facade라는 객체를 두고, 여기서 제공하는 인터페이스를 활용하여 기능을 처리하는 방식
✔️ Facade는 자신이 가지고 있는 각 클래스의 기능을 명확하게 알아야 함
✔️ 클래스 라이브러리 같은 어떤 소프트웨어의 다른 커다란 코드 부분에 대한 간략화된 인터페이스를 제공

✔️ 예제 코드

//필요한 정보들을 받아서 퍼사드 객체에서 관련 메소드들을 일괄적으로 수행해준다.
public class SftpClient {

    private Ftp ftp;
    private Reader reader;
    private Writer writer;

    public SftpClient(Ftp ftp, Reader reader, Writer writer) {
        this.ftp = ftp;
        this.reader = reader;
        this.writer = writer;
    }

    public SftpClient(String host, int port, String path, String fileName) {
        this.ftp = new Ftp(host, port, path);
        this.reader = new Reader(fileName);
        this.writer = new Writer(fileName);
    }

    public void connect() {
        ftp.connect();
        ftp.moveDirectory();
        writer.fileConnect();
        reader.fileConnect();
    }

    public void disconnect() {
        writer.fileDisconnect();
        reader.fileDisconnect();
        ftp.disConnect();
    }

    public void read() {
        reader.fileRead();
    }

    public void write() {
        writer.write();
    }
}

퍼사드 패턴 장/단점

  • 장점
    ✔️ 서브 객체 간의 의존 관계가 많을 경우 이를 감소시키고 의존성을 한 곳으로 모을 수 있음
    ✔️ 복잡한 코드를 감춤으로써, 클라이언트가 시스템의 코드를 모르더라도 Facade 클래스만 이해하고 사용 가능
    ✔️ 클라이언트 입장에서 서브 객체를 사용해야 할때 다루어야 할 객체의 수를 줄여줌

  • 단점
    ✔️ 퍼사드 클래스 자체가 서브 객체에 대한 의존성을 가지게 되어 의존성을 완전히 제거할 수 없음
    ✔️ 서브 객체가 많아질수록 퍼사드 클래스의 복잡도가 증가함
    ✔️ 퍼사드 클래스가 모든 클래스에 결합된 God 객체가 될 수 있음

✅ 전략 패턴(Strategy)

✔️ 객체지향의 꽃으로 불리며, 유사한 행위들을 캡슐화하여, 객체의 행위를 바꾸고 싶은 경우 직접 변경하는 것이 아닌 전략만 변경하여 유연하게 확장할 수 있는 패턴
✔️ SOLID 원칙 중에서 개방폐쇄 원칙(OCP)의존 역전 원칙(DIP)를 따름

✔️ 예제 코드

//인코딩 전략 객체들을 위한 인터페이스
public interface EncodingStrategy {
    String encode(String text);
}

//Normal 인코더 - 텍스트 그대로 출력
public class NormalStrategy implements EncodingStrategy{
    @Override
    public String encode(String text) {
        return text;
    }
}

//Base64 인코더 - Base64로 텍스트 인코딩
public class Base64Strategy implements EncodingStrategy{
    @Override
    public String encode(String text) {
        return Base64.getEncoder().encodeToString(text.getBytes());
    }
}

//인코딩 객체를 사용하는 컨텍스트
public class Encoder {
    private EncodingStrategy encodingStrategy;

    public String getMessage(String message) {
        return this.encodingStrategy.encode(message);
    }

    public void setEncodingStrategy(EncodingStrategy encodingStrategy) {
        this.encodingStrategy = encodingStrategy;
    }
}

//인코딩 객체를 생성해서 컨텍스트에 주입하는 클라이언트
Encoder encoder = new Encoder();

        //base64
        EncodingStrategy base64 = new Base64Strategy();
        //normal
        EncodingStrategy normal = new NormalStrategy();

        String message = "hello java";
        encoder.setEncodingStrategy(base64);
        String base64Result = encoder.getMessage(message);
        System.out.println(base64Result);

        encoder.setEncodingStrategy(normal);
        String normalResult = encoder.getMessage(message);
        System.out.println(normalResult);

전략 패턴 장/단점

  • 장점
    ✔️ 컨텍스트(Context)의 코드 변경 없이 새로운 전략을 추가 할 수 있음 (if~else 분기 제거 가능)
    ✔️ 확장에 용이한 코드 작성 가능 - 새롭게 필요한 전략 객체를 쉽게 추가할 수 있음
    ✔️ 런타임시 전략을 변경할 수 있음
  • 단점
    ✔️ 클래스로 분리한 각 전략들이 어느 상황에 사용되어야 할 지 알고 있어야 함 -> 전략들이 많아 질수록 유지보수가 어려워질 수 있음
    ✔️ 전략 객체를 위한 인터페이스가 비효율적일 수 있음 -> 각 전략 객체에서는 필요하지 않은 메소드들 또한 정의해줘야 함

참고 Reference

profile
하루하루 꾸준히 포기하지 말고

0개의 댓글