RDB에서 Json 타입 다뤄보기

이정진·2024년 8월 27일
1

개발

목록 보기
20/21
post-thumbnail

과거 과릿을 개발하던 당시, Json 타입을 도입했다 다양한 이유들(Ex: 조건문, 정렬, 부분 수정 가능성 등)로 인해, Json 타입의 칼럼을 여러 개의 칼럼들로 분리하게 되었다.
최근, 다시 Json 타입을 사용할만한 상황이 생겼고, 과거와 같은 행동을 반복하지 않기 위해 관련된 내용을 찾고 정리한다.

Json 타입 사용하기

Spring Boot + MySQL

먼저, Json 타입을 어떻게 Spring Boot, MySQL 환경에서 사용할 수 있는지 정리해보려고 한다.

먼저, Json 타입을 아래와 같이 저장하고 사용하는 것을 목표로 한다고 봐보자.

{
  "city": "서울시",
  "division_list": [
    {
      "district": "광진구"
    },
    {
      "district": "마포구"
    }
  ]
}

DTO

public record LocationRequest(
    String city,
    List<DivisionRequest> divisionList
) {
    public record DivisionRequest(
            String district
    ) {
    }
}

Entity

@Getter
@Entity
@Table(name = "location")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Location {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "city", columnDefinition = "json")
    @JdbcTypeCode(SqlTypes.JSON)
    private City city;

    @Builder
    public Location(City city) {
        this.city = city;
    }
}

Entity에서 City라는 객체를 활용하고 있는 것을 볼 수 있다.
이는 아래와 같이 작성되어 있다.

public record City(
        String city,
        List<Division> divisionList
) {

    public record Division(
            String district
    ) {
    }
}

Json 타입을 명시하는 법
hibernate 버전이 6 미만일 경우, 아래와 같이 사용한다.

@TypeDef(name = "json", typeClass = JsonStringType.class)
public class Entity {
    @Column(name = "column", columnDefinition = "json")
    @Type(type = "json")
    private Json column;

hibernate 버전이 6 이상일 경우는, @JdbcTypeCode(SqlTypes.JSON)만 추가해주면 된다.

API 테스트하기

위와 같이, 설정한 이후에 Postman을 통해 API 요청을 보내면 아래와 같이 결과를 받아볼 수 있다.

자유도 높게 사용하는 법

Json 타입을 사용하는 예시를 보면서, 아래와 같은 궁금증을 가진 분들도 있을 것이다.
DTO의 변수명을 지정해 준다면, 사용자의 입력 값이 value에만 들어가게 되어 Json 타입을 제대로 활용하지 못하는 거 아닌가?
이러한 자유도는 아래와 같은 방식으로 개발하여 해결할 수 있다.

DTO

public record FreeLocationRequest(
        Map<String, List<String>> city
) {
}

Entity

@Getter
@Entity
@Table(name = "location")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Location {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "free_city", columnDefinition = "json")
    @JdbcTypeCode(SqlTypes.JSON)
    private Map<String, List<String>> freeCity;

    @Builder
    public Location(Map<String, List<String>> freeCity) {
        this.freeCity = freeCity;
    }
}

API 테스트하기

위와 같은 방식에서, Map<String, List<String>>이 아닌 Map<String, Obejct>로 활용한다면, 더 높은 자유도를 가질 수 있다. 다만, 인덱스를 거는 등의 성능을 개선하는 과정에서 이득을 보기에는 어렵기에, 사용하는 과정에서 많은 고민이 필요할 수 있다.

Json 톺아보기

MySQL 5.7.8 이후 버전부터 공식적으로 Json 타입이 지원되고 있지만, 기존에는 Json 형태의 데이터를 Text로 변환하여 저장하고, 활용해오기도 했었다.
그렇기에, 둘 중 어떤 타입을 사용하는 것이 더 좋을지를 기준점을 잡으면 앞으로 활용할 때 더욱 빠르게 판단할 수 있을 것이라 생각해, 과거 당근 기술블로그를 읽었던 기억이 나 관련 내용을 기반으로 정리해보려고 합니다.

Json 타입과 Text 타입의 차이점

인덱스

Text는 Json 타입의 정보를 문자열로 저장하기에, 데이터 내의 특정 필드를 기준으로 하는 인덱스를 걸 수 없다. 이와 반대로, Json 타입은 특정 필드를 인덱스로 활용할 수 있다.

Json 타입에 대해서 인덱스를 걸어줄 때는 직접 거는 방식과 가상 칼럼을 활용하는 방식 등 총 2가지 방식으로 적용할 수 있다.
샘플은, 위에서 다루었던 시/군/구 Json 데이터를 기준으로 하겠다.

직접 걸기

CREATE INDEX idx_city ON location ((CAST(city->>'$.city' AS CHAR(255))));

위와 같이 인덱스를 건 이후, 아래와 같이 인덱스를 타는 조회 쿼리를 EXPLAIN을 활용해보면, 인덱스를 타고 있는 것을 알 수 있다.

EXPLAIN SELECT * FROM location WHERE city->>'$.city' = '서울시';

가상 칼럼 활용하기

Json 타입에 인덱스를 걸어줄 때, GENERATED COLUMN을 사용해서 걸 수 있다.

ALTER TABLE location ADD COLUMN city_virtual VARCHAR(255) GENERATED ALWAYS AS (city->>'$.city') VIRTUAL;
CREATE INDEX city_idx ON location (city_virtual);

Q. GEREATED ALWAYS AS, VIRTUAL은 뭐야?
A. GENERATED ALWAYS AS 는 열이 생성된 열이라는 것을 의미한다. (city->>'$.city') 는 JSON 데이터에서 'city' 키의 값을 추출하는 표현식이다. VIRTUAL은 이 열이 실제로 저장되지 않고 필요할 때 계산됨을 나타냅니다.

실제로 저장하면서, 활용하고 싶다면 STORED를 사용하면 된다.

  • VIRTUAL: 실제로 저장되지 않고 필요 시에만 계산
  • STORED: 데이터가 실제로 저장되고 업데이트 시 계산 (조회 성능에서 상대적으로 유리)

MySQL에서는 두 가지 유형의 생성된 열이 있습니다:

VIRTUAL: 데이터는 저장되지 않고 조회 시 계산됩니다.
STORED: 데이터가 실제로 저장되고 업데이트 시 계산됩니다.

위와 같이 가상 칼럼을 생성하고 인덱스를 걸면, 아래와 같이 가상 칼럼이 생성된 것을 확인할 수 있다.

아래와 같이 인덱스를 타는 조회 쿼리를 EXPLAIN을 활용해보면, 인덱스를 타고 있는 것을 확인할 수 있다.

EXPLAIN SELECT * FROM location WHERE city_virtual = '서울시';

직접 인덱스와 가상 칼럼 장단점 비교

JSON에 직접 인덱스를 사용할 경우, 가지는 장점

  • 추가적인 칼럼 없이 JSON 데이터에 직접 접근할 수 있다.
  • 테이블 구조를 변경하지 않고도 인덱스를 추가할 수 있다.
  • 저장 공간을 약간 더 절약할 수 있다.

가상 칼럼 사용이 가져오는 장점

  • 쿼리 작성이 더 간단해질 수 있다.
  • JSON 데이터의 특정 부분을 별도의 칼럼처럼 다룰 수 있어 가독성이 좋다.
  • 다른 테이블과의 조인 등에서 활용하기 쉽다.

위와 같은 장점을 기반으로 했을 때, 나는 가상 칼럼을 활용하여 인덱스를 거는 것이 더 많은 효용성을 가진다고 생각한다.

In-place 업데이트

In-place 업데이트란 데이터베이스에서 데이터를 수정할 때, 해당 데이터가 저장된 원래 위치에서 직접 변경을 수행하는 방식으로, 데이터를 새로운 위치로 이동시키거나 전체 레코드를 다시 쓰지 않고, 필요한 부분만 효율적으로 수정하는 기법을 말한다.

Text타입은 전체 레코드를 다시 써야하지만, 이와 반대로 Json타입은 특정 필드의 데이터만 직접 변경할 수 있다. 즉, Json 타입은 In-place 업데이트를 사용할 수 있다.

이제, In-place 업데이트의 장점과 사용법 등을 정리한다.

In-place 업데이트 장점

  • 성능 향상: 전체 데이터를 다시 쓰지 않아 I/O 작업이 줄어든다.
  • 동시성 개선: 다른 트랜잭션의 블로킹 시간이 줄어든다.
  • 리소스 효율성: 메모리와 디스크 사용이 최적화된다.

Json타입은 어떻게 In-place 업데이트가 가능할까?

  • MySQL에서 Json타입 컬럼에 대해 내부적으로 Binary Json (=BSON) 저장 포맷으로 변환한다.
  • 키-값 쌍을 별도의 내부 구조로 저장하며에, 별도의 내부 구조에서는 데이터 유형, 오프셋, 길이 등의 메타 데이터도 포함된다.
  • JSON 내의 각 요소에 대해 약간의 추가 공간을 할당해놓아, 이 공간을 활용해 전체 문서를 재작성하지 않고 일부만 수정이 가능하도록 한다.
  • MySQL에서는JSON_SET, JSON_REPLACE, JSON_REMOVE 등의 함수를 제공하고 있다. (출처: MySQL Json Funtion Docs)

위와 같은 특징을 기반으로, MySQL에서는 Json타입에 대해 In-place 업데이트를 지원한다.

In-place 업데이트 쿼리

UPDATE mytable
SET json_column = JSON_SET(json_column, '$.key', 'new_value')
WHERE id = 1;

MySQL server-side에서 필드의 값을 조회 및 변경 가능

처음에 기술 블로그에서, 이 표현을 듣고 무슨 의도인지를 파악하는데 일부 시간이 걸렸다.
정리하자면, 아래와 같은 의미라고 판단했다.

어플리케이션(=API 서버)에서 데이터를 조회하여, 수정 후 재저장하는 방식이 아닌, MySQL 서버 내에서 JSON_SET, JSON_REPLACE, JSON_REMOVE 등의 함수를 활용해서, 직접 데이터를 원하는 방식으로 조회 및 변경이 가능하다는 의미이다.

다만, JPA(+ QueryDSL)에서는 Json 관련 함수를 지원하지 않으므로, Custom Function을 활용해서 사용하고자 하는 함수들을 정의해놓아야 한다는 점이 불편한 점으로 느껴진다.
jOOQ에서는 JSON 관련 함수를 지원하고 있는 것으로 확인되어, Raw SQL을 JPA 환경에서 사용하기 보단, jOOQ를 도입하는 것이 더 현명한 방식일 것 같다. (참고: jOOQ JSON functions)

Json 타입을 사용해야 하는 요건

당근에서 제시하는 요건과 내가 직접 사용해보며 겪은 Json 타입을 사용해야 하는 요건을 정리해본다.

당근에서 제시하는 요건

  • JSON 데이터의 특정 필드만 접근이 가능해야 한 경우
  • JSON 데이터의 특정 필드(고정 길이 필드)만 자주 업데이트되는 경우
  • JSON 데이터의 특정 필드로 인덱스 생성이 필요한 경우

내가 생각하는 요건

  • JSON 데이터의 특정 필드를 기반으로 조건/정렬 등의 필터링이 필요한 경우
  • JSON 데이터의 업데이트 빈도가 매우 낮을 경우
  • 부득이하게 NoSQL 형태의 데이터를 RDB 상황에서 같이 사용해야 할 경우

공작소에 Json 타입 적용기

위와 같이, 내용을 정리한 이후 실제로 Json 타입을 적용을 고민하는 상황이 생겼다. 해당 적용기는 적용 완료 후 재정리할 예정이다.

관련 코드는 Github(샘플코드/공작소)에서 확인할 수 있습니다.


레퍼런스

0개의 댓글