Skip to content
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 Core] 안금서 미션 제출합니다. #386

Open
wants to merge 90 commits into
base: goldm0ng
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 63 commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
d888ea0
gradle 의존성 추가
goldm0ng Nov 6, 2024
7adbc1c
<Add> 어드민 페이지 응답 구현
goldm0ng Nov 6, 2024
a7e7b1c
<Add> 핵심 도메인 Reservation 추가
goldm0ng Nov 6, 2024
5ffe0d1
<Add> 컨트롤러 ReservationController 추가
goldm0ng Nov 6, 2024
3fce7dd
<Add> 필수 예약 데이터 누락 관련 예외 MissingReservationDataException 추가
goldm0ng Nov 6, 2024
4e5b6c3
<Add> 예약 찾기 관련 예외 NotFoundReservationException 추가
goldm0ng Nov 6, 2024
45a3092
<Add> 에외 관리 관련 GlobalExceptionHandler 추가
goldm0ng Nov 6, 2024
87cd948
<Add> 미션 단계별 테스트 추가
goldm0ng Nov 6, 2024
f2ceebe
<FIX> controller pakage 추가 및 ReservationController 수정
goldm0ng Nov 13, 2024
0ce07cb
<FIX> domain package 추가 및 도메인 클래스 수정
goldm0ng Nov 13, 2024
b80921f
<ADD> dto package 추가 및 예약 관련 DTO 추가
goldm0ng Nov 13, 2024
b5096b7
<DELETE> 유효성 검사 방법 변경에 따른 사용자 정의 예외 클래스 삭제 및 Handler 재구성
goldm0ng Nov 13, 2024
272e633
<ADD> dto 내에서 유효성 검사를 하기 위한 의존성 추가
goldm0ng Nov 13, 2024
e3ce304
<FIX> 외부에서 예외메세지를 전달하는 방식에서 기본 예외 메세지를 가지고 있는 방식으로 변경
goldm0ng Nov 13, 2024
2855044
<ADD> 예외처리 방식 일부 변경에 따른 Reservation에 관한 예외핸들러 추가
goldm0ng Nov 13, 2024
4c61b9a
-
goldm0ng Nov 13, 2024
3262ef2
<FIX> 도메인 필드 내 final 삭제
goldm0ng Nov 13, 2024
8254291
<FIX> Dto 생성자 추가
goldm0ng Nov 13, 2024
b747795
<ADD> 예약 관련 Repository 인터페이스 추가
goldm0ng Nov 13, 2024
19e40f1
<ADD> 메모리 기반 데이터 관리 구현체 추가
goldm0ng Nov 13, 2024
2bf90d0
<ADD> 예약 비즈니스 관련 Service 추가
goldm0ng Nov 13, 2024
51b6b55
<FIX> repository, service 추가에 따른 controller 재구성
goldm0ng Nov 13, 2024
51aadd1
<ADD> Spring jdbc Starter 의존성 추가
goldm0ng Nov 13, 2024
9ee624c
<ADD> DB 설정 추가
goldm0ng Nov 13, 2024
9d1d2a0
<ADD> 테이블 스키마 정의
goldm0ng Nov 13, 2024
8a1f8cd
<FIX> 1,2,3,4 단계 테스트 네이밍 수정
goldm0ng Nov 13, 2024
2a9ee79
<ADD> 5,6,7 단계 테스트 추가
goldm0ng Nov 13, 2024
3012b26
<FIX> 도메인 기본 생성자 추가
goldm0ng Nov 13, 2024
f0156fa
<FIX> 메모리 기반 데이터 저장소에서 @Repository 삭제
goldm0ng Nov 13, 2024
e72a1d9
<ADD> h2 기반 데이터 관리 용도의 Repository 구현체 추가
goldm0ng Nov 13, 2024
4b98968
<FIX> 미션 흐름에 따른 비즈니스 로직 메서드 순서 바꾸기
goldm0ng Nov 13, 2024
8b552a6
<FIX> DTO->Entity 변환 위치 및 로직 수정
goldm0ng Nov 19, 2024
98eeee3
<FIX> Controller와 RestController 분리
goldm0ng Nov 19, 2024
a293899
<FIX> 클래스 범위 수정, 예외 처리 핸들러 추가 및 패키지 구조 조정
goldm0ng Nov 19, 2024
ee1ec84
<ADD> 페이지 렌더링하는 뷰 컨트롤러에 대한 예외처리 분리
goldm0ng Nov 19, 2024
edf05cd
<FIX> 레이어드 아키텍처에 기반한 패키지 구조 조정
goldm0ng Nov 19, 2024
927c74c
<FIX> DTO->Entity 변환 위치 및 로직 수정
goldm0ng Nov 19, 2024
7be63ff
<FIX> Controller와 RestController 분리
goldm0ng Nov 19, 2024
552f755
<FIX> 클래스 범위 수정, 예외 처리 핸들러 추가 및 패키지 구조 조정
goldm0ng Nov 19, 2024
8ede9a7
<ADD> 페이지 렌더링하는 뷰 컨트롤러에 대한 예외처리 분리
goldm0ng Nov 19, 2024
d6d1e9d
<FIX> 레이어드 아키텍처에 기반한 패키지 구조 조정
goldm0ng Nov 19, 2024
fe2f464
<FIX> DTO -> Entity 변환 위치 변경에 따른 수정
goldm0ng Nov 22, 2024
f5d8642
<FIX> 패키지 구조 변경에 따른 수정
goldm0ng Nov 22, 2024
5f71d55
<FIX> DTO를 record 타입으로 변경
goldm0ng Nov 22, 2024
55a2797
<FIX> 자동 정렬
goldm0ng Nov 22, 2024
795a778
<FIX> 메서드 체이닝 적용
goldm0ng Nov 22, 2024
08fa6f4
<FIX> 예외처리 위치 변경에 따른 수정
goldm0ng Nov 22, 2024
4151de6
<FIX> 패키지 구조 수정
goldm0ng Nov 26, 2024
f42cf40
<ADD> Time Table 추가
goldm0ng Nov 26, 2024
1927a32
<FIX> 패키지 구조 조정에 따른 import 수정 사항
goldm0ng Nov 26, 2024
38998be
<ADD> 시간 관리 페이지 뷰 템플릿과 매핑 추가
goldm0ng Nov 26, 2024
c1a3e00
<ADD> 도메인 객체 Time 및 데이터 전송 객체 TimeDto 추가
goldm0ng Nov 26, 2024
d6f862d
<ADD> 인터페이스 TimeRepository 및 Jdbc 활용 구현체 JdbcTimeRepository 추가
goldm0ng Nov 26, 2024
2f0a216
<ADD> 비즈니스 로직을 수행하는 TimeService 추가
goldm0ng Nov 26, 2024
8db1972
<ADD> 시간 추가/조회/삭제 API를 담당하는 TimeController 추가
goldm0ng Nov 26, 2024
a2b40fc
<ADD> 설정한 시간을 찾지 못 했을 때 발생하는 사용자 정의 예외 추가
goldm0ng Nov 26, 2024
d1aa20d
<ADD> TimeController에서 발생하는 모든 예외를 처리하는 예외 핸들러 추가
goldm0ng Nov 26, 2024
a9c87d7
<ADD> 8단계 테스트 추가
goldm0ng Nov 26, 2024
47eabd6
<FIX> 에약 페이지 파일 수정 (templates/new-reservation.html)
goldm0ng Nov 26, 2024
4f8ec4e
<FIX> 테이블 스키마 재정의
goldm0ng Nov 26, 2024
3f36dff
<FIX> 테이블 스키마 재정의 수정
goldm0ng Nov 27, 2024
6f25741
<FIX> 예약 클래스 멤버 변수 time의 타입 String -> Time 으로 수정
goldm0ng Nov 27, 2024
8d0ac6b
<FIX> 예약 데이터 전송 객체 변수 time 타입 String -> Long 으로 수정
goldm0ng Nov 27, 2024
cff2f53
<FIX> 스키마 재정의에 따른 에약 관련 Repository 수정
goldm0ng Nov 27, 2024
0efc638
<FIX> 도메인 객체의 멤버 변수 타입 변경에 따른 Dto -> Entity 변환 로직 수정
goldm0ng Nov 27, 2024
50eb526
<FIX> 자동정렬 적용
goldm0ng Nov 27, 2024
1a5e85c
<ADD> 잘못된 타입으로 값이 들어왔을 때 발생하는 예외처리 추가 - 400 error
goldm0ng Nov 27, 2024
8da0faf
<ADD> 9단계, 10단계 테스트 코드 추가
goldm0ng Nov 27, 2024
b599e17
<FIX> 기존 예외 핸들러에서 공통 예외 분리
goldm0ng Dec 5, 2024
033d792
<FIX> 예약 시간 관련 도메인 및 Dto 이름 변경 (Time -> ReservationTime)
goldm0ng Dec 5, 2024
f7080fd
<DELETE> -
goldm0ng Dec 5, 2024
6a5885b
<ADD> Spring jdbc Starter 의존성 추가
goldm0ng Nov 13, 2024
5712acf
<ADD> DB 설정 추가
goldm0ng Nov 13, 2024
5208497
<ADD> 테이블 스키마 정의
goldm0ng Nov 13, 2024
bbc0aa7
<FIX> 1,2,3,4 단계 테스트 네이밍 수정
goldm0ng Nov 13, 2024
92555ec
<ADD> 5,6,7 단계 테스트 추가
goldm0ng Nov 13, 2024
3157680
<FIX> 도메인 기본 생성자 추가
goldm0ng Nov 13, 2024
03b7b9e
<FIX> 메모리 기반 데이터 저장소에서 @Repository 삭제
goldm0ng Nov 13, 2024
73a68e4
<ADD> h2 기반 데이터 관리 용도의 Repository 구현체 추가
goldm0ng Nov 13, 2024
d3bda44
<FIX> 미션 흐름에 따른 비즈니스 로직 메서드 순서 바꾸기
goldm0ng Nov 13, 2024
0cfa4d4
<FIX> DTO->Entity 변환 위치 및 로직 수정
goldm0ng Nov 19, 2024
1379add
<FIX> 레이어드 아키텍처에 기반한 패키지 구조 조정
goldm0ng Nov 19, 2024
50f9f7d
<FIX> DTO -> Entity 변환 위치 변경에 따른 수정
goldm0ng Nov 22, 2024
7f41fe9
<FIX> 패키지 구조 변경에 따른 수정
goldm0ng Nov 22, 2024
46d524d
<FIX> DTO를 record 타입으로 변경
goldm0ng Nov 22, 2024
66148ce
<FIX> 자동 정렬
goldm0ng Nov 22, 2024
fb00e38
<FIX> 메서드 체이닝 적용
goldm0ng Nov 22, 2024
f29f0c6
<FIX> 예외처리 위치 변경에 따른 수정
goldm0ng Nov 22, 2024
e739292
Merge branch 'goldmong-jdbc' into goldmong-core
goldm0ng Jan 14, 2025
3b1e132
<FIX> lombok 버전 수정
goldm0ng Jan 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,18 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:5.3.1'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
compileOnly 'org.projectlombok:lombok:1.18.28'
annotationProcessor 'org.projectlombok:lombok:1.18.28'

implementation 'org.springframework.boot:spring-boot-starter-validation'

implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
}

test {
useJUnitPlatform()
}

21 changes: 21 additions & 0 deletions src/main/java/roomescape/MainPageController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package roomescape;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class MainPageController {

@GetMapping("/")
public String showHomePage() {
return "home";
}

@GetMapping("/reservation")
public String showReservationForm() {
return "new-reservation";
}

@GetMapping("/time")
public String showTimeForm() { return "time"; }
}
1 change: 0 additions & 1 deletion src/main/java/roomescape/RoomescapeApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@ public class RoomescapeApplication {
public static void main(String[] args) {
SpringApplication.run(RoomescapeApplication.class, args);
}

}
42 changes: 42 additions & 0 deletions src/main/java/roomescape/business/ReservationService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package roomescape.business;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import roomescape.domain.Reservation;
import roomescape.domain.Time;
import roomescape.persistence.JdbcReservationRepository;
import roomescape.persistence.JdbcTimeRepository;
import roomescape.presentation.dto.ReservationDto;

import java.util.List;

@Service
@RequiredArgsConstructor
public class ReservationService {

private final JdbcReservationRepository reservationRepository;
private final JdbcTimeRepository timeRepository;
Copy link

Choose a reason for hiding this comment

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

ReservationRepository와 TimeRepository에 대한 인터페이스를 만드셔서 추상화를 해주셨던데, ReservationService 및 TimeService에서는 추상화 한 객체들에 의존하지 않고 구체 클래스에 의존하고 있네요. 어떤 이유에서 이런 선택을 해주셨나요?

Copy link
Author

Choose a reason for hiding this comment

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

예리한 지적이시네요 코코닭!

Spring MVC 미션에서는 메모리를 기반으로 repository를 쓰다가 Spring Jdbc로 넘어가면서 데이터베이스를 기반으로 repositroy를 쓰는 방식을 변경했기 때문에 변경에 유연하게 대처하기 위한 방법 중 하나로 인터페이스를 두는 것을 생각했습니다!

하지만,,, 그걸 service에 적용해볼 생각은 못 해봤네요.. service 같은 경우에는 현재 미션으로 봐선 따로 인터페이스를 둘 필요가 있나 싶은데, 코코닭님은 어떻게 생각하시나요? 나중에 프로젝트 규모가 커지고 비즈니스 로직이 복잡해진다면 인터페이스를 두어야 할 상황이 생기기도 하나요? 궁금합니다!

Copy link

Choose a reason for hiding this comment

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

소통의 오류가 있던 것 같아요. 제가 의미했던 바는 아래와 같아요.

AS-IS

private final JdbcReservationRepository reservationRepository;
private final JdbcTimeRepository timeRepository;

TO-BE

private final ReservationRepository reservationRepository;
private final TimeRepository timeRepository;

Repository를 추상화했다면, Service에서 의존하고 있는 Repository는 구체 클래스보다는 추상화한 클래스가 되는 편이 좋을 것 같습니다. 그래야 다형성을 이용해볼 수도 있을 것 같아요~!

Copy link
Author

Choose a reason for hiding this comment

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

아하 그런 의미였군요! 이해 했습니다!!!!! 반영하도록 할게요 😃


public Reservation addReservation(ReservationDto reservationDto) {
Reservation reservation = convertToReservationEntity(reservationDto);
return reservationRepository.save(reservation);
}

public List<Reservation> checkReservations() {
return reservationRepository.findAll();
}

public void deleteReservation(Long reservationId) {
reservationRepository.delete(reservationId);
}

private Reservation convertToReservationEntity(ReservationDto reservationDto) {
Time time = convertToTimeEntity(reservationDto);
return new Reservation(null, reservationDto.name(), reservationDto.date(), time);
}

private Time convertToTimeEntity(ReservationDto reservationDto) {
Long timeId = reservationDto.time();
return timeRepository.findById(timeId);
}
}
34 changes: 34 additions & 0 deletions src/main/java/roomescape/business/TimeService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package roomescape.business;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import roomescape.domain.Time;
import roomescape.persistence.JdbcTimeRepository;
import roomescape.presentation.dto.TimeDto;

import java.util.List;

@Service
@RequiredArgsConstructor
public class TimeService {

private final JdbcTimeRepository repository;

public Time addTime(TimeDto timeDto) {
Time time = convertToEntity(timeDto);
return repository.save(time);
}

public List<Time> checkTimes() {
return repository.findAll();
}

public void deleteTime(Long timeId) {
repository.delete(timeId);
}

private Time convertToEntity(TimeDto timeDto) {
return new Time(null, timeDto.time());
}

}
26 changes: 26 additions & 0 deletions src/main/java/roomescape/domain/Reservation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package roomescape.domain;

import lombok.Getter;

@Getter
public class Reservation {

private Long id;
private String name;
private String date;
private Time time;

public Reservation() {
}

public Reservation(Long id, String name, String date, Time time) {
this.id = id;
this.name = name;
this.date = date;
this.time = time;
}

public void setId(Long id) {
this.id = id;
}
}
18 changes: 18 additions & 0 deletions src/main/java/roomescape/domain/Time.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package roomescape.domain;

import lombok.Getter;

@Getter
public class Time {
Copy link

Choose a reason for hiding this comment

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

데이터베이스의 스키마의 이름은 time 이지만, 도메인의 이름이 스키마의 이름을 꼭 따라가야할까요?

도메인은 비즈니스를 정의하는 영역이죠. 데이터베이스와는 별개의 영역이에요. 조금 더 비즈니스 적으로 직관적인 이름을 가지도록 해보면 어떨까요?

이를테면 ReservationTime 이라는 이름을 지어볼 수 있을 것 같아요. Time이라는 도메인 명은, 이 도메인이 어느 비즈니스를 위해 사용되는지 모호한 이름일 수 있어요.

Copy link
Author

Choose a reason for hiding this comment

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

그러네요,,
코코닭님이 스터디 때 언급하셨던 것처럼,
시간이라는 게 예약 시간일 수도 있고, 예약 시작 시간 혹은 끝나는 시간일 수도 있고 등등 ... 시간에 관련된 객체들이 만들어질수록 구현하는 데 있어 혼란을 야기할 것 같습니다. 네임을 더 구체적이고 명확하게 표현하는 습관을 들이겠습니다! 수정하겠니다~!


private Long id;
private String time;
Copy link

Choose a reason for hiding this comment

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

시간을 다루는 객체에서, 가장 중요한 필드 값의 자료형이 적절하지 않다는 생각이 들어요!

도메인 객체가 만들어질 때, 그 도메인의 특징에 맞는 데이터가 들어갈 수 있도록, 데이터 무결성을 보장해야 한다고 생각해요.

현재는 time 값에 대한 검증이 없는 상태이기 때문에, Time 객체가 시간을 나타내는 객체로서 유효하지 않을 수 있겠다는 생각이 듭니다.

시간을 표현할 때 String보다 더 적절한 자료형이 있을지 고민해보면 좋겠어요.


public Time() {
}

public Time(Long id, String time) {
this.id = id;
this.time = time;
}
}
16 changes: 16 additions & 0 deletions src/main/java/roomescape/exception/MainPageExceptionHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package roomescape.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import roomescape.MainPageController;

@Slf4j
@ControllerAdvice(assignableTypes = MainPageController.class)
public class MainPageExceptionHandler {
@ExceptionHandler(Exception.class)
public String handleException(Exception e) {
log.error("error: " + e.getMessage());
return "error/500"; //view 렌더링 페이지는 만들지 않음!
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package roomescape.exception;

public class NotFoundReservationException extends RuntimeException {

private static final String NOT_FOUND_RESERVATION_MESSAGE = "예악을 찾을 수 없습니다.";

public NotFoundReservationException() {
super(NOT_FOUND_RESERVATION_MESSAGE);
}
}
10 changes: 10 additions & 0 deletions src/main/java/roomescape/exception/NotFoundTimeException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package roomescape.exception;

public class NotFoundTimeException extends RuntimeException {

private static final String NOT_FOUND_TIME_MESSAGE = "시간을 찾을 수 없습니다.";

public NotFoundTimeException(){
super(NOT_FOUND_TIME_MESSAGE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package roomescape.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import roomescape.presentation.ReservationController;

import java.util.HashMap;
import java.util.Map;

@Slf4j
@ControllerAdvice(assignableTypes = ReservationController.class)
public class ReservationExceptionHandler {

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException e) {
Map<String, String> errors = new HashMap<>();

for (FieldError error : e.getBindingResult().getFieldErrors()) {
errors.put(error.getField(), error.getDefaultMessage());
log.info("validation error on field {} : {}", error.getField(), error.getDefaultMessage());
}

return ResponseEntity.badRequest().body(errors);
}

@ExceptionHandler(NotFoundReservationException.class)
public ResponseEntity<String> handleNotFoundReservationException(NotFoundReservationException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}

@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<String> handleInvalidTypeException(HttpMessageNotReadableException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
Copy link

Choose a reason for hiding this comment

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

PR 본문에도 남겨뒀지만, 제 생각에는 HttpMessageNotReadableException 예외를 잡아서 400 응답을 내려주는 것이 자연스러운 행위라고 생각해요.

여기서 드는 한 가지 궁금증은, 이 예외를 Reservation에 대해서만 잡아도 괜찮을까? 인 것 같네요. TimeController에서도 같은 시나리오로 이 예외가 발생할 여지가 있어보여요. 어떻게 처리하면 좋을까요?

Copy link
Author

Choose a reason for hiding this comment

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

저도 코코닭님의 생각에 동의합니다!

Time의 경우 시간을 직접 키보드로 입력받지 않고 주어진 보기로부터 마우스로 선택하는 형태로 입력을 받다보니, 잘못된 타입의 값이 들어올 거라고는 생각을 못해서 처리를 안 해줬던 것 같네요. time과 date 같이, 주어진 보기를 마우스로 클릭하는 형태로 받는 것은 클라이언트 코드에 달려있는 것이겠죠? 그렇다면 잘못된 값이 들어올 수 있는 경우가 있을까요? 궁금해서 여쭤봅니다!

Copy link

Choose a reason for hiding this comment

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

좋은 질문을 남겨주셨었네요!

일단 제 생각은, 서버 개발자는 API 관점에서 이를 바라보는 게 좋을 것 같아요.

서버 개발자의 입장에서 클라이언트는 API를 사용하는 대상일 거예요. 이때 클라이언트는 아래와 같을 수 있어요.

  1. API를 사용하는 프론트 개발자
  2. API를 사용하는 제3자

첫 번째의 경우, 프론트 개발자의 설계대로(마우스로 클릭하는 형태)만 사용자가 움직여준다면 예외가 발생하지 않을 수 있겠네요. 다만, 프론트의 설계를 기반으로 백엔드 설계를 하게 된다면, 설계가 프론트의 구조에 의존하게 되면서 변화에 취약해질 수 있어요. 이를테면, 마우스로 클릭하는 형태에서 키보드로 입력하는 형태로 바뀐다면 이에 맞추어서 예외 처리 코드를 새로 만들어 주어야 될 거예요.

두 번째의 경우, 우리가 만든 API가 어떻게 사용될 지 예측할 수 없어요. 악의적으로 API 요청을 보내는 경우도 포함될 수 있구요.

이러한 것들을 모두 고려해봤을 때, 저는 서버 관점에서만 바라보았을 때 발생할 수 있는 모든 예외는 마땅히 처리되는 편이 좋다고 생각합니다~!


@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception e) {
return ResponseEntity.internalServerError().body(e.getMessage());
}
}
39 changes: 39 additions & 0 deletions src/main/java/roomescape/exception/TimeExceptionHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package roomescape.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import roomescape.presentation.TimeController;

import java.util.HashMap;
import java.util.Map;

@Slf4j
@ControllerAdvice(assignableTypes = TimeController.class)
public class TimeExceptionHandler {
Copy link

Choose a reason for hiding this comment

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

TimeExceptionHandler의 내용이 ReservationExceptionHandler와 중복적으로 겹치는 부분들이 보이네요. 왜 겹칠까요?

예를 들어, MethodArgumentNotValidException 같은 예외는 Time만의 예외인가요? TimeExceptionHandler 라는 영역에서 반드시 처리되어야 하는 예외인가요?

ReservationExceptionHandler와 TimeExceptionHandler와 같이 도메인 별로 예외 처리를 나누었을 때, 내부적으로 각각 어떤 예외를 처리하는 것이 가장 효과적일지 고민해보면 좋을 것 같아요.

Copy link
Author

@goldm0ng goldm0ng Dec 1, 2024

Choose a reason for hiding this comment

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

엇 좋은 지적 감사합니다 코코닭님!
각각 도메인만의 예외처리에만 집중해서 코드를 추가하다보니, 위 코코닭님이 말씀하신 부분에 대해서는 고려해보지 못했네요.

MethodArgumentNotValidException예외는 Dto 객체에 달린 @NotBlank 와 같은 어노테이션을 통한 유효성 검증에 실패했을 때 발생하는 예외인데요! 현재 코드에는 ReservationController의 범위에서 발생한 예외를 처리하는 ReservationExceptionHandler와, TimeController의 범위에서 발생한 예외를 처리하는 TimeExceptionHandler 내에 중복 코드로 존재합니다. MethodArgumentNotValidException예외 뿐만 아니라, HttpMessageNotReadableException예외, Exception예외도 마찬가지죠. 코코닭의 리뷰를 보고 각 핸들러 내에 있는 코드 중복은 왜 존재할까? 에 대해 생각해보았고, 결국 "공통적으로" 처리해야 하는 예외라는 결론을 지었어요!

NotFoundReservation, NotFoundTimeReservation_ 예외 같은 경우, 커스텀 예외이므로 공통으로 처리하기 보단, 각 도메인에 대한 범위를 지정하여 예외를 처리하는 것이 좋은 방법 같아요. 만약, 커스텀 예외가 아니라 NoSuchElementException예외나, EmptyResultDataAccessException 와 같은 이미 존재하는 예외로 처리했을 경우에는 지금처럼 각 도메인 관련 예외 핸들러를 만들지 않고 공통 예외로 다 뺐을 수도 있겠네요.

앞으로 예외처리를 할 때에는 예외 범위에 대해 잘 생각해서 공통 예외와 도메인별 예외를 잘 구별해야겠네요!


@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException e) {
Map<String, String> errors = new HashMap<>();

for (FieldError error : e.getBindingResult().getFieldErrors()) {
errors.put(error.getField(), error.getDefaultMessage());
log.info("validation error on field {} : {}", error.getField(), error.getDefaultMessage());
}

return ResponseEntity.badRequest().body(errors);
}

@ExceptionHandler(NotFoundTimeException.class)
public ResponseEntity<String> handleNotFoundTimeException(NotFoundTimeException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}

@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception e) {
return ResponseEntity.internalServerError().body(e.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package roomescape.persistence;

import lombok.RequiredArgsConstructor;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;
import roomescape.domain.Reservation;
import roomescape.domain.Time;
import roomescape.exception.NotFoundReservationException;

import java.sql.PreparedStatement;
import java.util.List;

@Repository
@RequiredArgsConstructor
public class JdbcReservationRepository implements ReservationRepository {

private final JdbcTemplate jdbcTemplate;

@Override
public Reservation save(Reservation reservation) {
String sql = "insert into reservation (name, date, time_id) values (?,?,?)";

KeyHolder keyHolder = new GeneratedKeyHolder();

jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"});
ps.setString(1, reservation.getName());
ps.setString(2, reservation.getDate());
ps.setLong(3, reservation.getTime().getId());
return ps;
}, keyHolder);

Long generatedAutoId = keyHolder.getKey().longValue();
return new Reservation(generatedAutoId, reservation.getName(), reservation.getDate(), reservation.getTime());
}

@Override
public Reservation findById(Long reservationId) {
String sql = "select id, name, date, time_id from reservation where id = ?";

try {
return jdbcTemplate.queryForObject(sql, reservationMapperForFindById(), reservationId);
} catch (EmptyResultDataAccessException e) {
throw new NotFoundReservationException();
}
}

@Override
public List<Reservation> findAll() {
String sql = "select \n" +
" r.id as reservation_id, \n" +
" r.name, \n" +
" r.date, \n" +
" t.id as time_id, \n" +
" t.time as time_value \n" +
"from reservation as r inner join time as t on r.time_id = t.id";

return jdbcTemplate.query(sql, reservationMapperForFindAll());
}

@Override
public void delete(Long reservationId) {
Reservation deletedReservation = this.findById(reservationId);

String sql = "delete from reservation where id = ?";
jdbcTemplate.update(sql, deletedReservation.getId());
}

private RowMapper<Reservation> reservationMapperForFindById() {
return ((rs, rowNum) -> {

Time time = new Time(rs.getLong("time_id"), null);

return new Reservation(
rs.getLong("id"),
rs.getString("name"),
rs.getString("date"),
time);
});
}

private RowMapper<Reservation> reservationMapperForFindAll() {
return ((rs, rowNum) -> {

Time time = new Time(rs.getLong("time_id"), rs.getString("time_value"));

return new Reservation(
rs.getLong("id"),
rs.getString("name"),
rs.getString("date"),
time
);
});
}
}
Loading