본문 바로가기

FastCampus - 한번에 끝내는 Java|Spring 웹 개발/04 JPA

JPA Ch 8 영속성 전이 (Cascade) - 패스트캠퍼스 챌린지 36일차

오늘 내용은 Cascade 입니다. DB의 그것과 비슷하면서도 완전히 다른 의미더라구요.

Cascade ("영속성 전이")

Cascade 는 아마 계단이 단계적으로 떨어지는 폭포를 의미할 겁니다 (*사전을 찾아봤지롱).

Cascade는 데이터베이스에도 있는 개념이지만, (cascade drop 이나 cascade set null 같은 거), JPA에서의 의미와는 확연히 다릅니다. 같은...건가? 아무튼 JPA에서의 Cascade JPA 오퍼레이션의 관점으로 언급되어 있습니다.

JPA의 Cascade 는 관계를 지정하는 @OneToOne, @OneToMany, @ManyToOne 등에 필드로 존재합니다.

JPA에서 Cascade가 일어나는 상황

먼저 상황을 봅시다. JPA 의 Cascade는 JPA의 개념이 녹아있는 만큼 이걸 말로만 설명하는 건 어렵거든요.

지금까지 배운 대로라면 (a) book 과 publisher를 하나씩 만들어서 (b) 둘의 관계를 엮은 뒤 (c) 둘 다 저장하는 건 다음과 같이 짜야 할 겁니다.

@SpringBootTest
class BookRepoTest {
    @Autowired private BookRepo bookRepo;
    @Autowired private PublisherRepo pubRepo;

    @Test
    void bookCascadeTest() {
        // (1) book 을 만들어서 저장합니다.
        Book book = new Book();
        book.setName("My awesome book");

        bookRepo.save(book);

        // (2) publisher 를 만들어서 저장합니다.
        Publisher publisher = new Publisher();
        publisher.setName("sneak campus!");

        pubRepo.save(publisher);

        // (3) 둘의 관계를 지정해서 다시 저장합니다.
        book.setPublisher(publisher);
        publisher.getBooks().add(book);
        bookRepo.save(book);

        System.out.println(bookRepo.findAll());
        System.out.println(pubRepo.findAll());
    }
}

굳이 book과 publisher 를 각자 저장한 뒤, 관계를 설정해서 다시 저장하는 게 보이시나요? 불필요한 짓을 번거롭게 하고있다는 생각이 듭니다. 그냥 한 쪽에 저장하면 알아서 들어가주면 안 될까요?


오류

흐름을 끊게 되지만 오류에 대해서는 설명해야겠습니다.

failed to lazily initialize a collection of role: kr.co.fastcampus.jpasandbox.publisher.Publisher.books, could not initialize proxy - no Session

위의 코드는 오류가 납니다. 해결 방법은 @Transactional 을 붙이거나 양 관계의 필드에 모두 @ToString.Exclude 를 붙여주는 겁니다.자세한 사항은 세션과 관련있어서 이후 챕터에서 설명해주신다네요.

강의 때에는 mysql 이 안 켜져있어서 hibernate SQL favor 가 다르다드니 하는 엉뚱한 오류가 나왔다고 설명을 해주신 부분도 추가로 있었습니다.

개발자라면 감으로 알아야 읍읍


다시 원래 하던 걸로 돌아와서...

우리가 하고싶은 건 아래와 같이 (1) 저장 없이 관계만 저장한 뒤 (2) 바로 저장하면 알아서 (결과) 둘 다 저장되어주는 것입니다.

@SpringBootTest
class BookRepoTest {
    @Autowired private BookRepo bookRepo;
    @Autowired private PublisherRepo pubRepo;
    
    @Test
    void bookCascadeTest() {
        // (1) 두 객체를 만든 뒤
        Book book = new Book();
        book.setName("My awesome book");
        
        Publisher publisher = new Publisher();
        publisher.setName("sneak campus!");

        // (2) 저장 없이 바로 관계를 설정합니다.
        book.setPublisher(publisher);
        publisher.getBooks().add(book);
        
        // (3) 저장합니다. 관계가 있는 나머지 한 쪽도 저장되도록 만들고 싶습니다.
        bookRepo.save(book);

        System.out.println(bookRepo.findAll());
        System.out.println(pubRepo.findAll());
    }
    
    생략

물론 이 상태로 위 코드를 실행하면 오류가 나겠죠. 왜 이런진 아시죠? 0_< 한 쪽이 아직 관리되고 있지 않아서잖아요. 그쵸?

org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : kr.co.fastcampus.jpasandbox.book.Book.publisher -> kr.co.fastcampus.jpasandbox.publisher.Publisher; nested exception is java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : kr.co.fastcampus.jpasandbox.book.Book.publisher -> kr.co.fastcampus.jpasandbox.publisher.Publisher

여기에 JPA의 cascade 설정이 끼어들게 됩니다.

JPA의 cascade

위 코드가 동작하려면 publisher 프로퍼티의 @ManyToOne 에 cascade로 Cascade.PERSIST 를 추가해주면 됩니다. 저장할 때 달려있는 다른 객체도 같이 저장하라는 뜻이죠. JPA 입장에서는 PERSIST 연산이죠?

생략
public class Book extends 생략 {
    생략
    //                     이거 추가
    //         vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    @ManyToOne(cascade = { CascadeType.PERSIST })
    @ToString.Exclude
    private Publisher publisher;
}
Hibernate: 
    insert into publisher 생략
Hibernate: 
    insert into book 생략
Hibernate: 
    update book set publisher_id=? where id=?
Hibernate: 
    select 생략 from book book0_ # <-- book0_ 은 변수 이름
[Book(super=BaseAuditingEntity(createdAt=2022-02-28T21:15:41.975665300, updatedAt=2022-02-28T21:15:41.975665300), id=1, name=My awesome book, category=null, authorId=null, publisher=Publisher(super=BaseAuditingEntity(createdAt=2022-02-28T21:15:42.003664900, updatedAt=2022-02-28T21:15:42.003664900), id=1, name=sneak campus!))]
Hibernate: 
    select 생략 from publisher publisher0_ # <-- publisher0_ 은 변수 이름
[Publisher(super=BaseAuditingEntity(createdAt=2022-02-28T21:15:42.003664900, updatedAt=2022-02-28T21:15:42.003664900), id=1, name=sneak campus!)]

JPA의 Persistence Context 에 Persist 할 때 연관된 객체를 같이 Persist 할 건지를 묻는 옵션입니다.

그렇다면 다른 cascade 옵션에 대해서도 손쉽게 짐작하실 수 있을 겁니다. JPA의 연산이 일어날 때 필드는 어떻게 할지를 말하는 거니까요.

기본값은 {} (아무것도 안 함, 빈 배열) 이구요.

  • ALL: 어떤 연산이든 같이 반영
  • PERSIST: 저장할 때
  • MERGE: 업데이트할 때 (저장)
  • REFRESH: 업데이트할 때 (로드)
  • DETACH: Persistent Context 에서 떼어버릴 때 연결된 객체도 같이 떼버립니다.
  • REMOVE: 삭제했을 때 (주의! 실수하기 쉬움! 다음 동영상에서 설명!)

각각에 대해 자세히 설명은 안 해주셨는데, 나중에 어디 공식 문서나 블로그같은 거라도 확인을 해봐야 할 것 같네요.

근데 ALL 에 {PERSIST, MERGE, REMOVE, REFRESH, DETACH} 가 다 포함되어있는데 이게 말이 되나요...? 아, detach 에 대해서 오해하고 있었나보네요. 어차피 연산이 이루어지는 대상은 필드를 가지고 있는 객체이고, 이 경우엔 Book 이 detach 될 때 Publisher 도 detach 할지를 정하는 거군요.

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.
패스트캠퍼스: https://bit.ly/37BpXiC

#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #한번에끝내는JavaSpring웹개발마스터초격차패키지Online

오늘은 꽤나 직관적으로 이해할 수 있는 내용이었습니다. 원래대로라면 자료를 찾아봤어야 하는데... 몸을 사려야 하는 상황인지라. 개인 프로젝트도 해야하구요. 좀만 더 방치하면 일주일을 넘고, 일주일을 넘으면 까먹어서 그대로 버려져요...