9 분 소요

비즈니스 로직 (Business Logic) : 시스템이 목표로 하는 비즈니스 영역의 규칙, 흐름, 개념

  • 소프트웨어의 가치는? by Clean Architecture
    • 행위 기치 : 소프트웨어의 기능
    • 구조 가치 : 소프트웨어의 아키텍처 → 소프트웨어를 Soft하게 만드는 것

    → 코드나 설계의 구조를 깔끔히 하는 대신 기능 구현만 목표하면, 엉망이 된 소프트웨어 대처에 더 많은 비용 발생

  • 문제 영역에서 비즈니스 로직을 분석 및 이해하고, 프로그래밍 언어로 잘 표현하는 것이 개발자의 역할
    • 기능이 정확하게 동작하는 것과 더불어, 이해하기 쉽고 변경하기 쉬운 시스템을 만드는 것

관심사의 분리 (Separation of Concerns) : 시스템의 각 영역이 처리하는 관심사가 분리해 관리되어야 함

  • 각 영역은 고유의 관심사에 의해 분리되어 집중되어야 함 → 모듈화, 계층화
    • 비즈니스 로직은 어플리케이션의 핵심 영역이므로, 기술에 영향을 적게 받게 설계해야
    • 기술과 비즈니스 로직을 분리했을 때, 어플리케이션의 복잡성이 낮아지고 유지보수성이 높아짐
    • 비즈니스 로직을 모두가 이해할 수 있게 구조화된 객체 모델로 표현되어야 함

    → 유연하고 확장성 있는 MSA 시스템을 개발하려면, 마이크로서비스의 내부 구조를 어떻게 유연하게 만들지 고민해야!

데이터베이스 중심 아키텍처 (Database Centric Architecture) : 데이터 중심 서비스 구현

  1. 특정한 관계형 데이터베이스에 의존한 데이터 모델링을 수행한다.
  2. 물리 테이블 모델을 중심에 두고 어플리케이션을 구현한다. (예시 : SpringBoot Application)
    • Controller, Service, DTO, Repository & Entity로 어플리케이션을 구성
    • SQL 매핑 프레임워크인 MyBatisJava Persistence APIJPA로 데이터를 처리

데이터베이스 중심 아키텍처에서 비즈니스 로직은 서비스에 존재해야 한다. 그러나,

  • 흐름 제어 로직만 서비스에 존재하고, 비즈니스 개념이나 규칙은 테이블이나 SQL 질의로 존재한다.
  • DTOSQL 질의를 통해 정보를 가져오는 정보 묶음 (Information Holder)의 역할밖에 할 수 없다.
  • 간단한 처리 로직의 경우에는 적합하나, 업무가 다양해지면 점점 복잡성을 제어할 수 없음
  • 업무 개념이 특정 관계형 테이블 데이터베이스의 테이블로 표현되어, 데이터 질의어인 SQL가 필요한 경우가 있음
  • 서비스의 비즈니스 개념과 규칙이 대부분 데이터베이스에 표현 → 성능이 데이터베이스에 의존
    • 데이터가 늘어남에 따라 데이터베이스의 성능은 지속적으로 떨이짐
      • 데이터베이스 서버의 Scale-outSQL 질의문 튜닝에 의존하게 됨

MSA 시스템의 확장성과 유연성을 위한 내부 어플리케이션의 아키텍처 구조

  • 클라우드의 풍부한 자원 환경에서는, 어플리케이션 자체의 성능보단 어플리케이션의 확장성과 유연성이 더 중요하다!

계층형 아키텍처 (Layered Architecture)

  • 티어 (Tear) : 물리적인 장비나 서버 컴퓨터 등의 물리 계층
  • 레이어 (Layer) : 물리적인 티어 내부에서 어플리케이션이 처리할 관심사를 구분하는 논리 계층
    • 프레젠테이션 (Presentation) : 화면 표현 및 전환 처리
    • 비즈니스 로직 (Business Logic) : 비즈니스 개념, 규칙, 흐름 제어
    • 데이터 액세스 (Data Access) : 데이터 처리

계층형 아키텍처를 개발하기 위하여 지켜야 할 규칙!

  • 상위 계층이 하위 계층을 호출하는 단방향성을 유지한다.
  • 상위 계층은 하위의 여러 계층을 모두 알 필요 없이 바로 밑의 근접 계층만을 활용한다.
  • 상위 계층이 하위 계층에 영향받지 않게 구성해야 한다.
  • 하위 계층은 자신을 사용하는 상위 계층을 알지 못하게끔 구성해야 한다.
  • 계층 간의 호출은 인터페이스를 통해 호출하는 것이 바람직하다.
    • 구현 클래스에 직접 의존하지 않게끔 하여 약결합을 유지해야 한다.
  • 의존성 역전의 원칙 (DIP) : ‘유연성이 극대화된 시스템은 코드 의존성이 구체가 아닌 추상에 의존한다.’ → O
  • 개방 폐쇄의 원칙 (OCP) : ‘소프트웨어 객체는 확장에 열리되, 변경에 닫혀 있어야 한다.’ → X
    • 개체의 행위는 확장될 수 있지만, 이때 객체를 변경해서는 안된다. 그러나 상위 계층에서 하위 계층으로 제어의 흐름 (Flow of Control)이 흐르는 계층형 아키텍처에서 소스 코드의 의존성 또한 그 방향을 따를 수 밖에 없다.
    • 상위 계층이 하위 계층의 구체 클래스가 아닌 추상 인터페이스에 의존시키고 그 인터페이스의 구현체를 달리해 의존성을 줄이면서 다형성을 유지할 수 있지만, 인터페이스는 그 계층이 정의하는 추상 특성의 한계를 벗어날 수 없다.

    → 즉, 하위 계층의 유형이 추가되어 확장될 때, 닫혀 있어야 할 상위 계층이 하위 계층이 정의한 특성이 영향받는다!

DIP를 철저히 적용하여 OCP가 가능하게 하는, 의존 관계 역전의 방법 또한 존재한다.

  • 프레젠테이션, 비즈니스 로직, 데이터 액세스을 갖는 3계층 시스템이라 가정할 때,
    • 고수준 영역인 비즈니스 로직이 저수준 영역인 데이터 액세스에 의존한다. (싱위 != 고수준 → 중요도)
    • 그러나 데이터 액세스 계층에서 정의한 인터페이스가 경계를 넘어 비즈니스 로직 계층에 존재하도록 하면,
      • 데이터 액세스의 구현체는 비즈니스 로직 계층의 인터페이스만을 보게 된다.

      → 아례 계층이 위 계층에 의존하게 하는 것이이 의존 관계 역전!

헥사고날 아키텍처 (Hexagonal Architecture)

DIP를 적용한 계층형 아키텍처의 한계?

  • 프레젠테이션, 데이터 액세스 계층만이 아닌 다양한 인터페이스를 필요로 하는 현대 어플리케이션
    • 어플리케이션을 호출하는 다양한 시스템 유형과 어플리케이션과 상호작용하는 다양한 저장소가 존재

→ 단방향 계층 구조가 가지는 근본적인 한계를 넘어서자!

  • 포트 엔드 어댑터 아키텍처 (Port and Adapter Architecture)
    • 저수준의 외부 영역 : 인터페이스 처리를 담당
      • 인바운드 어댑터 (Inbound Adaptor) : 서비스 외부에서 들어오는 요청을 처리
        • REST API를 발행하는 컨트롤러
        • 웹 페이지를 구성하는 스프링 MVC 컨트롤러
        • 이벤트 메시지를 구독하는 이벤트 핸들러 등
      • 아웃바운드 어댑터 (Outbound Adaptor) : 서비스 내부의 비즈니스 로직에 의해 호출되어 외부와 연계
        • 데이터 액세스 처리를 담당하는 DAO
        • 이벤트 메시지를 발행하는 이벤트 클래스
        • 외부 서비스를 호출하는 프락시 (Proxy)
    • 고수준의 내부 영역 : 순수한 비즈니스 로직 표현
      • 외부 영역과 연계되는 포트 (Port)를 가짐 → 어댑터가 포트를 호출
        • 인바운드 포트 (Inbound Port) : 내부 영역의 사용을 위해 표출된 API
        • 아웃바운드 포트 (Outbound Port) : 내부 영역이 외부를 호출하는 방법 정의

→ 고수준의 내부 영역이 외부의 구체 어댑터에 전혀 의존하지 않게끔 한다.

클린 아키텍처 (Clean Architecture)

  • Robert C. Martin : ‘소프트웨어는 행위 가치와 구조 가치를 가지며, 구조 가치는 더 중요하다.’
    • 왜? : 소프트웨어를 더 유연하게 하는 것이 구조 가치이므로
    • 소프트웨어를 유연하게 유지하는 방법? : 구조 중에서 선택할 수 있는 사항들을 오랫동안 여는 것
      • 특히 열어두어야 할 선택 사항은, 중요하지 않는 세부사항
  • 엔티티 (Entity) : 비즈니스 업무 규칙 (사업적으로 수익을 얻거나 비용을 줄일 수 있는 규칙)
    • 모든 시스템에는 해당 도메인의 업무를 규정하는 업무 규칙이 존재 → 시스템 내에서 자동화
    • 업무 규칙과 데이터를 결합하여 엔티티 객체로 만들 수 있음
  • 유스케이스 (UseCase) : 어플리케이션 업무 규칙
    • 자동화된 시스템을 사용하는 처리 절차를 기술
    • 어플리케이션에 특화된 업무 규칙을 표현하며, 엔티티 내의 업무 규칙을 호출해 시스템을 사용하는 흐름을 닫음
    • 엔티티는 프레임워크, 데이터베이스에 의존하지 않고 유스케이스 객체를 통해 조작되는 간단한 객체로 존재해야 함
  • 컨트롤러 (Controller) : 인터페이스 어댑터 (게이트웨어를 통해 연결)
  • 인터페이스 (Interface) : 프레임워크 & 디바이스 (프레젠터를 통해 연결)
  • 엔티티와 유스케이스를 감싸고 있는 나머지 모든 영역이 세부 사항
    • 입출력 장치, 저장소, 웹 시스템, 서버, 프레임워크, 통신 프로토클 등

→ 세부 사항과 유스케이스의 관계를 DIP를 통해 플러그인처럼 유연하게 처리해야 한다.

바람직한 마이크로서비스의 내부 아키텍처 → 클린 마이크로서비스 (Clean Microservice)

  • 마이크로서비스 시스템에서 정의해야 할 마이크로서비스의 내부 구조가 다양할 수 있음
    • 자율적인 마이크로서비스 팀에 의한 폴리글랏한 내부 구조를 가질 수 있기 때문
    • 간단한 기능이면 모노리스를, 복잡한 기능이면 헥사고날/클린 아키텍처의 구조를 기반으로 정의하는 게 바람직

클린 마이크로서비스 아키텍처가 지향해야 할 원칙

  • 지향하는 관심사에 따라 응집성을 높이고 관심사가 다른 영역과는 의존도를 낮추게 해야 한다.
  • 업무 규칙을 정의하는 비즈니스 로직 영역을 다른 기술 기반 영역으로부터 분리하기 위해 노력한다.
  • 세부 기술 중심, 저수준의 외부 영역과 핵심 업무 규칙이 정의된 고수준의 내부 영역으로 구분된다.
  • 고수준 영역은 저수준 영역에 의존하지 않게 해야 하며, 저수준 영역이 고수준 영역에 의존하게 해야 한다.
  • 저수준 영역은 언제든지 교체 및 확장이 가능해야 하며, 이 같은 변화가 고수준 영역에 영향을 줘서는 안 된다.
  • 인터페이스나 추상 클래스를 지원하는 Java의 경우, 구체 클래스가 추상 인터페이스에 의존하는 DIP를 적용한다.
  • 인터페이스는 고수준의 안정된 영역에 존재해야 하며, 저수주느이 어댑터가 이를 구현한다.

클린 마이크로서비스 아키텍처의 구조

  • 내부 영역 (Inbound Area) : 비즈니스 로직을 표현하는 영역

    • 도메인 (Domain) : 내부 영역의 중심부에 존재하는 영역
      • 핵심 비즈니스 개념과 규칙을 구현
      • 엔티티 (Entity)와 값 객체 (VO)를 갖는 에그리거트 (Aggregate)로 존재
    • 서비스 (Service) : 내부 영역에서 도메인을 감싸는 영역
      • 도메인을 호출하여 업무를 처리하는 절차를 기술
    • 서비스 인터페이스 (Service I/F) : 서비스 처리를 위한 인터페이스
      • 외부에서 내부 영역에 존재하는 서비스를 사용할 수 있도록 API를 제공
    • API 프록시 인터페이스 (API Proxy I/F) : 다른 서비스의 API 프록시 호출을 위한 인터페이스
      • 프록시 (Proxy) 패턴 : 프록시에게 어떤 일을 대신하게 함
        • 어떤 객체를 사용할 때, 객체를 직접 참조하지 않고 이에 대응되는 프록시 객체로 대상 객체에 접근
    • 레포지토리 인터페이스 (Repository I/F) : 저장소 처리를 위한 인터페이스
      • 비즈니스를 처리하는 데에 필요한 기본적인 저장소 처리 사항을 추상화해 정의
      • 외부 영역의 저장소 어댑터가 각 저장소에 맞는 저장소 처리 세부 기술로 구현
    • 도메인 이벤트 발행 인터페이스 (Domain Event Publish I/F) : 이벤트 메시지 발행을 위한 인터페이스
      • 도메인 이벤트 (Domain Event) : 어떤 사건에 따른 상태의 변경 사항
      • 하나의 도메인 이벤트를 각 명칭을 갖는 클래스로 구현 → 컨슈머 (Consumer)에 전달되어 발행 (Publish)
  • 외부 영역 (Outbound Area) : 기술 중심의 세부 사항을 의미하는 영역

    • API 퍼플리싱 어댑터 : 클라이언트 ↔ 서비스 인터페이스
      • 내부 영역의 서비스 인터페이스를 호출해 REST API를 발행하는 인바운드 어댑터
      • 명시적인 REST 리소스 명칭을 정의하고, 각 REST 메소드가 의도에 맞게 서비스 인터페이스를 호출
      • 엔티티를 직접 제공하지 않고 API에 맞는 DTO를 생성해 엔티티를 변환 및 매핑해 전달해야 함
    • API 호출 프록시 어댑터 : 프록시 인터페이스 ↔ 다른 서비스
      • 내부 영역에 정의된 프록시 인터페이스를 구현하여 다른 서비스의 API를 호출하는 아웃바운드 어댑터
      • REST API, 소켓이나 SOAP 프로토콜 등 각 기술에 맞는 적절한 통신 방법을 구현해야 함
    • 저장소 처리 어댑터 : 레포지토리 인터페이스 ↔ 데이터베이스
      • 데이터 처리 메커니즘의 선택 → SQL vs OR?
        • SQL 매핑 방식 (MyBatis) : SQL 질의문을 수동으로 작성할 수 있어 세밀한 SQL 제어가 가능
        • OR 매핑 방식 (JPA, Spring Data) : 런타임 시 OR 매퍼가 저장소에 따라 자동으로 질의문 생성
          • 질의문을 수동으로 작성할 필요가 줄어들어 균일한 질의문 품질과 생산성 향상 가능
    • 도메인 이벤트 발행 어댑터 : 이벤트 인터페이스 ↔ 메시지 브로커
      • 도메인 이벤트를 발행하여 송신하는 아웃바운드 어댑터
      • 실제 도메인 이벤트가 생성되는 위치는 내부 영역
        • 에그리거트 패턴을 적용했을 때의 도메인 이벤트는 에그리거트에서 발생한 사건이 됨
      • 도메인 이벤트 발행 어댑터는 내부 영역의 이벤트 인터페이스의 구현체
        • 특정 메시지 큐나 스트림 저장소에 발행하는 역할을 수행
    • 도메인 이벤트 핸들러 어댑터 : 메시지 브로커 ↔ 서비스 인터페이스
      • 발행된 도메인 이벤트를 수신할 수 있는 인바운드 어댑터
      • 외부에서 발행된 도메인 이벤트를 구독해서 내부 영역으로 전달
      • 이벤트의 상태에 따라 적절한 서비스 인터페이스를 호출해서 내부 영역에 이벤트를 전달해야 함

클린 마이크로서비스의 내부 영역에 참고할 만한 패턴

  • 서비스 인터페이스는 외부 영역이 내부 영역에 대해 많이 알지 못하게 함
    • 서비스 인터페이스가 없다면? : 추이 종속성이 발생할 수 있고, 정보 은닉성이 보장될 수 없음
  • 리포지토리 인터페이스, 도메인 이벤트 인터페이스, API 프록시 인터페이스는 DIP를 지원
    • 고수준 영역에 인터페이스가 존재하면, 저수준 영역의 외부 어댑터가 이러한 인터페이스를 구현하도록 해야 함
  • 도메인은 비즈니스 개념을 표현하는 엔티티의 역할울 수행
  • 서비스는 도메인을 활용하여 시스템의 흐름 처리를 수행하는 유스케이스의 역할을 수행

간단한 비즈니스 로직의 처리 → 트랜잭션 스크립트 (Transaction Script) 패턴

  • 비즈니스 개념을 표현하는 도메인 객체가 행위를 가지고 있지 않음
  • 모든 비즈내스 행위, 즉 무언가를 수행하는 책임은 서비스에 있음
  • 서비스가 비즈니스 절차에 따라 절차적으로 도메인 객체를 이용해 모든 처리를 수행
    • 절차식 프로그래밍 방식과 같기 때문에 객체지향 지식이 없어도 일반적으로 쉽게 이해할 수 있는 구조
    • 기존의 데이터베이스 중심 아키텍처에 익숙하다면 더 빠르게 적응할 수 있음
  • 비즈니스가 복잡해질수록 서비스 코드의 양이 늘어남
    • 서비스가 비대해지면서, 도메인 객체는 정보 묶음의 역할만을 수행
    • 비즈니스 로직 처리가 서비스에서 이루어지므로, 비슷한 유스케이스로 중복된 코드가 생겨나 유지보수에 어려움

복잡한 비즈니스 로직의 처리 → 도메인 모델 (Domain Model) 패턴

  • 도메인 객체가 데이터뿐만이 아니라 비즈니스 행위를 가짐
    • 도메인 객체가 소유한 데이터는 도메인 객체가 제공하는 행위에 의해 은닉
    • 도메인 객체는 각 비즈니스 개념 및 행위에 대한 책임을 수행
  • 서비스는 비즈니스 유스케이스를 구현하기 위해 서비스의 행위를 도메인 객체에 일부분 위임하여 처리
    • 서비스의 책임이 도메인으로 적절히 분산되므로 서비스 메소드가 단순해짐
  • 도메인 모델 패턴의 도메인 모델은 객체지향 설계의 객체 모델
    • 각기 적절한 책임을 가진 여러 클래스들로 구성되므로 이해하기 쉽고 관리 및 테스트가 용이
    • 잘 만들어진 도메인 모델은 복잡한 비즈니스 로직의 처리에 유용
    • 잘 정의된 도메인 모델은 코드의 양을 줄이고 재사용성을 높일 수 있음

복잡한 도메인 모델의 단순화 → 에그리거트 (Aggregate) 패턴

  • 도메인 모델링 : 객체간의 관계를 참조로 표현
    • 일대다 (One-to-Many) 관계의 객체를 쉽게 사용할 수 있음
    • 업무가 복잡해지면 참조로 인한 다단계 계층 구조가 생기고, 그로 인해 참조 관계가 복잡해짐

→ 즉, 복잡한 도메인 모델은 모델 내부의 경계가 불명확하다. 이 문제를 어떻게 해결할까?

  • 에그리거트 : 데이터 변경의 단위로 다루는 연관 객체의 묶음
    • Root Entity에서 개념적으로 묶인 엔티티의 집합
    • 1개 이상의 연관된 엔티티와 값 객체 (VO)로 구성된 하나의 묶음 전체
    • 개별 객체 수준의 모델 → 에그리거트 단위의 모델을 통해 도메인 모델을 단순화

에그리거트를 한 단위로 일관되게 처리하기 위한 규칙

  • 에그리거트 루트만 참조한다.
  • 에그리거트 내 상세 클래스를 바로 참조하지 않고 루트를 통해서만 참조해야 한다. 수정할 때도 동일하다.
  • 에그리거트 간의 참조는 객체를 직접 참조하는 대신 기본 키를 사용한다.
  • 기본 키를 사용하면 느슨하게 연관되고 수정이 필요없는 에그리거트를 함께 수정하는 실수를 방지한다.
  • 하나의 트랜잭션으로 하나의 에그리거트만을 생성 및 수정한다.

클린 마이크로서비스의 내부 영역에 참고할 만한 패턴

  • 어댑터는 각각의 인터페이스에 대한 동기/비동기 통신 및 저장소 처리를 작업한다.
    • 외부 영역은 내부 영역에 존재하는 서비스 인터페이스를 사용하는 인바운드 어댑터와,
      내부 영역에서 선언한 아웃바운드 인터페이스를 구현하는 아웃바운드 어댑터로 구성된다.
  • 어댑터는 플러그인처럼 언제든지 교체되거나 확장될 수 있어야 한다.
    • 내부 영역이 먼저 정의된 후에 외부 영역의 세부 사항은 늦게 정의돼도 상관없도록 해야 한다.

Reference