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

[1 - 3단계 방탈출 사용자 예약] 커찬(이충안) 미션 제출합니다. #2

Open
wants to merge 49 commits into
base: leegwichan
Choose a base branch
from

Conversation

leegwichan
Copy link
Member

@leegwichan leegwichan commented May 2, 2024

주어진 3일동안 정신없이 하다 보니, 아직 생각하지 못한 부분도 많네요;;
요구사항을 만족하는 것도 시간에 쫓기면서 하다 보니, 부족한 점이 많을 수 있습니다. 리뷰 부탁드리겠습니다!

아래에는 제 Spring 경험, 신경 쓴 점과 신경 쓰지 못한 점을 생각나는 대로 정리해 보았습니다!

리뷰 링크

초기 코드를 제외한 변경 사항 PR 입니다!

이전 Spring 경험

  • 6개월 동안 Java/Spring을 배우는 부트캠프를 수료한 적이 있습니다. (2022.02 ~ 2022.08)
    • 부트캠프 기간 중에 Spring을 이용하여 서비스를 배포해 본 경험이 있습니다.

신경 쓴 점

유효성 검사 위치

도메인을 최대한 안전하게 사용하기 위해서, 모든 유효성 검사를 도메인 객체에 위치시켰습니다. 처음에는 null 값이 들어오는 것 등은 DTO에 있어도 되지 않을까 생각했지만, 페어의 말에 설득 당해(?) 도메인에 위치시켰습니다. 이를 통해 도메인을 더 안전하게 사용할 수 있었습니다!

신경쓰지 못한 점

각 계층별 테스트

Service, Repository를 각각 테스트 해보려고 했었는데, 어떻게 테스트할지 고민하다가 구현 이후로 미뤘습니다.시간이 부족해 테스트를 해보지 못했습니다. 코멘트 이후에 한 번 해보도록 하겠습니다!

kyum-q and others added 30 commits April 30, 2024 13:36
kyum-q and others added 19 commits May 1, 2024 18:27
Copy link
Member

@Libienz Libienz left a comment

Choose a reason for hiding this comment

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

커찬 하이 👋

내가 잘 모르기도 해서 커찬에게 도움이 될 수 있는 코멘트를 남겼는지는 의문이지만 열심히 남겨보았다우

테스트 관련해서는 아직 작성하지 못한 것 같은데 나중에 꼭 커찬의 테스트 확인해보고 싶네
관련해서 같이 얘기도 나눠보고 싶고 👍

1차 제출 수고했으 👊👊
남은 미션 화이팅!

Comment on lines 38 to 42
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteReservation(@PathVariable long id) {
public ResponseEntity<Void> deleteReservation(@PathVariable Long id) {
service.deleteReservation(id);
return ResponseEntity.noContent().build();
}
Copy link
Member

Choose a reason for hiding this comment

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

id에 해당하는 예약을 삭제하는 요청에서 id를 Wrapper 클래스로 받았네!

널이 들어올 수 있다고 생각한 걸까?
그렇다면 그렇게 생각한 이유가 궁금!

Copy link
Member Author

Choose a reason for hiding this comment

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

null이 들어올 수 있다기보다는, 모든 곳에서 id의 타입을 Long으로 통일해서 사용해주고 싶었어!

Comment on lines +48 to +57
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
""";
Copy link
Member

Choose a reason for hiding this comment

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

startDate과 endDate을 파라미터로써 동작하도록 할 수 있구나

나는 쿼리안에 모든 내용(일주일, Limit 10)이 들어있어

내 코드에서 맥락을 떼어내는 개선이 필요하다고 커찬의 코드를 통해 알게되었네 땡큐 ㅎ

Copy link
Member Author

Choose a reason for hiding this comment

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

나도 내 코드에서 10은 빼야 할 듯;;

Comment on lines +5 to +12
public record Name(String value) {
public Name {
Objects.requireNonNull(value, "인자 중 null 값이 존재합니다.");
if (value.isEmpty()) {
throw new IllegalArgumentException("이름은 한글자 이상이어야 합니다.");
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

도메인에 레코드 사용하는 것 커찬은 괜찮다고 보는걸까?

원래의 레코드의 설계의의는 데이터 전송 객체를 위한 것이라고 들었어
커찬의 의견 궁금 👀

Copy link
Member Author

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로 바꿔줄 것 같아!

Comment on lines +32 to +41
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());
}
Copy link
Member

Choose a reason for hiding this comment

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

꼼꼼한 처리 굳 👍

Comment on lines +11 to +20
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);
}
Copy link
Member

Choose a reason for hiding this comment

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

주생성자 부생성자 개념 적용해보면 좋을 듯!

Copy link
Member Author

Choose a reason for hiding this comment

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

그러고 싶었지만, 골치 아파지는 코드 떄문에 그럴 수 없었다... 코드 전반적으로 개선해보면서 할 듯!

Comment on lines +20 to +21
name VARCHAR(255) NOT NULL,
date VARCHAR(255) NOT NULL,
Copy link
Member

Choose a reason for hiding this comment

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

date나 time은 varchar로 관리되어도 필요한 경우 변환이 스무스 하더라

하지만 데이터 무결성을 위해서 다른 타입으로 수정해보는 것이 좋을 듯!?

Copy link
Member Author

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 {
Copy link
Member

Choose a reason for hiding this comment

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

AdminController 충분히 좋은 이름일수도 있지만 다른 RestController와 다른 맥락이라는 의미에서 AdminViewController라는 이름은 어때?

Comment on lines +44 to +48
@ExceptionHandler({IllegalArgumentException.class, NullPointerException.class})
public ResponseEntity<ErrorResponse> handleException(RuntimeException e) {
return ResponseEntity.badRequest()
.body(new ErrorResponse(e.getMessage()));
}
Copy link
Member

Choose a reason for hiding this comment

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

전역적인 ExceptionHandler를 사용하지 않은 이유 궁금!
다른 컨트롤러에서도 똑같이 생긴 코드가 나올 수 밖에 없기에 전역적인 처리가 가질 수 있는 장점을 나는 높게 평가하는 편이야

Copy link
Member Author

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로 묶는게 좋을 것 같아;;

Comment on lines +28 to +32
@GetMapping("/popular")
public ResponseEntity<List<ThemeResponse>> readPopularThemes() {
List<ThemeResponse> response = themeService.readPopularThemes();
return ResponseEntity.ok(response);
}
Copy link
Member

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 설계했어?

Copy link
Member Author

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가 그렇게 어색하다고 보기도 힘들 것 같아;; 오늘 수업 떄 말한 브리 말처럼 다른 사이트에서 어떻게 사용하는지 봐야 할 듯;;

Comment on lines 29 to 33
@GetMapping("/available")
public ResponseEntity<List<AvailableTimeResponse>> readAvailableTimes(@RequestParam String date, @RequestParam Long themeId) {
List<AvailableTimeResponse> response = service.readAvailableTimes(date, themeId);
return ResponseEntity.ok(response);
}
Copy link
Member

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)),
Copy link
Member

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,
Copy link
Member

Choose a reason for hiding this comment

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

JsonFormat 이 반복해서 등장하는데 전역으로 처리할 수 있는 방법을 찾아보는 것도 좋을 것 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

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

싫어요.

목적은 각 record 응답마다 어떤 형식으로 변환하는지 명시해주기 위해서 그랬어! (전역으로 처리할 수 있는 방법이 있는지도 몰랐다는 사실을 비밀!)

해당 방법을 찾아보고, 한번 시도해보도록 할께! (근데 시간을 요청, 응답하는 객체가 더 많아진다면 확실이 고려해봐야 할 듯;;)

Comment on lines +22 to +23
time_id BIGINT,
theme_id BIGINT,
Copy link
Member

Choose a reason for hiding this comment

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

요구사항 상 not null이 추가되어야 할 것 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

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

엥 이거 왜 없지? 구현 끝나고 붙이려 했는데... TODO 써 놓을 걸 ㅠ_ㅠ

Comment on lines +50 to +54
@ExceptionHandler({IllegalArgumentException.class, NullPointerException.class})
public ResponseEntity<ErrorResponse> handleException(RuntimeException e) {
return ResponseEntity.badRequest()
.body(new ErrorResponse(e.getMessage()));
}
Copy link
Member

Choose a reason for hiding this comment

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

중복된 코드로 보이는데 제거할 수 있을 것 같아요! @ControllerAdvice 등

Copy link
Member Author

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 {
Copy link
Member

Choose a reason for hiding this comment

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

Service 레이어나 dao 레이어의 테스트틑 일부러 작성하지 않은건가요?

Copy link
Member Author

Choose a reason for hiding this comment

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

이번 리뷰어에게 받은 테스트 관련 리뷰 요약

image

Comment on lines 62 to 117
long id = keyHolder.getKey().longValue();
return reservation.changeId(id);
Long id = keyHolder.getKey().longValue();
return readReservationById(id).orElseThrow();
Copy link
Member

Choose a reason for hiding this comment

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

long 대신 Long 으로 바꾼 이유가 있나요?

Copy link
Member Author

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이 나는 게 별로였음

Copy link
Member Author

@leegwichan leegwichan left a comment

Choose a reason for hiding this comment

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

Hi 로빈, 리비!

일단은 생각 나는대로 즉흥적으로 답변 달아봤어!
혹시나 의문 사항이 생기면 즉시 연락해 줘!

Comment on lines +44 to +48
@ExceptionHandler({IllegalArgumentException.class, NullPointerException.class})
public ResponseEntity<ErrorResponse> handleException(RuntimeException e) {
return ResponseEntity.badRequest()
.body(new ErrorResponse(e.getMessage()));
}
Copy link
Member Author

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로 묶는게 좋을 것 같아;;

Comment on lines +50 to +54
@ExceptionHandler({IllegalArgumentException.class, NullPointerException.class})
public ResponseEntity<ErrorResponse> handleException(RuntimeException e) {
return ResponseEntity.badRequest()
.body(new ErrorResponse(e.getMessage()));
}
Copy link
Member Author

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로 묶는게 좋을 것 같아;;

Comment on lines +28 to +32
@GetMapping("/popular")
public ResponseEntity<List<ThemeResponse>> readPopularThemes() {
List<ThemeResponse> response = themeService.readPopularThemes();
return ResponseEntity.ok(response);
}
Copy link
Member Author

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가 그렇게 어색하다고 보기도 힘들 것 같아;; 오늘 수업 떄 말한 브리 말처럼 다른 사이트에서 어떻게 사용하는지 봐야 할 듯;;

Comment on lines +48 to +57
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
""";
Copy link
Member Author

Choose a reason for hiding this comment

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

나도 내 코드에서 10은 빼야 할 듯;;

Comment on lines +5 to +12
public record Name(String value) {
public Name {
Objects.requireNonNull(value, "인자 중 null 값이 존재합니다.");
if (value.isEmpty()) {
throw new IllegalArgumentException("이름은 한글자 이상이어야 합니다.");
}
}
}
Copy link
Member Author

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,
Copy link
Member Author

Choose a reason for hiding this comment

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

싫어요.

목적은 각 record 응답마다 어떤 형식으로 변환하는지 명시해주기 위해서 그랬어! (전역으로 처리할 수 있는 방법이 있는지도 몰랐다는 사실을 비밀!)

해당 방법을 찾아보고, 한번 시도해보도록 할께! (근데 시간을 요청, 응답하는 객체가 더 많아진다면 확실이 고려해봐야 할 듯;;)

Comment on lines +33 to +35
LocalDate currentDate = LocalDate.now();
LocalDate startDate = currentDate.minusDays(7);
LocalDate endDate = currentDate.minusDays(1);
Copy link
Member Author

Choose a reason for hiding this comment

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

안그래도 해당 피드백을 리뷰어한테 받았어! 바쁘게 구현하느라 미처 생각 못함;;
상수 추출 해보도록 할께!

Comment on lines +20 to +21
name VARCHAR(255) NOT NULL,
date VARCHAR(255) NOT NULL,
Copy link
Member Author

Choose a reason for hiding this comment

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

나도 그렇게 생각해. 이번 기회에 형식 바꿔봐야 겠다.

Comment on lines +22 to +23
time_id BIGINT,
theme_id BIGINT,
Copy link
Member Author

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 {
Copy link
Member Author

Choose a reason for hiding this comment

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

이번 리뷰어에게 받은 테스트 관련 리뷰 요약

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants