과거 과릿을 개발하던 당시, Json 타입을 도입했다 다양한 이유들(Ex: 조건문, 정렬, 부분 수정 가능성 등)로 인해, Json 타입의 칼럼을 여러 개의 칼럼들로 분리하게 되었다.
최근, 다시 Json 타입을 사용할만한 상황이 생겼고, 과거와 같은 행동을 반복하지 않기 위해 관련된 내용을 찾고 정리한다.
먼저, Json 타입을 어떻게 Spring Boot, MySQL 환경에서 사용할 수 있는지 정리해보려고 한다.
먼저, Json 타입을 아래와 같이 저장하고 사용하는 것을 목표로 한다고 봐보자.
{
"city": "서울시",
"division_list": [
{
"district": "광진구"
},
{
"district": "마포구"
}
]
}
public record LocationRequest(
String city,
List<DivisionRequest> divisionList
) {
public record DivisionRequest(
String district
) {
}
}
@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)
만 추가해주면 된다.
위와 같이, 설정한 이후에 Postman을 통해 API 요청을 보내면 아래와 같이 결과를 받아볼 수 있다.
Json 타입을 사용하는 예시를 보면서, 아래와 같은 궁금증을 가진 분들도 있을 것이다.
DTO의 변수명을 지정해 준다면, 사용자의 입력 값이 value에만 들어가게 되어 Json 타입을 제대로 활용하지 못하는 거 아닌가?
이러한 자유도는 아래와 같은 방식으로 개발하여 해결할 수 있다.
public record FreeLocationRequest(
Map<String, List<String>> city
) {
}
@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;
}
}
위와 같은 방식에서,
Map<String, List<String>>
이 아닌Map<String, Obejct>
로 활용한다면, 더 높은 자유도를 가질 수 있다. 다만, 인덱스를 거는 등의 성능을 개선하는 과정에서 이득을 보기에는 어렵기에, 사용하는 과정에서 많은 고민이 필요할 수 있다.
MySQL 5.7.8 이후 버전부터 공식적으로 Json 타입이 지원되고 있지만, 기존에는 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에 직접 인덱스를 사용할 경우, 가지는 장점
가상 칼럼 사용이 가져오는 장점
위와 같은 장점을 기반으로 했을 때, 나는 가상 칼럼을 활용하여 인덱스를 거는 것이 더 많은 효용성을 가진다고 생각한다.
In-place 업데이트란 데이터베이스에서 데이터를 수정할 때, 해당 데이터가 저장된 원래 위치에서 직접 변경을 수행하는 방식으로, 데이터를 새로운 위치로 이동시키거나 전체 레코드를 다시 쓰지 않고, 필요한 부분만 효율적으로 수정하는 기법을 말한다.
Text타입은 전체 레코드를 다시 써야하지만, 이와 반대로 Json타입은 특정 필드의 데이터만 직접 변경할 수 있다. 즉, Json 타입은 In-place 업데이트를 사용할 수 있다.
이제, In-place 업데이트의 장점과 사용법 등을 정리한다.
JSON_SET
, JSON_REPLACE
, JSON_REMOVE
등의 함수를 제공하고 있다. (출처: MySQL Json Funtion Docs)위와 같은 특징을 기반으로, MySQL에서는 Json타입에 대해 In-place 업데이트를 지원한다.
UPDATE mytable
SET json_column = JSON_SET(json_column, '$.key', 'new_value')
WHERE id = 1;
처음에 기술 블로그에서, 이 표현을 듣고 무슨 의도인지를 파악하는데 일부 시간이 걸렸다.
정리하자면, 아래와 같은 의미라고 판단했다.
어플리케이션(=API 서버)에서 데이터를 조회하여, 수정 후 재저장하는 방식이 아닌, MySQL 서버 내에서 JSON_SET
, JSON_REPLACE
, JSON_REMOVE
등의 함수를 활용해서, 직접 데이터를 원하는 방식으로 조회 및 변경이 가능하다는 의미이다.
다만, JPA(+ QueryDSL)에서는 Json 관련 함수를 지원하지 않으므로, Custom Function을 활용해서 사용하고자 하는 함수들을 정의해놓아야 한다는 점이 불편한 점으로 느껴진다.
jOOQ에서는 JSON 관련 함수를 지원하고 있는 것으로 확인되어, Raw SQL을 JPA 환경에서 사용하기 보단, jOOQ를 도입하는 것이 더 현명한 방식일 것 같다. (참고: jOOQ JSON functions)
당근에서 제시하는 요건과 내가 직접 사용해보며 겪은 Json 타입을 사용해야 하는 요건을 정리해본다.
당근에서 제시하는 요건
내가 생각하는 요건
위와 같이, 내용을 정리한 이후 실제로 Json 타입을 적용을 고민하는 상황이 생겼다. 해당 적용기는 적용 완료 후 재정리할 예정이다.
레퍼런스