자바 ORM 표준 JPA 프로그래밍

[자바 ORM 표준 JPA 프로그래밍] 9장 - 값 타입

jny0 2023. 7. 22. 01:49
 

자바 ORM 표준 JPA 프로그래밍 | 김영한 - 교보문고

자바 ORM 표준 JPA 프로그래밍 | 자바 ORM 표준 JPA는 SQL 작성 없이 객체를 데이터베이스에 직접 저장할 수 있게 도와주고, 객체와 관계형 데이터베이스의 차이도 중간에서 해결해준다. 이 책은 JPA

product.kyobobook.co.kr

 

JPA의 데이터 타입은 엔티티 타입과 값 타입으로 나눌 수 있다.

  • 엔티티 타입 : @Entity 로 정의하는 객체, 식별자를 통해 추적 가능
  • 값 타입 ; 단순히 값으로 사용하는 자바 기본 타입, 추적 불가능

 

값 타입의 종류

  • 기본값 타입 - 자바가 제공하는 기본 데이터 타입
    • 자바 기본 타입
    • 래퍼클래스
    • String
  • 임베디드 타입 - 사용자가 직접 정의한 값 타입
  • 컬렉션 값 타입 - 하나 이상의 값 타입을 저장

 

1. 기본 값 타입

@Entity
public class Member {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    private int age;
}
  • 값타입인 name, age 속성은 식별자 값도 없고 생명주기도 회원 엔티티에 의존한다.
  • 회원 엔티티 인스턴스를 제거하면 해당 속성도 제거된다.
  • 값 타입은 공유하면 안 된다.

 

2. 임베디드 타입(복합 값 타입)

  • JPA에서는 새로운 값 타입을 직접 정의해서 사용할 수 있는데, 이것을 임베디드 타입이라고 한다.
  • 임베디드 타입도 int, String처럼 값 타입이다.
  • 회원이 상세한 데이터를 그대로 가지고 있는 것은 객체지향적이지 않으며, 응집력이 떨어진다.
    • 임베디드 타입을 사용해 코드를 더 명확하게 한다.
@Entity
public class Member {

    @Id
    @GeneratedValue
    priate Long id;

    private String name;

    @Embedded
    private Period workPeriod;

    @Embedded
    private Address homeAddress;
}

// 기간 임베디드 타입
@Embeddable
public class Period {

    @Temporal(TemporalType.DATE)
    private Date startDate;

    @Temporal(TemporalType.DATE)
    private Date endDate;

        public boolean isWork(Date date){
            ...
        }
}

// 주소 임베디드 타입
@Embeddable
public class Address {

    private String city;
    private String street;
    private String zipcode;
}
  • @Embeddable 값 타입을 정의하는 곳에 표시
  • @Embedded 값 타입을 사용하는 곳에 표시
  • 임베디드 타입은 기본 생성자가 필수이다.
  • 모든 값타입은 엔티티의 생명주기에 의존하므로 엔티티와 임베디드 값 타입의 관계를 UML로 표현하면 컴포지션 관계가 된다.

 

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

  • 임베디드 타입은 엔티티의 값일 뿐이라, 값이 속한 엔티티의 테이블에 매핑한다.
  • 임베디드 타입 덕분에 객체와 테이블을 아주 세밀하게 매핑하는 것이 가능하다

 

2.2 임베디드 타입과 연관관계

  • 값 타입인 Address가 값 타입인 Zipcode 를 포함한다.
  • 값 타입인 PhoneNumber가 엔티티 타입인 PhoneServiceProvider 를 참조한다.
@Entity
public class Member {

    @Embedded
    private Address address;

    @Embedded
    private PhoneNumber phoneNumber;
}

@Embeddable
public class Address {

    private String street;
    private String city;
    private String state;

    @Embedded
    private Zipcode zipcode;
}

@Embeddable
public class Zipcode {

    private String zip;
    private String plusFour;
}

@Embeddable
public class PhoneNumber {

    private String areaCode;
    private String localNumber;

    @ManyToOne
    PhoneServiceprovider provider;
}

@Entity
public class PhoneServiceProvider {

    @Id
    private String name;
}

 

2.3 @AttributeOverride ; 속성 재정의

  • 임베디드 타입에 정의한 매핑정보를 재정의하려면 엔티티에 @AttributeOverride를 사용
@Entity
public class Member {

    @Id
    @GeneratedValue
    priate Long id;

    private String name;

    @Embedded
    private Address homeAddress;

    @Embedded
    private Address companyAddress;
}
@Entity
public class Member {

    @Id
    @GeneratedValue
    priate Long id;

    private String name;

    @Embedded
    private Address homeAddress;

        @Embedded
        @AttributeOverrides({
            @AttributeOverride(name = "city", column = @Column(name = "COMPANY_CITY")),
            @AttributeOverride(name = "street", column = @Column(name = "COMPANY_STREET")),
            @AttributeOverride(name = "zipcode", column = @Column(name = "COMPANY_ZIPCODE"))
        })
        private Address companyAddress;
}
  • 회사 주소를 하나 더 추가하면서 테이블에 매핑하는 컬럼명이 중복되는 문제 발생
  • @AttributeOverride를 사용해서 매핑정보를 재정의
  • @AttributeOverride를 너무 많이 사용하면 엔티티 코드가 지저분해질 수 있지만, 한 엔티티에 같은 임베디드 타입을 중복해서 사용하는 일은 많지 않다.

 

2.4 임베디드 타입과 null

  • 임베디드 타입이 null이면 매핑한 컬럼 값은 모두 null이 된다.

 

 

3. 값 타입과 불변 객체

  • 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다.

member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();

address.setCity("NewCity"); // 회원 1의 address 값을 공유해서 사용
member2.setHomeAddress(address);
  • 회원 2의 주소만 변경하길 원했지만, 회원 1과 회원 2의 주소가 모두 NewCity로 변경된다.
  • 이러한 공유 참조로 인해 발생하는 버그는 찾아내기 어렵고, 이런 부작용을 막으려면 값을 복사해서 사용하면 된다.

 

3.2 값 타입 복사

member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();

Address newAddress = address.clone();

newAddress.setCity("NewCity");
member2.setHomeAddress(newAddress);
  • 자신을 복사해서 반환하도록 clone() 메소드를 구현
  • 회원 1의 주소 인스턴스를 복사해서 사용하면, 의도한 대로 회원 2의 주소만 NewCity로 변경된다.
    • 영속성 컨텍스트는 회원 2의 주소만 변경된 것으로 판단해서 회원 2에 대해서면 UPDATE SQL을 실행한다.

 

Address a = new Address("Old");
//Address b = a; // 참조만 넘기면 부작용 발생
Address b = a.clone(); // 항상 복사해서 넘겨야 함
b.setCity("New");
  • 객체 타입은 항상 참조 값을 전달한다. 복사해서 사용하지 않으면 공유 참조의 부작용이 발생\
  • 객체를 대입할 때 인스턴스를 복사하지 않고 원본의 참조값을 직접 넘기는 것을 막을 방법이 없다. 따라서 객체의 공유 참조는 피할 수 없다.
  • 가장 단순한 해결책은 객체의 값을 수정하지 못하게 막는 것이다.
    • setCity() 같은 수정자 메서드를 모두 제거하는 방법

 

3.3 불변 객체

  • 객체를 불변하게 만들면 값을 수정할 수 없으므로 부작용을 원천 차단할 수 있다.
  • 따라서 값 타입은 될 수 있으면 불변객체로 설계해야 한다.
  • 가장 간단한 방법은 생성자로만 값을 설정하고 수정자를 만들지 않는 것이다.
  • 불변이라는 작은 제약으로 부작용이라는 큰 재앙을 막을 수 있다.
@Getter
@Embeddable
public class Address {

    private String city;

    protected Address() {} // JPA에서 기본 생성자는 필수다.

        //생성자로 초기 값을 설정한다.
    public Address(String city) {
        this.city = city;
    }
}

 

4. 값 타입의 비교

  • 동일성 비교: 인스턴스의 참조 값을 비교하며, == 사용
  • 동등성 비교: 인스턴스의 값을 비교하며, equals() 사용
    • 값 타입을 비교할 때는 인스턴스가 달라도 그 안의 값이 같으면 같은 것으로 봐야 하기 때문에 동등성 비교를 해야 한다.

 

5. 값 타입 컬렉션

값 타입을 하나 이상 저장하려면 컬렉션에 보관하고 @ElementCollection, @CollectionTable 어노테이션을 사용하면

@Entity
public class Member {

    @Id
    @GeneratedValue
    private Long id;

    @Embedded
    private Address homeAddress;

    @ElementCollection
    @CollectionTable(name = "FAVORITE_FOODS",
        joinColumns = @JoinColumn(name = "MEMBER_ID"))
    @Column(name = "FOOD_NAME")
    private Set<String> favoriteFoods = new HashSet<>();

    @ElementCollection
    @CollectionTable(name = "ADDRESS",
        joinColumns = @JoinColumn(name = "MEMBER_ID"))
    private List<Address> addressHistory = new ArrayList<>();
}

@Embeddable
public class Address {

    private String city;
    private String street;
    private String zipcode;
}

  • favoriteFoods는 기본 값 타입인 String을 컬렉션으로 가진다. 이것을 데이터베이스 테이블로 매핑해야 하는데, 관계형 데이터베이스의 테이블은 컬럼 안에 컬렉션을 포함할 수 없다.
  • 별도의 테이블을 추가하고 @CollectionTable를 사용해서 추가한 테이블을 매핑해야 한다.

 

5.1 값 타입 컬렉션 사용

값 타입 컬렉션은 영속선 전이고아 객체 제거 기능을 필수로 가진다

  • 값 타입 컬렉션도 조회할 때 페치 전략을 선택할 수 있는데 LAZY가 기본이다.
Member member = em.find(Member.class, 1L); // SELECT SQL 호출

Address homeAddress = member.getHomeAddress(); // 회원 조회할 때 같이 조회됨

Set<String> favoriteFodds = member.getFavoriteFoods(); // LAZY

// 실제 컬렉션 사용할 때 -> SELECT SQL 호출
for (String favoriteFood : favoriteFoods) {
    System.out.println("favoriteFood = " + favoriteFood);
}

List<Address> addressHistory = member.getAddressHistory(); // LAZY

addressHistory.get(0); // 실제 컬렉션 사용할 때 -> SELECT SQL 호출

 

5.2 값 타입 컬렉션의 제약사항

  • JPA 구현체들은 값 타입 컬렉션에 변경 사항이 발생화면, 값 타입 컬렉션이 매핑된 테이블의 연관된 모든 데이터를 삭제하고, 현재 값 타입 컬렉션 객체에 있는 모든 값을 데이터베이스에 다시 저장한다.
  • 실무에서 값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 일대다 관계를 고려해야 한다.
  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 한다. 따라서 기본 키 제약 조건으로 인해 컬럼에 null을 입력할 수 없고, 중복된 값을 저장할 수 없는 제약이 발생한다.
  • 따라서 값 타입 컬렉션을 사용하는 대신에 새로운 엔티티를 만들어서 일대다 관계로 설정하고, 추가로 영속성 전이, 고아 객체 제거 기능을 적용하면 값 타입 컬렉션처럼 사용할 수 있다.
@Entity
public class Member {

    @Id
    @GeneratedValue
    private Long id;

    @Embedded
    private Address homeAddress;

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "MEMBER_ID")
    private List<AddressEntity> addressHistory = new ArrayList<>();

}

@Entity
public class AddressEntity {

    @Id
    @GeneratedValue
    private Long id;

    @Embedded
    private Address address;

}