44. RestAPI 컨트롤러 요청과 응답v7

송민경's avatar
Mar 20, 2024
44. RestAPI 컨트롤러 요청과 응답v7

1. 규칙

  • 요청 : JSON 으로 통일
  • 응답 : JSON 으로 통일
  • Post, Put 요청 : 추가된 row 혹은 수정된 row를 응답
개인 정보를 뺀 최소한의 정보만 전달
  • 화면에 필요한 데이터만 전달 → DTO가 필요함
Entity는 필요없는 정보가 있어서 안됨
DTO를 만드는 책임은 Service에게 있음
- 디폴트 로직 : JPA를 안쓰고 네이티브 쿼리만 사용하면 모두 한번에 되는 쿼리
→ 다 DTO로 경로를 다받았으니 그대로 돌려주면 됨
- JPA 쿼리를 사용할 경우, open in view가 true : 커넥션 기간이 김
DB에서 응답받고 커넥션을 끊어버리면 다른 사용자가 커넥션을 사용할 수 있음
커넥션 시간이 Service에서 controller에 응답될 때 끊김
⇒ service에서 DTO를 만들면 Lazyloading 시간을 줄일 수 있어서!!
 

2. 무한 참조(infinite loop)

  • 프로그래밍에서 발생하는 일반적인 오류입
  • 프로그램이 특정한 조건하에서 끝나지 않고 반복되는 상황
보통은 잘못된 프로그램 로직이나 잘못된 조건문 등이 원인
 

3. Fail on empty beans

  • Spring Framework에서 사용되는 개념
  • Spring은 빈(Bean)을 관리하고 제공, 주로 애플리케이션의 객체
  • Fail on empty beans : 빈이 비어있을 때 실패하도록 설정하는 기능
  • Spring의 ApplicationContext 초기화 시에 사용
  • 애플리케이션이 올바르게 초기화되었는지 여부를 확인 가능
 
  • Spring의 IoC(Inversion of Control) 컨테이너에서 사용
  • 기본적으로 Spring IoC 컨테이너는 애플리케이션 구동 시에 빈을 생성하고 관리
  • 때로 빈이 정상적으로 생성되지 않는 경우가 있을 수 있음
→ Fail on empty beans 설정 / 빈이 비어있을 때 애플리케이션이 실패하도록 할 수 있음
 

4. DTO를 서비스에서 안 만들면 생기는 문제

  • 커넥션의 시간이 길어진다 내 서버의 가용량이 줄어든다 (많은 사람들이 한번에 같이 쓰지 못한다) 커넥션이 종료될 때까지
  • 필요없는 필드를 응답하게 된다.
  • MessageConvverter에서 json 만들 때, Bean 객체를 Lazy Loading 하고 기다렸다가 json을 만들어야 하는데, 안 기다리고 만들다가 오류가 난다! -> 타이밍의 문제
 
REST 컨트롤러에서의 처리 과정
  • String 반환 : 메서드가 문자열(String)을 반환,문자열 그대로 HTTP 응답으로 전송
  • Object 반환 : 메서드가 객체(Object)를 반환, Spring은 이를 JSON 형식으로 변환→ HTTP 응답
이 과정에서는 메시지 컨버터(Message Converter)가 사용
메시지 컨버터가 레이지 로딩을 수행하려면 발생하는 문제
  • 레이지 로딩 : 객체의 일부 필드를 필요한 시점에 로드하는 방식
  • REST 컨트롤러가 객체를 반환, 해당 객체의 일부 필드가 레이지 로딩으로 설정
이 필드는 실제로 사용되는 시점에 로드
  • 메시지 컨버터는 객체를 JSON으로 변환할 때 해당 필드를 사용
필요한 필드가 로드되지 않은 상태라면 오류가 발생
메시지 컨버터는 필드를 사용할 때 이미 로드되어 있어야 하기 때문
 
해결 방법
  • open in view : false -> 커넥션의 시간이 줄어듦 커넥션의 시간이 줄어들면 메세지 컨버터에 레이지 로딩으 못 맡김
  • 서비스 종료 직전에 레이지 로딩 발동
  • 서비스에서 DTO를 만들어야함!

5. 요청 Body 수정하기

  • @RequestBody 어노테이션 붙이기
 
  • UserController 에서 @RequestBody 붙이기
- update에 id를 파라미터로 넣은 이유
우리는 필요 없음
하지만 관리자가 있을 경우 필요함! 회원 정보를 수정할 수 없음
package shop.mtcoding.blog.user; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; @RequiredArgsConstructor @Controller public class UserController { private final UserService userService; private final HttpSession session; // TODO: 회원 정보 조회 API 필요함 /*@GetMapping("api/users/{id}")*/ @PutMapping("api/users/{id}") public String update(@PathVariable Integer id, @RequestBody UserRequest.UpdateDTO reqDTO) { User sessionUser = (User) session.getAttribute("sessionUser"); User newSessionUser = userService.update(sessionUser.getId(), reqDTO); session.setAttribute("sessionUser", newSessionUser); return "redirect:/"; } @PostMapping("/join") // 인증이 필요하지 않아 users를 붙이지 않음 public String join(@RequestBody UserRequest.JoinDTO reqDTO) { userService.join(reqDTO); return "redirect:/"; } @PostMapping("/login") public String login(@RequestBody UserRequest.LoginDTO reqDTO) { User sessionUser = userService.login(reqDTO); session.setAttribute("sessionUser", sessionUser); return "redirect:/"; } @GetMapping("/logout") public String logout() { session.invalidate(); return "redirect:/"; } }
 
  • BoardController 에서 @RequestBody 붙이기
package shop.mtcoding.blog.board; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import shop.mtcoding.blog.user.User; @RequiredArgsConstructor @Controller public class BoardController { private final HttpSession session; private final BoardService boardService; // TODO: 글 목록(model)을 보여줄 API가 필요함 /*@GetMapping("/")*/ // TODO: 글 상세보기 API 필요 /*@GetMapping("/api/boards/{id}/detail")*/ // TODO: 글 조회 API 필요 /*@GetMapping("/api/boards/{id}")*/ @PostMapping("/api/boards") public String save(@RequestBody BoardRequest.SaveDTO reqDTO) { User sessionUser = (User) session.getAttribute("sessionUser"); boardService.save(reqDTO, sessionUser); return "redirect:/"; } // @Transactional 트랜잭션 시간이 너무 길어져서 service에 넣어야함 @PutMapping("/api/boards/{id}") public String update(@RequestBody @PathVariable Integer id, BoardRequest.UpdateDTO reqDTO) { User sessionUser = (User) session.getAttribute("sessionUser"); boardService.update(id, sessionUser.getId(), reqDTO); return "redirect:/board/" + id; } @DeleteMapping("/api/boards/{id}") public String delete(@PathVariable Integer id) { User sessionUser = (User) session.getAttribute("sessionUser"); boardService.delete(id, sessionUser.getId()); return "redirect:/"; } }
 
  • ReplyController 에서 @RequestBody 붙이기
package shop.mtcoding.blog.reply; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import shop.mtcoding.blog.user.User; @RequiredArgsConstructor @Controller public class ReplyController { private final ReplyService replyService; private final HttpSession session; @DeleteMapping("/api/replies/{id}") public String delete(@PathVariable Integer replyId) { // json으로 응답하기에 페이지 id를 몰라도 됨 User sessionUser = (User) session.getAttribute("sessionUser"); replyService.delete(replyId, sessionUser.getId()); return "redirect:/board/"; } @PostMapping("api/replies") public String save(@RequestBody ReplyRequest.SaveDTO reqDTO) { User sessionUser = (User) session.getAttribute("sessionUser"); replyService.save(reqDTO, sessionUser); return "redirect:/board/" + reqDTO.getBoardId(); } }
 

6. 응답 Body 수정하기

  • ResponseEntity 사용하기
  • static으로 ok를 제공해줌
 
  • UserController 에서 응답시 ResponseEntity 사용하기
package shop.mtcoding.blog.user; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.springframework.data.repository.query.Param; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import shop.mtcoding.blog._core.utils.ApiUtil; @RequiredArgsConstructor @RestController public class UserController { private final UserService userService; private final HttpSession session; @GetMapping("users/{id}") public ResponseEntity<?> userinfo(@PathVariable int id) { User user = userService.lookUp(id); return ResponseEntity.ok(new ApiUtil<>(user)); } @PutMapping("/api/users/{id}") public ResponseEntity<?> update(@PathVariable Integer id, @RequestBody UserRequest.UpdateDTO reqDTO) { User sessionUser = (User) session.getAttribute("sessionUser"); User newSessionUser = userService.update(sessionUser.getId(), reqDTO); session.setAttribute("sessionUser", newSessionUser); // static으로 ok를 제공해줌 return ResponseEntity.ok(new ApiUtil(newSessionUser)); } @PostMapping("/join") // 인증이 필요하지 않아 users를 붙이지 않음 public ResponseEntity<?> join(@RequestBody UserRequest.JoinDTO reqDTO) { User user = userService.join(reqDTO); return ResponseEntity.ok(new ApiUtil(user)); } @PostMapping("/login") public ResponseEntity<?> login(@RequestBody UserRequest.LoginDTO reqDTO) { User sessionUser = userService.login(reqDTO); session.setAttribute("sessionUser", sessionUser); return ResponseEntity.ok(new ApiUtil(null)); } @GetMapping("/logout") public ResponseEntity<?> logout() { session.invalidate(); return ResponseEntity.ok(new ApiUtil(null)); } }
    package shop.mtcoding.blog.user; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import shop.mtcoding.blog._core.errors.exception.Exception400; import shop.mtcoding.blog._core.errors.exception.Exception401; import shop.mtcoding.blog._core.errors.exception.Exception404; import java.util.Optional; @RequiredArgsConstructor @Service // IoC에 등록 public class UserService { // 컨트롤러는 서비스가, 서비스는 레파지토리가 필요함 - 의존 관계 private final UserJPARepository userJPARepository; @Transactional // JPA 레파지토리가 아니라 호출하는 서비스가 가지고 있어야 함 public User join(UserRequest.JoinDTO reqDTO) { // 1. 유효성 검사(컨트롤러 책임) // 2. 유저네임 중복검사(서비스 체크) - DB 연결이 필요함 // 기존의 유저네임을 조회 Optional<User> userOP = userJPARepository.findByUsername(reqDTO.getUsername()); if(userOP.isPresent()){ throw new Exception400("중복된 유저네임입니다"); } // 2. 회원가입 return userJPARepository.save(reqDTO.toEntity()); } }
     
    • BoardController 에서 응답시 ResponseEntity 사용하기
    package shop.mtcoding.blog.board; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import shop.mtcoding.blog._core.utils.ApiUtil; import shop.mtcoding.blog.user.User; @RequiredArgsConstructor @RestController public class BoardController { private final HttpSession session; private final BoardService boardService; // TODO: 글 목록(model)을 보여줄 API가 필요함 /*@GetMapping("/")*/ // TODO: 글 상세보기 API 필요 /*@GetMapping("/api/boards/{id}/detail")*/ // TODO: 글 조회 API 필요 /*@GetMapping("/api/boards/{id}")*/ @PostMapping("/api/boards") public ResponseEntity<?> save(@RequestBody BoardRequest.SaveDTO reqDTO) { User sessionUser = (User) session.getAttribute("sessionUser"); Board board = boardService.save(reqDTO, sessionUser); return ResponseEntity.ok(board); // board에 연관된 객체가 있기에 위험함 / 무한 참조가 일어날 수 있음 } // @Transactional 트랜잭션 시간이 너무 길어져서 service에 넣어야함 @PutMapping("/api/boards/{id}") public ResponseEntity<?> update(@RequestBody @PathVariable Integer id, BoardRequest.UpdateDTO reqDTO) { User sessionUser = (User) session.getAttribute("sessionUser"); Board board = boardService.update(id, sessionUser.getId(), reqDTO); return ResponseEntity.ok(board); } @DeleteMapping("/api/boards/{id}") public ResponseEntity<?> delete(@PathVariable Integer id) { User sessionUser = (User) session.getAttribute("sessionUser"); boardService.delete(id, sessionUser.getId()); return ResponseEntity.ok(null); } }
    • 나중에 DTO를 리턴해줘야 함
    package shop.mtcoding.blog.board; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import shop.mtcoding.blog._core.errors.exception.Exception403; import shop.mtcoding.blog._core.errors.exception.Exception404; import shop.mtcoding.blog.user.User; import java.util.List; @RequiredArgsConstructor @Service public class BoardService { private final BoardJPARepository boardJPARepository; public Board update(int boardId, int sessionUserId, BoardRequest.UpdateDTO reqDTO) { Board board = boardJPARepository.findById(boardId) .orElseThrow(() -> new Exception404("게시글을 찾을 수 없습니다")); board.setTitle(reqDTO.getTitle()); board.setContent(reqDTO.getContent()); return board; // 변경된 객체를 리턴 } @Transactional public Board save(BoardRequest.SaveDTO reqDTO, User sessionUser) { Board board = boardJPARepository.save(reqDTO.toEntity(sessionUser)); return board; // 나중에 DTO를 리턴해줘야 함 } }
     
    • ReplyController 에서 응답시 ResponseEntity 사용하기
    package shop.mtcoding.blog.reply; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import shop.mtcoding.blog._core.utils.ApiUtil; import shop.mtcoding.blog.user.User; @RequiredArgsConstructor @RestController public class ReplyController { private final ReplyService replyService; private final HttpSession session; @DeleteMapping("/api/replies/{id}") public ResponseEntity<?> delete(@PathVariable Integer replyId) { // json으로 응답하기에 페이지 id를 몰라도 됨 User sessionUser = (User) session.getAttribute("sessionUser"); replyService.delete(replyId, sessionUser.getId()); return ResponseEntity.ok(new ApiUtil<>(null)); } @PostMapping("api/replies") public ResponseEntity<?> save(@RequestBody ReplyRequest.SaveDTO reqDTO) { User sessionUser = (User) session.getAttribute("sessionUser"); Reply reply = replyService.save(reqDTO, sessionUser); return ResponseEntity.ok(new ApiUtil<>(reply)); } }
      package shop.mtcoding.blog.reply; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import shop.mtcoding.blog._core.errors.exception.Exception403; import shop.mtcoding.blog._core.errors.exception.Exception404; import shop.mtcoding.blog.board.Board; import shop.mtcoding.blog.board.BoardJPARepository; import shop.mtcoding.blog.user.User; @RequiredArgsConstructor @Service public class ReplyService { private final BoardJPARepository boardJPARepository; private final ReplyJPARepository replyJPARepository; @Transactional public Reply save(ReplyRequest.SaveDTO reqDTO, User sessionUser) { Board board = boardJPARepository.findById(reqDTO.getBoardId()) .orElseThrow(() -> new Exception404("없는 게시글에 댓글을 작성할 수 없어요")); Reply reply = reqDTO.toEntity(sessionUser, board); replyJPARepository.save(reply); return reply; } }
       
      Share article

      vosw1