NLP Blog

9. 데이터 파이프라인 구축하기

|

9. 데이터 파이프라인 구축하기

  • 데이터 파이프라인에 있어서 카프카가 갖는 주요한 역할은 데이터 파이프라인의 다양한 단계 사이사이에 있어 매우 크고 안정적인 버퍼 역할을 해줄 수 있다는 점
    • 데이터 파이프라인의 데이터를 쓰는 쪽과 읽는 쪽을 분리
    • 하나의 원본에서 가져온 동일한 데이터를 서로 다른 적시성 timeliness과 가용성 요구 조건을 가진 여러 대상 애플리케이션이나 시스템으로 보낼 수 있게 함
    • 파이프라인의 양쪽을 분리 -> 신뢰성, 보안성, 효율성 증대 -> 데이터 파이프라인에 적합

9.1 데이터 파이프라인 구축 시 고려 사항

9.1.1 적시성

  • 좋은 데이터 통합 시스템은 각각의 데이터 파이프라인에 대해 서로 다른 적시성 요구 조건을 지원하면서도 업무에 대한 요구 조건이 변경되었을 때 이전하기 쉽다
  • 카프카를 이해하는 좋은 방법은 쓰는 쪽과 읽는 쪽 사이의 시간적 민감도에 대한 요구 조건을 분리시키는 거대한 버퍼로 생각하는 것
  • 백프레셔의 적용도 단순 -> 데이터의 소비 속도가 온전히 읽는 쪽에 의해 결정되므로, 카프카 자체에서 필요한 경우 쓰는 쪽에 대한 응답을 늦춤으로써 백프레셔를 적용

9.1.2 신뢰성

  • 우리는 단일 장애점을 최대한 피하는 한편 모든 종류의 장애 발생에 대해 신속하고 자동화된 복구를 수행해야 함
  • 또 다른 고려 사항은 전달 보장 delivery guarantee 이다.
    • 대부분의 경우 최소 한 번 보장을 요구하는 것이 보통 -> 원본 시스템에서 발생한 이벤트가 모두 목적지에 도착해야 함
    • ‘정확히 한 번’ 전달 보장을 요구하는 경우도 자주 볼 수 있음
  • 카프카는 자체적으로 ‘최소 한 번’전달을 보장
  • 트랜잭션 모델이나 고유 키를 지원하는 외부 데이터 저장소와 결합됐을 때 ‘정확히 한 번’까지도 보장 가능
    • 많은 엔드포인트들이 ‘정확히 한 번’ 전달을 보장하는 데이터 저장소이므로 대체로 카프카 기반 데이터 파이프라인 역시 ‘정확히 한 번’ 전달 보장
    • 카프카 커넥트 API가 오프셋을 다룰 때 외부 시스템과의 통합을 지원하는 API를 제공하기 때문에 커넥터 개발도 쉽다

9.1.3 높으면서도 조정 가능한 처리율

  • 처리율이 갑자기 증가해야 하는 경우에도 적응할 수 있어야 함
  • 카프카가 읽는 쪽과 쓰는 쪽의 버퍼 역할을 하므로, 더 이상 프로듀서의 처리율과 컨슈머의 처리율을 묶어서 생각하지 않아도 됨
    • 프로듀서 처리율이 컨슈머 처리율을 넘어설 경우 데이터는 컨슈머가 따라잡을 때까지 카프카에 누적되므로, 복잡한 백프레셔 메커니즘 개발할 필요 없음
    • 카프카는 독립적으로 프로듀서나 컨슈머를 추가하여 확장 가능 -> 요구 조건에 맞춰 파이프라인의 한쪽을 동적, 독립적으로 확장

9.1.4 데이터 형식

  • 데이터 파이프라인에서 가장 중요하게 고려해야 할 것 중 하나 : 서로 다른 데이터 형식과 자료형을 적절히 사용
  • 서로 다른 데이터베이스와 다른 저장 시스템마다 지원되는 자료형이 제각기 다름
    • Avro 타입을 사용하여 XML이나 관계형 데이터를 카프카에 적재
    • 엘라스틱 서치 : JSON
    • HDFS : Parquet
    • S3 : CSV
  • 카프카와 커넥트 API는 데이터 형식에 완전히 독립적

9.1.5 변환

  • 데이터 파이프라인을 구축하는 두가지 방식 : ETL, ELT

ETL (Extract-Transform-Load)

  • 데이터 파이프라인이 통과하는 데이터에 변경을 가하는 작업까지도 담당
  • 데이터를 수정한 뒤 다시 저장할 필요가 없어 시간과 공간 절약 가능
  • 연산과 저장의 부담을 데이터 파이프라인으로 옮긴다는 특성이 장점/단점이 될 수 있다
  • 누가 파이프라인에서 데이터 삭제를 일으키면, 사용처에서는 그걸 사용할수가 없다

ELT (Extract-Load-Transform)

  • 데이터 파이프라인이 대상 시스템에 전달되는 데이터가 원본 데이터와 최대한 비슷하도록 (자료형 변환 정도) 최소한 변환만을 수행
  • 대상 시스템에 ㅗ치대한의 유연성 제공
  • 변환 작업이 대상 시스템의 CPU와 자원을 잡아먹는다

  • 카프카 커넥트는 원본 시스템의 데이터를 카프카로 옮길 때 혹은 카프카의 데이터를 대상 시스템으로 옮길 때 단위 레코드를 변환할 수 있게 해주는 Single Message Transformation 기능 탑재
    • 다른 토픽으로 메시지 보내기
    • 필터링, 자료형 변환
    • 특정한 필드 삭제
  • Join, Aggreation과 같이 더 복잡한 변환 작업은 카프카 스트림으로 가능

9.1.6 보안

  • 보안에 대해 고려해야하는 점
    • 누가 카프카로 수집되는 데이터 접근가능?
    • 파이프라인을 통과하는 데이터가 암호화 되었나?
    • 누가 파이프라인을 변경가능한가?
    • 접근이 제한된 곳의 데이터를 읽거나 써야할 경우, 인증 통과 가능한가?
    • 개인 식별 정보 (Personally Identifiable Information, PII)를 저장, 접근, 사용할 때 법과 규제를 준수하는가?
  • 카프카는 데이터 암호화 지원
  • SASL을 이용한 인증/인가 지원
  • 허가받거나 허가받지 않은 접근 내역에 대한 감사 로그 지원
  • 외부 비밀 설정 지원 (ex, HashiCorp Vault)

9.1.7 장애 처리

  • 모든 데이터가 항상 완벽할 것이라고 가정하는 것은 위험
  • 모든 이벤트를 장기간에 걸쳐 저장하도록 카프카를 설정할 수 있기 때문에, 필요할 경우 이전 시점으로 돌아가서 에러르 ㄹ복구 가능

9.1.8 결합Coupling과 민첩성Agility

  • 데이터 파이프라인을 구현할 때 중요한 것 중 하나는 데이터 원본과 대상을 분리할 수 있어야 함
  • 의도치 않게 결합이 생기는 경우는 다음과 같음

임기응변Ad-hoc 파이프라인

  • 어떤 기업이나 조직들은 애플리케이션을 연결해야 할 때마다 커스텀 파이프라인 구축 (Logstash, Flume, Informatica…)
  • 이 경우 파이프라인이 특정 엔드포인트에 강하게 결합되어 설치, 유지 보수, 모니터링에 상당한 노력 필요

메타데이터 유실

  • 만약 데이터 파이프라인이 스키마 메타데이터 보존x, 스키마 진화 지원x이면 소스와 싱크의 소프트웨어들이 강하게 결합됨
  • 스키마 정보가 없으므로, 두 소프트웨어 모두 데이터 파싱/해석 방법을 알아야 함

과도한 처리

  • 파이프라인에서 데이터 처리를 너무 많이 하면 하단에 있는 시스템들이 데이터 파이프라인을 구축할 때 어떤 필드를 보존할지, 어떻게 데이터를 집적할지, 등에 선택지가 별로 남지 않게 됨
  • 하단 애플리케이션의 요구 조건이 자주 변경될 수 있는데, 그 때마다 데이터 파이프라인을 변경해야 하는 경우가 생긴다
  • 가공되지 않은 raw 데이터를 가능한 한 건드리지 않은 채로 하단에 있는 애플리케이션으로 내려보내고 (Kafka Streams 포함), 데이터를 처리하고 집적하는 방법은 애플리케이션이 알아서 결정하게 하는 것이 좀 더 유연하다

9.2 카프카 커넥트 vs. 프로듀서/컨슈머

  • 전통적인 프로듀서와 컨슈머를 사용하는 방법 vs 커넥트 API와 커넥터를 사용하는 방법
  • 카프카 커넥트는 카프카를 직접 코드나 API를 작성하지 않았고, 변경도 할 수 없는 데이터 저장소에 연결시켜야 할 떄 쓴다
  • 카프카 커넥트의 사용자들이 실제로 해 줘야 할 일은 설정 파일을 작성하는 것 뿐
  • 연결하고자 하는 데이터 저장소의 커넥터가 아직 없다면, 카프카 클라이언트 또는 커넥트 API 둘 중 하나를 사용해서 애플리케이션 직접 작성 가능
    • 커넥트 API를 사용하는 것이 여러 표준화된 관리 기능을 제공하여 편리하다

9.3 카프카 커넥트

  • 커넥트는 카프카와 다른 데이터 저장소 사이에 확장성과 신뢰성을 가지면서 데이터를 주고받을 수 있는 수단 제공
  • 커넥터 플러그인을 개발하고 실행하기 위한 API와 런타임 제공
  • 커넥터 플러그인은 카프카 커넥트가 실행시키는 라이브러리, 데이터 이동 담당
  • 커넥터는 여러 worker 프로세스들의 클러스터 형태로 실행됨
  • 사용자는 워커에 커넥터 플러그인을 설치한 뒤 REST API를 사용해서 커넥터별 설정을 잡아 주거나 관리해주면 됨
  • 커넥터는 대용량의 데이터 이동을 병렬화해서 처리하고 워커의 유휴 자원을 더 효율적으로 활용하기 위해 task를 추가로 실행
  • 소스 커넥터 task는 원본 시스템으로부터 데이터를 읽어 와서 커넥트 자료 객체의 형태로 워커 프로세스에 전달만 해주면 됨
  • 싱크 커넥트 task는 워커로부터 커넥트 자료 객체를 받아서 대상 시스템에 쓰는 작업을 담당
  • 커넥트는 자료 객체를 카프카에 쓸 때 사용되는 형식으로 바꿀 수 있도록 convertor사용
    • JSON, Avro, Protobuf, …

9.3.1 카프카 커넥트 실행하기

  • 카프카 커넥트를 프로덕션 환경에서 사용할 경우, 카프카 브로커와는 별도의 서버에서 커넥트를 실행시켜야 함
  • 카프카 커넥트 워커를 실행시키는 것은 브로커를 실행시키는 것과 매우 비슷
$ bin/connect-distributed.sh config/connect-distibuted.properties
  • 커넥트 워커의 핵심 설정은 다음과 같다

bootstrap.servers

  • 카프카 커넥트와 함께 장동하는 카프카 브로커의 목록
  • 커넥터는 다른 곳의 데이터를 이 브로커로 전달 혹은 브로커에서 다른 시스템으로 전송
  • 클러스터 내의 최소 3개 이상 권장

group.id

  • 동일한 그룹 ID를 갖는 모든 워커들은 같은 커넥트 클러스터를 구성

plugin.path

  • 카프카 커넥트는 커넥터, 컨버터, transformation, 비밀 제공자를 다운로드 받아서 플랫폼에 플러그인할 수 있음
  • 카프카 커넥트에는 커넥터와 그 의존성들을 찾을 수 있는 디렉토리를 1개 이상 설정 가능
  • 237p 참고

key.converter와 value.converter

  • 카프카에 저장될 메시지의 키와 밸류 부분에 각각에 대해 컨버터를 설정해 줄 수 있음
  • 기본값 아파치 카프카에 포함되어 있는 JSONConverter를 사용하는 JSON형식
  • AvroConverter, ProtobufConverter, JsonSchemaConverter 역시 사용 가능
  • 컨버터마다 설정할 수 있는 값이 따로 있음

rest.host.name과 rest.port

  • 커넥터를 설정하거나 모니터링할 때는 카프카 커넥트의 REST API를 사용하는 것이 보통, REST API에 사용할 특정한 포트값을 할당 가능
  • 다믐과 같이 REST API 호출하여 확인 가능
$ curl http://localhost:8083/ 
{"version":"3.0.0-SNAPSHOT", "commit":"faed88303akdf", "kafka_cluster_id":"pfkdIELWNDm8Rtl-vALDKdg"}%
  • REST API의 기준 URL을 호출함으로써 현재 실행되고 있는 버전 확인 가능
$ curl http://localhost:8083/connector-plugins

[
  {
    "class": "org.apache.kafka.connect.file.FileStreamSinkConnector",
    "type": "sink",
    "version": "3.0.0-SNAPHOT"
  },
  ...
]
  • 아파키 카프카 본체만 실행시키고 있는 만큼 사용 가능한 커넥터는 파일 소스, 파일 싱크, 그리고 미러메이커 2.0에 포함된 커넥터 뿐

9.3.2 커넥터 예제: 파일 소스와 파일 싱크

  • 분산 모드로 커넥트 워커 실행, 프로덕션 환경에서는 고가용성 보장을 위해 최소 두세 개의 프로세스를 실행시켜야 하겠지만, 여기서는 하나만…
$ bin/connect-distributed.sh config/connect-distributed.properties &
  • 다음 차례는 파일 소스 시작, 예제로는 카프카 설정 파일을 읽어오도록 커넥터 설정
$ echo '{"name":"load-kafka-config", "config":{"connector.class":"FileSreamSource","file":"config/server.properties","topic":"kafka-config-topic"}}' | curl -X POST -d @- http://localhost:8083/connectors \
    -H "Content-Type: application/json"
{
  "name": "load-kafka-config",
  "config": {
    "connector.class": "FileStreamSource",
    "file": "config/server.properties",
    "topic": "kafka-config-topic",
    "name": "load-kafka-config"
  },
  "tasks": [
    {
      "connector": "load-kafka-config",
      "task": 0
    }
  ],
  "type": "source"
}
  • 제대로 저장되었는지 확인
$ bin/kafka-console-consumer.sh --bootstrap-server=localhost:9002 \
  --topic kafka-config-topic --from-beginning
  • 제대로 했다면 240p 하단 결과를 볼 수 있음
  • 이것은 config/server.properties 파일의 내용물이 커넥터에 의해 줄 단위로 JSON으로 변환된 뒤 kafka-config-topic 토ㅠ픽에 저장된 것
  • JSON 컨버터는 레코드마다 스키마 정보를 포함시키는 것이 기본 작동
    • 이 경우 그냥 String 타입의 열인 payload하나만 있을 뿐, 각 레코드는 파일 한 줄씩을 포함
  • 이제 싱크 커넥터를 사용해서 토픽의 내용물을 파일로 내보내보자
    • 이렇게 생성된 파일은 원본 server.properties와 완전히 동일할 것
    • JSONConverter가 JSON 레코드를 텍스트 문자열로 원상복구 시킬 것이기 때문
$ echo 'P{"name":"dump-kafka-config", "config":
{"connector.class":"FileStreamSink","file":"copy-of-server-properties","topics":"kafka-config-topic"}}'
| curl -X POST -d @- http://localhost:8083/connectors --header "Content-Type:application/json"

{"name":"dump-kafka-config","config":
{"connector.class":"FileStreamSink","file":"copy-of-server-properties","topics":"kafka-config-topic","name":"dump-kafka-config"},"tasks":[]}
  • 소스 쪽 설정과 다른 부분
    • 클래스 이름이 이제 FileStreamSink
    • file속성은 있지만 레코드를 읽어 올 파일이 아닌 레코드를 쓸 파일을 가리킴
    • 토픽 하나를 지정하는 대신 topics를 지정
  • 여기까지 잘 했다면 kafka-config-topic에 넣어 줬던 config/server.properties 파일과 완전히 동일한 copy-of-server-properties 파일이 생성되었을 것
  • 커넥터를 삭제하려면 다음과 같이 한다
$ curl -X DELETE http://localhost:8083/connectors/dump-kafka-config
  • 실제 프로덕션에서 쓰면 안됨… 241p 하단 참조

9.3.3 커넥터 예제: MySQL에서 Elasticsearch로 데이터 보내기

  • p242~p249 참고

9.3.4 개별 메시지 변환

  • 데이터를 복사하는 것은 그 자체로 유용하지만, 대개 ETL 파이프라인에는 변환 단계가 포함됨
  • stateless 변환을 stateful한 스트림 처리와 구분하여 SMT(Single Message Transformation)이라고 부름
  • SMT는 보통 코드를 작성할 필요 없이 수행 됨
  • Join, Aggregation등은 카프카 스트림즈 사용 필요
  • SMT 종류들
    • Cast : 필드의 데이터 타입을 바꿈
    • MaskField : 특정 필드의 내용물을 null로 채움, 민감한 정보나 개인 식별 정보를 제거할 때 유용
    • Filter : 특정한 조건에 부합하는 모든 메시지를 제외하거나 포함
    • Flatten : 중첩된 자료 구조를 편다. 각 밸류값의 경로 안에 있는 모든 필드의 이름을 이어분틴 것이 새 키 값이 됨
    • HeaderFrom : 메시지에 포함되어 있는 필드를 헤더로 이동시키거나 복사
    • InsertHeader : 각 메시지의 헤더에 정적인 무자열을 추가
    • InsertField : 메시지에 새로운 필드를 추가해 넣는다. 오프셋과 같은 메타데이터에서 가져온 값일 수도 있고 정적인 값일 수도 있음
    • RegexRouter : 정규식과 교체할 문자열을 사용해서 목적지 토픽의 이름을 바꿈
    • ReplaceField : 메시지에 포함된 필드를 삭제하거나 이름을 변경
    • TimestampConverter : 필드의 시간 형식을 바꿈
    • TimestampRouter : 메시지에 포함된 타임스탬프 값을 기준으로 토픽 변경. 이것은 싱크 커넥터에서 특히나 유용, 타임스탬프 기준으로 저장된 특정 테이블의 파티션에 메시지를 복사해야 할 경우, 토픽 이름만으로 목적지 시스템의 데이터세트를 찾아야 하기 때문
  • 참고 자료

9.3.5 카프카 커넥트: 좀 더 자세히 알아보기

  • 카프카 커넥트를 사용하려면 워커 클러스터를 실행시킨 뒤 커넥터를 생성하거나 삭제해주어야 함
  • 각 시스템과 그들 사이의 상호작용에 대해 조금 더 자세히 살펴보자

1. 커넥터(connector)와 태스크(task)

  • 커넥터 플러그인은 커넥터 API를 구현함. 이것은 커넥터와 태스크, 두 부분을 포함한다
커넥터
  • 커넥터에서 몇 개의 태스크가 실행되어야 하는지 결정
  • 데이터 복사 작업을 각 태스크에 어떻게 분할해 줄지 결정
  • 워커로부터 태스크 설정을 얻어와서 태스크에 전달
태스크
  • 태스크는 데이터를 실제로 카프카에 넣거나 가져오는 작업을 담당
  • 모든 태스크는 워커로부터 컨텍스트를 받아서 초기화 됨
  • 각 컨텍스트는 각 커넥터가 초기화될 때 필요한 정보를 갖고 있음

2. 워커

  • 카프카 커넥트의 워커 프로세스는 커넥터와 태스크를 실행시키는 역할을 맡는 ‘컨테이너’ 프로세스라고 할 수 있음
  • 워커 프로세스는 커넥터와 그 설정을 정의하는 HTTP요청 처리, 커넥트 설정을 내부 카프카 토픽에 저장, 커넥터와 태스크 실행, 적절한 설정값 전달
  • 워커 프로세스가 크래시 날 경우, 다른 워커들이 이를 감지하여 커넥터와 태스크를 다른 워커로 재할당
  • 새로운 워커가 커넥트 클러스터에 추가된다면 다른 워커들이 이것을 감지하여 부하 균형이 잡히도록 커넥터와 태스크를 할당
  • 소스와 싱크 커넥터의 오프셋을 내부 카프카 토픽에 자동으로 커밋하는 작업과 태스크에서 에러가 발생할 경우, 재시도하는 작업도 담당
  • 커텍터와 태스크는 데이터 통합에서 데이터 이동 단계를 맡음
  • 워커는 REST API, 설정 관리, 신뢰성, 고가용성, 규모 확장성 그리고 부하 분산을 담당
  • 위와 같은 관심사의 분리 (separation of concerns)야말로 고전적인 컨슈머/프로듀서 API가 아닌, 커넥트 API를 사용할 때의 주된 이점
    • 고전적인 컨슈머/프로듀서를 사용해서 설정관리, 에러 처리, REST API, 모니터링, 배ㅗㅍ, 규모 확장 및 춧고, 장애 대응과 같은 기능을 구현할 경우 제대로 동작하게 만드려면 몇 달이 걸림
    • 워커가 위와 같은 일을 알아서 처리해줌

3. 컨버터 및 커넥트 데이터 모델

  • 카프카 커넥터 API에는 데이터 API가 포함되어 있음. 이 API는 데이터 객체와 이 객체의 구조를 나타내는 스키마 모두를 다룸
    • ex) JDBC 소스 커넥터는 데이터베이스의 여리을 읽어온 뒤, 베이터 베이스에서 리턴된 열의 데이터 타입에 따라 ConnectSchema 객체를 생성
      • 각각의 열에 대해, 우리는 해당 열의 이름과 저장된 값을 저장
      • 모든 소스 커넥터는 이것과 비슷한 작업을 수행 -> 원본 시스템의 이벤트를 읽어와서 Schema, Value 순서쌍 생성
      • 싱크 커넥터는 정확히 반대 작업을 수행
  • 커넥트 워커가 데이터 객체를 카프카에 어떻게 써야하는가?
    • 컨버터가 이곳에 사용됨 (현재 기본 데이터 타입, 바이트 배열, 문자열, Avro, JSON, 스키마 있는 JSON, Protobuf)
  • 싱크 커넥터는 정확히 반대 방향의 처리
  • 컨버터를 사용함으로써 커넥트 API는 커넥터 구현과는 무관하게, 카프카에 서로 다른 형식의 데이터를 저장할 수 있도록 해줌
  • 사용 가능한 컨버터만 있다면, 어떤 커넥터도 레코드 형식에 상관 없이 사용 가능

4. 오프셋 관리

  • 오프셋 관리는 워커 프로세스가 커넥터에 제공하는 편리한 기능 중 하나
  • 커넥터는 어떤 데이터를 이미 처리했는지 알아야 함, 그리고 커넥터는 카프카가 제공하는 API를 사용해서 어느 이벤트가 이미 처리되었는지에 대한 정보를 유지 관리할 수 있음
  • 소스 커넥터의 경우, 커넥터가 커넥트 워커에 리턴하는 레코드에는 논리적인 파티션과 오프셋이 포함됨
    • 이것은 카프카의 파티션과 오프셋이 아니라 원본 시스템에서 필요로 하는 파티션과 오프셋 ex) 파일 소스의 경우, 파일이 파티션 역할; 파일 안의 줄 혹은 문자 위치가 오프셋 역할; JDBC의 경우 테이블이 파티션, 테이블 레코드의 ID나 타임스탬프가 오프셋 역할
    • 소스 커넥터를 개발할 때 가장 중요한 것 중 하나는 원본 시스템의 데이터를 분할하고 오프셋을 추적하는 좋은 방법을 결정하는 것 -> 커넥터의 병렬성 수준이나 전달의 의미구조에 영향을 미칠 수 있음
  • 소스 커넥터가 레코드들을 리턴하면, 우커는 이 레코드를 카프카 브로커로 보냄, 만약 브로커가 해당 레코드를 성공적으로 쓴 뒤 해당 요청에 대한 응답을 보내면, 그제서야 워커는 방금 전 카프카로 보낸 레코드에 대한 오프셋을 저장
    • 이렇게 함으로써 커넥터는 재시작 혹은 크래시 발생 후에도 마지막으로 저장되었던 오프셋에서부터 이벤트를 처리할 수 있음
    • 이 오프셋을 카프카 내부 토픽에 저장할 수도 있지만, 다른곳에 할 수도 있음
  • 싱크 커넥터는 비슷한 과정을 정반대 순서로 실행

9.4 카프카 커넥트의 대안

9.4.1 다른 데이터 저장소를 위한 수집 프레임워크

  • 하둡의 경우 플룸, 엘라스틱서치의 로그스태시, Fluentd 등이 있음
  • 카프카를 중심으로 쓰고있다면 kafka connect를 쓰는것이 좋다면, 엘라스틱서치를 중점적으로 쓰고있다면 로그스태시 써라

9.4.2 GUI 기반 ETL 툴

  • 인포매티카, Talend, Pentaho, Apache NiFi, StreamSets
  • 이러한 시스템들의 주된 담점은 대개 복잡한 워크플로를 상정하고 개발 되었기에, 단순히 데이터 교환이 목적일 경우 다소 무겁고 복잡하다

9.4.3 스트림 프로세싱 프레임워크

  • 대부분의 스트림 프로세싱 프레임워크는 카프카에서 이벤트를 읽어와서 다른 시스템에 쓰는 기능을 포함하고 있음
  • 단 메시지 유실이나 오염과 같은 문제에 대응하기는 좀 어러울 수 있음

9.5 요약

  • 커넥트 써라 최고다

Comment  Read more

13. 서브클래싱과 서브타이핑

|

13. 서브클래싱과 서브타이핑

  • 상속의 두가지 용도
    • 타입 계층 구현 : 부모 클래스는 일반적인 개념 구현 (일반화 generalization), 자식 클래스는 특수한 개념 구현(특수화 specialization)
    • 코드 재사용 : 간단한 선언만으로 부모 클래스의 코드 재사용 가능, 하지만 부모 클래스와 자식 클래스가 강하게 결합되기 때문에 변경하기 어려운 코드를 얻게 될 확률이 높다.
  • 상속을 사용하는 일차적인 목표는 코드 재사용이 아니라 타입 계층을 구현하는 것이어야 함
  • 결론부터 말하자면 동일한 메시지에 대해 서로 다르게 활동할 수 있는 다형적인 객체를 구현하기 위해서는 객체의 행동을 기반으로 타입 계층을 구성해야 함
  • 타입 사이의 관계를 고려하지 않은 채 단순히 코드를 재사용하기 위해 상속을 사용해서는 안된다.

13.1 타입

개념 관점의 타입

  • 우리가 인지하는 세상의 사물의 종류
    • 자바, 루비, 자바스크립트, C : 프로그래밍 언어 -> 프로그래밍 언어라는 타입으로 분류
    • 어떤 대상이 타입으로 분류될 때, 그 대상을 타입의 인스턴스(instance)라고 부름
    • 일반적으로 타입의 인스턴스를 객체라고 부름
  • Martin98, Larman04
    • 심볼(symbol) : 타입에 이름을 붙인 것
    • 내연(intention) : 타입의 정의로서 타입에 속하는 객체들이 가지는 공통적인 속성이나 행동
    • 외연(extension) : 타입에 속하는 객체들의 집합

프로그래밍 언어 관점의 타입

  • 연속적인 비트에 의미와 제약을 부여하기 위해 사용

타입에 수행될 수 있는 유효한 오퍼레이션의 집합을 정의

  • 자바에서 + 연산자는 원시형 숫자 타입이나 문자열 타입의 객체에는 사용할 수 있지만, 다른 클래스의 인스턴스에 대해서는 사용할 수 없음
  • 하지만 C++와 C#에서는 연산자 오버로딩을 통해 +연산자를 사용하는 것이 가능
  • 모든 객체지향 언어들을 객체의 타입에 따라 적용 가능한 연산자의 종류를 제한함으로써 프로그래머의 실수를 막아줌

타입에 수행되는 오퍼레이션에 대해 미리 약속된 문맥을 제공

  • 예를 들어 자바에서 a + b라는 연산이 있을 때 ab의 타입이 int라면 두 수를 더할 것, 하지만 ab의 타입이 String이라면 두 문자열을 하나의 문자열로 합칠 것
  • 객체를 다루는 방법에 대한 문맥을 결정하는 것은 바로 객체의 타입임

객체지향 패러다임 관점의 타입

  • 타입이란
    • 개념 관점에서 타입이란 콩통의 특징을 공유하는 대상들의 분류
    • 프로그래밍 언어 관점에서 타입이란 동일한 오퍼레이션을 적용할 수 있는 인스턴스들의 집합
  • 객체지향 프로그래밍에서 오퍼레이션은 객체가 수신할 수 있는 메시지를 의미
  • 객체의 타입이란 객체가 수신할 수 있는 메시지의 종류를 정의하는 것 -> 퍼블릭 인터페이스

객체의 퍼블릭 인터페이스가 객체의 타입을 결정한다. 따라서 동일한 퍼블릭 인터페이스를 제공하는 객체들은 동일한 타입으로 분류된다

  • 객체에게 중요한 것은 속성이 아니라 행동
  • 객체들이 동일한 상태를 가지고 있더라도 퍼블릭 인터페이스가 다르다면 이들은 서로 다른 타입으로 분류
  • 객체가 외부에 제공하는 행동에 초점을 맞춰야 한다

13.2 타입 계층

타입 사이의 포함관계

프로그래밍 언어 타입의 인스턴스 집합

더 세분화된 타입을 부분집합화

  • 프로그래밍 언어 타입은 객체지향 언어 타입과 절차적 언어 타입을 포함
  • 객체지향 언어 타입은 클래스 기반 언어 타입과 프로토타입 기반 언어 타입을 포함
  • 동인한 인스턴스가 하나 이상의 타입으로 분류되는 것도 가능 -> 자바는 프로그래밍 언어인 동시에 객체지향 언어에 속하며 크래스 기반 언어이다
  • 타른 타입을 포함하는 타입은 포함되는 타입보다 좀 더 일반화된 의미를 표현할 수 있음
  • 다른 타입을 포함하는 타입은 포함되는 타입보다 더 많은 인스턴스를 가짐
  • 포함하는 타입은 외연 관점에서는 더 크고 내연 관점에서는 더 일반적
  • 포함되는 타입은 외연 관점에서는 더 작고 내연 관점에서는 더 특수함

프로그래밍 언어 타입 계층

  • 타입 계층을 구성하는 두 타입 관계에서 더 일반적인 타입을 슈퍼타입(supertype)
  • 더 특수한 타입을 서브타입(subtype)
  • 일반화 -> 좀 더 보편적이고 추상적으로 만드는 과정
  • 특수화 -> 어떤 타입의 정의를 좀 더 구체적이고 문맥 종속적으로 만드는 과정
  • Martin 98
    • 일반화는 다른 타입을 와전히 포함하거나 내포하는 타입을 식별하는 행위 또는 그 행위의 결과를 가리킴
    • 특수화는 다른 타입 안데 전체적으로 포함되거나 완전히 내포되는 타입을 식별하는 행위 또는 그 행위의 결과를 가리킴

객체지향 프로그래밍과 타입 계층

  • 슈퍼타입이란 서브타입이 정의한 퍼블릭 인터페이스를 일반화시켜 상대적으로 범용적이고 넓은 의미로 정의한 것
  • 서브타입이란 슈퍼타입이 정의한 퍼블릭 인터페이스를 특수화시켜 상대적으로 구체적이고 좁은 의미로 정의한 것
  • 서브타입의 인스턴스는 슈퍼타입의 인스턴스로 간주될 수 있다 -> 핵심, 상속과 다형성의 관계를 이해하기 위한 출발점

13.3 서브클래싱과 서브타이핑

  • 객체지향 프로그래밍 언어에서 타입 구현 -> 클래스 사용, 타입 계층 구현 -> 상속 이용

언제 상속을 사용해야 하는가?

  • 다음 두 가지 질문에 모두 “예”라고 대답 가능 해야 함

상속 관계가 is-a 관계를 모델링 하는가?

  • 일반적으로 “자식 클래스는 부모 클래스다” 라고 말해도 이상하지 않다면 상속을 사용할 후보로 간주 가능

클라이언트 입장에서 부모 클래스의 타입으로 자식 클래스를 사용해도 무방한가?

  • 상속 계층을 사용하는 클라이언트의 입장에서 부모 클래스와 자식 클래스의 차이점을 몰라야 한다
  • 이를 자식 클래스와 부모 클래스 사이의 행동 호환성이라고 부름
  • 클라이언트의 관점에서 두 클래스에 대해 기대하는 행동이 다르다면 비록 그것이 어휘적으로 is-a 관계로 표현할 수 있다고 해도 상속을 사용해서는 안됨

is-a 관계

  • 새는 펭귄이지만 날 수 없음
  • 클라이언트가 새에 대해 기대하는 행동이 “나는 것”이라면 펭귄은 새를 상송해서는 안됨

행동 호환성

  • 행동의 호환 여부를 판단하는 기준은 클라이언트의 관점
  • PenguinBird의 서브타입이 아닌 이유는 클라이언트의 입장에서 모든 새가 날 수 있다고 가정하기 때문
public void flyBird(Bird bird) {
  // 인자로 전달된 모든 bird는 날 수 있어야 함
  bird.fly();
}
  • 현재 Penguinbird의 자식 클래스이기 때문에 컴파일러는 업캐스팅을 허용
  • 따라서 flyBird 메서드의 인자로 Penguin의 인스턴스가 전달되는 것을 막을 수 있는 방법이 없음
  • 하지만 Penguin은 날 수 없고, 클라이언트는 모든 bird가 날 수 있기를 기대하기 때문에 flyBird 메서드로 전달돼서는 안됨
  • 따라서 이 둘을 상속 관계로 연결한 위 설계는 수정되어야함
  • 어거지로 수정 안하고 쓴다면?

방법 1. 오버라이딩해서 내부 구현 비우기

public class Penguin extends Bird {
  ...
  @Override
  public void fly() {}
}
  • 이 방법은 어떤 행동도 수행하지 않기 때문에 모든 bird가 날 수 있다는 클라이언트의 기대를 만족시키지 못함

방법 2. Penguindml fly 메서드를 오버라이딩한 후 예외를 던짐

pbulic class Penguin extends Bird {
  ...
  @Override
  public void fly() {
    throw new UnsupprotedOperationException();
  }
}
  • 클라이언트는 flyBird에서 예외가 발생할 것을 기대하지 않음

방법 3. flyBird 수정해서 인자가 Penguin이 아닐 경우에만 fly 메시지 전송

public boid flyBird(Bird bird) {
  // 인자로 전달된 모든 birtd가 Penguin의 인스턴스가 아닐 경우에만
  // fly() 메시지 전송
  if (!(bird instanceof Penguin)) {
    bird.fly();
  }
}
  • 또 다른 날지 못하는 새가 상속 계층에 추가된다면? if문 추가… -> 개방-폐쇄 원칙 위반

클라이언트의 기대에 따라 계층 분리하기

  • 다음 코드는 새에는 날 수 있는 새와 없는 새 두 분류가 존재하며, 그 중 펭귄은 날 수 없는 새에 속한다는 사실을 표현
public class Bird {
  ...
}

public class FlyingBird extends Bird {
  public void fly() {...}
  ...
}

public class Penguin extends Bird{
  ...
}

public void flyBird(FlyingBird bird) {
  bird.fly();
}
  • 이제 flyBird 메서드는 FlyingBird 타입을 이용해 날 수 있는 새만 인자로 전달돼야 한다는 사실을 코드에 명시 가능

  • 이 문제를 해결하는 다른 방법은 클라이너트에 따라 인터페이스를 분리하는 것
  • 만약 Bird가 날 수 있으면서 걸을 수도 있어야 하고, Penguin은 오직 걸을 수만 있다고 가정
  • 가장 좋은 방법은 fly 오퍼레이션을 가진 Flyer 인터페이스와 walk 오퍼레이션을 가진 Walker 인터페이스로 분리하는 것

클라이언트 기대에 따른 인터페이스 분리


  • 더 좋은 방법은 합성을 사용하는 것

합성을 이용한 코드 재사용


  • 이처럼 인터페이스를 클라이언트 기대에 따라 분리함으로써 변경에 의해 영향을 제어하는 설계 원칙을 인터페이스 분리 원칙 (Interface Segregation Principle, ISP) 이라고 부름
  • 자연어에 현혹되지 말고 요구사항 속에서 클라이언트가 기대하는 행동에 집중하라
    • 크래스의 이름 사이에 어떤 연관성이 있다는 사실은 아무런 의미도 없다
    • 두 클래스 사이에 행동이 호환되지 않는다면 올바른 타입 계층이 아니기 때문에 상속을 사용해서는 안된다

서브클래싱과 서브타이핑

서브클래싱(subclassing)

  • 다른 클래스의 코드를 재사용할 목적으로 상속을 사용하는 경우
  • 자식 클래스와 부모 클래스의 행동이 호환되지 않기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 없음
  • 구현 상속 (implementation inheritance) 또는 클래스 상속(class inheritance)이라고 부르기도 함

서브타이핑(subtyping)

  • 타입 계층을 구성하기 위해 상속을 사용하는 경우
  • 서브타이핑에서는 자식 클래스와 부모 클래스의 행동이 호환되기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 있음
  • 서브타이핑을 인터페이스 상속(interface inheritance) 이라고 부르기도 함

  • 서브 클래싱과 서브 타이핑을 나누는 기준은 상속을 사용하는 목적
  • 사실 10장에서 나쁜 설계의 예로 든 대부분의 상속은 구현을 재사용하기 위해 사용된 서브클래싱에 속함

추상 클래스를 상속한다는 것은 단순한 코드의 재사용을 위한 상속이 아니라 추상 클래스가 정의하고 있는 인터페이스를 상속하겠다는 의미인 것이다 [GOF 1994]

  • 슈퍼타입과 서브타입 사이의 관계에서 가장 중요한 것은 퍼블릭 인터페이스이다.
  • 슈퍼타입 인스턴스를 요구하는 모든 곳에서 서브타입의 인스턴스를 대신 사용하기 위해 만족해야 하는 최소한의 조건은 서브타입의 퍼블릭 인터페이스가 슈퍼타입에서 정의한 퍼블릭 인터페이스와 동일하거나 더 많은 오퍼레이션을 포함해야 함
  • 서브타이핑 관계가 유지되지 위해서는 서브타입이 슈퍼타입이 하는 모든 행동을 동일하게 할 수 있어야 한다 -> 행동 호환성 만족, 대체 가능성(substitutability)
  • 위 지침은 리스코프 치환 원칙이라는 이름으로 정리되어 소개되어 왔다

13.4 리스코프 치환 원칙

  • 상속 관계로 연결한 두 클래스가 서브 타이핑 관계를 만족 시키려면

여기서 요구되는 것은 다음의 치환 속성과 같은 것이다. S형의 각 객체 o1에 대해 T형의 객체 o2가 하나 있고, T에 의해 정의된 모든 프로그램 P에서 T가 S로 치환될 때, P의 동작이 변하지 않으면 S는 T의 서브타입이다.

  • 서브타입은 그것의 기반 타입에 대해 대체 가능해야 한다
  • 클라이언트가 차이점을 인식하지 못한 채 기반 클래스의 인터페이스를 통해 서브플래스를 사용할 수 있어야 한다
  • 10장에서 살펴본 StackVector는 리스코프 치환 원칙을 위반하는 전형적인 예다
    • 클라이언트가 부모 클래스인 Vector에 대해 기대하는 행동을 Stack에 대해서는 기대할 수 없기 때문에 행동 호환성을 만족시키지 않기 떄문
  • 정사각형은 직사각형이다 예 -> 454p ~ 457p 참조

클라이언트와 대체 가능성

  • 클라이언트 입장에서 정사각형을 추상화한 Square는 직사각형을 추상화한 Rectangle과 동일하지 않다는 점
  • 클라이언트 코드에서 RectangleSquare로 대체할 경우 Rectangle에 대해 세워진 가정을 위반할 확률이 높다
  • StackVector가 서브 클래싱 관계인 이유도 상속으로 인해 Stack에 포함돼서는 안되는 Vector의 퍼블릭 인터페이스가 Stack의 퍼블릭 인터페이스에 포함됐기 때문
  • 클라이언트와 격리한 채로 본 모델을 의미 있게 검증하는 것이 불가능하다[Martin02]
  • 대체 가능성을 결정하는 정은 클라이언트다

is-a 관계 다시 살펴보기

  • is-a는 클라이언트 관점에서 is-a일 때만 참이다
  • is-a 관계 역시 행동이 우선임

리스코프 치환 원칙은 유연한 설계의 기반이다

  • 새로운 자식 클래스를 추가하더라도 클라이언트 입장에서 동일하게 행동하기만 한다면 클라이언트를 수정하지 않고도 상속 계층을 확장 할 수 있다
  • 클라이언트의 입장에서 퍼블릭 인터페이스의 행동 방식이 변경되지 않는다면 클라이언트의 코드를 변경하지 않고도 새로운 자식 클래스와 협력할 수 있다

DIP, LSP, OCP가 조합된 유연한 설계

  • 의존성 역전 원칙
  • 리스코프 치환 원칙
  • 개방-폐쇄 원칙

타입 계층과 리스코프 치환 원칙

  • 클래스 상속은 타입 계층을 구현할 수 있는 다양한 방법 중 하나일 뿐
    • 자바와 C#의 인터페이스, 스칼라의 트레이트, 동적 타입 언더의 덕 타이핑 등의 기법을 사용하면 클래스 사이의 상속을 사용하지 않고 서브파이핑 관계를 구현할 수 있음
  • 리스코프 치환 원칙을 위반하는 예를 설명하는 데 클래스의 상속을 자주 사용하는 이유는 대부분의 객체 지향 언어가 구현 단위로서 클래스를 사용하고 코드 재사용의 목적으로 상속을 지나치게 남용하는 경우가 많기 때문

13.5 계약에 의한 설계와 서브타이핑

  • 클라이언트와 서버 사이읭 협력을 의무(obligation)와 이익(benefit)으로 구성된 계약의 관점에서 표현하는 것을 계약에 의한 설계(Design By Contract, DBC)라고 부른다
    • 클라이언트가 정상적으로 메서드를 실행하기 위해 만족시켜야하는 사전조건(precondition)과 메서드가 실행된 후에 서버가 클라이언트에게 보장해야 하는 사후조건(postcondition), 메서드 실행 전과 실행 후에 인스턴스가 만족시켜야 하는 클래스 불변식(class invariant)의 세 가지 요소로 구성됨
  • 리스코프 치환 원칙과 계약에 의한 설계 사이의 관계를 다음과 같은 한 문장으로 요약 가능

서브타입이 리스코프 치환 원칙을 만족시키기 위해서는 클라이언트와 슈퍼타입 간에 체결된 “계약”을 준수해야 한다

  • 이해를 돕기 위해 영화 예매 시스템에서 DiscountPolicy와 협력하는 Movie 클래스를 예로 들어보자
public class Movie {
  ...
  public Money calculateMovieFee(Screening screening) {
    return fee.minus(discountPolicy.calculateDiscountAmount(screening));
  }
}

MovieDiscountPolicy의 인스턴스에게 calculateDiscountAmount 메시지를 전송하는 클라이언트다. DiscountPolicyMovie의 메시지를 수신한 후 할인 가격을 계산해서 반환한다

public abstract class DiscountPolicy {
  public Money calculateDiscountAmount(Screening screening) {
    for(DiscountCondition each : conditions) {
      if (each.isSatisfiedBy(Screening)) {
        return getDiscountAmount(screening);
      }
    }

    return screening.getMovieFee();
  }

  abstract protected Money getDiscountAmount(Screening screening);
}
  • DBC에 따르면 협력하는 클라이언트와 슈퍼타입의 인스턴스 사이에는 어떤 계약이 맺어져 있다. 클라이언트와 슈퍼타입은 이 계약을 준수할 때만 정상적으로 협력할 수 있음
  • 서브타입이 슈퍼타입처럼 보일 수 있는 유일한 방법은 클라이언트가 슈퍼타입과 맺은 계약을 서브타입이 준수하는 것 뿐
  • 위 코드에서 암묵적인 사전조건과 사후조건이 존재
    • 사전 조건
      • DiscountPolicycalculateDiscountAmount 메서드는 인자로 전달된 screeningnull인지 여부를 확인하지 않음
      • 하지만 screeningnull이 전달된다면 screening.getMovieFee()가 실행될 때 NullPointerException 예외가 던져질 것
      • 단정문을 통해 사전조건 표현 가능 assert screening != null && screening.getStartTime().isAfter(LocalDateTime.now())
    • 사후 조건
      • MoviecalcualationMovieFee 메서드를 살펴보면 DiscountPolicycalculationDiscountAmount 메서드의 반환값은 항상 null이 아니어야 한다. 추가로 반환되는 값은 청구되는 요금이기 때문에 0원보다 커야 한다
      • assert amount != null && amount.isGreaterThanOrEqual(Money.ZERO);
public abstract class DiscountPolicy {
  public Money calculateDiscountAmount(Screening screening) {
    checkPrecondition(screening);

    Money amount = Money.ZERO;
    for(DiscountCondition each : conditions) {
      if (each.isSatisfiedBy(Screening)) {
        amount = getDiscountAmount(screening);
        checkPostcondition(amount);
        return amount;
      }
    }

    amount = screening.getMovieFee();
    checkPostcondition(amount);
    return amount;
  }

  protected void checkPrecondition(Screening screening) {
    assert screening != null && screening.getStartTime().isAfter(LocalDateTime.now());
  }

  protected void checkPostcondition(Money amount) {
    assert amount != null && amount.isGreaterThanOrEqual(Money.ZERO);
  }
  abstract protected Money getDiscountAmount(Screening screening);
}
public class Movie {
  ...
  public Money calculateMovieFee(Screening screening) {
    if (screening == null || screening.getStartTime().isBefore(LocalDateTime.now())) {
      throw new InvalidScreeningException();
    }
    return fee.minus(discountPolicy.calculateDiscountAmount(screening));
  }
}

서브타입과 계약

  • 계약의 관점에서 상속이 초래하는 가장 큰 문제는 자식 클래스가 부모 클래스의 메서드를 오버라이딩할 수 있다는 것
  • 서브타입이 계약조건을 만족하려면?

서브타입에 더 강력한 사전조건을 정의할 수 없다 서브타입에 슈퍼타입과 같거나 더 약한 사전조건을 정의할 수 있다. 서브타입에 슈퍼타입과 같거나 더 강한 사후조건을 정의할 수 있다 서브타입에 슈퍼타입과 더 약한 사후조건을 정의할 수 없다

Comment  Read more

5. 프로그램 내에서 코드로 카프카 관리하기

|

5. 프로그램 내에서 코드로 카프카 관리하기

  • 카프카를 관리하는 데 있어서는 많은 CLI, GUI 툴이 있다 (9장에서 살펴볼 것)
  • 하지만 클라이언트 애플리케이션에서 직접 관리 명령을 내려야 할 때도 있음 (eg. IoT)
  • 0.11 버전부터 CLI뿐만 아니라 프로그래밍 언어에서 사용가능한 AdminClient가 추가 됨
    • 토픽 목록 조회, 생성, 삭제
    • 클러스터 상세 정보 확인
    • ACL 관리
    • 설정 변경

5.1 AdminClient 개요

5.1.1 비동기적이고 최종적 일관성을 가지는 API

  • 가장 중요한 것: AdminClient는 비동기적으로 작동함
  • 각 메서드는 요청을 클러스터 컨트롤러로 전송한 뒤 바로 1개 이상의 Future 객체를 리턴
  • Future 객체는 비동기 작업의 결과를 가리킴
    • 비동기 작업의 결과를 확인
    • 취소
    • 완료까지 대기
    • 작업이 완료되었을 때 실행할 함수를 지정하는 메서드 보유
  • Future 객체를 Result 객체 안에 감싸는데, Result객체는 작업이 끝날 때까지 대기하거나 작업 결과에 대해 일반적으로 뒤이어 쓰이는 작업을 수행하는 헬퍼 메서드를 가지고 있음
    • ex) AdminClient.createTopics 메서드는 CreateTopicsResult 객체를 리턴
    • 이 객체는 모든 토픽이 생성될 떄까지 기다리거나, 각각의 토픽 상태를 하나씩 확인하거나, 아니면 특정한 토픽이 생성된 뒤 해당 토픽의 설정을 가져올 수 있도록 해줌
  • 카프카 컨트롤러로부터 브로커로의 메타데이터 전파가 비동기적으로 이루어지기 때문에, AdminClient API가 리턴하는 Future 객체들은 컨트롤러의 상태가 완전히 업데이트된 시점에서 완료된 것으로 간주된다.
  • 이 시점에서 모든 브로커가 전부 다 새로운 상태에 대해 알고 있지는 못할 수 있기 때문에, listTopics 요청은 최신 상태를 전달받지 않은 (이제 막 만들어진 토픽에 대해 알고 있지 않은) 브로커에 의해 처리될 수 있음
  • 이러한 속성을 최종적 일관성 (eventual consistency)이라고 함
  • 최종적으로 모든 브로커는 모든 토픽에 대해 알게될 것이지만, 그게 언제가 될 지에 대해서는 아무런 보장도 할 수 없다

5.1.2 옵션

  • AdminClient의 각 메서드는 메서드별로 특정한 Options 객체를 인수로 받음
    • listTopics 메서드는 ListTopicsOptions 객체를 인수로 받음
    • describeCluster 메서드는 DescribeClusterOptions를 인수로 받음
  • 모든 AdminClient 메서드가 가지고 있는 매개변수는 timeoutMs
    • TimeoutException을 발생시키기 전, 클러스터로부터의 응답을 기다리는 시간을 조정함

5.1.3 수평 구조

  • 모든 어드민 작업은 KafkaAdminClient에 구현되어 있는 아파치 카프카 프로토콜을 사용해서 이루어짐
    • 여기에는 객체 간의 의존 관계나 네임스페이스 따위가 없다
    • 인터페이스가 굉장히 크기 때문에 다소 논란이 있기는 하지만, 이러한 구조에는 장점이 있음
    • JavaDoc문서에서 메서드를 찾기 쉽고, IDE에서 자동완성 해줌
    • 엉뚱한 곳을 찾고 있는지 고민할 필요 없음, 없으면 구현 안된거임

5.1.4 추가 참고 사항

  • 클러스터의 상태를 변경하는 모든 작업(create, delete, alter)은 컨트롤러에 의해 수행
  • 클러스터 상태를 읽기만 하는 작업(list, describe)는 아무 브로커에서나 수행될 수 있음, 클라이언트 입장에서 보이는 가장 부하가 적은 브로커로 전달
  • 대부분의 어드민 작업은 AdminClient를 통해서 수행되거나, 주키퍼에 저장되어 있는 메타데이터를 직접 수정하는 방식으로 이루어짐.
  • 주키퍼를 직접 수정하는 것을 절대 쓰지 말 것을 강력히 권장, 카프카는 주키퍼 의존성을 제거할 계획

5.2 AdminClient 사용법: 생성, 설정, 닫기

  • AdminClient를 사용하기 위해서는 객체를 생성해야 함. 이를 위해 설정값 객체인 Properties를 인수로 주어야 함
  • close 메서드로 닫을 수 있음, 이 때 타임 아웃 매개변수를 받음

5.2.1 client.dns.lookup

  • 카프카는 부트스트랩 서버 설정에 포함된 호스트명을 기준으로 연결을 검증, 해석, 생성함
  • 이 단순한 모델은 대부분의 경우 잘 작동하지만 두 맹점이 있음
    • DNS alias를 사용한 경우와, 2개 이상의 IP 주소로 연결되는 하나의 DNS 항목을 사용할 경우

DNS 별칭을 사용하는 경우

  • broker1.hostname.com, broker2.hostname.com …와 같은 naming convention을 따르는 브로커들을 가지고 있다고 가정
  • 이 모든 부로커들을 부트스트랩 서버 설정에 일일이 지정하는것보다 이 모든 브로커 전체를 가리킬 하나의 DNS alias을 만들 수 있음 (ex. all-brokers.hostname.com)
  • 이는 매우 편리하지만, SASL을 사용해서 인증을 하려고 할 때는 문제가 생김
    • SASL을 사용할 경우 클라이언트는 all-brokers.hostname.com에 대해서 인증을 하려하는데, 서버의 보안 주체는 broker2.hostname.com인 탓 -> man-in-the-middle attack일 가능성이 있음 -> 인증 거부, 연결 실패
      • 이 경우, client.dns.lookup-=resolve_canonical_bootstrap_server_only
      • 위 성정은 DNS별칭에 포함된 모든 브로커 이름을 일일이 부트스트랩 서버 목록에 넣어 준 것과 동일하게 작동함

다수의 IP 주소로 연결되는 DNS 이름을 사용하는 경우

  • 최근 네트워크 아키텍처에서 모든 브로커를 프록시나 로드 밸런서 뒤로 숨기는 것은 매우 흔함
  • 이 경우, 로드 밸러서가 SPOF가 되는 걸 보고싶지는 않을 것임
  • 위 이유 때문에, broker.hostname.com를 여러 개의 IP 주소로 연결하는 것은 매우 흔함 (이 IP주소들은 모두 로드밸런서로 연결되고 따라서 모든 트래픽이 동일한 브로커로 전달됨)
  • 이 IP 주소들은 시간이 지남에 따라 변경될 수 있음
  • 기본적으로, 카프카 클라이언트는 해석된 첫 번째 호스트명으로 연결을 시도할 뿐.
    • 해석된 IP 주소가 사용 불능일 경우 브로커가 멀쩡하게 작동하고 있는데도 클라이언트는 연결에 실패할 수 있음
  • 이를 위해 client.dns.lookup=use_all_dns_ips 사용 권장

5.2.2 request.timeout.ms

  • 이 설정은 애플리케이션이 AdminClient의 응답을 기다릴 수 있는 시간의 최대값을 정의
  • 만약 애플리케이션에서 AdminClient 작업이 주요 경로 상에 있을 경우, 타임아웃 값을 낮게 잡아준 뒤 제 시간에 리턴되지 않는 응답은 조금 다른 방식으로 처리해야 할 수도 있음
    • 서비스가 시작될 때 특정한 토픽이 존재하는지를 확인
    • 브로커가 응답하는 데 30초 이상 걸릴 경우, 확인 작업을 건너뛰거나 일단 서버 기동을 계속한 뒤 나중에 토픽의 존재를 확인

5.3 필수적인 토픽 관리 기능

  • admin.listTopics()Future 객체들을 감싸고 있는 ListTopicsResult 객체를 리턴 함
  • Future 객체에서 get 메서드를 호출하면, 실행 스레드는 서버가 토픽 이름 집합을 리턴할 때까지 기다리거나 아니면 타임아웃 예외를 발생함
  • 좀 더 복잡한 작업이 필요할 수도 있음 (해당 토픽이 필요한 만큼의 파티션과 레플리카를 가지고 있는지 확인)
  • 카프카 커넥트와 컨플루언트의 스키마 레지스트리는 설정을 저장하기 위해 카프카 토픽을 사용함, 이들은 처음 시작할 때 아래의 조건을 만족하는 설정 토픽이 있는지를 확인
    • 하나의 파티션을 가짐. 이것은 설정 변경에 온전한 순서를 부여하기 위해 필요
    • 가용성을 보장하기 위해 3개의 레플리카를 가짐
    • 오래 된 설정값도 계속해서 저장되도록 토픽에 압착 설정이 되어 있음
    • 131p 예제 참고
  • 지금까지 우리가 살펴본 예제들은 서로 다른 AdminClient 메서드가 리턴하는 Future 객체에 블로킹방식으로 작동하는 get() 메서드를 호출 함
  • 만약 많은 어드민 요청을 처리할 것으로 예상되는 서버를 개발하고 있을 경우, 블로킹을 사용하는 것은 불리함
  • 사용자로부터 계속해서 요청을 받고, 카프카로 요청을 보내고, 카프카가 응답하면 그제서야 클라이언트로 응답을 보내는 게 더 합리적
  • 133p. 예제 참고
    • 여기서 중요한 것은 우리가 카프카로부터의 응답을 기다리지 않는다는 점
    • 카프카로부터 응답이 도착하면 DescribeTopicResult가 HTTP 클라이언트에게 응답을 보낼 것
    • 그 사이, HTTP 서버는 다른 요청을 처리 가능

5.4 설정 관리

  • 설정 관리는 ConfigResource 객체를 사용해서 할 수 있음
  • 설정 가능한 자원에는 브로커, 브로커 로그, 토픽이 있음
  • 위 설정은 kafka-configs.sh 혹은 다른 툴로 하는게 보통이지만, 애플리케이션에서 토픽의 설정을 확인하거나 수정하는 일도 흔하다
  • 많은 어플리케이션들은 정확한 작동을 위해 압착 설정이 된 토픽을 사용
  • 이 경우 애플리케이션이 주기적으로 해당 토픽에 실제로 압착 설정이 되어 있는지를 확인해서, 설정이 안 되어 있을 경우 설정을 교정해 주는 것이 합리적.
  • 135p 참고

5.5 컨슈머 그룹 관리

  • 다른 메시지 큐와는 달리, 카프카는 이전에 데이터를 읽어서 처리한 것과 완전히 동일한 순서로 데이터를 재처리할 수 있게 해준다는 점은 언급
  • 메시지를 재처리해야 하는 시나리오에는 여러 가지가 있을 수 있음
    • 사고가 발생한 와중에 오작동하는 애플리케이션을 트러블슈팅하는 경우
    • 재해 복구 상황에서 애플리케이션을 새로운 클러스터에서 작동시키려 하는 경우
  • 여기서는 AdminClient를 사용해서 프로그램적으로 컨슈머 그룹과 이 그룹들이 커밋한 오프셋을 조회하고 수정하는 방법에 대해서 살펴볼 것

5.5.1 컨슈머 그룹 살펴보기

  • `admin.listConsumerGroups().valid().get().forEach(System.out::println);
  • valid() 메서드, get() 메서트를 호출함으로써 리턴되는 모음은 클러스터가 에러 없이 리턴한 컨슈머 그룹만을 포함
  • errors() 메서드를 사용해서 모든 예외를 가져올 수 있음
  • 특정 그룹에 대해 더 상세한 정보를 보고싶다면
ConsumerGroupDescription groupDescription = admin.describeConsumerGroups(CONSUMER_GRP_LIST)
        .describedGroups().get(CONSUMER_GROUP).get();
System.out.println("Description of group " + CONSUMER_GROUP + ":" + groupDescription);
  • description은 해당 그룹에 대한 상세한 정보를 담는다.
    • 그룹 멤버, 멤버별 식별자, 호스트명, 멤버별로 할당된 파티션, 할당 알고리즘, 그룹 코디네이터의 호스트명
    • 트러블 슈팅 시 매우 유용
  • 하지만 가장 중요한 정보중 하나인 컨슈머 그룹이 읽고 있는 각 파티션에 대해 마지막으로 커밋된 오프셋 값, 최신 메시지에서 얼마나 뒤떨어졌는지 (lag)
  • 예전에는 커밋 메시지 가져와서 파싱하는 방법 뿐 -> 호환성 보장이 안됨
  • AdminClient를 사용해서 커밋 정보를 얻어오는 방법
  • 137p 참고

5.5.2 컨슈머 그룹 수정하기

  • AdminClient는 컨슈머 그룹을 수정하기 위한 메서드들 역시 가지고 있음
    • 그룹 삭제, 멤버 제외, 커밋된 오프셋 삭제 혹은 변경
    • SRE가 비상 상황에서 임기 응변으로 복구를 위한 툴을 제작할 때 자주 사용
  • 오프셋 변경 기능이 가장 유용함
    • 오프셋 삭제는 컨슈머를 맨 처음부터 실행시키는 가장 간단한 방법처럼 보이지만, 이는 컨슈머 설정에 의존
    • 만약 컨슈머가 시작되는데 커밋된 오프셋을 못찾을 경우, 맨 처음부터 시작해야할까? 아니면 가장 최신 메시지로 건너뛰어야 할까?
    • auto.offset.reset 설정값을 가지고 있지 않을 한 알 길이 없음
    • 명시적으로 커밋된 오프셋을 맨 앞으로 변경하면 컨슈머는 토픽의 맨 앞에서부터 처리를 시작하게 됨 -> 컨슈머가 reset 됨
  • 오프셋 토픽의 오프셋 값을 변경한다 해도 컨슈머 그룹에 변경 여부가 전달되지는 않는다
    • 컨슈머 그룹은 컨슈머가 새로운 파티션을 할당받거나 새로 시작할 때만 오프셋 토픽에 저장된 값을 읽어올 뿐
    • 컨슈머가 모르는 오프셋 변경을 방지하기 위해, 카프카에서는 현재 작업이 돌아가고 있는 컨슈머 그룹에 대한 오프셋을 수정하는 것을 허용하지 않음
  • 오프셋만 변경하면 집계값 등을 중복계산 할 가능성이 있음 -> 상태 저장소를 적절히 변경해주어야 함
  • 139p 예제

5.6 클러스터 메타데이터

  • 클러스터 메타데이터를 들여다 볼 일은 거의 없음
  • 관심 있으면 140p 한번 실행시켜 보던지

5.7 고급 어드민 작업

  • 잘 쓰이지 않고 위험하기까지 한, 하지만 필요할 때 사용하면 믿을수 없을 정도로 유용한 메서드 몇 개에 대해서 설명
  • 사고에 대응중인 SRE에게 매우 중요

5.7.1 토픽에 파티션 추가하기

  • 대체로 토픽의 파티션 수는 토픽이 생성될 때 결정됨
  • 여러 이유 때문에 토픽에 파티션을 추가해야 하는 경우는 매우 드물며 위험할 수 있음
  • 하지만 파티션이 처리할 수 있는 최대 처리량까지 부하가 차올라서 파티션 추가 외에는 선택지가 없는 경우도 있음
  • createPartitions 메서드 사용
Map<String, NewPartitions> newPartitions = new HashMap<>();
newPartitions.put(TOPIC_NAME, NewPartitions.increaseTo(NUM_PARTITIONS+2));
admin.createPartitions(newPartitions).all().get;

5.7.2 토픽에서 레코드 삭제하기

  • 개인정보 보호법 등의 이유로 인해 데이터를 삭제해야 하는 경우가 있음
  • deleteRecords 메서드는 호출 시점을 기준으로 지정된 오프셋보다 더 오래된 모든 레코드에 삭제 표시를 함으로써 컨슈머가 접근할 수 없도록 한다.
  • 이 메서드는 삭제된 레코드의 오프셋 중 가장 큰 값을 리턴하기 때문에 의도했던 대로 삭제가 이루어졌는지 확인 가능
  • 삭제 표시된 레코드를 디스크에서 실제로 지우는 작업은 비동기적으로 일어남
  • 142p. 예제 참고

5.7.3

선호 리더 선출 (preferred leader election)

  • 각 파티션은 선호 리더라 불리는 레플리카를 하나낀 가짐
  • 기본적으로, 카프카는 5분마다 선호 리더 레플리카가 실제로 리더를 맡고있는지를 확인해서, 리더를 맡을 수 있는데도 맡고 있지 않은 경우 해당 레플리카를 리더로 삼는다.
  • electLeader() 메서드 호출

언클린 리더 선출 (unclean leader election)

  • 만약 어느 파티션의 리더 레플리카가 사용 불능 상태가 되었는데 다른 레플리카들은 리더를 맡을 수 없는 상황 이라면 (대개 데이터가 없어서 그렇다). 해당 파티션은 리더가 없게 되고 따라서 사용 불능 상태가 됨
  • 해결 방법 중 하나가 리더가 될 수 없는 레플리카를 그냥 리더로 삼아버리는 것 (언클린 리더 선출)
  • 이는 데이터 유실을 초래

  • 이 메서드는 비동기적으로 작동, 시간이 좀 걸릴 수 있음
  • 143p 참고

5.7.4 레플리카 재할당

  • 레플리카 위치를 바꿔야 하는 경우들이 있음
    • 브로커에 너무 많은 레플리카가 올라가 있어 이동을 원할 때
    • 레플리카를 추가하고 싶을 때
    • 장비를 내리기 위해 모든 레플리카를 다른 장비로 내보내야 할 때
  • alterPartitionReassignments 사용하면 파티션에 속한 각각의 레플리카의 위치를 정밀하게 제어 가능
  • 레플리카 이동은 대량의 데이터 복제를 초래 함
  • 144p 참고

5.8 테스트하기

  • 아파치 카프카는 원하는 수만큼의 브로커를 설정해서 초기화할 수 있는 MockAdminClient 테스트 클래스 제공
  • 자주 사용되는 메서드가 매우 포괄적인 목업 기능을 제공함
  • 145p~147p 예제 참고

5.9 요약

  • AdminClient는 매우 유용하다!

Comment  Read more

8. 의존성 관리하기

|

8. 의존성 관리하기

  • 잘 설계된 객체지향 애플리케이션은 작고 응집도 높은 객체들로 구성
    • 작고 응집도 높은 객체 : 책임의 초점이 명확하고 한 가지 일만 잘 하는 객체
  • 이런 객체들이 단독으로 수행할 수 있는 작업은 거의 없기 때문에 객체간의 협력이 발생
  • 협력은 필수적이나, 다른 객체에 대한 지식을 요함 -> 의존성 발생
  • 과도한 의존성은 애플리케이션을 수정하기 어렵게 만듬
  • 협력을 위해 필요한 의존성은 유지하면서도 변경을 방해하는 의존성은 제거하는 것이 좋은 설계

1. 의존성 이해하기

변경과 의존성

  • 의존성은 실행 시점과 구현 시점에 서로 다른 의미를 가짐
    • 실행 시점: 의존하는 객체가 정상적으로 동작하기 위해서는 실행 시에 의존 대상 객체가 반드시 존재하야 한다.
    • 구현 시점: 의존 대상 객체가 변경될 경우 의존하는 객체도 함께 변경된다
  • 영화 예매 코드의 PereiodCondition 클래스를 이용해 의존성의 개념 설명
    • PeriodCondition 클래스의 isSatisfiedBy 메서드는 Screening 인스턴스에게 getStartTime 메시지를 전송항
public class PeriodCondition implements DiscountCondition {
  private DayOfWeek dayOfWeek;
  private LocalTime startTime;
  private LocalTime endTime;

  ...

  public boolean isStisfiedBy(Screening screening) {
    return screening.getStartTime().getDayofWeek().equals(dayofWeek) &&
      startTime.compareTo(screening.getStartTime().toLocalTime()) <= 0 &&
      endTime.compareTo(screening.getStartTime().toLocalTime()) >= 0;
  }
}
  • 실행 시점에 PeriodCondition의 인스턴스가 정상적으로 동작하기 위해서는 Screening의 인스턴스가 존재해야 함
  • 만약 Screening의 인스턴스가 존재하지 않거나, getStartTime의 메시지를 이해할 수 없다면 PeriodConditionisSatisfiedBy 메서드는 예상했던대로 동작하지 않을 것
  • 이런 상황을 두 객체 사이의 의존성이 존재한다고 말하며, 의존성은 단방향이다.
  • PeriodConditionScreening에 의존한다 (점선화살표)

  • 의존성은 변경에 의한 영향의 전파 가능성을 암시
  • 예시 코드에서, PeriodConditionDayofWeek, LocalTime, Screening에 대해 의존성을 가짐
  • 어떤 형태로든, DayofWeek, LocalTime, Screening, DiscountCondition 이 변경된다면 PeriodCondition도 함께 변경될 수 있음
  • 그림 8.3을 보면 의존성 종류별로 표현된 클래스 다이어그램을 볼 수 있음

의존성 전이

  • 의존성 전이 (transitive dependency)가 의미하는 것은 PeriodConditionScreening에 의존할 경우, PeriodConditionScreening이 의존하는 대상에 대해서도 자동적으로 의존하게 되는 것 (그림 8.4)
  • 의존성은 함께 변경될 수 있는 가능성을 의미하기 때문에 모든 경우에 의존성이 전이되는 것은 아님, 이는 변경의 방향과 캡슐과의 정도에 따라 달라짐
  • 의존성을 직접 의존성간접 의존성으로 나누기도 함
    • 특히 간접 의존성은 코드에 명시적으로 드러나지 않음

런타임 의존성과 컴파일타임 의존성

  • 런타임은 말 그대로 애플리케이션이 실행되는 시점
  • 컴파일타임은 미묘함, 작성된 코드를 컴파일하는 시점을 가리키지만, 문맥에 따라서는 코드 그 자체를 가리키기도 함
    • 동적 타입 언어는 컴파일 타임이 존재하지 않음!
  • 런타임의 주인공은 객체! 따라서 런타임 의존성이 다루는 주제는 객체 사이의 의존성
  • 코드 관점에서의 주인공은 클래스. 따라서 컴파일타임 의존성이 다루는 주재는 클래스 사이의 의존성
  • 이 둘은 다를 수 있다.
    • MovieAmountDiscountPolicyPercentDiscountPolicy모두와 협력할 수 있어야 함 -> 추상클래스 사용
    • Movie클래스는 오직 추상 클래스인 DiscountPolicy 클래스에만 의존
public class Movie {
  ...
  private Discount Policy discountPolicy;

  public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
    ...
    this.discountPolicy = discountPolicy;
  }

  public Money calculateMovieFee(Screening screening) {
    return fee.minus(discountPolicy.calculateDiscountAmount(screening));
  }
}

  • 하지만 런타임 의존성을 살펴보면 상황이 완전히 달라짐
  • 금액 할인 정책을 적용하기 위해서는 인스턴스와 협력해야 함
  • 컴파일 시점에는 전혀 모르던 AmountDiscountPolicyPercentDiscountPolicy 인스턴스와 협력해야 함
  • 컴파일타임 의존성을 런타임 의존성으로 잘 대체해야 함
  • 어떤 클래스의 인스턴스가 다양한 클래스의 인스턴스와 협려가기 위해서는 협력할 인스턴스의 구체적인 클래스를 알아서는 안된다
  • 클래스가 협력할 객체의 클래스를 명시적으로 드러내고 있다면 다른 클래스의 인스턴스와 협력할 가능성 자체가 사라짐

컨텍스트 독립성

  • 구체 클래스에 대해 의존하는 것은 클래스의 인스턴스가 어떤 문맥에서 사용될 것인지를 구체적으로 명시하는 것과 같다
  • Movie 클래스 안에 PercentDiscountPolicy 클래스에 대한 컴파일타임 의존성을 명시적으로 표현하는 것은 Movie가 비율 할인 정책이 적용된 영화의 요금을 계산하는 문맥에서 사용될 것이라는 것을 가정
  • 클래스가 특정한 문맥에 강하게 결합될수록 다른 문맥에서 사용하기는 더 어려워짐
  • 클래스가 사용된 특정한 문맥에 대해 최소한의 가정만으로 이뤄져 있다면 다른 무낵에서 재사용하기가 더 수월해진다 -> 컨텍스트 독립성

의존성 해결하기

  • 컴파일타임 의존성은 구체적인 런타임 의존성으로 대체돼야 함 -> 의존성 해결
    • 객체를 생성하는 시점에 생성자를 통해 의존성 해결
    • 객체 생성 후 setter 메서드를 통해 의존성 해결
    • 메서드 실행 시 인자를 이용해 의존성 해결
  • ex, 어떤 영화의 요금 계산에 금액 할인 정책을 적용하고 싶다고 가정해보자, 다음과 같이 Movie 객체를 생성할 때 AmountDiscountPolicy의 인스턴스를 Movie의 생성자에 인자로 전달하면 됨
Movie avatar = new movie("아바타",
  Duration.ofMinutes(120),
  Money.wons(10000),
  new AmountDiscountPolicy(...));
  • Movie의 생성자에 PercentDiscountPolicy의 인스턴스를 전달하면 비율 할인 정책에 따라 요금을 계산하게 될 것이다
Moview starWars = new Movie("스타워즈",
  Duration.ofMinutes(180),
  Money.wons(11000),
  new PercentDiscountPolicy(...));
  • 이를 위해 Movie클래스의 생성자 코드는 다음과 같다
public class Movie {
  public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
    ...
    this.discountPolicy = discountPolicy;
  }
}
  • 클래스 생성 후 메서드를 이용하는 방법
Movie avatar = new Movie(...);
avatar.setDiscountPolicy(new AmoutDiscountPolicy(...));

public class Movie {
  ...
  public void setDiscountPolicy (DiscountPolicy discountPolicy) {
    this.discountPolicy = discountPolicy;
  }
}
  • setter 메서드가 있으면 인스턴스를 중간에 바꿀 수 있음
  • 실행 시점에 의존 대상을 변경할 수 있으므로, 설계를 좀 더 유연하게 만들 수 있음
  • 단점은 객체가 생성된 후에 협력에 필요한 의존 대상을 설정하기 때문에 객체를 생성하고 의존 대상을 설정하기 전까지는 객체의 상태가 불완전할 수 있다.
  • 둘 다 쓰면 된다!

2. 유연한 설계

의존성과 결합도

  • 객체들이 협력하기 위해서는 서로의 존재와 수행 가능한 책임을 알아야 함
  • 이런 지식들이 객체 사이의 의존성을 낳는다
    • 의존성이 나쁜 것은 아니지만, 과하면 문제가 될 수 있다
  • Movie가 비율 할인 정책을 구현하는 PercentDiscountPolicy에 직접 의존한다면?
public class Movie {
  ...
  private PercentDisplayPolicy percentDiscountPolicy;

  public movie(String title, Duration runningTime, Money fee, 
  PercentDiscountPolicy percentDiscountPolicy) {
    ...
    this.percentDiscountPolicy = percentDiscountPolicy;
  }

  public Money calculateMovieFee(Screening screening) {
    return fee.minus(percentDiscountPolicy.calculateDiscountAmount(screening));
  }
}
  • 이 코드에서는 명시적으로 MoviePercentDiscountPolicy에 의존하고 있음을 보여줌
  • 의존성 자체가 문제는 아니지만, 너무 구체적인 클래스에 의존하고 있음
  • 자신이 전송하는 calculateDiscountAmount 메시지를 이해할 수 있고 할인된 요금을 계산할 수만 있다면 어떤 타입의 객체와 협력하더라도 상관 없음 -> 추상 클래스 DiscountPolicy 사용으로 해결
  • PercentDiscountPolicy에 대한 의존은 부적절, DiscountPolicy에 대한 의존은 적절
  • 바람직한 의존성이란? 재사용성과 관련됨, 다시 말해 컨텍스트에 독립적이여야 함
  • 세련된 표현이 존재: 결합도
    • loose coupling, weak coupling : 바람직한 의존성
    • tight coupling, strong coupling : 바람직하지 못한 의존성
  • 바람직한 의존성이란 설계를 재사용하기 쉽게 만드는 의존성

지식이 결합을 낳는다

  • 결합도의 정도는 한 요소가 자신이 의존하고 있는 다른 요소에 대해 알고 있는 정보의 양으로 결정
  • 서로에 대해 알고 있는 지식의 양이 결합도를 결정
  • 더 많이 알고 있다는 것은 더 적은 컨텍스트에서 재사용 가능하다는 것을 의미
  • 기존 지식에 어울리지 않는 컨텍스트에서 클래스의 인스턴스를 사용하기 위해서 할 수 있는 유일한 방법은 클래스를 수정하는 것뿐
  • 이 목적을 달성할 수 있는 가장 효과적인 방법은 추상화

추상화에 의존하라

  • 추상화란 어떤 양상, 세부사항, 구조를 좀 더 명확하게 이해하기 위해 특정 절차나 물체를 의도적으로 생략하거나 감춤으로써 복잡도를 극복하는 방법
  • 대상에 대해 알아야 하는 지식의 양을 줄일 수 있기 때문에 결합도를 느슨하게 유지할 수 있음
  • 일반적으로 추상화와 결합도의 관점에서 의존 대상을 다음과 같이 구분하는 것이 유용, 아래로 갈 수록 클라이언트가 알아야 하는 지식의 양이 적어지기 때문에 결합도가 느슨해짐
    • 구체 클래스 의존성 (concrete class dependency)
    • 추상 클래스 의존성 (abstract class depecdency)
    • 인터페이스 의존성 (interface dependency)
  • 추상 클래스의 클라이언트는 여전히 협력하는 대상이 속한 클래스 상속 계층이 무엇인지에 대해서는 알고있어야함
  • 인터페이스에 의존하면 상속 계층을 모르더라도 협력이 가능해짐

명시적인 의존성

  • 아래 코드는 한 가지 실수로 인해 결합도가 불필요하게 높아짐, 실수가 뭘까?
public class Movie {
  ...
  private DiscountPolicy discountPolicy;

  public Movie(String title, Duration runningTime, Money fee) {
    ...
    this.discountPolicy = new AmountDiscountPolicy(...);
  }
}
  • 생성자가 잘못됨
  • 타입만 추상화 한다고 끝이 아니라, 클래스 안에서 구체 클래스에 대한 모든 의존성을 제거해야 함
public class Movie {
  ...
  private DiscountPolicy discountPolicy;

  public Movie(String title, Duration RunningTime, Money fee, DiscountPolicy discountPolicy) {
    ...
    this.discountPolicy = discountPolicy;
  }
}
  • 생성자의 인자가 추상 클래스 타입으로 선언됐기 때문에, 자식 클래스라면 어떤 것이라도 전달 가능
  • 이렇게 퍼블릭 인터페이스에 인자로 의존성을 드러내는 경우를 명시적인 의존성 (explicit dependency) 이라고 부름
  • 반면 Movie 내부에서 AmountDiscountPolicy의 인스턴스를 직접 생성하는 방식은 MovieDiscountPolicy에 의존한다는 사실을 감춤 -> 숨겨진 의존성 (hidden dependency)
  • 숨겨진 의존성을 파악하는 것은 매우 고통스러운 일 (코드 구현을 다 봐야함)
  • 더 큰 문제는 클래스를 다른 컨텍스트에서 재사용하기 위해 내부 구현을 직접 변경해야 하는 것 -> 버그의 발생 가능성을 내포함
  • 의존성은 명시적으로 표현되어야 함
  • 경계해야 할 것은 의존성 자체가 아니라 의존성을 감추는 것

new는 해롭다

  • new를 잘못 사용하면 클래스 사이의 결합도가 극단적으로 높아짐
    • new연산자를 사용하기 위해서는 구체 클래스의 이름을 직접 기술해야 한다. 따라서 new를 사용하는 클라이언트는 추상화가 아닌 구체 클래스에 의존할 수 밖에 없기 때문에 결합도가 높아짐
    • new 연산자는 생성하려는 구체 클래스뿐만 아니라 어떤 인자를 이용해 클래스의 생성자를 호출해야 하는지도 알아야 한다. 따라서 new를 사용하면 클라이언트가 알아야 하는 지식의 양이 늘어나기 때문에 결합도가 높아짐
  • 272p. 예시 참고
  • new는 결합도를 높이기 때문에 해롭다. new는 여러분의 클래스를 구체 클래스에 결합시키는 것만으로 끝나지 않음
    • 협력할 클래스의 인스턴스를 생성하기 위해 어떤 인자들이 필요하고 그 인자들을 어떤 순서로 사용해야 하는지에 대한 정보도 노출시킬뿐만 아니라 인자로 사용되는 구체 클래스에 대한 의존성을 추가함
  • 해결 방법
    • 인스턴스를 생성하는 로직과 생성된 인스턴스를 사용하는 로직을 분리하는 것
    • Movie는 외부로부터 이미 생성된 AmountDiscountPolicy의 인스턴스를 전달받아야 함
    • 사용과 생성의 책임을 분리해서 Movie의 결합도를 낮추면 설계를 유연하게 만들 수 있음.
    • 객체를 생성하는 책임을 객체 내부가 아니라 클라이언트로 옮기는 것에서 시작

가끔은 생성해도 무방하다

  • 협력하는 기본 객체를 설정하고 싶은 경우, 객체 내에서 인스턴스를 직접 생성하는 방식이 유용할 수 있음
public class Movie {
  private DiscountPolicy discountPolicy;

  public Movie(String title, Duration runningTime, Money fee) {
    this(title, runningTime, fee, new AmountDiscountPolicy(...));
  }
  
  public Movie(String title, Duration runningTime, MOney fee, DiscountPolicy discountPolicy) {
    ...
    this.discountPolicy = discountPolicy;
  }
}
  • 위와 같이 대부분 AmountDiscountPolicy를 사용하는 경우는, 따로 생성자를 갖는 것도 유용할 수 있음
  • 메서드를 오버로딩하는 경우에도 사용 가능
  • 다음과 같이 DiscountPolicy의 인스턴스를 인자로 받는 메서드와 기본값을 생성하는 메서드를 함께 사용한다면 클래스의 사용성을 향상시키면서도 다양한 컨텍스트에서 유연하게 사용될 수 있는 여지를 제공가능
public class Movie {
  public Money calculateMovieFee(Screening screening) {
    return calculateMovieFee(screening, new AmountDiscountPolicy(...));
  }

  public Money calculateMovieFee(Screening screening, DiscountPolicy discountPolicy) {
    return fee.minus(discountPolicy.calculateDiscountAmount(screening));
  }
}
  • 설계는 트레이트오프, 재사용성을 높이고 결합도를 높인다면, 이것도 선택가능하다
  • 종종 모든 결합도가 모이는 새로운 클래스를 추가함으로써 사용성과 유연성이라는 두 마리 토끼를 잡을 수 있는 경우도 있음 -> FACTORY

표준 클래스에 대한 의존은 해롭지 않다

  • 변경될 확률이 거의 없는 클래스라면 의존성이 문제가 되지 않음
  • 구체 클래스에 의존해도 됨!

컨텍스트 확장하기

  • 할인 혜택을 제공하지 않는 영화의 예매 요금을 계산하는 경우
public class Movie {
  public Movie(String title, Duration runningTime, Money fee) {
    this(title, runningTime, fee, null);
  }

  public Movie(String title, Duration running Time, Money fee, DiscountPolicy discountPolicy) {
    ...
    this.discountPolicy = discountPolicy;
  }

  public Money calculateMovieFee(Screening screening) {
    if (discountPolicy == null) {
      return fee;
    }

    return fee.minus(discountPolicy.calculateDiscountAmount(screening));
  }
}
  • 위 코드는 적절하지 않음, 왜냐하면, 할인 정책이 없는 경우를 위해 코드 내부를 수정했기 때문
  • 할인 정책이 없는 경우도 할인 정책으로 만든다면?
public class NoneDiscountPolicy extends DiscountPolicy {
  @Override
  protected Money getDiscountAmount(Screening screening) {
    return Money.ZERO;
  }
}
  • 이제 Movie 클래스에 수정을 가하지 않고, 인스턴스 생성 시에 NoneDiscountPolicy를 인자로 주면 됨

  • 두 번째 예는 중복 적용이 가능한 할인 정책 구현
  • 이를 위해서는 Movie가 하나 이상의 DiscountPolicy와 협력할 수 있어야 함
  • List를 인자로 받게 바꾼다? -> Movie 클래스가 수정되야함
  • 이것 또한 중복 할인 정책을 할인 정책의 한가지로 간주하자
public class OverlappedDiscountPolicy extends DiscountPolicy {
  private List<DiscountPolicy> discountPolicies = new ArrayList<>();

  public OverlappedDiscountPolicy(DiscountPolicy ... discountPolicies) {
    this.discountPolicies = Arrays.asList(discountPolicies);
  }

  @Overried
  protected Money getDiscountAmount(Screening screening) {
    Money result = Money.ZERO;
    for (DiscountPolicy each : discountPolicies) {
      result = result.plus(each.caculateDiscountAmount(screening));
    }
    return result;
  }
}

조합 가능한 행동

  • 다양한 종류의 할인 정책이 필요한 컨텍스트에서 Movie를 재사용할 수 있었던 이유는 코드를 직접 수정하지 않고도 협력 대상인 DiscountPolicy 인스턴스를 교체할 수 있었기 때문
  • 어떤 객체와 협력하느냐에 따라 객체의 행동이 달라지는 것은 유연하고 재사용 가능한 설계가 가진 특징
  • 유옇나고 재사용 가능한 설계는 객체가 어떻게(how) 하는지를 장황하게 나영하지 않고도 객체들의 조합을 통해 무엇(what)을 하는지를 표현하는 클래스들로 구성
  • How가 아닌 What에 초첨!

Comment  Read more

1. 카프카 시작하기

|

1. 카프카 시작하기

  • 모든 기업은 데이터로 움직인다. 모든 애플리케이션은 데이터를 생성한다.
  • 데이터는 중요한 정보를 담고있으며, 이를 파악하기 위해서는 데이터를 생성된 곳에서 분석할 수 있는 곳으로 옮겨야 한다.
  • 이러한 작업을 더 빠르게 해낼수록 조직은 더 유연해지고 더 민첩해질 수 있음
    • 데이터를 이동 시키는데 더 적은 노력을 들일수록 핵심 비즈니스에 더 집중할 수 있다
    • Data-driven enterprise에서 pipeline이 중요한 핵심적인 이유

1.1 발생/구독 메시지 전달

  • publish/subscribe messaging의 개념과 데이터 주도 애플리케이션에서의 중요성을 이해해야 함
  • 이 패턴의 특징은 전송자가 데이터를 보낼 때 직접 수신자로 보내지 않는다는 것
    • 전송자는 어떤 형태로든 메시지를 분류해서 보내고, 수신자는 이런게 분류된 메시지를 구독한다
  • 메시지를 전달받고 중계해주는 브로커가 있다

1.1.1 초기의 발행/구독 시스템

그림 1-1 발행자와 구독자가 직접 연결된 단일 지표 발행자

  • 가운데 간단한 메시지 큐나 프로세스간 통신 채널을 놓는 것이 초기 형태
  • 초기에는 잘 동작하지만, 장기간에 걸쳐 지푯값을 분석하고자 하면 잘 되지 않는다.
  • 지표를 저장 및 분석하는 새로운 서비스를 하나 만들면, 그 때마다 새로운 연결이 여러개 추가된다 (그림 1-2)

그림 1-2 발행자와 구독자가 직접 연결된 여러 지표 발행자

  • 이 방식은 technical debt가 엄청나 보이기 때문에 개선이 필요하다
  • 모든 애플리케이션으로부터 지표를 받는 하나의 애플리케이션을 만들고, 이 지푯발들을 필요로 하는 어느 시스템이든 지표를 질의할 수 있도록 해주는 서버를 제공

그림 1-3 지표 발행 및 구독 시스템

1.1.2 개별 메시지 큐 시스템

  • 지표를 다루는 것과 동시에 로그 메시지에 대해서도 비슷한 작업을 해줘야 한다.
  • 그림 1-4 참고
  • 그림 1-2보다는 낫지만, 버그도 한계도 제각각인 다수의 데이터 큐 시스템을 유지 관리해야 함
    • 메시지 교환을 필요로 하는 사례가 추가로 생길 수 있음
  • 일반화된 유형의 데이터를 발행하고 구독할 수 있는 중앙 집중화된 시스템이 필요

1.2 카프카 입문

  • 아파치 카프카는 위 문제를 해결하기 위해 고안된 message pub/sub system임
    • 분산 커밋 로그 혹은 분산 스트리밍 플랫폼이라고 불리기도 함
  • 카프카에 저장된 데이터는 순서를 유지한 채지속성 있게 보관되며 결정적deterministic으로 읽을 수 있다
  • 확장시 성능 향상, 실패시에도 데이터 사용가능하도록 시스템 안에서 데이터를 분산 저장

1.2.1 메시지와 배치

  • 카프카에서 데이터의 기본 단위는 메시지다.
  • 메시지는 key라 불리는 메타데이터 포함 가능, key는 메시지를 저장할 파티션을 결정하기 위해 사용됨
    • 가장 간단한 방법은 키값에서 일정한 해시값을 생성 -> 토픽의 파티션 수로 modular -> 해당 파티션에 저장
  • 카프카는 효율성을 위해 메시지를 batch 단위로 저장
    • 같은 토픽의 파티션에 쓰여지는 메시지들의 집합
    • 네트워크 오버헤드를 줄일 수 있음 (모아서 보내기 때문에)
    • latency를 늘리고, throughput을 늘릴 수 있다 (trade off)
    • 효율적인 데이터 처리를 위해 압축을 사용하는 경우가 많다

1.2.2 스키마

  • 카프카 입장에서 메시지는 그냥 바이트 배열일 뿐이지만, 메시지 스키마가 있다
  • JSON이나 XML 등이 쓰인다
  • 하지만, 타입 처리 기능 등이 부실하여 Avro를 선호함 (Hadoop 프로젝트를 위한 직렬화 프레임워크)
    • 메시지 본체와 스키마를 분리하기 때문에, 스키마가 변경되어도 코드를 생성할 필요 X
    • 강력한 데이터 타이핑과 스키마 변경에 따른 상/하위 호환성 지원

1.2.3 토픽과 파티션

  • 카프카에 저장되는 메시지는 topic 단위로 분류됨
    • 토픽과 비슷한 개념으로는 데이터베이스의 테이블이나 파일시스템의 폴더가 있음
  • 토픽은 다시 여러 개의 partition으로 나뉘어 진다.
  • 커밋 로그의 관점에서는 파티션은 하나의 로그에 해당
  • 파티션에 메시지가 쓰여질 때는 append-only한 형태로 쓰여 짐
  • 토픽에 여러 개의 파티션이 있는 만큼 토픽 안의 메시지 전체에 대해 순서는 보장되지 않으며, 단일 파티션 안에서만 순서가 보장 됨
  • 그림 1-5 참고
  • 각 파티션이 서로 다른 서버에 저장될 수 있기 때문에 하나의 토픽이 여러 개의 서버로 수평적으로 확장 가능
  • 파티션은 복제 가능함, 서로 다른 서버들이 파티션의 복제본을 가질 수 있기 때문에 안정성 제공
  • stream : (파티션의 갯수와 상관없이) 하나의 토픽에 저장된 데이터로 간주되며, producer로부터 consumer로의 하나의 데이터 흐름을 나타 냄

1.2.4 프로듀서와 컨슈머

  • 카프카의 클라이언트 : producer, consumer
  • 고급 클라이언트 API : Kafka Connect, Kafka Streams
  • Producer
    • 새로운 메시지를 생성
    • 다른 pub/sub 시스템에서는 publisher 혹은 writer라고도 부름
    • 토픽에 손한 파티션들 사이에 고르게 나눠서 메시지를 작성 함
    • 특정 경우에 특정 파티션을 지정해서 메시지를 쓰기도 함
      • 이 경우 key과 key값의 hash를 특정 파티션으로 대응시켜주는 partitioner를 사용해서 구현
  • Consumer
    • 메시지를 읽음
    • 다른 pub/sub 시스템에서는 subscriber 혹은 reader라고도 함
    • 1개 이상의 토픽을 구독해서 여기에 저장된 메시지들을 각 파티션에 쓰여진 순서대로 읽어 옴
    • 메시지의 offset을 기록함으로써 어느 메시지까지 앍었는지를 유지
    • Offset은 지속적으로 증가하는 정수값, 카프카가 메시지를 저장할 때 각각의 메시지에 부여해주는 메타데이터
      • 주어진 파티션의 각 메시지는 고유한 오프셋을 가지며, 뒤에 오는 메시지가 앞의 메시지보다 더 큰 오프셋을 가짐
      • 저장되는 값이므로, 읽기 작업을 정지했다가 다시 시작하더라도 마지막으로 읽었던 메시지의 바로 다음 메시지부터 읽을 수 있음
    • 컨슈머는 컨슈머 그룹의 일원으로서 작동
  • Consumer Group
    • 토픽에 저장된 데이터를 읽어오기 위해 협업하는 하나 이상의 컨슈머로 이루어 짐
    • 그림 1-6 참고
    • 컨슈머에서 파티션으로의 대응 관계는 컨슈퍼의 파티션 ownership이라고 부름
    • 이 방법을 사용함으로써 대량의 메시지를 갖는 토픽들을 읽기 위해 컨슈머들을 수평 확장 할 수 있음
    • 장애 대응 가능 (하나가 뻗으면 컨슈머 그룹에 해당 파티션을 읽어오는 새로운 컨슈머 할당)

1.2.5 브로커와 클러스터

  • 하나의 카프카 서버를 broker라고 부른다
  • 브로커는 프로듀서로부터 메시지를 전달받아 오프셋을 항당한 뒤 디스크 저장소에 쓴다
  • 브로커는 컨슈머의 파티션 읽기 (fetch) 요청 역시 처리하고 발행된 메시지를 보내준다.
  • 하나의 브로커는 초당 수천 개의 파티션과 수백만 개의 메시지를 쉽게 처리할 수 있음
  • 카프카 브로커는 cluster의 일부로써 작동하도록 설계됨
  • 하나의 클러스터 안에 여러 개의 브로커가 포함될 수 있으며, 그중 하나의 브로커가 클러스터 controller의 역할을 하게 됨 (자동으로 선출 됨)
  • 컨트롤러는 파티션을 브로커에 할당해주거나 장애가 발생한 브로커를 모니터링하는 등의 관리 기능을 담당
  • 파티션은 클러스터 안의 브로커 중 하나가 담당하며, 그 브로커를 patrition leader라고 부른다
  • 그림 1-7 처럼 복제된 파티션이 여러 브로커에 할당될 수도 있는데 이것들은 파티션의 follower라고 부른다
  • 복제 기능은 파티션의 메시지를 중복 저장함으로써 리더 브로커에 장애가 발생했을 때 팔로워 중 하나가 리더 역할을 이어받을 수 있도록 함
  • 모든 프로듀서는 리더 브로커에 메시지를 발행해야 하지만, 컨슈머는 리더나 팔로워 중 하나로부터 데이터를 읽어올 수 있음
  • 일정 기간 동안 메시지를 지속성 있게 보관하는 보존 기능이 있음
    • 특정 기간 혹은 파티션의 크기가 특정 사이즈에 도달할 때까지 데이터를 보존
    • 한도값에 도달하면 메시지는 만료되어 삭제
    • 각각의 토픽마다 다르게 설정 가능 (사용자 활동 추적 토픽 ~ 며칠, 애플리케이션 지표 ~ 몇시간)
    • 로그 압착 기능 : 같은 키를 갖는 메시지 중 가장 최신의 것만 보존 (changelog 형태의 데이터에 유용)

1.2.6 다중 클러스터

  • 카프카가 활장되어감에 따라 다수의 클러스터를 운용하는 것이 나은 경우가 있음
    • 데이터의 유형별 분리
    • 보안 요구사항을 충족시키기 위한 격리
    • 재해 복구 (DR)를 대시한 다중 데이터센터
  • 특히 카프카가 다수의 데이터센터에서 운용될 때는 데이터센터 간에 메시지를 복제해 줄 필요가 있는 경우가 많음
  • 카프카 프로젝트는 데이터를 다른 클러스터로 복제하는 데 사용되는 MirrorMaker라는 툴을 포함 함
    • 근본적으로 미러메이커도 단지 큐로 연결된 카프카 컨슈머와 프로듀서에 불과
    • 하나의 카프카 클러스터에서 메시지를 읽어와서 다른 클러스터에 쓴다
    • 그림 1-8 참고 : 두 개의 로컬 클러스터의 메시지를 하나의 집적 클러스터로 모은 뒤 다른 데이터 센터로 복사

1.3 왜 카프카인가?

  • pub/sub 메시지 전달 시스템은 많은데, 왜 하필 카프카?

1.3.1 다중 프로듀서

  • 카프카는 자연스럽게 여러 프로듀서 처리 가능
  • 많은 프론트엔드 시스템으로부터 데이터를 수집하고 일관성을 유지하는 데 적격

1.3.2 다중 컨슈머

  • 많은 컨슈머가 상호 간섭 없이 어떠한 메시지 스트림도 읽을 수 있도록 설계 됨
  • 하나의 메시지를 하나의 클라이언트에서만 소비할 수 있도록 되어 있는 많은 큐 시스템과의 결정적인 차이점
  • 다수의 카프카 컨슈머는 컨슈머 그룹의 일원으로 작동함으로써 하나의 스트림을 여럿이서 나눠서 읽을 수 있다

1.3.3 디스크 기반 보존

  • 메시지를 지속성 있게 저장 가능
  • 메시지는 디스크에 쓰여진 뒤 설정된 보유 규칙과 함께 저장
  • 필요에 따라 서로 다른 기간동안 보존될 수 있음
  • 컨슈머를 정지하더라고 메시지는 카프카 안에 남아있음
  • 다시 시작해도 유실 없음

1.3.4 확장성

  • 어떠한 크기의 데이터도 쉽게 처리 가능
  • 클러스터 확장이 용이함

1.3.5 고성능

  • 위 특징들 덕분에 고부하 아래에서도 높은 성능을 보임

1.3.6 플랫폼 기능

  • 카프카 커낵트는 소스 데이터 시스템으로부터 카프카로 데이터를 가져오거나 카프카의 데이터를 싱크 시스템으로 내보내는 작업을 도와 줌
  • 카프카 스트림즈는 규모 가변성과 내고장성을 갖춘 스트림 처리 애플리케이션을 쉽게 개발할 수 있게 해주는 라이브러리

1.4 데이터 생태계

  • 데이터 생태계에 있어서 순환 시스템을 제공 (그림 1-9)
  • 모든 클라이언트에 대해 일관된 인터페이스를 제공하면서 다양한 인프라스트럭처 요소들 사이에 머시지를 전달하는 것
  • 메시지 스키마를 제공하는 시스템과 결합하면 프로듀서와 컨슈머는 더 이상 어떤 형태로든 밀접하게 결합되거나 연결될 필요가 없다
    • 필요할 때마다 관련 컴포넌트를 추가하거나 제거해주면 되며, 프로듀서는 누가 데이터를 사용하는지, 컨슈머 애플리케이션이 몇 개인지와 같은 것에 신경 쓸 필요가 없다

1.4.1 이용 사례

활동 추적

  • LinkedIn에서 사용자 활동 추적에 사용
  • 프론트엔드 애플리케이션의 작동 로그가 하나 이상의 토픽으로 발행되서 백엔드에서 작동 중인 애플맄이션에 전달

메시지 교환

  • 같은 룩앤필을 사용해서 메시지를 포매팅 혹은 데코레이팅
  • 여러 개의 메시지를 모아서 하나의 알림 메시지로 전송
  • 사용자가 원하는 메시지 수신 방식을 적용

지표 및 로그 수집

  • 카프카는 애플리케이션과 시스템의 지푯값과 로그를 수집하는 데도 이상적
  • 여러 애플리세이션이 같은 유형으로 생성한 메시지를 활용하는 대표적인 사례
  • 애플리케이션이 정기적으로 지푯값을 카프카 토픽에 발행하면, 모니터링과 경보를 맡고 있는 시스템이 이 지푯값들을 가져다 사용
  • 로그 메시지 역시 같은 방식으로 발행 가능
  • 목적 시스템을 변경해야 할 때 프론트 엔드 app이나 메시지 수집 방법을 변경할 필요 없음

커밋 로그

  • 데이터베이스에 가해진 변경점들이 (스트림의 형태로) 카르카로 발행될 수 있음
  • 메시지 보존 기능 역시 체인지로그를 저장하는 버퍼로서 유용하게 사용될 수 있음

스트림 처리

  • 메시지가 생성되자마자 실시간으로 데이터를 처리
  • 다수의 원본으로부터 들어온 데이터를 사용해서 메시지를 변환

1.5 카프카의 기원

  • 링크드인 내부에서 데이터 파이프라인 문제를 해결하기 위해 개발
  • 다양한 종유의 데이터를 다루고 고성능 메시지 교환 시스템 역할을 할 수 있도록 설계

1.5.1 링크드인이 직면한 문제

  • 링크드인에서는 로그나 메트릭 말고도 복잡한 요청 추적 기능이 있었는데, 사용자 요청이 내부의 여러 애플리케이션에 어떻게 전파되는지를 보여주는 것
  • 이는 매우 복잡하고, 기존의 폴링 방식은 구멍이 많고 유지보수가 힘들었음
  • HHTP 서비스, XML 형식 메시지 배치를 사용했으나, 형식에 일관성이 없고 파싱하는데 컴퓨팅이 많이 들었음
  • 추적되는 사용자 활동 유형을 변경하려면, 다른 애플리케이션에 많은 추가 작업이 필요
  • ActiveMQ를 사용해보았으나 규모 확장성이 떨어짐, 결함이 많았음
  • 직접 개발하자..!

1.5.2 카프카의 탄생

  • 똑똑한 사람들이 모여서 프로젝트 시작
  • 주된 목표
    • 푸시-풀 모델 (push-pull model)을 사용함으로써 프로듀서와 컨슈머를 분리 (decouple) 시킨다
    • 다수의 컨슈머가 사용할 수 있도록 메시지 교환 시스템의 데이터를 영속적으로 저장한다
    • 높은 메시지 처리량을 보일 수 있도록 최적화한다
    • 데이터 스트림의 양이 증가함에 따라 시스템을 수평 확장할 수 있도록 한다
  • 카프카는 링크드인 안에서 2020년 2월 기준 매일 7조개의 메시지를 쓰고 5PB가 넘는 데이터를 읽을 수 있는 시스템으로 성장

1.5.3 오픈소스

  • 2010년 말 github에 오픈소스로 공개
  • 2011년 7월 Apache Software Foundation의 인큐베이터 프로젝트 -> 2012년 10월 정식 프로젝트

1.5.4 상업적 제품

  • Confluent에서 제공
  • 여러 클라우드에서 상용제품 제공

1.5.5. 이름

  • 제이 크랩스가 프란트 카프카 작품 좋아해서 그렇게 지음

1.6 카프카 시작하기

  • 다음 장부터 시작해보자!

Comment  Read more