자바 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;
}
'자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 10.4 - QueryDSL (0) | 2023.07.22 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 10.2 - JPQL (0) | 2023.07.22 |
[자바 ORM 표준 JPA 프로그래밍] 8장 - 프록시와 연관관계 관리 (0) | 2023.06.13 |
[자바 ORM 표준 JPA 프로그래밍] 7장 - 고급 매핑 (0) | 2023.06.12 |
[자바 ORM 표준 JPA 프로그래밍] 6장 - 다양한 연관관계 매핑 (0) | 2023.05.15 |