Database/JPA / / 2024. 9. 13. 12:21

JPA 연관관계와 복합 키

JPA에서 Collection 타입의 필드가 별도의 테이블로 생성되는 이유는, RDBMS에서는 컬렉션을 직접 저장할 수 없기 때문이야. 객체 지향 프로그래밍에서는 리스트, 셋 등의 컬렉션 필드를 쉽게 정의하고 사용할 수 있지만, 관계형 데이터베이스에서는 이를 테이블 구조로 풀어서 저장해야 해. JPA는 이를 해결하기 위해 **@ElementCollection**을 사용하여 컬렉션 데이터를 별도의 테이블에 매핑하는 방식을 제공해.

 

왜 별도 테이블이 필요한가?

 

1. 1:N 관계 표현: 데이터베이스에서는 1개의 엔티티가 여러 개의 값을 가질 수 있는 컬렉션 필드를 관리할 수 있는 방식이 없어. 이를 해결하기 위해 JPA는 컬렉션 필드를 별도의 테이블로 분리해서 여러 값을 저장해.

2. 값 타입(Value Type) 저장: @ElementCollection으로 정의된 필드들은 주로 값 타입(예: String, Integer) 같은 기본 타입 또는 임베디드 타입이야. 이 값들을 별도의 테이블에 저장함으로써 엔티티의 필드와 관계를 유지할 수 있어.

3. 효율적인 데이터 관리: 컬렉션 데이터를 테이블로 분리하면 조회와 관리가 더 효율적이 돼. 특히 대용량 데이터를 다룰 때, 별도의 테이블로 분리하면 검색과 처리 성능이 향상될 수 있어.

 

예시

@Entity
public class Member {

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

    private String name;

    @ElementCollection
    @CollectionTable(name = "member_addresses", joinColumns = @JoinColumn(name = "member_id"))
    @Column(name = "address")
    private List<String> addresses = new ArrayList<>();
}

 

위 예시에서 addresses 필드는 별도의 테이블 **member_addresses**로 저장돼. 이 테이블은 member_id 외래 키를 통해 Member 테이블과 연관 관계를 맺고, 각각의 주소는 이 테이블에 여러 행으로 저장돼.

 

따라서, 컬렉션 필드를 테이블로 분리해 관리함으로써 관계형 데이터베이스의 제약을 극복하면서도 객체 지향적인 방식으로 데이터를 다룰 수 있게 해주는 거야.

 

@IdClass는 JPA에서 복합 키(Composite Key)를 사용하여 엔티티의 기본 키를 정의할 때 사용하는 어노테이션이야. 복합 키는 두 개 이상의 필드를 결합해 하나의 기본 키로 설정할 때 사용되며, 각 필드를 개별적으로 관리하는 대신, 한 번에 관리할 수 있도록 해줘.

 

복합 키(Composite Key)의 필요성

 

보통은 하나의 필드만을 기본 키로 사용하지만, 데이터베이스 설계에 따라 여러 필드가 결합된 기본 키가 필요한 경우도 있어. 예를 들어, 주문(Order)과 제품(Product) 간의 관계에서는 주문 번호와 제품 ID를 결합하여 유일한 기본 키로 설정할 수 있어. 이런 경우 복합 키를 사용하게 돼.

 

@IdClass의 사용 방법

 

@IdClass는 복합 키를 위한 별도의 클래스를 정의하고, 해당 클래스에서 여러 필드를 결합해 기본 키로 사용하도록 설정해. 이 클래스는 엔티티와 연결되고, 각 필드는 엔티티 내에서 사용되는 필드와 동일한 타입 및 이름을 가져야 해.

 

예시

 

1. 복합 키 클래스 정의:

import java.io.Serializable;
import java.util.Objects;

public class OrderProductId implements Serializable {
    private Long orderId;
    private Long productId;

    public OrderProductId() {}

    public OrderProductId(Long orderId, Long productId) {
        this.orderId = orderId;
        this.productId = productId;
    }

    // equals()와 hashCode()는 필수
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        OrderProductId that = (OrderProductId) o;
        return Objects.equals(orderId, that.orderId) && 
               Objects.equals(productId, that.productId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(orderId, productId);
    }
}

 

2. 엔티티에서 @IdClass 적용:

import javax.persistence.*;

@Entity
@IdClass(OrderProductId.class)
public class OrderProduct {

    @Id
    private Long orderId;

    @Id
    private Long productId;

    private int quantity;

    // getter, setter
}

 

@IdClass 사용 시 중요한 점

 

복합 키 클래스는 반드시 Serializable을 구현해야 하고, equals()hashCode() 메서드도 구현해야 해. 그래야 JPA가 해당 키를 기반으로 엔티티를 올바르게 식별하고 관리할 수 있어.

복합 키 클래스의 필드 이름과 타입은 엔티티 클래스 내의 기본 키 필드와 동일해야 해.

복합 키는 엔티티의 영속성 관리를 위해 사용되므로, 식별자 값이 변경될 일이 없도록 주의해야 해.

 

다른 복합 키 방법: @EmbeddedId

 

@IdClass 외에도 JPA에서 복합 키를 정의할 때 사용할 수 있는 또 다른 방법은 @EmbeddedId를 사용하는 거야. @EmbeddedId는 복합 키를 하나의 객체로 묶어서 사용하는 방식이야. @IdClass는 여러 필드를 나누어 정의하고, @EmbeddedId는 하나의 키 객체로 관리하는 차이가 있어.

 

Tip💡

SerializableJava에서 객체를 바이트 스트림으로 변환할 수 있게 하는 인터페이스야. 이를 통해 객체를 파일에 저장하거나 네트워크로 전송할 수 있어. Serializable 인터페이스는 아무 메서드도 가지고 있지 않아서 마커 인터페이스라고도 불리며, 단순히 객체가 직렬화 가능하다는 것을 알려주는 역할을 해.

 

직렬화(Serialization)란?

 

직렬화는 객체 상태를 바이트 스트림으로 변환하는 과정이야. 이를 통해 객체를 파일에 저장하거나 네트워크를 통해 전송할 수 있게 돼. 객체를 직렬화하면, JVM 외부로 객체를 내보내서 나중에 다시 복원(역직렬화, Deserialization)할 수 있어.

 

역직렬화(Deserialization)란?

 

역직렬화는 직렬화된 바이트 스트림을 다시 객체로 복원하는 과정이야. 직렬화된 객체는 언제든지 다시 Java 프로그램에서 사용할 수 있는 객체로 변환할 수 있어.

 

Tip💡

마커 인터페이스(Marker Interface)는 Java에서 메서드가 없는 인터페이스를 말해. 즉, 이 인터페이스를 구현한다고 해서 새로운 메서드나 기능이 추가되는 건 아니지만, 특정 클래스가 특정 기능을 지원하는지 여부를 알려주는 역할을 해. 마커 인터페이스는 JVM이나 라이브러리에서 그 클래스를 특별하게 처리할 때 사용돼.

 

대표적인 마커 인터페이스

 

1. Serializable: 객체를 직렬화할 수 있다는 것을 명시.

2. Cloneable: 객체를 복제할 수 있다는 것을 명시.

3. Remote: RMI(Remote Method Invocation)에서 원격 호출이 가능한 객체임을 명시.

 

역할과 특징

 

구분자 역할: 클래스가 특정 인터페이스를 구현함으로써, 해당 클래스가 어떤 특정 기능을 지원한다는 것을 컴파일러나 런타임 환경에 알리는 용도.

구현 메서드 없음: 마커 인터페이스에는 메서드가 없어서, 기능을 제공하기보다는 특정 속성을 표시하는 데 목적이 있어.

 

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유