Skip to content

Conversation

@mintcoke123
Copy link

@mintcoke123 mintcoke123 commented Jan 1, 2026

안녕하세요 신혜빈 리뷰어님!
새해 복 많이 받으세요!
이번에도 좋은 티키타카가 오갔으면 좋겠습니다!


변경한 파일


auth/
- AdminAuthInterceptor            : 관리자 권한(JWT role) 검증
- LoginMemberArgumentResolver     : 쿠키 토큰 → LoginMember 주입

member/
- MemberController                : 로그인/로그아웃/상태 조회
- MemberService                   : 로그인 처리
- MemberRepository                : JPA 리포지토리
- LoginMember                     : 로그인 사용자 DTO

reservation/
- Reservation                     : 엔티티 (member 세터 추가로 리플렉션 제거)
- ReservationRepository           : 파생쿼리 + existsForMemberOnSlot 래퍼
- ReservationService              : 예약 생성/삭제
- MyReservationService            : “내 예약/대기” 조립 전담
- ReservationController           : 생성/조회/삭제 + /reservations-mine 위임
- MyReservationResponse           : JSON 계약 유지 (reservationId 노출, getId는 @JsonIgnore)

waiting/
- Waiting                         : 대기 엔티티 (생성 순서 기준)
- WaitingRepository               : 존재/카운트 파생쿼리
- WaitingService                  : 중복 검증/생성/취소 + 서비스 계층 순번 계산
- WaitingController               : 생성/취소 API
- WaitingWithRank                 : 대기 + 순번 DTO
- WaitingResponse                 : 기본 생성자/세터 추가 (역직렬화 호환)

root/
- WebConfig                       : Interceptor, ArgumentResolver 등록

MyReservationResponse

초기 구현에서는 주어진 테스트 코드에 코드를 맞추기 위해 getId() 메서드를 두었는데,
Jackson 직렬화 과정에서 해당 getter가 자동으로 감지되어 의도하지 않게 id 필드가 JSON 응답에 함께 포함되는 문제가 발생했습니다.
따라서 자바 객체 내부에서는 getId()를 유지하면서JSON 응답에서는 id를 노출하지 않도록 @JsonIgnore를 적용해 직렬화 대상에서만 제외했습니다.


이번 미션에서는 구현 양이 방대해, 중간부터는 흐름을 놓치고 당장의 기능구현에 급급했던 것 같습니다..!
리뷰해주시면서 어색하거나 구조나 흐름 측면에서 보완이 필요해 보이는 부분이 있다면 의견 주시면 충분히 고민한 뒤 반영하겠습니다!!
감사합니다!

@mintcoke123 mintcoke123 changed the title Step 4to6 [그리디] 강동현 Spring JPA (1차) 4, 5, 6 단계 미션 제출합니다. Jan 1, 2026
@c0mpuTurtle
Copy link

c0mpuTurtle commented Jan 4, 2026

안녕하세요, 동현님🙂
만나뵙게 되어 너무너무 영광입니다!

많이 고민하시고 코드를 작성하신 게 눈에 잘 보여서 인상 깊게 봤습니다 :)
너무너무 수고하셨어요!

잘 해주셨지만 코드 잘 읽어보면서 몇 가지 사소한 개선 포인트가 보여서
조심스럽게 피드백 남겨봅니다.

<피드백>

1. 사용하지 않는 메서드 및 import문 존재

현재 사용되지 않는 메서드나 import 문은 정리해주시면
코드 가독성이 더 좋아질 것 같아요.

2. 반복되는 getter 메서드

반복되는 getter 메서드는 Lombok의 @Getter를 활용해보셔도 좋을 것 같습니다.

3. Dto 관리 포인트

DTO를 사용하신다면, 관련 DTO들을 한 위치에 모아두거나
역할이 명확하게 드러나도록 구조를 정리해보는 것도 좋을 것 같아요.

4. record 사용 권장

단순한 응답/전달용 DTO의 경우, record를 활용해보면
보일러플레이트를 줄이고 의도를 더 명확히 드러낼 수 있을 것 같습니다.

작은 제안들이라 꼭 반영하지 않으셔도 되고,
참고만 해주셔도 충분할 것 같아요 🙂 감사합니다!

<질문>

  1. AvailableTime과 Time 이렇게 time을 2가지로 분리해둔 이유가 따로 있을까요?


public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmailAndPassword(String email, String password);
Optional<Member> findByName(String name);

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 +54
public Long getReservationId() {
return reservationId;
}

public String getTheme() {
return theme;
}

public String getDate() {
return date;
}

public String getTime() {
return time;
}

public String getStatus() {
return status;
}

@JsonIgnore
public Long getId() {
return reservationId;
}
}

Choose a reason for hiding this comment

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

혹시 Lombok의 @Getter 어노테이션을 사용해보셨나요?
이를 활용하면 각 필드에 대한 get 메서드를 일일이 작성하지 않아도 되어 코드가 훨씬 간결해집니다.

지금처럼 단순 값 반환인 경우, 어노테이션 사용을 추천드립니다 :)

Comment on lines +18 to +39
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = JwtUtil.extractTokenFromCookies(request.getCookies());
if (token.isEmpty()) {
response.setStatus(401);
return false;
}

try {
Claims claims = JwtUtil.parseClaims(token, secretKey);
String role = claims.get("role", String.class);
if (!"ADMIN".equals(role)) {
response.setStatus(401);
return false;
}
return true;
} catch (Exception e) {
response.setStatus(401);
return false;
}
}
}
Copy link

@c0mpuTurtle c0mpuTurtle Jan 4, 2026

Choose a reason for hiding this comment

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

오류를 잘 걸러주셨어요 :)
다만 살짝 아쉬운 점은 문제상황에 같은 오류코드를 반환하고 있네요.

현재 토큰이 없는 경우와 권한이 없는 경우의 모든 실패 케이스에서 401을 반환하고 있는 데
이럴 때는 오류가 생겼을 때 오류의 원인이 모호할 수 있어요.

상황에 따라 상태코드를 분리해보는 건 어떨까요?

Choose a reason for hiding this comment

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

또 PageController를 보아하니 동현님께서 응집화에 많이 신경쓰고 계신 거 같아 하나 더 알려 드리자면,
개인적으로는 에러 또한 메시지를 한 곳에서 함께 관리하는 편이 유지보수에도 더 좋고, 확장할 때도 안정적이라고 느껴졌어요.

그래서 error 관련 내용을 enum으로 통일해서 관리해보는 건 어떨까 조심스럽게 제안드립니다 😊

ex)

@Getter
@AllArgsConstructor
public enum FailMessage {

    //400
    BAD_REQUEST(HttpStatus.BAD_REQUEST, 40000, "잘못된 요청입니다."),
    BAD_REQUEST_REQUEST_BODY_VALID(HttpStatus.BAD_REQUEST, 40001, "잘못된 요청본문입니다."),
    BAD_REQUEST_MISSING_PARAM(HttpStatus.BAD_REQUEST, 40002, "필수 파라미터가 없습니다."),
    BAD_REQUEST_METHOD_ARGUMENT_TYPE(HttpStatus.BAD_REQUEST, 40003, "메서드 인자타입이 잘못되었습니다."),
    BAD_REQUEST_NOT_READABLE(HttpStatus.BAD_REQUEST, 40004, "Json 오류 혹은 요청본문 필드 오류 입니다. ");
private final HttpStatus httpStatus;
    private final int code;
    private final String message;
}

package roomescape.auth;

import io.jsonwebtoken.Claims;
import jakarta.servlet.http.Cookie;

Choose a reason for hiding this comment

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

안 쓰는 import문도 지워주는 것이 좋아요!

Comment on lines +3 to +7
public class LoginMember {
private Long id;
private String name;
private String email;
private String role;
Copy link

@c0mpuTurtle c0mpuTurtle Jan 4, 2026

Choose a reason for hiding this comment

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

처음에는 이 객체가 어떤 역할을 하는 지 바로 와닿지 않았는 데, 쓰임을 보아하니 DTO 성격의 객체인 거 같습니다!

보통 DTO의 경우 LoginRequestDto처럼 클래스명에 Dto를 명시해 주는 경우가 많은데,
이렇게 하면 해당 객체가 엔티티인지, 전달용 객체인지를 빠르게 구분하기 쉽기때문이죠.

지금은 외부 api를 사용하는 프로젝트가 아니라서 필수는 아니지만,
그래도 이런 관례가 있다는 걸 알아두면 좋을 거 같아 남겨둡니다!

Comment on lines +62 to +88
public String createToken(Member member) {
return Jwts.builder()
.setSubject(member.getId().toString())
.claim("name", member.getName())
.claim("role", member.getRole())
.signWith(Keys.hmacShaKeyFor(secretKey.getBytes()))
.compact();
}


public String createTokenFromEmailAndPassword(String email, String password) {
Member member = memberService.login(email, password);
return createToken(member);
}


private String extractTokenFromCookie(Cookie[] cookies) {
if (cookies == null || cookies.length == 0) {
return "";
}
for (Cookie cookie : cookies) {
if ("token".equals(cookie.getName())) {
return cookie.getValue();
}
}
return "";
}
Copy link

@c0mpuTurtle c0mpuTurtle Jan 4, 2026

Choose a reason for hiding this comment

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

(1) 동현님은 컨트롤러의 역할/책임을 어디까지로 보고 계신가요?

(2) 지금 컨트롤러에 토큰 생성/파싱/쿠키 추출 로직이 같이 들어가 있는데, 혹시 JwtUtil로 분리하지 않고 컨트롤러에 둔 이유가 있을까요?

Copy link

@c0mpuTurtle c0mpuTurtle Jan 4, 2026

Choose a reason for hiding this comment

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

(3) 토큰 생성에 토큰 만료시간을 따로 설정 하지 않은 거 같은데요.
이 경우 토큰이 사실상 무기한 유효해질 수 있습니다. 이는 운영 관점에서 어떤 문제가 생길 수 있을까요?

Comment on lines +5 to +10
public class MyReservationResponse {
private Long reservationId;
private String theme;
private String date;
private String time;
private String status;
Copy link

@c0mpuTurtle c0mpuTurtle Jan 4, 2026

Choose a reason for hiding this comment

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

record를 사용해보신 적이 있을까요?
DTO의 경우 많은 분들이 record를 사용하곤 합니다.
getter와 생성자 같은 보일러플레이트 코드를 줄일 수 있고, 불변 데이터라는 의도가 코드로 명확히 드러난다는 장점이 있기 때문인데요.

물론 현재처럼 클래스로 사용해도 전혀 문제는 없지만,
한 번 직접 사용해보면 어떤 점이 더 편한지 체감하기 쉬워서
시간이 되신다면 record로 한 번 바꿔보는 것도 권장드려요 🙂

@Service
public class MemberService {
private MemberDao memberDao;
private MemberRepository memberRepository;

Choose a reason for hiding this comment

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

Service 계층은 여러 요청이 동시에 접근할 수 있어서 가변 상태를 줄이는 것도 중요해요.
생성자 주입을 사용하고 있으니, 의존성 필드에 final을 붙여 불변으로 두는 건 어떨까요?

Comment on lines +32 to +42
public ReservationResponse save(ReservationRequest reservationRequest, Long loginMemberId) {
Time time = timeRepository.findById(reservationRequest.getTime()).orElseThrow();
Theme theme = themeRepository.findById(reservationRequest.getTheme()).orElseThrow();
Reservation reservation = new Reservation(reservationRequest.getName(), reservationRequest.getDate(), time, theme);
if (loginMemberId != null) {
Member memberRef = memberRepository.getReferenceById(loginMemberId);
reservation.setMember(memberRef);
}
reservation = reservationRepository.save(reservation);

return new ReservationResponse(reservation.getId(), reservation.getName(), reservation.getTheme().getName(), reservation.getDate(), reservation.getTime().getValue());

Choose a reason for hiding this comment

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

여러 레포지토리를 사용하는 서비스 메서드라 @transactional로 경계를 잡아주면 더 안전할 것 같아요.!

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