값 타입

9. 값 타입

JPA의 데이터 타입

  • 엔티티 타입
  • 값 타입
    • 기본 값 타입
      • 자바 기본 타입(int, double 등)
      • 래퍼 클래스(Integer 등)
      • String
    • 임베디드 타입(복합 값 타입)
      • 사용자 정의 타입
    • 컬렉션 값 타입
      • 하나 이상의 값 타입 저장

9.1 기본 값 타입

  • Member는 엔티티 타입이다.
  • Long, String, int는 값 타입이다.
@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

    private String name;
    private int age;
}

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

  • 사용자 정의 값 타입이다.
  • @Embeddable: 값 타입을 정의하는 곳에 표시한다.
  • @Embedded: 값 타입을 사용하는 곳에 표시한다.
  • 임베디드 타입은 기본 생성자가 필수이다.
@Entity
@Getter @Setter
public class Member {

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

    @Embedded private Period period;   // 근무 기간
    @Embedded private Address address; // 집 주소
}
@Embeddable
@Getter
public class Period {

    private LocalDateTime startDateTime;
    private LocalDateTime endDateTime;

    public Period() {
    }

    public Period(LocalDateTime startDateTime, LocalDateTime endDateTime) {
        this.startDateTime = startDateTime;
        this.endDateTime = endDateTime;
    }

    public boolean isWork(LocalDateTime dateTime) {
        return (dateTime.isAfter(startDateTime) && dateTime.isBefore(endDateTime))
                || dateTime.isEqual(startDateTime)
                || dateTime.isEqual(endDateTime);
    }
}
@Embeddable
@Getter
public class Address {

    @Column(name = "city")
    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;
    }
}

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

  • 임베디드 타입은 엔티티의 테이블에 매핑된다. 임베디드 타입 사용 전과 사용 후의 테이블의 매핑은 같다.

9.2.1 임베디드 타입과 연관관계

  • 임베디드 타입은 값 타입을 포함하거나 엔티티를 참조할 수 있다.

@Entity
@Getter @Setter
public class Member {

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

    @Embedded private PhoneNumber phoneNumber;
    @Embedded private Address address;
}
@Embeddable
@Getter
public class Address {

    @Column(name = "city")
    private String city;
    private String street;
    private String state;
    @Embedded
    private Zipcode zipcode;

    public Address() {
    }

    public Address(String city, String street, String state, Zipcode zipcode) {
        this.city = city;
        this.street = street;
        this.state = state;
        this.zipcode = zipcode;
    }
}
@Embeddable
@Getter
public class Zipcode {

    private String zip;
    private String plusFor;

    public Zipcode() {
    }

    public Zipcode(String zip, String plusFor) {
        this.zip = zip;
        this.plusFor = plusFor;
    }
}
@Embeddable
@Getter @Setter
public class PhoneNumber {

    private String areaCode;
    private String localNumber;

    @ManyToOne
    @JoinColumn(name = "provider_id")
    private PhoneServiceProvider provider;

    public PhoneNumber() {
    }

    public PhoneNumber(String areaCode, String localNumber, PhoneServiceProvider provider) {
        this.areaCode = areaCode;
        this.localNumber = localNumber;
        this.provider = provider;
    }
}
@Entity
@Getter @Setter
public class PhoneServiceProvider {

    @Id @GeneratedValue
    private Long id;
    private String name;
}
public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Member member = new Member();
            member.setName("member");

            Zipcode zipcode = new Zipcode("zipcode", "1234");
            Address address = new Address("city", "street", "state", zipcode);

            PhoneServiceProvider phoneServiceProvider = new PhoneServiceProvider();
            phoneServiceProvider.setName("LGU+");
            PhoneNumber phoneNumber = new PhoneNumber("010", "010", phoneServiceProvider);

            member.setPhoneNumber(phoneNumber);
            member.setAddress(address);

            em.persist(phoneServiceProvider);
            em.persist(member);

            em.flush();
            em.clear();
            
            tx.commit();
        }
        catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }
        finally {
            em.close();
        }

        emf.close();
    }
}

9.2.3 @AttributeOverride: 속성 재정의

  • 임베디드 타입에 정의한 매핑정보를 재정의할 때 @AttributeOverride를 사용한다.
@Entity
@Getter @Setter
public class Member {

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

    @Embedded
    private Address homeAddress;

    @Embedded
    @AttributeOverrides({@AttributeOverride(name = "city", column = @Column(name = "company_city")),
            @AttributeOverride(name = "street", column = @Column(name = "company_street")),
            @AttributeOverride(name = "zipcode", column = @Column(name = "company_zipcode")),
    })
    private Address companyAddress;
}
@Embeddable
@Getter
public class Address {

    @Column(name = "city")
    private String city;
    private String street;
    private String zipcode;
}

9.2.4 임베디드 타입과 null

  • 임베디드 타입이 null이면 매핑한 컬럼 값은 모두 null이 된다.

9.3 값 타입과 불변 객체

9.3.1 값 타입 공유 참조

  • 임베디드 타입 같은 값 타입은 여러 엔티티에서 공유하면 안된다.
  • member1과 member2는 같은 Address를 참조하기 때문에 데이터베이스에서 둘 다 newCity로 변경된다.
member1.setHomeAddress(new Address("Oldcity"));
Address address = member1.getHomeAddress();

address.setCity("NewCity");
member2.setHomeAddress(address);

9.3.2 값 타입 복사

  • 값 타입의 인스턴스는 복사해서 사용해야 한다.
  • 하지만 복사하지 않고 원본의 참조 값을 직접 넘기는 것을 막을 방법이 없다.
  • 따라서 근본적으로 객체의 공유 참조를 막으려면 객체의 값을 수정하지 못하게 하는 것이다.
member1.setHomeAddress(new Address("Oldcity"));
Address address = member1.getHomeAddress();

Address newAddress = address.clone();

newAddress.setCity("NewCity");
member2.setHomeAddress(newAddress);

9.3.3 불변 객체

  • 객체를 불변하게 만들면 값을 수정할 수 없으므로 값 타입 객체의 공유 참조 부작용을 막을 수 있다.
  • 불변 객체는 생성자로만 값을 설정하고 수정자를 만들지 않으면 된다.
  • 값 타입 객체의 값을 수정하려면 새로운 객체를 생성해서 사용해야 한다.
  • Integer, String은 자바가 제공하는 대표적인 불변 객체이다.
@Embeddable
@Getter
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;
    }
}

9.4 값 타입의 비교

  • 동일성 비교: 인스턴스의 참조 값을 비교, == 사용
  • 동등성 비교: 인스턴스의 값을 비교, equals() 사용
  • int a와 b는 == 비교시 참 이지만, Address a와 b는 == 비교시 서로다른 인스턴스 이므로 거짓이다.
  • 객체 기반의 값 타입을 비교할 때는 equals() 를 사용해서 동등성 비교를 해야한다.
  • 따라서 equals()를 재정의해야 한다.
int a = 10;
int b = 10;

Address a = new Address("서울시", "종로구", "1번지");
Address b = new Address("서울시", "종로구", "1번지");

9.5 값 타입 컬렉션

  • 값 타입을 하나이상 저장하려면 컬렉션에 보관한다. 그리고 @ElementCollection, @CollectionTable 어노테이션을 사용한다.
  • 컬렉션은 데이터베이스에서 별도의 테이블을 추가하여 매핑된다.
@Entity
@Getter @Setter
public class Member {

    @Id @GeneratedValue
    private Long id;
    private String name;

    @Embedded
    private Address homeAddress;

    @ElementCollection
    @CollectionTable(name = "favorite_foods", joinColumns = @JoinColumn(name = "member_id"))
    @Column(name = "food_name")
    private Set<String> favoriteFoods = new HashSet<>();

    @ElementCollection
    @CollectionTable(name = "address", joinColumns = @JoinColumn(name = "member_id"))
    private List<Address> addressHistory = new ArrayList<>();
}
@Embeddable
@Getter
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;
    }
}

9.5.1 값 타입 컬렉션 사용

  • 총 6번의 INSERT SQL을 실행한다.
  • 값 타입 컬렉션은 영속성 전이(Cascade) + 고아 객체 제거(Orphan Remove) 기능을 필수로 가진다고 볼 수 있다.
  • 값 타입 컬렉션의 페치 전략은 LAZY가 기본이다.
public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Member member = new Member();
            member.setHomeAddress(new Address("통영", "뭉돌해수용장", "660-123"));

            member.getFavoriteFoods().add("짬뽕"); // FAVORITE_FOODS INSERT SQL
            member.getFavoriteFoods().add("짜장"); // FAVORITE_FOODS INSERT SQL
            member.getFavoriteFoods().add("탕수육"); // FAVORITE_FOODS INSERT SQL

            member.getAddressHistory().add(new Address("서울", "강남", "123-123")); // ADDRESS INSERT SQL
            member.getAddressHistory().add(new Address("서울", "강북", "000-000")); // ADDRESS INSERT SQL

            em.persist(member); // MEMBER INSERT SQL

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

            tx.commit();
        }
        catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }
        finally {
            em.close();
        }

        emf.close();
    }
}
insert into Member (city, street, zipcode, name, id) values (?, ?, ?, ?, ?)
insert into address (member_id, city, street, zipcode) values (?, ?, ?, ?)
insert into address (member_id, city, street, zipcode) values (?, ?, ?, ?)
insert into favorite_foods (member_id, food_name) values (?, ?)
insert into favorite_foods (member_id, food_name) values (?, ?)
insert into favorite_foods (member_id, food_name) values (?, ?)
  • 지연 로딩시 총 3번의 SELECT SQL을 실행한다.
Member foundMember = em.find(Member.class, member.getId()); // MEMBER SELECT SQL
Address homeAddress = foundMember.getHomeAddress();

Set<String> favoriteFoods = foundMember.getFavoriteFoods();
for (String favoriteFood : favoriteFoods) {
    System.out.println("favoriteFood = " + favoriteFood); // FAVORITE_FOODS SELECT SQL
}

List<Address> addressHistory = foundMember.getAddressHistory();
for (Address address : addressHistory) {
    System.out.println("address = " + address); // ADDRESS SELECT SQL
}
select m.id, m.city, m.street, m.zipcode, m.name 
from Member m 
where m.id=?

select f.member_id, f.food_name 
from favorite_foods f
where f.member_id=?

select a.member_id, a.city, a.street, a.zipcode 
from address a 
where a.member_id=?
Member foundMember = em.find(Member.class, member.getId());

// 임베디드 값 타입 수정
foundMember.setHomeAddress(new Address("city", "street", "zipcode"));

// 기본 값 타입 컬렉션 수정
foundMember.getFavoriteFoods().remove("탕수육");
foundMember.getFavoriteFoods().add("치킨");

// 임베디드 값 타입 컬렉션 수정
foundMember.getAddressHistory().remove(new Address("서울", "강북", "000-000"));
foundMember.getAddressHistory().add(new Address("서울", "서초", "000-000"));
-- 임베디드 값 타입 수정
update Member set city=?, street=?, zipcode=?, name=? 
where id=?

-- 기본 값 타입 컬렉션 수정 : 수정 할 값 삭제 후 새로운 값을 삽입한다.
delete from favorite_foods
where member_id=? and food_name=?

insert into favorite_foods (member_id, food_name) values (?, ?)

-- 임베디드 값 타입 컬렉션 수정 : 모든 데이터 삭제 후 수정하여 다시 삽입한다. 
delete from address
where member_id=?

insert into address (member_id, city, street, zipcode) values (?, ?, ?, ?)
insert into address (member_id, city, street, zipcode) values (?, ?, ?, ?)

9.5.2 값 타입 컬렉션의 제약사항

  • 값 타입은 식별자라는 개념이 없고 단순한 값들의 모음이므로 값 변경시 데이터베이스에서 원본 데이터를 찾기 어렵다.
  • 값 타입 컬렉션에 변경 사항이 발생하면, 값 타입 컬렉션이 매핑된 테이블의 연관된 모든 데이터를 삭제하고, 현재 값 타입 컬렉션 객체에 있는 모든 값을 데이터베이스에 다시 저장한다.
  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 한다. 기본 키 제약 조건으로 컬럼에 null을 입력할 수 없고, 같은 값을 중복해서 저장할 수 없다.

Tags:

Categories:

Updated:

Leave a comment