-
Notifications
You must be signed in to change notification settings - Fork 169
[Spring MVC] 하수한 미션 제출합니다. #509
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
Changes from 6 commits
f4a870c
4837053
538640f
54ff16b
a94f852
acaec61
ea93181
0a9114a
147570b
b28c395
171cc5c
3c2b610
1e8d8bf
5f23995
a0ef501
1619e6e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package roomescape.controller; | ||
|
|
||
| import org.springframework.stereotype.Controller; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
|
|
||
| @Controller | ||
| public class HomeController { | ||
| @GetMapping("/") | ||
| public String home() { | ||
| return "home"; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| package roomescape.controller; | ||
|
|
||
| import java.net.URI; | ||
| import java.util.List; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.stereotype.Controller; | ||
| import org.springframework.web.bind.annotation.DeleteMapping; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.PathVariable; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestBody; | ||
| import org.springframework.web.bind.annotation.ResponseBody; | ||
| import roomescape.dto.ReservationCreateRequest; | ||
| import roomescape.dto.ReservationCreateResponse; | ||
| import roomescape.model.Reservation; | ||
| import roomescape.service.ReservationService; | ||
|
|
||
| @Controller | ||
| public class ReservationController { | ||
| private final ReservationService reservationService = new ReservationService(); | ||
|
|
||
| /* | ||
| * View Mapping | ||
| */ | ||
|
|
||
| @GetMapping("/reservation") | ||
| public String reservation() { | ||
| return "reservation"; | ||
| } | ||
|
|
||
| /* | ||
| * API Mapping | ||
| */ | ||
|
|
||
| @ResponseBody | ||
| @GetMapping("/reservations") | ||
| public List<Reservation> getReservations() { | ||
| return reservationService.getReservations(); | ||
| } | ||
|
|
||
| @ResponseBody | ||
| @PostMapping("/reservations") | ||
| public ResponseEntity<ReservationCreateResponse> createReservation(@RequestBody ReservationCreateRequest request) { | ||
| Reservation reservation = reservationService.createReservation(request); | ||
|
|
||
| URI location = URI.create("/reservations/" + reservation.id()); | ||
| return ResponseEntity.created(location).body(ReservationCreateResponse.from(reservation)); | ||
| } | ||
|
|
||
| @ResponseBody | ||
| @DeleteMapping("/reservations/{id}") | ||
| public ResponseEntity<Void> deleteReservation(@PathVariable int id) { | ||
| reservationService.deleteReservation(id); | ||
|
|
||
| return ResponseEntity.noContent().build(); | ||
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| package roomescape.dto; | ||
|
|
||
| public record ErrorResponse(String message) { } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| package roomescape.dto; | ||
|
|
||
| public record ReservationCreateRequest(String name, String date, String time) { } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package roomescape.dto; | ||
|
|
||
| import roomescape.model.Reservation; | ||
|
|
||
| public record ReservationCreateResponse(int id, String name, String date, String time) { | ||
| public static ReservationCreateResponse from(Reservation reservation) { | ||
| return new ReservationCreateResponse(reservation.id(), reservation.name(), reservation.date(), reservation.time()); | ||
| } | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package roomescape.exception; | ||
|
|
||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.ControllerAdvice; | ||
| import org.springframework.web.bind.annotation.ExceptionHandler; | ||
| import roomescape.dto.ErrorResponse; | ||
|
|
||
| @ControllerAdvice | ||
| public class GlobalExceptionHandler { | ||
| @ExceptionHandler(ReservationNotFoundException.class) | ||
| public ResponseEntity<ErrorResponse> handleReservationNotFoundException(ReservationNotFoundException e) { | ||
| return new ResponseEntity<>(new ErrorResponse(e.getMessage()), HttpStatus.BAD_REQUEST); | ||
| } | ||
|
|
||
| @ExceptionHandler(ReservationValidationException.class) | ||
| public ResponseEntity<ErrorResponse> handleReservationValidationException(ReservationValidationException e) { | ||
| return new ResponseEntity<>(new ErrorResponse(e.getMessage()), HttpStatus.BAD_REQUEST); | ||
| } | ||
|
|
||
| @ExceptionHandler(Exception.class) | ||
| public ResponseEntity<ErrorResponse> handleFallbackException() { | ||
| return new ResponseEntity<>(new ErrorResponse("내부 서버 오류가 발생했습니다."), HttpStatus.INTERNAL_SERVER_ERROR); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package roomescape.exception; | ||
|
|
||
| public class ReservationNotFoundException extends RuntimeException { | ||
| public ReservationNotFoundException(String message) { | ||
| super(message); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package roomescape.exception; | ||
|
|
||
| public class ReservationValidationException extends RuntimeException { | ||
| public ReservationValidationException(String message) { | ||
| super(message); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| package roomescape.model; | ||
|
|
||
| public record Reservation(int id, String name, String date, String time) { } | ||
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| package roomescape.service; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.concurrent.atomic.AtomicInteger; | ||
| import roomescape.dto.ReservationCreateRequest; | ||
| import roomescape.exception.ReservationNotFoundException; | ||
| import roomescape.exception.ReservationValidationException; | ||
| import roomescape.model.Reservation; | ||
|
|
||
| public class ReservationService { | ||
| private final AtomicInteger id; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 좋은 시도입니다. 그런데 ReservationController에서 ReservationService를 사용할 때,
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 문제의 경우 이와 관련해서 혹시 Spring 자체적으로 처리하는 방식이 있는지 궁금하여 찾아보니 171cc5c 커밋에 반영했습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 맞습니다! 정확히는 @service 어노테이션 내부에 있는 https://github.com/component 어노테이션이 주인공입니다. |
||
| private final List<Reservation> reservations; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 동시성을 우려해 AtomicInteger를 쓰셨는데, 그럼 이 List는 동시성에서 안전한 자료구조인가요?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 현재 생성 요청 시 증가되는 ID 값에 대한 동시성 문제만 고려하고 있는데, 말씀해주신대로 실제 Reservation이 배열에 삽입 / 삭제되는 과정에서도 동시성 문제가 발생할 수 있다는 사실을 간과한 것 같습니다. 이와 관련해서 ArrayList 기반 구조를 유지하면서 동시성이 보장되는 자료구조에 대해 찾아보니 크게 두가지가 있었습니다.
두 자료구조 모두 thread-safe하여 동시성이 보장된다는 것은 확인하였지만, 147570b 커밋에 반영했습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 잘 학습해주셨습니다. 대신 Collections.synchronizedList()는 읽기/쓰기 연산에 synchronized를 그냥 쓰기 때문에 병렬성이 없습니다. 따라서 읽기가 많은 작업에서는 데이터베이스의 SERIALIZABLE 격리레벨 만큼이나 느린 성능을 보입니다. 적절하게 쓰는 것이 중요하다는 사실도 기억해주시면 좋을 것 같아요! |
||
|
|
||
| public ReservationService() { | ||
| this.id = new AtomicInteger(0); | ||
| this.reservations = new ArrayList<>(); | ||
|
|
||
| // populateDefaults(); | ||
| } | ||
|
|
||
| public List<Reservation> getReservations() { | ||
| return reservations; | ||
| } | ||
|
|
||
| public Reservation createReservation(ReservationCreateRequest request) { | ||
| if (request.name() == null || request.name().isBlank()) { | ||
|
||
| throw new ReservationValidationException("이름은 공백일 수 없습니다."); | ||
| } | ||
| if (request.date() == null || request.date().isBlank()) { | ||
| throw new ReservationValidationException("날짜는 공백일 수 없습니다."); | ||
| } | ||
| if (request.time() == null || request.time().isBlank()) { | ||
| throw new ReservationValidationException("시간은 공백일 수 없습니다."); | ||
| } | ||
|
|
||
| Reservation reservation = new Reservation(id.incrementAndGet(), request.name(), request.date(), request.time()); | ||
|
||
| reservations.add(reservation); | ||
|
|
||
| return reservation; | ||
| } | ||
|
|
||
| public void deleteReservation(int id) { | ||
| boolean removed = reservations.removeIf((reservation -> reservation.id() == id)); | ||
|
|
||
| if (!removed) { | ||
| throw new ReservationNotFoundException("예약을 찾을 수 없습니다."); | ||
|
||
| } | ||
| } | ||
|
|
||
| private void populateDefaults() { | ||
| reservations.addAll( | ||
| List.of( | ||
| new Reservation(id.incrementAndGet(), "브라운", "2025-01-01", "10:00"), | ||
| new Reservation(id.incrementAndGet(), "브라운", "2025-01-02", "11:00"), | ||
| new Reservation(id.incrementAndGet(), "브라운", "2025-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.
위처럼 사용하는 것과
@RestController를 사용하는 것 중 어느 쪽을 더 선호하시나요? 선호하시는 이유도 궁금합니다!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.
추가로, 이 메서드 그대로
@ResponseBody만 떼면 어떤 일이 일어나나요?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.
@RestController의 경우@Controller에@ResponseBody가 결합된 형태와 같다고 알고있습니다. 보통의 JSON 데이터만을 반환하는 컨트롤러의 경우@RestController를 선호하긴 하나, 현재 구현의 경우 컨트롤러 내부에 view template을 반환하는 로직이 함께 있어 기본 형태는@Controller로 정의하고, JSON 데이터로 반환이 필요한 엔드포인트의 경우@ResponseBody어노테이션을 추가하여 JSON 데이터를 반환할 수 있도록 설정해두었습니다!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.
말씀해주신 상황을 테스트해보니, 현재
ReservationController의 경우@Controller어노테이션이 달려 있어@ResponseBody가 없는 경우 view template을 우선적으로 찾으려고 시도하는것을 확인할 수 있었습니다. 하지만 해당 메소드의 경우 view template를 리턴하지 않기 때문에 다음과 같은 오류가 출력되고 있음을 확인했습니다: