카테고리 없음

TOSS SLASH22 지속 성장 가능한 코드를 만들어가는 방법 요약

voider 2022. 6. 11. 23:47

TOSS SLASH22 지속 성장 가능한 코드를 만들어가는 방법 요약

패키지 구조, 레이어 구조 얘기도 재밌지만 마지막에 나오는 모듈 구조 이야기가 재밌네요.
멀티 모듈 플젝으로 구성해서 비즈니스 레이어 -> 프레젠테이션 레이어로 역류 참조할 수 없도록 구조적으로 만들고
스프링도 모듈로 격리시켜서 비즈니스 레이어가 있는 모듈에서는 오직 순수한 비즈니스 로직만 있게 만드는 구조는 엄청 나네요

토스페이먼츠가 만드는 소프트웨어에 대한 발표.
토스페이먼츠 팀은 코드 품질에 관심을 지속해서 관심을 가지고 확장 가능한 방식으로 코드를 관리하고 있음.
처음부터 최고의 설계, 최고의 품질을 유지하는 게 아닌, 최소 규칙을 지켜 동작하는 소프트웨어를 빠르게 만든다.
코드 한 줄 한 줄에 왜?라는 질문을 하는 것을 중요하다고 생각.

중요하다고 생각하는 방법 중 하나에 대해 얘기하려고 함.

Kotlin으로 작성한 예제 코드

햄버거를 만드는 HamburgerService . 이런 코드를 쉽게 마주할 수 있음.
구현에 관한 내용이 없어서 무슨 일을 하는지 정확히 알 수는 없지만 생성자를 보고 대충 유추해볼 수 있음. 이처럼 생성자는 클래스 생성에 꼭 필요한 것들을 나열해두기 때문에 클래스 의존도, 클래스의 목적을 대충 유추할 수 있게 힌트를 줄 수 있음. 이것만 보고도 반드시 필요한 의존인지, 너무 과한 의존은 아닌지 고민해볼 수도 있다.

코드를 열었을 때 import 문을 얼마나 자주 보나? 요새 IDE는 위 예제처럼 import 문이 길어지만 자동으로 ...으로 요약한다.

import 문을 열었을 때

import 문을 중심으로 어떻게 코드를 개선할 수 있는지 크게 세 갈래로 발표.
- package 구조
- layer 구조
- module 구조

Package 구조

패키지를 구성하는 데에는 다양한 관점이 있을 수 있음.
햄버거 가게를 운영한다고 가정해보면, 햄버거 세트를 어떻게 포장해야 할 지 전략을 짜야 한다.
- 햄버거를 포장지로 감싸고
- 감자튀김을 종이곽에 담고
- 콜라도 용기에 담고
- 고객이 들고 가기 쉽게 손잡이 있는 봉투에 모두 담아야 함.
이런 고민 없이 그냥 큰 봉투에 햄버거 넣고 감튀 넣고 콜라 부으면 어떨까? 그것도 햄버거 세트라고 할 수 있을까?
또한 여기서 끝이 아니라, 패키징 전략은 매우 유연해야 함.
만약 햄버거 가게가 커져서 햄버거 세트를 한 번에 100개씩 포장해야 한다면?
- 각 햄버거 100개를 포장해서 박스에 담고
- 감튀 100개를 포장해서 박스에 담고
- 콜라도 캐리어에 담아줌
이렇듯 패키지는 현재 상황을 계속 점검하면서 전략에 따라 응집하면서 유연하게 관리해야 함.

두 번째 예제

대략적으로 생성자를 통해 무슨 일을 할 지 예측 가능하고, 비즈니스 로직 흐름도 보임.
그럼 이제 import 를 보자.

이걸 보면 어떤 생각이 드는지? 패키지 구조가 올바른가?
위 패키지 구조는 Card에 대한 개념에 대한 응집이 잘 이루어지지 않은 구조임.

CardService 는 개념적으로 카드라는 개념에 속하는 클래스임. 근데 Card, CardPaymentRequest, CustomCard 와 같이 같은 개념에 속한 클래스들을 import 를 통해 사용해야 한다는 점이 아쉬움. import 문 자체도 아주 다른 결로 나뉘어서 매우 분산된 방식으로 패키지가 구성되어 있다는 느낌을 줌.
햄버거로 비교하면 빵, 패티, 치즈가 각각 별도 포장되어 있는 것 같은 느낌.
CardService 생성자에 있는 요소들 모두 카드와 관련된 컴포넌트 같은데 import 문까지 필요한가? 라는 고민을 하게 됨. 이렇게 응집에 대해 고민하면서 패키지를 수정해보자.

변경된 코드

도메인 구조로 패키지 구성을 변경했더니 일단 import 양이 눈에 띄게 줄었음.
카드 관련 개념에 속한 클래스들의 import 문이 없어졌기 떄문
추가적으로 느낄 수 있는 건 CardReader, CardValidator 등 클래스들은 생성자에 존재하면서 import 까지 필요없다는 점. 좀더 풀어서 얘기하면 CardService 가 존재하려면 이 서비스들이 '꼭' 필요한 동시에 가까운 곳에 응집되어 있다는 것을 의미.
기존 패키지가 역할에 따라 나누어졌다면, 현재는 개념끼리 응집되어 있다는 느낌.

이렇게 개념 기준으로 패키지를 응집시켰을 때, 한 개념에 클래스가 너무 많다면 어떻게 해야 하나?
import 를 줄이는 게 중요하니 패키지를 더 나누면 안되는 걸까?
다음 예제를 보면서 얘기.

import 문을 줄이기 위해 한 패키지에 너무 많은 클래스를 몰아넣게 되면 이것 또한 응집이 깨지는 결과를 초래한다. 카드에 대한 개념이 한 곳에서 커지고 있다면 그 속에서 더 구체적인 개념화를 진행하면 된다. (개념화 -> 세부 개념화)
위 예제 import 문을 보면 카드 개념 안에 소유라는 개념을 넣어서 패키지를 구성했다. 이럴 경우 CardServiceimport 문이 생기게 된다. 그렇다고 하더라도 카드 하위에 새 개념이 생기는 것이므로 여전히 응집되어 있다.

이렇게 개념을 구체화하고 응집을 고민하며 import 문에서 신호를 느끼면 더 나은 패키지를 구성할 수 있음.

패키징을 할 때 중요한 요소는 현재 상황임.

Layer 구조

레이어 속에서 코드를 관리할 때 import 문은 어떤 신호를 내뿜을까?
코드로 알아보기 전에 토스페이먼츠의 표준 레이어 핵심 규칙만 얘기함.
- 레이어는 아래로만 참조한다.
- 역류 참조 불가
- layer 건너 뛰지 않기

이제 코드 예제

위 예제를 보면 프레젠테이션 레이어에서 비즈니스 레이어로 객체를 그대로 전달하고 있음. MobilePaymentHttpRequestimport 문을 확인해보면 컨트롤러와 같은 패키지에 위치하고 있다. 이 코드 자체만으로 봤을 때는 특별히 이상할 게 없어보임.

하지만 비즈니스 레이어 코드를 보면 다름.

빨간색 표시된 것을 보면 레이어는 아래로만 흐르고 역류하지 말아야 한다는 규칙을 위반하고 있음. MobilePaymentHttpRequest 는 컨트롤러, 즉 프레젠테이션 레이어에 속한 객체인데 비즈니스 레이어에 속한 클래스 import 문에 존재하고 있다. 이건 비즈니스 레이어 -> 프레젠테이션 레이어로 의존성이 역류하고 있다는 뜻.

레이어 규칙을 지키는 방향으로 개선한다면 어떤 형태일까?

프레젠테이션 레이어에서 개념화된 클래스로 변경하여 비즈니스 레이어로 전달

비즈니스 레이어에서도 더이상 프레젠테이션 레이어를 역류 참조하지 않음

레이어 간 잘못된 참조는 장기적으로는 코드 복잡도를 높이고, 확장에 발목을 잡는 등 병목을 만듦.

이렇게 import 문만 제대로 봐도 코드에 문제가 있다는 신호를 받을 수 있음. import 또한 코드로서, 지속적으로 주의를 기울여야 함.

module 구조

토스페이먼츠 모듈 구조(여기서 모듈은 gradle 프로젝트를 뜻하는 듯)

화살표 방향은 gradle 구성에서 의존하는 방향을 뜻한다.
실제로 런타임에는 Runnable한 모듈을 중심으로 의존성이 주입되어 실행된다고 이해하면 됨.
회색 원은 외부 기능을 확장할 때 새로운 모듈을 만들어간다는 의미를 나타낸다.
이런 모듈 분리는 여러 장점이 있다.
모듈화의 장점
- 기술을 격리할 수 있다
- 모듈 별로 테스트가 가능하다
- 역할과 경계를 뚜렷하게 정의할 수 있

단일 모듈일 때 문제되는 예제 코드

단일 모듈로 작업하게 되면 의도하지 않아도 비즈니스 로직 안에 특정 라이브러리(redis, aws, feign, ..)에 대한 의존이 들어갈 수 있음. 라이브러리의 경우 버전 업데이트, 또는 내부 요구사항에 의해 변경될 수 있다. 이럴 때 비즈니스 로직에 라이브러리가 침투되어 있다면, 비즈니스 로직도 라이브러리 변경에 맞춰 같이 변경해주어야 한다.
이런 라이브러리 의존적인 부분도 import 를 통해 확인할 수 있다.

모듈을 분리해서 기술을 격리하면 외부 라이브러리 의존성 침투를 막을 수 있다.
이렇게 하면 비즈니스 로직에서 특정 라이브러리에 대한 import 를 할 수 없게 된다. 그럼 비즈니스 로직은 더 뚜렷해지고, 라이브러리를 교체해도 비즈니스 로직에 영향을 끼치지 않는다.

다음 예제

위 코드처럼 스토어 클래스가 프레젠테이션 레이어 클래스로 반환하는 로직을 가지고 있다면, 레이어를 역류하는 import 문이 생기게 된다. 이 import 문은 StoreService 외에도 Store 클래스 내부에 같은 import 문이 있을 것.

상황에 따라 선택이 필요하지만, 프레젠테이션 레이어 자체를 모듈화 시켜 격리할 수도 있다.

또한 Spring 자체도 격리하는 설계로 가져갈 수 있다.
스프링을 격리하는 모듈 구조를 살펴보자.

토스페이먼츠 모듈 구조에서 Payments API 도메인을 가진 형태로 존재한다. 또한 API 서빙을 하기 위해 스프링에 관한 의존성을 가지고 있다. 이와 같은 구조에서 도메인 코드가 잘 정리되어 있고 외부 의존성 격리를 잘 해두었다면 어렵지 않게 모듈 구조를 변경할 수 있다.

스프링을 격리한 모듈 구조

도메인 모듈이 Payments API에서 분리되면서 스프링과 의존성이 끊어졌다.
또한 선택에 따라 스토리지 모듈 자체를 캡슐화할 수 있다. Payments API 스토리지 모듈을 런타임에 의존하게 하고, 스토리지 모듈은 도메인 모듈의 명세에 따라 구현체의 역할만 하는 구조로 수정할 수 있다. 이 구조에서 payments API는 오직 도메인 모듈만 알 스토리지 모듈이 어떻게 구성되어 있는지, 어떤 구현체를 쓰는지 아예 모르는 형태가 된다.

그러므로 HTTP 응답으로 JPA Entity를 사용한다거나 도메인이 HTTP 스펙을 알고 있는 문제를 만들지 않을 수 있다.
이런 격리된 환경으로 인해 더 유연하게 비즈니스를 확장할 수 있다.

모듈화로 격리해서 비즈니스 레이어에서 프레젠테이션을 참조할 수 없는 환경을 구조적으로 만듦

또한 비즈니스 로직에서는 스프링에 대한 import 도 불가능하고, 순수한 비즈니스 로직만 남는다.

 

아직 영상 안 보신 분들 있다면 추천드립니다.

재미있네요.

https://www.youtube.com/watch?v=RVO02Z1dLF8