📌 REST API 구조 전환 핵심 요약
항목 | 변경 전 | 변경 후 |
URL 스타일 | /board/save , /reply/{id}/delete 등 동사 포함 주소 | /board , /reply/{id} 등 RESTful 주소 |
요청 타입 | application/x-www-form-urlencoded | application/json |
HTTP 메서드 | GET , POST 만 사용 | GET , POST , PUT , DELETE 구분 |
데이터 전달 방식 | request.setAttribute() 로 모델 전달 | DTO 객체를 JSON 으로 직렬화 |
응답 구조 | 뷰 리턴 ( redirect: ) | @ResponseBody + Resp<?> JSON 구조 |
엔티티 직렬화 | Lazy 로딩 충돌 발생 | DTO로 변환 후 안전 직렬화 |
✅ 주요 변화 상세 정리
1. 주소에서 동사 제거 + HTTP 메서드 분리
- 기존에는 URL 자체가 기능을 표현함 (
/reply/save
,/reply/{id}/delete
)
- → REST API에서는 자원(명사)을 주소로, 동작은 HTTP 메서드로
기능 | 변경 전 | 변경 후 |
댓글 저장 | POST /reply/save | POST /reply |
댓글 삭제 | POST /reply/{id}/delete | DELETE /reply/{id} |
게시글 저장 | POST /board/save | POST /board |
게시글 목록 | GET /board/list (뷰 반환) | GET /board/list (JSON 응답) |
게시글 상세 | GET /board/{id} (뷰 반환) | GET /board/{id}/detail (JSON 응답) |
회원 가입 | POST /join (뷰 or redirect) | POST /join (JSON 응답) |
회원 수정 | POST /user/update 등 | PUT /user |
뷰 모델 세팅 | request.setAttribute("model", dto) | ❌ 사용 안 함 (클라이언트 렌더링 or API 전용) |
✅ 기능별 구조 전환
1. 글 목록 보기 (GET)
@GetMapping("/board/list")
public @ResponseBody Resp<?> getList(@RequestParam(defaultValue = "") String keyword, ...) {
BoardResponse.ListDTO listDTO = boardService.글목록보기(keyword, ...);
return Resp.ok(listDTO);
}
/board/list
로 조회
- Entity → DTO 변환 후 응답
- Lazy 로딩 방지 위해 내부에서
.map(BoardResponse.DTO::new)
사용
📦 JSON 응답 예시:
{
"status": 200,
"msg": "성공",
"body": {
"boards": [
{
"id": 20,
"title": "제목20",
"content": "내용20",
"isPublic": true,
"userId": 1,
"createdAt": "2025-05-07 11:27:51.027589"
},
{
"id": 19,
"title": "제목19",
"content": "내용19",
"isPublic": true,
"userId": 1,
"createdAt": "2025-05-07 11:27:51.02659"
},
{
"id": 18,
"title": "제목18",
"content": "내용18",
"isPublic": true,
"userId": 1,
"createdAt": "2025-05-07 11:27:51.02659"
}
],
"prev": -1,
"next": 1,
"current": null,
"size": 3,
"totalCount": 19,
"totalPage": 7,
"isFirst": true,
"isLast": false,
"numbers": [
0,
1,
2,
3,
4
],
"keyword": ""
}
}
2. 글 보기(업데이트 글보기) (GET)
@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);
}
- 로그인한 유저의 ID를 기반으로 게시글을 조회함
- 예: 본인이 쓴 글인 경우, 수정/삭제 버튼 활성화 등을 컨트롤 가능
- JSON으로 응답 → 프론트엔드 분리 구조에 적합
@ResponseBody
+Resp.ok()
조합으로 표준화된 응답 구조 사용
📦 JSON 응답 예시:
{
"status": 200,
"msg": "성공",
"body": {
"id": 4,
"title": "제목4",
"content": "내용4",
"isPublic": true,
"userId": 3,
"createdAt": "2025-05-07 10:34:26.612766"
}
}
3. 글 상세 보기 (GET)
@GetMapping("/board/{id}/detail")
public @ResponseBody Resp<?> getBoardDetail(@PathVariable Integer id) {
BoardResponse.DetailDTO detailDTO = boardService.글상세보기(id);
return Resp.ok(detailDTO);
}
/board/{id}/detail
로 API 요청
- 기존
request.setAttribute()
제거
- 댓글 리스트까지 포함해 JSON으로 응답
📦 JSON 응답 예시:
{
"status": 200,
"msg": "성공",
"body": {
"id": 4,
"title": "제목4",
"content": "내용4",
"isPublic": true,
"isOwner": true,
"isLove": false,
"loveCount": 2,
"username": "love",
"createdAt": "2025-05-07T01:34:26.612+00:00",
"loveId": null,
"replies": [
{
"id": 3,
"content": "댓글3",
"username": "ssar",
"isOwner": false
},
{
"id": 2,
"content": "댓글2",
"username": "cos",
"isOwner": false
},
{
"id": 1,
"content": "댓글1",
"username": "ssar",
"isOwner": false
}
]
}
}
4. 글 쓰기 (POST)
@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);
}
@RequestBody
를 통해 JSON으로 데이터 받음
- 뷰 리턴 대신 DTO로 응답
5. 글 수정 (PUT)
@PutMapping("/board/{id}")
public @ResponseBody Resp<?> update(@PathVariable Integer id, @Valid @RequestBody BoardRequest.UpdateDTO updateDTO, Errors errors) {
User sessionUser = (User) session.getAttribute("sessionUser");
BoardResponse.DTO respDTO = boardService.글수정(id, updateDTO, sessionUser);
return Resp.ok(respDTO);
}
- HTTP 메서드
PUT
사용으로 의미 명확화
- JSON 기반 DTO 전달
6. 댓글 쓰기 (POST)
@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);
}
7. 댓글 삭제 (DELETE)
@DeleteMapping("/reply/{id}")
public @ResponseBody Resp<?> delete(@PathVariable Integer id) {
User sessionUser = (User) session.getAttribute("sessionUser");
replyService.댓글삭제(id, sessionUser.getId());
return Resp.ok(null);
}
DELETE
메서드 사용으로 URL에 동사 제거
- 댓글 삭제 시에도 JSON 응답 유지
📦 JSON 응답 예시:
{
"status": 200,
"msg": "성공",
"body": null
}
8. 회원가입 (POST)
@PostMapping("/join")
public @ResponseBody Resp<?> join(@Valid @RequestBody UserRequest.JoinDTO reqDTO, Errors errors) {
UserResponse.DTO respDTO = userService.회원가입(reqDTO);
return Resp.ok(respDTO);
}
- 기존
@ModelAttribute
방식 제거
- JSON 기반 요청으로 전환
9. 회원정보 수정 (PUT)
@PutMapping("/user")
public @ResponseBody Resp<?> 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 Resp.ok(null);
}
- PUT 메서드로 REST 설계에 부합
- 수정 후 세션 갱신 처리
🧠 기타 정리: ByteBuddyInterceptor 관련
Lazy 로딩 중 JSON 직렬화 과정에서 연관 객체를 접근하면 발생하는 예외
com.fasterxml.jackson.databind.JsonMappingException: ByteBuddyInterceptor
예외 발생
- 이유: 엔티티를 JSON으로 직렬화할 때 연관 객체에 접근 → 세션 종료로 인한 Lazy 로딩 실패
해결
→ 반드시 Entity가 아닌 DTO로 변환해서 JSON 직렬화할 것
this.boards = boards.stream()
.map(board -> new DTO(board))
.toList();
Share article