본문 바로가기

JPA

[JPA] JPA 기본값 타입

 

 

 

기본값 타입

JPA에서 데이터 타입은 크게 두 가지로 분류할 수 있습니다: 엔티티 타입과 값 타입입니다.

엔티티 타입

  • 정의: @Entity로 정의된 객체.
  • 특징:
    • 데이터가 변해도 식별자(ID)로 지속해서 추적 가능.
    • 식별자를 통해 동일한 객체를 유지.
    • 예를 들어, 회원 엔티티에서 키나 나이 값이 변경되어도 동일한 회원으로 인식 가능.

값 타입

  • 정의: 자바 기본 타입이나 객체로 단순히 값으로 사용.
  • 특징:
    • 식별자가 없고 값만 있음.
    • 값이 변경되면 추적 불가.
    • 예를 들어, 숫자 100을 200으로 변경하면 완전히 다른 값으로 인식.

값 타입 분류

기본 값 타입

  • 예시: String name, int age
  • 특징:
    • 생명 주기가 엔티티에 의존.
    • 예를 들어, 회원을 삭제하면 이름과 나이 필드도 함께 삭제됨.
    • 값 타입은 공유 불가.
    • 예를 들어, 회원 이름을 변경하면 다른 회원의 이름도 변경되면 안됨.
    • 자바의 기본 타입은 절대 공유되지 않음.
    • 기본 타입은 항상 값을 복사.
    • Integer 같은 래퍼 클래스나 String 같은 특수한 클래스는 공유 가능한 객체지만 변경되지 않음.

 

 

임베디드 타입은 JPA에서 값 타입을 재사용하고 응집력을 높이며, 엔티티의 일부로서 다루기 위한 중요한 개념입니다. 임베디드 타입을 사용하는 방법과 장점에 대해 정리해 보겠습니다.

 

1. 임베디드 타입 정의

임베디드 타입은 @Embeddable 어노테이션을 사용하여 정의합니다. 이 클래스는 값 타입을 표현하며, 엔티티의 일부로 사용됩니다. 임베디드 타입 클래스에는 기본 생성자가 필요합니다.

import javax.persistence.Embeddable;

@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;

    public Address() {}

    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }

    // Getters and Setters
}

 

2. 엔티티에서 임베디드 타입 사용

엔티티 클래스에서 @Embedded 어노테이션을 사용하여 임베디드 타입을 포함시킵니다. 이렇게 하면 해당 엔티티는 임베디드 타입의 속성을 자신의 속성처럼 사용할 수 있습니다.

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Embedded;

@Entity
public class Member {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @Embedded
    private Address address;

    public Member() {}

    public Member(String name, Address address) {
        this.name = name;
        this.address = address;
    }

    // Getters and Setters
}

 

임베디드 타입의 장점

  1. 재사용성: 한 번 정의한 임베디드 타입을 여러 엔티티에서 재사용할 수 있습니다. 예를 들어, Address 타입을 여러 엔티티에서 사용할 수 있습니다.
  2. 높은 응집도: 관련된 데이터를 하나의 값 타입으로 묶어 응집도를 높일 수 있습니다. 이는 코드의 가독성과 유지보수성을 높여줍니다.
  3. 의미 있는 메소드 추가: 임베디드 타입 내에서 해당 타입에만 관련된 비즈니스 로직 메소드를 추가할 수 있습니다. 예를 들어, Period 타입에 isWork() 메소드를 추가할 수 있습니다.
  4. 엔티티의 생명주기에 의존: 임베디드 타입은 그것을 포함하는 엔티티의 생명주기에 의존합니다. 엔티티가 영속화되거나 삭제되면, 임베디드 타입도 함께 처리됩니다.

임베디드 타입과 테이블 매핑

  • 임베디드 타입은 엔티티의 값일 뿐이며, 이를 사용하기 전과 후의 테이블 매핑은 동일합니다.
  • ORM 애플리케이션을 잘 설계하면, 매핑된 테이블 수보다 클래스 수가 많아지는 경우가 많습니다. 이는 객체와 테이블을 세밀하게 매핑할 수 있게 해주기 때문입니다.

 

@AttributeOverride: 속성 재정의

@AttributeOverride는 엔티티에서 같은 값 타입을 여러 번 사용할 때 컬럼 이름이 중복되는 문제를 해결하기 위해 사용됩니다. @AttributeOverrides와 함께 사용하여 특정 컬럼의 속성을 재정의할 수 있습니다.

예제: @AttributeOverride 사용법

Address 클래스 (임베디드 타입)

import javax.persistence.Embeddable;

@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;

    public Address() {}

    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }

    // Getters and Setters
}

 

import javax.persistence.*;

@Entity
public class Member {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @Embedded
    private Address homeAddress;

    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "city", column = @Column(name = "work_city")),
        @AttributeOverride(name = "street", column = @Column(name = "work_street")),
        @AttributeOverride(name = "zipcode", column = @Column(name = "work_zipcode"))
    })
    private Address workAddress;

    public Member() {}

    public Member(String name, Address homeAddress, Address workAddress) {
        this.name = name;
        this.homeAddress = homeAddress;
        this.workAddress = workAddress;
    }

    // Getters and Setters
}

 

설명

  1. Address 클래스: @Embeddable로 정의된 임베디드 타입입니다. city, street, zipcode 필드를 가지고 있습니다.
  2. Member 클래스: @Entity로 정의된 엔티티입니다. homeAddress와 workAddress라는 두 개의 Address 임베디드 타입을 가지고 있습니다.
  3. @AttributeOverride: workAddress 필드에 적용되어, 기본 컬럼 이름이 중복되지 않도록 각각의 컬럼 이름을 재정의합니다. homeAddress와 workAddress의 각 필드는 서로 다른 컬럼 이름을 가집니다.

임베디드 타입과 null

  • 임베디드 타입의 값이 null: 임베디드 타입 자체가 null이면 해당 임베디드 타입이 매핑된 모든 컬럼 값이 null로 저장됩니다.

예를 들어, Member 엔티티의 homeAddress가 null이면, homeAddress에 매핑된 city, street, zipcode 컬럼 값도 모두 null로 저장됩니다.

 

 

값 타입과 객체 타입의 한계 및 해결 방법

값 타입의 개념

  • 값 타입은 복잡한 객체 세상을 단순화하려고 만든 개념입니다.
  • 값 타입은 단순하고 안전하게 다룰 수 있어야 합니다.
  • 값 타입은 식별자가 없고, 단순히 값으로서의 의미를 가집니다.
  • 대표적인 예: int, String, Integer 등

값 타입의 공유 참조 문제

  • 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 부작용이 발생할 수 있습니다.
  • 값 타입을 공유할 경우, 한 엔티티에서 값을 변경하면 다른 엔티티에서도 값이 변경되는 문제가 발생할 수 있습니다.

값 타입 복사

  • 값 타입의 실제 인스턴스를 공유하는 것은 위험합니다.
  • 대신 값 타입을 사용할 때는 값을 복사해서 사용해야 합니다.

객체 타입의 한계

  • 기본 타입은 값을 복사하여 사용하기 때문에 안전합니다.
  • 객체 타입은 참조를 공유하기 때문에 부작용이 발생할 수 있습니다.
 // 기본 타입
int a = 10;
int b = a; // 기본 타입은 값을 복사
b = 4; // a는 여전히 10

// 객체 타입
Address a = new Address("Old");
Address b = a; // 객체 타입은 참조를 전달
b.setCity("New"); // a도 "New"로 변경됨

 

불변 객체 (Immutable Object)

  • 객체 타입의 한계를 극복하기 위해 불변 객체로 설계할 수 있습니다.
  • 불변 객체는 생성 시점 이후 값을 변경할 수 없는 객체입니다.
  • 불변 객체로 설계하면 부작용을 원천 차단할 수 있습니다.

불변 객체로 설계하는 방법

  • 생성자에서만 값을 설정하고, 수정자(Setter)를 제공하지 않습니다.
  • 예제: Address 클래스를 불변 객체로 설계
@Embeddable
public final class Address {
    private final String city;
    private final String street;
    private final String zipcode;

    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }

    public String getCity() {
        return city;
    }

    public String getStreet() {
        return street;
    }

    public String getZipcode() {
        return zipcode;
    }
}

 

 

  • 불변 객체는 생성자에서 모든 값을 설정하고 이후에는 값을 변경할 수 없도록 합니다.
  • 자바의 대표적인 불변 객체로는 Integer, String 등이 있습니다.

 

값 타입의 비교

  • 동일성(Identity) 비교: 두 객체의 참조 값을 비교합니다. 이는 객체가 동일한 인스턴스를 참조하는지를 확인합니다. == 연산자를 사용합니다.
  • 동등성(Equivalence) 비교: 두 객체의 실제 값을 비교합니다. 이는 객체가 동일한 데이터를 포함하는지를 확인합니다. equals() 메소드를 사용합니다.
    • 값 타입의 객체는 equals() 메소드를 적절하게 재정의해야 합니다. 일반적으로 모든 필드를 사용하여 재정의합니다.

값 타입 컬렉션

  • 값 타입 컬렉션은 값 타입을 하나 이상 저장할 때 사용합니다.
    • @ElementCollection과 @CollectionTable 어노테이션을 사용합니다.
    • 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없으므로, 별도의 테이블을 만들어 저장합니다.
  • 값 타입 컬렉션은 지연 로딩(Lazy Loading) 전략을 사용합니다.
@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;
    
    // getters, setters, equals(), hashCode() 정의
}

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;
    
    @ElementCollection
    @CollectionTable(name = "member_address", joinColumns = @JoinColumn(name = "member_id"))
    private List<Address> addresses = new ArrayList<>();
    
    // getters, setters
}

 

 

값 타입 컬렉션의 제약사항

  • 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장합니다.
  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 합니다. 이는 null 입력 및 중복 저장을 방지합니다.
  • 값 타입은 엔티티와 다르게 식별자 개념이 없고, 값이 변경되면 추적이 어렵습니다.

값 타입 컬렉션 대안

  • 실무에서는 값 타입 컬렉션 대신 일대다 관계를 고려할 수 있습니다.
    • 일대다 관계를 위한 엔티티를 만들고, 이 엔티티에서 값 타입을 사용합니다.
    • 영속성 전이(Cascade)와 고아 객체 제거(orphanRemoval)를 사용하여 값 타입 컬렉션처럼 사용합니다.

값 타입 컬렉션 대신 일대다 관계 사용

@Entity
public class AddressEntity {
    @Id @GeneratedValue
    private Long id;
    
    private String city;
    private String street;
    private String zipcode;
    
    // getters, setters
}

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;
    
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "member_id") // 연관관계의 주인 설정
    private List<AddressEntity> addresses = new ArrayList<>();
    
    // getters, setters
}

 

 

엔티티와 값 타입 정리

엔티티 타입의 특징

  • 식별자 (Identifier): 고유 식별자를 가집니다.
  • 생명 주기 관리: 엔티티는 영속성 컨텍스트에 의해 생명 주기를 관리받습니다.
  • 공유 가능: 엔티티는 여러 곳에서 참조되고 공유될 수 있습니다.

값 타입의 특징

  • 식별자 없음 (No Identifier): 값 타입은 고유 식별자를 가지지 않습니다.
  • 엔티티에 의존적인 생명 주기: 값 타입은 이를 소유한 엔티티의 생명 주기에 의존합니다.
  • 공유하지 않는 것이 안전: 값 타입은 복사하여 사용하고, 공유하지 않는 것이 안전합니다.
  • 불변 객체로 설계: 값 타입은 생성 이후에 변경되지 않는 불변 객체로 설계하는 것이 안전합니다.

값 타입 사용 시 주의사항

  • 값 타입은 정말 값 타입이라 판단될 때만 사용해야 합니다.
  • 엔티티와 값 타입을 혼동하여 엔티티를 값 타입으로 만들면 안 됩니다.
  • 식별자가 필요하고, 지속적으로 값을 추적하거나 변경해야 한다면 이는 값 타입이 아니라 엔티티로 설계해야 합니다.

 

잘못된 예 : 엔티티를 값 타입으로 사용

@Entity
public class Address {
    @Id @GeneratedValue
    private Long id;
    private String city;
    private String street;
    private String zipcode;

    // getters, setters, equals, hashCode
}

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @Embedded
    private Address address; // 값 타입처럼 사용하려고 하는 예

    // getters, setters
}

 

 

올바른 예: 값 타입으로 정의

@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;

    // getters, setters, equals, hashCode
}

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @Embedded
    private Address address; // 올바른 값 타입 사용

    // getters, setters
}

 

결론

  • 엔티티는 고유 식별자를 가지며, 생명 주기를 관리받고, 여러 곳에서 공유될 수 있습니다.
  • 값 타입은 식별자를 가지지 않으며, 소유한 엔티티의 생명 주기에 의존하고, 공유되지 않도록 복사하여 사용하며, 불변 객체로 설계하는 것이 좋습니다.
  • 식별자 필요, 지속적 값 추적 및 변경이 필요한 경우에는 값 타입이 아니라 엔티티로 설계해야 합니다.

'JPA' 카테고리의 다른 글

[JPA] JPA 활용1 - 도메인 분석 설계  (0) 2024.06.30
[JPA] JPA 활용1 - 프로젝트 환경설정  (0) 2024.06.30
[JPA] JPA 프록시와 연관관계 관리  (0) 2024.06.30
[JPA] JPA 고급 매핑  (0) 2024.06.29
[JPA] JPA 연관관계 매핑 (2)  (0) 2024.06.29