기본값 타입
JPA의 데이터 타입 분류
최상위 레벨로 보면 JPA는 데이터 타입을 두 가지로 분류한다.
엔티티 타입
- @Entity로 정의하는 객체
- 데이터가 변해도 식별자로 지속해서 추적 가능
⇒ 엔티티 내부의 모든 값들을 바꿔도 식별자만 유지되면 추적이 가능하다는 의미 - Ex: 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능
- 예를 들어 식별자가 100번일 경우 키나 나이 값이 바껴도 식별자는 그대로 100번이라는 것
값 타입
- int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
- 식별자가 없고 값만 있으므로 변경시 추적 불가
- ex. 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체
- ex. 게시판의 String 컨텐츠가 바뀌면 추적이 불가능. 단, Board라는 엔티티는 추적이 가능.
값 타입 분류
기본값 타입
- 자바 기본 타입(int, double)
- 래퍼 클래스(Integer, Long)
- String
임베디드 타입(embedded type, 복합 값 타입)
- Ex: 우편번호 , 좌표 같은 복합 값을 Position클래스로 만들어 쓰려고하는 것을 임베디드 타입
컬렉션 값 타입(collection value type)
- Java collection(Array, Map, Set)에 값을 넣을수 있는 것을 컬렉션 값 타입이라 한다.
기본값 타입
- Ex: String name, int age
- 생명주기를 엔티티에 의존
- 회원을 삭제하면 이름, 나이 필드도 함께 삭제
- 값 타입은 절대 공유하면 안된다.
- Side Effect → 회원 이름 변경시 다른 회원의 이름도 함께 변경되면 안된다.
※ 참고
자바의 기본 타입은 절대 공유가 되지 않는다.
- int, double 같은 기본 타입(primitive type)은 절대 공유되지 않는다.
- 기본 타입은 항상 값을 복사한다.
int a = 10;
int b = a; // a 값이 b로 복사되기 때문에 a, b는 저장공간을 따로 갖는다.
b = 20;
System.out.println("a = " + a); // 20
System.out.println("b = " + b); // 10
그러나, Integer같은 래퍼 클래스나 String 같은 특수한 클래스는 공유 가능한 객체이다.
- 하지만 복사할 때 값이 복사되는 것이 아닌 주소 값(reference)이 복사된다.
Integer a = new Integer(10);
Integer b = a;
System.out.println("a = " + a); // 10
System.out.println("b = " + b); // 10
- 그런데 만약 실제로는 없는 Method 지만 a.setValue(); 라는 메소드가 있다고 가정해서 값을 변경한다면 a, b는 Referece를 공유하고 있기 때문에 값이 같이 변경될 것이다.
- 하지만 가정이라고 말했다시피 setValue라는 메소드는 존재하지 않기 때문에 Integer같은 래퍼 클래스나 String 같은 특수한 클래스는 공유 가능한 객체지만 변경할 수 없다. - side effect 자체가 존재하지 않는다!!
임베디드 타입(복합 값 타입)
개요
- 새로운 값 타입을 직접 정의할 수 있다.
- JPA는 임베디드 타입(embedded type)이라 한다.
- 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 한다.
- int, String과 같은 값 타입 (즉, 추적이 불가능하고 변경하면 끝이다)
예제를 통해 알아보면 단번에 임베디드 타입이 무엇을 얘기하는 건지 파악할 수 있을 것이다.
Example
1. 회원 엔티티는 이름, 근무 시작일, 근무 종료일, 주소 도시, 주소 번지, 주소 우편번호를 가진다.
그런데 테이블을 살펴보니 연관시켜서 한번에 관리하는 것이 더 수월하다는 생각이 든다.
- city, street, zipcode는 주소로 합칠 수 있을 것 같다.
- 근무 시작일, 근무 종료일은 근무시간으로 합칠 수 있을 것 같다.
그래서 회원 엔티티를 좀 더 추상화시켜서 아래와 같이 얘기할 수 있다.
2. 회원 엔티티는 이름, 근무 기간, 집 주소를 가진다
그렇다면 JPA에서는 위와 같이 구성하려면 어떻게 해야할까?
임베디드 타입 사용법
- @Embeddable: 값 타입을 정의하는 곳에 표시
- @Embedded: 값타입을 사용하는 곳에 표시
- 기본 생성자 필수
임베디드 타입의 장점
- 재사용
- Period나 Address는 다른 객체에서도 사용 할 수 있어 재사용성을 높힌다.
- 높은 응집도
- Period나 Address와 같이 연관된 속성들을 한번에 관리하기 때문에 응집도가 높다.
- 그러므로 Period.isWork()처럼 해당 값 타입만 사용하는 의미있는 메소드를 만들 수 있다. (객체 지향적 설계)
private boolean isWork(){
...
}
- 임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명주기를 의존한다.
임베디드 타입과 테이블 매핑
임베디드 타입을 통해 객체를 분리하더라도 테이블은 하나만 매핑된다
즉, 임베디드 타입을 사용하든 안하든 DB 테이블 입장에서는 변경될게 없다. 대신 매핑만 살짝 설정해주면 된다.
DB 테이블은 데이터 관리가 목적이기 때문에 아래 그림처럼 설계되는 것이 맞다.
그러나 객체는 데이터 뿐만 아니라 메소드라고 하는 기능(행위)까지 존재하기 때문에 임베디드 타입처럼 관련 속성들을 묶었을 때 가져갈 수 있는 장점이 많다.
코드를 통해 좀 더 자세히 알아보자.
우선 임베이드 타입을 사용하지 않고 Member 엔티티를 설계했을 때는 다음과 같다.
@Entity
public class Member{
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
private LocalDateTime startDate;
private LocalDateTime endDate;
private String city;
private String street;
private String zipcode;
}
쿼리 또한 정의된 속성들이 날라가는 것을 확인할 수 있다.
그런데 좀 더 객체 지향적으로 설계를 원하기 때문에 다음과 같이 공통된 속성들을 임베티드 타입을 이용해서 하나로 묶었다.
- 기간(Period) : startDate, endDate
- 주소(Address) : city, street, zipcode
@Embeddable //값 타입이 정의되는 곳에 @Embeddable 사용
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
public Period() { }
}
@Embeddable //값 타입이 정의되는 곳에 @Embeddable 사용
public class Address {
private String city;
private String street;
private String zipcode;
public Address() { }
}
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
/*
//임베디드 타입을 사용하지 않으면 주석 내의 기존 형태로 값 타입으로 선언해줘야 한다.
//Period
private LocalDateTime startDate;
private LocalDateTime endDate;
//Address
private String city;
private String street;
private String zipcode;
*/
@Embedded //값 타입이 사용되는 곳에 @Embedded 사용
private Period workPeriod;
@Embedded //값 타입이 사용되는 곳에 @Embedded 사용
private Address homeAddress;
}
결과를 살펴보면 기존 임베이드 타입을 사용하지 않았을 때처럼 테이블 생성 쿼리가 발생하는 것을 확인할 수 있다.
그런데 차이점은 자바 코드 상에서 좀 더 객체 지향적으로 Period, Address를 활용할 수 있다는 사실이다.
장점
- 임베디드 타입은 엔티티의 값일 뿐이다.
- 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다. (자료3 참고)
- 객체와 테이블을 아주 세밀하게(find-grained) 매핑하는 것이 가능하다.
- 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많음.
임베디드 타입과 연관관계
- Member Entity는 Address라는 임베디드 타입을 가질 수 있고, Address라는 임베디드 타입 역시 zipcode라는 값 타입을 가질 수 있다.
- 그런데 Member Entity는 PhoneNumber라는 임베디드 타입을 가질 수 있고, PhoneNumber라는 임베디드 타입은 PhoneEntity라는 Entity를 가질 수 있다.
=> 생각해보면 간단하다 PhoneNumber 입장에선 PhoneEntity의 FK값만 갖고 있으면 되기 때문이다. - 임베디드 타입 클래스 안에서 Column도 사용 가능하다
@Embeddable
public class Address {
private String city;
private String street;
@Column(name = "ZIPCODE") // 이것 역시 가능하다.
private String zipcode;
private Member member; //가능하다.
public Address() { }
}
@AttributeOverride: 속성 재정의
만약 Member안에 동일한 임베디드 타입이 있다면 어떻게 될까?
결과를 보면 알겠지만 반복된 컬럼이 매핑됐다는 error 메세지를 띄운다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
@Embedded
private Period workPeriod;
@Embedded
private Address homeAddress;
//workAddress라는 동일한 homeAddress와 동일한 타입이 추가된다면 어떻게 될까.
@Embedded
private Address workAddress;
//error MappingException: Repeated column in mapping for entity
}
@AttributeOverride 를 사용해서 컬러 명 속성을 재정의 해준다.
@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_STREET"))
})
private Address workAddress;
즉, 한 엔티티에서 같은 값 타입을 사용하면 컬럼 명이 중복되므로 @AttributeOverrides, @AttributeOverride를 사용해서 컬러 명 속성을 재정의해줘야 한다.
※ 참고
임베디드 타입의 값이 null이면 매핑한 컬럼 값은 모두 null
값 타입과 불변 객체
값 타입은 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념이다. 따라서 값 타입은 단순하고 안전하게 다 룰 수 있어야 한다.
값 타입 공유 참조
- 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다.
- (문제!!) 회원 1, 회원 2가 같은 값 타입인 주소를 보고 있을 때 city 값을 NewCity로 변경하면 회원1, 회원2는 각각 테이블을 갖고 있으므로 두 테이블의 주소 값이 NewCity로 변경된다.
1. member1 와 member2가 같은 address를 바라보고 있다.
Address address = new Address("city", "street", "10000");
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(address);
em.persist(member);
Member member2 = new Member();
member2.setUsername("member2");
member2.setHomeAddress(address);
em.persist(member2);
2. member의 주소지를 변경하고싶어서 Address를 수정한다.
- SideEffect 발생 - member2의 Address정보까지 바뀌어 버린다.
- 결과를 확인해보면 두 member의 값이 변경되는 것을 확인할 수 있다. (update 쿼리도 두번 나간다.)
- 이런 버그는 잡기 정~~말 힘들다...
- 물론 개발자 일부러 이렇게 공유해서 사용하고 싶어서 그랬을 수도 있다. 그러나 이럴 때는 주소를 엔티티로 만들어서 사용하는 것이 좋다.
member.getHomeAddress().setCity("newCity");
값 타입 복사
위처럼 값 타입의 실제 인스턴스인 값을 공유하는 것은 위험하다. 그러므로 대신 값(인스턴스)를 복사해서 사용해야 한다.
Address address = new Address("city", "street", "10000");
Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());
객체 타입의 한계
그런데 누군가 실수로 값 복사가 아닌 기존 값을 넣는다면 막을 수 있을까? 그런 방법은 없다...
- 항상 값을 복사해서 사용하면 공유참조로 인해 발생하는 부작용을 피할 수 있다.
- 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본타입(primitive type)이 아닌 객체 타입이다.
- 자바 기본 타입에 값을 대입하면 값을 항상 복사된다.
- 그러나 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다.
- 즉, 객체의 공유 참조는 피할 수 없다.
Address address = new Address("city", "street", "10000");
Address copyAddress = new Address(address.getCity(),
address.getStreet(),
address.getZipcode());
...
member2.setHomAddress(member.getHomeAddress()); //막을 수 없다.
- 기본 타입(primitive type)은 '='으로 값을 복사한다.
하지만, 객체 타입에서 '='을 통한 대입은 참조를 전달한다. → 인스턴스가 하나이기에 같이 변경된다.
불변 객체
불변이라는 작은 제약으로 부작용이라는 큰 재앙을 막을 수 있다.
- 객체 타입을 수정할 수 없도록 부작용을 원천 차단한다.
- 값 타입은 불변 객체(immutable object)로 설계해야 한다.
- 불변 객체: 생성 시점 이후 절대 값을 변경할 수 없는 객체
- 생성자로만 값을 설정하고 수정자(Setter)를 만들지 않으면 된다.
- 혹은 setter를 private으로 설정하며 내부에서만 사용할 수 있도록 한다.
- 참고: Integer, String은 자바가 제공하는 대표적인 불변 객체
@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;
}
public String getCity() { return city; }
public String getStreet() { return street; }
public String getZipcode() { return zipcode; }
}
물론 위 코드처럼 setter가 없으면 getter에 다음과 같은 메세지가 뜰것이다.
for property-based access both setter and getter should be present
이는 JPA가 FIELD, 프로퍼티(getter,setter) 엑세스 두가지 방법이 있는데, 아마 getter 같은게 보여서 정확히 판단이 어려워서 그렇게 나오는 것 같다.
참고로 프로퍼티 접근은 최근에는 권장하지 않는다고 한다.
해결방법은 2가지가 있다.
1. @Access로 필드 접근을 명시한다.
@Embeddable
@Access(AccessType.FIELD)
public class HelloEmb {
2. 첫 컬럼에 @Column으로 필드 접근 방식을 사용하고 있다는 것을 명시한다.
@Embeddable
public class HelloEmb {
@Column
private String hello;
값을 변경해야 하는 경우에는 어떻게 하나요?
- 새로 만들어 바꿔줘야 한다.(ex: city가 바뀌게 된다.)
Address newAddress = new Address("newCity", address.getStreet(), address.getZipcode());
member.setHomwAddress(newAddress);
참고