포스트

2023-03-11-TIL

2023-03-11-TIL

Today I Learned

Logging

Thread Local에 요청과 응답을 저장해두었다가 로깅하기도 함.

서블릿 기반 서버는 요청 하나를 스레드 하나가 처음부터 끝까지 처리하므로, 요청 단위 정보를 ThreadLocal에 두면 어느 계층에서든 꺼내 쓸 수 있다. SLF4J의 MDC가 바로 이 방식으로, 요청 ID나 사용자 ID를 넣어 두면 그 스레드의 모든 로그에 함께 찍힌다. 주의할 점은 스레드가 풀로 돌아가 재사용된다는 것이다. 요청이 끝날 때 finally에서 값을 지우지 않으면 다음 요청의 로그에 이전 요청 정보가 섞이고, @Async처럼 다른 스레드로 넘어가면 값이 전달되지 않는다.


JMX

JMX(Java Management Extensions)는 애플리케이션, 장치, 서비스를 관리하고 모니터링하는 도구를 만들기 위한 자바 표준 기술이다. 관리 대상은 MBean으로 노출되고, JConsole이나 VisualVM 같은 도구가 원격으로 붙어 힙 사용량, 스레드 수, 커넥션 풀 상태 같은 지표를 읽거나 설정을 바꾼다. Spring Boot Actuator도 엔드포인트를 HTTP와 함께 JMX로 노출할 수 있다.

  • https://www.oracle.com/java/technologies/javase/javamanagement.html

Pull Request Best Practices

여러 글의 권고가 대체로 겹친다. PR은 한 가지 목적에 집중해 작게 만들고, 설명에는 무엇을 왜 바꿨는지와 검증 방법을 적으며, 리뷰를 요청하기 전에 스스로 한 번 읽어 본다. Creative Commons의 PR 가이드라인은 언어별 코드 스타일을 지키고, 함수를 작고 모듈화된 형태로 유지하며, 기존 테스트가 모두 통과하는지 확인하라고 요구한다. 리뷰어 쪽에서는 코드가 아니라 사람을 향한 표현을 피하고, 반드시 고쳐야 할 것과 제안을 구분해 남기는 것이 공통 권고다.

  • https://opensource.creativecommons.org/contributing-code/pr-guidelines/
  • https://rewind.com/blog/best-practices-for-reviewing-pull-requests-in-github/
  • https://leoneperdigao.medium.com/pull-request-best-practices-fa20f7daeb3c
  • https://doordash.engineering/2022/08/23/6-best-practices-to-manage-pull-request-creation-and-feedback/
  • https://blog.codemagic.io/10-tips-for-better-pull-requests-and-code-review/
  • https://linearb.io/blog/pull-request-best-practices-our-tips/

ArgumentResolver vs Interceptor

Tecoble 글의 정리로 둘은 목적이 다르다. HandlerMethodArgumentResolver는 요청 값에서 컨트롤러 파라미터 객체를 만들어 주는 전략 인터페이스다. 토큰에서 ID를 꺼내 LoginMember 객체로 만들어 넘기는 것처럼, 컨트롤러가 필요한 객체를 받아야 할 때 쓴다. HandlerInterceptor는 핸들러 실행을 가로채는 것으로, preHandle이 false를 반환하면 컨트롤러가 실행되지 않는다. 객체를 넘길 필요 없이 인가 여부만 확인하면 될 때 쓴다.

실행 순서는 DispatcherServlet이 HandlerMapping으로 컨트롤러를 찾은 뒤 인터셉터의 preHandle을 실행하고, 그다음 HandlerAdapter가 컨트롤러를 호출하기 위해 파라미터를 만들 때 ArgumentResolver가 실행된다. 즉 인터셉터가 먼저다.

  • https://tecoble.techcourse.co.kr/post/2021-05-24-spring-interceptor/

Optimistic Lock vs Pessimistic Lock

같은 데이터를 동시에 수정할 때 나중 커밋이 앞선 수정을 덮어쓰는 갱신 분실(lost update)을 막는 두 방법이다.

  • 낙관적 락: 충돌이 드물다고 가정하고 DB 락을 쓰지 않는다. JPA에서는 @Version 필드를 두면 수정할 때마다 버전이 증가하고, 조회 시점과 수정 시점의 버전이 다르면 예외가 난다. 충돌은 커밋할 때 알게 되므로 재시도 로직이 필요하다.
  • 비관적 락: 충돌이 잦다고 가정하고 DB 락을 건다. LockModeType.PESSIMISTIC_WRITE는 SELECT ... FOR UPDATE로 배타 락을 걸어, 다른 트랜잭션은 락이 풀릴 때까지 기다린다. 확실하지만 대기와 데드락 가능성을 감수해야 한다.

hudi.blog 글에 따르면 @Version은 Long, Integer, Short, Timestamp 타입에 쓸 수 있다. 충돌이 드문 일반적인 수정에는 낙관적 락, 재고 차감처럼 충돌이 잦고 실패 비용이 큰 곳에는 비관적 락을 고려한다.

  • https://sabarada.tistory.com/175
  • https://unluckyjung.github.io/db/2022/03/07/Optimistic-vs-Pessimistic-Lock/
  • https://jaehoney.tistory.com/159
  • https://velog.io/@yhlee9753/%EC%96%B8%EC%A0%9C-%EB%B9%84%EA%B4%80%EC%A0%81%EB%9D%BDpessimistic-lock%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B3%A0-%EC%96%B8%EC%A0%9C-%EB%82%99%EA%B4%80%EC%A0%81%EB%9D%BDoptimistic-lock%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%A0%EA%B9%8C
  • https://hudi.blog/jpa-concurrency-control-optimistic-lock-and-pessimistic-lock/
  • https://devbksheen.tistory.com/227
  • https://escapefromcoding.tistory.com/727
  • https://k3068.tistory.com/92
  • https://willbfine.tistory.com/576?category=971447
  • https://okky.kr/articles/1023929
  • https://stackoverflow.com/questions/129329/optimistic-vs-pessimistic-locking

Distributed Lock

서버가 여러 대이면 JVM 안의 synchronized로는 동시성을 제어할 수 없으므로, 모든 서버가 공유하는 저장소에 락을 둔다. Redis 문서는 단일 인스턴스 기준으로 SET resource_name my_random_value NX PX 30000을 소개한다. NX는 키가 없을 때만 설정하고, PX는 밀리초 단위 만료 시간을 건다. 값은 클라이언트와 요청마다 고유한 난수로 두고, 해제할 때는 “키의 값이 내가 넣은 값일 때만 삭제”하는 스크립트를 써야 다른 클라이언트의 락을 지우지 않는다. 여러 Redis 노드에 걸쳐 더 안전하게 락을 잡는 알고리즘으로 Redlock도 제안한다. 자바에서는 Redisson 같은 클라이언트가 이를 구현해 제공한다. 함께 저장한 채널톡 블로그 글은 지금 404를 반환한다.

  • https://redis.io/docs/manual/patterns/distributed-locks/
  • https://soyeon207.github.io/db/2021/08/29/distributed-lock.html
  • https://channel.io/ko/blog/distributedlock_2022_backend

Golang Crawler

Go언어를 빨리 습득하기 위한 방법으로 동료가 크롤러 개발을 추천해주었다. 예를 들면, 인스타그램에서 내가 팔로우 하는 사람들의 피드 및 게시물을 일괄적으로 다운로드하는 크롤러라던가, 특정 RSS를 구독해서 매일 스크랩 한다던가 하는 크롤러를 개발하면 빨리 늘 것 같다.

크롤러는 HTTP 요청, HTML 파싱, 동시성(고루틴과 채널), 에러 처리, 파일 저장을 한 번에 다뤄 볼 수 있어서 언어 연습용으로 좋다. Go에서는 Colly가 대표적인 스크래핑 프레임워크로, 크롤러, 스크레이퍼, 스파이더를 깔끔한 인터페이스로 작성할 수 있게 해 준다. 인스타그램처럼 로그인이 필요한 서비스는 이용 약관과 차단 정책이 있으므로, 개인 연습이라면 공개 RSS나 크롤링을 허용하는 사이트로 시작하는 편이 낫다.

  • https://blog.muabow.com/196
  • https://github.com/gocolly/colly
  • https://github.com/PuerkitoBio/gocrawl
  • https://github.com/topics/instagram-crawler
  • https://github.com/adw0rd/instagrapi
  • https://github.com/huaying/instagram-crawler

AWS Elastic Beanstalk

애플리케이션 코드를 올리면 EC2, 로드 밸런서, 오토 스케일링, 모니터링 같은 실행 환경을 대신 구성해 주는 AWS의 PaaS형 서비스다.

  • https://docs.aws.amazon.com/ko_kr/elasticbeanstalk/latest/dg/Welcome.html

ForkJoinPool vs CompletableFuture vs ExecutorService

ExecutorService는 작업을 스레드 풀에 제출하고 Future로 결과를 받는 기본 추상화다. Stack Overflow 답변의 표현처럼 서로 독립적인 요청 여러 개를 동시에 처리할 때 쓴다. ForkJoinPool은 큰 작업 하나를 잘게 쪼개 병렬로 처리하는 분할 정복용 풀이다. Baeldung에 따르면 각 워커 스레드가 자기 덱(deque)을 가지고, 할 일이 없는 스레드는 바쁜 스레드의 덱 끝에서 작업을 훔쳐 오는 work-stealing 알고리즘으로 부하를 나눈다. RecursiveTask, RecursiveAction으로 작업을 정의한다.

CompletableFuture는 실행기라기보다 비동기 결과를 조합하는 API다. thenApply, thenCompose, allOf로 작업을 이어 붙이고, Executor를 지정하지 않으면 ForkJoinPool.commonPool()에서 실행된다. Spring에서는 ThreadPoolTaskExecutor가 ExecutorService를 감싼 구현으로 @Async의 기본 실행기 역할을 한다.

  • https://stackoverflow.com/questions/33944471/forkjointask-vs-completablefuture
  • https://www.java-success.com/10-%E2%99%A6-executorservice-vs-forkjoin-future-vs-completablefuture-interview-qa/
  • https://www.baeldung.com/java-completablefuture
  • https://reflectoring.io/java-completablefuture/
  • https://www.geeksforgeeks.org/java-forkjoinpool-vs-executorservice/
  • https://www.baeldung.com/java-fork-join
  • https://hamait.tistory.com/612
  • https://stackoverflow.com/questions/21156599/javas-fork-join-vs-executorservice-when-to-use-which
  • https://erim1005.tistory.com/entry/ExecutorService%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-multi-thread-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0-Java
  • https://www.baeldung.com/java-executor-service-tutorial
  • https://docs.spring.io/spring-framework/docs/4.2.x/spring-framework-reference/html/scheduling.html

Query vs Command

Bertrand Meyer가 Eiffel 언어를 설계하며 제시한 CQS(Command Query Separation) 원칙이다. 메서드는 값을 반환하되 상태를 바꾸지 않는 쿼리이거나, 상태를 바꾸되 값을 반환하지 않는 커맨드여야 하고 둘을 섞지 말라는 것이다. 참고한 글은 파이썬의 max(l)을 쿼리, 리스트를 뒤집기만 하고 반환값이 없는 l.reverse()를 커맨드의 예로 든다. 쿼리는 몇 번을 불러도 안전하므로 마음 놓고 호출할 수 있다. 이 원칙을 메서드가 아니라 시스템의 읽기, 쓰기 모델 수준으로 넓힌 것이 CQRS다.

  • https://medium.com/eleven-labs/cqrs-pattern-c1d6f8517314
  • https://shoark7.github.io/programming/knowledge/command-and-query-method

Distributed Transaction

여러 DB나 서비스에 걸친 작업을 하나의 트랜잭션처럼 묶는 문제다. 고전적인 방법은 2단계 커밋(2PC)이다. 참고한 글의 설명대로 코디네이터가 첫 단계(prepare)에서 모든 참여 노드에 커밋 준비를 요청하고, 모두 성공하면 두 번째 단계(commit)에서 커밋을 지시한다. 자바에서는 X/Open XA 표준을 따르는 JTA 구현체(Atomikos, Bitronix 등)로 쓸 수 있다. 다만 모든 참여자가 응답할 때까지 락을 잡고 기다려야 하고, 코디네이터 장애에 약하며, 메시지 브로커나 NoSQL처럼 XA를 지원하지 않는 자원이 많다. 그래서 마이크로서비스에서는 로컬 트랜잭션과 보상 트랜잭션을 이어 붙이는 사가(saga) 패턴으로 최종 일관성을 맞추는 경우가 많다.

  • https://junhyunny.github.io/msa/design-pattern/distributed-transaction/
  • https://ko.wikipedia.org/wiki/%EB%B6%84%EC%82%B0_%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98
  • https://velog.io/@hgs-study/saga-1
  • https://lion-king.tistory.com/entry/MSA-distributed-transaction
참고한 자료외부 출처 7

외부 출처

이 글은 저작권자의 CC BY 4.0 라이선스를 따릅니다.

댓글

아직 댓글이 없습니다