@Lob 붙였는데 왜 저장이 안 돼? — 데이터 컬럼 자료형 (TEXT, VARCHAR)

성유진·2025년 7월 1일

공공데이터 포털에서 식품의약품안전처_의약품 제품 허가정보 API를 불러와서 MySQL 데이터베이스에 의약품 정보를 저장하는 작업을 진행하는 과정에서 발생한 오류를 정리하였다.

작성하다 보니 너무 장황해져서 해결한 문제 상황과, 그에 따라서 학습한 내용을 먼저 정리하여 소개하면 다음과 같다.

요약

문제 상황 및 해결 방안

  • API 응답 데이터가 커서 저장에 실패한 컬럼의 자료형을 TEXT, MEDIUMTEXT, VARCHAR(500) 로 변경하여 해결
  • 저장 실패한 컬럼은 수정하여, 페이지 단위 재호출 로직으로 API 데이터 45,048건 전체 저장 완료

주요 학습 개념

데이터 컬럼 자료형, LOB 타입

  • @Lob을 선언하면 TINYTEXT로 매핑됨
  • @Column의 columnDefinition 속성에 자료형 명시 가능
  • TEXT, MEDIUMTEXT는 LOB 타입 → 큰 데이터는 행 외부에 저장
  • TEXT(LOB 타입 자료형)와 VARCHAR는 성능 및 저장 방식 차이 존재 (메모리 할당, 인덱스 등)

문제 상황 - 데이터 저장 실패

데이터베이스에 의약품 정보를 저장하는 작업을 진행하는 과정에서 ee_doc_data 컬럼에 저장하려는 데이터가 너무 커서 저장에 실패했다는 오류가 발생했다.


@Lob 어노테이션을 달아서 entity를 생성해서 문제가 없을 것이라고 생각했는데 아니었나보다
사실 뭘 알고 붙인게 아니라 지피티한테 물어보고 그냥 붙였다,,, 하핳

반성하면서 왜 발생한 문제인지 스스로 알아보면서 해결해보자.

문제 분석 - 자료형 확인

우선, DB에 접속해서 테이블 정보를 실제로 확인해보았다.
SHOW CREATE TABLE new_medicine; 명령어의 실행 결과가 아래와 같았다.
문제가 되었던 컬럼인 ee_doc_data 컬럼의 자료형은 TINYTEXT로 선언된 것을 확인할 수 있었다.
추가로 utf8mb4로 인코딩된다는 것도 확인할 수 있었다.

TINYTEXT 저장할 수 있는 크기가 몇인지 살펴보자.
255byte이다.
text라서 큰 줄 알았는데, 정말 tiny하다.

자료형최대 크기 (바이트)저장 가능한 문자 수 예시 (인코딩에 따라 달라짐)
TINYTEXT255 bytes약 63~255자 (utf8mb4 기준 1자 = 최대 4byte)
TEXT65,535 bytes (64KB)약 16,000~65,535자 (utf8mb4 기준)
MEDIUMTEXT16,777,215 bytes (16MB)수백만 자 (인코딩 따라 상이)
LONGTEXT4,294,967,295 bytes (4GB)거의 무제한에 가까운 문자 수

한글 기준으로 몇 글자 저장 가능?

그래서 한글 데이터를 저장하는 거면 그럼 몇 글자 정도를 저장할 수 있는 것인가?
위에서 SHOW CREATE TABLE new_medicine;의 결과에서 utf8mb4로 인코딩 된 것을 확인했었다.

utf8mb4
문자 하나당 utf8mb3은 최대 3바이트를 사용하고, utf8mb4는 최대 4바이트를 사용한다.
utf8mb3은 Basic Multilingual Plane(BMP), 즉 유니코드에 해당하는 문자만 저장 가능하다.
utf8mb4는 이모지도 저장 가능하다. 이모지의 경우 4바이트로 저장된다.

아무튼 그래서 한글이면 몇 바이트인가?
지금처럼 utf8mb4로 인코딩하는 경우라면, 문자 하나의 크기가 3~4byte이기 때문에
TINYTEXT 사용한다면 최대 63~85길이의 글자만 저장가능한 것이다.

의약품마다 천차만별이긴 하지만, 조금 긴 효능효과 데이터(ee_doc_data)의 글자수를 확인해보았다.
역시나 TINYTEXT로는 택도 없다.


해결 과정 - 컬럼 자료형 변경

그러면 이제 문제가 되었던 컬럼의 자료형을 변경하려 한다.
결과부터 말하면 처음 발견하여 문제가 되었던
ee_doc_data 컬럼의 자료형은 TINYTEXT → MEDIUMTEXT로 변경하였다.

추가로, item_name 컬럼에서도 같은 문제가 발생하여 varchar(255) -> VARCHAR(500)으로 변경하였다.
TEXT가 아닌 VARCHAR(500)으로 변경한 이유를 뒤에서 확인 가능하다.

+) 컬럼 크기 변경 가능 여부 확인

이 때, '이미 만들어진 테이블의 컬럼 크기를 변경 가능한가?' 라는 걱정이 들었다.
다행히 가능하다!
ALTER TABLE new_medicine MODIFY COLUMN ee_doc_data TEXT; 명령어를 실행하여 아래와 같이 변경된 것을 확인 할 수 있었다.

자료형 결정 근거

자 다시 돌아와서...
TINYTEXT보다 더 큰 자료형이어야 할테니, TEXT 또는 MEDIUMTEXT 중에 선택하려고 한다.
이미 데이터의 크기가 큰 것을 확인했고, 데이터를 저장하는 과정에서 중간에 실패하면 귀찮다...는 이유로 이미 MEDIUMTEXT로 마음이 기울긴 했다.

그래도 조금 더 논리적으로 접근해보자. 성능상에 차이가 있을까?
이에 대한 답을 얻기 위해서 개념 공부를 좀 해보자.

LOB(Large Object) 타입

우선, TEXT, MEDIUMTEXT는 모두 LOB 타입의 데이터이다.

그래서 LOB가 무엇이냐 하면
Large Object의 약자로, 크기가 큰 데이터를 저장하는 데 사용되는 타입이다.

RDBMS는 일반적으로 데이터를 행 내부에 저장한다. 정확히는, B-Tree에 저장한다. 이를 Inline 저장이라고 한다.
하지만 LOB 타입처럼 용량이 큰 데이터는 행 외부에 저장하고 참조한다. 즉, B-Tree 외부의 Off-Page 페이지에 저장된다.

VARCHAR와 TEXT

행 내부에 저장되는 데이터와 외부에 저장되는 데이터를 비교해보기 위해서 VARCHAR와 TEXT를 비교해서 살펴보고자 한다.

저장 방식과 인덱스
B-Tree에 저장되는 VARCHAR는 인덱스를 만들 수 있지만, LOB 타입은 인덱스를 만들 수 없다고 이야기하는 경우도 있다.
그러나 MySQL은 LOB타입의 데이터라고 무조건 Off-Page에 저장하는 것이 아니다. 데이터의 크기가 커서 저장공간이 많이 필요한 경우에만 외부에 저장한다.
따라서, 실제로는 VARCHAR와 TEXT 모두 크기 제한만 충족하면 인덱스를 생성할 수 있다.

이렇게 살펴본 결과 LOB 타입인지 아닌지의 여부에 따라서(정확히는 저장되는 데이터의 크기에 따라서) MySQL에서 데이터 저장 및 참조 방식이 달라진다는 것을 확인할 수 있었다.

메모리 활용
하지만 메모리 활용 측면에서 VARCHAR와 TEXT는 다른 동작을 한다.
VARCHAR 타입은 최대 크기가 정해져 있기 때문에, 해당 크기만큼 메모리 공간을 미리 할당받을 수 있다.
하지만, TEXT와 같은 LOB 타입은 사전에 메모리가 할당되지 않는다. 따라서 컬럼을 읽을 때마다 필요한 만큼 메모리를 할당하고 사용 후 해제해야 한다.


TEXT와 MEDIUMTEXT은 어차피 둘 다 LOB타입이므로 저장방식이 크게 다르지 않을 것이라고 판단하였다.
따라서 ee_doc_data 컬럼의 자료형은 MEDIUMTEXT 로 결정!

하지만 item_name 컬럼은은 아래 두 가지의 이유로부터 데이터 타입을 변경하였다.
1. 의약품 상세정보가 아닌, 의약품 목록에서도 필요한 정보이기 때문에 더 많은 조회가 필요할 것이라고 예상됨
2. 실제 오류가 발생한 데이터를 확인해보았을 때 데이터의 크기가 크지 않음
따라서 item_name 컬럼의 자료형은 TEXT가 아닌 VARCHAR(500) 로 결정!

결과

위에서 item_name에 대해서 언급했 듯이 ee_doc_data 외에도 자료형 크기를 벗어나는 필드의 경우, 로그를 남겨서 확인하고 컬럼의 자료형을 수정하였다.

예를 들어, item_name 저장 중에 발생한 에러 로그 예시이다.

페이지별 공공 API 호출 기능 또한 만들어 두었기 때문에, 해당 페이지의 정보를 다시 호출하는 방식으로 데이터베이스를 완성하였다.

API 응답에서부터 제공하는 의약품 정보의 수가 45,048개인 것을 확인했었고,

API에서 제공하는 45,048개의 의약품 데이터가 데이터베이스에 모두 저장된 것을 확인할 수 있었다.

사실 이 작업은 로컬에서 진행하였고, 서버에 띄워서 새로 구축할 예정이다.
그에 따라서 결과적으로 수정한 NewMedicine entity는 다음과 같다.

@Entity
@Getter
@Setter
@Table(name = "new_medicine")
public class NewMedicine {

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

    @Column(name = "item_seq", nullable = false)
    private String itemSeq; // 품목기준코드

    @Column(name = "item_name", length = 500)
    private String itemName; // 품목명

    @Column(name = "entp_name")
    private String entpName; // 업체명

    @Column(name = "cns_gn_manuf", columnDefinition = "TEXT")
    private String cnsGnManuf; // 위탁제조업체

    @Column(name = "etc_otc_code")
    private String etcOtcCode; // 전문일반

    @Column(name = "chart", columnDefinition = "TEXT")
    private String chart; // 성상

    @Column(name = "material_name", columnDefinition = "TEXT")
    private String materialName; // 원료성분

    @Column(name = "insert_file")
    private String insertFile; // 첨부문서

    @Column(name = "storage_method", columnDefinition = "TEXT")
    private String storageMethod; // 저장방법

    @Column(name = "valid_term")
    private String validTerm; // 유효기간

    @Column(name = "pack_unit", columnDefinition = "TEXT")
    private String packUnit; // 포장단위

    @Column(name = "make_material_flag")
    private String makeMaterialFlag; // 완제/원료구분

    @Column(name = "newdrug_class_name")
    private String newdrugClassName; // 신약

    @Column(name = "induty_type")
    private String indutyType; // 업종구분

    @Column(name = "ee_doc_data", columnDefinition = "MEDIUMTEXT")
    private String eeDocData; // 효능효과 문서 데이터

    @Column(name = "ud_doc_data", columnDefinition = "MEDIUMTEXT")
    private String udDocData; // 용법용량 문서 데이터

    @Column(name = "nb_doc_data", columnDefinition = "MEDIUMTEXT")
    private String nbDocData; // 주의사항(일반) 문서 데이터

    @Column(name = "pn_doc_data", columnDefinition = "MEDIUMTEXT")
    private String pnDocData; // 주의사항(전문) 문서 데이터

    @Column(name = "main_item_ingr", columnDefinition = "TEXT")
    private String mainItemIngr; // 유효성분

    @Column(name = "ingr_name", columnDefinition = "TEXT")
    private String ingrName; // 첨가제

    @Column(name = "atc_code")
    private String atcCode; // ATC코드

    @Column(name = "item_eng_name")
    private String itemEngName; // 품목영문명

    @Column(name = "entp_eng_name")
    private String entpEngName; // 업체영문명

    @Column(name = "main_ingr_eng", columnDefinition = "TEXT")
    private String mainIngrEng; // 주성분영문명

    @Column(name = "bizrno")
    private String bizrno; // 사업자등록번호

    @Column(name = "rare_drug_yn")
    private String rareDrugYn; // 희귀의약품여부
}

그래서 @Lob는 뭐죠..?

Entity에 @Lob 어노테이션이 없어진걸 확인할 수 있을 것이다.
사실 @Lob가 명확히 무슨 역할을 해주는지 이해할 수 없어서 우선은 없앴다.
column의 자료형을 columnDefinition으로 명시적으로 선언해주었고, 그에 따라서 DB 엔진의 동작 방식이 달라질 것이다.

그러면 @Lob 어노테이션을 달아야 하는 이유가 무엇일까..?
스프링 내부에서 동작 방식이 달라지는 것일까?
조금 찾아본 결과로는 구현체가 Hibernate의 경우에 LOB 필드에 대해 기본적으로 Lazy 로딩을 적용한다는 것 같았다.

이걸 확인할 수 있는 방법이나 그 외에 @Lob에 대해서 더 알게되는 싶은 내용이 생긴다면 다른 글로 돌아와보도록 하겠다..!


쓰고 나니 별 것 아닌 문제인 것 같지만,,, 그래도 MySQL의 자료형에 대해서 알 수 있었다.
DB 엔진 동작에 대해서도 공부해보고 싶었는데, 어떻게 보면 조금 맛보기...? 시간을 가질 수 있었던 것 같다.
내부 동작까지 이해하지 못하고 작성한 내용이라서 틀린 내용이 많을 것 같기도 하다.
공부하는대로 보완해보도록 해야겠다.
그리고 이 글을 누구라도 본다면! 마구 지적해주면 좋겠다.

Reference

https://dev.mysql.com/doc/refman/8.4/en/storage-requirements.html
https://dev.mysql.com/doc/refman/8.4/en/charset-unicode-utf8mb4.html
https://mangkyu.tistory.com/407
https://medium.com/daangn/varchar-vs-text-230a718a22a1

0개의 댓글