[SPRING] ๐ŸŒฑ JPA ์—ฐ๊ด€๊ด€๊ณ„, ์ƒ์†, ํ”„๋ก์‹œ, ํŠธ๋žœ์žญ์…˜ ์ •๋ฆฌ

๋ฆผ๋ฏผ์ง€ยท2025๋…„ 5์›” 17์ผ

Today I Learn

๋ชฉ๋ก ๋ณด๊ธฐ
58/62

๐Ÿ”— ์—ฐ๊ด€๊ด€๊ณ„ ๋งคํ•‘์ด๋ž€?

โœ”๏ธ ํ˜„์‹ค ์„ธ๊ณ„์ฒ˜๋Ÿผ, ๊ฐ์ฒด๋“ค๋„ ๊ด€๊ณ„๋ฅผ ๋งบ๋Š”๋‹ค
ํ˜„์‹ค์—์„œ "ํ•™์ƒ์€ ํ•™๊ต์— ๋‹ค๋‹Œ๋‹ค", "๊ฐ€๊ฒŒ๋Š” ์—ฌ๋Ÿฌ ์ƒํ’ˆ์„ ํŒ๋‹ค"์ฒ˜๋Ÿผ ๊ด€๊ณ„๊ฐ€ ์žˆ๋“ฏ์ด, ๊ฐ์ฒด ์‚ฌ์ด์—๋„ ๊ด€๊ณ„๊ฐ€ ์žˆ๋‹ค!
JPA๋Š” ์ด ๊ด€๊ณ„๋ฅผ ํ…Œ์ด๋ธ”์ด ์•„๋‹ˆ๋ผ ๊ฐ์ฒด ์ค‘์‹ฌ์œผ๋กœ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๋„๊ตฌ์ด๋‹ค (ORM)

๐Ÿค ์—ฐ๊ด€๊ด€๊ณ„

๐Ÿ‘จโ€๐Ÿซ 1 : N ์—ฐ๊ด€๊ด€๊ณ„

ํ•˜๋‚˜์˜ ๋ถ€๋ชจ๊ฐ€ ์—ฌ๋Ÿฌ ์ž์‹์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์ง€๋งŒ, ์ž์‹์€ ๋ถ€๋ชจ๊ฐ€ ํ•˜๋‚˜๋ฟ!
ex) ํ•˜๋‚˜์˜ ๋„์„œ๊ด€์— ์—ฌ๋Ÿฌ ๊ถŒ์˜ ์ฑ…! โ†’ ๋„์„œ๊ด€์€ ์—„์ฒญ ๋งŽ์€ ์ฑ…์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์ง€๋งŒ, ์ฑ…์€ ๋„์„œ๊ด€๋ฟ

1๏ธโƒฃ 1 : N ๋‹จ๋ฐฉํ–ฅ @OneToMany

:ํ•œ ๊ฐ์ฒด(๋ถ€๋ชจ)๊ฐ€ ๋‹ค๋ฅธ ์—ฌ๋Ÿฌ ๊ฐ์ฒด(์ž์‹)๋ฅผ ์ฐธ์กฐํ•˜์ง€๋งŒ, ์ž์‹์€ ๋ถ€๋ชจ๋ฅผ ๋ชจ๋ฅด๋Š” ๊ตฌ์กฐ
๋„์„œ๊ด€์€ ์ฑ…์„ ์•Œ๊ณ  ์žˆ์ง€๋งŒ, ์ฑ…์€ ์ž์‹ ์ด ์–ด๋А ๋„์„œ๊ด€์— ์†ํ–ˆ๋Š”์ง€ ๋ชจ๋ฅธ๋‹ค

//๋„์„œ๊ด€ ๊ฐ์ฒด
@Entity
public class Library {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany
    @JoinColumn(name = "library_id") // ์ฑ… ํ…Œ์ด๋ธ”์— ์™ธ๋ž˜ ํ‚ค ์ถ”๊ฐ€ -> ์ฑ…์€ ์–˜๋ฅผ ์ฐธ์กฐ
    private List<Book> books = new ArrayList<>();
}

//์ฑ… ๊ฐ์ฒด
@Entity
public class Book {
    @Id @GeneratedValue
    private Long id;
    private String title;
}

์™ธ๋ž˜ ํ‚ค(library_id)๋Š” Book ํ…Œ์ด๋ธ”(์ž์‹ ๊ฐ์ฒด)์— ์ƒ์„ฑ!
ํ•˜์ง€๋งŒ Library ์—”ํ‹ฐํ‹ฐ์—์„œ ๊ด€๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ์—, Book์„ ์ €์žฅํ•œ ๋’ค Library์— ์ถ”๊ฐ€ํ•ด๋„ ์‹ค์ œ SQL์ด 2๋ฒˆ ์ด์ƒ ๋‚˜๊ฐˆ ์ˆ˜ ์žˆ์œผ๋ฎคใ… 
โ†’ ์„ฑ๋Šฅ ์ธก๋ฉด์—์„œ ๋น„ํšจ์œจ์ 

๐Ÿ’ก 1:N ๋‹จ๋ฐฉํ–ฅ ๋งคํ•‘์€ Entity๊ฐ€ ๊ด€๋ฆฌํ•˜๋Š” ์™ธ๋ž˜ ํ‚ค๊ฐ€ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์— ์žˆ๊ณ  ์ถ”๊ฐ€์ ์ธ Update SQL์ด ์‹คํ–‰๋˜์–ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์„ค๊ณ„๊ฐ€ ๋ณต์žกํ•ด์ ธ๋„ N:1 ์–‘๋ฐฉํ–ฅ ๋งคํ•‘์„ ์‚ฌ์šฉํ•˜๋ฉด ๊ด€๋ฆฌํ•˜๊ธฐ ์‰ฝ๋‹ค!

2๏ธโƒฃ 1 : N ์–‘๋ฐฉํ–ฅ @ManyToOne

์–‘์ชฝ์ด ์„œ๋กœ๋ฅผ ์ฐธ์กฐํ•˜๋Š” ๊ตฌ์กฐ!
๋„์„œ๊ด€๋„ ์ฑ…์„ ์•Œ๊ณ , ์ฑ…๋„ ์ž์‹ ์ด ์–ด๋А ๋„์„œ๊ด€์— ์†ํ–ˆ๋Š”์ง€ ์•Œ๊ฒŒ๋จ!
์—ฐ๊ด€๊ด€๊ณ„์˜ ์ฃผ์ธ์ด ๋˜์ง€ ์•Š๋„๋ก insertable = false, updatable = false ์„ค์ •

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

    @OneToMany(mappedBy = "library") <-- ์—ฌ๊ธฐ!
    private List<Book> books = new ArrayList<>();

    public void addBook(Book book) {
        books.add(book);
        book.setLibrary(this);
    }
}

@Entity
public class Book {
    @Id @GeneratedValue
    private Long id;
    private String title;

    @ManyToOne
    @JoinColumn(name = "library_id")
    private Library library;

    public void setLibrary(Library library) {
        this.library = library;
    }
}

books.add(book); ์—ฌ๊ธฐ์„œ
๋„์„œ๊ด€ ์ž…์žฅ์—์„œ๋Š” "๋‚ด๊ฐ€ ๊ฐ€์ง„ ์ฑ… ๋ชฉ๋ก์— ์ด๊ฑฐ ์ถ”๊ฐ€ํ• ๊ฒŒ!" ์ด๊ณ 
์ฑ… ์ž…์žฅ์—์„œ๋Š” "๋‚ด ๋„์„œ๊ด€์€ ์ด ๋„์„œ๊ด€์ด์•ผ!"๋ผ๊ณ  ๋งํ•˜๋Š” ๊ฒƒ๊ณผ ๊ฐ™๋‹ค

public void setLibrary(Library library) {
        this.library = library;
    }

โžก๏ธ insertable = false, updatable = false๋กœ ํ•˜๋ฉด ์ฝ๊ธฐ ์ „์šฉ์œผ๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค

Book์ด ์™ธ๋ž˜ ํ‚ค(library_id)๋ฅผ ์‹ค์ œ๋กœ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฏ€๋กœ, ์—ฐ๊ด€๊ด€๊ณ„์˜ ์ฃผ์ธ์€ Book
mappedBy = "library"๋กœ Library๋Š” ๋‹จ์ˆœํžˆ ๋ณด์—ฌ์ฃผ๊ธฐ์šฉ(์ฝ๊ธฐ์šฉ) ๊ด€๊ณ„๊ฐ€ ๋จ
addBook()์ฒ˜๋Ÿผ ์–‘์ชฝ ๊ด€๊ณ„๋ฅผ ๋งž์ถฐ์ฃผ๋Š” ํŽธ์˜ ๋ฉ”์„œ๋“œ๋ฅผ ๊ผญ ๋งŒ๋“ค์–ด์ฃผ๋Š” ๊ฒŒ ์ข‹๋‹ค!
โ†’ ๊ฐ์ฒด ๊ฐ„ ๊ด€๊ณ„๊ฐ€ ๋ช…ํ™•ํ•จ but, ๋ณต์žกํ•ด์งˆ ์ˆ˜ ์žˆ๊ณ  ์–‘์ชฝ์˜ ์—ฐ๊ด€๊ด€๊ณ„ ๋™๊ธฐํ™”๊ฐ€ ํ•„์š”ํ•˜๋‹ค


๐Ÿ‘ฌ 1 : 1 ์—ฐ๊ด€๊ด€๊ณ„

1๏ธโƒฃ ๋‹จ๋ฐฉํ–ฅ @OneToOne

ํŠœํ„ฐ๋Š” ์ž์‹ ์˜ ์ฃผ์†Œ(Address)๋ฅผ ์•Œ๊ณ  ์žˆ์–ด์š”

@OneToOne
@JoinColumn(name = "address_id", unique = true)
private Address address;

2๏ธโƒฃ ์–‘๋ฐฉํ–ฅ

์ฃผ์†Œ๋„ ๋ˆ„๊ฐ€ ์‚ฌ๋Š”์ง€ ์•Œ์•„์š”.

@OneToOne(mappedBy = "address")
private Tutor tutor;

๐Ÿ“Œ ์™ธ๋ž˜ ํ‚ค๋Š” ์–ด๋А ํ…Œ์ด๋ธ”์— ๋‘ฌ๋„ ๋˜์ง€๋งŒ, ์„ฑ๋Šฅ, ๋ฌด๊ฒฐ์„ฑ ๋“ฑ์„ ๊ณ ๋ คํ•ด์„œ ๊ฒฐ์ •ํ•ด์•ผ ํ•ด์š”.

๐Ÿ‘ฅ N : M ์—ฐ๊ด€๊ด€๊ณ„

ํŠœํ„ฐ๋Š” ์—ฌ๋Ÿฌ ์–ธ์–ด๋ฅผ ์“ฐ๊ณ , ์–ธ์–ด๋„ ์—ฌ๋Ÿฌ ํŠœํ„ฐ์—๊ฒŒ ์‚ฌ์šฉ๋ผ์š”.

@ManyToMany
@JoinTable(
  name = "tutor_language",
  joinColumns = @JoinColumn(name = "tutor_id"),
  inverseJoinColumns = @JoinColumn(name = "language_id")
)
private List<Language> languages;

๐Ÿ“› ๋ฌธ์ œ์ :
์ถ”๊ฐ€ ์ •๋ณด(level, license ๋“ฑ)๋ฅผ ๋ชป ๋‹ด์Œ
์ค‘๊ฐ„ ํ…Œ์ด๋ธ”์ด ์ˆจ๊ฒจ์ ธ ์žˆ์–ด ๊ด€๋ฆฌ ์–ด๋ ค์›€

๐Ÿ‘‰ ํ•ด๊ฒฐ: ์ค‘๊ฐ„ ํ…Œ์ด๋ธ”์„ ์—”ํ‹ฐํ‹ฐ๋กœ ์ง์ ‘ ์ƒ์„ฑํ•ด์„œ @OneToMany + @ManyToOne์œผ๋กœ ๋‚˜๋ˆ ์š”.


๐Ÿงฌ ์ƒ์†๊ด€๊ณ„ ๋งคํ•‘

JPA๋Š” ์ž๋ฐ”์˜ ์ƒ์† ๊ตฌ์กฐ๋ฅผ ํ…Œ์ด๋ธ”๋กœ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ๋„๋ก 3๊ฐ€์ง€ ์ „๋žต์„ ์ œ๊ณตํ•œ๋‹ค

์ „๋žตํŠน์ง•
SINGLE_TABLEํ•œ ํ…Œ์ด๋ธ”์— ๋ชจ๋‘ ์ €์žฅ. ์„ฑ๋Šฅ ๋น ๋ฆ„.
JOINEDํ…Œ์ด๋ธ”์„ ๋‚˜๋ˆ„๊ณ  JOIN. ์ •๊ทœํ™”์— ์ข‹์Œ.
TABLE_PER_CLASS๊ฐ๊ฐ ํ…Œ์ด๋ธ”. ๊ฑฐ์˜ ์‚ฌ์šฉ โŒ
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype")

์ƒํ™ฉ์— ๋”ฐ๋ผ ๋งž๊ฒŒ ์„ ํƒํ•ด์•ผํ•œ๋‹ค


๐Ÿ•ต๏ธ Proxy์™€ Entity ์กฐํšŒ

๐Ÿ“ em.find vs em.getReference
em.find() โ†’ ์‹ค์ œ ๊ฐ์ฒด๋ฅผ ๋ฐ”๋กœ ์กฐํšŒ

em.getReference() โ†’ Proxy ๊ฐ์ฒด(๊ฐ€์งœ ๊ฐ์ฒด)๋ฅผ ๋ฐ˜ํ™˜ โ†’ ์‹ค์ œ ์‚ฌ์šฉ ์‹œ ์กฐํšŒ

Tutor proxy = em.getReference(Tutor.class, id);
proxy.getName(); // ์ด๋•Œ DB ์กฐํšŒ ๋ฐœ์ƒ!

๐Ÿ“› ์ฃผ์˜: ํ”„๋ก์‹œ๋Š” ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ ๋ฐ–์—์„œ๋Š” ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค โ†’ ์˜ˆ์™ธ ๋ฐœ์ƒโ—


๐Ÿข Lazy vs โšก๏ธ Eager

Lazy Loading (์ง€์—ฐ ๋กœ๋”ฉ)

โ†’ ์‹ค์ œ ์‚ฌ์šฉํ•  ๋•Œ DB์—์„œ ์กฐํšŒ
โžก๏ธ์—ฐ๊ด€๋œ ๊ฐ์ฒด๊ฐ€ ํ•„์š” ์—†์œผ๋ฉด SQL ์•ˆ ๋‚ ์•„๊ฐ โ†’ ํšจ์œจ์ !

@ManyToOne(fetch = FetchType.LAZY)

Eager Loading (์ฆ‰์‹œ ๋กœ๋”ฉ)

: ๊ฐ์ฒด๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ๋•Œ ์—ฐ๊ด€ ๊ฐ์ฒด๋„ ํ•จ๊ป˜ ์กฐํšŒ

โžก๏ธ ๋ฌด์กฐ๊ฑด JOIN โ†’ ์˜ˆ๊ธฐ์น˜ ์•Š์€ ์„ฑ๋Šฅ ์ด์Šˆ ๋ฐœ์ƒ ๊ฐ€๋Šฅ

@ManyToOne(fetch = FetchType.EAGER)

๋Œ€๋ถ€๋ถ„์€ LAZY๋ฅผ ์“ฐ๊ณ , ๊ผญ ํ•„์š”ํ•œ ๊ฒฝ์šฐ๋งŒ EAGER!


๐Ÿ”„ ์˜์†์„ฑ ์ „์ด (Cascade)

โœ”๏ธ ๋ถ€๋ชจ ์—”ํ‹ฐํ‹ฐ ์ €์žฅ ์‹œ ์ž์‹๋„ ์ž๋™ ์ €์žฅ!

@OneToMany(cascade = CascadeType.ALL)

ALL, PERSIST, REMOVE ๋“ฑ ํ•„์š”์— ๋”ฐ๋ผ์„œ ์„ ํƒํ•ด ์‚ฌ์šฉํ•˜๊ธฐ
โžก๏ธ ์ƒ๋ช…์ฃผ๊ธฐ๊ฐ€ ์™„์ „ํžˆ ๊ฐ™์€ ๊ฒฝ์šฐ์—๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค!!

๐Ÿ—‘๏ธ ๊ณ ์•„ ๊ฐ์ฒด (OrphanRemoval)

๋ถ€๋ชจ์™€ ๊ด€๊ณ„๊ฐ€ ๋Š๊ฒผ์„๋•Œ,๊ทธ ๋ถ€๋ชจ์˜ ์ž์‹์€ ๊ณ ์•„๊ฐ์ฒด๊ฐ€ ๋œ๋‹ค
๊ทธ๋Ÿฌ๋ฉด ~ ์ž์‹ ๊ฐ์ฒด๋„ ์ž๋™์œผ๋กœ ์‚ญ์ œ๋˜๋Š” ๊ฒƒ
โœ”๏ธ ๋ถ€๋ชจ์™€ ๊ด€๊ณ„ ๋Š๊ธฐ๋ฉด ์ž์‹ ์ž๋™ ์‚ญ์ œ!

@OneToMany(orphanRemoval = true)

๐Ÿ”„ ํŠธ๋žœ์žญ์…˜ ์ „ํŒŒ @Transactional

: ๊ธฐ๋ณธ์ ์œผ๋กœ ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜ ์•ˆ์—์„œ ์ฒ˜๋ฆฌ
โ†’ ์ด ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ถ™์€ ์ฝ”๋“œ๋Š”, ๋ฐ”๋กœ๋ฐ”๋กœ DB์— ๋ฐ˜์˜๋˜์ง€ ์•Š๊ณ  ์ค‘๊ฐ„์— ํ•˜๋‚˜๋ผ๋„ ์˜ค๋ฅ˜๊ฐ€ ์ƒ๊ธฐ๋ฉด ๋‹ค ์ทจ์†Œํ•  ์ˆ˜ ์žˆ๋„๋ก
ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๊ด€๋ฆฌ!
โžก๏ธ ๋ฌธ์ œ ๋ฐœ์ƒ ์‹œ ์ „์ฒด ROLLBACK

๐Ÿ“Œ REQUIRES_NEW๋ฅผ ์“ฐ๋ฉด ๋‹ค ๋กค๋ฐฑ๋˜์ง€ ์•Š๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Œ
์˜ˆ๋ฅผ ๋“ค์–ด, ํšŒ์›๊ฐ€์ž…์€ ๋˜์ง€๋งŒ, ํฌ์ธํŠธ๋Š” ์ง€๊ธ‰๋˜์ง€ ์•Š๋„๋ก

@Transactional(propagation = Propagation.REQUIRES_NEW)

โžก๏ธ ๋‘ ์ž‘์—…์„ ๋…๋ฆฝ์  ํŠธ๋žœ์žญ์…˜์œผ๋กœ ์ฒ˜๋ฆฌ!!!

0๊ฐœ์˜ ๋Œ“๊ธ€