course/inflearn

[JPA 프로그래밍 기본편] 값 타입

hjkim0502 2022. 9. 18. 18:55

기본값 타입

  • JPA의 데이터 타입 분류
    • 엔티티 타입: @Entity로 정의하는 객체
      • 데이터가 변해도 식별자로 지속해서 추적 가능
      • 예) 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능
    • 값 타입: int, Integer, String과 같이 단순히 값으로 사용하는 자바 기본 타입이나 객체
      • 식별자가 없고 값만 있으므로 변경 시 추적 불가
      • 예) 숫자 100을 200으로 변경하면 완전 대체
  • 값 타입 분류
    • 기본값 타입
    • 임베디드 값 타입
    • 컬렉션 값 타입
  • 기본값 타입
    • 예) String name, int age
    • 생명주기가 엔티티에 의존
      • 회원 엔티티 삭제 시 이름, 나이 필드 함께 삭제
    • 값 타입은 공유하면 안됨
      • 예) 회원 이름 변경 시 다른 회원의 이름도 변경되면 안됨
    • 참고: int, double 같은 기본 타입(primitive type)은 절대 공유되지 않음 -> 항상 값 복사
    • Integer같은 래퍼 클래스나 String 같은 특수 클래스는 공유 가능한 객체이지만 변경 방법이 없음
int a = 10;
int b = a; // 10이란 값만 복사

b = 20;

// a = 10, b = 20

Integer a = new Integer(10);
Integer b = a // 참조값 복사

a.changeVal(20); // 변경 불가 (이런 방법 존재 X)

// a = b = 10

 

임베디드 타입(복합값 타입)

  • 새로운 값 타입을 직접 정의할 수 있음
  • JPA는 임베디드 타입이라고 함 (주로 기본값 타입을 모아 만들기 대문에 복합값 타입이라고도 함)
  • int, String처럼 값 타입임
  • @Embeddable: 값 타입 정의하는 곳에 표시 (클래스 위)
  • @Embedded: 값 타입 사용하는 곳에 표시 (필드 위)

  • 회원은 이름, 근무기간, 집주소를 가진다 (근무기간, 집주소에 맞게 클래스 생성)
  • 장점:
    • 재사용성
    • 응집성
    • 해당 값 타입만의 의미있는 메소드 사용 가능
    • 해당 값 타입을 소유한 엔티티의 생명주기에 의존

 

  • 매핑하는 DB는 변화없음
// 임베디드 타입 사용 전
@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    //근무 기간
    private LocalDateTime startDate;
    private LocalDateTime endDate;

    //집주소
    private String city;
    private String street;
    private String zipcode;

}
  • 기본 생성자 필수!
@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    //근무 기간
    @Embedded // 생략가능하지만 명시적으로 알려주기 위해
    private Period workPeriod;

    //집주소
    @Embedded
    private Address homeAddress;

}

@Embeddable
public class Period {
        private LocalDateTime startDate;
    private LocalDateTime endDate;

    public LocalDateTime getStartDate() {
        return startDate;
    }

    public void setStartDate(LocalDateTime startDate) {
        this.startDate = startDate;
    }

    public LocalDateTime getEndDate() {
        return endDate;
    }

    public void setEndDate(LocalDateTime endDate) {
        this.endDate = endDate;
    }

    public Period() {
    }
}

@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }

    public String getZipcode() {
        return zipcode;
    }

    public void setZipcode(String zipcode) {
        this.zipcode = zipcode;
    }
    
    public Address() {
    }
}
  • 객체와 테이블을 아주 세밀하게 매핑 가능
  • 매핑한 테이블의 수보다 클래스의 수가 더 많은 것이 좋은 설계

  • 임베디드 타입이 다시 임베디드 타입이나 엔티티를 필드로 가질 수 있다
   // 같은 값 타입 사용 시
   
   //집주소
    @Embedded
    private Address homeAddress;

    //근무 주소
    @Embedded
    @AttributeOverride() // 내부 설정 통해 재정의
    private Address workAddress;
  • 임베디드 타입의 값이 null이면 그 클래스의 필드 값들 모두 null

 

값 타입과 불변 객체

  • 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 한번에 다같이 값이 변경될 수 있음 (side effect)
  • 예) 회원1이 city 값을 NewCity로 변경하면 회원2의 city 값도 함께 변경됨

  • 따라서 값(인스턴스)를 복사하여 사용

  • 이렇게 항상 값을 복사하여 사용하면 위와 같은 부작용을 피할 수 있다
  • 그러나 임베디드 타입처럼 직접 정의한 값 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없음
    • 기본 타입은 직접 대입해도 알아서 값만 복사함
  • 불변 객체: 생성 이후 값이 변하지 않는 객체
    • 객체 타입을 수정할 수 없게하여 부작용 원천 차단
    • 값 타입은 불변 객체로 설계해야함
    • 생성자로만 값을 설정하고 setter를 만들지 않는 것
      • setter를 아예 안 만들거나, private으로 설정
    • Integer, String은 자바의 대표적인 불변 객체
    • 불변 객체의 값을 바꾸고 싶다면?
Member member = new Member();
member.setUsername("a");

Address address = new Address("1", "2", "3");
member.setHomeAddress(address);

em.persist(member);

Address newAddress = new Address("A", "2", "3"); // 일부만 바꾸고 싶어도 새로 만들어 갈아끼움
member.setHomeAddress(newAddress);

 

값 타입의 비교

  • 값 타입: 인스턴스가 달라도 그 값 자체가 같다면 같은 것으로 봐야함
  • 예) int a = 10, int b = 10 일 때 a == b가 true 인 것 처럼
  • 동일성 비교(==): 참조 값 비교
  • 동등성 비교(equals()): 값 자체 비교
  • 따라서 값 타입은 equals() 사용하여 비교하여야 함
    • equals()도 기본 설정이 == 비교이기 때문에 재정의 해주어야 함 (주로 모든 필드에 대해)
    • 해당 불변 객체에 인텔리제이 'Generate > equals() and hashcode()' 사용하기
    @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);
    }

 

 

값 타입 컬렉션

  • 값 타입을 하나 이상 저장할 때 사용
  • 해당 컬렉션 필드 위에 @ElementCollection, @CollectionTable 사용
  • DB에는 한 테이블에 컬렉션을 넣을 수 있는 방법이 없기 때문에 컬렉션 저장을 위한 별도의 테이블 생성하는 개념

  • 별도 생성된 테이블의 컬럼은 모두 PK -> 따로 식별자를 생성한다면 엔티티와 같아진다
  • 기존에 배운 기본값 타입, 복합값 타입과 같이 생애주기가 주인을 따르기 때문에 persist 한번에 모두 저장
  • 기본적으로 지연 로딩 전략 사용
  • 참고: cascade, orphanRemoval 기능을 필수로 가진다고 볼 수 있다
// 저장
Member member = new Member();
member.setHomeAddress(new Address("1", "2", "3"));

member.getFavoriteFoods().add("chicken");
member.getFavoriteFoods().add("pizza");
member.getFavoriteFoods().add("coke");

member.getAddressHistory().add(new Address("a", "b", "c"));

em.persist(member);

em.flush();
em.clear();

// 조회
Member findMember = em.find(Member.class, member.getId());

List<Address> addressHistory = findMember.getAddressHistory();
for (Address address : addressHistory) {
    System.out.println("address = " + address.getCity());
}

Set<String> favoriteFoods = findMember.getFavoriteFoods();
for (String food : favoriteFoods) {
    System.out.println("food = " + food);
}

// 복합값 타입 수정
//findMember.getHomeAddress().setCity("new");
Address address = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("new", address.getStreet(), address.getZipcode())); // 아예 새로 세팅

// 값 타입 컬렉션 수정
findMember.getFavoriteFoods().remove("chicken"); // delete 문 한번
findMember.getFavoriteFoods().add("new"); // insert 문 한번

// Address 클래스에 equals()가 제대로 재정의 되어있어야 함
findMember.getAddressHistory().remove(new Address("old1", "b", "c")); // delete 한번
findMember.getAddressHistory().add(new Address("new1", "b", "c")); // insert 두번
  • 제약 사항:
    • 값 타입 컬렉션에 변경이 생기면 주인 엔티티와 연관된 모든 데이터를 삭제한 후 존재해야할 값들을 모두 다시 저장
      • 예) old1, old2 저장된 상태에서 old1 -> new1 수정되면 먼저 다 지운 후, old2, new1 다시 저장
    • 값 변경 시 추적이 어렵다
    • 식별자 개념이 없다(모든 컬럼을 묶어 기본키로 구성) -> null 입력 불가능, 중복 저장 불가능
    • 따라서 실무에서는 상황에 따라 일대다 관계를 대안으로 고려
  • 정말 단순한 상황이 아닌 이상 값 타입 컬렉션을 엔티티로 승격하여 관리
// Member class

//@ElementCollection
//@CollectionTable(name = "ADDRESS", joinColumns =
//    @JoinColumn(name = "MEMBER_ID")
//)
//private List<Address> addressHistory = new ArrayList<>();

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();

// AddressEntity class
@Entity
@Table(name = "ADDRESS")
public class AddressEntity {

    @Id @GeneratedValue
    private Long id;

    @Embedded
    private Address address;
  • 위와 같이 엔티티로 만들어 일대다 연관관계를 맺어주면 훨씬 유연하게 관리 가능하다
    • 특히 지속해서 값을 추적, 변경해야 하는 경우

 

  • getter 사용하여 equals() and hashCode() 생성 권장
    • 사용하지 않으면 필드에 직접 접근하는데, 프록시일 때는 작동하지 않음

 

출처: 김영한님 JPA 프로그래밍 - 기본편