다양한 디자인 패턴 - (1) 구조 패턴

귀엽다

구조 패턴?

 구조 패턴은 클래스나 객체 구조의 조합을 사용하여 더 큰 구조를 형성하는 방법을 다루는 패턴이다. 시스템이 복잡한 경우, 클래스나 객체의 조합을 상요하여 구조를 단순화하고 모듈성을 증가시키는 데 도움을 주고, 유연한 구조의 시스템을 만들어 코드의 재사용성을 높일 수 있다.

 

 

퍼사드(Facade) 패턴

 퍼사드 패턴은 복잡한 시스템 내의 일련의 서브시스템에 대한 간단한 인터페이스를 제공하여 시스템의 복잡성을 감소시키는 패턴이다. 시스템이 각 서브시스템에 직접 접근하는 대신, 퍼사드를 통해 통합된 인터페이스의 형태로 제공한다.

 

public static void treatFile() {
    String readContent = null;
    try (BufferedWriter writer = new BufferedWriter(new FileWriter(FILE_NAME))) {
        writer.write(FILE_CONTENT);
    } catch (IOException e) {
        e.printStackTrace();
    }
    try (BufferedReader reader = new BufferedReader(new FileReader(FILE_NAME))) {
        StringBuilder stringBuilder = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            stringBuilder.append(line);
            stringBuilder.append(System.lineSeparator());
        }
        readContent = stringBuilder.toString();
    } catch (IOException e) {
        e.printStackTrace();
    }
    System.out.println(readContent);
}

 

위의 treatFile() 메소드는 BufferedReader, BufferedWriter, StringBuilder와 같은 다양한 라이브러리를 사용하여 파일을 쓰고, 읽는 작업을 하고 있다. 이러한 다양한 라이브러리를 활용하기 위해서 객체들을 초기화하고, 올바른 순서로 메소드를 실행하는 등의 작업을 비즈니스 로직 내에서 수행해야 한다. 

public class FileFacade {
    public String readFile(String fileName) {
        try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
            StringBuilder stringBuilder = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                stringBuilder.append(line);
                stringBuilder.append(System.lineSeparator());
            }
            return stringBuilder.toString();
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    public void writeFile(String fileName, String content) {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) {
            writer.write(content);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
public static void treatFile() {
    FileFacade fileFacade = new FileFacade();
    fileFacade.writeFile(FILE_NAME, FILE_CONTENT);
    String readContent = fileFacade.readFile(FILE_NAME);
    System.out.println(readContent);
}

 

대신 FileFacade라는 클래스를 만들고, 퍼사드 클래스의 메소드를 통해서 기능을 제공하고 있다. 이렇게 퍼사드 패턴은 비슷한 역할이나 연관된 기능을 하는 여러개의 클래스나 메소드들을 하나의 단일 인터페이스로 묶어서 제공하는 디자인 패턴이다.

 

퍼사드 패턴은 다음과 같은 장점들을 제공한다

- 퍼사드를 통해서 비즈니스 코드는 서브시스템과 직접적으로 상호작용하지 않는다. 위 예시에서는 자바 라이브러리만을 사용했지만, 복잡한 외부 라이브러리를 다루어야 하는 경우에 비즈니스 코드는 외부 라이브러리와 직접 상호작용하지 않으므로 의존성이 최소화된다.

- 서브시스템의 변경이 발생해도 퍼사드만 수정하면 되므로 유지보수가 용이하다. 퍼사드를 사용하지 않았다면 인터페이스를 사용한 모든 클래스를 수정해야 할 것이다.

- 퍼사드를 통해서 단일 인터페이스를 사용하므로, 비즈니스 코드의 복잡성이 감소하고 가독성이 증가한다.

 

 

프록시 패턴, 데코레이터 패턴

일반적인 클라이언트-서버 구조에서 클라이언트는 서버쪽으로 요청을 보내고, 서버는 해당 요청을 받아 처리한다. 그런데 이러한 과정 중간에 클라이언트의 요청을 받아 특정한 일을 하는 프로세스가 추가될 수 있는데, 이렇게 특별한 프로세스를 처리하는 단계를 프록시(Proxy)라고 할 수 있다.

 

 

중요한 점은 클라이언트는 프록시의 존재는 모른다는 것이다. 중개 서버가 추가되는 것과의 차이라고 할 수 있는데, 클라이언트는 여전히 기존 서버로 요청을 보내고, 프록시의 존재 유무와 상관없이 동일한 결과가 반환되기를 기대한다. 다시 말하면 서버에서 처리하는 서비스 코드가 변경될 때, 프록시에 변경이 전이되면 안된다.

 

https://refactoring.guru/ko/design-patterns/proxy

 

따라서 클라이언트는 인터페이스에 의존하여 자신이 프록시에 의존하고 있는지, 서비스 로직에 의존하고 있는지를 모르게 하고, 서비스 코드와 프록시를 위한 코드는 모두 동일한 인터페이스에 의존하여 핵심 로직을 구현하도록 한다. 또한 프록시는 실제 객체(위 그림에서 Service)에 의존한다. 일반적으로 프록시 클래스가 로직 클래스를 감싸서 로직을 추가하는 형태로 구현한다.

 

이러한 디자인 패턴을 프록시 패턴이라고 하는데, 데코레이터 패턴도 동일한 구조를 갖는다. 프록시를 접근 제어(캐싱, 인가)를 목적으로 사용할 때는 프록시 패턴, 기능 추가(로깅)를 목적으로 할 때는 데코레이터 패턴이라고 부른다.

 

 

어댑터 패턴

 아주 유용한 라이브러리가 존재한다고 하자. 그런데 이 라이브러리는 특정 패턴의 데이터만을 받아들일 수 있다. 이 때 다양한 데이터들이 해당 라이브러리를 이용하기 위해서는 라이브러리가 지원하는 데이터로 변환하는 과정이 필요할 것이다. 이렇게 호환되지 않는 인터페이스들을 연결하는 디자인 패턴을 '어댑터 패턴'이라고 한다.

 

https://refactoring.guru/ko/design-patterns/adapter

 

자바에서 어댑터 패턴은 인터페이스 구현과 객체 합성을 이용하면 된다. 위 다이어그램에서 Adapter 클래스의 목표는 공통 프로토콜인 Client Interface를 통해서 Service클래스의 메서드를 호출하는 것이다. 따라서 중간의 Adapter 클래스는 Service클래스를 참조하고 있으며 동시에 Client Interface를 구현하고 있다.