F-lab 멘토링을 하다보면 다양한 주제를 다룹니다. 하지만 아는 내용이 많지가 않네요. 그래서 이번에는 클린 아키텍처에 대해 알아보기로 하였습니디. 멘토님께서 번역글을 소개해 주셨고 프론트엔드에서 적용할 수 있는 클린 아키텍처에 대해 고민해보고 비슷한 예시로 코드를 작성해 보았습니다.
번역글에서 클린 아키텍처를 다음과 같이 정의합니다.
클린 아키텍처는 애플리케이션 도메인과의 근접성(proximity)에 따라 책임과 기능의 일부를 분리하는 방법입니다.
그렇다면 도메인, 근접성에 대해 글을 읽고 나서 정리한 내용을 작성해 보려고 합니다.
제가 생각하는 도메인은 어플리케이션의 주제 단위입니다. 우리는 어플리케이션을 설계하다보면 여러 주제들이 나오게 됩니다. 글에서 예시로 알려주는 쿠키 상점 어플리케이션의 경우 유저, 제품(쿠키), 장바구니, 주문 등 여러 주제들이 융합되어 어플리케이션이 완성됩니다. 이러한 주제들은 외부로 부터 독립적입니다. 예를 들어 유저가 Oauth로 로그인하던지 id, pw로 로그인 하던지 유저 정보는 변함이 없습니다.
또한 도메인은 UI Framework나 서드파티 라이브러리에 영향을 받지 않습니다. 외부로 부터 독립적이고 비즈니스 모델의 핵심적인 요소임을 알 수 있습니다.
어플리케이션은 도메인 바로 위 계층입니다. 해당 계층에서는 유즈케이스를 묘사합니다. 즉, 유저의 행동에 대한 어플리케이션의 동작을 정의합니다.
예를 들어 카트에 추가하기
유즈케이스에 대한 동작을 정의합니다. 유저가 카트에 쿠키를 추가했을 때
어플리케이션 계층에는 포트가 존재합니다. 제가 생각하는 포트는 API 명세와 비슷합니다. 어플리케이션 계층과 외부 계층이 소통하기 위한 규칙을 나타냅니다. 따라서 어플리케이션 계층은 외부 계층을 신경쓰지 않고 기능을 구현합니다. 또한 외부 계층은 어플리케이션에서 필요한 데이터, 동작을 어떻게 전달할 지 포트를 통해 파악합니다.
어댑터는 가장 외부 계층입니다. 어댑터들은 호환되지 않는 외부 기능들을 어플리케이션에서 사용할 수 있도록 하는 동작들을 수행합니다. 이 동작들은 포트에 기반하여 작성될 것입니다. 포트에 어플리케이션이 원하는 것들이 작성되어있기 때문에 외부 기능들이 변경되어도 어댑터 코드만 수정해주면 됩니다.
위 계층들을 나타낸 이미지는 아래와 같습니다.
서비스의 핵심인 도메인이 가장 안쪽에 위치하며 어댑터로 갈수록 외부에 위치합니다. 외부에 위치할 수록 변경이 잦으며 내부에 있을 수록 변경이 되지 않습니다.
번역글에는 쿠키 상점의 코드를 통해 실제 구현을 설명해줍니다. 저는 비슷한 주제인 도서 대여 서비스를 설정하고 실습을 진행하였습니다. 실습을 진행하며 겪었던 시행착오와 느낀점 등을 중점적으로 작성해 보려합니다.
도메인을 설정하는 것은 생각보다 어려운 일이었습니다. 경계를 나누는 것이 어려운 일이었습니다. 저는 도서 대여 서비스의 주요 기능을 3가지로 정했습니다.
이를 바탕으로 유저와 도서라는 도메인을 만들기로 하였습니다. 그런데 고민이 생겼습니다. 유저가 대여한 도서 목록을 어디에 저장할지에 대한 고민이었습니다. 처음에는 각 유저가 대여한 도서는 유저마다 다르니 유저 도메인에 저장해야겠다 생각했습니다. 하지만 이런 방식이라면 유저가 할 수 있는 모든 동작의 결과물이 유저 도메인에 저장되어야 했습니다. 이는 올바르지 않다고 판단하여 도서 도메인으로 옮겼습니다. 하지만 이것도 좋은 해결책은 아니라 생각했습니다. 책은 책일뿐 대여를 하거나 반납하는 동작과 결이 맞지 않기 때문입니다. 쿠키 상점의 예로 들면 Product 도메인에 Cart 도메인의 동작과 데이터를 넣은 꼴이었습니다. 이보다는 도서 대여라는 도메인을 따로 만드는 것이 더 좋은 방식일 것 같았습니다. 이를 통해 도메인의 범위가 너무 넓어서 역할이 너무 많지 않은지 미리 고민해야 이런 실수를 범하지 않겠다라고 생각했습니다.
처음 도서대여를 생각하고 나서 도서 목록을 조회하고 대여, 반납만 하면 되겠지 라고 생각했습니다. 그런데 도서 대여를 어떻게 할지에 대해 고민이었습니다. 첫 생각에는 도서 도메인에 대여, 반납 기능을 연결시키면 되겠지 라고 생각했습니다. 하지만 생각해보니 도서관에서는 이용자들은 책 여러권을 한번에 대여하고 반납하였습니다. 그러면 책 여러권에 대해 각각 대여 호출을 해야하는데.. 점점 생각이 꼬이게 되었습니다. 저는 최고로 단순한 도서 대여를 만들려 했는데 자꾸 무언가 붙었습니다. 그래서 나는 우선 각각에 대해 대여, 반납을 할 것이다라고 정하고 나서야 코드 진전이 있었습니다. 구현하고자 하는 서비스의 기획에 대한 명확한 이해가 있어야 견고한 설계가 가능함을 느꼈습니다.
프론트엔드 개발을 하다보면 많은 라이브러리들을 사용합니다. 특히 tanstack query가 많이 생각났습니다. tanstack query는 서버상태를 다루기 위한 라이브러리인데 어댑터 계층 역할을 하는것이 아닌가 하는 생각을 하였습니다. 외부의 서버와의 통신을 통해 얻은 데이터를 tanstack query가 저장하고 컴포넌트는 별다른 의심없이 도메인 데이터를 조회해 화면을 그린다 생각하였습니다. 이런 큰 계층의 역할을 하는 라이브러리들이 인기를 얻는 것 같다는 생각을 하게 되었습니다.
또한 라이브러리는 계속 변화합니다. 라이브러리를 변경할때 기존 라이브러리에서의 변환이 쉬운지 어려운지 고민하게 됩니다. 만약 라이브러리를 외부 서비스라 생각하고 어댑터를 만들어 사용한다면 라이브러리의 변경에 쉽게 대처할 수 있지 않을까 하는 생각을 하게 되었습니다. 하지만 프레임워크의 경우에는 추가 레이어를 두기 어려운 것이 현실입니다. 그래서 리액트 같이(라이브러리긴 하지만) 고착화된 라이브러리들은 그대로 사용하는 것이 더 낫겠다고 생각했습니다.
매우 작은 기능이었지만 직접 고민하며 작성해보니 쉽지 않은 과정이었습니다. 작게 만들고 계속 수정해 나가는 것이 중요하다고 하지만 첫 설계가 주는 영향은 무시 못할 것이라 생각했습니다. 설계가 한번 이루어지면 변경하기 어려운 계층이 생기기 마련이기 때문입니다. 항상 모든 코드를 도메인, 어플리케이션, 어댑터로 구분할 수는 없지만 해당 코드가 어느 계층에 가까운지 고민해 보는 것이 좋겠다 생각했습니다. 그리고 만약 외부 계층에 가깝다면 계층을 추가하여 추후 변화에 대응할 수 있도록 설정하는 것이 좋겠다는 생각을 하였습니다.