본문 바로가기

JAVA

디자인 패턴

디자인패턴이란?

디자인 패턴은 모듈의 세분화된 역할이나 모듈들 간의 인터페이스 구현 방식을 설계할때 참조할 수 있는 전형적인 해결 방식이다.

디자인 패턴을 통해 설계 문제, 해결 방법, 해결 방법언제 적용해야 할지, 그 결과는 무엇인지 등을 알 수 있다.

또한 디자인 패턴은 한 패턴에 변형을 가하거나 어떠한 요구사항을 반영하면 다른 패턴으로 변형되는 특징이 있다.

확장과 수정에 용이하여 설계 이후에도 추가적인 유지 보수에 비용이 적게들어가는 코드를 설계하는 것이 목표이다.

객체지향 방법론 에서 좋은 코드를 설계하기 위해 지켜야 하는 원칙이 SOLID 원칙이고, 이러한 원칙에 기반해서 좀 더 구체적인 코드로 분류한 것이 디자인 패턴이다.

✔ 아키텍쳐 패턴디자인 패턴
아키텍쳐 패턴은 주어진 상황의 소프트웨어 구조에서 발생하는 문제점을 해결하기 위한 일반화된 재사용 가능한 솔루션이다. 디자인 패턴보다 큰 범주이며 계층화 패턴, 클라이언트-서버 패턴, 브로커 패턴, P2P 패턴 등이 속한다.
아키텍쳐 패턴이 전체적인 시스템의 구조를 설계하기 위한 참조 모델이라면,
디자인 패턴은 서브시스템에 속하는 컴포넌트들끼리의 관계를 설계하기 위한 참조 모델이다.

GoF 디자인 패턴

Gof 디자인 패턴은 크게 3가지로 분류 가능하다. 사용하는 목적에 따라서 생성, 구조, 행동으로 나눌 수 있다.

객체는 객체를 생성하는데에 관련된 패턴들이고, 구조는 프로그램 구조에 관련된 패턴들이다. 마지막으로 행위는 객체 사이에 알고리즘이나 책임 분배에 관련한 패턴이다.

이렇게 한번 나누어진 패턴은 scope에 따라서 분류할 수 있다. 주로 적용되는 대상이 Class인지, Object인지에 따라서 나뉜다.

Class 패턴은 클래스와 서브클래스 간의 관련성을 다루고 주로 상속을 통해 연관되며, 컴파일 타임에 정적으로 결정된다.

Object 패턴객체 간의 관련성을 다루고, 런타임에 변경될 수 있는 동적인 성격을 가진다.

1. 생성(Creational)

객체 생성(인스턴스화)와 관련된 패턴들이다.

객체의 인스턴스 과정을 추상화하는 패턴으로 특정 개체가 생성되거나 변경되어도 프로그램 구조에 영향을 받지 않도록 유연성을 제공한다.

객체를 생성하는 일부를 서브클래스가 담당하도록 하며, 객체 생성을 다른 객체에게 위임한다.

패턴 설명
팩토리 메소드(Factory Method) 부모 클래스에 알려지지 않은 구체 클래스를 생성하는 패턴.
자식 클래스가 어떤 객체를 생성할지를 결정함.
추상 팩토리(Abstract Factory)  서로 관련이 있는 객체들을 통째로 묶어서 팩토리 클래스로 만들고, 이들 팩토리를 조건에 따라 생성하도록 다시 팩토리를 만들어서 객체를 생성하는 패턴
빌더(Builder) 불필요한 생성자를 만들지 않고 데이터의 순서에 상관 없이 객체를 만들어 내며, 사용자가 봤을때 명시적이고 이해할 수 있는 패턴
프로토타입(Prototype) 원형이 되는 인스턴스로 새로운 인스턴스를 만드는 방식, 즉 객체에 의해 생성될 객체의 타입이 결정되는 패턴
싱글톤(Singleton) 전역 변수를 사용하지 않고 객체를 하나만 생성하도록 하며, 생성된 객체를 어디에서든지 참조할 수 있도록 하는 패턴

2. 구조(Structural)

프로그램 구조에 관련된 패턴들이다. 프로그램 내의 자료구조나 인터페이스 구조 등 프로그램의 구조를 설계하며 클래스나 객체를 조합하여 더 큰 구조를 만들 때 활용한다.

구조 Class 패턴은 상속을 통해 클래스나 인터페이스를 합성하고,

구조 Object 패턴은 객체를 합성하는 방법을 정의한다.

패턴 설명
어댑터(Adapter) - Class / Object 한 클래스의 인터페이스를 클라이언트에서 사용하고자하는 다른 인터페이스로 변환. 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있게 해주는 패턴
브리지(Bridge) 구현부에서 추상층을 분리하여 각자 독립적으로 변형할 수 있게 하는 패턴
컴포지트(Composite) 여러 개의 객체들로 구성된 복합 객체와 단일 객체를 클라이언트에서 구별 없이 다루게 해주는 패턴
데코레이터(Decorator) 객체의 결합을 통해 기능을 동적으로 유연하게 확장할 수 있게 해주는 패턴
퍼싸드(Facade) 어떤 서브시스템의 일련의 인터페이스에 대한 통합된 인터페이스를 제공하고, 고수준 인터페이스를 정의하기 때문에 서브시스템을 더 쉽게 사용할수 있다.
플라이웨이트(Flyweight)  비용이 큰 자원을 공통으로 사용할 수 있도록 만드는 패턴.
중복이 생성될 가능성이 높거나 생성 비용에 비해 사용 빈도가 낮은 경우 사용
프록시(Proxy) 실제 기능을 수행하는 객체 대신 가상의 객체를 사용해 로직의 흐름을 제어하는 패턴

3. 행위(Behavioral)

반복적으로 사용되는 객체들의 상호작용과 객체들 사이의 알고리즘 또는 책임분배에 관련된 패턴이다. 행위 패턴은 하나의 객체로 수행할 수 없는 작업을 여러 객체로 분배하면서 그들 간의 결합도를 최소화 할 수 있도록 도와준다. 행위 클래스 패턴은 상속을 통해 알고리즘과 제어 흐름을 기술하고, 행위 객체 해턴은 하나의 작업을 수행하기 위해 객체 집합이 어떻게 협력하는지를 기술한다.

패턴 설명
인터프리터(Interpreter) 언어가 주어지면 해당 표현을 사용하여 언어로 문장을 해석하는 인터프리터를 사용하여 문법 표현을 정의하는 패턴
템플릿 메소드(Template Method) 어떤 작업을 처리하는 일부분을 서브 클래스로 캡슐화해 전체 일을 수행하는 구조는 바꾸지 않으면서 특정 단계에서 수행하는 내역을 바꾸는 패턴
책임 연쇄(Chain of Responsibility) 요청을 처리할 수 있는 객체가 여러 개이고 처리객체가 특정적이지 않을 경우 사용하는 패턴
커맨드(Command) 실행될 기능을 캡슐화함으로써 주어진 여러 기능을 실행할 수 있는 재사용성이 높은 클래스를 설계하는 패턴
반복자(Iterator) 컬렉션 구현 방법을 노출시키지 않으면서도 그 집합체안에 들어있는 모든 항목에 접근할 수 있게 해 주는 방법을 제공해 주는 패턴
중재자(Mediator) 클래스 간의 복잡한 관계들을 캡슐화하여 하나의 클래스에서 관리하도록 처리하는 패턴. M:N 관계를 M:1 관계로 만들어 복잡도를 낮춤
메멘토(Memento) 객체의 상태 정보를 저장하고 사용자의 필요에 의하여 원하는 시점의 데이터를 복원 할 수 있는 패턴
옵저버(Observer)  한 객체의 상태 변화에 따라 다른 객체의 상태도 연동되도록 일대다 객체 의존 관계를 구성하는 패턴
상태(State)  객체의 상태에 따라 객체의 행위 내용을 변경해주는 패턴
전략(Strategy) 행위를 클래스로 캡슐화해 동적으로 행위를 자유롭게 바꿀 수 있게 해주는 패턴
방문자(Visitor) 실제 로직을 가지고 있는 객체(Visitor)가 로직을 적용할 객체(Element)를 방문하면서 실행하는 패턴

 


GoF 디자인 패턴의 종류

1-1. 팩토리 메서드

객체를 생성하기 위해 인터페이스를 정의하지만, 어떤 클래스의 인스턴스를 생성할지에 대한 결정은 서브클래스가 내리도록 합니다.

팩토리 메서드는 부모 클래스에 알려지지 않은 구체 클래스를 생성하는 패턴이며 팩토리 메서드의 자식 클래스가 어떤 객체를 생성할지를 결정한다. 부모 클래스 코드에 구체 클래스 이름을 감추기 위한 방법으로도 사용한다.

Factory Method가 중첩되기 시작하면 구조가 복잡해지고 부모 클래스를 확장하지 않기 때문에 이 패턴은 extends 관계를 잘못 이용한 것으로 볼 수 있다. extends 관계를 남발하게 되면 프로그램의 엔트로피가 높아질 수 있으므로 Factory Method 패턴의 사용을 주의해야 한다.

구현 시 고려할 점

팩토리 메소드 패턴의 구현 방법

  • Factory가 구체 클래스이고, 팩토리 메소드의 기본 구현을 제공하는 방법.

팩토리 메소드의 인자를 통해 다양한 클래스들(Book을 implements한 class)을 생성하게 한다.

  • 팩토리 메소드에 잘못된 인자가 들어올 경우의 런타임 에러 처리를 해야 함
  • enum 등을 사용하면 해결할 수 있음

예제

    abstract class Book {
        public abstract void read();
    }

    class LiteratureBook extends Book {
        public LiteratureBook() {
            System.out.println("문학소설을 구매합니다.");
        }

        @Override
        public void read() {
            System.out.println("문학소설을 읽습니다.");
        }
    }

    class MysteryBook extends Book {
        public MysteryBook() {
            System.out.println("추리소설을 구매합니다.");
        }

        @Override
        public void read() {
            System.out.println("추리소설을 읽습니다.");
        }
    }

    class EducationBook extends Book {
        public EducationBook() {
            System.out.println("교육서적을 구매합니다.");
        }

        @Override
        public void read() {
            System.out.println("교육서적을 읽습니다.");
        }
    }

다음 코드는 Book 인터페이스와, 이를 implements 한 Literature 클래스이다.

객체 생성은 자식클래스를 이용하는 직접적으로 생성하지 않고 팩토리 메서드를 이용해서 생성한다.

    enum BookType {
        LITERATURE, MYSTERY, EDUCATION
    }

잘못된 인자가 입력되는 것을 막기 위해서 enum 타입을 정의한 후 사용한다.

 

예시 코드

Factory 클래스

    public class BookFactory {
        public Book buy(BookType bookType) {            
            if (bookType == null) {
                return null;
            }

            if (bookType == BookType.LITERATURE) {
                return new LiteratureBook();
            }else if (bookType == BookType.MYSTERY) {
                return new MysteryBook();
            } else if (bookType == BookType.EDUCATION) {
                return new EducationBook();
            }
            return null;
        }
    }

팩토리 메서드 구현 부분이다. 자식 클래스를 직접 호출해서 인스턴스를 생성하지 않고 팩토리메서드에 입력변수로 타입을 입력해서 인스턴스를 생성한다.

실행

public static void main(String[] args) {
        Book book1 = new BookFactory().buy(BookType.LITERATURE);
        Book book2 = new BookFactory().buy(BookType.MYSTERY);
        Book book3 = new BookFactory().buy(BookType.EDUCATION);

        book1.read();
        book2.read();
        book3.read();
        /*
        문학소설을 구매합니다.
        추리소설을 구매합니다.
        교육서적을 구매합니다.
        문학소설을 읽습니다.  
        추리소설을 읽습니다.  
        교육서적을 읽습니다. 
        */
    }

main에서 클래스를 직접 호출해서 생성하는 일을 피하게 되면서 결합도를 낮출 수 있다.


1-2. 추상 팩토리(Abstract Factory)

상세화된 서브클래스를 정의하지 않고도 서로 관련성이 있거나 독립적인 여러 객체의 군을 생성하기 위한 인터페이스를 제공합니다.

추상 팩토리는 서로 관련이 있는 객체들을 통째로 묶어서 팩토리 클래스로 만들고, 이들 팩토리를 조건에 따라 생성하도록 다시 팩토리를 만들어서 객체를 생성하는 패턴이다. 추상 팩토리는 실제 객체가 정확히 무엇인지 알지 못해도 객체를 생성하고 조작할 수 있도록 하고 다양한 환경에서 작동하는 코드를 쉽게 만들 수 있도록 해준다. 구현 클래스가 아닌 인터페이스를 통해 이용하기 때문에 추상메서드 에서는 사용하고 있는 환경이 어떻게 구성되어 있는지 알지 못한다.

추상 팩토리의 특징

  • 객체가 생성되거나 구성, 표현되는 방식과 무관하게 시스템을 독립적으로 만듬
  • 여러 제품군 중 하나를 선택해서 시스템을 설정. 한번 구성한 제품을 다른 것으로 대체할 수 있음
  • 관련된 제품 객체들이 함께 사용되도록 설계하고 외부에서 제약이 지켜지도록 함
  • 제품에 대한 클래스 라이브러리를 제공하고, 그들의 구현이 아닌 인터페이스를 노출시킴

✔팩토리 메서드 패턴 VS 추상 팩토리 패턴

팩토리 메서드 패턴
조건에 따른 객체 생성을 팩토리 클래스로 위임하여, 팩토리 클래스에서 객체를 생성하는 패턴

추상 팩토리 패턴
서로 관련이 있는 객체들을 통째로 묶어서 팩토리 클래스로 만들고, 이들 팩토리를 조건에 따라 생성하도록 다시 팩토리를 만들어서 객체를 생성하는 패턴

추상 팩토리 패턴은 팩토리 메서드 패턴을 좀 더 캡슐화한 방식이라고 볼 수 있다.

(예시)


1-3. 빌더(Builder)

복잡한 객체를 생성하는 방법과 표현하는 방법을 정의하는 클래스를 별도로 분리하여 서로 다른 표현이라도 이를 생성할 수 있는 동일한 구축 공정을 제공할 수 있도록 합니다.

자바로 코딩할 때 다음과 같은 스타일로 객체를 생성하는 코드가 있다면, 빌더 패턴을 사용했다고 할 수 있다.

Member customer = Member.build() .name("홍길동") .age(30) .build();

 

빌더는 복합 객체의 생성 과정과 표현 방법을 분리하여 동일한 생성 절차에서 서로 다른 표현 결과를 만들 수 있게 하는 패턴이다. 불필요한 생성자를 만들지 않고 데이터의 순서에 상관 없이 객체를 만들어 내며, 사용자가 봤을때 명시적이고 이해할 수 있어야 한다. 클래스의 인스턴스들이 어떻게 만들고 어떻게 결합하는지에 대한 부분을 외부에 알리지 않기 때문에 빌더 패턴과 비슷한 점층적 생성자 패턴, 자바빈 패턴 보다 훨씬 효율적이고 유연하다.

 

빌더의 특징

  • 불필요한 생성자를 만들지 않고 객체를 만든다.
  • 데이터의 순서에 상관 없이 객체를 만들어 낸다.
  • 사용자가 봤을때 명시적이고 이해할 수 있어야 한다.
  • 코드 읽기와 유지보수에 유리하다

 

점층적 생성자 패턴 : new Member("이름", "나이", "취미");

자바빈 패턴

Member member = new Member();
member.setName("이름");
member.setAge("나이");
member.setHobby("취미");

빌더 패턴

  •  
public class Member {
    private final String name;
    private final int age;
    private final String hobby;
    private final String location;
    private final String job;
    private final int weight;
    private final int height;
    private final String bloodType;

    public static class Builder {

        // Optional parameters - initialized to default values(선택적 인자는 기본값으로 초기화)
        
        private String hobby = "";
        private String location = "";
        private String job = "";
        private int weight = 0;
        private int height = 0;
        private String bloodType = "";

        public Builder(String name, int age) {
            this.name = name;
            this.age =age;
        }

        public Builder hobby(String val) {
            hobby = val;
            return this;    // 이렇게 하면 . 으로 체인을 이어갈 수 있다.
        }
        public Builder location(String val) {
            location = val;
            return this;
        }
        public Builder job(String val) {
            job = val;
            return this;
        }
        public Builder weight(int val) {
            weight = val;
            return this;
        }
        public Builder height(int val) {
            height = val;
            return this;
        }
        public Builder bloodType(String val) {
            bloodType = val;
            return this;
        }
        public Member build() {
            return new Member(this);
        }
    }

    private Member(Builder builder) {
        servingSize  = builder.servingSize;
        name = builder.name;
        age = builder.age;
        hobby = builder.hobby;
        location = builder.location;
        job = builder.job;
        weight = builder.weight;
        height = builder.height;
        bloodType = builder.bloodType;
    }
}

 

실행 코드

Member member = new Member.Builder("name", 24)
                .hobby("hobby")
                .location("location")
                .job("job")
                .weight(100)
                .height(200)
                .bloodType("B");

 

 

장점

  • 각 인자가 어떤 의미인지 알기 쉽다.
  • `setter` 메소드가 없으므로 변경 불가능 객체를 만들 수 있다.
  • 한 번에 객체를 생성하므로 객체 일관성이 깨지지 않는다.
  • `build()` 함수가 잘못된 값이 입력되었는지 검증하게 할 수도 있다.

추가로 Lombok에서도 빌더 패턴을 사용할 수 있다.

@Builder
public class Member {
    private final String name;
    private final int age;
    private final String hobby;
    private final String location;
    private final String job;
    private final int weight;
    private final int height;
    private final String bloodType;
}

 


참고자료

https://gmlwjd9405.github.io/2018/07/06/design-pattern.html

https://4z7l.github.io/2020/12/25/design_pattern_GoF.html

https://johngrib.github.io/wiki/factory-method-pattern/#fn:head-example

https://johngrib.github.io/wiki/abstract-factory-pattern/

 

 

'JAVA' 카테고리의 다른 글

Proxy 객체  (0) 2021.09.07
proxy 패턴  (0) 2021.09.07
Object 클래스 - 메서드  (0) 2021.08.05
Constant Pool  (0) 2021.08.05
StringBuffer / StringBuilder의 차이점  (0) 2021.08.05