
1. 더미 데이터 집어넣기
- data.sql 파일에 추가하기
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 board_tb(title, content, user_id, created_at) values('제목1','내용1', 1, now());
insert into board_tb(title, content, user_id, created_at) values('제목2','내용2', 1, now());
insert into board_tb(title, content, user_id, created_at) values('제목3','내용3', 1, now());
insert into board_tb(title, content, user_id, created_at) values('제목4','내용4', 2, now());
insert into reply_tb(comment, board_id, user_id, created_at) values('댓글1', 1, 1, now());
insert into reply_tb(comment, board_id, user_id, created_at) values('댓글2', 4, 1, now());
insert into reply_tb(comment, board_id, user_id, created_at) values('댓글3', 4, 1, now());
insert into reply_tb(comment, board_id, user_id, created_at) values('댓글4', 4, 2, now());
2. 더미 데이터 확인하기


- board_id는 외래키
- 중복해서 데이터를 넣으면 조인을 안해도 됨
조인을 안해서 select가 빨라짐
- 데이터를 공유해서 쓰니까 조인한 테이블의 내용이 수정해도 수정할 필요가 없음
3. 조인 연습하기(게시글 목록과 댓글 같이 보기)
- 조인을 한 이유 : 데이터를 다 보여주기 위해서

1) inner join
- 댓글이 있는 게시글만 보이게 되서 안됨
select * from board_tb bt inner join reply_tb rt on bt.id = rt.board_id;

- 10개의 컬럼을 담을 java object가 필요함 → DTO 만들기
댓글의 갯수에 따라 게시글 항목 갯수가 추가됨 / 중복되는 데이터들이 있음
중복되는 이름 변경해야함
row의 갯수만큼 DTO가 있어야 함 → 컬렉션으로 만들어서 .add로 추가해야 함
- 동일한 데이터가 있으면 Reply라는 내부 class를 만들어서 컬럼을 담음
List에 내부 class에서 만든 Reply로 만들어서 객체를 생성함
DTO가 2개가 되서 NEW 2번 하면 중복 안되게 데이터가 깔끔하게 들어감
→ 나중에 JPA가 자동으로 해줌 / ORM을 대신 해줌
→ board_tb에 List,Reply> replyes를 추가하면 board_tb에 다 담을 수 있음

2) outter join
- board_tb가 다 보여져야하니까 left
select bt.*, rt.* from board_tb bt left outer join reply_tb rt on bt.id = rt.board_id;

- 댓글에 필요한 내용
select bt.*, rt.id from board_tb bt left outer join reply_tb rt on bt.id = rt.board_id;

- group by로 묶어주기
select bt.*, count(rt.id) from board_tb bt left outer join reply_tb rt on bt.id = rt.board_id group by bt.id, bt.title, bt.content, bt.user_id, bt.created_at;


- 화면에 출력할 때, for문을 6번 돌아서 게시글이 6번 출력
→ 게시글은 4번만 출력되어야 함
select bt.id, bt.title, bt.content, bt.user_id, bt.created_at,
ifnull(rt.id, 0) rid, rt.board_id, ifnull(rt.comment,'')
from board_tb bt
left outer join reply_tb rt on bt.id = rt.board_id;

- DB에 들고오는 DTO, 화면에 가져오는 DTO 총 2개가 있어야 함
4개 담고 각각의 컬렉션 크기에 따라 만들어서 옮겨서 줘야 화면에 뿌릴 수 있음
게시판DTO 하나 댓글 DTO 하나 만들어서
게시판DTO가 들고있는 댓글 컬렉션을 NEW해서 초기화를 해서 들고 있으면 넣을 수 있음


직접 매핑하기 코드
- BoardList
distinct해서 중복 제거해서 4개로 줄이기
for문을 돌리면서 BoardList에 담기
- ReplyList
for문을 돌리면서 ReplyList에 담기
- Bid에 맞게 BoardList내용에 추가하기
- index.mustache 추가하기
{{> /layout/header}}
<h1>
{{#sessionUser}}
{{username}}
{{/sessionUser}}
</h1>
<div class="container p-5">
{{#boardList}}
<div class="card mb-3">
<div class="card-header">
댓글1, 댓글2
</div>
<div class="card-body">
<h4 class="card-title mb-3">{{title}}</h4>
<a href="/board/{{id}}" class="btn btn-primary">상세보기</a>
</div>
</div>
{{/boardList}}
<ul class="pagination d-flex justify-content-center">
<li class="page-item disabled"><a class="page-link" href="#">Previous</a></li>
<li class="page-item"><a class="page-link" href="#">Next</a></li>
</ul>
</div>
{{> /layout/footer}}
- Board
package shop.mtcoding.blog.board;
import jakarta.persistence.*;
import lombok.Data;
import shop.mtcoding.blog.reply.Reply;
import java.time.LocalDateTime;
import java.util.List;
@Table(name="board_tb")
@Data
@Entity // 테이블 생성하기 위해 필요한 어노테이션
public class Board { // User 1 -> Board N
@Id // PK 설정
@GeneratedValue(strategy = GenerationType.IDENTITY) // auto_increment 전략
private int id;
@Column(length = 30)
private String title;
private String content;
private int userId; // 테이블에 만들어 질때 user_id
private LocalDateTime createdAt;
}
- BoardController에서 index2 추가하기
package shop.mtcoding.blog.board;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import shop.mtcoding.blog.user.User;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@RequiredArgsConstructor
@Controller
public class BoardController {
private final HttpSession session;
private final BoardRepository boardRepository;
// ?title=제목1&content=내용1
// title=제목1&content=내용1
@PostMapping("/board/{id}/update")
public String update(@PathVariable int id, BoardRequest.UpdateDTO requestDTO){
// 1. 인증 체크
User sessionUser = (User) session.getAttribute("sessionUser");
if(sessionUser == null){
return "redirect:/loginForm";
}
// 2. 권한 체크
Board board = boardRepository.findById(id);
if(board.getUserId() != sessionUser.getId()){
return "error/403";
}
// 3. 핵심 로직
// update board_tb set title = ?, content = ? where id = ?;
boardRepository.update(requestDTO, id);
return "redirect:/board/"+id;
}
@GetMapping("/board/{id}/updateForm")
public String updateForm(@PathVariable int id, HttpServletRequest request){
// 1. 인증 안되면 나가
User sessionUser = (User) session.getAttribute("sessionUser");
if(sessionUser == null){
return "redirect:/loginForm";
}
// 2. 권한 없으면 나가
// 모델 위임 (id로 board를 조회)
Board board = boardRepository.findById(id);
if(board.getUserId() != sessionUser.getId()){
return "error/403";
}
// 3. 가방에 담기
request.setAttribute("board", board);
return "board/updateForm";
}
@PostMapping("/board/{id}/delete")
public String delete(@PathVariable int id, HttpServletRequest request) {
// 1. 인증 안되면 나가
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null) { // 401
return "redirect:/loginForm";
}
// 2. 권한 없으면 나가
Board board = boardRepository.findById(id);
if (board.getUserId() != sessionUser.getId()) {
request.setAttribute("status", 403);
request.setAttribute("msg", "게시글을 삭제할 권한이 없습니다");
return "error/40x";
}
boardRepository.deleteById(id);
return "redirect:/";
}
@PostMapping("/board/save")
public String save(BoardRequest.SaveDTO requestDTO, HttpServletRequest request) {
// 1. 인증 체크
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null) {
return "redirect:/loginForm";
}
// 2. 바디 데이터 확인 및 유효성 검사
System.out.println(requestDTO);
if (requestDTO.getTitle().length() > 30) {
request.setAttribute("status", 400);
request.setAttribute("msg", "title의 길이가 30자를 초과해서는 안되요");
return "error/40x"; // BadRequest
}
// 3. 모델 위임
// insert into board_tb(title, content, user_id, created_at) values(?,?,?, now());
boardRepository.save(requestDTO, sessionUser.getId());
return "redirect:/";
}
@GetMapping("/test")
public @ResponseBody List<BoardResponse.MainDTO> index2(HttpServletRequest request) {
return boardRepository.findAllV3();
}
@GetMapping("/")
public @ResponseBody List<BoardResponse.BoardDTO> index(HttpServletRequest request) {
//List<Board> boardList = boardRepository.findAll();
//request.setAttribute("boardList", boardList);
//return "index";
return boardRepository.findAllV2();
}
// /board/saveForm 요청(Get)이 온다
@GetMapping("/board/saveForm")
public String saveForm() {
// session 영역에 sessionUser 키값에 user 객체 있는지 체크
User sessionUser = (User) session.getAttribute("sessionUser");
// 값이 null 이면 로그인 페이지로 리다이렉션
// 값이 null 이 아니면, /board/saveForm 으로 이동
if (sessionUser == null) {
return "redirect:/loginForm";
}
return "board/saveForm";
}
@GetMapping("/board/{id}")
public String detail(@PathVariable int id, HttpServletRequest request) {
// 1. 모델 진입 - 상세보기 데이터 가져오기
BoardResponse.DetailDTO responseDTO = boardRepository.findByIdWithUser(id);
// 2. 페이지 주인 여부 체크 (board의 userId와 sessionUser의 id를 비교)
User sessionUser = (User) session.getAttribute("sessionUser");
boolean pageOwner;
if (sessionUser == null) {
pageOwner = false;
} else {
int 게시글작성자번호 = responseDTO.getUserId();
int 로그인한사람의번호 = sessionUser.getId();
pageOwner = 게시글작성자번호 == 로그인한사람의번호;
}
request.setAttribute("board", responseDTO);
request.setAttribute("pageOwner", pageOwner);
return "board/detail";
}
}
- BoardRepository에서 findAllV2()와 findAllV3() 만들기
package shop.mtcoding.blog.board;
import com.sun.tools.javac.Main;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@RequiredArgsConstructor
@Repository
public class BoardRepository {
private final EntityManager em;
public List<BoardResponse.MainDTO> findAllV3() {
Query query = em.createNativeQuery("select bt.id, bt.title, bt.content, bt.user_id, bt.created_at, ifnull(rt.id, 0) rid, rt.board_id, ifnull(rt.comment,'') from board_tb bt left outer join reply_tb rt on bt.id = rt.board_id");
List<Object[]> rows = (List<Object[]>) query.getResultList();
List<BoardResponse.MainDTO> mainList = new ArrayList<>();
for (Object[] row : rows) {
Integer id = (Integer) row[0];
String title = (String) row[1];
String content = (String) row[2];
Integer userId = (Integer) row[3];
Timestamp createdAt = (Timestamp) row[4];
Integer rid = (Integer) row[5];
Integer boardId = (Integer) row[6];
String comment = (String) row[7];
BoardResponse.MainDTO main = new BoardResponse.MainDTO(
id, title, content, userId, createdAt.toLocalDateTime(),rid, boardId, comment
);
mainList.add(main);
}
return mainList;
}
public List<BoardResponse.BoardDTO> findAllV2() {
Query query = em.createNativeQuery("select bt.id, bt.title, bt.content, bt.user_id, bt.created_at, ifnull(rt.id, 0) rid, rt.board_id, ifnull(rt.comment,'') from board_tb bt left outer join reply_tb rt on bt.id = rt.board_id");
List<Object[]> rows = (List<Object[]>) query.getResultList();
List<BoardResponse.BoardDTO> boardList = new ArrayList<>();
List<BoardResponse.ReplyDTO> replyList = new ArrayList<>();
for (Object[] row : rows) {
// BoardDTO
Integer id = (Integer) row[0];
String title = (String) row[1];
String content = (String) row[2];
Integer userId = (Integer) row[3];
Timestamp createdAt = (Timestamp) row[4];
BoardResponse.BoardDTO board = new BoardResponse.BoardDTO(
id, title, content, userId, createdAt.toLocalDateTime()
);
boardList.add(board);
// ReplyDTO
Integer rid = (Integer) row[5];
Integer boardId = (Integer) row[6];
String comment = (String) row[7];
BoardResponse.ReplyDTO reply = new BoardResponse.ReplyDTO(
rid, boardId, comment
);
replyList.add(reply);
}
// 크기 4
boardList = boardList.stream().distinct().toList();
for (BoardResponse.BoardDTO b : boardList){
// 6 바퀴
for (BoardResponse.ReplyDTO r : replyList){
if(b.getId() == r.getBoardId()){
b.addReply(r);
}
}
}
return boardList;
}
public List<Board> findAll() {
Query query = em.createNativeQuery("select * from board_tb order by id desc", Board.class);
return query.getResultList();
}
public Board findById(int id) {
Query query = em.createNativeQuery("select * from board_tb where id = ?", Board.class);
query.setParameter(1, id);
Board board = (Board) query.getSingleResult();
return board;
}
public BoardResponse.DetailDTO findByIdWithUser(int idx) {
Query query = em.createNativeQuery("select b.id, b.title, b.content, b.user_id, u.username from board_tb b inner join user_tb u on b.user_id = u.id where b.id = ?");
query.setParameter(1, idx);
Object[] row = (Object[]) query.getSingleResult();
Integer id = (Integer) row[0];
String title = (String) row[1];
String content = (String) row[2];
int userId = (Integer) row[3];
String username = (String) row[4];
System.out.println("id : " + id);
System.out.println("title : " + title);
System.out.println("content : " + content);
System.out.println("userId : " + userId);
System.out.println("username : " + username);
BoardResponse.DetailDTO responseDTO = new BoardResponse.DetailDTO();
responseDTO.setId(id);
responseDTO.setTitle(title);
responseDTO.setContent(content);
responseDTO.setUserId(userId);
responseDTO.setUsername(username);
return responseDTO;
}
@Transactional
public void save(BoardRequest.SaveDTO requestDTO, int userId) {
Query query = em.createNativeQuery("insert into board_tb(title, content, user_id, created_at) values(?,?,?, now())");
query.setParameter(1, requestDTO.getTitle());
query.setParameter(2, requestDTO.getContent());
query.setParameter(3, userId);
query.executeUpdate();
}
@Transactional
public void deleteById(int id) {
Query query = em.createNativeQuery("delete from board_tb where id = ?");
query.setParameter(1, id);
query.executeUpdate();
}
@Transactional
public void update(BoardRequest.UpdateDTO requestDTO, int id) {
Query query = em.createNativeQuery("update board_tb set title=?, content=? where id = ?");
query.setParameter(1, requestDTO.getTitle());
query.setParameter(2, requestDTO.getContent());
query.setParameter(3, id);
query.executeUpdate();
}
}
- BoardResponse에서 MainDTO, BoardDTO, ReplyDTO 만들기
package shop.mtcoding.blog.board;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
public class BoardResponse {
@AllArgsConstructor
@Data
public static class MainDTO {
private Integer id;
private String title;
private String content;
private Integer userId;
private LocalDateTime createdAt;
private Integer rid;
private Integer boardId;
private String comment;
}
@NoArgsConstructor
@Data
public static class BoardDTO {
private Integer id;
private String title;
private String content;
private Integer userId;
private LocalDateTime createdAt;
private List<ReplyDTO> replies = new ArrayList<>();
public void addReply(ReplyDTO reply){
replies.add(reply);
}
public BoardDTO(Integer id, String title, String content, Integer userId, LocalDateTime createdAt) {
this.id = id;
this.title = title;
this.content = content;
this.userId = userId;
this.createdAt = createdAt;
}
}
@NoArgsConstructor
@AllArgsConstructor
@Data
public static class ReplyDTO {
private Integer rid;
private Integer boardId;
private String comment;
}
@Data
public static class DetailDTO {
private int id;
private String title;
private String content;
private int userId;
private String username;
}
}

select * from board_tb bt
left outer join reply_tb rt on bt.id = rt.board_id
where bt.id=2;

- 필요한 정보만 가져오게 수정하기
select bt.id, bt.title, bt.content, bt.user_id, bt.created_at,
rt.id r_id, rt.user_id r_user_id, rt.comment
from board_tb bt
left outer join reply_tb rt on bt.id = rt.board_id
where bt.id=2;

select bt.id, bt.title, bt.content, bt.user_id, bt.created_at,
rt.id r_id, rt.user_id r_user_id, rt.comment
from board_tb bt
left outer join reply_tb rt on bt.id = rt.board_id
where bt.id=4;

- inner join을 추가로 해서 게시글 작성자 username과 댓글 작성자 username 가져오기
but.username : board 게시판에 작성한 username
rut.username : reply 게시판에 작성한 username
select
bt.id, bt.title, bt.content, bt.user_id, but.username, bt.created_at,
rt.id r_id, rt.user_id r_user_id, rut.username, rt.comment
from board_tb bt
left outer join reply_tb rt on bt.id = rt.board_id
inner join user_tb but on bt.user_id=but.id
left outerjoin user_tb rut on rt.user_id=rut.id
where bt.id=4;

- selecct 2번 할지 join 할지는 선택의 문제
select * from board_tb where id=4;
select * from reply_tb where board_id=4;

- select를 두 번하면 가독성이 좋음
- join을 하면 성능이 좋음
select * from board_tb bt
inner join user_tb ut on bt.user_id = ut.id
where bt.id = 4;

select * from reply_tb rt
inner join user_tb ut on rt.user_id = ut.id
where rt.board_id = 4;

3) index
- 검색의 기본은 풀 스캔 : DB를 검색을 할 때 전체 데이터를 순차적으로 스캔
검색을 빠르게 하려면 중복이 없어야 함
중복이 된다면 군집화, 정렬되어있어야 함
- 풀 스캔 시 군집화가 되어있으면 시간 복잡도는 무조건 O(n)
n은 페이지 갯수
- 인덱스 : 책의 목차와 같이 특정 항목을 빠르게 찾을 수 있도록 돕는 자료 구조
랜덤 엑세스 : 번호 보고 다이렉트로 찾음 / 직접적인 찾기
- 시간 복잡도 : 인덱스 검색 시간 + 찾는 시간
메모리에 있음 → IO가 일어나지 않음
- 무조건 풀 스캔보다 빠른 것은 아님
내가 찾는 데이터의 15%이하이면 무조건 인덱스의 효율이 좋음
- PK는 무조건 15%가 넘음
만들어질 때 자동으로 목차를 다 달아줌 → 랜덤 엑세스
UK도 달아줌
제목 검색 → 풀 스캔, PK이면 효율이 안 좋음
항상 내가 찾는 내용이 PK가 아닐 수도 있음 → 엘라스틱 서치(검색 엔진)를 사용
Share article