2025년 1월 1일부터 레거시 모놀리식 웹 앱인 JPetStore-6 웹 앱을 마이크로서비스로 전환하는 프로젝트를 진행하고 있다. 이 웹 앱의 경우 오픈 소스 웹 앱으로 비교적 접근이 쉽고 지금 내가 해왔던 연구들에서도 계속 실험 대상 웹 앱으로 쓰고 있기 때문에 익숙하다. 매번 새로운 것을 만들어내려고 하는 것보다는 이렇게 있는 것을 내가 가진 지식을 접목해 목표를 설정하고 달성해 나가는 것이 더 중요한 프로젝트라는 생각이 든다.
JPetStore-6 웹 앱의 마이크로서비스 전환기
프로젝트 소개
- 먼저, 프로젝트는 두 명이서 시작했다.
- 내가 아이디어를 계획했고, 최근에 같이 일을 하고 있는 학부 연구생에게 제안을 하게 되면서 실행에 옮기게 되었다.
- JPetStore-6의 경우 내가 제안한 여러 마이크로서비스 식별 기법으로 마이크로서비스들을 식별하면, 총 네 가지의 마이크로서비스가 식별된다.
1) Account service: 사용자 계정을 관리하는 기능을 제공한다.
2) Order service: 주문 관련 기능들을 제공한다.
3) Product service: 상품에 관한 정보를 제공한다.
4) Cart service: 장바구니 관련 기능들을 제공한다.
- 기법에 의해 서비스별로 어떤 컴포넌트가 속해있는지 식별이 완료되었기 때문에 식별 결과를 토대로 서브 모듈을 구성했다.
- 하지만, Common component와 같이 모든 서비스에 속해 있어야만 하는 컴포넌트들도 있고, 필요에 따라 여러 서비스에 중복된 컴포넌트가 존재할 수 있음을 알고 있기 때문에 명확하게 정의하고 시작하지는 않았다.
- 예를 들어, DTO의 역할을 하는 컴포넌트들의 경우 다른 서비스로부터 데이터를 넘겨받을 때 해당 서비스에서 정의된 DTO가 데이터를 받는 서비스 쪽에서도 정의되어 있어야 한다.
프로젝트 목표
- 현재 글을 작성하고 있는 시점에서는 초기 목표보다 더 많은 목표들이 생겼는데, 아무래도 앞으로 진행하면서도 끊임없이 요구사항이 추가될 것이라고 생각하고 있다.
- 일단 최초에 설정된 목표는 다음과 같다.
1) Stripes framework를 걷어내기
- 해당 프레임워크를 걷어내는 이유는 추후 SSR에서 SPA로 전환이 복잡해지기 때문이다.
- 현재는 SSR 형식이지만 실제 마이크로서비스로 가게 되면 서버는 데이터만 제공해주고, 페이지 구성은 클라이언트 쪽에서 해아한다.
- 그 이유는 클라이언트의 종류가 다양해지면 서버 역시 다양한 종류에 맞는 페이지를 제공해주어야 하는데 그러면 중복된 코드가 발생하기도 하고 확장성이 떨어진다.
- Stripes framework의 경우 JSP에서 Controller 역할의 ActionBean 클래스에게 요청을 전달할 때, ActionBean의 메소드를 호출하는 형태이다.
- 또한 ActionBean이 통째로 세션에 저장되기 때문에 싱글톤으로 사용되는 일반적인 Controller와는 차이가 있다.
- 또 Form에서 전달된 입력값들을 받기 위해서는 ActionBean에 동일한 이름의 필드와 Setter가 필요하다. 또, JSP에서 ActionBean에 있는 값을 사용할 수 있는데, 이를 위해서는 Getter 역시 정의되어 있어야 한다.
- 그렇기 때문에 둘의 의존성이 너무 강하다. 나는 이것이 맘에 들지 않아서 걷어내기로 했다.
- 그리고 일반적인 Spring-Web-MVC에서 Controller를 만드는 형태로 변경하기로 목표를 설정했다.
2) Session은 분할하지 않고, 서비스들이 공유한다.
- 이런 목표를 설정한 이유는 내가 마이크로서비스의 API를 식별하는 연구의 프로토 타입을 수행했을 때로 거슬러 가야 알 수 있다.
- 그 당시에 마이크로서비스 식별 기법에 의해 식별된 마이크로서비스들을 실제로 설계하기 위해서는 그 다음 단계인 API 식별 혹은 인터페이스 식별 연구가 필요했다.
- 그런데 곰곰히 생각해보면 이것은 아주 간단했는데, 기본적으로 서비스에서 다른 서비스의 컴포넌트를 호출할 때, 그 메소드의 인자와 리턴 타입이 API로 식별되는 원리였다.
- 하지만, 모놀리식의 경우 여러 가지 웹 앱의 공유 공간을 사용하기 때문에 위와 같이 호락호락하지는 않았다.
- 메소드의 인자에는 없어도 메소드 내부에서 세션 공간으로부터 어떤 정보를 가져온다면 실제로는 해당 정보도 호출하는 쪽에서 전달해줘야 하는 것이었다.
- 이 작업은 AOP를 이용해서 추적하는 건 쉬웠지만 웹 앱마다 다르기 때문에 까다로운 점들이 존재했다.
- 그러다가 여러 서비스들이 세션을 외부 공간에서 공유할 수 있다는 것을 알게 되었다.
- 원래는 웹 앱 내 공유 공간까지 쪼개는 연구였다면, 세션을 공유할 수 있다면 굳이 쪼갤 필요가 없었다.
- 그래서 언젠가는 이 기술을 꼭 써봐야지 하고 생각했고, 최근에 TrinityParser라는 웹 앱을 만들 때, 백엔드 서버를 다중화하고 세션을 공유하는 형태로 적용했다.
- 하지만, 조금 억지스러운 형태라 마음에 들지 않았고 이번 프로젝트에서 제대로 적용하게 되었다고 할 수 있다.
- Redis를 사용하면 세션 공간을 외부에 두고 여러 서비스들이 공유할 수 있기 때문에 모든 서비스들이 하나의 Redis를 바라보고 있는 형태이다.
- 이 형태의 가장 큰 단점은 Redis가 단일 장애 지점이 될 수 있다는 것인데 Redis의 경우 고가용성을 보장하기 위해 좋은 기술들을 제공하기 때문에 괜찮다고 생각한다.
3) 각 서비스는 저마다의 JSP를 제공한다.
- 이것이 정말 특이한 점이라면 특이하다고 얘기할 수 있다.
- 내가 수행한 연구들이 기본적으로 뷰들까지도 분할하기 때문에 나도 그것에 맞게 각 서비스들이 자신들에게 알맞는 뷰들을 제공하도록 했다.
- 아쉬운 점은 JPetStore-6의 경우 중심이 되는 JSP에서 헤더와 바텀을 Include하는 형태이다.
- 그런데 이 헤더와 바텀이 모든 서비스에서 공통이기 때문에 각 서비스에 이 헤더와 바텀은 중복으로 포함되어 있어야 한다.
4) 개발 과정에서 내가 제공하는 인터페이스와 내가 필요로 하는 인터페이스를 꼭 명시할 것!
- 이것은 협업을 위해서 정말 필요한 내용이었는데, 내가 이 조건을 정말 정말 강조하였기 때문에 빨리 분할을 수행할 수 있었다.
- 이것은 주저리 주저리 말로 설명하는 것보다는 아래와 같이 그림으로 설명하는 것이 좋아 보인다.
- 그런데 자세히 보면 내가 제공할 인터페이스의 경우 인자가 빠져 있다.
- 그 이유는 프로젝트가 작기 때문에 그 사람이 작성하고 있는 컨트롤러를 보면 바로 알 수 있었기 때문이다.
- 컨트롤러의 인자에는 무슨 데이터인지 바로 알 수 있게 변수명을 잘 작성했고, 혹여나 어려운 내용이 있으면 주석으로 간단하게 남겨놓았다.
- 또, 리턴 타입 역시 세션에 담기는 데이터와 실제로 반환되는 데이터 두 가지 종류가 공존하기 때문에 명세를 해놓아도 컨트롤러를 한 번씩 확인해야 한다. 따라서 명세에 너무 공을 들이면서 시간이 소비되지 않도록 적절하게 조절을 했다고 보면 된다.
- 두 번째 그림과 같이 필요로 하는 인터페이스의 경우 해당 서비스를 개발하는 사람에게 직접 전달을 했다.
5) 서비스 간 통신에는 HTTP를 사용한다.
- 이것은 기술적 한계 때문이라고 볼 수 있다.
- 메시징 방식을 사용하는 것도 고려를 했으나, 최신 기술 도입에 너무 힘을 쏟기 보다는 일단 전환을 하고 바꿔나가는 쪽으로 결정했다.
- 현재는 분산 트랜잭션에서 Kafka를 도입해서 보상 트랜잭션을 하는 등 도입을 시도하고 있는 중이다.
이렇게 다섯 가지의 목표를 가지고 시작한 프로젝트이다.
전환
- 현재는 순조롭게 전환은 끝낸 상태이다.
- 본 섹션에서는 전환 과정에서 예기치 못한 어려움 몇 가지를 소개한다.
- 나는 이런 서브 모듈 단위 개발을 몇 번 겪어봐서 그런지 어렵지 않았는데 팀원은 환경 설정으로 조금 어려워해서 개발이 늦춰졌다.
- 그래서 기술적인 부분에서 어려움은 별로 없었다.
1) 세션에 담길 도메인들은 모든 서비스에서 정의되어 있어야 한다.
- 이 사실은 정말 개발 마지막 단계에서 깨달았는데, JPetStore-6의 로직을 그대로 따르다가 알게 된 사실이다.
- 해당 웹 앱에서는 주문을 진행할 때, 주문 객체를 세션에 담아둔다.
- 그러고 나서 주문 단계 별로 계속 세션에 저장된 주문 객체를 업데이트하면서 주문을 진행하고, 주문이 완료되면 세션에서 해당 객체를 제거하는 식이다.
- 그런데 주문을 수행하고 나면 메인 페이지로 돌아와야 하는데 메인 페이지에서 계속 역직렬화 오류가 떴다.
- 한참을 고민하다가 알은 사실인데, Redis에 담겨 있는 객체들은 모든 서비스에서 역직렬화 할 수 있는 상태여야 한다.
- 따라서 다른 서비스에서 Order 객체를 Redis에 담았으면, 다른 서비스에서는 이것을 꺼내지는 않더라도 역직렬화를 할 수는 있어야 한다.
- 즉, 다른 서비스에서도 Order 객체를 지니고 있어야 한다.
- 이로 인해서 거의 대부분의 도메인 객체들은 모든 서비스에 포함되도록 구성되었다.
- 아래는 해당 내용을 기록한 이슈이다.
- https://github.com/Microservice-Dev/JPetStore6-microservices/issues/14
Redis에서 역직렬화 오류와 ClassNotFound 오류가 날 경우 · Issue #14 · Microservice-Dev/JPetStore6-microservices
만약 Order service에서 Order 객체를 세션에 넣은 상태에서 Account service에 접속할 시, Account service에서 역직렬화를 할 수 있어야 하기 때문에 Order 클래스를 찾는다. 하지만 Account service에 Order 클래스
github.com
2) Form 태그에서 POST 요청으로 넘긴 데이터는 Body에 있는 것이 아니다.
- 와,,, 이건 진짜 당황했다.
- 기존에 API를 설계할 때 Controller의 바디에서 데이터를 가져오기 위해서 @RequestBody 어노테이션을 사용하는 것은 다들 알고 있을 것이다.
- 그래서 나도 그렇게 했는데, 이상하게 자꾸 Form에서 보낸 내용들을 받아올 수가 없었다.
- 한참의 서치 끝에 알아냈는데, Form에서 보낸 요청은 기본적으로 Urlenocded이기 때문에 URL에 포함되어 있다.
- 따라서 @RequestParam으로 받아야 했다.
- 나는 이게 마음에 들지 않아서 JSON으로 보내고 싶었는데, 기본적으로 Form 태그에서 지원하지 않았고 그렇기 때문에 내가 JS를 작성해야 했다.
- 하지만, 이것은 조금 아닌 것 같아서 @RequestParam으로 데이터를 받아오도록 구현했다.
- 아래는 해당 내용을 기록한 이슈이다.
- https://github.com/Microservice-Dev/JPetStore6-microservices/issues/2
Form에서 넘긴 input을 받을 때 @RequestBody 쓰지 말것 · Issue #2 · Microservice-Dev/JPetStore6-microservices
@RequestBody의 경우 application/json 요청을 매핑할 때 사용된다. Form은 기본적으로 콘텐트 타입을 x-www-form-urlencoded 로 사용하기 때문에 @RequestBody 어노테이션을 사용할 경우 정상적으로 요청의 바디에
github.com
3) Redirect 시에 파라미터를 전달할 수 있다.
- JPetStore-6의 경우, 어떤 에러가 발생하면 이전 페이지로 리다이렉트 한 후 메시지를 출력한다.
- 기존의 웹 앱은 에러 처리가 부실했기 때문에 내가 로직을 추가하면서 고민했던 내용이고, 팀원이 방법이 있다고 해서 찾아보다가 알게 되었다.
- 그 방법은 리다이렉트를 발생시킬 컨트롤러에서 RedirectAttribute를 인자로 받아 setAttribute(key, value)로 데이터를 담는다.
- 그러고 Redirect를 발생시키면 URL에 파라미터가 삽입된다.
- 사실 URL에 파라미터를 담으면 해결되는 문제이기도 하다!
- https://github.com/Microservice-Dev/JPetStore6-microservices/issues/13
Redirect 시 파라미터를 전달하고 싶은 경우 · Issue #13 · Microservice-Dev/JPetStore6-microservices
RedirectAttributes에 파라미터를 담을 수 있다. 이 경우 url에 쿼리 스트링으로 값이 담긴다. 컨트롤러에서 RedirectAttributes를 인자로 받는다. 메소드 내부에서 redirectAttributes.addAttributes("key", "value"); 로
github.com
- 아무래도 가장 큰 당황스러움은 1) 어려움이었다.
- 그러면서 느낀점은 마이크로서비스를 식별할 때, 도메인 객체나 DTO에 대해서 분할을 논하는 것이 의미가 있는지 잘 모르겠다는 것이다.
추후 목표
- JPetStore-6에는 딱 하나의 시나리오에서 분산 트랜잭션이 필요하다.
- 따라서, 이것을 해결하고 있는 과정이며, 그 해결 과정에 대해서 다음 게시글에서 쓸 예정이다.
- 그 외 목표하고 있는 것들은 다음과 같다.
1) JSP를 제거하고 클라이언트를 따로 만들기
2) Gateway를 두어 인증 관리와 모듈간 의존성을 떨어뜨리기
3) 벤치마킹 서비스를 추가하여 응답 속도를 비교할 수 있도록 구현하기
4) 변하지 않는 데이터들을 상대로는 캐싱을 할 것
- 현재는 2), 3)이 분산 트랜잭션 이후의 목표이다.
프로젝트 주소
https://github.com/Microservice-Dev/JPetStore6-microservices
GitHub - Microservice-Dev/JPetStore6-microservices
Contribute to Microservice-Dev/JPetStore6-microservices development by creating an account on GitHub.
github.com
'Legacy to Microservices' 카테고리의 다른 글
[Legacy to Microservices] 분산 트랜잭션 설계 최종 (2) | 2025.03.05 |
---|---|
[Legacy to Microservices] 설계의 변경 (0) | 2025.02.25 |
[Legacy to Microservices] 분산 트랜잭션에서 상대 서버가 정상 응답을 제공하지 않는 경우, 재요청 지연시키기 (0) | 2025.02.16 |
[Legacy to Microservices] JPetStore-6 개선: CSRF 토큰을 JSP에 추가하기 (0) | 2025.02.16 |
[Legacy to Microservices] JPetStore-6에서 주문 처리 분산 트랜잭션 구현 (0) | 2025.02.15 |