본문 바로가기
source-code/FrontEnd

내가 프론트엔드에 클린 아키텍처를 도입한 이유

by mattew4483 2024. 6. 12.
728x90
반응형

Backgrounds

정부 지원사업 과제 프론트엔드 파트 총괄을 맡게 되면서,

기존 서비스들을 개발하며 느낀 문제점들을 더는 답습하지 않겠다 라는 자그마한 목표를 세웠습니다.

 

Existing Problems

제가 느꼈던 기존 프론트엔드 프로젝트들의 문제점은... 한 문장으로 요약이 가능합니다.

→ 어느 순간부터, 프로젝트를 유지보수하기가 지나치게 어려워진다는 것!

생산성이 끌어올려지지 않은 순간이 찾아옵니다

시간이 지날수록 제품의 유지보수가 어려워지는 이유는 무엇일까요?

 

자동화 테스트의 부재

간단합니다. 기존 코드를 수정했을 때 애플리케이션이 정상 동작하는지 보장할 수 없기 때문이죠!

 

코드 수정으로 인해 기존 동작에 오류가 발생했다는 사실을,

코드를 작성한 뒤 1초 만에 알 수 있다면 어떨까요?

코드를 변경하고 새로운 기능을 추가하는 일이 그리 어렵지만은 않을 것입니다.

 

이는 반대로, 자동화된 테스트가 존재하지 않는 순간

개발자는 수정 사항이 발생할 때마다

애플리케이션의 모든 기능이 정상 동작하는지를 직접 일일이 확인해야 함을 의미합니다.

 

그리고... 프로젝트 규모가 개발자의 수동 테스트 범위를 넘어서는 순간,

생산성은 기하급수적으로 하락하고

서비스에는 뜬금없는 버그가 발생하며

개발자는 코드 수정을 꺼려하기 시작합니다. 

→ 유지보수가 점점 어려워지고 마는 것이죠!

 

business 로직과 view 로직의 결합

그렇다면, 테스트 코드를 작성하면 만사 해결이군요!

맘처럼 안되더랍니다.

아쉽게도 기존 프로젝트 내 모듈들은 테스트 코드를 작성하기 어려웠습니다. 불가능에 가까울 정도로요!

 

그 이유가 무엇인지 생각해 보니...

역시나 간단했습니다. business 로직과 view로직이 지나치게 결합되어 있었습니다!

(business 로직은 서비스 정책, 기대 동작 등의 도메인 논리로,

view 로직은 화면에 어떻게 보일지 결정하는 UI 논리로 이해할 수 있겠죠)

 

import { useState } from 'react';

export default function MyApp() {
  const [posts, setPosts] = useState<Post[]>([]);

  // 어쩌고 저쩌고 도메인 논리
  // ex) 게시글을 작성을 완료하면 게시글이 추가되고, 알림이 뜬다
  // (...)
  
  return (
    <div>
      <h1>post</h1>
      <MyButton count={count} onClick={handleClick} />
      <MyButton count={count} onClick={handleClick} />
 
      // 어쩌고 저쩌고 UI 논리
      // (...)
    </div>
  );
}

 

모든 프론트엔드 프로젝트에서 React를 사용했었고,

대부분의 React 컴포넌트들은 위와 같이 작성되어 있었습니다.

→ 하나의 컴포넌트 내부에 도메인 정책(비즈니즈 로직)과 UI(뷰 로직)가 함께 존재하는 형태로 말이죠!

 

이로 인해, 도메인 로직을 테스트하고자 할 때도 React 컴포넌트를 테스트해야만 했고,

이때 각 React 컴포넌트들은 (React를 포함한) 외부 모듈, 라이브러리 등에 강하게 의존했기 때문에

테스트 묶음 당 필요한 mock과 spy가 기하급수적으로 늘어나게 되었으며,

이는 자연스레 단위 테스트 난이도의 증가를 야기하게 되었습니다.

 

Solutions

즉, 해결 과제는 다음과 같이 요약할 수 있습니다.

business 로직과 view 로직을 분리하고, 테스트 코드를 작성해, 유지보수를 용이하게 한다.

이를 어떻게 달성할 수 있을까요?

 

Software Architecture

저는 높은 수준의 정책과 낮은 수준의 세부사항을 분리하는 것을

첫 번째 단계이자, 본질적인 해결책으로 진단했습니다.

→ 높은 수준의 정책을 낮은 수준의 세부 사항으로부터 격리시켜,

낮은 수준의 세부 사항을 바꿔도 높은 수준의 정책에는 아무런 영향이 없도록 하는 것이죠.

 

 

이때 높은 수준의 정책은 앞서 이야기 한 business 로직으로,

낮은 수준의 세부 사항은 UI, BackEnd Server, Store(상태 저장소), third-party library 등으로 이해할 수 있을 텝니다!

 

소프트웨어에서, 이러한 분리와 격리를 만드는 주요한 수단은 '추상화'라 할 수 있습니다.

추상화를 통해 높은 수준의 정책(본질)은 증폭하고,

낮은 수준의 세부 사항(지엽적인 부분)은 분리해서 격리시키는 것이죠.

낮은 수준의 세부사항은, 어디로도 나갈 수 없어야 합니다!

만약 혼자서 개발하는 프로젝트였다면, 자신이 가장 이해하기 편한 방법으로 이를 구현하면 되겠죠.

하지만 모든 프로젝트는 협업이라는 과정을 거쳐야 하며,

따라서 최대한 많은 사람들이 이해할 수 있는, 공통의 개념을 사용해 프로젝트를 추상화할 필요가 있습니다.

Software Architecture를 도입, 해당 구조를 따라 모든 코드들을 작성하기로 결정했습니다.

 

Frontend Software Architecture

사실 소프트웨어 설계의 중요성은 오랫동안 논의되어 왔으며, 그 결과 수많은 아키텍처들이 존재합니다.

프론트엔드 역시 MVC, MVVM 등, 다양한 아키텍처들이 존재합니다.

 

이러한 후보군들을 비교하고, 조금씩 적용해 본 결과...

저는 포트와 어댑터 아키텍처,

즉 최근 헥사고날 아키텍처(Hexagonal Architecture)로 널리 알려진 설계 방법론을 적용하기로 결정했습니다!

 

헥사고날 아키텍처

헥사고날 아키텍처가 무엇인가 에 대한 설명은 아주 쉽게 찾을 수 있습니다.

https://engineering.linecorp.com/ko/blog/port-and-adapter-architecture

 

지속 가능한 소프트웨어 설계 패턴: 포트와 어댑터 아키텍처 적용하기

들어가며 헥사고날 아키텍처(Hexagonal Architecture)로 더 잘 알려져 있는 포트와 어댑터 아키텍처(Ports and Adapters Architecture)는 인터페이스나 기반 요소(infrastructure)의 변경에 영향을 받지 않는 핵심 ..

engineering.linecorp.com

 

헥사고날 아키텍처의 가장 큰 목표는

애플리케이션의 핵심 비즈니스 로직을 외부의 다양한 인터페이스(입출력, 데이터베이스, 웹 등)로부터 분리하는 것입니다.

이를 위해 소프트웨어를 도메인(Entity) / 애플리케이션(Use Case) / 외부 인프라(Infrastructure)로 분리합니다.

 

이때 높은 수준의 정책인 도메인과 애플리케이션은 Port에만 의존하며,

낮은 수준의 세부 사항인 외부 인프라는 오직 Adapter를 통해서만 해당 Layer에 접근할 수 있습니다.

→ 여기서 가장 중요한 핵심은 의존성이 방향이 바깥쪽에서 안쪽으로만 가능하다는 것!

 

왜 헥사고날 아키텍처인가?

그렇다면, 해당 아키텍처를 신규 프로젝트에 도입한 이유는 무엇일까요?

 

물론 가장 큰 이유는 

최초 목표였던, 'business 로직과 view 로직을 분리하고, 테스트 코드를 작성해, 유지보수를 용이하게 한다'를

가장 효과적이라 생각했기 때문입니다.

 

헥사고날 아키텍처에서는, 도메인과 애플리케이션 수준에서는 어떠한 외부 종속성도 가질 수 없습니다.

심지어 React 조차 말이죠!

 

따라서 도메인과 애플리케이션 Layer는 

입력을 받고, 특정한 동작을 수행하고, 값을 반환하는 함수들의 집합으로 구성되며,

해당 Layer에 작성된 business 로직 역시 단위 테스트를 작성하기 크게 어렵지 않습니다.

(외부 의존성이 없기 때문에, 복잡한 Mock이나 Spy가 필요하지 않고 → 이는 테스트 난이도 하락으로 이어지겠죠!)

 

언제든 교체 가능한 외부 인프라

헥사고날 아키텍처가 유지 보수에 큰 도움을 주리라는 가장 큰 확신은...

외부 인프라(Infrastructure) 계층을 언제든 교체 가능한 것으로 보는 아키텍처 철학에서 비롯되었습니다.

 

앞서 설명한 바와 같이

헥사고날 아키텍처에서 Infrastructure 계층은

프로젝트 고수준 정책(Domain, Application)과 오직 Adapter를 통해서만 의존할 수 있습니다.

즉, 고수준 정책에서 정의한 Port의 인터페이스만 구현했다면, 어떠한 Adapter라도 언제든지 변경할 수 있는 것이죠!

 

그리고 이러한 특징이 개발해야 했던 정부 지원 사업 과제에 큰 이점을 가지지라 생각했습니다.

 

해당 프로젝트에는

1) 자연어 생성 모델 2) 미용 메뉴 추천 모델 3) 챗봇 4) 백엔드 서버 5) 기존 앱 서비스 등 

수많은 third-party-library들이 필요한 상황이었습니다.

 

이 들 중에는 개발이 완료된 것도 있지만

대부분 연구가 진행 중이거나, 성능 지표를 위해 더 나은 모델로 변경할 가능성이 컸습니다.

따라서... 언제든지 해당 구현체가 변경될 수 있는 상황이었던 것이죠!

레고 블럭처럼, 구현체를 언제든 갈아끼울 수 있어야 했습니다.

따라서 헥사고날 아키텍처를 통해 Infrastructure의 구현은 얼마든지 교체 가능하도록 유지하며

비즈니스 로직은 오직 Port에만 의존해, 기본적인 동작과 테스트를 수행할 수 있도록 했습니다.

→ 추후 연구가 완료되었을 때 Infrastructure 계층을 얼마든지 수정할 수 있게 만든 것이죠!

 

명확히 정의된 도메인 정책

연구 개발의 특성상, 결과물이 수행할 동작이 사전에 명확하게 정의되어 있었습니다.

따라서, 요구 사항에 대한 비즈니스 로직은 변경 가능성이 적은 상태였죠.

 

게다가 각 비즈니스 로직은 상황별로 복잡한 use case와 상태를 갖고 있었기 때문에

Domain, Application에 비즈니스 로직을 미리 작성하고, 테스트할 수 있다는

헥사고날 아키텍처의 장점을 극대화할 수 있겠다 판단했습니다.

 

프론트엔드에서, 헥사고날 아키텍처를 어떻게 구현할 것인가?

다음 게시글에서 계속됩니다.

728x90
반응형