Cache 는 서버의 동작을 이해하는 데 있어서 빼놓을 수 없는 부분이며, 서버의 부하를 줄여주고 서버가 가진 능력을 최대한으로 활용할 수 있게 해줄 뿐만 아니라, 클라이언트에도 중요한 역할을 한다.


흔히 말하는 Cache 의 종류로는 Redis 나 Memcached 를 사용하며 대부분 인메모리 형태로 서버의 값을 저장하고, 필요할 때에 해당 값을 반환함으로써 서버의 작업 공수를 줄여준다.


Spring Framework 는 프레임워크 레벨에서 캐시의 추상화를 지원해준다.


Cache의 추상화란, 흔히 캐시를 사용할 때 작업이 필요한 부분에 대한 인터페이스를 제공해준다는 뜻이다.


가령, 웹서버에 캐싱 기능을 적용하기 위해서는 다음과 같은 캐싱의 기본 로직이 탑제되어야 한다.


(1) Memory 혹은 원격 캐시에 연결된 객체를 생성한다. (이를 Cache Manager라 한다.)


(2) 캐시의 값을 불러온다.


(3) 캐시에 값이 존재하지 않는다면 캐싱할 값을 일정한 기준을 갖고 등록한다.


(4) 등록된 캐시값에 대해 조회가 가능하다.


(5) 필요할 때에 캐시의 값을 불러오고 적당한 때에 캐시를 업데이트 한다.


위의 단계들은 기본적으로 캐시가 가져야할 역할이며, 위의 역할 정도는 수행할 수 있어야 서버측에서 "캐시" 로써 동작한다고 할 수 있다.


Spring 에서 위와 같은 단계는 다음 Annotation 들로 대체될 수 있다.


Cache 의 생성 : Spring 의 Cache Configuration 참조


Cache Key Value 의 등록 : @Cacheable



@Cacheable(value="user")
public List<User> getUserListFromDB() {
return selectUserListFromDB();
}

@Cacheable(value="user", key="#uid", condition="#result!=null")
public User getUserFromDB(String uid) {
return selectUserFromDB(uid);
}


@Cacheable 어노테이션과 함께 저장할 캐시의 이름을 value에 명시하고, key 값을 지정하면 해당 결과값을 설정된 Cache에 캐싱할 수 있다.


값은 캐싱될 뿐 아니라, 다시 해당 함수로 접근 시 캐싱된 값이 있다면 내부 함수를 수행하지 않는다.


condition 은 해당 캐시에 적용 시 어떤 항목들에 대해 캐싱하거나 캐싱하지 않을 지를 결정할 수 있다.



Cache Key Value 의 삭제 : @CacheEvict



@CacheEvict(value="user", key="#user.uid", beforeInvocation=false)
public void putUserToDB(User user) {
insertUserToDB(user);
}


@CacheEvict 어노테이션을 이용하면 해당 캐시 이름과 Key 에 저장되어 있는 Cache Value 를 제거할 수 있다. 

이 때 종종 사용되는 옵션으로 beforeInvocation 옵션이 있는데, 이 옵션을 true 로 지정하면 함수가 시작되기 전에 캐시를 비우는 작업을 수행한다.


Cache 의 갱신 : @CachePut


@CachePut 어노테이션은 값이 변경되었을 경우에만 해당 캐시를 비운다.


여러 개의 Caching 동작에 대해 : @Caching



@Caching(evict = {
@CacheEvict(value="user", key="#user.uid")),
@CacheEvict(value="userGroup", key="#user.groupNo")
})
public void addNewUserToDB(User user) {
insertUserToDB(user);
insertUserGroupToDB(user.getUserGroup());
}


Cache 에 대한 여러 동작을 수행하고자 할 때에는 @Caching 어노테이션을 사용한다.



Spring 의 Cache 어노테이션은 내부적으로 SPEL(Spring Expression Language) 라는 문법을 사용한다.

위의 간단한 예시만으로도 사용하는 데 큰 지장은 없을 것이다.




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(파일의 사용자 그룹 변경) 명령어도 잘 사용되므로 알아두도록 하자.





Java 에서 사용하는 Thread Local 이란 간단히 설명해서 쓰레드 지역 변수이다.


즉, Thread Local 로 정의된 객체는 같은 Thread 라는 Scope 내에서 공유되어 사용될 수 있는 값으로 다른 쓰레드에서 공유변수를 접근할 시 발생할 수 있는 동시성 문제의 예방을 위해 만들어졌다.


한 쓰레드 내에서만 사용되는 변수라더라도 전역변수처럼 State 값을 부여해서 사용하게 되므로 가능한 가공이 없는 참조용 객체의 경우가 사용되며, 지나친 사용은 재사용성을 떨어트리고 부작용을 발생시킬 수 있다.


아래의 그림을 보자.




위의 그림에서 Thread A 와 Thread B는 각 쓰레드 내부에서 사용되는 ThreadLocal 을 갖고 있으며 이 객체는 해당 Thread 내에서 일정하며 다른 Thread 로부터 격리되어 있다.


Java 에서는 이 값을 Set 하고 Get 함으로써 전역변수처럼 사용이 가능하다.


(1) Thread Local 객체 생성


(2) Set 을 통해 Thread Local 에 값 세팅


(3) Get 을 통해 Thread Local 로부터 값 조회


(4) Remove 를 통해 Thread Local 의 값 해제



주로 쓰레드 로컬은 서버에서 클라이언트 요청들에 대해 각 쓰레드에서 처리하게 될 경우, 해당 유저의 인증 및 세션정보나 참조 데이터를 저장하는 데 사용된다.


이런 경우 Spring 등에서는 Interceptor 를 이용해서 쓰레드 로컬의 작업을 제어하는 경우가 많고, 그에 따라 처리할 수 있는 정보와 처리할 수 없는 정보로 디자인을 나누는 것이 중요하다.

실무에서 Use case 를 통해 활용 방법을 학습하는 것이 중요하다.




Counting Sort 는 O(n+k) 의 시간 복잡도와 O(n+k) 의 공간복잡도를 이용해서 정렬을 할 수 있는 알고리즘이다. 여기서 k는 가장 큰 키 값을 나타낸다.

일반적으로 "비교 기반" 의 정렬 알고리즘은 O(n log N) 이 가장 효율적인 알고리즘인 것이 수학적으로 증명되었으나, 

Counting Sort 는 비슷한 종류의 Radix Sort 와 마찬가지로 비교 기반의 알고리즘이 아니기 때문에 정렬에 있어 O(n) 의 시간 복잡도를 가질 수 있다.


단, Counting sort 는 비교 기반의 정렬 알고리즘이 아니기 때문에 다소 제약 사항이 존재한다.


Counting sort 는 간단하게 설명해서는 구별되는 키(Distinct Key) 와 해당 키를 Counting 함으로써 이루어지기 때문에, 해당 키값(일반적으로 정수)을 저장할 수 있을 만큼의 Hash table 을 요한다.


말이 Hash table 이지, 키 값을 구별할 수 있는 index table 을 counting array 형태로 사용해도 무방하다.


아래는 Counting sort에 대한 간단한 로직과 구현이다.



(1) Element 의 빈도수(Frequency)를 카운팅(Count)한다.


(2) 카운팅한 빈도수들을 누적합 형태로 정리한다.


(3) Input array 를 순회하며 해당 요소들의 카운트를 차감시키면서 Index 에 배치한다.



    public static int[] countingSort(int[] arr, int noMax) {
        int n = arr.length;
        int[] counts = new int[noMax + 1];
        int[] results = new int[n];
        for (int i = 0; i < n; i++) {
            counts[arr[i]]++;
        }
        for (int i = 1; i <= noMax; i++) {
            counts[i] += counts[i - 1];
        }
        for (int i = n - 1; i >= 0; i--) {
            results[counts[arr[i]] - 1] = arr[i];
            --counts[arr[i]];
        }
        return results;
    }



Counting Sort 는 위와 같이 Element 의 크기에 영향을 받으며, 만약 정렬하고자 하는 요소들 간에 데이터 간격이 Sparse 하다면 불필요한 메모리 사용을 야기한다.


만약 n이 작은 상황이지만 k값이 크다면 당연히 일반 비교 기반 정렬 알고리즘 보다 느리게 된다.


데이터를 잘 분석해서 사용할 만한 경우에 사용하는 것이 필요하다.



Spring 의 Bean 이 정의될 경우 정의하는 Bean 의 Scope 에 대해 옵션을 부여할 수 있다.


예를 들어 Prototype Scope 는 해당 Bean 을 사용할 때마다 재정의하게끔 성질을 부여한다.


반면 Bean 을 Spring project 전역에서 단 하나의 객체로 취급하고 접근하고자 할 경우 해당 Bean 의 Scope 는 Singleton 으로 정의되는 것이 옳다.


Spring 프레임워크는 현재 5개의 Scope 를 제공하고 있다.


(1) Singleton : Spring 컨테이너 하나당 한개의 Bean 만이 생성되고 해당 Bean 이 요청될 때마다 모든 객체는 한 객체를 가리킨다.


(2) Prototype : 하나의 Bean 은 사용할 때마다 새로운 객체를 할당하여 사용하게 된다.


(3) Request : HTTP request 가 상주하는 Spring Application Context 내에서만 유효한 객체를 생성하고 재사용한다. (web-aware)


(4) Session : Bean 을 해당 세션이 가진 Application Context에 바인딩한다. (web-aware)


(5) Global session : HTTP 세션 전역에서 같은 Bean 을 사용가능하다. (web-aware)


Spring Bean 들의 Default scope 는 Singleton 이기 때문에 대부분 Bean 을 사용할 경우 @Autowired 를 통해 간편하게 사용이 가능하다.


만약 어플리케이션에 따라 다른 종류의 Scope 가 필요하다면 다음 예시와 같이 설정에서 변경이 가능하다.



<bean id = "..." class = "..." scope = "prototype">
//...
</bean>



보통의 어플리케이션에서는 Singleton 으로 설계하고 사용할 수 있으나 간혹 prototype 등으로 특정할 수 있으니 상황에 따라 유용하게 사용할 수도 있다.


<보다 나은 예시 참조 : https://www.tutorialspoint.com/spring/spring_bean_scopes.htm>



Spring 의 Bean 들은 대게 Singleton 패턴으로 정의되며 Application Context 에서 관리되는 형태로 개발자는 사용하게 된다.


그렇게 때문에 상당수의 개발자들이 Spring 의 각 Bean 들은 Thread Safety 가 보장된 안전한 Bean Object 이며 Spring 은 해당 동시성 문제에서 자유롭다고 여기는 경향이 있다.


하지만 정확히 Spring Bean 들은 Thread 의 Safety 와 무관하다. 


Spring Container 는 Object 들 각각에 대한 Life cycle 을 갖고 있으며 Container 내에서 하나만 존재하도록 보장하지만 그것이 Thread-Safe 를 말하는 것은 결코 아니며, Spring framework 는 오히려 이 책임을 개발자에게 맡긴다.


만약 Non-Thread-Safe Object 가 Injection 된다면 해당 객체는 쓰레드에 안전하지 않으며 개발자가 직접 핸들링해주어야 한다.


그렇다면 Bean 에 대해 어떤 방식으로 Thread Safety 를 부여할 수 있을까?


(1) Builder Pattern 의 사용

  간단하면서도 Tricky 한 방식으로 Builder Pattern 을 사용하면 좋다.

 Builder 패턴을 이용하면 객체의 Setter 를 정의하지 않은 상태에서 생성자만으로 객체의 Mutation 을 관리할 수 있으며, Spring 의 Bean 들은 Container 에 의해 life cycle 관리가 위임된다.

 따라서 Builder Pattern 을 통해 Set 을 관리하면 간단하면서도 명확히 Thread Safe 를 구현할 수 있다.


(2) Stateless Bean

  Bean 을 상태값과 무관하게 동작할 수 있는 Bean 으로 설계하는 것이다. 

  Bean 이 특정 상태를 나타내는 변수를 계속 메모리에 들고 상주하는 형태가 아닌, 가장 단순한 형태의 도메인 모델로 사용이 추천된다.


(3) Lock the beans

  가장 최후의 수단으로 여겨야 하는 방법으로 Bean 에 대해 Thread Safe 하게 설계를 하는 것이다. Spring 은 Lower level Library 들을 통해 Bean 단위의 Lock 을 지원하고 있으며 이를 통해 병렬처리에 있어서 동시성 문제의 해결이 필요하다.


물론 실제로 Safe 하지 않은 상황을 고려해야할 경우는 많지 않지만, 각기 다른 Request 를 공통으로 분류해야한다거나, 내부에서 Internal Thread 를 구동하는 경우에는 반드시 신경써보자.



 Char형의 뒤에는 캐릭터 형의 길이를 명시하기 위해 (숫자) 와 같은 형식으로 값이 붙게 되는데 Int형 뒤에도 붙일 수 있다. 


하지만 Int형의 괄호는 숫자 개수의 제약을 의미하는 것이 아니라 Zerofill을 위한 것이다. 


예를 들어 Int(5)는 5자리 내 숫자는 모두 0으로 채워준다는 뜻이다. 단 ORACLE에서는 실제 자리 수를 표현하는데 사용된다.





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))



Java 11 이 나오는 와중에 아직 Java9 와 Java10을 정리하지 못했다.(반성;;)

본 포스팅에서는 Java8 에 비해 달라진 Java9 의 Feature 들에 대해 정리해보았다.


1. Java Platform Module System (Jigsaw Project)

Java8 의 주특징이 Lambda 와 Stream 을 중심의 함수형 프로그래밍 도구들이었다면, Java9 의 가장 특징적인 부분 중 하나는 모듈(Module) 의 등장이다.


Java9 부터 사용할 수 있는 모듈은 Class 나 Interface, Package 등을 포함할 수 있는 캡슐화 도구이다.


가령 모듈은 다음과 같이 정의할 수 있다.



module sample {
    exports com.myproject.module;
    requires commons;
}

module commons {
    requires java.base;
    requires java.xml;
}



위의 예시에서 module sample 은 commons 모듈을 import 하고, commons 모듈은 java.base 모듈과 java.xml 모듈을 import 하면서 com.myproject.module 로 export 되게 된다.


여기서 requires 로 지정된 모듈 각각은 서로 dependency 를 가진다. commons 모듈을 통해 java.base 모듈과 java.xml 모듈을 import할 수 있다.


모듈이 등장한 이유는 그 이전까지의 Java 버전이 사용했던 JAR 배포에 있다. 


JAR 파일은 JVM 에 의해 로딩되는 File Archive 로 dependency 에 대한 컨셉을 갖고있지 않다.

Module 은 이를 개선하려 하며, 로딩하는 과정에서 Dependency 를 정의하고자 한다.


또한 Module 은 JAR 파일이 지원하지않는 Encapsulation 에 대한 메커니즘을 지원한다. 포함된 package 의 컬렉션 각각에 대해 필요한 곳과 불필요한 곳을 적절히 정의하고 분류하며


Java의 모듈들을 보기 위해서는 CMD를 이용해서 다음 커맨드를 사용하면 된다.


> java --list-modules



2. JShell 의 지원


이제 Java를 별도의 컴파일 과정이나 main 함수 작성을 위한 Project Structure 의 구성없이 작동시킬 수 있는 REPL 도구인 JShell 이 제공된다.


REPL(Read - Eval - Print - Loop)이란 대화식 언어도구로, JShell 은 이를 지원하는 콘솔로 보면 된다.


대략 다음과 같이 JShell 을 이용할 수 있다.


<예시>


마치 Python 의 Shell 을 흉내낸듯한 CLI 를 통해 보다 쉽게 Java 언어를 구현하고 테스트해볼 수 있다.



3. JavaDoc 이 업그레이드되었다.


JavaDoc 을 사용하면 HTML 형식의 API Document 를 작성할 수 있는데, HTML5 가 지원되도록 추가되었다.

추가로 Java Lint 기능과 Module 에 대한 지원도 되었다.



4. Stream 및 Optional API 지원이 강화되었다.


몇몇 쓰기 불편했던 Java8 의 Stream API 들이 개선되었고, Optional 에 대한 상호 지원이 추가되었다.


iterate(), takeWhile(), dropWhile() 등과 같은 Stream 연산자들은 이제 Stream 의 Laziness 를 강화하며, 이를 이용하면 마치 Python 의 Yield 처럼의 동작도 구현 가능하다.


가령 takeWhile() 연산을 이용하면 스트림을 해당 조건을 만족하는 부분까지 Process 한 후 멈춰둘 수 있다. 이는 Stream Pipeline 을 좀 더 유연하게 사용할 수 있는 흘륭한 도구이다.


가령 Stream 에 Optional에 대한 지원이 추가됨으로써 Nullable 한 상황에 대한 Stream 레벨에서 핸들링이 가능해졌다. 

이전까지 ifPresent 로 조건 체크만 가능했다면, ifPresentOrElse() 와 같은 메서드는 Stream 내에서 Optional 을 지원함으로써 흐름의 분기도 구현이 가능하다.


마찬가지로 Optional 에도 Stream 지원이 추가되어 보다 복잡한 Stream Pipeline 을 구성할 수 있게 되었다.



5. Collection Factory methods


각종 Java Collection 들을 기존까지는 "생성 후 add" 방식으로 이용했다면, 이제는 Factory Method 로 손쉽게 초기화가 가능하다.


가령 다음과 같이 하면 손쉽게 Collection 을 만들 수 있다.



List<String> texts = List.of("hello", "world");



6. Private Interface method


Java 의 인터페이스가 private 메서드를 제공한다. 이제 Interface 구현시에도 외부에 공개할 필요없는 method 를 생성함으로써 캡슐화를 유지할 수 있다.


단, Private method 의 default 구현은 public 이기 때문에 사용에 있어 주의하자.



7. 익명클래스에 대한 Diamond Operator


이제는 <>(Diamond Operator) 가 허용되며, 제약없이도 익명클래스의 사용이 가능하다.



8. HTTP/2 


Java 의 HTTPURLConnection 이 HTTP/2 와 Websocket 등을 지원한다.

또한 Multiplexing 을 통한 다중 요청 처리 후 순서에 따른 응답이 가능해졌고, Push 기능을 지원한다.


그 외에도 JAR 의 다중 Release 가 가능해져서 프로젝트 관리가 쉬워진 부분, Language 지원, 시스템 프로세스 접근의 용이성을 위한 Process API 의 지원 등 기능이 추가되었다.





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 에 비하자면 아직은 쓰기 불편하지만, 익혀둘만한 기술임은 틀림없다.



+ Recent posts