💻 Backend

아키텍처의 보은 feat. hexagonal

thumbnail
hexagonal-arch-4-ports-adapters2.webp
category
💻 Backend
tags
CleanArchitecture
kotlin
Developer
summary
진실은 언제나 하나? 🧞
→ 본 내용은 DDD, Kotlin, SOLID, JPA, DB Migration 등을 포함하지만 아키텍처가 ‘주’입니다.
 

서비스 출시를 앞두고 주어지는 미션 리스트


  • BFF가 아닌 REST로의 인터페이스 개편
  • 안정적인 시스템 구현
  • 데이터베이스 마이그레이션
 
23년도 봄에서 여름으로 향할 무렵, 위와 같은 미션이 주어졌다.
사내 기존 서비스의 아키텍처와 인프라 전면 개편으로 인해 내가 리드를 맡았던 새로운 서비스에서도 급하게 미션이 추가된 상황이었다.
출시를 앞두고 바쁜 조건속에서 정확성을 요구하는 도전이었다.
 

당시 MSA, 당시 아키텍처


우리는 대부분 MSA구조로 운영되고 있다. 대다수 기업들이 선택하는 것처럼 서비스 단위를 작게 도메인 단위로 시스템을 구현해나가고 있다.
이를 위해 다양한 기술 스택을 채택하는 것으로 알고 있는데, 우리는 트래픽이 크지 않고 개발자 수가 부족하다는 점을 고려해서 주로 다음과 같은 스택을 사용했다.
→ Java, SpringBoot3, Mysql5.7, Redis, RabbitMq, resilience4j … 등등
(인프라 관련해서도 개편과 어우러져 할 얘기가 산더미다.)
 
여기서 한가지 정말 다행인 것은 ‘헥사고날 아키텍처’를 사용했다는 점이다.
 
헥사고날 아키텍처에 대해서 설명할 포스팅은 아니기 때문에 관련해서 더 알고 싶다면 링크를 첨부한다.
 
이 포스팅에서 설명할 내용 정도만 간단히 언급하면, DDD를 기본으로 가져가는 것이 좋다는 점과 어댑터와 포트로 분리하여 비즈니스 로직 등 도메인은 지키는 것을 기본으로 한다.
 

첫 미션 해결, BFF에서 벗어나기.


이건 서버 입장에서 난이도 높지 않았다.
정확히는 클라이언트와 협의할 일이 더 많았다고 말하는 게 옳겠다.
 
BFF도 마찬가지로, 이 포스팅에서 설명할 것이 아니기 때문에 링크로 첨부한다.
 
지금까지 서비스는 확장성을 크게 고려하지 않았다.
나는 BFF에 대한 관점이 ‘서버’를 어떻게 정의하고 있는지에 따라 시작된다고 생각한다.
 
예전에는 한번도 고민해본 적이 없는 게 어떤 아이디어를 기반으로 기획이 만들어지고 나면, 오직 이것을 수행하기 위한 방법에 대해서 생각했기 때문에, 일종의 기술 선택권을 가진 사람으로써 고민은 부족했다.
 
클라이언트와 서버 개념은 단순한 만큼 되새길 가치가 있다.
나는 서버를 개발하는 사람이고, 내가 개발하는 도메인은 하나의 큰 객체라고 본다.
이 객체와 소통하기 위한 방법을 가장 단순한 방법으로 풀어내면 된다.
 
당시 API의 응답은 아래와 같았다.
만약 도메인이 company, member 로 구분되었다고 하면,
{
	"companyId" : "50",
	"companyName" : "SAMSUNG",
	"memberId" : "3054",
	"memberAge" : "29",
	...
}
클라이언트에서 홈화면과 같이 다양한 도메인의 데이터가 필요한 경우에 저렇게 모든 도메인의 값을 묶어서 내려주고 있었다.
 
BFF를 벗어나면서 아래와 같이 바뀌었다.
{
	"companyId" : "50",
	"companyName" : "SAMSUNG",
	...
}
아주 간단한데, company는 회사의 정보만 다룬다.
member의 정보가 알고 싶다면 member한테 요청해라.
 
생각해보면 너무 당연하다.
클라이언트는 서버에게 필요한 것을 요청하는데, 서버는 자기 도메인에 대한 내용만 알고 있다.
멤버가 알고 싶으면 멤버 도메인에 요청하는 게 자연스럽다.
 
물론, 그렇다고 해서 화면에 필요한 도메인이 10개가 넘어가는데 10번을 다 조회하는 것은 낭비다.
때문에 BFF를 따로 구축할 필요가 있는 것이고, 여기서 말하는 서버는 BFF서버가 아니다.
 

두번째 미션 해결, 데이터베이스 마이그레이션


해야되는 게 여러 건이 있었는데, 먼저 mysql을 5.7에서 8로 올려야했다.
이 과정에서 rds가 새로 추가되었다.
그리고 mongo 도입으로 이것과 통신할 방법도 필요했다.
 

왜 필요했나

작은 스타트업으로, 현재 개발자들은 처음 mysql 5.7을 도입해서 서비스 개발을 하던 당시의 개발자들이 아니었다.
이게 생각보다 크리티컬 했는데, 히스토리조차 모르는 케이스가 너무 많았기 때문이다.
DB 관련해서도 간혹 이슈가 생기곤 해서 늘 애를 먹었다.
그래서 전면 개편을 실시하면서 1순위로 고려했던 것이 마이그레이션이다.
 

작업 순서

데이터베이스는 늘 떨리고 긴장된다.
학부시절부터 IT기업은 데이터가 자산이고 전부다 라는 말을 들으며 배웠기 때문에 데이터베이스 작업을 할 때는 항상 순서를 정리해서 놓치는 게 없을 지 점검하고 시작하는 편이다.
  1. RDS MySql 8.x 개발계와 운영계로 구분하여 생성
  1. 이중화
  1. flyway 적용
  1. 스키마 떠서 검증
  1. mysql 5.7 인스턴스 백업 후 종료
 
1번과 2번 이중화까지는 이미 경험이 있던지라 수월하게 지나갔다.
첫 걸림돌은 flyway 였는데, 처음 적용해봐서 생기는 일반적인 이슈들이었다.
딱히 트러블 슈팅이라고 할 것은 없고 적당한 시간내에 해결할 수 있었다.
 
문제는 개발계 배포 후였는데..
 

다 돌려놔, 롤백

로그도 남지 않는 원인 모를 이유로 계속해서 기능 몇 가지가 동작하지 않았다.
그게 모두 디비 액세스와 관련된 부분에서 먹통이었다.
5.7과 8버전에서 튜닝해야할 게 따로 있나?
기존 5.7에 히스토리 모르게 튜닝되어있던 게 있나?
등등 별별 생각이 다들면서 둘이서 찾기 시작했다.
 
결말 내지 못한 채 우선 서비스 개발을 해야함으로 미뤄두자! 가 되었고..
이제 곧 출시를 앞두고 마지막 테스트에서 역시나 해결하지 못했다.
환경에 따른 차이가 있을 지 싶어서 기존 운영에서도 트래픽 제한을 걸어서 내부 크루만 들어오도록 한 뒤에 테스트를 진행했다.
근데 여기서도 이슈가 발생한 것이다. → 롤백!
 
밤샘이 시작되었고, 결론은 어이없게도 보안 이슈였다.
보안 솔루션 하나가 새로운 데이터베이스의 특정 path를 공격 시도로 인식하고 주기적으로 차단하고 있던 것이다.
이 원인을 파악하기 까지 얼마나 많은 길을 거쳤는지…
 
 

세번째 미션 해결, 이 모든것에 영향을 준 아키텍처


안정적인 서비스를 만들고 유지하기 위해 BFF에서 벗어나면서 클라이언트와 협의를 거칠 때에도,
데이터베이스를 갈아끼우면서 동시에 롤백을 거칠 때에도,
이 모든 것에 영향을 준 게 바로 아키텍처다.
 

헥사고날

포트와 어댑터라고도 불리는 이 구조를 잠깐 코드로 살펴보면 다음과 같다.
data class Member(
	val id: Int, 
	val name: String, 
	val email: String,
)
아주 간단한 멤버 도메인이 있다고 가정하고,
interface UseCase {
    fun createMember(name: String, email: String): Member
    fun getMember(id: Int): Member
    fun updateMember(id: Int, name: String, email: String): Member
    fun deleteMember(id: Int)
}
이런식으로 유즈 케이스를 만들어둘 수 있으며,
interface MemberPort {
    fun createMember(name: String, email: String): Member
    fun getMember(id: Int): Member
    fun updateMember(id: Int, name: String, email: String): Member
    fun deleteMember(id: Int)
}
여기까지 되고 나면 포트 구현이 끝났으므로 어댑터만 각각 만들어서 끼워넣으면 된다.
이렇게 포트↔어댑터 라고하는 간단한 구조 덕분에 앞, 뒤를 변경할 때 모두 이득을 보았다!
 
컨트롤러를 변경할 때는 유즈케이스가 달라지지 않음으로 유즈케이스 기반의 새로운 컨트롤러를 얼마든지 갈아 끼울 수 있고, 데이터베이스가 달라질 때에도 포트를 기반으로 아무런 문제가 없다 😃
 
notion image
도식화하면 위와 같은 구조가 나온다.
컨트롤러나 레포지토리는 모두 어댑터라는 추상 개념속에 속하는 실제 구현체라고 보면 된다.
그림으로 보면 어댑터를 교체하는 것이 얼마나 쉬운 일인지 알 수 있다!
 

다른 아키텍처는 안돼?

보통 많이 쓰는 Layered 또는 3-tier 아키텍처가 있을텐데, 둘다 의존에 문제가 있다.
쉽게 변경이 가능하다는 것은 곧 커플링이 적다는 이야기인데, 이 2가지 아키텍처는 반대로 커플링이 강하게 맺어져 있다.
때문에 가장 많이 묶여있는 쪽을 변경하게 된다면 (예를 들어 하위 계층) 전체를 다 조금씩 바꿔야할 정도로 일이 커져버릴 수 있다.
 

그럼 헥사고날은 정답인가?

항상 조심해야할 것이 극단적이지 않는 것이다.
마치 수학 문제를 풀다가 해답지를 본 것처럼 정답이라고 맹신하고 무조건 따르는 일은 개발자가 지양해야할 일이다.
다른 아키텍처도 제각각 특성이 있으니, 이를 잘 파악하여 적재적소에 응용하는 것을 추천한다 😄