Data Types

Entity의 속성 정보를 담기 위한 타입의 종류에 대해서 다뤄보자

Basic Value Type

@Entity
public class Member{
  @Id @GeneratedValue
  private Long id;

  private String name;
  private int age;
}

위에 쓰이는 Long, String, int와 같은 자바 기본 타입과 Integer, Double 과 같은 래퍼 클래스가 기본 타입에 속한다.

EmbeddedType

Embedded Type을 활용하지 않은 경우

@Entity
public class Member{
  @Id @GeneratedValue
  private Long idl
  private String name;

  //근무 기간
  @Temporal(TemporalType.DATE) jav.util.Date startDate;
  @Temporal(TemporalType.DATE) jav.util.Date endDate;

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

위 처럼 근무 기간이나, 집주소와 같은 정보들은 서로 연관되어 있는 정보로 클래스 형태로 묶어서 의미를 부여하는 것이 좋다

Embedded Type 활용

@Entity
public class Member{
  @Id @GeneratedValue
  private Long idl
  private String name;

  //근무 기간
  @Embedded Period workPeriod;

  //집주소 
  @Embedded Address homeAddress;
}

@Embeddable
public class Period{
  @Temporal(TemporalType.DATE) jav.util.Date startDate;
  @Temporal(TemporalType.DATE) jav.util.Date endDate;
}

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

@Embedded: Embedded Type를 이용하는 곳에 명시

@Embeddable: Embedded Type를 정의하는 곳에 명시

embedded_table_mapping

이와 같이 클래스를 멤버 변수로 가지고 있는 관계를 Composition 관계라고 한다. ORM을 이용해서 이와 같이 객체 지향적인 클래스를 설계를 할 수 있고, 이러한 설계는 DB에 그대로 반영된다.(그렇다고 DB column 에 테이블이 저장되는 것이 아니라 풀어서 저장은 되지만, JPA에서는 객체로 인식하게 된다.)

객체 지향이 잘 설계된 JPA에서는 클래스의 수가 테이블의 수 보다 많아 지게된다.

Emebedded Type Association

EmbeddedType 안에서 다른 엔티티에 대한 참조를 포함하는 것도 가능하다.

@Entity
public class Member{

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

    private String name;

    @Embedded
    private Phone phone;
}

@Embeddable
public class Phone {

    private String areaCode;
    private String number;

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

@Entity
public class PhoneServiceProvider {

    @Id @GeneratedValue
    private Long id;

    private String name;
}

위 처럼, Embedded Type인 Phone은 PhoneServiceProvider와 다대일 연관관계를 가지고 있으며, 이를 이용해서 멤버에도 Provider 정보를 추가하게 된다.

@AttributeOverride

Embedded Type에 명시한 맵핑정보를 재정의할 수 있다.

@Entity
public class Member{
  @Id @GeneratedValue
  private Long idl
  private String name;

  //근무 기간
  @Embedded Period workPeriod;

  //집주소 
  @Embedded Address homeAddress;

  //회사 주소
  @Embedded Address companyAddress;
}

위의 엔티티를 보면 homeAddress 내의 column과 companyAddress의 column의 겹치는 것을 알 수 있는데, 이를 @AttributeOverride를 이용해서 컬럼명을 재정의 해줄 수 있다.

@Entity
public class Member{
  @Id @GeneratedValue
  private Long id;
  private String name;

  //근무 기간
  @Embedded Period workPeriod;

  //집주소 
  @Embedded 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"))
    }) 
    Address companyAddress;
}

Embedded null value

만약 setHomeAddress(null)과 같이 Embedded Type에 null값을 저장하게 되면, Embedded Type 내 모든 필드값이 null로 등록된다.

Data Type & Immutable Objects

Data Type의 공유 참조

shared_data_type

만약 위처럼 값을 공유해서 저장하게 되면 어떻게 될까?

member1.setHomeAddress(new Address("Old City"));
Address address=member1.getHomeAddress();

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

위 처럼 Address 객체에 대해서 공유해서 member1, member2에 저장하게 되면 member2에서만 변경을 수행하려고 하던 것이 member1에도 수정이 적용된다. 이는 객체가 서로 공유되어서 발생하는 문제이다.

이를 방지하기 위해서는 새로운 인스턴스를 생성해서 사용하거나, 인스턴스를 복사해서 사용해야한다.

cloning_data_types

member1.setHomeAddress(new Address("Old City"));
Address address=member1.getHomeAddress();

Address newAddress=address.clone();

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

Embedded Type과 같이 객체 타입의 경우 값을 저장할때, 참조값을 이용해서 저장을 하므로 같은 인스턴스에 접근하게 되는 것이다.

Immutable Object

객체를 불변 객체로 만들게 되면 위와 같은 공유되는 인스턴스에 대한 문제가 발생하지 않는다. 애초에 수정을 못하므로, 값이 공유되어도 문제가 발생하지 않는다.

@Embeddable
public class Address{
  private String city;
  
  public Address(){}

  public Address(String city){
    this.city=city;
  }

  public String getCity(){
    return this.city
  }

}

위 처럼 embedded type을 만들때 필드의 접근 수준을 private으로 만들고, setter함수를 생성하지 않으면 외부에서 해당 필드에 대한 수정이 불가능하다.

Equivalance Test

객체에 대한 값을 비교할 때는 == 와 equals가 존재하는데, ==는 동일 인스턴스 인지 비교하는 것이고, equals는 인스턴스의 값을 비교하는 것이다.

int a=10;
int b=10;

a==b //True

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

a==b //False
a.equals(b) //True

자바의 기본 타입에 대해서는 ==를 하면 값에 대한 비교를 진행해서 기대한 결과를 보이지만, 객체에 대해서 ==을 수행하면 의도한 값이 나오지 않는다. 이는 a, b 가 서로 다른 인스턴스이기 때문이다. 따라서, 값에 따른 비교를 하기 위해서는 equals() 메소드를 override해야한다.

HashSet, HashMap 과 같이 해시를 활용하는 컬렉션의 경우 hashCode 메소드도 정의를 해야 정상적으로 동작한다.

Collection Type

컬럼에 여러 개의 값을 저장하기 위해 컬렉션을 이용할 수 있다.

@Entity
public class Member{
  @Id @GeneratedValue
  private Long idl
  private String name;

  //근무 기간
  @Embedded Address homeAddress;

  @ElementCollection
  @CollectionTable(name="FAVORITE_FOOD",joinColumns= @JoinColumn(name="MEMBER_ID"))
  @Column(name="FOOD_NAME")
  private Set<String> favoriteFoods=new HashSet<String>();

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

collection_type_table

DB에서는 컬럼에 컬렉션을 저장할 수 없다. 따라서 해당 컬럭션에 대응되는 테이블을 생성해서 연관관계를 맺어줘야한다. 위 예제를 봐도 알 수 있듯이, Embedded Type을 컬렉션으로 지정할 수도 있다.

@CollectionTable을 명시하지 않으면 엔티티이름_컬럼 으로 된 테이블이 생성된다. 위의 경우 MEMBER_addressHistory와 같은 테이블이 생성된다고 볼 수 있다

Practice

JPA ```java Member member=new Member();

//Embedded type 데이터 추가 member.setHomeAddress(new Address(“통영”, “몽돌 해수욕장”, “660-123”));

//컬렉션 데이터 추가 member.getFavoriteFoods().add(“짬봉”); member.getFavoriteFoods().add(“짜장”); member.getFavoriteFoods().add(“탕수육”);

//Embedded Type컬렉션 데이터 추가 member.getAddressHistory().add(new Address(“서울”,”강남”,”123-123”)) member.getAddressHistory().add(new Address(“서울”,”강북”,”000-000”))

em.persist(member)


위와 같은 Member 객체를 생성해서 DB에 저장하면 아래와 같은 sql문들이 호출된다.

> SQL

```sql
-- Embedded type 데이터 추가
INSERT INTO MEMBER(ID,CITY,STREET,ZIPCODE) VALUES(1,"통영","몽돌 해수욕장","660-123")

--컬렉션 데이터 추가
INSERT INTO FAVORITE_FOOD(MEMBER_ID,FOOD_NAME) VALUES(1,"짬봉");
INSERT INTO FAVORITE_FOOD(MEMBER_ID,FOOD_NAME) VALUES(1,"짜장");
INSERT INTO FAVORITE_FOOD(MEMBER_ID,FOOD_NAME) VALUES(1,"탕수육");

-- Embedded Type컬렉션 데이터 추가
INSERT INTO ADDRESS(MEMBER_ID,CITY,STREET,ZIPCODE) VALUES(1,"서울","강남","123-123");
INSERT INTO ADDRESS(MEMBER_ID,CITY,STREET,ZIPCODE) VALUES(1,"서울","강북","000-000");

위와 같이 컬렉션 형태의 data type에 대해서도 지연로딩/즉시로딩을 설정할 수 있다.

@ElementCollection(fetch=FetchType.LAZY)

Updating Collection Type

Member member=em.find(Member.class,1L);

//Embedded type 수정
member.setHomeAddress(new Address("새로운 도시","신도시1","123456"));

//컬렉션 데이터 수정
Set<String> favoriteFoods=member.getFavoriteFoods();
favoriteFoods.remove("탕수육");
favoriteFoods.add("치킨");

//Embedded Type컬렉션 데이터 추가
List<Address> addressHistory=member.getAddressHistory();
addressHistory.remove(new Address("서울","강남","123-123"))
addressHistory.add(new Address("새로운 도시","새로운 주소","000-000"))
  1. Embedded Type에 대해서는 새로운 값을 설정해주는 것으로 수정할 수 있다.
  2. String 과 같은 불변 객체에 대한 컬렉션 타입에 대한 수정은 할 수 없다. 따라서 기존의 인스턴스를 제거하고 새로운 인스턴스를 추가해준다.
  3. Embedded Type 또한 불변객체로 만들어져 있어 기존의 값을 제거하고 새로운 값을 등록해준다.

이때, Embedded Type에 대해 remove 할 때 new 방식으로 Address 인스턴스를 생성해서 해당 인스턴스와 같은 값을 가지는 인스턴스가 있으면 제거하는 것인데, hashCode, equals 메소드가 구현되어 있지 않으며 삭제가 제대로 수행되지 않는다.

Type Collection 주의사항

@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;
    }
    ...
}

Embedded Type을 보면 Entity 와 달리 식별자 값이 존재하지 않는다. 식별자를 가지는 데이터의 경우, 식별자를 이용한 탐색이 가능하기 때문에 해당 데이터에 직접 접근해서 데이터를 수정하는 것이 가능하다. 하지만 Type Collection과 같이 식별자가 없는 일반 데이터의 모음 같은 경우는 해당 데이터만을 식별할 수가 없어 전체 컬렉션에 대해 다시 저장해야한다.

Practice

public static void logic(EntityManager em){
    Member member=new Member();
    member.setName("member1");

    //Embedded Type컬렉션 데이터 추가
    member.getAddressHistory().add(new Address("서울","강남","123-123"));
    member.getAddressHistory().add(new Address("서울","강북","000-000"));
    member.getAddressHistory().add(new Address("서울","송파구","123-123"));

    em.persist(member);
    em.flush();
    em.clear();

    //컬렉션 데이터 수정과정
    Member newMember=em.find(Member.class,1L);
    List<Address> addressHistory=newMember.getAddressHistory();
    addressHistory.remove(new Address("서울","강남","123-123"));

    em.persist(newMember);
    em.flush();
}

위의 예제를 실행해보면

우선, AddressHistory에 대한 Collection Table에 3개의 INSERT SQL문이 실행되는 것을 확인할 수 있다.

Hibernate: 
    insert into ADDRESS(MEMBER_ID, city, street, zipcode) values(?, ?, ?, ?)
Hibernate: 
    insert into ADDRESS(MEMBER_ID, city, street, zipcode) values(?, ?, ?, ?)
Hibernate: 
    insert into ADDRESS(MEMBER_ID, city, street, zipcode) values(?, ?, ?, ?)

그리고 아래에서 addressHistory collection 중에 값을 하나 삭제를 하고자 한다. 원래대로면, DELETE SQL문이 하나만 실행되야 하는데 아래를 보면 DELETE SQL 1개와 INSERT SQL 2개가 실행되는 것을 확인할 수 있다.

Hibernate:
delete from ADDRESS where MEMBER_ID=?
Hibernate: 
    insert into ADDRESS(MEMBER_ID, city, street, zipcode) values(?, ?, ?, ?)
Hibernate: 
    insert into ADDRESS(MEMBER_ID, city, street, zipcode) values(?, ?, ?, ?)

컬렉션에서 해당 멤버 엔티티와 관련된 모든 데이터에 대해 삭제를 진행하고 수정된 컬렉션을 새롭게 저장하는 것을 확인할 수 있다. 따라서, 컬렉션에 저장되는 값이 많은 경우에는 일대다 연관관계를 설정하는 것이 더 좋다.

@Entity
public class AddressEntity{
  @Id
  @GeneratedValue
  private Long id;

  @Embedded Address address;
}

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

이와 같이 Embedded Type에 대해 식별자를 부여해, 컬렉션 수정을 진행한다.

References

book: 자바 ORM 표준 JPA 프로그래밍 -김영한 저

book_link

태그: ,

카테고리:

업데이트:

댓글남기기