🤍연관 관계와 관계형 데이터베이스 설계
- 관계형 데이터베이스에서는 개체(entity)간의 관계(relation)라는 것에 대해 고민하게 됨.
- 일대일(1:1), 일대다(1:N), 다대일(N:1), 다대다(M:N) 관계를 이용하여 데이터가 서로 간에 어떻게 구성되었는지 표현함.
예를 들어, 회원과 게시글의 관계를 정의해보면
- 한 명의 회원은 여러 게시글을 작성할 수 있다.
- 하나의 게시글은 한 명의 회원에 의해 작성된다.
🤍게시판 프로젝트 시작 전 테스트
1. 엔티티 클래스 추가
Member
Board
Reply
- 하나의 게시글에는 여러 개의 댓글이 달릴 수 있다. -> @ManyToOne
- 한 명의 사용자는 여러 개의 게시글을 작성할 수 있다. -> @ManyToOne
- 완성된 테이블 관계도
2. Repository 인터페이스 작성
ReplyRepository
MemberRepository
BoardRepository
3. 연관 관계 테스트
- 100명의 회원 더미 데이터 넣기
- 100개의 게시글 더미 데이터 넣기
- 300개의 댓글 더미 데이터 넣기
- 1부터 100까지 임의의 번호(Math.random)를 이용하여 300개의 댓글 추가
글 조회 테스트 코드를 작성하고 실행해 보면
@Transactional
@Test
public void testRead1(){
Optional<Board> result = boardRepository.findById(100L); //데이터베이스에 존재하는 번호
Board board = result.get();
System.out.println(board);
System.out.println(board.getWriter());
}
@ManyToOne을 선언해놓았기 때문에 자동으로 left outer joi 처리가 된 것을 확인할 수 있다.
💡JPQL과 left (outer) join
- 목록 화면에서 게시글 정보, 댓글 수를 같이 가져오기 위해서는 단순히 하나의 엔티티 타입을 이용할 수 없음. -> 이에 대한 해결책 중 가장 많이 쓰이는 방식은 JPQL의 조인(join)을 이용해서 처리하는 방식임.
🤍게시판 프로젝트 적용
1. DTO 계층 작성 (PageRequestDTO, PageResultDTO는 이전에 만든 코드를 그대로 가져와서 활용했다.)
2. 게시물 등록 서비스 계층 작성
- 게시물 등록에는 작성하는 Member, 게시물 Board의 연관관계가 필요함.
- 이때 Member는 실제 데이터베이스에 있는 이메일 주소를 사용해야함.
3. 게시물 등록 테스트 코드 작성
등록 완료!!
4. 게시물 목록 처리 서비스 계층 작성
4. 게시물 조회, 삭제, 수정 처리 서비스 계층 작성
- 삭제 시 중요한 것은 댓글의 유무이다. 실제 개발 시에는 댓글의 상태(state)를 컬럼으로 따로 지정하고 이를 변경하는 형태로 처리하지만 여기에서는 이를 고민하지 않고 게시물 삭제가 가능하다고 가정하고 진행한다.
- 다만, 게시물을 삭제하려면 FK로 게시물을 참조하고 있는 reply 테이블 역시 삭제해야함.
- 가장 중요한 것은 위의 두 가지 작업이 트랜잭션으로 처리되어야 한다는 점임.
5. 게시물 조회, 수정, 삭제 테스트 코드 작성
삭제 테스트 코드를 실행해 보면 reply 테이블이 먼저 삭제되고 board 테이블을 조회한 후에 삭제되는 것을 확인할 수 있다.
6. UI는 이전에 만들어둔 화면을 그대로 가져와서 게시판에 맞게 수정하여 사용했다. (gno->bno, writer->writerName)
list.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:replace="~{/layout/basic::setContent(~{this::content})}">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- Bootstrap core CSS -->
<link th:href="@{/vendor/bootstrap/css/bootstrap.min.css}" rel="stylesheet">
<!-- Custom styles for this template -->
<link th:href="@{/css/simple-sidebar.css}" rel="stylesheet">
<!-- Bootstrap core JavaScript -->
<script src="vendor/jquery/jquery.min.js"></script>
<script src="vendor/bootstrap/js/bootstrap.bundle.min,js"></script>
</head>
<body>
<th:block th:fragment="content">
<h1>Board List Page
<span>
<a th:href="@{/board/register}">
<button type="button" class="btn btn-outline-primary">REGISTER</button>
</a>
</span>
</h1>
<!--검색-->
<form action="/board/list" method="get" id="searchForm">
<div class="input-group">
<input type="hidden" name="page" value="1">
<div class="input-group-prepend">
<select class="custom-select" name="type">
<option th:selected="${pageRequestDTO.type == null}">-------</option>
<option value=t"" th:selected="${pageRequestDTO.type == 't'}">제목</option>
<option value="c" th:selected="${pageRequestDTO.type == 'c'}">내용</option>
<option value="w" th:selected="${pageRequestDTO.type == 'w'}">작성자</option>
<option value="tc" th:selected="${pageRequestDTO.type == 'tc'}">제목+내용</option>
<option value="tcw" th:selected="${pageRequestDTO.type == 'tcw'}">제목+내용+작성자</option>
</select>
</div>
<input class="form-control" name="keyword" th:value="${pageRequestDTO.keyword}">
<div class="input-group-append" id="button-addon4">
<button class="btn btn-outline-secondary btn-search" type="button">Search</button>
<button class="btn-outline-secondary btn-clear" type="button">Clear</button>
</div>
</div>
</form>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Title</th>
<th scope="col">Writer</th>
<th scope="col">Regdate</th>
</tr>
</thead>
<tbody>
<tr th:each="dto : ${result.dtoList}">
<th scope="row">
<a th:href="@{/board/read(bno=${dto.bno},page=${result.page}, type=${pageRequestDTO.type}, keyword=${pageRequestDTO.keyword})}">
[[${dto.bno}]]
</a>
</th>
<th scope="row">[[${dto.bno}]]</th>
<td>[[${dto.title}]] ------- [<b th:text="${dto.replyCount}"></b> </td>
<td>[[${dto.writerName}]] <small>[[${dto.writerEmail}]]</small></td>
<td>[[${#temporals.format(dto.regDate,'yyyy/MM/dd')}]]</td>
</tr>
</tbody>
</table>
<ul class="pagination h-100 justify-content-center align-items-center">
<li class="page-item " th:if="${result.prev}">
<a class="page-link" th:href="@{/board/list(page=${result.start -1}, type=${pageRequestDTO.type}, keyword=${pageRequestDTO.keyword})}" tabindex="-1">Previous</a>
</li>
<li th:class=" 'page-item '+${result.page == page?'active':''}" th:each="page:${result.pageList}">
<a class="page-link" th:href="@{/board/list(page=${page}, type=${pageRequestDTO.type}, keyword=${pageRequestDTO.keyword})}">
[[${page}]]
</a>
</li>
<li class="page-item" th:if="${result.next}">
<a class="page-link" th:href="@{/board/list(page=${result.end +1}, type=${pageRequestDTO.type}, keyword=${pageRequestDTO.keyword})}">Next</a>
</li>
</ul>
<div class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modal title</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<p>Model body text goes here.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</div>
<script th:inline="javascript">
var msg = [[${msg}]];
console.log(msg);
if(msg){
$(".modal").modal();
}
//검색 버튼
var searchForm = $("#searchForm");
$('.btn-search').click(function (e){
searchForm.submit();
});
//모든 검색 조건 없이 새로 목록 페이지
$('.btn-clear').click(function (e){
searchForm.empty().submit();
});
</script>
</th:block>
</body>
</th:block>
</html>
modify.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:replace="~{/layout/basic::setContent(~{this::content})}">
<th:block th:fragment="content">
<h1 class="mt-4">Board Modify Page</h1>
<form action="/board/modify" method="post">
<!--페이지 번호-->
<input type="hidden" name="page" th:value="${requestDTO.page}">
<input type="hidden" name="type" th:value="${requestDTO.type}">
<input type="hidden" name="keyword" th:value="${requestDTO.keyword}">
<div class="form-group">
<label>Bno</label>
<input type="text" class="form-control" name="bno" th:value="${dto.bno}" readonly>
</div>
<div class="form-group">
<label>Title</label>
<input type="text" class="form-control" name="title" th:value="${dto.title}">
</div>
<div class="form-group">
<label>Content</label>
<textarea class="form-control" name="content">[[${dto.content}]]</textarea>
</div>
<div class="form-group">
<label>Writer</label>
<input type="text" class="form-control" name="writer" th:value="${dto.writerName}" readonly>
</div>
<div class="form-group">
<label>RegDate</label>
<input type="text" class="form-control" th:value="${#temporals.format(dto.regDate, 'yyyy/MM/dd HH:mm:ss')}" readonly>
</div>
<div class="form-group">
<label>ModDate</label>
<input type="text" class="form-control" th:value="${#temporals.format(dto.modDate, 'yyyy/MM/dd HH:mm:ss')}" readonly>
</div>
<a th:href="@{/board/modify(bno=${dto.bno},page=${requestDTO.page})}">
<button type="button" class="btn btn-primary">Modify</button>
</a>
<a th:href="@{/board/list(page=${requestDTO.page})}">
<button type="button" class="btn btn-info">List</button>
</a>
</form>
<button type="button" class="btn btn-primary modifyBtn">Modify</button>
<button type="button" class="btn btn-info listBtn">List</button>
<button type="button" class="btn btn-danger removeBtn">Remove</button>
</th:block>
</th:block>
<script th:inline="javascript">
var actionForm = $("form"); //form 태그 객체
//삭제 이벤트 처리
$(".removeBtn").click(function (){
actionForm
.attr("action","/board/remove")
.attr("method","post");
actionForm.submit();
});
//수정 이벤트 처리
$(".modifyBtn").click(function (){
if(!confirm("수정하시겠습니까?")){
return;
}
actionForm
.attr("action","/board/modify").attr("method","post").submit();
});
//목록으로 이동 이벤트 처리
$(".listBtn").click(function (){
//var pageInfo = $("input[name='page']");
var page = $("input[name='page']");
var type = $("input[name='type']");
var keyword = $("input[name='keyword']");
actionForm.empty(); //form 태그의 모든 내용을 지우고
//actionForm.append(pageInfo); //목록 페이지 이동에 필요한 내용을 다시 추가
actionForm.append(page);
actionForm.append(type);
actionForm.append(keyword);
actionForm.attr("action","/board/list").attr("method","get");
console.log(actionForm.html()); //먼저 확인 후 주석 처리
actionForm.submit(); //확인 후 주석 해제
});
</script>
</html>
read.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:replace="~{/layout/basic::setContent(~{this::content})}">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<th:block th:fragment="content">
<h1 class="mt-4">Board Read Page</h1>
<div class="form-group">
<label>Bno</label>
<input type="text" class="form-control" name="gno" th:value="${dto.bno}" readonly>
</div>
<div class="form-group">
<label>Title</label>
<input type="text" class="form-control" name="title" th:value="${dto.title}" readonly>
</div>
<div class="form-group">
<label>Content</label>
<textarea class="form-control" name="content" readonly>[[${dto.content}]]</textarea>
</div>
<div class="form-group">
<label>Writer</label>
<input type="text" class="form-control" name="writer" th:value="${dto.writerName}" readonly>
</div>
<div class="form-group">
<label>RegDate</label>
<input type="text" class="form-control" name="regDate" th:value="${#temporals.format(dto.regDate, 'yyyy/MM/dd HH:mm:ss')}" readonly>
</div>
<div class="form-group">
<label>ModDate</label>
<input type="text" class="form-control" name="modDate" th:value="${#temporals.format(dto.modDate, 'yyyy/MM/dd HH:mm:ss')}" readonly>
</div>
<a th:href="@{/board/modify(bno=${dto.bno},page=${requestDTO.page}, type=${requestDTO.type}, keyword=${requestDTO.keyword})}">
<button type="button" class="btn btn-primary">Modify</button>
</a>
<a th:href="@{/board/list(page=${requestDTO.page}, type=${requestDTO.type}, keyword=${requestDTO.keyword})}">
<button type="button" class="btn btn-info">List</button>
</a>
</th:block>
</body>
</th:block>
</html>
register.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:replace="~{/layout/basic::setContent(~{this::content})}">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- Bootstrap core JS-->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/js/bootstrap.bundle.min.js"></script>
<!-- Core theme JS-->
<script th:src="@{/js/scripts.js}"></script>
</head>
<body>
<th:block th:fragment="content">
<h1 class="mt-4">Board Register Page</h1>
<form th:action="@{/board/register}" th:method="post">
<div class="form-group">
<label>Title</label>
<input type="text" class="form-control" name="title" placeholder="Enter Title">
</div>
<div class="form-group">
<label>Content</label>
<textarea class="form-control" rows="5" name="content"></textarea>
</div>
<div class="form-group">
<label>Writer Email</label>
<input type="text" class="form-control" name="writerEmail" placeholder="Enter Writer Email">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</th:block>
</body>
</th:block>
</html>
7. 컨트롤러 계층 작성
- 목록, 등록
- 조회, 수정, 삭제
8. 실행 화면
- 목록 출력 잘 되고 페이징도 잘 됨.
- 게시물 등록
- 게시물 수정
- 게시물 검색
앞서 진행했던 방명록 프로젝트에서 코드를 조금 변형한 공부라서 어제 이해가 안 갔던 부분을 조금 더 잘 이해할 수 있었던 것 같다. 그래도 아직 스프링부트의 간결한 코드에는 적응하기 어렵다.......
'💻 my code archive > 🏷️JAVA & Spring(Boot)' 카테고리의 다른 글
[스프링부트 블로그 만들기] 비밀번호 해쉬화(암호화), 스프링 시큐리티 로그인 구현하기 (0) | 2022.03.19 |
---|---|
스프링부트 공부기록(25) - 게시판 프로젝트, @RestController, JSON, Ajax 댓글 처리 (1) | 2022.03.19 |
스프링부트 공부기록(23) - 방명록 작성:: 게시물 검색 구현 (0) | 2022.03.18 |
스프링부트 공부기록(22) - 방명록 작성:: 게시물 조회, 수정, 삭제 구현 (0) | 2022.03.18 |
스프링부트 공부기록(21) - 방명록 작성 :: 게시물 등록, 페이징 처리 구현하기 (0) | 2022.03.18 |