Contents
모든 컨트롤러가 json 을 return 한다Return json 처리⬇1. 모든 view 파일 삭제2. 유저1. 회원가입2. 로그인(JWT 배우기 까지 미완)3. 회원정보수정(JWT 배우기 까지 미완)3. 게시글1. 글상세보기(댓글,좋아요 포함)2. 글보기(api용 주소 추가)3. 글수정하기4. 글쓰기4. 댓글1. 댓글쓰기2. 댓글삭제5. 좋아요1. 댓글쓰기2. 댓글삭제Optional 처리⬇1. 유저UserRepositoryUserService2. 댓글ReplyRepositoryReplyService3. 좋아요LoveRepositoryLoveService4. 게시글BoardRepositoryBoardService주소 처리⬇1. 유저UserController2. 댓글ReplyController3. 좋아요LoveController4. 게시글BoardController인터셉터 수정공통 레포지토리 만드는 법13. 스프링부트 블로그 v2 를 기준으로 v3 를 만듬. 참고해야함
모든 컨트롤러가 json 을 return 한다
이제 api 에 대한 문서를 잘 만들어야 프론트가 잘 사용한다
insert, update 는 실행한 결과를 return 해야 한다
주소에서 동사를 제거한다
json 으로 요청받고 json 으로 응답한다
Return json 처리⬇
1. 모든 view 파일 삭제

- .keep 파일을 넣지 않으면 templates 폴더가 git에 저장되지 않는다
- 내부 파일이 없는 폴더는 git이 무시한다
2. 유저
1. 회원가입
☕UserController
@PostMapping("/join")
public String join(@Valid UserRequest.JoinDTO joinDTO, Errors errors) {
userService.회원가입(joinDTO);
return "redirect:/login-form";
}
⬇ mustache 파일이 아니라 → json 을 return 한다
@PostMapping("/join")
public @ResponseBody Resp<?> join(@Valid @RequestBody UserRequest.JoinDTO reqDTO, Errors errors) {
UserResponse.DTO respDTO = userService.회원가입(reqDTO);
return Resp.ok(respDTO);
}
- 회원가입은 유저정보를 insert 하는 것. 따라서 insert 한 유저정보를 return 해야 한다
@ResponseBody
→ 파일이 아니라 json 을 return 한다고 알려주는 어노테이션
@RequestBody
→ mime type 을x-www-form-urlencoded
가 아니라application/json
으로 받겠다는 어노테이션
☕UserService
@Transactional
public void 회원가입(UserRequest.JoinDTO joinDTO) {
// 1. 해당 username이 사용 중인지 확인
User alreadyUser = userRepository.findByUsername(joinDTO.getUsername());
// 2. 사용 중이면 예외!
if (alreadyUser != null) {
throw new Exception400("해당 username은 이미 사용중 입니다");
}
// 3. 아니면 회원가입 성공
userRepository.save(joinDTO.toEntity());
}
⬇ insert 된 user 를 DTO 에 담아 return 한다
// RestAPI 규칙 1 -> insert 는 그 행을 DTO 에 담아 돌려줘야 한다
@Transactional
public UserResponse.DTO 회원가입(UserRequest.JoinDTO reqDTO) {
try {
User userPS = userRepository.save(reqDTO.toEntity());
return new UserResponse.DTO(userPS);
} catch (Exception e) {
throw new Exception400("잘못된 요청입니다");
}
}
- RestAPI 규칙 1 -> insert 는 그 행을 DTO 에 담아 돌려줘야 한다
☕UserResponse
package shop.mtcoding.blog.user;
import lombok.Data;
public class UserResponse {
// 가장 기본이 되는 DTO 는 그냥 DTO 로 만든다(User -> DTO)
// RestAPI 규칙 2 -> DTO 에 민감 정보 삭제, 날짜는 String (날짜타입을 공부할 때까지)
@Data
public static class DTO {
private Integer id;
private String username;
private String email;
private String createdAt;
public DTO(User user) {
this.id = user.getId();
this.username = user.getUsername();
this.email = user.getEmail();
this.createdAt = user.getCreatedAt().toString();
}
}
}
- 유저 정보를 return 하기 위한 DTO
- 가장 기본적인 정보를 가진 DTO 는 이름을 그냥 DTO 라고 적는다
- RestAPI 규칙 2 -> DTO 에 민감 정보 삭제

2. 로그인(JWT 배우기 까지 미완)
☕UserController
@PostMapping("/login")
public String login(@Valid UserRequest.LoginDTO loginDTO, Errors errors, HttpServletResponse response) {
User sessionUser = userService.로그인(loginDTO);
session.setAttribute("sessionUser", sessionUser);
if (loginDTO.getRememberMe() == null) {
Cookie cookie = new Cookie("username", null);
cookie.setMaxAge(0); // 브라우저가 MaxAge가 0인 쿠키는 자동 삭제함
response.addCookie(cookie);
} else {
Cookie cookie = new Cookie("username", loginDTO.getUsername());
cookie.setMaxAge(24 * 60 * 60 * 7); // MaxAge에 값을 넣으면 브라우저를 새로 켜도 유지됨
response.addCookie(cookie);
}
return "redirect:/";
}
⬇
@PostMapping("/login") // password랑 id가 노출되면 안되기 때문에 post로 받는다
public String login(@Valid @RequestBody UserRequest.LoginDTO loginDTO, Errors errors, HttpServletResponse response) {
//System.out.println(loginDTO);
User sessionUser = userService.로그인(loginDTO);
session.setAttribute("sessionUser", sessionUser);
if (loginDTO.getRememberMe() == null) {
Cookie cookie = new Cookie("username", null);
cookie.setMaxAge(0); // 즉시 만료
response.addCookie(cookie);
} else {
Cookie cookie = new Cookie("username", loginDTO.getUsername());
cookie.setMaxAge(60 * 60 * 24 * 7);
response.addCookie(cookie);
}
return "redirect:/";
}
- 로그인이 post 맵핑을 사용하는 이유
- 로그인에는 사용자의 민감한 정보(비밀번호)가 들어있기 때문에 조회 기능 이긴 하나 post 맵핑을 사용해 body 로 데이터를 받음
☕UserService
public User 로그인(UserRequest.LoginDTO loginDTO) {
// 1. username에 대한 데이터가 있는지 확인
User user = userRepository.findByUsername(loginDTO.getUsername());
// 2. 없으면 예외!
if (user == null) {
throw new Exception401("username 혹은 password가 맞지 않습니다");
}
// 3. 있으면 password 비교
if (!(user.getPassword().equals(loginDTO.getPassword()))) {
throw new Exception401("username 혹은 password가 맞지 않습니다");
}
return user;
}
⬇
// TODO -> A4용지에다가 id, username 적어, A4용지를 서명함, A4용지를 돌려주기
public User 로그인(UserRequest.LoginDTO loginDTO) {
User user = userRepository.findByUsername(loginDTO.getUsername());
if (user == null) throw new Exception401("유저네임 혹은 비밀번호가 틀렸습니다");
if (!user.getPassword().equals(loginDTO.getPassword())) {
throw new Exception401("유저네임 혹은 비밀번호가 틀렸습니다");
}
return user;
}
☕UserResponse
3. 회원정보수정(JWT 배우기 까지 미완)
☕UserController
@PostMapping("/user/update")
public String update(@Valid UserRequest.UpdateDTO updateDTO, Errors errors) {
User sessionUser = (User) session.getAttribute("sessionUser");
User userPS = userService.회원정보수정(updateDTO, sessionUser.getId());
// 세션 동기화
session.setAttribute("sessionUser", userPS);
return "redirect:/";
}
⬇ post → put 으로 맴핑 수정
// TODO -> JWT 이후에 하기
@PutMapping("/user") // 인증에 관련된 id 는 주소로 받는게 아닌 session 에서 가져온다
public String update(@Valid @RequestBody UserRequest.UpdateDTO updateDTO, Errors errors) {
User sessionUser = (User) session.getAttribute("sessionUser");
User userPS = userService.회원정보수정(updateDTO, sessionUser.getId()); // 토큰도 새로 만들어 줘야 한다
// 세션 동기화
session.setAttribute("sessionUser", userPS);
return "redirect:/";
}
☕UserService
@Transactional
public User 회원정보수정(UserRequest.UpdateDTO updateDTO, Integer userId) {
User userPS = userRepository.findByUserId(userId);
if (userPS == null) throw new Exception404("회원을 찾을 수 없습니다");
userPS.update(updateDTO.getPassword(), updateDTO.getEmail()); // 영속화 된 객체(db에서 조회한 것)의 상태변경
return userPS;
} // 더티체킹 -> 상태가 변경되면 update를 날린다
⬇
// TODO RestAPI 규칙 3 -> update 된 data 도 돌려줘야 한다
@Transactional
public User 회원정보수정(UserRequest.UpdateDTO updateDTO, Integer userId) {
User userPS = userRepository.findById(userId);
// Exception404
if (userPS == null) throw new Exception404("자원을 찾을 수 없습니다");
userPS.update(updateDTO.getPassword(), updateDTO.getEmail()); // 영속화된 객체의 상태변경
return userPS; // 리턴한 이유는 세션을 동기화해야해서!!
} // 더티체킹 -> 상태가 변경되면 update을 날려요!!
- TODO RestAPI 규칙 3 -> update 된 data 도 돌려줘야 한다
☕UserResponse
3. 게시글
1. 글상세보기(댓글,좋아요 포함)
☕BoardController
@GetMapping("/board/{id}")
public String detail(@PathVariable("id") Integer id, HttpServletRequest request) {
User sessionUser = (User) session.getAttribute("sessionUser");
Integer sessionUserId = sessionUser == null ? null : sessionUser.getId();
BoardResponse.DetailDTO detailDTO = boardService.상세보기(id, sessionUserId);
request.setAttribute("model", detailDTO);
return "board/detail";
}
⬇ 주소에 detail 이 추가된 이유 → 상세보기 화면에는 기존 board 보다 더 추가된 데이터가 있기 때문
@GetMapping("/board/{id}/detail") // 전달하는 데이터를 보고 주소를 설계함
public @ResponseBody Resp<?> getBoardDetail(@PathVariable("id") Integer id) {
User sessionUser = (User) session.getAttribute("sessionUser");
Integer sessionUserId = (sessionUser == null ? null : sessionUser.getId());
BoardResponse.DetailDTO detailDTO = boardService.글상세보기(id, sessionUserId);
return Resp.ok(detailDTO);
}
- 게시물 상세화면에 필요한 컨트롤러이기 때문에 이름을 getBoardDetail 로 작성하였다
☕BoardService
@Transactional
public BoardResponse.DetailDTO 글상세보기(Integer id, Integer userId) {
Board boardPS = boardRepository.findByIdJoinUserAndReplies(id);
Love love = loveRepository.findByUserIdAndBoardId(userId, id);
Long loveCount = loveRepository.findByBoardId(id);
Integer loveId = love == null ? null : love.getId();
Boolean isLove = love == null ? false : true;
BoardResponse.DetailDTO detailDTO = new BoardResponse.DetailDTO(boardPS, userId, isLove, loveCount.intValue(), loveId);
return detailDTO;
}
☕BoardResponse
// 상세보기 화면에 필요한 데이터
@Data
public static class DetailDTO {
private Integer id;
private String title;
private String content;
private Boolean isPublic;
private Boolean isOwner;
private Boolean isLove;
private Integer loveCount;
private String username;
private Timestamp createdAt;
private Integer loveId;
private List<ReplyDTO> replies;
@Data
public class ReplyDTO {
private Integer id;
private String content;
private String username;
private Boolean isOwner;
public ReplyDTO(Reply reply, Integer sessionUserId) {
this.id = reply.getId();
this.content = reply.getContent();
this.username = reply.getUser().getUsername();
this.isOwner = reply.getUser().getId().equals(sessionUserId);
}
}
public DetailDTO(Board board, Integer sessionUserId, Boolean isLove, Integer loveCount, Integer loveId) {
this.id = board.getId();
this.title = board.getTitle();
this.content = board.getContent();
this.isPublic = board.getIsPublic();
this.isOwner = sessionUserId == board.getUser().getId();
this.username = board.getUser().getUsername();
this.createdAt = board.getCreatedAt();
this.isLove = isLove;
this.loveCount = loveCount;
this.loveId = loveId;
List<ReplyDTO> repliesDTO = new ArrayList<>();
for (Reply reply : board.getReplies()) {
ReplyDTO replyDTO = new ReplyDTO(reply, sessionUserId);
repliesDTO.add(replyDTO);
}
this.replies = repliesDTO;
}
}

2. 글보기(api용 주소 추가)
☕BoardController
@GetMapping("/board/{id}") // 전달하는 데이터를 보고 주소를 설계함
public @ResponseBody Resp<?> getBoardOne(@PathVariable("id") int id) {
User sessionUser = (User) session.getAttribute("sessionUser");
BoardResponse.DTO respDTO = boardService.글보기(id, sessionUser.getId());
return Resp.ok(respDTO);
}
- 전달하는 데이터를 보고 주소를 설계
- getBoardOne → api 용 board 를 단 건으로 조회할 수 있는 주소
☕BoardService
// 규칙4 -> 화면에 보이는 데이터 + 반드시 PK 를 넣어야 한다
public BoardResponse.DTO 글보기(int id, Integer sessionUserId) {
Board boardPS = boardRepository.findById(id);
if (boardPS == null) throw new Exception404("자원을 찾을 수 없습니다");
if (!boardPS.getUser().getId().equals(sessionUserId)) {
throw new Exception403("권한이 없습니다");
}
return new BoardResponse.DTO(boardPS);
}
- 규칙4 -> 화면에 보이는 데이터 + 반드시 PK 를 넣어야 한다
☕BoardResponse
@Data
public static class DTO {
private Integer id;
private String title;
private String content;
private Boolean isPublic;
private Integer userId;
private String createdAt;
public DTO(Board board) {
this.id = board.getId();
this.title = board.getTitle();
this.content = board.getContent();
this.isPublic = board.getIsPublic();
this.userId = board.getUser().getId();
this.createdAt = board.getCreatedAt().toString();
}
}
- 가장 기본적인 board 와 같은 DTO

3. 글수정하기
☕BoardController
@PostMapping("/board/{id}/update")
public String update(@PathVariable("id") Integer id, @Valid BoardRequest.UpdateDTO reqDTO, Errors errors) {
User sessionUser = (User) session.getAttribute("sessionUser");
boardService.글수정(id, reqDTO, sessionUser.getId());
return "redirect:/board/" + id;
}
⬇ post → put 맵핑 변경
@PutMapping("/board/{id}")
public @ResponseBody Resp<?> update(@PathVariable("id") Integer id, @Valid @RequestBody BoardRequest.UpdateDTO reqDTO, Errors errors) {
User sessionUser = (User) session.getAttribute("sessionUser");
BoardResponse.DTO respDTO = boardService.글수정하기(reqDTO, id, sessionUser.getId());
return Resp.ok(respDTO);
}
- 수정된 결과를 DTO 에 넣어 return
☕BoardService
@Transactional
public User 회원정보수정(UserRequest.UpdateDTO updateDTO, Integer userId) {
User userPS = userRepository.findByUserId(userId);
if (userPS == null) throw new Exception404("회원을 찾을 수 없습니다");
userPS.update(updateDTO.getPassword(), updateDTO.getEmail()); // 영속화 된 객체(db에서 조회한 것)의 상태변경
return userPS;
} // 더티체킹 -> 상태가 변경되면 update를 날린다
⬇
// TODO RestAPI 규칙 3 -> update 된 data 도 돌려줘야 한다
@Transactional
public User 회원정보수정(UserRequest.UpdateDTO updateDTO, Integer userId) {
User userPS = userRepository.findById(userId);
// Exception404
if (userPS == null) throw new Exception404("자원을 찾을 수 없습니다");
userPS.update(updateDTO.getPassword(), updateDTO.getEmail()); // 영속화된 객체의 상태변경
return userPS; // 리턴한 이유는 세션을 동기화해야해서!!
} // 더티체킹 -> 상태가 변경되면 update을 날려요!!
- TODO RestAPI 규칙 3 -> update 된 data 도 돌려줘야 한다
☕BoardResponse
@Data
public static class DTO {
private Integer id;
private String title;
private String content;
private Boolean isPublic;
private Integer userId;
private String createdAt;
public DTO(Board board) {
this.id = board.getId();
this.title = board.getTitle();
this.content = board.getContent();
this.isPublic = board.getIsPublic();
this.userId = board.getUser().getId();
this.createdAt = board.getCreatedAt().toString();
}
}
- Board 엔티티와 동일한 DTO

4. 글쓰기
☕BoardController
@PostMapping("/board/save")
public String save(@Valid BoardRequest.SaveDTO reqDTO, Errors errors) {
User sessionUser = (User) session.getAttribute("sessionUser");
boardService.글쓰기(reqDTO, sessionUser);
return "redirect:/";
}
⬇ 주소에 save 동사 삭제
@PostMapping("/board")
public @ResponseBody Resp<?> save(@Valid @RequestBody BoardRequest.SaveDTO reqDTO, Errors errors) {
User sessionUser = (User) session.getAttribute("sessionUser");
BoardResponse.DTO respDTO = boardService.글쓰기(reqDTO, sessionUser);
return Resp.ok(respDTO);
}
- insert 한 board 를 DTO 에 담아서 return
☕BoardService
@Transactional
public void 글쓰기(BoardRequest.SaveDTO saveDTO, User sessionUser) {
Board board = saveDTO.toEntity(sessionUser);
boardRepository.save(board);
}
⬇
@Transactional
public BoardResponse.DTO 글쓰기(BoardRequest.SaveDTO reqDTO, User sessionUser) {
Board board = reqDTO.toEntity(sessionUser);
Board boardPS = boardRepository.save(board);
return new BoardResponse.DTO(boardPS);
}
- insert 된 board 엔티티를 DTO 에 담아서 return
☕BoardResponse
@Data
public static class DTO {
private Integer id;
private String title;
private String content;
private Boolean isPublic;
private Integer userId;
private String createdAt;
public DTO(Board board) {
this.id = board.getId();
this.title = board.getTitle();
this.content = board.getContent();
this.isPublic = board.getIsPublic();
this.userId = board.getUser().getId();
this.createdAt = board.getCreatedAt().toString();
}
}
- Board 엔티티와 동일한 DTO
4. 댓글
1. 댓글쓰기
☕ReplyController
@PostMapping("/reply/save")
public String saveReply(@Valid ReplyRequest.SaveDTO reqDTO, Errors errors) {
User sessionUser = (User) session.getAttribute("sessionUser");
replyService.댓글등록(reqDTO, sessionUser.getId());
return "redirect:/board/" + reqDTO.getBoardId();
}
⬇
@PostMapping("/reply")
public @ResponseBody Resp<?> save(@Valid @RequestBody ReplyRequest.SaveDTO reqDTO, Errors errors) {
User sessionUser = (User) session.getAttribute("sessionUser");
ReplyResponse.DTO respDTO = replyService.댓글쓰기(reqDTO, sessionUser);
return Resp.ok(respDTO);
}
☕ReplyService
@Transactional
public void 댓글등록(ReplyRequest.SaveDTO reqDTO, Integer sessionUserId) {
replyRepository.save(reqDTO.toEntity(sessionUserId));
}
⬇
@Transactional
public ReplyResponse.DTO 댓글쓰기(ReplyRequest.SaveDTO reqDTO, User sessionUser) {
Reply replyPS = replyRepository.save(reqDTO.toEntity(sessionUser));
return new ReplyResponse.DTO(replyPS);
}
☕ReplyResponse
@Data
public static class DTO {
private Integer id;
private String content; // 댓글 내용
private Integer userId;
private Integer boardId;
private String createdAt;
public DTO(Reply reply) {
this.id = reply.getId();
this.content = reply.getContent();
this.userId = reply.getUser().getId();
this.boardId = reply.getBoard().getId();
this.createdAt = reply.getCreatedAt().toString();
}
}
2. 댓글삭제
☕ReplyController
@PostMapping("/reply/{id}/delete")
public String deleteReply(@PathVariable Integer id) {
User sessionUser = (User) session.getAttribute("sessionUser");
Integer boardId = replyService.댓글삭제(id, sessionUser.getId());
return "redirect:/board/" + boardId;
}
⬇
@DeleteMapping("/reply/{id}")
public @ResponseBody Resp<?> delete(@PathVariable("id") Integer id) {
User sessionUser = (User) session.getAttribute("sessionUser");
replyService.댓글삭제(id, sessionUser.getId());
return Resp.ok(null);
}
- 삭제한 후에는 null 을 넣어 return 한다
☕ReplyService
@Transactional
public Integer 댓글삭제(Integer id, Integer sessionUserId) {
Reply replyPS = replyRepository.findById(id);
if (replyPS == null) throw new Exception404("해당 댓글이 없습니다");
if (!(replyPS.getUser().getId().equals(sessionUserId))) throw new Exception403("권한이 없습니다");
replyRepository.deleteById(id); // 삭제 쿼리를 날리기만 한다
return replyPS.getBoard().getId(); // pc 객체는 남아 있다
}
⬇
// TODO -> 2단계 RestAPI 주소 변경 json 돌려주기할때 void로 변경하기
// 규칙 5 -> 삭제는 응답할 데이터가 없다. void를 사용한다
@Transactional
public void 댓글삭제(Integer id, Integer sessionUserId) {
Reply replyPS = replyRepository.findById(id);
// Exception404
if (replyPS == null) throw new Exception404("자원을 찾을 수 없습니다");
if (!replyPS.getUser().getId().equals(sessionUserId)) {
throw new Exception403("권한이 없습니다");
}
replyRepository.deleteById(id);
}
- 규칙 5 -> 삭제는 응답할 데이터가 없다. void 를 사용한다

5. 좋아요
1. 댓글쓰기
☕ReplyController
@PostMapping("/reply/save")
public String saveReply(@Valid ReplyRequest.SaveDTO reqDTO, Errors errors) {
User sessionUser = (User) session.getAttribute("sessionUser");
replyService.댓글등록(reqDTO, sessionUser.getId());
return "redirect:/board/" + reqDTO.getBoardId();
}
⬇
@PostMapping("/reply")
public @ResponseBody Resp<?> save(@Valid @RequestBody ReplyRequest.SaveDTO reqDTO, Errors errors) {
User sessionUser = (User) session.getAttribute("sessionUser");
ReplyResponse.DTO respDTO = replyService.댓글쓰기(reqDTO, sessionUser);
return Resp.ok(respDTO);
}
☕ReplyService
@Transactional
public void 댓글등록(ReplyRequest.SaveDTO reqDTO, Integer sessionUserId) {
replyRepository.save(reqDTO.toEntity(sessionUserId));
}
⬇
@Transactional
public ReplyResponse.DTO 댓글쓰기(ReplyRequest.SaveDTO reqDTO, User sessionUser) {
Reply replyPS = replyRepository.save(reqDTO.toEntity(sessionUser));
return new ReplyResponse.DTO(replyPS);
}
☕ReplyResponse
@Data
public static class DTO {
private Integer id;
private String content; // 댓글 내용
private Integer userId;
private Integer boardId;
private String createdAt;
public DTO(Reply reply) {
this.id = reply.getId();
this.content = reply.getContent();
this.userId = reply.getUser().getId();
this.boardId = reply.getBoard().getId();
this.createdAt = reply.getCreatedAt().toString();
}
}
2. 댓글삭제
☕ReplyController
@PostMapping("/reply/{id}/delete")
public String deleteReply(@PathVariable Integer id) {
User sessionUser = (User) session.getAttribute("sessionUser");
Integer boardId = replyService.댓글삭제(id, sessionUser.getId());
return "redirect:/board/" + boardId;
}
⬇
@DeleteMapping("/reply/{id}")
public @ResponseBody Resp<?> delete(@PathVariable("id") Integer id) {
User sessionUser = (User) session.getAttribute("sessionUser");
replyService.댓글삭제(id, sessionUser.getId());
return Resp.ok(null);
}
- 삭제한 후에는 null 을 넣어 return 한다
☕ReplyService
@Transactional
public Integer 댓글삭제(Integer id, Integer sessionUserId) {
Reply replyPS = replyRepository.findById(id);
if (replyPS == null) throw new Exception404("해당 댓글이 없습니다");
if (!(replyPS.getUser().getId().equals(sessionUserId))) throw new Exception403("권한이 없습니다");
replyRepository.deleteById(id); // 삭제 쿼리를 날리기만 한다
return replyPS.getBoard().getId(); // pc 객체는 남아 있다
}
⬇
// TODO -> 2단계 RestAPI 주소 변경 json 돌려주기할때 void로 변경하기
// 규칙 5 -> 삭제는 응답할 데이터가 없다. void를 사용한다
@Transactional
public void 댓글삭제(Integer id, Integer sessionUserId) {
Reply replyPS = replyRepository.findById(id);
// Exception404
if (replyPS == null) throw new Exception404("자원을 찾을 수 없습니다");
if (!replyPS.getUser().getId().equals(sessionUserId)) {
throw new Exception403("권한이 없습니다");
}
replyRepository.deleteById(id);
}
- 규칙 5 -> 삭제는 응답할 데이터가 없다. void 를 사용한다

Optional 처리⬇
22. 옵셔널(Optional)1. 유저
UserRepository
findById
public User findById(Integer id) {
return em.find(User.class, id);
}
⬇
// Optional<User> -> 이거 null 일 수 있다. 알려줌
public Optional<User> findById(Integer id) {
return Optional.ofNullable(em.find(User.class, id));
}
findByUsername
public User findByUsername(String username) {
try {
return em.createQuery("select u from User u where u.username = :username", User.class)
.setParameter("username", username)
.getSingleResult();
} catch (Exception e) {
return null;
}
}
⬇
public Optional<User> findByUsername(String username) {
try {
User userPS = em.createQuery("select u from User u where u.username = :username", User.class)
.setParameter("username", username)
.getSingleResult();
return Optional.of(userPS);
} catch (Exception e) {
return Optional.ofNullable(null);
}
}
UserService
로그인
public User 로그인(UserRequest.LoginDTO loginDTO) {
User user = userRepository.findByUsername(loginDTO.getUsername());
if (user == null) throw new Exception401("유저네임 혹은 비밀번호가 틀렸습니다");
if (!user.getPassword().equals(loginDTO.getPassword())) {
throw new Exception401("유저네임 혹은 비밀번호가 틀렸습니다");
}
return user;
}
⬇
public User 로그인(UserRequest.LoginDTO loginDTO) {
User userPS = userRepository.findByUsername(loginDTO.getUsername())
.orElseThrow(() -> new Exception401("유저네임 혹은 비밀번호가 틀렸습니다"));
if (!userPS.getPassword().equals(loginDTO.getPassword())) {
throw new Exception401("유저네임 혹은 비밀번호가 틀렸습니다");
}
return userPS;
}
중복체크
public Map<String, Object> 유저네임중복체크(String username) {
User user = userRepository.findByUsername(username);
Map<String, Object> dto = new HashMap<>();
if (user == null) {
dto.put("available", true);
} else {
dto.put("available", false);
}
return dto;
}
⬇
public Map<String, Object> 유저네임중복체크(String username) {
Optional<User> userOP = userRepository.findByUsername(username);
Map<String, Object> dto = new HashMap<>();
if (userOP.isPresent()) {
dto.put("available", false);
} else {
dto.put("available", true);
}
return dto;
}
회원정보수정
@Transactional
public User 회원정보수정(UserRequest.UpdateDTO updateDTO, Integer userId) {
User userPS = userRepository.findById(userId);
// Exception404
if (userPS == null) throw new Exception404("자원을 찾을 수 없습니다");
userPS.update(updateDTO.getPassword(), updateDTO.getEmail()); // 영속화된 객체의 상태변경
return userPS; // 리턴한 이유는 세션을 동기화해야해서!!
} // 더티체킹 -> 상태가 변경되면 update을 날려요!!
⬇
@Transactional
public User 회원정보수정(UserRequest.UpdateDTO updateDTO, Integer userId) {
User userPS = userRepository.findById(userId)
.orElseThrow(() -> new Exception404("자원을 찾을 수 없습니다"));
userPS.update(updateDTO.getPassword(), updateDTO.getEmail()); // 영속화된 객체의 상태변경
return userPS; // 리턴한 이유는 세션을 동기화해야해서!!
} // 더티체킹 -> 상태가 변경되면 update을 날려요!!
2. 댓글
ReplyRepository
ReplyService
댓글삭제
@Transactional
public void 댓글삭제(Integer id, Integer sessionUserId) {
Reply replyPS = replyRepository.findById(id);
// Exception404
if (replyPS == null) throw new Exception404("자원을 찾을 수 없습니다");
if (!replyPS.getUser().getId().equals(sessionUserId)) {
throw new Exception403("권한이 없습니다");
}
replyRepository.deleteById(id);
}
⬇
@Transactional
public void 댓글삭제(Integer id, Integer sessionUserId) {
Reply replyPS = replyRepository.findById(id)
.orElseThrow(() -> new Exception404("자원을 찾을 수 없습니다"));
if (!replyPS.getUser().getId().equals(sessionUserId)) {
throw new Exception403("권한이 없습니다");
}
replyRepository.deleteById(id);
}
3. 좋아요
LoveRepository
findByUserIdAndBoardId
public Love findByUserIdAndBoardId(Integer userId, Integer boardId) {
Query query = em.createQuery("select lo from Love lo where lo.user.id = :userId and lo.board.id = :boardId", Love.class);
query.setParameter("userId", userId);
query.setParameter("boardId", boardId);
try {
return (Love) query.getSingleResult();
} catch (Exception e) {
return null;
}
}
⬇
public Optional<Love> findByUserIdAndBoardId(Integer userId, Integer boardId) {
Query query = em.createQuery("select lo from Love lo where lo.user.id = :userId and lo.board.id = :boardId", Love.class);
query.setParameter("userId", userId);
query.setParameter("boardId", boardId);
try {
return Optional.of((Love) query.getSingleResult());
} catch (Exception e) {
return Optional.ofNullable(null);
}
}
findById
public Love findById(Integer id) {
return em.find(Love.class, id);
}
⬇
public Optional<Love> findById(Integer id) {
return Optional.ofNullable(em.find(Love.class, id));
}
LoveService
좋아요취소
@Transactional
public LoveResponse.DeleteDTO 좋아요취소(Integer id, Integer sessionUserId) {
Love lovePS = loveRepository.findById(id);
if (lovePS == null) throw new ExceptionApi404("자원을 찾을 수 없습니다");
if (!lovePS.getUser().getId().equals(sessionUserId)) {
throw new ExceptionApi403("권한이 없습니다");
}
Integer boardId = lovePS.getBoard().getId();
loveRepository.deleteById(id);
Long loveCount = loveRepository.findByBoardId(boardId);
return new LoveResponse.DeleteDTO(loveCount.intValue());
}
⬇
@Transactional
public LoveResponse.DeleteDTO 좋아요취소(Integer id, Integer sessionUserId) {
Love lovePS = loveRepository.findById(id)
.orElseThrow(() -> new ExceptionApi404("자원을 찾을 수 없습니다"));
if (!lovePS.getUser().getId().equals(sessionUserId)) {
throw new ExceptionApi403("권한이 없습니다");
}
Integer boardId = lovePS.getBoard().getId();
loveRepository.deleteById(id);
Long loveCount = loveRepository.findByBoardId(boardId);
return new LoveResponse.DeleteDTO(loveCount.intValue());
}
4. 게시글
BoardRepository
findByIdJoinUserAndReplies
public Board findByIdJoinUserAndReplies(Integer id) {
Query query = em.createQuery("select b from Board b join fetch b.user left join fetch b.replies r left join fetch r.user where b.id = :id order by r.id desc", Board.class);
query.setParameter("id", id);
return (Board) query.getSingleResult();
}
⬇
public Optional<Board> findByIdJoinUserAndReplies(Integer id) {
Query query = em.createQuery("select b from Board b join fetch b.user left join fetch b.replies r left join fetch r.user where b.id = :id order by r.id desc", Board.class);
query.setParameter("id", id);
try {
return Optional.of((Board) query.getSingleResult());
} catch (Exception e) {
return Optional.ofNullable(null);
}
}
findByIdJoinUser
public Board findByIdJoinUser(Integer id) {
Query query = em.createQuery("select b from Board b join fetch b.user where b.id = :id", Board.class);
query.setParameter("id", id);
return (Board) query.getSingleResult();
}
⬇
public Optional<Board> findByIdJoinUser(Integer id) {
Query query = em.createQuery("select b from Board b join fetch b.user where b.id = :id", Board.class);
query.setParameter("id", id);
try {
return Optional.of((Board) query.getSingleResult());
} catch (Exception e) {
return Optional.ofNullable(null);
}
}
findById
public Board findById(Integer id) {
return em.find(Board.class, id);
}
⬇
public Optional<Board> findById(Integer id) {
return Optional.ofNullable(em.find(Board.class, id));
}
BoardService
글수정하기
@Transactional
public BoardResponse.DTO 글수정하기(BoardRequest.UpdateDTO reqDTO, Integer boardId, Integer sessionUserId) {
Board boardPS = boardRepository.findById(boardId);
if (boardPS == null) throw new Exception404("자원을 찾을 수 없습니다");
if (!boardPS.getUser().getId().equals(sessionUserId)) {
throw new Exception403("권한이 없습니다");
}
boardPS.update(reqDTO.getTitle(), reqDTO.getContent(), reqDTO.getIsPublic());
return new BoardResponse.DTO(boardPS);
} // 더티 체킹 (상태 변경해서 update)
⬇
@Transactional
public BoardResponse.DTO 글수정하기(BoardRequest.UpdateDTO reqDTO, Integer id, Integer sessionUserId) {
Board boardPS = boardRepository.findById(id)
.orElseThrow(() -> new Exception404("자원을 찾을 수 없습니다"));
if (!boardPS.getUser().getId().equals(sessionUserId)) {
throw new Exception403("권한이 없습니다");
}
boardPS.update(reqDTO.getTitle(), reqDTO.getContent(), reqDTO.getIsPublic());
return new BoardResponse.DTO(boardPS);
} // 더티 체킹 (상태 변경해서 update)
글삭제
@Transactional
public void 글삭제(Integer id, Integer sessionUserId) {
Board boardPS = boardRepository.findById(id);
if (boardPS == null) throw new Exception404("자원을 찾을 수 없습니다");
if (!boardPS.getUser().getId().equals(sessionUserId)) {
throw new Exception403("권한이 없습니다");
}
boardRepository.deleteById(id);
}
⬇
@Transactional
public void 글삭제(Integer id, Integer sessionUserId) {
Board boardPS = boardRepository.findById(id)
.orElseThrow(() -> new Exception404("자원을 찾을 수 없습니다"));
if (!boardPS.getUser().getId().equals(sessionUserId)) {
throw new Exception403("권한이 없습니다");
}
boardRepository.deleteById(id);
}
글상세보기
@Transactional
public BoardResponse.DetailDTO 글상세보기(Integer id, Integer userId) {
Board boardPS = boardRepository.findByIdJoinUserAndReplies(id);
Love love = loveRepository.findByUserIdAndBoardId(userId, id);
Long loveCount = loveRepository.findByBoardId(id);
Integer loveId = love == null ? null : love.getId();
Boolean isLove = love == null ? false : true;
BoardResponse.DetailDTO detailDTO = new BoardResponse.DetailDTO(boardPS, userId, isLove, loveCount.intValue(), loveId);
return detailDTO;
}
⬇
@Transactional
public BoardResponse.DetailDTO 글상세보기(Integer id, Integer userId) {
Board boardPS = boardRepository.findByIdJoinUserAndReplies(id)
.orElseThrow(() -> new Exception404("자원을 찾을 수 없습니다"));
Love lovePS = loveRepository.findByUserIdAndBoardId(userId, id)
.orElseThrow(() -> new Exception404("자원을 찾을 수 없습니다"));
Long loveCount = loveRepository.findByBoardId(id);
Integer loveId = lovePS.getId();
Boolean isLoved = lovePS == null ? false : true;
BoardResponse.DetailDTO detailDTO = new BoardResponse.DetailDTO(boardPS, userId, isLoved, loveCount.intValue(), loveId);
return detailDTO;
}
글보기
public BoardResponse.DTO 글보기(int id, Integer sessionUserId) {
Board boardPS = boardRepository.findById(id);
if (boardPS == null) throw new Exception404("자원을 찾을 수 없습니다");
if (!boardPS.getUser().getId().equals(sessionUserId)) {
throw new Exception403("권한이 없습니다");
}
return new BoardResponse.DTO(boardPS);
}
⬇
public BoardResponse.DTO 글보기(int id, Integer sessionUserId) {
Board boardPS = boardRepository.findById(id)
.orElseThrow(() -> new Exception404("자원을 찾을 수 없습니다"));
if (!boardPS.getUser().getId().equals(sessionUserId)) {
throw new Exception403("권한이 없습니다");
}
return new BoardResponse.DTO(boardPS);
}
주소 처리⬇
- s → 인증 필요
- api → restAPI 리턴
1. 유저
유저인증관련은 api 주소를 안 붙인다
UserController
// TODO -> JWT 이후에 하기
@PutMapping("/s/api/user") // 인증에 관련된 id 는 주소로 받는게 아닌 session 에서 가져온다
public String update(@Valid @RequestBody UserRequest.UpdateDTO updateDTO, Errors errors) {
User sessionUser = (User) session.getAttribute("sessionUser");
User userPS = userService.회원정보수정(updateDTO, sessionUser.getId()); // 토큰도 새로 만들어 줘야 한다
// 세션 동기화
session.setAttribute("sessionUser", userPS);
return "redirect:/";
}
@GetMapping("/api/check-username-available/{username}")
public @ResponseBody Resp<?> checkUsernameAvailable(@PathVariable("username") String username) {
Map<String, Object> dto = userService.유저네임중복체크(username);
return Resp.ok(dto);
}
@PostMapping("/join")
public @ResponseBody Resp<?> join(@Valid @RequestBody UserRequest.JoinDTO reqDTO, Errors errors) {
UserResponse.DTO respDTO = userService.회원가입(reqDTO);
return Resp.ok(respDTO);
}
- join 은 단독으로 사용 api 를 붙이지 않음
@PostMapping("/login") // password랑 id가 노출되면 안되기 때문에 post로 받는다
public String login(@Valid @RequestBody UserRequest.LoginDTO loginDTO, Errors errors, HttpServletResponse response) {
//System.out.println(loginDTO);
User sessionUser = userService.로그인(loginDTO);
session.setAttribute("sessionUser", sessionUser);
if (loginDTO.getRememberMe() == null) {
Cookie cookie = new Cookie("username", null);
cookie.setMaxAge(0); // 즉시 만료
response.addCookie(cookie);
} else {
Cookie cookie = new Cookie("username", loginDTO.getUsername());
cookie.setMaxAge(60 * 60 * 24 * 7);
response.addCookie(cookie);
}
return "redirect:/";
}
- login 은 단독으로 사용 api 를 붙이지 않음
@GetMapping("/logout")
public String logout() {
session.invalidate();
return "redirect:/login-form";
}
- logout 은 단독으로 사용 api 를 붙이지 않음
2. 댓글
ReplyController
@DeleteMapping("/s/api/reply/{id}")
public @ResponseBody Resp<?> delete(@PathVariable("id") Integer id) {
User sessionUser = (User) session.getAttribute("sessionUser");
replyService.댓글삭제(id, sessionUser.getId());
return Resp.ok(null);
}
@PostMapping("/s/api/reply")
public @ResponseBody Resp<?> save(@Valid @RequestBody ReplyRequest.SaveDTO reqDTO, Errors errors) {
User sessionUser = (User) session.getAttribute("sessionUser");
ReplyResponse.DTO respDTO = replyService.댓글쓰기(reqDTO, sessionUser);
return Resp.ok(respDTO);
}
3. 좋아요
LoveController
@PostMapping("/s/api/love")
public Resp<?> saveLove(@RequestBody @Valid LoveRequest.SaveDTO reqDTO, Errors errors) {
User sessionUser = (User) session.getAttribute("sessionUser");
LoveResponse.SaveDTO respDTO = loveService.좋아요(reqDTO, sessionUser.getId());
return Resp.ok(respDTO);
}
@DeleteMapping("/s/api/love/{id}")
public Resp<?> deleteLove(@PathVariable("id") Integer id) {
User sessionUser = (User) session.getAttribute("sessionUser");
LoveResponse.DeleteDTO respDTO = loveService.좋아요취소(id, sessionUser.getId());
return Resp.ok(respDTO);
}
4. 게시글
BoardController
@PutMapping("/s/api/board/{id}")
public @ResponseBody Resp<?> update(@PathVariable("id") Integer id, @Valid @RequestBody BoardRequest.UpdateDTO reqDTO, Errors errors) {
User sessionUser = (User) session.getAttribute("sessionUser");
BoardResponse.DTO respDTO = boardService.글수정하기(reqDTO, id, sessionUser.getId());
return Resp.ok(respDTO);
}
@GetMapping("/api/board/{id}") // 전달하는 데이터를 보고 주소를 설계함
public @ResponseBody Resp<?> getBoardOne(@PathVariable("id") int id) {
User sessionUser = (User) session.getAttribute("sessionUser");
BoardResponse.DTO respDTO = boardService.글보기(id, sessionUser.getId());
return Resp.ok(respDTO);
}
@GetMapping("/api/board/{id}/detail") // 전달하는 데이터를 보고 주소를 설계함
public @ResponseBody Resp<?> getBoardDetail(@PathVariable("id") Integer id) {
User sessionUser = (User) session.getAttribute("sessionUser");
Integer sessionUserId = (sessionUser == null ? null : sessionUser.getId());
BoardResponse.DetailDTO detailDTO = boardService.글상세보기(id, sessionUserId);
return Resp.ok(detailDTO);
}
@GetMapping({"/", "/api/board"})
public @ResponseBody Resp<?> list(@RequestParam(required = false, value = "page", defaultValue = "0") Integer page,
@RequestParam(required = false, value = "keyword", defaultValue = "") String keyword) {
User sessionUser = (User) session.getAttribute("sessionUser");
BoardResponse.ListDTO respDTO;
if (sessionUser == null) {
respDTO = boardService.글목록보기(null, page, keyword);
} else {
respDTO = boardService.글목록보기(sessionUser.getId(), page, keyword);
}
return Resp.ok(respDTO);
}
- 메인 화면은 2가지 주소를 주어 사용할 수 있다
@PostMapping("/s/api/board")
public @ResponseBody Resp<?> save(@Valid @RequestBody BoardRequest.SaveDTO reqDTO, Errors errors) {
User sessionUser = (User) session.getAttribute("sessionUser");
BoardResponse.DTO respDTO = boardService.글쓰기(reqDTO, sessionUser);
return Resp.ok(respDTO);
}
인터셉터 수정
package shop.mtcoding.blog._core.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.web.servlet.HandlerInterceptor;
import shop.mtcoding.blog._core.error.ex.ExceptionApi401;
import shop.mtcoding.blog.user.User;
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
System.out.println("uri: " + uri);
HttpSession session = request.getSession();
User sessionUser = (User) session.getAttribute("sessionUser");
if (uri.startsWith("/s") && sessionUser == null) {
throw new ExceptionApi401("인증이 필요합니다");
}
return true;
}
}
- /s 로 시작하는 주소는 인증이 필요하기 때문에 변경
공통 레포지토리 만드는 법
Share article