본문 바로가기
Legacy to Microservices

[Legacy to Microservices] JPetStore-6 웹 앱의 마이크로서비스 전환기

by 뿔난 도비 2025. 2. 15.
반응형

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) 개발 과정에서 내가 제공하는 인터페이스와 내가 필요로 하는 인터페이스를 꼭 명시할 것!

  • 이것은 협업을 위해서 정말 필요한 내용이었는데, 내가 이 조건을 정말 정말 강조하였기 때문에 빨리 분할을 수행할 수 있었다.
  • 이것은 주저리 주저리 말로 설명하는 것보다는 아래와 같이 그림으로 설명하는 것이 좋아 보인다.

Account service 개발을 수행할 때, 내가 미리 설계한 인터페이스들을 명시
Account service 개발을 수행할 때, 내가 필요로 하는 인터페이스 명시

  • 그런데 자세히 보면 내가 제공할 인터페이스의 경우 인자가 빠져 있다.
  • 그 이유는 프로젝트가 작기 때문에 그 사람이 작성하고 있는 컨트롤러를 보면 바로 알 수 있었기 때문이다.
  • 컨트롤러의 인자에는 무슨 데이터인지 바로 알 수 있게 변수명을 잘 작성했고, 혹여나 어려운 내용이 있으면 주석으로 간단하게 남겨놓았다.
  • 또, 리턴 타입 역시 세션에 담기는 데이터와 실제로 반환되는 데이터 두 가지 종류가 공존하기 때문에 명세를 해놓아도 컨트롤러를 한 번씩 확인해야 한다. 따라서 명세에 너무 공을 들이면서 시간이 소비되지 않도록 적절하게 조절을 했다고 보면 된다.
  • 두 번째 그림과 같이 필요로 하는 인터페이스의 경우 해당 서비스를 개발하는 사람에게 직접 전달을 했다.

 

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

 

반응형