-
Notifications
You must be signed in to change notification settings - Fork 170
[Spring MVC] 이고은 3,4단게 미션제출합니다 #515
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
Conversation
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.
안녕하세요, 고은님😄
다시 리뷰어로 만나게 되어 반갑네요!!
코드 구조에 대한 설명 감사합니다! 덕분에 더 쉽게 이해할 수 있었어요.
질문 사항에 대한 답변
- 예약을 삭제한 후 새로운 예약을 추가하면 ID가 1, 2, 4, 5처럼 건너뛰게 됩니다. 찾아보니 삭제된 ID를 재사용하면 참조 문제가 발생할 수 있고, 삭제된 데이터의 추적 가능성 때문에 DB에서도 ID는 한 번 사용되면 다시 사용되지 않는 것이 권장된다고 합니다. 하지만 사용자 입장에서는 화면에 표시되는 번호가 1, 2, 3으로 연속적으로 보이는 것이 더 직관적일 것 같습니다. 그래서 내부 ID는 그대로 유지하되 화면에만 순번(1, 2, 3...)을 별도로 표시하는 방식을 생각하였는데, 이 방법은 내부 ID와 화면의 순번이 달라 오히려 혼동이 생길 것 같았습니다. 이 경우 일반적으로 어떻게 처리하는지 궁금합니다.
-
말씀하신 대로 삭제된 ID를 재사용하면 참조 무결성 문제가 발생할 수 있고, 데이터 이력 관리를 위해서라도 ID를 재사용하지 않는 것이 일반적입니다. 잘 찾아보셨네요!
-
화면에 보이는 '순번'은 DB에 저장하지 않는 것을 권장합니다. 검색이나 필터링 기능이 적용되면 데이터의 순서는 언제든 바뀔 수 있기 때문에, DB에 고정된 값을 저장하면 오히려 관리가 어려워집니다. ID는 데이터의 고유 식별자로만 남겨두고, 화면상의 순번은 클라이언트가 데이터를 받아 리스트를 렌더링할 때 인덱스를 붙여서 보여주거나, 서버에서 페이징 처리를 할 때 계산된 순번 필드를 DTO에 담아 반환하는 방식을 주로 사용합니다.
-
관련해서 한 가지 질문을 드리고 싶어요. 현재 구현하신 코드를 보면 데이터 존재 여부를 확인 후 실제로 삭제(
Hard Delete)하고 계신데요. 실무에서는 데이터 복구나 이력 추적을 위해 실제 데이터를 삭제하지 않고deleted_at이나is_deleted같은 컬럼을 두어 상태만 변경하는 논리적 삭제(Soft Delete) 방식도 많이 사용합니다. -
고은 님은 이번 프로젝트에서 물리적 삭제 방식을 선택하셨는데, 혹시 그렇게 결정하신 특별한 이유가 있으실까요?
- 예외 처리를 exception이란 패키지로 따로 빼 두었는데, 여러 코드들을 추가로 찾아보니 예외처리를 dto안에서 하는 경우도 있더라고요. 예외 클래스를 별도 exception 패키지로 분리하는 것과 dto에 포함하는 것의 차이나 기준이 있는지 궁금합니다.
-
먼저 현재 작성하신
InvalidReservationException처럼 커스텀 예외를 만들고,GlobalExceptionHandler에서 처리하는 구조는 잘 잡으셨다고 생각해요! -
질문하신 DTO 안에 예외 처리가 있는 경우는 아마 DTO 내부에서 데이터 유효성 검증을 수행하며 예외를 발생시키는 코드를 보신 게 아닐까 싶어요.
-
일반적으로
Exception클래스 자체를 DTO 파일 내부에 정의하는 것은 권장하지 않습니다. 예외 클래스는 시스템 전반에서 공통으로 사용될 수 있으므로, 지금처럼 별도의exception패키지로 분리하여 관리하는 것이 재사용성과 유지보수 측면에서 훨씬 유리합니다. -
다만, DTO 객체가 생성될 때 데이터의 무결성을 보장하기 위해 생성자나 빌더 패턴 내부에서 검증 로직을 넣고, 조건에 맞지 않으면 예외를
throw하는 방식은 자주 사용됩니다. 즉, 예외 클래스의 정의는 별도 패키지에서 하고, 예외 발생은 필요한 곳에서 수행한다고 이해하시면 좋을 것 같네요!
답변이 되셨으면 좋겠네요! 혹시라도 추가 질문 사항이 있으면 언제든지 코멘트 해주세요!
수정 요청드릴 만한 부분은 따로 없어서 코멘트만 확인하시고 답변 부탁드릴게요!
그리고 늦게 리뷰 달아드려 죄송합니다..! 항상 너무 잘해주고 계셔서 감사해요 👍👍👍
남은 미션도 파이팅🔥🔥
| @Controller | ||
| public class ReservationController { | ||
|
|
||
| private final AtomicLong index = new AtomicLong(0); |
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.
일반적인 Long 클래스가 아닌 AtomicLong 클래스를 사용하셨는데, 이유가 있으실까요? 궁금하네요!
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.
메서드 네이밍이 직관적이라 좋네요👍👍👍
| reservations.add(new Reservation(index.incrementAndGet(), "브라운", "2023-01-01", "10:00")); | ||
| reservations.add(new Reservation(index.incrementAndGet(), "브라운", "2023-01-02", "11:00")); | ||
| reservations.add(new Reservation(index.incrementAndGet(), "브라운", "2023-01-03", "12:00")); |
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.
현재 객체를 생성할 때 사용하고 계신 방식을 위치 기반 인수 방식이라고 합니다!
이 방식은 파라미터가 적을 때는 간단하지만, 파라미터가 많아지면 각 위치가 어떤 필드를 의미하는지 파악하기 어렵고, 순서를 잘못 입력할 실수를 하기도 쉬워요.
이러한 실수를 방지하기 위해 빌더 패턴이란 것을 사용하는데, 직접 구현도 가능하고 Lombok 라이브러리를 활용하면 어노테이션으로 쉽게 사용이 가능합니다!
나중에 한번 찾아보시면 도움이 될 것 같네요 😄
| public void setName(String name) { | ||
| this.name = name; | ||
| } | ||
|
|
||
| public void setDate(String date) { | ||
| this.date = date; | ||
| } | ||
|
|
||
| public void setTime(String time) { | ||
| this.time = 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.
setName, setDate, setTime 메서드는 사용하지 않고 계시는데, 정의를 미리 해두신 이유가 있을까요??
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.
처음에는 예약 정보를 수정하는 상황을 고려해 setName, setDate, setTime 같은 setter를 만들었습니다.
하지만 다시 보니 예약 객체는 불변성을 유지하는 편이 더 명확하고 안전한 것 같네요ㅜㅜ.
예약 정보를 수정하는 상황이 생기면 cancelReservation(), reschedule()처럼 실제 의도를 드러내는 메서드로 처리하는 것이 더 좋을 것 같습니다..!
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.
아직, @Transactional과 JPA 같은 ORM을 사용하는 방식 혹은 MyBatis나 JDBC처럼 SQL을 직접 매핑하는 방식 등에 대해서 배우셨을지 모르겠지만, 흔하게 사용되는 JPA 방식에서 알아뒀으면 하는 것들 몇 개를 적어봤어요!
우선, 엔티티를 업데이트하는 방식에는 크게 2가지가 있습니다.
첫 번째는 '변경 감지'를 이용하는 방식입니다. 고은님이 사용하신 것과 같이 setter 함수를 이용하는 것이 여기에 해당합니다. @Transactional 안에서 엔티티를 findById로 조회하면, 이 엔티티는 JPA의 영속성 컨텍스트에서 관리됩니다. 이 객체의 setter나 updateName 같은 메서드를 호출해서 상태를 바꾸면, 트랜잭션이 끝날 때 JPA가 처음 조회했을 때의 스냅샷과 현재 상태가 다른 것을 감지하고 UPDATE 쿼리를 자동으로 날려줍니다. 이 방식은 repository.save()를 다시 호출할 필요가 없는 것이 핵심입니다.
두 번째는 '병합'을 이용하는 방식입니다. 불변 객체 특성을 고려하여 새로운 객체를 생성하여 업데이트하는 방식이 여기에 해당될 수 있습니다. 업데이트할 데이터로 새로운 엔티티 객체를 만들어서 repository.save()에 전달하면, 이 객체는 영속성 컨텍스트가 관리하지 않는 '준영속' 상태이므로 JPA가 merge 로직으로 처리합니다. merge는 DB에서 ID로 실제 데이터를 SELECT한 다음, 우리가 전달한 준영속 객체의 모든 필드 값을 그 조회된 영속 엔티티에 덮어씌우고 업데이트를 진행합니다.
사실 대부분의 경우엔 첫 번째 '변경 감지' 방식이 훨씬 안전하고 권장됩니다. merge 방식은 DTO에서 빠진 필드가 기존 DB에 있던 값까지 null로 덮어씌워 데이터가 유실될 위험이 크거든요!
| @DeleteMapping("/reservations/{id}") | ||
| @ResponseBody | ||
| public ResponseEntity<Void> deleteReservation(@PathVariable Long id) { | ||
| boolean removed = reservations.removeIf(reservation -> reservation.getId().equals(id)); | ||
|
|
||
| if (!removed) { | ||
| throw new InvalidReservationException("존재하지 않는 예약입니다."); | ||
| } | ||
|
|
||
| return ResponseEntity.noContent().build(); | ||
| } |
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.
Void 타입이 아닌 다른 타입이 들어가면 안 되나요? 만약 그렇다면, 그 이유도 같이 설명해주세요!
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.
void가 아닌 다른 타입도 가능한 걸로 알고 있습니다!
하지만 제가 void를 선택한 이유는 코드 목적이 @DeleteMapping 어노테이션을 사용하여 특정 예약을 삭제하는 API이기 때문입니다.
RESTful API 원칙을 보면, 리소스를 성공적으로 삭제했을 때는 클라이언트에게 "삭제가 성공적으로 완료되었고, 반환할 데이터는 없다"고 알려주는 것이 일반적입니다. 즉 void가 아무런 데이터도 담지 않겠다는 것을 명시적으로 잘 보여준다고 생각했습니다!
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.
친절하게 설명해주셔서 감사합니다!
| @ExceptionHandler(IllegalArgumentException.class) | ||
| public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException e) { | ||
| return ResponseEntity.badRequest().body(e.getMessage()); | ||
| } | ||
|
|
||
| @ExceptionHandler(InvalidReservationException.class) | ||
| public ResponseEntity<String> handleInvalidReservationException(InvalidReservationException e) { | ||
| return ResponseEntity.badRequest().body(e.getMessage()); | ||
| } |
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.
-
두 예외 처리 모두
badRequest()방식으로 처리를 해주셨는데,HttpStatus의 다양한 선택 기준이 있었을텐데, 두 예외 모두badRequest를 선택한 이유가 있으실까요? -
두 예외 처리 모두 동일한 로직으로 확인되는데, 커스텀 예외를 따로 했을 때의 이점이 있었나요?
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.
- 이것도 badRequest()는 요구사항에서 Status Code를 400으로 응답하라는 요구사항이 있었기 때문입니다..! 테스트 코드도 400일 때 통과되도록 명시해 두어서 badRequest를 선택했습니다.
- 두 예외처리가 코드상으로는 비슷해 보이지만 예약 도메인 문제와 입력 검증 문제 이 두개를 다르게 분리하고 싶어서 커스텀 예외 처리를 했습니다..! 스터디 시간에
예외 설계는 코드 읽는 사람에게 의도를 전달하는 수단이다.
라고 배워서 커스텀 예외를 하는 것이 이름 그 자체만으로 어떤 규칙을 위반했다는 것을 명확하게 표현하는데 훨씬 이점이 있는 것 같습니다.
|
안녕하세요 리뷰어님! 삭제 방식은 데이터 이력 관리가 필요하지 않아 물리적 삭제를 사용했습니다. 물론, 데이터 이력 추적이나 복구의 중요성이 높은 서비스에서는 논리적 삭제를 쓰는 것이 알맞다고 생각합니다.😊 질문에 대해 자세히 답변 주셔서 이번에도 많은 도움이 되었습니다!! |

안녕하세요 준수님 !! 최근에 리뷰어 리뷰이로 자주 만나뵙는 것 같네요 ㅎㅎ
이번 미션은 3,4 단계 구현하는 것이였고 3단계 - 예약 추가 / 취소, 4단계 - 예외 처리 입니다!
이번 미션부터는 볼륨이 커져서 MVC 에 맞춰 코드 분리를 해 보았습니다
코드 구조에 대해 설명을 드리자면
입니다!
다음은 구현 과정에서 발생한 질문 사항입니다!
제 코드에 대한 질문이 있으시다면 편하게 질문 주시고, 이번 주차 미션도 잘 부탁드립니다!!
++
이전 주차 커밋이랑 섞여 있네요 ㅜㅜ..!!
e453ed6
여기서 부터 확인해 주시면 됩니다!