Skip to content

[그리디] 정명준 Spring Data JPA 6단계 제출합니다. - #270

Open
htdufhc-bit wants to merge 86 commits into
next-step:htdufhc-bitfrom
htdufhc-bit:roomescape-JPA
Open

[그리디] 정명준 Spring Data JPA 6단계 제출합니다.#270
htdufhc-bit wants to merge 86 commits into
next-step:htdufhc-bitfrom
htdufhc-bit:roomescape-JPA

Conversation

@htdufhc-bit

@htdufhc-bit htdufhc-bit commented Jul 20, 2026

Copy link
Copy Markdown

안녕하세요, 다빈님! 그리디 4기 정명준입니다.
이번 JPA 미션도 잘 부탁드립니다. 그리고 시간 내서 리뷰해주셔서 항상 감사합니다!!

🚗 단계별 설명

🚀 EntityManager 기반 Repository를 JpaRepository 기반으로 리팩터링

🚀 6단계 - 예약 대기 기능

  • 예약 대기 요청 기능 구현
  • 예약 대기 취소 기능 구현

💭 고민한 내용

EntityManager → JpaRepository

기존 entity manager의 구조에서 JpaRepository로 변경하면서, 최대한 기존 설계 의도가 유지되도록 노력했습니다. 이 과정에서 JpaRepository 메서드 네이밍을 공부했습니다. 복잡한 쿼리문을 제외하고는 직접 쿼리문을 작성하는 것이 아닌, 메서드 네이밍을 통해 구현하였습니다.

테스트 구현

미션에서 제공하는 테스트만 구현하다 보니 기능적으로 수정이 있더라도, 매번 실제 서버를 돌리며 테스트해야 해서 어려움이 있었습니다. 아직 Auth와 Reservaion과 관련된 api 테스트만 구현하였지만, 최대한 api 별로, 요구사항 내용 기준으로 테스트를 구현하고자 했습니다.
아래는 현재 테스트 구현 계획입니다.

  • Auth: 로그인 성공/실패, 토큰 재발급, 로그인 확인, 로그아웃
  • Reservation: 예약 생성/중복/삭제, 내 예약 조회
  • Member: 회원가입 성공/검증 실패
  • Theme: 테마 생성/조회/삭제 (soft delete 제외 조회 여부 체크)
  • Time: 시간 생성/조회/삭제, 예약 가능 시간 조회 (soft delete 제외 조회 여부 체크)
  • Waiting: 대기 생성/삭제, 순번 계산

❓ 질문사항

쿼리문 최적화

연관관계를 조회하는 과정에서 불필요한 추가 쿼리가 발생하는 것을 확인했고, 일부는 JOIN FETCH를 사용해 해결했습니다. 다만 현재 확인한 부분 외에도 불필요한 쿼리가 발생할 가능성이 있다고 생각했습니다. 그래서 저는 이번 미션에서는 api 별로 테스트를 작성하고, 테스트 실행 시 발생하는 쿼리를 확인하면서 최적화를 진행하고자 했습니다. (시간이 부족해 아직 Auth와 Reservation까지 만들었지만, 다른 api 테스트도 만들어보겠습니다!!)

서버를 직접 실행하고 각 API를 수동으로 호출하면서 쿼리를 확인하는 방식은 반복 작업에 드는 비용이 크다고 판단해, 테스트를 통해 쿼리 발생 여부를 지속적으로 확인하는 방식을 선택했습니다.
이러한 접근 방식에 대해 다빈님은 어떻게 생각하시나요? 또한 쿼리 발생 횟수나 성능을 더 효과적으로 검증할 수 있는 방법이 있다면 조언 부탁드립니다!

🌱 원하는 피드백

일정 때문에 이번 미션에 많은 시간을 쏟지 못한 것 같습니다.. 리뷰 남겨주시면 빠르게 반영하고, 제 나름대로 테스트 구현 등을 통해 코드 발전시켜보겠습니다!!

…VC-auth

# Conflicts:
#	src/main/java/roomescape/config/WebConfig.java
#	src/main/java/roomescape/member/MemberRequest.java
#	src/main/java/roomescape/member/repository/MemberDao.java
#	src/main/java/roomescape/reservation/ReservationService.java
#	src/main/java/roomescape/reservation/controller/ReservationController.java
#	src/test/java/roomescape/MissionStepTest.java

@70825 70825 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 명준님 잘부탁드립니다~ 드디어 현대 기술을 사용하는 미션까지 왔군요 ㅋㅋ

쿼리문 최적화
연관관계를 조회하는 과정에서 불필요한 추가 쿼리가 발생하는 것을 확인했고, 일부는 JOIN FETCH를 사용해 해결했습니다. 다만 현재 확인한 부분 외에도 불필요한 쿼리가 발생할 가능성이 있다고 생각했습니다. 그래서 저는 이번 미션에서는 api 별로 테스트를 작성하고, 테스트 실행 시 발생하는 쿼리를 확인하면서 최적화를 진행하고자 했습니다. (시간이 부족해 아직 Auth와 Reservation까지 만들었지만, 다른 api 테스트도 만들어보겠습니다!!)

잘하셨군요 나중에 쿼리쪽만 정확히 파악하신다면 RepositoryTest를 만드는 것도 좋아보여요

저는 우테코때 아래 종류로 테스트를 구분했었는데, 테스트 코드 만드는거 좋아하신다면 아래 방법처럼 역할별로 테스트 코드 만드는 방법도 있다는 점 참고하시면 좋아보여요

서버를 직접 실행하고 각 API를 수동으로 호출하면서 쿼리를 확인하는 방식은 반복 작업에 드는 비용이 크다고 판단해, 테스트를 통해 쿼리 발생 여부를 지속적으로 확인하는 방식을 선택했습니다.
이러한 접근 방식에 대해 다빈님은 어떻게 생각하시나요? 또한 쿼리 발생 횟수나 성능을 더 효과적으로 검증할 수 있는 방법이 있다면 조언 부탁드립니다!

넘 좋은 접근 방식이에요 👍 저도 회사에서 테스트 코드 열심히 작성하고 있어요 ㅋㅋ 😄
초반에만 테스트 코드 작성하는데 시간이 좀 들지, 나~중에 버그 생기거나, QA 생겨서 고칠 시간 생각하면 아깝지 않은 시간 투자라고 생각해요

쿼리 발생 횟수는 효과적으로 검증하는 방법은 이런거 사용해서 확인하는 방법도 있을 것 같아요
아니면 저 라이브러리 코드가 간단하기도 하고, 명준님이 보고 이해하지 못할 코드는 아니라서 QueryCountInterceptor부터 시작해서 뜯어본다음 비슷하게 만들어볼 수도 있을 것 같네요
저도 대학생때나 지금 회사에서나 N+1은 현재 명준님처럼 테스트 코드 실행할 때 or 개발하는 도중에 발견해서 대응하고 있어요

성능을 효과적으로 검증할 수 있는 방법은 저는 우테코할 때 AOP를 활용해 API 요청 - 응답이 될 때마다 로그를 남겨두고 확인했었는데요. 나중에는 모니터링 시스템을 구축해서 저 로그 데이터를 통해 요청이 들어와서 응답이 얼마나 걸렸는지 시간 체크를 효율적으로 했던 기억이 나네요

회사에서도 API 성능, 쿼리문 성능 확인하는 모니터링 시스템이 있기도 하고, 쿼리문에 대한건 매일 관련 메일이 와서 확인하는 편이에요

private Long id;

@ManyToOne
@ManyToOne(fetch = FetchType.LAZY)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지연 로딩에 대해 장점은 본문에 나와있는 것처럼 잘 학습하셨을 것 같아요
그런데 지연 로딩을 사용하게 되면 조심해야할 사항이나 문제점이 어떤게 있을까요?

@htdufhc-bit htdufhc-bit Jul 21, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

조심해야 할 사항에는 즉시 로딩과 마찬가지로 N + 1 문제를 조심해야 합니다.
지연 로딩은 단순히 연관된 엔티티의 조회 시점을 실제 사용하는 순간으로 미루는 것이지, 필요한 데이터를 한 번의 쿼리로 조회해 주는 기능은 아닙니다. 따라서 여러 엔티티를 조회한 후 각각의 연관 엔티티에 접근하면, 최초 조회 쿼리 1번에 엔티티 수만큼 추가 쿼리가 실행될 수 있습니다.
이 N + 1 문제는 JOIN FETCH나 EntityGraph를 통해 해결할 수 있습니다!


문제점에 대해서는 정확히 알지 못해 블로그를 참고했습니다.

사용자에게 보여줄 데이터를 한 번에 조회하지 않고 필요한 시점마다 추가로 조회하면, 빠르게 스크롤할 때 데이터 로딩이 지연되어 불편을 줄 수 있습니다. 특히 화면에 즉시 표시되어야 하는 정보라면 지연 로딩으로 인한 성능 저하를 사용자가 더 직접적으로 체감할 수 있다고 생각합니다.

[참고 자료] - 과한 지연 로딩의 문제점

@70825 70825 Jul 22, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 좋습니다 참고로 application.properties에 Batch Size 설정하는 것으로도 해결할 수 있어요

저는 최근에 회사에서 지연로딩 문제 때문에 에러 발생한 경험이 생각나서 그거 생각하고 질문을 드렸는데 지금 생각해보니 어려운 내용이네요
작업중 하나가 비즈니스 로직과 연관 없는거라 비동기로 작업하는 일이 있었는데요. 지연로딩된 엔티티를 비동기 메서드 파라미터로 넘겼는데, 해당 엔티티의 내부값에 있는 연관관계 엔티티를 조회하려고 하면 LazyInitializationException 에러가 나오더라구요
요거는 ThreadLocal이랑 생명주기 개념이랑 연관 있는거라 시간날 때 찾아보시면 좋아보입니다


void save(RefreshToken refreshToken);
@Repository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

시간이 좀 있으면 JpaRepository 내부 구현체가 대략적으로 어떤 클래스로 나뉘고 각각 어떤 역할을 하는지 찾아보시면 좋아보여요~

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵! 한 번 따로 공부해보겠습니다!!

Comment on lines 69 to 71
refreshTokenRepository.deleteByMemberId(member.getId());
refreshTokenRepository.flush();
refreshTokenRepository.save(new RefreshToken(member, refreshToken));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flush를 사용한 이유가 member 유니크 제약조건 때문에 사용하는 것으로 이해했습니다

궁금한 점

refreshToken은 만료되지 않았으면 그대로 재사용해도 됐을 것 같은데 다시 초기화해서 생성하는 이유가 무엇인지 궁금합니다

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Access Token은 탈취 시 피해를 최소화하기 위해 짧은 유효기간으로 설정합니다. 하지만 이 경우 사용자가 자주 재로그인해야 하는 불편함이 생기므로, 이를 보완하기 위해 유효기간이 긴 Refresh Token을 별도로 사용합니다.

Refresh Token을 이용하는 가장 큰 이유 중 하나는 탈취를 방지하기 위함으로 알고 있습니다. 따라서 Refresh Token이 만료되지 않았더라도, 탈취 시 악용 가능 기간과 노출 위험을 최소화하기 위해 로그인할 때마다 새로 발급하고 이전 토큰은 폐기하도록 설계했습니다!!

.orElseThrow();
validateDuplicateReservation(date, time, theme);

Member member = resolveMember(request, loginMember);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveName 대신 더 나은 네이밍으로 지을 수 있어보여요~

@htdufhc-bit htdufhc-bit Jul 21, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 메서드의 역할이 원하는 멤버의 이름을 받아오는 것이기 때문에 getTargetMember()로 수정해보겠습니다!

[반영 커밋] - 6eaf8f3

Comment on lines +35 to +38
public void markDeleted() {
this.deleted = true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 여기는 softDelete를 사용하게된 이유가 있을까요?
  2. 참고로 softDelete를 사용하게 된다면 boolean보다 LocalDateTime 같은걸 사용하는게 언제 삭제됐는지 기록이 되서 더 좋더라구요

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Theme 및 Time 삭제 기능에 대한 명확한 요구사항이 없어, 도메인 영향도를 고려해 다음과 같이 가정하고 설계했습니다.

    • Hard Delete 방식을 사용할 경우, Theme나 Time 삭제 시 기존 예약 데이터까지 함께 삭제되거나, 외래 키(FK) 제약조건으로 인해 삭제 자체가 불가능해집니다. 관리자 입장에서 "이후 추가 예약만 막고, 기존 예약 내역은 유지"하고 싶을 때 Hard Delete는 적합하지 않다고 판단했습니다.
    • 따라서 기존 예약을 안전하게 보존하면서 특정 시간과 테마의 신규 예약만 제한할 수 있도록 Soft Delete 방식을 적용했습니다.
    • 회원 탈퇴의 경우, Soft Delete를 적용하면 탈퇴한 회원의 예약 정보만 남아 "방문할 사용자는 없는데 예약만 존재하는" 상태가 발생하는 문제가 있습니다. 따라서 서비스 데이터의 정합성을 위해 탈퇴 시 관련 데이터를 완벽히 제거하는 Hard Delete 방식으로 구현했습니다. (Hard Delete를 위해 Cascade로 삭제되도록 수정했습니다!!)
  2. 남겨주신 리뷰대로 LocalDateTime을 활용하고, null이냐 아니냐로 삭제 여부를 판단하게 되면 soft delete도 구현하고 언제 삭제되었는지에 대한 기록도 남아 더 좋을 것 같습니다. 수정하겠습니다!!

@htdufhc-bit htdufhc-bit Jul 21, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Time, Theme처럼 Soft Delete가 필요한 엔티티마다 deletedAt 관련 필드와 로직이 중복되고 있었습니다. 이를 공통 상위 클래스로 추출해, Soft Delete가 필요한 엔티티는 상속만 받으면 되도록 했습니다.

[반영 커밋] - f2fd69f

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theme 및 Time 삭제 기능에 대한 명확한 요구사항이 없어, 도메인 영향도를 고려해 다음과 같이 가정하고 설계했습니다.
Hard Delete 방식을 사용할 경우, Theme나 Time 삭제 시 기존 예약 데이터까지 함께 삭제되거나, 외래 키(FK) 제약조건으로 인해 삭제 자체가 불가능해집니다. 관리자 입장에서 "이후 추가 예약만 막고, 기존 예약 내역은 유지"하고 싶을 때 Hard Delete는 적합하지 않다고 판단했습니다.
따라서 기존 예약을 안전하게 보존하면서 특정 시간과 테마의 신규 예약만 제한할 수 있도록 Soft Delete 방식을 적용했습니다.
회원 탈퇴의 경우, Soft Delete를 적용하면 탈퇴한 회원의 예약 정보만 남아 "방문할 사용자는 없는데 예약만 존재하는" 상태가 발생하는 문제가 있습니다. 따라서 서비스 데이터의 정합성을 위해 탈퇴 시 관련 데이터를 완벽히 제거하는 Hard Delete 방식으로 구현했습니다. (Hard Delete를 위해 Cascade로 삭제되도록 수정했습니다!!)

Hard Delete, Soft Delete 사용한 근거까지 좋네요 👍 이런 내용들이 코딩하기 전에 설계할 때 중요하더라구요

남겨주신 리뷰대로 LocalDateTime을 활용하고, null이냐 아니냐로 삭제 여부를 판단하게 되면 soft delete도 구현하고 언제 삭제되었는지에 대한 기록도 남아 더 좋을 것 같습니다. 수정하겠습니다!!

이거는 본인이 과거에 데이터 삭제한걸 깜빡하고 문의주시는 사용자가 생각보다 있어서 유용했어요 ㅋㅋ..

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

public class WaitingRequest {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DTO쪽은 모두 record로 변경되면 좋아보여요~

@htdufhc-bit htdufhc-bit Jul 21, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일관성 있게 모든 dto를 record로 변경하겠습니다!!

[반영 커밋] - 795d0ad / d1da150

Comment on lines +45 to +51
.map(wr -> new MyReservationResponse(
wr.getWaiting().getId(),
wr.getWaiting().getTheme().getName(),
wr.getWaiting().getDate().toString(),
wr.getWaiting().getTime().getTimeValue(),
(wr.getRank() + 1) + "번째 예약대기"
))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 곳들은 자바 미션때 활용했던 정팩메로 내부로 넣어주면 핵심 Service 로직만 보여서 더 유지보수하기 수월할 것 같아요~

@htdufhc-bit htdufhc-bit Jul 21, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

원래 정적 팩토리 메서드를 활용하는 이유를 잘 몰랐는데, 남겨주신 리뷰대로 좀 더 코드를 깔끔하게 변경해 service 로직만 확실하게 보일 수 있는 것 같습니다!!
일관성있게 정적 팩토리 메서드를 적용할 수 있는 모든 entity와 dto에 적용했습니다!

[반영 커밋] - d1da150

import static org.hamcrest.Matchers.is;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나중에 시간되면 팀프로젝트 진행할 때 테스트 코드도 많이 만들 수도 있는데요. 많이 만들어 뒀을 때 이때도 DirtiesContext로 실행하게되면 테스트 코드 전체 돌릴 때마다 5분 넘게 걸릴 수도 있으니 컨텍스트 캐싱도 찾아보시면 좋아보여요~

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그동안 테스트를 하면서 노트북이 안 좋아서 좀 더 오래 걸리는 거라고 생각했는데 아니었군요...

컨택스트 캐싱 찾아서 적용해봤습니다!! 얼마 안되지만 테스트 코드 돌리는 시간이 확실히 줄었습니다!

[반영 커밋] - 69db99b

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헉 이거는 바로 구현하시라고 남긴 코멘트가 아니였는데 넘 고생하셨어요
컨텍스트 캐싱이 어떻게 동작하는지 궁금하시다면 스프링 문서 요거 참조하시면 됩니다~

@htdufhc-bit htdufhc-bit left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

남겨주신 리뷰를 전부 반영하지는 못했지만, 가능한 범위 내에서 최대한 반영했습니다.
제가 알기로는 금요일까지 리뷰를 주고받는 것으로 알고 있는데요. 진행 방향이 적절한지 중간에 다빈님의 피드백을 받아보고 싶어 코멘트 남깁니다!!

개인적으로는 이번 미션에 복잡한 서비스 로직이 많지 않다고 판단해, 남은 시간 동안에는 리포지토리 테스트에 더해 API 테스트를 보완하는 방향으로 진행하려고 합니다. 이 방향에 대해서도 의견 주시면 감사하겠습니다!


쿼리 발생 횟수는 효과적으로 검증하는 방법은 이런거 사용해서 확인하는 방법도 있을 것 같아요
아니면 저 라이브러리 코드가 간단하기도 하고, 명준님이 보고 이해하지 못할 코드는 아니라서 QueryCountInterceptor부터 시작해서 뜯어본다음 비슷하게 만들어볼 수도 있을 것 같네요

Image

테스트 코드에 적용시키려고 하다 보니 어려움이 있어 AI에 도움을 조금 받았습니다.
사진처럼 쿼리를 보이고, 불필요한 쿼리가 발생하면 에러를 찍도록 만들었습니다!!

[반영 커밋] - 5b1a969


Repository Test도 많이 작성해본 경험이 없어, 올려주신 링크를 참고해 작성해봤습니다!
Repository에서 사용되는 모든 메서드를 테스트하기보다는 soft delete, fetch join처럼 별도의 검증이 필요한 경우나 직접 작성한 쿼리 메서드를 중심으로 테스트해도 충분하다고 판단했습니다.

[반영 커밋] - 72349b2

Comment on lines 69 to 71
refreshTokenRepository.deleteByMemberId(member.getId());
refreshTokenRepository.flush();
refreshTokenRepository.save(new RefreshToken(member, refreshToken));

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Access Token은 탈취 시 피해를 최소화하기 위해 짧은 유효기간으로 설정합니다. 하지만 이 경우 사용자가 자주 재로그인해야 하는 불편함이 생기므로, 이를 보완하기 위해 유효기간이 긴 Refresh Token을 별도로 사용합니다.

Refresh Token을 이용하는 가장 큰 이유 중 하나는 탈취를 방지하기 위함으로 알고 있습니다. 따라서 Refresh Token이 만료되지 않았더라도, 탈취 시 악용 가능 기간과 노출 위험을 최소화하기 위해 로그인할 때마다 새로 발급하고 이전 토큰은 폐기하도록 설계했습니다!!

Comment on lines +35 to +38
public void markDeleted() {
this.deleted = true;
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Theme 및 Time 삭제 기능에 대한 명확한 요구사항이 없어, 도메인 영향도를 고려해 다음과 같이 가정하고 설계했습니다.

    • Hard Delete 방식을 사용할 경우, Theme나 Time 삭제 시 기존 예약 데이터까지 함께 삭제되거나, 외래 키(FK) 제약조건으로 인해 삭제 자체가 불가능해집니다. 관리자 입장에서 "이후 추가 예약만 막고, 기존 예약 내역은 유지"하고 싶을 때 Hard Delete는 적합하지 않다고 판단했습니다.
    • 따라서 기존 예약을 안전하게 보존하면서 특정 시간과 테마의 신규 예약만 제한할 수 있도록 Soft Delete 방식을 적용했습니다.
    • 회원 탈퇴의 경우, Soft Delete를 적용하면 탈퇴한 회원의 예약 정보만 남아 "방문할 사용자는 없는데 예약만 존재하는" 상태가 발생하는 문제가 있습니다. 따라서 서비스 데이터의 정합성을 위해 탈퇴 시 관련 데이터를 완벽히 제거하는 Hard Delete 방식으로 구현했습니다. (Hard Delete를 위해 Cascade로 삭제되도록 수정했습니다!!)
  2. 남겨주신 리뷰대로 LocalDateTime을 활용하고, null이냐 아니냐로 삭제 여부를 판단하게 되면 soft delete도 구현하고 언제 삭제되었는지에 대한 기록도 남아 더 좋을 것 같습니다. 수정하겠습니다!!

Comment on lines +35 to +38
public void markDeleted() {
this.deleted = true;
}

@htdufhc-bit htdufhc-bit Jul 21, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Time, Theme처럼 Soft Delete가 필요한 엔티티마다 deletedAt 관련 필드와 로직이 중복되고 있었습니다. 이를 공통 상위 클래스로 추출해, Soft Delete가 필요한 엔티티는 상속만 받으면 되도록 했습니다.

[반영 커밋] - f2fd69f

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

public class WaitingRequest {

@htdufhc-bit htdufhc-bit Jul 21, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일관성 있게 모든 dto를 record로 변경하겠습니다!!

[반영 커밋] - 795d0ad / d1da150

.orElseThrow();
validateDuplicateReservation(date, time, theme);

Member member = resolveMember(request, loginMember);

@htdufhc-bit htdufhc-bit Jul 21, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 메서드의 역할이 원하는 멤버의 이름을 받아오는 것이기 때문에 getTargetMember()로 수정해보겠습니다!

[반영 커밋] - 6eaf8f3

Comment on lines +45 to +51
.map(wr -> new MyReservationResponse(
wr.getWaiting().getId(),
wr.getWaiting().getTheme().getName(),
wr.getWaiting().getDate().toString(),
wr.getWaiting().getTime().getTimeValue(),
(wr.getRank() + 1) + "번째 예약대기"
))

@htdufhc-bit htdufhc-bit Jul 21, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

원래 정적 팩토리 메서드를 활용하는 이유를 잘 몰랐는데, 남겨주신 리뷰대로 좀 더 코드를 깔끔하게 변경해 service 로직만 확실하게 보일 수 있는 것 같습니다!!
일관성있게 정적 팩토리 메서드를 적용할 수 있는 모든 entity와 dto에 적용했습니다!

[반영 커밋] - d1da150

private Long id;

@ManyToOne
@ManyToOne(fetch = FetchType.LAZY)

@htdufhc-bit htdufhc-bit Jul 21, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

조심해야 할 사항에는 즉시 로딩과 마찬가지로 N + 1 문제를 조심해야 합니다.
지연 로딩은 단순히 연관된 엔티티의 조회 시점을 실제 사용하는 순간으로 미루는 것이지, 필요한 데이터를 한 번의 쿼리로 조회해 주는 기능은 아닙니다. 따라서 여러 엔티티를 조회한 후 각각의 연관 엔티티에 접근하면, 최초 조회 쿼리 1번에 엔티티 수만큼 추가 쿼리가 실행될 수 있습니다.
이 N + 1 문제는 JOIN FETCH나 EntityGraph를 통해 해결할 수 있습니다!


문제점에 대해서는 정확히 알지 못해 블로그를 참고했습니다.

사용자에게 보여줄 데이터를 한 번에 조회하지 않고 필요한 시점마다 추가로 조회하면, 빠르게 스크롤할 때 데이터 로딩이 지연되어 불편을 줄 수 있습니다. 특히 화면에 즉시 표시되어야 하는 정보라면 지연 로딩으로 인한 성능 저하를 사용자가 더 직접적으로 체감할 수 있다고 생각합니다.

[참고 자료] - 과한 지연 로딩의 문제점

import static org.hamcrest.Matchers.is;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그동안 테스트를 하면서 노트북이 안 좋아서 좀 더 오래 걸리는 거라고 생각했는데 아니었군요...

컨택스트 캐싱 찾아서 적용해봤습니다!! 얼마 안되지만 테스트 코드 돌리는 시간이 확실히 줄었습니다!

[반영 커밋] - 69db99b


void save(RefreshToken refreshToken);
@Repository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵! 한 번 따로 공부해보겠습니다!!

@70825 70825 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

남겨주신 리뷰를 전부 반영하지는 못했지만, 가능한 범위 내에서 최대한 반영했습니다.
제가 알기로는 금요일까지 리뷰를 주고받는 것으로 알고 있는데요. 진행 방향이 적절한지 중간에 다빈님의 피드백을 받아보고 싶어 코멘트 남깁니다!!

안녕하세요 명준님! 저는 주말에도 리뷰 주고 받아도 됩니다. 대신 답변 시간이 좀 느릴 수도 있어요
그리고 시간 날 때 찾아보라는 코멘트는 미션 이후에라도 나중에 생각나면 찾아보면 좋다는 코멘트라 꼭 미션 기간에 다 찾아보지 않으셔도 됩니다~
쿼리 카운터랑 Repository test 요거는 당장 구현하실 거라는 생각은 전혀 못하고 있었는데 감동이네요

개인적으로는 이번 미션에 복잡한 서비스 로직이 많지 않다고 판단해, 남은 시간 동안에는 리포지토리 테스트에 더해 API 테스트를 보완하는 방향으로 진행하려고 합니다. 이 방향에 대해서도 의견 주시면 감사하겠습니다!

넵넵 저도 이미 미션 요구사항은 만족되었다고 생각이 들어서 명준님 더 공부하고 싶은 방향으로 진행하시면 될 것 같아요
테스트 코드 관심 있으시면 저번에 링크 올린 코드의 구현 의도가 아래 링크에 적혀있어서 읽어보셔도 좋을 것 같습니다

Comment on lines +9 to +11
public static LoginCheckResponse from(LoginMember loginMember) {
return new LoginCheckResponse(loginMember.name());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍👍 팀마다 다르긴한데 제 팀은 DTO에 정팩메 쓰는게 암묵적 컨벤션이라 정팩메 있으니 편안하네요

private LocalDate date;

@ManyToOne(fetch = FetchType.LAZY)
@OnDelete(action = OnDeleteAction.CASCADE)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

public void markDeleted() {
this.deleted = true;
public static Time from(TimeRequest request) {
return new Time(request.value());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

레이어드 아키텍처 관점으로 보면 request라는 DTO가 도메인을 침범하게 되는데요
도메인 계층은 가장 바닥에 있는 계층인데, 외부로 노출되어 있는 프레젠테이션 영역의 DTO를 알게 되니 의존성 방향이 뒤집어지게 됩니다 (보통 import문에 나와있으면 의존하게 된다고 말함)
그래서 도메인에 정적 팩토리 메서드는 String timeValue를 파라미터로 받도록 하거나, 값 변경하는게 있다면 Time 클래스를 파라미터로 받아서 수정하는 것도 좋아요

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀하신 대로 도메인 엔티티(Member, Theme, Time)가 프레젠테이션 계층의 Request DTO를 import해서 의존성 방향이 역전되는 문제가 있었네요. 제안해주신 대로 각 정적 팩토리 메서드가 DTO 대신 파라미터 값을 직접 받도록 수정했습니다!

[반영 커밋] - 14acc26

Comment on lines +8 to +9
@TestConfiguration
public class QueryCounterTestConfig {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Comment on lines +30 to +33
@BeforeEach
void setUp() {
databaseCleaner.clear();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 방식으로 설정할 수도 있고, beforeEach 코드가 중복 코드라 불편하다라고 생각하시면 JUnit에 BeforeEachCallback이라는 기능을 활용할 수도 있긴해요

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀하신 대로 BeforeEachCallback을 구현한 DatabaseCleanerExtension을 만들고, ApiTest에 @ExtendWith(DatabaseCleanerExtension.class)로 등록해서 각 테스트 클래스는 ApiTest만 상속받으면 되도록 수정했습니다!

[반영 커밋] - dfba04f

@htdufhc-bit htdufhc-bit left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리뷰 감사합니다!!
간단하게 몇 개 api 테스트 추가했습니다. 시간이 되는대로 더 많은 테스트를 작성해보고 싶었지만, 생각보다 테스트 작성이 더 많은 시간과 노력이 필요하다는 걸 느꼈습니다...
그래도 남겨주신 리뷰 덕분에 어떻게 테스트 코드를 작성하면 좋을지 감을 잡은 것 같아 이후에도 테스트 코드에 적용해보겠습니다!!

public void markDeleted() {
this.deleted = true;
public static Time from(TimeRequest request) {
return new Time(request.value());

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀하신 대로 도메인 엔티티(Member, Theme, Time)가 프레젠테이션 계층의 Request DTO를 import해서 의존성 방향이 역전되는 문제가 있었네요. 제안해주신 대로 각 정적 팩토리 메서드가 DTO 대신 파라미터 값을 직접 받도록 수정했습니다!

[반영 커밋] - 14acc26

Comment on lines +30 to +33
@BeforeEach
void setUp() {
databaseCleaner.clear();
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀하신 대로 BeforeEachCallback을 구현한 DatabaseCleanerExtension을 만들고, ApiTest에 @ExtendWith(DatabaseCleanerExtension.class)로 등록해서 각 테스트 클래스는 ApiTest만 상속받으면 되도록 수정했습니다!

[반영 커밋] - dfba04f

@70825 70825 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 명준님! 테스트 코드 잘 확인했어요
처음에만 힘들지 이번 기회에 잘 흡수하셨으면 이미 만들어봤기 때문에 다음에는 더 빠르고 잘 만들 수 있을거에요
다음 미션에서도, 팀프로젝트도 모두 화이팅입니다~ 고생하셨어요 🥳

Comment on lines +24 to +25
@ExtendWith(DatabaseCleanerExtension.class)
@Import(QueryCounterTestConfig.class)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants