-
Notifications
You must be signed in to change notification settings - Fork 81
4주차 : 고양이 장난감가게 만들기 #92
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
| public ProductResponse getProduct(Long id) { | ||
| Product product = productRepository.findById(id).orElseThrow(ProductNotFoundException::new); | ||
| return new ProductResponse(product); | ||
| } |
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.
도메인 객체를 바로 반환하지 않으려고 하신 것 같아요. ProductResponse를 Controller에서 응답을 결정하는 객체인 것 같네요. 서비스 레이어에서 웹 컨트롤러 레이어에서 사용되는 개념에 의존하고 있네요.
이러면 안쪽 레이어에서 바깥 쪽 레이어에 의존하게 돼요. 클린 아키텍처는 의존성의 방향이 자주 바뀌는 것에서 덜 바뀌는 쪽으로, 덜 중요한 곳에서 더 중요한 곳으로 향해야해요. 만약 응답을 다르게 해야된다고 했을 때, 서비스 레이어에서 영향을 받을 수 있을 것 같아요.
따라서 서비스 레이어에서 반환하는 것과 웹 컨트롤러에서 응답하는 것을 분리하는 것이 좋아 보입니다.
See also
| public class ProductAdvice { | ||
|
|
||
| @ExceptionHandler(ProductNotFoundException.class) | ||
| @ResponseStatus(value = HttpStatus.NOT_FOUND) |
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.
| @ResponseStatus(value = HttpStatus.NOT_FOUND) | |
| @ResponseStatus(HttpStatus.NOT_FOUND) |
|
|
||
| @Slf4j | ||
| @Service | ||
| public class ProductService { |
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가 붙을 수 밖에 없었던 것 같아요. 여러가지 일을 할 수록 보편적인 이름을 붙일 수 밖에 없게 돼죠. 서비스가 하나의 일만 하도록 나눠보는 것은 어떨까요?
See also
| @Test | ||
| @DisplayName("상품 목록을 조회하면, 상품 목록이 반환된다.") | ||
| public void testGetProductList() { |
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.
Given When Then에 충분히 익숙해지신 것 같아요. 이렇게 테스트를 작성할 때 좋은 프레임워크가 있는데 바로 BDD테스트에요. BDD를 적용해 보는 것은 어떠신가요?
See also
|
CodeSoom/spring-week3-assignment-1#100 (comment) 책이 집에 있어서 답변이 조금 늦었습니다. 위에 커멘트도 한 번 확인 부탁드립니다. |
| @Nested | ||
| @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) | ||
| class createProduct_메서드는 { | ||
| @Nested | ||
| @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) | ||
| 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.
테스트 이름이랑 설명 주석이 중복이라서, 중복을 제거하셨네요. 테스트를 깔끔하게 작성하려고 노력을 하신 것 같습니다.
코틀린과 kotest를 같이 쓰면 이런 형식으로 작성하는 것도 가능합니다. 자바에서는 한계가 있긴 하지만, 더 좋은 방향이 있다는 것을 아는 것도 중요합니다.
beforeEach {
categoryCreator = CategoryCreator(repository)
}
describe("CategoryCreator") {
describe("create") {
beforeEach {
every {
repository.save(any())
} returns category
}
it("생성된 카테고리를 반환한다") {
val result = categoryCreator.create(name)
result.name shouldBe name
}
}
}| @ExceptionHandler(ProductNotFoundException.class) | ||
| @ResponseStatus(HttpStatus.NOT_FOUND) | ||
| @ResponseBody | ||
| public ErrorResponse ProductNotFoundExceptionHandler(ProductNotFoundException e) { |
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.
메서드 이름이 camelCase가 아니군요.
| Assertions.assertEquals(PRODUCT_REQUEST.getName(), product.getName()); | ||
| Assertions.assertEquals(PRODUCT_REQUEST.getMaker(), product.getMaker()); | ||
| Assertions.assertEquals(PRODUCT_REQUEST.getPrice(), product.getPrice()); | ||
| Assertions.assertEquals(PRODUCT_REQUEST.getImageUrl(), product.getImageUrl()); |
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.
단순 비교라서 AssertJ를 쓰는 것이 엄청 좋아보이지는 않지만, 다른 곳에서는 AssertJ를 사용하고 있으니 일관성을 위해서 여기서도 AssertJ를 쓰는게 좋겠습니다.
| Product product = createProduct(); | ||
| productRepository.save(product); | ||
| productDeleter.deleteProduct(product.getId()); | ||
| Assertions.assertThat(productRepository.findById(product.getId())).isEmpty(); |
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.
궁금한 점이 있습니다
저런 문맥상의 일의 구분을 두는 개행 이외에
혹시 개행 관련 해서 암묵적으로 이렇게 한다 이런게 있나요?
메서드 시작 시 한 줄 개행 한다던가...
뭔가 찾아보는데 이렇다 할만한 게 없어서 그냥 느낌으로만 하는 느낌입니다.
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.
음 그런건 없는 것 같아요. 팀의 컨벤션에 따르는게 좋을 것 같습니다. 느낌으로 하는게 맞아요. 제가 이 코드들은 한 번에 읽어야 합니다. 라고 말하고 있는 것이니까요.
제가 레퍼런스를 남기지 않았었군요.
Grouping From the other side of the looking glass, white space is grouping, making
sure that related statements are grouped together.
In writing, thoughts are grouped into paragraphs. A well-written paragraph contains
only sentences that relate to a particular thought. It shouldn’t contain extraneous sen-
tences. Similarly, a paragraph of code should contain statements that accomplish a
single task and that are related to each other.Blank lines Just as it’s important to group related statements, it’s important to sepa-
rate unrelated statements from each other. The start of a new paragraph in English is
identified with indentation or a blank line. The start of a new paragraph of code
should be identified with a blank line.Using blank lines is a way to indicate how a program is organized. You can use them
to divide groups of related statements into paragraphs, to separate routines from one
another, and to highlight comments.Although this particular statistic may be hard to put to work, a study by Gorla,
Benander, and Benander found that the optimal number of blank lines in a program
is about 8 to 16 percent. Above 16 percent, debug time increases dramatically (1990).
See also
- Code Complete 2 - [7부] 소프트웨어 장인정신 > 31장: 레이아웃과 스타일 > 31.2 레이아웃 기법
| productRepository.save(testProduct); | ||
|
|
||
| Assertions.assertThat(productReader.getProductList()).isNotEmpty(); | ||
| Product product = productReader.getProductList().iterator().next(); |
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.
interator를 직접 사용하고 있는 것이 익숙하진 않네요.
| class 수정할_상품이_있다면 { | ||
| @BeforeEach | ||
| void setUp() { | ||
| productRepository.deleteAll(); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("해당_상품정보를_수정한_후_해당_상품을_반환한다") | ||
| void 해당_상품정보를_수정한_후_해당_상품을_반환한다() { | ||
| Product product = createProduct(); | ||
| productRepository.save(product); | ||
| ProductRequest productRequest = new ProductRequest("updateProduct", "updateMaker", 2000, "updateImage"); | ||
| Product updatedProduct = productUpdater.updateProduct(product.getId(), productRequest); | ||
|
|
||
| Assertions.assertThat(updatedProduct.getName()).isEqualTo(productRequest.getName()); | ||
| Assertions.assertThat(updatedProduct.getMaker()).isEqualTo(productRequest.getMaker()); | ||
| Assertions.assertThat(updatedProduct.getPrice()).isEqualTo(productRequest.getPrice()); | ||
| Assertions.assertThat(updatedProduct.getImageUrl()).isEqualTo(productRequest.getImageUrl()); | ||
| } | ||
| } |
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.
class 수정할_상품이_있다면 {
@BeforeEach
void setUp() {
productRepository.deleteAll();
Product product = createProduct();
productRepository.save(product);
}
@Test
@DisplayName("해당_상품정보를_수정한_후_해당_상품을_반환한다")
void 해당_상품정보를_수정한_후_해당_상품을_반환한다() {
ProductRequest productRequest = new ProductRequest("updateProduct", "updateMaker", 2000, "updateImage");
Product updatedProduct = productUpdater.updateProduct(product.getId(), productRequest);
Assertions.assertThat(updatedProduct.getName()).isEqualTo(productRequest.getName());
Assertions.assertThat(updatedProduct.getMaker()).isEqualTo(productRequest.getMaker());
Assertions.assertThat(updatedProduct.getPrice()).isEqualTo(productRequest.getPrice());
Assertions.assertThat(updatedProduct.getImageUrl()).isEqualTo(productRequest.getImageUrl());
}
}
이렇게 하면 위의 BeforeEach와 차이점이 바로 보여서 무엇이 다른지 명확하게 보일 것 같아요
| } | ||
|
|
||
| public void deleteProduct(Long id) { | ||
| Product product = productRepository.findById(id).orElseThrow(ProductNotFoundException::new); |
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.
ProductReader.getProduct를 사용할 수도 있겠네요
| return StreamSupport.stream(products.spliterator(), false) | ||
| .collect(Collectors.toList()); |
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.
stream을 사용하셨군요. 아마도 Iterable을 받아서 List로 변환할려고 하신 것 같습니다. productRepository.findAll()의 결과로 바로 List를 받을 수 있을까요?
| @Transactional | ||
| public Product updateProduct(Long id, ProductRequest productRequest) { | ||
| Product product = productRepository.findById(id).orElseThrow(ProductNotFoundException::new); | ||
| product.update(productRequest.toProductEntity()); | ||
| return product; | ||
| } |
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.
Transaction때문에 Entity.save를 명시적으로 호출하지 않아도 자동으로 반영되겠네요.
Note that the call to save is not strictly necessary from a JPA point of view, but should still be there in order to stay consistent to the repository abstraction offered by Spring Data.
하지만 추상화와 일관성을 위해서 명시적으로 작성하는 것도 좋다는 의견도 있으니 참고하시면 좋을 것 같습니다.
참고
| public class ProductController { | ||
|
|
||
| private final ProductCreator productCreator; | ||
| private final ProductReader productReader; | ||
| private final ProductUpdater productUpdater; | ||
| private final ProductDeleter productDeleter; |
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.
각 서비스도 분리해주었으니, 컨트롤러도 분리할 수 있겠어요
| @Getter | ||
| @Setter | ||
| @ToString | ||
| public class ErrorResponse { |
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.
에러 응답 형식을 규격화 하셨네요. 애플리케이션에서 일관성있는 에러 응답으로 인해 예측 가능성이 높아졌네요. 규격을 정한다면 다양한 상황에서도 적절히 사용할 수 있어야겠습니다. 그래서 여러개의 에러 응답이 있는 경우에도 대응이 가능하도록 만들면 더 좋을 것 같습니다.
예를들어서 세탁기가 문이 안닫혀 있다고 해서 문을 닫았더니, 물이 부족하다는 에러가 발생하고, 물을 채웠더니 세제가 부족하다고 하고, 세제를 채웠더니 옷이 너무 많다고 에러가 나면 짜증나겠죠? 여러 에러가 있을 경우 한 번에 에러를 반환할 수 있어야 합니다. 에를들어서 다음과 같이 할 수 있습니다.
{
"message": "Invalid Request",
"errors": [
{
"source": "Door",
"type": "DOOR_IS_OPENED",
"message": "Door must be closed"
},
{
"source": "Detergent",
"type": "DETERGENT_MISSING",
"message": "Detegent is mandatory"
}
]
}이렇게 한 번에 피드백을 전달하면 상호작용을 단순화할 수 있고 리퀘스트 요청 횟수도 줄일 수 있습니다.
See also
- 일상 속 사물이 알려주는 웹 API 디자인 > 5장 직관적인 API 디자인하기 > 5.2.4 철저한 에러 피드백 반환하기
| @Test | ||
| @DisplayName("상품 생성시 올바르지 않은 요청인 경우 InvalidProductRequest 발생 .") | ||
| void createProductInvalidRequest() throws Exception { | ||
| // given | ||
| ProductRequest productRequest = new ProductRequest("", "", 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.
좀 과해보일 수 있지만 @ParameterizedTest를 이용해서 상품의 이름만 없는 경우, 상호만 없는 경우 등을 엄격하게 테스트할 수 있겠네요.
See also
| @DisplayName("상품 삭제 요청 시 없는 경우 ProductNotFound 예외 발생") | ||
| void deleteProductsNotFound() throws Exception { |
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.
예외가 발생하는 것은 시스템 관점인 것 같아요. 웹 컨트롤러 테스트를 작성할 때는 프론트엔드 사용자 입장에서 어떤 응답을 받는지를 테스트하는 것이 좋을 것 같습니다.
안녕하세요 4주차 과제 리뷰요청 드립니다!
부족한 부분은 시간날때 조금씩 작성하겠습니다