스프링캠프 2024 - 실전 MSA 개발 가이드(김용욱) 정리
스프링캠프 2024 - 실전 MSA 개발 가이드
MSA를 약식(?)으로만 구현해봐서 학습해보려던 차에
올해 스프링캠프에서 비기너를 위한 세션이 있어 새로 알게된 내용을 정리해본다
기존의 마이크로서비스 아키텍처의 정의만 보면 왜 적용해야하는지에 대해 명확히 알기 어렵다는 문제가 있다
왜 하는지 모르기 때문에 우리 시스템에 적절하게 맞는지 아닌지도 알기 어렵다
또한 도입을 하더라도 이정도면 충분히 MSA를 적용한것인가? 에 대한 기준도 애매하다
데이터베이스를 분리해서 개발한다는것 자체가 난해하다
API 속도와 데이터 정합성에 대한 불안감이 생긴다
DB를 제대로 분리하지 못하면 결국 이런 혼종을 낳게 되는데...
첫번째 우려
단순하게 다른 여러 서비스의 데이터를 보여준다고 해서 api 속도가 느려지지 않는다
데이터는 화면상에서 조합하기 때문
위의 이미지처럼 상담 정보를 조회하는 api 가 있을때
모놀리식의 경우 여러 테이블을 조인해 한방 쿼리로 조회 하기 때문에 문제될게 없다
MSA 라면 상담 정보 API로 조회한 결과를 가지고
다른 서비스(고객, 부서 등)에 다시 조회를 해야 하기 때문에 실제로 느려질 수 있다
참조 빈도가 낮다면 문제가 될게 없다 다시 API로 조회하면 그만
자주 많이 참조하는 데이터의 경우 문제가 생길 수 있는데..
자주 많이 참조하지만 변경이 적은 데이터는 튜닝의 여지가 있다
자주 많이 참조하면서 변경도 잦은 데이터는 까다로운데(대표적으로 유저 세션정보)
이런 경우가 많지도 않고 이런 경우 별도의 이것들을 위한 솔루션을 이용하면 된다
변경 빈도가 높지 않은 데이터는 여러가지로 개선할 방법이 있는데
먼저 데이터를 복제하는 선택을 할 수 있다
복제한 데이터를 어딘가에 다시 저장하고 관리해야된다는 점에서 관리포인트가 늘어난다는 단점이 존재하지만
구현이 단순해지고 원본 소스에 장애가 났을때 영향받지 않고 계속 조회할수 있다
내가 사용했던 방식이 이러한 방식이었다
근데 update가 잦은 데이터는 아니었지만 create가 많던 테이블이라 배치성 쿼리도 많이 나갔다
원본 스키마 통으로 떠가는게 아니라
꼭 필요한 데이터만 잘 발라서 복사 하는게 중요한데
서비스마다 동기화빈도와 결합도를 줄일수 있기 때문이다
모델링을 변경하는것도 도움이 될 수있다
유데미와 같은 교육 서비스를 MSA로 구축한다고 가정해보자
주요 업무가 교육과정 기획, 교육과정 개발, 과정운영 이라고 할때
공통적으로 "과정" 이라는 속성을 가지는데
과정 속성을 한쪽으로 몰아 버리면
나머지 서비스에서 과정 속성이 있는 서비스에 주구장창 AP를 날리게 된다
공통 속성을 각자 복사해서 가지고
특정 업무에서 공통 속성이 변경되는 경우 다른 서비스에 전파하는게 더 낫다
일괄 조회시
특정 서비스의 데이터를 조회하고 타 서비스에 있는 부가속성을 조회할때
매번 부가속성을 가지고 있는 서비스를 조회하면
JPA의 N+1 과 같은 문제가 발생할 수 있는데
API 조회 속도가 느려지고 이런방식은 지양해야한다
1차적으로 데이터를 조회하고 foreign key를 모아서 한번에 타 서비스에 조회 쿼리를 날려야 한다
쿼리만 잘 날려도 실제 성능상에 문제될 여지를 많이 줄일 수 있다
병렬로 처리하면 어떨까
100개의 데이터를 조회한다고 할 때 순차적으로 조회하는게 아니라
병렬로 100번 조회 하는 방식을 고려할 수 있는데 과도한 부하로
대부분의 경우 안티패턴이다
자주 변경되지 않는 데이터라면 로컬 캐시를 활용해보자
자주 사용되는 20%만 잘 캐싱해도 80%의 성능향상을 얻을 수 있다
단, 모니터링하면서 데이터 사이즈가 너무 크지 않게 관리한다 (GC 빡세지 않게)
로컬캐시는 자바의 힙에 올라가기 때문에 서로 동기화 되지 않는다
물론 EHCache같은 로컬캐시는 동기화를 지원하지만 사용을 권장하지 않는다
안티패턴이다
MSA와 같이 노드가 많은 경우 동기화 효율이 떨어진다
노드가 늘어나고 캐싱된 데이터도 많다면 성능 저하가 발생할 여지가 있다
Redis보다 약 20배 빠르고 동기화도 필요없기 때문에 로컬캐시를 권장한다
지금까지 알아본 방법론별로 성능을 비교해보자
모놀리식 : 꾸준히 안정적인 속도
N + 1 : 100건 조회부터 사용하기 힘든 속도
일괄조회 : 모놀리식보다 2~3배 느리지만 여전히 쾌적
일괄 + 캐시 : 모놀리식보다 좋은 속도(캐싱 후)
여기까지 읽기 작업에 관핸 내용이었다
데이터를 쓸 때는 어떨까
MSA 환경에서 쓰기 작업시 트랜잭션중 원자성, 독립성 보장이 되지 않는다
여러 서비스의 DB에 저장, 수정시 롤백이 어려워진다
일반적으로 isolation 레벨을 read commited 나 reapeatabled 수준으로 맞추는데
네트워크 통신시에는 read uncommitted 상태가 된다
두 서비스를 걸쳐서 쓰기작업을 할때
a 서비스에서 데이터를 쓰고 그 id를 가지고 b 서비스에 데이터를 쓰고
그 결과를 다시 a 서비스에 쓰는 트랜잭션이 있다고 하면
모놀리식의 경우 중간단계에서 쓰기가 실패했을때 롤백이 되겠지만
MSA 의 경우 실패할 경우 이전에 쓰기에 성공한 데이터를 삭제해줘야한다
실패한 요청의 정합을 맞추려고 삭제 요청이 날아가는 와중에
b 서비스에 장애가 생겨 서버가 죽는경우
삭제 요청이 실패할 수 있고
a서비스는 정상 처리됐지만 b서비스는 데이터 정합이 맞지 않게 된다
이런 경우
데이터 정합을 맞추는 별도의 대사작업(배치 프로세스)를 진행해야한다
그럼 자동으로 재시도한다면?
아래 링크의 영상으로 비유
https://www.youtube.com/watch?v=oY2nVQNlUB8
상대 서비스의 상태를 정확히 알지 못한채로
재시도 요청을 반복해서 보내게되면 불필요한 트래픽으로 부하가 부담될 수 있다
API가 실패했을때(나의 서비스) 자동으로 재시도 하는것은 대부분 안티패턴이다
반면에 이벤트로 재시도 하면 안정적으로 재시도할 수 있다
이벤트는 최종 말단에서 자기가 데이터를 끌어가는 구조라 위험하지 않다
(이벤트는 전달을 보장하기 때문에)
다만 이벤트는 최소 1회 보장이라 여러번 실행되도 같은 결과가 나오도록 구현해야한다
실패했다고 결과값을 받아도 실제 성공한 요청일 수 있다
이벤트를 활용해서 트랜잭션을 편하게 구성할 수 있는데
대부분의 경우 두번째 트랜잭션에 실패했다고 해서
전체 트랜잭션을 취소할 필요가 없다
간단한 예로 결제 트랜잭션에 성공하고 결제 알림 트랜잭션에 실패한 경우
결제 트랜잭션을 취소시킬 필요가 없다
안전한 방식으로 결제 알림 트랜잭션을 재시도 하면된다
독립적으로 상태를 확인하고 실행하도록 분리해 구성하는 방법도 좋은 설계
모델링을 변경하는게 도움이 될 수도 있다
위 이미지의 예처럼
상담 유의사항이 고객 서비스에 있으면
상담 업무시 고객 서비스에 요청해야해서 불필요한 api호출이 늘어난다
이 경우 상담 서비스에 배치하는게 더 효율적이다
참조도 줄고 트랜잭션도 편하고 스키마 변경에도 유연하다
DDD 설계 원칙
(바운디드 컨텍스트는 바운디드 컨텍스트 단위로 모델링하고
서로 중복된 엔티티를 가질 수 있는데
자기들의 고유한 속성은 자기들이 킵한다)
과 같다
서비스 구현 난이도가 너무 올라간다면
서비스를 합치거나 경계를 적절히 변경하는게 좋다
서비스간에 일시적으로 데이터 정합이 맞지 않을 수 있다
정교하게 데이터가 맞아떨어져야 하는 경우에는
애플리케이션단에서 케이스에 맞게 (기준 시간을 갖고 조회 하는등) 처리해야한다
동시성을 위해 락을 고려할 수 있는데
db단의 장애를 피하기 위해
sql의 락 보다 application단의 방식을 사용한다
확실히 이벤트 기반이 유리하고 DB 를 어디까지 쪼개고 복사할지가 관건인듯하다