Skip to content

Latest commit

 

History

History
473 lines (418 loc) · 18.6 KB

[민경] 9장.md

File metadata and controls

473 lines (418 loc) · 18.6 KB

9장 값 타입

  • JPA 데이터 타입
    1. 엔티티 타입 : @Entity로 정의하는 객체
    2. 값 타입 : int, Integer, String 등 단순히 값으로 사용하는 자바 기본 타입이나 객체
  • 값 타입 분류
    1. 기본값 타입 : 자바가 제공하는 기본 데이터 타입
    1. 자바 기본 타입 (int, double 등)
    2. 래퍼 클랫(Integer 등)
    3. String
    1. 임베디드 타입(복합 값 타입) : 사용자가 직접 정의한 값 타입
    2. 컬렉션 값 타입 : 하나 이상의 값 타입 저장 용도

기본값 타입

@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 image
    • 임베디드 타입 포함한 모든 값 타입은 엔티티의 생명주기에 의존하므로 엔티티와 임베디드 타입의 관계는 컴포지션
    • composition
      • 전체와 부분이 강력한 연관 관계를 맺고, 전체와 부분이 같은 생명 주기 가짐
      • 예를 들어, 'Car'와 'Engine', 'House'와 'Room'의 관계
    • Aggregation
      • 전체와 부분이 연관 관계를 맺지만, 전체와 부분이 각자의 생명 주기 가짐
      • 예를 들어, 'Person'과 'Address', '선생님'과 '부서'

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

스크린샷 2021-10-03 오후 3 36 45

* 임베디드 타입을 데이터베이스 테이블에 매핑하는 방법에 대해서 알아보자 * 임베디드 타입은 그저 엔티티의 값 -> 값이 속한 엔티티의 테이블에 매핑 * 임베디드 타입을 사용하면 전과 후에 매핑하는 테이블은 같음 * 대신, 임베디드 타입을 사용하면 객체와 테이블을 세밀하게 매핑 가능 * 잘 설계한 ORM application은 매핑한 테이블의 수보다 클래스의 수가 더 많음

임베디드 타입과 연관관계

  • 임베디드 타입은 값 타입 포함 or 엔티티 참조 가능
  • 참고
    • 엔티티는 공유가 가능하므로 '참조'라고 표현, 값 타입은 특정 엔티티에 소속되고 논리적인 개념상 공유되지 않으므로 '포함'이라고 표현
    • 스크린샷 2021-10-03 오후 3 36 58
  • 값 타입인 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이면 매핑한 컬럼 값은 모두 null
member.setAddress(null); 
em.persist(member);
  • 회원 테이블의 주소와 관련된 city,street,zipcode 컬럼 값은 모두 null

값 타입과 불변 객체

값 타입 공유 참조

  • 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 문제 발생
    • 회원2의 주소만 "NewCity" 로 변경하고 싶었는데, 회원 1의 주소도 변경됨
    스크린샷 2021-10-03 오후 3 37 15
member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();

address.setCity("NewCity"); // 회원1의 address 값을 공유해서 사용
member2.setHomeAddress(address);
  • 이렇게 공유 참조로 인해 발생하는 "부작용"은 정말 찾기가 어려움
    • 값을 복사해 사용해서 부작용 방지

값 타입 복사

스크린샷 2021-10-03 오후 3 37 21

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는 각각 같다고 표현
  • 자바가 제공하는 객체 비교
    1. 동일성(Identity) 비교 : 인스턴스의 참조 값 비교(==사용)
    2. 동등성(Equivalence) 비교 : 인스턴스의 값 비교(equals() 사용)

값 타입 컬렉션

  • 하나 이상의 값 타입 저장하려면 컬렉션에 보관
    • Set, List 사용
    • 값 타입을 묶어서 PK 설정
  • @ElementCollection, @CollectionTable 사용

스크린샷 2021-10-03 오후 3 37 35

@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);
  1. member : 회원만 조회. 이때 임베디드 값 타입인 homeAddress도 함깨 조회. SELECT SQL 1번 실행
  2. member.homeAddress : 1번에서 회원 조회할 때 같이 조회
  3. member.favoriteFoods : LAZY로 설정해서 실제 컬렉션 사용할 때 SELECT SQL 1번 호출
  4. 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"));
  1. 임베디드 값 타입 수정 : homeAddress 임베디드 값 타입은 MEMBER 테이블과 매핑했기 때문에 MEMBER 테이블만 UPDATE -> Member 엔티티를 수정하는 것과 같음
  2. 기본값 타입 컬렉션 수정 : 탕수육을 치킨으로 변경 -> 자바의 String 타입은 수정 불가
  3. 임베디드 값 타입 컬렉션 수정 : 값 타입은 불변해야 함 -> 컬렉션에서 기존 주소 삭제하고 새로운 주소 등록

값 타입 컬렉션의 제약사항

  • 엔티티는 식별자 존재
    • 엔티티 값 변경해도 식별자로 디비에 저장된 원본 데이터 쉽게 찾아 변경 가능
    • 특정 엔티티 하나에 소속된 값 타입은 값이 변경되어도 자신이 소속된 엔티티를 디비에서 찾고 값 변경하면 됨
  • 값 타입은 식별자 없음
    • 값 변경하면 디비에 저장된 원본 데이터 찾기 어려움
    • 문제는 값 타입 컬렉션
      • 값 타입 컬렉션에 보관된 값 타입들은 별도의 테이블에 보관
      • 여기에 보관된 값 타입의 값 변경되면 디비에 있는 원본 데이터 찾기 어려움
      • 이런 문제들로 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 값 추적 필요 없는 경우 사용