포스트

2023-02-04-TIL

2023-02-04-TIL

Today I Learned

Go 1.20 릴리스 노트, 리액티브 ORM, HikariCP 데드락, 빌더 패턴, 분산 추적, 서킷 브레이커, 그리고 Spring 트랜잭션과 AOP 위빙을 정리했다.


Go 1.20 Release Notes

Go 1.20은 2023년 2월 1일에 릴리스되었다. 릴리스 노트에서 눈에 띄는 변경은 다음과 같다.

  • 슬라이스를 배열로 바로 변환할 수 있다. Go 1.17에서 배열 포인터로의 변환이 생겼고, 1.20부터 *(*[4]byte)(x) 대신 [4]byte(x)로 쓸 수 있다.
  • go build, go install 등이 -pgo 플래그로 프로파일 기반 최적화(PGO)를 지원한다. 실행 중에 수집한 CPU 프로파일을 컴파일러에 넘겨 자주 실행되는 경로를 최적화한다.
  • 에러 래핑이 확장되었다. fmt.Errorf에서 %w를 여러 번 쓸 수 있고, 여러 에러를 묶는 errors.Join이 추가되었다.
  • net/http에 ResponseController 타입이 추가되었다.

  • https://go.dev/doc/go1.20

Hibernate Reactive

기본적으로 JPA는 동기방식으로 동작한다. 따라서 스프링 환경에서 멀티스레딩의 비동기 방식으로 ORM을 사용하고 싶다면 Hibernate Reactive같은 ORM을 R2DBC와 조합해서 사용해야한다.

여기서 하나 바로잡으면, Hibernate Reactive가 쓰는 드라이버는 R2DBC가 아니다. 프로젝트 README에 따르면 Hibernate Reactive는 논블로킹 DB 드라이버를 쓰는 Hibernate ORM의 리액티브 API로, Quarkus나 Vert.x 같은 리액티브 환경을 주요 대상으로 하며 DB 접근에는 Vert.x 리액티브 SQL 클라이언트를 쓴다. Spring 쪽에서 R2DBC를 쓰려면 ORM이 아닌 Spring Data R2DBC를 쓰게 되고, 링크된 글처럼 기존 JPA와 R2DBC를 한 애플리케이션에서 함께 설정하는 방법도 있다.

  • https://hibernate.org/reactive/
  • https://jiwhunkim.github.io/develop/2020/02/01/spring-using-together-jpa-and-r2dbc.html
  • https://www.baeldung.com/spring-data-r2dbc

HikariCP Deadlock

우아한형제들 글은 커넥션 풀 크기, DB에 쓰는 스레드 수, 작업 하나가 동시에 필요로 하는 커넥션 수의 관계를 몰라 HikariCP에서 데드락이 발생한 사례를 다룬다. 스레드들이 이미 커넥션을 하나씩 잡은 채로 두 번째 커넥션을 기다리면, 풀이 바닥나 아무도 진행하지 못한다. 글은 Spring Boot 2에서 @GeneratedValue(특히 GenerationType.SEQUENCE, AUTO)를 쓸 때 ID 생성을 위해 추가 커넥션이 필요한 경우를 원인으로 짚는다.

HikariCP 위키의 풀 크기 문서는 데드락을 피하는 최소 풀 크기를 다음 식으로 제시한다. Tn은 최대 스레드 수, Cm은 스레드 하나가 동시에 잡는 최대 커넥션 수다. 다만 위키는 풀을 키우기 전에 애플리케이션 수준에서 한 스레드가 여러 커넥션을 잡지 않도록 고치는 것을 먼저 검토하라고 권한다.

1
pool size = Tn x (Cm - 1) + 1
  • https://techblog.woowahan.com/2664/

OLTP vs OLAP

OLTP(Online Transaction Processing)는 주문, 결제처럼 짧은 트랜잭션을 많이 처리하는 운영 시스템이고, OLAP(Online Analytical Processing)은 대량의 이력 데이터를 집계해 분석하는 시스템이다. OLTP는 정규화된 스키마와 행 단위 읽기·쓰기에, OLAP은 비정규화된 스키마와 컬럼 단위 대량 읽기에 최적화된다. 운영 DB에서 무거운 분석 쿼리를 돌리지 않고 데이터 웨어하우스로 복제해 분석하는 이유가 여기에 있다.

  • https://www.stitchdata.com/resources/oltp-vs-olap/

@Builder and Builder Pattern

빌더 패턴은 생성자 파라미터가 많을 때 이름 있는 메서드로 값을 하나씩 지정한 뒤 build()로 객체를 만드는 생성 패턴이다. 어떤 값에 무엇을 넣는지 드러나고 선택적 파라미터를 다루기 쉽다. 팩터리 패턴이 “어떤 구현체를 만들지”를 숨긴다면, 빌더는 “복잡한 객체를 어떤 단계로 조립할지”를 다룬다.

Lombok @Builder는 이 코드를 자동 생성한다. JPA 엔티티에 쓸 때는 클래스 대신 필요한 필드만 받는 생성자에 붙여, ID나 연관관계 컬렉션처럼 외부에서 지정하면 안 되는 필드가 빌더에 노출되지 않게 하는 것이 일반적인 권장이다. 클래스에 붙이면 필드 초기값이 무시되므로 @Builder.Default가 필요하다는 점도 자주 걸린다.

  • https://junha.tistory.com/25
  • https://www.baeldung.com/lombok-builder
  • https://www.youtube.com/watch?v=FPeOxVtpHDA
  • https://www.tutorialspoint.com/design_pattern/builder_pattern.htm
  • https://refactoring.guru/design-patterns/builder/java/example#example-0–Demo-java
  • https://www.digitalocean.com/community/tutorials/builder-design-pattern-in-java
  • https://betterprogramming.pub/understanding-the-builder-design-pattern-f4f56fa18c9
  • https://medium.com/javarevisited/design-patterns-101-factory-vs-builder-vs-fluent-builder-da2babf42113
  • https://www.geeksforgeeks.org/builder-design-pattern/
  • https://stackoverflow.com/questions/757743/what-is-the-difference-between-builder-design-pattern-and-factory-design-pattern
  • https://stackoverflow.com/questions/31885326/best-practice-and-implementation-of-a-builder-pattern-when-using-jpa

Java Lombok Best Practices

Lombok, JUnit 5, Mockito를 함께 쓸 때의 관례를 정리한 글이다. Lombok은 반복 코드를 줄여 주지만, @Data처럼 많은 것을 한꺼번에 생성하는 애너테이션은 엔티티에서 의도치 않은 setter와 equals/hashCode를 만들기 때문에 필요한 애너테이션만 골라 쓰는 편이 안전하다.

  • https://zhenye-na.github.io/blog/2022/04/23/java-best-practices-lombok-junit5-and-mockito.html

Redis

build-your-own.org는 Redis, 데이터베이스, 컴파일러, 웹 서버 같은 소프트웨어를 처음부터 직접 구현하며 배우는 책을 모아 둔 사이트다. Redis 편은 소켓 프로그래밍, 이벤트 루프, 해시 테이블 같은 내부 구조를 직접 만들어 보며 Redis가 왜 빠른지를 이해하는 데 도움이 된다.

  • https://build-your-own.org/

Transaction ID

Spring에서 현재 코드가 트랜잭션 안에서 실행 중인지는 TransactionSynchronizationManager.isActualTransactionActive()로 확인할 수 있다. Javadoc에 따르면 현재 스레드가 실제 트랜잭션과 연결되어 있는지를 돌려준다. 트랜잭션이 기대대로 적용되는지 로그나 테스트로 확인할 때 유용하다. 함께 저장한 Okta 지원 문서는 제목처럼 이벤트 로그의 transaction ID를 찾고 활용하는 방법에 관한 것으로, DB 트랜잭션과는 다른 의미의 ID다.

  • https://support.okta.com/help/s/article/How-to-find-the-transaction-ID-in-an-event-log-what-is-it-used-for-when-and-how-to-use-it?language=en_US
  • https://www.baeldung.com/spring-transaction-active

Zipkin and Sleuth

분산 추적(distributed tracing)은 요청 하나가 여러 서비스를 거칠 때 전체 경로를 하나의 trace ID로 묶어 보는 기법이다. 각 구간은 span으로 기록되고, Sleuth는 trace ID와 span ID를 만들어 로그와 HTTP 헤더에 실어 전파하며, Zipkin은 이 데이터를 수집해 시각화한다. Feign 클라이언트를 쓰면 Sleuth가 헤더 전파를 자동으로 처리한다.

다만 Spring Cloud Sleuth 프로젝트 페이지에 따르면 Sleuth의 마지막 마이너 버전은 3.1이고 Spring Boot 3.x에서는 동작하지 않는다. 핵심 기능은 Micrometer Tracing으로 옮겨졌으므로 Boot 3 이상에서는 Micrometer Tracing을 써야 한다.

  • https://twofootdog.tistory.com/65
  • https://velog.io/@dnjscksdn98/Spring-Boot-MSA-Sleuth-and-Zipkin-Distributed-tracing
  • https://docs.spring.io/spring-cloud-sleuth/docs/current-SNAPSHOT/reference/html/integrations.html
  • https://stackoverflow.com/questions/48211360/how-to-implement-sleuth-tracing-with-feign-builder
  • https://sarc.io/index.php/cloud/2190-sleuth-zipkin#:~:text=Sleuth%EB%9E%80%3F,%EC%9D%B4%EB%A5%BC%20Zipkin%EC%97%90%20%EC%A0%84%EB%8B%AC%ED%95%9C%EB%8B%A4.
  • https://www.baeldung.com/spring-cloud-sleuth-get-trace-id

Circuit Breaker Pattern

서킷 브레이커는 실패가 반복되는 외부 호출을 잠시 차단해 장애가 연쇄적으로 퍼지는 것을 막는 패턴이다. Azure 아키텍처 문서는 이를 세 상태의 상태 기계로 설명한다.

  • Closed: 요청을 그대로 보내고 최근 실패 횟수를 센다. 일정 시간 안의 실패가 임계값을 넘으면 Open으로 바뀌고 타이머를 시작한다.
  • Open: 요청을 보내지 않고 즉시 예외를 돌려준다. 타이머가 끝나면 Half-Open으로 바뀐다.
  • Half-Open: 제한된 수의 요청만 통과시켜 본다. 성공하면 Closed로 돌아가고, 실패하면 다시 Open이 된다.

Spring 환경에서는 Resilience4j가 이 패턴을 구현한다.

  • https://learn.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker

Aspect Weaving

위빙이란 Aspect 클래스에 정의 한 Advice 로직을 타깃에 적용하는 것을 의미한다. 쉽게 말해서 공통코드(advice)를 핵심 로직 코드에 삽입하는 것이 위빙이다.

위빙 시점은 세 가지다. 컴파일 시점 위빙은 AspectJ 컴파일러가 바이트코드에 직접 어드바이스를 넣고, 로드 시점 위빙(LTW)은 클래스 로더가 클래스를 읽어 들일 때 바이트코드를 조작하며, 런타임 위빙은 프록시 객체를 만들어 호출을 가로챈다. Spring AOP는 런타임 프록시 방식이라 public 메서드 호출에만 적용되고 자기 호출에는 적용되지 않는다. 필드 접근이나 생성자까지 가로채야 하면 AspectJ 위빙이 필요하다.

  • https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=tmondev&logNo=220564638014

Spring Transaction

선언적 트랜잭션이란, 트랜잭션 시작과 커밋, 롤백 코드를 비즈니스 로직에 직접 쓰지 않고 @Transactional 같은 애너테이션이나 설정으로 트랜잭션 경계를 선언하는 방식이다. 앞의 위빙과 연결하면, Spring은 이 선언을 보고 프록시를 만들어 메서드 호출 앞뒤에 트랜잭션 처리를 끼워 넣는다. 반대로 TransactionTemplate이나 PlatformTransactionManager를 코드에서 직접 호출하는 방식을 프로그래밍 방식 트랜잭션이라고 한다.

  • https://sigridjin.medium.com/spring-transaction-%EA%B4%80%EB%A6%AC%EC%97%90-%EB%8C%80%ED%95%9C-%EB%A9%94%EB%AA%A8-f391fd2885b4
  • https://shxrecord.tistory.com/213
  • https://devmg.tistory.com/m/250

Spring @Transactional and Snapshot

@Transactional의 propagation은 이미 트랜잭션이 있을 때 새 메서드가 그 트랜잭션에 참여할지(REQUIRED, 기본값), 새로 시작할지(REQUIRES_NEW) 등을 정하고, isolation은 동시에 실행되는 트랜잭션끼리 서로의 변경을 얼마나 보게 할지를 정한다. Spring 문서상 기본 격리 수준은 ISOLATION_DEFAULT로 DB의 기본값을 따른다. MySQL InnoDB의 기본인 REPEATABLE READ에서는 일관된 읽기를 위해 트랜잭션 안의 첫 읽기 시점 스냅샷을 계속 보게 된다.

  • https://www.baeldung.com/spring-transactional-propagation-isolation

Multi Module vs MSA

멀티 모듈은 하나의 저장소 안에서 코드를 모듈로 나눠 의존 방향을 빌드 수준에서 강제하는 방법이고, MSA는 서비스를 독립적으로 배포하고 운영하는 아키텍처다. 우아한형제들의 멀티 모듈 글은 공통 코드를 common 모듈 하나에 모았다가 겪은 문제(글에서는 “common의 저주”라고 부른다)를 소개하고, 역할별로 모듈을 나누는 기준을 제시한다. 멀티 모듈로 경계를 먼저 다져 두면, 필요할 때 특정 모듈을 별도 서비스로 분리하기 쉬워진다.

  • https://velog.io/@vpdls1511/Multi-Module-VS-MSA
  • https://hyeon9mak.github.io/woowahan-multi-module/
  • https://letsmakemyselfprogrammer.tistory.com/60
  • https://angryfullstack.tistory.com/entry/%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%EC%86%8C%ED%94%84%ED%8A%B8-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98MSA-3%ED%8E%B8-%EB%A9%80%ED%8B%B0-%EB%AA%A8%EB%93%88Multi-Module
  • https://jojoldu.tistory.com/444
  • https://github.com/always0ne/springboot-multimodule-msa
  • https://www.daddyprogrammer.org/post/13156/spring-boot-change-multi-module/
  • https://anaog.tistory.com/m/25
  • https://techblog.woowahan.com/2637/
이 글은 저작권자의 CC BY 4.0 라이선스를 따릅니다.

댓글

아직 댓글이 없습니다