Web/gRPC 2020. 5. 3. 14:02

이번 포스팅은 gRPC의 세세한 기능을 다루기 이전에 간단하게 java로 gRPC 서버와 클라이언트 코드를 작성해보고 감을 익혀보는 포스팅이다. 오늘 구성할 프로젝트 구조는 아래와 같다.

 

  1. grpc-common : .proto 파일을 이용하여 client와 server가 공통으로 사용할 소스를 generate한 프로젝트. client와 server에서 submodule로 추가할 프로젝트이다.
  2. grpc-server : server역할을 할 gRPC 서버 애플리케이션이다.
  3. grpc-client : client역할을 할 gRPC 클라이언트 애플리케이션이다.

 

grpc-common

모든 소스코드는 아래 깃헙 주소를 참고하면 된다.

 

 

yoonyeoseong/grpc-common

Contribute to yoonyeoseong/grpc-common development by creating an account on GitHub.

github.com

우선 gradle 프로젝트를 생성해준다. 그 이후 build.gradle을 아래와 같이 세팅한다.

 

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
50
51
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.8'
    }
}
 
plugins {
    id 'java'
}
 
apply plugin: 'com.google.protobuf'
 
group 'org.example'
version '1.0-SNAPSHOT'
 
repositories {
    mavenCentral()
}
 
dependencies {
    /**
     * gRPC
     */
    compile group: 'io.grpc', name: 'grpc-netty-shaded', version: '1.21.0'
    compile group: 'io.grpc', name: 'grpc-protobuf', version: '1.21.0'
    compile group: 'io.grpc', name: 'grpc-stub', version: '1.21.0'
    implementation "com.google.protobuf:protobuf-java-util:3.8.0"
    compile group: 'com.google.protobuf', name: 'protobuf-java', version: '3.8.0'
 
    testCompile group: 'junit', name: 'junit', version: '4.12'
}
 
protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.7.1"
    }
    plugins {
        grpc {
            artifact = 'io.grpc:protoc-gen-grpc-java:1.21.0'
        }
    }
    generateProtoTasks {
        all()*.plugins {
            grpc {}
        }
    }
}
 
cs

 

gRPC를 위한 의존서을 추가해주었고, .proto 파일에 작성된 리소스를 자바코드로 generate하기 위한 설정들이 들어가있다. 

 

이제 src/main/proto 디렉토리를 하나 생성해준다. 그 이후에 아래와 같은 .proto 파일을 작성해준다.

 

syntax = "proto3";

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

package grpc.sample;

message SampleRequest {
  string userId = 1;
  string message = 2;
}

message SampleResponse {
  string message = 1;
}

service SampleService {
  rpc SampleCall (SampleRequest) returns (SampleResponse) {}
}

 

아직은 해당 파일에 대한 세세한 내용은 다루지 않는다. 추후에 다루어 볼것이니 일단 몰라도 넘어가자. proto 파일을 생성해주었으면 generateProto task를 실행시켜준다. 이후 소스가 잘 생성되었는지 확인하자.

 

 

grpc-server

 

 

yoonyeoseong/grpc-server

Contribute to yoonyeoseong/grpc-server development by creating an account on GitHub.

github.com

 

이제는 grpc 서버 역할을 할 애플리케이션을 작성한다. 우선 바로 위에서 생성한 프로젝트를 submodule로 추가해준다.

 

> git submodule add <grpc-common 깃 주소>

 

필자는 spring의 injection을 사용하기 위해서 스프링부트 프로젝트로 생성해주었다. 일반 gradle 프로젝트도 상관없으니 편할대로 생성해주면 될듯하다.

 

build.gradle에 grpc-common에 생성된 소스를 sourceSets로 넣는 설정을 하나 넣어준다. 그리고 필요한 dependency를 추가한다. 물론 grpc-server에 필요하지 않은 의존성이 있긴하지만 우선 추가하자.

 

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.8'
    }
}

plugins {
    id 'org.springframework.boot' version '2.2.6.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id 'java'
}

apply plugin: 'com.google.protobuf'

group = 'com.levi'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '14'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    /**
     * gRPC
     */
    compile group: 'io.grpc', name: 'grpc-netty-shaded', version: '1.21.0'
    compile group: 'io.grpc', name: 'grpc-protobuf', version: '1.21.0'
    compile group: 'io.grpc', name: 'grpc-stub', version: '1.21.0'
    implementation "com.google.protobuf:protobuf-java-util:3.8.0"
    compile group: 'com.google.protobuf', name: 'protobuf-java', version: '3.8.0'

    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    testImplementation 'io.projectreactor:reactor-test'
}

test {
    useJUnitPlatform()
}

sourceSets {
    main {
        java {
            srcDirs += [
                    'grpc-common/build/generated/source/proto/main/grpc',
                    'grpc-common/build/generated/source/proto/main/java'
            ]
        }
    }
}

 

이제 우리가 정의한 service를 구현해보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
@Service
public class SampleServiceImpl extends SampleServiceGrpc.SampleServiceImplBase {
 
    @Override
    public void sampleCall(SampleRequest request, StreamObserver<SampleResponse> responseObserver) {
        log.info("SampleServiceImpl#sampleCall - {}, {}", request.getUserId(), request.getMessage());
        SampleResponse sampleResponse = SampleResponse.newBuilder()
                .setMessage("grpc service response")
                .build();
 
        responseObserver.onNext(sampleResponse);
        responseObserver.onCompleted();
    }
}
cs

 

단순히 우리가 정의한 service(sampleCall)을 구현하는 것 뿐이다. 요청으로 SampleRequest 객체를 받고, 응답으로 SampleResponse를 내보내주면 된다. 비즈니스 로직은 없고 단순히 로그를 찍고 SampleResponse를 리턴하는 단순한 로직이다. 사실 client-server 통신 방법은 크게 4가지가 있는데, 4가지중 "client : server = 1 : 1" 로 통신하는 구조로 만들었다.

 

gRPC 서버를 구동하기 위한 클래스를 하나 작성한다. 뭐 여러가지 튜닝할 요소가 있을지는 모르겠지만, 우선 띄우는 것에 집중하자. gRPC-server로 결국은 server이다. 포트를 할당 받아서 서버형태로 띄운다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class GrpcRunner implements ApplicationRunner {
 
    private static final int PORT = 3030;
    private static final Server SERVER = ServerBuilder.forPort(PORT)
            .addService(new SampleServiceImpl())
            .build();
 
    @Override
    public void run(ApplicationArguments args) throws Exception {
        SERVER.start();
    }
}
cs

 

이제 spring application을 실행시키면 grpc server가 구동된다.

 

grpc-client

 

 

yoonyeoseong/grpc-client

Contribute to yoonyeoseong/grpc-client development by creating an account on GitHub.

github.com

 

마지막으로 grpc-server와 통신할 grpc-client이다. grpc-common 프로젝트를 submodule로 추가한다.

 

> git submodule add <grpc-common 깃 주소>

 

이제 build.gradle을 작성하자. grpc-server와 같이 필요없는 의존성이 있을 수 있지만 일단 추가하자.

 

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.8'
    }
}

plugins {
    id 'org.springframework.boot' version '2.2.6.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id 'java'
}

apply plugin: 'com.google.protobuf'

group = 'com.levi'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '14'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    /**
     * gRPC
     */
    compile group: 'io.grpc', name: 'grpc-netty-shaded', version: '1.21.0'
    compile group: 'io.grpc', name: 'grpc-protobuf', version: '1.21.0'
    compile group: 'io.grpc', name: 'grpc-stub', version: '1.21.0'
    implementation "com.google.protobuf:protobuf-java-util:3.8.0"
    compile group: 'com.google.protobuf', name: 'protobuf-java', version: '3.8.0'

    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    testImplementation 'io.projectreactor:reactor-test'
}

sourceSets {
    main {
        java {
            srcDirs += [
                    'grpc-common/build/generated/source/proto/main/grpc',
                    'grpc-common/build/generated/source/proto/main/java'
            ]
        }
    }
}

test {
    useJUnitPlatform()
}

 

이제 stub 객체를 이용하여 grpc-server의 원격 메서드를 호출하는 client 코드를 간단하게 작성한다.

 

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
@Slf4j
@Service
public class GrpcClient {
    private static final int PORT = 3030;
    public static final String HOST = "localhost";
    private final SampleServiceGrpc.SampleServiceStub asyncStub = SampleServiceGrpc.newStub(
            ManagedChannelBuilder.forAddress(HOST, PORT)
            .usePlaintext()
            .build()
    );
 
    public String sampleCall() {
        final SampleRequest sampleRequest = SampleRequest.newBuilder()
                .setUserId("levi.yoon")
                .setMessage("grpc request")
                .build();
 
        asyncStub.sampleCall(sampleRequest, new StreamObserver<SampleResponse>() {
            @Override
            public void onNext(SampleResponse value) {
                log.info("GrpcClient#sampleCall - {}", value);
            }
 
            @Override
            public void onError(Throwable t) {
                log.error("GrpcClient#sampleCall - onError");
            }
 
            @Override
            public void onCompleted() {
                log.info("GrpcClient#sampleCall - onCompleted");
            }
        });
        return "string";
    }
}
cs

 

우선 client에서는 크게 2가지가 등장한다. (Stub & Channel)

 

  1. Stub : Remote procedure call을 하기 위한 가짜 객체라고 생각하면 된다. 마치 자신의 메서드를 호출하는 듯한 느낌이지만, 실제 원격(grpc-server) 메서드를 호출한다. Stub에는 async, blocking, future등 여러 스텁 종류가 존재하지만, 우리는 asyncStub을 이용한다.
  2. Channel : 결국 Stub도 원격 서버의 메서드를 호출한다. 그렇기 때문에 grpc-server와 통신할 channel을 정의해준다.

 

우리가 정의한 service의 스텁 객체를 생성해주고, 매개변수로 Channel을 생성해서 넣어준다. 우리가 띄울 grpc-server의 host와 port를 넣는다.

 

.proto로 message를 정의하면 java에서 사용할 수 있는 빌더패턴의 메서드, setter&getter등을 만들어준다. 요청을 위한 SampleRequest 객체를 만들었다. 이제 stub의 sampleCall(우리가 정의한 서비스)를 호출한다. 그런데 매개변수로 StreamObserver가 들어가는데, 해당 Stub은 asyncStub이기 때문에 callback 객체를 넣어주는 것이다. 간단히 로그를 찍는 callback 로직을 넣어주었다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
@SpringBootApplication
@RequiredArgsConstructor
public class GrpcApplication {
 
    private final GrpcClient grpcClient;
 
    public static void main(String[] args) {
        SpringApplication.run(GrpcApplication.class, args);
    }
 
    @GetMapping("/")
    public Mono<String> test() {
        return Mono.just(grpcClient.sampleCall());
    }
 
}
cs

 

위는 간단하게 요청을 받을 controller를 하나 정의하였다.

 

실행

이제 grpc-server와 grpc-client를 띄우고 요청을 보내보자.

 

> curl http://localhost:9090/

#grpc-server
2020-05-03 13:59:18.781  INFO : SampleServiceImpl#sampleCall - levi.yoon, grpc request

#grpc-client
2020-05-03 13:57:09.014  INFO : GrpcClient#sampleCall - message: "grpc service response"
2020-05-03 13:57:09.032  INFO : GrpcClient#sampleCall - onCompleted

 

와우 ! client-server 간의 통신이 잘 이루어졌고, 우리가 찍었던 로그가 잘 보인다. 지금까지 간단하게 java 기반의 grpc를 다루어보았다. 사실 다룰 내용이 훨씬 많다. 그렇지만 이렇게 한번 간단하게 사용해보면 추후에 세세한 부분을 공부할때, 더 수월할 수도 있을 것 같다.

posted by 여성게
:
Web/gRPC 2020. 5. 2. 22:14

오늘은 gRPC가 무엇인지 알아본다. 그동안 스터디한다고 마음만 먹고 매일 미루기만 했는데, 황금연휴에 맘잡고 gRPC에 대해 다루어 볼 것이다.

 

개요

gRPC를 사용하면 클라이언트 애플리케이션에서 마치 자신의 메서드를 호출하는 것처럼 원격서버(gRPC서버)의 메서드를 직접 호출 할 수 있으므로 MSA환경의 서비스를 보다 쉽게 만들 수 있다. 여타 다른 RPC와 마찬가지로 gRPC는 IDL(Interface Definition Language)를 이용하여 서비스를 정의하고 페이로드를 정의하며 gRPC서버는 이 인터페이스를 구현하고 클라이언트 호출을 처리하기 위해 gRPC서버를 실행한다. 클라이언트 측에서 클라이언트는 서버와 동일한 인터페이스를 가지는 스텁 객체를 가지고 있다.

 

 

ProtoBuf

gRPC는 기본적으로 구조화된 데이터를 직렬화하기 위해 Google의 ProtoBuf를 사용한다. 

 

ProtoBuf로 작업할 때는 첫번째로 .proto 파일에 직렬화하려는 데이터의 구조를 정의한다. 메시지라는 오브젝트(?)로 구성되며, 각 메시지는 데이터 타입과 key&value쌍을 이루는 필드를 하나이상 가지고 있다.

 

message Person {
  string name = 1;
  int32 id = 2;
  bool has_ponycopter = 3;
}

 

그 이후 protoc를 사용하여 원하는 언어로 컴파일이 가능하다. 컴파일된 소스는 각 필드에 대한 간단한 setter/getter를 제공한다. 또한 .proto 파일을 이용하여 위 proto message가 매개변수, 리턴 값으로 사용된 service를 정의할 수 있다.

 

// The greeter service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

 

 

일반 RPC와 다를게 없을 수도 있는데, gRPC가 왜 좋은가? 

 

성능

gRPC 메시지는 효율적인 이진 메시지 형식인 ProtoBuf를 사용하여 직렬화된다. protobuf는 서버와 클라이언트에서 엄청 빠르게 직렬화된다. 또한 적은 용량의 페이로드로 형성되어 있어, 제한된 대역폭에서 아주 중요한 역할을 한다.

 

코드 생성

모든 gRPC 프레임워크는 코드 생성에 대한 최고 수준의 지원을 제공한다. gRPC 개발에 대한 핵심 파일은 gRPC 서비스 및 메시지의 계약을 정의하는 .proto file이다. 이 파일에서 gRPC 프레임워크는 서비스 기본 클래스, 메시지 및 전체 클라이언트를 코드 생성한다.

서버와 클라이언트 간에 proto 파일을 공유하여 메시지와 클라이언트 코드를 종단 간에 생성할 수 있다. 클라이언트의 코드 생성은 클라이언트와 서버에서 메시지의 중복을 제거하고 강력한 형식의 클라이언트를 만든다. 클라이언트를 작성하지 않아도 되므로 많은 서비스를 갖춘 응용 프로그램의 개발 시간이 상당히 절감된다.

 

엄격한 사양

일반적으로 JSON을 주고 받는 HTTP는 엄격한 사양이 존재하지 않는다. 그래서 개발자끼리 URL, HTTP payload, 응답코드 등을 논의해야한다. 하지만 gRPC는 gRPC가 따라야하는 명확한 형식이 있기 때문에 HTTP와 같이 큰 논의가 필요없다. 단순히 생성된 코드를 사양에 맞게 사용하면 된다.

 

그렇다면 gRPC는 어떠한 상황에서 사용하는 것이 좋을까?

 

  • 마이크로 서비스 - gRPC는 대기 시간이 짧고 처리량이 높은 통신을 위해 설계되었습니다. gRPC는 효율성이 중요한 경량 마이크로 서비스에 적합합니다.
  • gRPC – 지점 간 실시간 통신은 양방향 스트리밍을 위한 뛰어난 지원 기능을 제공합니다. gRPC 서비스는 폴링을 사용하지 않고 실시간으로 메시지를 푸시할 수 있습니다.
  • Polyglot 환경 – gRPC 도구는 널리 사용되는 모든 개발 언어를 지원하며, 따라서 gRPC는 다중 언어 환경에 적합합니다.
  • 네트워크 제한 환경 – gRPC 메시지는 경량 메시지 형식인 Protobuf를 사용하여 직렬화됩니다. gRPC 메시지는 항상 해당하는 JSON 메시지보다 작습니다.

 

여기까지 gRPC가 무엇이고, 어떠한 특징을 가지며 장점이 무엇인지 다루어보았다. 다음 포스팅부턴 제대로된 실습을 해본다.

posted by 여성게
:
Web/Spring 2020. 4. 29. 20:50

 

오늘 다루어볼 내용은 spring application.yaml(properties)파일들의 로드 규칙 및 순서이다. 기본적인 내용일수는 있겠지만 필자는 이번에 해당 순서의 중요성을 다시 한번 알게되서 한번더 정리해보려고 한다.

 

 

Spring Boot Features

If you need to call remote REST services from your application, you can use the Spring Framework’s RestTemplate class. Since RestTemplate instances often need to be customized before being used, Spring Boot does not provide any single auto-configured RestT

docs.spring.io

 

위 링크를 보면 PropertySource의 적용 순서가 나와있다. 우리가 신경 쓸 것은 application.properties와 application-{profiles}.properties의 순서이다. 위 링크에서는 application-{profiles}.properties가 더 우선 순위를 갖는다고 이야기하고 있다. 그 말은 무엇일까? 아래 간단히 application.properties 파일을 확인해보자.

 

#application.yaml
spring:
  application:
    name: name-1
    
#application-dev.yaml
spring:
  application:
    name: name-2

 

과연 위 yaml파일들을 모두 적용하고 나서 아래 코드를 실행한다면 어떤 값이 출력될까? 실행할때 VM option에

 

"-Dspring.profiles.active=dev"

 

을 넣어서 run 해야한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Slf4j
@RestController
@SpringBootApplication
public class JenkinsSampleApplication implements CommandLineRunner {
 
    @Value("${spring.application.name}")
    private String appName;
 
    public static void main(String[] args) {
        SpringApplication.run(JenkinsSampleApplication.class, args);
    }
 
    @Override
    public void run(String... args) throws Exception {
        log.info(appName);
    }
 
}
cs

 

결과는 "name-2"가 출력된다. 그 말은 application-{profiles}.yaml이 우선 적용이 된다는 뜻이다. 그렇다면 아래와 같은 config가 있다면 어떨까?

 

#application.yaml
spring:
  application:
    name: name-1
    
#application-dev.yaml
spring:
  profiles:
    include: dev-common
  application:
    name: name-2
    
#application-dev-common.yaml
spring:
  application:
    name: name-3

 

출력결과는 "name-3"이다. 그 말은 application-dev.yaml에서 include한 application-dev-common.yaml이 가장 큰 우선순위를 갖는 것이다. 

 

#application.yaml
spring:
  profiles:
    include: dev-common
  application:
    name: name-1
    
#application-dev.yaml
spring:
  application:
    name: name-2
    
#application-dev-common.yaml
spring:
  application:
    name: name-3

 

위 파일은 application-dev-common.yaml파일을 application.yaml에 include하였다. 이때 결과는 어떻게 될것인가? "name-2"를 출력할 것이다. 그 이유는 application.yaml에서 include했지만 application-dev.yaml이 더 우선순위를 갖기 때문이다. 즉, include된 설정값이 가장 우선순위를 갖기 위해서는 application-{profiles}.yaml에 include해야한다.

(만약 application-{profiles}.yaml이 없었다면 application-dev-common.yaml의 설정인 "name-3" 설정이 적용이 될것이다.)

 

해당 규칙들을 잘 활용해서 관리하기 쉽게 설정값들을 유지하면 좋을 것 같다. 여기까지 간단하게 spring application 설정의 적용 규칙 및 순서에 대해 다루어보았다.

posted by 여성게
:

오늘 다루어볼 내용은 Model mapping을 아주 쉽게 해주는 Mapstruct라는 라이브러리를 다루어볼 것이다. 그 전에 Model mapping에 많이 사용되는 ModelMapper와의 간단한 차이를 이야기해보면, Model Mapper는 Runtime 시점에 reflection으로 모델 매핑을 하게 되는데, 이렇게 런타임시에 리플렉션이 자주 이루어진다면 앱성능에 아주 좋지 않다. 하지만 Mapstruct는 컴파일 타임에 매핑 클래스를 생성해줘 그 구현체를 런타임에 사용하는 것이기 때문에 앱 사이즈는 조금 커질수 있지만 성능상 크게 이슈가 없어서 ModelMapper보다 더 부담없이 사용하기 좋다. 그리고 아주 다양한 기능을 제공하기 때문에 조금더 세밀한 매핑이 가능해진다.

 

바로 간단하게 Mapstruct를 맛보자.

 

build.gradle

build.gradle를 채워넣어보자 !

buildscript {
	ext {
		mapstructVersion = '1.3.1.Final'
	}
}

...

// Mapstruct
implementation "org.mapstruct:mapstruct:${mapstructVersion}"
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
testAnnotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"

...

compileJava {
	options.compilerArgs = [
			'-Amapstruct.suppressGeneratorTimestamp=true',
			'-Amapstruct.suppressGeneratorVersionInfoComment=true'
	]
}

 

Java
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
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class CarDto {
    private String name;
    private String color;
}
 
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class Car {
    private String modelName;
    private String modelColor;
}
 
@Mapper
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(source = "color", target = "modelColor")
    Car to(CarDto carDto);
}
cs

 

사실 간단한 것이라, 어노테이션만 보아도 어떠한 기능을 제공하는지 확실하다. 하지만 각 클래스에 getter/setter 및 기본생성자는 꼭 생성해주자.(필드명이 동일할 일은 많이 없겠지만, 각 객체의 필드명이 동일하면 @Mapping 어노테이션은 생략가능하다.) 해당 코드로 테스트코드를 간단하게 작성하자.

 

1
2
3
4
5
6
7
8
9
10
11
public class MapstructTest {
 
    @Test
    public void test() {
        CarDto carDto = CarDto.of("bmw x4""black");
        Car car = CarMapper.INSTANCE.to(carDto);
 
        assertEquals(carDto.getName(), car.getModelName());
        assertEquals(carDto.getColor(), car.getModelColor());
    }
}
cs

 

테스트가 성공하는 것을 볼 수 있다. 아주 간단하면서도 파워풀한 라이브러리인 것 같다. 그러면 실제로 생성된 코드를 한번 볼까?

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Generated(
    value = "org.mapstruct.ap.MappingProcessor"
)
public class CarMapperImpl implements CarMapper {
 
    @Override
    public Car to(CarDto carDto) {
        if ( carDto == null ) {
            return null;
        }
 
        Car car = new Car();
 
        car.setModelName( carDto.getName() );
        car.setModelColor( carDto.getColor() );
 
        return car;
    }
}
cs

 

우리가 작성하지 않았지만, 코드가 생성되었다. 리플렉션과 같이 비용이 큰 기술이 사용되지 않았다. 간단한 validation 코드와 생성자, getter, setter 코드 뿐이다. 이제 더 많은 Mapstruct 기능들을 살펴보자.

 

하나의 객체로 합치기

여러 객체의 필드값을 하나의 객체로 합치기가 가능하다. 특별히 다른 옵션을 넣는 것은 아니다.

 

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
50
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class UserDto {
    private String name;
}
 
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class AddressDto {
    private String si;
    private String dong;
}
 
@Mapper
public interface UserInfoMapper {
 
    UserInfoMapper INSTANCE = Mappers.getMapper(UserInfoMapper.class);
 
    @Mapping(source = "user.name", target = "userName")
    @Mapping(source = "address.si", target = "si")
    @Mapping(source = "address.dong", target = "dong")
    UserInfo to(UserDto user, AddressDto address);
}
 
@Generated(
    value = "org.mapstruct.ap.MappingProcessor"
)
public class UserInfoMapperImpl implements UserInfoMapper {
 
    @Override
    public UserInfo to(UserDto user, AddressDto address) {
        if ( user == null && address == null ) {
            return null;
        }
 
        UserInfo userInfo = new UserInfo();
 
        if ( user != null ) {
            userInfo.setUserName( user.getName() );
        }
        if ( address != null ) {
            userInfo.setDong( address.getDong() );
            userInfo.setSi( address.getSi() );
        }
 
        return userInfo;
    }
}
cs

 

이미 생성된 객체에 매핑

새로운 인스턴스를 생성하는 것이 아니라, 기존에 이미 생성되어 있는 객체에 매핑이 필요한 경우이다.

 

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
@Mapper
public interface UserInfoMapper {
 
    UserInfoMapper INSTANCE = Mappers.getMapper(UserInfoMapper.class);
 
    @Mapping(source = "user.name", target = "userName")
    @Mapping(source = "address.si", target = "si")
    @Mapping(source = "address.dong", target = "dong")
    void write(UserDto user, AddressDto address, @MappingTarget UserInfo userInfo);
}
 
@Generated(
    value = "org.mapstruct.ap.MappingProcessor"
)
public class UserInfoMapperImpl implements UserInfoMapper {
 
    @Override
    public void write(UserDto user, AddressDto address, UserInfo userInfo) {
        if ( user == null && address == null ) {
            return;
        }
 
        if ( user != null ) {
            userInfo.setUserName( user.getName() );
        }
        if ( address != null ) {
            userInfo.setDong( address.getDong() );
            userInfo.setSi( address.getSi() );
        }
    }
}
 
cs

 

타입 변환

대부분의 암시적인 자동 형변환이 가능하다. (Integer -> String ...) 다음은 조금 유용한 기능이다.

1
2
3
4
5
6
7
8
9
@Mapper
public interface CarMapper {
 
    @Mapping(source = "price", numberFormat = "$#.00")
    CarDto carToCarDto(Car car);
 
    @IterableMapping(numberFormat = "$#.00")
    List<String> prices(List<Integer> prices);
}
cs

 

만약 클래스에 있는 List<Integer> 필드값을 List<String>의 다른 포맷으로 변경하고 싶다면 @IterableMapping 어노테이션을 이용하자. 위와 비슷하게 날짜 데이터를 문자열로 변환하는 dateFormat이라는 옵션도 존재한다.

 

Source, Target mapping policy

매핑될 필드가 존재하지 않을 때, 엄격한 정책을 가져가기 위한 기능을 제공한다.

 

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
ja@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class Car {
    private String modelName;
    private String modelColor;
    private String modelPrice;
    private String description;
}
 
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class CarDto {
    private String name;
    private String color;
    private Integer price;
}
 
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(source = "color", target = "modelColor")
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    Car to(CarDto carDto);
 
}
cs

 

위 코드는 타겟이 되는 오브젝트 필드에 대한 정책을 가져간다. Car 클래스에는 description 필드가 있는데, CarDto 클래스에는 해당 필드가 존재하지 않기때문에 컴파일시 컴파일에러가 발생한다.(ERROR, IGNORE, WARN 정책존재) 만약 특정 필드는 해당 정책을 피하고 싶다면 아래와 같이 어노테이션하나를 달아준다.

 

1
2
3
4
5
6
7
8
9
10
11
12
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(source = "color", target = "modelColor")
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    @Mapping(target = "description", ignore = true)
    Car to(CarDto carDto);
 
}
cs

 

null 정책

Source가 null이거나 혹은 Source의 특정 필드가 null일때 적용가능한 정책이 존재한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Mapper(
        unmappedTargetPolicy = ReportingPolicy.ERROR,
        nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT
)
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(source = "color", target = "modelColor")
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    @Mapping(target = "description", ignore = true)
    Car to(CarDto carDto);
 
}
cs

 

위 코드는 Source 오브젝트가 null일때, 기본생성자로 필드가 비어있는 Target 오브젝트를 반환해준다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Mapper(
        unmappedTargetPolicy = ReportingPolicy.ERROR,
        nullValueMappingStrategy = NullValueMappingStrategy.RETURN_NULL
)
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(source = "color", target = "modelColor")
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    @Mapping(
            source = "description"
            target = "description"
            ignore = true,
            nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT
    )
    Car to(CarDto carDto);
 
}
cs

 

위 코드는 각 필드에 대해 null 정책을 부여한다. 만약 SET_TO_DEFAULT로 설정하면, List 일때는 빈 ArrayList를 생성해주고, String은 빈문자열, 특정 오브젝트라면 해당 오브젝트의 기본 생성자 등으로 기본값을 생성해준다.

 

특정 필드 매핑 무시

특정 필드는 매핑되지 않길 원한다면 @Mapping 어노테이션에 ignore = true 속성을 넣어준다.

 

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
@Mapper(
        unmappedTargetPolicy = ReportingPolicy.ERROR,
        nullValueMappingStrategy = NullValueMappingStrategy.RETURN_NULL
)
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(source = "color", target = "modelColor")
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    @Mapping(target = "description", ignore = true)
    Car to(CarDto carDto);
 
}
 
public class MapstructTest {
 
    @Test
    public void test() {
        CarDto carDto = CarDto.of(
                "bmw x4",
                "black",
                10000,
                "description");
        Car car = CarMapper.INSTANCE.to(carDto);
        System.out.println(car.toString());
    }
}
 
result =>
 
Car(modelName=bmw x4, modelColor=black, modelPrice=$10000.00, description=null)
cs

 

매핑 전처리, 후처리

매핑하기 이전과 매핑 이후 특정 로직을 주입시킬 수 있다.

 

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
50
51
52
53
54
55
56
57
@Mapper(
        unmappedTargetPolicy = ReportingPolicy.ERROR,
        nullValueMappingStrategy = NullValueMappingStrategy.RETURN_NULL,
        componentModel = "spring"
)
public abstract class CarMapper {
 
    @BeforeMapping
    protected void setColor(CarDto carDto, @MappingTarget Car car) {
        if (carDto.getName().equals("bmw x4")) {
            car.setModelColor("red");
        } else {
            car.setModelColor("black");
        }
 
    }
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(target = "modelColor", ignore = true)
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    public abstract Car to(CarDto carDto);
 
    @AfterMapping
    protected void setDescription(@MappingTarget Car car) {
        car.setDescription(car.getModelName() + " " + car.getModelColor());
    }
}
 
<Generate Code>
 
@Generated(
    value = "org.mapstruct.ap.MappingProcessor"
)
@Component
public class CarMapperImpl extends CarMapper {
 
    @Override
    public Car to(CarDto carDto) {
        if ( carDto == null ) {
            return null;
        }
 
        Car car = new Car();
 
        setColor( carDto, car );
 
        car.setModelName( carDto.getName() );
        if ( carDto.getPrice() != null ) {
            car.setModelPrice( new DecimalFormat( "$#.00" ).format( carDto.getPrice() ) );
        }
        car.setDescription( carDto.getDescription() );
 
        setDescription( car );
 
        return car;
    }
}
cs

 

전처리와 후처리를 위한 메서드는 private을 사용해서는 안된다. 그 이유는 generate된 코드에 전,후 처리 메서드가 들어가는 것이 아니라 추상 클래스에 있는 메서드를 그대로 사용하기 때문이다.

 

이외에도 지원하는 기능이 너무 많다.. 다 못쓰겠다. 아래 공식 레퍼런스를 살펴보자..

 

 

MapStruct 1.3.1.Final Reference Guide

The mapping of collection types (List, Set etc.) is done in the same way as mapping bean types, i.e. by defining mapping methods with the required source and target types in a mapper interface. MapStruct supports a wide range of iterable types from the Jav

mapstruct.org

 

posted by 여성게
:
인프라/네트워크(기초) 2020. 4. 22. 23:41

 

오늘 다루어볼 내용은 DNS Record type중에 A record와 CNAME의 차이점을 간단하게 다루어본다.

 

A record

DNS의 레코드 타입중에 A record type이란 간단하게 도메인(domain) name에 IP Address를 매핑하는 방법이다.

 

> nslookup coding-start.tistory.com
Server:		10.20.30.60
Address:	10.20.30.60#53

Non-authoritative answer:
Name:	coding-start.tistory.com
Address: 211.231.99.250

 

위 nslookup <domain> 명령을 치면 211.231.99.250이라는 IP가 매핑되어 있는 것을 볼 수 있다. IP 매핑은 VIP로 매핑하여 여러 IP를 하나의 도메인에 매핑할 수도 있고, A 타입 레코드에 각 서버의 IP를 여러개 넣을 수도 있다.

 

#VIP?
VIP는 하나의 호스트에 여러 개의 IP주소를 할당하는 기술이다. 이 기술을 이용하면, 
하나의 네트워크 인터페이스에 여러 개의 IP 주소를 줄 수 있다. 
바깥에서는 마치 하나 이상의 네트워크 인터페이스가 있는 것으로 보일 것이다. 
VIP는 흔히 HA나 로드밸런싱을 위해서 널리 사용된다.

vip : 211.231.99.250
ip list : a.b.c.d, d.a.e.s, d.e.a.h, d.e.a.a ...

 

CNAME(Canonical Name)

 

Canonical Name의 줄임말로 하나의 도메인에 도메인 별칭을 부여하는 방식이다. 즉, 도메인의 또 다른 도메인 이름으로 생각하면 좋을 것 같다.

 

coding-start.com -> coding-start.tistory.com (CNAME)
coding-start.tistory.com -> 211.231.99.250 (A record)

 

A record는 직접적으로 IP가 할당되어 있기 때문에 IP가 변경되면 직접적으로 도메인에 영향을 미치지만, CNAME은 도메인에 도메인이 매핑되어 있기 때문에 IP의 변경에 직접적인 영향을 받지 않는다.

 

여기까지 간단하게 A record와 CNAME의 차이점을 알아보았고, 혹시나 DNS record의 여러가지 type을 보고 싶다면 아래 위키 페이지를 참조하자.

 

 

List of DNS record types - Wikipedia

From Wikipedia, the free encyclopedia Jump to navigation Jump to search This list of DNS record types is an overview of resource records (RRs) permissible in zone files of the Domain Name System (DNS). It also contains pseudo-RRs. Resource records[edit] Ty

en.wikipedia.org

 

posted by 여성게
:
인프라/Docker&Kubernetes 2020. 4. 21. 11:43

이번 포스팅에서 다루어볼 내용은 DOOD로 도커를 띄웠을 때, proxy 설정하는 방법이다. 그전에 간단하게 Docker in Docker(dind)와 Docker Out Of Dcoker(DooD)에 대해 알아보자.

 

Docker in Docker(dind)

도커 내부에 격리된 Docker 데몬을 실행하는 방법이다. CI(Jenkins docker agent) 측면에서 접근하면 Agent가 Docker client와 Docker Daemon 역할 두가지를 동시에 하게 된다. 하지만 이 방법은 단점이 존재한다. 내부의 도커 컨테이너가 privileged mode로 실행되어야 한다.

 

> docker run --privileged --name dind -d docker:1.8-dind

 

privileged 옵션을 사용하면 모든 장치에 접근할 수 있을뿐만 아니라 호스트 컴퓨터 커널의 대부분의 기능을 사용할 수 있기 때문에 보안에 좋지않은 방법이다. 하지만 실제로 실무에서 많이 쓰는 방법이긴하다.

 

Docker out of Docker(dood)

Docker out of Docker는 호스트 머신에서 동작하고 있는 Docker의 Docker socket과 내부에서 실행되는 Docker socket을 공유하는 방법이다. 간단하게 볼륨을 마운트하여 두 Docker socket을 공유한다.

 

> docker run -v /var/run/docker.sock:/var/run/docker.sock ...

 

이 방식을 그나마 Dind보다는 권장하고 있는 방법이긴하다. 하지만 이 방식도 단점은 존재한다. 내부 도커에서 외부 호스트 도커에서 실행되고 있는 도커 컨테이너를 조회할 수 있고 조작할 수 있기 때문에 보안상 아주 좋다고 이야기할 수는 없다.

 

DooD proxy 설정

dood로 도커를 띄운 경우, 호스트 머신에 동작하고 있는 도커에 proxy 설정이 되어있어야 내부 도커에도 동일한 proxy 설정을 가져간다.

 

방법1. /etc/sysconfig/docker에 프록시 설정
> sudo vi /etc/sysconfig/docker
HTTP_PROXY="http://proxy-domain:port"
HTTPS_PROXY="http://proxy-domain:port"

#docker restart
> service docker restart

 

방법2. 환경변수 설정
> mkdir /etc/systemd/system/docker.service.d
> cd /etc/systemd/system/docker.service.d
> vi http-proxy.conf

[Service]
Environment="HTTP_PROXY=http://proxy-domain:port"
Environment="HTTPS_PROXY=http://proxy-domain:port"
Environment="NO_PROXY=hostname.example.com, 172.10.10.10"

> systemctl daemon-reload
> systemctl restart docker

> systemctl show docker --property Environment
Environment=GOTRACEBACK=crash HTTP_PROXY=http://proxy-domain:port HTTPS_PROXY=http://proxy-domain:port NO_PROXY= hostname.example.com,172.10.10.10

 

여기까지 dood로 도커 엔진을 띄웠을 때, proxy 설정하는 방법이다.

 

 

참고

 

How to configure docker to use proxy – The Geek Diary

 

www.thegeekdiary.com

 

posted by 여성게
:

오늘 다루어볼 내용은 자바에서 Stream(java 8 stream, reactor ...)을 사용할때 유용한 팁이다. 많은 사람들이 아는 해결법일 수도 있고, 혹은 필자와 같은 스타일을 선호하지 않는 사람들도 있을 것이다. 하지만 필자가 개발할때 이러한 상황에서 조금 유용했던 Stream pipeline Tip을 간단히 소개한다.

 

중첩이 많고, 이전 스트림보다 더 이전의 스트림의 결과 값을 사용해야 할때

상황은 아래와 같은데, 간단히 바로 이전 스트림의 결과가 아닌, 더 전의 스트림 원자를 로직에서 사용하려면 대게 아래와 같이 스트림 파이프 라인을 이어나간다.

 

Mono.just("id")
	.flatMap(id -> 
		return Mono.just(service.getById(id))
				.map(entity -> {
					...
				})
	)

 

파이프라인의 시작인 id 값을 파이프라인의 중간에서 사용하려면 Stream의 pipeline을 점점 안으로 중첩해 나가면서 사용해야한다. 이렇게 된다면 복잡한 로직일 수록 점점 안쪽으로 파고드는 파이프라인이 될 것이다. 그렇다면 훨씬 가독성 좋은 코드는 어떻게 작성해 볼 수 있을까?

 

Tuple을 사용해서 넘겨주자.

reactor에 있는 Tuple을 사용해서 이전 파이프라인의 값을 뒤로 넘겨줘보자.

 

void stream() {
	Mono.just("id")
		.flatMap(str -> {
			...
			return Mono.just(Tuples.of(str, "str2"));
		})
		.flatMap(tuple -> {
			final String str = tuple.getT1();
			...
			return Mono.just("result"); 
		})
}

 

위처럼 튜플을 이용하여 해당 스트림의 결과와 이전 스트림의 결과가 나중에 쓰일 수 있도록 튜플에 값을 넣어서 넘겨준다. 이렇게 파이프라이닝하면 같은 depth로 파이프라인을 이어나갈 수 있기 때문에 가독성이 훨씬 좋다. 만약 튜플을 사용하지 않았다면 점점 depth가 깊어지는 중첩 파이프라인을 이어나가야하기 때문에 가독성도 훨씬 떨어지게 될 것이다.

 

여기까지 중첩이 많고, 이전 스트림의 결과를 미래 파이프라인에서 사용할 때 이용할 수 있는 팁이었다.

posted by 여성게
:
인프라/Docker&Kubernetes 2020. 4. 20. 21:48

공식 dockerhub가 아닌, 개인 혹은 사내 dockerhub로 push하는 방법이다. (dockerHubHost가 개인 혹은 사내 도커허브 도메인)

 

> docker build -t dockerHubHost/levi.yoon/jenkins_example
> docker push dockerHubHost/levi.yoon/jenkins_example

 

뭔가 다른 방법이 있을 것 같긴한대.. 간단하게 위 방법으로도 가능하다 !

posted by 여성게
: