프로그래머스 웹백엔드 스터디 7기 / 5주차 - Kafka를 이용한 이벤트 시스템과 웹 노티피케이션
10 Dec 2020 | Spring 스터디 프로그래머스 웹백엔드프로그래머스 웹백엔드 스터디 7기 활동을 하면서 공부한 내용과 기능 구현시 했던 고민들을 정리한 내용입니다.
Index
- 스터디 5주차
- 느낀점
- 고민
- 공부한 내용
- TODO 기능 구현
- References
스터디 5주차
카프카를 이용한 이벤트 전파 시스템과 Web Notification
느낀점
이번 주차는 MSA란 무엇이고 이벤트 시스템 그리고 아파치 카프카의 개념에 대해 알아보았다. 이전에는 HTTP API를 이용해서 동기적인 통신만 해왔는데 - 물론 비동기 처리 방식의 외부 라이브러리를 사용해본적은 있지만 극히 일부분에만 적용되었음 - 메시징 시스템을 이용해 비동기적으로 통신하고 다뤄보는 건 처음이라서 신기했다.
학습한 개념을 코드로 직접 구현해보니 Event Driven 방식이 무엇인지, Pub/sub 구조에서 메시징 처리 방식은 어떻게 동작하는지 확실히 더 잘 와닿았다. 개인적으로 2주차에 했었던 Spring Security보다 이번 이벤트 시스템이 훨씬 재미있었다. 메시징 방식이라고 들어는 많이 봤지만 애매하고 추상적이기만 해서 의문의 대상이었는데, 기능 구현을 통해 그동안 궁금했던 곳을 시원하게 싹 긁어주는 느낌이었기 때문이다. (물론 시간은 더 걸렸지만..)
특히 이번 미션인 구글에서 제공해주는 EventBus 라이브러리를 이용해 유저가 회원가입을 하면 웹 푸시를 보내는 것과, API 서버와 PUSH 서버 두 개의 서비스로 분리해서 카프카 이벤트 전파 시스템을 이용해 웹 노티피케이션을 해보는 기능 구현을 통해 많은 것을 배웠고 사고의 범위가 새로운 영역으로 확장되는 느낌이 들었다.
찾아보니 이외 더 많은 것들이 있었다. 좀 더 공부가 필요할 것 같다. 그래도 이벤트 시스템에 대한 구조를 익히고 기본 구현을 경험해 본 것에 만족한다.
고민했던 점
- Event Bus로 이벤트 발생시키는 방법. 어디서 이벤트를 발생시켜야 하고, 이벤트 버스는 그 이벤트와 연결된 리스너를 어떻게 찾는걸까?
- 카프카는 이벤트를 어떻게 받아오지? 이벤트 전파 프로세스
공부한 내용
- 이벤트 시스템
- 서비스를 만들 때, 이벤트를 정의하고 이 이벤트에 의해서 엔티티 상태들이 변경 되도록 작성하는 프로그래밍 방식
- 상태가 변결될 때마다 이벤트가 전파되는 구조
- 도메인 이벤트란?
- 특정 도메인에서 일어난 일을 도메인 이벤트라 하며 이 이벤트에 의해서 도메인 상태가 변경된다.
- Post(도메인)가 작성되면 ‘작성되었다’라는 created 값이 생기면서 포스트의 상태가 바뀜
- Post(도메인)을 누가 클릭하면, 포스트 클릭 횟수(count)가 변함
- Event란 일어난 일, 특정한 도메인에서 일어난 이벤트를 Domain Event라 한다.
- 이벤트란 fact(일어난 사실)이어야 한다.
- 예) Post(도메인)가 작성되면 created라는 이벤트가 발생한다. 주문이 생성/거절/취소됐다. 댓글이 달렸다.
- 특정 도메인에서 일어난 일을 도메인 이벤트라 하며 이 이벤트에 의해서 도메인 상태가 변경된다.
- Aggregate: 여러 도메인 관리해서 트랜잭션을 보장시키며 특정 이벤트 만드는 친구
- Event Publisher: 발생한 이벤트를 subscriber들에게 알리는 친구 ex) Post가 작성됐어
- Subscriber: 이벤트 리스너, reative하게 반응하고 동작하는 애 ex) Post가 작성됐으니까 DB에 기록해야지. 다른 서비스한테 알려서 통계처리를 하게 해야지
-
도메인 이벤트 처리 장점
-
특정 이벤트 발생시, 해당 이벤트에 반응하는 핸들러들을 중간에 추가해도 이벤트 발생시키는 애랑 듣는애 간에 코드 결합도가 없어서 원하는 서비스를 추가해도 복잡도에 영향 없다.
-
스프링에서 A 서비스가 B 서비스 사용을 위해 주입 받아 B 서비스의 메소드를 런타임때 호출하는 구조였다. 메소드를 다이렉트하게 호출하는 구조이므로 메소드 시그니처가 바뀌면 A도 변경해줘야 한다. 다른 C 서비스를 사용하게 된다면 C 서비스도 주입받고 호출해야하는 구조이다. (다른 서비스의 특정 행동을 호출하는 개념)
-
이벤트 기반으로 한다면 B 서비스에 특정 이벤트가 발생시, A 서비스는 듣다가 반응만 하는 구조. A 서비스는 다른 서비스의 존재를 알 필요가 없다. 대신 서비스 간에 이벤트를 전달해주는 EventBus가 필요. 각 서비스는 event가 오면 반응하고 그에 따른 행동을 하면 된다. 따라서 서비스간 결합도 훨씬 떨어진다.
-
Guava EventBus, Spring의 ApplicationEventPublisher 사용
-
새로운 기능 추가시 특정한 이벤트(Post read/write)를 만들어서 Guava EventBus에 publish 하면 Guava EventBus를 subscribe하는 애가 동작하게 할 수 있다.
-
Event Bus를 하나의 인스턴스/서비스가 아니라 여러개의 서비스로 확장시키면 MSA 환경에서 Event Driven하게 MSA를 구성할 수 있다.
-
-
MSA(마이크로서비스) [사진 출처] https://bcho.tistory.com/948
- Monolithic 아키텍쳐
- 1개의 서버(WAS) 안에 모든 비즈니스 로직이 들어가 있는 형태
- controller service 1:1 맵핑 구조
- 각 컴포넌트 간의 상호 호출은 함수를 이용한 call-by-reference 구조를 취한다.
-
Microservices 아키텍처
- 물리적으로 / 코드 베이스로 / 서비스별로 또는 아예 팀으로 나눈것. 빌드/배포 또한 독립적. 이럴 경우 어느정도 규모가 필요.
- 서비스마다 독립적으로 수행 가능
- 기술 스택의 독립 처리
- 각 서비스별로 적합한 언어로 비즈니스 로직 구현과 DB 선택
- 관계가 중요한 경우 RDBMS, 양이 많은 고속 데이터는 NoSQL 등
- 서비스별로 별도의 DB 존재
- Mono(통짜구조)에서는 모든 서비스가 동일한 DB를 공유하는 구조
- API 게이트웨이를 앞단에서 활용 (프록시처럼)
- 각 서비스의 공통 기능 처리
- 인증, 로깅, 메시지변환(xml -> json), 프로토콜 변환(http ->https)
- EndPoint 통합과 라우팅
- 각 서비스의 공통 기능 처리
- 물리적으로 분리되어 있는 서비스들. 서로 통신은 어떻게 하지?
- grpc 또는 HTTP Rest API 이용(synchronize하게 통신)
- 카프카 같은 메시지 브로커 이용한 EventDriven 방식
- 특정 이벤트를 구독해서 이벤트가 발생되면 그에 맞는 행동을 함
- 통신 방식 예시
- 결제를 위해 사용자 정보가 필요한 결제 서비스가 있다. MSA 구조에서 결제를 위해 어떻게 처리해야 할까? 결제가 완료되면 알림 이메일도 발송한다.
- Rest API 방식
- 결제할 때마다 사용자 정보를 API로 찔러 처리한다. 결제가 완료되면 이메일 API를 찔러 이메일을 발송한다.
- Event Driven 방식
- 사용자 가입/탈퇴를 이벤트로 만들어서 이 이벤트를 메시지 브로커(카프카)/Event Bus에 publish(전파)한다.
- 그럼 해당 이벤트에 관심있는 아이(결제 서비스)가 subscribe하다가 반응해서 처리한다.
- 즉, 결제할때 마다 사용자 정보를 API로 찔러서 가져오는게 아니라 사용자가 가입을 할때, subscribe해서 필요한 사용자 정보를 결제 서비스의 DB에 저장해둔다.
- 결제 서비스 안에서 필요한 사용자 정보를 들고 있게 되므로 실결제 발생시 자체 DB에서 사용자 정보를 가져와 결제를 완료시키고 publish한다.
- 그럼 이메일 서비스가 결제 완료 이벤트에 반응해 이메일 발송
- 가입 후 유저 정보가 변경되었다면? => 변경된 것도 이벤트로 만들어 결제 서비스에 전파해줘야 한다.
- 하지만, 결제 서비스가 유저의 마지막 상태를 반드시 알고 있어야 한다면 할 수 없이 api 호출을 통해 가져와야 한다.
- 모노리틱 구조라면, 결제 발생 -> 사용자 DB Repository 조회 -> 필요한 정보 합쳐서 응답해주고 -> email 발송 api 호출. Synchronize하게 처리됨.
- Event Driven 방식은 매번 API를 호출해야하는 Rest API 방식보다 부하가 훨씬 적다. 이벤트 통해서 전파하면 이미 사용자 db를 해당 서비스에서 들고 있기 때문에 api 호출없이 자체적으로 해결 가능하기 때문이다.
-
MSA 장점
- 확장성
- 부하가 많은 서비스만 확장이 가능(많은 자원 요구하거나 트래픽이 몰리는 곳). 필요한 부분만 확장 가능
- 이메일 발송에만 트래픽이 몰릴 경우 이메일 서비스만 서버 수를 늘리고, 상대적으로 트래픽이 적은 서비스(결제는 한달에 1~2번)는 서버 수를 줄일 수 있음
- 모노리틱 하다면, 특정 서비스에만 부하가 생기면 그 서비스 때문에 전체 서버 수를 늘리거나 CPU 수를 늘려줘야 하지만, MSA 경우 부하를 많이 받는 서비스 컴포넌트만 확장해주면 된다.
- 확장성
MSA에서는 시스템 간의 호출이 많기 때문에 서비스간 결합도를 낮추기 위해서, 비동기 요청, 성능, 안정성 등 여러가지 이점이 있어서 메시징 시스템을 많이 사용
-
MSA 구성시 Spring MVC가 없는 모듈이 있을 수 있나?
- MSA 구성시 REST API가 없는 서비스가 있을 수 있다. 이 경우 mvc 모듈이 필요없다 대신 카프카 통해 subscriber를 띄어야 한다
-
이벤트 드리븐 방식 언제 사용?
- 외부 시스템과 연동해야하는 상황, 언제 변할지 모를 때, 동기적이지 않을 때, 주로 사용.
- ex.주문 시스템, 여행사 예약 시스템
- Monolithic 아키텍쳐
-
아파치 카프카 등장 배경
- HTTP API 요청으로 서비스 A가 B로 요청을 하기 때문에 A에서 B로 메시지가 흘러간다 즉 하나의 파이프라인이 생성된다로 표현
- 새로운 공급자(서비스 B api 호출하는 애들)가 생기면 여러개의 파이프라인이 생김
- 새로운 소비자도 생기면 (빅데이터 분석을 위한 서비스)
- 엄청난 복잡한 형태의 데이터 파이프라인이 만들어 지게된다.
- 카프카를 사용하게 되면 이러한 복잡한 형태를 좀 더 단순하게 사용할 수 있게 된다.
-
카프카의 주요 용어
- Producer: 메시지/데이터를 생성하고 제공. ex) 회원 가입시 회원 정보를 생성
- Brokers: 생성된 메시지를 Consumer들에게 전달해주는 역할
- Consumers: 필요한 데이터를 소비 ex) 생성된 회원 정보를 DB에 저장, 결제 처리를 위해 필요한 회원 정보를 따로 보관, 이메일 발송
-
카프카
- 모든 메시지를 카프카로 쏘기 때문에 고가용성 필요.
- 서버를 여러대로 나눠 분산 처리. (대량신속처리 분산 메시지 시스템)
- 분산 커밋 로그.
- 메시지 -> 카프카 -> 카프카 내부에서 각 메시지를 로그로 보고 파일 시스템에 기록(commit). 메시지간 순서가 보장됨.
-
그런데 카프카 내부 로그에 write되는 속도와 읽어가는 속도(소비 속도)는 달라질 수 있다.
-
모든것은 분산되어 있다. (프로듀서, 컨슈머, 카프카)
- 카프카 클러스터링
- 카프카는 여러개의 서버로 클러스터링 되어 있다.
- M-S 구조로 관리하기 위해서 이를 관리해주는 주키퍼 서버가 필요하다.
-
토픽과 파티션
- 데이터가 저장되는 곳이 토픽이며, 토픽 내부는 파티션으로 분할되어 있다.
- 토픽으로 데이터를 저장하고 읽어온다.
- subscriber는 토픽 기준으로 subscribe함. 코드상에서 나는 이 event.join 토픽을 subscribe하겠다.
- 예시
- 스프링부트로 만들어진 인사 관리 서버/시스템/서비스, 고객 서비스 - 고객의 정보만 관리하는 서비스와 가입/탈퇴 처리 전용 서비스가 따로 분리되어있다. 가입 발생 -> 카프카 특정 토픽을 향해 customer created 메시지 전달 -> 커슈머들은 해당 이벤트를 리슨하고 해당 토픽에 있는 메시지들을 컨슘.
- 토픽 구조
- 토픽은 파티션으로 구성되며, 파티션 단위로 분산처리가 된다
- 메시지는 특정 토픽 안에 특정 파티션 안에 저장된다.
- 파티션이란 데이터를 나누는 개념. 무슨 기준으로 나눌까?
- 월단위로 파티셔닝, 모듈러 연산 (3으로 쪼개서 1,2,3에 넣기), 도단위로 파티셔닝
- 이런식으로 데이터 분산처리 (좀더 찾아보기 파티셔닝방법)
- 토픽의 파티션은 불변. 한 번 커밋(기록)되면 변경 불가
- 주문을 되돌리고 싶으면 주문을 취소하는 커밋을 해야함. 토픽, 파티션 자체를 지울순 있어도 파티션 사용중 내용을 바꿀순 없음
-
컨슈머 Offset 포인팅
- 컨슈머는 어디까지 읽었는지 카프카에 offset으로 기록
- 컨슈머를 재시동하면 처음부터가 아닌 이전에 읽어왔던 지점부터 읽어오게됨
- offset, partition, topic으로 메시지를 추적함. 어느 토픽의 어느 파티션을 어디 까지 읽었냐
-
컨슈머와 컨슈머 그룹
- 컨슈머 그룹 개념을 통해 큐와 pub/sub 모델 지원
- 파티셔닝은 하나의 컨슈머 접근만 허용. 3개의 파티셔닝. 메시지가 특정 파티션에 들어가면, 새 메시지는 모든 커슈머 그룹에 브로드 캐스트 된다. 커슈머 그룹은 0번 파티션에 써진 메시지를 모두 읽어간다. 하지만, 하나의 그룹 내에서는 하나의 컨슈머만 해당 메시지를 읽어간다.
위 그림에서 만약 컨슈머 그룹 A에 A4가 추가되었다. A4는 어느 파티션을 읽을까? => 아무것도 안 읽는다. 하나가 죽으면 대신 처리하는 역할만하게 됨. 안 죽으면 가만히 있음 => groupB처럼 컨슈머가 1개만 있는 경우 3개를 모두 다 읽어간다. 죽으면 해당 그룹에서는 아무것도 안한다. 어떻게 분산을 하고 하나의 컨슈머 그룹안에서 몇개의 인스턴스를 들고 갈지 고민이 필요한다.
- A3가 죽으면 컨슈머 스스로 파티션 재분배가 일어난다.
- 서버 3개, 또는 서버 1개 내 쓰레드 3개로 구성 등 다양한 방식으로 구성 가능
-
Replication(복제)
- 데이터 손실 방지를 위해 파티션 단위로 복제를 한다.
- 복제를 안하면 데이터 날라가면 끝.
- –replication-factor 옵션을 이용해 설정
- 파티션 3, replicafactor 2 주면 총 6개 파티션이 생성됨.
-
lag(렉)
- 소비속도가 공급 속도보다 느리면 렉이 발생하게된다.
-
추가 학습 / 정리한 내용
- [Kafka - 기본 개념 잡기] 포스트 보러가기
- [Kafka - 로컬 환경에서 띄우기] 포스트 보러가기
TODO 기능 구현
1.멀티모듈화 Maven프로젝트를 부모-자식 프로젝트로 변경
social-server를 다음 2개로 멀티 모듈화
- social-server-api
- social-server-push
2. 카프카를 이용해 social-server-api 프로젝트에서 Notification을 subscribe하는 API 구현
- kafkaTemplate의 sendAndReceive를 이용해서 Subscription정보를 Push서버에서 처리하도록 한다.
- 토픽이름: v1.event.subscription.request
- 토픽이름: v1.event.subscription.response
3. 포스트 댓글 작성에 대한 이벤트와 리스너 생성
- CommentCreatedEvent
4. CommentCreatedEvent 발생시 카프카로 이벤트 전파
- 토픽 이름: v1.event.comment-created
- CommentCreatedEvent 이벤트를 구독하여 콘솔에 다음과 같이 출력
- ${작성자의 subscription 정보 중 endpoint} -> “${코멘트작성자의이름}가 코멘트를 달았습니다.”푸쉬발송
- user별 subscription 정보를 처리하는 DB가 별도로 존재하여 Subscription 이벤트가 발생할 때 해당 정보를 디비에 기록한다.
References
MSA
https://bcho.tistory.com/948?category=431297
이벤트시스템
https://www.slideshare.net/koneru9999/guavas-event-bus
https://coding-start.tistory.com/132
카프카
https://www.popit.kr/kafka-consumer-group/
https://www.popit.kr/kafka-%EC%9A%B4%EC%98%81%EC%9E%90%EA%B0%80-%EB%A7%90%ED%95%98%EB%8A%94-%EC%B2%98%EC%9D%8C-%EC%A0%91%ED%95%98%EB%8A%94-kafka/
https://victorydntmd.tistory.com/344?category=798367
https://www.confluent.io/blog/apache-kafka-spring-boot-application/