포스트

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로 컨버터를 추가한다.


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/
이 글은 저작권자의 CC BY 4.0 라이선스를 따릅니다.

댓글

아직 댓글이 없습니다