이 글은 이동욱 님의 스프링 부트와 AWS로 혼자 구현하는 웹 서비스를 읽고 정리한 글입니다.
1. 서버 템플릿 엔진과 머스테치 소개
1) 서버 템플릿 엔진과 클라이언트 템플릿 엔진
먼저 서버 템플릿 엔진이란 지정된 템플릿 양식과 데이터가 합쳐져 HTML 문서를 출력하는 소프트웨어를 말한다.
서버 템플릿 엔진을 이용한 화면 생성은 서버에서 JAVA 코드로 문자열을 만든 뒤 이 문자열을 HTML로 변환하여 브라우저로 전달한다.
반면에 클라이언트 템플릿 엔진은 서버 템플릿과 마찬가지로 지정된 템플릿 양식과 데이터가 합쳐져 HTML 문서를 출력하지만 브라우저 위에서 작동한다. 즉, 브라우저 위에서 작동하기 때문에 서버 템플릿 엔진에서 제어할 수가 없다. 그렇기 때문에 서버에서 Json, Xml 형식의 데이터만 전달해 주고 클라이언트에서 코드와 조립한다.
2) 머스테치란
머스테치는 루비, JS, Python, 자바, Go 등 현존하는 대부분의 언어를 지원하고 있다. 그러다보니 자바에서는 서버 템플릿 엔진으로, JS에서는 클라이언트 템플릿 엔진으로 모두 사용할 수 있다.
자바 진영에는 JSP, Velocity, Freemarker, Thymleaf 등 다양한 서버 템플릿 엔진이 존재한다.
하지만 각자 단점을 가지고 있다.
먼저 JSP, Velocity는 스프링부트에서 권장하지 않는 템플릿 엔진이고, Freemarker는 높은 자유도를 가지고 있어, 내부에 비즈니스 로직이 포함될 가능성이 있다. 마지막으로 Thymleaf는 스프링 진영에서 적극적으로 밀고 있지만 문법이 어려운 경향이 있다.
이에 반해 머스테치는 문법이 간단하고, 로직 코드를 사용할 수 없어 서버와 View의 역할이 명확히 구분되고, 하나의 문법으로 클라이언트/서버 템플릿을 모두 사용하다는 장점을 가지고 있다.
3) 머스테치 플러그인 설치
앞서 설명한 장점들 외에도 머스테치는 인텔리제이 커뮤니티 버전에서도 사용할 수 있다는 장점을 가지고 있다. Thymleaf와 JSP는 커뮤니티 버전에는 플러그인이 존재하지 않는다.
IntelliJ의 마켓플레이스에서 Handlebars/Mustache를 설치한 후 IntelliJ를 재시작하여 플러그인이 작동하는 것을 확인하자.
2. 기본 페이지 만들기
1) 의존성 추가
스프링 부트 프로젝트에 머스테치를 편하게 사용할 수 있도록 머스테치 의존성을 build.gradle에 추가한다.
//build.gradle
compile('org.springframework.boot:spring-boot-starter-mustache')
2) index.mustache 생성
머스테치의 파일 위치는 기본적으로 src/main/resources/templates다. 이 위치에 머스테치 파일을 두면 스프링 부트에서 자동으로 로딩한다. 첫 페이지를 담당할 index.mustache를 이 위치에 생성한다.
{{!index.mustache}}
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>스프링 부트 웹 서비스</title>
</head>
<body>
<h1>스프링 부트로 시작하는 웹 서비스</h1>
</body>
</html>
간단하게 h1 크기로 "스프링 부트로 시작하는 웹 서비스"를 출력하는 페이지 코드이다.
이 머스테치에 URL을 매핑한다. URL 매핑은 당연하게도 Controller에서 진행한다.
3) IndexController 클래스 생성
web 패키지 내에 IndexController 클래스를 생성한다.
// IndexController.java
package com.cotato.study.SpringnAWS.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class IndexController {
@GetMapping("/")
public String index(){
return "index";
}
}
머스테치 스타더가 컨트롤러에서 문자열이 반환되면 앞의 경로와 뒤의 파일 확장자를 자동으로 지정해 준다.
즉, 위 코드에서의 반환되는 문자열이 "index"이므로 src/main/resources/templates/index.mustache로 전환되어 View Resolver가 처리해 준다.
이제 테스트 코드로 검증해 보자.
4) IndexControllerTest 클래스 생성
test 패키지 하단의 web 패키지에 IndexControllerTest 클래스를 생성한다.
// IndexControllerTest.java
package com.cotato.study.SpringnAWS.web;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class IndexControllerTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
public void 메인페이지_로딩(){
//when
String body = this.restTemplate.getForObject("/", String.class);
//then
assertThat(body).contains("스프링 부트로 시작하는 웹 서비스");
}
}
실제로 URL 호출 시에 페이지의 내용이 의도한 대로 호출되었는지에 대한 테스트이다.
메인페이지_로딩() 메서드를 실행하면 다음과 같이 테스트를 통과함을 알 수 있다.
직접 눈으로 화면이 잘 나오는지 확인해 보자. main 메서드를 실행하고, 브라우저에서 http://localhost:8080으로 접속해 보자.
정상적으로 화면이 생성됨을 확인할 수 있다.
3. 게시글 등록 화면 만들기
여기서는 게시글 등록 화면을 만들어 볼 것이다. 3장에서 API를 구현하였으니, 여기서는 이를 사용할 화면을 개발하게 보겠다.
HTML보다 더 시각적으로 화려하게 보이게 하기 위해, 오픈소스인 부트스트랩을 사용하자. 원래 부트스트랩과 같은 다른 외부 라이브러리를 사용할 수 있는 방법에는 두 가지 방법이 존재한다. 하나는 외부 CDN을 사용하는 것이고, 다른 하나는 직접 라이브러리를 받아 사용하는 것이다.
여기서는 외부 CDN을 사용하겠다. 이 방법이 훨씬 편리하고 간단하기 때문이다. 하지만 실제 서비스에선 CDN 서버에 우리의 서비스가 의존하게 된다는 단점이 있어 직접 다운로드하는 방식을 사용한다.
부트스트랩과 제이쿼리를 index.mustache에 추가해야 한다. 하지만 여기서는 바로 추가하는 것이 아니라 레이아웃 방식을 사용하겠다. 레이아웃 방식은 공통 영역을 별도의 파일로 분리하여 필요한 곳에 가져다 쓰는 방식을 이야기한다.
이번에 추가할 부트스트랩과 제이쿼리는 머스테치 화면 어디서나 필요하기 때문에 매 파일마다 이들을 추가하는 것은 귀찮은 일이니 레이아웃 파일을 추가하여 사용한다.
1) header/footer.mustache 파일 생성
src/main/resources/templates에 layout 디렉토리를 추가로 생성하고 header.mustache, footer.mustache 파일을 생성한다.
{{!header.mustache}}
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>스프링 부트 웹 서비스</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
</head>
<body>
{{!footer.mustache}}
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
</body>
</html>
위의 코드들을 보면 css와 js의 위치가 다름을 알 수 있다. 페이지 로딩속도를 높이기 위해 css는 header에, js는 footer에 두었다.
HTML은 위에서부터 실행되기 때문에 header가 다 실행되고, body가 실행된다. 즉, head가 다 불러지지 않으면 사용자 입장에선 백지 화면만 보게 될 수 있다. head에 용량이 큰 js가 위치하면 body의 실행이 늦어지게 되므로 js는 body 하단에 두어 화면이 다 그려진 뒤에 호출하는 것이 좋다.
반면 css는 화면을 그리는 역할을 하기 때문에 head에서 불러오는 것이 좋다. 그렇지 않으면 사용자 입장에서는 css가 적용되지 않은 깨진 화면을 보게 된다.
추가적으로 bootstrap.js는 제이쿼리에 의존하기 때문에 부트스트랩보다 먼저 호출되도록 코드를 작성했다.
2) index.mustache 파일 변경
라이브러리 등 다른 HTML 태그들이 모두 레이아웃에 추가되었기 때문에 index.mustache에는 필요한 코드만 남아도 된다.
{{!index.mustache}}
{{>layout/header}}
<h1>스프링 부트로 시작하는 웹 서비스</h1>
{{>layout/footer}}
{{> }}는 현재 머스테치 파일을 기준으로 다른 파일을 가져온다는 의미이다.
레이아웃으로 파일을 분리했다. 이제 글 등록 버튼을 하나 추가해 보겠다.
{{!index.mustache}}
{{>layout/header}}
<h1>스프링 부트로 시작하는 웹 서비스</h1>
<div class="col-md-12">
<div class="row">
<div class="col-md-6">
<a href="/posts/save" role="button" class="btn btn-primary">글 등록</a>
</div>
</div>
</div>
{{>layout/footer}}
<a> 태그를 이용해 글 등록 페이지로 이동하는 버튼을 생성했다. 이동할 페이지의 주소는 href에 적힌 대로 /posts/save이다.
3) 게시글 등록 화면 구현
이 주소에 해당하는 컨트롤러를 IndexController에 생성하겠다.
// IndexController.java
@RequiredArgsConstructor
@Controller
public class IndexController {
...
@GetMapping("/posts/save")
public String postsSave(){
return "posts-save";
}
}
/posts/save를 호출하면 posts-save.mustache를 호출하는 메소드가 추가되었다. 이제 호출될 post-save.mustache를 생성하자. 파일의 위치는 index.mustache와 동일하다.
{{!posts-save.mustache}}
{{>layout/header}}
<h1>스프링 부트로 시작하는 웹 서비스</h1>
<div class="col-md-12">
<div class="col-md-4">
<form>
<div class="form-group">
<label for="title">제목</label>
<input type="text" class="form-control" id="title" placeholder="제목을 입력하세요">
</div>
<div class="form-group">
<label for="author">작성자</label>
<input type="text" class="form-control" id="title" placeholder="작성자를 입력하세요">
</div>
<div class="form-group">
<label for="title">내용</label>
<input type="text" class="form-control" id="title" placeholder="내용을 입력하세요">
</div>
</form>
<a href="/" role="button" class="btn btn-secondary">취소</a>
<button type="button" class="btn btn-primary" id="btn-save">등록</button>
</div>
</div>
{{>layout/footer}}
UI가 완성되었으니 다시 main 메서드를 실행하고 브라우저에서 http://localhost:8080/로 접근해 보자.
글 등록 버튼을 클릭하면 글 등록 화면(/posts/save)으로 넘어간다.
하지만 등록 버튼을 눌러도 아무런 반응이 없다. 이는 API를 호출하는 JS가 없기 때문이다. 이제 JS를 등록해 주자.
4) index.js 파일 생성
src/main/resources에 static/js/app 디렉토리를 생성한 후, 여기에 index.js까지 생성한다.
// index.js
var main = {
init: function () {
var _this = this;
$('#btn-save').on('click', function () {
_this.save();
});
},
save: function () {
var data = {
title: $('#title').val(),
author: $('#author').val(),
content: $('#content').val()
};
$.ajax({
type: 'POST',
url: '/api/v1/posts',
dataType: 'json',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(data)
}).done(function () {
alert('글이 등록되었습니다.');
window.location.href = '/'; // 글 등록이 성공하면 메인페이지(/)로 이동
}).fail(function (error) {
alert(JSON.stringify(error));
});
}
};
main.init();
첫 문장에서 var main = {...}라는 코드를 선언했는데 이는 브라우저의 스코프가 공용 공간이기 때문이다. 예를 들어 index.mustache에 a.js라는 파일이 추가되었다고 하자. a.js도 init과 save 함수를 가지게 된다면 나중에 로드된 a.js가 먼저 로드된 index.js의 init, save 함수를 모두 덮어쓰게 된다. 이런 문제를 방지하기 위해 index.js 만의 스코프(유효 범위)를 만들어 사용한다. 이렇게 하면 index 객체 안에서만 function이 유효하기 때문에 다른 JS와 겹쳐질 위험이 사라진다.
머스테치 파일들이 index.js를 사용할 수 있게 footer.mustache에 추가하자.
{{!footer.mustache}}
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
{{!index.js 사용}}
<script src="/js/app/index.js"></script>
</body>
</html>
스프링 부트는 기본적으로 /src/main/resources/static에 위치한 자바스크립트, CSS, 이미지 등의 정적 파일들은 URL에서 자동으로 /로 설정된다.
5) 등록 기능 테스트
이제 등록 기능을 브라우저에서 직접 테스트를 해보자.
위와 같이 입력한 후 등록 버튼을 클릭하면 아래와 같은 Alert가 노출된다.
이제 H2 데이터베이스에서도 실제로 등록되었는지 확인해 보자.
등록 기능이 정상적으로 작동하는 것을 확인했다.
4. 전체 조회 화면 만들기
1) index.mustache 변경
전체 조회를 위해 index.mustache의 UI를 변경하겠다.
{{!index.mustache}}
{{>layout/header}}
<h1>스프링 부트로 시작하는 웹 서비스 Ver.2</h1>
<div class="col-md-12">
<div class="row">
<div class="col-md-6">
<a href="/posts/save" role="button" class="btn btn-primary">글 등록</a>
</div>
</div>
<br>
{{!목록 출력 영역}}
<table class="table table-horizontal table-bordered">
<thead class="thead-strong">
<tr>
<th>게시글 번호</th>
<th>제목</th>
<th>작성자</th>
<th>최종수정일</th>
</tr>
</thead>
<tbody id="tbody">
{{#posts}} {{!posts라는 list를 순회, for문}}
<tr>
<td>{{id}}</td> {{!List에서 뽑아낸 객체의 필드 id를 사용}}
<td>{{title}}</td> {{!List에서 뽑아낸 객체의 필드 title을 사용}}
<td>{{author}}</td> {{!List에서 뽑아낸 객체의 필드 author를 사용}}
<td>{{modeifiedDate}}</td> {{!List에서 뽑아낸 객체의 필드 modifiedDate를 사용}}
</tr>
{{/posts}}
</tbody>
</table>
</div>
{{>layout/footer}}
2) PostsRepository 인터페이스 변경
이제 UI는 완성되었으니 Controller, Repository, Service 코드를 작성하겠다. 먼저 PostsRepository 인터페이스에 쿼리를 추가한다.
// PostsRepository.java
package com.cotato.study.SpringnAWS.domain.posts;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
public interface PostsRepository extends JpaRepository<Posts, Long> {
@Query("SELECT p FROM Posts p ORDER BY p.id DESC") // SpringDataJpa에서 제공하지 않는 메서드는 직접 쿼리로 작성
List<Posts> findAllDesc();
}
3) PostsService 클래스 변경
다음으로 PostsService에 코드를 추가하겠다.
//PostsService.java
...
import java.util.List;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@Service
public class PostsService {
private final PostsRepository postsRepository;
...
@Transactional(readOnly = true)
public List<PostsListReponseDto> findAllDesc(){
return postsRepository.findAllDesc().stream() // postsRepository의 결과로 넘어온 Posts의 stream을
.map(PostsListReponseDto::new) // map을 통해 PostsListResponseDto로 변환하고
.collect(Collectors.toList()); // 이를 List로 반환하는 메소드
}
}
@Transactional의 (readOnly=true) 옵션은 트랜잭션 범위는 유지하되 조회 기능을 남겨두어 조회 속도가 개선되기 때문에 등록, 수정, 삭제 기능이 전혀 없는 서비스 메서드에서 사용하면 좋다.
4) PostsListResponseDto 클래스 생성
위 코드의 PostsListResponseDto 클래스는 아직 생성되지 않았기 때문에 이를 dto 패키지에 생성한다.
// PostsListResponseDto.java
package com.cotato.study.SpringnAWS.web.dto;
import com.cotato.study.SpringnAWS.domain.posts.Posts;
import lombok.Getter;
import java.time.LocalDateTime;
@Getter
public class PostsListReponseDto {
private Long id;
private String title;
private String author;
private LocalDateTime modifiedDate;
public PostsListReponseDto(Posts entity){
this.id = entity.getId();
this.title = entity.getTitle();
this.author = entity.getAuthor();
this.modifiedDate = entity.getModifiedDate();
}
}
5) IndexController 클래스 변경
마지막으로 IndexController를 변경한다.
package com.cotato.study.SpringnAWS.web;
import com.cotato.study.SpringnAWS.service.posts.PostsService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@RequiredArgsConstructor
@Controller
public class IndexController {
private final PostsService postsService;
@GetMapping("/")
public String index(Model model) { // Model : 서버 템플릿 엔진에서 사용할 수 있는 객체
model.addAttribute("posts", postsService.findAllDesc());
return "index";
}
@GetMapping("/posts/save")
public String postsSave(){
return "posts-save";
}
}
6) 등록 기능 테스트
이제 다시 localhost:8080/로 접속해서 등록 화면을 사용해 하나의 데이터를 등록하고, 다음과 같이 정상적으로 작동하는지 확인한다.
5. 게시글 수정, 삭제 화면 만들기
마지막으로 게시글을 수정하고 삭제하는 화면을 만들어 보겠다.
1) 게시글 수정
먼저 게시글 수정 화면 머스테치 파일을 생성한다.
{{!posts-update.mustache}}
{{>layout/header}}
<h1>게시글 수정</h1>
<div class="col-md-12">
<div class="col-md-4">
<form>
<div class="form-group">
<label for="id">글 번호</label>
{{!readonly는 input 태그를 수정할 수 없고 오로지 읽기만 가능하도록 함}}
<input type="text" class="form-control" id="id" value="{{post.id}}" readonly>
</div>
<div class="form-group">
<label for="title">제목</label>
<input type="text" class="form-control" id="title" value="{{post.title}}">
</div>
<div class="form-group">
<label for="author">작성자</label>
<input type="text" class="form-control" id="author" value="{{post.author}}" readonly>
</div>
<div class="form-group">
<label for="content">내용</label>
<textarea class="form-control" id="content">{{post.content}}</textarea>
</div>
</form>
<a href="/" role="button" class="btn btn-secondary">취소</a>
<button type="button" class="btn btn-primary" id="btn-update">수정 완료</button>
</div>
</div>
{{>layout/footer}}
그리고 btn-update를 id로 가지는 버튼을 클릭하면 update 기능을 호출할 수 있게 index.js 파일에도 update function 하나를 추가한다.
// index.js
var main = {
init : function () {
...
// btn-update라는 id를 가진 HTML 엘리먼트에 click 이벤트가 발생할 때 update function 실행
$('#btn-update').on('click', function () {
_this.update();
});
},
...
update : function () {
var data = {
title: $('#title').val(),
content: $('#content').val()
};
var id = $('#id').val();
$.ajax({
type: 'PUT',
url: '/api/v1/posts/'+id, // 어떤 글을 수정하는지 URL Path로 구분하기 위해 Path에 id를 추가
dataType: 'json',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(data)
}).done(function () {
alert('글이 수정되었습니다.');
window.location.href = '/';
}).fail(function (error) {
alert(JSON.stringify(error));
});
}
};
main.init();
마지막으로 전체 목록에서 수정 페이지로 이동할 수 있게 페이지 이동 기능을 추가하겠다. <a> 태그를 활용하여 타이틀을 클릭하면 해당 게시글의 수정 화면으로 이동하게 하였다.
...
<tbody id="tbody">
{{#posts}} {{!posts라는 list를 순회, for문}}
<tr>
<td>{{id}}</td> {{!List에서 뽑아낸 객체의 필드 id를 사용}}
<td><a href="/posts/update/{{id}}">{{title}}</a></td> {{!List에서 뽑아낸 객체의 필드 title을 사용}}
<td>{{author}}</td> {{!List에서 뽑아낸 객체의 필드 author를 사용}}
<td>{{modifiedDate}}</td> {{!List에서 뽑아낸 객체의 필드 modifiedDate를 사용}}
</tr>
{{/posts}}
...
UI 작업을 다 마쳤으니 이제 수정 화면을 연결할 IndexController 코드를 작업하자.
// IndexController.java
...
@RequiredArgsConstructor
@Controller
public class IndexController {
...
@GetMapping("/posts/update/{id}")
public String postsUpdate(@PathVariable Long id, Model model){
PostsReponseDto dto = postsService.findById(id);
model.addAttribute("post", dto);
return "posts-update";
}
}
이렇게 게시글 수정을 위한 모든 구성을 마쳤다. 수정 기능을 한 번 사용해 보자.
메인화면으로 이동하여 타이틀 항목에 링크 표시가 된 것을 확인할 수 있다.
해당 링크를 클릭하면 수정 페이지로 이동한다. 글 번호와 작성자가 읽기 전용 상태인 것을 확인하고, 제목과 내용을 수정한다.
수정 완료 버튼을 누르면 수정 완료 메시지가 나타나며, 제목이 테스트2로 잘 변경됨을 확인할 수 있다.
2) 게시글 삭제
삭제 기능을 구현해 보자. 삭제는 본문을 확인하고 진행해야 하기 때문에, 수정 화면에 추가하겠다.
{{!posts-update.mustache}}
...
<a href="/" role="button" class="btn btn-secondary">취소</a>
<button type="button" class="btn btn-primary" id="btn-update">수정 완료</button>
<button type="button" class="btn btn-danger" id="btn-delete">삭제</button>
</div>
</div>
{{>layout/footer}}
삭제 이벤트를 진행할 JS 코드도 추가한다.
// index.js
var main = {
init : function () {
...
$('#btn-delete').on('click', function () {
_this.delete();
});
},
...
delete: function () {
var id = $('#id').val();
$.ajax({
type: 'DELETE',
url: '/api/v1/posts/'+id,
dataType: 'json',
contentType: 'application/json; charset=utf-8'
}).done(function () {
alert('글이 삭제되었습니다.');
window.location.href = '/';
}).fail(function (error) {
alert(JSON.stringify(error))
})
}
};
main.init();
UI 쪽 코드 작성을 마쳤다. 이제 삭제 API를 만들어보자. 먼저 서비스 메소드 코드부터 추가하겠다.
//PostsService.java
...
@RequiredArgsConstructor
@Service
public class PostsService {
...
@Transactional
public void delete(Long id){
Posts posts = postsRepository.findById(id)
.orElseThrow(() -> new
IllegalArgumentException("해당 게시글이 없습니다. id=" + id));
postsRepository.delete(posts); // JpaRepository에서 제공하는 delete 메서드를 활용
}
}
다음으로 PostsApiController가 위의 delete 메서드를 사용하도록 코드를 추가한다.
//PostsApiController.java
...
@RequiredArgsConstructor
@RestController
public class PostsApiController {
...
@DeleteMapping("/api/v1/posts/{id}")
public Long delete(@PathVariable Long id){
postsService.delete(id);
return id;
}
}
컨트롤러까지 생성되었으니 테스트를 진행하자. 게시글 수정 화면에서 삭제 버튼을 클릭한다.
다음과 같이 삭제 성공 메시지와 기존 게시글이 목록에서 삭제되었음을 확인할 수 있다.
이렇게 수정과 삭제 기능까지 완성되었다. 기본적인 게시판 기능이 완성되었으니 다음 장에선 로그인 기능을 구현해 보겠다.
'개발 > 스프링 부트와 AWS로 혼자 구현하는 웹 서비스' 카테고리의 다른 글
[Spring Boot & AWS] Chpt 6 - AWS 서버 환경을 만들어보자 - AWS EC2 (0) | 2023.01.24 |
---|---|
[Spring Boot & AWS] Chpt 5 - 스프링 시큐리티와 OAuth 2.0으로 로그인 기능 구현하기 (0) | 2023.01.23 |
[Spring Boot & AWS] Chpt 3 - 스프링 부트에서 JPA로 데이터베이스 다뤄보자 (0) | 2023.01.13 |
[Spring Boot & AWS] Chpt 2 - 스프링 부트에서 테스트 코드를 작성하자 (0) | 2023.01.11 |
[Spring Boot & AWS] Chpt 1 - 인텔리제이로 스프링 부트 시작하기 (0) | 2023.01.11 |