모르지 않다는 것은 아는것과 다르다.

객체지향

디자인 패턴

채마스 2022. 2. 28. 20:05

디자인 패턴

  • 자주 사용하는 설계 패턴을 정형화 해서 이를 유형별로 가장 최적의 방법으로 개발할 수 있도록 도와준다.
  • 언어에 종속적이지 않고, 다양한 프레임워크에 적용시킬 수 있다.
  • 특정 영역에 종속적이지 않고 일반적으로 활용할 수 있다.
  • 좋은 설계에 도움이 된다.

 

Gof 디자인 패턴

  • 소프트웨어를 설계 할 때는 기존에 경험이 매우 중요하다. -> 그러나 모든 사람들이 다양한 경험을 가지고 있을 수는 없다.
  • 이러한 지식을 공유하기 위해서 나온 것이 GOF 의 디자인 패턴이다. 객체지향 개념에 따른 설계 중 재사용할 경우 유용한 설계를 디자인 패턴으로 정리 해둔 것이다.
  • Gof 의 디자인 패턴은 총 23개 이며, 이를 잘 이해하고 활용한다면, 경험이 부족하더라도 좋은 소프트웨어 설계가 가능하다.

 

디자인 패턴의 장점

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

 

디자인 패턴의 단점

  • 객체지향 설계/구현
  • 초기 투자 비용 부담

 

생성패턴

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

 

구조 패턴

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

 

행위 패턴

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

 

Singleton pattern

  • Singleton 패턴은 어떠한 클래스가 유일하게 1개만 존재할 때 사용한다.
  • 이를 주로 사용하는 곳은 서로 자원을 공유할 때 사용하는데, 실물 세계에서는 프린터가 해당되며, 실제 프로그래밍에서는 TCP Socket 통신에서 서버와 연결된 connect 객체에 주로 사용한다.
  • 코드 예시
public class ClazzA {

    private SocketClient socketClient;

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

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

}
public class ClazzB {

    private SocketClient socketClient;

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

    public SocketClient getSocketClient(){
        return this.socketClient;
    }
}
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으로 설정함으로써 외부에서 객체를 생성하지 못하도록 막아두었다.
  • getInstance() 메소드를 static으로 선언해서 외부에서 참조할 수 있도록 설정한다.
  • 마지막으로 외부에서 getInstance를 호출시 만약 SocketClient 자기 자신을 담는 필드인 socketClient 변수에 인스턴스가 저장되어 있다면 해당 인스턴스를 반환하며 null일 경우 객체를 생성하여 반환하도록 설정했다.
public class Main {

    public static void main(String[] args) {
        ClazzA clazzA = new ClazzA();
        ClazzB clazzB = new ClazzB();

        SocketClient ClientA = clazzA.getSocketClient();
        SocketClient ClientB = clazzB.getSocketClient();

        System.out.println(ClientA.equals(ClientB));
    }
}
  • 이렇게 하면 당연히 결과는 true가 나온다.
  • 그렇다면 싱글톤의 원칙을 깨는 코드는 어떤코드일까?
public class SocketClient {

    private static SocketClient socketClient = null;

    //생성은 막아준다.
    public SocketClient(){};

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

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

}
  • 위와 같이 생성자를 public으로 바꾸면 외부애서 객체를 생성할 수 있게된다.
public class ClazzA {

    private SocketClient socketClient;

    public ClazzA(){
        this.socketClient = new SocketClient();
    };

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

}
  • 위와 같이 생성자를 통해서 socketClient 변수에 객체를 할당한다.
public class ClazzB {

    private SocketClient socketClient;

    public ClazzB(){
        this.socketClient = new SocketClient();
    }

    public SocketClient getSocketClient(){
        return this.socketClient;
    }
}
  • ClazzB 도 마찬가지로 바꿔준다.
public class Main {

    public static void main(String[] args) {
        ClazzA clazzA = new ClazzA();
        ClazzB clazzB = new ClazzB();

        SocketClient ClientA = clazzA.getSocketClient();
        SocketClient ClientB = clazzB.getSocketClient();

        System.out.println(ClientA.equals(ClientB));
    }
}
  • 이렇게 되면 당연히 결과값은 false가 나온다.

 

Adapter pattern

  • Adapter는 실생활에서는 100v 를 220v로 변경하거나 , 그 반대로 해주는 흔히 돼지코 라고 불리는 변환기를 예로 들 수 있다. 호환성이 없는 기존 클래스의 인터페이스를 변환하여 재사용 할 수 있도록 한다.
  • SOLID중 개방폐쇠 원칙(OCP)을 따른다.
  • 코드 예시
public interface Electronic110V {
    void powerOn();
}
public interface Electronic220V {
    void connect();
}
public class AirConditioner implements Electronic220V{
    @Override
    public void connect() {
        System.out.println("에어컨 220V on");
    }
}
public class Cleaner implements Electronic220V{
    @Override
    public void connect() {
        System.out.println("청소기 220v on");
    }
}
public class HairDryer implements Electronic110V{
    @Override
    public void powerOn() {
        System.out.println("충전기 110v on");
    }
}
  • 위와 같이 에어컨(220v), 청소기(220v) , 헤어드라이어(110v) 3가지 가전 제품이 있고 각각 제품들은 110V와 220V 방식을 구현한다.
public class Main {

        //adapter pattern

        //드라이기 110V
        HairDryer hairDryer = new HairDryer();
        //청소기 220v
        Cleaner cleaner = new Cleaner();

        connect(hairDryer);
        //청소기는 220v를 구현하기 떄문에 연결되지 않는다.
        connect(cleaner); //에러발생!!!!!!!!

    }

    //콘센트
    public static void connect(Electronic110V electronic110V){
        electronic110V.powerOn();
    };
}
  • 위의 코드에서 connect 메소드는 110v만 받을 수 있지만 cleaner는 220v이다.
  • 위의 문제를 어댑퍼 패턴을 이용해서 사이에 interface로 해결할 수 있다.
  • 어댑터를 구현한 코드는 아래와 같다.
//220v를 110v로 변환해줄 adapter
public class SocketAdapter implements Electronic110V {

    private Electronic220V electronic220V;
    //220v를 받아서
    public SocketAdapter(Electronic220V electronic220V){
        this.electronic220V = electronic220V;
    }
    //110v에서 사용하게 해준다.
    @Override
    public void powerOn() {
        electronic220V.connect();
    }
}
  • 보는 것과 같이 Electronic110V 을 상속받으므로써 110와 연결할 수 있게 되었다.
  • 이제 아래 코드는 문제없이 돌아갈 것이다.
public class Main {

        //adapter pattern

        //드라이기 110V
        HairDryer hairDryer = new HairDryer();
        //청소기 220v
        Cleaner cleaner = new Cleaner();
        //에어컨 220v
        AirConditioner airConditioner = new AirConditioner();

        connect(hairDryer);
        //청소기는 220v를 구현하기 떄문에 연결되지 않는다.
        //connect(cleaner);

        //어뎁터에 청소기를 연결함
        //중간에 자기자신의 상태는 변환시키지 않고 인터페이스 형식을
        //맞추는 형식을 adapter pattern 이라 한다.
        Electronic110V adapter = new SocketAdapter(cleaner);
        //연결이 된다.
        connect(adapter);

        Electronic110V airAdapter = new SocketAdapter(airConditioner);

        connect(airAdapter);

    }

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

 

Proxy pattern

  • Proxy는 대리인 이라는 뜻으로써, 뭔가를 대신해서 처리하는 것
  • Proxy Class를 통해서 대신 전달하는 형태로 설계되며, 실제 Client는 Proxy 로 부터 결과를 받는다.
  • Cache의 기능으로도 활용이 가능하다.
  • SOLID 중에서 개방폐쇄 원칙 (OCP)과 의존 역전 원칙 (DIP) 를 따른다.
  • 또한 AOP에서도 프록시 패턴을 이용한다.
  • 코드 예시 (AOP)
public class AopBrowser implements IBrowser {

    private String url;
    private Html html;
    private Runnable before;
    private Runnable after;

    public AopBrowser(String url, Runnable before, Runnable after) {
        this.url = url;
        this.before = before;
        this.after = after;
    }

    @Override
    public Html show() {

        before.run();

        if(html == null) {
            html = new Html(url);
            System.out.println("AopBrowser html loading from " + url);
            try {
                Thread.sleep(1500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        after.run();

        System.out.println("AopBrowser html cache " + url);
        return html;
    }
}
  • 위의 코드처럼 객체가 캐시에 없다면 객체를 새로 생성해준다.
  • 생성되는데 1.5초가 걸리다고 가정해보자 -> Thread.sleep(1500);
  • 캐시에 객체가 이미 있다면 객체를 새로 생성하지말고 캐시에 값(프록시)을 가져다쓴다.
public class Main {

    public static void main(String[] args) {

        // 시간 체크, 동시성 문제로 AtomicLong을 사용
        AtomicLong start = new AtomicLong();
        AtomicLong end = new AtomicLong();

        IBrowser aopBrowser = new AopBrowser("www.naver.com",
                ()->{
                    System.out.println("before");
                    start.set(System.currentTimeMillis());
                },
                ()->{
                    long now = System.currentTimeMillis();
                    end.set(now - start.get());
                });
        aopBrowser.show();
        System.out.println("loading time : " + end.get());

        aopBrowser.show();
        System.out.println("loading time : " + end.get());
    }
}
  • 첫 번째 aopBrowser.show(); 에서는 객체를 새로 생성한다. -> Thread.sleep(1500); 때문에 시간이 오래걸린다.
  • 두 번째 aopBrowser.show(); 에서는 프록시로 가져오기 때문에 빠르다.

 

Decorator pattern

  • 데코레이터 패턴은 기존 뼈대는 유지하되, 이후 필요한 형태로 꾸밀 때 사용한다. 확장이 필요한 경우 상속의 대안으로도 활용한다.
  • SOLID 중에서 개방폐쇄 원칙 (OCP)과 의존 역전 원칙(DIP)를 따른다.
  • 코드 예시
  • public interface ICar { int getPrice(); void showPrice(); }
  • ICar 인터페이스는 가격을 리턴하는 getPrice() 와 가겨을 출력하는 showPrice() 두 메서드를 가지고 있다.
public class Audi implements ICar{

    private int price;

    public Audi(int price){
        this.price = price;
    }

    @Override
    public int getPrice() {
        return price;
    }

    @Override
    public void showPrice() {
        System.out.println("audi의 가격은 " + this.price+"원 입니다.") ;
    }
}
  • Audi 클래스는 ICar를 구현하고 필드로 Int 타입의 price를 갖는다.
  • 이제 가장 중요한 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()+"원 입니다.");
    }
}
  • 위 코드를 ICar 인터페이스를 구현하고 있다.
  • AudiDecorator를 활용해 아래와 같이 여러가지 모델을 만들어낼 수 있으며 만들어지는 클래스들은 AudiDecorator를 상속받아 구현되며 필요에 따라 AudiDecorator의 메소드인 getPrice와 showPrice를 Overriding할 수 있다 .
public class A3 extends AudiDecorator{
    public A3(ICar audi, String modelName) {
        super(audi, modelName,1000);
    }
}
public class A4 extends AudiDecorator{
    public A4(ICar audi, String modelName) {
        super(audi, modelName,2000);
    }
}
public class A5 extends AudiDecorator{
    public A5(ICar audi, String modelName) {
        super(audi, modelName,3000);
    }
}
  • 위의 코드처럼 super를 통해서 AudiDecorator 의 생성자를 사용해서 구현할 수 있다.
public class Main {

    //decorator pattern

    ICar audi = new Audi(1000);
    audi.showPrice();


    //a3
    ICar audi3 = new A3(audi,"A3");
    audi3.showPrice();

    //a4
    ICar audi4 = new A4(audi,"a4");
    audi4.showPrice();

    //a5
    ICar audi5 = new A5(audi,"a5");
    audi5.showPrice();
}

 

Observer pattern

  • 관찰자 패턴은 변화가 일어 났을 때, 미리 등록된 다른 클래스에 통보해주는 패턴을 구현한 것이다.
  • 많이 보이는 곳은 event listener 에서 해당 패턴을 사용하고 있다.
  • 코드 예시
public interface IButtonListener {
    void clickEvent(String event);
}
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 buttonListener){
        this.buttonListener = buttonListener;
    }
}
  • 위와 같이 버튼 클래스를 만들어준다.
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("메시지전달: click1");
        button.click("메시지전달: click2");
        button.click("메시지전달: click3");
        button.click("메시지전달: click4");
}
  • 버튼 클릭시 이벤트가 작동할 수 있도록 버튼에 리스너를 달아준다. 리스너를 등록하면 button 클래스 필드에 할당 되고
    click 메서드 실행시 리스너로 메세지가 전달되고 출력되는 형태이다

 

Facade pattern

  • Facade는 건물의 앞쪽 정면이라는 뜻을 가진다. 여러 개의 객체와 실제 사용하는 서브 객체의 사이에 복잡한 의존관계가 있을 때, 중간에 facade라는 객체를 두고, 여기서 제공하는 interface만을 활용하여 기능을 사용하는 방식이다. Facade는 자신이 가지고 있는 각 클래스의 기능을 명확히 알야야 한다.
  • 코드 예시
pulic class Ftp{

    private String host;
    private int port;
    private String path;

    public Ftp(String host, int port, String path){
        this.host = host;
        this.port = port;
        this.path = path;

        public connect(){
            System.out.println("connect");
        }
        public moveDirectory(){
            System.out.println("moveDirectory");
        }
        public disConnect(){
            System.out.println("disConnect");
        }
    }
}
public class Reader {
    private String fileName;

    public Reader(String fileName){
        this.fileName = fileName;
    }

    public void fileConnect(){
        String msg = String.format("Reader %s 로 연결 합니다.", fileName);
        System.out.println(msg);
    }

    public void fileRead(){
        String msg = String.format("Reader %s 의 내용을 읽어 옵니다.", fileName);
        System.out.println(msg);
    }

    public void fileDisconnect(){
        String msg = String.format("Reader %s 로 연결 종료 합니다.", fileName);
        System.out.println(msg);
    }
}
public class Writer {
    private String fileName;

    public Writer(String fileName){
        this.fileName = fileName;
    }

    public void fileConnect(){
        String msg = String.format("Reader %s 로 연결 합니다.", fileName);
        System.out.println(msg);
    }

    public void fileWrite(){
        String msg = String.format("Reader %s 의 내용을 씁니다.", fileName);
        System.out.println(msg);
    }

    public void fileDisconnect(){
        String msg = String.format("Reader %s 로 연결 종료 합니다.", fileName);
        System.out.println(msg);
    }
}
  • 먼저 Facade pattern 을 적용시키지 않았을 경우 아래와 같이 호출 할 수 있다.
public static void main(String[] args) {
    Ftp ftpClient = new Ftp("www.hyunwook.dev", 22, "/home/etc");
    ftpClient.connect(); //연결
    ftpClient.moveDirectory(); //디렉토리로 이동

    Writer writer = new Writer("text.tmp");
    writer.fileConnect();
    writer.filreWrite();

    Reader reader = new Reader("text.tmp");
    reader.fileConnect();
    reader.fileRead();

    reader.fileDisconnect();
    writer.fileDisconnect();
    fcpClient.disConnect();
}
  • 이제 Facade pattern 을 적용시켜 보겠다.
public class FacadeClient {

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

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

    public FacadeClient(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.fileWrite();
    }
}
public static void main(String[] args) {
    FacadeClient facadeClient = new FacadeClient("www.hyunwook.dev", 22, "/home/etc", "text.tmp");
    facadeClient.connect();
    facadeClient.write();
    facadeClient.read();
    facadeClient.disConnect();
}
  • 이렇게 구현하면 Ftp, Reader, Writer 객체는 Facade 객체만을 바라보게 구현할 수 있다.

 

Strategy pattern

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

    @Override
    public String encode(String text){
        return text;
    }
}
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;
    }

}
public static void main(String[] args) {
    Encoder encoder = new Encoder();

    //base64
    EncodingStrategy base64 = new Base64Strategy();

    // normal
    EncodingStrategy normal = new NormalStrategy();

    String message = "hello hyunwook";

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

    encoder.setEncodingStrategy(normal);
    String normalResult = encoder.getMessage(message);
    System.out.println(normalResult);
}
  • encoder 객체는 변하지 않고 전략의 주입을 통해서 결과가 달라지도록 할 수 있다.

 

 

REFERENCES

'객체지향' 카테고리의 다른 글

디자인 패턴에 함수형 프로그래밍 적용하기  (0) 2022.03.06
다형성  (0) 2022.02.28
SOLID  (0) 2022.02.28
POJO  (0) 2022.02.28
refactoring  (0) 2022.02.28