👩💻 개발개념
엔티티와 값 타입
케로⸝⸝◜࿀◝ ⸝⸝
2024. 5. 7. 20:49
📢 안내
해당 글은
- 김영한 님의 JPA 프로그래밍 [9장 값 타입] 중 일부
- 최범균 님의 도메인 주도 개발 시작하기 [1.6 엔티티와 밸류] 중 일부
를 발췌하여 정리한 것입니다.
0. 들어가며
Entity와 DTO*, VO*를 구분하는 다양한 글들이 많은데 이런 글들을 읽다 보면 이해가 잘 안 가는 부분들이 있다. 이것을 단순 개념으로 접근하기보다는, 데이터베이스의 테이블과 객체를 매핑해주는JPA에 대한 배경 지식이 있어야 아하! 하고 이해가 되는 지점이 있다는 것을 알게 되었다. 따라서, JPA 관점에서 이해하고, 글을 정리해 보았다 :)
* DTO: Data Transfer Object, 데이터 교환을 위한 객체
* VO: Value Object, 값 객체 혹은 값 타입
배경지식 간단 정리
JPA
- Java Persistence API
- 자바 진영의 ORM 기술 표준
- ORM
- Object-Relational Mapping
- 객체와 관계형 데이터베이스를 매핑한다는 뜻으로, ORM 프레임워크는 객체와 테이블을 매핑해서 패러다임의 불일치 문제를 개발자 대신 해결해준다.
@Entity
- JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 애노테이션을 필수로 붙여야 한다.
- @Entity가 붙은 클래스는 JPA가 관리하는 것으로, 엔티티라 부른다.
영속성 컨텍스트
- persistence context
- 엔티티를 영구 저장하는 환경
- 엔티티 매니저로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
- ex) em.persist(member);
- 영속성 컨텍스트는 엔티티 매니저를 생성할 때 만들어진다. 이것은 논리적인 개념이고 눈에 보이지도 않는다!
- 엔티티 매니저를 통해서 영속성 컨텍스트에 접근할 수 있고, 영속성 컨텍스트를 관리할 수 있다.
- 영속성 컨텍스트가 관리하는 엔티티를 영속 상태라고 한다.
- 영속성 컨텍스트는 엔티티를 식별자 값(@Id로 테이블의 기본 키와 매핑한 값)으로 구분한다.
- 따라서, 영속 상태는 식별자 값이 반드시 있어야 한다.
1. 개요
JPA의 데이터 타입을 가장 크게 분류하면 엔티티 타입과 값 타입으로 나눌 수 있다.
- 엔티티 타입
- @Entity로 정의하는 객체
- 식별자를 통해서 지속해서 추적 가능
- 값 타입
- int, Interger, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
- 식별자가 없고, 숫자나 문자 같은 속성만 있으므로 추적할 수 없다.
- 기본값 타입 basic value type
- 자바 기본 타입 (ex. int, double)
- 래퍼 클래스 (ex. Integer)
- String
- 임베디드 타입 embedded type (복합 값 타입)
- 컬렉션 값 타입 collection value type
2. 임베디드 타입(복합 값 타입)
- 임베디드 타입은 재사용할 수 있고, 응집도도 아주 높으며, 해당 값 타입만 사용하는 의미 있는 메서드도 만들 수 있다.
- 코드의 의미를 더 잘 이해할 수 있도록 도와준다.
- 임베디드 타입을 사용하려면, 다음 2가지 애노테이션이 필요하다(둘 중 하나는 생략해도 된다).
- @Embeddable: 값 타입을 정의하는 곳에 표시
- @Embedded: 값 타입을 사용하는 곳에 표시
- 기본 생성자가 필수
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded
Period workPeriod;
@Embedded
Address homeAddress;
}
@Embeddable
public class Period {
@Temporal(TemporalType.DATE)
Date startDate;
@Temporal(TemporalType.DATE)
Date endDate;
public boolean isWork(Date date) {
//..값 타입을 위한 메소드를 정의할 수도 있다
}
}
👉 startDate, endDate를 합해서 Period 클래스를 만들었다.
@Embeddable
public class Address {
@Column(name = "city") // 매핑할 컬럼 정의 가능
private String city;
private String street;
private String zipcode;
// ..
}
👉 city, street, zipcode를 합해서 Addresss 클래스를 만들었다.
3. 값 타입과 불변 객체
값 타입 공유 참조
임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다.
member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();
address.setCity("NewCity);
member2.setHomeAddress(address);
- 회원 2에 새로운 주소를 할당하려고, 회원 1의 주소를 그대로 참조해서 사용했다.
- 회원 2의 주소만 "NewCity"로 변경되길 기대했지만, 회원 1의 주소도 "NewCity"로 변경된다.
- 회원 1과 회원 2는 같은 address 인스턴스를 참조하기 때문이다.
값 타입 복사
값 타입의 실제 인스턴스인 값을 공유하는 것은 위험하다. 대신에 값(인스턴스)을 복사해서 사용해야 한다.
member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();
// 회원 1의 address 값을 복사해서 새로운 newAddress 값을 생성
Address newAddress = address.clone();
newAddress.setCity("NewCity);
member2.setHomeAddress(newAddress);
- 이처럼 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.
- 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입(primitive type)이 아니라 객체 타입이라는 것이다.
- 자바는 기본 타입에 값을 대입하면 값을 항상 복사해서 전달한다. 하지만, 객체에 값을 대입하면 항상 참조 값을 전달한다.
int a = 10;
int b = a; // 기본 타입은 항상 값을 복사한다
b = 4;
Address a = new Address("Old");
Address b = a; // 객체 타입은 항상 참조 값을 전달한다
b.setCity("New");
// 의도는 b.city 값만 변경하려 했으나, 공유 참조로 a.city 값도 변경됨
- 물론 객체를 대입할 때마다 인스턴스를 복사해서 대입하면 공유 참조를 피할 수 있다.
- 문제는 복사하지 않고 원본의 참조 값을 직접 넘기는 것을 막을 방법이 없다는 것이다.
- 객체의 공유 참조는 피할 수 없다.
- 따라서 근본적인 해결책이 필요한데, 가장 단순한 방법은 객체의 값을 수정하지 못하게 막으면 된다.
불변 객체(immutable object)
- 값 타입은 부작용 없이 사용할 수 있어야 한다. 부작용이 일어나면 값 타입이라고 할 수 없다...!
- 객체를 불변하게 만들면 값을 수정할 수 없으므로 부작용을 원천 차단할 수 있다.
- 값 타입은 될 수 있으면 불변 객체로 설계해야 한다.
- 불변 객체의 값은 조회할 수 있지만 수정할 수 없다.
- 불변 객체도 결국 객체이기 때문에 인스턴스의 참조 값 공유를 피할 수는 없지만, 참조 값을 공유해도 인스턴스의 값을 수정할 수는 없으므로 부작용이 발생하지 않는다.
- 불변 객체를 구현하는 다양한 방법이 있지만, 가장 간단한 방법은 생성자로만 값을 설정하고 수정자를 만들지 않는 것이다.
- 기본 생성자의 접근 제한자를 protected로 변경하여 new 사용을 제한한다.
@Embeddable
public class Address {
// JPA는 기본 생성자는 필수
protected Address() {}
// 생성자로 초기값을 설정한다
public Address(String city) {
this.city = city;
}
private String city;
// 접근자(Getter)는 노출한다
public String GetCity() {
return city;
}
// 수정자(Setter)는 만들지 않는다
}
Address address = member1.getHomeAddress();
// 회원 1의 주소값을 조회해서 새로운 주소값을 생성
Address newAddress = new Address(address.getCity());
member2.setHomeAddress(newAddress);
- Address는 이제 불변 객체이므로 값을 수정할 수 없고, 공유해도 부작용이 발생하지 않는다.
- 만약 값을 수정해야 하면 새로운 객체를 생성해서 사용해야 한다.
- 참고로 Integer, String은 자바가 제공하는 대표적인 불변 객체다.
불변이라는 작은 제약으로
부작용(side effect)이라는 큰 재앙을 막을 수 있다.
4. 값 타입 비교
동일성(Identity) 비교: == 비교, 객체 인스턴스의 주소값을 비교한다.
동등성(Equivalence) 비교: equals() 메서드를 사용해 객체 내부의 값을 비교한다.
Address a = new Address("서울시", "종로구", "1번지");
Address b = new Address("서울시", "종로구", "1번지");
- Address 값 타입을 a == b로 동일성 비교를 하면, 둘은 서로 다른 인스턴스이므로 결과는 거짓이다.
- 값 타입은 비록 인스턴스가 달라도 그 안의 값이 같으면 같은 것으로 봐야 한다.
- 따라서, 값 타입을 비교할 때는 a.equals(b)를 사용해서 동등성 비교를 해야 한다.
- 물론, Address의 equals() 메서드를 재정의 해야 한다.
- 값 타입의 equals() 메서드를 재정의할 때는 보통 모든 필드의 값을 비교하도록 구현한다.
참고)
자바에서 equals()를 재정의하면, hashCode()도 재정의하는 것이 안전하다. 그렇지 않으면 해시를 사용하는 컬렉션(HashSet, HashMap)이 정상 동작하지 않는다.
import java.util.Objects;
public class Address {
private String city;
private String street;
private String zipcode;
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Address address = (Address) o;
return Objects.equals(city, address.city) &&
Objects.equals(street, address.street) &&
Objects.equals(zipcode, address.zipcode);
}
@Override
public int hashCode() {
return Objects.hash(city, street, zipcode);
}
}
5. 정리
엔티티 타입 Entity Type과 값 타입 Value Type의 특징은 다음과 같다.
엔티티 타입의 특징
- 식별자(@id)가 있다.
- 엔티티 타입은 식별자가 있고, 식별자로 구별할 수 있다.
- 생명 주기가 있다.
- 생성하고, 영속화하고, 소멸하는 생명 주기가 있다.
- em.persist(entity)로 영속화한다.
- em.remove(entity)로 제거한다.
- 공유할 수 있다.
- 참조 값을 공유할 수 있다. 이것을 공유 참조라 한다.
- 예를 들어, 회원 엔티티가 있다면 다른 엔티티에서 얼마든지 회원 엔티티를 참조할 수 있다.
값 타입의 특징
- 식별자가 없다.
- 생명 주기를 엔티티에 의존한다.
- 스스로 생명주기를 가지지 않고 엔티티에 의존한다.
- 의존하는 엔티티를 제거하면 같이 제거된다.
- 공유하지 않는 것이 안전하다.
- 엔티티 타입과는 다르게 공유하지 않는 것이 안전하다. 대신에 값을 복사해서 사용해야 한다.
- 오직 하나의 주인만이 관리해야 한다.
- 불변 Immutable 객체로 만드는 것이 안전하다.
- 값 타입은 정말 값 타입이라고 판단될 때만 사용해야 한다. 특히, 엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만들면 안 된다.
- 식별자가 필요하고 지속해서 값을 추적하고 구분하고 변경해야 한다면, 그것은 값 타입이 아닌 엔티티이다.
- 개념적으로 의미있는 값을 담고 이를 위한 기능을 추가하기 위해 사용(ex. Address, Money, ...)한다.
반응형