1. JPA/Hibernate의 타입 매핑 구조
- JPA와 Hibernate는 Java 객체를 관계형 데이터베이스에 영속화할 때 다양한 타입 매핑 방식을 제공하며 대표적으로 다음 세 가지 범주가 있음
- 기본 타입(Basic Types): Integer, Long, String, Float, Enum 등과 같이 데이터베이스의 단일 컬럼에 매핑되는 타입
- 임베디드 타입(Embeddable Types): 여러 컬럼을 하나의 Java 컴포넌트로 그룹화해 매핑할 수 있음 i.g. 주소( Address)와 같은 객체가 여러 컬럼을 가질 때 사용
- 엔티티 타입(Entity Types): 데이터베이스의 테이블과 직접적으로 매핑되는, 각각의 식별자 (Primary Key)를 가지는 객체
2. 컬럼 타입의 컴팩트함이 중요한 이유
- 데이터베이스에서 동일한 페이지 (메모리 또는 디스크 단위)에 더 많은 행을 저장할 수 있음
- 인덱스와 자주 사용되는 테이블 페이지가 메모리에 더 잘 적재됨
- 대용량 데이터 처리 시 성능과 확장성, 비용 측면에서 매우 중요함
- i.g. 동일한 정보를 1바이트 (tinyint)로 저장할 수 있는데도 8바이트 (varchar 8)로 저장하면 메모리, 디스크, 네트워크 효율이 저하됨
3. Java Enum을 매핑하는 여러 전략
- JPA와 Hibernate에서 @Enumerated 어노테이션을 사용해 Enum 매핑 전략을 선택할 수 있음
3.1 EnumType.STRING
- Enum의 이름 (문자열 값, 예: "PENDING")이 데이터베이스 컬럼에 저장
- 가독성이 높고, 데이터만 보고도 의미를 쉽게 파악할 수 있음
- 단점은, enum 값이 길수록 더 큰 varchar 컬럼이 필요하고, 공간이 비효율적일 수 있음
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Enumerated(EnumType.STRING) | |
@Column(length = 8) | |
private PostStatus status; | |
INSERT INTO post (id, title, status) VALUES (1, '...', 'PENDING') |
3.2 EnumType.ORDINAL
- Enum의 순서 (ordinal, 0부터 시작하는 숫자)가 데이터베이스 컬럼에 저장
- 매우 컴팩트 (i.g. tinyint로 1바이트 사용 가능), 인덱스/저장 효율 최상
- 단점은 숫자만 보면 의미를 알 수 없음 (설명력이 떨어짐), Enum 순서 변경 시 데이터 불일치 위험
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Enumerated(EnumType.ORDINAL) | |
@Column(columnDefinition = "tinyint") | |
private PostStatus status; | |
INSERT INTO post (id, title, status) VALUES (1, '...', 0) |
3.3 EnumType.ORDINAL + 설명 테이블 조합
- 효율성과 설명력을 모두 확보하는 방법
- Enum ordinal 값과 이름/설명을 매핑하는 별도 테이블 (PostStatusInfo 등)을 생성
- Post 엔티티에서 status 컬럼은 숫자로 저장, statusInfo와 다대일 연관관계로 의미까지 조회 가능
- statusInfo 테이블: id(ordinal), name, description 등
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Entity(name = "PostStatusInfo") | |
@Table(name = "post_status_info") | |
public class PostStatusInfo { | |
@Id | |
@Column(columnDefinition = "tinyint") | |
private Integer id; | |
private String name; | |
private String description; | |
} | |
@Entity(name = "Post") | |
@Table(name = "post") | |
public class Post { | |
@Id | |
private Long id; | |
@Enumerated(EnumType.ORDINAL) | |
@Column(columnDefinition = "tinyint") | |
private PostStatus status; | |
@ManyToOne(fetch = FetchType.LAZY) | |
@JoinColumn(name = "status", insertable = false, updatable = false) | |
private PostStatusInfo statusInfo; | |
private String title; | |
} |
4. PostgreSQL ENUM 타입 활용 및 Hibernate 연동
- PostgreSQL은 사용자 정의 ENUM 타입을 지원
- ENUM 타입은 문자열처럼 보이지만 내부적으로 4바이트로 저장됨
- Java Enum과 PostgreSQL ENUM 매핑을 위해 Hibernate 커스텀 타입이 필요함
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class PostgreSQLEnumType extends org.hibernate.type.EnumType { | |
public void nullSafeSet( | |
PreparedStatement st, | |
Object value, | |
int index, | |
SharedSessionContractImplementor session) throws HibernateException, SQLException { | |
if (value == null) { | |
st.setNull(index, Types.OTHER); | |
} else { | |
st.setObject(index, value.toString(), Types.OTHER); | |
} | |
} | |
} | |
@Entity(name = "Post") | |
@Table(name = "post") | |
@TypeDef(name = "pgsql_enum", typeClass = PostgreSQLEnumType.class) | |
public class Post { | |
@Id | |
private Long id; | |
private String title; | |
@Enumerated(EnumType.STRING) | |
@Column(columnDefinition = "post_status_info") | |
@Type(type = "pgsql_enum") | |
private PostStatus status; | |
// 중략 | |
} |
5. Hibernate 커스텀 타입의 필요성
- Hibernate는 다양한 내장 타입 지원을 제공하지만, 실제 현업에서는 종종 기본 타입만으로는 충분하지 않을 수 있음
- i.g. IPv4/IPv6 주소, JSON, 배열, 복합 구조 등 데이터베이스 고유 타입을 완벽하게 매핑하려면 커스텀 타입 필요
- 적절한 데이터베이스 타입을 선택하면 데이터 접근 성능이 크게 향상됨
- Hibernate는 개발자가 직접 새로운 Type을 손쉽게 추가할 수 있도록 유연한 확장 구조를 제공
5.1 IPv4 주소 저장 방식 예시
- IPv4 주소(예: 192.168.123.231/24)를 데이터베이스에 저장하는 방법은 여러 가지가 있음
- BIGINT 또는 NUMERIC(15): 4바이트(IP) + 1바이트(서브넷 마스크)를 합쳐 5바이트 정보 저장 가능하지만 비트 연산, 변환 로직이 필요해 개발 난이도가 높고, 가독성이 떨어질 수 있음
- VARCHAR(18): 사람이 읽기 쉬운 문자열(최대 18자)로 저장, 접근성과 변환은 쉽지만 공간 효율이 떨어짐
- 데이터베이스 고유 타입: PostgreSQL의 inet 혹은 cidr 타입은 네트워크 주소 전용으로 설계되어 7바이트로 저장하며,
주소 비교, 포함, 변환 등 다양한 연산자와 함수 지원- i.g. host(inet), netmask(inet), <, >, && 등
PostgreSQL inet 타입과 Hibernate 커스텀 타입 구현
- PostgreSQL의 inet 타입은 IPv4/IPv6를 효율적으로 저장할 수 있으며, 다양한 네트워크 연산을 데이터베이스 레벨에서 바로 수행할 수 있음
- Hibernate에서 PostgreSQL inet 타입을 완벽하게 활용하려면, 커스텀 타입을 직접 구현해야 함
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Hibernate용 PostgreSQL INET 타입 매핑 커스텀 타입. | |
* | |
* - 이 타입은 Java의 String(예: "192.168.0.1/24")을 PostgreSQL의 INET 타입 컬럼에 매핑 | |
* - PostgreSQL INET 타입은 IPv4 및 IPv6 네트워크 주소를 저장하며, 다양한 네트워크 연산자를 제공 | |
* - 이 커스텀 타입은 Hibernate의 타입 시스템에 통합되어, JPA 엔티티 속성에 @Type(type="ipv4")로 적용할 수 있음 | |
public class IPv4Type extends AbstractSingleColumnStandardBasicType<String> implements DynamicParameterizedType { | |
// Hibernate가 사용할 수 있도록 싱글턴 인스턴스 선언. | |
public static final IPv4Type INSTANCE = new IPv4Type(); | |
/** | |
* 생성자. | |
* - SQL 타입 디스크립터: PostgreSQL INET 타입(Types.OTHER) | |
* - Java 타입 디스크립터: String | |
*/ | |
public IPv4Type() { | |
super( | |
InetSqlTypeDescriptor.INSTANCE, // SQL 타입 매핑 정의 | |
StringTypeDescriptor.INSTANCE // Java String 타입 매핑 정의 | |
); | |
} | |
@Override | |
public String getName() { | |
return "ipv4"; | |
} | |
@Override | |
public void setParameterValues(Properties parameters) { | |
// 별도의 파라미터 처리는 필요없음 | |
} | |
/** | |
* PostgreSQL INET 타입에 대한 SQL 타입 디스크립터. | |
* - PreparedStatement에 값 바인딩(set), ResultSet에서 get 동작 정의 | |
*/ | |
public static class InetSqlTypeDescriptor implements SqlTypeDescriptor { | |
public static final InetSqlTypeDescriptor INSTANCE = new InetSqlTypeDescriptor(); | |
@Override | |
public int getSqlType() { | |
// PostgreSQL INET 타입은 JDBC의 Types.OTHER로 매핑됨 | |
return Types.OTHER; | |
} | |
@Override | |
public boolean canBeRemapped() { | |
return true; | |
} | |
/** | |
* 값 바인딩 구현. | |
* - Hibernate에서 INET 컬럼에 값을 저장할 때 호출됨. | |
* - Java String 값을 PGobject("inet")로 래핑하여 JDBC에 전달. | |
*/ | |
@Override | |
public <X> BasicBinder<X> getBinder(final org.hibernate.type.descriptor.java.JavaTypeDescriptor<X> javaTypeDescriptor) { | |
return new BasicBinder<X>(javaTypeDescriptor, this) { | |
@Override | |
protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { | |
if (value == null) { | |
// null이면 JDBC에 null로 바인딩 | |
st.setNull(index, Types.OTHER); | |
} else { | |
// PGobject를 통해 INET 타입으로 명시적 바인딩 | |
PGobject obj = new PGobject(); | |
obj.setType("inet"); | |
// Java String 값을 PGobject의 값으로 설정 | |
obj.setValue(javaTypeDescriptor.toString(value)); | |
st.setObject(index, obj); | |
} | |
} | |
}; | |
} | |
/** | |
* 값 추출 구현 | |
* - JDBC ResultSet에서 값을 읽어올 때 호출됨 | |
* - String으로 받아서 도메인 객체(String)로 변환 | |
*/ | |
@Override | |
public <X> BasicExtractor<X> getExtractor(final org.hibernate.type.descriptor.java.JavaTypeDescriptor<X> javaTypeDescriptor) { | |
return new BasicExtractor<X>(javaTypeDescriptor, this) { | |
@Override | |
protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException { | |
// ResultSet에서 String으로 추출 | |
String val = rs.getString(name); | |
// null이면 null, 아니면 JavaTypeDescriptor를 통해 변환 | |
return val == null ? null : javaTypeDescriptor.fromString(val); | |
} | |
}; | |
} | |
} | |
} | |
@Entity | |
@Table(name = "event") | |
@TypeDef(name = "ipv4", typeClass = IPv4Type.class) | |
public class Event { | |
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) | |
private Long id; | |
private String name; | |
@Type(type = "ipv4") | |
@Column(columnDefinition = "inet") | |
private String ip; // 예: "192.168.1.100/24" | |
// Getter, Setter | |
public Long getId() { return id; } | |
public void setId(Long id) { this.id = id; } | |
public String getName() { return name; } | |
public void setName(String name) { this.name = name; } | |
public String getIp() { return ip; } | |
public void setIp(String ip) { this.ip = ip; } | |
} | |
CREATE TABLE event ( | |
id SERIAL PRIMARY KEY, | |
name VARCHAR(255), | |
ip inet | |
); | |
CREATE INDEX idx_event_ip_gist ON event USING gist (ip inet_ops); | |
@DataJpaTest | |
@Transactional | |
public class EventRepositoryTest { | |
@PersistenceContext | |
EntityManager em; | |
@Test | |
public void testIPv4TypePersistAndQuery() { | |
// 저장 | |
Event event = new Event(); | |
event.setName("Class C Event"); | |
event.setIp("192.168.123.231/24"); | |
em.persist(event); | |
em.flush(); | |
em.clear(); | |
// 단순 조회 | |
Event found = em.find(Event.class, event.getId()); | |
assert found.getIp().equals("192.168.123.231/24"); | |
// INET 연산자 활용: 특정 네트워크 대역에 포함된 이벤트 찾기 | |
List<Event> list = em.createNativeQuery( | |
"SELECT * FROM event WHERE ip <<= :subnet", Event.class) | |
.setParameter("subnet", "192.168.123.0/24") | |
.getResultList(); | |
assert list.stream().anyMatch(e -> e.getIp().equals("192.168.123.231/24")); | |
} | |
} |
부연 설명
- 커스텀 타입을 통해 Hibernate/JPA에서 PostgreSQL INET 컬럼과 완벽하게 매핑
- GIST 인덱스를 사용하면 네트워크 연산(<<=, >>=, && 등)도 효율적으로 실행
- JPQL/Criteria, Native Query 모두에서 커스텀 타입 안전하게 사용 가능
- 주소, 네트워크 마스크 추출 등 PostgreSQL 함수 (host(ip), netmask(ip) 등)도 활용 가능
참고
인프런 - 고성능 JPA & Hibernate (High-Performance Java Persistence)
반응형
'DB > JPA' 카테고리의 다른 글
[Hibernate/JPA] 관계 (0) | 2025.06.02 |
---|---|
[Hibernate/JPA] 식별자 생성 최적화 전략 (0) | 2025.05.30 |
[Hibernate/JPA] Connection (0) | 2025.05.29 |
[JPA] Hibernate MultipleBagFetchException (0) | 2023.06.28 |
[JPA] 준영속(Detached) 상태 엔티티 수정하는 방법 (0) | 2023.05.07 |