포스트

2023-12-29-TIL

2023-12-29-TIL

Today I Learned

SQL Mapper vs ORM

deveun 블로그는 둘 다 개발자가 JDBC 코드를 직접 짜지 않게 해 주는 영속성 프레임워크라고 정리한다. 차이는 무엇을 매핑하느냐다.

  • SQL Mapper(JdbcTemplate, MyBatis): 개발자가 SQL을 직접 쓰고, 그 결과를 객체 필드에 매핑한다. 실행되는 SQL을 그대로 통제할 수 있지만, 테이블이 바뀌면 SQL을 일일이 고쳐야 한다.
  • ORM(JPA, Hibernate): 객체와 테이블을 매핑하고, SQL은 프레임워크가 만든다. 객체 중심으로 코드를 쓸 수 있지만, 어떤 SQL이 언제 나가는지 이해하지 못하면 N+1 같은 성능 문제를 만나기 쉽다.

  • https://deveun.tistory.com/entry/SQL-Mapper%EC%99%80-ORM-%EC%B0%A8%EC%9D%B4

Netflix Tech Stack

Alex Xu(ByteByteGo)가 Netflix의 기술 스택을 정리해 올린 LinkedIn 게시물이다. 대규모 스트리밍 서비스가 계층마다 어떤 기술을 고르는지 한눈에 보려고 저장해 두었다. 게시물 본문은 로그인 없이 열리지 않아 지금은 내용을 다시 확인하지 못했다.

  • https://www.linkedin.com/posts/alexxubyte_systemdesign-coding-interviewtips-activity-7104851972184236032-F9Oe

CompletableFuture vs Virtual Thread

CompletableFuture는 Java 8에 들어온, 나중에 완료될 비동기 계산을 표현하는 클래스다. 작업을 thenApply, thenCompose, thenCombine 같은 메서드로 이어 붙이는 콜백 스타일이다. Javadoc에 따르면 Executor를 넘기지 않은 async 메서드는 ForkJoinPool.commonPool()에서 실행된다.

가상 스레드는 같은 목표를 다른 방식으로 푼다. 블로킹 코드를 그대로 순차적으로 써도 블로킹 동안 OS 스레드를 놓아주므로, 콜백 체인 없이 높은 동시성을 얻는다. dev.to 글도 둘 다 비동기 프로그래밍을 단순하게 하려는 도구지만 접근이 다르다고 비교한다. 여러 결과를 조합하는 흐름이 복잡하면 CompletableFuture의 조합 API가, I/O 대기가 많은 요청 처리 코드라면 가상 스레드의 순차 코드가 읽기 쉽다.

  • https://dev.to/jackynote/completablefuture-vs-virtual-thread-in-java-21-1gfp#:~:text=CompletableFuture%20is%20high%2Dlevel%2C%20flexible,for%20fewer%20long%20running%20tasks.

CompletableFuture vs Parallel Stream

Java 튜토리얼의 병렬성 절에 따르면 병렬 스트림은 문제를 하위 문제로 나눠 각각 다른 스레드에서 동시에 풀고 결과를 합친다. 스트림은 기본적으로 순차이고 parallelStream()을 호출해야 병렬이 된다. 병렬 스트림은 내부적으로 fork/join 프레임워크의 공용 풀을 쓰고, 이는 위의 CompletableFuture 기본 실행기와 같은 풀이다.

그래서 쓰임새가 갈린다. 병렬 스트림은 큰 컬렉션에 대한 CPU 연산을 코어 수만큼 나누는 데이터 병렬 처리에 맞고, 실행기를 고를 수 없다. CompletableFuture는 실행기를 따로 지정할 수 있어서, 외부 API 호출처럼 I/O 대기가 긴 작업을 공용 풀과 분리된 스레드 풀에서 돌리기에 맞다. I/O 작업을 공용 풀에 올리면 같은 풀을 쓰는 다른 병렬 스트림까지 느려진다.

  • https://www.linkedin.com/pulse/completablefuture-parallel-stream-henry-xiloj-herrera
  • https://pupupee9.tistory.com/167
  • https://medium.com/@mesfandiari77/parallel-stream-in-java-ac47c54176e0

Virtual Thread

가상 스레드는 JEP 444로 JDK 21에서 정식 기능이 됐다(JDK 19와 20에서는 프리뷰). JEP의 설명은 이렇다. 기존 Java 스레드는 OS 스레드를 감싼 것이라 비싸고 개수가 제한된다. 그래서 요청 하나에 스레드 하나를 쓰는 thread-per-request 방식은 동시 요청 수를 늘리기 어려웠다. 가상 스레드는 JDK가 관리하는 가벼운 스레드로, CPU에서 계산하는 동안에만 OS 스레드(캐리어)를 차지하고 블로킹 I/O를 기다리는 동안에는 캐리어를 놓아준다.

JEP가 강조하는 주의점이다.

  • 가상 스레드는 더 빠른 스레드가 아니다. 지연 시간이 아니라 처리량을 높이기 위한 것이다. CPU 연산만 하는 작업은 코어 수 이상으로 스레드를 늘려도 빨라지지 않는다.
  • 싸므로 풀링하지 말고 작업마다 새로 만든다. 동시 접근 수를 제한하려고 풀 크기를 쓰던 관행은 Semaphore 같은 도구로 바꾼다.
  • synchronized 블록이나 메서드, 네이티브 메서드를 실행하는 동안에는 캐리어에 고정(pinned)된다. 고정된 상태로 블로킹 I/O를 하면 캐리어 OS 스레드까지 막혀 확장성이 떨어진다.

Spring 블로그는 네트워크로 다른 서비스와 데이터 저장소를 호출하며 대기 시간이 긴 애플리케이션에서 가상 스레드가 의미 있다고 본다.

  • https://blogs.oracle.com/javamagazine/post/virtual-threads-futures
  • https://gdams.medium.com/embracing-virtual-threads-migration-tips-for-java-developers-afc90738fe1
  • https://medium.com/@jorgegfx/unleashing-the-power-of-virtual-threads-turbocharge-your-java-concurrency-with-project-loom-e33cbb1e35f7
  • https://spring.io/blog/2022/10/11/embracing-virtual-threads/
  • https://blog.rockthejvm.com/ultimate-guide-to-java-virtual-threads/
  • https://techblog.woowahan.com/15398/

Thread Local vs Virtual Thread

JEP 444에 따르면 가상 스레드도 플랫폼 스레드처럼 ThreadLocal과 InheritableThreadLocal을 지원하므로 기존 코드가 그대로 돈다. 하지만 가상 스레드는 매우 많이 만들어질 수 있으므로 신중하게 쓰라고 한다. 특히 스레드 풀의 스레드를 여러 작업이 공유한다는 전제로 비싼 객체를 ThreadLocal에 캐싱하던 패턴은 쓰지 말라고 명시한다. 가상 스레드는 작업마다 새로 만들어지므로 캐시가 재사용되지 않고, 가상 스레드 수만큼 객체가 생긴다.

  • https://medium.com/@mesfandiari77/exploring-thread-local-and-virtual-thread-in-java-a04a5d43a00d

Virtual Thread vs Coroutine

perfectacle 블로그는 Roman Elizarov의 발표를 바탕으로 둘을 비교한다. 코틀린 코루틴은 suspend 함수와 일반 함수를 언어 수준에서 구분하고, 컴파일러가 중단 지점을 알고 그에 맞게 최적화한다. 가상 스레드는 그런 구분 없이 기존 블로킹 코드를 그대로 쓰는 대신, 어디서 양보가 일어날지 미리 알 수 없어 그런 최적화가 어렵다는 것이 글의 해석이다. 반대로 코루틴 안에서 블로킹 I/O가 있는 레거시 코드를 부르면 제때 양보하지 않으므로 Dispatchers.IO 같은 별도 디스패처로 보내야 한다. 정리하면 코루틴은 명시적인 대신 함수 색깔을 신경 써야 하고, 가상 스레드는 투명한 대신 고정(pinning) 같은 런타임 특성을 알아야 한다.

  • https://perfectacle.github.io/2023/07/10/java-virtual-thread-vs-kotlin-coroutine/
  • https://stackoverflow.com/questions/77053797/java-virtual-threads-vs-kotlin-coroutines
  • https://dev.to/devsegur/java-virtual-threads-vs-kotlin-coroutines-4ma8
  • https://velog.io/@stella6767/Kotlin-%EC%BD%94%EB%A3%A8%ED%8B%B4-%EB%8C%80-%EC%9E%90%EB%B0%94-%EA%B0%80%EC%83%81-%EC%8A%A4%EB%A0%88%EB%93%9C-%EB%B2%88%EC%97%AD

Goroutine

Effective Go는 고루틴을 같은 주소 공간에서 다른 고루틴과 동시에 실행되는 함수로 정의한다. 스택 공간 할당 정도의 비용만 들 만큼 가볍고, 스택은 작게 시작해 필요할 때 힙에서 늘어난다. 고루틴은 여러 OS 스레드에 다중화되어, 하나가 I/O로 막혀도 다른 고루틴은 계속 실행된다. girishg4t의 글은 런타임 스케줄러가 고루틴(G)을 OS 스레드(M)에 배치하는 구조를 설명한다. 가상 스레드와 마찬가지로 런타임이 사용자 수준 스레드를 OS 스레드 위에서 스케줄링하는 M:N 모델이다.

  • https://dev.to/girishg4t/internals-of-goroutines-and-channels-397p
  • https://www.linkedin.com/pulse/internal-workings-gos-concurrency-model-david-solis
  • https://www.linkedin.com/pulse/internal-workings-gos-concurrency-model-david-solis

Parallel vs Async

병렬로 처리하면 멀티코어 자원을 활용하지만, 비동기 처리를 할 때 멀티코어 자원을 사용할 수 있는가?

비동기와 병렬은 다른 축의 개념이다. 비동기는 작업을 시작해 두고 결과를 기다리지 않고 다른 일을 하는 실행 방식이고, 병렬은 여러 작업이 실제로 같은 순간에 여러 코어에서 실행되는 것이다. 비동기 작업이 멀티코어를 쓰는지는 그 작업을 실행하는 실행기에 달려 있다. 스레드가 하나뿐인 이벤트 루프에서의 비동기는 동시성(concurrency)은 있지만 병렬은 아니고, 여러 스레드를 가진 풀에서 실행되는 CompletableFuture는 비동기이면서 병렬로 실행될 수 있다.

javaspecialists 뉴스레터 311호는 이 구분을 가상 스레드에 적용한다. 가상 스레드는 CPU 집약 작업에 쓰지 말고 큰 데이터 처리에는 계속 병렬 스트림을 쓰라고 권하며, 가상 스레드 안에서 병렬 스트림을 호출할 때의 주의점을 다룬다.

  • https://www.javaspecialists.eu/archive/Issue311-Virtual-Threads-and-Parallel-Streams.html
  • https://stackoverflow.com/questions/42504277/java-8-call-methods-async-in-parallel-and-combine-their-results
  • https://medium.com/@fullstacktips/understanding-asynchronous-non-blocking-concurrent-and-parallel-calls-9ebb7da8d955
  • https://www.iodigital.com/en/history/foreach/parallel-and-asynchronous-programming-in-java-8

JPA

JPA를 정리한 GitBook의 1장이다. 데이터 저장소와 자바의 데이터 접근 방법에서 시작해, SQL을 직접 다룰 때의 문제점, 객체와 관계형 데이터베이스의 차이를 ORM이 어떻게 메우는지, 언어별 ORM 프레임워크와 JPA의 장점 순서로 다룬다. 위의 SQL Mapper와 ORM 비교의 출발점이 되는 내용이다.

  • https://ultrakain.gitbooks.io/jpa/content/chapter1/chapter1.html

CodeCrafters

CodeCrafters는 Redis, Git, SQLite 같은 도구를 직접 다시 만들어 보는 프로그래밍 과제 사이트다. 원하는 언어로 단계별 과제를 구현하고 git push하면 테스트가 돌아가는 방식이다.

  • https://codecrafters.io/
  • https://codecrafters.io/videos/demo
  • https://app.codecrafters.io/courses/redis/introduction

Interviews for BackEnd Developer

back-end-developer-interview-questions는 백엔드 개발자 면접 질문을 일반, 디자인 패턴, 데이터베이스, HTTP, ORM, 웹 서비스, 운영체제 등 주제별로 모은 저장소다. 스스로 답을 설명해 보며 빈 곳을 찾는 용도로 쓰기 좋다. 같이 열어 둔 배치 처리 관련 링크는 아래 항목으로 이어진다.

  • https://github.com/tvandame/back-end-developer-interview-questions/blob/master/README.md
  • https://aws.amazon.com/ko/what-is/batch-processing/
  • https://velog.io/@smallcherry/%EB%B0%B0%EC%B9%98%EC%99%80-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%9F%AC

Batch vs Scheduler

배치는 실시간이 아니라 주기적으로 일괄 작업하는 것을 말한다. 다시 말해, 사용자와 상호작용 없이 여러 개의 작업을 미리 정해진 순서에 따라 중단 없이 처리하는 것이다. 스케줄러는 특정한 시간에 등록한 작업을 자동으로 실행시키는 것이다.

즉 배치는 “무엇을 어떻게 일괄 처리하느냐”이고, 스케줄러는 “언제 실행하느냐”다. 둘은 대체 관계가 아니라 함께 쓰인다. Spring 생태계로 보면 Spring Batch가 대량 데이터를 청크 단위로 읽고 처리하고 쓰는 일과, 실패 시 재시작 지점 관리를 맡고, @Scheduled나 Quartz, cron 같은 스케줄러가 그 배치 잡을 정해진 시각에 실행한다.

  • https://velog.io/@jch9537/%ED%95%9C-%EC%A4%84-%EC%9A%A9%EC%96%B4%EB%B0%B0%EC%B9%98Batch%EB%9E%80
  • https://king-ja.tistory.com/81
  • https://yermi.tistory.com/entry/Spring-Batch%EC%99%80-Scheduler%EC%9D%98-%EC%B0%A8%EC%9D%B4-Spring-Scheduler-%EC%82%AC%EC%9A%A9%EB%B0%A9%EB%B2%95

Reverse Proxy vs Load Balancer

NGINX 용어집은 둘 다 클라이언트와 서버 사이의 중개자라는 점은 같다고 본다. 로드 밸런서는 요청이 서버 한 대로 감당하기 어려울 때 여러 서버에 요청을 나눠 보내는 것이 목적이다. 반면 리버스 프록시는 서버가 한 대뿐이어도 의미가 있다. 사이트의 공개된 얼굴 역할을 하며 뒤쪽 서버를 숨기고, 보안, 캐싱, TLS 처리 같은 일을 앞단에서 맡는다. NGINX처럼 한 소프트웨어가 두 역할을 함께 하는 경우가 많다.

  • https://www.nginx.com/resources/glossary/reverse-proxy-vs-load-balancer/#:~:text=A%20reverse%20proxy%20accepts%20a,server%20to%20the%20appropriate%20client.
  • https://www.upguard.com/blog/reverse-proxy-vs-load-balancer
  • https://www.linkedin.com/pulse/load-balancer-vs-reverse-proxy-api-gateway-rishikesh-roy

Generic

제네릭은 타입을 파라미터로 받아 컴파일 시점에 타입 검사를 강화하는 기능이다. List<String>에 Integer를 넣으면 컴파일 오류가 나고, 꺼낼 때 캐스팅도 필요 없다. Oracle 튜토리얼이 기본 문법을 다룬다.

Java 제네릭은 타입 소거(type erasure)로 구현된다. 튜토리얼의 타입 소거 절에 따르면 컴파일러는 타입 파라미터를 경계 타입이나 Object로 바꾸고, 필요한 곳에 캐스팅을 넣는다. 그래서 바이트코드에는 일반 클래스만 남고 런타임 비용이 없다. 대신 런타임에는 List<String>과 List<Integer>를 구분할 수 없어서 new T()나 instanceof List<String> 같은 코드는 쓸 수 없다.

  • https://www.nextree.io/generic-ihaehagi/
  • https://hahahoho5915.tistory.com/69
  • https://docs.oracle.com/javase/tutorial/java/generics/types.html
  • https://docs.oracle.com/javase/tutorial/extra/generics/intro.html
  • https://medium.com/%EC%8A%AC%EA%B8%B0%EB%A1%9C%EC%9A%B4-%EA%B0%9C%EB%B0%9C%EC%83%9D%ED%99%9C/java-generic-%EC%9E%90%EB%B0%94-%EC%A0%9C%EB%84%A4%EB%A6%AD-f4343fa222df
  • https://medium.com/@joongwon/java-java%EC%9D%98-generics-604b562530b3
  • https://en.wikipedia.org/wiki/Generics_in_Java
  • https://www.geeksforgeeks.org/generics-in-java/
  • https://walbatrossw.github.io/java/2018/02/17/java-basic-26-generics.html
  • https://goldengiant.tistory.com/124

Software Documentation Best Practices

Swimm 글은 문서가 정확하고 최신 정보를 담아 독자가 소프트웨어를 효과적으로 쓰게 도와야 한다고 정의하고, 문서마다 독자(사용자, 개발자, 운영자 등)를 먼저 정하라고 권한다. 노션 가이드는 제목대로 팀 위키로 문서 우선(docs-first) 문화를 만드는 방법을 다룬다.

  • https://swimm.io/learn/software-documentation/software-documentation-best-practices-to-improve-your-docs
  • https://helpjuice.com/blog/software-documentation
  • https://medium.com/technical-writing-is-easy/developer-documentation-best-practices-3ebcf6587c11
  • https://www.notion.so/ko-kr/help/guides/build-a-docs-first-culture-with-a-beautiful-team-wiki-powered-by-a-database
  • https://www.linkedin.com/pulse/%EA%B3%B5%EC%9C%A0-%EB%AC%B8%EC%84%9C%EB%A5%BC-%EC%99%9C-%EC%9E%91%EC%84%B1%ED%95%98%EB%8A%94%EA%B0%80-gunwoo-kim

공변

wjdtn7823 블로그의 정의를 빌리면, A가 B의 하위 타입일 때 T<A>가 T<B>의 하위 타입이면 공변(covariance), 반대 방향이면 반공변(contravariance), 아무 관계가 없으면 무공변(invariance)이다. Java 제네릭은 기본적으로 무공변이다. Oracle 튜토리얼의 예처럼 Integer가 Number의 하위 타입이어도 Box<Integer>는 Box<Number>의 하위 타입이 아니다.

와일드카드로 변성을 지정한다. ? extends T는 공변으로 값을 꺼내 읽는 쪽(producer)에, ? super T는 반공변으로 값을 넣는 쪽(consumer)에 쓴다. 코틀린에서는 같은 개념을 out과 in으로 쓴다.

1
2
double sum(List<? extends Number> nums) { ... }      // List<Integer>, List<Double> 모두 받음
void fill(List<? super Integer> out) { out.add(1); } // List<Number>, List<Object>에 넣을 수 있음
  • https://wjdtn7823.tistory.com/88
이 글은 저작권자의 CC BY 4.0 라이선스를 따릅니다.

댓글

아직 댓글이 없습니다