[Spring] CH19 스프링부트 레파지토리(저장소) - 테이블 연관관계와 Lazy, Eager - 2

jaegeunsong97·2023년 3월 30일
0

[Fast Campus] Spring

목록 보기
35/44
post-thumbnail

📕 1교시


숙제 리뷰

기본세팅

사용자가 3명

일단 복습 먼저

실행

board를 select하면서 바로 user를 join

Eager전략이라는 것, Lazy로 바꾸자

다시 실행

em.find(Board.class, 1)

만약 Eager
-> Board + User Outer join 해서 가져옴
-> Orm

만약 Lazy
-> Board 가져옴
-> getter(username)를 호출하면 select (조건: Board 영속화)
-> Orm

전략에는 2개가 있다 Eager, Lazy

만약 Board는 select하면

DB에서 바로 join Query를 날려서 당겨온다

그리고 ORM까지 한다.

1, 2 단계에서 DB로부터 가져오는 것은 Flat Data를 가져온다. 그것을 자바 객체(User user)에 ORM을 하는것이다.

이때 board와 user모두 영속화가 되어있다.

Lazy는 다르다. board만 들고온다. 물론 Flat하게 들고오겠지만, 영속화 된것은 board밖에 없다.

그 다음에 lazy loading을 한다.

이떄 user의 id를 제외한 나머지들을 getter로 소환할때만 lazy loading을 한다.

id는 board가 들고 있기 떄문에 따로 lazy loading을 할 필요가 없기 떄문이다.

마지막에 ORM을 하는 것은 똑같다.

이제 다시 코드로 가보자

여기서 중요하게 생각해야하는 것은 ORM을 해준다는 것이 매우 중요하다.

Lazy전략으로 실행을 하면

먼저 board select 하고

getusername을 했기 때문에

다음과 같은 결과가 나온다

만약 user_id를 조회하면?

board select하고 끝이 난다.

getUser까지는 lazyLoading을 하지 않는다.

Eager는 join을 하고 Lazy는 select 2번을 한다.

그림으로 한번 언제 Eager, Lazy를 사용하는 알아보자

이 상태에서 조회를 하면

여기서 보면 3개가 lazyLoading을 하는 것이 이득이다. 왜냐하면 board만 필요하기 떄문이다. 마지막꺼만 board와 user가 필요하다.

마지막꺼만 select 2번이 된다.

만약 Eager전략을 가면 낭비가 생긴다.

실무에서는 Lazy만 사용하기는 한다. 왜냐하면 Lazy가 안정적이다. 필요할때만 쿼리를 직접 작성해서 가져오는 것이다.

다시 과제로 돌아가서 BoardRepo에다가 코드를 추가하자

추가

boardRepo를 보면 sqlString이 다음과 같이 되어있다.

하이버네이트 기본 전략이 DB _ 기법을 사용하면 자바에는 카멜 표기법으로 매칭이 된다.

그리고 id를 받는다

이제 NativeQuery이기 때문에 Object Mapping을 하지 않는다.

따라서 직접 Object타입으로 받아줘야한다.

이렇게 받으면 이 데이터를 Flat Data라고 한다.

이것을 board로 옮기는 것이 ORM이다.

보면 user 부분은 user 객체를 만들어서 집어넣었다

너무 귀찮다.................... 그냥 em.find() 하면되는데

일단 테스트를 실행해보자

실제로 DB에서 들고올때는 Flat하게 들고온다. DB에는 ORM이 없다.

한번 ObjectMapper로 변환을 해보자

이것을 만약 Flat하게 받으면

FrontENd가 정말정말 싫어한다.

이제 default size를 하는 코드 대신에 직접 in query를 짜는 코드를 보자

findAll이니까 1건이 아니라 여러 row가 들어온다

그것을 조회를 하고 boardDto에 옮겨 담는다. 전부 담기고 나면 아마 다음과 같이 구성되어있을 것이다.

id만 따로 뽑아서 담는다.

쿼리를 날리는 코드를 만든다, 하지만 ?가 몇개인지 모르기 때문에 userIds.size()만큼 돈다

받아서 다음과 같이 작성한다.

내가 만약 defalutsize를 2로한다는 것은 어떤 의미일까?

사이즈가 바로 디폴트 패치 사이즈이다.

지금 이러한 상태이므로

OM을 해줘야한다. 마무리 하면된다.

이렇게 짜면 미친짓이다. 일단 해당 test를 실행 해보자

board selct 1번

그리고 그 결과를 통해서 in query를 ORM했다

따라서 이 코드를 사용하는 것이 가장 좋다. 이렇게 하면 알아서 ? 개수를 적어준다.




📕 2교시


이번에는 join을 직접 해보자

이제는 Lazy or Eager 전략이 중요하지 않다. 이유는 직접 join을 할 것이기 때문이다.

기존에 findAll()은 다음과 같이 적었다.

board select를 하고, N + 1문제 생겨서 default batch size를 설정을 해줬다.

이렇게 적는 것은 JPQL을 적는 것이다.

이떄 Board의 b를 레퍼런스의 변수로 보면되는 것이다. 그러면 b.user는 board의 user를 의미하는 것이다.

select b from Board b join b.user는 어떻게 나오지는지 확인을 해보자

실행

inner join을 했다. 하지만 projection을 하지는 않았다.

즉, user를 join은 했지만, projection은 하지 않은 것이다.

하이버네이트가 관리하고있는 board는 user를 들고있지 않는다. 왜냐하면 user를 projection을 하지 않았기 때문이다.

그래서 user 쿼리를 3번 날린다.

즉, findAllJoin()은 실행을 해봤자 의미가 없다

그래서 여기다가 fetch를 적어줘야한다. fetch는 user에 연관된 field를 projection할때 사용한다.

한번 실행을 해보자

한방 쿼리로 보낸다

board에서 user를 projection하는게 fetch다. 즉, 연관된 애들을 projection해서 fetch하라는 것이다.

in query를 사용할 필요가 없고 fetch를 사용하면 된다.

쿼리가 총 3가지가 있다

원래 findAll()하면 select * from board를 1번하고 user select를 3번을 했다.

정리하면

  • select b from Board b -> user select 3번
  • select b from Board b join b.user -> user select 3번(user가 fetch 되지 않았기 떄문에)
  • select b from Board b join b.user + defalut fetch size = 100-> user select 1번(in query 발동, board select 1번 user select 1번 총 2번)
  • select b from Board b join fetch b.user -> 1발 쿼리(board + user select 1번)

따라서 Lazy로 걸고 한방 쿼리를 작성해주면 되는 것이다.

한번 이번에는 findById를 fetch를 사용해서 만들어보자

이 쿼리가 날라가면 Lazy는 전혀 상관이 없다 그냥 fetch는 신경쓰지 않는다.

그래서 실무에서는 Lazy로 고정을 하고, 내가 필요한 것은 join fetch를 통해서 당겨온다.

한방에 join을 한다.

엄청나게 복잡한 통계쿼리경우는
1) QueryDSL -> ORM O
2) NativeQuery 직접 -> ORM X(직접해야 한다)

Lazy와 Eager의 차이는?

Eager은 join해서 가져오는 것, Lazy는 지연로딩을 할 때

이제 level3를 해보자

Reply는 user, board를 FK로 들고있다

ReplyRepo 기본

BoardRespDto

Board 주석 해제

보면 onetoMany이다. 따라서 FK를 들고 있지 않는다.

Reply가 FK를 들고 있다.

Board는 Reply한테 연관관계의 주인이 아니다.

다음과 같이 실행하면 서버가 터진다. 이유는 replyList를 column으로 만들어내지 못한다.

DB에는 List를 만들지 못한다.

그래서 하이버네이트에게 replyList는 연관관계의 주인이 아니니까, 테이블 만들지 말라고 알려줘야한다

따라서 연관관계 주인의 변수이름을 알려줘야한다.

여기까지 적으면 '양방향 Mapping'이다. 이것을 왜 사용할까?

이전에 EAGER인 경우는 board를 당기면 user가 join되어서 왔다.

이것또한 마찬가지로 board를 조회하면 board에 있는 모든 Reply를 조회해서 가져오겠다는 것이다.

이거는 FK의 용도가 아니다.즉, board를 조회하면 해당 board의 모든 Reply를 가져오겠다는 것이다.

화면을 보면 보통 게시글이 있다. 화면에는 전부 하이퍼링크가 걸려있을 것이다.

하이퍼링크를 클릭하면 게시글 상세보기로 이동한다.

게시글 상세보기를 보면 작성자명, 제목, 내용1 밑에는 댓글 리스트가 있다. 그리고 댓글의 작성자도 있을 것이다.

내가 만약 board만 select하면 전부 select해서 가져오면 얼마나 편할까?

보면 board select -> 내용 select + reply select

이거를 한방에 조회하겠다는 것이고, 이게 바로 '양방향 mapping'이다.

다음과 같이 하기위해서는 2가지 전략이 있다.

1) 양방향 mapping
2) board select -> user 자동 join -> 그리고 reply 를 select (select 2번 전략)

즉, board select를 하면 다음과 같이 되고

reply select

select 2번을 사용하는게 편하다

OnetoMany -> Lazy(defalut)
ManyToOne -> Eager(defalut)

이제 level3test를 보자

일단 board 해당 부분을 주석 처리하자

Reply

test

user 3건, 게시글 3건

이제 나는 reply를 3건 만들것이다.

원래는 이렇게 Reply 객체를 넣어야 한다.

Reply의 생성자 변화

MyDummyEntity

댓글을 만들고 싶으면 user, board의 FK를 넣으면 된다.

다시 level3로 와서

즉, Reply에 ssarPS와 cosBoardPS가 ORM되어 있는 것이다.

이 부분만 test를 해보자

그러면 user3건과 board 3건 전부 save가 되고, reply1건도 save가 될것이다.

insert가 총 7건이 될 것이다.

원래는 이렇게 Reply를 save해야한다. 즉, user, board 를 전부 PS시키고 그것을 reply에 넣고 replyPS로 만들어야 한다는 것이다.

하지만 양방향 mapping을 하게 되면, 이렇게 넣을 필요가 없다.

board 주석 해제

컬렉션 1건 추가, 이렇게 한다고 insert는 되지 않는다. 그냥 replyList에 1건이 추가된 것이다.

newReply를 보면 board는 들어있지 않다.

1건이 채워지고

reply.synboard -> 자기의 게시글을 reply에 채워넣어주는 것

reply.synboard 주석해제

그림으로 보자

board 객체

영속화되어있는 것

자기가 쓴 board에 본인이 적었다. 인스타를 생각하자

board 안에는 reply 1건도 없다.

board 에다가 reply 1건을 추가

ssar이라는 사람이 자기 board에 Reply를 단 것이다. 아직 DB에 들어간 것은 아니고, 객체를 만들어서 replyList에 추가한 것이다.

결국 개념은 내가 비영속 객체를 1개 만들어서 1건을 replyList에 add 한 것이다.

그러면 결국 PC안에 안들어간다. 왜냐하면 영속성 컨텍스트에 들어갈 수 없기 떄문이다. 컬렉션이기 떄문에!!

즉, 아래와 같은 상태로 되어있기는 하나, Board와 user는 영속화가 되어있지만, ReplyList는 리스트이기 떄문에 DB에는 존재할수 없어 영속화가 되어있지 않다

따라서 실행을 해도 reply 대해서는 어떠한 쿼리도 나오지 않는다.

실행을 해보자 user 1건 board 1건

이것을 해결하기 위해서는, Board에 영속성 전이라는 것을 걸어주자

조건은 Board가 영속성이 되어있어야 영속성전이가 된다. 쉽게, 바이러스에 전염된다고 생각하면된다.

이렇게 되면 flush될떄 같이 insert가 된다.

정리하면, reply를 직접 영속화 시키지않아도 board가 영속화가 되어있고 영속성 전이를 가지고있으면 flush 될떄 같이 insert가 된다.

그림으로 보면

비영속인 reply를 board에다가 넣어도 영속화가 되지는 않는다.

add 하는 순간 영속화가 된다.

만약 sync를 하지 않으면 어떻게 될까?

이런 상태인 것이다. 자기의 게시글 board를 가지고 있지 않은 것이다.

실행

내부의 board 값을 보자

실행

이부분에서 null이 떴다. 즉, board의 값을 못받은 것이다.

그래서 양방향 mapping을 하는 것이다. 그렇지 않으면 한쪽 방향은 고아가 되기 떄문이다.

이런 고아가 생기면 데이터가 망가질 수도 있다. 그래서 고아가 생기면 자동으로 삭제하라는 것을 적어줘야한다.

고아가 되면 무결성이 다꺠진것이기 때문이다.

다시 정리해보자

mappedBy를 적는 이유는 관계의 주인이 아니라는 것을 알려주기위해서이다. 그리고 board는 변수의 이름값을 넣어줘야한다.

fetch의 기본 전략은 Lazy이지만 우리는 나중에 Lazy만 사용할 것이다. 그리고 내가 필요하면 직접 join fetch를 할 것이다. 그래야 쿼리가 성능이 좋다.

CascadeType는 영속성 전이를 해준다. replyList에 값이 들어오면 전파를 해준다. 조건은 Board도 영속성 되야한다.

orphanRomoval 은 고아를 자동 삭제해주는 것

sync 가 없으면 Reply에는 board가 없는 상태가 되니까(Replt가 board를 참조할 수 없는 상태 == 고아)

이제 가장 중요한 양방향 mapping을 왜 할까?

OOP관점으로 board 1개로 모든 것을 다 할 수 있다.

내가 board1개를 작성하고 싶으면 user1명이 있어야 되고, 댓글을 달고 싶으면 board에 reply를 넣고 영속성 전이만 해주면 되기 때문이다.

이렇게 양방향 mapping을 하지 않으면 정방향으로 mapping을 해야한다.

정방향으로 mapping을 한다는 것은 reply 객체를 직접 만들어야 한다는 것이다.

물론 장단점이 있다. 양방향 Mapiing은 프로그램이 매우 복잡해진다. 고아라는 개념, 영속성전이, 등등 알아야 할게 엄청 많다.

따라서 select를 2번 날리는게 좋다.




📕 3교시


인스타그램을 생각해보자

이효리가 insert를 하면 100개 이상의 select를 한다.

인스타에는 사진이 메인 테이블이 되고 나머지는 연관관계가 된다.

이게 이제 양방향 mapping이다.

reply(N) -> OneToMany
tag(N) -> OneToMany
user(1) - follow(N)

양방향 mapping

결국 RDB는 photo 1개를 select해서 끝내지 못한다.

join을 한다는 것

내가 join을 하면

select * from board inner join user

여기서 board를 드라이빙 테이블
user를 드리븐 테이블 이라고 한다.

먼저 select할 떄는 from 절을 퍼올린다. 즉, board를 전부 퍼올리는 것이다. 풀스캔을 해서 게시글 1 2 3 을 전부 퍼올린다.

그리고 u1을 찾는다. 한방에 찾았다. 조회를 멈춘다. 그리고 u2가 조회를 다시 한다.

찾은 데이터는 따로 뺴놓는다. 그리고 다음에 찾는 것들을 옆에다가 join을 하는 것이다.

즉, board를 플 스캔하고, 드리븐 테이블을 인덱스로 접근해서 빠르게 값들을 찾고, 찾은 것들은 따로 저장을 한뒤에 찾을 때마다 옆에다가 join을 하는 것이다.

만약 드라이빙 테이블과 드리븐 테이블의 위치가 반대면 어떻게 될까?

드리븐 테이블을 찾아도, 게시글의 u1은 FK이기 떄문에 중복이 가능하다. 따라서 계속해서 찾게 된다.

NoSQL은 모든 테이블을 한번에 때려 넣는다.

즉, 중복되서 데이터를 넣는 것이다.

정리하자

이제 테스트를 해보자

물론 정방향으로 Reply를 영속화를 시켜서 할 수도 있다.

실행을 하면 user는 Eager 이니까 join해서 가져올 것이다.

하지만 reply는 들고오지 않았다.

2번쨰 테스트를 보자

한방에 다 가져온다.

그리고 ORM까지 다 되어있을 것이다.

이번에는 findByIdTwoWayJsonFailTest를 보자

아마 실행이 되면 board와 user 쿼리만 날라갈 것 이다.

여기에 Reply에는 값이 없을 것이다.

실행을 하면, 먼저 예상이 가능한 select를 하고

그리고 이 객체를 JSON으로 변경하면 MessageConverter가 발동하면서 터지게 된다.

왜냐하면 결국 Json으로 파싱하기위해서 messageconverter가 getter를 떄린다. board에 있는 모든 변수들을 getter를 떄리게 된다.

그중에 replyList의 내부도 다 꺼내서 getter를 떄리기 때문이다.

그러면 Reply안에는 Board가 다시 있고, 다시 Board를 호출하고 Board에는 Reply가 있고, 다시 .... 이게 무한 반복이 되어서 이렇게 파싱하다가 stackoverflow가 발생하는 것이다.

이것을 양방향 순환참조 오류라고 한다.

그래서 Messageconverter가 발동하지 않으면 터지지 않는다. 실행해보자

이렇게 되면 controller 단에서 전부 터지게 된다.

이것을 해결하기 위해서는 다음 방법이 있다.

위 코드는 messageconverter가 board를 Json으로 파싱할 떄 @JsonIgnore 부분만 무시하고 파싱을 한다는 것이다.

따라서 터지지 않는다!!

reply는 없을 것이다.

근데 만약 내가 reply를 들고 오고 싶으면 어떻게 해야할까?

lazy loading을 하면 된다.

위 코드는 Reply도 파싱하지만, Reply properties 중 board만 무시하고 나머지를 파싱한다는 것이다.

reply중에서 board만 제외시키라는 것이다.

만약 user도 제외시키고 싶으면

원래 Entity를 외부에 노출을 잘 시키지 않는다.

Dto 쓰지 말고, 원하는 데이터만 Entity로 응답을 하자

따라서 dto를 만드는 이유는 1) 무한참조 문제 떄문에 2) 화면에 필요한 데이터는 화면마다 다르다, 따라서 entity하나로 처리는 힘들다.

마지막 테스트를 해보자

boadPS에는 reply는 들어가지 않았다.

필요한 것만 get을 사용해서 옮긴다.

reply도 필요한 것만 직접 get을 해서 떄린다.

여기서 핵심은 BoardRespDto를 Dto로 만들어서 Json을 가져오면 어떠한 이점이 있을까?

내가 영속화되어있는 것을 전부 DTO에 옮겼다. 옮기고 나서 mmessageconverter가 일어나면 lazyloading이 일어나지 않는다.

영속화 되어있는 것을 Dto로 바꾸면, 나중에 최종적으로는 controller에서 client로 응답될 떄 무조건 messageConverter가 발동한다. Json으로 돌려줘야된니까

그떄 lazy loading이 일어나지 않는다.

왜냐하면 boardRespDto는 영속화된 Entity가 아니기 때문이다.

즉, Dto로 바꾸기만 하면 모든 문제를 해결할 수 있다.

간단하게 말하면 boardPS를 boardRespDto로 옮긴것 뿐이다.

실행하면

최종적으로 정리하면 Dto를 만들면 내가 필요한 것만 뽑아서 가져올 수 있다.

또한 장점은 이름을 바꿀 수 있다.

다시 실행 해보자

Entity는 이렇게 이름들을 변경하지 못한다. 이미 정해져 있기 떄문에

여기서 핵심은 Entity를 messageconverter한테 넘기면 messageconverter가 lazy loading하다가 순환참조때문에 무한참조가 일어날 수 있다.

그렇기 떄문에 하이버네이트로 관리되고 있는 것들은 바로 controller로 넘기지 않고 dto로 바꿔서 넘긴다.

물론 넘긴다고 해도 다음과 같은 방식으로 하면 터지지 않는다.

detach or clear

즉 영속성 컨테이너에서 꺼내기만 하면 lazy loading이 일어나지 않는다.

remove 하면 null execpt

그림을 보자

PC에서 reply가 없으면 결국에는 controller에서 JSON으로 바꾸면서 다 터지게 된다.

그래서 controller 직전에 OSIV를 이용해서 꺼내면 된다. 매우 쉽게 해결할 수 있다.

수업 끝

profile
블로그 이전 : https://medium.com/@jaegeunsong97

0개의 댓글