[Java] 디자인 패턴 - 생성, 구조, 행위 패턴

희원·2022년 1월 20일
0
post-custom-banner

디자인 패턴

자주 사용하는 설계 패턴을 정형화해서 이를 유형별로 가장 최적의 방법으로 개발할 수 있도록 정해둔 설계

Gof(Gang of Four) 디자인 패턴

객체지향 개념에 따른 설계 중 재사용할 경우 유용한 설계를 디자인 패턴으로 정리해둔 것이다.
Gof 디자인 패턴을 잘 이해하고 활용한다면 좋은 소프트웨어 설계가 가능하다.

장점

  • 개발자와 설계자 간의 원활한 소통이 가능하다.
  • 소프트웨어의 구조 파악이 용이하다.
  • 재사용을 통해 개발 시간이 단축된다.
  • 설계 변경, 수정 요청에 대한 유연한 대처가 가능하다.

단점

  • 객체지향의 개념을 알고 있어야 하며, 객체지향적인 설계와 구현을 해야한다.
  • 초기 투자 비용이 부담될 수 있다.

1. 생성 패턴

객체를 생성하는 것과 관련된 패턴으로, 객체의 생성과 변경이 전체 시스템에 미치는 영향을 최소화하고 코드의 유연성을 높여준다.
Singleton Factory Method Prototype Builder Abstart Factory Chaining

1.1. 싱글톤 패턴 (Singleton pattern)

  • 싱글톤 패턴은 어떠한 객체가 유일하게 1개만 존재할 때 사용한다.
  • 서로 자원을 공유할 때 주로 사용하게 되는데, 예를 들면 서버와 연결된 connect 객체가 있다.
  • getInstance() 메서드를 통해서 객체를 생성하거나 생성된 객체를 얻을 수 있다.

싱글톤 패턴 만드는 방법

  1. 클래스 내부에 private static 멤버변수 instance를 선언한다. data type은 반드시 해당 클래스와 같아야 한다.
  2. 클래스의 default constructor(기본생성자)를 private로 선언한다. 즉, 클래스 내부에서만 객체 생성이 가능하며 외부 클래스에서는 해당 클래스의 객체화가 불가능하게 만든다.
  3. 멤버변수 instance의 getter를 작성한다. 이로 인해 외부 클래스에서는 getter를 통해서만 class type 객체의 사용이 가능해진다.

SocketClient.java

package com.company.designpattern.singleton;

public class SocketClient {

    private static SocketClient socketClient = null;
    
    private SocketClient() {
    }

    public static SocketClient getInstance() {
        if (socketClient == null) {
            socketClient = new SocketClient();
        }
        return socketClient;
    }

    public void connect() {
        System.out.println("connect");
    }
}
  • 싱글톤 객체는 자기 자신의 객체를 가지고 있어야 한다.
  • 기본 생성자를 통해서 생성할 수 없도록 private 으로 선언하여 외부로부터 감춘다.

SocketA.java

package com.company.designpattern.singleton;

public class SocketA {

    private SocketClient socketClient;

    public SocketA() {
        this.socketClient = SocketClient.getInstance();
    }

    public SocketClient getSocketClient() {
        return this.socketClient;
    }
}

SocketB.java

package com.company.designpattern.singleton;

public class SocketB {

    private SocketClient socketClient;

    public SocketB() {
        this.socketClient = SocketClient.getInstance();
    }

    public SocketClient getSocketClient() {
        return this.socketClient;
    }
}
  • SocketA와 SocketB는 SocketClient 객체를 받아오는 동일한 코드를 가진다.
  • 이처럼 여러 객체에서 싱글톤 객체를 생성할 경우 모두 동일한 객체를 공유하게 된다.
  • 반면에 new 예약어를 통해 생성하는 객체는 생성될 때마다 새로운 객체를 얻는다.

2. 구조 패턴

프로그램 내의 자료구조나 인터페이스 구조 등 프로그램 구조를 설계하는 데 활용할 수 있는 패턴이다.
클래스, 객체들의 구성을 통해 더 큰 구조로의 확장을 용이하게 해준다.
서로 의존관계를 가지는 클래스 간의 복잡한 구조를 개발하기 쉽게, 유지보수하기 쉽게 만들어 준다.
Adapter Decorator Proxy Facade Composite Flyweight Bridge

2.1. 어댑터 패턴 (Adaptor pattern)

  • 어댑터는 흔히 110V를 220V로 또는 반대로 변환해주는 변환기를 예로 들 수 있다.
  • 호환성이 없는 기존 클래스의 인터페이스를 변환하여 재사용 할 수 있도록 한다.
  • SOLID 중에서 개발 폐쇄 원칙(OCP) 을 따른다.
  • Electronic110V 인터페이스와 이를 구현하는 HairDryer 클래스,
    Electronic220V 인터페이스와 이를 구현하는 AirConditioner, Cleaner 클래스가 있다.

  • 220V를 사용하는 Cleaner 클래스는 110V 객체를 매개변수로 받는 connect 메서드를 사용할 수 없다.
  • 220V 객체도 110V 객체 메서드를 사용할 수 있도록 변환기 역할의 어댑터 클래스 를 만들어 보자.

SocketAdaptor.java

package com.company.designpattern.adaptor;

public class SocketAdaptor implements Electronic110V {

    private Electronic220V electronic220V;

    public SocketAdaptor(Electronic220V electronic220V) {
        this.electronic220V = electronic220V;
    }

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

Main.java

package com.company.designpattern;

import com.company.designpattern.adaptor.Cleaner;
import com.company.designpattern.adaptor.Electronic110V;
import com.company.designpattern.adaptor.HairDryer;
import com.company.designpattern.adaptor.SocketAdaptor;

public class Main {

    public static void main(String[] args) {

        HairDryer hairDryer = new HairDryer();
        connect(hairDryer);

        Cleaner cleaner = new Cleaner();
        Electronic110V adaptor = new SocketAdaptor(cleaner);
        connect(adaptor);
    }

    // 콘센트
    public static void connect(Electronic110V electronic110V) {
        electronic110V.powerOn();
    }
}

해당 객체와 타입이 다른 인터페이스를 구현하는 어댑터 객체를 통해
자기 자신의 형태는 변환시키지 않고 그 인터페이스의 속성을 활용할 수 있다.

2.2. 데코레이터 패턴 (Decorator pattern)

  • 데코레이터 패턴은 기존 뼈대는 유지하되, 이후 필요한 형태로 꾸밀 때 사용한다.
  • 상속보다 유연한 구현 방식으로, 확장이 필요한 경우 상속의 대안으로도 활용된다.
  • SOLID 중에서 개발 폐쇄 원칙(OCP) 과 의존 역전 원칙(DIP) 를 따른다.
  • 예를 들어, 아메리카노에 우유를 추가하면 카페 라떼, 라떼에 모카 시럽을 추가하면 모카 라떼 가 되는 커피 프로그램이라면 데코레이터 패턴을 활용할 수 있다.
    여기서 아메리카노는 컴포넌트고, 우유, 시럽은 데코레이터 이다.

Coffee.java

아메리카노 컴포넌트

public abstract class Coffee {
	public abstract void brewing();
}

Decorator.java

public abstract class Decorator extends Coffee{

	Coffee coffee;
	public Decorator(Coffee coffee){
		this.coffee = coffee;
	}
	
	@Override
	public void brewing() {
		coffee.brewing();
	}
}

Latte.java

우유 데코레이터

public class Latte extends Decorator{

	public Latte(Coffee coffee) {
		super(coffee);
	}
  
	public void brewing() {
		super.brewing();
		System.out.print("Adding Milk ");
	}
}

Mocha.java

시럽 데코레이터

public class Mocha extends Decorator{

	public Mocha(Coffee coffee) {
		super(coffee);
	}

	public void brewing() {
		super.brewing();
		System.out.print("Adding Mocha Syrup ");
	}
}
  • 데코레이터 패턴은 지속적인 기능의 추가 또는 제거에 용이하다.
    만약 다른 메뉴가 새로 생기더라도 간단하게 추가할 수 있다.

WhippedCream.java

새로운 메뉴 추가 휘핑 크림 데코레이터

public class WhippedCream extends Decorator{

	public WhippedCream(Coffee coffee) {
		super(coffee);
	}

	public void brewing() {
		super.brewing();
		System.out.print("Adding WhippedCream ");
	}
}

2.3. 프록시 패턴 (Proxy pattern)

  • Proxy는 대리인 이라는 뜻을 가지며, 뭔가를 대신해서 처리하는 것을 의미한다.
  • 즉 Proxy Class를 통해 대신 전달하는 형태로 설계되며, 실제 Client는 Proxy로부터 결과를 받는다.
  • SOLID 중에서 개방 폐쇄 원칙(OCP) 과 의존 역전 원칙(DIP) 을 따른다.

  • Cache의 기능으로도 활용이 가능하다.

    인터넷 통신에서 이미 받아둔 결과가 있다면 그 결과를 그대로 내려주거나,
    서버에서도 자주 변경되지 않는 데이터 같은 경우 메모리에 캐싱을 해놨다가 같은 요청이 들어왔을 때 결과를 내려주는 형태로 사용된다.

2.3.1. 프록시 패턴 적용 전

Browser 인터페이스는 html을 보여주는 역할을 하고, 구현체인 ChromeBrowser는 특정 url의 html 객체를 로딩한다.

Browser.java

package com.company.designpattern.proxy;

public interface Browser {
    Html show();
}

ChromeBrowser.java

package com.company.designpattern.proxy;

public class ChromeBrowser implements Browser{

    private String url;

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

    @Override
    public Html show() {
        System.out.println("ChromeBrowser loading html from : " + url);
        return new Html(url);
    }
}
  • 같은 url을 입력받은 객체임에도 show() 메서드를 호출할 때마다 로딩이 되고 있다.

여러번 같은 url을 요청받을 경우 이미 받아둔 결과를 돌려주는 ProxyBrowser를 만들어 보자.

2.3.2. 프록시 패턴 적용 후

ProxyBrowser.java

package com.company.designpattern.proxy;

public class ProxyBrowser implements Browser{

    private String url;
    private Html html;

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

    @Override
    public Html show() {
        if (html == null) {
            this.html = new Html(url);
            System.out.println("ProxyBrowser loading html from : " + url);
        } else {
            System.out.println("ProxyBrowser use cache html : " + url);
        }
        return html;
    }
}

imageimage

2.4. 파사드 패턴 (Facade pattern)

  • Facade는 건물의 앞쪽 정면 이라는 뜻으로, 건물의 앞쪽만 보이고 뒤쪽은 뭐가 있는지 모르는 형태이다.
  • 여러 개의 객체와 실제 사용하는 서브 객체 사이에 복잡한 의존관계가 있을 때, 중간에 facade라는 객체를 두고 여기서 제공하는 인터페이스만을 활용하여 기능을 사용하는 방식이다.
  • Facade는 자신이 가지고 있는 각 클래스의 기능을 명확히 알아야 한다.

  • FTP(파일 전송 프로토콜)을 통해 파일을 쓰고 읽어오는 예제

FtpProtocol.java

package com.company.design.facade;

public class FtpProtocol {

    public FtpProtocol(String host, int port, String path){
        System.out.println("ftp server create");
    }


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

    public void moveDirectory(){
        System.out.println("move path");
    }

    public void disConnect(){
        System.out.println("ftp server disConnected");
    }
}

FileWriter.java

package com.company.design.facade;

public class FileWriter {

    public FileWriter(String fileName){

    }

    public void fileConnect(){
        System.out.println("FileWriter Connected");
    }

    public void fileWrite(String content){
        System.out.println("write : "+content);
    }

    public void fileDisconnect(){
        System.out.println("FileWriter disConnected");
    }
}

FileReader.java

package com.company.design.facade;

public class FileReader {

    public FileReader(String fileName){

    }

    public void fileConnect(){
        System.out.println("FileReader Connected");
    }

    public String fileRead(){
        return "content";
    }

    public void fileDisconnect(){
        System.out.println("FileReader disConnected");
    }
}

파일을 써서 보내고 읽는 작업을 실행한다고 할 때, 다음과 같은 과정으로 진행하게 된다.
1. FTP와 연결하고 디렉토리 이동한다.
2. FileWriter와 연결하고 파일을 쓴다.
3. FileReader와 연결하고 파일을 읽는다.
4. FTP, FileWriter, FileReader와 연결을 끊는다.

이 클래스들의 구조 사이에 모든 의존관계를 포함하는 Facade 객체를 둠으로써 복잡한 구조를 해결할 수 있다.

SftpClient.java

package com.company.design.facade;

public class SftpClient {

    private FtpProtocol ftpProtocol;
    private FileReader fileReader;
    private FileWriter fileWriter;

    public SftpClient(String host, int port, String path, String fileName){
        this.ftpProtocol = new FtpProtocol(host, port, path);
        this.fileReader = new FileReader(fileName);
        this.fileWriter = new FileWriter(fileName);
    }

    public void connect(){
        this.ftpProtocol.connect();
        this.ftpProtocol.moveDirectory();
        this.fileReader.fileConnect();
        this.fileWriter.fileConnect();

    }

    public void write(String content){
        this.fileWriter.fileWrite(content);
    }

    public String read(){
        return this.fileReader.fileRead();
    }

    public void disConnect(){
        this.fileReader.fileDisconnect();
        this.fileWriter.fileDisconnect();
        this.ftpProtocol.disConnect();
    }

}

3. 행위 패턴

반복적으로 사용되는 객체들의 상호작용을 패턴화한 것. 클래스나 객체들이 상호작용하는 방법과 책임을 분산하는 방법을 제공한다.
행위 패턴은 행위 관련 패턴을 사용하여 독립적으로 일을 처리하고자 할 때 사용한다.
Observer Strategy Template Method Interpreter Iterator Visitor Chain of responsibility Command Mediator State Memento

3.1. 옵저버 패턴 (Observer pattern)

  • 관찰자 패턴은 변화가 일어났을 때, 미리 등록된 다른 클래스에 통보해주는 패턴을 구현한 것이다.
  • 특정 이벤트에 대해 리스너를 통해 전달하는 event listener에서 해당 패턴을 사용하고 있다.

  • 버튼을 누르면 클릭 이벤트가 발생하는 예제
    옵저버 패턴은 다음과 같이 버튼을 클릭하는 이벤트가 일어났을 때, 리스너를 통해서 이벤트를 전달해주는 형태를 가진다.

MyButton.java

package com.company.design.observer;

public class MyButton {
    private String name;
    private IButtonClickListener buttonClickListener;

    public MyButton(String buttonName){
        this.name = buttonName;
    }

    public void click(String clickEvent){
        buttonClickListener.clickEvent(this.name+", "+clickEvent);
    }

    public void addListener(IButtonClickListener buttonClickListener){
        this.buttonClickListener = buttonClickListener;
    }
}

IButtonClickListener.java

package com.company.design.observer;

public interface IButtonClickListener {
    void clickEvent(String event);
}

Main.java

public class Main {

    public static void main(String[] args) {
        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");
    }
}

image

3.2. 전략 패턴 (Strategy pattern)

  • 전략 패턴으로 불리며, 객체지향의 꽃이다.
  • 유사한 행위들을 캡슐화하여 객체의 행위를 바꾸고 싶은 경우 직접 변경하지 않고, 전략만 변경하여 유연하게 확장하는 패턴이다.
  • SOLID 중에서 개방 폐쇄 원칙(OCP) 과 의존 역전 원칙(DIP) 를 따른다.
  • 전략 메서드를 가진 전략 객체(Normal Strategy, Base64 Strategy)
    전략 객체를 사용하는 컨텍스트 (Encoder)
    전략 객체를 생성해 컨텍스트에 주입하는 클라이언트

profile
모든 시작은 사소함으로부터
post-custom-banner

0개의 댓글