[Spring Data JPA] 안금서 미션 제출합니다.#110
Conversation
|
먼저 답변부터 진행해볼게요! 나머지는 너무 졸려서 내일 작성할 수도 있고, 아니면 적는 곳까지는 적을 수도 있을 것 같아요 쉬운 순서대로 짧은 순서대로 진행해볼게요 4번 1번 일반적으로는 service레이어에서 이렇게 진행해왔던 것 같아요 딱 제가 짠다고 해도 이렇게 짤 것 같아요! 2번, 3번
답변오늘 글을 읽다보니 저보다 금서님이 더 jpa 에 대해서 더 잘 알고 계실 수도 있을 것 같다는 생각이 들지만 뉴비의 시선에서 말씀드린다고 생각해주세요 :) 대부분의 경우에 jpa 의 특정한 기능을 사용하기 보다는 순수 저장 순서들을 조절하고, 삭제 순서들을 조절하는 방식으로 삭제를 해왔던 것 같아요 실무에서 가장 중요하게 보는 것은 프로젝트를 누군가가 이어서 작업하는 것이 가능해야한다는 전제가 가장 중요한 프로젝트의 핵심인 것 같아요 이 전제를 기준으로 봤을 때 jpa 는 러닝커브가 정말 깊은 끝없는 그런 기술입니다 그렇다보니 cascade, onDelete 옵션같은 것들을 많이 쓰지 않는 것 같아요 그렇다면 반대로 jpa 를 몰라도 되냐? 라고 했을 때는 jpa 에서 다루고 있는 개념들은 db 에 핵심적인 개념일 때가 많습니다 그렇기에 배워두면 무조건 좋죠 일단 꼰대인 제가 생각하는 jpa 에 대한 생각을 먼저 말씀드렸으니 다음은 다시 답변으로 돌아가볼게요
저는 프로젝트로 간단하게 oneToMany, manyToOne 정도까지를 그냥 간단한 프로젝트를 하면서 배웠던 것 같아요 오히려 저는 db 쪽을 더 중요시 하는 것 같은데요 |
be-student
left a comment
There was a problem hiding this comment.
정말 너무 잘 해주셔서 뭐라 달 말이 많이 없네요
천천히 보시고 잠결에 쓰다보니 뭔소리를 하는지 모르겠다 하시면 언제든 질문해주시면 됩니다!
일단 언제든 머지가 가능하게 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.
우선, 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.
솔직히 말씀드려서 말씀해주신 2가지가 같이 케이스는 없었던 것 같아요
일반적으로는 html 을 렌더링하는 controller 가 없으니까요! (프론트가 있다보니...)
1번은 정확하게 모르겠어요 저도 실무에서는 전혀 볼 일이 없는 코드다보니...?
비슷하게 응답하는 방법처럼 진행해주신 방법은 좋은 것 같습니다! 일반 응답과 많이 달라지면 그 처리를 클라이언트에서 하지 못해서 에러가 많이 발생하는 것 같아요
2번의 경우에는 실제로 많이 쓰는데요
상태 코드 + 오류 메시지 + 서버의 오류 타입을 추가해서 이렇게 총 3가지를 내려주는 것 같아요
상태코드 400 에도 다양한 에러 메시지가 있을텐데, 이거를 프론트에서 분기하려면 오류 메시지를 의존하는 것 보다는 오류 타입에 의존하는 것이 좋은 것 같아요
{
"message":"요청에 시간이 없습니다",
"type":"invalid_time"
}
같은 형태이려나요?
There was a problem hiding this comment.
오류 메세지가 세분화 되어있다보니, 프론트 입장에서는 분기할 때 번거로움이 있을 수 있겠네요!
이 점은 고려하지 못했던 것 같아요.
세분화된 오류 메세지를 공통된 오류타입으로 묶어서 분리하면, 훨씬 효율적으로 분기처리할 수 있겠다는 생각이 듭니다.
말씀하신 방향으로 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.
jpa 자체에 exists 라는 named 쿼리가 있을거에요!
https://hungseong.tistory.com/73
추가로 exists 라는 쿼리 메소드도 있으니 참고해도 좋을 것 같아요!
참고로 저희 팀에서 사용하는 순서는 이렇게 됩니다
- named 쿼리 <-- 최대한 이쪽으로 풀어보려고 함
- jpql
- native query <--- 거의 사용하지 않기 위해 노력함
There was a problem hiding this comment.
감사합니다! 쿼리 메서드 사용해서 수정하였습니다~
추가적으로, 말씀해주신 3가지에 대해서도 더 학습을 해봐야겠네요.
Q. named 쿼리로 최대한 많이 풀어보려고 한다고 하셨는데, 팀에서 jpql보다 named 쿼리를 더 우선순위로 두는 이유가 뭔가요?
There was a problem hiding this comment.
JPQL 의 경우에는 문법이 그냥 sql 과 달라서 익숙하지 않은 것이 가장 크구요
잘못된 필드가 나왔을 때 에러 메시지가 가장 직관적이었던 것 같아요
be-student
left a comment
There was a problem hiding this comment.
고생하셨습니다!
이미 너무 잘 해주셔서 수정할 부분이 거의 없는 것 같네요!
참고용으로 간단한 내용을 남겨드릴게요!
너무 잘 해주셔서 별다른 내용을 달아드릴만한 것이 없다보니
이번 미션 내용은 아니지만 보안을 지키는 코딩에 관한 내용을 조금 남겨두려고 합니다! 🙇
실제 다른 리뷰에서 진행했던 내용인데요
#107 (comment)
토큰에 있는 role 을 믿어서는 안되는 이유인데요
secure coding 관점에서는 진짜 사용자의 어떠한 인풋도 신뢰하면 안되는... 그런 문제가 항상 있는 것 같아요
보안과 권한 관리에서 어디까지 타협하는 것이 좋은지는 경험에 많이 의존하게 되는 것 같은데요
이런 부분도 고민을 해보시면 많이 배울 수 있을 것 같아요
#107 (comment)
관련해서 다양한 보안 정책이 나오고, 이런 것들은 언젠가는 한번쯤 고민해보셔야 될 것이다보니 미리 슬쩍 던져봅니다 🙇
| .orElseThrow(() -> new MemberNotFoundException("가입된 회원이 아닙니다.")); | ||
| } | ||
|
|
||
| validateDuplicateReservation(reservationRequest); |
There was a problem hiding this comment.
서버 코드로 검증을 하시고 계시는 것은 아주 좋은 것 같아요 👍
하지만 저희는 멀티 스레드 환경이다보니 테이블에 실제 insert 가 잘못되는 것은 막을 수 없는데요
다음 미션때 date, time, theme 에 해당하는 unique index 를 추가해보면 좋을 것 같아요!
date, time, theme 에 unique index 를 추가하면 같은 예약이 2개 생기는 것을 db 레벨에서도 막을 수 있을 것 같아요
There was a problem hiding this comment.
좋은 제안 감사합니다!!!!
멀티스레드 환경에서 동시에 여러 요청이 들어온다면, 서버에서의 중복 검증만으로는 위험하다는 생각이 드네요.
멀티 스레드 환경이다보니 테이블에 실제 insert 가 잘못되는 것은 막을 수 없는데요.
라는 말씀이 무슨 말인지는 대충 이해했는데,
뭔가 예시를 찾아보고 더 와닿게 이해하고 싶어서 정리해봤습니다.
우선, 서버는 여러 스레드에서 클라이언트 요청을 처리합니다. 예를 들어, 두 개의 클라이언트가 같은 시점에 동일한 data, time, theme 로 예약을 시도한다고 가정해봅시다!
서버의 validateDuplicateReservation 메서드가 중복 여부를 확인하기 위해 DB를 조회합니다.
두 요청이 동시에 들어오게 된다면?
- 첫 번째 요청이 DB를 확인하고 중복되지 않는다고 판단.
- 두 번째 요청도 거의 동시에 DB를 확인하고 중복되지 않는다고 판단.
결과적으로, 두 요청이 거의 동시에 동일한 데이터를 삽입하려고 시도할 수 있습니다.
- 두 요청이 중복 검증을 통과했더라도, 실제로 DB에 삽입될 때 중복된 데이터가 생성될 가능성이 있는 것이죠. -> 이게 만약 대규모 예약 시스템일 경우,,, 중복 예약이 된 사용자들은 정말 큰 불편함을 겪겠네요. 🥶 (상상만 해도 끔찍)
There was a problem hiding this comment.
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가 안 담기는 것도 아닙니다 😭
일단, 계속 원인을 찾아보겠습니다! + 코드 리팩토링까지도요!
추가적으로 궁금한 사항이 생기면 더 적어놓겠습니다!
날카로운 리뷰 부탁드립니다 누누님!
화이팅!!