Web/Spring 2020. 6. 25. 15:56

 

Spring Data MongoDB - Reference Documentation

As of version 3.6, MongoDB supports the concept of sessions. The use of sessions enables MongoDB’s Causal Consistency model, which guarantees running operations in an order that respects their causal relationships. Those are split into ServerSession inst

docs.spring.io

 

posted by 여성게
:
Web/Gradle 2020. 6. 23. 23:46

> $HOME/.gradle/caches/modules-2/files-2.1/
> rm -rf <삭제할 스냅샷 혹은 라이브러리>
> ./gradlew --refresh-dependencies
posted by 여성게
:

https://github.com/robert-bor/aho-corasick/tree/master/src/main/java/org/ahocorasick/trie

 

robert-bor/aho-corasick

Java implementation of the Aho-Corasick algorithm for efficient string matching - robert-bor/aho-corasick

github.com

https://www.slideshare.net/ssuser81b91b/ahocorasick-algorithm

 

Aho-Corasick Algorithm(아호 코라식 알고리즘)

Aho-Corasick Algorithm 장홍준 hongjun7@korea.ac.kr

www.slideshare.net

 

posted by 여성게
:
Tools/Git&GitHub 2020. 6. 6. 15:40

 

오늘 다루어볼 내용은 평소에 조금 헷갈렸던 Git reset과 revert이다. 깃에서 되돌리기 위한 방법은 크게 2가지가 있다. 바로 reset과 revert이다. 그렇다면 둘의 차이점은 무엇일까?

 

  • reset : 시계를 마치 과거로 돌리는 듯한 행위
  • revert : 특정 사건을 없었던 일로 만드는 행위
git reset <--option> "돌아가고 싶은 커밋 hash"

먼저 reset을 알아보자. reset은 특정 커밋으로 돌아가는 행위다. 그말은 특정 커밋 이후의 커밋이력 모두 없어지게 되는 것이다.(물론 옵션마다 상태가 다르긴하다) reset에는 옵션이 3가지가 있다. hard, soft, mixed 각 옵션에 대한 설명은 아래와 같다.

 

  • hard : 돌아가려는 커밋 이후의 모든 내용을 다 지워버린다.
  • soft : 돌아가려는 커밋 이력으로 되돌아 갔고, 이후의 내용은 stage 상태로 남아있는다.(git add 상태) 즉, 커밋을 다시 할 수 있는 상태가 되는 것이다.
  • mixed : 돌아가려는 커밋 이력으로 되돌아 갔고, 이후 내용은 남아있지만, unstage된 상태로 남아있다.(git add 이전 상태, tracked file list)

이제 각 옵션에 대해 실습해보자. 우선 쓰레기 코드가 들어간 커밋이 마지막에 찍혀있는 것을 보자.

 

 

각 reset 옵션 적용 후 상태를 확인하자.

 

<hard>

 

이전 커밋이력으로 돌아갔다. 그리고 이전에 추가했던 trash_code.txt도 감쪽 같이 사라졌다.

 

 

<soft>

 

쓰레기코드가 추가된 커밋이 사라졌다. 하지만 Uncommitted changes 상태가 되었다.(git add)

 

 

<mixed>

 

이전 커밋으로 잘 돌아갔고, 파일은 untracked된 상태이다.

 

 

만약 현재 커밋으로부터 몇개 이전으로 커밋을 되돌리고 싶다면 아래 문법으로도 reset이 가능하다.

 

#현재 커밋 기준 6번째 전 커밋으로 되돌린다.
git reset HEAD~6

 

여기까지 reset에 대해 다루어보았다. 다음은 revert에 대해 다루어보자.

 

git revert "지우고 싶은 커밋 hash"

revert는 특정 커밋을 아예 날려버리는 행위이다. 하지만 reset과는 조금 다른 이력이 남는다. 바로 실습내용을 확인해보자.

 

 

나는 가장 마지막 커밋을 revert 할것이다. 과연 이력은 어떻게 남을 걸까?

 

 

reset과는 다르게 내가 특정 커밋을 revert했다라는 이력이 남아있다.(물론 trash_code.txt가 추가됬다는 것도 남아있다.) 이것이 reset과의 차이점이며, 또 하나는 reset은 특정 커밋 이후의 이력을 모두 지우지만, revert는 중간에 껴있는 특정 커밋 이력을 날려버릴 수 있다.

 

그렇다면 revert와 reset은 어떠한 상황에서 사용해야 할까?

보통 reset은 원격 레포지토리로 푸시하기 이전에만 사용한다. 만약 이미 원격 레포지토리에 푸시한 이후에 reset을 하고, push를 하면 아래와 같은 메시지가 보일 것이다. 그 이유는 로컬의 커밋이 원격 커밋보다 뒤에 있기 때문에.. 물론 git push --force 옵션을 사용하여 push할 수 있지만, 이미 다른 사람들이 작업한 커밋이 푸시되어있다면? 윽 생각도 하기 싫다. 

 

 

reset은 혼자만 사용하는 브랜치일 때와 다른 사람들이 해당 브랜치를 pull한 적이 없을 때 정도만 사용가능할 듯하다. 그 이유는 로컬에서만 작업한 내용에서 실수로 다른 커밋을 찍었고 아직 push하기 이전에 reset을 한다면 커밋이력이 훨씬 깔금해지기 때문이다. 하지만 push 이후에는 왠만하면 revert 옵션을 사용하자.

 

push 이후에 revert를 사용하면 내가 어떠한 커밋에 대해 revert를 했는지가 커밋 로그에 남기때문에 훨씬 이력관리에 좋고, 다른 사람이 제가 이력을 지운 커밋이후에 작업을 했더라고, 그 커밋들을 건드리지 않기때문에 크게 문제 없다. 여기까지 git reset, revert에 대해 다루어보았다.

 

<참고>

 

개발바보들 1화 - git "Back to the Future"

  이 내용에 대한 자세한 기술적인 설명이 듣고 싶나요? 연속되는 다음글을 참조하세요    

www.devpools.kr

posted by 여성게
:
인프라/Docker&Kubernetes 2020. 6. 4. 17:10

이번 포스팅은 간단하게 싱글 노드 카프카를 도커로 띄우는 방법이다.

 

git clone https://github.com/wurstmeister/kafka-docker
cd kafka-docker

 

설정 파일은 docker-compoese로 되어있으며, 아래와 같다.

 

version: '2'
services:
  zookeeper:
    image: wurstmeister/zookeeper
    ports:
      - "2181:2181"
  kafka:
    build: .
    ports:
      - "9092:9092"
    environment:
      KAFKA_ADVERTISED_HOST_NAME: 127.0.0.1
      KAFKA_CREATE_TOPICS: "test:1:1"
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

 

docker compose 명령으로 실제 컨테이너를 띄운다.

 

docker-compose -f docker-compose-single-broker.yml up -d

 

posted by 여성게
:
Web/Spring 2020. 5. 15. 14:12

 

MongoDbConfig를 작성할때, 몽고디비 서버 호스트관련하여 ClusterSettings.Builder를 작성해줘야하는데, mongo host에 모든 클러스터 서버 호스트를 명시하지 않고, 하나의 DNS(여러 서버를 하나로 묶은) 혹은 여러 서버 리스트 중 하나의 primary 호스트(ex. primary host를 명시하면 밑에 예외는 발생하지 않지만, 읽기 부하분산이 안된다.)만 명시한경우에는 반드시 multiple mode를 명시해주어야 한다. 내부적으로 host의 갯수를 보고 single mode인지 multiple mode인지 판단하기 때문이다. 해당 코드는 아래와 같다.

 

private ClusterSettings(final Builder builder) {
        // TODO: Unit test this
        if (builder.srvHost != null) {
            if (builder.srvHost.contains(":")) {
                throw new IllegalArgumentException("The srvHost can not contain a host name that specifies a port");
            }

            if (builder.hosts.get(0).getHost().split("\\.").length < 3) {
                throw new MongoClientException(format("An SRV host name '%s' was provided that does not contain at least three parts. "
                        + "It must contain a hostname, domain name and a top level domain.", builder.hosts.get(0).getHost()));
            }
        }

        if (builder.hosts.size() > 1 && builder.requiredClusterType == ClusterType.STANDALONE) {
            throw new IllegalArgumentException("Multiple hosts cannot be specified when using ClusterType.STANDALONE.");
        }

        if (builder.mode != null && builder.mode == ClusterConnectionMode.SINGLE && builder.hosts.size() > 1) {
            throw new IllegalArgumentException("Can not directly connect to more than one server");
        }

        if (builder.requiredReplicaSetName != null) {
            if (builder.requiredClusterType == ClusterType.UNKNOWN) {
                builder.requiredClusterType = ClusterType.REPLICA_SET;
            } else if (builder.requiredClusterType != ClusterType.REPLICA_SET) {
                throw new IllegalArgumentException("When specifying a replica set name, only ClusterType.UNKNOWN and "
                                                   + "ClusterType.REPLICA_SET are valid.");
            }
        }

        description = builder.description;
        srvHost = builder.srvHost;
        hosts = builder.hosts;
        mode = builder.mode != null ? builder.mode : hosts.size() == 1 ? ClusterConnectionMode.SINGLE : ClusterConnectionMode.MULTIPLE;
        requiredReplicaSetName = builder.requiredReplicaSetName;
        requiredClusterType = builder.requiredClusterType;
        localThresholdMS = builder.localThresholdMS;
        serverSelector = builder.packServerSelector();
        serverSelectionTimeoutMS = builder.serverSelectionTimeoutMS;
        maxWaitQueueSize = builder.maxWaitQueueSize;
        clusterListeners = unmodifiableList(builder.clusterListeners);
    }

 

ClusterSettings.Builder.build 메서드의 일부인데, mode를 set하는 부분에 mode를 명시적으로 넣지 않았다면 작성된 호스트 갯수를 보고 클러스터 모드를 결정한다. 만약 MonoDb 서버 여러개를 하나의 도메인으로 묶어 놓았다면, 보통 DNS하나만 설정에 넣기 마련인데, 이러면 write 요청이 secondary에 들어가게 되면 아래와 같은 에러가 발생하게 된다.(먄약 실수로 secondary host를 넣었다면 쓰기요청에 당연히 아래 예외가 계속 발생한다.)

 

MongoNotPrimaryException: Command failed with error 10107 (NotMaster): 'not master' on server bot-meta01-mongo1.dakao.io:27017. 

 

왠지, SINGLE모드일때는 secondary로 write요청이 들어왔을때 primary로 위임이 안되는듯하다.(이건 조사해봐야할듯함, 왠지 싱글모드이면 당연히 프라이머리라고 판단해서 그럴듯?..) 그렇기 때문에 클러스터는 걸려있고, 서버 리스트를 여러 서버를 묶은 DNS 하나만 작성한다면 반드시 ClusterSetting에 "MULTIPLE"을 명시해서 넣야야한다 !

posted by 여성게
:

오늘 다루어볼 내용은 java 14에서 도입된 record 타입의 클래스입니다. 

 

record란?

레코드(record)란 "데이터 클래스"이며 순수하게 데이터를 보유하기 위한 특수한 종류의 클래스이다. 코틀린의 데이터 클래스와 비슷한 느낌이라고 보면 된다. 밑에서 코드를 보겠지만, record 클래스를 정의할때, 그 모양은 정말 데이터의 유형만 딱 나타내는 듯한 느낌이다. 훨씬더 간결하고 가볍기 때문에 Entity 혹은 DTO 클래스를 생성할때 사용되면 굉장히 좋을 듯하다.

 

sample code

간단하게 샘플코드를 살펴보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SampleRecord {
   private final String name;
   private final Integer age;
   private final Address address;
 
   public SampleRecord(String name, Integer age, Address address) {
      this.name = name;
      this.age = age;
      this.address = address;
   }
 
   public String getName() {
      return name;
   }
 
   public Integer getAge() {
      return age;
   }
 
   public Address getAddress() {
      return address;
   }
}
cs

 

위와 같은 코드가 있다고 가정하자. 해당 클래스는 모든 인스턴스 필드를 초기화하는 생성자가 있고, 모든 필드는 final로 정의되어 있다. 그리고 각각 필드의 getter를 가지고 있다. 이러한 클래스 같은 경우는 record 타입의 클래스로 변경이 가능하다.

 

1
2
3
4
5
public record SampleRecord(
   String name,
   Integer age,
   Address address
) {}
cs

 

엄청 클래스가 간결해진 것을 볼 수 있다. 이 record 클래스에 대해 간단히 설명하면 아래와 같다.

 

  • 해당 record 클래스는 final 클래스이라 상속할 수 없다.
  • 각 필드는 private final 필드로 정의된다.
  • 모든 필드를 초기화하는 RequiredAllArgument 생성자가 생성된다.
  • 각 필드의 getter는 getXXX()가 아닌, 필드명을 딴 getter가 생성된다.(name(), age(), address())

 

만약 그런데 json serialize가 되기 위해서는 위와 같이 선언하면 안된다. 아래와 같이 jackson 어노테이션을 붙여줘야한다.

 

1
2
3
4
5
public record SampleRecord(
   @JsonProperty("name") String name,
   @JsonProperty("age") Integer age,
   @JsonProperty("address") Address address
) {}
cs

 

record 클래스는 static 변수를 가질 수 있고, static&public method를 가질 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public record SampleRecord(
   @JsonProperty("name"String name,
   @JsonProperty("age") Integer age,
   @JsonProperty("address") Address address
) {
   
   static String STATIC_VARIABLE = "static variable";
   
   @JsonIgnore
   public String getInfo() {
      return this.name + " " + this.age;
   }
 
   public static String get() {
      return STATIC_VARIABLE;
   }
}
cs

 

또한 record 클래스의 생성자를 명시적으로 만들어서 생성자 매개변수의 validation 로직등을 넣을 수도 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public record SampleRecord(
   @JsonProperty("name"String name,
   @JsonProperty("age") Integer age,
   @JsonProperty("address") Address address
) {
 
   public SampleRecord {
      if (name == null || age == null || address == null) {
         throw new IllegalArgumentException();
      }
   }
 
   static String STATIC_VARIABLE = "static variable";
 
   @JsonIgnore
   public String getInfo() {
      return this.name + " " + this.age;
   }
 
   public static String get() {
      return STATIC_VARIABLE;
   }
}
cs

 

이러한 record 클래스를 spring의 controller와 연계해서 사용하면 더 간결한 injection이 가능해지기 때문에 훨씬 깔끔한 컨트롤러 클래스작성이 가능하다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
======================================================================================
 
public record SampleRecord(
   @JsonProperty("name"String name,
   @JsonProperty("age") Integer age,
   @JsonProperty("address") Address address
) {
   
   static String STATIC_VARIABLE = "static variable";
   
   @JsonIgnore
   public String getInfo() {
      return this.name + " " + this.age;
   }
 
   public static String get() {
      return STATIC_VARIABLE;
   }
}
 
======================================================================================
 
public record Address(
    @JsonProperty("si"String si,
    @JsonProperty("gu"String gu,
    @JsonProperty("dong"String dong
) {}
 
======================================================================================
 
@Service
public class SampleRecordService {
    public Mono<SampleRecord> sampleRecordMono(SampleRecord sampleRecord) {
        return Mono.just(sampleRecord);
    }
}
 
======================================================================================
 
@RestController
public record SampleController(SampleRecordService sampleRecordService) {
    @PostMapping("/")
    public Mono<SampleRecord> sampleRecord(@RequestBody SampleRecord sampleRecord) {
        System.out.println(sampleRecord.getInfo());
        return sampleRecordService.sampleRecordMono(sampleRecord);
    }
}
 
======================================================================================
cs

 

여기까지 간단하게 jdk 14의 new feature인 record 클래스에 대해 간단하게 다루어 보았다. 모든 코드는 아래 깃헙을 참고하자.

 

 

yoonyeoseong/jdk_14_record_sample

Contribute to yoonyeoseong/jdk_14_record_sample development by creating an account on GitHub.

github.com

 

참조 : https://dzone.com/articles/jdk-14-records-for-spring-devs

 

JDK 14 Records for Spring - DZone Java

In this article, we'll discuss several use cases for JDK 14 Records to write cleaner and more efficient code.

dzone.com

 

posted by 여성게
:
Web/gRPC 2020. 5. 3. 14:41

이전 포스팅에서는 grpc를 이용하여 간단하게 client-server 애플리케이션을 작성하였다. 간단하게 감을 익혀봤으니, 세세한 부분을 스터디 해본다. 오늘은 Protobuf(proto3)에 대해 다루어본다.

 

메시지 유형 정의

간단한 예를 보자. 검색 요청 메시지이며, 쿼리 문자열과 페이징을 위한 필드를 가지고 있다.

 

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

 

첫 줄은 proto3 syntax를 사용하고 있음을 지정한다. default는 proto2이다. 그리고 message SearchRequest를 정의한다. message는 자바로 비교하면 하나의 dto(model) 클래스라고 생각하면 좋다. 그리고 해당 message안에 필요한 필드를 선언한다. 위의 모든 필드는 scala type으로 지정되어 있지만, enum 및 다른 message 유형을 참조하여 복합 유형을 지정할 수도 있다. 

 

그리고 하나의 proto 파일에 여러 message 유형을 정의할 수 있다.

 

필드 번호

message의 각 필드에는 번호가 할당되어 있다. 이 필드 번호는 message가 이진형식으로 직렬화될때 필드를 식별하는데 사용되는 번호이다.

 

주석

proto 파일은 아래와 같이 주석 작성이 가능하다.

 

syntax = "proto3";

/*
검색 요청을 위한 요청 객체를 표현하는 message 정의
 */
message SearchRequest {
  string query = 1; //쿼리 문자열
  int32 page_number = 2;
  int32 result_per_page = 3;
}

 

Scala value types

스칼라 타입 유형은 아래와 같이 지원하고 있다.

 

TYPE

Notes

Java

Python

double

 

double

float

float

 

float

float

int32

Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead.

int

int

int64

Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead.

long

int/long[3]

uint32

Uses variable-length encoding.

int[1]

int/long[3]

uint64

Uses variable-length encoding.

long[1]

int/long[3]

sint32

Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s.

int

int

sint64

Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s.

long

int/long[3]

fixed32

Always four bytes. More efficient than uint32 if values are often greater than 228.

int[1]

int/long[3]

fixed64

Always eight bytes. More efficient than uint64 if values are often greater than 256.

long[1]

int/long[3]

sfixed32

Always four bytes.

int

int

sfixed64

Always eight bytes.

long

int/long[3]

bool

 

boolean

bool

string

A string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot be longer than 232.

String

str/unicode[4]

bytes

May contain any arbitrary sequence of bytes no longer than 232.

ByteString

str

 

Default Value

messagefmf deserialize할때, 값이 없다면 각 타입에 대해 아래와 같은 기본 값을 가진다.

 

Type Default Value
string ""
bytes empty bytes
boolean false
numeric 0
enums 첫번째 정의된 enum value(must be 0)

 

Enum Type

protobuf에서도 자바와 같은 Enum 타입을 생성할 수 있다.

 

syntax = "proto3";

/*
검색 요청을 위한 요청 객체를 표현하는 message 정의
 */
message SearchRequest {
  string query = 1; //쿼리 문자열
  int32 page_number = 2;
  int32 result_per_page = 3;
  Corpus corpus = 4;
}

enum Corpus {
  UNIVERSAL = 0;
  WEB = 1;
  IMAGES = 2;
  LOCAL = 3;
  NEWS = 4;
  PRODUCTS = 5;
  VIDEO = 6;
}

 

열거형은 반드시 시작을 0으로 시작해야한다. 그래야 역직렬화할때, 값이 존재하지 않을경우 0에 할당된 값을 기본값으로 쓰기 때문이다. 

 

다른 message 유형 사용

다른 message 타입을 필드 타입으로 사용할 수 있다.

 

message SearchResponse {
  SearchResult results = 1;
}

message SearchResult {
  string url = 1;
  string title = 2;
}

 

그렇다면 다른 .proto 파일에 있는 message 타입을 사용하려면 어떻게 해야할까?

 

#SearchResult.proto
syntax = "proto3";

option java_multiple_files = true;
option java_outer_classname = "SearchResultProto";
option java_package = "com.levi.yoon.proto";

package grpc.sample;

message SearchResult {
  string url = 1;
  string title = 2;
}

#SearchProto.proto
syntax = "proto3";
import "SearchResult.proto";

option java_multiple_files = true;
option java_outer_classname = "SearchProto";
option java_package = "com.levi.yoon.proto";

package grpc.sample;

/*
검색 요청을 위한 요청 객체를 표현하는 message 정의
 */
message SearchRequest {
  string query = 1; //쿼리 문자열
  int32 page_number = 2;
  int32 result_per_page = 3;
  Corpus corpus = 4;
}

enum Corpus {
  UNIVERSAL = 0;
  WEB = 1;
  IMAGES = 2;
  LOCAL = 3;
  NEWS = 4;
  PRODUCTS = 5;
  VIDEO = 6;
}

message SearchResponse {
  SearchResult results = 1;
}

 

import 문을 통해서 다른 .proto 파일을 가져와서 사용할 수 있다.

 

중첩 타입

message 타입을 중첩해서 사용가능하다. 마치 자바에서 클래스 안에 클래스를 가지듯 !

 

syntax = "proto3";

message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
  }
  repeated Result results = 1;
}

 

Map type

필드 타입으로 Map을 사용할 수 있다.

 

syntax = "proto3";

option java_multiple_files = true;
option java_outer_classname = "ExampleRequestProto";
option java_package = "com.levi.yoon.proto";

package grpc.sample;

message ExampleRequest {
  map<string, string> requests = 1;
}

 

해당 필드를 set/get 할때는 아래와 같은 메서드를 지원한다.

 

ExampleRequest exampleRequest = ExampleRequest.newBuilder()
        .putRequests("a", "a")
        .putRequests("b", "b")
        .build();
exampleRequest.getRequestsMap();
exampleRequest.getRequestsOrDefault("a", "defaultValue");
exampleRequest.getRequestsOrThrow("a");

 

주의해야할 점은 Map 타입은 repeated를 사용할 수 없다.

 

Package

protobuf message 타입간 이름 충돌을 피하기 위해 파일에 선택적으로 package 지정자를 추가할 수 있다.

 

#ExampleRequestProto1.proto
syntax = "proto3";

package grpc.sample1;

message ExampleRequest {
  map<string, string> requests = 1;
}

#ExampleRequestProto2.proto
syntax = "proto3";

package grpc.sample2;

message ExampleRequest {
  map<string, string> requests = 1;
}

 

서비스 정의

client에서 stub 객체로 호출할 원격 프로시져(서비스)를 정의할 수 있다.

 

service SearchService {
  rpc Search (SearchRequest) returns (SearchResponse);
}

 

우리가 정의한 SearchRequest message를 매개변수로 받고, 응답으로 SearchResponse를 반환하는 서비스를 정의하였다. 해당 interface를 gRPC server에서 override하여 구현하고, client는 Stub 객체 안에 해당 서비스이름의 메서드를 콜하여 원격 gRPC service를 호출하게 된다.

 

기타 옵션

option java_multiple_files = true; -> proto 파일안의 message와 enum, service 등이 각 java 파일로 생성된다.

option java_outer_classname = "SearchProto"-> 생성될 자바코드의 클래스명이 된다.

option java_package = "com.levi.yoon.proto"-> 생성된 자바코드의 package 경로가 된다.

 

여기까지 간단하게 Protobuf에 대한 기능들 몇가지를 다루어보았다. 미쳐 다루지 못한 것은 실제 grpc 실습에서 다루어본다.

'Web > gRPC' 카테고리의 다른 글

gRPC - convert proto generate java to jsonString  (0) 2020.07.15
gRPC - java gRPC 간단한 사용법  (0) 2020.05.03
gRPC - gRPC란 무엇인가?  (0) 2020.05.02
posted by 여성게
: