spring을 공부하며 Jpa를 자연스레 접하게 되었는데 단어만 아는 수준으로 넘어갔던 영속성 컨텍스트. cascade가 등장하고 연관 관계의 주인과 헷갈리게 되었고 차이점을 알아보다 보니 jpa에 대해서 아는 게 너무 없었다. 그래서 영속성 컨텍스트와 연관 관계 주인을 이해할 때까지 , 의문점이 사라질 때까지 직접 코드를 작성하며 해결하였다. 강의에서 나오는 것을 그대로를 설명하는 것이 절대 아니어서 도움이 많이 될 것 같은 글이다. 개념 설명하고 코드로 다시 한번 설명하겠다. 코드 부분이 진국이라고 생각한다. 바로 시작하겠다. (너무 기본적인 것은 생략한다.)
Jpa를 사용하면 DB와 spring 사이에 영속성 컨텍스트라는 것이 생긴다.
영속성 컨텍스트
1차 캐시, 스냅샷, 쓰기 지연 SQL 저장소. 영속성 컨텍스트에 있는 공간이다.
1차 캐시에 엔티티가 올라가 있으면 영속 상태라고 한다.(1차 캐시는 엔티티를 관리한다. 연관관계 같은 정보는 모른다!!!!! 연관 관계는 DB가 신경 쓰는 것이다.) 1차 캐시에 있으면 (영속 상태면) 그 이후로는 entity의 값을 변경해 주면영속성 컨텍스트에서도 값이 변경된다(flush라는 요청이 있기 전까지는 1차 캐시에서 값을 변경하고 있는 것이다. db에 반영 X). db에도 없는 상태일 때 (완전 처음) persist()를 이용해서 1차 캐시에 등록한다. 또는 조회 요청을 했을 때 1차 캐시에서 우선 찾고 없으면 db에서 찾아와서 1차 캐시에 저장 후 조회 결과를 보여준다. 이때! 1차 캐시에 처음 올라갈 때 스냅샷이 생긴다. 이거 정말 이렇게 자세히 알려주는 곳 없다. 맨 처음 1차 캐시에 등록될 때의 엔티티의 값들을 따로 저장해 주는 것이다.(기억해 두자 코드 리뷰에서 기똥찬 장면이 나온다.)
쓰기 지연 SQL 저장소 이것에 대한 설명은 여러 군데 잘 나온다. 테이블이 생성되고 테이블에 등록하고 수정하고 등등 모든 SQL 쿼리문은 우리가 작성한 코드를 읽었을 때 바로 DB로 날아가지 않는다. 쓰기 지연 저장소에 차곡차곡 정리해 둔다. 이 말은? 바로 DB에 반영되지 않는 것이다. 쿼리가 DB에 입력되어야 DB는 일을 한다. 그렇다면 언제 쿼리가 DB로 이동할까. flush(), commit()가 실행될 때이다. 여기서도 어려웠는데 사실 commit()을 하면 flush()가 실행된다. 우선 flush()가 실행되면 dirty checking을 한다. dirty checking은 1차 캐시의 엔티티의 상태와 스냅샷을 비교한다. 다르다면 엔티티에 수정이 일어난 것이니까 update 쿼리를 만들어준다! 이때 1차 캐시는 사라지지 않는다. 그대로 유지한다. 다시 update 쿼리를 바로 db에..... 주지 않죠 아까 말했던 쓰기 지연 SQL 저장소에 담아둔다. 그 이후 지금까지 쌓아둔 쿼리를 db로 드디어 날려준다.
정리 flush()는 dirty checking, 쿼리문 db로 보내기 (드디어 이때 db에 우리가 입력한 값이 반영되는 것이다. 그전까지는 1차 캐시 안에서만 값을 컨트롤한 것이다.). 방금 말했듯이 commit이 일어나면 flush가 실행된다. 차이는 flush는 rollback이 가능하다. commit은 rollback 불가능(transaction이 완전히 끝났다는 것을 의미하는 것이 commit이다. 이 부분이 이해 안 되면 db transaction, commit, rollback을 공부해 보자.) 테스트에 @Transactional을 작성하거나 repository에 JpaRepository를 사용했다면 commit은 동작이 끝나고 자동으로 실행된다.
이 글을 다 읽었다면 코드를 꼭 봐주기 바란다. 이해가 한 번에 쏙쏙 일 것이다..
연관 관계 주인
연관 관계 주인에서 값을 변경하여야 한다. 이때 DB관점, 객체 관점을 나눠서 이해해야 한다. 연관 관계 주인 자체가 생소하면
대망의 코드 리뷰다. 시행착오가 많았지만 덕분에 뿌듯하고 이해가 많이 되었다.
기본 베이스는 Order, Member의 다 : 1 관계이다. 당연히 다 인 Order가 연관관계 주인이고 두 엔티티 모두 id, name 필드만 작성하였고 (각각 Member meber, List <Order> orders 포함 ) 양방향 관계(List <Order> orders에 mappedby작성)로 매핑하였다.
@Test()
public void mappingTest1() throws Exception {
Order order = new Order(); //연관 관계 주인!!!!!
Member member = new Member();
order.setName("cofee");
member.setName("teho");
em.persist(order);
em.persist(member);
//영속성 컨텍스트에 등록!
member.getOrders().add(order); //연관 관계 주인 아닌 곳에서 세팅
//주인아닌 곳에 해서 Order에 member_id(FK) 매핑 안돼!
}
연관 관계 주인인 Order에서 매핑을 하지 않아서 사진과 같이 매핑이 되지 않고 null로 표현되었다.
@Test
public void mappingTest2() throws Exception {
Member member = new Member();
Order order = new Order();
member.setName("teho");
order.setName("cofee");
em.persist(member);
em.persist(order);
order.setMember(member); //연관 관계 주인에서 매핑!
em.flush(); //db에 쿼리 날려서 반영 (1차 캐시 비우는 기능 없음.)
em.clear(); //1차 캐시 비워줌
Member member1 = em.find(Member.class, member.getId()); //1차 캐시 비웠으니 db에서 값 찾아온다!!
System.out.println("============================================");
System.out.println("order.getMember().getName() = " + order.getMember().getName());
System.out.println("member.getOrders().size() = " + member.getOrders().size());
System.out.println("member1.getOrders().size() = " + member1.getOrders().size());
System.out.println("============================================");
//연관관계 매핑 됨! 근데 객체 입장에서 볼때 member에 orders가 세팅안돼
//member.getOrders().add(order)까지 해주는게 정석.
}
위와 같이 작성해야 성공적으로 매핑이 된다. 여기서 문제. 출력은 어떻게 나올까? 순서대로 teho, 0, 1 나온다. 일단 order에서 매핑해줬으므로 연관관계 매핑은 성공이다. 그다음 flush로 db에 반영해 주고(이때 member.orders에 값이 들어가는 것이다. 1차 캐시에서는 연관관계고 매핑이고 신경 안 쓴다. 엔티티만 관리해주는 것이다. 그래서 위의 코드에서 flush, clear 안 해주면 member.orders의 크기 0으로 나온다.) clear로 1차 캐시를 비운다.(clear 안해주면 member1 찾을 때 엔티티 그 자체 즉 db에 반영 안 돼서 또 member.orders 크기 0 나온다. 개념 완벽히 이해하면 이해가 될 것이다.) 그리고 member1을 찾으려 한다. 1차 캐시 먼저 봤는데 아까 지워서 없으니까 db에서 꺼내온 후 1차 캐시에 등록한다.
여기서 집중! member.getOrders(). size()의 값은????? 0이다. 왜냐면 이건 순수 객체이다. order.setMember(member)만 해주었지 member에는 orders를 설정하지 않았다. 그래서 객체와 테이블을 일치시키기 위해서 order.setMember(member), member.getOrders(). add(order) 둘 다 작성해 주는 것을 권장한다.
@Test
public void mappingTest3() throws Exception {
Order order = em.find(Order.class, 2);//1차 캐시 저장 order.name=coke로 가정 이때 값으로 스냅샷!!!
System.out.println("order.getName() = " + order.getName());//
order.setName("coffee");
Order order1 = em.find(Order.class, 2);
System.out.println("===================================");
System.out.println("order1.getName() = " + order1.getName());//coffee라고 나올거
order.setName("coke");
System.out.println("order.getName() = " + order.getName());
// db에서 꺼내간값이 a 일때 a->c->a하고 commit되면 update쿼리안나감 스냅샷이랑 비교했을때 같아서
// dirty checking 해도 차이없어 대박대박
}
db에 이름 coke, id 값이 2인 데이터가 있다고 가정한다. 이때 find 할 때 1차 캐시에 이 값 그대로 등록된다. 그래서 이름을 coffee로 바꾸고 다시 coke로 바꾸면 스냅샷과 1차 캐시에 있는 이름이 coke로 같기에 update쿼리가 날아가지 않는다.
맨 처음 1차 캐시에 올라올 때의 값을 스냅샷으로 저장한다. (아까 말한 기똥찬 장면이다 ㅎ..)
결론: 연관관계 매핑할 때 객체와 테이블 관점을 따로 생각해야 한다. 영속성 컨텍스트는 JPA를 이해하기 위해 정말 중요하다.
모르겠으면 직접 테스트를 해보자. 구석구석 가려운 부분 다 긁을 수 있다.
(cascade는 persist를 전파하는 것을 조절한다. persist(order), persist(member) 둘 중 하나만 쓸 수 있게... 여러 옵션이 있으나 생략. cascade와 연관관계 주인이 하는 행동이 결국 주인을 중심으로 코드를 쓰면 따라온다는 것 아닌가?라는 생각으로 헷갈렸었는데 공부하면서 아예 다른 개념인걸 알게 되었다.).
'Spring' 카테고리의 다른 글
@ModelAttribute, @SessionAttribute name 속성 차이 (0) | 2023.09.26 |
---|---|
[Spring] 나만 보는 스프링 사전(스프링 어노테이션) (0) | 2023.06.30 |
[Spring] JPA란? (간단정리) (0) | 2023.02.04 |
[Spring] @JoinColumn & 연관관계 맵핑 @Mappedby (외래키 주인? 연관관계 주인?) (1) | 2023.01.13 |
[Spring] Gradle vs Maven (빌드? 빌드 도구?) (0) | 2022.12.29 |