GYUD-TECH

[스터디: 오브젝트] 디자인패턴과 프레임워크 본문

스터디

[스터디: 오브젝트] 디자인패턴과 프레임워크

GYUD 2024. 3. 18. 16:22

 

객체지향을 공부하면서 디자인패턴에 대해서 정말 많이 들었다.

디자인 패턴을 공부하면서 왜 패턴이 필요한지 잘 이해가 안 되었는데, 오브젝트 책을 읽으며 객체지향을 공부하니 디자인 패턴이 왜 필요하고, 패턴을 사용하면 조금 더 쉽게 좋은 설계를 할 수 있겠다는 생각을 하였다.

 

이번 장에서는 책에서 지금까지 사용했던 설계들을 디자인 패턴으로 소개해준다.

이 외에도 다양한 디자인 패턴이 있기 때문에 앞으로 객체지향을 공부하며 디자인 패턴도 계속 공부할 계획이다.

 


패턴

패턴이란 한 컨텍스트에서 유용한 규칙이나 형식을 다른 컨텍스트에서도 유용하게 적용되는 것을 뜻한다.

 

최소 세가지의 다른 시스템에 특별한 문제 없이 적용되며, 유용한 경우에만 패턴으로 인정될 수 있다.

 

패턴의 분류

소프트웨어에서의 패턴은 적용 범위나 단계에 따라 아키텍처 패턴, 분석 패턴, 디자인 패턴, 이디엄으로 구분한다.

 

아키텍처 패턴

프로그래밍 언어나 패러다임에 독립적으로 구체적인 소프트웨어 아키텍처를 위한 템플릿을 제공한다.

 

분석 패턴

기술적인 문제보다는 도메인 내의 개념적인 문제를 해결해주는데 초점을 맞추는 패턴이다.

 

디자인 패턴

특정 상황에서 설계 문제를 해결하면서, 협력하는 컴포넌트들 사이의 반복적으로 발생하는 구조를 제공해주는 패턴으로 프로그래밍 언어나 패러다임에 독립적이다.

 

이디엄

특정 프로그래밍 언어에만 국한된 하위레벨 패턴으로, 주어진 언어의 기능을 사용해 컴포넌트간의 기능을 구현하는 방법을 서술한다.

 

우리는 이 패턴들 중 설계 문제를 해결하기 위한 디자인 패턴에 집중해서 객체지향적 설계를 공부한다.

 


디자인패턴

디자인 패턴이란 소프트웨어 설계에서 반복적으로 발생하는 문제에 대해 그 해결방법을 지정해 놓은 것으로 일반적으로 컴포넌트간의 협력을 유연하게 만드는 것을 목표로 한다.

 

다양한 패턴들 중 책에서는 Composite 패턴, Strategy 패턴, Template Method 패턴, Decorator 패턴을 소개하였고, 이해를 위해 추가적인 자료를 더 찾아보며 정리하였다.

 

composite 패턴

복합 객체와 단일 객체를 동일한 컴포넌트로 취급하며 클라이언트에서는 이를 구분하지 않고 동일한 인터페이스를 사용하도록 하는 구조의 패턴이다.

 

이를 이해하기 위해서는 파일 구조를 떠올리며 개념을 이해하는 것이 좋다.

파일 구조에서는 폴더안에 폴더를 담을 수도 있고, 파일을 담을 수도 있다.

이를 코드로 구현할 때 최상위 폴더에서 폴더만 따로 저장하여 관리하고, 파일만 따로 저장하여 관리하는 것은 매우 복잡한 일이다.

 

다행히도 데이터가 계층적 트리 형태로 표현되어 있기 때문에 이를 재귀로 표현하면 자기 자신 객체만 관리하고, 그 하위는 하위 객체에게 책임을 부여함으로써 단순하게 표현할 수 있다.

 

폴더와 파일을 한개의 공통 인터페이스을 구현한 각각의 객체라고 가정하자.

이때 폴더는 다른 폴더 또는 파일을 담을 수 있기 때문에 Composite 객체라고 부른다.

반면 파일은 더이상 다른 객체를 담을 수 없는 트리 구조상 끝에 해당되기 때문에 Leaf 객체라고 부른다.

 

위 구조를 일반적인 Composite 패턴에서 표현하는 구조로 나타내면 아래와 같이 표현할 수 있을 것이다.

 

Component 에서는 Leaf와 Composite 객체에서 동시에 구현하는 operation() 인터페이스를 정의한다.

public interface Component {
    void operation();
}

 

Leaf 에서는 파일을 클릭했을때 내용을 보여주는 방식으로 operation 메서드를 재정의하면 된다.

public class Leaf implements Component {

    @Override
    public void operation() {
        System.out.println(this + " 호출");
    }
}

 

반면 Composite 에서는 내용을 보여주고, Component가 포함한 다른 Component의 operation 메서드를 호출해주는 방식으로 구현하면, 재귀적인 파일 시스템 구조를 만들 수 있다.

public class Composite implements Component {

    List<Component> components = new ArrayList<>();
    
    @Override
    public void operation() {
        System.out.println(this + " 호출");
        for (Component component : components) {
            component.operation();
        }
    }

    public void add(Component c) {
        components.add(c);
    }

    public void remove(Component c) {
        components.remove(c);
    }
    
    public List<Component> getChild() {
        return components;
    }
}

 

Composite 패턴을 사용하면 계층적 트리 형태의 데이터를 간단하게 다룰 수 있다는 장점이 있다.

또한 추상화된 인터페이스인 Component 에만 의존하기 때문에 수평적, 수직적으로 확장이 용이하다.

 

하지만 재귀 호출을 사용하기 때문에 트리의 깊이가 깊어지면 디버깅이 어렵다는 단점도 존재한다.

 

만약 데이터의 구조가 계층적인 트리구조라면 Composite 패턴을 사용하여 설계를 단순화하는 방식을 고민해보자.

 

 

Strategy 패턴

알고리즘을 다양하게 선택해야하는 경우에 사용하여 런타임에 알고리즘을 동적으로 선택할 수 있도록 해주는 패턴이다.

 

이전에 Movie에서 DiscountPolicy를 선택할 때 합성을 통해서 Movie의 종류에 따라서 다양한 DiscountPolicy를 런타임에 선택할 수 있도록 설계하였는데, 이 예시가 Strategy 패턴을 적용한 대표적인 예시이다.

어떤 알고리즘을 택할지 구현 객체로 캡슐화를 하고, 합성을 사용하여 런타임에 구현 객체를 선택할 수 있다.

 

 

Template Method 패턴

Strategy 패턴과 동일하게 알고리즘의 구현을 객체 내부로 캡슐화 하지만, 합성이 아닌 상속을 사용하는 방식이다.

상속을 사용하기 때문에 합성에 비해서 부모객체와 자식객체의 결합도가 높아진다는 단점이 있다.

하지만, 어떤 객체인지 명시적으로 표현 가능하기 때문에 알고리즘의 변경이 자주 되지 않는 경우에 유용하게 사용할 수 있다.

 

 

Decorator 패턴

합성과 믹스인 포스트에서 예시로 든 휴대전화 비용 계산 기능 설계에서 사용한 패턴으로, 추가적인 기능의 조합을 런타임에 동적으로 생성할 수 있도록 도와주는 패턴이다.

 

https://gyuwon-tech.tistory.com/38

 

[스터디: 오브젝트] 합성과 믹스인

앞선 장의 마지막에 상속보다는 합성을 사용하자고 강조했다. 이번 장에서는 상속과 합성의 차이를 비교하고 왜 상속보다 합성을 사용해야 하는지에 초점을 맞추어서 공부하였다. 또한 중복 코

gyuwon-tech.tistory.com

 

예시에서 추가 요금 정책을 구현하기 위해 별도의 추상 클래스를 정의하고 뒤이어 다른 할인 정책을 적용할 수 있도록 RatePolicy를 인스턴스 변수를 가지도록 하였다.

 

이를 Decorator 패턴으로 일반화 해보면 아래와 같이 표현할 수 있다.

Component 인터페이스를 통해 원본 객체와 장식 객체 모두를 하나로 묶어 추상화한다.

Concrete Component와 달리 Decorator 클래스에서는 Component 타입의 wrappee 필드를 두어 operation 메서드에서 다음 Component의 operation을 호출할 수 있도록 한다.

 

이 패턴을 적용함으로써 객체를 여러 Decorator로 래핑하여 런타임에 동적으로 추가할 수 있기 때문에 유연하게 설계를 확장할 수 있다.

 

하지만 Decorator의 순서에 따라 동작방식이 달라지기 때문에 순서에 의존하지 않는 Decorator를 구현하기는 어렵고, 객체 생성 초기에 Decorator까지 생성해줘야 하기 때문에 아래와 같은 복잡한 코드를 작성해야 한다.

Phone phone = new Phone(
                    new TaxablePolicy(0.05, 
                        new RateDiscountablePolicy(Money.wons(1000),
                            new RegularPolicy(...)));

패턴은 출발점이다

이 외에도 다양한 패턴이 존재하고, 이는 설계를 유연하게 만드는 것을 도와준다.

하지만 서비스의 특성을 고려하지 않고 패턴을 맹목적으로 사용하는 것은 오히려 불필요하게 코드를 복잡하게 만들고 유지보수하기 어려운 시스템을 만들 수 있다.

 

따라서 이를 남용하지 않기 위해서는 다양한 트레이드오프 관계를 고려하여 패턴을 적용해야한다.

 


프레임워크

디자인패턴이 효율적인 설계를 위한 아이디어를 제공해 주지만, 언어에 종속적인 구현 코드를 정의하지는 않기 때문에 패턴 구현을 위해서는 개발자가 매번 코드를 작성해줘야 한다.

 

이와 달리 설계와 코드까지 제공해 주면서 개발자의 수고를 덜어주기 위한 것이 프레임워크이다.

 

프레임워크는 설계적 측면과 코드와 설계의 재사용의 관점에서 두가지로 정의할 수 있다.

설계 관점:
추상 클래스나 인터페이스를 정의하고, 인스턴스 사이의 상호작용을 통해 시스템 전체 혹은 일부를 구현해 놓은 것

재사용 관점:
애플리케이션 개발자가 현재의 요구사항에 맞게 커스터마이징 및 재사용 할 수 있는 애플리케이션의 골격

 

프레임워크는 코드의 일부를 미리 구현해놓고 이를 재사용이 가능하도록 제공하는 것이 핵심 역할이라고 정의할 수 있다.

프레임워크가 여러 애플리케이션에 걸쳐 재사용 되기 위해서는 애플리케이션마다 자주 변하지 않는 것을 따로 분리해서 제공해야 한다.

 

이전에 사용했던 휴대폰 요금 계산 기능 예시에서, 계산을 위해 RatePolicy에 메시지를 보내고, 기본 정책과 추가 정책을 사용하여 요금을 계산하는 골격은 어떤 요금 정책이 추가되더라도 바뀌지 않는다.

기본 요금 정책은 여러 요금 규칙의 조합이며 요금 규칙은 요금 적용 조건과 단위 금액을 포함하는 구현체를 의존하는 것도 변하지 않을 것이다.

 

따라서 아래와 같이 변하지 않는 부분을 별도의 패키지로 분리할 수 있다.

하위 패키지에서는 모두 상위 패키지의 추상화에 의존하고 있기 때문에 상위 패키지만을 분리하여 따로 독립적으로 사용하는 것이 가능하다.

 

이때 위의 상위 패키지를 휴대전화 요금 계산 로직 기능을 제공하는 프레임워크라고 할 수 있다.

 

프레임워크를 사용하지 않고 라이브러리를 사용해서 애플리케이션을 작성하는 경우를 생각해보자.

애플리케이션이 필요한 라이브러리를 구체적으로 호출하기 때문에 애플리케이션은 스스로 자신이 사용할 라이브러리를 제어한다.

 

하지만 프레임워크를 사용하면 프레임워크의 메인 프로그램을 사용하기만 하면 프레임워크가 알아서 구현을 적용하여 사용자가 원하는 결과를 전달해준다.

프로그램의 제어권이 프레임워크에게 넘어간 것이기 때문에 이를 제어의 역전이라고 한다.

 

이렇게 하면 개발자가 자신의 코드를 제어할 수는 없지만, 객체의 생성이나 관리를 할 필요 없이 구현 코드를 작성하는데만 집중을 하면 되기 때문에 생산 효율이 높아진다.

또한 같은 프레임워크를 사용하는 다양한 애플리케이션에서 일관성 있게 코드를 작성할 수 있기 때문에 재사용이 가능하고, 이해하기도 쉽다는 장점이 있다.

 

Spring Framework는 자바의 웹 프레임워크로 스프링에서 정의한 형식을 따르면, 자바 언어로만 개발하였을 때보다 더 편하게 웹 개발을 할 수 있는 것도 프레임워크가 제어권을 가지고 이를 관리해 주기 때문이다.

 

결국 프레임워크는 여러 애플리케이션에서 공통적으로 사용할 수 있는 해결책만 제공하고, 애플리케이션에 따라 달라질 수 있는 구현은 비워둔다.

이렇게 프레임워크에서 완성되지 않은 채로 남겨진 구현 부분을 훅(hook)이라고 한다.

제어의 역전으로 인해 개발자는 훅부분만 구현해주면 프로그램을 쉽게 동작 시킬 수 있다.

 

면접 질문으로 라이브러리와 프레임워크와의 차이에 대한 질문이 자주 나오는데, 프로그램의 제어권을 누가 들고 있느냐가 가장 큰 차이라는 것을 알 수 있었다.

 


참고자료

https://inpa.tistory.com/entry/GOF-💠-복합체Composite-패턴-완벽-마스터하기

 

💠 복합체(Composite) 패턴 - 완벽 마스터하기

Composite Pattern 복합체 패턴(Composite Pattern)은 복합 객체(Composite) 와 단일 객체(Leaf)를 동일한 컴포넌트로 취급하여, 클라이언트에게 이 둘을 구분하지 않고 동일한 인터페이스를 사용하도록 하는

inpa.tistory.com

https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0Decorator-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90

 

💠 데코레이터(Decorator) 패턴 - 완벽 마스터하기

Decorator Pattern 데코레이터 패턴(Decorator Pattern)은 대상 객체에 대한 기능 확장이나 변경이 필요할때 객체의 결합을 통해 서브클래싱 대신 쓸수 있는 유연한 대안 구조 패턴이다. Decorator을 해석하

inpa.tistory.com