목표
1. 게시글 작성 등과 같은 UI를 세션 사용에 맞춰 변경한다.
2. 어드민 계정을 추가하고 일반 계정과 다른 Role을 적용하도록 한다.
3. Specification(명세)를 추가하여 validation을 할 수 있도록 한다.
4. validation의 결과로 Exception를 통해 client에 결과를 전달하도록 변경한다.
이전 게시글에서 미처 변경하지 못했던 내용들을 수정할 것이다.
세션을 이용해 로그인한 유저 정보를 가져오므로 BoardForm에는 게시글 제목과 내용만 있으면 된다.
@Getter
@NoArgsConstructor
public class BoardForm {
private String contents;
private String title;
@Builder
public BoardForm(String contents, String title){
this.contents = contents;
this.title = title;
}
public BoardEntity convertBoardEntity(UserEntity user){
return BoardEntity.builder()
.writer(user)
.contents(this.contents)
.title(this.title)
.build();
}
}
게시글 목록에서 글 작성 날짜를 볼 수 있도록 BoardDto에 createdAt
컬럼을 추가한다.
@NoArgsConstructor
@Getter
public class BoardDto{
private long id;
private UserDto writer;
private String contents;
private String title;
private LocalDateTime createdAt;
public BoardDto(BoardEntity board) {
this.writer = UserDto.of(board.getWriter());
this.id = board.getId();
this.contents = board.getContents();
this.title = board.getTitle();
this.createdAt = board.getCreatedAt();
}
public static BoardDto of(BoardEntity board) {
return new BoardDto(board);
}
}
<script id="tableList" type="text/x-handlebars-template">
<table class="table table-striped" style="width: 50% !important;">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">제목</th>
<th scope="col">글쓴이</th>
</tr>
</thead>
<tbody>
\{{#contents}}
<tr onclick='fetchDetails(\{{@index}})'>
<th scope="row">\{{math @index '+' 1}}</th>
<td>\{{title}}</td>
<td>\{{writer.accountId}}</td>
</tr>
\{{/contents}}
</tbody>
</table>
</script>
작성자 정보를 UserDto
의 형태로 받으므로 writer의 attribute인 accountId를 글쓴이로 보여주도록 변경하였다.
<script id="writeModal" type="text/x-handlebars-template">
<div class="modal fade" id="boardModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">New message</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<form>
<div class="form-group">
<label for="recipient-name" class="col-form-label">Writer:</label>
<input type="text" class="form-control" id="user" disabled="disabled" value="\{{this}}" required />
</div>
<div class="form-group">
<label for="message-text" class="col-form-label">Title:</label>
<textarea class="form-control" id="title" required></textarea>
</div>
<div class="form-group">
<label for="message-text" class="col-form-label">Contents:</label>
<textarea class="form-control" id="contents" required></textarea>
</div>
<input type="submit" id="requiredBtn" style="visibility: hidden;" />
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal" id="closeBtn">Close</button>
<button type="button" class="btn btn-primary" id="saveBtn">Write</button>
<button type="button" class="btn btn-primary" id="deleteBtn" style="display: none">Delete</button>
</div>
</div>
</div>
</div>
</script>
패스워드를 입력하는 div
를 제거하였다.
userId를 입력받는 input
은 disabled='disabled'
를 이용해 작성자 명을 바꾸지 못하도록 하였다. 처음 이 모달 템플릿을 붙일 때 데이터로 세션에서 받아온 accountId를 넘기는데 이를 해당 태그의 value에 넣음으로써 자동으로 셋팅되게 하였다.
required
를 활성화시키기 위해선 같은 form 태그 내에 submit 버튼이 존재해야하기 때문에, style='visibility: hidden;'
을 이용하여 보이지 않도록 하였다.
saveBtn 버튼을 누르면 requiredBtn 이 눌리게 되고 이 때 필수 인풋 중 비어있는게 있는지 확인하도록 js 로직을 작성할 것이다.
{{#partial "header"}}
<title>Main Page</title>
{{/partial}}
<!--body-->
{{#partial "contents"}}
<h1>이곳은 게시판입니다.</h1>
{{#if accountId}}
<div>로그인한 계정 : {{accountId}} <button onclick='location.href="logout"' class='btn btn-primary'>로그아웃</button></div>
{{/if}}
<div id="tableSpace"></div>
<div id="pageMarkerSpace"></div>
<div id="writeModalSpace"></div>
{{#if accountId}}
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#boardModal">글쓰기</button>
{{/if}}
{{/partial}}
<!--body-->
<!--js-->
{{#partial "js"}}
{{> template/table}}
{{> template/pageMarker}}
{{> template/modal}}
<script>
let nowBoardList = new Object();
let nowBoardIndex = 0;
let accountId = '{{accountId}}';
const WRITE = "Write";
const MODIFY = "Modify";
window.onload = () => {
apiRequest(attachBoard);
attachWriteModal();
}
var attachWriteModal = () => {
var template = Handlebars.compile($("#writeModal").html());
$("#writeModalSpace").html(template(accountId));
}
var apiRequest = (callback = null, page = 1) => {
const SIZE = 10;
$.ajax({
method : 'GET',
url : 'api/boards?page='+page+"&size="+SIZE
}).done(function (response) {
nowBoardList = response.contents;
if(typeof callback != 'undefined'){
callback(response);
}
})
}
var attachBoard = (response) => {
if(typeof response != 'undefined') {
var template = Handlebars.compile($("#tableList").html());
$("#tableSpace").html(template(response));
var template = Handlebars.compile($("#pageMarker").html());
$("#pageMarkerSpace").html(template(response));
}
}
var pageConvert = (page) => {
apiRequest(attachBoard, page);
}
var fetchDetails = (index) => {
nowBoardIndex = index;
setModalData(index);
setModifyModal();
$('#boardModal').modal("show");
}
var setModalData = (index) => {
var $modal = $('#boardModal');
$modal.find('#user').val(nowBoardList[index].writer.accountId);
$modal.find('#title').val(nowBoardList[index].title);
$modal.find('#contents').val(nowBoardList[index].contents);
}
var setModifyModal = () => {
var $modal = $('#boardModal');
$modal.find('#saveBtn').text(MODIFY);
if($modal.find('#user').val() === accountId){
$modal.find('#deleteBtn').css('display', 'block');
}else{
setDisableContents($modal);
}
}
var setDisableContents = ($modal) => {
$modal.find('#title').attr('disabled', true);
$modal.find('#contents').attr('disabled', true);
$modal.find('#saveBtn').css('display','none');
}
var setAbleContents = ($modal) => {
$modal.find('#title').attr('disabled', false);
$modal.find('#contents').attr('disabled', false);
$modal.find('#saveBtn').css('display', 'block');
}
var resetModalData = () => {
var $modal = $('#boardModal');
$modal.find('#user').val(accountId);
$modal.find('#title').val("");
$modal.find('#contents').val("");
}
var setNewWriteModal = () => {
var $modal = $('#boardModal');
$modal.find('#saveBtn').text(WRITE);
$modal.find('#deleteBtn').css('display', 'none');
setAbleContents($modal);
}
var setBoardForm = () => {
var boardForm = new Object();
var $modal = $('#boardModal');
boardForm.title = $modal.find('#title').val();
boardForm.contents = $modal.find('#contents').val();
return boardForm;
}
var requiredInputHaveNullSpec = (boardForm) => {
var spec = false;
$.each(boardForm, function (attribute, value) {
if(value == null || value.trim() == ""){
spec = true;
return false;
}
})
return spec;
}
$(document).on('show.bs.modal', '#boardModal', function (event) { // 모달 열릴 때 이벤트
})
$(document).on('hidden.bs.modal', '#boardModal', function () { // 모달 닫힐 때 이벤트
resetModalData();
setNewWriteModal();
})
$(document).on('click', '#deleteBtn', function() {
var password = $('#boardModal').find('#password').val();
$.ajax({
method : 'DELETE',
url : 'api/boards/'+nowBoardList[nowBoardIndex].id+'?password='+password
}).done(function (response) {
if(typeof response != 'undefined'){
if(response == false) {
alert(" 비밀 번호가 틀렸습니다. ");
} else {
$('#boardModal').modal("hide"); //닫기
}
}
})
})
$(document).on('click', '#saveBtn', function() {
$('#requiredBtn').click();
var boardForm = setBoardForm();
if(requiredInputHaveNullSpec(boardForm) == true){
return false;
}
var method, url;
if($('#saveBtn').text() === WRITE){
method = 'POST';
url = 'api/boards';
}else{
method = 'PUT';
url = 'api/boards/'+nowBoardList[nowBoardIndex].id;
}
$.ajax({
method : method,
url : url,
contentType: 'application/json',
data : JSON.stringify(boardForm)
}).done(function (response) {
if(typeof response != 'undefined'){
if(response == false){
alert(" 비밀 번호가 틀렸습니다. ");
}else {
$('#boardModal').modal("hide"); //닫기
}
}
})
})
</script>
{{#block "helper"}}{{/block}}
{{/partial}}
<!--js-->
{{> static/helper/helper}}
{{> layout/layout}}
마지막의 $(document).on('click', '#saveBtn', function() {...})
을 보면 위에서 form내에 숨겨둔 submit 버튼을 강제로 클릭하는 것을 볼 수 있다. 이 때, required
가 지정된 input 중 내용이 빈 것을 확인하여 사용자에게 경고창을 띄워준다.
실제로 writeBtn은 일반 버튼이기 때문에, submit 버튼의 validation과 상관없이 무조건 아래의 로직이 실행된다.
이를 막기 위해서 requiredInputHaveNullSpec
함수를 생성하여 필수 Input 중 Null인 것이 하나라도 있으면 로직을 끝내버리도록 (return false
) 구성하였다.
새글 작성 | 내가 쓴 글 클릭한 경우 | 다른 사람이 쓴 글 클릭한 경우 |
---|---|---|
User Entity에 Role을 추가하고, 이를 통해 일반 계정과 관리자 계정을 분리하도록 할 것이다.
우선 domain/user
패키지 하위에 enums
라는 패키지를 생성하고, UserRole
이라는 Enum을 추가하였다.
❗️NOTE
enum
이라는 이름의 패키지는 생성 불가하다. 정확히 말하면 생성은 가능하나 하위에.java
파일을 만들 수 없다.
package com.freeboard01.domain.user.enums;
public enum UserRole {
NORMAL, ADMIN
}
role은 간단하게 일반 유저를 뜻하는 NORMAL
과 관리자인 ADMIN
, 두가지만 생성하였다.
UserEntity 에도 컬럼을 추가해주자!
@Entity
@Getter
@Table(name = "user")
@NoArgsConstructor
public class UserEntity extends BaseEntity {
@Column
private String accountId;
@Column
private String password;
@Setter
@Column
@Enumerated(EnumType.STRING)
private UserRole role;
@Builder
public UserEntity(String accountId, String password, UserRole role){
this.accountId = accountId;
this.password = password;
this.role = role;
}
}
📌 @Enumerated(EnumType)
Enum을 Database에 저장할 때 어떤 형식으로 저장할 것인지를 명시한다. attribute로 추가가능한 EnumType은 STRING 과 ORDINAL 인데, 디폴트는 ORDINAL 이다.
STRING으로 지정할 시 Enum의 이름이 그대로 Database 컬럼에 들어가고, ORDINAL로 지정할 시 index 값인 number가 들어가게 된다.
UserService에서 회원 가입 시 UserRole.NORMAL을 추가한 뒤 데이터 베이스에 저장하도록 로직을 변경해 주었다.
public Boolean join(UserForm user) {
UserEntity userEntity = userRepository.findByAccountId(user.getAccountId());
if(userEntity == null){
UserEntity newUser = user.convertUserEntity();
newUser.setRole(UserRole.NORMAL); //여기!!!
userRepository.save(newUser);
return true;
}
return false;
}
UserApiControllerTest
에서 회원가입 테스트를 실행해보자!
성공적으로 NORMAL role이 추가됐음을 확인 할 수 있다.
현재 서비스에서 사용하고 있는 if
문을 보자. 사실 비교값이 하나이고 간단하게 작성되어 있기 때문에 딱히 문제가 없어보이기는 한다.🤔 하지만 로직이 복잡해지면서 하나의 분기문 내에 여러개의 조건이 들어가게되고 (&&
나 ||
로 엮일 것이다.) 가독성이 떨어질 가능성이 크다. 뿐만 아니라 동일한 조건을 판별하는 구문들이 여러 곳에서 반복적으로 사용될 것인데, 세부 조건이 바뀔 때마다 이를 하나씩 찾아내 바꾸는 것은 유지보수 측면에서 매우 좋지않다.
따라서 이를 Specification(명세)라는 이름의 일종의 validation을 수행하는 팩토리 클래스를 사용하는 것으로 변경할 것이다.
❗️NOTE
interface
의 static 메소드는 body가 반드시 필요하며 오버라이딩이 불가능하다.
모든 Spec 클래스는 팩토리이기 때문에 interface나 추상 클래스를 사용한 구현이 불가하다. 단, 통일되도록 confirm이라는 이름의 static 메소드를 사용할 것을 염두에 두고 작성할 것이다.🤔
domain/user
패키지 하위에 specification
패키지를 만들고 IsWriterEqualToUserLoggedIn
클래스와 HaveAdminRoles
클래스를 추가해 줄것이다.
spec은 하나의 static 메소드를 수행함으로써 false/true를 판정하는 것이 목표이므로 단번에 의도가 들어날 수 있게 클래스 이름을 구체적으로 작성해주었다.
IsWriterEqualToUserLoggedIn
public class IsWriterEqualToUserLoggedIn {
public static boolean confirm(UserEntity writer, UserEntity loginUser) {
return writer.getAccountId().equals(loginUser.getAccountId());
}
}
HaveAdminRoles
public class HaveAdminRoles {
static List<UserRole> upperRoleList;
static {
upperRoleList = Arrays.asList(UserRole.ADMIN);
}
public static boolean confirm(UserEntity user){
return upperRoleList.stream().anyMatch(role -> role.equals(user.getRole()));
}
}
static { ... }
은 정적 변수로 선언된 upperRoleList를 초기화해주는 정적 변수 초기화 블럭이다.
이 블럭은 여러개 작성할 수 있으며 위에서 부터 차례로 읽어진다. 따라서 중복으로 초기화 한 경우에는 마지막 블럭의 값으로 최종 저장된다.
boardService의 update, delete 메소드를 Spec을 사용하도록 변경해주었다.
public Boolean update(BoardForm boardForm, UserForm userForm, long id) {
UserEntity user = userRepository.findByAccountId(userForm.getAccountId());
if(user == null){
new Exception();
}
BoardEntity target = boardRepository.findById(id).get();
if(IsWriterEqualToUserLoggedIn.confirm(target.getWriter(), user) || HaveAdminRoles.confirm(user)){
target.update(boardForm.convertBoardEntity(target.getWriter()));
return true;
}
return false;
}
public boolean delete(long id, UserForm userForm) {
UserEntity user = userRepository.findByAccountId(userForm.getAccountId());
if(user == null){
new Exception();
}
BoardEntity target = boardRepository.findById(id).get();
if(IsWriterEqualToUserLoggedIn.confirm(target.getWriter(), user) || HaveAdminRoles.confirm(user)){
boardRepository.deleteById(id);
return true;
}
return false;
}
앞선 delete 테스트 두 개를 다음과 같이 수정하고, 관리자 계정일 경우 로그인 계정과 글 작성자가 일치하지 않아도 삭제를 수행하는 테스트를 추가하였다.
@ExtendWith({MockitoExtension.class, SpringExtension.class})
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/applicationContext.xml"})
public class BoardServiceUnitTest {
@InjectMocks
private BoardService sut;
@Mock
private BoardRepository mockBoardRepo;
@Mock
private UserRepository mockUserRepo;
@Test
@DisplayName("로그인한 유저와 글을 작성한 유저가 다를 경우 삭제를 진행하지 않는다.")
public void delete1() {
UserEntity writer = UserEntity.builder().accountId("mockUser").password("mockPass").build();
UserForm userLoggedIn = UserForm.builder().accountId("wrongUser").password("wrongUser").build();
BoardEntity boardEntity = BoardEntity.builder().contents("contents").title("title").writer(writer).build();
given(mockUserRepo.findByAccountId(anyString())).willReturn(userLoggedIn.convertUserEntity());
given(mockBoardRepo.findById(anyLong())).willReturn(Optional.of(boardEntity));
sut.delete(anyLong(), userLoggedIn);
verify(mockBoardRepo, never()).deleteById(anyLong());
}
@Test
@DisplayName("로그인한 유저와 글을 작성한 유저가 동일할 경우 삭제를 수행한다.")
public void delete2() {
final String PASSWORD = "myPass";
UserForm userForm = UserForm.builder().accountId("mockUser").password(PASSWORD).build();
UserEntity userLoggedIn = userForm.convertUserEntity();
BoardEntity boardEntity = BoardEntity.builder().writer(userLoggedIn).contents("contents").title("title").build();
given(mockUserRepo.findByAccountId(anyString())).willReturn(userLoggedIn);
given(mockBoardRepo.findById(anyLong())).willReturn(Optional.of(boardEntity));
doNothing().when(mockBoardRepo).deleteById(anyLong());
sut.delete(anyLong(), userForm);
verify(mockBoardRepo, times(1)).deleteById(anyLong());
}
@Test
@DisplayName("관리자 계정일 경우 삭제를 수행한다.")
public void delete3() {
UserForm userLoggedIn = UserForm.builder().accountId("admin").build();
UserEntity userLoggedInEntity = userLoggedIn.convertUserEntity();
userLoggedInEntity.setRole(UserRole.ADMIN);
UserEntity writer = UserEntity.builder().accountId("mockUser").password("mockPass").build();
BoardEntity boardEntity = BoardEntity.builder().writer(writer).contents("contents").title("title").build();
given(mockUserRepo.findByAccountId(anyString())).willReturn(userLoggedInEntity);
given(mockBoardRepo.findById(anyLong())).willReturn(Optional.of(boardEntity));
sut.delete(anyLong(), userLoggedIn);
verify(mockBoardRepo, times(1)).deleteById(anyLong());
}
}
client에 Role을 넘겨서 관리자 계정일 경우 수정/삭제 버튼을 항상 활성화 시킬 수 있도록 한다.
HomeController 에서 사용할 메소드를 추가하였다.
public UserRole findUserRole(UserForm user){
return userRepository.findByAccountId(user.getAccountId()).getRole();
}
@Controller
public class HomeController {
private UserService userService;
@Autowired
public HomeController(UserService userService){
this.userService = userService;
}
@GetMapping("/")
public String home() {
return "index";
}
@GetMapping("/board")
public String board(HttpSession httpSession, Model model) {
UserForm loginUser = (UserForm) httpSession.getAttribute("USER");
if(loginUser != null ) {
model.addAttribute("accountId", loginUser.getAccountId());
// 추가한 로직
model.addAttribute("accountRole" , userService.findUserRole(loginUser));
}
return "board";
}
@GetMapping("/join")
public String join() { return "join"; }
@GetMapping("/logout")
public String logout(HttpSession httpSession) {
httpSession.removeAttribute("USER");
return "index";
}
}
findUserRole을 사용하기 위해 UserService를 추가한 뒤, board
메소드에 model의 attribute로 UserRole
넣는 로직을 추가하였다.
accountRole 저장 | 어드민 유저 판정 |
---|---|
accountRole : 현재 로그인한 유저의 Role
adminRole : 관리자에 준하는 Role 배열
adminRole.includes(accountRole)
로 관리자 계정인지 판단하여 분기시킨다.
톰캣을 실행해서 어드민 유저로 로그인 해보자!
어드민 유저로 로그인해서 일반 유저가 쓴 글을 클릭한 경우 | 어드민이 수정을 수행한 뒤 모습 |
---|---|
커스텀 Exception 처리를 수행하도록 하겠다.
Exception을 사용하여 서버의 로직이 중단되는 것에 그치는 것이 아니라 client로 어떤 문제가 발생하였는지 전달할 필요가 있다.
Spring에서는 이를 위해 강력한 어노테이션을 제공하는데 바로 @ExceptionHandler
이다.
@ControllerAdvice
@Controller
어노테이션을 가지거나, xml 설정 파일에서 컨트롤러로 명시된 클래스에서 Exception이 발생되면 이를 감지하겠다는 것이다.Controller
와 RestController
만 ExceptionHandler의 감시 대상이 된다.Service
만 감시 대상으로 등록할 수는 없단 소리@ControllerAdvice(com.freeboard01.api.BoardApi)
와 같이 특정한 클래스만 명시하는 것도 가능하다.@ResponseBody
String
또는 ModelAndView
만 가능하다.@ExceptionHandler
RuntimeException.class
나 더 상위 클래스인 Exception.class
등을 넘기면 된다.나는 RuntimeException
을 상속받는 FreeBoardException
을 만들었는데, 현시점에서는 이 클래스에서 모든 커스텀 예외 처리를 수행하도록 할 것이다.
util
패키지 하위에 exception
패키지를 만들고 ExceptionHandler
클래스를 추가하였다.
완성된 코드는 다음과 같다.
@ControllerAdvice
public class ExceptionHandler {
@ResponseBody
@org.springframework.web.bind.annotation.ExceptionHandler(FreeBoardException.class)
public ResponseEntity<Error> exception(FreeBoardException exception){
return new ResponseEntity<>(Error.create(exception.getExceptionType()), HttpStatus.OK);
}
@Getter
@NoArgsConstructor
@AllArgsConstructor
static class Error{
private int code;
private int status;
private String message;
static Error create(BaseExceptionType exception){
return new Error(exception.getErrorCode(), exception.getHttpStatus(), exception.getErrorMessage());
}
}
}
동일한 댑스에 freeboardException
이라는 예외 처리 클래스를 만들었다.
public class FreeBoardException extends RuntimeException {
@Getter
private BaseExceptionType exceptionType;
public FreeBoardException(BaseExceptionType exceptionType){
super(exceptionType.getErrorMessage());
this.exceptionType = exceptionType;
}
}
사실상 Client로 전달되는 값은 static class Error
이므로 본 클래스는 Handler가 감시할 커스텀 예외를 명시하기 위한 수단이라고 생각할 수 있다.
따라서 멤버변수로 ExceptionType을 추가하였다.
domain
패키지 하위에 BaseExceptionType
클래스를 만들었다.
public interface BaseExceptionType {
int getErrorCode();
int getHttpStatus();
String getErrorMessage();
}
BaseExceptionType
을 만든 이유는 모든 예외처리를 수행할 클래스가 freeboardException
이기에 각 도메인의 ExceptionType Enum을 업캐스팅하여 받아 올 수 있게 하기 위해서이다.
또한 이를 Error
객체로 만들 때 다운캐스팅하여 Enum 내 attribute를 convert 하기 위해서 @Getter
어노테이션을 사용했을 시 만들어지는 메소드와 동일한 추상 메소드를 포함하도록 하였다.
freeboardException | static class Error in ExceptionHandler |
---|---|
업캐스팅 | 다운캐스팅 |
domain/user/enums
패키지 하위에 UserExceptionType
클래스를 만들었다.
@Getter
public enum UserExceptionType implements BaseExceptionType {
NOT_FOUND_USER(1001, 200, "해당하는 사용자가 존재하지 않습니다."),
DUPLICATED_USER(1002, 200, "이미 존재하는 사용자 아이디입니다."),
WRONG_PASSWORD(1003, 200, "패스워드를 잘못 입력하였습니다."),
LOGIN_INFORMATION_NOT_FOUND(1004, 200, "로그인 정보를 찾을 수 없습니다. (세션 만료)");
private int errorCode;
private int httpStatus;
private String errorMessage;
UserExceptionType(int errorCode, int httpStatus, String errorMessage) {
this.errorCode = errorCode;
this.httpStatus = httpStatus;
this.errorMessage = errorMessage;
}
}
❗️NOTE. ExceptionHandler의 Bean 설정
한참을 또 삽질했는데 🤔 아무리 Exception을 발생시켜도 Handler가 작동하지 않는 것이었다.
인터넷에 찾아봐도 다른 configuration에 대한 언급이 없었기에 최후의 시도라는 마음으로 dispatcher-servlet.xml에<bean class="com.freeboard01.util.exception.ExceptionHandler"></bean>
를 추가하였더니 그제서야 정상작동 되는 것을 볼 수 있었다.예측하건데, Controller가 어노테이션 사용 설정으로 Bean으로 등록되고 필요 종속들이 자동 주입되는 것처럼 (혹은 Handler를 빈으로 등록하는 것처럼) ControllerAdvice인 클래스 또한 빈으로 명시해줄 필요가 있는가보다.
join
메소드 변경
@PostMapping
private void join(@RequestBody UserForm user){
userService.join(user);
}
join
메소드 변경
public void join(UserForm user) {
UserEntity userEntity = userRepository.findByAccountId(user.getAccountId());
if (userEntity != null){
throw new FreeBoardException(UserExceptionType.DUPLICATED_USER);
}
UserEntity newUser = user.convertUserEntity();
newUser.setRole(UserRole.NORMAL);
userRepository.save(newUser);
}
톰캣을 실행 한 뒤 포스트맨으로 join 요청을 날려보도록 하겠다.
2020.05.07 추가
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserApiController {
private final HttpSession httpSession;
private final UserService userService;
@PostMapping
private void join(@RequestBody UserForm user){
userService.join(user);
}
@PostMapping(params = {"type=LOGIN"})
private void login(@RequestBody UserForm user){
userService.login(user);
httpSession.setAttribute("USER", user);
}
}
로그인의 반환값이 Boolean 이었던 것을 void 로 변경하고 예외처리가 일어나지 않은경우는 무조건 로그인 성공으로 간주하고 세션에 추가한다.
@Service
@Transactional
public class UserService {
private UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public UserRole findUserRole(UserForm user){
return userRepository.findByAccountId(user.getAccountId()).getRole();
}
public void join(UserForm user) {
if (userRepository.findByAccountId(user.getAccountId()) != null){
throw new FreeBoardException(UserExceptionType.DUPLICATED_USER);
}
UserEntity newUser = user.convertUserEntity();
newUser.setRole(UserRole.NORMAL);
userRepository.save(newUser);
}
public void login(UserForm user) {
UserEntity userEntity = userRepository.findByAccountId(user.getAccountId());
if (userEntity == null){
throw new FreeBoardException(UserExceptionType.NOT_FOUND_USER);
}
if (userEntity.getPassword().equals(user.getPassword()) == false){
throw new FreeBoardException(UserExceptionType.WRONG_PASSWORD);
}
}
}
로그인 메소드의 첫 번째 분기문은 계정 정보가 존재하지 않는 경우, 두 번째 분기문은 비밀번호를 잘못 입력한 경우이다.
아이디를 잘못 입력한 경우 | 비밀번호를 잘못 입력한 경우 |
---|---|
@Getter
public enum BoardExceptionType implements BaseExceptionType {
NO_QUALIFICATION_USER(2001, 200, "권한없는 유저입니다."),
NOT_MATCH_WRITER(2002, 200, "작성자만 수행가능한 작업입니다."),
NOT_FOUNT_CONTENTS(2003, 200, "존재하지 않는 게시글입니다.");
private int errorCode;
private int httpStatus;
private String errorMessage;
BoardExceptionType(int errorCode, int httpStatus, String errorMessage) {
this.errorCode = errorCode;
this.httpStatus = httpStatus;
this.errorMessage = errorMessage;
}
}
기본적인 구조는 UserExceptionType과 일치한다.
@Service
@Transactional
public class BoardService {
private BoardRepository boardRepository;
private UserRepository userRepository;
@Autowired
public BoardService(BoardRepository boardRepository, UserRepository userRepository) {
this.boardRepository = boardRepository;
this.userRepository = userRepository;
}
public Page<BoardEntity> get(Pageable pageable) {
return boardRepository.findAll(PageUtil.convertToZeroBasePageWithSort(pageable));
}
public BoardEntity post(BoardForm boardForm, UserForm userForm) {
UserEntity user = Optional.of(userRepository.findByAccountId(userForm.getAccountId())).orElseThrow(() -> new FreeBoardException(UserExceptionType.NOT_FOUND_USER));
return boardRepository.save(boardForm.convertBoardEntity(user));
}
public void update(BoardForm boardForm, UserForm userForm, long id) {
UserEntity user = Optional.of(userRepository.findByAccountId(userForm.getAccountId())).orElseThrow(() -> new FreeBoardException(UserExceptionType.NOT_FOUND_USER));
BoardEntity target = Optional.of(boardRepository.findById(id).get()).orElseThrow(() -> new FreeBoardException(BoardExceptionType.NOT_FOUNT_CONTENTS));
if (IsWriterEqualToUserLoggedIn.confirm(target.getWriter(), user) == false && HaveAdminRoles.confirm(user) == false) {
throw new FreeBoardException(BoardExceptionType.NO_QUALIFICATION_USER);
}
target.update(boardForm.convertBoardEntity(target.getWriter()));
}
public void delete(long id, UserForm userForm) {
UserEntity user = Optional.of(userRepository.findByAccountId(userForm.getAccountId())).orElseThrow(() -> new FreeBoardException(UserExceptionType.NOT_FOUND_USER));
BoardEntity target = Optional.of(boardRepository.findById(id).get()).orElseThrow(() -> new FreeBoardException(BoardExceptionType.NOT_FOUNT_CONTENTS));
if (IsWriterEqualToUserLoggedIn.confirm(target.getWriter(), user) == false && HaveAdminRoles.confirm(user) == false) {
throw new FreeBoardException(BoardExceptionType.NO_QUALIFICATION_USER);
}
boardRepository.deleteById(id);
}
}
Optional
을 이용하여 NullPointerException을 처리하였다.
❗️Optional 참고링크
@RestController
@RequestMapping("/api/boards")
@RequiredArgsConstructor
public class BoardApiController {
private final HttpSession httpSession;
private final BoardService boardService;
@GetMapping
public ResponseEntity<PageDto<BoardDto>> get(@PageableDefault(page = 1, size = 10, sort = "createdAt", direction = Sort.Direction.DESC )Pageable pageable){
Page<BoardEntity> pageBoardList = boardService.get(pageable);
List<BoardDto> boardDtoList = pageBoardList.stream().map(boardEntity -> BoardDto.of(boardEntity)).collect(Collectors.toList());
return ResponseEntity.ok(PageDto.of(pageBoardList, boardDtoList));
}
@PostMapping
public ResponseEntity<BoardDto> post(@RequestBody BoardForm form){
if(httpSession.getAttribute("USER") == null){
throw new FreeBoardException(UserExceptionType.LOGIN_INFORMATION_NOT_FOUND);
}
BoardEntity savedEntity = boardService.post(form, (UserForm) httpSession.getAttribute("USER"));
return ResponseEntity.ok(BoardDto.of(savedEntity));
}
@PutMapping("/{id}")
public void update(@RequestBody BoardForm form, @PathVariable long id){
if(httpSession.getAttribute("USER") == null){
throw new FreeBoardException(UserExceptionType.LOGIN_INFORMATION_NOT_FOUND);
}
boardService.update(form, (UserForm) httpSession.getAttribute("USER"), id);
}
@DeleteMapping("/{id}")
public void delete(@PathVariable long id){
if(httpSession.getAttribute("USER") == null){
throw new FreeBoardException(UserExceptionType.LOGIN_INFORMATION_NOT_FOUND);
}
boardService.delete(id, (UserForm) httpSession.getAttribute("USER"));
}
}
board.hbs | index.hbs & join.hbs |
---|---|
ajax로 서버에 요청 후 응답받은 데이터에 따라 분기, response.message는 서버에서 보낸 Error
객체의 멤버 변수이다.
모든 코드는 Github에서 확인할 수 있습니다.