Kafka

- 데이터 파이프라인을 안전하고 확장성 높게 운영할 수 있도록 설계되었고, 여기에 추가적으로 카프카 주변 생태계를 지탱하는 애플리케이션들은 카프카를 데이터 파이프라인으로 더욱 빠르게 적용시킬 수 있도록 도와줌
데이터 레이크 아키텍처의 종류
레거시 데이터 수집 플랫폼

- 초기 빅데이터 플랫폼은 End To End 로 각 서비스 애플리케이션으로부터 데이터를 배치로 모음
람다 아키텍처(lambda architecture)

- 레거시 데이터 수집 플랫폼을 개선하기 위해 구성한 아키텍처
카파 아키텍처(kappa architecture)

- 람다 아키텍처와 유사하지만 배치 레이어를 제거하고 모든 데이터를 스피드 레이어에 넣어서 처리한다는 점이 다름
스트리밍 데이터 레이크 아키텍처

- 스피드 레이어에 데이터를 분석, 프로세싱, 저장함으로써 단일 진실 공급원
- 데이터가 필요한 모든 고객과 서비스 애플리케이션은 스트리밍 데이터 레이크의 스피드 레이어만 참조함으로써 데이터의 중복, 비정합성과 같은 문제에서 벗어날 수 있음
카프카 브로커, 클러스터, 주키퍼

- 카프카 브로커는 카프카 클라이언트와 데이터를 주고받기 위해 사용하는 주체이자 데이터를 분산 저장하여 장애가 발생하더라도 안전하게 사용할 수 있도록 도와주는 애플리케이션
- 하나의 서버에는 한 개의 카프카 브로커 프로세스가 실행되는데 카프카 브로커 서버 1대로도 기본 기능이 실행되지만 데이터를 안전하게 보관하고 처리하기 위해 3대 이상의 브로커 서버를 1개의 클러스터로 묶어서 운영
- 카프카 클러스터로 묶인 브로커들은 프로듀서가 보낸 데이터를 안전하게 분산 저장하고 복제하는 역할을 수행
컨트롤러(Controller)
- 클러스터의 다수 브로커 중 한 대가 컨트롤러의 역할을 수행
- 컨트롤러는 다른 브로커들의 상태를 체크하고 브로커가 클러스터에서 빠지는 경우 해당 브로커에 존재하는 리더 파티션을 재분배
- 카프카는 지속적으로 데이터르 처리해야 하므로 브로커의 상태가 비정상이라면 빠르게 클러스터에서 빼내는 것이 중요
- 컨트롤러 역할을 하는 브로커에 장애가 생기면 다른 브로커가 컨트롤러 역할을 수행
컨슈머 오프셋 저장
- 컨슈머 그룹은 토픽이 특정 파티션으로부터 데이터를 가져가서 처리하고 이 파티션의 어느 레코드까지 가져갔는지 확인하기 위해 오프셋을 커밋
- 커밋한 오프셋은
_consumer_ offsets 토픽에 저장하는데 여기에 저장된 오프셋을 토대로 컨슈머 그룹은 다음 레코드를 가져가서 처리
코디네이터(coordinator)
- 클러스터의 다수 브로커 중 한 대는 코디네이터의 역할을 수행
- 코디네이터는 컨슈머 그룹의 상태를 체크하고 파티션을 컨슈머와 매칭되도록 분배하는 역할을 수행
- 컨슈머가 컨슈머 그룹에서 빠지면 매칭되지 않은 파티션을 정상 동작하는 컨슈머로 할당하여 끊임없이 데이터가 처리되도록 도와주는데 이렇게 파티션을 컨슈머로 재할당하는 과정을 리밸런스(rebalance) 라고 부름
주키퍼(zookeeper)
- 분산 코디네이션 서비스를 제공하는 주키퍼는 카프카의 클러스터 설정 리더 정보와 컨트롤러 정보를 담고 있어 카프카를 실행하는 데에 필요한 필수 애플리케이션으로 카프카의 메타 데이터를 관리하는 데에 사용
토픽과 파티션

토픽
- 토픽은 카프카에서 데이터를 구분하기 위해 사용하는 단위
- 토픽은 1개 이상의 파티션을 소유
파티션
- 파티션에는 프로듀서가 보낸 데이터들이 들어가 저장되는데 이 데이터를 레코드(record) 라고 부름
- 파티션은 카프카의 병렬 처리의 핵심으로써 그룹으로 묶인 컨슈머들이 레코드를 병렬로 처리할 수 있도록 매칭
- 컨슈머의 처리량이 한정된 상황에서 많은 레코드를 병렬로 처리하는 가장 좋은 방법은 컨슈머의 개수를 늘려 스케일 아웃하는 것
- 컨슈머의 개수를 늘림과 동시에 파티션 개수도 늘리면 처리량이 증가하는 효과를 볼 수 있음
- Queue 와 비슷한 구조
- FIFO 구조와 같이 먼저 들어간 레코드는 컨슈머가 먼저 가져가게 되는데 다만 일반적인 자료구조로 사용되는 큐는 데이터를 가져가면(pop) 레코드를 삭제하지만 카프카에서는 삭제하지 않음
- 파티션의 레코드는 컨슈머가 가져가는 것과 별개로 관리
- 이러한 특징 때문에 토픽의 레코드는 다양한 목적을 가진 여러 컨슈머 그룹들이 토픽의 데이터를 여러 번 가져갈 수 있음
레코드(record)
- 타임스탬프, 메시지 키, 메시지 값, 오프셋, 헤더로 구성
- 프로듀서가 생성한 레코드가 브로커로 전송되면 오프셋과 타임스탬프가 지정되어 저장
- 브로커에 한 번 적재된 레코드는 수정할 수 없고, 로그 리텐션 기간 또는 용량에 따라서만 삭제
- 타임스탬프는 프로듀서에서 해당 레코드가 생성된 시점(Create Time) 의 유닉스 타임이 설정
- 컨슈머는 레코드의 타임스탬프를 토대로 레코드가 언제 생성되었는지 알 수 있음
- 다만 프로듀서가 레코드를 생성할 때 임의의 타임스탬프 값을 설정할 수 있고 토픽 설정에 따라 브로커에 적재된 시간(Log Append Time)으로 설정될 수 있다는 점을 유의
- 메시지 키를 사용하면 프로듀서가 토픽에 레코드를 전송할 떄 메시지 키의 해시값을 토대로 파티션을 지정하게 되는데 동일한 메시지 키라면 동일 파티션에 들어가는 것
- 다만 어느 파티션에 지정될지 알 수 없고, 파티션 개수가 변경되면 메시지 키와 파티션 매칭이 달라지게 되므로 주의해야 하는데 만약 메시지 키를 사용하지 않는다면 프로듀서에서 레코드를 전송할 때 메시지 키를 선언하지 않으면 됨
- 메시지 키를 선언하지 않으면 null로 설정되는데 메시지 키가 null로 설정된 레코드는 프로듀서 기본 설정 파티셔닝에 따라서 파티션에 분배되어 적재
- 메시지 값에는 실질적으로 처리할 데이터가 들어있는데 메시지 키와 메시지 값은 직렬화되어 브로커로 전송되기 때문에 컨슈머가 이용할 때는 직렬화 한 형태와 동일한 형태로 역직렬화를 수행해야 함
- 직렬화, 역직렬화 할 때는 반드시 동일한 형태로 처리해야 함
- 만약 프로듀서가 StringSerializer로 직렬화한 메시지 값을 컨슈머가 IntegerDeserializer 로 역직렬화하면 정상적인 데이터를 얻을 수 없음
- 레코드의 오프셋은 0 이상의 숫자로 이루어져 있는데 레코드의 오프셋은 직접 지정할 수 없고, 브로커에 저장될 때 이전에 전송된 레코드의 오프셋 +1 값으로 생성
- 오프셋은 카프카 컨슈머가 데이터를 가져갈 때 사용되는데 오프셋을 사용하면 컨슈머 그룹으로 이루어진 카프카 컨슈머들이 파티션의 데이터를 어디까지 가져갔는지 명확히 지정할 수 있음
- 헤더는 레코드의 추가적인 정보를 담는 메타데이터 저장소 용도로 사용
- 헤더는 키/값 형태로 데이터를 추가하여 레코드의 속성(스키마 버전 등)을 저장하여 컨슈머에서 참조할 수 있음
Producer API

- 카프카에서 데이터의 시작점은 프로듀서
- 프로듀서 애플리케이션은 카프카에 필요한 데이터를 선언하고 브로커의 특정 토픽의 파티션에 전송
- 프로듀서는 데이터를 전송할 떄 리더 파티션을 가지고 있는 카프카 브로커와 직접 통신
- 프로듀서를 구현하는 가장 기초적인 방법은 카프카 클라이언트를 라이브러리로 추가하여 애플리케이션을 만드는 것
- 프로듀서는 데이터 직렬화하여 카프카 브로커로 보내기 때문에 선언 가능한 모든 형태를 브로커로 전송할 수 있음
- 직렬화된 자바 또는 외부 시스템에서 사용 가능하도록 바이트 형태로 데이터를 변환하는 기술
- 직렬화를 사용하면 프로듀서는 자바 기본형과 참조형 뿐만 아니라 동영상, 이미지 같은 바이너리 데이터도 프로듀서를 통해 전송할 수 있음
Consumer API
- 프로듀서가 전송한 데이터는 카프카 브로커에 적재
- 컨슈머는 적재된 데이터를 사용하기 위해 브로커로부터 데이터를 가져와서 필요한 처리를 수행
- 마케팅 문자를 고객에게 보내는 기능이 있다면 컨슈머는 토픽으로부터 고객 데이터를 가져와서 문자 발송 처리
- 토픽의 파티션으로부터 데이터를 가져가기 위해 컨슈머를 운영하는 방법은 크게 2가지인데, 하나는 1개 이상의 컨슈머로 이루어진 컨슈머 그룹을 운영하는 것이고 다른 하나는 토픽의 특정 파티션만 구독하는 컨슈머를 운영하는 것
Admin API
- 실제 운영환경에서는 프로듀서와 컨슈머를 통해 데이터를 주고받는 것만큼 카프카에 설정된 내부 옵션을 설정하고 확인하는 것이 중요
- 내부 옵션을 확인하는 가장 확실한 방법은 브로커 중 한 대에 접속하여 카프카 브로커 옵션을 확인하는 것이지만 매우 번거로운 작업
- 카프카 커맨드 라인 인터페이스로 명령을 내려 확인하는 방법도 있지만 일회성 작업에 그침
- 카프카 클라이언트에서는 내부 옵션들을 설정하거나 조회하기 위해 AdminClient 클래스를 제공
- AdminClient 클래스를 활용하면 클러스터의 옵션과 관련된 부분을 자동화할 수 있음
CQRS
Command and Query Responsibility Segregation
- 데이터 저장소로부터 읽기와 업데이트 작업을 분리하는 패턴
- 애플리케이션의 퍼포먼스, 확장성, 보안성을 극대화할 수 있고 시스템의 유연성을 바탕으로 시간이 지나면서 지속적으로 시스템을 발전 시켜나갈 수 있고 여러 요청으로 들어온 복수의 업데이트 명령들에 대한 충돌도 방지할 수 있음
전통적인 방식의 문제점
- 전통적인 아키텍처는 데이터베이스에서 데이터를 조회하고 업데이트 하는데 동일한 데이터 모델을 사용
- 간단한 CRUD 작업에 대해서라면 문제없이 동작하는데 복잡해지면 유지보수를 어렵게 만드는데 애플리케이션은 데이터 조회 시 각기 다른 형태의 DTO들에 대해 객체 매핑을 하게 됨
- 복잡성이 증가하고 데이터를 쓰거나 업데이트 할 때는 유효성 검사와 비즈니스 로직이 수행되어야 하는데 이 모든걸 하나의 데이터 모델이 수행하면 너무 많은 것을 수행하는 복잡한 모델이 됨
- 읽기와 쓰기의 부하는 보통 같지 않기 때문에 각각 다른 성능이 요구됨
- 읽기와 쓰기 작업에서 사용되는 데이터의 표현이 다를 수 있음
- 병렬로 작업하면 데이터 경합이 발생
- 보안 관리도 복잡
→ 해결책
- CQRS는 읽기와 쓰기를 각각 다른 모델로 분리하는데 명령(Command)을 통해 데이터를 쓰고 Query를 이용해서 데이터를 읽음
- 명령은 데이터 중심적이 아니라 수행할 작업 중심
- 명령은 보통 동기적으로 처리되기보다 비동기적으로 큐에 쌓인 후 수행
- 쿼리는 데이터베이스를 절대로 수정하지 않는데 쿼리는 어떠한 도메인 로직도 캡슐화하지 않은 DTO 만을 반환
- 읽기/쓰기 모델은 서로 격리될 수 있는데 이렇게 읽기/쓰기 모델을 분리하는 것은 애플리케이션 디자인과 구현을 더욱 간단하게 만들어주지만 CQRS 코드는 ORM 툴을 통해 DB 스키마로부터 자동으로 생성되도록 할 수 없다는 단점이 있음
- 확실한 격리를 위해서 물리적으로 읽기와 쓰기를 분리할 수 있는데 읽기 DB의 경우 복잡한 조인문이나 ORM 매핑을 방지하기 위해서 materialized view를 가지는 조회에 최적화된 별도의 DB 스키마를 가질 수 있도록 만드는 것으로 단지 다른 DB 스키마가 아니라 아예 다른 타입의 데이터 저장소를 사용할 수도 있는데 쓰기는 RDBMS를 사용하고 읽기의 경우 MongoDB와 같은 NoSQL을 사용하는 것