
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