구글 Protobuf 는 강력한 데이터구조이며 환경간 이식성이 뛰어나고 패킷으로 사용할 때, 자체의 성능(통신 속도 및 작은 패킷 크기)이 뛰어나지만, 정해진 형식이 있다보니 사용에 있어서 알아두어야할 점들이 몇가지 있다.

 

실무에서 사용하면서 알아두어야 했던 점들 및 특징적인 점들 몇가지를 정리해보았다.


1. protobuf 는 패킷간 상속과 추상화를 지원하지않는다.

프로토버프를 도입하기 가장 꺼려지는 이유인데, protobuf 의 Data Structure는 상속과 추상화와 같은 생산성을 위한 개발자들의 구조를 부정한다.

Protobuf 는 애초에 범용(?) 목적으로 설계되었다기 보다는 확실한 단일 목적에 대해 Compact 하게 설계된 Data Structure 이다.

즉, 확실히 의미를 갖는 필요한 데이터만 저장할 수 있게 되어있으며, 그렇기 때문에 Stream 으로 사용할 시 필요한 정보만 주고받는 효율성을 이점으로 취할 수 있는 반면, 데이터를 담는 데 있어서 유연하지 못하다.

 

그렇기 때문에 프로토콜 버퍼를 패킷으로 통신을 해야 한다면 모든 패킷에 필요한 정보들을 분류해서 정의해주는 것이 필요하다.

 


2. protobuf 의 패킷 넘버링은 생각보다 엄격하지않다. 하지만 패킷의 순서는 매우 중요하다.

다음과 같은 에러 처리를 위한 프로토콜 버퍼 Data Structure가 있다고 가정해보자.

위의 데이터 구조에서 time_stamp 패킷의 넘버링을 4로 바꿔도 무방하다. 프로토콜 버퍼에서 구조 내에서 중요한 것은 패킷들의 순서를 지키는 일이다.

다만, Protocol Buffer 를 enum 형태로 정의해서 쓰는 경우라면 조금 얘기가 다른데,

위와 같은 Enum 패킷에서는 numbering 이 의미를 갖는다. enum 의 경우 넘버링이 Enum 클래스의 ordinal 과 동치되기 때문에, Compile 해서 사용할 경우 패킷이 다르다면 예기치 못한 에러가 발생할 수 있다.

 

 

3. Protobuf 구조가 프로토콜로 정해졌다면 데이터 구조는 변경하지 않는 것이 좋다.

통신에 있어서 Protobuf 를 사용하는데, 기존 데이터 구조가 삭제 또는 변경된다면 그 구조를 삭제 & 변경하는 것이 아니라 새로 패킷을 추가하는 편이 좋다.

그 이유는 하위호환성 때문으로, 통신하는 양쪽의 Protobuf 빌드가 항상 동기화가 완벽하다면 문제가 없지만, 개발을 하다보면 버전이 달라질 수밖에 없다.

이 때 문제를 방지하기 위해 데이터 구조의 크기를 늘리는 방법을 선택해야 한다.

하지만 패킷의 크기가 커질지는 염려하지 않아도 된다. Protobuf 의 특징 상 입력되지않는 패킷은 보내지않도록 퍼포먼스 측면에서 최적화가 지원된다.

 


4. 구글이 지원해주는 protobuf 라이브러리가 있으며, 이를 사용하면 프로토버프의 사용이 매우 편리해진다. 

다음 링크를 참조하자.

https://github.com/protocolbuffers/protobuf

 

protocolbuffers/protobuf

Protocol Buffers - Google's data interchange format - protocolbuffers/protobuf

github.com

protobuf 라이브러리는 언어별로 필요한 기능들을 유틸리티 형태로 지원한다.

아주 방대하니 전체를 쓸 생각보다는 환경에 맞게 필요한 만큼만 모듈을 가져다 쓰는 것이 좋다.

또한 protobuf 라이브러리에는 proto 파일 내에서 사용할 수 있는 공통 protobuf 형식들도 정의되어 있으므로 참고하는 것이 좋다. 
가령 proto 파일 내에서 Collection 이나 timestamp 등의 기능을 사용할 수 있도록 정리되어있다. 

 

 

5. 당연한 얘기지만 proto 파일의 주석조차 compile 결과로 빌드된 소스에 포함 된다. 

만약 소스 자체로 어떤 스크립트가 실행되어야 한다면 환경에 주의하자.

주석에 한글 특수문자가 포함된 proto 파일을 자동화 스크립트로 돌리다가 문제가 생기는 일이 더러 있었다.

 

 

프로토콜 버퍼는 구글이 지원하고 있는 직렬화방식이고 강력하며 쓰임새도 다양하다.

하지만 현재로써는 아는만큼 사용할 수 있는 도구임이 분명하다. 사용에 있어 유의하고 항상 공식 지원을 참고하는 것이 좋겠다.

 

면접에서 단골처럼 등장하는 질문이자, 컴퓨터 공학과 시험에서 한번쯤은 보았을 법한 CS 기본 지식을 정리하고자 한다.

 

컴퓨터는 데이터를 저장할 수 있는 몇가지 종류의 공간들을 갖고 있고, 해당 공간들은 쓰임새가 다르고 만들어진 이유가 다르기 때문에 각각 I/O 작업에 있어서 다른 퍼포먼스를 낸다.

 

그 중에서도 Access 에 대한 다음 Computing Operation 의 속도 비교는 알아두어야 한다.

 

 - CPU Register

 - Context Switch

 - Memory Access (RAM)

 - Disk Seek (HDD)

 

위의 Operation 들에 대한 속도 비교 결과는 빠른 순서대로 다음과 같다.

 

1. CPU Register Access

2. Memory Access

3. Context Switching

4. Disk Seek

 

(1) CPU 레지스터에 대한 접근은 단 한번의 CPU 사이클만으로 이루어지기 때문에 즉각적으로 이루어진다.

한 사이클이라는 것은 말그대로 번개와 같은 속도로 이루어진다는 뜻이다.

 

(2) Memory Access 는 일반적으로 RAM 에서 데이터를 읽어내는 것을 말하며, 당연히 RAM 의 목적에 맞게 HDD 로부터 읽어오는 것보다 빠르다.

일반적인 상태에서의 작업은 레지스트리에 접근하는 것에 비견될만큼 빠를 수 있지만 논리 구조 위에서 동작하기 때문에 Virtual Memory Swapping 과 같은 작업에서 자유로울 수 없으며 이런 경우에는 Disk Access 만큼 느려질 수도 있다. 

 

(3) Context Switching 는 대체적으로 빠른 접근이 보장이 된다. 하지만 여러개의 프로세스가 동시에 실행되며 스위칭이 빈번하게 이뤄질 경우 굉장히 느려질 수도 있다.

 

(4) Disk Seek. HDD 에 대한 Disk Seek 은 위에 언급한 Operation 들에 비해 빠를 수가 없는 작업이지만 캐싱을 통해 비약적인 성능 향상이 가능하다.

BUS 에서의 병목을 피할 수 있으며 캐싱을 통해 Main Memory 에 Access 하는 것 만큼의 퍼포먼스를 기대할 수도 있다.

 

 

면접에서 갑작스레 질문받은 내용이라 당황했던 적이 있었다.

알고있었던 내용이라 답변은 잘 했으나... 끝나고나서 다시 점검해볼만큼 기본기가 아직 충분치 못한 것 같아 정리해둔다.

 

 


메서드와 함수의 차이점은 간단하면서도 기초적인 내용이지만 자주 되새겨지는 개념이 아닌데다가, 비슷하게 혼용되어 사용되다보니 많이 잊게 되는 내용이다.


차이점 먼저 서술을 하자면, Method 는 "객체" 에 대한 코드를 말하고 동작(Operation)의 결과로 동작을 수행한 객체가 영향을 주거나 받는다.


즉, Operation 은 해당 메서드를 소유한 객체 중심으로 발생한다.


반면, Function 은 특정 형태의 Data 를 받아서 내부 동작(Operation)을 수행한 후 특정 형태의 Output Data 를 반환한다.


즉, Operation 은 객체와 무관한 독립적인 그 자체의 코드 조각으로의 의미를 지닌다.



- 정리


: Method - Member. 객체에 대한 Operation 을 수행하는 코드 조각.


: Function - Free. 객체와 독립적인 Operation 을 수행하는 코드 조각.



C 에서 사용되는 Logic 들은 모두 함수(Function) 이며 Java 에서 사용되는 Logic 들은 모두 메서드(Method) 이다.







캐시란 데이터를 임시로 저장해두는 장소를 말한다. 임시로 저장하여 사용하는 데이터의 종류는 제한이 없기 때문에 작게는 메모리에서 크게는 Logic 혹은 그 이상을 저장할 수 있다.


Cache 의 목적은 로직을 처리하는 데 있어서 빠른 접근성을 제공하는 것이며, 단순히 수동적으로 보관하는 것에 그치지 않고 이를 응용해서 작업의 결과를 저장함으로써 해당 로직의 불필요한 수행을 줄여주는 능동적인 역할까지 수행한다.


캐시가 될 수 있는 것은 일반적으로 로컬 메모리부터 별도의 디스크 볼륨까지 다양하지만 Cache 로 사용하기 위한 가장 중요한 요건은 데이터로의 접근성이다.


Cache 에 대한 접근성은 어떤 경우에도 로직 상에서 원하는 데이터에 직접 접근하거나 만들어내는 비용보다 저렴해야 한다. 그래야만 캐시로서의 의의가 있는 것이다.


그렇기 때문에 Cache 는 접근성이 빠른 공간(Space)에 빠른 자료구조를 사용한다.


캐시서버로 이용되는 서버들은 I/O 에 최적화된 공간이 사용되며 당연히 이에 대한 접근성에 있어서 효율적인 REST 등의 방법을 사용한다. 


컴퓨터 내에서 사용되는 캐시는 RAM보다 빠른 L1,L2 레지스터를 캐시로 사용하고, 프로그램 내에서 구현된 Software Cache 라면 접근에 용이한 Map 과 같은 자료구조에 인메모리(Inmemory)로 저장한다.


다음은 캐시를 이해하는 데 중요한 용어들이다.


 - origin : origin 혹은 origin server 는 캐시에 저장할 실 데이터가 존재하는 공간이다. 웹 캐시라면 DB 서버일 수도 있고, SW 내에서라면 파일 혹은 실행 함수 그 자체일 수도 있다.


 - cache expire : 프로세스 내에서 사용하는 인메모리 캐시나 영구히 상주해야하는 정보를 가진 캐시가 아니라면 Cache 는 Expire Date 를 갖고 있으며 해당 시간이 지나면 상한(Stale) 상태가 된다.


 - cache freshness : 캐시가 만료되지 않은 경우를 fresh 한 캐시라 하고 만료된 경우 stale cache 라 한다.


 - cache hit : 참조하려는 데이터가 캐시에 존재할 때 해당 캐시를 조회하는 걸 Cache hit 이라 한다.


 - cache miss : 참조하려는 데이터가 캐시에 존재 하지 않는 경우


 - cache hit ratio : 적중률로 전체 참조 횟수 대비 Cache hit 된 비율을 의미한다. 실질적으로 캐시의 설계는 Cache hit Ratio 를 높이는 데 초점을 둔다.



다음은 캐시의 동작에 대한 정책들이다.


 Cache Read : 


 - Cache-aside 방식 : 데이터를 참조 하기 전에 참조하고자 하는 값이 캐시에 존재하는지 확인한다. 여기서 값을 직접 비교하기 보다는 키를 이용해서 캐시에 접근한다. 

 Cache에 존재한다면 Cache에서 데이터를 가져온다. 만약 Cache에 존재하지 않는다면 origin에서 데이터를 가져오고 이를 캐시에 저장한다.


 - RT/WT/Write back 방식 : 캐시를 Main Data Source 로 사용하기 때문에 캐시에서만 데이터를 조회한다.

 RT/WT 방식(Read Through / Write Through) 은 Read Scalability 가 가장 뛰어나다.



 Cache Write :


 - Cache-aside 방식 : 캐시를 Application Level 에서 직접 갱신시켜준다. 개발자가 Flow 를 이해하고 Update / Evict 시켜줘야 하며, 그렇지 않으면 Cache 데이터와 DB 데이터가 불일치하는 Stale 현상이 발생한다.


 - Read Through / Write Through 방식 : 데이터의 쓰기시 캐시와 실제 저장공간의 데이터 둘다 최신화 시키는 작업이다. 

 캐시를 메인 Database 로 사용하는게 특징적이며, 캐시에 데이터를 먼저 업데이트하고 캐시에서 Main Database 를 즉시 갱신시킨다.

 양쪽의 데이터를 동일하게 유지할 수 있지만, 쓰기 시에 추가 부하가 생긴다는 단점이 있다.


 - Write Back 방식 : 데이터의 쓰기시 캐시의 데이터만 최신화 하고 해당 Cache 를 Evict 시켜놓는다. 이 후 RT/WT 방식처럼 캐시 값을 마킹된 기준으로 origin 으로 직접 반영(Write) 하는데, 캐시가 별도의 큐를 이용해서 Database Source를 비동기로 Update 시켜준다.

 Write Performance 와 DB Scalability 에 있어서 가장 뛰어나다.

 쓰기 작업이 Cache 에서만 발생하지만, Cache 가 만료되는 시점까지 Origin 에 Write Failure 가 발생한다면 데이터를 영구 손실할 위험이 존재한다. 



 Cache Replacement


  웹 캐시의 경우 자동 expire 하거나 명시적으로 cache 를 지워주는 동작을 해주지만, 캐시를 Scheduling 에 사용하는 컴퓨터나 알고리즘의 경우 Replicement Policy 를 갖는다.



 다음은 몇가지 대표적인 알고리즘들이다.


  - FIFO(First In First Out) : 오래된 캐시를 먼저 비우고 새로운 캐시를 추가하는 방식이다.


  - LIFO(Last In First Out) : 가장 최근에 반영된 캐시가 먼저 지워진다.


  - LRU(Least Recently Used) : 가장 최근에 사용되지 않는 순서대로 캐시를 교체한다. 가장 오랫동안 사용되지 않은 캐시가 삭제되며 일반적으로 사용되는 방식이다.


  - MRU(Most Recently Used) : 가장 최근에 많이 사용되는 순서대로 캐시를 교체한다. 휘발성 메모리를 이용해야 하는 특수한 상황에 사용된다.


  - Random : 말그대로 랜덤으로 캐시를 교체한다.


 운영체제를 배웠다면 페이지 교체 알고리즘이 Cache Replacement 정책을 사용한다는 것을 알 수 있을 것이다.



본 포스팅에서는 넓은 범위의 Cache 의 정의와 목적, 정책들에 대해 정리해보았다.


이론적인 부분이고 웹 캐시와는 조금 다르기도 하지만 중요한 기본 개념은 잘 숙지해두자.


참조 : 

https://en.wikipedia.org/wiki/Cache_replacement_policies

https://codeahoy.com/2017/08/11/caching-strategies-and-how-to-choose-the-right-one/

https://gomguard.tistory.com/115

https://onecellboy.tistory.com/260

https://dzone.com/articles/using-read-through-amp-write-through-in-distribute



컴퓨터는 CPU에서 tick 을 발생시키며 1970년 1월 1일부터 발생된 tick 의 수를 계산해서 시간을 측정한다.


이 시작점을 UNIX 계열에서는 POSIX time 혹은 Epoch time 이라 하며, 1970년 1월 1일 00:00:00 UTC 이고, 


이때부터 경과 시간을 초로 환산해서 사용한다.


이런 방식에는 몇가지 문제가 있는데, 먼저 윤초(예를 들어 1998년 12월 31일 23:59:60)는 표현할 수 없이 무시된다.


현재 32비트 운영체제에서 초 시간을 지정하는 time_t 자료형은 32비트 Integer 이기 때문에, 


2038년이 되면 overflow 문제가 발생한다.


이를 2038 Years Problem 이라 하며 2038년 1월 19일 03:14:07 UTC 가 되면 오버플로우가 발생해서 


32비트 유닉스 시스템의 시간은 음수가 되어버린다.


그렇기 때문에 현재 int32 를 int64 로 바꾸는 노력을 계속하고 있고, 지속적으로 수정이 진행되고 있다.





Unix 기반의 운영체제는 File System 에 Access Permission 을 관리할 수 있는 장치를 갖고 있다.


이는 특정 사용자 또는 그룹에게 어떤 종류의 권한을 허용하는 가에 대한 정보를 저장하게 된다. 


파일 접근 권한에 대한 분류는 다음과 같이 나눌 수 있다.


 (1) 접근 대상에 대한 분류

 - User

 - Group

 - Others(public)


 (2) 접근 권한에 대한 분류

 - Read

 - Write

 - Execute


Unix 기반의 운영체제에서 File System 내의 모든 요소는 File 로 관리되기 때문에 각 파일이 위의 대상들 각각에 대한 권한을 모두 갖고 있다.

즉, 하나의 파일은 User / Group / Others 에 대한 Read / Write / Execute 권한을 전부 관리하게 된다.


먼저 각 접근 권한에 대해 알아보자.


Unix 시스템은 접근 권한을 bit 단위로 관리하며, 각 접근 권한은 다음 비트들에 매핑된다.


Read(4), Write(2), Execute(1)


이게 무슨 의미냐면, Unix 시스템은 위와 같은 이진 주소에 Access / Deny 정보를 관리한다는 뜻이다. 

즉, Read 는 4의 위치(2^2) 에서 1/0 으로 구분이 되며, Write 는 2의 위치(2^1), Execute 는 1의 위치(2^0) 에서 1/0 으로 권한 비트를 구별한다.


정리하자면 권한에 대한 비트는 다음과 같이 각각에 대한 Flag 값의 조합으로 이루어진다.


101 => Read(4 * 1) + Write(2 * 0) + Execute(1 * 1) => 5

111 => Read(4 * 1) + Write(2 * 1) + Execute(1 * 1) => 7


위와 같은 플래그 값으로 File System 의 권한을 나타낼 수 있으며, Unix File System 은 각 접근 대상 들에 대해 이를 똑같이 적용한다.


소유자(User)    /    그룹(Group)    /    공개(Public)

    rwx(7)                  rwx(7)                 rwx(7)


가령 위와 같이 정리되어 있을 경우 이 파일은 Permission Code 777 을 가졌다고 하며, 모든 소유자, 그룹, 공개된 사용자들에 대해 읽기(Read), 쓰기(Write), 실행(Execute) 권한을 모두 부여함을 뜻한다.



Permission 에 대한 권한은 chmod 명령어를 이용해서 변경할 수 있고, 다음과 같이 사용 가능하다.


chmod 755 /sample : /sample 폴더에 사용자(R, W, X), 그룹(R, X), 공개된 사용자(R, X) 권한을 부여한다.


chmod 사용에는 다음과 같은 옵션을 부여해서 같이 사용할 수 있다.


-c : 실제로 파일의 권한이 바뀐 파일만 자세히 기술한다. 

-f : 파일의 권한이 바뀔 수 없어도 에러 메시지를 출력하지 않는다. 

-v : 변경된 권한에 대해서 자세히 기술한다. 

-R : 디렉토리와 파일들의 권한을 재귀적으로 모두 바꾼다.


chmod 외에도 chown(파일의 소유자 및 소유 그룹변경) 명령어나 chgrp(파일의 사용자 그룹 변경) 명령어도 잘 사용되므로 알아두도록 하자.





CQRS(Command and Query Responsibility Segregation) 란 .Net 기반으로 발전되고 있는 설계 방법론으로 명령과 쿼리의 역할을 구분하는 것이다. 


이는 데이터에 대한 조작 Create, Insert, Delete 와 데이터에 대한 조회 Select 를 구분하는 것에서 출발한다.


어플리케이션을 개발할 때, 컨텐츠를 위한 데이터 모델은 계속해서 복잡도가 올라가게 된다.




특히 주로 사용되는 위의 모델처럼 데이터 변경과 조회는 보통 하나의 데이터모델을 사용하게 되는데, 어플리케이션의 복잡도가 증가할 수록 각 API 기능의 책임이 어떤 데이터 모델에 있는지는 불분명해진다.


이는 설계에 있어서 초기 의도를 지워버리는 역할을 하며 많은 경우의 레거시 코드들이 이런 기반으로 생겨나게 된다.


CQRS 는 이러한 고민에서 출발하며, 데이터에 대한 조회(Query) 와 데이터에 대한 조작(Command) 을 분리함으로써 이 문제를 해결하고자 한다.


기본적으로 CQRS 를 적용하기 위해서는 Command 을 위한 도메인 모델과 Query 를 위한 도메인 모델을 분리한다.



분리된 각각의 도메인 모델을 DB에 적용하는 방안으로는 몇가지가 있다.


(1) Simple


 : 같은 Scheme 을 가진 DB를 사용하며, Command / Query 시에 데이터에 대한 Converting 을 거친 후 DB에 CRUD 에 대한 작업을 수행한다.

 이 경우에는 일반 어플리케이션과 같으며 도메인 모델만 분리한 상태로 개발이 쉽고 적용이 간단하다.



(2) Premium


 : Command 를 위한 DB와 Query 를 위한 DB를 분리하는 형식으로, 데이터의 정합성을 위한 RDB를 Command 용 DB로 분리하고 Query 가 간편한 NoSQL 을 Query 용 DB로 주로 사용한다.

 이렇게 동일한 데이터에 대해 다수의 저장소를 운용하는 방식을 Polyglot Storage 라 하며 이 경우 용도에 맞는 저장소를 골라서 좀 더 알맞게 사용이 가능하다.

 하지만 분리된 저장소 각각에 대한 데이터 동기화 이슈를 Broker 등을 이용해 처리해주어야 하는 점은 이슈가 되며 책임의 소재나 로깅 등에 있어 신뢰도 확보를 위한 작업이 필요하다.



(3) Event Sourcing


 : Event Sourcing 이란 Application 내의 설계를 컨텐츠 기반이 아닌 기능 기반으로 하면서 이러한 "이벤트(Event)" 자체를 DB에 저장하는 방식을 말한다.

 이렇게 함으로써 이벤트에서 사용하는 도메인 모델은 컨텐츠를 위한 DB에 Write 되고 Query 시에는 이벤트를 저장한 DB로부터 해당 컨텐츠를 바탕으로 데이터를 만들어서 가져온다.

 도메인 모델에 대한 Command 가 따로 저장되고, Query 를 위한 도메인 모델은 Event DB로부터 불러오는 방식 때문에

 Event Sourcing 의 Architecture를 적용함에 있어서 CQRS 는 필수적인 설계 방식이 된다.

 CQRS 를 적용하는 데 있어서도 가장 큰 시너지를 낼 수 있는 Architecture 의 하나이다.


<향후 Event Sourcing Architecture에 대해서는 추가로 정리한다.>


CQRS 가 실무에 적용되는 데 있어 아직은 국내외적으로 불확실성이 있는 듯 하지만, 주목해볼만한 패턴인 것은 틀림없다.


(참고자료 : https://docs.microsoft.com/en-us/previous-versions/msp-n-p/dn568103(v=pandp.10))



Protocol Buffer 는 구글이 만든 언어 및 플랫폼 중립적이고 확장성을 갖춘 새로운 형태의 직렬화 매커니즘이자 데이터 포맷이다. 

 

XML과 유사하지만 더 작고 빠르며, 포맷을 정의하기만 하면 컴파일을 통해서 C++ / C# / Java / Python / Go / Ruby 등의 언어에 대해서 바로 코드형태로 바꿀 수 있다. 


프로토버프의 파일 포맷은 .proto 이며 이 안에 자료 구조를 정의하면 프로토버프 모듈을 통해 각 언어에 맞게 컴파일을 한 뒤 사용하는 방식이다.

언어에 무관하게 포맷만 서로 공유가 되면 각 언어에 맞는 모듈들이 알아서 Serialize / Deserialize 를 해주기 때문에 쉽게 사용할 수 있다.


.proto 의 샘플 데이터 포맷은 다음과 같다.


syntax = "proto2";
option java_package = "com.model.protobuf";

message SimpleProtoBufMessage {
required int32 id = 1;
required string message = 2;
enum MessageType {
PING = 0;
REQUEST = 1;
RESPONSE = 2;
}
optional MessageType messageType = 3 [default = PING];
}


 syntax 는 컴파일할 대상 protobuf 버전을 지칭하며 java_package 와 같은 것들은 option 으로 지정되어 자바로 컴파일 시에만 적용이 된다.

위의 message 는 클래스로 매핑이 되며 그 안의 멤버변수들은 1, 2, 3 과 같은 연속된 숫자가 값으로 할당되는데 이는 직렬화 순서를 의미한다.


protobuf 2 버전과 3 버전에는 몇가지 문법에 있어서 차이가 있다.

예를 들어 위의 예제 양식에서 required 는 proto3에서 없어졌고, optional 은 proto3에서 기본형이라 역시 사라졌다. 

Default 값 지정도 proto3 에서 사라진 내용으로, protobuf 의 현 방향은 기본값을 배제함으로써 null 체크에 대해 좀 더 엄격해진 모습이라고 이해하면 된다.


위의 파일을 proto3 버전으로 바꾼다면 다음과 같이 된다.


syntax = "proto3";
package PB.Simple;
option java_package = "com.model.protobuf";

message SimpleProtoBufMessage {
int32 id = 1;
string message = 2;
enum MessageType {
PING = 0;
REQUEST = 1;
RESPONSE = 2;
}
MessageType messageType = 3;
}



.proto 를 컴파일 하기 위해서는 protobuf 모듈을 OS에 맞게 다운받아서 protoc 모듈을 이용하면 된다.


위와 같이 생성된 .proto 파일을 protoc 모듈을 이용해서 각 언어별로 컴파일을 수행하면, 다소 복잡한 형태의 코드를 얻을 수 있으며 구글에서는 최대한 이 파일을 변형하지 않고 사용하길 권장한다.


<proto 컴파일된 Class 파일 예시>



위처럼 생성된 파일은 해당 언어에서 바로 모듈 혹은 클래스의 형태로 이용이 가능하다. 


대게 프로젝트에서는 직접 이용하기 보다는 별도의 Converter 를 만들어두고, 통신시 혹은 직렬화시에만 Convert 해서 사용한다.


프로젝트 Protobuf 로 통신은 application/x-google-protobuf 와 같은 새로운 형식이며 서버 클라이언트간 통신 시 이 형태로 consume 및 produce 가 일어나야 한다.

이때, 서버와 클라이언트는 동일한 .proto 파일을 공유하기만 하면 각기 환경에 알맞게 컴파일해서 내부 클래스로 사용하면 된다. 

주로 REST 서버로 POST 에서 RequestBody 에 protobuf 모델이 담겨서 통신하게 된다.


통신에 있어서 특징적인 점은, Protobuf 통신시에 값이 default 값과 같다면 통신 시 전송하지 않는다는 점이다. 

이는 프로토버프가 프로토콜 설계시 성능을 크게 고려했다는 점을 알 수 있는 부분이다.


실무에서 사용 결과 JSON 보다 확실히 우월한 퍼포먼스를 내는 것을 실감하고 있다. Json 에 비하자면 아직은 쓰기 불편하지만, 익혀둘만한 기술임은 틀림없다.




YAML 은 JSON이나 XML과 마찬가지로 설정 파일 등의 목적을 가진 데이터를 기술하기 위해 만든 포맷으로 JSON과 비교하자면 좀 더 구조가 복잡하지만 사람이 보기에 가독성 측면에서 좀 더 자연스러운 포맷이다. 


YAML 은 E-mail 양식에서 아이디어를 얻어 읽기 쉬운 데이터 포맷으로 고안되었으며 문서 Markup Language 가 아닌 데이터 표현을 위한 가벼운 형태의 언어이다.


JSON과는 다르게 자료형을 정의하게 되어있으며 기본은 primitive 형이다. 

Scalar 라고 하는 숫자 / String 형태, Sequence 라 하는 배열/리스트 형태, Mapping 이라 하는 key-value pair 형태 등이 존재하며 #은 주석이다. 

Space를 이용한 들여쓰기로 구조체를 구분한다. (Tab이 아니다.)


하지만 프로그래밍 언어처럼 반드시 문서에 형을 명시해야하는 것은 아니고, 다음과 같이 분류만 해주면 된다.


integer : 100

string : "100"

float : 100.0

boolean : Yes


다음은 간단히 만들어본 YAML 파일 예시이다. (에디터의 사소한 플러그인 에러로 인해 보이는게 이상하지만 신경쓰지말자 ;;)


---
# List of pets
pets:
- dog
- cat
- bird
- fish

# pets in shop (list inline)
shops: ["dog", "cat"]

# Dictionary of owner's pet (hashtable inline)
map: { james: dog, jenny: cat }

# Dictionary of pet's owner
dog:
- james,
- tom,
- kate
cat:
- jenny

pageNo : 10
valid : Yes



YAML 은 몇가지 지시자를 사용하여 특정 작업들을 수행할 수 있다.

가령 %YAML 문자는 문서의 YAML 버전을, %TAG 지시자는 URI 주소를 나타낸다.


근래에 많은 오픈소스들의 YAML 을 활용하고 있고, 커뮤니케이션 포맷으로도 사용되는 것 같다. 알아두는 것이 좋을 것 같다.




예전에 네트워크 수업 시간에 배운 내용 정리 겸, 가끔 인터넷에 잘못 알려진 내용들이 있어서 다시 복습정리해보았다.


TCP 의 통신 방식은 흔히 알고 있는 3-Way Handshake 방식이다.

즉, 요청을 위한 ACK, 응답을 위한 SYN+ACK, 응답에 대한 응답을 위한 ACK 패킷이 그것이다.


원칙적으로는 전송되는 모든 패킷에 대해 TCP는 3-Way Handshake 를 고안하게 되어있다.


여기서 TCP의 특징은 TCP는 신뢰성을 위해 어느정도의 비효율을 감소한다는 점이다.

가령, "Hello World" 라는 메시지를 전송해야한다고 가정하자.


이 때, 만약 어플리케이션이 몇가지 이슈로 인해 H, e, l, l, o, , W, o, r, l, d 와 같이 메시지를 끊어서 보내야한다고 할 때, 이는 TCP 통신에 있어서 커다란 비효율이 된다.

TCP 스러운 통신을 위해 모든 통신은 3-Way handshake 를 거쳐야 하며 그를 통해 신뢰성을 검증하게 된다.


이 상황은 상당히 빈번하게 발생할 수 있는데, 가령 통신 이슈가 생기거나 Host 간의 Window 크기에 있어서도 영향을 받는다.

(자세한 건 다음 내용을 참고하자.)

(http://jins-dev.tistory.com/entry/%ED%95%9C-%EB%88%88%EC%97%90-%EC%A0%95%EB%A6%AC%ED%95%98%EB%8A%94-TCP%EC%99%80-UDP-%EC%A0%95%EB%A6%AC-%EB%82%B4%EC%9A%A9?category=760148)


이 때 발생하는 주요 문제점은 단순한 메시지의 전달에 대해서도 네트워크 비용이 막대하게 발생한다는 점이다.


메시지 하나하나의 처리량과 반응속도는 높아질 지언정 전체 "Hello World" 메시지를 통신하기 위해서는 글자수만큼의 네트워크 비용을 소모한다.


더군다나 매 통신 시, 컴퓨터는 메시지에 별도의 헤더 등과 같은 추가 정보들을 추가하며 이는 동일한 정보더라도 한 패킷의 사이즈가 커지는 결과를 만들어낸다.

즉, 하나의 메시지 H 를 보내는데 메시지보다 헤더의 데이터가 더 큰 비효율이 발생할 수 있는 것이다.


Nagle's Document 에 따라 이러한 문제는 Small Packet Problem 으로 정의되며 Nagle 알고리즘은 이 문제를 해결하는 방법을 제시한다.


Nagle Algorithm 은 송신에 있어서 버퍼를 둔 뒤 상대방 Host 의 Window 사이즈를 고려한 후, 어느정도 길이만큼의 패킷을 한번에 전송하는 기술이다.

다음은 위키에서 발췌한 Nagle Algorithm 의 수도코드이다.


<출처 : https://en.wikipedia.org/wiki/Nagle%27s_algorithm>



당연히 이렇게 하면 통신 횟수가 적어져서 네트워크 비용은 절감되게 된다. 대신 패킷 하나당 처리량이 늘어나므로 반응속도는 조금 느려질 수 있다.

가령 Health Check ping 을 날리는 경우나, signal 을 전송하는 경우에는 오히려 Nagle Algorithm 을 적용하지 않음으로써 반응속도의 효율을 좋게 만들 수 있다.


+ Recent posts