tech/project

[postgreSQL] varchar VS text 무엇이 더 좋을까

sunm2n 2026. 1. 7. 18:53

일반적인 RDBMS(예: MySQL, Oracle)를 사용해 왔다면 다소 의아할 수 있지만, PostgreSQL에서는 VARCHAR(n)보다 TEXT (혹은 길이 제한 없는 VARCHAR)를 사용하는 것을 권장하거나, 적어도 성능상의 불이익이 전혀 없다고 한다.

 

왜 그럴까? 이유를 보자

 

1. 내부 구현의 동일성 (Under the Hood)

가장 핵심적인 이유는 PostgreSQL 내부에서 두 타입이 처리되는 방식이 완전히 똑같기 때문이다.

  • 구조: PostgreSQL 엔진 입장에서 VARCHAR(n), VARCHAR, TEXT는 모두 내부적으로 동일한 데이터 구조(varlena)를 사용한다.
  • 차이점: 유일한 차이점은 VARCHAR(n)의 경우 데이터를 저장할 때 "길이가 n을 넘는지 확인하는 CPU 사이클(유효성 검사)"이 추가된다는 점뿐이다.

PostgreSQL 공식 문서에서도 다음과 같이 언급하고 있다.

"There is no performance difference among these three types, apart from increased CPU cycles to check the length when applying a limit." (이 세 가지 타입 사이에는 길이 제한을 확인할 때 추가되는 CPU 사이클을 제외하고는 성능 차이가 없습니다.)

 

2. 성능 (Performance)

  • 속도: 앞서 말씀드린 대로 VARCHAR(n)은 길이 체크 로직이 들어가므로, 이론적으로는 TEXT가 아주 미세하게 더 빠릅니다. (물론 체감할 수 있는 수준은 아니지만, 적어도 TEXT가 느리지는 않다.)
  • 인덱싱: TEXT 타입도 B-tree 인덱스를 포함한 모든 인덱스 기능을 VARCHAR와 동일하게 사용할 수 있다.

 

3. 저장 공간 (Storage)

  • 가변 길이: 두 타입 모두 가변 길이(Variable Length)로 저장된다. "안녕하세요"라는 5글자를 저장할 때, TEXT를 쓰든 VARCHAR(100)을 쓰든 디스크에서 차지하는 용량은 똑같다. (고정 길이인 CHAR(n)과 다르다.)
  • TOAST: 데이터가 매우 길어질 경우, PostgreSQL은 TOAST(The Oversized-Attribute Storage Technique)라는 기법을 사용하여 데이터를 자동으로 압축하고 별도 저장소로 뺀다. 이 메커니즘 역시 TEXT와 VARCHAR 모두 동일하게 적용된다.

 

4. 스키마 유지보수의 유연성 (Flexibility)

 

실무에서 TEXT를 선호하는 가장 큰 현실적인 이유이다.

  • 상황: 처음에 VARCHAR(50)으로 설정했다가, 나중에 요구사항이 바뀌어 100자로 늘려야 하는 상황이 자주 발생한다.
  • 문제: 과거 버전의 데이터베이스나 특정 상황에서는 컬럼의 길이를 변경하는 작업이 테이블 잠금(Lock)을 유발하거나 번거로운 마이그레이션이 필요했다. (최신 PostgreSQL에서는 단순히 길이만 늘리는 것은 메타데이터만 수정하므로 빠르지만, 줄이는 것은 여전히 테이블 전체 검사가 필요하다.)
  • 해결: 처음부터 TEXT를 사용하면 DB 스키마 변경(Alter Table) 없이 애플리케이션 레벨에서 유효성 검사(Validation)만 수정하면 되므로 유연성이 극대화된다.

 

이렇게만 봤을 때는 그럼 엄격하게 길이 제한이 필요하지 않는 이상 text를 쓰면 되겠구나!

 

라고 생각하면 큰 오산이다.

 

지금까지는 DB에서의 관점이지 만약 ORM을 사용하는 애플리케이션 계층까지 고려하면 이야기가 조금 달라진다.

 

필자는 스프링으로 개발을 해서 스프링을 기준으로 이야기를 해보겠다.

 

 

1. DDL 자동 생성의 불편함 (hbm2ddl.auto)

 

Spring Boot에서 개발 초기 단계나 테스트 환경에서는 ddl-auto: update나 create를 많이 사용한다. 이때 JPA의 기본 동작 방식과 충돌이 생길 수 있다.

  • 기본 동작: JPA에서 엔티티의 필드를 String으로 선언하면, 하이버네이트는 기본적으로 DB 스키마를 VARCHAR(255)로 생성하려고 한다.
  • TEXT 적용 시: 이를 TEXT로 생성하게 하려면 어노테이션을 매번 명시해야 합니다.
// 귀찮은 방식
@Column(columnDefinition = "TEXT")
private String content;

// 혹은 @Lob을 쓰기도 하지만, 이는 뒤에서 설명할 다른 문제를 야기할 수 있다.

 

 

문제점: 모든 String 필드에 columnDefinition = "TEXT"를 붙이는 것은 생산성을 떨어뜨리고 코드를 지저분하게 만든다.

 

 

2. 애플리케이션 메모리(Heap) 안전장치 부재

이게 가장 치명적인 이유가 될 수 있다.

  • DB 입장: TEXT 컬럼에 1GB짜리 문자열이 들어와도 TOAST 기술로 잘 저장한다.
  • Spring 서버 입장: 누군가 악의적이거나 실수로 그 1GB짜리 데이터를 저장했다고 가정해 보자 JPA로 findAll()이나 해당 엔티티를 조회하는 순간, 자바 힙 메모리(Heap Memory)에 그 거대한 String 객체를 로드하게 된다.
  • 결과: 바로 OOM(Out Of Memory) 에러가 발생하며 서버가 뻗어버릴 수 있다.

VARCHAR의 장점: VARCHAR(255) 같은 제약이 DB에 걸려 있다면, 애초에 거대한 데이터가 DB에 들어가는 것을 막아주므로 애플리케이션의 메모리 안정성을 지키는 마지막 보루 역할을 합니다.

 

3. @Lob 어노테이션과 PostgreSQL의 관계

 

JPA에서 큰 텍스트를 다루기 위해 @Lob을 사용하는 경우가 있는데, PostgreSQL에서 이것이 오해를 낳거나 성능 이슈를 만들 수 있다.

  • JPA 표준: @Lob을 붙이면 하이버네이트는 이를 CLOB(Character Large Object)으로 취급하려 한다.
  • PostgreSQL 특성: 과거 버전의 드라이버나 특정 설정에서는 @Lob이 붙은 필드를 처리할 때, 일반적인 SELECT가 아니라 별도의 OID(Object ID)를 통한 스트리밍 방식으로 처리하거나, 트랜잭션 내에서만 접근해야 하는 제약이 생기기도 했다.
  • 현재: 최신 드라이버는 많이 개선되었지만, 단순히 긴 문자열을 저장하고 싶은데 @Lob을 붙여서 불필요하게 처리가 복잡해지는 경우가 있고 PostgreSQL의 TEXT는 그냥 String 취급을 받는 게 가장 성능이 좋다.

 

그래서 어떤게 좋을까?

 

Spring + JPA + PostgreSQL 환경에서는 하이브리드 전략을 고려해볼 수 있다.

전략 1. 짧은 데이터는 그냥 둔다 (VARCHAR 선호)

이름, 이메일, 전화번호, 코드 등 길이가 예측 가능한 데이터는 굳이 TEXT로 바꾸려 애쓰지 말고, JPA 기본 전략인 VARCHAR(255) (또는 @Column(length=50))를 그대로 따르는 것이 좋다.

  • 이유: 개발 편의성 + 혹시 모를 대용량 데이터 입력 방지(안전장치)

전략 2. 긴 데이터는 명시적으로 TEXT 사용

게시글 본문, JSON 데이터, 로그, 리뷰 등 정말 긴 데이터에 대해서만 TEXT 타입을 적용한다.

 

@Column(columnDefinition = "TEXT") 
private String description;

 

 

전략 3. (중요) 검증은 애플리케이션(Java)에서

DB 컬럼을 TEXT로 하더라도, 애플리케이션 레벨에서 반드시 길이 제한을 두어야 한다.

 

@Entity
public class Product {
    
    // DB에는 TEXT로 저장되어 유연성을 가지지만
    @Column(columnDefinition = "TEXT")
    private String description;

    // 저장하기 전, 자바단에서 2000자 못 넘게 막아서 메모리 폭발 방지
    @PrePersist
    @PreUpdate
    public void validate() {
        if (description != null && description.length() > 2000) {
            throw new IllegalArgumentException("너무 길어요!");
        }
    }
}
// 혹은 DTO에서 @Size(max=2000) 등으로 검증

 

물론 모든 선택에는 항상 trade off 가 존재하므로 현 개발 상황에 맞춰서 최선의 선택을 하는 것이 가장 중요한 거 같다.