2023-02-23-TIL
2023-02-23-TIL
Today I Learned
@Transactional and Propagation
전파(propagation) 속성은 이미 트랜잭션이 진행 중일 때 새 @Transactional 메서드가 어떻게 참여할지를 정한다. Spring Propagation Javadoc의 정의를 옮기면 다음과 같다.
REQUIRED(기본값): 현재 트랜잭션이 있으면 참여하고, 없으면 새로 만든다.REQUIRES_NEW: 항상 새 트랜잭션을 만들고, 진행 중인 트랜잭션은 잠시 중단한다.NESTED: 현재 트랜잭션이 있으면 그 안의 중첩 트랜잭션(세이브포인트)으로 실행한다.SUPPORTS: 트랜잭션이 있으면 참여하고, 없으면 트랜잭션 없이 실행한다.MANDATORY: 트랜잭션이 반드시 있어야 하고, 없으면 예외를 던진다.NOT_SUPPORTED,NEVER: 트랜잭션 없이 실행한다.NEVER는 트랜잭션이 있으면 예외를 던진다.
REQUIRED로 참여한 내부 메서드에서 런타임 예외가 나면 바깥 트랜잭션 전체가 롤백 전용으로 표시된다. 바깥에서 예외를 잡아도 커밋 시점에 UnexpectedRollbackException이 나는 이유다. 실패해도 따로 남겨야 하는 이력 저장 같은 작업에는 REQUIRES_NEW를 쓴다.
- https://repo.yona.io/doortts/blog/issue/12
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Propagation.html
Problem Object
RFC 7807 “Problem Details for HTTP APIs”는 HTTP API의 에러 응답 본문 형식을 표준화한다. 미디어 타입은 application/problem+json이고, 문제 유형을 식별하는 URI인 type, 사람이 읽을 요약 title, 상태 코드 status, 이번 발생에 대한 설명 detail, 문제가 발생한 리소스를 가리키는 instance를 기본 필드로 두며, 필요한 필드를 더할 수 있다.
1
2
3
4
5
6
7
8
{
"type": "https://example.com/probs/out-of-credit",
"title": "You do not have enough credit.",
"detail": "Your current balance is 30, but that costs 50.",
"instance": "/account/12345/msgs/abc",
"balance": 30,
"accounts": ["/account/12345", "/account/67890"]
}
RFC의 예시로, 403 응답 본문에 기본 필드와 함께 balance, accounts 같은 확장 필드를 담았다.
Zalando의 problem 라이브러리는 이 형식을 자바 객체로 다루게 해 주고, Spring Framework 6.0도 ProblemDetail로 RFC 7807 지원을 넣었다. 각자 에러 형식을 설계하는 대신 표준을 따르면 클라이언트가 여러 API의 에러를 같은 방식으로 처리할 수 있다. 이 RFC는 이후 RFC 9457로 대체되었다.
- https://github.com/zalando/problem
- https://www.rfc-editor.org/rfc/rfc7807
API Throttling
Throttle이란 목, 파이프, 같은 어떤 관을 조르거나 조절하는 의미를 가지고 있다. 그리고 API Throttling이란, 요청에 속도와 횟수를 제한하는 것을 말한다. Rate Limit 기능이다. 보통 API Gateway에서 요청이 너무 많으면 429 Too Many Requests 응답을 발생시키도록 임계치를 조절할 수 있다.
AWS API Gateway 문서에 따르면 API Gateway는 토큰 버킷 알고리즘으로 요청을 조절한다. 토큰 하나가 요청 하나이고, 버킷에는 일정 속도(steady-state rate)로 토큰이 채워지며 버킷 크기만큼은 순간적인 폭주(burst)를 허용한다. 두 한도를 넘으면 클라이언트는 429를 받는다. 계정 단위, API와 스테이지 단위, 메서드 단위, 사용량 계획(usage plan)으로 클라이언트별 한도를 따로 걸 수 있다. 문서는 이 한도가 최선의 노력(best-effort) 기준으로 적용되므로 보장된 상한이 아니라 목표치로 봐야 한다고 덧붙인다.
- https://medium.com/@jaeyeoulahn/%EC%B4%88%EB%B3%B4%EA%B0%9C%EB%B0%9C%EC%9E%90-api-gateway-throttling-%EC%84%A4%EA%B3%84-%EB%B0%8F-%EA%B5%AC%ED%98%84-68cbe774dd5
- https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html
- https://www.tibco.com/reference-center/what-is-api-throttling
API Versioning
API를 바꾸면서 기존 클라이언트를 깨뜨리지 않기 위한 전략이다. xMatters 글은 대표적인 방식을 이렇게 나눈다.
- URI 경로:
/v1/orders. 가장 눈에 잘 띄고 라우팅이 쉬워서 많은 회사가 쓴다. - 쿼리 파라미터:
/orders?version=1. 간단하지만 요청을 버전별로 라우팅하기 어렵다. - 커스텀 헤더:
X-API-Version: 1. URL은 깔끔하지만 버전이 드러나지 않는다. - 콘텐츠 협상:
Accept: application/vnd.example.v1+json. 리소스 표현 단위로 버전을 매길 수 있지만 다루기 번거롭다.
어떤 방식을 고르든 하위 호환을 지키는 변경(필드 추가)은 버전을 올리지 않고, 깨지는 변경만 새 버전으로 내는 것이 공통 원칙이다.
- https://www.wallarm.com/what/what-is-api-versioning-standards-strategy
- https://blog.bitsrc.io/api-versioning-dos-and-don-ts-4bd9db073f2a
- https://www.digitalml.com/api-versioning-best-practices/
- https://stackoverflow.com/questions/389169/best-practices-for-api-versioning
- https://www.akana.com/blog/api-versioning
- https://www.xmatters.com/blog/blog-four-rest-api-versioning-strategies/
POST vs PUT vs PATCH
MDN 기준으로 정리하면 PUT은 대상 리소스를 요청 본문으로 생성하거나 통째로 교체하고, PATCH는 리소스의 일부를 수정하는 지시를 보낸다. 핵심 차이는 멱등성이다. 같은 요청을 여러 번 보내도 서버에 미치는 의도된 효과가 한 번 보낸 것과 같으면 멱등하다고 한다. PUT과 DELETE는 멱등하지만 POST와 PATCH는 멱등성이 보장되지 않는다. 예를 들어 카운터를 1 올리라는 PATCH는 보낼 때마다 결과가 달라진다. 멱등한 요청은 네트워크 오류로 도달 여부가 불확실할 때 안전하게 재시도할 수 있다는 점이 실무에서 중요하다. 저장해 둔 yona 저장소 글은 지금 접속되지 않는다.
- https://repo.yona.io/doortts/blog/issue/12
ERD Tools
dbdiagram.io는 DBML이라는 텍스트 문법으로 테이블과 관계를 적으면 ERD를 그려 주는 도구라서, 다이어그램을 코드처럼 버전 관리하기 좋다. Vertabelo(현재 Redgate Data Modeler) 블로그 글은 온라인 ERD 도구 여러 개를 비교한다.
- https://dbdiagram.io
- https://vertabelo.com/blog/er-diagram-tools/
Hibernate
Hibernate, JPA, Querydsl 조합이 정말 좋은 도구인지 묻는 글이다. 객체 중심으로 개발하고 단순한 CRUD를 줄여 주는 대신, 영속성 컨텍스트, 지연 로딩, 실제로 실행되는 SQL을 이해하지 못하면 N+1 같은 성능 문제를 만들기 쉽다는 점이 늘 따라붙는 논점이다.
- https://happy-coding-day.tistory.com/entry/Hibernate-JPA-Querydsl%EB%8A%94-%EA%B3%BC%EC%97%B0-%EC%A2%8B%EC%9D%80-%EB%8F%84%EA%B5%AC%EC%9D%BC%EA%B9%8C
Complex Query with JPA and QueryDSL
QueryDSL은 조건이 선택적으로 붙는 동적 쿼리에 강하다. 조건마다 BooleanExpression을 반환하는 메서드를 만들고 값이 없으면 null을 반환하게 하면, where()가 null 조건을 무시하므로 조합이 깔끔해진다. 다만 JPQL로 표현하기 어려운 쿼리(DB 전용 함수, 복잡한 서브쿼리, 윈도 함수 등)는 QueryDSL로도 한계가 있어서, 이런 부분은 네이티브 쿼리나 별도 SQL 도구로 처리하는 경우가 많다.
1
2
3
4
5
6
7
8
9
public List<Member> search(String name, Integer age) {
return queryFactory.selectFrom(member)
.where(nameEq(name), ageGoe(age))
.fetch();
}
private BooleanExpression nameEq(String name) {
return name != null ? member.name.eq(name) : null;
}
- https://www.inflearn.com/questions/40192/qdsl%EB%A1%9C-%EB%B3%B5%EC%9E%A1%ED%95%9C-%EC%BF%BC%EB%A6%AC%EB%A5%BC-%EC%96%B4%EB%8A%90%EC%A0%95%EB%8F%84-%EC%BB%A4%EB%B2%84-%EA%B0%80%EB%8A%A5%ED%95%9C%EA%B0%80%EC%9A%94
- https://kth990303.tistory.com/378
- https://gimquokka.github.io/spring%20data/Querydsl%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%B3%B5%EC%9E%A1%ED%95%9C-%EB%8F%99%EC%A0%81%EC%BF%BC%EB%A6%AC-%EA%B5%AC%ED%98%84%EA%B8%B0/
- https://velog.io/@peppermint100/JPA%EC%97%90%EC%84%9C-%EB%B3%B5%EC%9E%A1%ED%95%9C-%EC%BF%BC%EB%A6%AC-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0
CQRS Pattern
CQRS(Command Query Responsibility Segregation)는 데이터를 바꾸는 쓰기 모델과 데이터를 보여 주는 읽기 모델을 분리하는 패턴이다. Azure 문서는 읽기와 쓰기의 성능, 확장 요구가 서로 다른데 하나의 모델로 둘을 최적화하기 어렵다는 문제에서 출발한다. 분리하면 각 모델을 따로 최적화하고, 필요하면 읽기 전용 저장소를 두어 이벤트로 동기화할 수 있다. Martin Fowler는 반대로 대부분의 시스템에서 CQRS가 위험한 복잡도를 더한다고 경고하며, 읽기와 쓰기가 정말 다른 일부 영역에만 적용하라고 권한다.
- https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs
- https://microservices.io/patterns/data/cqrs.html
- https://martinfowler.com/bliki/CQRS.html
- https://always-kimkim.tistory.com/entry/cqrs-pattern
Saga Pattern
서비스마다 DB가 따로 있는 마이크로서비스에서는 하나의 분산 트랜잭션으로 여러 DB를 묶기 어렵다. microservices.io의 정의로 사가(saga)는 로컬 트랜잭션의 연속이다. 각 로컬 트랜잭션은 자기 DB를 갱신하고 메시지나 이벤트를 발행해 다음 로컬 트랜잭션을 시작시킨다. 중간에 비즈니스 규칙 위반으로 실패하면, 앞서 완료된 단계를 되돌리는 보상 트랜잭션(compensating transaction)을 차례로 실행한다. 조율 방식은 두 가지다.
- 코레오그래피(choreography): 각 서비스가 도메인 이벤트를 발행하고, 다른 서비스가 이를 구독해 다음 단계를 실행한다.
오케스트레이션(orchestration): 오케스트레이터가 각 참여자에게 실행할 로컬 트랜잭션을 지시한다.
- https://microservices.io/patterns/data/saga.html
- https://learn.microsoft.com/en-us/azure/architecture/reference-architectures/saga/saga
- https://azderica.github.io/01-architecture-msa/
- https://www.baeldung.com/cs/saga-pattern-microservices
Spring Batch Admin
스프링 배치에서 자체적으로 어드민 페이지를 제공하는 것 같다. 하지만 너무 옛날에 개발되었고 7~8년 전 이후로는 개발한 기록이 없어서 사용하기가 꺼려진다.
Spring Batch Admin은 Spring Batch 잡을 관리하고 모니터링하는 웹 UI와 REST API였다. GitHub 저장소는 spring-attic 조직으로 옮겨졌고, 2019년 3월에 보관(archive) 처리되어 읽기 전용이 되었다. 지금 같은 기능이 필요하면 다른 도구를 찾거나, 배치 메타데이터 테이블(BATCH_JOB_EXECUTION 등)을 조회하는 화면을 직접 만드는 쪽을 검토해야 한다.
- https://docs.spring.io/spring-batch-admin/2.x/
- https://docs.spring.io/spring-batch-admin/trunk/getting-started.html
- https://docs.spring.io/spring-batch-admin/2.x/reference/reference.xhtml
- https://github.com/spring-attic/spring-batch-admin
Redis Increments
Redis INCR은 키에 저장된 숫자를 1 증가시키는 명령이다. 키가 없으면 0으로 두고 증가시키며, 64비트 부호 있는 정수 범위로 제한된다. 단일 명령이 원자적으로 실행되므로 여러 서버가 동시에 증가시켜도 값이 꼬이지 않는다. Redis 문서는 이를 활용한 패턴으로 카운터와 rate limiter를 소개한다. 예를 들어 IP:현재초를 키로 INCR하고 EXPIRE를 걸어, 값이 한도를 넘으면 요청을 거절하는 식이다. 위의 API Throttling을 애플리케이션에서 직접 구현할 때 가장 단순한 방법이다.
- https://m.blog.naver.com/wideeyed/221987968760
- http://redisgate.kr/redis/command/incr.php
- https://redis.io/commands/incr/
댓글
아직 댓글이 없습니다