[그리디] 김민욱 Spring Data JPA 4, 5단계 제출합니다.#263
Conversation
sangjun121
left a comment
There was a problem hiding this comment.
안녕하세요 민욱님!ㅎ 이번주 미션도 고생하셨습니다~~ PR 본문 읽어보니 JPA 학습부터 꼼꼼히 미션 진행해주신 것 같아서 리뷰 남겨드리기 좋았습니다! 감사해요
PR 본문에는 별도의 질문 사항이 없어, 코드에만 남겨두었습니다. 리뷰 반영 중에 질문사항 추가 된다면 편하게 남겨주세요!
| } catch (RuntimeException e) { | ||
| throw new AuthenticationException(); | ||
| } |
There was a problem hiding this comment.
RuntimeException을 모두 AuthenticationException으로 변환하면 인증 실패와 서버 내부 오류를 구분하기 어려워질 것 같습니다.
인증 실패에 해당하는 예외만 변환하고, 다른 예외는 그대로 전파하는 편이 장애 원인을 파악하기 쉬울 것 같아요!
There was a problem hiding this comment.
RumtimException 으로 인증을 실패를 했을 경우도, 결과적으로는 인증에 실패한 것이기 때문에AuthenticationException 를 반환을 해야한다고 생각했었습니다!
하지만 서버의 오류, 잘못된 enum 반환, 예상하지 못한 데이터 형태를 처리해야할 경우 모두 401로 처리하는 것은 잘못되었다고 생각합니다 !
애초에 에러를 반환을 해야하는 이유가, 개발자가 원인을 파악하여 해결하기 위함인데 서비스 장애를 가지고 AuthenticationException 에러를 발생시키는 것은 본질적인 원인을 알려주지 못한 것이 때문에 해당 부분을 수정해야 한다고 생각합니다 !
올바른 에러 "원인" 을 반환할 수 있도록(서비스 vs 로그인) 리팩토링 해보겠습니다!
There was a problem hiding this comment.
추가로 궁금한 사항이 있는데 쿠키가 없거나 토큰이 없을 경우에는 로그인이 실패 RuntimeException 인지 AuthenticationException 인지 어떻게 정할 수 있을까요 ? 현실세계의 상황을 고려해서 개발자들끼리 정하면 될까요?
쿠키가 없는 경우에 서비스가 진행되지 않아서 RuntimeException 일 수 있고 인증이 되지 않았기 때문에 AuthenticationException 일 수 있기 때문에 궁금증이 생겨서 여쭤봅니다 !
상준 리뷰어님은 어떤 기준으로 판단하시나요 !?
| } catch (RuntimeException e) { | ||
| return handleAuthenticationFailure(optional); | ||
| } |
There was a problem hiding this comment.
이 부분도 마찬가지로 모든 RuntimeException를 감싸버리면 장애 원인을 찾기 어려울 것 같아요
| return jwtParser() | ||
| .parseClaimsJws(token) | ||
| .getBody(); |
There was a problem hiding this comment.
parseClaimsJws()가 isValidSignature()와 parseClaims()에서 각각 호출되어 동일한 토큰을 두 번 파싱하고 있는 것 같습니다.
There was a problem hiding this comment.
isValidSignature 라는 메서드를 통해서 JWT를 검증 의도를 분리하는 메서드를 만들고 싶었습니다 !
그 과정에서 2번을 파싱하게 되는 것이 괜찮다고 생각했습니다. 하지만 WT 구조 파싱, 서명 검증, 만료 시간 검증 등을 수행등을 똑같이 더 해야하며 모든 인증 요청에 대해서 비용이 2개가 된 다는 점을 리뷰 답변을 준비하는 과정을 통해 배울 수 었습니다.
뿐만 아니라 이미 구현되어 있는 parseClaimsJws 자체에서 구체적인 JWT 예외 처리를 하고 있기 때문에 isValidSignature 메서드를 만들어서 검증할 필요 없이 parseClaims 내부에서 파싱과 검증을 2가지를 동시에 처리할 수 있다고 생각합니다.
질문
그렇다면, parseClaims 라는 메서드는 파싱과 검증(이미 스프링에서 구현되어 있는 parseClaimsJws 통해서) 2가지 기능을 하게 되는데 이것은 괜찮을까요? 호출자에 입장에서 "토큰을 얻거나 실패한다." 라는 관점에서 본다면 해당 구현은 합당하다고 생각하는데 리뷰어 님은 어떻게 생각하시나요 ?
|
|
||
| @Component | ||
| public class JwtTokenProvider { | ||
| private static final String SECRET_KEY = "roomescape-secret-key-for-jwt-token"; |
There was a problem hiding this comment.
JWT Secret이 코드에 하드코딩되어 있는데, 운영 환경에서는 외부 설정으로 관리해야 합니다. @value 등을 이용해 주입받는 방식을 고려해주세요!
| .setParameter("password", password) | ||
| .getSingleResult(); | ||
| } catch (NoResultException e) { | ||
| throw new EmptyResultDataAccessException(1); |
There was a problem hiding this comment.
만약에 예외 처리를 한다면, JPA를 사용하고 있는데 Spring JDBC 계열 예외인 EmptyResultDataAccessException으로 변환하기 보다는 커스텀 예외나 JPA 예외를 그대로 사용하는 것 이 좋을 것 같습니다.
There was a problem hiding this comment.
네 !
이건 ![해당리뷰] 에 관련된 내용이과 비슷한 결이군요! 이미 검증되어 잇는 api 의 예외를 처리하는 방식을 사용해보도록 하겠습니다 !
JPA 예외
- .getSingleResult(); 사용
커스텀 예외
public class MemberNotFoundException extends
RuntimeException {
public MemberNotFoundException() {
super("회원을 찾을 수 없습니다.");
}
}DAO에서:
catch (NoResultException e) {
throw new MemberNotFoundException();
}| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
| private String name; | ||
| private String email; |
There was a problem hiding this comment.
이메일 조회에서 getSingleResult()를 사용하고 있어 이메일이 유일하다는 전제가 있는 것 같습니다.
애플리케이션의 중복 검사만으로는 동시 요청 시 중복 저장을 완전히 막기 어려운데요, DB나 엔티티 매핑에서도 이메일의 UNIQUE 제약을 보장해야 안전합니다.
There was a problem hiding this comment.
넵! 현재 코드에서는 중복이 생성되면, 예외를 던질뿐이고 실질적인 중복은 막지 못하고 있네요
DB에 UNIQUE 제약을 보장하는 방식으로 리팩토링을 하겠습니다 !
| (rs, rowNum) -> new Theme(rs.getLong("id"), rs.getString("name"), rs.getString("description")), | ||
| reservationRequest.getTheme()); | ||
| public Reservation save(String date, Member member, Long timeId, Long themeId) { | ||
| Time time = entityManager.getReference(Time.class, timeId); |
There was a problem hiding this comment.
Time과 Theme을 getReference()로 가져오고 있는데, 이 방식은 id가 실제 존재하는지의 여부를 체크하지 않는 것으로 알고 있습니다. 존재하지 않는 시간이나 테마 ID가 전달된 경우 예외처리가 필요해보입니다.
이 요청 id가 유효한지 검증해야 한다면 find()나 별도의 조회 로직을 사용해보시면 좋을 것 같아요!
There was a problem hiding this comment.
서비스 단에서 themeId와 timeId 의 존재 여부를 검증하기 때문에 이미 유효한 timeId와 themaId 를 getReference()를 사용해서 가져올, 또 검증을 해야하는지 의문이 들었습니다.
하지만 누군가가 ReservationDao.save()를 서비스 검증 없이 직접 호출해서 사용하게 된다면 없는 id 도 프록폭시로 들어가게 되어 예외가 터질 수 있다고 생각하게 되었습니다 !
따라서, 저장 사이의 경쟁 조건이나 DAO 단독 호출 가능성을 고려하면, 더 명확하게 하려면 서비스에서 findById().orElseThrow(...)로 실제 엔티티를 조회한 뒤 DAO에 넘기는 방식으로 수정해보겠습니다 !
| return entityManager.createQuery( | ||
| "select r from Reservation r join fetch r.member join fetch r.theme join fetch r.time", |
There was a problem hiding this comment.
3중 조인을 선택한 이유가 있으신가요? 아마 예외 목록 응답에서 세 연관관계가 항상 필요해서 선택하신 것 같아요! 그렇다면 연관 객체가 10개가 넘어가는 경우 10개의 테이블 모두를 매핑해야할텐데요,
N+1을 방지한다는 장점이 있지만 모든 조회에서 연관 엔티티 전체를 로딩해야해서, 부담이 발생합니다.
조회 목적에 따라 다른 방식을 고민해보셨으면 좋겠어요!
There was a problem hiding this comment.
N+1을 막기위해 전부 fetch join 하게 되면, 쿼리가 점점 무거워질 수 있다는 단점이 있기 때문에 join을 사용할 수 있겠군요 !
findMine() 에서 실제로 필요한 것은 엔티티 전체가 아니라
reservation.id
theme.name
reservation.date
time.value
status
해당 값들입니다.
따라서 전체가 아닌 필요한 값을 가져올 수 있도록 join과 DTO projection 을 같이 사용한다면, 필요한 값들만 가져올 수 있게 할 수 있기 때문에 더 효율적으로 프로그램이 작동될 수 있겠군요 !
| public Member(Long id, String name, String email, MemberRole role) { | ||
| this(id, name, email, null, role); | ||
| } | ||
|
|
||
| public Member(String name, String email, String password, MemberRole role) { | ||
| this(null, name, email, password, role); | ||
| } |
There was a problem hiding this comment.
ID는 @GeneratedValue를 통해 영속화 과정에서 생성되는데, 외부에서 ID를 직접 전달할 수 있는 public 생성자를 만든 이유가 궁금합니다.
There was a problem hiding this comment.
id 를 썻던 이유는 @GeneratedValue 를 사용이 익숙해지지 않아서 였습니다 ...!
@GeneratedValue는 id 값을 애플리케이션 코드에서 직접 넣는 게 아니라, DB가 생성한 값을 JPA가 엔티티에 채워준다는 뜻입니다.
따라서, Long id 값을 받을 필요가 없기 때문에 해당 코드는 삭제되어야합니다 !
id를 받지 않는 쪽으로 수정해보겟습니다 !
| } | ||
|
|
||
| public Member(Long id, String name, String email, MemberRole role) { | ||
| this(id, name, email, null, role); |
There was a problem hiding this comment.
이 생성자를 사용하면 password가 null인 Member가 만들어질 수 있는데 이 해당 객체가 영속화될 가능성은 없을까요? 다른 개발자가 해당 생성자를 실수로 사용한다면 치명적인 버그가 발생할 것 같습니다.
일부 회원 정보만 필요해서 추가한 생성자라면, 엔티티 대신 별도의 DTO를 사용하여 엔티티의 안정성을 보장하는 것이 좋아보입니다.
There was a problem hiding this comment.
password가 없기 때문에 무결성이 깨진 객체가 entityManager.persist(member)를 통해서 영속화가 된다면 Member라는 도메인 객체가 가져야 할 필수 조건을 생성자 단계에서 깨뜨릴 수 있기 때문에 문제가 password를 사용하지 않는 생성자는 문제가 발생할 수 있다는 것에 동의합니다!
해당 생성자는 지금 코드에서 사용되지 않는 생성자이고 제가 수정하는 과정에서 해당 코드를 지우지 않았습니다!...
일부 회원 정보만 필요해서 추가한 생성자라면, 엔티티 대신 별도의 DTO를 사용하여 엔티티의 안정성을 보장하는 것이 좋아보입니다.
해당 리뷰를 참고하여, 필요한 정보만 추가한 생성자일 경우 별도의 DTO를 사용해보도록 하겠습니다!
| } catch (RuntimeException e) { | ||
| throw new AuthenticationException(); | ||
| } |
There was a problem hiding this comment.
RumtimException 으로 인증을 실패를 했을 경우도, 결과적으로는 인증에 실패한 것이기 때문에AuthenticationException 를 반환을 해야한다고 생각했었습니다!
하지만 서버의 오류, 잘못된 enum 반환, 예상하지 못한 데이터 형태를 처리해야할 경우 모두 401로 처리하는 것은 잘못되었다고 생각합니다 !
애초에 에러를 반환을 해야하는 이유가, 개발자가 원인을 파악하여 해결하기 위함인데 서비스 장애를 가지고 AuthenticationException 에러를 발생시키는 것은 본질적인 원인을 알려주지 못한 것이 때문에 해당 부분을 수정해야 한다고 생각합니다 !
올바른 에러 "원인" 을 반환할 수 있도록(서비스 vs 로그인) 리팩토링 해보겠습니다!
| } catch (RuntimeException e) { | ||
| throw new AuthenticationException(); | ||
| } |
There was a problem hiding this comment.
추가로 궁금한 사항이 있는데 쿠키가 없거나 토큰이 없을 경우에는 로그인이 실패 RuntimeException 인지 AuthenticationException 인지 어떻게 정할 수 있을까요 ? 현실세계의 상황을 고려해서 개발자들끼리 정하면 될까요?
쿠키가 없는 경우에 서비스가 진행되지 않아서 RuntimeException 일 수 있고 인증이 되지 않았기 때문에 AuthenticationException 일 수 있기 때문에 궁금증이 생겨서 여쭤봅니다 !
상준 리뷰어님은 어떤 기준으로 판단하시나요 !?
| } catch (RuntimeException e) { | ||
| return handleAuthenticationFailure(optional); | ||
| } |
|
|
||
| @Component | ||
| public class JwtTokenProvider { | ||
| private static final String SECRET_KEY = "roomescape-secret-key-for-jwt-token"; |
| return jwtParser() | ||
| .parseClaimsJws(token) | ||
| .getBody(); |
There was a problem hiding this comment.
isValidSignature 라는 메서드를 통해서 JWT를 검증 의도를 분리하는 메서드를 만들고 싶었습니다 !
그 과정에서 2번을 파싱하게 되는 것이 괜찮다고 생각했습니다. 하지만 WT 구조 파싱, 서명 검증, 만료 시간 검증 등을 수행등을 똑같이 더 해야하며 모든 인증 요청에 대해서 비용이 2개가 된 다는 점을 리뷰 답변을 준비하는 과정을 통해 배울 수 었습니다.
뿐만 아니라 이미 구현되어 있는 parseClaimsJws 자체에서 구체적인 JWT 예외 처리를 하고 있기 때문에 isValidSignature 메서드를 만들어서 검증할 필요 없이 parseClaims 내부에서 파싱과 검증을 2가지를 동시에 처리할 수 있다고 생각합니다.
질문
그렇다면, parseClaims 라는 메서드는 파싱과 검증(이미 스프링에서 구현되어 있는 parseClaimsJws 통해서) 2가지 기능을 하게 되는데 이것은 괜찮을까요? 호출자에 입장에서 "토큰을 얻거나 실패한다." 라는 관점에서 본다면 해당 구현은 합당하다고 생각하는데 리뷰어 님은 어떻게 생각하시나요 ?
| .setParameter("password", password) | ||
| .getSingleResult(); | ||
| } catch (NoResultException e) { | ||
| throw new EmptyResultDataAccessException(1); |
There was a problem hiding this comment.
네 !
이건 ![해당리뷰] 에 관련된 내용이과 비슷한 결이군요! 이미 검증되어 잇는 api 의 예외를 처리하는 방식을 사용해보도록 하겠습니다 !
JPA 예외
- .getSingleResult(); 사용
커스텀 예외
public class MemberNotFoundException extends
RuntimeException {
public MemberNotFoundException() {
super("회원을 찾을 수 없습니다.");
}
}DAO에서:
catch (NoResultException e) {
throw new MemberNotFoundException();
}| .getSingleResult(); | ||
| } catch (NoResultException e) { | ||
| throw new EmptyResultDataAccessException(1); | ||
| } |
There was a problem hiding this comment.
모든 조회 메서드 마다 getSingleResult()호출을 통해서 “조회 결과 없음”을 NoResultException으로 표현하고, 그걸 각 메서드마다 직접 잡아서 변환을 하다 보면 DAO 의 오류 메서드 일관성이 무너질 수 있겠네요!
현재 조회 메서드는 전부 EmptyResultDataAccessException 로 처리히자만 나중에 return을 null 이나 MemberNotFoundException 로 하게 되면 일관성 유지가 안 되기 때문에 위의 리뷰처럼 optional로 표현하는 방식이 일관성을 유지할 수 있는 좋은 방법이 될 수 있겟네요 !
| return entityManager.createQuery( | ||
| "select r from Reservation r join fetch r.member join fetch r.theme join fetch r.time", |
There was a problem hiding this comment.
N+1을 막기위해 전부 fetch join 하게 되면, 쿼리가 점점 무거워질 수 있다는 단점이 있기 때문에 join을 사용할 수 있겠군요 !
findMine() 에서 실제로 필요한 것은 엔티티 전체가 아니라
reservation.id
theme.name
reservation.date
time.value
status
해당 값들입니다.
따라서 전체가 아닌 필요한 값을 가져올 수 있도록 join과 DTO projection 을 같이 사용한다면, 필요한 값들만 가져올 수 있게 할 수 있기 때문에 더 효율적으로 프로그램이 작동될 수 있겠군요 !
| (rs, rowNum) -> new Theme(rs.getLong("id"), rs.getString("name"), rs.getString("description")), | ||
| reservationRequest.getTheme()); | ||
| public Reservation save(String date, Member member, Long timeId, Long themeId) { | ||
| Time time = entityManager.getReference(Time.class, timeId); |
There was a problem hiding this comment.
서비스 단에서 themeId와 timeId 의 존재 여부를 검증하기 때문에 이미 유효한 timeId와 themaId 를 getReference()를 사용해서 가져올, 또 검증을 해야하는지 의문이 들었습니다.
하지만 누군가가 ReservationDao.save()를 서비스 검증 없이 직접 호출해서 사용하게 된다면 없는 id 도 프록폭시로 들어가게 되어 예외가 터질 수 있다고 생각하게 되었습니다 !
따라서, 저장 사이의 경쟁 조건이나 DAO 단독 호출 가능성을 고려하면, 더 명확하게 하려면 서비스에서 findById().orElseThrow(...)로 실제 엔티티를 조회한 뒤 DAO에 넘기는 방식으로 수정해보겠습니다 !
| public Member(Long id, String name, String email, MemberRole role) { | ||
| this(id, name, email, null, role); | ||
| } | ||
|
|
||
| public Member(String name, String email, String password, MemberRole role) { | ||
| this(null, name, email, password, role); | ||
| } |
There was a problem hiding this comment.
id 를 썻던 이유는 @GeneratedValue 를 사용이 익숙해지지 않아서 였습니다 ...!
@GeneratedValue는 id 값을 애플리케이션 코드에서 직접 넣는 게 아니라, DB가 생성한 값을 JPA가 엔티티에 채워준다는 뜻입니다.
따라서, Long id 값을 받을 필요가 없기 때문에 해당 코드는 삭제되어야합니다 !
id를 받지 않는 쪽으로 수정해보겟습니다 !
🙋♂️소개
안녕하세요! 저는 세종대학교 그리디 4기 백엔드를 진행중인 김민욱이라고 합니다!
이번엔 JPA Entity를 적용해서 1~2 단계를 진행하는 미션을 수행하였습니다.
PR은 다음과 같은 순서로 작성됩니다.
PR을 요약하자면, 따로 질문은 없고 제가 학습한 내용 정리, 셀프 리뷰로 PR의 구성되어 있습니다.
이번 미션을 통해서 단일 책임 원책을 잘 지켰는가?
JPA를 잘 활용하여, 미션을 진행하였는가?
등등 에 대해서 피드백을 받고 싶습니다!
리뷰어님께서 바쁜신 것을 고려하여, 제 셀프 리뷰에 대한 내용을 위주로 리뷰를 봐주시면 감사하겠습니다 !
바쁜 시간 내주셔서 해주신 코드 리뷰를 통해, 성장하도록 하겠습니다!
감사해요 !
💻JPA 학습 내용 정리
자바 객체와 DB 테이블을 연결해서, SQL의 직접적인 사용을 줄이고 DB를 다룰 수 있게 해주는 기술입니다. JPA는 자바 객체를 데이터베이스에 저장하고 조회하는 표준 방식입니다.
자바에서
다음과 같은 객체가 있을 경우,
데이터 베이스에는
이런 테이블이 존재합니다.
이것을 연결해주는 것이 JPA입니다.
이렇게 표시해두면, JPA가 Reservation 객체를 DB의 reservation 테이블과 연결해서 다룰 수 있도록 도와줍니다.
🚀순수 JPA를 이번 미션에 적용하기
1. Entity 만들기: DB 테이블과 연결될 자바 클래스를 먼저 생성합니다.
여기서 사용된 어노테이션을 정리하겠습니다.
@entity : 이 클래스가 DB 테이블과 연결되는 객체라는 뜻입니다.
@id 이 필드가 기본키(PK)라는 뜻입니다.
@GeneratedValue: id 값을 DB가 자동으로 만들어 준다는 뜻입니다.
2. EntityManager 사용하기
순수 JPA에서는 EntityManager라는 객체로 DB 작업을 합니다!
3. 트랜잭션 안에서 실행하기
DB에 저장, 수정 삭제하는 작업은 보통 트랜잭션 안에서 실행되어야 합니다.
@Transcational은 이메서드 안의 DB 작업을 하나의 작업 단위로 묶겠다라는 의미를 가집니다.
트랜잭션 사용하기
순수 JPA에서 EntutyManager를 직접 사용할 때, 트랜젝션이라는 것은 필수적인 기술이라고 합니다.
왜냐하면 persist()로 저장할 때, remove()로 삭제할 때 트랜젝션이 필요하다고 합니다.
Service에서 Transactional을 붙이는 방식으로 사용한다고 하네요.
이것은 DB 작업을 하나의 작업 단위로 묶어주는 기능입니다.
Transactional 이 DB 작업을 하나의 단위로 묶어준 뒤 JPA 가 EntityManager로 객체를 DB에 저장/조회/삭제 함으로써 JPA를 활용할 수 있습니다.
💡JPA를 사용했을 때의 이점
JPA를 사용하게 되면 SQL을 직접 작성해야했습니다.
하지만 JPA를 쓰면 객체를 저장하거나 조회하는 방식으로 처리할 수 있습니다.
JPA를 사용하게 된다면, 쿼리분을 작성에 신경쓰기 보다는 객체를 어떻게 다룰까에 더 집중할 수 있게 됩니다.
트랜잭션 안에서 객체 값을 바꾸면, JPA가 변경 내용을 감지해서 DB에 반영할 수 있게 됩니다.
MySQL, H2 같은 DB는 SQL 문법이 조금씩 다릅니다.
JPA를 사용하면, JPA가 DB에 맞게 처리해주게 됩니다.
따라서, JPA는 자바 객체와 DB 테이블을 연결해서 객체를 저장하고 조회하듯이 DB를 사용할 수 있게 해주는 기술임을 학습하였습니다!
🕵️셀프리뷰
셀프 리뷰는 다음과 같은 목적으로 진행되었습니다.
객체 지향적으로 프로젝트를 진행을 하였는가? (코드 품질 개선 작업)
JPA entity를 적용을 해서 1~2 단계 미션을 진행하였는가?
1. 테마 기능을에 service를 추가
member, reservation, time 에 서비스가 있지만 theme에만 서비스가 없습니다.
이렇게 되었을 때, theme에 대하여 트랜잭션, 예외 처리, 비즈니스 규칙 처리가 일관되지 않다고 생각했습니다. 따라서 theme에 서비스를 추가함으로써, theme의 컨트롤러는 service만 호출하게 수정해야한다고 생각했습니다.
단일 책임 원칙을 지키켜야 한다는 생각으로 theme에 서비스단을 추가하였습니다.
뿐만 아니라 Theme 파일에 엔티티에 DB모델, 도메인 모델, API, DTO 역할을 동시에 하는 구조였습니다.
TimeRequest, ThemeRequest, ThemeResponse 같은 DTO를 분리함으로써 단일 책임의 원칙을 지키고자 하였습니다.
반영 커밋:
f6b104e
74b7d5e
2.DAO의 책임은 엔티티 저장/조회만 하도록 하자.
reservation DAO에서 요청 DTO 해석을 처리하고 있었습니다.
이것을 service에서 처리하고 DAO는 Reservation 엔티티,date,timeId,name 같은 저장에 필요한 값들만 받도록 수정했습니다.
반영 커밋: 34599e8
3. JPA를 쓰는 목적을 살려 Reservation이 Meber @manytoone 관계를 가지고 DB에서도 meber_id를 두는 구조 변경
예약(Reservation)이 회원 이름 문자열을 들고 있던 구조를, 실제 회원(Member) 엔티티와 연결되는 구조로 바꾼 것입니다.
이름이 중복된 경우, 문제가 발생할 수 있다고생각했습니다.
그래서 내 예약 조회도 이름이 아니라 로그인한 회원의 ID로 조회하도록 합니다!
예약이 “회원 이름”을 저장하던 구조에서, JPA답게 “회원 엔티티”와 관계를 맺는 구조로 바꿨습니다. 그래서 내 예약 조회도 name 기준이 아니라 member_id 기준으로 바뀌었습니다.
반영 커밋: 34599e8
(2번 내용의 커밋과 동일)
4. 에러 관리
반영 커밋: e0bca6d
패키지: e0ac584