2023-06-12-TIL
2023-06-12-TIL
Today I Learned
GraphQL
server간 통신에서도 under fetching over fetching을 완화할 수 있다.
GraphQL은 클라이언트가 필요한 필드를 쿼리로 직접 지정하고, 서버는 스키마에 정의된 타입 시스템에 따라 그 모양 그대로 응답하는 API 질의 언어다. REST에서는 엔드포인트가 응답 모양을 정하므로 화면에 필요 없는 필드까지 받거나(over-fetching), 필요한 데이터를 모으려고 여러 엔드포인트를 호출해야 하는(under-fetching) 일이 생긴다. GraphQL은 한 요청에서 필요한 필드만, 연관 데이터까지 묶어서 가져오므로 두 문제를 줄인다. 프런트엔드와 백엔드 사이만이 아니라 서비스 사이의 호출에서도 같은 이점이 있다는 것이 위 메모의 요지다. 반대로 쿼리가 자유로운 만큼 HTTP 캐싱이 어렵고, 비싼 중첩 쿼리를 막기 위한 깊이나 복잡도 제한이 필요하다.
Over-fetching과 Under-fetching
링크한 글은 두 개념을 예시로 정리한다. 사용자 목록 화면에 이름만 필요한데 GET /users가 주소, 연락처까지 모두 내려주면 over-fetching이다. 사용자와 그 사용자의 게시글, 팔로워 수를 한 화면에 보여 주려고 GET /users/1, GET /users/1/posts, GET /users/1/followers를 차례로 불러야 하면 under-fetching이다. REST에서도 필드 선택 파라미터(?fields=name)나 화면 전용 엔드포인트(BFF)로 완화할 수 있지만, GraphQL은 이를 질의 언어 차원에서 해결한다.
- https://ivvve.github.io/2019/07/24/server/graphql/over-under-fetching/
JPA와 Hibernate의 연관관계 매핑
엔티티 사이의 관계는 다중성(@ManyToOne, @OneToMany, @OneToOne, @ManyToMany)과 방향(단방향, 양방향)으로 정한다. 테이블에서는 외래 키 하나로 양쪽을 오갈 수 있지만, 객체에서는 참조를 어느 쪽에 둘지 정해야 하고, 양방향이면 외래 키를 관리하는 연관관계의 주인을 하나로 정해야 한다(mappedBy가 없는 쪽). 실무에서는 @ManyToOne 단방향을 기본으로 두고, 반대 방향 탐색이 꼭 필요할 때만 @OneToMany(mappedBy = ...)를 더하는 방식이 흔히 권장된다.
Spring의 HttpMessageConverter
@RequestBody로 받은 요청 본문을 객체로 바꾸고, @ResponseBody나 ResponseEntity로 반환한 객체를 응답 본문으로 쓰는 일은 HttpMessageConverter가 맡는다. Spring MVC는 등록된 컨버터 목록을 순서대로 보며, 요청의 Content-Type이나 응답의 Accept 헤더와 대상 타입을 처리할 수 있는 컨버터를 고른다. JSON은 Jackson 기반 컨버터, 문자열은 StringHttpMessageConverter, 바이트 배열은 ByteArrayHttpMessageConverter가 처리한다. 커스텀 미디어 타입이 필요하면 WebMvcConfigurer의 configureMessageConverters나 extendMessageConverters로 컨버터를 추가한다.
- https://www.baeldung.com/spring-httpmessageconverter-rest
- Message Converters (Spring Framework Reference)
Sealed Class
Kotlin 문서에 따르면 sealed 클래스와 인터페이스는 상속 계층을 통제한다. 직접 하위 클래스는 모두 컴파일 시점에 알려지고, sealed 클래스가 정의된 모듈과 패키지 밖에서는 새 하위 클래스가 나타날 수 없다. 그래서 when 식에서 모든 하위 타입을 다루면 else 없이도 컴파일러가 빠짐없이 처리했는지 확인해 준다. 성공과 실패처럼 경우의 수가 정해진 결과를 타입으로 표현할 때 유용하다.
1
2
3
4
5
6
7
8
sealed interface Result<out T>
data class Success<T>(val value: T) : Result<T>
data class Failure(val error: Throwable) : Result<Nothing>
fun <T> render(result: Result<T>) = when (result) {
is Success -> "ok: ${result.value}"
is Failure -> "error: ${result.error.message}"
}
Java도 17부터 sealed와 permits로 같은 개념을 지원한다.
- https://kotlinworld.com/165
- https://kotlinlang.org/docs/sealed-classes.html
Rust의 Result 타입
Rust는 예외 대신 Result<T, E>로 실패 가능성을 반환 타입에 드러낸다. 표준 라이브러리 문서대로 Result는 성공 값 Ok(T)와 오류 값 Err(E) 두 변형을 가진 열거형이다. 호출하는 쪽은 match로 두 경우를 처리하거나, ? 연산자로 오류를 그대로 호출자에게 전파한다. 실패를 무시하고 넘어갈 수 없게 타입이 강제한다는 점이 checked exception과 비슷하지만, 오류도 평범한 값이라 map, and_then 같은 조합기로 이어 쓸 수 있다. 위의 sealed class로 만든 Result가 바로 이 모델을 흉내 낸 것이다.
- https://doc.rust-lang.org/std/result/
Resolver
같은 Resolver라는 이름이 맥락마다 다른 것을 가리켜서 정리했다.
- DNS resolver: 도메인 이름을 IP 주소로 바꾸기 위해 루트, TLD, 권한 있는 네임서버에 차례로 질의하고 결과를 캐시하는 서버나 클라이언트 구성 요소다.
- Spring의
HandlerMethodArgumentResolver: 링크한 velog 글이 다루는 것으로, 컨트롤러 메서드 파라미터를 만들어 넣는 구성 요소다. 세션 사용자 정보처럼 여러 컨트롤러에서 반복해서 꺼내는 값을 파라미터로 주입하는 데 쓴다. GraphQL resolver: 스키마의 각 필드 값을 실제로 가져오는 함수다.
- https://www.computerhope.com/jargon/d/dns-resolver.htm
- https://velog.io/@hsjung2015/Resolver%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80
JPA @Version 프로퍼티
@Version은 낙관적 락을 위한 버전 필드다. Vlad Mihalcea의 글에 따르면 엔티티를 수정하면 Hibernate는 UPDATE product SET quantity = ?, version = 1 WHERE id = 1 AND version = 0처럼 WHERE 절에서 읽었던 버전을 확인하고 SET 절에서 버전을 올린다. 그 사이 다른 트랜잭션이 먼저 수정해 버전이 바뀌었다면 갱신된 행이 0건이 되고, Hibernate는 StaleStateException을 JPA의 OptimisticLockException으로 감싸 던진다. 락을 잡지 않고 갱신 손실(lost update)을 막는 방식이라 충돌이 드문 경우에 적합하다. 충돌 시 재시도하거나 사용자에게 다시 시도하라고 알리는 처리가 필요하다.
- https://vladmihalcea.com/jpa-entity-version-property-hibernate/
Enum 타입 검증 메시지
Enum 타입의 요청값이 안 맞을때 커스텀 메시지를 응답해줄 수는 없을까?
JSON 본문의 enum 필드에 정의되지 않은 값이 오면 Jackson 역직렬화 단계에서 실패해 HttpMessageNotReadableException이 발생하므로, Bean Validation 메시지가 쓰일 기회가 없다. 방법은 전역 예외 처리기에서 이 예외의 원인(InvalidFormatException)을 꺼내 필드 이름과 허용 값을 담은 메시지를 만들거나, 필드를 문자열로 받아 커스텀 제약 애너테이션으로 검증하는 것이다. 쿼리 파라미터(@RequestParam)의 enum 변환 실패는 경로가 달라서 MethodArgumentTypeMismatchException이 발생하므로 따로 처리해야 한다. Baeldung 글은 이 경우 커스텀 Converter<String, Enum>을 등록하는 방법도 보여 준다.
- https://stackoverflow.com/questions/64575842/how-to-return-a-proper-validation-error-for-enum-types-in-spring-boot
- https://stackoverflow.com/questions/18205787/how-to-use-hibernate-validation-annotations-with-enums
- https://github.com/ekino/spring-boot-starter-errors/issues/125
- https://www.baeldung.com/spring-enum-request-param
대소문자를 구분하지 않는 Enum 매핑
Enum.valueOf는 대소문자를 구분하므로 ?status=active는 ACTIVE로 변환되지 않는다. 쿼리 파라미터는 Converter<String, Status>를 만들어 toUpperCase() 후 변환하도록 등록하면 되고, JSON 본문은 Jackson의 MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS를 켜면 된다(Spring Boot에서는 spring.jackson.mapper.accept-case-insensitive-enums=true). 두 경로의 변환 방식이 다르다는 점은 앞의 검증 메시지 문제와 같다.
- https://devwithus.com/enum-mapping-spring-boot/
댓글
아직 댓글이 없습니다