DB/JPA

[Hibernate/JPA] 타입

꾸준함. 2025. 5. 29. 10:50

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 컬럼이 필요하고, 공간이 비효율적일 수 있음

 

@Enumerated(EnumType.STRING)
@Column(length = 8)
private PostStatus status;
INSERT INTO post (id, title, status) VALUES (1, '...', 'PENDING')
view raw .java hosted with ❤ by GitHub

 

3.2 EnumType.ORDINAL

  • Enum의 순서 (ordinal, 0부터 시작하는 숫자)가 데이터베이스 컬럼에 저장
  • 매우 컴팩트 (i.g. tinyint로 1바이트 사용 가능), 인덱스/저장 효율 최상
  • 단점은 숫자만 보면 의미를 알 수 없음 (설명력이 떨어짐), Enum 순서 변경 시 데이터 불일치 위험

 

@Enumerated(EnumType.ORDINAL)
@Column(columnDefinition = "tinyint")
private PostStatus status;
INSERT INTO post (id, title, status) VALUES (1, '...', 0)
view raw .java hosted with ❤ by GitHub

 

3.3 EnumType.ORDINAL + 설명 테이블 조합

  • 효율성과 설명력을 모두 확보하는 방법
  • Enum ordinal 값과 이름/설명을 매핑하는 별도 테이블 (PostStatusInfo 등)을 생성
  • Post 엔티티에서 status 컬럼은 숫자로 저장, statusInfo와 다대일 연관관계로 의미까지 조회 가능
    • statusInfo 테이블: id(ordinal), name, description 등

 

@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;
}
view raw .java hosted with ❤ by GitHub

 

4. PostgreSQL ENUM 타입 활용 및 Hibernate 연동

  • PostgreSQL은 사용자 정의 ENUM 타입을 지원
  • ENUM 타입은 문자열처럼 보이지만 내부적으로 4바이트로 저장됨
  • Java Enum과 PostgreSQL ENUM 매핑을 위해 Hibernate 커스텀 타입이 필요함

 

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;
// 중략
}
view raw .java hosted with ❤ by GitHub

 

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 타입을 완벽하게 활용하려면, 커스텀 타입을 직접 구현해야 함


/**
* 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"));
}
}
view raw .java hosted with ❤ by GitHub

 

부연 설명

  • 커스텀 타입을 통해 Hibernate/JPA에서 PostgreSQL INET 컬럼과 완벽하게 매핑
  • GIST 인덱스를 사용하면 네트워크 연산(<<=, >>=, && 등)도 효율적으로 실행
  • JPQL/Criteria, Native Query 모두에서 커스텀 타입 안전하게 사용 가능
  • 주소, 네트워크 마스크 추출 등 PostgreSQL 함수 (host(ip), netmask(ip) 등)도 활용 가능

 

참고

인프런 - 고성능 JPA & Hibernate (High-Performance Java Persistence)

 

반응형