https://www.youtube.com/watch?v=uk5fRLUsBfk
위의 발표를 보고 정리한 포스트
분산 시스템이란?
![](https://velog.velcdn.com/images/dbwogml15/post/e6e6939a-7eef-4b69-b511-f8589817e9b0/image.png)
데이터를 전달하는 방법
Remote API
![](https://velog.velcdn.com/images/dbwogml15/post/bef1ade5-440e-4f3d-a7ef-dd4add8f9164/image.png)
- 데이터 처리는 서버측에서 일어날 수도 (C, U, D) 혹은 조회한 클라이언트에서 처리할 수도 있다. (R)
- 주로 사용자 요청에 즉각 응답하는 방식에 Remote API를 많이 사용한다.
MessageQueue 방식
![](https://velog.velcdn.com/images/dbwogml15/post/8acc0874-c499-4afe-9fa4-9a01f31cbe9f/image.png)
- Publisher가 MessageQueue에 데이터를 공급하면 Consumer가 데이터를 처리하는 구조
- Publisher - MessageQueue - Consumer 구조 비교적 복잡
(Server - Client)보다 하나 더 늘어난 컴포넌트
효율적인 방법?
![](https://velog.velcdn.com/images/dbwogml15/post/3d17c9fc-65c5-43d2-a6b7-ce43829e91af/image.png)
- 개발자는 데이터를 전달함에 있어 네트워크 사용은 필수고, 네트워크는 신뢰할 수 없는 매체이기때문에 항상 데이터 유실에 대비해야한다.
- TCP/IP는 데이터 전달을 보장하는 프로토콜 아닌가? 왜 믿을 수 없을까 관련글
- https://kukuta.tistory.com/219
![](https://velog.velcdn.com/images/dbwogml15/post/1c38f09f-7623-4eb9-99ea-23f21a42c31a/image.png)
- 어떤 방식을 사용하던간에 (REST-API, MessageQueue) end to end까지 효율적으로 전달하는 추상화된 방식을 설명할것.
1. At-most once delivery(최대 한 번 전달)
![](https://velog.velcdn.com/images/dbwogml15/post/d79607a0-0a4d-4ed0-aeaa-565736728a05/image.png)
- 데이터가 유실 되든말든 Prod는 메시지 큐에 단 한 번 전송 (Fire and Forget)
- Consumer는 데이터를 못 받을 수 있음 네트워크 상에서 패킷이 유실 될 수 있다.
- Prod 어플리케이션에서 메시지를 전송 중 예외가 발생할 수도 혹은 Client 앱에서 데이터 처리를 정상적으로 하지 못할 수도 있다.
2. At-least-once delivery
![](https://velog.velcdn.com/images/dbwogml15/post/2578fdac-650c-4aef-9ce1-2790ef43b8c7/image.png)
- at-most once delivery 다음으로 정확도가 더 높은 방식
- Prod는 Consumer에게서 수신 확인 응답 패킷(ACK)을 받을때까지 데이터 전송
- 장점에 써있는 바와 같이 발송을 보장하고 Prod측에서 상태관리만 하면 되기 때문에 비교적 쉬운 개발
- 멱등성 (여러번 실행해도 같은 결과를 나타내는 것), 같은 메시지를 여러번 받더라도 Consumer는 항상 같은 결과를 반환해야한다.
3. Exactly-once-delivery
![](https://velog.velcdn.com/images/dbwogml15/post/c4d4c0bd-f3e4-4564-b2e2-e462a801f2d4/image.png)
우리들의 어플리케이션은 데이터를 효율적으로 전달하고 있는가?
- 신뢰성있는 어플리케이션은 최소 한 번의 전송은 해야하며 메시지를 안전하게 처리해야 한다.
- SpringBoot를 사용하고 있다는 가정하에, 기본 설정, 혹은 복붙한 설정(코드)을 사용하고 있다면 최소 한 번 전달의 원칙을 지키지 않고 있을 가능성이 놓다.
어떻게 최소 한번 데이터를 전송하는 신뢰성 있는 어플리케이션을 개발 할 수 있는가?
1. RDB를 사용하는 어플리케이션에서 전달 방법
![](https://velog.velcdn.com/images/dbwogml15/post/672b805f-8bc0-41b7-8b83-e767be2ff1ac/image.png)
- 서비스별 데이터베이스 패턴
- 각 서비스는 자신만의 독립된 데이터 저장소를 가지고 있고 각 컴포넌트마다 자신의 데이터 저장소의 데이터를 처리한다.
어떤 컴포넌트에서 다른 컴포넌트로 데이터 전파을 해야한다면
![](https://velog.velcdn.com/images/dbwogml15/post/a8e878b3-1cc8-4f81-8a10-645b646246dc/image.png)
- 제일 처음 DB트랜잭션을 시작해서 데이터를 정상적으로 저장
![](https://velog.velcdn.com/images/dbwogml15/post/81c9dcba-faa8-40b1-a374-b0b900f8d254/image.png)
- 그 후 다른 컴포넌트에게 REST-API로 전파한다고 가정
일반적인 코드
![](https://velog.velcdn.com/images/dbwogml15/post/22bf0bd9-0bd1-4684-b75b-d1d644eaf418/image.png)
- task를 생성후 Repository에 저장할것이다.
- 다른 컴포넌트에게 전파하기 위해 나의 event,메시지 혹은 REST-API콜 전달
- 마지막에 @Transactional을 붙일것이다. 왜냐? RDB를 사용하니까 데이터를 안전하게 저장하고 싶기 때문에
@Transactional??
![](https://velog.velcdn.com/images/dbwogml15/post/608c57f3-00d2-4e9d-99a2-45a02a3fed53/image.png)
- Proxy 객체가 한 번 감싸고 있다.
- 그렇다면 실질적인 순서는?
- 데이터를 저장하고
- 이벤트를 전달하고
- 트랜잭션에 의해 커밋, 혹은 롤백이 된다.
실행 순서
![](https://velog.velcdn.com/images/dbwogml15/post/bb441d35-47eb-44e1-b9fe-2f9b4ea8aa2d/image.png)
![](https://velog.velcdn.com/images/dbwogml15/post/4bfa504d-7d9b-45b2-9b71-1ec68c6b1c07/image.png)
- 그후 트랜잭션이 실행됨
- 만약 commit이 정상적으로 되지 않는다면???
- 영속되지 않은 데이터로 REST-API만 전달된 상황
- 원본데이터는 존재하지 않음
해결책
![](https://velog.velcdn.com/images/dbwogml15/post/ebe58c28-4e66-46ec-b016-a6624d725a85/image.png)
- SpringFramework에서 제공하는 대표적인 두 가지 방법
- @TransactionalEventlListener의 경우
![](https://velog.velcdn.com/images/dbwogml15/post/3cd67385-8997-479a-900a-39fc2ff002c7/image.png)
- Eventhandler의 propagate메서드
- REST-API를 이용하거나 MeesageQueue를 이용해 다른 컴포넌트에게 데이터를 전달을 하는 경우 위 어노테이션을 propagate메서드에 붙여준다.
- 우리가 원했던 트랜잭션 커밋 후 데이터 전파 실행
- 그러나 다 해결되지 않는다.
![](https://velog.velcdn.com/images/dbwogml15/post/f994a092-73ec-428b-b7cb-ab01f9de6a72/image.png)
- 커밋은 완료됐지만 전달이 실패한다면? 네트워크는 신뢰할 수 없는 매체
- 데이터 저장만하고 데이터 전파는 실패
![](https://velog.velcdn.com/images/dbwogml15/post/d135ff0f-ced6-4149-b382-851873a50d16/image.png)
- @Retryable을 붙여주면 실패했을때 재시도를 할 수 있다.
- 그런데.. 이것 마저 실패한다면??
![](https://velog.velcdn.com/images/dbwogml15/post/45cb3607-d914-467b-8791-671ab319118d/image.png)
- maxAttempts와 backoff를 설정한다.
- 최대 실행 횟수, 지연시간
- 이것마저 실패한다면??
마이크로 서비스 아키텍쳐 패턴
- 우리가 원하는것이 트랜잭션 커밋과 더불어 보장되는 데이터 전파라면.
![](https://velog.velcdn.com/images/dbwogml15/post/4e3e2a1a-39d1-4ed9-a78d-fffc2d507aa1/image.png)
- Transactional Outbox Pattern과 Polling Publisher Pattern을 섞어 쓴다.
- RDB를 MessageQueue처럼 사용하는 방식
Transactional Outbox Pattern
![](https://velog.velcdn.com/images/dbwogml15/post/d690a6a0-d8ac-442f-82d4-faae411f6520/image.png)
- 하나의 트랜잭션에 이벤트, 메세지가 발행될때 RDB에 같이 저장한다면 둘 다 성공 혹은 둘 다 실패할것이다.
- 어떻게 구현할 것인가?
Polling Publisher Pattern을 활용해야한다.
![](https://velog.velcdn.com/images/dbwogml15/post/e7c61449-d10d-444c-8c03-a1ed5793cbb8/image.png)
- RDB에 저장되어 있는 데이터들을 주기적으로 Polling하고 발행하는 역할을 추가한다.
RDB에서 MessageQueue처럼 사용할 테이블을 설계해야한다.
![](https://velog.velcdn.com/images/dbwogml15/post/6091d8f9-3512-4cdb-b8ca-ad75a4958bd6/image.png)
화자의 주요 필드 4가지
- event_id : PK로 빠른 인덱스와 이벤트 순서 보장
- created_at : 이벤트 발생시간으로 Consumer에서 처리할 데이터 구분
- status : 완료 여부 상태
- payload : 메시지 저장
By Code
변경된 코드
![](https://velog.velcdn.com/images/dbwogml15/post/bd295d97-cd72-408b-b26a-095e4f416ec0/image.png)
- taskRepository와 eventRepository에 One트랜잭션에 데이터, 이벤트저장
Polling Publisher Pattern 코드
![](https://velog.velcdn.com/images/dbwogml15/post/ee4b5cd3-639e-4536-8dfc-252c2e499961/image.png)
- @Scheduled 애노테이션 4초마다 실행
- Ready상태인 이벤트들을 조회
- restTemplate로 이벤트 전송
- 전송한 이벤트들을 done으로 업데이트
- 저장
이과정에서 예외가 발생한다면 !!
장단점
![](https://velog.velcdn.com/images/dbwogml15/post/1796b2e1-4c3d-491e-85bb-514d40221259/image.png)
- 실시간성이 필요한 데이터는 해당 패턴 사용 불가
- 대용량 이벤트를 발행해야한다, 트랜잭션 하나에 많은 이벤트가 발생한다면 이 패턴은 사용하기 부적합하다.
RabbitMQ를 사용한 전달방법
![](https://velog.velcdn.com/images/dbwogml15/post/97f4a6cc-6c44-40ad-be3d-7da300bdb62d/image.png)
- 응답처리 메커니즘 처리시 -> ACK, 실패시 -> NACK
![](https://velog.velcdn.com/images/dbwogml15/post/27652c23-f3f8-4e80-a431-c597f1fa2b64/image.png)
![](https://velog.velcdn.com/images/dbwogml15/post/6eca01c9-8c7e-4798-ac17-36992072694a/image.png)
- exchange -> queue 라우팅 과정중 네트워크에서 실패할 가능성이 있다.
- 이를 위한 해결책 두 가지 과정
Producer Confirm
![](https://velog.velcdn.com/images/dbwogml15/post/38d5878b-09ba-4568-9bed-849b8097e7b5/image.png)
- exchange에서 응답 패킷을 보내는것이 Producer Confirm
![](https://velog.velcdn.com/images/dbwogml15/post/edb34265-7919-42dd-b5a7-3cc26c7e6232/image.png)
- 스프린에서 제공하는 CorrelationData 클래스 제공
![](https://velog.velcdn.com/images/dbwogml15/post/d58c8fde-6c4f-4a03-83cd-af9c075b0626/image.png)
- rabbitTemplate send() 메서드에 이 객체를 사용하는 코드
![](https://velog.velcdn.com/images/dbwogml15/post/6f5ef061-8faf-4b50-9a51-9f88fb32a3e1/image.png)
- 콜백하는 코드 setConfirtmCallback 메서드에 correclationData 파라미터
- 파라미터에 대해
- correlationData -> 메시지를 발행할때 생성한 객체
- ack -> boolean 성공 실패 여부
- cause -> 실패 원인을 확인하기 위함 (String)
callback이기 때문에 실시간으로 즉각 대응하기 어렵지만 어떤 메세지가 실패했는지 파악 가능하다.
설정 코드
![](https://velog.velcdn.com/images/dbwogml15/post/2c6b3825-795b-4f49-9273-cb5c12d0587a/image.png)
Consumer Acknowledge
![](https://velog.velcdn.com/images/dbwogml15/post/43c4b557-05ae-44da-b1b4-5f1387ef7a70/image.png)
![](https://velog.velcdn.com/images/dbwogml15/post/94a24e12-1498-4309-a73a-38938ba1725e/image.png)
![](https://velog.velcdn.com/images/dbwogml15/post/c9a918d1-75fd-4d0c-8956-936003f3b0f8/image.png)
- @RabbitListener를 사용한 방법
- 일반적으로 이벤트 리스너의 receiveMessage를 구현할때 message하나만 받지만 해당 패턴에선 channel을 붙여준다.
- SpringFramework가 자동 적용해주지만 수동 전송도 가능하다.
![](https://velog.velcdn.com/images/dbwogml15/post/8b1cc3be-8b00-4104-b5e4-826c06af7028/image.png)
버그로 인해 컨슈머가 NACK만 날릴 경우.
![](https://velog.velcdn.com/images/dbwogml15/post/bc0c645e-b6b0-4d48-90a5-8f492bae38f5/image.png)
- Consumer Ack을 구현할때의 주의점
- Consumer는 NACK만 전송하고 큐에 메세지는 계속 쌓이는 경우가 발생할 수 있다.
- Dead Letter 큐를 활용해 해결가능
![](https://velog.velcdn.com/images/dbwogml15/post/4efeca2f-639a-4df5-8fcc-a355f6902a70/image.png)
- Dead Letter설정
- 큐에서 정상적으로 처리하지 못한 메세지를 DeadLetter Exchange로 넘기고
- 라우팅으로 Dead Letter Queue에 넘겨준다.
- requeue시 basic reject, basic nack로 메세지를 처리하는 경우
- 큐내의 메세지가 TTL이 초과할경우 (오래됐을 경우)
- 큐가 가득찼을 경우
*requeue를 true로 설정하면 Dead Letter로 가지 않고 기존의 메세지 큐에 다시 들어간다.
![](https://velog.velcdn.com/images/dbwogml15/post/2a8ce6e8-65dc-4e9c-9fab-c354ff3b23d2/image.png)
- Dead Letter 적용 코드
- Builder사용
- maxAttempts 최대 실행 횟수
- backOff 간격 설정
- recover Requeue하지 말고 DeadLetter로 넣어라
![](https://velog.velcdn.com/images/dbwogml15/post/14921004-a1c6-4721-b9bc-bab3cd999d37/image.png)
- 실패시 처리 코드
- 수동으로라도 데이터를 다시 전달 할 수있다.
Kafka를 이용한 방법
마찬가지로 Producher Confierm, Consumer Ack 방식
![](https://velog.velcdn.com/images/dbwogml15/post/f477420f-7ee4-4588-b351-f1e5b1adc7c3/image.png)
- kafkaTemplate.send 메서드를 활용해서 메세지 발행
- 응답은 ListenableFutuer
- 두 가지 콜백을 등록할 수 있음
- success, fail CallBack
- success -> reulst ->
- fail -> throwable
![](https://velog.velcdn.com/images/dbwogml15/post/2ed67ec9-9e1e-4d14-abd8-bcd153b93f22/image.png)
- onMessage메서드
- Acknowledgment 파라미터 확인
![](https://velog.velcdn.com/images/dbwogml15/post/fec83284-2f41-426a-ba8e-c10604454e18/image.png)
- acknowledgment.acknowledge()로 콜백
![](https://velog.velcdn.com/images/dbwogml15/post/874afb42-d62a-4c35-a0bf-cf0678b0eadf/image.png)
![](https://velog.velcdn.com/images/dbwogml15/post/7383c85e-2c9b-4d2e-913f-23c39b903527/image.png)