ajouevent.com

[MySQL] URL 저장을 위한 데이터 타입 VARCHAR vs TEXT

simzard 2025. 1. 19. 20:01

문제 상황 - URL 길이 초과로 인한 에러 발생

[MYSQL] “Data truncation: Data too long for column”

ajouevent.com 서비스에서 학교 공지사항을 크롤링하던 중, 데이터베이스에 이미지 URL을 저장하는 과정에서 다음과 같은 에러가 발생했다.

 

2025/01/03 16:11:24 utils.go:156: {"timestamp":"2025-01-03T07:11:24.327+00:00","message":"could not execute statement [Data truncation: Data too long for column 'url' at row 1] [insert into club_event_image (club_event_id,url) values (?,?)]; SQL [insert into club_event_image (club_event_id,url) values (?,?)]","details":"uri=/api/webhook/crawling"}

 

크롤링 중 에러가 발생한 이미지 URL은 다음과 같다.

https://job.ajou.ac.kr/upload/Recruit/20250103_96001865/250103%20[%EC%A0%95%EA%B7%9C,%EA%B3%84%EC%95%BD][%EB%B8%8C%EB%A6%BF%EC%A7%95%EA%B7%B8%EB%A3%B9%EC%BD%94%EB%A6%AC%EC%95%84]%20%ED%95%B4%EC%99%B8%EC%98%81%EC%97%85%ED%8C%80(MICE)%20%EC%B1%84%EC%9A%A9%EA%B3%B5%EA%B3%A01.jpg

 

원인 분석

varchar(255) 타입으로 url을 저장하고 있었다.

이미지 URL의 길이가 총 276자로 VARCHAR(255)의 최대 길이를 초과했기 때문에 “Data truncation: Data too long for column ‘url’ at row1” 에러가 발생한 것이다.

 

해결 방안

따라서 URL 저장 시 데이터 타입과 길이를 재설정해야 했다.

이를 위해 URL 저장 방식, 데이터 타입, 그리고 URL의 길이 제한을 다시 고려했다.

 

URL 저장 방식

공지사항 크롤링을 통해 가져온 이미지 URL

  • 학교 공지사항 글에 포함된 이미지는 이미 학교 서비스를 통해 URL 형태로 생성된 상태다.

https://www.ajou.ac.kr/_attach/ajou/editor-image/2024/12/lwDjcIfEGlYfXOdCCNwsPtkhVK.pngaj

ajouevent.com에서 게시글을 작성하면서 첨부하는 이미지

 

DB에 이미지 URL 저장 vs 이미지 데이터 직접 저장

이미지 데이터를 직접 저장하려면 BLOB, LONGBLOB과 같은 데이터 타입을 사용해야 하지만, 이는 아래와 같은 문제를 유발한다.

  1. 데이터베이스 크기 증가: 이미지 파일이 데이터베이스의 크기를 증가시킴
  2. 데이터베이스 성능 저하: 이미지를 바이너리 데이터로 변환하여 데이터베이스에 저장하고 읽는, 이미지 읽기와 쓰기 과정에서 느려질 가능성이 높다.

→ 이미 학교 서비스를 통해 만들어진 URL, S3를 통해 만들어진 이미지 URL을 바로 MySQL DB의 column으로 저장하는 방식을 택했다.

 

데이터 타입 결정: CHAR vs VARCHAR vs TEXT

URL을 DB column으로 저장할 때 데이터 타입을 어떻게 할지 고민이 있었다.

CHAR vs VARCHAR

  • CHAR: 고정 길이 데이터(고정형) 저장에 적합.
  • VARCHAR: 가변 길이 데이터(가변형) 저장에 적합하며, 길이에 따라 메모리 공간을 효율적으로 사용.

→ URL의 길이는 고정되지 않는 가변 길이이므로, VARCHAR가 더 적합하다.

 

VARCHAR vs TEXT

VARCHAR와 TEXT의 차이에 대해서는 Real MySQL 8.0의 저자인 이성욱 당근 DBA께서 잘 설명해놓으신 글이 있다.

https://medium.com/daangn/varchar-vs-text-230a718a22a1

 

VARCHAR vs TEXT

개요

medium.com

 

요약하자면

  • MySQL 서버는 레코드의 컬럼 데이터를 B-Tree(Clustering Index)에 Inline으로 저장한다. 만약 레코드의 전체 크기가 InnoDB 스토리지 엔진의 최대 레코드 크기 제한(16KB 페이지에서는 8,117 바이트)을 초과하면, 길이가 긴 컬럼을 선택해서 off-Page로 저장하게 된다. (*이때, VARCHAR, TEXT 두 타입의 컬럼 모두 컬럼에 저장된 값으로 인해 최대 레코드 크기 제한을 넘어서면 off-page로 저장된다)
    • → VARCHAR 컬럼에 너무 큰 길이를 사용하면, off-Page에 저장될 수 있고, 이는 성능 이슈와 직결된다.
  • VARCHAR 타입은 최대 크기가 설정되기 때문에, 메모리 공간을 records[2] 버퍼에 최대 크기로 미리 할당해두어 메모리를 활용한다. records[2] 메모리 버퍼에 할당되면 많은 커넥션에 의해 재사용된다. 반면에 TEXT는 실제 최대 크기만큼 메모리를 할당해두면 메모리 낭비가 너무 심해진다.
    • 대신, VARCHAR 타입에 저장된 값이 길어서 off-Page로 저장된 경우 MySQL 서버는 records[2] 버퍼를 사용하지 못하고 새롭게 메모리 공간을 할당해서 사용한다. 그래서 VARCHAR 타입에 매우 큰 값이 빈번하게 저장되는 경우는 주의가 필요하다.

따라서 VARCHAR에 저장되는 URL의 최대 길이를 정하고, VARCHAR 컬럼이 추가되면서 레코드의 크기가 InnoDB 스토리지 엔진의 최대 레코드 크기 제한을 넘는지 비교해야했다.

 

URL 길이 제한

MySQL DB의 column으로 저장되는 URL의 길이가 어느정도인지에 따라 VARCHAR를 쓸지 TEXT를 쓸지 정해진다고 판단했다.

 

일반적인 브라우저 지원 길이

출처: Chronium 공식 문서

 

출처: https://support.microsoft.com/ko-kr/topic/internet-explorer%EC%9D%98-%EC%B5%9C%EB%8C%80-url-%EA%B8%B8%EC%9D%B4%EB%8A%94-2-083%EC%9E%90%EC%9E%85%EB%8B%88%EB%8B%A4-174e7c8a-6666-f4e0-6fd6-908b53c12246

 

출처: https://saturncloud.io/blog/what-is-the-maximum-length-of-a-url-in-different-browsers/, 출처: https://serpstat.com/blog/how-long-should-be-the-page-url-length-for-seo/

 

  • 크롬, 인터넷 익스플로러, 엣지의 경우는 2,083자 Firefox는 65,536자, Safari는 80,000자까지 허용한다.

 

Presigned URL 및 CDN URL

Presigned URL

  • AWS S3 Presigned URL, Google Cloud Storage와 같은 스토리지에서 생성되는 URL은 보안 토큰, 만료 시간, 키 경로 등을 포함하여 길이가 길어질 수 있다.
    • 따라서 복잡한 쿼리 파라미터가 추가되면 더 늘어날 가능성은 있다.

 

기존 DB에 저장된 이미지 URL과 비교

왼쪽: DB에 저장된 이미지 URL 길이 탑10, 오른쪽: 현재 DB에 저장되어있는 이미지 URL

기존에 DB에 저장된 2016년부터의 아주대학교 공지사항의 20만 개 이미지 URL을 분석했을 때, 2025년 1월 18일 기준 가장 긴 URL의 길이는 291자다.

 

이미지 생성 패턴

 

그래서 DB column에 저장되는 URL 길이 제한은 얼마로?

따라서 URL의 길이 제한은

  • 지금까지 저장된 아주대학교 공지사항의 이미지 URL 길이가 300자를 넘지 않았다.
  • 외부 URL을 가져오는 경우는 드물며, 가져오더라도 길이가 짧은 편이다.
  • URL에 쿼리 매개변수가 과도하게 포함되거나, 이미지 데이터를 base64로 인코딩하거나, AWS S3, 구글 드라이브처럼 외부 서비스에서 생성된 링크가 길고 복잡하더라도 브라우저에서 지원하는 URL 길이 제한을 초과하는 경우는 거의 없다는 점을 고려하여

→ 점유율이 가장 높은 크롬 브라우저의 URL 길이 제한과 동일하게 2083자로 설정했다.

*그래도 추가적으로 백엔드 로직에서 URL 길이가 2,083자를 넘으면 로그를 기록하거나 에러 메시지를 반환하도록 처리했다.

 

 

결론 - VARCHAR(2083) 선택

결론적으로 VARCHAR(2083)를 선택한 이유는 아래와 같다.

  • 이미지 url을 사용하는 이미지 테이블의 레코드가 image_id(bigint), url(VARCHAR(2083), post_id(bigint)로 설정하면 기타 오버헤드를 포함해도 2100바이트 정도로, 현재 InnoDB 스토리지 엔진의 최대 레코드 크기 제한(16KB 페이지에서는 8,117 바이트)을 넘어서지 않는다
    • club_event_image 테이블에 추가적인 컬럼이 들어갈 가능성이 낮다
    • 추가되더라도 VARCHAR(2083) 같은 긴 컬럼이 3개~4개 추가될 가능성이 적기 때문에, 컬럼이 추가되면서 레코드 용량을 넘어설 가능성이 적다.
CREATE TABLE club_event_image (
    image_id BIGINT AUTO_INCREMENT PRIMARY KEY, -- 8바이트
    club_event_id BIGINT NOT NULL, -- 8바이트
    url VARCHAR(2083) NOT NULL -- 최대 2083바이트 + 2바이트(길이 오버헤드)
);
image_id (8) + club_event_id (8) + url(2085) + 기타 오버헤드 <= 8117바이트

 

InnoDB 스토리지 엔진의 레코드 크기 제한은 페이지 크기의 약 절반. 즉 현재 DB에서 페이지 크기 = 16KB, 최대 레코드 크기 ≈ 8,117 바이트

 

  • URL의 길이는 일반적으로 2,083자를 넘지 않는다. → VARCHAR(2083)을 사용하면 “inline” 방식으로 데이터를 저장하여 효율적인 처리가 가능
    • 매우 긴 URL이라고 하더라도 브라우저에서 지원하는 URL을 넘길 가능성이 적다.
  • 게시글을 조회할 때마다 이미지 테이블을 조회하고, 항상 URL(이미지) 컬럼의 데이터가 필요하다. (자주 접근) → TEXT보다 VARCHAR가 효율적

 

글을 작성하며 알게 된 사실

  • VARCHAR라고 무조건 Inline으로 저장하고, TEXT라고 무조건 off-Page 저장 X
    • InnoDB 스토리지 엔진은 레코드의 전체 크기가 제한을 초과하면 길이가 긴 컬럼을 선택해서 Off-Page로 저장하게 된다 → VARCHAR 컬럼에 저장된 값이 큰 경우에도 off-Page로 저장된다
    • VARCHAR를 사용한다고 하더라도 off-Page로 넘어가면(off-page 저장소로 분리 저장되는 순간) 메모리 버퍼를 사용할 수 없기 때문에 조심해야 한다. (매번 매모리를 새로 할당 및 해제)
  • VARCHAR를 쓰면 실제 저장되는 데이터 크기 만큼 메모리 공간을 차지함
    • ex) VARCHAR(2083) 컬럼에 “Hello”라는 5글자를 저장하면: 저장 공간 = 5(데이터) + 1(오버헤드) = 6바이트만 사용.
    • 다만 TABLE 구조체의 records[2] 버퍼를 이용하는 과정에서 실제 레코드의 데이터 크기에 관계 없이 최대 크기로 메모리 할당을 해두기 때문에 VARCHAR(255)와 VARCHAR(2083)의 성능 차이가 발생한다.
  • VARCHAR는 index를 생성할 수 있고, TEXT는 index를 생성할 수 없다. X
    • 둘 다 최대 크기 길이 제한만 충족 시키면 인덱스 생성 가능
    • B-Tree 인덱스뿐만 아니라 전문 검색 인덱스도 TEXT 타입과 VARCHAR 타입 컬럼 모두 동일하게 생성할 수 있음
-- // 컬럼 그대로 사용시, 인덱스 생성 불가
mysql> ALTER TABLE tb_varchar ADD INDEX ix_fd (fd);
ERROR 1071 (42000): Specified key was too long; max key length is 3072 bytes

mysql> ALTER TABLE tb_lob ADD INDEX ix_fd (fd);
ERROR 1170 (42000): BLOB/TEXT column 'fd' used in key specification without a key length

-- // 컬럼 값의 길이(프리픽스)를 지정하면, 인덱스 생성 가능
mysql> ALTER TABLE tb_varchar ADD INDEX ix_fd ( fd(50) );
mysql> ALTER TABLE tb_lob ADD INDEX ix_fd ( fd(50) );

 

 

참고

https://medium.com/daangn/varchar-vs-text-230a718a22a1

 

VARCHAR vs TEXT

개요

medium.com