신규 MSA 전환기

조성권·2023년 1월 9일
0
post-thumbnail

1. 개발 배경

최근, MSA가 각광받으면서 기능 단위로 프로젝트를 분리하여 개발하기 시작했습니다.
올해 새롭게 몸담은 저희 회사 저희 팀 역시 MSA로 구성된 프로젝트로 목적에 따라 분리되어 있었죠!
기존 legacy에서 설계하던 방식인 모놀로식(Monolothic) 설계 방식 대비 MSA의 장점이라 한다면 다음과 같다고 볼 수 있습니다.

MSA 장점
1. 독립성 : 각 기능 별 Micro 단위로 프로젝트가 구성되어 있기 때문에 각 서비스 간, 의존성이 낮습니다.
2. 편의성 : 각 프로젝트가 독립적이기 때문에 빌드 및 테스트 시간이 단축됩니다.
3. 안정성 : 기능 별, 독립적으로 동작하기 때문에 특정 서비스에 장애가 발생할 시, 오류로 인한 피해 범위가 줄어듭니다.

위와 같은 장점도 있지만 무엇이든 과하면 좋지 않은 법이겠죠 :)
과도하게 분리할 경우, 그만큼 관리해야 하는 프로젝트 수가 늘어나기 때문에 유지보수 면에서 인적/물적 비용이 증가할 것이고 통합 테스트를 하는데 있어서 복잡해지는 이슈가 있을 것입니다.

어찌됐든 저희 팀은 MSA로 아키텍쳐가 설계되어 있고 제가 이번에 독립적으로 맡게 된 개인 프로젝트 역시 A+B 기능을 하던 MSA 프로젝트에 대해 B 기능을 별로도 분리하여 새로운 MSA 기반, 프로젝트를 띄우는 것이었습니다.

2. 주요 설계

2-1. API Naming

가장 처음 고민했던 것은 API 네이밍이었습니다. 어떻게 하면 조금 더 이해하기 쉽고 RESTful다운 네이밍을 지을 수 있을까... 구글링도 해보고 팀원분들께 계속해서 피드백을 받으며 고민하는 시간을 가졌고 나름대로 몇가지 원칙을 토대로 작명을 시작했습니다.

2-1-1. 기능에 따라 HTTP Method를 구분하자!

기존에는 POST, GET만으로 모든 API를 구분했습니다. POST는 생성을 위해 사용된다지만 사실 맘만 먹으면 CRUD에 모두 사용할 수 있는 만능이기 때문이죠!
하지만 이러한 방식은 HTTP Method의 의미를 퇴색시킬 뿐만 아니라 URI에서 명확하게 표현이 되어있지 않다면 API의 구체적인 기능을 알기에 난해하다는 이슈가 발생할 수 있다고 판단했기 때문에 "각 기능에 따라 HTTP Method를 확실히 구분해서 만들자!"는 생각을 하게 되었습니다.

2-1-2. 행위를 표현하는 동사형보다는 명사적 표현 사용하자!

앞서 1번에서 언급한 것처럼 HTTP 기반으로 동작하는 RESTful API에서 HTTP Method는 어떤 행위를 하는지에 대한 힌트를 제공합니다. POST(생성), GET(조회), PUT(수정), DELETE(삭제)
그렇기 때문에 우리가 특정 데이터를 찾는다해서 URI에서 다시 한번 ~/retrieve, ~/search, ~/get과 같은 동사적인 표현을 사용하는 것은 HTTP Method의 기능과 충돌하며 중복적인 부분이라고 생각하게 되었죠.
그렇기 때문에 동사적인 표현은 최대한 줄이고 모든 URI는 명사적인 단어로 종결시키는 네이밍 방식을 채택했습니다.

2-1-3. API 버전에 대해 명시하자!

최초 만든 API가 평생 간다는 보장은 없습니다.
고객의 니즈가 바뀌고 이에 따라 필요한 데이터가 바뀐다면 Request/Response 데이터가 달라질 수 있겠죠! 그렇기 때문에 API에 대해 버전을 명시하기로 했습니다.
~/v1/branches/unit, ~/v1/areas/zipcode/unit과 같이 말이죠!
이렇게 명시함으로써 개발자 입장에서 API에 대한 히스토리를 파악하고 운영하는데 보다 편리함을 가질 수 있다고 판단했습니다.

2-2. '가공'과 '전송' 서비스 로직 구분

저는 제휴사와의 주문/배송/클레임에 필요한 API를 개발하는 업무를 하고 있습니다.
그렇기 때문에 제휴사와의 API 통신은 필수불가결한 부분으로 정보를 요청하고 응답받기 위해 항상 내부적으로 Request에 필요한 데이터를 가공하는 로직이 있고 이를 전송하는 부분 이 존재하죠!
기존에는 하나의 Service단 함수 안에서 Request 전문을 위한 데이터를 '가공'하는 로직과 '전송'하는 로직이 함께 존재했습니다.
하지만, 이를 분리한다면 얻는 장점은 무엇이 있을까요?

2-2-1. 단위 테스트 용이

단위 테스트는 본인이 확인하고자 하는 특정 함수를 호출해 Response를 확인하는 방식으로 확인하는 방식으로 이루어집니다.
이때, 만약 하나의 함수 내, 가공전송 로직이 함께 합져져 있는 형태라면 어떨까요?
전송까지 정상적으로 되는지 전체 Flow를 보고 싶었다면 만족스럽겠지만 가끔은 이런 생각이 들 수도 있습니다.
"나는 Request 전문이 어떻게 만들어졌는지만 보고 싶은데 제휴사에 전송까지 하게되서 더미 데이터를 지워달라는 요청을 해야하네.." 라는 생각 말이죠!
위와 같은 경우, 해결할 수 있는 방법은 여러 가지가 있을 것입니다!
디버깅으로 확인하는 방식도 있을 것이구 SLf4J log를 통해 실시간 로그를 확인하는 방법도 있겠죠.
하지만, 가공 영역과 전송 영역이 독립적으로 구분되어 있다면 우리는 JUnit에서 가공을 담당하는 함수 호출만으로도 쉽게 Request 전문을 확인할 수 있을 것입니다.

2-2-2. 로그 분석 용이

가공전송을 분리한다면 로그 역시 분리가 가능해집니다.
호출된 함수 별로 Request전문Response전문을 파악할 수 있다면 우리는 가공 영역에서 유효하지 않은 데이터가 만들어진 것인지, 전송 영역에서 제휴사 통신 중, 이슈가 발생한 것인지 보다 수월하게 파악할 수 있죠!
그리고 빠른 원인파악은 그만큼 빠른 조치로 이어질 수 있으니 분명 훌륭한 장점이라 생각합니다!

2-2-3. 기능 별 독립적인 구조

보통 legacy 코드의 가장 큰 문제는 의존성이라고 생각합니다.
오랜 시간 운영되다보니 서로 얼키고 설킨 구조를 갖게 되고 결과적으로 하나를 고치면 도미노처럼 Exception이 발생하는 이슈가 발생하죠!
이러한 부분에서 서로의 역할에 따라 함수를 독립적으로 구성하는 것은 유지보수 및 신규 인원이 코드를 파악하는데 있어서 시간을 단축시켜주는 장점이 될 것이라 생각합니다.

2-3. Request Scope Bean

또한, 로그 수집에 필요한 주요 기술로 Reqeust Scope Bean을 활용했습니다.
Spring에서 Bean은 다양한 Scope가 존재합니다. Scope 종류에 대한 부분은 제가 이전에 작성했던 글을 통해 참고 부탁드리겠습니다 :)

다양한 Scope 중에, RESTful API의 시작과 종결은 HTTP 통신의 라이프사이클과 동일하기 때문에 Request Scope를 준 Bean을 통해 원하는 값을 담게 되었죠!

이 글을 통해 로깅에 사용된 모든 로직을 설명할 순 없지만 간단히 주요 구간만 설명하자면 크게 Interceptor 구간, Custom Annotation, AOP 구간으로 나눌 수 있습니다.

2-3-1. Interceptor 구간

Interceptor 구간에서 우리가 알 수 있는 것은 크게 호출된 URI와 Request 전문 정도일 것입니다.
우리는 여기서 하나의 API 송/수신 사이클에 대해 구분지을 수 있는 최소 단위 구분값(ex) 영업점 코드)을 저장합니다.
그리고 Request URI를 통해 '영업점 정보'를 바꾸기 위한 것인지 '휴무일'을 바꾸고 싶은 것인지 등을 파악할 수 있기 때문에(API 네이밍을 그렇게 설계함..) 이를 Enum을 통해 관리했습니다.
(추후, 이 값은 각 기능별 저장되는 I/F 테이블을 구분지어 저장하기 위함)

2-3-2. Custom Annotation 구간

Custom Annotation 구간은 로그 데이터를 수집하고 싶은 구간에 대해 마킹을 위한 목적이라고 볼 수 있습니다. 원하는 함수에 마킹을 해놓고 AOP 구간에서 해당 Custom Annotation을 pointcut으로 주면 해당 함수의 Request / Response 데이터를 기반으로 로그 수집이 가능하죠!

2-3-3. AOP 구간

AOP 구간에선 Interceptor 구간에서 수집한 데이터와 Custom Annotation으로 마킹된 함수에서 발생한 데이터를 조합하여 최종적으로 로그 데이터를 가공 및 DB 적재하는 마지막 관문이라고 할 수 있습니다.
앞서, 우리는 Interceptor 구간에서 요청 API URI와 전문을 통해 수집하여 Request Scope Bean에 저장했습니다. 하지만 해당 구간에선 제휴사별, 상세 전문을 확인할 수 없기 때문에 추가적으로 Custom Annotation으로 마킹된 함수에서 Request / Response 데이터를 기반으로 구체적인 전문을 획득하는 과정을 밟게 됩니다.
이렇게 두 구간에서 획득한 데이터를 기반으로 어느 로그 테이블에 데이터를 적재할지, 실제 적재할 데이터가 무엇인지 파악하고 실제 과정을 진행하게 되는 것이죠!

3. 회고

이번 프로젝트를 단독으로 진행하며 느꼈던 점은 제가 아직 부족하다는 것이었습니다.
기존의 것에 너무 익숙해진 나머지 새롭게 구조를 재설계한다던가 새로운 기술을 도입한다던가 하는 틀을 깨는 방안을 스스로 기획하고 설계하는 것을 어려워하는 제 모습을 보면서 안타까움을 느꼈죠 ㅠ
팀원분들의 힘을 빌어 더 단단한 아이디어를 얻고 지속적인 PR을 통해 피드백을 받으며 이번 프로젝트를 완수하면서 조금 더 성장할 수 있었지만 다음 신규 프로젝트가 있다면 그때는,, 제가 스스로 능동적인 아이디어를 떠올리고 새로운 기술을 도입할 수 있는 힘을 기르기 위해 23년도 열심히 달려봐야겠다는 생각을 하게 되었습니다 :)

profile
천천히, 완벽히 배워나가고자 하는 웹 서비스 엔지니어

0개의 댓글