Tagged: java

Spring boot 에 systemd 유닛 만들기

요즘 프로젝트를 하고 있는데, 역시나 자바 시스템이 있다. Spring Boot3 을 사용하고 있고 자바 17을 쓰는등 나름대로 괜찮은 환경에서 개발이 이루어지고 있다. 그런데, 이것을 서버에서 배포를 하고 Spring Boot 를 실행해야 하는데, 어떻게 하나 봤더니 초보자 수준도 못 벗어나는 설정을 하고 있으니… 안타까운 마음에 어떻게 하는 것이 좋은 것인지 한번 적어봤다.

Spring boot, jar 실행 파일

Spring Boot3 를 컴파일 하면 jar 파일 나온다. 그리고 별다른 서버 없이도 바로 실행하고 접속이 가능해 진다. 한가지 재미있는 사실은 많은 사람들이 Spring boot3 에 실행 파일 jar 이 내장된 WAS 서버가 구동되면서 실행된다는 걸 모른다는 거다. 심지여 그것이 Tomcat 이라는 것도. 물론 어떤 프로그래밍 모델인지에 따라서 Tomcat 이 되기도 하고 Netty 되기도 하지만, 과연 Reactive Programming Model 로 짜는 사람이 몇이나 있나 싶다. 대부분 Spring MVC 이고 Servlet 이면 기본적으로 Tomcat 이 구동된다. 내장형 Tomcat

프로젝트에서 쓰는 방법

현재 프로젝트에서는 jar 실행을 위해서 스크립트 파일을 만들었다. 대략 다음과 같다.

자바 옵션인 -D 옵션과 jar 실행 파일을 인자로 주고 있다. 그런데, 특이하게도 데몬화를 하지 않았고 많은 초보자식 설정인 nohup 도 없다. 이렇게되면 프로그램이 포그라운드(Foreground) 실행이되어서 쉘이 묶이게 된다.

어떻게 했지? 봤더니 systemd 에 이 실행파일을 넣고 systemd 를 통해서 시작/중지를 하고 있었다. 다음과 같다.

‘ExecStart=’ 에 쉘 스크립트 실행 파일을 지정해 주고 있다.

왜 이렇게 해야 하나? 왜? 이해 할 수 없는 구조다.

Spring 공식 문서

Systemd 유닛에 등록하는 방법은 Spring 공식문서에도 나와 있다.

57.1.2 Installation as a systemd service

이 문서에 나온 systemd 유닛 파일은 프로젝트 유닛 파일과 다르다. 공식 문서에 내용은 그냥 아주 기초적인 내용만 적혀 있다. 실제 프로젝트에 적용하기에는 무리가 있다.

설정파일 작성

Spring boot 를 위한 설정을 systemd 유닛에서 사용할 수 있는 별도의 파일로 작성한다. 이 설정은 JAVA_OPTS 의 내용을 담으면 된다.

이것을 이제 systemd 에서 사용하면 다음과 같다.

공식문서에 나와 있는 내용과 EnvironmentFile 속성으로 외부에서 JAVA_OPTS 를 지정한 내용을 읽어오도록 하면 충분히 사용가능해 진다.

프로젝트에서 처럼 스크립트로 한번 감싸고 이것을 다시 systemd 유닛에서 실행되도록 할 필요가 없다는 것이다.

몇가지 개선 설정

공식문서는 간단한 예이다. 실행이 가능한 정도의 유닛 파일일 뿐이다. systemd 를 이용해 자바 애플리케이션을 실행할 때에는 여러가지를 고려해야 한다.

일단 자바 애플리케이션은 네트워크를 기반으로 한다. 따라서 네트워크 온라인 상태여야 한다. 또, StdOut, StdErr 를 Journal 로 내보내도록 해야 한다.

G1 Collector 기본 설정

G1 GC 의 기본적인 목표는 짧은 STW 시간을 가지고 가는 것이다. 어짜피 STW 를 피하지 못할 바에야 이 시간을 짧게 가지고 가는게 유리하다.

여기서 구분해야할는게 있는데 STW 는 Young GC 일때도 발생한다. 하지만 그 시간이 매우 짧아서 못 느낄 정도일 뿐이다. Old GC 의 경우에는 Young GC 대비 STW 시간이 매우 길다. 그 이유는 Heap 메모리 전체를 청소해야하기 때문이며 청소해야할 공간이 클 수록 Garbage Collector 작동을 위한 자원 소모가 많아진다.

다음과 같은 목표를 갖는다.

  • 짧은 STW 시간을 갖도록 한다.
  • Full GC 가 발생되지 않도록 한다.

여기서 주목해야하는 것이 Full GC 발생하지 않도록 이다.

Full GC 발생 않도록…

MaxTenuringThreshold

이 말은 결국에는 Young GC 만 만들어야 한다는 것을 의미한다. 문제는 live Object 는 결국 최종적으로는 Old Generation 영역으로 이동할 수밖에 없다는데 있다. 그래서 G1 에서는 최대한 Old Generation 영역으로 이동하는 것을 늦출려고 한다. 이에 대한 파라메터가 -XX:MaxTenuringThreshold 다.

  • -XX:MaxTenuringThreshold: Sets the maximum amount of iterations to keep live objects in the new generation. This defaults to 15.

live 객체를 new generation 에 머물게하기 위한 최대 반복횟수로 번역된다. new generation 에서 Young GC 가 발생하면 오래 살아남을 live 객체를 선별되게 된다. 오래 살아남을 live 객체는 당연히 Old generation 영역으로 이동해야 하는데, 이것을 한번에 판단하지 않겠다는 뜻이다.

적어도 여러번.. 기본값으로 15번 정도는 같은 live 객체가 new generation 에서 생존해 있어야 한다. 그래야 ‘아~ 이놈은 오래 살아남을 놈이구나.. Old Generation 으로 옮기자’ 가 된다.

이러한 메커니즘은 Full GC 를 피하기 위한 것이다. Old Generation 에 live 객체가 많아지면 많아질 수록 Full GC 발생가능성은 커진다. 그래서 왠만하면 Young Generation 에 객체를 머물게하고 Garbage 가 되길 기다린다.

MaxGCPauseMillis – Do not set the new generation size unless required.

이 값을 꼭 지정하도록 하고 있다. G1 은 a pause time-target 을 갖는다. 이값은 MaxGCPauseMillis 로 지정하게 되는데 문제는 이 값을 어떻게든 충족되도록 동작하기 뒤해서 G1은 Young Generation 크기를 자동으로 조정하게 된다.

따라서 반드시 Young Generation 크기를 지정해서는 안된다. 오로지 MaxGCPauseMillis 값을 지정해주고 이를 통해서 자동으로 조정되도록 해야 한다. 200 ~ 500 사이에 값을 지정하면 되는데 될수있으면 그냥 기본값으로 둔다.

G1 에서 추천하는 JVM Options

다음은 G1 에서 추천하는 JVM Options 이다.

JVM OptionDetails
-XX:+DisableExplicitGC기본 추천 옵션으로 명시적 GC 를 사용하지 못하게 한다.
-XX:+UseStringDeduplication기본값이 disabled 인데, 켜준다. String deduplication 에 대한 메모리 사용을 줄여준다.
-XX:MaxMetaspaceSize최대 사용가능한 클래스 메타데이터 크기. 네이티브 메모리를 사용한다. 256MB 를 권장하지만 값은 유동적이다.
-XX:MaxTenuringThreshold기본값: 15. new generation 에 live 객체를 유지하기 위한 최대 반복 횟수. 오랫동안 살아남아야할 live 객체가 많다면 값을 낮추고 그렇지않다면 기본값 사용 권장.
-XX:+ParallelRefProcEnabled기본값으로 Disabled 인데, 사용하길 권장.

대략 다음과같이 Command Line 을 사용할 수 있다.

참고: Best practice for JVM Tuning with G1 GC

Spring Boot, error: constructor … in class .. cannot be applied to given types 오류

Spring Boot 로 프로그램을 작성하고 난 후에 Compile 단계에서 다음과 같은 오류를 만날 수 있다.

에러 코드를 보면 new 연산자를 이용해 Employee 객체를 생성하는 부분인데, 이부분이 문제가 된다는 것이다. required: no arguments 라고 나오지만 Employee 는 다음과 같이 되어 있다.

@AllArgsConstructor 어노테이션을 줘서 자동으로 모든 객첵 멤버변수를 인자로 받는 컨스트럭터를 생성하도록 하고 했다. 이것은 Lombok 을 이용한 것으로 다음과 같이 gradle 에서 의존성을 줬다.

문제는 Eclipse 나 IDE 툴에서는 SpringBoot 애플리케이션이 잘만 실행되지만 정작 컴파일을 할려고 하면 위와같은 오류가 나오는다는 것이다. 아무리 봐도 소스코드상에서는 아무런 문제가 없는데도 이런다면 다음과 같이 gradle 에서 annotationProcessor 를 추가해주면 된다.

Maven 을 사용할 경우에는 아무런 문제가 없었지만, Gradle 을 사용하면서 Lombok 을 사용한다면 반드시 annotationProcessor 를 추가해줘야 한다.

IntelliJ 에서는 설정에서 “Enabling Annotation Processing” 체크하면 된다.

컨테이너에서 Java 힙 덤프 뜨기

Kubernetes 에서 Java 애플리케이션을 운영할때에, Java 힙 덤프를 떠야하는 경우가 있다. 하지만 다음과 같이 덤프를 떠지지 않는다.

“Unable to get pid of LinuxThread manager thread” 오류가 발생한다.

이 오류가 나오는 이유는 Java 애플리케이션의 Pid 값이 1이기 때문이다. 이를 해결하는 방법을 소개한다.

Container 이미지에 tini 설치, 배포

먼저 Openjdk 의 컨테이너 이미지에 tini 프로그램을 설치해야 한다. 이 tini 라는 프로그램은 인자값을 받은 프로그램을 실행 시켜 준다. 이렇게 하면 tini 는 Pid 1을 가지지만 tini 가 실행시킨 프로그램은 1보다 큰 Pid 값을 가지게 된다.

문제는 Openjdk 에 tini 라는 프로그램이 없다. 더군다나 아무 컨테이너 이미지에 이것을 설치할 수 있는게 아니라 Alpine 기반 이미지에서 쉽게 설치가 가능하다. Alpine 에서는 패키지로 제공하기 때문에 명령어로 간단하게 설치할 수 있다.

Dockerfile 을 다음과 같이 수정한다.

apk 명령어를 이용해 tini 를 설치해주고 ENTRYPOINT 에 실행 명령어에 tini 를 넣고 인자값으로 java 애플리케이션 명령어를 넣는다.

이렇게 컨테이너 이미지를 제작하고 배포를 한다.

tini 프로그램 이 후 Pid

tini 프로그램으로 Java 애플리케이션이 어떻게 실행되는 다음과 같이 확인 할 수 있다.

jcmd 뿐만 아니라 jstat, jmap 등 모든 명령어를 사용할 수 있다.

Log4j 취약점, 한국 IT 보안에 대한 단상

몇일 전에 Apache 재단에서 제작해 배포하는 Log4j2 에 대해, 보안 취약점 등급 10등급으로 즉시 취약을 말하면서 보안설정과 패치를 하라는 권고를 했다. 이는 전 세계적인 현상으로 우리나라 뿐만 아니라 미국등에도 매우 심각한 현상임을 인지하고 뉴스에도 나올만큼 큰 뉴스거리가 되었다.

Log4j2 만 문제냐

하지만 이 업계에 10년 넘게 밥벌이를 하는 입장에서 별로 놀랍지도 않고 왜 그렇게 호들갑이냐 하는 생각이 먼저 든다. 모르는 사람이야 뉴스에도 나오고 전문가들이 심각하다고 하는데 ‘호들갑’ 이라고 말하니 내가 뭔가 잘못 말한것처럼 생각이 들겠지만 실상을 알고 나면 그렇지 않다는걸 깨닫게 된다.

먼저, 내가 현재 맡아서 하고 있는 것들을 한번 보자. 대략 다음과 같은 스펙으로 서비스되어지고 있다.

  1. RedHat Enterprise Linux 7.7
  2. Spring Boot 1.5
  3. Java 1.8
  4. WildFly 13

대충 이렇게 나열해 보면 이게 대체 뭐가 문제냐 하는 생각이 들 것이다.

KISA 정보에서 발행한 ‘주요정보통신기반시설 기술적 취약점 분석, 평가 방법 상세가이드’ 라는 것이 있다. 여기에는 특정 요소요소에 대해서 보안 설정을 어떻게 해야하는지를 가이드하고 있는데, 여기에 ‘U-42. 4.1 최신 보안패치 및 벤더 권고사항 적용’ 이란게 있다.

보안 가이드 패치권고

요약을 하자면 항상 최신의 패치 적용을 유지하라는 것이다.

그런데, RedHat Enterprise Linux 7.x 에 최신 버전은 7.9 이다. 더 최신은 RedHat Enterprise Linux 8.5 다. 시스템에 설치된 어떤 프로그램이 보안 취약점을 들어내고 있는지에 대해서 알지도 못하고 어짜피 네트워크가 단절되어 있어서 외부에 침입이 어렵다는 변명으로 운영체제(OS)에 대한 최신 패치 적용을 전혀 하지 않는다.

최신의 RHEL 을 사용하지 않는건 일단 내려놓더라도 쓰지도 않는 각종 프로그램들이 다 설치되어 있다. 대표적인게 gcc 컴파일러이다. 어디 gcc 컴파일러 뿐이랴.. g++ 도 있고 automake, autoconf, bison 등 C 소스코드를 컴파일 할 수 있도록 아주 다 깔려 있다.

크랙커가 가장 좋아라하는 시스템이 뚫어서 들어가보니 각종 컴파일러가 설치되어 있는 시스템이다. 컴파일러만 설치되어 있으면 마음대로 시스템을 가지고 놀수 있다. 그래서 될 수 있으면 컴파일러는 설치를 하지 말아야 한다. 아니 더 정확하게는 사용하지 않거나 불필요한 프로그램들과 명령어는 아예 설치를 하지 말아야 한다.

더 놀라운 것은 Java 시스템인데도 각종 gcc, g++ 컴파일러가 설치되어 있고 각종 라이브러리까지 아주 풍부하다는 것이다. 이에 대해서 이러면 안된다고 하면, 각종 보안 프로그램들이 철벽으로 작동하고 있고 ISMS 심사를 받았으니 됐다고 말한다.

Spring Boot 1.5 – 지원 끝

지원이 끝난 프레임워크다. 일단 지원이 끝났다면 될수 있는대로 빠르게 업데이트를 해야 한다. 하지만, 작동하는데 아무런 문제가 없으니까 계속 사용한다. 뭔가 문제가 생기면 그 라이브러리만 업데이트 하는 식이다.

Spring boot 1.5 전체를 패키지는 방대한데, 그중에서 자주 사용하는 일부가 문제가 생겼다는 뉴스가 나오면 그것만 바꾸는 식이다.

이렇다보니 Spring boot 2.x 에 대한 기술 습득은 꿈도 못꾼다. Reactive 가 뭐고 그래서 그게 뭐가 좋은지도 모르고 그것을 하면 뭐가 이득이 된다는 것은 둘째고 기술지원도 끝난 프레임워크를 그대로 고수하면서 DB 구조를 파악하고 데이터를 넣다 뺏다 하는 CRUD 프로그래밍으로 화면에 원하는 것을 이루게 되면 그것이 프로그래밍이 된 것이라는 사고방식에 젖어 있다.

고민따위는 안중에도 없고, 구글에서 검색해서 읽어본 기사에 뭘 좀 안다고 지식자랑하기에만 바쁘고 정작 책상에 앉아서 하는 일이라곤 지식자랑과는 한참 뒤처진 기술들을 다루고 있을 뿐이다.

지원이 끝났다는 것은 더 이상 보안관련 업데이트를 해주지 않는다는 것을 뜻한다. 물론 전 세계적으로 문제가 될 경우에 End Of Life 가 된 경우라도 업데이트를 제공해 주긴 한다. 하지만 지원이 끝난 것들은 더 이상 보안에 대해서 신경을 놔버리는 경우고 거들떠도 안보다는 사실을 알아야 한다.

아무도 신경을 안쓰는 거들떠보지도 않는 프레임워크로 일을 하는 것이 뭐 그리 대단한지, 단가 900 은 받아야겠다고 하는데 괜히 프리 경력이 정규직 경력을 인정받지 못하는게 아니다.

이렇게 글을 적어놓으면

프로젝트 오퍼를 낸 조직이 가이드를 내놓고 어떻게 하라고 해야지 그걸 왜 프리보고 뭐라 하냐?

이렇게 말할게 뻔하다. 그러니까 정규직 경력이 안되는 거다. 정규직에서는 적극적인 사람을 원하지 그렇게 수동적으로 뭐 가이드 안주면 못하겠다는 식의 사고를 가진 사람을 뽑으려 하지 않는다. 그것도 적어도 경력이 10년 이상이라면 이렇게 해서는 안된다, 이런 것쯤은 좀 바꿔야 한다는 정도도 있어야 하고 적극적으로 문제에 대해서 문제가 있으며 바꿔야 한다는 것을 적어도 어필 정도는 해줘야 하는데, 그런게 안되잖아..

더 큰 문제는 대충한다는데 있다. Spring Framework 3.x 처럼 오래된 것이긴 하지만 나름대로 프로그래밍 방법들이 존재한다. 과거에는 쓸만했던 것이여서 자료를 찾아보면 그때당시에 자료들도 상당하다. 그러면 Spring Framework 3.x 에 대한 그 때 당시에 쓸만했던 것을 지금에 쓰고 있느냐… 그냥 다 대충한다. 어짜피 딴데가면 안쓸거… 구형인데 뭐… 이거 열심해서 뭐하냐…. 아무도 열심히 안한다.

어짜피 구형이라 열심히 안하고 그렇다고 뭐 바꾸자는 제안조차 못하고… 그냥 돌아가게만 만들자식이란게 문제다.

Wildfly 13 – End Of Life

Wildfly 는 예전에 Jboss AS 라는 오픈소스 엔터프라이즈 자바 서버였다. 그러던 것이 Redhat 에서 프로젝트를 인수(?) 하고 자사의 상용 엔터프라이즈 자바 서버를 Jboss EAP 라는 이름을 붙이자 네이밍에 차이를 두기 위해서 Wildfly 로 이름을 바꾼다.

Wildfly 는 이름을 바꾼것만이 아니라 릴리즈 주기를 바꾸게 되는데, 분기마다 메이저 업데이트를 하는 정책으로 바꾼 것이다. (분기마다 인지 1년마다 인지… 정확하지는 않음. 몇달 주기로 메이저 업데이트를 하겠다는 정책임) 이러한 정책은 Wildfly 의 Long Term Support 버전이 없다는 것을 말한다.

업데이트를 못 쫓아가는건 당연하고 무엇보다 JEE 스펙도 버전이 올라감에 따라 달라지다 보니 Servlet 엔진을 필요로하는 경에 버전 호환성이 맞지 않는 경우가 생긴다. 그것조차도 그렇다 치더라도 Wildfly 13 도 지원이 끝나기는 마찬가지다.

이렇게 주기적인 메이저 업데이트를 하는 자바 서버에 경우에 주기적으로 업데이트를 할 수 없는 경우에는 선택을 하지 말았어야 했다. RHEL 도 한번 설치하면 업데이트를 안하고 버티는 경우가 부지기수인데, 주기적인 업데이트를 고려해서 이것을 선택햇을리는 만무하다.

금융권은 더 심각

현재 내가 하고 있는 프로젝트의 경우에 한정해서 말을 하다보면, “그건 니가 있는 곳이 ㅈ같은 경우지… 똥 밟은 너를 탓해라” 하는 인간들이 대다수다.

그렇다면 과거, 그것도 긴 과거가 아닌 올해 있었던 금융권 프로젝트를 예를들어보자.

  • RedHat Enterprise Linux 7.7
  • gcc, g++ 컴파이러, automake, autoconf
  • WebLogic 12.1.x
  • Spring Framework 3.x

이런 환경에서 마이데이터니 뭐니,, 모바일이니 뭐니 서비스하고 앉아 있는게 현실이다.

더 웃긴건, WebLogic 서버를 사용하는데 환경은 AWS 클라우드에 올렸다는데 있다. 이게 뭐가 문제가 되느냐 하겠지만 WebLogic 는 Admin 서버와 Managed 서버로 나뉘며 Admin 서버가 반드시 있어야 하고 Managed 서버가 Admin 서버에 등록이 되어 있어야 한다.

이렇게 되면 AWS 클라우드에서 반드시 사용해야만 하는 AutoScaling 을 이용할 수가 없다. 물론 아예 이용할 수 없는 것은 아니지만, 별도의 노력이 조금 더 들어간다. 하지만, 그 별도의 노력조차도 안해서 그런지 AutoScaling 은 아예 접었고 닭 한마리 이벤트를 날리면 접속자가 폭주해 장애가 나는 곳이 금융 시스템이다.

KISA 도 결국 문제

이래저래 문제 해결을 위해서 노력한다고 하지만 한개는 있다. 그중에 다음과 같은 말들을 많이 한다. 적어도 금융권에서는..

Redhat Enterprise Linux 8.x 는 아직 보안 인증이 안됐다.

누가 어떻게 RHEL 8.x 에 대해서 보안인증을 해주나? KISA 가? KISA 가 발행하는 취약점 가이드에 Linux 에 대해서 RHEL 7 기준으로 설명되어 있어서 RHEL 8.x 는 아직 인증 없는 거다?

KISA 보안 가이드 유의사항

KISA 가 발행하는 취약점 가이드에는 위와같이 유의사항이 나와 있다. 하지만 현장에 있다보면 ‘인증이 아직 안됐다’ 라는 말을 많이 듣는다. 공공금융 프로젝트에서 인증을 안해준건지 뭔지 모르겠지만, KISA 라는 ISMS 주관사에서 저렇게 하고 있음에도 불구하고 다른 권위도 없고 보안에 관련해 국가적 위임도 받지 않는 곳에서 ‘인증’ 이란걸 하고 있다면 KISA 가 적극적으로 나서서 못하게 해야 한다.

더군다나 KISA 의 경우에 보안 취약점 가이드에는 절대로 하지 말아야 하는 항목들은 없다. 예를들어 ‘gcc 컴파일러 설치 금지’ 와 같은 것들도 없고 End of Life 된 운영체제나 프레임워크에 대한 금지 항목도 없다. ISMS 라는게 결국에는 가이드에 맞춰서 그걸 했냐하는 정도에 그치다보니 ‘그것만으로 됐다’ 하는 것으로 끝이다.

국가기관이 저렇게 나약하게, ‘절대적 기준 아니다’, ‘다양한 점검 방업을 사용하여 취약점 분석평가를 수행’ 등.. 말이 좋아서 저런 표현이지 그냥 대놓고 “니들이 알아서 잘 해봐라” 식과 뭐가 다르냐?

Log4j2 보안 취약점 패치

현재 프로젝트에서 log4j 보안 취약점 패치를 위해서 살펴보니 log4j 1.x 버전이였다. 지금의 log4j2 의 보안취약점은 JNDI 관련되어 있는데, log4j 1.x 에는 JNDI 기능 자체가 없다. 따라서 본 취약점에는 아무런 문제가 없다.

이런식의 결론에 도달하게 되어서 결국에는 아무런 조치도 하지 않았다. log4j 1.x 는 더 이상 유지보수가 안되는 버전이다. 하지만 이번에 터진 취약점과는 관계가 없기 때문에 그런대로 넘어가는 거다.

라고 윗분들의 결정했다. 이게 대한민국의 현주소다.

그래서 log4j 를 그냥 놔둘수도 없고 해서 logback 으로 바꿔 놓을려고 생각중이다. 테스트를 해보니 잘 될거 같다. 보고 따위는 안한다.. 그냥 바꿔놓는 거지.. 그냥 바꿔놔도 모른다. 아무도.. ㅋㅋ

JEE 7, JEE 8 and Jakarta EE 8

Java EE 7 Specification

Java EE 7 Overview
Java EE 7 Specification

Java EE 8 Specification

Java EE 8 Overview
Java EE 8 Specification

Jakarta EE 8 Specification

The Jakarta EE 8 has the same set of specifications from Java EE 8 with no changes in its features. The only change is the new process to evolve these specifications. With this, Jakarta EE 8 is a milestone in Java enterprise history, as it inserts these specifications in a new process to boost the specifications to a cloud-native application approach.

Jakarta EE 8: The new era of Java EE explained

Java EE version history

Java EE version history
https://www.packtpub.com/product/building-restful-web-services-with-java-ee-8/9781789532883

Reactive Stream 간단 사용

Reactive Stream 을 사용하기 위해서는 다음과 같은 것을 구현해줘야 한다.

  • Publisher
  • Subscriber
  • Subscription

한가지 문제가 있다. 이것을 구현하기가 쉽지가 않다. 특히 Publisher 의 경우에는 Reactive Stream 의 기본 아이디어만 가지고 간단하게 구현할 수가 없다. 어떤 것을 Publisher 할지도 영향을 주는것도 문제지만 Reactive 의 Spec 을 마춰짜야 하는데 이게 쉽지가 안다.

Publisher 를 간단하게 구현 방법이 없을까? SubmissionPublisher 를 이용하면 간단하게 사용해 볼 수 있다.

이 문서는 SubmissionPublisher 를 이용해 어떻게 Reactive Stream 을 구현하는지를 설명한다.

SubmissionPublisher Class

이 클래스를 간단히 살펴보자.

이 클래스는 Publisher 인터페이스를 구현한 구현체 클래스 이다. 하지만 아주 간단하지만은 않다.

SubmissionPublisher 클래스가 가지는 메소드가 꽤 있다. 자세히 보면 get 혹은 is 로 시작하는 메소드가 많음을 알 수 있다. 이 클래스들은 Publisher 의 상태 정보를 체크하기 위한 것들이다. 반대로 set 으로 시작하는, 그러니까 뭔가를 지정하는 메소드는 많지가 않다.

이 클래스에 설명은 다음과 같이 시작한다.

Flow.Publisher that asynchronously issues submitted (non-null) items to current subscribers until it is closed. Each current subscriber receives newly submitted items in the same order unless drops or exceptions are encountered. Using a SubmissionPublisher allows item generators to act as compliant reactive-streams Publishers relying on drop handling and/or blocking for flow control.

Flow.Publisher 는 Null 이 아닌 제출된 아이템들을(item) 현재 구독자에게 닫힐때까지 비동기적으로 발행한다. 각각의 현재 구독자들은 삭제 또는 예외가 발생하지 않는한 새롭게 제출된 아이템들을 같은 순서로 받는다. SubmissionPublisher 를 사용하는 것은 아이템 생성기를 흐름제어를 위해 삭제 핸들링과 블럭킹을 필요로하는 reactive-streams Publisher 와 호환되는 것처럼 행동하게 해준다.

https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/SubmissionPublisher.html

어찌되었든 간에 이것을 이용하면 Reactive Streams Publisher 처럼 만들어 준다 것에 주목해야 한다.

SubmissionPublisher 사용하기

사용방법은 아주 간단하다.

SubmissionPublisher 의 객체를 하나 생성한다. 그리고 Reactive Stream 이 Publisher 에 요구하는 subscribe 메소드를 호출해 준다.

하지만 subscribe() 메소드는 인자값으로 Subscriber 클래스를 요구한다. Subscriber 클래스는 Interface 클래스여서 구현체 클래서를 넣어야 한다. 다양한 방법이 있지만 여기서는 다음과 같이 작성해 본다.

Reactive Stream 이 요구하는 Subscriber 에 요구 메소드들이(onNext, onError, onComplete) 모두 나온다. 다음과 같이 모두 구현해 준다.

위와같이 구현을 해주면 된다. 여기서 중요한 것이 존재하는데, Subscription 객체 변수를 반드시 써야 한다는 것이다. onSubscribe, onNext 메소드에서 공통으로 보이는 subscription.request(1) 이 바로 그것인데 onNext 메소드에 이를 사용하기 위해서 Subscriber 구현체에서 멤버변수를 만들어두고 onSubscribe 시에 주입된 객체변수를 할당해 준다.

이제 다음과 같이 publisher 의 submit 메소드를 사용해 아이템을 넣어준다.

submit 메소드는 아이템을 제출해 비동기적으로 Subscriber 에게 생성해준다. SubmissionPublisher 의 비동기는 Thread 를 이용한다. 그래서 모든 Thread 가 완료되기를 Main Thread 를 몇초간 중지줘야해서 Thread.sleep(100) 코드가 필요하다.

모든 비동기 제출이 완료되면 제출자를 닫아준다. publisher.close() 다.

이렇게 완료된 전체 코드는 다음과 같다.

Reactive Stream 을 어떻게하면 간단하게 사용해볼까. Reactive Stream 이 요구하는 최소사항을 어떻게하면 직접 사용해볼 수 있을까? 그렇다면 SubmissionPublisher 클래스를 활용하라.

자바 Reactive Stream 구현체

자바(Java) 에서 Reactive Stream 구현체를 살펴본다.

이전 글에서 Reative Stream 의 기본 아이디어를 설명했었다.

  • Flow control
  • Publisher – Subscriber pattern

또한 이 아이디어를 위한 포맷은 Publisher – Subscriber 이다. Reactive Stream 에서 이 모델은 1) Subscriber 가 Publisher 에게 가입을 요청하면 2) Publisher 는 Subscriber 와 통신을 위한 채널인 Subscription 을 생성하고 이를 통보한다. 이는 다음 그림과 같이 묘사할 수 있다.

출처:https://dzone.com/articles/reactive-streams-in-java-9

여기서 주목해야할 것이 Subscriber 는 Subcription 을 통해서 데이터를 요청하고 받게 된다는데 있다.

java.util.concurrent.Flow

자바에서 구현은 위 클래스에 구현 되어 있다. 그리고 이 클래스 첫줄에 다음과 같이 설명이 시작된다.

Interrelated interfaces and static methods for establishing flow-controlled components in which Publishers produce items consumed by one or more Subscribers, each managed by a Subscription

흐름 제어 컴포넌트를 설정하기 위한 (밀접하게) 상호연관된 인터페이스(Interface) 와 스태틱 메소드(Static method)

java.util.concurrent.Flow

주석에 내용에 등장하는 컴포넌트 Publisher, Subscriber, Subscription 이다. Flow.class 는 다음과 같이 구성되어 있다.

Publisher

FunctionalInterface 인게 눈에 뛴다. 이것은 오직 한개의 메소드만을 가진다는 것과 Lambda 식을 지원하게 된다. subscribe 메소드는 인자로 Subscriber 를 받는다.

중간에 주석에 주목할만한 내용이 나온다.

Subscribers may enable receiving items by invoking the request method of this Subscription, and may unsubscribe by invoking its cancel method.

Subscriber 는 이 Subcription 의 request 메소드를 호출함으로써 아이템들을 수신할 수 있으며 cancel 메소드를 호출함으로써 가입이 해제된다.

Subcriber 는 Subscription 의 request 메소드를 호출함으로써 데이터(아이템)을 수신한다는 것이다.

Subscriber

4개의 메소드를 가지고 있다. 이 메소드들이 다루는 것이 무엇인지를 아는것이 제일 중요하다. 각 메소드의 주석에는 Subscription 이 항상 나온다.

Method invoked with a Subscription’s next item. If this method throws an exception, resulting behavior is not guaranteed, but may cause the Subscription to be cancelled

Subscription’s 의 다음 아이템을 호출하는 메소드. 만약 이 메소드가 예외를 발생시키면, 최종 결과를 보장하지 않지만, Subcription 은 취소될 수 있다.

onNext

Subscriber 가 다음 아이템을 호출하는데 Publisher 를 대상으로 하지 않고 Subscription 을 대상으로 한다는 것을 꼭 기억해야 한다.

Subscription

Publisher 와 Subscriber 를 연결하는 메시지 제어를 하는게 목적이다 .request 메소드는 Subcription 에 대해 현재 처리되지 않은 요청을 위한 아이템의 개수를 추가하게 된다. 몇개의 아이템을 요청할 것인지를 요청하는 것이다.

SubmissionPublisher

SubmissionPublisher 는 non-null 아이템을 생성해주는 Publisher 이다. 생성을 해준다고 해서 자동으로 생성해주는 것이 아니라 아이템을 제출해야 한다. 이 제출된 아이템은 순차적으로 Publish 해준다.

submit

이 메소드는 비동기적으로 Subscriber의 onNext 메소드를 호출함으로써 인수로 받은 아이템을 Subscriber 에 아이템을 발행한다.

Reactive Stream Family

Reactive Stream 에는 다양한 프레임워크, 라이브러리들이 존재한다. 이것들은 각자의 고유한 이름과 특징을 가지고 있다. 어떤 것들이 있는지 간단하게 정리해 본다.

Reactive Streams

저수준의 규약으로 주로 자바 인터페이스(Interface) 로 구현 되어 있다. 명시적인 back-pressure 으로 Publisher 와 Subscriber 의 기본 빌딩 블록을 표현한다. Java 9 에서 java.util.concurrent.Flow 로 구현되어 있다.

RxJava

이것은 Reactive Extension 이다. Neflix 에서 개발해 오픈소스로 전화하면서 세상에 알려졌다. ReactiveX 라고도 불린다. 브릿지 Reactive Stream 이라고 말하기도 하는데, Reactive Stream 을 위한 타입 전환을 지원한다.

Reactor

자바 프레임워크 이다. Spring 배포로 유명한 피보탈(Pivotal) 에서 제작해 오픈소스로 배포하고 있다.

Spring Framework

HTTP 서버/클라이언트을 툴을 비롯해 리액트 기능을 가지고 있다. Spring 의 특징은 어노테이션을 이용한 기능 주입등을 사용할 수 있다. 비동기 HTTP 서버에서 구동가능하다. (Netty, Undertow, Tomcat 8 이상)

Akka Stream

Actor Model 을 기반으로 애플리케이션 개발을 위한 Java, Scala 를 위한 툴킷이다.

이것을 정리하는 이유가 있다. Reactive Stream 은 스펙이기 때문이다. 이 스펙을 만족하면 Reactive Stream 이라고 할 수 있다. 물론 자바의 저수준 인터페이스 이름도 Reactive Stream 이라 헷깔릴 수 있지만 Reactive Stream 스펙을 만족하는 다양하고 라이브러리, 프레임워크, 툴킷들이 있다.