[Springboot] 19. 댓글 시작 ( 양방향 매핑, 인라인뷰 )

김호정's avatar
Sep 13, 2024
[Springboot] 19. 댓글 시작 ( 양방향 매핑, 인라인뷰 )
 
 
댓글은 어디에 저장되어야 할까 ?
 
댓글은 어디에 속할까 ? 유저 ? 보드 ?
 
유저 / 보드 →
 
댓글을 어디다 쓰느냐에 따라서 다르다!
 
지금 게시글 안에 쓸거니까 (게시글이랑 독립적이지 않으니까 ) → 보드에 속한다.
 
💡
최소 3개의 데이터가 필요하면 스칼라가 아니라 오브젝트! 댓글 작성자명, 댓글, 댓글작성일… 등 최소 3개의 데이터 필요 → 1개의 데이터가 아니니까 무조건 테이블을 따로 만들어야한다. → 오브젝트니까! 1개의 데이터만 필요하면
 
 
1 제목1 내용1 3(love) 글잘적었네, 못적었네, 굳(→댓글)
 
1정규화 → 컬럼을 , 해서 적을 수 없다는 거. (원자성)
 
1정규화 , 2 정규화, 3 정규화 하는 것보다 아래걸로 하는게 쉬움!
 
  1. 테이블을 쪼개는 근거
    1. (1) 내가 필드를 추가하고 싶은데, 오브젝트로 표현해야 할떄(스칼라로 표현이 안될때)
      → 테이블 쪼개기
      예) 댓글번호, 댓글내용, 댓글시간, 댓글주인, 댓글게시글번호 → 오브젝트
      Reply {
      r_num
      content
      created_at
      username
      board_id
      }
      → 이렇게 Reply 오브젝트 로 표현해야할 때 테이블을 쪼갠다.
       
      (2) 내가 필드를 추가하고 싶은데, 컬렉션으로 표현해야할 때 → 테이블 쪼개기
      즉, 같은 타입인데 여러개를 적어야할 때 !
      → 예 ) 1번 게시글에 댓글1 말고도 다른 댓글2, 3, 4 …. 도 들어와야 한다.
       
      1번 게시글 - 댓글1, 댓글2, 댓글3
      → 이렇게 컬렉션으로 표현해야 할때 테이블을 쪼갠다.
       
      ⇒ 지금 만드는 댓글은 오브젝트로 표현해야 하는 경우 + 컬렉션으로 표현해야 하는경우 둘다에 해당한다 → 무조건 테이블 쪼개야함
       
  1. 연관관계
    1. 지금 유저, 게시글, 댓글이 있는데 서로의 관계를 봐야한다.
       
      1대 N 이런건 “다른 테이블에 자기가 행을 몇개나 만들 수 있느냐”에 따라 정해진다.
      유저(1)(1) 게시글(N)(1) → 1 대 N ( 유저는 게시글 테이블에 행을 여러개 만들 수 있으니 1대 N이고
      게시글은 유저 테이블에 자기 행을 1개밖에 못만드니 1대 1이다 → 1 : N )
      유저(1)(1) 댓글(N)(1) → 1대 N
      게시글(1)(1) 댓글(N)(1) → 1대 N ( 게시글은 댓글 테이블에 여러행을 만들 수 있으니 1대N이고
      댓글은 게시글 테이블에 댓글행을 1개씩 밖에 못만드니 1대1 → 1 : N )
       
       
      N쪽에 fk랑 driving table이 있어야 하니,
      댓글(N)
       
      ⇒ PK, user_id, board_id, comment(댓글내용), created_at(등록일)
       
      이런식으로 테이블을 설계한다!
      notion image
       
      우리 프로젝트로 생각하면
       
      영화
      영화관(CGV_부산대연)
      상영관(1관, 2관, 3관)
      상영시간(10시, 11시, 12시)
      고객
       
      영화(1)(N) 영화관(N)(1) → N대 N
       
      영화를 배급한다. (행위) → N : N은 중간에 무조건 행위테이블이 나온다.
       
      어떤 영화관이 어떤 영화를 들고있는지 배급 테이블이 필요해진다.
       
      배급 테이블
      1 영화_ID, 영화관_ID
영화관(1)(1) 상영관(N)(1) → 1대N
 
 

 
Board 테이블안에 reply 패키지를 만듦
→ board를 조회하면 join해서 댓글을 상세페이지에 뿌리면 되니까 따로 C S R 이 필요없음
 
/board/1/reply/2
게시글 몇번에 댓글 몇번 삭제가 더 쉽게 와닿는다!
 
그래서 BOARD안에 REPLY 패키지를 만들어놓고
REPLY 엔티티, 레파지토리만 만들고 서비스랑 컨트롤러 안 만드는 회사가 있다.
게시글 상세보기 하는 비즈니스 안에서 댓글이 필요한 거니까!
(boardController랑 Service에서 사용)
 
notion image
 
위처럼 만드는데 우리는 일단 이렇게 안하고 ( → 어려울 수 있으니) 기존 방식대로 한다.
 
shop.mtcoding.springv3 패키지 아래에 reply 패키지를 생성하고
Reply 엔티티를 만들어준다.
notion image
notion image
reply 엔티티
@NoArgsConstructor @Entity // DB에서 조회하면 자동 매핑이됨 @Getter @Setter @Table(name = "reply_tb") // 테이블 명 설정해주기 public class Reply { @GeneratedValue(strategy = GenerationType.IDENTITY) @Id // PK 설정 private Integer id; private String comment; // 댓글 내용 @ManyToOne(fetch = FetchType.LAZY) private Board board; @JsonIgnoreProperties({"password"}) @ManyToOne(fetch = FetchType.LAZY) // 이걸 안적으면 하이버네이트가 오브젝트로 인식. 이걸 적어줘야 아 fk구나 하고 이해한다. private User user; @CreationTimestamp // em.persist 할때만 발동. 네이티브 쿼리 쓰면 발동안한다. 네이티브 쿼리쓰면 now()라고 내가 넣어줘야한다. private Timestamp createdAt; }
 
++
name = “user_tb” 테이블 이름에 _tb 왠만하면 붙여주기.
_tb 안붙이면 안만들어질 때가 있다.
그렇다고 user 테이블 member 라고 만들지 말기
user_tb 로 만들고 users 이렇게 임의로 수정해서 만들지 말기!
컨벤션을 지키자 : )
 
 
notion image
이렇게 만들어짐!
 
  • fk 설정 → 마이바티스에서는 board_id 이렇게 넣는데 JPA는 객체를 넣는다
→ Board, User
  • @ManyToOne 안 걸어주면 하이버네이트가 오브젝트로 인식해서 어떻게 테이블 만들어라는 거야 하는데 걸어주면 fk구나 하고 알게된다.
 

양방향 매핑

상세보기를 할때 게시글 , 유저, 댓글이 같이 나와야하는데
셋을 조인하기위해 만들 mFindByIdWithReply 매서드를 사용해서 값을 다 들고와도ㅛ
같이 담길 오브젝트가 없음
 
비즈니스적으로 필요한 데이터가 있을때는 반대방향으로 한번 걸어줘야 한다.
 
양방향 매핑!
 
notion image
 
Board 에 replies를 추가하고 관계설정하고 mappedBy도 써준다.
→ mappedBy 안해주면 이걸 fk로 인식하기 때문에 mappedBy 까지 적어줘야 한다.
→ mappedBy 를 적어줌으로써 List<Reply> replies는 “ 난 fk가 아니야. Reply의 “board”가 fk 야” 라고 알려줄 수 있음
 
 
만약 내가 조회를 2번 실행할꺼면 이렇게 할 필요는 없다.
board & user 조인해서 1번 들고오고 (select), reply 1번 들고오면 되는데 (select)
양방향매핑으로 한꺼번에 (1번만 select) 들고오려면 이렇게 설정해줘야 한다.
 
서버 재실행해주면 Reply 테이블이 잘 만들어지는 걸 확인할 수 있다.

 
@Builder 어노테이션은 왜 Class 위에 안달고 생성자에 달까?
 
builder를 클래스위에 달려면 @allArgsContructor 가 필요하다
 
근데 빌더패턴은 컬렉션을 포함시키지 못한다.
→ class에 @Builder 를 달 경우에 지역필드에 있는 List<Reply> 에서 에러남.
notion image
List 컬렉션
List 컬렉션
그래서 클래스에 빌더 어노테이션을 붙이면 돌다가 터질수있다.
 
빌더패턴안에 컬렉션 빼주기 !
 
양방향 매핑때문에 컬렉션이 생성자 안에 들어가서 에러가나는데 이럼 디버깅이 안된다 ㅎ
 
→ 양방향 매핑을 하려면 Board에 List<Reply> replies를 추가해 줘야 하는데
( fk 아님 . 양방향 매핑 해줘서 테이블 생성 시 column 으로 생성되지도 않음. )
class Board 위에 빌더 어노테이션 붙이면 매개변수로 저 컬렉션도 들어가니까
따로 생성자 만들어서 붙여주기 ;)
 
이안에 위의 REPLY 컬렉션이 들어가면 안돼!
이안에 위의 REPLY 컬렉션이 들어가면 안돼!
  • 컬렉션 들어가면 안돼
  • User는 fk
 
이제 댓글 추가해보자!
 
제일 먼저 화면 만들고 더미 만들기! 화면이 없으면 아무것도 못함
 
 
detail.mustache 에 댓글 추가
<!-- 댓글 --> <div class="card mt-3"> <!-- 댓글등록 --> <div class="card-body"> <form action="/reply/save" method="post"> <textarea class="form-control" rows="2" name="comment"></textarea> <div class="d-flex justify-content-end"> <button type="submit" class="btn btn-outline-primary mt-1">댓글등록</button> </div> </form> </div> <!-- 댓글목록 --> <div class="card-footer"> <b>댓글리스트</b> </div> <div class="list-group"> <!-- 댓글아이템 --> <div class="list-group-item d-flex justify-content-between align-items-center"> <div class="d-flex"> <div class="px-1 me-1 bg-primary text-white rounded">ssar</div> <div>댓글1</div> </div> <form action="#" method="post"> <button class="btn">🗑</button> </form> </div> </div> </div>
 
detail.mustache 전체코드
{{>layout/header}} <div class="container p-5"> <!-- 수정삭제버튼 --> {{#model.isOwner}} <div class="d-flex justify-content-end"> <a href="/api/board/{{model.id}}/update-form" class="btn btn-warning me-1">수정</a> <form action="/api/board/{{model.id}}/delete" method="post"> <button class="btn btn-danger">삭제</button> </form> </div> {{/model.isOwner}} <div class="d-flex justify-content-end"> <b>작성자</b> : {{model.username}} </div> <!-- 게시글내용 --> <div> <h2><b>{{model.title}}</b></h2> <hr/> <div class="m-4 p-2"> {{{model.content}}} </div> </div> <!-- 댓글 --> <div class="card mt-3"> <!-- 댓글등록 --> <div class="card-body"> <form action="/reply/save" method="post"> <textarea class="form-control" rows="2" name="comment"></textarea> <div class="d-flex justify-content-end"> <button type="submit" class="btn btn-outline-primary mt-1">댓글등록</button> </div> </form> </div> <!-- 댓글목록 --> <div class="card-footer"> <b>댓글리스트</b> </div> <div class="list-group"> <!-- 댓글아이템 --> <div class="list-group-item d-flex justify-content-between align-items-center"> <div class="d-flex"> <div class="px-1 me-1 bg-primary text-white rounded">ssar</div> <div>댓글1</div> </div> <form action="#" method="post"> <button class="btn">🗑</button> </form> </div> </div> </div> </div> {{>layout/footer}}
 
 
insert into reply_tb(comment, board_id, user_id, created_at) values('댓글1', 5, 1, now()); insert into reply_tb(comment, board_id, user_id, created_at) values('댓글2', 5, 1, now()); insert into reply_tb(comment, board_id, user_id, created_at) values('댓글3', 5, 2, now()); insert into reply_tb(comment, board_id, user_id, created_at) values('댓글4', 4, 2, now()); insert into reply_tb(comment, board_id, user_id, created_at) values('댓글5', 3, 2, now());
 
댓글 더미 데이터 추가
게시글 5 에 댓글 3개, 게시글 4에 댓글 1개, 게시글 3에 댓글 1개
 
insert into user_tb(username, password, email, created_at) values ('ssar', '1234', 'ssar@nate.com', now()); insert into user_tb(username, password, email, created_at) values ('cos', '1234', 'cos@nate.com', now()); insert into user_tb(username, password, email, created_at) values ('love', '1234', 'love@nate.com', now()); insert into board_tb(title, content, created_at, user_id) values ('제목1', '내용1', now(), 1); insert into board_tb(title, content, created_at, user_id) values ('제목2', '내용2', now(), 1); insert into board_tb(title, content, created_at, user_id) values ('제목3', '내용3', now(), 2); insert into board_tb(title, content, created_at, user_id) values ('제목4', '내용4', now(), 2); insert into board_tb(title, content, created_at, user_id) values ('제목5', '내용5', now(), 2); insert into reply_tb(comment, board_id, user_id, created_at) values('댓글1', 5, 1, now()); insert into reply_tb(comment, board_id, user_id, created_at) values('댓글2', 5, 1, now()); insert into reply_tb(comment, board_id, user_id, created_at) values('댓글3', 5, 2, now()); insert into reply_tb(comment, board_id, user_id, created_at) values('댓글4', 4, 2, now()); insert into reply_tb(comment, board_id, user_id, created_at) values('댓글5', 3, 2, now());
 
엔포드는 테이블이 2개일때만 !
→ 지금 만드는 매서드처럼
신경써야하는 테이블이 3개 ( User, reply, board )일때는 신경 쓰지 않아도 된다.
 
notion image
SELECT * FROM board_tb bt INNER JOIN user_tb ut ON bt.user_id = ut.id;
 
각각의 게시글에 user를 옆에다 하나씩 붙여줌
 
여기서 게시글 id 가 5인걸 찾자
 
notion image
SELECT * FROM board_tb bt INNER JOIN user_tb ut ON bt.user_id = ut.id where bt.id = 5;
 
마이바티스와 다르게 하이버네이트에서는 이 필드를 받는 dto를 안만들어도 됨
 
board객체안에 reply객체가 들어가 있으니까 !
 
위의 조인된 결과를 하나의 테이블로 보고! 여기다가 reply도 조인
 
 

 
인라인뷰 라는 서브쿼리를 써보자
( → from 절에 결과를 집어넣어서 테이블화 시키는 것 )
notion image
SELECT * FROM ( SELECT bt.id, bt.title, bt.content, ut.username FROM board_tb bt INNER JOIN user_tb ut ON bt.user_id = ut.id where bt.id = 5 );
 
인라인 뷰는 from 뒤의 괄호 안에 결과를 집어넣어서
그거 자체를 테이블로 만드는 것!
 
notion image
SELECT bt.id, bt.title, bt.content, ut.id user_id, ut.username FROM board_tb bt INNER JOIN user_tb ut ON bt.user_id = ut.id where bt.id = 5
 
ut.id 에 user_id 라고 별칭을 주고 조회해도 잘 된다.
 
notion image
 
where 조건에 위에서 적은 별칭을 사용하니 “별칭이름 not found”라고 뜬다.
 
왜 별칭을 인식하지 못할까?
 
쿼리를 제일 먼저 실행하는게 from이다.
하드에서 퍼올려서 메모리에 올림.
그리고 나서 하는게 where 절이 실행된다.
select 절은 마지막에 실행된다.( 프로젝션 )
(→ select 절이 먼저 실행되면 쓸데없는 프로젝션을 많이 해야한다… )
→ 그래서 select 절에 u_id 라고 별칭을 줬는데
몰라서 에러가 남.
 
이걸 해결할 수 있는게 인라인 뷰
 
notion image
select * from ( SELECT bt.id, bt.title, bt.content, ut.id u_id, ut.username FROM board_tb bt INNER JOIN user_tb ut ON bt.user_id = ut.id );
 
from이 실행될때 연산이 메모리에 있기 때문에 u_id가 이미
실행이 되어서
select * 여기서 프로젝션 할 때 u_id를 알고 있다.
 
이럴때 인라인 뷰를 쓴다.
 
where 절 붙여주기
 
notion image
select * from ( SELECT bt.id, bt.title, bt.content, ut.id u_id, ut.username FROM board_tb bt INNER JOIN user_tb ut ON bt.user_id = ut.id ) where u_id=2;
 
인라인 뷰로 바꾸니까 별칭 u_id로 조회된다.
 
notion image
SELECT * FROM ( SELECT bt.id, bt.title, bt.content, ut.username FROM board_tb bt INNER JOIN user_tb ut ON bt.user_id = ut.id where bt.id = 5 );
아까 작성한 board & user 조인한 쿼리에다가 reply를 조인 한 번 해보자!
 
  • 한번 조인한거에 또 조인하면 인라인뷰를 쓴다고 생각하면 됨
 
notion image
SELECT * FROM ( SELECT bt.id, bt.title, bt.content, ut.username FROM board_tb bt INNER JOIN user_tb ut ON bt.user_id = ut.id where bt.id = 5 ) t1 inner join reply_tb t2 on t1.id = t2.board_id;
 
t1 이라는 별칭을 주고, t1.id 와 t2.board_id 를 on 뒤에 적어준다.
 
댓글 조인 완료!
 
이거 자체를 테이블로 보고 또 조인하면 된다.
 
 
위의 user_id 가 댓글의 user_id인데 헷갈리면 별칭 주기!
 
이렇게 별칭을 줄 수 있다.
이렇게 별칭을 줄 수 있다.
 
notion image
네모를 하나의 테이블로 본다.
 
이 테이블에 h1이라는 별칭을 주자.
 
조인할 user_tb에는 h2라는 별칭을 주자.
 
h1의 user_id 와 h2의 id가 같은걸 뽑자!
notion image
근데 * 쓰면 id 를 못찾아서…. 에러남
 
원래 이전 select * 에서 * 쓰지말고 bt.title 이런식으로
잡고 왔어야 함.
 
++
복습할때 * 안쓰고 별칭으로 가져와봄
notion image
 
SELECT h1.id, h1.title, h1.content, h1.u_id b_u_id, h1.email, h1.password, h1.username b_username, h1.r_id, h1.r_user_id, h1.created_at, h1.comment, h2.id, h2.username, h2.password, h2.email FROM ( SELECT t1.id, t1.user_id, t1.title, t1.content, t1.u_id, t1.email, t1.password, t1.username, t2.id r_id, t2.user_id r_user_id, t2.created_at, t2.comment FROM ( SELECT bt.id, bt.user_id, bt.title, bt.content, ut.id u_id, ut.email, ut.password, ut.username FROM board_tb bt INNER JOIN user_tb ut ON bt.user_id = ut.id where bt.id = 5 ) t1 inner join reply_tb t2 on t1.id = t2.board_id ) h1 inner join user_tb h2 on h1.r_user_id = h2.id;
 
인라인 뷰 안쓰는 방법으로 다시 써보자
 
 
notion image
 
인라인 뷰 안쓰고 하니 잘 나온다.
 
SELECT * FROM board_tb bt INNER JOIN user_tb ut ON bt.user_id = ut.id INNER JOIN reply_tb rt on bt.id = rt.board_id INNER JOIN user_tb rut on rut.id = rt.user_id where bt.id = 5;
notion image
 
여기서 화면에 필요한 것만 골라보자
 
notion image
게시글 3줄이 중복된다. 1줄만 있으면 된다
notion image
유저도 3줄이 중복된다. 1줄만 있으면 된다
notion image
댓글은 3줄이 다 필요하다.
 
 
notion image
게시글 2번을 조회하면 결과가 안나온다.
 
게시글 2에는 댓글이 없는데 이너조인해서 그럼
 
이럴땐 아우터 조인으로 바꾼다.
 
notion image
SELECT * FROM board_tb bt INNER JOIN user_tb ut ON bt.user_id = ut.id LEFT OUTER JOIN reply_tb rt on bt.id = rt.board_id INNER JOIN user_tb rut on rut.id = rt.user_id where bt.id = 2;
이렇게 했는데 결과가 안나오네 ?
 
이럴땐 한줄씩 실행해보기
notion image
여기는 되고
 
notion image
여기까지 되고
→ left outer join 했으니까 댓글이 없는 게시글 1, 2, 도 조회된다.
 
if ) 만약 댓글을 inner join으로 하면 댓글이 없는 게시물은 조회되지 않는다 :)
notion image
 
notion image
여까지 되는데
 
왜 안되는 거지
 
 
notion image
마지막줄을 left outer join으로 바꾸니까 된다.
 
중간에 reply_tb가 없을 수 잇는데 LEFT OUTER JOIN 을 했으니
뒤에 줄에도 LEFT OUTER JOIN 을 해줘야 한다.
SELECT * FROM board_tb bt INNER JOIN user_tb ut ON bt.user_id = ut.id LEFT OUTER JOIN reply_tb rt on bt.id = rt.board_id LEFT OUTER JOIN user_tb rut on rut.id = rt.user_id where bt.id = 2;
notion image
 
게시글 5번으로 where 조건을 줘서 보면
 
  1. board_tb
  1. user_tb
  1. reply_tb
  1. user_tb
 
이렇게 4개가 join 되어서 나온 것을 볼 수 있다.
 
중간에 user_tb가 있는데 왜 또 user_tb를 join 했는가 ?
 
위에 inner join 해서 가져온 user_tb는 게시글 작성자에 해당하는 user 정보만 가지고 온 것이고,
아래에 left outer join 해서 가져온 user_tb 는 reply 테이블의 user, 즉 reply 작성한 user 정보를
가지고 오기 위해서 한 번 더 join 한 것이다. !
 
결국 게시글 5번의 게시글 정보, 게시글작성자 정보, 게시글 댓글 정보, 댓글작성자 정보 를
한꺼번에 JOIN으로 가져온 것 !
 
네이티브 쿼리를 사용하면 이렇게 가져와야 하고
 
notion image
 
JPQL로 작성하면 이렇게 간단하다!
 
근데 이렇게 쿼리를 짜면 네이티브 쿼리 짜는 실력이 안는다.
 
그래서 신입일때는 네이티브 쿼리로 짜는게 좋을 수 있다 !
 
 
 
Share article

keepgoing