프로그래밍/클린 아키텍처

[클린아키텍처] 3부. 설계원칙 OCP

2KB 2021. 3. 1. 16:53

소프트웨어에서 좋은 아키텍처를 정의하는 원칙이 바로 SOLID이다.

SOLID의 원칙의 목적은 중간 수준(=모듈)의 소프트웨어 구조가 아래와 같도록 만드는 데 있다.

1. 변경에 유연하다.

2. 이해하기 쉽다.

3. 많은 소프트웨어 시스템에 사용될 수 있는 컴포넌트의 기반이 된다.

** 모듈 : 독립적인 개발 단위. 라이브러리 또는 API 인터페이스 등. 단순히 함수와 데이터구조로 구성된 집합인 클래스를 의미하기도 함.

 

OCP: 개방-폐쇄 원칙


OCP (The Open Closed Principle)

개방 폐쇄의 원칙
클래스는 확장에 열려 있어야 하며, 변경에 닫혀 있어야 한다.
추상화와 다형성을 통한 확장, 기존 구성요소의 변경 최소화를 의미
확장 되어야 할 것과 변경하지 않을 것을 명확히 구분한다.
확장 되어야 하는 기능을 인터페이스로 정의 한다.
정의된 인터페이스에 따라 구현 코드를 작성한다.

"소프트웨어 개체는 확장에는 열려있어야 하고, 변경에는 닫혀 있어야 한다."

즉, 어떠한 요구사항에 대한 반영을 위해 행위(=메서드)를 확장(=추가)하는 것은 괜찮으나, 개체 자체가 가지고 있는 기존 구성요소의 변경이 요구되면 안된다는 것이다.

우리말 대사전. 변경의 사전적 의미

사고 실험

  재무제표를 웹 페이지로 보여주는 시스템이 있다고 생각해보자.

  이러한 재무제표는 제무 데이터를 기준으로 보고서 형태로 변환해서 웹페이지에 출력할 수 있고, 보고서 형태의 이 데이터는 흑백 프린트로 출력이 가능해야 한다.

위의 요구사항에 대해 SRP를 적용하면 다음과 같은 간단한 설계를 얻을 수 있다.

요구사항에 대해 SRP 적용 설계 예제

재무 데이터는 "재무 분석기"에 의해 보고서 형태의 데이터로 변환되고, 이러한 보고서 형태의 데이터는 웹 또는 프린터로 출력된다.

여기서 얻을 수 있는것은, 생성된 보고서의 출력이 웹 또는 프린터 출력. 즉, 2개의 책임으로 나뉘어 진다는 것이다.

이러한 책임이 분리되었다면, 두 책임 중에서 하나에서 변경이 발생하더라도, 다른 하나는 변경되지 않도록 소스 코드 의존성을 신경써서 설계 하여야 한다.

또한 이렇게 설계한 구조에서는 행위가 확장될 때 다른 곳에서 변경이 발생하지 않음을 보장해야 한다.

위의 요구사항을 각각의 4개 컴포넌트로 나누면 다음과 같다.

4개의 컴포넌트는 각각, Controller, Interactor, Presenter, Database이며 위의 컴포넌트들끼리 연결된 화살표 중

열린 화살표는 사용(Using)관계이며, 닫힌 화살표는 구현(implemet) 또는 상속(inheritance) 관계이다.

여기서 주목할 점은 모든 의존성은 소스 의존성을 나타낸다는 것이다.

또한 화살표가 A클래스에서 B클래스로 향한다면, A클래스에서는 B클래스를 호출하지만, B클래스에서는 A클래스를 호출하지 않는다.

public class FinancialReportController {

    private final FinancialReportRequester financialReportRequester;
    private final FinancialReportPresenter financialReportPresenter;

    FinancialReportController(final FinancialReportRequester financialReportRequester,
                              final FinancialReportPresenter financialReportPresenter) {
        this.financialReportRequester = financialReportRequester;
        this.financialReportPresenter = financialReportPresenter;
    }

    public void showViewByFinancialReportResponse(final FinancialReportRequest request) {
        final FinancialReportResponse response = getFinancialReportResponseByRequest(request);
        financialReportPresenter.showView(response);
    }
    
    private FinancialReportResponse getFinancialReportResponseByRequest(final FinancialReportRequest request) {
        return financialReportRequester.getFinancialReportResponseByRequest(request);
    }

}

 

public interface FinancialReportPresenter {

    void showView(FinancialReportResponse financialReportResponse);

}

 

public interface FinancialReportRequester {

    FinancialReportResponse getFinancialReportResponseByRequest(FinancialReportRequest request);

}

FinancialReportController는 FinancialReportPresenter와 FinancialReportRquester를 사용하지만, 두 클래스는 FinancialReportController에 대한 의존성이 전혀 없는 것을 위의 예제 코드를 통해서도 확인할 수 있다.

컴포넌트 관계는 단반향으로 이루어진다.

여기서 알아볼 수 있는것은 이러한 컴포넌트들의 관계는 "단방향" 이라는 것이다. 위의 그림에서 나온 화살표는 "변경으로 부터 보호하려는 컴포넌트를 향하도록 그려진다.

즉, FinancialReportInteractor가 FinancialReportController의 변경에 대해 영향을 받지 않기 위해서는 Controller에 대한 의존성을 가지지 않으면 되고, FinancialReportController가 FinancialReportInteractor (FinancialReportRequester, FinancialReportPresenter)에 대해 아래의 코드와 같이 의존성을 가지면 된다.

 

public class FinancialReportController {

    private final FinancialReportRequester financialReportRequester;
    private final FinancialReportPresenter financialReportPresenter;

    FinancialReportController(final FinancialReportRequester financialReportRequester,
                              final FinancialReportPresenter financialReportPresenter) {
        this.financialReportRequester = financialReportRequester;
        this.financialReportPresenter = financialReportPresenter;
    }

    public void showViewByFinancialReportResponse(final FinancialReportRequest request) {
        final FinancialReportResponse response = getFinancialReportResponseByRequest(request);
        financialReportPresenter.showView(response);
    }
    
    private FinancialReportResponse getFinancialReportResponseByRequest(final FinancialReportRequest request) {
        return financialReportRequester.getFinancialReportResponseByRequest(request);
    }

}

 

자 그러면, 위의 컴포넌트 관계의 방향성을 보았을 때, 가장 OCP를 잘 지킬 수 있는, 즉 다른 컴포넌트의 변경사항에 대해 가장 영향이 없는 컴포넌트는 어디인가? 바로 "FinancialReportInteractor" 컴포넌트가 될 것이다. 

Interactor 컴포넌트는 다른 모든 것에서 발생한 변경으로 부터 보호가 된다.

Interactor는 애플리케이션에서 가장 높은 수준의 정책과 중요한 문제인 "제무재표용 데이터 생성"을 담당하고 있기 때문이다.

결국 이러한 보호의 계층 구조는 화살표의 방향에 의해 "수준(Level)"을 개념으로 생성되며, 위의 컴포넌트 방향 그림으로 보았을때 컴포넌트의 보호 계층 순위를 나누어 보면,

Interactor가 1순위.  Controller가 2순위. Presentor가 3순위. View가 4순위의 보호 수준을 이루게 된다.

이것이 바로 아키텍처 수준에서 OCP가 동작하는 방식으로서, 기능을 분리하고 분리된 기능을 컴포넌트의 계층구조로 조직화 시키는 것이다.

이를 통해, 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있는 원칙이 세워지게 된다.

 

결론


OCP의 목표는 시스템을 확장하기 쉬운 동시에 변경으로 인해 시스템이 많은 영향을 받지 않도록 하는데 있다.

이러한 목표를 달성학 ㅣ위해서는 시스템을 컴포넌트 단위로 분리하고, 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보보할 수 있는 형태의 의존성 계층 구조가 만들어지도록 해야 한다.