주말의 휴식을 통해서 다시 코드를 잡게 되는 날이다. 프로젝트를 다시 손에 잡게 되고, 오랜만에 다시 CRUD를 구현하는 과정을 거쳐갔다. 다시 잡은 CRUD는 이번에 새로웠다. 컨트롤러를 클래스 내에서 관리함으로써, 해당 기능을 적용하는 식으로 진행해 나갔다. 저번 팀원이 바뀌는 만큼, ApiResponse를 따로 만들어서 감싸주는 클래스를 하나 만들어서 오류코드와, 응답 메시지 등 해당 클래스내에서 관리 하도록 만들게 하고 컨트롤러 단에서 많은 변화가 생겼다.
package com.example.bookreviewapp.common.response;
import org.springframework.http.ResponseEntity;
import com.example.bookreviewapp.common.code.BaseCode;
import com.example.bookreviewapp.common.code.BaseErrorCode;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class ApiResponse<T> {
private final Boolean isSuccess; // 성공 여부
private final String code; // 응답 코드
private final String message; // 응답 메시지
@JsonInclude(JsonInclude.Include.NON_NULL)
private final T payload; // 실제 응답 데이터 (성공 시 포함)
// 성공 응답
public static <T> ResponseEntity<ApiResponse<T>> onSuccess(BaseCode code, T payload) {
ApiResponse<T> response = new ApiResponse<>(true, code.getReasonHttpStatus().getCode(),
code.getReasonHttpStatus().getMessage(), payload);
return ResponseEntity.status(code.getReasonHttpStatus().getHttpStatus()).body(response);
}
// 성공 응답 (payload 없음)
public static <T> ResponseEntity<ApiResponse<T>> onSuccess(BaseCode code) {
ApiResponse<T> response = new ApiResponse<>(true, code.getReasonHttpStatus().getCode(),
code.getReasonHttpStatus().getMessage(), null);
return ResponseEntity.status(code.getReasonHttpStatus().getHttpStatus()).body(response);
}
// 실패 응답
public static <T> ResponseEntity<ApiResponse<T>> onFailure(BaseErrorCode code) {
ApiResponse<T> response = new ApiResponse<>(false, code.getReasonHttpStatus().getCode(),
code.getReasonHttpStatus().getMessage(), null);
return ResponseEntity.status(code.getReasonHttpStatus().getHttpStatus()).body(response);
}
}
이번에 적용하게 되는 ApiRepsonse 클래스다. 팀원분 중 한분의 의견을 수렴하여 새로운 컨트롤러를 적용하여 응답 코드를 받아 보는 식으로 진행하게 되었다. 이 클래스는 API 성공여부를 Boolean 타입으로, code / message에서 성공,실패 응답코드와 상태 메세지 를 다룰수 있고, 실제 응답 데이터들을 Payload 부분에서 관리 한다.
Dto와 entity는 항상 있다고 가정하도록 하겠다.
BookRepository는 JPA가 관리한다.
📖 도서 CRUD
1️⃣ 도서 생성


컨트롤러에서 ApiReponse로 다루게 된다는 점이 이번에 처음 적용해보는 경험을 가질수 있게 되었고, 인증/인가 역할을 맡으신 분을 통해서 Spring Security의 UserDetails를 통해 유저정보를 쉽게 가져올수 있게 작성할 수 있었다.
서비스는 Book 엔티티의 생성자를 통해서 비즈니스 로직을 쉽게 짤수 있다는 것을 다시 깨닫게 되는 시간이었고, 이제는 예외처리를 RuntimeException이 아닌 직접만든 클래스로 관리하게 해보았다. 트랜잭션을 달아주는것을 빼먹지 않았다 ! !
2️⃣ 도서 목록 조회 ( + 페이징 )


컨트롤러에서 페이징 처리는 아주 간단하게 할수 있는 방법으로 @PageableDefault를 활용하는 것이다. 이를 통해 쉽게 페이징 처리를 할수 있고 이 어노테이션에서는 기본값으로 size와 page 처리가 10, 0으로 처리되어 있다. direction을 통해서 Sort.Direction.desc[asc] 정렬을 할수 있고, sort는 어떤 컬럼을 기준으로 정렬할 것인지를 결정한다.
서비스에서는 from 메소드는 book 엔티티의 static 메소드로 선언하여, BookResponseDto의 모든 컬럼들을 다 가져와서 출력하도록 하였고 이 메소드의 목적은 entitiy 정보들을 dto로 변환하는 메소드 역할을 한다.
3️⃣ 도서 수정


컨트롤러에서 쿼리 파라미터로 id값을 받고, 수정할 데이터들을 따로 requestDto를 만들어서 서비스단으로 데이터를 보내준다.
서비스에서 주요 로직이 이루어지는데, 수정에서도 트랜잭션을 걸어주고, DB에 등록된 도서가 없다면 예외처리를 반환해주고, 역시 이번에 적용해본 ApiException으로 처리하고, Book 엔티티의 메소드인 update메소드를 만들어줘서 수정할 데이터 값을 실행 시켜준다. 그러고 그 데이터를 변수로 저장하여 save를 통해 저장시켜주고, 결과값을 반환 받는다.
4️⃣ 도서 삭제


컨트롤러에서 삭제는 가장 간단해서, 바로 Book 데이터를 가져오기 위해 @PathVariable로 id값을 가져오고, 서비스단으로 데이터를 보내준다.
서비스 에서 DB에 등록된 도서가 없다면 예외처리를 해주고, 찾은 값이면, delete 메소드를 통해 삭제 처리를 해주면 끝난다.
CRUD에 관해서는 이제 거의 기본에 가까워서 자세한 설명은 생략하도록 하겠다.
💥 트러블 슈팅
1️⃣ 도서 생성 트러블 슈팅
배경
도서를 생성할때 nullPointerException 되는 현상.

과정


final을 적용해주지 않아서 생성자 주입이 되지 않았던 상황이다 아무리 @RequiredArgsConstructor어노테이션을 적용해도 final을 해주지 않으면 생성자의 필요성을 느끼지 않다는것을 깨달았다.
해결
final을 주입해줌으로써 정상적으로 작동이 가능하게 되었다.
추가적인 문제상황 ( 배경 2 )
정상적인 요청값을 보내 이번엔 도서 생성이 되지만, 생성일과 수정일이 null 표시가 된다는 현상 발견.
과정

처음에는 BaseEntity가 잘못됫나 싶어서 확인해보았지만, 전 프로젝트를 참고하면서 보니까 정상적으로 잘 작성 되어 있었다. 그런데도 왜 안됬었냐? 하고 보니까 SpringBoot 프로젝트의 api 실행하는 시점에서 @EnableJpaAuditing 가 누락 되어 있어서 자동으로 일정일을 Auditing 하고 있지 않아서, null이 발생한다는 점을 인지하였다.
해결
과정을 기반으로 해결을 하여 해당 어노테이션을 추가함으로써 문제 상황을 해결할 수 있게 되었다.
2️⃣ 도서 조회 트러블 슈팅
배경
조회 코드 작성후 IntelliJ내 'Type expected', 'Identifier expected' 에러 하지만 서버는 정상적으로 작동
과정

33번 줄 pageable 뒤쪽에서 빨간줄 이 생겼는데 제 아무리 코드상으로 문제는 없고 서버가 잘 작동하였다. 그래서 도저히 문제가 해결이 되지가 않자 AI한테 문제를 제시해보았다. 그러더니 IDE 캐시나 인데스 문제일 가능성이 높다고 하였다. 내부적으로 코드를 분석한 정보(캐시, 인덱스)가 꼬였을 때 이런일이 발생할 가능성이 있다고 답변해주어 인지하게 되었다.
해결
IntelliJ "Invalidate Caches / Restart..." (캐시 무효화 및 재시작) 기능을 사용하여 문제를 해결할 수 있었다.
🔍 AI의 답변
IDE 캐시나 인덱스 문제 (이게 제일 흔해! 짱 중요!)
놓치기 쉬운 이유: 코드는 전혀 건드리지 않았는데 갑자기 에러가 뜨거나, 분명 수정한 것 같은데 이전 에러가 그대로 남아있는 경우야. IDE(IntelliJ, Eclipse 등)가 파일 변경 사항을 제대로 반영하지 못하거나, 내부적으로 코드를 분석한 정보(캐시, 인덱스)가 꼬였을 때 이런 일이 일어나.
왜 에러가 뜰까?: IDE는 자기 머릿속 정보로 코드를 검사하는데, 그 정보가 최신이 아니거나 잘못되면 멀쩡한 코드를 이상하다고 생각하고 에러 표시를 띄우는 거지.
해결: IDE 메뉴에서 "Invalidate Caches / Restart..." (캐시 무효화 및 재시작) 기능을 사용해보자! 이게 마법처럼 해결될 때가 많단다!
- 놓치기 쉬운 이유: 코드는 전혀 건드리지 않았는데 갑자기 에러가 뜨거나, 분명 수정한 것 같은데 이전 에러가 그대로 남아있는 경우야. IDE(IntelliJ, Eclipse 등)가 파일 변경 사항을 제대로 반영하지 못하거나, 내부적으로 코드를 분석한 정보(캐시, 인덱스)가 꼬였을 때 이런 일이 일어나.
- 왜 에러가 뜰까?: IDE는 자기 머릿속 정보로 코드를 검사하는데, 그 정보가 최신이 아니거나 잘못되면 멀쩡한 코드를 이상하다고 생각하고 에러 표시를 띄우는 거지.
- 해결: IDE 메뉴에서 "Invalidate Caches / Restart..." (캐시 무효화 및 재시작) 기능을 사용해보자! 이게 마법처럼 해결될 때가 많단다!
📅 내일 할것
- 상세조회 ( + 평점/리뷰/좋아요 조회수 포함 ) 구현
- 평점을 SQL AVG 함수를 통해서 한번 다뤄볼까 생각중이다.
- 성능 최적화( Cache, Redis ) , CI/CD, 동시성 제어 아이디어 생각해보기
💡 참고
✅ 검색 기능
GetMapping 사용시 -> 쿼리 파라미터를 통해 url에 키워드를 삽입해서 검색
PostMapping 사용시 -> json body에 키워드를 담아서 검색 ( 쿼리 파라미터 특성상 특수문자가 깨질 위험이있음 )
✅ Cache 기능 고민
좋아요 캐싱 기능 을 고민해보자
인플루언서가 게시물을 올라오면 좋아요가 아주 많이 올라올 것이다.
요청이 있을때마다, cache에 한번에 모았다가 한번에 빵 한번에 빵 등등 캐시전략을 고민해봤으면 좋겠다!!
'[내일배움캠프-Sparta] > Spring 6기' 카테고리의 다른 글
| TIL 60 [ Spring 실전 프로젝트 ( Day 3 ) - 기본 API 구현/병합/전체 테스트, Cache + Redis 세션 ] (0) | 2025.05.20 |
|---|---|
| 📦 Redis : 개념부터 캐싱 전략까지 (0) | 2025.05.20 |
| TIL 58 [ Spring 실전 프로젝트 ( Day 1 ) - 설계 ( ERD, API, 와이어프레임 ) ] (0) | 2025.05.16 |
| TIL 57 [ AWS( EC2, ELB, RDS ) ] (0) | 2025.05.15 |
| TIL 56 [ 통합 테스트 + JPA 심화 플러스 과제 ( Lv 3-1 ) ] (0) | 2025.05.14 |