👩‍💻 개발개념

엔티티와 값 타입

케로⸝⸝◜࿀◝ ⸝⸝ 2024. 5. 7. 20:49

📢 안내

해당 글은

를 발췌하여 정리한 것입니다.

 

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, ...)한다.

 

반응형