오늘 다루어볼 내용은 jdk1.8의 날짜&시간을 다루는 java.time 패키지를 다루어볼 것이다. 바로 java.time 패키지 내용을 다루기 전에 우선 프로그래밍에서의 날짜와 시간에 대해 표준인 ISO-8601에 대해 먼저 알아본다.

 

ISO-8601

<wiki>
ISO 8601 Data elements and interchange formats - Information interchange - Representation of dates and times은 날짜와 시간과 관련된 데이터 교환을 다루는 국제 표준이다. 이 표준은 국제 표준화 기구(ISO)에 의해 공포되었으며 1988년에 처음으로 공개되었다. 이 표준의 목적은 날짜와 시간을 표현함에 있어 명백하고 잘 정의된 방법을 제공함으로써, 날짜와 시간의 숫자 표현에 대한 오해를 줄이고자함에 있는데, 숫자로 된 날짜와 시간 작성에 있어 다른 관례를 가진 나라들간의 데이터가 오갈때 특히 그렇다.

일반적으로, ISO 8601는 그레고리력 (proleptic Gregorian도 가능)에서의 날짜와 (부가적으로 시간대 정보를 포함하는) 24시간제에 기반하는 시간, 시간 간격(time interval) 그리고 그들의 조합에 대한 표현과 형식에 적용된다. 이 표준은 표현할 날짜/시간 요소에 어떠한 특정 의미도 할당하지 않는다; 그 의미는 사용 맥락에 따라 달라질 것이다. 추가로, 표현될 날짜와 시간은 표준 내에서의 지정된 의미의 숫자(예를 들자면, 중국 달력의 년도 이름)가 아니고서는 단어를 포함할 수 없으며 단어들은 문자(예: 이미지, 소리)를 사용하지 않는다.

교환을 위한 표현에서, 날짜와 시간은 재배치되어서, 가장 큰 시간 용어(년도)가 왼쪽에 놓이며 각각의 더 작은 용어들은 이전 용어의 우측에 놓이게 된다. 표현은 아라비아 숫자와 표준 내에서 특정 의미를 제공하는 ("-", ":", "T", "W" 그리고 "Z"와 같은) 어떤 문자들로 작성되어야 한다. 그것이 의미하는 바는, "January" 혹은 "Thursday"처럼 날짜의 일부를 작성하는 어떤 평범한 방법이 교환 표현에서는 허용되지 않는다는 것이다.

참조 : https://ko.wikipedia.org/wiki/ISO_8601

 

위 내용은 위키 내용을 인용하였는데, 한마디로 세계에서 각기 다른 시간대에서 사용하기 위한 날짜&시간에 대한 표준을 정해놓은 것이다. 포맷으로는 보통 아래와 같은 포맷을 다룬다.(물론 기본형식이 있지만 아래 확장 형식을 대부분 사용하는 듯하다.)

 

- 날짜(년월일) : YYYY-MM-DD
- 날짜(년월) : YYYY-MM
- 날짜&시간 : YYYY-MM-DDThh:mm:ss(YYYY-MM-DDThh:mm:ss.sss)
- 시간 : hh:mm:ss(hh:mm:ss.sss)

 

날짜와 시간을 다루는데 아주 중요한 것중 하나는 "표준 시간대 지정자"이다. ISO-8601의 표준 시간대는 (불특정 위치의) "지역 시간"(local time), "UTC" 혹은 "UTC의 오프셋"으로 표현된다.

 

만약 UTC 관계 정보에 시간 표현이 함께 주어지지 않는다면, 시간은 지역 시간으로 간주된다. 동일한 시간대에서 통신 시 지역 시간을 가정하는 것이 가장 안전할지 몰라도, 시간대가 다른(국가와 국가간 시차) 지역간의 통신에서 지역시간을 사용하는 경우는 아주 모호하게 된다.

(UTC 오프셋이 표현되지 않는다면 해당 시간이 어느나라 기준의 시간인지 알수 없기에 각자 나라의 시간대로 표현이 불가능하다.)

 

UTC

시간이 UTC인 경우, 시간 뒤에 빈칸없이 "Z" 를 직접 추가해야 한다. Z는 오프셋이 0인 UTC를 위한 지역 지정자이다. 그러므로 "09:30:12"의 UTC는 "09:30:12Z"로 표현된다. 

 

UTC에서의 시간 오프셋

UTC에서의 오프셋은 위에서 Z를 붙였던 것과 동일한 방법으로 시간뒤에 덧붙인다. 우리나라는 기준시보다 9시간이 빠른 나라이기 때문에 시간을 UTC 오프셋으로 표현하게 되면 "09:30:12+09:00"으로 표현하게 된다. 이렇게 UTC 오프셋으로 시간을 표현하게 되면 기준시에 오프셋이 붙어있는 것이기 때문에 해당 시간으로 다른 시간대의 나라의 시간으로도 표현이 명확히 가능하게 된다.(느린 시간대라면 -로 오프셋을 표현한다.)

 

java.time 패키지의 핵심 클래스

날짜와 시간을 하나로 표현하는 Calendar클래스와 달리, java.time 패키지에서는 날짜와 시간을 별도의 클래스로 분리해 놓았다. 시간을 표현할 때는 LocalTime 클래스를 사용하고, 날짜를 표현할 때는 LocalDate클래스를 사용한다. 그리고 날짜와 시간이 모두 필요할 때는 LocalDateTime클래스를 사용하면 된다. 만약 여기에 Time-Zone까지 다뤄야 한다면, ZonedDateTime클래스를 사용한다.

 

LocalDateTime은 기본적으로 Time-zone이 없는 형태이다. 그 말은 ZoneOffset을 표기 하지 않는 날짜 형식을 출력해준다. 물론 ZoneOffset을 고려해서 LocalDateTime 오브젝트를 만들수 있다. 하지만 Time-zone이 없는 개념이기 때문에, 아래와 같이 출력이 된다.

 

LocalDateTime.now() - 2020-07-30T00:38:55.215245
ZonedDateTime.now() - 2020-07-30T00:38:55.215245+09:00[Asia/Seoul]

 

Jdk1.8 이전의 날짜&시간을 다루는 Calendar는 ZonedDateTime처럼, 날짜와 시간 그리고 시간대까지 모두 가지고 있다. Date와 유사한 클래스로는 Instant가 있는데, 이 클래스는 날짜와 시간을 초 단위(엄밀히는 나노초까지)로 표현한다. 날짜와 시간을 초단위로 표현한 값을 time stamp라고 부르는데, 이 값은 날짜와 시간을 하나의 정수로 표현할 수 있으므로 날짜와 시간의 차이를 계산하거나 순서를 비교하는데 유리해서 데이터 베이스에서 많이 사용한다.(타임스탬프는 타임존에 대한 정보없이 절대적인 시간을 다루기 때문에 아주 명확한 값을 가지게 된다.) 이외에도 날짜를 더 세부적으로 다룰 수 있는 Year, YearMonth, MonthDay와 같은 클래스도 있다.

 

예제 코드는 millisecond는 조금 다르지만 같은 시간이라 가정하자.

LocalDateTime.now() 
-> 2020-07-30T01:04:38.488822

LocalDateTime.ofInstant(Instant.ofEpochMilli(System.currentTimeMillis()), ZoneId.systemDefault())
-> 2020-07-30T01:04:38.490

LocalDateTime.ofInstant(Instant.ofEpochMilli(System.currentTimeMillis()), ZoneId.of("Asia/Seoul"))
-> 2020-07-30T01:04:38.490

LocalDateTime.ofInstant(Instant.ofEpochSecond(System.currentTimeMillis() / 1000), ZoneId.systemDefault())
-> 2020-07-30T01:04:38

LocalDateTime.ofInstant(Instant.ofEpochSecond(System.currentTimeMillis() / 1000), ZoneId.of("Asia/Seoul"))
-> 2020-07-30T01:04:38

LocalDateTime.ofInstant(Instant.ofEpochSecond(System.currentTimeMillis() / 1000), ZoneId.systemDefault()).plusDays(7)
-> 2020-08-06T01:04:38

LocalDateTime.ofEpochSecond(System.currentTimeMillis() / 1000, 0, ZoneOffset.UTC)
-> 2020-07-29T16:04:38

LocalDateTime.ofEpochSecond(System.currentTimeMillis() / 1000, 0, ZoneOffset.ofHours(9))
-> 2020-07-30T01:04:38

ZonedDateTime.now()
-> 2020-07-30T01:04:38.491441+09:00[Asia/Seoul]

LocalDateTime.now().atZone(ZoneId.of("Asia/Seoul"))
-> 2020-07-30T01:04:38.491568+09:00[Asia/Seoul]

 

작성중...

posted by 여성게
:

리액터의 switchIfEmpty라는 메서드를 다루기 전에 자바의 Lazy evaluation(지연 평가)에 대해 다루어보자. 자바는 논리 operation을 평가할때 lazy evaluation을 사용한다. 예제 코드를 예로 들면 아래와 같다.

 

@Test
void lazyEvaluationTest() {
    boolean isLazy = lazyEvaluation();
    if (true || isLazy) {
        System.out.println("method execute!!!");
    }
}
private boolean lazyEvaluation() {
    System.out.println("lazy evaluation");
    return true;
}

 

위 테스트에서 결과는 어떻게 될 것인가?

 

lazy evaluation
method execute!!!

 

위와 같이 사용하지 않는 boolean 변수이지만, 미리 isLazy를 만들기 위해 메서드를 호출한다. 여기서 사용하지 않는 변수라는 뜻은 자바에서는 논리 operation에서 이미 참 혹은 거짓이라고 판단이 난 논리식이면 나머지 값 자체를 참조하지 않는다. 이 예는 위와 같이 true || isLazy일때, 이미 앞의 true에서 참이 되었기에 뒤의 isLazy 자체를 참조하지 않는다. 이것을 테스트 하기 위해 아래와 같이 코드를 만들어보았다.

 

@Test
void lazyEvaluationTest() {
    if (true || lazyEvaluation()) {
        System.out.println("method execute!!!");
    }
}

private boolean lazyEvaluation() {
    System.out.println("lazy evaluation");
    return true;
}

 

위 예제코드에서는 lazyEvaluation()를 호출자체를 하지 않는다. 그렇다면 아래와 같은 코드는 결과가 어떻게 될까?

 

@Test
void lazyEvaluationTest() {
    System.out.println("before boolean operation");
    if (true && lazyEvaluation()) {
        System.out.println("method execute!!!");
    }
}

private boolean lazyEvaluation() {
    System.out.println("lazy evaluation");
    return true;
}

 

결과는 아래와 같다.

 

before boolean operation
lazy evaluation
method execute!!!

 

실제 lazyEvaluation()가 필요한 시점에 호출하게 되는 것이다. 여기까지 자바에서 논리 operation은 lazy evaluation 한다는 것은 알았고, 다른 상황에서는 어떻게 될까?

 

자바는 어떠한 지역변수에 값을 할당할때, 그리고 어떠한 메서드의 매개변수를 만들때는 eager evaluation 전략을 따른다. 그 말은 무엇이냐면 어떠한 메서드를 처리하기 전에 그 메서드의 매개변수의 값이 미리 준비가 되어 있어야한다는 것이다. 아래 예제코드를 한번 살펴보자.

 

@Test
void eagerEvaluationTest() {
    System.out.println(convertBooleanToString(eagerEvaluation(true)));
}

private String convertBooleanToString(final boolean bool) {
    System.out.println("convertBooleanToString");
    return bool ? "true" : "false";
}

private boolean eagerEvaluation(final boolean bool) {
    System.out.println("eager evaluation!!!!");
    return bool;
}

 

위 메서드의 결과는 어떻게 될까?

 

eager evaluation!!!!
convertBooleanToString
true

 

convertBooleanToString을 실행시키기전에 매개변수의 값을 미리 만들기 위해서 eagerEvaluation 메서드를 호출해 값을 만든다. 그렇다면 lazy evaluation하게 하려면 어떻게 하면 될까?

 

@Test
void lazyEvaluationTest2() {
    System.out.println(convertBooleanToString(() -> lazyEval(true)));
}

private String convertBooleanToString(final Supplier<Boolean> f) {
    System.out.println("convertBooleanToString");
    return f.get() ? "true" : "false";
}

private boolean lazyEval(final boolean bool) {
    System.out.println("lazy evaluation");
    return bool;
}

 

위와 같이 매개변수로 람다를 전달하면 된다. 람다를 전달하게 되면 해당 람다가 사용되는 시점에 메서드를 호출하기 때문에 lazy하게 프로그래밍하게 할 수 있다. 이것은 꼭 실행하지 않아도될 로직을 처리하지 않게 할 수 있고, 혹은 실행 시점을 뒤로 미룰 수도 있기 때문에 알고 있으면 아주 좋은 개념이 될것이다. 위 예제를 결과는 아래와 같다.

 

convertBooleanToString
lazy evaluation
true

 

실행 순서가 바뀐 것을 볼 수 있다. 그리고 매서드의 매개변수로 담기는 메서드의 lazy 로딩하는 것 외에 지역변수의 초기화를 지연 시킬 수 도 있다.

 

@Test
void lazyEvaluationTest3() {
    Supplier<Boolean> supplier = () -> lazyEval(true);
    System.out.println("before method");
    if (supplier.get()) {
        System.out.println("method !!");
    }
}

 

여기까지 자바의 eager, lazy evaluation을 다루어보았고, 이제 본론으로 switchIfEmpty를 사용할때 주의해야할 점을 알아보자 !

 

@Test
void switchIfEmptyTest() {
    Mono.just("str")
            .map(s -> {
                System.out.println(s);
                return s;
            })
            .switchIfEmpty(defaultStr())
            .subscribe();
}

private Mono<String> defaultStr() {
    System.out.println("defalutStr");
    return Mono.just("default");
}

<console>
defalutStr
str

 

위 코드를 보면 우리는 보통 Mono.just가 Mono.empty를 리턴하면 switchIfEmpty를 실행하겠지? 라는 생각을 하기 쉽다. 하지만, 실제 동작은 그렇지 않다. 자바에서는 보통 메서드의 매개변수의 값을 미리 결정시켜놓으려한다.(eager evaluation) 그래서 실제 map을 실행하기 전에 switchIfEmpty를 먼저 실행시켜 값을 만들어 놓는다. 만약 switchIfEmpty안에 실행되는 메서드가 비용이 크고, 실제로 Mono.just는 empty를 반환하지 않는다면, 굳이 실행되지 않아도 되는 비싼 비용의 메서드를 호출해야한다. 이럴때는 우리는 lazy evaluation 전략을 사용하여 switchIfEmpty안의 실행을 실제 필요시점으로 미룰 수 있게 하는 것이다. 

 

@Test
void switchIfEmptyTest2() {
    Mono.just("str")
            .map(s -> {
                System.out.println(s);
                return s;
            })
            .switchIfEmpty(Mono.defer(this::lazyDefaultStr))
            .subscribe();
}

@Test
void switchIfEmptyTest3() {
    Mono.just("str")
            .map(s -> {
                System.out.println(s);
                return s;
            })
            .switchIfEmpty(Mono.fromSupplier(() -> "defaultStr"))
            .subscribe();
}

private Mono<String> lazyDefaultStr() {
    System.out.println("defalutStr");
    return Mono.just("default");
}

<console>
str

 

위와 같이 Mono.defer 혹은 Mono.fromSupplier를 사용하면 해당 메서드의 매개변수로 Supplier를 넘기기 때문에 미리 값을 만들어 놓지 않고, 실제 호출되는 시점으로 실행을 지연시킬 수 있다. 위 코드에서는 아예 메서드 호출자체를 하지도 않는다. 필자도 지금까지는 이러한 것을 크게 인지하지 못하고 코딩을 했었는데, 이렇게 lazy programming을 조금 생각하고 코딩하게 된다면 조금이라도 성능향상을 할 수 있지 않을까 생각이 든다.

posted by 여성게
:
Middleware/Kafka&RabbitMQ 2020. 7. 26. 17:32

 

 

yoonyeoseong/rabbitmq-sample

Contribute to yoonyeoseong/rabbitmq-sample development by creating an account on GitHub.

github.com

 

오늘 포스팅할 내용은 래빗엠큐이다. 그 동안에는 카프카를 사용할 일이 많아 카프카에 대한 포스팅이 주였는데, 이번에 래빗엠큐를 사용할 일이 생겨 간단히 래빗엠큐에 대해 간단히 다루어 볼것이다.(예제 코드는 위 깃헙에 올려놓았습니다.)

 

비동기 작업에 있어 큐를 사용하려면 중간에 메시지 브로커라는 개념이 존재하는데, 이러한 메시지 브로커에는 RabbitMQ, Kafka 등이 대표적으로 있다. 해당 포스트에서는 표준 MQ프로토콜인 AMQP를 구현한 RabbitMQ(래빗엠큐)에 대해 다루어볼 것이다.

 

간단하게 메시지큐는 아래 그림과 같은 워크 플로우로 이루어져있다. 

 

 

대부분의 메시지큐는 프로듀서가 있고, 해당 프로듀서가 브로커로 메시지를 발행하면, 적절한 컨슈머가 해당 메시지를 구독(읽다)하는 구조이다. 그렇다면 래빗엠큐는 상세하게 어떠한 구조로 되어있을까?

 

래빗엠큐는 단순히 프로듀서가 브로커로 메시지를 전달하여 컨슈머가 큐를 읽어가는 구조라는 면에서는 동일하지만, 프로듀싱하는 과정에서 조금 더 복잡한 개념이 들어간다. 바로 exchange와 route, queue라는 개념이다.

 

간단하게 워크 플로우를 설명하자면 아래와 같다.

 

  1. Producer는 Message를 Exchange에게 보내게 됩니다.
    1. Exchange를 생성할때 Exchange의 Type을 정해야 합니다.
  2. Exchange는 Routing Key를 사용하여 적절한 Queue로 Routing을 진행합니다.
    1. Routing은 Exchange Type에 따라 전략이 바뀌게 됩니다.
  3. Exchange - Queue와 Binding이 완료된 모습을 볼 수 있습니다.
    1. Message 속성에 따라 적절한 Queue로 Routing이 됩니다.
  4. Message는 Consumer가 소비할때까지 Queue에 대기하게 됩니다.
  5. Consumer는 Message를 소비하게 됩니다.

 

위에서 1번에 Exchange라는 개념이 등장하는데, Exchange는 정해진 규칙으로 메시지를 라우팅하는 기능을 가지고 있다. 여기서 정해진 규칙은 크게 4가지가 존재하는데, 규칙들은 아래와 같다.

 

exchange routeing 전략

  • Direct Exchange
    • Message의 Routing Key와 정확히 일치하는 Binding된 Queue로 Routing
  • Fanout Exchange
    • Binding된 모든 Queue에 Message를 Routing
  • Topic Exchange
    • 특정 Routing Pattern이 일치하는 Queue로 Routing
  • Headers Exchange
    • key-value로 정의된 Header 속성을 통한 Routing

 

 

 

익스체인지는 위와 같은 전략으로 메시지를 라우팅하게 된다. 그리고 자주 사용되는 Exchange의 옵션으로는 아래와 같이 있다.

 

exchange options
  • Durability
    • 브로커가 재시작 될 때 남아 있는지 여부
    • durable -> 재시작해도 유지가능
    • transient -> 재시작하면 사라집니다.
  • Auto-delete
    • 마지막 Queue 연결이 해제되면 삭제

마지막으로 래빗엠큐에서 사용되는 용어가 있다.

 

  • Vhost(virutal host)
    • Virtual Host를 통해서 하나의 RabbitMQ 인스턴스 안에 사용하고 있는 Application을 분리할 수 있습니다.
  • Connection
    • 물리적인 TCP Connection, HTTPS -> TLS(SSL) Connection을 사용
  • Channel
    • 하나의 물리적인 Connection 내에 생성되는 가상의 Connection
    • Consumer의 process나 thread는 각자 Channel을 통해 Queue에 연결 될 수 있습니다.

 

여기까지 간단하게 래빗엠큐의 동작과 용어에 대해서 다루어봤으니 실제 코드 레벨로 실습을 진행해본다.

 

docker rabbitmq 설치
sudo docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 \
	--restart=unless-stopped -e RABBITMQ_DEFAULT_USER=username \
    -e RABBITMQ_DEFAULT_PASS=password rabbitmq:management

 

위처럼 도커로 래빗엠큐를 설치해주고, http://localhost:15672(username/password)로 접속해보자.

 

springboot rabbitmq 예제

build.gradle

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

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

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-amqp'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    testImplementation 'org.springframework.amqp:spring-rabbit-test'
}

test {
    useJUnitPlatform()
}

 

application.yml

rabbitmq:
  test:
    username: username
    password: password
    host: localhost
    port: 5672
    virtualHost: levi.vhost
    routeKey: test.route
    exchangeName: test.exchange
    queueName: testQueue
    deadLetterExchange: dead.letter.exchange
    deadLetterRouteKey: dead.letter.route

 

application.yml에서 프로퍼티를 읽어오는 클래스를 아래 하나 만들었다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
@ConfigurationProperties(prefix = "rabbitmq")
@Getter
@Setter
public class RabbitMqProperty {
    private RabbitMqDetailProperty test;
 
    @Getter
    @Setter
    public static class RabbitMqDetailProperty {
        private String username;
        private String password;
        private String host;
        private int port;
        private String virtualHost;
        private String routeKey;
        private String exchangeName;
        private String queueName;
        private String deadLetterExchange;
        private String deadLetterRouteKey;
    }
}
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
@SpringBootApplication(exclude = {
        RabbitAutoConfiguration.class
})
@EnableRabbit
@RestController
@RequiredArgsConstructor
public class RabbitmqApplication {
    private final RabbitTemplate testTemplate;
 
    public static void main(String[] args) {
        SpringApplication.run(RabbitmqApplication.class, args);
    }
 
    @GetMapping("/")
    public String inQueue() {
        testTemplate.convertAndSend(SampleMessage.of("message!!"));
        return "success";
    }
 
    @Data
    @AllArgsConstructor(staticName = "of")
    @NoArgsConstructor
    public static class SampleMessage {
        private String message;
    }
}
cs

 

래빗엠큐 auto configuration을 꺼주었고, 메시지를 인큐하기 위한 컨트롤러를 하나 만들어 주었다.

 

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
58
59
60
@Slf4j
@Configuration
@RequiredArgsConstructor
public class RabbitMqConfig {
    private final RabbitMqProperty rabbitMqProperty;
 
    @Bean
    public ConnectionFactory testConnectionFactory() {
        final RabbitMqProperty.RabbitMqDetailProperty test = rabbitMqProperty.getTest();
        final CachingConnectionFactory factory = new CachingConnectionFactory();
        factory.setHost(test.getHost());
        factory.setPort(test.getPort());
        factory.setUsername(test.getUsername());
        factory.setPassword(test.getPassword());
        factory.setVirtualHost(test.getVirtualHost());
 
        return factory;
    }
 
    @Bean
    public SimpleRabbitListenerContainerFactory testContainer(final ConnectionFactory testConnectionFactory) {
        final SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(testConnectionFactory);
        factory.setConnectionFactory(testConnectionFactory);
        factory.setErrorHandler(t -> log.error("ErrorHandler = {}", t.getMessage()));
        factory.setDefaultRequeueRejected(false); //true일 경우 리스너에서 예외가 발생시 다시 큐에 메시지가 쌓이게 된다.(예외상황 해결안될 시 무한루프)
        factory.setMessageConverter(jacksonConverter());
        //예외 발생시 recover할 수 있는 옵션, 위 에러 핸들러와 잘 구분해서 사용해야할듯
//위 핸들러와 적용 순서등도 테스트가 필요(혹은 둘다 동시에 적용되나?)
//        factory.setAdviceChain(
//                RetryInterceptorBuilder
//                .stateless()
//                .maxAttempts(3) //최대 재시도 횟수
//                .recoverer() //recover handler
//                .backOffOptions(2000, 4, 10000) //2초 간격으로, 4번, 최대 10초 내에 재시도
//                .build()
//        );
        return factory;
    }
 
    @Bean
    public RabbitTemplate testTemplate(final ConnectionFactory testConnectionFactory, final MessageConverter jacksonConverter) {
        final RabbitMqProperty.RabbitMqDetailProperty property = rabbitMqProperty.getTest();
        final RabbitTemplate template = new RabbitTemplate();
        template.setConnectionFactory(testConnectionFactory);
        template.setMessageConverter(jacksonConverter);
        template.setExchange(property.getExchangeName());
        template.setRoutingKey(property.getRouteKey());
        return template;
    }
 
    @Bean
    public MessageConverter jacksonConverter() {
        return new Jackson2JsonMessageConverter();
    }

//app에서 큐만들거나 바인딩 하기 위해서는 RabbitAdmin 필요
    @Bean
    public RabbitAdmin testAdmin(final ConnectionFactory testConnectionFactory) {
        return new RabbitAdmin(testConnectionFactory);
    }
}
cs

 

브로커 연결 및 메시지를 프로듀싱하기 위한 RabbitTemplate 설정이다. 이중 RabbitAdmin 빈을 만들고 있는 수동이 아니라, 앱에서 큐를 만들고 바인딩 하기 위해서는 RabbitAdmin이 빈으로 떠있어야한다. 기타 설정은 주석을 참고하자 !

 

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
@Configuration
@RequiredArgsConstructor
public class RabbitMqBindingConfig {
    private final RabbitMqProperty rabbitMqProperty;
 
    @Bean
    public TopicExchange testTopicExchange() {
        return ExchangeBuilder
                .topicExchange(rabbitMqProperty.getTest().getExchangeName())
                .durable(true)
                .build();
    }
 
    @Bean
    public TopicExchange dlExchange() {
        return ExchangeBuilder
                .topicExchange(rabbitMqProperty.getTest().getDeadLetterExchange())
                .durable(true)
                .build();
    }
 
    @Bean
    public Queue testQueue() {
        final RabbitMqProperty.RabbitMqDetailProperty testDetail = rabbitMqProperty.getTest();
        return QueueBuilder
                .durable(testDetail.getQueueName())
                .deadLetterExchange(testDetail.getDeadLetterExchange())
                .deadLetterRoutingKey(testDetail.getDeadLetterRouteKey())
                .build();
    }
 
    @Bean
    public Queue dlq() {
        return QueueBuilder
                .durable("DEAD_LETTER_QUEUE")
                .build();
    }
 
    @Bean
    public Binding testBinding(final Queue testQueue, final TopicExchange testTopicExchange) {
        return BindingBuilder
                .bind(testQueue)
                .to(testTopicExchange)
                .with(rabbitMqProperty.getTest().getRouteKey());
    }
 
    @Bean
    public Binding dlBinding(final Queue dlq, final TopicExchange dlExchange) {
        return BindingBuilder
                .bind(dlq)
                .to(dlExchange)
                .with(rabbitMqProperty.getTest().getDeadLetterRouteKey());
    }
}
cs

 

 

프로듀싱과 컨슈밍을 위해 브로커에 exchange와 queue를 binding하는 설정이다. 하나 추가적으로는 컨슈밍에서 예외가 발생하였을 때, Recover를 위한 Dead Letter Queue를 선언하였다.

 

이제 앱을 실행시키고, localhost:8080으로 요청을 보내보자 ! 요청을 보내면 메시지를 프로듀싱 할것이고, 해당 메시지를 컨슈밍해서 로그를 찍게 될 것이다. 여기까지 간단하게 래빗엠큐에 대해 다루어봤다.

 

 

 

https://jonnung.dev/rabbitmq/2019/02/06/about-amqp-implementtation-of-rabbitmq/

 

조은우 개발 블로그

 

jonnung.dev

https://velog.io/@hellozin/Spring-Boot%EC%99%80-RabbitMQ-%EC%B4%88%EA%B0%84%EB%8B%A8-%EC%84%A4%EB%AA%85%EC%84%9C

 

Spring Boot와 RabbitMQ 초간단 설명서

이번 포스트에서는 Spring boot 프로젝트에서 RabbitMQ를 사용하는 간단한 방법을 알아보겠습니다. Consumer 코드와 Producer 코드는 GitHub에 있습니다. 먼저 RabbitMQ 서버를 실행해야 하는데 Docker를 사용하�

velog.io

https://nesoy.github.io/articles/2019-02/RabbitMQ

 

RabbitMQ에 대해

 

nesoy.github.io

 

posted by 여성게
:

 

오늘은 클로저(Closure)와 커링(Currying)에 대해 다루어본다. 사실 이전에 자바스크립트를 간단히 공부하면서 봤던 기억이 있는 개념이었는데, 사실 정확한 개념을 알지 못하고 사용했던 것 같은데 이번에 정리해본다.

 

클로저(Closure)

클로저는 내부함수가 외부함수의 맥락(context)에 접근할 수 있는 것을 뜻한다. 그 뜻은 외부 함수안에 있는 내부 함수가 외부함수의 지역변수를 사용할 수 있다라는 뜻이다. 특이한 것은 외부 함수가 종료되더라도 내부함수에서 참조하는 외부함수의 context는 유지 된다는 것이다. 그것을 간단하게 자바 코드로 짜면 아래와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
public class closure {
    @Test
    void closure() {
        final var supplier = outerMethod();
        System.out.println(supplier.get());
    }
 
    private Supplier<String> outerMethod() {
        final String str = "outer method local variable";
        return () -> str;
    }
}
cs

 

위 코드를 보면 outerMethod는 Supplier를 반환하는데, 그 내부의 Supplier는 외부 메서드의 지역변수를 참조해 그대로 리턴하고 있다. 그리고 해당 메서드를 사용하는 @Test 메서드를 보자. outerMethod()를 호출했고 그것을 변수로 받고 있는데, 이 시점에는 outerMethod()는 종료되어 소멸되었지만, Supplier를 get하면 이미 종료된 함수의 지역변수를 그대로 출력하고 있다. 어떻게 이미 종료된 외부함수의 지역변수를 참조할 수 있는 것일까? 그 이유는 클로저가 생성되는 시점에 함수 자체가 복사되어 따로 컨텍스트를 유지하기 때문이다. 조금더 자세히 설명하면 익명 클래스에 컨텍스트를 넘겨주는 것이 클로저다. 컴파일러는 이 필요한 정보를 복사해서 넘겨주는데 이를 Variable capture 라고 한다.

 

자바에서 클로저가 어떻게 동작하는지 조금 더 자세히 살펴보면, 내부함수가 사용하는 외부함수의 지역변수를 클로저가 생성되는 시점에 final로 간주된다. final로 간주된다는 뜻은 새로운 인스턴스를 할당하지 못하게 되는 것이다. 1.7이전 자바는 명시적으로 final을 붙여줘야했지만 1.8 이후부터는 외부함수의 지역변수는 유사파이널로 간주되어 final를 명시적으로 붙이지 않아도 컴파일 타임에 final로 간주하게 된다.

 

그리고 위 코드를 아래와 같이 변경하게 되면 컴파일 에러가 난다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
void closure() {
    final var supplier = outerMethod();
    System.out.println(supplier.get());
}
 
private Supplier<String> outerMethod() {
    String str = "outer method local variable";
    return () -> {
        str = "aa";
        return str;
    };
}
cs

 

final로 간주되는 str 변수에 새로운 주소값을 할당하려하니 컴파일 에러가 나는 것이다. 하지만 이를 우회하는 방법으로 객체를 사용할 수 있다. 객체는 final로 생성되더라도, 안에 프로퍼티를 변경 할수 있기 때문이다.

 

그렇다면 자바에서 람다와 클로저의 차이점은 무엇일까?

 

람다와 클로저의 차이점

람다와 클로저는 모두 익명의 특정 기능 블록이고, 차이점은 클로저는 외부 변수를 참조하고, 람다는 자신이 받는 매개변수만 참조한다는 것이다.

 

// Lambda.
(server) -> server.isRunning();

// Closure. 외부의 server 라는 변수를 참조
() -> server.isRunning();

 

즉, 자바에서 클로저는 외부 변수를 참조하는 익명 클래스이고, 람다는 메서드의 매개변수만 참조하는 익명클래스가 되는 것이다.

 

private Supplier<String> outerMethod() {
    String str = "outer method local variable";
    return new Supplier<String>() {
        @Override
        public String get() {
            return str;
        }
    };
}

 

 

커링(Currying)

Currying 은 1967년 Christopher Strachey 가 Haskell Brooks Curry의 이름에서 착안한 것이다. Currying은 여러 개의 인자를 가진 함수를 호출 할 경우, 파라미터의 수보다 적은 수의 파라미터를 인자로 받으면서 누락된 파라미터를 인자로 받는 기법을 말한다. 즉 커링은 함수 하나가 n개의 인자를 받는 과정을 n개의 함수로 각각의 인자를 받도록 하는 것이다. 부분적으로 적용된 함수를 체인으로 계속 생성해 결과적으로 값을 처리하도록 하는 것이 그 본질이다.

 

private static List<Integer> calculate(List<Integer> list, Integer a) {
  return list.map(new Function<Integer, Function<Integer, Function<Integer, Integer>>>() {
    @Override
    public Function<Integer, Function<Integer, Integer>> apply(final Integer x) {
      return new Function<Integer, Function<Integer, Integer>>() {
        @Override
        public Function<Integer, Integer> apply(final Integer y) {
          return new Function<Integer, Integer>() {
            @Override
            public Integer apply(Integer t) {
              return x + y * t;
            }
          };
        }
      };
    }
  }.apply(b).apply(a));
}

 

위와 같이 매개변수를 하나씩 받고 해당 매개변수가 일부반영된 Function을 다시 리턴하는 식으로 마지막 적용될 함수에 매개변수를 일부씩 적용시키는 것이다. 이것을 조금더 간소화 시키면,

 

private Stream<Integer> calculate(Stream<Integer> stream, Integer a) {
  return stream.map(((F3) x -> y -> z -> x + y * z).apply(b).apply(a));
}

 

위와 같이 적용도 가능하다.

 

https://futurecreator.github.io/2018/08/09/java-lambda-and-closure/

 

Java Lambda (7) 람다와 클로저

람다와 클로저 자바 커뮤니티에서는 클로저와 람다를 혼용하면서 개념 상 혼란이 있었습니다. 그래서 자바 8부터 클로저를 지원한다는 글을 보기도 합니다. 이번 포스트에서는 자바에서의 람다

futurecreator.github.io

 

http://egloos.zum.com/ryukato/v/1160506

 

Java 8의 문제점: 커링(currying)대 클로져(closure))

원문을 번역한 것입니다.Closure 예제커링 사용하기자동 커링currying의 다른 응용들정리

Java 8의 문제점: 커링(currying)대

 

egloos.zum.com

 

posted by 여성게
:
인프라/Docker&Kubernetes 2020. 7. 19. 21:41

 

오늘 간단히 다루어볼 내용은 쿠버네티스 리소스(cpu, memory) 할당과 관리에 대한 이야기이다.  

 

리소스 관리

쿠버네티스에서 Pod를 어느 노드에 배포할지 결정하는 것을 스케쥴링이라고 한다. 팟에 대한 스케쥴링시, 노드에 애플리케이션이 동작할 수 있는 충분한자원(CPU, 메모리 등)이 확보되어야 배포가 가능하다. 이때문에 쿠버네티스 manifast 파일에 아주 중요한 설정이 있는데, 그것은 request, limit 에 대한 설정이다.

 

Request&Limit

 

컨테이너에 적용될 리소스의 양을 정의하는데, request와 limit이라는 설정을 사용한다. request는 컨테이너가 생성될때 최소한 있어야하는 자원 요청이고, limit은 request만큼 할당된 것보다 더 많은 리소스가 필요할때, 해당 컨테이너에게 최대로 줄 수 있는 자원의 양을 뜻한다. 간단히 예를 들어보면, request가 500이고, limit이 1000 이라면, 컨테이너는 처음 시작될때 500을 할당 받고 실행되며, 많은 트래픽이 몰려 리소스가 부족하다면 최대 500만큼의 자원을 더 받을 수 있다.

 

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-service
spec:
  replicas: 4
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2 //팟을 시작&종료할때 2개씩 작업한다.
      maxUnavailable: 0 //롤링 업뎃시 모든 팟(4개)이 서비스 가능하도록. 
                          만약 1이면, 리플리카 4개중 1개는 작업불능
  template:
    spec:
      containers:
      - name: web-service
        resources:
          requests:
            cpu: 2000m
            memory: 2Gi
          limits:
            cpu: 4000m
            memory: 4Gi

 

Request&Limit을 지정해야하는 이유는? Overcommitted 상태

이  request와 limit의 개념이 있기 때문에 생기는 문제인데, request 된 양에 따라서 컨테이너를 만들었다고 하더라도, 컨테이너가 운영이되다가 자원이 모자르면 limit 에 정의된 양까지 계속해서 리소스를 요청하게 된다. 컨테이너의 총 Limit의 양이 실제 시스템이 가용한 resource의 양보다 많을 수 있는 경우가 발생한다. 이를 overcommitted 상태라고 한다. Overcommitted 상태가 발생하면, CPU의 경우에는 실제 사용량을 requested 에 정의된 상태까지 낮춘다. 예를 들어 limit이 500, request가 100인 경우, 현재 500으로 가동되고 있는 컨테이너의 CPU할당량을 100으로 낮춘다. 그래도 Overcommitted 상태가 해결되지 않는 경우, 우선 순위에 따라서 운영중인 컨테이너를 강제 종료 시킨다. 메모리의 경우에는 할당되어 사용중인 메모리의 크기를 줄일 수 는 없기 때문에, 우선 순위에 따라서 운영 중인 컨테이너를 강제 종료 시킨다.  Deployment,RS/RC에 의해 관리되고 있는 컨테이너는 다시 리스타트가 되고 초기 requested 상태의 만큼만 자원 (메모리/CPU)를 요청해서 사용하기 때문에, overcommitted  상태가 해제된다.

 

Best practice

구글 문서에 따르면 데이타 베이스등 아주 무거운 애플리케이션이 아니면, 일반적인 경우에는 CPU request를 100m 이하로 사용하기를 권장한다. 또한 세밀하게 클러스터를 운영하기 어려운 경우에는 request와 limit의 사이즈를 같게 하는 것을 권장한다. limit이 request보다 클 경우 overcommitted 상태가 발생할 수 있는데, 이때 CPU가 throttle down 되면, 실제 필요한 CPU양 보다 작은 CPU양으로 줄어들기 때문에 성능저하가 발생할 수 있다.


<참조>

 

쿠버네티스 #21 - 리소스(CPU/Memory) 할당과 관리

쿠버네티스 리소스(CPU/Memory)할당과 관리 조대협 리소스 관리 쿠버네티스에서 Pod를 어느 노드에 배포할지를 결정하는 것을 스케쥴링이라고 한다. Pod에 대한 스케쥴링시에, Pod내의 애플리케이션�

bcho.tistory.com

 

posted by 여성게
:
인프라/Docker&Kubernetes 2020. 7. 18. 23:12

 

오늘 다루어볼 포스팅은 "도커 이미지 만들기"이다. 이전에 한번 정리해야지해야지 하면서 미뤄왔었는데, 간단하게 다루어 볼것이다. 필자도 대충은 알았지, 뭔가 깊게 이해하지 못하고 이미지를 빌드했었는데, 이참에 기초부터 한번 정리해봐야겠다.

 

<Dockerfile 작성을 위한 인스트럭션>

 

 

1. FROM : 도커 이미지의 바탕이 될 베이스 이미지를 지정한다. Dockerfile로 이미지를 빌드할 때 먼저 FROM 인스트럭션에 지정된 이미지를 내려받는다. FROM에서 받아오는 도커 이미지는 도커 허브(Docker Hub)라는 레지스트리를 참조한다. 도커 특정 버전 이상에서는 Multi stage build가 가능해져서, 하나의 베이스 이미지(FROM ..)가 아닌 여러 베이스 이미지를 사용하여 빌드가 가능하다(FROM을 여러번 사용)

 

2. RUN : 도커 이미지를 실행할 때 컨테이너 안에서 실행할 명령을 정의하는 인스트럭션이다. 인자로 도커 컨테이너 안에서 실행할 명령을 그대로 기술한다.

 

3. COPY : 도커가 동작 중인 호스트 머신의 파일이나 디렉터리를 도커 컨테이너 안으로 복사하는 인스트럭션이다.

 

4. CMD : 도커 컨테이너를 실행할 때 컨테이너 안에서 실행할 프로세스를 지정한다. 2번의 RUN 인스트럭션은 이미지를 빌드할 때 실행되고, CMD는 컨테이너를 시작할 때 한 번 실행된다.

 

"> go run  /echo/main.go"를 CMD 인스트럭션에 기술하면 아래와 같다.
CMD ["go", "run", "/echo/main.go"]

 

5. ENTRYPOINT : CMD와 마찬가지로 컨테이너 안에서 실행할 프로세스를 지정하는 인스트럭션이다. ENTRYPOINT를 지정하면 CMD의 인자가 ENTRYPOINT에서 실행하는 파일(셸 등)에 인자로 전달된다. 즉, ENTRYPOINT에 지정된 값이 기본 프로세스를 지정하는 것이다.

 

FROM golang:1.10

#./entry.sh을 실행시키면서 ARG1, ARG2를 entry.sh의 인자로 전달한다.
ENTRYPOINT ["./entry.sh"]
CMD ["ARG1", "ARG2"]

 

6. LABEL : 이미지를 만든 사람의 이름 등을 적을 수 있다.

 

7. ENV : 도커 컨테이너 안에서 사용할 수 있는 환경변수를 지정한다.

 

8. ARG : 이미지를 빌드할 때 정보를 함께 넣기 위해 사용한다. 이미지를 빌드할 때만 사용할 수 있는 일시적인 환경변수다.

 

 

posted by 여성게
:
Web/gRPC 2020. 7. 15. 20:27

 

이번 시간에 다루어볼 내용은 proto message로 생성한 Java를 Json String으로 변환하는 방법이다. proto로 생성한 java 인스턴스를 아래와 같이 json string으로 바꾸려면 예외가 발생한다.

 

#protoJava - proto로 생성한 java instance
ObjectMapper mapper = new ObjectMapper();
mapper.writeValueAsString(protoJava);

 

그렇기 때문에 protobuf의 JsonFormat으로 jsonString을 변환해주면 된다.

 

#protoJava - proto file로 생성한 java instance
final String jsonString = JsonFormat.printer().print(protoJava);

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

gRPC - Protobuf란? 구글 프로토콜 버퍼(protocol buffers)  (0) 2020.05.03
gRPC - java gRPC 간단한 사용법  (0) 2020.05.03
gRPC - gRPC란 무엇인가?  (0) 2020.05.02
posted by 여성게
: