본문으로 건너뛰기
2026. 6. 28·약 4분

컬럼일까요, JSON 컬럼일까요?

개별 컬럼들과 JSON 묶음이 저울에 올라간 그림

필드 네 개를 저장해야 했습니다. 처음 든 생각은 당연히 "컬럼 네 개 추가"였는데, 리뷰에서 "JSON 컬럼 하나로 묶는 건 어떨까요"라는 제안을 받았습니다. 정규화가 정답 아니었나? 어렴풋이 "컬럼 추가는 로우마다 비용이 든다"는 이야기도 기억나는데, 그건 지금도 맞는 말일까? 조사해 보니 제가 알던 상식 절반은 유효 기간이 지나 있었습니다.

개별 컬럼과 JSON 컬럼, 무엇을 기준으로 갈라야 할까요?


이 글에서 다루는 내용​

필드를 개별 컬럼으로 둘지 JSON 컬럼으로 묶을지 판단하는 기준을 다룹니다. 컬럼 추가 시 기존 행을 다시 쓰는 비용과, 이후 값을 채우고 스키마를 관리하는 비용을 구분합니다. 이어서 DB의 조회·검증 요구, 노출을 통제할 단위, 백필과 변경 관리 비용을 비교합니다.





컬럼 추가 비용부터 확인하기​

컬럼을 추가할 때 항상 모든 행을 다시 쓰는 것은 아닙니다. DB와 변경 조건에 따라 메타데이터만 바꾸는 방식도 쓸 수 있습니다.

  • MySQL 8.0 계열: 지원 버전과 조건에 맞는 컬럼 추가는 ALGORITHM=INSTANT로 처리할 수 있습니다. 기존 행을 다시 쓰지 않고 메타데이터를 변경합니다.
  • PostgreSQL: nullable 컬럼 추가는 카탈로그만 변경합니다. PostgreSQL 11부터는 상수 디폴트가 있는 컬럼 추가도 테이블 재작성 없이 끝납니다.

적용 조건과 잠금 대기는 별도로 확인해야 합니다. 무중단 마이그레이션 글에서 이 제약을 다룹니다. 행을 다시 쓰지 않는다고 이후 운영 비용까지 사라지는 것은 아니므로, 컬럼 추가 비용만으로 JSON을 선택하기는 어렵습니다.




판단 기준 세 가지​

행 재작성 여부를 확인했다면 실제 사용 방식과 운영 비용을 비교합니다.

① DB가 그 필드를 알아야 하는가. 필터·정렬·인덱스·제약(constraint)이 필요한 필드는 개별 컬럼을 먼저 검토합니다. 필드의 타입과 조회 조건이 스키마에 드러나기 때문입니다. 애플리케이션이 통째로 읽고 쓰는 값이라면 JSON으로 묶을 수 있습니다.

JSON으로 묶을 때는 내부 필드의 검증을 어디에서 할지도 정해야 합니다. JSON 컬럼을 선언하는 것만으로 내부 필드 각각의 타입과 제약이 생기지는 않습니다. 나중에 조회 요구가 생기면 MySQL의 JSON 경로에 생성 컬럼(generated column)을 만들어 인덱스를 거는 방법도 검토할 수 있습니다.

② 노출을 묶음 단위로 통제하고 싶은가. 암호화·마스킹·삭제를 필드 하나하나가 아니라 블록 단위로 처리하고 싶다면 JSON 묶음이 유리합니다. 지우거나 잠글 때도 값 하나만 다루면 됩니다. 필드를 따로 떼어 두는 것이 오히려 관리를 어렵게 만드는 값이 여기 해당합니다.

③ 백필과 스키마 거버넌스 비용. JSON은 내부 필드를 추가해도 테이블 DDL을 생략할 수 있습니다. 다만 기존 JSON에 값을 채워야 한다면 백필은 여전히 필요하고, 동시 갱신 시 다른 필드를 덮어쓰지 않도록 해야 합니다. 개별 컬럼은 필드마다 마이그레이션 체인지셋·리뷰·배포가 따라붙습니다. 대신 그 절차를 거치므로 스키마만 보고도 어떤 필드가 있는지 알 수 있습니다. JSON 안에 무엇이 들었는지는 스키마만 봐서는 모릅니다.

프론트엔드 상태도 개별 구독과 파생 계산이 필요하면 나누고, 항상 함께 읽고 쓰면 객체로 묶습니다. 저장 단위는 그 데이터를 조회하고 변경하는 쪽의 요구로 정합니다.




DB 저장 형식을 어댑터에서 분리하기​

서두의 병역 필드 네 개(사항·종류·시작일·종료일)에 세 기준을 대보면 이렇게 읽힙니다. ① DB 레벨의 조회 조건으로 쓸 일이 잘 없습니다. 병역으로 필터링하는 화면을 떠올리기 어렵습니다. ② 개인의 사정이 함께 읽히는 값이라, 노출을 필드별로가 아니라 묶음 단위로 통제하는 편이 관리하기 쉽습니다. ③ JSON 내부 필드는 테이블 DDL 없이 추가할 수 있습니다. 다만 기존 행에도 값을 채워야 한다면 JSON 문서의 백필은 여전히 필요합니다. 세 기준이 모두 같은 방향을 가리키는 사례입니다.

이때 저장은 JPA의 컨버터가 맡게 됩니다. 값객체와 JSON 문자열 사이를 오가는, 필드 하나짜리 직렬화 계층입니다.

// DB 어댑터: 값객체 ↔ JSON 문자열을 오가는 컨버터. 모델은 이 사정을 모른다
@Convert(converter = MilitaryServiceConverter::class)
@Column(name = "military_service")
val military: MilitaryService = MilitaryService()

도메인 모델, API 응답, DB 저장 형식은 서로 독립적으로 설계할 수 있습니다. 도메인 안에서 병역은 정합성 규칙을 가진 값객체고, 공개 API에서는 flat한 4개 필드며, DB에서만 JSON 문자열입니다.

"모른다"와 "해당 없다"는 다른 값입니다. UNKNOWN은 미입력 상태이고 NOT_APPLICABLE은 사용자가 선택한 값이므로, 응답과 집계에서도 구분해야 합니다. TypeScript에서 undefined와 null을 구분해 쓰는 것과 같은 문제입니다.

다음 글에서는 스키마 변경 이력을 마이그레이션 파일로 관리하는 방법을 살펴봅니다.




FE ↔ BE 대응표​

이 글에서 다룬 대응입니다.

BE 개념FE에서 가장 가까운 것대응의 핵심
개별 컬럼 vs JSON 컬럼정규화된 atom들 vs 상태 객체그 단위로 일하는 쪽이 누구인가
저장 형태의 독립성스토리지 포맷 ≠ 앱 상태 모양표현들이 서로를 구속하지 않는다
UNKNOWN ≠ NOT_APPLICABLEundefined ≠ null미입력과 명시적 값의 구분



References​

컬럼 추가 비용

  1. MySQL 8.0 Reference Manual: Online DDL Operations (Instant ADD COLUMN)
  2. PostgreSQL Documentation: ALTER TABLE (Notes)

JSON 컬럼

  1. MySQL 8.0 Reference Manual: The JSON Data Type


좋은 사람들과 재미있는 일을 하며 열정적이고 즐겁게 살고 싶은 개발자