Spring JPA에서 PostgreSQL JSONB 타입 활용하기

2025. 4. 21. 12:46·Study

PostgreSQL의 JSONB 타입은 JSON 데이터를 이진 형식으로 저장하는 강력한 데이터 타입입니다. 이 타입을 사용하면 구조화된 데이터를 유연하게 저장하고 효율적으로 쿼리할 수 있습니다. 이번 포스팅에서는 Spring JPA에서 PostgreSQL의 JSONB 타입을 활용하는 방법에 대해 상세히 알아보겠습니다.

목차

  1. PostgreSQL JSONB 타입 소개
  2. Spring JPA 프로젝트 설정
  3. Entity에 JSONB 타입 매핑하기
  4. JSON 데이터 변환을 위한 Converter 구현
  5. JSONB 필드 쿼리하기
  6. 고급 쿼리 기법
  7. 성능 최적화 팁
  8. 실제 사용 사례
  9. 마무리

PostgreSQL JSONB 타입 소개

PostgreSQL은 JSON 데이터를 저장하기 위한 두 가지 데이터 타입을 제공합니다:

  • JSON: 입력된 텍스트를 그대로 저장하며, 처리 시 매번 파싱이 필요합니다.
  • JSONB: 이진 형식으로 저장되어 처리 속도가 빠르고 인덱싱이 가능합니다.

JSONB의 주요 장점:

  • 더 빠른 쿼리 처리
  • 인덱싱 지원
  • 중복 키 자동 제거
  • 키 순서 보존하지 않음 (내부적으로 최적화)

Spring JPA 프로젝트 설정

먼저 Spring Boot 프로젝트에 필요한 의존성을 추가합니다:

// build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.postgresql:postgresql'
    implementation 'com.vladmihalcea:hibernate-types-52:2.16.2' // JSONB 지원 라이브러리
    implementation 'com.fasterxml.jackson.core:jackson-databind'
}

Maven을 사용하는 경우:

<!-- pom.xml -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
    </dependency>
    <dependency>
        <groupId>com.vladmihalcea</groupId>
        <artifactId>hibernate-types-52</artifactId>
        <version>2.16.2</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
</dependencies>

application.properties 또는 application.yml 파일에서 데이터베이스 연결 설정:

# application.properties
spring.datasource.url=jdbc:postgresql://localhost:5432/yourdb
spring.datasource.username=postgres
spring.datasource.password=password
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=update

Entity에 JSONB 타입 매핑하기

JSONB 타입을 사용하는 방법에는 크게 세 가지가 있습니다:

1. Hibernate Types 라이브러리 사용

Vlad Mihalcea의 hibernate-types 라이브러리를 활용하여 쉽게 JSONB 타입을 매핑할 수 있습니다:

import com.vladmihalcea.hibernate.type.json.JsonBinaryType;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.hibernate.annotations.TypeDefs;

import javax.persistence.*;
import java.util.Map;

@Entity
@Table(name = "products")
@TypeDefs({
    @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
})
public class Product {

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

    private String name;

    @Type(type = "jsonb")
    @Column(columnDefinition = "jsonb")
    private Map<String, Object> attributes;

    // 또는 구체적인 클래스를 사용할 수도 있습니다
    @Type(type = "jsonb")
    @Column(columnDefinition = "jsonb")
    private ProductDetails details;

    // Getters and setters
}

2. 구체적인 클래스 매핑

public class ProductDetails {
    private String color;
    private String size;
    private Integer weight;
    private Map<String, String> specifications;

    // Getters and setters
}

3. JPA Attribute Converter 사용

JPA의 AttributeConverter를 구현하여 직접 변환 로직을 작성할 수도 있습니다:

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.io.IOException;

@Converter
public class JsonConverter implements AttributeConverter<Object, String> {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public String convertToDatabaseColumn(Object attribute) {
        try {
            return objectMapper.writeValueAsString(attribute);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("Error converting object to JSON", e);
        }
    }

    @Override
    public Object convertToEntityAttribute(String dbData) {
        try {
            return objectMapper.readValue(dbData, Object.class);
        } catch (IOException e) {
            throw new RuntimeException("Error converting JSON to object", e);
        }
    }
}

Entity 클래스에서 Converter 사용:

@Entity
@Table(name = "products")
public class Product {

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

    private String name;

    @Column(columnDefinition = "jsonb")
    @Convert(converter = JsonConverter.class)
    private Map<String, Object> attributes;

    // Getters and setters
}

JSON 데이터 변환을 위한 Converter 구현

특정 클래스를 JSON으로 변환하는 더 구체적인 Converter:

@Converter
public class ProductDetailsConverter implements AttributeConverter<ProductDetails, String> {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public String convertToDatabaseColumn(ProductDetails attribute) {
        try {
            return objectMapper.writeValueAsString(attribute);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("Error converting ProductDetails to JSON", e);
        }
    }

    @Override
    public ProductDetails convertToEntityAttribute(String dbData) {
        try {
            return objectMapper.readValue(dbData, ProductDetails.class);
        } catch (IOException e) {
            throw new RuntimeException("Error converting JSON to ProductDetails", e);
        }
    }
}

사용 예:

@Column(columnDefinition = "jsonb")
@Convert(converter = ProductDetailsConverter.class)
private ProductDetails details;

JSONB 필드 쿼리하기

1. Spring Data JPA Repository 메서드

기본적인 쿼리 메서드:

public interface ProductRepository extends JpaRepository<Product, Long> {
    
    // JSONB 필드의 특정 키-값 쌍으로 검색
    @Query("SELECT p FROM Product p WHERE p.attributes ->> 'color' = :color")
    List<Product> findByAttributesColor(@Param("color") String color);
    
    // 중첩된 JSONB 속성으로 검색
    @Query("SELECT p FROM Product p WHERE p.details -> 'specifications' ->> 'material' = :material")
    List<Product> findByDetailsMaterial(@Param("material") String material);
}

2. 네이티브 쿼리 사용

PostgreSQL의 JSONB 연산자를 활용한 네이티브 쿼리:

@Query(value = "SELECT * FROM products WHERE attributes @> '{\"color\": \"red\"}'::jsonb", nativeQuery = true)
List<Product> findRedProducts();

// 특정 키가 존재하는지 확인
@Query(value = "SELECT * FROM products WHERE attributes ? :key", nativeQuery = true)
List<Product> findProductsWithKey(@Param("key") String key);

// 여러 조건 (AND)
@Query(value = "SELECT * FROM products WHERE attributes @> :filter::jsonb", nativeQuery = true)
List<Product> findByAttributesFilter(@Param("filter") String jsonFilter);

고급 쿼리 기법

1. 복잡한 조건 처리

// JSONB 배열 내 요소 검색
@Query(value = "SELECT * FROM products WHERE details->'tags' @> CAST(:tag AS jsonb)", nativeQuery = true)
List<Product> findByTag(@Param("tag") String tag);

// 여러 JSONB 필드 비교
@Query(value = "SELECT * FROM products p1, products p2 WHERE p1.id != p2.id AND p1.attributes->'dimensions' = p2.attributes->'dimensions'", nativeQuery = true)
List<Product> findProductsWithSameDimensions();

2. 동적 쿼리 구성

Specification 인터페이스와 Criteria API를 활용하여 동적 쿼리를 구성할 수 있습니다:

@Repository
public class ProductSpecifications {
    
    public static Specification<Product> hasAttributeValue(String key, String value) {
        return (root, query, builder) -> {
            Expression<String> expression = builder.function(
                "jsonb_extract_path_text", 
                String.class, 
                root.get("attributes"), 
                builder.literal(key)
            );
            return builder.equal(expression, value);
        };
    }
    
    public static Specification<Product> hasAttributeGreaterThan(String key, Number value) {
        return (root, query, builder) -> {
            Expression<Number> expression = builder.function(
                "CAST", 
                Number.class,
                builder.function(
                    "jsonb_extract_path_text", 
                    String.class, 
                    root.get("attributes"), 
                    builder.literal(key)
                ),
                builder.literal("numeric")
            );
            return builder.gt(expression, value);
        };
    }
}

사용 예:

Specification<Product> spec = Specification.where(null);

if (color != null) {
    spec = spec.and(ProductSpecifications.hasAttributeValue("color", color));
}

if (minPrice != null) {
    spec = spec.and(ProductSpecifications.hasAttributeGreaterThan("price", minPrice));
}

List<Product> products = productRepository.findAll(spec);

3. JSONB 인덱싱

PostgreSQL에서 JSONB 필드 인덱싱:

-- GIN 인덱스 생성 (전체 JSONB 객체에 대한 검색)
CREATE INDEX idx_product_attributes ON products USING GIN (attributes);

-- 특정 경로에 대한 인덱스
CREATE INDEX idx_product_attributes_color ON products USING btree ((attributes ->> 'color'));

Spring JPA에서 초기화 스크립트로 추가:

-- src/main/resources/data.sql
CREATE INDEX IF NOT EXISTS idx_product_attributes ON products USING GIN (attributes);

성능 최적화 팁

  1. 인덱스 활용: 자주 검색하는 JSONB 필드에 인덱스를 생성하여 성능 향상
  2. 부분 인덱스: 특정 조건에 맞는 레코드만 인덱싱
  3. CREATE INDEX idx_product_premium ON products USING GIN (attributes) WHERE (attributes->>'premium')::boolean = true;
  4. 정규화 고려: 빈번히 검색되는 중요 필드는 별도 컬럼으로 정규화하는 것을 고려
  5. 쿼리 최적화: EXPLAIN ANALYZE를 사용하여 쿼리 성능 분석
  6. 패치 크기 조정: 대량의 JSONB 데이터를 가져올 때 페이징 처리

실제 사용 사례

사례 1: 동적 속성을 가진 상품 카탈로그

@Entity
@Table(name = "products")
@TypeDefs({
    @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
})
public class Product {
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
    private String category;
    private BigDecimal basePrice;
    
    @Type(type = "jsonb")
    @Column(columnDefinition = "jsonb")
    private Map<String, Object> attributes;
    
    @Type(type = "jsonb")
    @Column(columnDefinition = "jsonb")
    private List<PriceHistory> priceHistory;
    
    // getters and setters
}

class PriceHistory {
    private LocalDate date;
    private BigDecimal price;
    private String reason;
    
    // getters and setters
}

사례 2: 설정 데이터 저장

@Entity
@Table(name = "application_settings")
@TypeDefs({
    @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
})
public class ApplicationSettings {
    @Id
    private String applicationName;
    
    @Type(type = "jsonb")
    @Column(columnDefinition = "jsonb")
    private Map<String, Object> settings;
    
    @Type(type = "jsonb")
    @Column(columnDefinition = "jsonb")
    private List<SettingsHistory> history;
    
    // getters and setters
}

사례 3: 이벤트 로깅 시스템

@Entity
@Table(name = "event_logs")
@TypeDefs({
    @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
})
public class EventLog {
    @Id
    @GeneratedValue
    private Long id;
    
    private String eventType;
    private LocalDateTime timestamp;
    private String userId;
    
    @Type(type = "jsonb")
    @Column(columnDefinition = "jsonb")
    private Map<String, Object> eventData;
    
    // getters and setters
}

마무리

PostgreSQL의 JSONB 타입과 Spring JPA의 조합은 관계형 데이터베이스의 안정성과 NoSQL 스타일의 유연성을 동시에 제공합니다. 이를 통해 스키마 변경 없이 다양한 형태의 데이터를 저장하고 효율적으로 쿼리할 수 있습니다.

적절한 인덱싱과 쿼리 최적화를 통해 JSONB 필드를 포함한 데이터베이스 작업을 효율적으로 처리할 수 있으며, 다양한 비즈니스 요구사항에 유연하게 대응할 수 있습니다.

다음 단계로는 실제 프로젝트에 이를 적용하고, PostgreSQL의 다양한 JSONB 함수와 연산자를 활용해 보시기 바랍니다. 또한 JPA와 함께 QueryDSL을 활용하면 타입 안전한 방식으로 복잡한 JSONB 쿼리를 구성할 수 있습니다.

추가 질문이나 의견이 있으시면 댓글로 남겨주세요!

반응형
저작자표시 비영리 변경금지 (새창열림)

'Study' 카테고리의 다른 글

그 외 플랫폼에서의 REST API 작성 예시 (4/4)  (0) 2025.04.26
Nest.JS에서의 REST API 작성 예시 (3/4)  (0) 2025.04.26
Spring Boot에서의 REST API 작성 예시 (2/4)  (0) 2025.04.26
REST API 란 무엇인가? (1/4)  (0) 2025.04.26
'Study' 카테고리의 다른 글
  • 그 외 플랫폼에서의 REST API 작성 예시 (4/4)
  • Nest.JS에서의 REST API 작성 예시 (3/4)
  • Spring Boot에서의 REST API 작성 예시 (2/4)
  • REST API 란 무엇인가? (1/4)
모리군
모리군
    반응형
  • 모리군
    나의 일상 그리고 취미
    모리군
  • 전체
    오늘
    어제
    • 분류 전체보기 (23)
      • 독백 (0)
      • Study (11)
        • Authentication (4)
        • Supabase (2)
      • Javascript (3)
        • node.js (0)
        • react.js (0)
        • vue.js (0)
        • LeetCode (3)
      • AI (0)
      • PHP (0)
      • HTML, CSS (0)
      • 툴, 플러그인 (0)
      • 취미 (1)
        • 보드게임 (1)
      • 교통 (0)
        • 철도 (0)
        • 도로 (0)
      • 부동산 (2)
        • 서울 (0)
        • 경기 성남 (0)
        • 경기 수원 (0)
        • 경기 화성 (2)
        • 경기 남양주 (0)
        • 광주 (0)
      • 역사 (4)
        • 이 주의 역사 (1)
      • 영어기사 읽기 (1주일 1번) (0)
        • 스포츠 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    벡터데이터베이스
    대리운전
    express
    카카오T 대리
    PostgreSQL
    토큰관리
    웹 서비스
    algorithm
    node.js
    FastAPI
    java
    초기 영화
    임베딩
    supabase
    ChatGPT
    typescript
    윌리엄 딕슨
    한국발명
    슈파베이스
    LeetCode
    백엔드
    광주민주화운동
    javascript
    JWT
    REST API
    OpenAI
    Spring Boot
    백엔드개발
    프롬프트엔지니어링
    Rag
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
모리군
Spring JPA에서 PostgreSQL JSONB 타입 활용하기
상단으로

티스토리툴바