본문 바로가기
Tech Notes

PostgreSQL JSONB 타입에 문자열 저장하는 완벽 가이드

by miracle-tech 2025. 8. 12.
728x90
반응형
 

JSONB 타입 이해하기

PostgreSQL의 JSONB 타입은 JSON 데이터를 바이너리 형태로 저장하는 데이터 타입입니다. 이 타입을 사용할 때 가장 많이 하는 실수 중 하나가 일반 문자열을 그대로 저장하려고 하는 것입니다.

핵심 포인트: JSONB 컬럼에는 반드시 유효한 JSON 형식의 데이터만 저장할 수 있습니다.

기본 규칙

✅ 저장 가능한 데이터

  • JSON 객체: {"key": "value"}
  • JSON 배열: ["item1", "item2"]
  • JSON 문자열: "hello world"
  • JSON 숫자: 123
  • JSON 불린: true, false
  • JSON null: null

❌ 저장 불가능한 데이터

  • 일반 문자열 (따옴표 없이): hello world
  • 쉼표로 구분된 문자열: WORD,BULL,KEY
  • 유효하지 않은 JSON 형식

JSON 형식 문자열 저장

이미 JSON 형식으로 변환된 문자열은 그대로 PostgreSQL JSONB 컬럼에 저장할 수 있습니다.

예제 1: JSON 배열 문자열

// 이미 JSON 형식의 문자열
const jsonString = '["WORD","BULL","KEY"]';

// PostgreSQL에 그대로 저장 가능
await query('INSERT INTO users (tags) VALUES ($1)', [jsonString]);

// 조회 후 사용할 때
const result = await query('SELECT tags FROM users WHERE id = $1', [userId]);
const tagsArray = JSON.parse(result.rows[0].tags); // '["WORD","BULL","KEY"]'

예제 2: JSON 객체 문자열

// 이미 JSON 형식의 문자열
const jsonString = '{"name":"홍길동","age":30}';

// PostgreSQL에 그대로 저장 가능
await query('INSERT INTO users (profile) VALUES ($1)', [jsonString]);

// 조회 후 사용할 때
const result = await query('SELECT profile FROM users WHERE id = $1', [userId]);
const profileObj = JSON.parse(result.rows[0].profile); // {name: "홍길동", age: 30}

일반 문자열 저장

일반 문자열을 JSONB 컬럼에 저장하려면 반드시 JSON.stringify()를 사용하여 JSON 형식으로 변환해야 합니다.

예제 1: 쉼표로 구분된 문자열을 배열로 저장

// 일반 문자열
const tagsString = "WORD,BULL,KEY";

// 1. 배열로 변환
const tagsArray = tagsString.split(',');

// 2. JSON 문자열로 변환
const jsonString = JSON.stringify(tagsArray);

// 3. PostgreSQL에 저장
await query('INSERT INTO users (tags) VALUES ($1)', [jsonString]);

// 조회 후 사용할 때
const result = await query('SELECT tags FROM users WHERE id = $1', [userId]);
const tagsArray = JSON.parse(result.rows[0].tags); // ["WORD","BULL","KEY"]

예제 2: 단순 문자열을 JSON 문자열로 저장

// 일반 문자열
const message = "안녕하세요";

// JSON 문자열로 변환 (따옴표가 추가됨)
const jsonString = JSON.stringify(message); // "안녕하세요"

// PostgreSQL에 저장
await query('INSERT INTO users (message) VALUES ($1)', [jsonString]);

// 조회 후 사용할 때
const result = await query('SELECT message FROM users WHERE id = $1', [userId]);
const messageString = JSON.parse(result.rows[0].message); // "안녕하세요"

실무 예제

Node.js에서 실제 사용 예제

상황 입력 데이터 처리 방법 저장될 값
이미 JSON 문자열 '["a","b","c"]' 그대로 저장 ["a","b","c"]
JavaScript 배열 ["a","b","c"] JSON.stringify() 적용 ["a","b","c"]
쉼표 구분 문자열 "a,b,c" split() → JSON.stringify() ["a","b","c"]
단순 문자열 "hello" JSON.stringify() 적용 "hello"

완전한 CRUD 예제

// 사용자 태그 관리 예제
class UserTagService {
  // 태그 저장 (여러 형태의 입력 처리)
  async saveTags(userId, tags) {
    let jsonString;
    
    if (typeof tags === 'string') {
      if (tags.startsWith('[') && tags.endsWith(']')) {
        // 이미 JSON 배열 문자열
        jsonString = tags;
      } else {
        // 쉼표로 구분된 문자열
        const tagsArray = tags.split(',').map(tag => tag.trim());
        jsonString = JSON.stringify(tagsArray);
      }
    } else if (Array.isArray(tags)) {
      // JavaScript 배열
      jsonString = JSON.stringify(tags);
    } else {
      throw new Error('유효하지 않은 태그 형식입니다.');
    }
    
    await query('UPDATE users SET tags = $1 WHERE id = $2', [jsonString, userId]);
  }
  
  // 태그 조회
  async getTags(userId) {
    const result = await query('SELECT tags FROM users WHERE id = $1', [userId]);
    return JSON.parse(result.rows[0].tags);
  }
  
  // 태그 추가
  async addTag(userId, newTag) {
    const currentTags = await this.getTags(userId);
    if (!currentTags.includes(newTag)) {
      currentTags.push(newTag);
      await this.saveTags(userId, currentTags);
    }
  }
}

베스트 프랙티스

1. 타입 검증 함수 만들기

function ensureJsonString(data) {
  if (typeof data === 'string') {
    try {
      JSON.parse(data); // 유효한 JSON인지 확인
      return data;
    } catch (e) {
      // 유효하지 않은 JSON이면 문자열로 처리
      return JSON.stringify(data);
    }
  }
  return JSON.stringify(data);
}

2. 에러 처리

try {
  await query('INSERT INTO users (tags) VALUES ($1)', [invalidJsonString]);
} catch (error) {
  if (error.code === '22P02') { // invalid input syntax for type json
    console.error('유효하지 않은 JSON 형식입니다:', invalidJsonString);
    // 적절한 변환 후 재시도
    const validJson = JSON.stringify(invalidJsonString);
    await query('INSERT INTO users (tags) VALUES ($1)', [validJson]);
  }
}

3. 성능 최적화

팁: JSONB는 인덱싱이 가능하므로 자주 검색하는 필드에 대해서는 GIN 인덱스를 생성하는 것이 좋습니다.

-- JSONB 컬럼에 GIN 인덱스 생성
CREATE INDEX idx_user_tags ON users USING GIN (tags);

-- 특정 태그 검색 시 인덱스 활용
SELECT * FROM users WHERE tags @> '["WORD"]';

정리

핵심 요약

  • JSON 형식 문자열: 그대로 저장 가능
  • 일반 문자열: JSON.stringify() 후 저장
  • 조회 후 사용: JSON.parse()로 변환
  • 에러 방지: 저장 전 항상 JSON 유효성 검증

PostgreSQL의 JSONB 타입을 올바르게 사용하면 NoSQL의 유연성과 SQL의 강력함을 동시에 활용할 수 있습니다. 위의 가이드를 참고하여 안전하고 효율적인 데이터 저장을 구현해보세요!

728x90