기존에 만들어둔 index.html은 static 폴더 아래에 생성하여 정적 페이지로 사용하여 Thymeleaf는 사용하지 못하지만 서버 측에서 매번 템플릿 엔진을 거쳐 처리하는 방식이 아니라 웹 서버가 해당 파일을 직접 제공하여 별도의 서버 사이드 템플릿 엔진을 사용하지 않아도 되어 성능 상의 이점이 있습니다.
index.html을 제외한 나머지 html 파일들은 resource 아래에 templates 폴더에 생성을 하여 동적 페이지로 사용을 하며, Thymeleaf를 사용할 수 있습니다.
header와 footer등 여러 페이지에서 중복 사용 되는 코드를 따로 모와서 한 번에 관리하는 것이 유지보수 측면에서 편하기 때문에 공통 영역을 처리해줍니다.
templates 폴더 아래에 framgments 폴더를 생성한 후 그 아래에 header.html, nav.html, footer.html 파일을 생성해줍니다.
<!DOCTYPE html>
<html data-bs-theme="auto" lang="ko" xmlns:th="http://www.thymeleaf.org">
<head th:fragment="fragment-header(title)">
<title th:text="${title}"></title>
<script src="/js/color-modes.js"></script>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1" name="viewport">
<meta content="" name="description">
<meta content="Mark Otto, Jacob Thornton, and Bootstrap contributors" name="author">
<meta content="Hugo 0.118.2" name="generator">
<link href="https://getbootstrap.com/docs/5.3/examples/carousel/" rel="canonical">
<link href="https://cdn.jsdelivr.net/npm/@docsearch/css@3" rel="stylesheet">
<!--CSS CDN-->
<link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" rel="stylesheet">
<!-- Favicons -->
<link href="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBwgHBgkIBwgKCgkLDRYPDQwMDRsUFRAWIB0iIiAdHx8kKDQsJCYxJx8fLT0tMTU3Ojo6Iys/RD84QzQ5OjcBCgoKDQwNGg8PGjclHyU3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3N//AABEIAHsAewMBEQACEQEDEQH/xAAcAAEAAQUBAQAAAAAAAAAAAAAABAECAwYHBQj/xAA5EAABBAADBgMFBgUFAAAAAAABAAIDBAUGERIhMUFRYRNxgQciI5GhFDJCcpLRM1KxweFDU2Kywv/EABoBAQADAQEBAAAAAAAAAAAAAAABAgMEBgX/xAAuEQACAQMDAQUIAwEAAAAAAAAAAQIDBBESITEFEyJBUdEyYXGRobHB4RSB8FL/2gAMAwEAAhEDEQA/AO4oAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIC1z2tBLiABzKBb7IwDEKRdsi3XLuglbr/VRqRfs5/8skbQPDgpKFUAQBAU1QGKW3Wh/jTxR/neB/VQ2kWjCUuEViswTfwZo5PyPBRNMSjKPKMmqkqVQBAEAQBAaZmPN0kcr6WCbL5Qdl9lw1aw8w0cz34eawnVxtE+3ZdLU0qlfZeXn8TTrNC3ibi/EbM1kn/deSB5DgPRYtOXJ9uDpUVinFIwSZbh2dPCZp+VR2Zorl+ZjhixTCHbeF3rFYjg1j/d/SfdPyTvR4ZWdOjX2qRT+/z5NnwP2jSQvbXzHBsjh9rgbuH5m/3HyWsa+PaPj3XRdnK3f9P8HQq1mC1BHPWlZLDINpkjDq1w7FdKeeD4MoyhJxksNGtZozzh2BvfWh1uXxuMEbtAw/8AN3Ly3nssp1ox28T6Fn0yrc957R8/Q59iGYsw448+PdfWgd/o1T4bR6j3j6lc0pzlyz0NHp9tQ4jl+b3/AEQWYCyQ7cjQ9x4udvJ9VXQdna42Rkbgogd4kOsbxwcw7JHqFOkh1dSxLdGw4LmzF8IkbHdkfeqcCJDrI0dnc/X6LSNSUeT5lz0uhWWYLTL6fI6XhuIVsTpx2qcokieOPMHmCORXUmmso8zWozozcJrDRKUmQQBAavnjGHUqrKNd5bPZ12nDixnM+Z4fNZVZYWEfV6XaqrPtJcR+rNSowsY0AALFI+/Ukz04wFc5ZZL3AaKSqIdmIOB3KrRvCRr+JUmuDvdCykjshI8upimL4NFYqYZckggsbntH4T1b/Ke4VVOUVhEVrSjXkp1I5a/25hoVN+pG/mTxKhI3ex7teMN0WiMZMmseGhSZ4LZZhohKR59qUHVVZokT8m4+cHxdkcr9KdlwZIOTTwDv37eStTnpZw9StFcUW17UePQ68OC7DyIQBAcnzJcN3M955OrYnCFnYN4/XX5rjnLM2ew6fS7O1gvPf5/rBSKYNTJvKJJbaA5qcmTpl/2rup1EdmWOsgpqJVMjTOY8EdVVm0YtHl2abXPBAVGjdMNYyIcFHBOMlHWmtU5K6DE68BzTUNBHkvd1GRpIstsHmoyWwRJJg/ceB4qGyy2eTvGUr7sSy5h9qQ6yPhAkPVw3H6hd8HmKZ4i+pKjczguM7fA9hXOQoUBw+WcnEbxJ1Jsy/wDcrgftM95SjilBe5fYyifTmmS+kqLOnNRqJ7Mr9q7pqI7MtdaPVNRbsyz7X3Uai3ZlTbGyd6ZGg8+zc6FVbNVAgS2ndVGRpIz7LuqZKtGF9k9VOSjRidYPVQCjZdTxQI7l7KXl+Tq+p10mlA/UV3UN4HketLF4/gvsbgtj5QQHDMcjNPMmKVzu2bL3DycdofQrgmsTZ7uznrtqcvcvpt+COZFQ6ki0yFQXSLTIoyWwUMndMk4LC9BgsdIdNFBbBFkcoBGeVJRsjvKGbZhcd6kqzE4oVL4uIQlH0H7N6pq5Nw0OGhkYZf1OJH0IXfRWII8b1Weu8n7tvkbMtT5wQHKPanh7quO18QYPh249h2g4PZ182kfpK47iOJZPVdDr66LpPmL+j/f3NTa/ULHwPtotc/RVNEY/EUGg8QIChegLHOQGB7kIbI8jkM2yNI5SZtmBzkKtlmqEE/CKUuJX69KAfFsSNjbu4anj5Dj6Jht4RE6ipQdSXC3PpupBHVqw14W7McTAxg6ADQL6aWFg8DObnJyfLMqkqEB4mcMFGOYFPUYALA+JA48njh8949VSpDXHB2WF1/Grqb44fwOIMLmkskaWuaS1zXDQtI3EFfPPcZT3RbK7QIy6ZFdLoVU0UinjKCclfGQZBkCDJhfIpKNkaSRSZtkZ70KNmMu1UlclWbygR1b2N5eMk8uOWGe5GDFW1HF34neg3epXRbwy9R8Trd1pgrePju/wvydbXWeaCAIAd6A5d7TctOrzPxyjGfCeR9ra0fdd/P5Hgfn1XLXp47yPTdGvtS/jz5XHp6GgPdtNXMfePNsOLSoJyYfG7qCdRUTd0J1DxkGoxvmQo5GB8qkzcjGX6qSmSgOpQZNhyhl6zmLFo6VcFrPvTS6bomcz59B1VoQc3hGVzcwtqTqS/pebPozDaNfDaMFOnHsQQMDGN7Dr3X0FFRWEeLq1ZVZucuWSlJmEAQBAWSxslifHKwPY8bLmuGoIPEFCU2nlHF89ZRky/ObVNrn4ZI73TxMJP4Xduh9OPHiq0tG64PYdN6irqOiftr6+/wBTR7Y3arA+kzznP0KkzbLfE7qMDWV8TumCNZY6RTgq5FhdqpwV1FRqVBKyz1sBwa5jWIRUqERkmk+TRzcTyAUpOTwhUqwowdSo8JH0PlHLVTLOFtqV/fld708xG+R39gOQ/wArvpwUFsePvbud1U1PhcLyPdVzkCAIAgCAIDFYgisQvhnY2SJ7S17HjUOHQhHuTGTi1KLw0cdz57P58M8S/gzHz0d7nwje+H92/ULiq0XHePB6qw6rGulTrbS8/B/s5nNGeKwPqSRGOoVjF5RTVCuSnFAXsYShZRybFlbKuI5juCCjF7jT8Wd/3Ix3PXsphCU3hFbi5pWsNVR/14s77lXLNDLVH7PSZtSu0M07h70h/boF3QpqCwjyV3eVLqeqfHgvI9xXOUIAgCAIAgCAIAUBoGcvZtTxYyXMILKl12rnM0+HKfT7p7j5LnqUFLePJ9my6vOliFbvR+q9TjOM4Hdwm26tiFZ8EzeThucOoPAjuFytOLwz0dOVOtDXTeUeY6A68EyOzL4qznOA04nQDqoJjTOk5O9mFvEDHaxvap1DvEOnxZB/5Hnv7LenQct5bI+XedWp0e7R70vPwXqdiwzDqmF02VKFeOCBn3WMH17nuuuMVFYR5qrVnVk5zeWSlJmEAQBAEAQBAEAQBAEBBxfCKGM1TWxKrHPEeAcN7e4PEHuFWUVJYZtRuKtCWum8M0iz7JMJkmL4L1uJh/Adl2nrosHbLwZ9aPXaqXegn8zYMu5IwXAHtmrV/GsjhPY0c5v5eTfQLSFGMdziuupXFwtLeI+S/wBubLotTgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAID//2Q=="
rel="icon">
<style>
:root {
--bs-white: #fff;
}
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
.b-example-divider {
width: 100%;
height: 3rem;
background-color: rgba(0, 0, 0, .1);
border: solid rgba(0, 0, 0, .15);
border-width: 1px 0;
box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
}
.b-example-vr {
flex-shrink: 0;
width: 1.5rem;
height: 100vh;
}
.bi {
vertical-align: -.125em;
fill: currentColor;
}
.nav-scroller {
position: relative;
z-index: 2;
height: 2.75rem;
overflow-y: hidden;
}
.nav-scroller .nav {
display: flex;
flex-wrap: nowrap;
padding-bottom: 1rem;
margin-top: -1px;
overflow-x: auto;
text-align: center;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
.btn-bd-primary {
--bd-violet-bg: #712cf9;
--bd-violet-rgb: 112.520718, 44.062154, 249.437846;
--bs-btn-font-weight: 600;
--bs-btn-color: var(--bs-white);
--bs-btn-bg: var(--bd-violet-bg);
--bs-btn-border-color: var(--bd-violet-bg);
--bs-btn-hover-color: var(--bs-white);
--bs-btn-hover-bg: #6528e0;
--bs-btn-hover-border-color: #6528e0;
--bs-btn-focus-shadow-rgb: var(--bd-violet-rgb);
--bs-btn-active-color: var(--bs-btn-hover-color);
--bs-btn-active-bg: #5a23c8;
--bs-btn-active-border-color: #5a23c8;
}
.bd-mode-toggle {
z-index: 1500;
}
.bd-mode-toggle .dropdown-menu .active .bi {
display: block !important;
}
header{
margin-bottom: 30px;
}
</style>
<!-- Custom styles for this template -->
<link href="/css/carousel.css" rel="stylesheet">
</head>
</html>
header.html에는 html head 부분에서 공통으로 사용할 부분을 가져와줍니다.
여기서 주의할 점은 Bootstrap CDN 링크 같은 경우 index.html은 static 폴더에 있기 때문에 <script src="js/color-modes.js"></script> 이런식으로 바로 js 폴더 부터 찾았지만 다른 html 파일들은 전부 templates 파일에 저장하기 때문에 앞에 /을 붙여야합니다.
th:fragment 다음에 오는 fragment-header는 fragment 이름이고 괄호 안에 있는 값은 fragment 변수로 값을 전달해주는 기능을 합니다.
nav바 아래 공백을 위해 margin 값도 추가를 해줍니다.
<!DOCTYPE html>
<html data-bs-theme="auto" lang="ko" xmlns:th="http://www.thymeleaf.org">
<body>
<header data-bs-theme="dark" th:fragment="fragment-nav">
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">Carousel</a>
<button aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation"
class="navbar-toggler"
data-bs-target="#navbarCollapse" data-bs-toggle="collapse" type="button">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav me-auto mb-2 mb-md-0">
<li class="nav-item">
<a aria-current="page" class="nav-link active" href="#">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item">
<a aria-disabled="true" class="nav-link disabled">Disabled</a>
</li>
</ul>
<form class="d-flex" role="search">
<input aria-label="Search" class="form-control me-2" placeholder="Search" type="search">
<button class="btn btn-outline-success" type="submit">Search</button>
</form>
</div>
</div>
</nav>
</header>
</body>
</html>
<!DOCTYPE html>
<html data-bs-theme="auto" lang="ko" xmlns:th="http://www.thymeleaf.org">
<body>
<footer class="container" th:fragment="fragment-footer">
<p class="float-end"><a th:href="@{#}">Back to top</a></p>
<p>© 2017–2023 Company, Inc. · <a href="#">Privacy</a> · <a href="#">Terms</a></p>
</footer>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<body>
<!--다크 모드 설정 이미지-->
<svg class="d-none" xmlns="http://www.w3.org/2000/svg" th:fragment="fragment-image">
<symbol id="check2" viewBox="0 0 16 16">
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
</symbol>
<symbol id="circle-half" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 0 8 1v14zm0 1A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/>
</symbol>
<symbol id="moon-stars-fill" viewBox="0 0 16 16">
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"/>
<path d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z"/>
</symbol>
<symbol id="sun-fill" viewBox="0 0 16 16">
<path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"/>
</symbol>
</svg>
<!--다크 모드 버튼-->
<div class="dropdown position-fixed bottom-0 end-0 mb-3 me-3 bd-mode-toggle" th:fragment="fragment-button">
<button aria-expanded="false"
aria-label="Toggle theme (auto)"
class="btn btn-bd-primary py-2 dropdown-toggle d-flex align-items-center"
data-bs-toggle="dropdown"
id="bd-theme"
type="button">
<svg class="bi my-1 theme-icon-active" height="1em" width="1em">
<use href="#circle-half"></use>
</svg>
<span class="visually-hidden" id="bd-theme-text">Toggle theme</span>
</button>
<ul aria-labelledby="bd-theme-text" class="dropdown-menu dropdown-menu-end shadow">
<li>
<button aria-pressed="false" class="dropdown-item d-flex align-items-center" data-bs-theme-value="light"
type="button">
<svg class="bi me-2 opacity-50 theme-icon" height="1em" width="1em">
<use href="#sun-fill"></use>
</svg>
Light
<svg class="bi ms-auto d-none" height="1em" width="1em">
<use href="#check2"></use>
</svg>
</button>
</li>
<li>
<button aria-pressed="false" class="dropdown-item d-flex align-items-center" data-bs-theme-value="dark"
type="button">
<svg class="bi me-2 opacity-50 theme-icon" height="1em" width="1em">
<use href="#moon-stars-fill"></use>
</svg>
Dark
<svg class="bi ms-auto d-none" height="1em" width="1em">
<use href="#check2"></use>
</svg>
</button>
</li>
<li>
<button aria-pressed="true" class="dropdown-item d-flex align-items-center active"
data-bs-theme-value="auto"
type="button">
<svg class="bi me-2 opacity-50 theme-icon" height="1em" width="1em">
<use href="#circle-half"></use>
</svg>
Auto
<svg class="bi ms-auto d-none" height="1em" width="1em">
<use href="#check2"></use>
</svg>
</button>
</li>
</ul>
</div>
</body>
</html>
<!doctype html>
<html data-bs-theme="auto" lang="ko" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{/fragments/header.html :: fragment-header(게시판)}"></head>
<body>
<!--다크 모드 설정 이미지-->
<svg th:replace="~{/fragments/darkmode.html :: fragment-image}"></svg>
<!--다크 모드 버튼-->
<div th:replace="~{/fragments/darkmode.html :: fragment-button}"></div>
<header th:replace="~{/fragments/nav.html :: fragment-nav}"></header>
<main>
<div class="container">
<h2 th:marginheight="30">게시판</h2>
<table class="table">
<thead class="table-dark">
<tr>
<th scope="col">#</th>
<th scope="col">First</th>
<th scope="col">Last</th>
<th scope="col">Handle</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr>
<th scope="row">1</th>
<td>Mark</td>
<td>Otto</td>
<td>@mdo</td>
</tr>
<tr>
<th scope="row">2</th>
<td>Jacob</td>
<td>Thornton</td>
<td>@fat</td>
</tr>
<tr>
<th scope="row">3</th>
<td colspan="2">Larry the Bird</td>
<td>@twitter</td>
</tr>
</tbody>
</table>
</div>
<!-- FOOTER -->
<footer th:replace="~{/fragments/footer.html :: fragment-footer}"></footer>
</main>
<!--JS CDN-->
<script crossorigin="anonymous"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
fragment를 전부 만들었으면 frament를 사용하는 html 파일에서는 th:replace로 가져와서 사용하면 됩니다.
HeidiSQL을 사용하여 springmvc라는 DB를 생성해줍니다.

새로운 DB 생성 후 사용자를 추가해줍니다.

위 사진에 있는 사람 그림을 클릭하여 myadmin이라는 이름으로 springmvc에 대한 전체권한을 가지는 사용자를 만들어줍니다.

사용자를 생성 후 myadmin 계정으로 DB에 로그인을 해줍니다.


id, title, content 값을 가지는 테이블을 생성해줍니다. 여기서 id 값은 pk로 설정을 해야 됩니다.

id값에 AUTO_INCREMENT를 적용해줍니다.
build.gardle에서 주석 처리를 했던 의존성을 풀어줍니다.
그 후 DB를 사용하기 위해 application.properties에 DB이름, 사용자, 비밀번호, 드라이버를 추가합니다.
spring.datasource.url=jdbc:mariadb://localhost:3306/springmvc
spring.datasource.username=myadmin
spring.datasource.password=(비밀번호)
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
여기서 비밀번호를 바로 작성하면 github에 업로드시 비밀번호가 유출되는 문제가 발생할 수 있습니다. 이를 예방하기 위해서는 민감한 정보들을 환경변수로 등록을 해줍니다.
IntelliJ에서 Edit Configurations에 들어가서 Modify Options을 클릭 후
Environment Variables를 클릭해줍니다. 그 후 $ 표시를 클릭하여 name과 value를 저장해줍니다.


name과 value를 설정했으면 이제 application.properties에 url과 비번 등을 직접 넣지 않고 변수로 넣어줍니다.
server.port = 8088
spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USER}
spring.datasource.password=${DB_PASSWORD}
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
springmvcboard 아래에 model 패키지를 생성한 후 board 클래스를 만들어줍니다.
package toyproject.springmvcboard.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Data;
@Entity
@Data
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String title;
private String content;
}
@Entity 어노테이션은 클래스가 JPA 엔티티로 지정됨을 나타냅니다. 엔티티 클래스는 데이터베이스 테이블과 매핑되는 클래스로서, 해당 클래스의 객체는 데이터베이스 레코드와 상호작용할 수 있습니다.
@Data 어노테이션은 Lombok 라이브러리에서 제공되는 어노테이션으로, Getter, Setter, equals(), hashCode(), toString() 메서드 등을 자동으로 생성합니다. 이것은 코드의 간결성과 가독성을 향상시키는 데 도움이 됩니다.
@Id 어노테이션은 엔티티 클래스에서 기본 키(primary key) 필드를 나타냅니다. 기본 키는 각 레코드를 고유하게 식별하는 데 사용되며 데이터베이스 테이블의 기본 키에 해당하는 필드입니다.
@GeneratedValue 어노테이션은 기본 키의 값을 자동으로 생성하는 방법을 정의합니다. strategy 속성을 사용하여 생성 전략을 지정할 수 있으며, GenerationType.IDENTITY는 데이터베이스에서 기본 키 값을 자동으로 생성하는 방식 중 하나입니다. 일반적으로 이 방식은 자동 증가 열 (auto-increment)을 사용하는 데이터베이스에서 사용됩니다.
Srping Data JPA를 사용하기 전에 repository 폴더를 생성해줍니다.
respository 폴더에 BoardRepository 인터페이스를 생성합니다.
package toyproject.springmvcboard.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import toyproject.springmvcboard.model.Board;
public interface BoardRepository extends JpaRepository<Board, Long> {
}
JpaRepository<T, ID>에서 T는 엔터티의 클래스를 의미하고, ID는 엔터티 클래스 기본 키의 데이터 타입을 의미합니다.
Spring Data JPA를 사용하면 간단한 CRUD 쿼리는 직접 작성하지 않아도 사용할 수 있어 생산성이 향상 되고 복잡한 쿼리를 간소화 할 수 있으며 유지 보수성을 향상 시켜주는 장점이 있습니다.
이전에 만들어둔 list.html을 수정해줍니다.
<main>
<div class="container">
<h2 th:marginheight="30">게시판</h2>
<table class="table">
<thead class="table">
<tr>
<th scope="col">번호</th>
<th scope="col">제목</th>
<th scope="col">작성자</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr th:each="board : ${boards}">
<td th:text="${board.id}"></td>
<td th:text="${board.title}"></td>
<td>홍길동</td>
</tr>
</tbody>
</table>
</div>
<!-- FOOTER -->
<footer th:replace="~{/fragments/footer.html :: fragment-footer}"></footer>
</main>
java - toyproject.springmvcboard 폴더 아래에 controller 폴더를 생성한 뒤 BoardController 클래스를 생성해줍니다.
package toyproject.springmvcboard.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import toyproject.springmvcboard.model.Board;
import toyproject.springmvcboard.repository.BoardRepository;
import java.util.List;
@Controller
@RequestMapping("/board")
public class BoardController {
private final BoardRepository boardRepository;
public BoardController(BoardRepository boardRepository) {
this.boardRepository = boardRepository;
}
@GetMapping("/list")
public String list(Model model) {
List<Board> boards = boardRepository.findAll();
model.addAttribute("boards", boards);
return "board/list";
}
}
Controller 어노테이션은 @Component를 구체화하여 만든 어노테이션으로 해당 클래스를 IoC컨테이너에 Bean으로 등록해주며 Controller로 사용된다는 것을 알려줍니다.
데이터를 확인하기 전에 현재 테이블에 아무런 정보가 없기 때문에 데이터를 넣어줍니다.
INSERT INTO board VALUES(1, 'dd', 'dddd');로 값을 삽입한 후 게시판을 확인해줍니다.

개발한 기능을 테스트하기 위해 main 메소드에서 실행하거나 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행하는 건 시간이 오래 걸리고, 반복 실행이 어렵고, 여러 테스트를 한 번에 실행하기 어렵기 때문에 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결해줍니다.
테스트 클래스의 메소드들은 한국어로 적어도 됩니다. 빌드할 때 테스트 코드는 실제 코드에 포함되지 않기 때문에 한국어로 쓰면 직관적으로 알아볼 수 있습니다.
테스트 케이스를 작성하기 전에 먼저 Board 클래스를 수정해줍니다.
처음에 Board 클래스를 생성했을 때는 @Data를 사용하여 Getter, Setter를 사용하려 했습니다. 하지만 Setter의 사용은 단점이 있습니다.
이러한 이유로 인해 Setter 대신에 Builder Patton을 사용해 문제를 해결해줍니다.
package toyproject.springmvcboard.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String title;
private String content;
@Builder
public Board(String title, String content) {
this.title = title;
this.content = content;
}
}
테스트 코드를 작성할 때 Given-When-Then Pattern 을 사용해줍니다.
package toyproject.springmvcboard.repository;
import jakarta.transaction.Transactional;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import toyproject.springmvcboard.model.Board;
import java.util.List;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
@SpringBootTest
@Transactional
class BoardRepositoryTest {
@Autowired
private BoardRepository boardRepository;
@BeforeEach
void beforeEach() {
boardRepository.deleteAll();
}
@AfterEach
void afterEach() {
boardRepository.deleteAll();
}
@Test
void 게시글_저장확인() {
//given
String title = "제목";
String content = "내용";
boardRepository.save(Board.builder()
.title(title)
.content(content)
.build());
//when
List<Board> boardList = boardRepository.findAll();
//then
Board board = boardList.get(0);
assertThat(board.getTitle()).isEqualTo(title);
assertThat(board.getContent()).isEqualTo(content);
}
}
@SpringBootTest는
Spring Boot 애플리케이션의 통합 테스트를 위한 어노테이션입니다. 이 어노테이션을 사용하면 Spring Boot 애플리케이션의 컨텍스트를 설정하고 필요한 빈들을 로드하여 테스트를 수행할 수 있습니다.
@Transactional어노테이션은
테스트 클래스 또는 메서드에 적용되며, 트랜잭션 관리를 활성화합니다. 일반적으로 테스트 메서드가 실행될 때 데이터베이스 트랜잭션이 시작되고, 테스트가 완료될 때 자동으로 롤백됩니다. 이것은 테스트에서 데이터베이스의 일시적인 변경을 보호하고 테스트 간의 상태 오염을 방지하는 데 유용합니다.
@BeforeEach어노테이션은
JUnit 테스트에서 사용되며, 각 테스트 메서드가 실행되기 전에 해당 어노테이션이 붙은 메서드가 실행됩니다. 주로 초기화 작업을 수행하기 위해 사용됩니다.
@AfterEach어노테이션은
JUnit 테스트에서 사용되며, 각 테스트 메서드가 실행된 후에 해당 어노테이션이 붙은 메서드가 실행됩니다. 주로 정리 작업을 수행하기 위해 사용됩니다.
assertThat은 테스트 프레임워크에서 사용되는 어설션 라이브러리 중 하나입니다. 주로 테스트 결과를 검증하는 데 사용됩니다. 예를 들어, 특정 값이 예상한 값과 일치하는지 확인하거나 컬렉션의 크기를 확인하는 데 사용됩니다. assertThat을 사용하면 테스트 결과를 더 가독성 있게 검증할 수 있습니다. JUnit, AssertJ, Hamcrest와 같은 라이브러리에서 제공됩니다.
테스트 코드에서는 의존성을 주입할 때 @Autowired로 주입을 해도 되고 public 접근자를 굳이 사용하지 않아도 됩니다.
위 테스트 코드에서 @Transactional을 사용하여 DB의 변경을 막고 테스트를 위해 @BeforeEach와 @AfterEach를 사용하여 원하는 값으로 테스트를 할 수 있게해줍니다.

AOP (Aspect-Oriented Programming)는
소프트웨어 개발에서 모듈화와 코드 재사용을 개선하기 위한 프로그래밍 패러다임 중 하나입니다.
AOP라는 패키지를 생성한 후 TimeTraceAop 클래스를 작성해줍니다.
package toyproject.springmvcboard.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class TimeTraceAop {
//toyproject.springmvcboard 패키지 하위에 있는 모든 것에 적용
@Around("execution(* toyproject.springmvcboard..*(..))")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
//어떤 메소드를 호출하는지 확인
System.out.println("START: " + joinPoint.toString());
try {
//다음 메소드로 진행
return joinPoint.proceed();
} finally {
long finish = System.currentTimeMillis();
long timeMs = finish - start;
System.out.println("END: " + joinPoint.toString() + " " + timeMs + "ms");
}
}
}
컴포넌트 스캔 방식으로 작성해줍니다.
컴포넌트 스캔(Component Scanning)은 스프링 프레임워크에서 사용되는 중요한 기능 중 하나로, 자동으로 빈(Bean)을 찾아 스프링 애플리케이션 컨텍스트에 등록하는 프로세스를 가리킵니다. 스프링 애플리케이션의 컴포넌트를 찾아 등록하는 과정에서 XML 구성 파일을 사용하지 않고도 애플리케이션을 구성할 수 있게 해줍니다.

게시판 조회 시 위에 사진처럼 어떤 메소드를 실행하는지와 실행 시간을 알 수 있게 됩니다.