Clean Architecture : 현실의 문제를 해결한다는 것

강지혁·2022년 9월 3일
0

OOP에 대해 나름의 정의를 내려보았던 지난 게시글에서,
왜 객체지향 프로그래밍인가? 에 대한 개인적인 의견을 기록해두었었습니다.

"현실의 문제 및 클라이언트의 요구사항은 매번 변하며, 문제를 해결하는 동료도 수시로 변한다.
그렇기 때문에, 협업과 유지보수는 프로그래밍을 잘 하는데 있어 아주 중요한 고려사항이다."

- 나, 2022 😅

그동안 접해온 개발 관련 서적이나 토막글들 & 다양한 분야에 걸친 다양한 방법론은 일관적으로 변경이 쉬운, 유지 보수하기 쉬운 프로그래밍을 말하고 있다고 느꼈습니다. 실무에서 부딪히게 되는 상황도 변경의 연속 이었고요.

실무로서 개발을 한 지는 갓 1년이 지난 시점에서,
그리고 누군가에게 프로그래밍이란 이런 것이다~ 라고 설명해야 하는 입장에서,

앞선 개인적인 견해를 검증해보고, 제 생각을 더 공고히 해야겠다 싶었습니다.
그렇게 클린 아키텍처를 읽어보았습니다.


감상

나름 제대로 느끼며 개발해왔구나 ~ 😌 싶으면서, 어떻게 해야 잘 할 수 있는지 구체적으로 배울 수 있었던 것 같습니다.

  • "나름 잘 느끼면서" :
    핵심은 아니지만, 책을 읽다 보면 로버트 마틴의 어린시절부터 프로그래밍이 어떻게 발전했는지 따라갈 수 있습니다. 그동안 프로그래머들은 어떤 문제들을 겪어왔으며, 그로 인해 무엇이 변했고, 변하지 않은 것은 무엇인지 간접적으로 경험해보게 되었습니다.
    특히, 현실의 문제에 유연하게 대처하기 위한 눈물의 사례들과 경험들을 많이 볼 수 있었습니다.
    현실의 문제들은 변수를 제어할 수가 없고, 우리는 미래를 볼 수도 없습니다.
    그래서 1. 변경을 최소화하는 것, 2. 변경을 편하게 하는 것 에 집중해야 합니다.
    수많은 이야기를 통해 개발에 있어 중요한 것은 무엇인가? 느껴왔던 점들이 더욱 공고해졌던 것 같습니다.
  • "어떻게 할 지 구체적으로":
    책의 중후반부에서는 위 1, 2번을 어떻게 해결할 수 있는지 자세히 가이드해주고 있습니다.
    do & don't를 바탕으로, 도메인 - 세부사항 분리 그리고 의존성 규칙을 통해 변경사항에 유연하게 대처하기 위한 원칙을 제시해줍니다.

좋은 얘기만 해뒀는데,
정확히 어떤 구체적인 이야기를 해뒀는지 짤막하게 정리해두고자 합니다.


클린 아키텍처 4부 이후: 컴포넌트 원칙과 아키텍처의 내용을 간단히 정리 & 기록해두었습니다.

컴포넌트 응집도

어떤 클래스가 어느 컴포넌트에 들어갈 지는 신중하게 결정되어야 합니다.
컴포넌트의 응집도를 잘 지켜나가기 위해 저자는 다음과 같은 원칙을 제시합니다.

  • REP : 재사용 단위를 릴리즈 단위와 일치시켜야 한다.
  • CCP : 서로 다른 이유로 변경되는 컴포넌트는 분리해라. (OCP, SRP를 이해하고 있으면 좋다)
  • CRP : 사용하지 않는 클래스에 의존하지 않도록 해라. (ISP)

  • 딜레마
    • REP를 버리면 재사용이 어렵고,
    • CCP를 버리면 변경이 너무 잦고,
    • CRP를 버리면 쓸 데 없는 릴리스가 많아진다.

응집도를 위한 세 가지 요소를 모두 충족하는 것은 불가능합니다.
CRP를 준수하다보면 컴포넌트 크기가 작아질 테지만, 나머지 두 원칙은 컴포넌트 크기를 키우는 원칙들입니다.

무엇이 중요할까요?

  • 저자는 중점에 두어야 하는 원칙이 개발 생명주기에 따라 다르다고 이야기합니다
  • 서비스 초기에는 재사용에 대한 고려를 거의 할 필요가 없습니다. 이후 성장함에 따라 재사용성을 점차 고려해나가게 됩니다. 오른쪽에서 왼쪽으로 점진적으로 이동하게 되는 것이죠..

컴포넌트 결합

컴포넌트 끼리는 어떠한 관계를 맺어야 할까?
컴포넌트 설계는 절대로 프로젝트 초기에 완성할 수 없습니다.
컴포넌트 의존성은 프로젝트의 기능을 기술하는 것과는 큰 관련이 없고, 유지보수성 & 빌드가능성을 비추는 관점입니다.
소프트웨어가 존재하지도 않는데 빌드나 유지보수를 잘 한다는 것은 어불성설입니다.
그러나 점차 프로젝트가 성장해감에 따라, 의존성 관리 & 변경사항의 임팩트 줄이기에 대한 요구사항은 증가할 수밖에 없습니다.

컴포넌트의 의존관계에 대한 3원칙은 아래와 같습니다.

  • ADP : 의존성은 비순환 해야 한다.
  • SDP : 안정된 (변경이 잦은) 컴포넌트에 의존해라.
  • SAP : 컴포넌트는 안정된 만큼만 추상화되어야 한다.
    • 고수준 (비즈니스 정책 등) 원칙 ⇒ 변동이 많지 않음
      => 안정적이면서도 유연하게 관리되어야 한다. ⇒ 어떻게? ⇒ 추상화를 통해!

    • 추상화와 안정성

      • 추상화 (A) : Na (추상클래스 + 인터페이스) / Nc (모든 클래스 수)
      • 불안정성 (I) : Fan-out / (Fan-in + Fan-out)
    • 주계열

      • A + I = 1, 컴포넌트의 안정성 & 추상화는 주계열 주변에 있는 것이 좋다.
      • 고통의 구역 : 구체적이며 안정적이라서, 변경이 매우 어렵다.
      • 쓸모없는 구역 : 추상적이지만 의존하는 컴포넌트가 없다. 쓸 데가 없다..

아키텍처란?

아키텍처의 주된 목적은, 시스템의 생명주기를 지원하는 것입니다.
시스템의 수명과 관련된 비용은 최소화하고, 생산성을 최대화하기 위해 아키텍처가 필요합니다.

  • 개발
    • 잘 구성된 아키텍처를 통해 시스템이 성장해도 개발 비용을 안정적으로 유지할 수 있다.
  • 배포
    • 배포 비용이 높을 수록 유용성이 떨어지는 소프트웨어다. 쉬운 배포도 중요한 고려 가치 중 하나
  • 운영
    • 운영은 다른 요소들에 비해 아키텍처가 미치는 영향이 크진 않다.
    • 다만 개발자에게 시스템의 운영 방식을 잘 드러내서, 일을 더 잘하게 하는 방식으로 기여할 수 있다.
  • 유지보수
    • 새로운 기능을 추가하려면, 기존 코드를 읽는 비용이 들고 (탐사), 수정 사항에 대한 예상치 못한 사이드이펙트가 발생한다. (의도치 않은 결함)
    • 좋은 아키텍처는 앞선 두 위험부담에 대한 비용을 크게 줄일 수 있다.
      • 시스템을 잘 분리하고, 인터페이스를 두어 서로 격리 시키면 되니까...
  • 선택사항 열어두기
    • 정책과 세부사항을 분리해서, 정책에 무관하게 시스템의 세부사항을 (나중에) 결정할 수 있다.

좋은 아키텍처는 위에서 말했든 운영, 개발, 배포를 지원해야 합니다.
그리고 당연하지만 시스템의 기능과 의도, 즉 UseCase를 지원하는 데 방해가 되어서는 안됩니다.

UseCase 관점에서의 결합 분리

유즈케이스는 아키텍처를 수직적으로 분할합니다. (UI - 업무 로직 - DB에 대해 수직적으로 걸쳐 있다는 의미)
그러나 서로 다른 유즈케이스의 경우, 서로 다른 속도로 발전합니다.
이런 경우 개발, 배포, 운영 관점에서 서로 다른 유즈케이스는 독립적으로 조직되어야 할 수 있습니다.

결합 분리 모드

  • 소스 수준 분리 (모듈 사이 의존성)
  • 배포 수준 분리 (독립적으로 배포 가능하게 구성) - 같은 프로세스에서 돌수도 있음
  • 서비스 수준 분리 - 아예 네트워크 패킷을 통해서만 통신하도록 구성
  • 운영 측면에서, 트래픽 등을 이유로 서비스 수준의 분리를 진행할수도 있고, 개발 및 배포 독립성을 위해 배포 수준의 분리를 진행할 수도 있습니다.

좋은 아키텍처는 선택권을 열어둔다.

  • 앞선 분리 모드 가운데 어느 것을 택할 것인지는, 프로젝트의 상황마다 다를 수밖에 없습니다.
  • 다만 좋은 아키텍처라면, 분리 모드를 유연하게 적용할 수 있는 구조를 갖추고 있습니다 ❗️

좋은 시스템 아키텍처는 유즈케이스의 핵심 규칙들을 제외한 세부사항들을 유연하게 바꿀 수 있도록 지원합니다.
저자는 이들 사이에 적절한 경계선을 긋는 것이 필요하다고 말합니다.
핵심 규칙이란 무엇이고, 그렇지 않은 세부사항들은 무엇일까요?

좋은 아키텍처는 제어 흐름대로 의존성이 결합되지 않는다 !!

  • 고수준의 업무 규칙이란 무엇이고..
  • 저수준의 세부사항은 뭘까요?

업무 규칙이란

  • 수익을 얻거나 비용을 얻을 수 있는 규칙, 그리고 절차
  • 시스템의 최상단, 가장 독립적이고, 많이 재사용할 수 있는 코드
    • 안정적인가? O
    • 추상화되어있는가? X???
  • Entity
    • 핵심 업무 데이터를 포함하여, 핵심 업무 규칙을 수행하는 객체
  • UseCase
    • Application-Specific 업무 규칙
    • 엔티티의 흐름을 제어
    • 입력 - 출력 - 처리 레벨 제어
    • 유즈케이스는 사용자 인터페이스를 정의하지 않는다 !!!!!
      • 데이터가 어떻게 전달되는 지 ⇒ 유즈케이스의 영역이 아님

세부사항들

  • 앞선 업무 규칙 이외의, 시스템 운영에 필요한 거의 대부분의 것들입니다.
    • 데이터베이스
    • 프레임워크
  • 책의 6부 '세부사항' 파트에서, 이들이 왜 세부사항인지 자세히 논의하긴 합니다만, 이유는 간단합니다.
    • 결국 소프트웨어가 수행하고자 하는 핵심 도메인과 달리,
    • 이들은 프로젝트의 상황에 따라 유연하게 결정되어야 하는 것들입니다.

아키텍처에 경계선 긋기

결론적으로 우리는..

  • 핵심 업무 규칙 / 그렇지 않은 필수 기능들을 잘 분리해야 합니다.
  • 저수준 세부사항고수준 추상화 컴포넌트를 향해 배치되어야 합니다.
    • 즉 세부사항들이 핵심 도메인에 의존하고 있어야 합니다.
    • DIP, SAP를 활용하면, 소스 코드 레벨에서 이것을 가능하게 할 수 있습니다.

소리치는 아키텍처

  • 아키텍처는 시스템 (유즈케이스)을 이야기하고 있어야 합니다.
  • Framework ⇒ 도구일 뿐!
  • POO (Plain Old Object) 기반의 도메인 계층이 중요합니다.
    • POJO를 중시하는 스프링의 설계 이념과도 상통하는 듯 하네요..~~

클린 아키텍처 ⭐️

  • Hexagonal Architecture

  • DCI (Data, Context, and Interaction), BCE (Boundary-Control Entity)

  • 등등의 아키텍처는 모두..

    • 궁극적으로 관심사의 분리를 추구합니다.

    • 공통적으로, 시스템 인터페이스 ↔ 업무 규칙 두 계층을 포함한다.

    • 이를 통해, 아래와 같은 목표를 달성합니다.
      - Framework-Independent Architecture
      - Testability
      - UI-Independent , DB, And Anything Else

      위 구조에서 경계 횡단하는 법

    • 유즈케이스가 프레젠터를 직접 불러서 호출할 수는 없다.

    • 따라서, out 포트를 프레젠터가 구현하게 두고, 의존성 주입해서 굴리는 식으로 코드를 작성하면 됨

    • (사견)

      • 도메인과 영속성 객체 사이의 중복이 매번 생기면, 그건 좋은 설계일까?

      테스트와 Humble 객체

    • 테스트 하기 어려운 행위는 Humble 객체에게 토스할 수 있다.

    • Humble 객체는 유의미한 일을 하지 않는, 쓸모 없는 (humble) 객체

      부분적 경계

    • 클린 아키텍처를 위해 양방향 인터페이스를 두고, DS를 선언하는 것은 비싸고 번거로운 선택지입니다.

      • 현실적인 이유로 매번 처음부터 이런 식으로 설계하는 것은 적절치 않을 수 있습니다.
    • 저자는 현재와 미래를 적절히 절충할 수 있는 몇가지 대안을 제시합니다,,

      1. 마지막 단계 스킵하기 : 같은 컴포넌트 안에서, 아키텍처만 분리해두기
      2. 일차원 경계 : 전략 패턴 쓰기
      3. 파사드 패턴 쓰기

      계층과 경계

      💡 목표를 달성하려면, 빈틈없이 지켜봐야 한다.

      요구사항이 발전함에 따라, 아키텍처의 경계가 필요해지는 시점이 언제인지 적절하게 파악하는 것은 아주 중요합니다. 저자는 이를 위해 항상 프로젝트를 면밀히 지켜보는 것이 중요하다고 강조합니다.

    • 움퍼스 사냥 게임
      - YAGNI vs 나중에 후회하기
      - 보통 오버 엔지니어링이 더 눈물 나온다.
      - 그러나 반대라고 값싼 건 아니다 ..

      Main Component

    • 메인 컴포넌트는 필요한 모든 것을 준비한 후, 고수준의 시스템에게 제어권을 넘깁니다.

    • 메인 컴포넌트는 궁극적인 세부사항 (가장 저수준의 정책)이자, 궂은 일을 담당하는 오브젝트입니다.


크고 작은 아키텍처

  • 서비스는 그 자체로 아키텍처가 아닙니다.
  • 시스템 확장성 및 개발 가능성 측면에서는 유용할 지 모르지만...
  • 행위를 격리하는 것은, 고수준의 정책 ↔ 저수준의 세부사항으로부터 격리하는 것과 관련이 없습니다.

결합 분리의 오류

  • 서비스를 나누면 완벽히 격리된 것 같겠지만 ..
  • 데이터를 주고받게 되면 그 형식에 대해 강한 의존성이 생길 수밖에 없음

개발 및 배포 독립성의 오류

  • 팀 단위의 독립적인 개발, 유지보수, 운영 또한 거품이라고 하네요,, (의존성을 적절히 제어하지 못하므로!)
  • 서비스는 유일한 선택지가 아니다..

야옹이 문제

  • 횡단 관심사에 대해 변경사항이 발생하면 ⇒ 앱 전반에 수정이 필요해짐
  • 어떻게 수정해야 할까요?
    Object To Rescue, 적절한 OCP를 지키는 설계로 문제 해결 가능
    ⇒ 즉 서비스 분리가 되었다고 하더라도, 적절한 의존성 제어를 갖춰야 변경에 대한 내성을 가진다는 의미
  • 서비스로 쪼개진 상황에서도, 각 서비스에서 컴포넌트 구조를 갖춰야 클린한 아키텍처라고 할 수 있겠습니다.

결론

  • 서비스는 우리가 하기 나름에 따라,
    적절한 아키텍처적 선택일수도,
    아무 짝에도 의미 없는 단순한 분리일 수도 있습니다. 😂

빠져 있는 장 : Practice

아래 이미지는 4가지의 코드 아키텍처를 보여주고 있습니다.
왼쪽부터 간단히 설명해보면, 아래와 같습니다.

  1. 계층형 아키텍처
    • 기능 계층을 바탕으로 레이어를 구분했습니다.
    • 간단하고 전통적인 방식이지만, 아키텍처가 도메인을 설명해주지 못한다는 단점이 있습니다.
  2. 기능형 아키텍처
    • 앞선 방식과 달리, 도메인을 기반으로 수직적인 구분을 두는 방식입니다.
  3. 유즈케이스 기반 아키텍처 (포트 앤 어댑터 방식)
    • 핵심 도메인을 분리하고, 의존성을 포트 & 어댑터 방식으로 제어하는 모습입니다.
  4. 컴포넌트 기반 아키텍처
    • 서비스를 컨테이너, 컴포넌트, 클래스 세 가지 계층으로 나누고, 컴포넌트와 관련된 모든 내용을 추상화합니다. (C4 아키텍처 참고)
    • 사용자 인터페이스라고도 할 수 있는 웹 컴포넌트를 제외하면, Order와 관련된 컴포넌트 (영속성을 포함해서)에 대해 전부 추상화를 수행했습니다.

제어자를 잘 사용하자

  • 앞선 예시는 모놀리틱 애플리케이션에 대한 소스 코드 레벨의 아키텍처입니다.
  • 이 때 제어자를 신중하게 사용해야 합니다.
  • 모든 컴포넌트가 public 하다면, 위 네 가지 아키텍처는 본질적으로 차이가 없게 됩니다.
  • 모놀리틱 애플리케이션의 경우, 적절한 제어자 사용을 통해 패키지 관리를 수행해야 아키텍처의 이점을 100% 누릴 수 있습니다.
  • 패키지 수준에서 소스를 나누지 말고, 더 상위의 소스 트리를 아예 분리해버리는 것도 방법입니다. (e.g. gradle에서 프로젝트를 분리해버리기)

0개의 댓글