JPA 데이터 타입
엔티티 타입 : @Entity로 정의하는 객체
값 타입 : int, Integer, String 등 단순히 값으로 사용하는 자바 기본 타입이나 객체
값 타입 분류
기본값 타입 : 자바가 제공하는 기본 데이터 타입
자바 기본 타입 (int, double 등)
래퍼 클랫(Integer 등)
String
임베디드 타입(복합 값 타입) : 사용자가 직접 정의한 값 타입
컬렉션 값 타입 : 하나 이상의 값 타입 저장 용도
@ Entity
public class Member {
@ Id @ GeneratedValue
private Long id ;
private String name ;
private int age ;
...
}
Member 엔티티 : 식별자 값(id)도 가지고, 생명주기 있음
값 타입 : 삭별자 값도 없고, 생명주기도 회원 엔티티에 의존
즉, 회원 엔티티 인스턴스 제거하면 name, age 값도 제거됨
값 타입은 공유 불가능 -> 다른 회원 엔티티의 이름 변경한다고 내 이름이 변경되면 안 되기 때문!
@ Entity
public class Member {
@ Id @ GeneratedValue
private Long id ;
private String name ;
private int age ;
//근무 기간
@ Temporal (TemporalType .DATE ) java .util .Date startDate ;
@ Temporal (TemporalType .DATE ) java .util .Date endDate ;
//집 주소 표현
private String city ;
private String street ;
private String zipcode ;
//...
}
회원 엔티티는 이름, 근무 시작일, 종료일, 주소 도시, 번지, 우편번호를 가진다
회원 엔티티는 이름, 근무기간, 집 주소를 가진다 -> 객체지향적
[근무기간, 집주소]를 가지도록 임베디드 타입을 사용해보자.
이렇게 이름, 근무기간, 집 주소 같이 비슷한 속성으로 묶어 낼 수 있는 것이 임베디드 타입
임베디드 타입의 사용
어노테이션 사용(둘 중 하나는 생략 가능)
@Embeddeable : 값 타입 정의하는 곳에 표기
@Embedded : 값 타입 사용하는 곳에 표시
기본 생성자 필수
@ Entity
public class Member {
@ Id @ GeneratedValue
private Long id ;
private String name ;
private int age ;
@ Embedded Period workPeriod ; // 근무 기간
@ Embedded Address homeAddress ; // 집 주소
...
}
@ Embeddable
public class Period {
@ Temporal (TemporalType .DATE ) java .util .Date startDate ;
@ Temporal (TemporalType .DATE ) java .util .Date endDate ;
//..
public boolean isWork (Date date ){
//.. 메소드 정의 가능
}
}
@ Embeddable
public class Address {
@ Column (name ="city" ) //매핑할 컬럼 정의 가능
private String city ;
private String street ;
private String zipcode ;
//..
}
이렇게 임베디드 타입으로 정의한 값 타입들은 재사용할 수 있고 응집도도 높음
회원 엔티티가 더욱 의미있고 응집력 있게 변한 것을 알 수 있음
UML
임베디드 타입 포함한 모든 값 타입은 엔티티의 생명주기에 의존하므로 엔티티와 임베디드 타입의 관계는 컴포지션
composition
전체와 부분이 강력한 연관 관계를 맺고, 전체와 부분이 같은 생명 주기 가짐
예를 들어, 'Car'와 'Engine', 'House'와 'Room'의 관계
Aggregation
전체와 부분이 연관 관계를 맺지만, 전체와 부분이 각자의 생명 주기 가짐
예를 들어, 'Person'과 'Address', '선생님'과 '부서'
* 임베디드 타입을 데이터베이스 테이블에 매핑하는 방법에 대해서 알아보자
* 임베디드 타입은 그저 엔티티의 값 -> 값이 속한 엔티티의 테이블에 매핑
* 임베디드 타입을 사용하면 전과 후에 매핑하는 테이블은 같음
* 대신, 임베디드 타입을 사용하면 객체와 테이블을 세밀하게 매핑 가능
* 잘 설계한 ORM application은 매핑한 테이블의 수보다 클래스의 수가 더 많음
임베디드 타입은 값 타입 포함 or 엔티티 참조 가능
참고
엔티티는 공유가 가능하므로 '참조'라고 표현, 값 타입은 특정 엔티티에 소속되고 논리적인 개념상 공유되지 않으므로 '포함'이라고 표현
값 타입인 Addresss가 값 타입인 Zipcode 포함하고, 값 타입인 PhoneNumber가 엔티티 타입인 PhoneServiceProvider 참조
@ Entity
public class Member {
@ Embedded Address address ; //임베디드 타입 포함
@ Embedded PhoneNumber phoneNumber ; //임베디드 타입 포함
//...
}
@ Embeddable
public class Address {
String street ;
String city ;
String state ;
@ Embedded Zipcode zipcode ; // 임베디드 타입 포함
}
@ Embeddable
public class Zipcode {
String zip ;
String plusFour ;
}
@ Embeddable
public class PhoneNumber {
String areaCode ;
String localNumber ;
@ ManyToOne PhoneServiceProvider provider ; // 엔티티 참조
...
}
@ Entity
public class PhoneSerivceProvider {
@ Id String name ;
...
}
@AttributeOverride : 속성 재정의
임베디드 타입에 정의한 매핑정보를 재정의하고 싶을 때 사용
회원에게 집주소 외 회사주소가 하나 더 필요한 상황이라면?
@ Entity
public class Member {
@ Id @ GeneratedValue
private Long id ;
private String name ;
@ Embedded Address homeaddress ;
@ Embedded Address companyAddress ;
}
이때, 테이블에 매핑하는 컬럼명이 중복되는 문제점 발생
Address의 city, street, zipcode 값이 homeAddress인지, companyAddress 인지 알 수 없음
@ Entity
public class Member {
@ Id @ GeneratedValue
private Long id ;
private String name ;
@ Embedded Address homeaddress ;
@ Embedded
@ AttributeOverrides ({
@ AttributeOverride (name ="city" , column =@ Column (name ="COMPANY_CITY)),
@AttributeOverride(name=" street ", column =@ Column (name ="COMPANY_STREET)),
@AttributeOverride(name=" state ", column =@ Column (name ="COMPANY_STATE))
})
Address companyAddress;
}
@AttributeOverride 어노테이션 너무 많이 사용하면 엔티티 코드 많이 지저분해짐
다행히, 한 엔티티에 같은 임베디드 타입 중복해 사용하는 일은 많지 않음
임베디드 타입이 null이면 매핑한 컬럼 값은 모두 null
member .setAddress (null );
em .persist (member );
회원 테이블의 주소와 관련된 city,street,zipcode 컬럼 값은 모두 null
임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 문제 발생
회원2의 주소만 "NewCity" 로 변경하고 싶었는데, 회원 1의 주소도 변경됨
member1 .setHomeAddress (new Address ("OldCity" ));
Address address = member1 .getHomeAddress ();
address .setCity ("NewCity" ); // 회원1의 address 값을 공유해서 사용
member2 .setHomeAddress (address );
이렇게 공유 참조로 인해 발생하는 "부작용"은 정말 찾기가 어려움
member1 .setHomeAddress (new Address ("OldCity" ));
Address address = member1 .getHomeAddress ();
//회원1의 address 값을 복사해서 새로운 newAddress 값을 생성
Address newAddress = address .clone ();
newAddress .setCity ("NewCity" );
member2 .setHomeAddress (address );
회원2에 새로운 주소 할당하기 위해 clone() 메소드 사용
값 복사해서 사용하면 공유 참조로 발생하는 부작용 방지 기능
임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입 이라서 값 복사를 해야됨
자바는 기본 타입에 값 대입하면 값 복사해 전달
int a = 10 ;
int b = a ;
b = 4 ;
// a=10, b=4
int b=a에서 a의 값인 10 복사해서 b에 넘겨줌
a,b는 완전히 독립된 값 가지고 부작용도 없음
Address 같은 객체 타입은 객체에 값을 대입하면 항상 값이 아닌 참조값 전달
Address a = new Address ("Old" );
Address b = a ; //객체 타입은 항상 참조 값 전달
b .setCity ("New" );
Address b=a에서 a의 값 "old"가 아닌 a가 참조하는 인스턴스의 참조 값을 넘겨줌
결국 공유 참조로 인해 부작용이 발생해, b뿐 아니라 a의 city값도 변경됨
그래서 위에서 했던 방법처럼, 객체 대입할 때마다 인스턴스 복사해 대입하면 공유 참조 피할 수 있음
Address a = new Address ("Old" );
Address b = a .clone ();
b .setCity ("New" );
복사하지 않고 원본의 참조 값 직접 넘기는 것을 막을 방법이 없다는 것이 문제
객체의 공유 참조를 완벽히 피할 수는 없지만 그래도 주의해서 부작용이 발생하지 않도록 값 타입을 복사해야 된다는 점을 기억!
값 타입은 부작용 걱정 없이 사용할 수 있어야 함
객체를 불변하게 만들면? -> 값 수정할 수 없어 부작용 원천 차단 가능
결론: 값 타입은 될 수 있으면 불변 객체로 설계하는 것이 좋음
불변 객체도 결국 객체라, 공유 참조 피할 수는 없지만 공유 참조가 일어난다고 해도 인스턴스의 값 수정할 수 없어 부쟉용 발생하지는 않음
생성자로만 값 설정하고 수정자 만들지 않는 방법 사용
@ Embeddable
public class Address {
private String city ;
protected Address () {} //기본 생성자
//생성자로 초기 값 설정
public Address (String city ) {this .city = city }
//접근자(Getter)는 노출
public String getCity () {
return city ;
}
//수정자(Setter)는 만들지 않는다.
}
Address address = memeber1 .getHomeAddress ();
//회원1의 주소값 조회해서 새로운 주소값 생성
Address newAddress = new Address (address .getCity ());
member2 .setHomeAddress (newAddress );
Address는 불변 객체라서 만약 값을 수정해야 한다면 새로운 객체를 생성해서 사용해야 한다
참고 : Integer, String 은 자바가 제공하는 대표적인 불변 객체
int a = 10 ;
int b = 10 ;
Address a = new Address ("서울시" ,"종로구" ,"1번지" )
Address b = new Address ("서울시" ,"종로구" ,"1번지" )
int a와 b, Adderss a와 b는 각각 같다고 표현
자바가 제공하는 객체 비교
동일성(Identity) 비교 : 인스턴스의 참조 값 비교(==사용)
동등성(Equivalence) 비교 : 인스턴스의 값 비교(equals() 사용)
하나 이상의 값 타입 저장하려면 컬렉션에 보관
Set, List 사용
값 타입을 묶어서 PK 설정
@ElementCollection, @CollectionTable 사용
@ Entity
public class Member {
@ Id @ GemeratedValue
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<String>();
@ElemnetCollection
@CollectionTable(name=" ADDRESS ",
joinColumns = @ JoinColumn (name ="MEMBER_ID" ))
private List <Address > addressHistory = new ArrayList <Address > ();
//...
}
@ Embeddable
public class Address {
@ Column
private String city ;
private String street ;
private String zipcode ;
//...
}
Member엔티티에 favoriteFoods, addressHistory 두 가지 값 타입 지정
favoriteFoods는 기본값 타입인 String을 컬렉션으로 가짐
addressHistory는 임베디드 타입인 Address를 컬렉션으로 가짐
관계형 데이터베이스의 테이블은 컬럼안에 컬렉션 포함 불가능
별도의 테이블 추가하고 @CollectionTable 사용해 추가한 테이블 매핑
favoriteFoods처럼 값으로 사용되는 컬렉션이 하나면 @Column 사용해 컬럼명 지정 가능
addressHistory는 임베디드 타입이므로 다른 주소 추가하고 싶으면 @AttributeOverride 사용해 재정의 가능
Member member = new Member ();
//임베디드 값 타입
member .setHomeAddress (new Address ("통영" ,"몽돌해수욕장" ,"660-123" ));
//기본값 타입 컬렉션
member .getFavoriteFoods ().add ("짬뽕" );
member .getFavoriteFoods ().add ("짜장" );
member .getFavoriteFoods ().add ("탕수육" );
//임베디드 값 타입 컬렉션
member .getAddressHistory ().add (new Address ("서울" ,"강남" ,"123-123" ));
member .getAddressHistory ().add (new Address ("서울" ,"강북" ,"000-0000" ));
em .persist (member );
JPA는 member 엔티티의 값 타입(기본값인지, 임베디드 값인지)도 함께 저장
실제 데이터베이스에서 실행되는 INSERT SQL
member : INSERT SQL 1번
member.addressHistory : INSERT SQL 2번
결론 : em.persist(member) 한 번 호출로 총 6번의 INSERT SQL 실행
값 타입 컬렉션은 영속성 전이 + 고아 객체 제거 기능을 필수로 가짐
값 타입 컬렉션 조회할 때 패치 전략 선택 가능
LAZY가 기본
@ElementCollection(fetch=FetchType.LAZY)
지연로딩으로 모두 설정했을 때, 아래 코드를 실행해보자
//SQL : SELECT ID,CITY,STREET,ZIPCODE FORM MEMEBR WHERE ID=1
Member member = em .find (Member .class ,1L ); //1. member
//2. member.homeAddress
Address homeAddress = member .getHomeAddress ();
//3. member.favoriteFoods
Set <String > favoriteFoods = member .getFavoriteFoods (); //LAZY
//SQL : SELECT MEMBER_ID,FOOD_NAME FROM FAVORITE_FOODS
//WHERE MEMBER_ID=1
for (String favoriteFood : favoriteFoods ) {
System .out .println ("favoriteFoods =" + favoriteFood );
}
//4. member.addressHistory
List <Address > addressHistory = member .getAddressHistory (); //LAZY
//SQL: SELECT MEMBER_ID,CITY,STREET,ZIPCODE FROM ADDRESS
//WHERE MEMBER_ID=1
addressHistory .get (0 );
member : 회원만 조회. 이때 임베디드 값 타입인 homeAddress도 함깨 조회. SELECT SQL 1번 실행
member.homeAddress : 1번에서 회원 조회할 때 같이 조회
member.favoriteFoods : LAZY로 설정해서 실제 컬렉션 사용할 때 SELECT SQL 1번 호출
member.addressHistory : LAZY로 설정해서 실제 컬렉션 사용할 때 SELECT SQL 1번 호출
Member member = em .find (Member .class , 1L );
//1. 임베디드 값 타입 수정
member .setHomeAddress (new Address ("새로운도시" ,"신도시1" ,"123456" ));
//2. 기본값 타입 컬렉션 수정
Set <String > favoriteFoods = member .getFavoriteFoods ();
favoriteFoods .remove ("탕수육" );
favoriteFoods .add ("치킨" );
//3. 임베디드 값 타입 컬렉션 수정
List <Address > addressHistory = member .getAddressHistory ();
addressHistory .remove (new Address ("서울" ,"기존 주소" ,"123-123" ));
addressHistory .add (new Address ("새로운도시" ,"새로운 주소" ,"123-456" ));
임베디드 값 타입 수정 : homeAddress 임베디드 값 타입은 MEMBER 테이블과 매핑했기 때문에 MEMBER 테이블만 UPDATE -> Member 엔티티를 수정하는 것과 같음
기본값 타입 컬렉션 수정 : 탕수육을 치킨으로 변경 -> 자바의 String 타입은 수정 불가
임베디드 값 타입 컬렉션 수정 : 값 타입은 불변해야 함 -> 컬렉션에서 기존 주소 삭제하고 새로운 주소 등록
엔티티는 식별자 존재
엔티티 값 변경해도 식별자로 디비에 저장된 원본 데이터 쉽게 찾아 변경 가능
특정 엔티티 하나에 소속된 값 타입은 값이 변경되어도 자신이 소속된 엔티티를 디비에서 찾고 값 변경하면 됨
값 타입은 식별자 없음
값 변경하면 디비에 저장된 원본 데이터 찾기 어려움
문제는 값 타입 컬렉션
값 타입 컬렉션에 보관된 값 타입들은 별도의 테이블에 보관
여기에 보관된 값 타입의 값 변경되면 디비에 있는 원본 데이터 찾기 어려움
이런 문제들로 JPA 구현체들은 값 타입 컬렉션에 변경사항 발생하면 값 타입 컬렉션이 매핑된 테이블의 연관된 모든 데이터 삭제 후 현재 값 타입 컬렉션 객체에 있는 모든 값을 디비에 다시 저장
DELETE FROM ADDRESS WHERE MEMBER_ID= 100
INSERT INTO ADDRESS (MEMBER_ID, CITY, STREET, ZIPCODE) VALUES (100 ,...)
INSERT INTO ADDRESS (MEMBER_ID, CITY, STREET, ZIPCODE) VALUES (100 ,...)
실무에서는, 값 타입 컬렉션이 매핑된 테이블에 데이터가 많으면 값 타입 컬렉션 대신에 일대다 관계 고려하기
값 타입 컬렉션 매핑하는 테이블은 모든 컬럼 묶어서 기본 키 구성해야 됨
디비 기본 키 제약 조건으로 인해 컬럼에 null 입력 불가
값은 값 중복해서 저장 불가능
값 타입 컬렉션의 대안
값 타입 컬렉션을 엔티티로 승격
새로운 엔티티 만들어 일대다 관계로 설정 + 영속성 전이 + 고아 객체 제거 기능 적용 -> 값 타입 컬렉션처럼 사용 가능
값 타입 컬렉션은 양방향 설계 불가. 엔티티로 승격함으로써 양방향 매핑 가능
식별자 개념 추가되면서 값 추적 가능
@ Entity
public class AddressEntity {
@ Id
@ GenerateValue
private Long id ; // 식별자 생김
@ Embedded Address address ; //여기서 값 타입 사용
public AddressEntity (){
}
// 이런식으로 값 타입에 생성자 이용해서 인스턴스 생성해 넣어줌
public AddressEntity (String city , String street , String zipcode ){
this .address = new Address (city , street , zipcode );
}
...
}
그럼 값 타입 컬렉션은 언제 사용하나?
정말 단순한, 중복 가능한 select box 있을 때 사용
즉, 진짜 단순한 경우 or 값 추적 필요 없는 경우 사용