μ›Ή 개발/Spring Boot

[κΉ€μ˜ν•œ_μžλ°” ORM ν‘œμ€€ JPA ν”„λ‘œκ·Έλž˜λ° - 기본편] 8. κ°’ νƒ€μž…

shj718 2022. 6. 18. 16:57

🌠 κ°’ νƒ€μž…

JPA의 데이터 νƒ€μž… λΆ„λ₯˜

  • μ—”ν‹°ν‹° νƒ€μž… (@Entity둜 μ •μ˜ν•˜λŠ” 객체 → 데이터가 변해도 μ‹λ³„μžλ‘œ μ§€μ†ν•΄μ„œ 좔적 κ°€λŠ₯)
  • κ°’ νƒ€μž…

κ°’ νƒ€μž… λΆ„λ₯˜

  • κΈ°λ³Έκ°’ νƒ€μž… (Ex. int, double, Integer, Long, String, ···)
  • μž„λ² λ””λ“œ νƒ€μž… (볡합 κ°’ νƒ€μž… → 직접 μ •μ˜ κ°€λŠ₯)
  • μ»¬λ ‰μ…˜ κ°’ νƒ€μž… (Ex. Set, List, ···)

 

🌠 μž„λ² λ””λ“œ νƒ€μž…

μž„λ² λ””λ“œ νƒ€μž…μ€ μƒˆλ‘œμš΄ κ°’ νƒ€μž…μ„ 직접 μ •μ˜ν•  수 μžˆλ‹€. 주둜 κΈ°λ³Έ κ°’ νƒ€μž…μ„ λͺ¨μ•„μ„œ λ§Œλ“€κΈ° λ•Œλ¬Έμ— '볡합 κ°’ νƒ€μž…'이라고도 ν•œλ‹€. μž„λ² λ””λ“œ νƒ€μž…μ€ int, Stringκ³Ό 같은 'κ°’ νƒ€μž…'으둜써 μ—”ν‹°ν‹°κ°€ μ•„λ‹ˆκ³ , ν•œλ²ˆ 값을 λ³€κ²½ν•˜λ©΄ 좔적이 λΆˆκ°€λŠ₯ν•˜λ‹€.

 

μž„λ² λ””λ“œ νƒ€μž…μ€ μ–Έμ œ μ‚¬μš©ν• κΉŒ?

DB의 νšŒμ› ν…Œμ΄λΈ”μ— 근무 μ‹œμž‘μΌ, 근무 μ’…λ£ŒμΌ, μ£Όμ†Œμ˜ λ„μ‹œ, μ£Όμ†Œμ˜ λ„λ‘œλͺ…, μ£Όμ†Œμ˜ μš°νŽΈλ²ˆν˜Έκ°€ μžˆλŠ” 상황을 μƒκ°ν•΄λ³΄μž.

객체 κ΄€μ μ—μ„œ λ³΄μ•˜μ„ λ•Œ, 근무 μ‹œμž‘μΌκ³Ό μ’…λ£ŒμΌμ„ Period둜, μ£Όμ†Œ κ΄€λ ¨ 속성듀을 Address둜 λ¬Άμ–΄μ„œ λ³„λ„μ˜ 클래슀λ₯Ό λ§Œλ“€λ©΄ 쒋지 μ•Šμ„κΉŒ? μ΄λ ‡κ²Œ ν•˜λ©΄ 클래슀 μž¬μ‚¬μš©μ΄ κ°€λŠ₯해지고 Period.isWork() 같은 μ˜λ―ΈμžˆλŠ” λ©”μ†Œλ“œλ₯Ό λ§Œλ“€ 수 있게 λœλ‹€.

 

 

 

μž„λ² λ””λ“œ νƒ€μž… μ‚¬μš© 방법을 μ•Œμ•„λ³΄μž.

@Embeddable은 κ°’νƒ€μž…μ„ μ •μ˜ν•˜λŠ” 곳에 ν‘œμ‹œν•˜κ³ , @EmbeddedλŠ” κ°’νƒ€μž…μ„ μ‚¬μš©ν•˜λŠ” 곳에 ν‘œμ‹œν•œλ‹€. 그리고 κ°’νƒ€μž…μ„ μ •μ˜ν•˜λŠ” 곳에 λ°˜λ“œμ‹œ κΈ°λ³Έμƒμ„±μžλ₯Ό λ§Œλ“€μ–΄μ•Ό ν•œλ‹€.

 

μ½”λ“œ μ˜ˆμ‹œ:

@Entity
public class Member {

@Embedded
private Period workPeriod;

@Embedded
private Address homeAddress;
@Embeddable
public class Period {

    private LocalDateTime startDate;
    private LocalDateTime endDate;

    public Period() {

    }
@Embeddable
public class Address {

    private String city;
    private String street;
    private String zipcode;

    public Address() {

    }

 

μ‹€ν–‰ κ²°κ³Ό:

Member member = new Member();
member.setUsername("shj");
member.setHomeAddress(new Address("μ„œμšΈ", "μ™€μš°μ‚°λ‘œ", "94"));
member.setWorkPeriod(new Period(LocalDateTime.now(), LocalDateTime.now().plusYears(1L)));
em.persist(member);
transaction.commit(); // 컀밋

 

참고둜 μž„λ² λ””λ“œ νƒ€μž… μ•ˆμ— μ—”ν‹°ν‹°λ₯Ό λ„£λŠ” 것도 κ°€λŠ₯ν•˜λ‹€. (Ex. Memberκ°€ Address μž„λ² λ””λ“œ νƒ€μž…μ„ μ°Έμ‘°ν•˜κ³  Address μž„λ² λ””λ“œ νƒ€μž…μ΄ House μ—”ν‹°ν‹°λ₯Ό μ°Έμ‘°ν•˜λŠ” 것이 κ°€λŠ₯)

 

λ§Œμ•½, ν•œ μ—”ν‹°ν‹°μ—μ„œ 같은 κ°’νƒ€μž…μ„ μ—¬λŸ¬λ²ˆ μ‚¬μš©ν•˜κ³  μ‹ΆμœΌλ©΄ μ–΄λ–»κ²Œ ν•΄μ•Όν• κΉŒ? μ•„λž˜μ™€ 같이 μ‚¬μš©ν•˜κ³  싢을 수 μžˆμ„ 것이닀. 그럴 λ•ŒλŠ” @AttributeOverrides, @AttributeOverrideλ₯Ό μ“°λ©΄ λœλ‹€.

@Entity
public class Member extends BaseEntity {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "MEMBER_ID")
    private Long id;   
    
    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "city", column = @Column(name = "HOME_CITY")),
            @AttributeOverride(name = "street", column = @Column(name = "HOME_STREET")),
            @AttributeOverride(name = "zipcode.zip", column = @Column(name = "HOME_ZIP")),
            @AttributeOverride(name = "zipcode.plusFour", column = @Column(name = "HOME_PLUS_FOUR")),
    })
    private Address homeAddress;

    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "city", column = @Column(name = "COMPANY_CITY")),
            @AttributeOverride(name = "street", column = @Column(name = "COMPANY_STREET")),
            @AttributeOverride(name = "zipcode.zip", column = @Column(name = "COMPANY_ZIP")),
            @AttributeOverride(name = "zipcode.plusFour", column = @Column(name = "COMPANY_PLUS_FOUR")),
    })
    private Address companyAddress;
}

@Embeddable
public class Address {

    private String city;
    private String street;

    @Embedded
    private Zipcode zipcode;
}

@Embeddable
public class Zipcode {

    private String zip;
    private String plusFour;
}

 

🌠 객체 νƒ€μž…μ˜ ν•œκ³„, λΆˆλ³€ 객체

μž„λ² λ””λ“œ νƒ€μž…μ€ κ°’ νƒ€μž…μ΄κ³  객체 νƒ€μž…μ΄λ‹€. 객체 νƒ€μž…μ€ μžλ°”μ˜ κΈ°λ³Έ νƒ€μž…(Ex. int, double)κ³Ό λ‹€λ₯΄κ²Œ '=' 연산을 ν•  λ•Œ 값을 λ³΅μ‚¬ν•˜λŠ” 것이 μ•„λ‹ˆλΌ μ°Έμ‘°λ₯Ό μ „λ‹¬ν•œλ‹€.

 

λ”°λΌμ„œ, μ•„λž˜μ™€ 같은 μ½”λ“œλ₯Ό μž‘μ„±ν•  경우, b의 μ£Όμ†Œλ§Œ λ°”κΎΈλ €λŠ” μ›λž˜μ˜ μ˜λ„μ™€ λ‹€λ₯΄κ²Œ a의 μ£Όμ†ŒκΉŒμ§€ λ°”λ€Œκ²Œ λœλ‹€.

Address a = new Address(“Old”); 
Address b = a; // 객체 νƒ€μž…μ€ μ°Έμ‘°λ₯Ό 전달
b.setCity(“New”);

이런 λΆ€μž‘μš©μ„ 막기 μœ„ν•΄μ„œλŠ” ⭐️값 νƒ€μž…μ„ λΆˆλ³€ 객체둜 섀계⭐️해야 ν•œλ‹€. λΆˆλ³€ 객체둜 λ§Œλ“œλŠ” κ°€μž₯ μ‰¬μš΄ 방법은 setter()λ₯Ό λ§Œλ“€μ§€ μ•ŠλŠ” 것이닀. β­οΈμƒμ„±μžλ‘œλ§Œ 값을 섀정⭐️할 수 있게 λ§Œλ“€λ©΄ λœλ‹€. 값을 μˆ˜μ •ν•˜κ³ μž ν•  경우 μƒˆλ‘œμš΄ 객체λ₯Ό μƒμ„±ν•˜λ©΄ λœλ‹€. 참고둜 Integer, String은 μžλ°”κ°€ μ œκ³΅ν•˜λŠ” λŒ€ν‘œμ μΈ λΆˆλ³€ 객체이닀.

 

🌠 κ°’ νƒ€μž…μ˜ 비ꡐ

κ°’ νƒ€μž…μ˜ λΉ„κ΅λŠ” == 으둜 ν•˜λ©΄ μ•ˆλ˜κ³ , equals() λ©”μ†Œλ“œλ₯Ό μ μ ˆν•˜κ²Œ μž¬μ •μ˜ν•΄μ„œ μ‚¬μš©ν•΄μ•Ό ν•œλ‹€. (주둜 λͺ¨λ“  ν•„λ“œλ₯Ό 비ꡐ함) 참고둜 μΈν…”λ¦¬μ œμ΄λŠ” 'Generate equals() and hashCode()' κΈ°λŠ₯을 μ œκ³΅ν•˜κ³  μžˆλ‹€.

 

🌠 κ°’ νƒ€μž… μ»¬λ ‰μ…˜

κ°’ νƒ€μž… μ»¬λ ‰μ…˜μ€ κ°’ νƒ€μž…μ„ μ»¬λ ‰μ…˜μ— λ‹΄μ•„μ„œ μ“°λŠ” 것을 λ§ν•œλ‹€.

그런데 일반적으둜 DBμ—λŠ” μ»¬λ ‰μ…˜μ„ 담을 수 μžˆλŠ” νƒ€μž…μ΄ μ—†λ‹€. λ”°λΌμ„œ, μΌλŒ€λ‹€ κ°œλ…μ²˜λŸΌ λ³„λ„μ˜ ν…Œμ΄λΈ”λ‘œ λ§Œλ“€μ–΄μ•Ό ν•œλ‹€.

 

예λ₯Ό λ“€μ–΄, Set<String> favoriteFoods μ»¬λ ‰μ…˜μ€ Member ν…Œμ΄λΈ”μ— μ €μž₯λ˜λŠ” 것이 μ•„λ‹ˆλΌ FAVORITE_FOODλΌλŠ” λ³„λ„μ˜ ν…Œμ΄λΈ”μ„ λ§Œλ“ λ‹€.

그리고 FAVORITE_FOOD의 PKλŠ” λͺ¨λ“  μ»¬λŸΌλ“€μ˜ 쑰합이닀. 즉, FK인 MEMBER_ID + FOOD_NAME λͺ¨λ‘μ˜ 쑰합이 PKκ°€ λœλ‹€.

 

λ‹€μ‹œ λ§ν•˜μžλ©΄, κ°’ νƒ€μž… μ»¬λ ‰μ…˜μ€ λ³„λ„μ˜ ν…Œμ΄λΈ”λ‘œ λ§Œλ“€λ©°, λ³„λ„μ˜ μ‹λ³„μž 없이 ν…Œμ΄λΈ”μ˜ μ™Έλž˜ 킀와 λ‹€λ₯Έ μ»¬λŸΌλ“€μ„ μ‘°ν•©ν•΄ PK둜 μ“΄λ‹€. (λ³„λ„μ˜ μ‹λ³„μžκ°€ μ‘΄μž¬ν•œλ‹€λ©΄ 엔티티와 λ‹€λ₯Ό 게 μ—†μœΌλ‹ˆκΉŒ.)

 

μ½”λ“œ μ˜ˆμ‹œ:

@Entity
public class Member {
    ...
    @ElementCollection
    @CollectionTable(
        name = "FAVORITE_FOOD",
        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<>();
    ...
}

 

κ°’ νƒ€μž… μ»¬λ ‰μ…˜ μ €μž₯

이제 κ°’ νƒ€μž… μ»¬λ ‰μ…˜μ„ μ €μž₯ν•΄λ³΄μž.

μ•„λž˜ μ½”λ“œμ—μ„œ λ³Ό 수 μžˆλ“―μ΄ FavoriteFoods와 AddressHistory에 λŒ€ν•΄μ„œλŠ” em.persist();λ₯Ό ν•˜μ§€ μ•Šκ³  Member에 λŒ€ν•΄μ„œλ§Œ em.persists(member);λ₯Ό 해도 DB에 6개의 ν–‰ λͺ¨λ‘κ°€ μ €μž₯λœλ‹€. (Member와 라이프사이클을 ν•¨κ»˜ν•œλ‹€.)

⭐️즉, κ°’ νƒ€μž… μ»¬λ ‰μ…˜μ€ μ˜μ†μ„± 전이(cascade=CascadeType.ALL)와 κ³ μ•„ 객체 제거 κΈ°λŠ₯(orphanRemoval=true)을 ν•„μˆ˜λ‘œ 가진닀.⭐️

Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "10000"));

member.getFavoriteFoods().add("μΉ˜ν‚¨");
member.getFavoriteFoods().add("쑱발");
member.getFavoriteFoods().add("ν”Όμž");

member.getAddressHistory().add(new Address("old1", "street1", "10001"));
member.getAddressHistory().add(new Address("old2", "street2", "10002"));

em.persist(member);

 

κ°’ νƒ€μž… μ»¬λ ‰μ…˜ 쑰회

λ˜ν•œ, κ°’ νƒ€μž… μ»¬λ ‰μ…˜μ€ λ””ν΄νŠΈλ‘œ μ§€μ—°λ‘œλ”© μ „λž΅μ„ μ‚¬μš©ν•œλ‹€. λ”°λΌμ„œ Memberλ₯Ό μ‘°νšŒν•˜λ©΄ FavoriteFoods와 AddressHistory ν…Œμ΄λΈ”κΉŒμ§€ λΆˆλŸ¬μ˜€λŠ” 것이 μ•„λ‹ˆλΌ, 이후 FavoriteFoods와 AddressHistoryλ₯Ό μ‘°νšŒν•  λ•Œ 쿼리문이 λ‚˜κ°„λ‹€.

 

κ°’ νƒ€μž… μ»¬λ ‰μ…˜ μˆ˜μ •

μœ„μ—μ„œ μ–ΈκΈ‰ν–ˆλ‹€μ‹œν”Ό κ°’ νƒ€μž…μ€ λΆˆλ³€μ΄λ‹€. λ”°λΌμ„œ κ°’ νƒ€μž… μ»¬λ ‰μ…˜μ„ μˆ˜μ •ν•˜κ³ μž ν•œλ‹€λ©΄, μƒˆλ‘œμš΄ 객체λ₯Ό 생성해야 ν•œλ‹€. (set ν•¨μˆ˜ μ‚¬μš© X)

μ˜ˆμ‹œ 1(κ°’ νƒ€μž…):

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

// homeCity -> newCity 
// findMember.getHomeAddress().setCity("newCity"); // ν‹€λ¦° 방법 

Address address = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", address.getStreet(), address.getZipCode())); // μƒˆλ‘œ 생성

μ˜ˆμ‹œ2(κ°’ νƒ€μž… μ»¬λ ‰μ…˜):

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

// 쑱발 -> 햄버거
findMember.getFavoriteFoods().remove("쑱발");
findMember.getFavoriteFoods().add("햄버거");

+κ°’ νƒ€μž… μ»¬λ ‰μ…˜ μš”μ†Œλ₯Ό μ§€μšΈ λ•Œ equals ν•¨μˆ˜κ°€ μ œλŒ€λ‘œ κ΅¬ν˜„λ˜μ–΄ μžˆλŠ” 것이 μ€‘μš”ν•˜λ‹€.

 

κ°’ νƒ€μž… μ»¬λ ‰μ…˜μ˜ μ œμ•½μ‚¬ν•­

⭐️값 νƒ€μž… μ»¬λ ‰μ…˜μ— 변경사항이 λ°œμƒν•˜λ©΄, 주인 엔티티와 κ΄€λ ¨λœ λͺ¨λ“  데이터λ₯Ό μ‚­μ œν•˜κ³ , κ°’ νƒ€μž… μ»¬λ ‰μ…˜μ— μžˆλŠ” ν˜„μž¬κ°’μ„ λͺ¨λ‘ λ‹€μ‹œ μ €μž₯ν•œλ‹€.⭐️

예λ₯Ό λ“€μ–΄ μ•„λž˜μ˜ μ˜ˆμ‹œμ—μ„œλŠ” ADDRESS ν…Œμ΄λΈ”μ— 있던 기쑴의 λͺ¨λ“  데이터("another city", "old city")λ₯Ό μ‚­μ œν•˜κ³ , 기쑴에 있던 "another city"λ₯Ό λ‹€μ‹œ μ €μž₯ν•˜κ³  "new city"λ₯Ό μ €μž₯ν•œλ‹€.

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

// ν˜„μž¬ ADDRESS ν…Œμ΄λΈ”μ—λŠ” "another city", "old city" 쑴재
// old city -> new city
findMember.getAddressHistory().remove(new Address("old city", "street", "12345"));
findMember.getAddressHistory().add(new Address("new city", "street", "12345"));
// 이제 "another city", "new city" 쑴재

 

κ·Έλž˜μ„œ... 결둠은?

이런 μ €λŸ° μ œμ•½μ‚¬ν•­μ΄ λ§Žμ€ κ°’ νƒ€μž… μ»¬λ ‰μ…˜μ€ μ‹€λ¬΄μ—μ„œ μ‚¬μš©ν•˜μ§€ μ•ŠλŠ”λ‹€. (μ—…λ°μ΄νŠΈλ‚˜ 좔적할 ν•„μš” μ—†λŠ” λ‹¨μˆœν•œ 상황일 λ•Œλ§Œ μ‚¬μš©)

λŒ€μ‹ μ—, μ—”ν‹°ν‹°λ‘œ κ°μ‹Έμ„œ(wrapping) μΌλŒ€λ‹€ κ΄€κ³„λ‘œ μš°νšŒν•œλ‹€.

μ˜ˆμ‹œ:

 

AddressEntity

@Entity
@Table(name = "ADDRESS")
public class AddressEntity {
    @Id
    @GeneratedValue
    private Long id;

    private Address address; // μ—”ν‹°ν‹°λ‘œ 감싼 λͺ¨μŠ΅
}

Member

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