-
Notifications
You must be signed in to change notification settings - Fork 56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Spring Data JPA] 안금서 미션 제출합니다. #110
base: goldm0ng
Are you sure you want to change the base?
Conversation
먼저 답변부터 진행해볼게요! 나머지는 너무 졸려서 내일 작성할 수도 있고, 아니면 적는 곳까지는 적을 수도 있을 것 같아요 쉬운 순서대로 짧은 순서대로 진행해볼게요 4번 1번 일반적으로는 service레이어에서 이렇게 진행해왔던 것 같아요
딱 제가 짠다고 해도 이렇게 짤 것 같아요! 2번, 3번
답변오늘 글을 읽다보니 저보다 금서님이 더 jpa 에 대해서 더 잘 알고 계실 수도 있을 것 같다는 생각이 들지만 뉴비의 시선에서 말씀드린다고 생각해주세요 :) 대부분의 경우에 jpa 의 특정한 기능을 사용하기 보다는 순수 저장 순서들을 조절하고, 삭제 순서들을 조절하는 방식으로 삭제를 해왔던 것 같아요 실무에서 가장 중요하게 보는 것은 프로젝트를 누군가가 이어서 작업하는 것이 가능해야한다는 전제가 가장 중요한 프로젝트의 핵심인 것 같아요 이 전제를 기준으로 봤을 때 jpa 는 러닝커브가 정말 깊은 끝없는 그런 기술입니다 그렇다보니 cascade, onDelete 옵션같은 것들을 많이 쓰지 않는 것 같아요 그렇다면 반대로 jpa 를 몰라도 되냐? 라고 했을 때는 jpa 에서 다루고 있는 개념들은 db 에 핵심적인 개념일 때가 많습니다 그렇기에 배워두면 무조건 좋죠 일단 꼰대인 제가 생각하는 jpa 에 대한 생각을 먼저 말씀드렸으니 다음은 다시 답변으로 돌아가볼게요
저는 프로젝트로 간단하게 oneToMany, manyToOne 정도까지를 그냥 간단한 프로젝트를 하면서 배웠던 것 같아요 오히려 저는 db 쪽을 더 중요시 하는 것 같은데요 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
정말 너무 잘 해주셔서 뭐라 달 말이 많이 없네요
천천히 보시고 잠결에 쓰다보니 뭔소리를 하는지 모르겠다 하시면 언제든 질문해주시면 됩니다!
일단 언제든 머지가 가능하게 approve 를 드리지만, 추가적으로 요청을 주시거나 dm 으로 요청을 주시면 언제든 확인해드릴게요!
고생 많으셨습니다
src/main/java/roomescape/exception/GeneralExceptionHandler.java
Outdated
Show resolved
Hide resolved
src/main/java/roomescape/exception/GeneralExceptionHandler.java
Outdated
Show resolved
Hide resolved
@Slf4j | ||
@ControllerAdvice(assignableTypes = PageController.class) | ||
public class PageExceptionHandler { | ||
@ExceptionHandler(Exception.class) | ||
public String handleException(Exception e) { | ||
log.error("error: " + e.getMessage()); | ||
return "error/500"; //view 렌더링 페이지는 만들지 않음 | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저도 이건 처음보는데 열심히 찾아보셨군요! 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
우선, GeneralExceptionHandler와 PageExceptionHandler를 나눈 이유는 예외처리를 할 때 상황에 따라 응답을 다르게 해주기 위해서입니다.
현재 구현되어 있는 컨트롤러들은 역할에 따라 크게 두가지로 나눌 수 있을 것 같아요.
- HTML 페이지를 렌더링 하는 역할을 하는 PageController
- API 요청을 처리하는 그 외 나머지 Controller( ex: MemberController, ReservationController, WaitingController, TimeController, ThemeController 등)
만약, 이 두 컨트롤러에서 500 에러와 같은 예상치 못한 예외가 발생한다면?
같은 예외라 하더라도,
PageController는 오류 페이지를 랜더링해서 보여주는 방식으로 응답을 내릴 수 있고,
그 외 다른 API 컨트롤러는 응답 바디에 상태 코드를 넣고 오류 메세지를 전달하는 방식으로 내릴 수 있습니다.
1번의 컨트롤러의 경우, 뷰 렌더링 과정에서 문제가 생기면 원래 받아야할 HTML 응답과 비슷한 형태의 예외 응답을 주는 것이 적절할 것이라고 판단하였습니다! 위 코드처럼 예외 페이지를 응답하거나 리다이렉트를 하는 방향으로요!
2번의 컨트롤러의 경우 상태 코드나, 오류 메세지 등을 전달해주는 방식으로 예외 응답을 줘서 프론트 측에서 예외 응답에 따라 적절한 조치를 취할 수 있도록 하는 것이죠.
Q. 누누님은 이런 방식에 대해 어떻게 생각하시나요?
그리고 실제로 이런식으로 행해지는 것인지도 궁금하네요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
솔직히 말씀드려서 말씀해주신 2가지가 같이 케이스는 없었던 것 같아요
일반적으로는 html 을 렌더링하는 controller 가 없으니까요! (프론트가 있다보니...)
1번은 정확하게 모르겠어요 저도 실무에서는 전혀 볼 일이 없는 코드다보니...?
비슷하게 응답하는 방법처럼 진행해주신 방법은 좋은 것 같습니다! 일반 응답과 많이 달라지면 그 처리를 클라이언트에서 하지 못해서 에러가 많이 발생하는 것 같아요
2번의 경우에는 실제로 많이 쓰는데요
상태 코드 + 오류 메시지 + 서버의 오류 타입을 추가해서 이렇게 총 3가지를 내려주는 것 같아요
상태코드 400 에도 다양한 에러 메시지가 있을텐데, 이거를 프론트에서 분기하려면 오류 메시지를 의존하는 것 보다는 오류 타입에 의존하는 것이 좋은 것 같아요
{
"message":"요청에 시간이 없습니다",
"type":"invalid_time"
}
같은 형태이려나요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오류 메세지가 세분화 되어있다보니, 프론트 입장에서는 분기할 때 번거로움이 있을 수 있겠네요!
이 점은 고려하지 못했던 것 같아요.
세분화된 오류 메세지를 공통된 오류타입으로 묶어서 분리하면, 훨씬 효율적으로 분기처리할 수 있겠다는 생각이 듭니다.
말씀하신 방향으로 Core 미션에 반영해보도록 할게요!
@Query("SELECT COUNT(r) > 0 FROM Reservation r WHERE r.date = :date AND r.time.id = :timeId AND r.theme.id = :themeId") | ||
boolean existsByDateAndTimeIdAndThemeId(@Param("date") String date, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
jpa 자체에 exists 라는 named 쿼리가 있을거에요!
https://hungseong.tistory.com/73
추가로 exists 라는 쿼리 메소드도 있으니 참고해도 좋을 것 같아요!
참고로 저희 팀에서 사용하는 순서는 이렇게 됩니다
- named 쿼리 <-- 최대한 이쪽으로 풀어보려고 함
- jpql
- native query <--- 거의 사용하지 않기 위해 노력함
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
감사합니다! 쿼리 메서드 사용해서 수정하였습니다~
추가적으로, 말씀해주신 3가지에 대해서도 더 학습을 해봐야겠네요.
Q. named 쿼리로 최대한 많이 풀어보려고 한다고 하셨는데, 팀에서 jpql보다 named 쿼리를 더 우선순위로 두는 이유가 뭔가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JPQL 의 경우에는 문법이 그냥 sql 과 달라서 익숙하지 않은 것이 가장 크구요
잘못된 필드가 나왔을 때 에러 메시지가 가장 직관적이었던 것 같아요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
고생하셨습니다!
이미 너무 잘 해주셔서 수정할 부분이 거의 없는 것 같네요!
참고용으로 간단한 내용을 남겨드릴게요!
너무 잘 해주셔서 별다른 내용을 달아드릴만한 것이 없다보니
이번 미션 내용은 아니지만 보안을 지키는 코딩에 관한 내용을 조금 남겨두려고 합니다! 🙇
실제 다른 리뷰에서 진행했던 내용인데요
#107 (comment)
토큰에 있는 role 을 믿어서는 안되는 이유인데요
secure coding 관점에서는 진짜 사용자의 어떠한 인풋도 신뢰하면 안되는... 그런 문제가 항상 있는 것 같아요
보안과 권한 관리에서 어디까지 타협하는 것이 좋은지는 경험에 많이 의존하게 되는 것 같은데요
이런 부분도 고민을 해보시면 많이 배울 수 있을 것 같아요
#107 (comment)
관련해서 다양한 보안 정책이 나오고, 이런 것들은 언젠가는 한번쯤 고민해보셔야 될 것이다보니 미리 슬쩍 던져봅니다 🙇
.orElseThrow(() -> new MemberNotFoundException("가입된 회원이 아닙니다.")); | ||
} | ||
|
||
validateDuplicateReservation(reservationRequest); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
서버 코드로 검증을 하시고 계시는 것은 아주 좋은 것 같아요 👍
하지만 저희는 멀티 스레드 환경이다보니 테이블에 실제 insert 가 잘못되는 것은 막을 수 없는데요
다음 미션때 date, time, theme 에 해당하는 unique index 를 추가해보면 좋을 것 같아요!
date, time, theme 에 unique index 를 추가하면 같은 예약이 2개 생기는 것을 db 레벨에서도 막을 수 있을 것 같아요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋은 제안 감사합니다!!!!
멀티스레드 환경에서 동시에 여러 요청이 들어온다면, 서버에서의 중복 검증만으로는 위험하다는 생각이 드네요.
멀티 스레드 환경이다보니 테이블에 실제 insert 가 잘못되는 것은 막을 수 없는데요.
라는 말씀이 무슨 말인지는 대충 이해했는데,
뭔가 예시를 찾아보고 더 와닿게 이해하고 싶어서 정리해봤습니다.
우선, 서버는 여러 스레드에서 클라이언트 요청을 처리합니다. 예를 들어, 두 개의 클라이언트가 같은 시점에 동일한 data, time, theme 로 예약을 시도한다고 가정해봅시다!
서버의 validateDuplicateReservation 메서드가 중복 여부를 확인하기 위해 DB를 조회합니다.
두 요청이 동시에 들어오게 된다면?
- 첫 번째 요청이 DB를 확인하고 중복되지 않는다고 판단.
- 두 번째 요청도 거의 동시에 DB를 확인하고 중복되지 않는다고 판단.
결과적으로, 두 요청이 거의 동시에 동일한 데이터를 삽입하려고 시도할 수 있습니다.
- 두 요청이 중복 검증을 통과했더라도, 실제로 DB에 삽입될 때 중복된 데이터가 생성될 가능성이 있는 것이죠. -> 이게 만약 대규모 예약 시스템일 경우,,, 중복 예약이 된 사용자들은 정말 큰 불편함을 겪겠네요. 🥶 (상상만 해도 끔찍)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unique Index 라는 개념은 처음 들어보는데, 한 번 학습해보고 Core 미션에 적용해볼 수 있으면 해보도록 할게요!
간단하게, 서버 중복 검증에 의한 동시성 문제를 어떻게 Unique Index를 사용해 해결할 수 있는지 정리해보고 다음 미션으로 넘어가보도록 하겠습니다!
<데이터베이스 레벨에서 Unique Index 를 사용하게 될 경우>
동일한 조합의 데이터가 삽입될 경우, DB 자체에서 중복을 감지하고 오류를 발생시킨다고 합니다.
ex) 두 요청이 거의 동시에 삽입을 시도함.
-> 첫 번째 요청이 성공적으로 삽입되면,
Unique Index에 의해 두 번째 요청은 Integrity Constraint Violation 에러가 발생함.
안녕하세요 누누 리뷰어님!
첫 리뷰어 매칭이네요 반갑습니다:)
끈질긴 감기녀석 때문에 이번 미션 안 그래도 어려운데 🥶 배로 어렵게 느껴졌네요,,,
요즘 독감 유행이라던데 건강 잘 챙기세요 누누님.
Spring Data JPA 미션은 유독 에러도 많고 시행착오도 정말 많았는데,
이번 미션도 지난 미션과 마찬가지로 진행하면서 겪었던 생겼던 시행착오에 대해 말씀드리고 조언을 구하고 싶습니다!
<겪었던 시행착오 및 해결방안 혹은 고민사항>
'Repository에서의 Optional 예외처리, 어디서 어떻게 처리하면 좋을까?' 에 대한 고민사항이 있습니다.
데이터 영속성 문제에 대한 시행착오
이 문제는 특정 객체를 생성하고 저장할 때 발생했습니다.
이 코드에서는
라는 예외가 발생했는데요.
이는 JPA/Hibernate에서 영속되지 않은 엔티티를 참조하려고 시도할 때 발생한다고 합니다.
Reservation 엔티티의 theme 필드와 time 필드가 아직 데이터베이스에 저장되지 않은 Theme 객체, Time 객체를 참조하고 있다는 의미입니다. 즉, Reservation 객체를 저장하려고 할 때, Reservation 엔티티에 매핑된 Theme와 Time 객체가 영속 상태가 아니기 때문에 Hibernate가 이 관계를 처리할 수 없으므로 예외가 발생하는 것으로 보입니다.
해결 방안으로 총 두가지를 찾아봤습니다.
첫번째로, Theme 객체 및 Time 객체 (Reservation에 매핑된 객체) 들을 먼저 데이터베이스에 저장 후, Reservation을 저장
`
ex)
`
두번째로, Cascade 설정 추가
Reservation 엔티티의 theme과 time 필드에 CascadeType.PERSIST 또는 CascadeType.ALL을 추가하면 Reservation이 저장될 때 Theme도 자동으로 저장됩니다.
`
ex)
`
이 방법을 사용하면 Reservation을 저장할 때, Hibernate가 자동으로 Theme 객체를 먼저 저장한다고 합니다!
이는 앞서 말했던 데이터 영속성 문제와 비슷한 맥락으로 시행착오를 겪었는데요.
(정보: 관리자는 관리자 페이지로 접속해 예약 생성 및 삭제, 테마 추가 및 삭제, 시간 추가 및 삭제를 할 수 있습니다.)
관리자로 접속하여 예약 추가와 테마 및 시간 추가/삭제는 되는데 예약 삭제 시, 다음과 같은 예외가 발생했습니다.
해당 예외는 데이터베이스에서 참조 무결성 제약 조건이 위반되어서 발생하는 예외라고 합니다.
즉, Time/Theme 테이블에서 삭제하려는 id가 Reservation 테이블의 외래 키(time_id/theme_id)로 참조되고 있기 때문에 삭제가 불가능하다는 것입니다. 이 문제 또한 위와 비슷한 방식으로 문제를 해결할 수 있습니다!
첫번째로, 참조된 데이터를 먼저 삭제하기
`
ex) Reservation 테이블에서 time_id = 2인 데이터를 먼저 삭제한 후 Time 테이블의 행 삭제
`
두번째로는, 외래 키 설정에 ON DELETE CASCADE 추가하기
데이터베이스 외래 키를 ON DELETE CASCADE로 설정하면, TIME 테이블의 행이 삭제될 때 참조된 RESERVATION 데이터도 자동으로 삭제된다고 합니다.
`
ex)
`
세번째로, @onDelete 활용하기
@onDelete는 외래 키가 참조하는 부모 엔티티가 삭제될 때의 동작을 정의하는 어노테이션이며,
부모 엔티티가 삭제되었을 때, 관련된 자식 엔티티를 자동으로 삭제하도록 데이터베이스에 위임한다고 합니다.
`
ex)
`
이러한 데이터 영속성 및 무결성 위반에 대한 시행착오를 겪고 ... JPA에 대해 더 깊이 공부해야겠다는 생각이 드네요.
혹시 누누님은 처음 JPA 학습하실 때, 어떤식으로 하셨나요? 아니면 같이 알면 좋을 법한 내용이나, 키워드, 공부 방향성 같은 것들을 알려주셔도 좋아요 🤩
(이건,, 계속 붙잡고 있었는데 도저히 뭐가 문제인지 모르겠어서 도움 요청 드립니다! 저도 계속 보긴 할 거지만! 혹시나 코드 보다가 원인을 찾으셨다면 공유 부탁드려요 🥺)
우선 상황 설명 드리겠습니다!
<문제 상황>
예약 대기 취소 버튼을 누르면 예상치 못한 500 error 가 발생합니다.
예외 내용은 다음과 같아요.
문제 발생은 WaitingController 중,
`
`
이 부분에서, id가 매핑이 안 되는 문제 같아보입니다.
요청이 "DELETE /waitings/undefined" 로 오더라고요..
그렇다고 해서 예약 대기 응답 객체에 id가 안 담기는 것도 아닙니다 😭
일단, 계속 원인을 찾아보겠습니다! + 코드 리팩토링까지도요!
추가적으로 궁금한 사항이 생기면 더 적어놓겠습니다!
날카로운 리뷰 부탁드립니다 누누님!
화이팅!!