2차 팀 프로젝트 : BookBox (Back-End)

도서 대여/예약 앱 플랫폼 : 대여/반납, 유저
SHIN's avatar
Oct 22, 2024
2차 팀 프로젝트 : BookBox (Back-End)
 
 
JPQL Update한 객체를 바로 Return 받을 수 있을까 ?

LendRepository.java

// 대여중인 도서 연장 @Modifying @Query("update Lend l SET l.extendStatus = true where l.user.id = :userId AND l.book.isbn13 = :isbn13") void mExtendLend(@Param("userId") Long userId, @Param("isbn13") String isbn13);
 
→ JPQL update 후 update 한 걸 service 단에서 Response DTO 에 담아서 return 해야해서
void 가 아닌 Lend 객체를 return 하려고 했는데 안됨.
 
챗 지피티한테 물어보니
 
💡
JPQL의 update 쿼리는 기본적으로 엔티티를 반환하지 않기 때문에, update 쿼리에서 바로 수정된 엔티티를 반환하는 것은 불가능합니다. JPQL에서 update는 단순히 영향을 받은 행의 수를 반환하며, 수정된 엔티티는 반환하지 않습니다.
하지만, update 쿼리 이후에 해당 Lend 엔티티를 다시 조회하여 반환할 수 있습니다. 즉, update 쿼리를 실행한 다음에 다시 한 번 select 쿼리를 사용하여 수정된 엔티티를 조회하는 방식으로 해결할 수 있습니다.
아래는 예시 코드입니다:
java 코드 복사 @Transactional @Modifying @Query("update Lend l SET l.extendStatus = true where l.user.id = :userId AND l.book.isbn13 = :isbn13") void updateExtendStatus(@Param("userId") Long userId, @Param("isbn13") String isbn13); @Query("select l from Lend l where l.user.id = :userId AND l.book.isbn13 = :isbn13") Lend findUpdatedLend(@Param("userId") Long userId, @Param("isbn13") String isbn13);
그리고 서비스 레이어에서 이를 다음과 같이 처리할 수 있습니다:
java 코드 복사 @Transactional public Lend extendLend(Long userId, String isbn13) { lendRepository.updateExtendStatus(userId, isbn13); // extendStatus를 업데이트 return lendRepository.findUpdatedLend(userId, isbn13); // 업데이트된 Lend 엔티티 반환 }

요약:

  • @Modifyingupdate 쿼리에서는 엔티티를 반환할 수 없으므로, 업데이트 후에 별도의 select 쿼리를 통해 수정된 데이터를 다시 조회해야 합니다.
  • 트랜잭션을 사용하여 updateselect가 동일한 트랜잭션 내에서 일어날 수 있도록 처리해야 합니다.
이 방식으로 update한 후에 수정된 Lend 엔티티를 반환할 수 있습니다.
 
JPQL에서 Update 쿼리는 데이터베이스의 값을 수정하기 위한 것이며,
update문은 기본적으로 변경된 객체를 반환하지 않는다고 한다.
그래서 JPQL에서는 update 쿼리를 수행한 후 select 쿼리를 통해 변경된 객체를 다시 조회해서 변경된 객체를 가져와야 한다.
 
 
error: package does not exist
 
notion image
 
💡
C:\metacoding\springwork\bookbox-api\src\main\java\green\mtcoding\bookbox\book\Book.java:4: error: package green.mtcoding.bookbox.comment does not exist import green.mtcoding.bookbox.comment.Comment; ^
 
import 경로 맞게 되어있는데 서버 실행 시 does not exist 에러가 발생하면
 
notion image
 
세팅 → annotation processors → enable annotation processing 체크하고
 
💡
Enable annotation processing 옵션을 활성화하는 이유 : IntelliJ에서 Enable annotation processing 옵션을 활성화하면, 애노테이션 프로세싱을 사용하도록 설정하게 됩니다. 애노테이션 프로세싱이란 컴파일 시점에 애노테이션을 기반으로 추가 코드를 생성하거나, 기존 코드를 수정하는 기능을 제공하는 과정입니다. 이 옵션을 체크하면 주로 다음과 같은 변화가 있습니다:

1. Lombok과 같은 라이브러리에서 자동 코드 생성 활성화

  • Lombok과 같은 라이브러리를 사용할 때, @Getter, @Setter, @Builder, @AllArgsConstructor 등의 애노테이션을 붙이면, 해당 애노테이션을 처리해서 자동으로 필요한 메서드나 생성자를 생성합니다.
  • 애노테이션 프로세싱이 활성화되지 않으면 이러한 코드를 컴파일 시점에 생성하지 않으므로, 코드를 제대로 작성했더라도 컴파일 오류가 발생할 수 있습니다.

2. 컴파일 시 애노테이션 기반 코드 생성 활성화

  • Hibernate Validator, MapStruct, QueryDSL, Dagger와 같은 라이브러리에서도 애노테이션을 사용해 컴파일 시 자동으로 코드를 생성합니다.
  • 예를 들어, MapStruct를 사용하여 매핑 메서드를 자동 생성하려면 애노테이션 프로세싱이 필요합니다.

3. 왜 이걸 체크해야 하는가?

  • 많은 라이브러리들이 애노테이션을 활용하여 추가적인 코드를 자동 생성합니다. 이러한 라이브러리들이 제대로 동작하려면 애노테이션 프로세서가 활성화되어 있어야 합니다.
  • 기본적으로는 애노테이션 프로세서를 활성화하지 않아도 괜찮은 경우도 있지만, 특정 라이브러리를 사용할 때는 반드시 필요합니다. Lombok과 같은 라이브러리가 대표적인 예입니다. 이를 활성화하지 않으면 package does not exist 같은 오류가 발생할 수 있습니다.

결론:

  • *Enable annotation processing*을 체크하면 컴파일 시 애노테이션 기반으로 추가적인 코드를 자동으로 생성할 수 있게 됩니다. 만약 Lombok이나 다른 애노테이션을 사용하는 라이브러리를 사용하고 있다면, 이를 반드시 활성화해야 코드 생성 및 빌드가 정상적으로 진행됩니다.
 
notion image
 
gradle → 위 2개 intelliJ IDEA 로 설정해주기
+ 추가로 아래 Gradle JVM 제대로 설정되어있는지 확인하기
 
 
그리고 실행하면 돌아간다.
 
 
 
자동 반납 기능 → @Scheduled 사용
 
서버가 켜져 있을 때 자동으로 매서드를 실행시키고 싶을 때
 
여러가지 방법을 사용할 수 있다.
 
💡
Spring에서 스케줄링 작업을 설정하는 방법 중 하나로 @Scheduled 애노테이션을 사용하는 방법은 매우 편리하고 간단한 방법입니다. 하지만 이 방법에는 장점과 단점이 모두 존재합니다. 또한, @Scheduled 외에도 스케줄링을 설정하는 다른 방법들이 있습니다.

@Scheduled 애노테이션을 사용하는 이유 (장점)

  1. 간편한 설정:
      • @Scheduled 애노테이션을 메서드에 붙이는 것만으로 손쉽게 주기적인 작업을 설정할 수 있습니다. 추가적인 설정 파일이나 복잡한 구성 없이 바로 사용할 수 있습니다.
        • java 코드 복사 @Scheduled(cron = "0 0 0 * * *") // 매일 자정에 실행 public void autoReturnBooks() { // 작업 내용 }
  1. 다양한 스케줄링 옵션 제공:
      • @Scheduled는 다양한 스케줄링 방법을 제공합니다.예시:
        • fixedDelay: 이전 작업이 끝난 후 일정 시간 지연 후 다음 작업 실행.
        • fixedRate: 이전 작업이 시작된 시점으로부터 일정 간격으로 작업 실행.
        • cron: cron 표현식을 사용하여 스케줄을 설정.
        • java 코드 복사 @Scheduled(fixedDelay = 5000) // 5초마다 실행 @Scheduled(fixedRate = 10000) // 10초마다 실행
  1. 빠른 테스트와 유지보수:
      • 복잡한 설정 없이 간단히 메서드에 애노테이션을 추가해 테스트하거나 수정할 수 있어 유지보수가 용이합니다.
  1. 멀티 스레드 지원:
      • 여러 개의 스케줄러가 동시에 실행되더라도 스프링의 TaskExecutor가 이를 관리해 주므로 멀티 스레드 환경에서 잘 동작합니다.

@Scheduled 애노테이션을 사용하지 말아야 할 이유 (단점)

  1. 복잡한 스케줄 관리에 적합하지 않음:
      • 단순한 작업에는 적합하지만, 동적 스케줄 변경, 여러 서버 간의 작업 분배, 작업 실패 시 재시도 같은 복잡한 스케줄링 요구 사항에는 적합하지 않습니다.
      • 주기적으로 변경되거나 외부 시스템과 연동된 스케줄 설정이 필요할 경우 불편할 수 있습니다.
  1. 클러스터 환경에서 문제:
      • 여러 인스턴스(서버)가 있는 클러스터 환경에서는 모든 인스턴스에서 같은 스케줄이 실행될 수 있습니다. 이로 인해 중복 작업이 발생하거나 리소스가 낭비될 수 있습니다.
      • 예를 들어, 스케줄링이 단일 인스턴스에서만 실행되어야 할 경우 @Scheduled는 직접적으로 이를 제어할 방법이 없습니다.
  1. 스케줄링 변경이 불편:
      • 스케줄 변경을 위해서는 코드를 수정한 후 재배포해야 합니다. 즉, 스케줄 주기를 동적으로 변경하는 것이 불가능합니다.
  1. 상태 저장이 불가능:
      • @Scheduled 방식은 상태 저장이 어렵고, 스케줄의 실행 결과를 기록하거나 실패 시 재시도 등의 로직을 구현하기 어렵습니다.

스케줄링을 설정하는 다른 방법

  1. Quartz Scheduler:
      • Quartz는 매우 강력하고 유연한 스케줄링 라이브러리입니다. @Scheduled와 달리, Quartz는 클러스터링, 작업 재시도, 영구 저장, 복잡한 스케줄 관리 등 다양한 기능을 제공합니다.
        • 장점:
        • 분산 환경에서도 안정적으로 스케줄을 관리할 수 있습니다.
        • 스케줄 작업의 영속성(작업 스케줄을 DB에 저장 가능)을 지원하며, 작업이 중단되거나 실패했을 때 재시도할 수 있는 기능이 있습니다.
        • 다양한 트리거(시간 조건)를 지원하여 매우 세밀한 스케줄 관리가 가능합니다.
        • 단점:
        • 복잡한 설정과 추가적인 의존성 관리가 필요합니다.
        • 설정과 코드가 복잡해질 수 있으므로 간단한 작업에는 과도할 수 있습니다.
        • 예시:
          java 코드 복사 @Bean public JobDetail jobDetail() { return JobBuilder.newJob(MyJob.class) .withIdentity("myJob") .storeDurably() .build(); } @Bean public Trigger trigger() { return TriggerBuilder.newTrigger() .withIdentity("myTrigger") .withSchedule(CronScheduleBuilder.cronSchedule("0 0 12 * * ?")) .forJob(jobDetail()) .build(); }
  1. Spring TaskScheduler:
      • Spring에서 제공하는 TaskScheduler 인터페이스를 사용하면 @Scheduled 애노테이션 없이 프로그래밍 방식으로 스케줄을 관리할 수 있습니다.
      • 동적으로 스케줄을 변경하거나 실행할 수 있는 장점이 있습니다.
        • 장점:
        • 동적으로 작업을 추가하거나 삭제할 수 있습니다.
        • 더 많은 제어권을 제공하며, @Scheduled보다 유연한 방식으로 스케줄을 관리할 수 있습니다.
        • 단점:
        • 코드가 더 복잡해질 수 있습니다.
        • 예시:
          java 코드 복사 @Bean public TaskScheduler taskScheduler() { return new ConcurrentTaskScheduler(); // 기본 TaskScheduler 구현체 } public void scheduleTask() { taskScheduler().schedule(new RunnableTask(), new CronTrigger("0 0 0 * * *")); }
  1. Externally Managed Schedulers:
      • Kubernetes CronJobs 또는 AWS Lambda와 같은 외부 시스템의 스케줄링 기능을 사용하여 서버 외부에서 작업을 스케줄링할 수 있습니다.
      • 이를 통해 서버에 부담을 주지 않고 스케줄을 관리할 수 있으며, 관리 도구를 사용해 쉽게 모니터링할 수 있습니다.
      장점:
      • 클러스터 환경이나 서버 리소스를 최적화할 수 있으며, 외부 시스템에서 쉽게 관리할 수 있습니다.
      • 서버에 종속적이지 않고, 분산 시스템에 적합한 스케줄링을 할 수 있습니다.
      단점:
      • 외부 도구와의 통합이 필요하므로 추가적인 설정이나 복잡성이 있을 수 있습니다.

결론:

  • @Scheduled는 간단한 스케줄 작업에 적합하고, 코드 수정 없이 빠르게 주기적 작업을 설정하는 데 유용합니다.
  • 그러나 복잡한 스케줄링 요구 사항이 있거나 클러스터링 환경에서 안정적인 작업 관리가 필요할 경우에는 Quartz나 외부 스케줄러를 사용하는 것이 더 적합합니다.
  • Spring의 TaskScheduler를 사용하면 동적으로 스케줄 작업을 추가할 수 있어 유연한 스케줄링 관리가 가능합니다.
 
Quartz 사용을 고려해보았지만
 
지금 우리 프로젝트에서 스케줄 작업을 하는건 자동반납 기능밖에 없고
 
개발 기간이 짧아 빠르게 테스트와 유지보수가 가능해야 했기 때문에
 
Spring 프레임워크에 내장된 @Scheduled 를 사용해보았다. ( ← Spring의 Task 모듈에서 제공 )
 
 
어노테이션 사용을 위해
 
notion image
 
  1. 먼저 애플리케이션의 설정 클래스에 @EnableScheduling 어노테이션 추가
 
 
 
notion image
 
  1. 스케줄링할 메서드에 @Scheduled 어노테이션 붙이기
 
@Scheduled(cron = "0 0 0 * * *")
→ 자정에 실행시키도록 하는 cron이 있어서 이걸 사용했다.
 
notion image
 
  1. application.properties 파일에 서버 측에서 사용하는 기본 시간대를 서울로 설정하는 옵션 추가.
( → 스케줄링 작업 시, spring.mvc.timezone을 설정하지 않으면, 기본적으로 UTC 시간대가 적용되어서
데이터 베이스에 현재 한국/서울 시간이 저장되지 않는다. ) 아니면
💡
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MySQL;INIT=SET TIMEZONE 'Asia/Seoul'
 
이렇게 H2 데이터베이스에 시간대를 명시적으로 지정해 줄 수도 있다.
 
  • 추가 ) spring.jackson.time-zonespring.mvc.timezone 의 차이
 
💡
spring.jackson.time-zonespring.mvc.timezone 의 차이
Spring Boot의 application.properties 파일에서 설정하는 두 가지 속성인 spring.jackson.time-zonespring.mvc.timezone는 각각 다른 용도로 사용되며, 주로 시간대(Time Zone)를 다루는 방법에서 차이가 있습니다. 이 두 속성의 역할을 구체적으로 살펴보겠습니다:

1. spring.jackson.time-zone

  • 역할: JSON 직렬화 및 역직렬화 시 Jackson 라이브러리가 사용할 시간대를 설정하는 옵션입니다.
  • 적용 범위: 이 설정은 주로 HTTP 요청/응답 바디에서 시간 데이터를 직렬화하거나 역직렬화할 때, 즉 JSON 형식의 데이터를 주고받을 때 적용됩니다.
  • 사용 시점: 서버에서 Date, LocalDateTime 같은 시간 데이터를 JSON으로 변환하거나 JSON 데이터를 시간 객체로 변환할 때, 지정된 시간대로 처리합니다.
    • properties 코드 복사 spring.jackson.time-zone=Asia/Seoul
      이 설정을 사용하면 서버가 JSON으로 응답을 보낼 때 Asia/Seoul 시간대를 기준으로 시간 데이터를 직렬화하며, 클라이언트로부터 받은 시간 데이터도 해당 시간대로 역직렬화하게 됩니다.

2. spring.mvc.timezone

  • 역할: Spring MVC에서 서버 측에서 사용하는 기본 시간대를 설정하는 옵션입니다.
  • 적용 범위: 이 설정은 Spring MVC에서 컨트롤러가 처리하는 요청에서 날짜와 시간을 다룰 때 사용됩니다. 주로 웹 애플리케이션 전반에서 시간대를 다룰 때 기본적으로 사용할 시간대를 지정합니다.
  • 사용 시점: 서버가 사용자 요청을 처리할 때, 또는 @RequestParam, @ModelAttribute 같은 방법으로 시간 관련 데이터를 받아 처리하는 곳에서 이 시간대를 기준으로 처리하게 됩니다.
    • properties 코드 복사 spring.mvc.timezone=Asia/Seoul
      이 설정을 사용하면 Spring MVC 애플리케이션의 모든 시간대 관련 작업에서 Asia/Seoul을 기본으로 적용합니다. 예를 들어, 사용자가 시간을 입력하여 폼을 제출할 때 이 시간대가 기준이 됩니다.

차이점 요약

  • spring.jackson.time-zone: JSON 직렬화 및 역직렬화에서 Jackson이 사용할 시간대를 설정합니다. 주로 API 통신 시 JSON 형태의 데이터에 적용됩니다.
  • spring.mvc.timezone: Spring MVC 애플리케이션 전반에서 시간 데이터를 처리할 때 사용할 기본 시간대를 설정합니다. 주로 서버 측에서 시간대 관련 처리에 영향을 미칩니다.
따라서, **spring.jackson.time-zone**은 JSON 데이터의 직렬화/역직렬화 시 사용되는 Jackson 라이브러리에 적용되고, **spring.mvc.timezone**은 MVC 레이어에서 시간 데이터를 다룰 때 적용됩니다.
 
 
깃허브에만 있는 브랜치 가져오기
 
집에서 작업하면서 로컬에 user/join-check 브랜치를 만들었고, 작업 후 깃허브에 push 했다.
( → 아직 dev 브랜치에 merge 는 되지 않은 상태이다. )
 
그리고 다음날 학원에서 이어서 작업하기 위해서 dev 브랜치로 이동해서
 
git pull origin dev 를 해주었고, 이어서 git checkout user/join-check 를 하려고 하니까
 
notion image
 
error: pathspec 'user/join-check' did not match any file(s) known to git
 
이런 에러가 발생했다.
 
 
찾아보니
 
”이 에러 메시지는 user/join-check라는 브랜치가 원격 저장소(GitHub) 또는 로컬 저장소에서 현재 존재하지 않음을 의미합니다.
즉, Git이 해당 브랜치를 찾을 수 없다는 뜻입니다.”
 
 
→ 아직 원격 저장소에 생성된 브랜치가 로컬에 안 만들어져서 발생한 에러였다.
 
 
그래서 로컬의 dev 브랜치에서
 
  1. git fetch origin
  1. git checkout -b user/join-check origin/user/join-check
 
을 해주니 origin/user/join-check 을 트래킹해서 로컬에도 user/join-check 브랜치가 생겼다!
 
notion image
 
어제 집에서 user/join-check 브랜치에 push 한 내용도 현재 학원 로컬에 잘 받아져 있다.
 
 
 
도움 : 챗지피티
 
💡
현재 상황을 보면 1번 컴퓨터에서 user/join-check라는 브랜치를 만들고, GitHub에 푸시한 상태입니다. 그러나 2번 컴퓨터에서는 user/join-check라는 브랜치가 아직 로컬에 존재하지 않아서 checkout 명령어를 실행했을 때 "pathspec 'user/join-check' did not match any file(s) known to git"라는 에러가 발생한 것입니다.
이럴 때는 로컬에서 브랜치를 새로 만들 필요는 없습니다. 이미 GitHub에 user/join-check 브랜치가 존재하기 때문에, 로컬에 해당 브랜치를 가져오는 방법으로 해결할 수 있습니다.

해결 방법:

  1. 브랜치 목록을 업데이트하기 위해 아래 명령어를 사용하세요:
    1. git fetch origin
  1. 그리고 원격에 있는 브랜치를 체크아웃하면서 로컬에도 동일한 브랜치를 생성하려면, 다음 명령어를 사용하세요:
    1. git checkout -b user/join-check origin/user/join-check
이 명령어는 원격에 있는 user/join-check 브랜치를 로컬로 가져오고, 로컬에서도 동일한 브랜치로 체크아웃해줍니다. 이렇게 하면 로컬에도 동일한 브랜치가 생기고, 그 후부터는 해당 브랜치에서 작업할 수 있습니다.
이제 필요한 경우 git pull을 해서 최신 상태로 유지할 수 있습니다.
 
 
git bash 에서 커밋 메시지 작성할때 오타
notion image
 
git bash 에서 commit 메시지 작성할 때 오타가 나거나 띄어쓰기 잘못하면 이상한 문자가 들어가는데
 
깃허브에 push 하면 이렇게 이상한 문자가 들어간채로 올라간다.
PR요청할 때 Add a title 부분을 잘 적고 dev에 merge 하면
dev에는 정상적인 커밋 메시지가 뜨지만
기존에 push한 브랜치에는 이상한 문자가 들어가있다.
 
→ 깔끔하게 유지하려면 커밋할 때 한번에 잘 적어야 할 거 같다.
 
→ 이미 커밋된 이후라면 Commit 메시지를 수정하는 방법을 사용할 수 있다.
  1. 마지막 Commit 메시지 수정하기 → git commit —amend -m “바꿀 메시지”
  1. Commit 메시지 여러개 수정하기 →
    1. git rebase -i HEAD~3 ( ⇒ 여기서 마지막 숫자는 불러올 커밋수. 최근순 )
    2. 메시지를 수정하고 싶은 Commit 내역의 pick을 reword로 바꾸고  :  와 wq를 눌러서 터미널을 종료
    3. 메시지를 수정하고 wq로 저장
 
  1. Remote에 이미 push 했을 경우 → 커밋 메시지 수정 후 강제 push 하는 방법이 있지만 권장되지 않음
references :
 
 
 
반납일에 해당하는 도서 select 하기
 
자동 반납 기능에는 반납일에 해당하는 도서만 List<Lend> 에 담기도록 쿼리를 작성해야 한다.
 
 
@Query("SELECT l FROM Lend l WHERE FUNCTION('DAYOFMONTH', l.returnDate) = FUNCTION('DAYOFMONTH', CURRENT_TIMESTAMP) AND l.returnStatus = false") List<Lend> mFindAllByReturnDateAndReturnStatusFalse();
→ 처음에 day만 비교하는 쿼리를 사용했는데, 그러니 오늘이 10월 16일 이면
9월 16일, 11월 16일 것도 조회되는 대참사가….
 
→ 12시에 오늘 “일(day)”로 바뀌었을 때 날짜를 비교하는 것 까지는 맞지만
“연-월-일”을 모두 비교해야지 정확한 결과가 나온다.
 
@Query("SELECT l FROM Lend l WHERE CAST(l.returnDate AS date) = CURRENT_DATE AND l.returnStatus = false") List<Lend> mFindAllByReturnDateAndReturnStatusFalse();
→ “연-월-일”을 모두 비교해서 일치하는 것만 조회하도록 수정하니 정확한 결과가 나온다.
 
@Query("SELECT l FROM Lend l WHERE FUNCTION('DATE', l.returnDate) = CURRENT_DATE AND l.returnStatus = false") List<Lend> mFindAllByReturnDateAndReturnStatusFalse();
중간에 아래와 같이 쿼리를 작성해서 실행해보았는데,
 
H2는 DATE() 함수 대신 다른 방식을 사용해야 한다고 해서
CAST() 또는 TRUNC() 함수로 날짜 부분만 추출하도록 했다.
 
 
💡
이 에러는 H2 데이터베이스에서 DATE() 함수가 지원되지 않아서 발생한 것입니다. H2는 DATE() 함수 대신 다른 방식을 사용해야 하며, returnDate 필드를 날짜 부분만 추출하여 비교하는 방법을 사용할 수 있습니다.

해결 방법:

H2에서는 CAST() 또는 TRUNC() 함수로 날짜 부분만 추출할 수 있습니다. CAST()를 사용하여 returnDate의 날짜 부분만 가져오는 방식으로 쿼리를 수정해 보세요:
java 코드 복사 @Query("SELECT l FROM Lend l WHERE CAST(l.returnDate AS date) = CURRENT_DATE AND l.returnStatus = false") List<Lend> mFindAllByReturnDateAndReturnStatusFalse();
위 쿼리는 returnDatedate로 캐스팅하여 CURRENT_DATE와 비교하도록 변경되었습니다. H2와 같은 내장 데이터베이스는 특정 함수나 쿼리 구문에서 차이가 있을 수 있으므로, 이러한 방식으로 쿼리를 조정하면 해결될 가능성이 큽니다.
만약 CAST()도 작동하지 않으면, returnDateYEAR(), MONTH(), DAY() 함수들을 조합해서 직접 날짜를 비교하는 방법을 사용할 수도 있습니다.
 
 
ResponseDTO의 특정 필드 JSON 응답에서 제외시키기
notion image
 
→ 로그인 기능 구현 초기 코드인데, JWT를 만들어서 Controller로 전달하기 위해
UserResponse.LoginDTO에 담았다.
 
notion image
 
→ 로그인DTO는 이렇게 생겼는데 accessToken이 필드로 들어가 있다.
 
notion image
→ UserController단 에서
먼저, LoginDTO의 accessToken을 꺼내서 응답헤더에 넣어주었고
LoginDTO는 응답body에 넣어서 그대로 return 해주었다.
 
notion image
notion image
그렇게 하니 응답 헤더에도 토큰이 들어가고 응답 바디에도 토큰이 들어간다
 
→ 컨트롤러에서 JSON 응답으로 보낼 때 accessToken을 제외시키고 싶은데 어떻게 해야할까 ?
 
notion image
 
→ 이렇게 ResponseDTO에서 @JsonIgnore 어노테이션을 달아주면
해당 필드를 JSON 직렬화 또는 역직렬화 과정에서 무시하게 해준다.
 
→ 서비스에서 컨트롤러로 넘어갈 때는 값이 전달되지만
클라이언트에게 전달하기 위해 JSON으로 응답 보낼때는 제외되는 것!
 
notion image
notion image
 
→ 응답 헤더로만 토큰이 전달 된 것을 확인할 수 있다 : )
 
 
💡
@JsonIgnore 애노테이션은 Jackson 라이브러리에서 제공하는 애노테이션으로, 특정 필드나 메서드를 JSON 직렬화 또는 역직렬화 과정에서 무시하도록 설정하는 데 사용됩니다. 이를 통해 특정 데이터를 클라이언트에 노출시키지 않거나, 역직렬화 시 특정 필드를 무시할 수 있습니다.

@JsonIgnore 주요 사용 목적

  1. JSON 직렬화 과정에서 필드 제외
      • 객체를 JSON으로 변환할 때 특정 필드를 포함하고 싶지 않을 때 사용합니다. 예를 들어, 비밀번호와 같은 민감한 정보는 클라이언트에게 노출하지 않도록 설정할 수 있습니다.
  1. JSON 역직렬화 과정에서 필드 제외
      • 클라이언트로부터 JSON 데이터를 받을 때 특정 필드는 무시하고 싶을 때 사용합니다. 예를 들어, 클라이언트가 값을 변경할 수 없게 하기 위해, 역직렬화 시 특정 필드의 값은 무시할 수 있습니다.

사용 방법

1. 필드에 적용

필드에 @JsonIgnore를 붙이면 그 필드는 JSON 응답에서 제외됩니다. 직렬화 시 해당 필드는 JSON에 포함되지 않으며, 역직렬화 시에도 무시됩니다.
java 코드 복사 import com.fasterxml.jackson.annotation.JsonIgnore; public class User { private String username; @JsonIgnore private String password; // 비밀번호 필드를 제외 // getters and setters }
위의 예시에서 password 필드에 @JsonIgnore를 붙였기 때문에, 객체를 JSON으로 변환할 때 password 필드는 JSON 응답에 포함되지 않습니다.

2. 메서드에 적용

@JsonIgnore를 getter 메서드에 붙이면 해당 필드가 JSON으로 변환될 때 제외됩니다.
java 코드 복사 import com.fasterxml.jackson.annotation.JsonIgnore; public class User { private String username; private String password; @JsonIgnore public String getPassword() { // getter에 @JsonIgnore 적용 return password; } // getters and setters }

3. 역직렬화에서 무시

클라이언트로부터 데이터를 받을 때도 @JsonIgnore는 적용됩니다. 역직렬화 시에도 해당 필드는 무시되며, 데이터가 전달되더라도 해당 필드의 값은 변경되지 않습니다.
java 코드 복사 import com.fasterxml.jackson.annotation.JsonIgnore; public class User { private String username; @JsonIgnore // 이 필드는 역직렬화 시 무시됨 private String password; // getters and setters }
위의 코드에서 클라이언트가 password 값을 JSON으로 보내도, 해당 값은 무시되고 기존 객체의 값에 영향을 주지 않습니다.

@JsonIgnore 관련 추가 애노테이션

  • @JsonIgnoreProperties: 클래스 단위에서 특정 필드들을 무시할 때 사용합니다. 여러 필드를 한 번에 무시하고 싶을 때 유용합니다.
    • java 코드 복사 @JsonIgnoreProperties({"password", "email"}) public class User { private String username; private String password; private String email; // getters and setters }
      위 코드에서는 passwordemail 필드가 JSON 변환에서 무시됩니다.
  • @JsonIgnoreType: 특정 타입 전체를 무시하고 싶을 때 사용합니다. 이 타입에 속하는 모든 객체가 직렬화 과정에서 무시됩니다.
    • java 코드 복사 @JsonIgnoreType public class SensitiveData { // 이 클래스는 직렬화/역직렬화 과정에서 완전히 무시됨 }

사용 시 주의사항

  • @JsonIgnore를 사용하면 해당 필드나 메서드는 직렬화와 역직렬화 모두에서 제외됩니다. 만약 직렬화에서만 제외하고, 역직렬화에서는 사용하고 싶다면, 대신 @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) 같은 애노테이션을 고려할 수 있습니다.
    • java 코드 복사 @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private String password;
      이 경우, password는 클라이언트로부터 값을 받을 수 있지만, JSON 응답에서는 제외됩니다.

정리

@JsonIgnore는 주로 객체를 JSON으로 변환할 때 보안상의 이유로 특정 데이터를 숨기거나, 역직렬화 시 불필요한 데이터를 무시하기 위해 사용됩니다. 상황에 따라 적절하게 사용하면 데이터 노출을 방지하고, 역직렬화 시 불필요한 처리를 줄일 수 있습니다.
 
error: 대여중인 도서 반납 기능 에러 해결

LendService

// book_Tb -> 대여상태 false, 대여수 -1 // lend_tb -> 반납한 일자 (now), 반납상태 -> true @Transactional public LendResponse.ReturnDTO 직접반납하기(Long userId, LendRequest.ReturnDTO request) { // 1. 대여상태인지 확인 Boolean b = bookRepository.mCheckLendStatus(request.getIsbn13()).orElseThrow(() -> new ExceptionApi404("요청하신 도서가 존재하지 않습니다.")); if (!b) { throw new ExceptionApi404("대여중인 도서가 아닙니다."); } // 2. booktb 대여 상태 바꾸기 int updateCount = bookRepository.mUpdateLendStatusAndCountReturn(request.getIsbn13()); // 업데이트가 성공했는지 확인 (1이 아니면 실패) if (updateCount != 1) { throw new ExceptionApi500("도서 반납 처리 중 문제가 발생했습니다."); } // 3.lend_tb 대여 상태 바꾸기 int returnStatus = lendRepository.mReturnLend(userId, request.getIsbn13()); // 업데이트 성공했는지 확인 (1이 아니면 실패) if (returnStatus != 1) { throw new ExceptionApi500("도서 반납 처리 중 문제가 발생했습니다."); } // 4. 반납정보 return Lend lendPS = lendRepository.mFindLend(userId, request.getIsbn13()); return new LendResponse.ReturnDTO(lendPS); }
 
→ 처음에 구현한 “도서반납하기” 기능의 서비스 레이어 코드.
 
마지막 4. 반납정보 return 부분의 mFindLend 의 쿼리는
 

LendRepository

// 해당 user와 book의 lend 데이터 조회 @Query("select l from Lend l where l.user.id = :userId AND l.book.isbn13 = :isbn13") Lend mFindLend(@Param("userId") Long userId, @Param("isbn13") String isbn13);
 
이렇게 user_id와 isbn13(book의 pk)를 가지고 lend 데이터 조회하는 쿼리다.
 
해당 유저가 여러번 해당 도서를 빌리고 반납했다면 lend_tb에 그 히스토리가 남기때문에 ( → return_status = false, true 로 반납된 도서인지 아닌지 구분하기 때문에 lend_tb에 대여한 도서 내역과 지금 대여중인 도서 내역이 전부다 들어가 있다. )
userId와 isbn13으로 조회하면 하나의 객체가 아니라 List가 출력될텐데
그 부분을 생각하지 못했다.
 
notion image
 
→ 기존에 빌린 내역이 없는 책은 이렇게 성공적으로 반납이 되어서 잘 되는줄 알았다.
 
하지만 기존에 9791190669238 이 도서를 빌린 내역이 있으면
 
notion image
 
💡
{"reason": Query did not return a unique result: 2 results were returned}
 
이렇게 List로 받지 못해서 에러가 터진다.
 
→ 그래서 service 레이어 에서 반납정보를 찾아오는 쿼리를 수정했다.
 

LendRepository

// 가장 최근에 반납한 도서 1개를 select 하거라 @Query(value = "SELECT * FROM lend_tb WHERE user_id = :userId AND book_id = :isbn13 AND return_status = true ORDER BY return_date DESC LIMIT 1", nativeQuery = true) Optional<Lend> findLatestReturnedLendNative(@Param("userId") Long userId, @Param("isbn13") String isbn13);
 
: 대여 내역중에 가장 최근에 반납한 ( = 방금 반납한 그 반납내역 ) 것만 select 하는 쿼리 !
ORDER BY return_date DESC LIMIT 1 를 추가해서
 
return_date를 내림차순으로 정렬해서 그 중 맨 윗단에 있는 1개 ( = 방금 반납한 그 반납내역 )를
지정하도록 했다.
 

LendService

// 5. 반납정보 return Lend lendPS = lendRepository.findLatestReturnedLendNative(userId, request.getIsbn13()) .orElseThrow(() -> new ExceptionApi404("반납된 기록을 찾을 수 없습니다."));
 
방금 만든걸로 바꿔줬다.
 

LendService

// book_Tb -> 대여상태 false, 대여수 -1 // lend_tb -> 반납한 일자 (now), 반납상태 -> true @Transactional public LendResponse.ReturnDTO 직접반납하기(Long userId, LendRequest.ReturnDTO request) { // 1. 대여상태인지 확인 Boolean b = bookRepository.mCheckLendStatus(request.getIsbn13()).orElseThrow(() -> new ExceptionApi404("요청하신 도서가 존재하지 않습니다.")); if (!b) { throw new ExceptionApi404("대여중인 도서가 아닙니다."); } // 2. booktb 대여 상태 바꾸기 int updateCount = bookRepository.mUpdateLendStatusAndCountReturn(request.getIsbn13()); // 업데이트가 성공했는지 확인 (1이 아니면 실패) if (updateCount != 1) { throw new ExceptionApi500("book도서 반납 처리 중 문제가 발생했습니다."); } // 3.lend_tb 대여 상태 바꾸기 int returnStatus = lendRepository.mReturnLend(userId, request.getIsbn13()); // 업데이트 성공했는지 확인 (1이 아니면 실패) if (returnStatus != 1) { throw new ExceptionApi500("lend도서 반납 처리 중 문제가 발생했습니다."); } // 4. 반납 후 예약자가 있는지 확인하여 처리 - TODO: 신민재 boolean hasReservations = reservationRepository.countCurrentReservations(request.getIsbn13()) > 0; if (hasReservations) { // 첫 번째 예약자에게 자동 대여 처리 reservationService.자동대여(request.getIsbn13()); } // 5. 반납정보 return Lend lendPS = lendRepository.findLatestReturnedLendNative(userId, request.getIsbn13()) .orElseThrow(() -> new ExceptionApi404("반납된 기록을 찾을 수 없습니다.")); return new LendResponse.ReturnDTO(lendPS); }
 
⇒ “반납 기능” 서비스 레이어 최종 코드
 
그리고 나서 기존 9791190669238 도서의 대여 내역을 여러개 만들고
이 있는 도서를 다시 빌린 후 반납해야하는 상황에서 반납하기를 요청하면
 
notion image
→ 반납이 잘 된다!
 
Share article

SHIN