[SLASH 22] 지속 성장 가능한 코드를 만들어 가는 방법
Toss 의 2022년 컨퍼런스를 보며 재미있게 봤던 내용들을 정리하는 글 입니다.
패키지 구조와 계층에 대하여 요즘 생각이 정말 많았는데, 적절한 시점에 아주 좋은 컨퍼런스 영상을 보게되어 행복했다!
1. 지속 성장 가능한 코드를 만들어 가는 방법
예제를 통해 알아보자!
다음 코드는 햄버거를 만드는 가상의 HamburgerService 입니다!
위 코드를 보면 구현에 관한 부분은 파악할수가 없다. 이는 당연하다!
다만 생성자를 통해 의존하는 class를 확인할 수 있고, 대략적인 행동을 예상할 수 있다.
이처럼 생성자를 통해 Class 의존도와 무슨일을 할지 힌트를 얻을 수 있습니다.
연사깨서 한가지 질문을 하셨다.
"여러분은 코드를 열었을때 import문을 얼마나 자주 보시나요?"
나같은 경우 Entity에서 다른 DTO와 같은 곳에 의존성이 있는지 확인할때 자주 살펴보곤 했다.
보통 요즘 Intellij만 해도 import 부분을 접어서 숨겨주곤 한다.
이를 열어보면 다음과 같다...
이전에는 생성자를 통해 해당 Service를 예상했다면, import문을 확인한 후에는 어떻게 느껴지시나요?
이번 발표에서는 import문을 통해 어떤 부분이 아쉬운지, 어떤 방향으로 개선이 가능한지를 확인해보는 중요한 시간이였습니다!
(진짜 너무 좋았던 내용이라 정리중!)
2. Import문을 중심으로 살펴보기
import를 중심으로 3가지 주제를 살펴보자.
1) Package 구조
2) Layer 구조
3) Module 구조
2-1) Package
우선 일상생활을 잠시 생각해보자.
우리가 햄버가 가게에서 포장을 해올때, 햄버거는 포장지로 포장하고, 감자튀김도 종이곽에 담고, 콜라도 컵에 담아 손잡이가 있는 큰 봉투에 햄버거, 감자튀김, 콜라를 담게됩니다.
이러한 포장에 대한 고민없이 그냥 봉투안에 다 몰빵해서 막 담아버린다면...
다음 코드를 살펴보자.
생성자를 통해 해당 Class가 어떤일을 하는지 대략적으로 보이고, addPayment의 로직도 흐름이 보이는것 같습니다.
이에 대한 import 문은 다음과 같습니다.
위 코드에서는 카드에 대한 개념이 응집에 잘 이루어져 있지 않다고 하셨다.
내가 생각해도 Card 와 관련된 Class들이 분산되어 흩어져 있는 느낌이 든다.
어떤 부분들이 문제가 될까? 다음 빨간색 영역들을 확인해보자!
CardService는 개념적으로 카드라는 개념에 속하는 class 입니다.
Card, CardPaymentRequest, CustomerCard 모두 같은 Card라는 개념에 속하는데,
import를 통해 사용해야 한다는점이 매우 아쉬운 점 입니다. 서로 다른 패키지 안에 있기 때문이죠
햄버거와 비교하면, 빵 - 패티 - 치즈 등이 별도로 포장되어있는 것 같달까?
응집에 대해 고민하면서 코드를 조금 수정해봅시다!
같은 Card라는 페키지 안에 최대한 관련된 개념들을 포장하는 것 이죠!
1. 우선 import의 양이 엄청 줄어들었다.
2. Card 관련 개념에 속한 class들의 import 문이 사라졌다.
3. CardReader, CardPaymentProcessor, CardValidator와 같은 class들은 생성자에 존재하면서 import까지는 필요없게 되었다.
즉, CardService가 존재 하려면 이 클래스들이 꼭 필요하면서 가까운곳에 응집되 있다는것을 의미합니다.
개념끼리 더욱 응집한것 이죠!
그럼 하나의 package 안에 너무많은 class가 생기면 어떻게 될까? 우선 다음 코드를 살펴보자.
이또한 응집이 깨지는 결과를 초래하게 된다...
"적절한 시기에 개념을 더 세분화 해야한다!"
카드 소유에 대한 새로운 개념이 탄생하여 새로운 owner package가 추가되었다. 위 코드에서 초록색 음영 영역을 잘 살펴보자!
이렇게 되면 기존의 cardService에 import문이 추가되게 됩니다!
다만 이 경우에는, 아예 다른 결의 package가 아닌, 하위에 새로운 개념임을 인식할 수 있게됩니다.
2-2) Layer
Layer 속에서 코드를 관리할 때 'import'문은 어떤 신호를 보낼까?
우선 Toss의 표준 Layer 규칙에 대해 알아보자!
▶ 순방향으로만 참조하기
위 내용은 사실 영한님의 강의를 들으면서 많이 들었던 내용이라, 나도 구현을 할때 순방향으로만 참조하려 노력한다.
▶ 역방향, 건너뛰기 금지
코드로 알아봅시다~~ 우선 표현 계층부터 살펴보면 다음과 같습니다.
우선 MobilePaymentHttpRequest 는 DTO에 해당될 것이다.
헤당 DTO를 Service로 직접 넘겨주고 있다.
또한 MobilePaymentHttpRequest는 import가 되어있지 않은것으로 미루어 보아 같은 package안에 있을것 입니다.
import도 깔끔하고 좋은것 같습니다.
다음으로는 Business Layer를 살펴봅시다.
아까 위에서 언급한 Layer 규칙중 '레이어는 역방향으로 참조하면 안된다' 라는 규칙이 있었는데, 이에 위반되는 코드 입니다.
이를 import문이 신호를 보내주고 있습니다!!
즉, Service 계층에서 Controller 쪽에 역참조를 하고 있는것이 문제이죠!
그럼 이를 문제가 없도록 변경하면 어떠한 모습일까요? 다음 코드를 살펴봅시다.
Presentation Layer에서 Business Layer에 전달할 떄, 개념화된 Class로 변환하여 전달하게 됩니다.
Presentation Layer는 여전히 순방향으로 흐르고, import문 또한 여전히 깔끔합니다.
Layer의 역류가 없으며, MobileService가 어떤 다른 개념에 의존적인지도 알수 있게 된다.
Layer간의 잘못된 참조는 장기적으로 코드의 복잡도를 높히고, 확장의 발목을 잡거나 문제를 만들게 됩니다.
이렇게 Layer간의 잠재적 문제를 Import문을 통해 신호를 느낄 수 있게 되었습니다.
2-3) Module
module 관계에서 import문이 어떤 역할을 할 수 있을까?
다음은 서버 개발자들이 공유받는 표준 module 구조 입니다.
화살표의 방향은 Gradle의 구성에서 의존하는 방향을 의미합니다.
Runtime 때는 Runnable한 모듈을 중심으로 동작하게 됩니다.
이런식으로 Module을 분리하면, 기술을 격리할 수 있게 되고, 각 Module 별로 테스트가 가능하고, 역할과 경계를 뚜렷하게 정의할 수 있게됩니다.
다음 코드를 살펴봅시다.
만약 단일 module로 작업하게 되면 의도하지 않게 Business 로직안에 특정 라이브러리에 대한 의존이 들어갈수도 있다.
라이브러리 같은 경우 버전 업데이트와 같은 내부 요구사항에 의해 변경될 수 있습니다.
즉 Business 로직에 라이브러리가 침투되어 있으니, 라이브러리의 API등이 바뀌면 당연히 Business 로직에서도 코드를 변경해줘야 한다.
이또한 import문을 통해 확인이 가능하다.
이럴때 Module을 분리하고 격리하면 의존성 침투를 막을 수 있습니다.
Toss의 Module 구조처럼 라이브러리에 대한 부분을 격리하면 Business 로직에서 특정 라이브러리에 대한 import를 할수 없게 됩니다.
독립적인 Business는 그 역할이 더욱 뚜렷해 지고, 추후 라이브러리가 교체 되어도 Business 로직에 영향없이 사용할 수 있게 됩니다.
다음 코드를 살펴보시죠!
만약 위 코드처럼 Store class가 Presentation Layer의 class로 변환하는 로직을 가지고 있다면, Layer를 역류하는 import문이 생기게 됩니다.
이는 단순 StoreService class 말고도 Store 에도 같은 import문이 있을것 입니다.
그냥 Presentation Layer 자체를 Module화 시켜 격리시킬 수 있습니다.
이를 위한 Module 구조를 살펴봅시다.
Payments API는 Domain 을 가지고 있는 형태입니다. 이러한 Module 구조를 다음과 같이 변경해 봅시다.
우선 Domain이 API로부터 분리되었고, Spring과 멀어져 의존성이 격리되었습니다.
또한 Storage 모듈 자체를 은닉할 수 있는데, API가 Storage 모듈을 Runtime에 의존하게 하고, Storage 모듈은 Domain모듈의 명세를 따라 구현체의 역할만 하는 구조로 수정할 수 있습니다.
이 구조에서 API는 오직 Domain 모듈 구조만 알고, Storage에 대해서는 알지 못하게 됩니다.
그러므로 HTTP 응답으로 JPA Entity를 사용한다거나, Domain이 HTTP 스펙을 알고있는 문제를 만들지 않을 수 있습니다.
이구조로 변경된 코드는 다음과 같다.
비지니스 계층에서 Presentation Layer 에 대한 의존성이 없으며, 또한 Spring에 대한 import 또한 불가능 하다.
단일 Module로써 독립적으로 사용할 수 있게된 것 이다!
여기서의 나의 의문점? 아니 그려면 Bean 주입은 전부 수동으로 하나?? Transation은??
다음과 같은 답변을 댓글에서 확인할 수 있었다.