PostgreSQL의 JSONB 타입은 JSON 데이터를 이진 형식으로 저장하는 강력한 데이터 타입입니다. 이 타입을 사용하면 구조화된 데이터를 유연하게 저장하고 효율적으로 쿼리할 수 있습니다. 이번 포스팅에서는 Spring JPA에서 PostgreSQL의 JSONB 타입을 활용하는 방법에 대해 상세히 알아보겠습니다.
목차
- PostgreSQL JSONB 타입 소개
- Spring JPA 프로젝트 설정
- Entity에 JSONB 타입 매핑하기
- JSON 데이터 변환을 위한 Converter 구현
- JSONB 필드 쿼리하기
- 고급 쿼리 기법
- 성능 최적화 팁
- 실제 사용 사례
- 마무리
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);
성능 최적화 팁
- 인덱스 활용: 자주 검색하는 JSONB 필드에 인덱스를 생성하여 성능 향상
- 부분 인덱스: 특정 조건에 맞는 레코드만 인덱싱
- CREATE INDEX idx_product_premium ON products USING GIN (attributes) WHERE (attributes->>'premium')::boolean = true;
- 정규화 고려: 빈번히 검색되는 중요 필드는 별도 컬럼으로 정규화하는 것을 고려
- 쿼리 최적화: EXPLAIN ANALYZE를 사용하여 쿼리 성능 분석
- 패치 크기 조정: 대량의 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 |