-
Notifications
You must be signed in to change notification settings - Fork 0
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
[1 - 3단계 방탈출 사용자 예약] 커찬(이충안) 미션 제출합니다. #2
base: leegwichan
Are you sure you want to change the base?
[1 - 3단계 방탈출 사용자 예약] 커찬(이충안) 미션 제출합니다. #2
Conversation
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
…를 조회할 수 있다 Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
Co-authored-by: leegwichan <[email protected]>
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.
커찬 하이 👋
내가 잘 모르기도 해서 커찬에게 도움이 될 수 있는 코멘트를 남겼는지는 의문이지만 열심히 남겨보았다우
테스트 관련해서는 아직 작성하지 못한 것 같은데 나중에 꼭 커찬의 테스트 확인해보고 싶네
관련해서 같이 얘기도 나눠보고 싶고 👍
1차 제출 수고했으 👊👊
남은 미션 화이팅!
@DeleteMapping("/{id}") | ||
public ResponseEntity<Void> deleteReservation(@PathVariable long id) { | ||
public ResponseEntity<Void> deleteReservation(@PathVariable Long id) { | ||
service.deleteReservation(id); | ||
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.
id에 해당하는 예약을 삭제하는 요청에서 id를 Wrapper 클래스로 받았네!
널이 들어올 수 있다고 생각한 걸까?
그렇다면 그렇게 생각한 이유가 궁금!
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.
null이 들어올 수 있다기보다는, 모든 곳에서 id의 타입을 Long으로 통일해서 사용해주고 싶었어!
String sql = """ | ||
SELECT theme.id, theme.name, theme.description, theme.thumbnail, COUNT(reservation.theme_id) AS reservation_count | ||
FROM theme | ||
JOIN reservation | ||
ON reservation.theme_id = theme.id | ||
WHERE reservation.date >= ? AND reservation.date <= ? | ||
GROUP BY theme.id, theme.name, theme.description, theme.thumbnail | ||
ORDER BY reservation_count DESC | ||
LIMIT 10 | ||
"""; |
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.
startDate과 endDate을 파라미터로써 동작하도록 할 수 있구나
나는 쿼리안에 모든 내용(일주일, Limit 10)이 들어있어
내 코드에서 맥락을 떼어내는 개선이 필요하다고 커찬의 코드를 통해 알게되었네 땡큐 ㅎ
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.
나도 내 코드에서 10은 빼야 할 듯;;
public record Name(String value) { | ||
public Name { | ||
Objects.requireNonNull(value, "인자 중 null 값이 존재합니다."); | ||
if (value.isEmpty()) { | ||
throw new IllegalArgumentException("이름은 한글자 이상이어야 합니다."); | ||
} | ||
} | ||
} |
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.
아 이정도는 사용해도 되지 않을까? 싶음.
만약에 Name에 로직을 실행하는 public method가 들어있다면, class를 고려해볼 것 같아.
추후 다른 로직이 추가된다면, class로 바꿔줄 것 같아!
public boolean isBefore(LocalDateTime currentDateTime) { | ||
LocalDate currentDate = currentDateTime.toLocalDate(); | ||
if (date.isBefore(currentDate)) { | ||
return true; | ||
} | ||
if (date.isAfter(currentDate)) { | ||
return false; | ||
} | ||
return time.isBefore(currentDateTime.toLocalTime()); | ||
} |
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.
꼼꼼한 처리 굳 👍
public ReservationTime(LocalTime startAt) { | ||
this.id = null; | ||
this.startAt = Objects.requireNonNull(startAt, "인자 중 null 값이 존재합니다."); | ||
} | ||
|
||
public ReservationTime(Long id, LocalTime startAt) { | ||
String errorMessage = "인자 중 null 값이 존재합니다."; | ||
this.id = Objects.requireNonNull(id, errorMessage); | ||
this.startAt = Objects.requireNonNull(startAt, errorMessage); | ||
} |
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.
그러고 싶었지만, 골치 아파지는 코드 떄문에 그럴 수 없었다... 코드 전반적으로 개선해보면서 할 듯!
name VARCHAR(255) NOT NULL, | ||
date VARCHAR(255) NOT NULL, |
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.
date나 time은 varchar로 관리되어도 필요한 경우 변환이 스무스 하더라
하지만 데이터 무결성을 위해서 다른 타입으로 수정해보는 것이 좋을 듯!?
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.
나도 그렇게 생각해. 이번 기회에 형식 바꿔봐야 겠다.
public class LoadController { | ||
public class AdminController { |
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.
AdminController 충분히 좋은 이름일수도 있지만 다른 RestController와 다른 맥락이라는 의미에서 AdminViewController라는 이름은 어때?
@ExceptionHandler({IllegalArgumentException.class, NullPointerException.class}) | ||
public ResponseEntity<ErrorResponse> handleException(RuntimeException e) { | ||
return ResponseEntity.badRequest() | ||
.body(new ErrorResponse(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.
전역적인 ExceptionHandler를 사용하지 않은 이유 궁금!
다른 컨트롤러에서도 똑같이 생긴 코드가 나올 수 밖에 없기에 전역적인 처리가 가질 수 있는 장점을 나는 높게 평가하는 편이야
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.
이거... IllegalArgumentException.class, NullPointerException.class과 같이 공용적인 예외를 사용하는 상황에서는, 각 컨트롤러가 가지고 있는 것이 맞다고 생각했었어. 특정 부분에서는 내가 생각하지도 못하는 상황에서 예외가 터질 수 있으니까.
근데 이정도로 중복되면, 그냥 Advice로 묶는게 좋을 것 같아;;
@GetMapping("/popular") | ||
public ResponseEntity<List<ThemeResponse>> readPopularThemes() { | ||
List<ThemeResponse> response = themeService.readPopularThemes(); | ||
return ResponseEntity.ok(response); | ||
} |
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.
엔드포인트를 다음과 같이 설계한 크루도 있더라
/themes?show-popular=true
커찬은 어떤 생각을 가지고 API 설계했어?
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.
기존에 REST API에 대해서 공부한 적이 있어서, /themes/popular
로 설계했던 것 같아. (리소스를 명시하는 느낌으로)
그런데 정말로 내 엔드포인트가 REST API를 따랐다고는 볼 수 없을 것 같아. 그리고 /themes?show-popular=true
가 그렇게 어색하다고 보기도 힘들 것 같아;; 오늘 수업 떄 말한 브리 말처럼 다른 사이트에서 어떻게 사용하는지 봐야 할 듯;;
@GetMapping("/available") | ||
public ResponseEntity<List<AvailableTimeResponse>> readAvailableTimes(@RequestParam String date, @RequestParam Long themeId) { | ||
List<AvailableTimeResponse> response = service.readAvailableTimes(date, themeId); | ||
return ResponseEntity.ok(response); | ||
} |
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.
엔드포인트 설계에 대해서 궁금
available은 리소스를 의미하기 보다는 필터링 하는 조건을 의미하는 것 같은데
그렇다면 쿼리 파라미터가 좀 더 적절하지 않을까 하는 개인적인 생각!
/times?available=true
new ReservationTime( | ||
resultSet.getLong("time_id"), | ||
resultSet.getString("start_at")) | ||
resultSet.getObject("start_at", LocalTime.class)), |
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.
이런 방법도 있구나!
|
||
public record ReservationResponse(Long id, | ||
String name, | ||
@JsonFormat(pattern = "yyyy-MM-dd") LocalDate 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.
JsonFormat 이 반복해서 등장하는데 전역으로 처리할 수 있는 방법을 찾아보는 것도 좋을 것 같아요!
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.
싫어요.
목적은 각 record 응답마다 어떤 형식으로 변환하는지 명시해주기 위해서 그랬어! (전역으로 처리할 수 있는 방법이 있는지도 몰랐다는 사실을 비밀!)
해당 방법을 찾아보고, 한번 시도해보도록 할께! (근데 시간을 요청, 응답하는 객체가 더 많아진다면 확실이 고려해봐야 할 듯;;)
time_id BIGINT, | ||
theme_id BIGINT, |
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.
요구사항 상 not null이 추가되어야 할 것 같아요!
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.
엥 이거 왜 없지? 구현 끝나고 붙이려 했는데... TODO 써 놓을 걸 ㅠ_ㅠ
@ExceptionHandler({IllegalArgumentException.class, NullPointerException.class}) | ||
public ResponseEntity<ErrorResponse> handleException(RuntimeException e) { | ||
return ResponseEntity.badRequest() | ||
.body(new ErrorResponse(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.
중복된 코드로 보이는데 제거할 수 있을 것 같아요! @ControllerAdvice 등
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.
이거... IllegalArgumentException.class, NullPointerException.class과 같이 공용적인 예외를 사용하는 상황에서는, 각 컨트롤러가 가지고 있는 것이 맞다고 생각했었어. 특정 부분에서는 내가 생각하지도 못하는 상황에서 예외가 터질 수 있으니까.
근데 이정도로 중복되면, 그냥 Advice로 묶는게 좋을 것 같아;;
import org.springframework.boot.test.web.server.LocalServerPort; | ||
|
||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) | ||
class AdminIntegratedTest { |
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.
Service 레이어나 dao 레이어의 테스트틑 일부러 작성하지 않은건가요?
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 id = keyHolder.getKey().longValue(); | ||
return reservation.changeId(id); | ||
Long id = keyHolder.getKey().longValue(); | ||
return readReservationById(id).orElseThrow(); |
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 대신 Long 으로 바꾼 이유가 있나요?
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 값들은 전부 Long
으로 통일하기 위해서!
long
으로 사용하다 보니까 Long -> long 될 때 NullPointerException
이 나는 게 별로였음
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.
Hi 로빈, 리비!
일단은 생각 나는대로 즉흥적으로 답변 달아봤어!
혹시나 의문 사항이 생기면 즉시 연락해 줘!
@ExceptionHandler({IllegalArgumentException.class, NullPointerException.class}) | ||
public ResponseEntity<ErrorResponse> handleException(RuntimeException e) { | ||
return ResponseEntity.badRequest() | ||
.body(new ErrorResponse(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.
이거... IllegalArgumentException.class, NullPointerException.class과 같이 공용적인 예외를 사용하는 상황에서는, 각 컨트롤러가 가지고 있는 것이 맞다고 생각했었어. 특정 부분에서는 내가 생각하지도 못하는 상황에서 예외가 터질 수 있으니까.
근데 이정도로 중복되면, 그냥 Advice로 묶는게 좋을 것 같아;;
@ExceptionHandler({IllegalArgumentException.class, NullPointerException.class}) | ||
public ResponseEntity<ErrorResponse> handleException(RuntimeException e) { | ||
return ResponseEntity.badRequest() | ||
.body(new ErrorResponse(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.
이거... IllegalArgumentException.class, NullPointerException.class과 같이 공용적인 예외를 사용하는 상황에서는, 각 컨트롤러가 가지고 있는 것이 맞다고 생각했었어. 특정 부분에서는 내가 생각하지도 못하는 상황에서 예외가 터질 수 있으니까.
근데 이정도로 중복되면, 그냥 Advice로 묶는게 좋을 것 같아;;
@GetMapping("/popular") | ||
public ResponseEntity<List<ThemeResponse>> readPopularThemes() { | ||
List<ThemeResponse> response = themeService.readPopularThemes(); | ||
return ResponseEntity.ok(response); | ||
} |
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.
기존에 REST API에 대해서 공부한 적이 있어서, /themes/popular
로 설계했던 것 같아. (리소스를 명시하는 느낌으로)
그런데 정말로 내 엔드포인트가 REST API를 따랐다고는 볼 수 없을 것 같아. 그리고 /themes?show-popular=true
가 그렇게 어색하다고 보기도 힘들 것 같아;; 오늘 수업 떄 말한 브리 말처럼 다른 사이트에서 어떻게 사용하는지 봐야 할 듯;;
String sql = """ | ||
SELECT theme.id, theme.name, theme.description, theme.thumbnail, COUNT(reservation.theme_id) AS reservation_count | ||
FROM theme | ||
JOIN reservation | ||
ON reservation.theme_id = theme.id | ||
WHERE reservation.date >= ? AND reservation.date <= ? | ||
GROUP BY theme.id, theme.name, theme.description, theme.thumbnail | ||
ORDER BY reservation_count DESC | ||
LIMIT 10 | ||
"""; |
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.
나도 내 코드에서 10은 빼야 할 듯;;
public record Name(String value) { | ||
public Name { | ||
Objects.requireNonNull(value, "인자 중 null 값이 존재합니다."); | ||
if (value.isEmpty()) { | ||
throw new IllegalArgumentException("이름은 한글자 이상이어야 합니다."); | ||
} | ||
} | ||
} |
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.
아 이정도는 사용해도 되지 않을까? 싶음.
만약에 Name에 로직을 실행하는 public method가 들어있다면, class를 고려해볼 것 같아.
추후 다른 로직이 추가된다면, class로 바꿔줄 것 같아!
|
||
public record ReservationResponse(Long id, | ||
String name, | ||
@JsonFormat(pattern = "yyyy-MM-dd") LocalDate 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.
싫어요.
목적은 각 record 응답마다 어떤 형식으로 변환하는지 명시해주기 위해서 그랬어! (전역으로 처리할 수 있는 방법이 있는지도 몰랐다는 사실을 비밀!)
해당 방법을 찾아보고, 한번 시도해보도록 할께! (근데 시간을 요청, 응답하는 객체가 더 많아진다면 확실이 고려해봐야 할 듯;;)
LocalDate currentDate = LocalDate.now(); | ||
LocalDate startDate = currentDate.minusDays(7); | ||
LocalDate endDate = currentDate.minusDays(1); |
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.
안그래도 해당 피드백을 리뷰어한테 받았어! 바쁘게 구현하느라 미처 생각 못함;;
상수 추출 해보도록 할께!
name VARCHAR(255) NOT NULL, | ||
date VARCHAR(255) NOT NULL, |
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.
나도 그렇게 생각해. 이번 기회에 형식 바꿔봐야 겠다.
time_id BIGINT, | ||
theme_id BIGINT, |
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.
엥 이거 왜 없지? 구현 끝나고 붙이려 했는데... TODO 써 놓을 걸 ㅠ_ㅠ
import org.springframework.boot.test.web.server.LocalServerPort; | ||
|
||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) | ||
class AdminIntegratedTest { |
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일동안 정신없이 하다 보니, 아직 생각하지 못한 부분도 많네요;;
요구사항을 만족하는 것도 시간에 쫓기면서 하다 보니, 부족한 점이 많을 수 있습니다. 리뷰 부탁드리겠습니다!
아래에는 제 Spring 경험, 신경 쓴 점과 신경 쓰지 못한 점을 생각나는 대로 정리해 보았습니다!
리뷰 링크
초기 코드를 제외한 변경 사항 PR 입니다!
이전 Spring 경험
신경 쓴 점
유효성 검사 위치
도메인을 최대한 안전하게 사용하기 위해서, 모든 유효성 검사를 도메인 객체에 위치시켰습니다. 처음에는 null 값이 들어오는 것 등은 DTO에 있어도 되지 않을까 생각했지만, 페어의 말에 설득 당해(?) 도메인에 위치시켰습니다. 이를 통해 도메인을 더 안전하게 사용할 수 있었습니다!
신경쓰지 못한 점
각 계층별 테스트
Service, Repository를 각각 테스트 해보려고 했었는데, 어떻게 테스트할지 고민하다가 구현 이후로 미뤘습니다.시간이 부족해 테스트를 해보지 못했습니다. 코멘트 이후에 한 번 해보도록 하겠습니다!