4. Encoding and Evolution

애플리케이션은 기능이 추가되거나 변경됨에 따라 시간에 따라 변하게 된다. 많은 경우 기능 변경은 저장하는 데이터의 변경도 수반한다. 데이터베이스의 유형에 따라 이런 변경에 대처하는 방법도 달라진다. 그러나 큰 애플리케이션에서는 코드 변경으로만은 충분치 않다. 서버 단에서는 한 번에 일부 노드에 대해서만 새 버전을 배포한 뒤 이 동작을 체크해 전체 노드로 업그레이드를 확장해 나가는 롤링 업그레이드를 수행할 수도 있다. 이는 서버 다운 없이 배포를 가능케 한다. 또한 업그레이드를 제 때 수행하지 않는 사용자를 고려해야 할 수도 있다. 그러므로 후방 호환성과 전방 호환성을 모두 고려해야 한다. 이를 고려한 데이터 인코딩을 알아보자.

Formats for Encoding Data

프로그램은 최소 2개 이상의 표현식을 가진 데이터를 다룬다: 메모리 내 최적화된 자료구조 형태와 이것이 인코딩된 독립적인 바이트 시퀀스. 이들간의 변환을 인코딩/디코딩이라 한다.

Language-Specific Formats

몇몇 언어들은 자체적인 인코딩/디코딩을 지원하기도 하나, 다른 언어와 호환되지 않을 수도 있고, 보안 문제가 있을 수도 있다. 또한 전방/후방 호환성이 고려되지 않았을 수도 있고, 성능 문제가 있을 수도 있다.

JSON, XML, and Binary Variants

많은 프로그래밍 언어에서 읽고 쓸 수 있는 표준화된 인코딩으로는 JSON, XML, CSV가 있다. 그러나 이들도 몇 가지 문제가 있는데, 숫자들을 인코딩할 때의 모호성, 이진 문자열에 대한 미흡한 지원, 최적의 스키마를 학습하는 장벽(XML/JSON), 포맷의 모호성(CSV) 등이 있다. 그러나 이들은 그러한 문제점에도 불구하고 범용적으로 쓰기에 좋다.

Binary Encoding

조직 내부에서만 쓰이는 데이터에 대해서는 인코딩 포맷 대신 컴팩트함과 파싱 속도가 더 고려 대상일 것이다. 그래서 JSON/XML에 대한 바이너리 인코딩이 이 경우 많이 쓰인다.

Thrift and Protocol Buffers

Thrift와 Protocol Buffer는 많이 쓰이는 범용 바이너리 인코딩 라이브러리이다.

Field tags and schema evolution

스키마는 시간에 따라 변화한다. Thrift와 Protocol Buffer는 전방/후방 호환성에 어떻게 대처할까? 기록들은 인코딩된 필드와 태그 번호로만 인코딩된다. 그래서 필드 명을 바꾸는 것은 상관이 없다. 태그 번호 충돌만 없다면 필드를 추가/삭제해도 전방/후방 호환성은 깨지지 않는다.

Datatypes and schema evolution

필드의 데이터 형태가 바뀌면 어떻게 하나? 프로토콜 버퍼는 repeated 마커로 이를 대처한다. Thrift는 단일 값에서 복수 값으로 바꾸는 것은 허용하지 않지만, 중첩 목록은 지원한다.

Avro

Apache Avro는 색다른 흥미로운 바이너리 인코딩 포맷이다. 이는 인코딩될 데이터의 구조를 특정하는 스키마를 가진다. 이 인코딩은 값만을 연결시킨 바이트 시퀀스로 이루어진다. 이 때 인코딩한 스키마와 호환 가능한 스키마로 디코딩해야만 제대로 디코딩된다.

The writer’s schema and the reader’s schema

Avro에서 쓰는 스키마와 읽는 스키마가 같을 필요는 없다. 호환 가능하기만 하면 된다.

Schema evolution rules

호환성을 유지하려면 기본값을 가진 필드만 추가/삭제할 수 있다. Avro에서는 null이 기본값으로는 충분치 않고, 공용체 타입이어야 한다. 대신 optional/required 마커는 필요없이 필드의 데이터 타입을 바꾸는 것은 가능하다.

But what is the writer’s schema?

쓰기 스키마들의 예는 어떻게 되나? 기록이 많은 큰 파일, 독립적으로 기록된 데이터베이스, 네트워크 연결을 통해 전달되는 기록들 모두 용례에 속한다.

Dynamically generated schemas

Avro는 태그 번호를 지원하지 않으므로 동적으로 생성된 스키마에 대해서 더 친화적이다. 데이터베이스 스키마가 변경되면 업데이트된 데이터베이스 스키마에 대한 Avro 스키마를 생성하고 데이터를 그 스키마로 내보내면 된다. 이는 Thrift나 Protocol Buffer에 대해 갖는 이점이다.

Code generation and dynamically typed languages

Thrift나 Protocol Buffer는 코드 생성에 의존하는데, 이는 동적 타입 언어에서는 방해물이 된다. Avro에서는 정적 타입 언어에서도 코드 생성을 지원하긴 하지만, 동적 타입 언어에서는 이것이 필요없다.

The Merits of Schemas

Protocol Buffer, Thrift, Avro 모두 바이너리 인코딩 포맷을 묘사하기 위해 스키마를 쓴다. 이들은 특별히 새로운 발상은 아니다. 많은 데이터베이스들도 저작권이 있는 자체적 바이너리 인코딩을 사용한다. 이런 스키마 기반 바이너리 인코딩은 바이너리 JSON 등보다 훨씬 컴팩트하고, 문서화로서의 기능을 제공하며, 전방/후방 호환성을 체크할 수도 있고, 정적 타입 프로그래밍 언어에서 코드 생성은 유용하다. 즉, 유연성은 비슷하면서 데이터 보장은 더 강화한다고 볼 수 있다.

Modes of Dataflow

데이터의 흐름에 대해 알아보자.

Dataflow Through Databases

데이터베이스에서는 데이터베이스에 쓰는 프로세스가 데이터를 인코딩하고 데이터베이스를 읽는 프로세스가 데이터를 디코딩한다. 즉, 데이터베이스에 저장하는 건 미래의 자신에게 메시지를 보내는 것이라 봐도 좋다. 그러므로 후방/전방 호환성은 반드시 필요하다. 동시성 문제도 중요한 고려 요소이다.

Different values written at different times

데이터베이스는 일반적으로 어떤 시점에도 아무 값이나 업데이트할 수 있다. 즉, 몇 년 전에 쓰여진 데이터도 업데이트하지 않고 들고 있을 수 있다. 데이터를 새 스키마로 이주시키는 것은 매우 비싸기 때문에 많은 데이터베이스에서는 null 기본값을 가진 새 열을 추가하는 방식을 쓴다. 스키마 변화를 통해서 전체 데이터베이스는 마치 단일 스키마로 인코딩된 것처럼 보일 수 있다.

Archival storage

백업이나 데이터 웨어하우스에서의 로드의 목적으로 데이터베이스의 스냅샷을 기록하고 싶을 수 있다. 이 경우 데이터 덤프는 변경할 수 없으므로 Avro 같은 포맷이 잘 맞는다.

Dataflow Through Services: REST and RPC

네트워크를 통해 통신해야 하는 프로세스가 있을 경우 이 통신을 이루는 데에는 몇 가지 다른 방법이 있다. 가장 흔한 방식은 서버와 클라이언트로 나누는 것이다. 서버는 네트워크에 API를 노출하고 클라이언트는 서버에 연결해 이 API에 대한 서비스를 요청한다. 웹은 이 방식으로 동작한다. 웹 브라우저만이 클라이언트는 아니며 네이티브 앱이나 데스크톱 컴퓨터, 또한 서버 그 자체도 웹의 클라이언트가 될 수 있다. 서비스는 클라이언트로 데이터를 전송하고 쿼리할 수 있게 하는 면에서 데이터베이스와 비슷하다고 볼 수 있다. 이런 서비스 지향/마이크로서비스의 핵심 설계 목표는 애플리케이션이 서비스를 독립적으로 배포 가능하고 개선 가능하게 할 수 있도록 변화와 유지를 쉽게 하는 것이다.

Web services

HTTP가 서비스의 기반 프로토콜이 될 경우 이를 웹 서비스라 할 수 있다. 이는 웹에서만 쓰이는 것은 아니고, 클라이언트 애플리케이션, 같은/다른 조직 내의 다른 서비스들도 쓸 수 있다. 웹 서비스에는 크게 두 가지 접근법인 REST, SOAP가 있다. REST은 프로토콜은 아니고 HTTP의 원리에 기반한 설계 철학이다. SOAP는 네트워크 API 요청을 만드는 XML 기반 프로토콜이다. SOAP 웹 서비스의 API는 WSDL이라는 XML 기반 언어로 표현된다. WSDL은 사람이 읽을 수는 없으므로 SOAP 사용자는 여러 도구 지원에 크게 의존한다. SOAP와 그 확장은 표면적으로는 표준화되었을지라도 다른 구현체간 상호 운영성은 종종 문제가 된다. RESTful API는 더 간단한 접근법을 사용한다.

The problems with remote procedure calls (RPCs)

웹 서비스는 네트워크에 API 요청을 만들기 위한 오랜 기간의 기술 중 가장 최근 것일 것이다. 이전의 기법들은 원격 프로시져 호출(RPC)에 의존하였는데, 이는 근본적으로 문제가 있었다. 네트워크 요청은 로컬 함수 호출과 달리 예측할 수 없고, 타임아웃이 발생할 때 원인을 알 수 없고, 호출 재요청 시 프로토콜 중복이 발생할 수 있고, 지연 시간도 문제가 되고, 매개변수가 복사로 전달되므로 큰 오브젝트가 전달될 시 문제가 있고, 서로 다른 프로그래밍 언어간 호환 문제도 있다. 그러므로 원격 프로시져 호출은 프로그래밍 언어에서의 로컬 오브젝트와 근본적으로 다르게 봐야 한다.

Current directions for RPC

이러한 문제들이 있더라도, 원격 프로시져 호출은 없어지지 않는다. 대신 원격 요청이 로컬 함수 호출과는 다르다는 것을 명시하는 방향을 변화한다. 이런 프레임워크들은 서비스 발견, 즉 클라이언트가 어떤 IP와 포트 번호에서 특정 서비스를 발견할 수 있는지에 대한 기능을 제공한다. RPC 프로토콜은 바이너리 인코딩 포맷과 함께 커스텀화되었을 때 제네릭한 것들에 대해 더 좋은 성능을 낸다. 하지만, 제네릭한 RESTful API는 실험과 디버깅에도 더 좋고, 호환성과 생태계 면에서도 더 좋다. 그러므로 REST는 공용 API에 널리 쓰인다.

Data encoding and evolution for RPC

발전 가능성에 대해, RPC 클라이언트와 서버는 독립적으로 변경되고 배포될 수 있다는 것이 중요하다. 그러므로 후방 호환은 요청에 대해서만, 전방 호환은 반응에 대해서만 지원되면 충분하다. 이런 RPC 스킴의 전방/호환성은 어떤 인코딩을 쓰냐에 따라 상속된다. RPC가 여러 다른 조직간 사용됨으로써 서비스의 제공자가 그 클라이언트의 업그레이드를 강제할 수 없기 때문에 서비스 호환성은 더 힘들어졌다. 그러므로 호환성은 오랜 기간 동안 유지보수되어야 한다. API 버전을 어떻게 유지할 것이냐에 대해서는 정답은 없다.

Message-Passing Dataflow

비동기 메시지 전달 시스템도 많이 쓰인다. 이는 클라이언트의 요청이 낮은 지연 시간으로 전송된다는 것에서 RPC와 비슷하고, 메시지가 직접적인 네트워크 연결로 전달되지 않고 메시지 브로커라는 중간자를 통해 전달된다는 점에서 데이터베이스와 비슷하다. 데이터 브로커를 쓰는 것은 직접 RPC에 비해 몇 가지 이점이 있다: 버퍼링을 통해 시스템 가용성을 높이고, 재전송을 통해 신뢰성을 높이고, 전송자를 통해 IP 주소와 포트 번호를 캡슐화하고, 한 메시지를 여러 수신자에게 전달할 수 있도록 하고, 전송자와 수신자를 논리적으로 분리한다. 이런 패턴을 비동기적이라 한다.

Message brokers

과거에는 메시지 브로커를 사용할 시 여러 구현체간 방식은 달랐지만 공통된 것은 한 프로세스가 메시지를 이름 붙은 큐에 넣고 브로커가 이 큐의 소비자들에게 메시지가 전달되었음을 보장하였다. 이는 일방통행 데이터 흐름만 제공하였다. 메시지 브로커는 특정한 데이터 모델을 강제하지는 않았다. 데이터 포맷에 대한 호환성은 전달자와 수신자의 몫이다.

Distributed actor frameworks

행위자 모델은 단일 프로세스에서의 동시성 프로그래밍 모델이다. 스레드를 직접적으로 다루는 대신 로직은 행위자로서 캡슐화된다. 이를 여러 노드에서 쓰이는 애플리케이션으로 확장한 것이 분산형 행위자 프레임워크이다. 이는 RPC에 비해 장소 투명성이 더 강화된다. 행위자 모델은 단일 프로세스에 대해서도 메시지 유실에 대해 책임지지 않기 때문이다. 분산 행위자 프레임워크는 메시지 브로커와 행위자 프로그래밍 모델을 단일 프레임워크로 결합한 것이라 볼 수 있다. 3가지 예시로 Akka, Orleans, Erlang OTP가 있다.

Summary

이 장에서는 자료구조를 네트워크나 디스크에서 바이트로 변환시키는 여러 방법을 알아보았다. 이러한 인코딩의 세부 사항이 효율성뿐만 아니라 애플리케이션의 구조나 이를 어떻게 변화시켜야 할지에 어떤 영향을 끼치는지도 알아보았다.

구체적으로, 많은 서비스는 완만한 업그레이드를 지원해야 한다. 이는 서비스의 새 버전이 한 번에 소수의 노드들에 대해서만 배포되고 모든 노드를 동시에 배포하지 않는 것을 말한다. 완만한 업그레이드는 서비스의 새 버전이 다운 시간 없이 배포될 수 있게 한다. (즉, 드문 큰 릴리즈 대신 빈번한 작은 릴리즈를 유도한다) 또한, 배포들의 리스크를 줄인다. (문제 있는 릴리즈는 많은 사용자들에게 배포되기 이전에 감지되고 롤백될 수 있다) 이런 특성들은 애플리케이션의 변경 용이성인 발전 가능성 면에서 매우 이득이 된다. 완만한 업그레이드나 다른 여러 이유들에 따라, 여러 노드들은 애플리케이션 코드의 서로 다른 버전을 다루고 있을 것이라고 가정해야 한다. 그러므로, 시스템에서 흐르는 모든 데이터는 후방 호환성 (새 코드가 오래된 데이터를 읽을 수 있어야 한다), 전방 호환성 (오래된 코드가 새 데이터를 읽을 수 있어야 한다)를 제공하도록 인코딩되어야 한다.

여러 인코딩 포맷과 그 호환성 특성도 알아보았다: 프로그래밍 언어별 인코딩은 단일 프로그래밍 언어로 제한되고 전방/후방 호환성을 제공하는 데 있어 적합하지 않기도 하다. JSON, XML, CSV 등의 텍스트 포맷은 많이 쓰이며, 이 호환성은 어떻게 쓰느냐에 따라 달려 있다. 이들은 부가적인 스키마 언어를 가지며 이는 도움이 되기도 하고 방해가 되기도 한다. 이런 포맷들은 데이터 타입에 대해서는 모호성을 가지므로 숫자나 바이너리 문자열에 대해서는 주의해야 한다. 바이너리 스키마 지향 포맷들은 전방/후방 호환성이 명료하게 정의된 컴팩트하고 효율적인 인코딩을 지원한다. 이 스키마는 정적 타입 언어들에 대한 문서화나 코드 생성에 대해 유용하다. 하지만, 이런 포맷들은 데이터가 사람이 읽기 전에 디코딩되어야 한다는 단점도 수반한다.

또한, 데이터 흐름의 여러 모드들과 그 데이터 인코딩들이 어떤 상황들에서 각각 유용한지도 알아보았다: 데이터베이스는 데이터베이스에 쓰는 프로세스가 데이터를 인코딩하고 데이터베이스를 읽는 프로세스가 데이터를 디코딩한다. RPC와 RESP API는 클라이언트가 요청을 인코딩하고 서버가 요청을 디코딩해 반응을 인코딩하고 클라이언트가 반응을 디코딩한다. 비동기 메시지 전달 (메시지 브로커나 행위자)는 노드간에 전달자에 의해 인코딩되고 수신자에 의해 디코딩되는 메시지를 교환한다. 조금 신경을 쓰면 전방/후방 호환성과 완만한 업그레이드는 어렵지 않게 얻을 수 있다. 애플리케이션이 빠르게 진화되고 배포를 빈번히 수행할 수 있도록 하라.

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google photo

Google의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중