포스트

2023-01-11-TIL

2023-01-11-TIL

Today I Learned

Docker의 Wasm 지원 소식에서 WebAssembly를 다시 보고, 고루틴, 코루틴, Java 19 가상 스레드를 비교했다. 동기·비동기와 블로킹·논블로킹의 차이, 파일 업로드 API 설계, 선분이력, AWS KMS도 함께 정리했다.


Docker + Wasm

Docker는 2022년 10월 24일 블로그 글로 Docker + Wasm 기술 프리뷰를 발표했다. 글에 따르면 이미지 관리를 containerd로 옮기는 작업을 바탕으로, WasmEdge와 함께 containerd shim을 만들어 OCI 아티팩트에서 Wasm 모듈을 꺼내 WasmEdge 런타임으로 실행한다. 사용자는 컨테이너를 실행할 때 Wasm 런타임과 플랫폼을 지정하면 리눅스 컨테이너와 같은 도구 흐름으로 Wasm 워크로드를 다룰 수 있다.

참고로 현재 Docker 문서에는 Wasm 워크로드가 더 이상 활발히 유지되지 않고 향후 Docker Desktop에서 제거될 예정이라는 안내가 붙어 있다.

  • https://www.docker.com/blog/docker-wasm-technical-preview/
  • https://docs.docker.com/desktop/wasm/

WebAssembly

WebAssembly(Wasm)는 스택 기반 가상 머신을 위한 이진 명령어 형식이다. C, C++, Rust 같은 언어를 Wasm으로 컴파일해 브라우저에서 네이티브에 가까운 속도로 실행하려고 만들어졌고, 샌드박스 안에서 실행되어 호스트 자원에 직접 접근하지 못한다. 이 이식성과 격리성 때문에 브라우저 밖의 서버와 엣지 환경에서도 컨테이너보다 가벼운 실행 단위로 주목받았고, 위의 Docker + Wasm도 그 흐름에 있다.

  • https://tecoble.techcourse.co.kr/post/2021-11-24-web-assembly/

Goroutine, Coroutine, and Java 19 Virtual Thread

세 기술은 모두 OS 스레드보다 훨씬 가벼운 실행 단위를 대량으로 만들어 동시성을 다루려는 시도지만 접근이 다르다.

  • 고루틴: Go 런타임이 관리하는 경량 실행 단위로 여러 OS 스레드 위에 다중화된다. 언어에 처음부터 내장되어 있고 채널로 통신한다.
  • Kotlin 코루틴: suspend 함수와 컴파일러 변환으로 구현한 라이브러리 수준의 기능이다. 중단 지점이 코드에 명시되고, 구조화된 동시성(scope)으로 생명주기를 관리한다.
  • Java 가상 스레드: JEP 425로 Java 19에 프리뷰 API로 들어왔다. JEP의 목표는 요청당 스레드(thread-per-request) 방식으로 작성한 서버 애플리케이션이 하드웨어를 거의 최적으로 활용하도록 확장하는 것이며, 기존 java.lang.Thread API를 쓰는 코드가 최소한의 변경으로 가상 스레드를 쓸 수 있게 하는 것이다. 블로킹 코드를 그대로 쓰면서 JVM이 대기 중인 가상 스레드를 캐리어 스레드에서 내려놓는다.

코루틴과 가상 스레드의 비교 글들은 가상 스레드가 기존 블로킹 코드를 그대로 확장시키는 데 강하고, 코루틴은 구조화된 동시성과 취소, Flow 같은 고수준 도구에서 여전히 장점이 있다고 정리한다.

  • https://medium.com/@14407744/go-goroutine-vs-java-19-virtual-thread-vs-kotlin-coroutines-664defdaad95
  • https://itnext.io/kotlin-coroutines-vs-java-virtual-threads-a-good-story-but-just-that-91038c7d21eb
  • https://medium.com/javarevisited/how-to-use-java-19-virtual-threads-c16a32bad5f7
  • https://discuss.kotlinlang.org/t/kotlin-coroutines-or-jvm-virtual-threads/24050/4
  • https://www.javai.net/post/202209/java19-vt/

Sync, Async, Blocking, Non-Blocking

두 쌍은 서로 다른 축을 설명한다.

  • 블로킹/논블로킹: 호출한 함수가 작업이 끝날 때까지 제어권을 돌려주지 않는가(블로킹), 바로 돌려주는가(논블로킹)의 문제다.
  • 동기/비동기: 호출한 쪽이 작업 완료를 직접 확인하고 결과를 챙기는가(동기), 완료되면 콜백이나 이벤트로 통지받는가(비동기)의 문제다.

그래서 네 조합이 모두 가능하다. 일반적인 read() 호출은 동기·블로킹, 논블로킹 소켓을 반복해서 폴링하는 것은 동기·논블로킹, 콜백을 등록하고 바로 반환하는 방식은 비동기·논블로킹이다. 비동기·블로킹은 비동기 API를 쓰면서 결과를 기다리느라 결국 멈추는 경우로, 의도치 않게 나타나는 일이 많다. 링크된 글들도 대부분 이 2x2 조합으로 설명한다.

  • https://black7375.tistory.com/90
  • https://goodgid.github.io/Blocking-NonBlocking-Synchronous-Asynchronous/
  • https://stackoverflow.com/questions/2625493/asynchronous-and-non-blocking-calls-also-between-blocking-and-synchronous
  • https://jh-7.tistory.com/25
  • https://velog.io/@maketheworldwise/SyncAsync-BlockingNon-Blocking-%EB%AC%B4%EC%8A%A8-%EC%B0%A8%EC%9D%B4%EC%9D%BC%EA%B9%8C
  • https://musma.github.io/2019/04/17/blocking-and-synchronous.html
  • https://developer.ibm.com/articles/l-async/
  • https://homoefficio.github.io/2017/02/19/Blocking-NonBlocking-Synchronous-Asynchronous/
  • https://interconnection.tistory.com/141
  • https://haneepark.github.io/2021/07/18/blocking-nonblocking-sync-async/

@RequestPart

@RequestPart는 multipart/form-data 요청의 각 파트를 메서드 인자에 연결한다. Javadoc에 따르면 MultipartFile이나 Part뿐 아니라 일반 객체도 받을 수 있고, 이때 파트의 Content-Type을 보고 HttpMessageConverter로 변환한다. 즉 파일과 JSON 메타데이터를 한 요청에 담아 받을 수 있다. @RequestParam도 멀티파트 파트를 받을 수 있지만, 그쪽은 메시지 컨버터 대신 타입 변환기를 쓴다는 점이 다르다.

1
2
3
@PostMapping(value = "/posts", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void create(@RequestPart("post") PostRequest post,
                   @RequestPart("image") MultipartFile image) { ... }
  • https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/RequestPart.html
  • https://somuchthings.tistory.com/160?category=983431

선분이력

일반적인 로그나 매출 기록은 사건이 일어난 순간만 기록하는 점 이력이다. 선분이력은 각 행에 유효 시작 시점과 종료 시점을 함께 저장해 “그 값이 언제부터 언제까지 유효했는가”를 관리한다. 값이 바뀌면 기존 행의 종료 시점을 닫고 새 행을 추가하므로, 과거 특정 시점의 상태를 시작 <= 기준시점 < 종료 조건 하나로 조회할 수 있다. 종료 시점이 없는 현재 행은 9999-12-31 같은 최대값을 넣어 조건을 단순하게 만든다. 대신 기간이 겹치거나 비지 않도록 갱신 로직을 엄격하게 관리해야 한다.

  • https://moons08.github.io/programming/history_management/

AWS KMS

AWS KMS는 암호화 키를 생성하고 보관하며 사용 권한을 관리하는 서비스다. KMS 문서는 봉투 암호화(envelope encryption)를 데이터는 데이터 키로 암호화하고, 그 데이터 키를 다시 다른 키로 암호화하는 방식으로 설명한다. 최상위 키는 KMS 키로 KMS의 HSM 밖으로 평문 상태로 나가지 않으며, 사용하려면 KMS API를 호출해야 한다. 큰 데이터를 KMS로 직접 보내지 않고 로컬에서 데이터 키로 암호화한 뒤, 암호화된 데이터 키만 함께 저장하는 것이 일반적인 패턴이다.

  • https://docs.aws.amazon.com/ko_kr/kms/latest/developerguide/concepts.html
  • https://bluese05.tistory.com/71

Spring File Upload

파일 업로드 시 하나의 API에서 MultiPart로 다 받는지, 아니면 별도의 API로 두 번 요청하는지? 인스타그램의 경우, 이미지 업로드 시에 먼저 서버측에 API를 호출해서 등록된 이미지의 id를 발급받는다. 그리고 발급된 이미지 id와 함께 게시물을 등록한다. 이때 이미지 id가 없으면 에러가 발생하거나, 이미지가 없는 채로 등록되는 등 정책에 따라 수행하면 될 것이다.

두 방식의 차이를 정리하면 다음과 같다. 한 번에 받는 방식은 @RequestPart로 파일과 본문을 함께 받으므로 트랜잭션 경계가 단순하지만, 업로드가 실패하면 본문 입력도 다시 보내야 한다. 분리하는 방식은 이미지를 먼저 올려 미리보기를 보여 주거나 업로드를 재시도하기 쉽지만, 게시물에 연결되지 않은 고아 파일이 생기므로 일정 시간이 지난 미사용 파일을 정리하는 작업이 필요하다.


Get Content Type from HttpServletRequest

ServletRequest.getContentType()은 요청 바디의 MIME 타입을 돌려주고, 알 수 없으면 null을 반환한다. multipart/form-data; boundary=...처럼 파라미터가 붙어 올 수 있으므로 문자열 비교 대신 startsWith나 MediaType.parseMediaType()으로 비교하는 편이 안전하다.

  • https://www.tabnine.com/code/java/methods/javax.servlet.http.HttpServletRequest/getContentType
  • https://docs.oracle.com/javaee/6/api/javax/servlet/ServletRequest.html#getContentType()

Todo

  • Logback 정리
  • Virtual Thread 정리
  • AOP로 Reqeust Body 로깅 정리
이 글은 저작권자의 CC BY 4.0 라이선스를 따릅니다.

댓글

아직 댓글이 없습니다