코딩공작소

JPA 프로그래밍 학습 정리 (6) - 값 타입 본문

어플리케이션개발/JPA

JPA 프로그래밍 학습 정리 (6) - 값 타입

안잡아모찌 2024. 1. 27. 23:16

JPA에서는 데이터 타입이 2개 있다.

  • 엔티티 타입
  • 값 타입

 

값 타입

값 타입은 2가지 타입이 있다.

  • 기본값 타입
  • 임베디드 타입(복합 값 타입)

 

✔️ 기본 값 타입은 말 그대로, String, int 등의 기본 타입이다.

✔️ 새로운 값 타입을 직접 정의하고 사용해야 할 때 임베디드 타입을 사용한다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Entity
public class Member {
    @Embedded Period workPeriod; // 근무 기간
    @Embedded Address homeAddress; // 집 주소
}
 
@Embeddable
public class Period {
    @Temporal(TemporalType.DATE) java.util.Date startDate;
    @Temporal(TemporalType.DATE) java.util.Date endDate;
    // ..
 
    public boolean isWork(Date date) {
        //.. 값 타입을 위한 메소드를 정의할 수 있다.
    }
}
 
@Embeddable
public class Address {
    @Column(name = "city"// 매핑할 컬럼 정의 가능
}
cs

 

 : @Embeddable : 값 타입을 정의하는 곳에 표시
 : @Embedded : 값 타입을 사용하는 곳에 표시
→ 임베디드 타입은 기본 생성자가 필수다.

 

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Entity
public class Member {
    @Embedded Address address;
    @Embedded PhoneNumber phoneNumber;
 
    //...
}
 
@Embeddable
public class Address{
    @Embedded Zipcode zipcode; // 임베디드 타입 포함
    //...
}
 
@Embeddable
public class Zipcode{
    String zip;
    String plusFour;
}
 
@Embeddable
public class PhoneNumber {
    @ManyToOne PhoneServiceProvider provider; // 엔티티 참조
}
 
@Entity
public class PhoneServiceProvider {
    @Id String name;
    //...
}
cs

 

 

테이블에 매핑하는 컬럼명이 중복 되면 어떻게 해야할까?

1
2
3
4
@Entoty public Member {
    @Embedded Address homeAddress;
    @Embedded Address companyAddress;
}
cs

 

이런 경우, @AttributeOverrides를 사용해서 매핑정보를 재정의 하면 된다.

1
2
3
4
5
6
7
8
9
10
11
@Entity
public class Member {
 
    @Embedded
    @AttributeOverrieds({
       @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;
}
cs
1
2
3
4
5
6
7
8
9
CREATE TABLE MEMBER {
    COMPANY_CITY varchar(255),
    COMPANY_STREET varchar(255),
    COMPANY_ZIPCODE varchar(255),
    city varchar(255),
    street varchar(255),
    zipcode varchar(255),
    ...
}
cs

해당 어노테이션을 사용하면 이렇게 테이블에 매핑되어 설정된다.

 

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

 


임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다.

인스턴스를 공유하면 다른곳에서 변경해도 영향을 받는다. 예상치 못한 곳에서 문제가 발생하게 된다.

 

값 타입 복사

이런 경우, 값(인스턴스)를 복사해서 사용해야 한다.

1
2
3
4
5
6
7
8
member1.setHomeAddress(new Address("Oldcity"));
Address address = member1.getHomeAddress();
 
//회원1의 address 값을 복사해서 새로운 newAddress 값을 생성
Address newAddress = address.clone();
 
newAddress.setCity("NewCity");
member2.setHomeAddress(newAddress);
cs

이런식으로 항상 값을 복사해서 넘기면 부작용이 발생할 수 있다.

하지만 객체의 공유 참조는 피할 수 없다. 그래서 수정자 메소드를 모두 제거하면 부작용의 발생을 막을 수 있다.

 

불변 객체

불변 객체는 값은 조회할 수 있지만 수정할 수 없다.

1
2
3
4
5
6
7
8
9
10
11
12
@Embeddable
public class Address {
    //생성자로 초기 값을 설정한다.
    public Address(String city) {this.city = city;}
 
    //접근자(Getter)는 노출한다.
    public String getCity(){
        return city;
    }
 
    //수정자(Setter)는 만들지 않는다.
}
cs

 

값 타입의 비교
  • 동일성 : 참조 값을 비교, == 사용
  • 동등성 : 인스턴스의 값을 비교, equals() 사용

값 타입을 비교할 때는 a.equals(b)를 사용해서 동등성 비교를 해야 한다.

 

값 타입 컬렉션

값 타입을 하나 이상 저장하려면 컬렉션에 보관하고 @ElementCollection, @CollectionTable 어노테이션을 사용하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Entity
public class Member {
 
    @ElementCollection
    @CollectionTable(name = "FAVORITE_FOODS",
        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"))
    @Column(name = "FOOD_NAME")
    private List<Address> addressHistory = new ArrayList<Address>();
}
 
@Embeddable
public class Address {
    @Column
    private String city;
    private String street;
    private String zipcode;
}
cs

@CollectionTable를 사용해서 추가한 테이블을 매핑해야한다. 그리고 사용되는 컬럼이 하나면 @Column을 사용해서 컬럼명을 지정할 수 있다. 테이블 매핑정보는 @AttributeOverride를 사용해서 재정의할 수 있다.

 

 

 

값 타입 컬렉션 사용

[등록]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Member member = New Member();
 
//임베디드 값 타입
member.setHomeaddress(new Adress("통영""해수욕장""010-01000"));
 
//기본 값 타입 컬렉션
member.getFavoriteFoods().add("A");
member.getFavoriteFoods().add("B");
member.getFavoriteFoods().add("B");
 
//임베디드 값 타입 컬렉션
member.getAddressHistory().add(new Address("서울","강남","123-123"));
member.getAddressHistory().add(new Address("서울","강북","123-123"));
 
em.persist(member);
cs
1
2
3
4
5
6
INSERT INTO MEMBER ();
INSERT INTO FAVORITE_FOODS();
INSERT INTO FAVORITE_FOODS();
INSERT INTO FAVORITE_FOODS();
INSERT INTO ADDRESS();
INSERT INTO ADDRESS();
cs

총 6 번의 INSERT SQL을 실행한다.

값 타입 컬렉션은 영속성 전이 + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.
그리고 LAZY가 기본이다.

 

[조회]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SQL : SELECT IF, CITY, STREET, ZIPCODE FROM MEMBER WHERE ID = 1
Member member = em.find(Member.class, 1L); // 1.member
 
// 2. member.homeAddress
Address homeAddress = member.getHomeAddress();
 
// 3. member.favoriteFoods
Set<String> favoriteFoods = member.getFavoriteFoods(); // LAZY
 
//SQL : SELECT MEMBER_ID, FOOD_NAME FROM FAVORITE_FOODS
// WHERE MEMBER_ID = 1
for(String favoriteFood : favoriteFoods) { }
 
// 4. member.addressHistory
List<Address> addressHistory = member.getAddressHistory(); //LAZY
 
//SQL : SELECT MEMBER_ID, CITY, STREET, ZIPCODE FROM ADDRESS WHERE MEMBER_ID = 1
addressHistory.get(0);
cs

지연로딩을 통해 실제 컬렉션을 사용할 때 SELECT문을 사용하게 된다.

 

[수정]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Member member = em.find(Member.class, 1L);
 
//1. 임베디드 값 타입 수정
member.setHomeAddress(new Address());
 
//2. 기본값 타입 컬렉션 수정
Set<String> favoriteFoods = member.getFavoriteFoods();
favoriteFoods.remove("A");
favoriteFoods.add("B");
 
//3. 임베디드 값 타입 컬렉션 수정
List<Address> addressHistory = member.getAddressHistory();
addressHistory.remove(new Address());
addressHistory.add(new Address())
cs

1. 임베디드 값 타입 수정 : Member 엔티티만 UPDATE된다. 

2. 기본값 타입 컬렉션 수정 : 제거하고 추가해야한다. 수정 할 수 없다.

3. 임베디드 값 타입 컬렉션 수정 : 값 타입은 불변해야하므로 삭제하고 등록해야한다. 값 타입은 꼭 equals, hashcode를 구현해야 한다.

 

실무에서는 값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신에 일대다 관계를 고려해야 한다.
일대다 매핑을 하고 영속성 전이 + 고아객체제거 기능을 적용하면 값 타입 컬렉션처럼 사용할 수 있다.




정리

엔티티 타입의 특성 값 타입의 특성
식별자가 있다.
생명주기가 있다.
공유할 수 있다.
식별자가 없다.
생명주기를 엔티티에 의존한다.
공유하지 않는 것이 안전하다.
📌 식별자가 필요하고 지속해서 값을 추적하고 구분하고 변경해야 한다면 그것은 값 타입이 아닌 엔티티다.