-
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
Changes from all commits
b6dc20d
b7fab18
7a652f1
7f83bbb
ce8b6da
f152427
c89696c
a3e15d3
a8be822
9fb095d
6987f97
3fcdf27
2629f0c
6e5c2a9
feb3230
07895af
a2ab229
cf1fbd0
e8cafd3
c9a9e37
921019e
377cd7f
b0f3227
c693974
1ba2012
f2c036d
d5e7b72
d72292b
6134110
908298d
95e3bbd
b2395a3
227d922
d237689
07dc77c
22dbd7e
7357651
0e31033
2a2513b
a95ec89
5a564ea
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,31 @@ | ||
| package com.codesoom.assignment.common; | ||
|
|
||
| import com.codesoom.assignment.common.dto.ErrorResponse; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| public abstract class BaseException extends RuntimeException { | ||
| public final List<ErrorValidation> errors = new ArrayList<>(); | ||
|
|
||
| public BaseException() { | ||
| } | ||
|
|
||
| public BaseException(String message) { | ||
| super(message); | ||
| } | ||
|
|
||
| public abstract int getStatusCode(); | ||
|
|
||
| public void addValidation(String source, String type, String message) { | ||
| errors.add(new ErrorValidation(source, type, message)); | ||
| } | ||
|
|
||
| public ErrorResponse toErrorResponse() { | ||
| return new ErrorResponse( | ||
| String.valueOf(getStatusCode()), | ||
| getMessage(), | ||
| errors | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.codesoom.assignment.common; | ||
|
|
||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| public class ErrorValidation { | ||
| private final String source; | ||
| private final String type; | ||
| private final String message; | ||
|
|
||
| @Builder | ||
| public ErrorValidation(String source, String type, String message) { | ||
| this.source = source; | ||
| this.type = type; | ||
| this.message = message; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package com.codesoom.assignment.common.dto; | ||
|
|
||
| import com.codesoom.assignment.common.ErrorValidation; | ||
| import lombok.Getter; | ||
| import lombok.Setter; | ||
| import lombok.ToString; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @Getter | ||
| @Setter | ||
| @ToString | ||
| public class ErrorResponse { | ||
| private String code; | ||
| private String message; | ||
| private List<ErrorValidation> errors; | ||
|
|
||
| public ErrorResponse(String code, String message, List<ErrorValidation> errors) { | ||
| this.code = code; | ||
| this.message = message; | ||
| this.errors = errors != null ? errors : List.of(); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package com.codesoom.assignment.product.application; | ||
|
|
||
| import com.codesoom.assignment.common.BaseException; | ||
|
|
||
| public class InvalidProductRequest extends BaseException { | ||
| public static final String MESSAGE = "유효하지 않은 상품 정보입니다"; | ||
|
|
||
| public InvalidProductRequest() { | ||
| super(MESSAGE); | ||
| } | ||
|
|
||
| @Override | ||
| public int getStatusCode() { | ||
| return 400; | ||
| } | ||
|
|
||
| public boolean hasErrors() { | ||
| return !errors.isEmpty(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package com.codesoom.assignment.product.application; | ||
|
|
||
| import com.codesoom.assignment.product.domain.Product; | ||
| import com.codesoom.assignment.product.domain.dto.ProductRequest; | ||
| import com.codesoom.assignment.product.infra.persistence.ProductRepository; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| @Service | ||
| public class ProductCreator { | ||
| private final ProductRepository productRepository; | ||
|
|
||
| public ProductCreator(ProductRepository productRepository) { | ||
| this.productRepository = productRepository; | ||
| } | ||
|
|
||
| public Product createProduct(ProductRequest product) { | ||
| return productRepository.save(product.toProductEntity()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package com.codesoom.assignment.product.application; | ||
|
|
||
| import com.codesoom.assignment.product.domain.Product; | ||
| import com.codesoom.assignment.product.infra.persistence.ProductRepository; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| @Service | ||
| public class ProductDeleter { | ||
|
|
||
| private final ProductRepository productRepository; | ||
| private final ProductReader productReader; | ||
|
|
||
| public ProductDeleter(ProductRepository productRepository, ProductReader productReader) { | ||
| this.productRepository = productRepository; | ||
| this.productReader = productReader; | ||
| } | ||
|
|
||
| public void deleteProduct(Long id) { | ||
| Product product = productReader.getProduct(id); | ||
| productRepository.delete(product); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package com.codesoom.assignment.product.application; | ||
|
|
||
| import com.codesoom.assignment.common.BaseException; | ||
|
|
||
| public class ProductNotFoundException extends BaseException { | ||
| public static final String MESSAGE = "해당하는 상품이 존재하지 않습니다"; | ||
|
|
||
| public ProductNotFoundException() { | ||
| super(MESSAGE); | ||
| } | ||
|
|
||
| @Override | ||
| public int getStatusCode() { | ||
| return 404; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package com.codesoom.assignment.product.application; | ||
|
|
||
| import com.codesoom.assignment.product.domain.Product; | ||
| import com.codesoom.assignment.product.infra.persistence.ProductRepository; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| import java.util.List; | ||
| import java.util.stream.Collectors; | ||
| import java.util.stream.StreamSupport; | ||
|
|
||
| @Service | ||
| public class ProductReader { | ||
|
|
||
| private final ProductRepository productRepository; | ||
|
|
||
| public ProductReader(ProductRepository productRepository) { | ||
| this.productRepository = productRepository; | ||
| } | ||
|
|
||
| public List<Product> getProductList() { | ||
| return productRepository.findAll(); | ||
| } | ||
|
|
||
| public Product getProduct(Long id) { | ||
| return productRepository.findById(id).orElseThrow(ProductNotFoundException::new); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package com.codesoom.assignment.product.application; | ||
|
|
||
| import com.codesoom.assignment.product.domain.Product; | ||
| import com.codesoom.assignment.product.domain.dto.ProductRequest; | ||
| import com.codesoom.assignment.product.infra.persistence.ProductRepository; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| import javax.transaction.Transactional; | ||
|
|
||
| @Service | ||
| public class ProductUpdater { | ||
| private final ProductRepository productRepository; | ||
|
|
||
| public ProductUpdater(ProductRepository productRepository) { | ||
| this.productRepository = productRepository; | ||
| } | ||
|
|
||
| @Transactional | ||
| public Product updateProduct(Long id, ProductRequest productRequest) { | ||
| Product product = productRepository.findById(id).orElseThrow(ProductNotFoundException::new); | ||
| product.update(productRequest.toProductEntity()); | ||
| return product; | ||
| } | ||
|
Comment on lines
+18
to
+23
Contributor
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. Transaction때문에 Entity.save를 명시적으로 호출하지 않아도 자동으로 반영되겠네요.
하지만 추상화와 일관성을 위해서 명시적으로 작성하는 것도 좋다는 의견도 있으니 참고하시면 좋을 것 같습니다. 참고 |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| package com.codesoom.assignment.product.domain; | ||
|
|
||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.ToString; | ||
|
|
||
| import javax.persistence.Entity; | ||
| import javax.persistence.GeneratedValue; | ||
| import javax.persistence.Id; | ||
|
|
||
| @Entity | ||
| @Getter | ||
| @ToString | ||
| public class Product { | ||
| @Id | ||
| @GeneratedValue | ||
| private Long id; | ||
| private String name; | ||
| private String maker; | ||
| private Integer price; | ||
| private String imageUrl; | ||
|
|
||
| public Product() { | ||
| } | ||
|
|
||
| @Builder | ||
| public Product(Long id, String name, String maker, Integer price, String imageUrl) { | ||
| this.id = id; | ||
| this.name = name; | ||
| this.maker = maker; | ||
| this.price = price; | ||
| this.imageUrl = imageUrl; | ||
| } | ||
|
|
||
| public void update(Product source) { | ||
| this.name = source.name; | ||
| this.maker = source.maker; | ||
| this.price = source.price; | ||
| this.imageUrl = source.imageUrl; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package com.codesoom.assignment.product.domain.dto; | ||
|
|
||
| import com.codesoom.assignment.product.application.InvalidProductRequest; | ||
| import com.codesoom.assignment.product.domain.Product; | ||
| import lombok.Getter; | ||
| import lombok.ToString; | ||
| import org.springframework.util.StringUtils; | ||
|
|
||
| @ToString | ||
| @Getter | ||
| public class ProductRequest { | ||
| private final String name; | ||
| private final String maker; | ||
| private final Integer price; | ||
| private final String imageUrl; | ||
|
|
||
| public ProductRequest(String name, String maker, Integer price, String imageUrl) { | ||
| this.name = name; | ||
| this.maker = maker; | ||
| this.price = price; | ||
| this.imageUrl = imageUrl; | ||
| } | ||
|
|
||
| public Product toProductEntity() { | ||
| return Product.builder() | ||
| .name(this.name) | ||
| .maker(this.maker) | ||
| .price(this.price) | ||
| .imageUrl(this.imageUrl).build(); | ||
| } | ||
|
|
||
| public void validate() { | ||
| InvalidProductRequest invalidProductRequest = new InvalidProductRequest(); | ||
|
|
||
| if (StringUtils.isEmpty(this.name)) { | ||
| invalidProductRequest.addValidation("name", "name is empty", "상품명은 필수입니다."); | ||
| } | ||
| if (StringUtils.isEmpty(this.maker)) { | ||
| invalidProductRequest.addValidation("maker", "maker is empty", "제조사은 필수입니다."); | ||
| } | ||
| if (this.price == null || this.price < 0) { | ||
| invalidProductRequest.addValidation("price", "price is empty", "가격은 필수입니다."); | ||
| } | ||
|
|
||
| if (invalidProductRequest.hasErrors()) { | ||
| throw invalidProductRequest; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package com.codesoom.assignment.product.domain.dto; | ||
|
|
||
| import com.codesoom.assignment.product.domain.Product; | ||
| import lombok.Getter; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| @Getter | ||
| public class ProductResponse { | ||
| private final Long id; | ||
| private final String name; | ||
| private final String maker; | ||
| private final Integer price; | ||
| private final String imageUrl; | ||
|
|
||
| public ProductResponse(Product product) { | ||
| this.id = product.getId(); | ||
| this.name = product.getName(); | ||
| this.maker = product.getMaker(); | ||
| this.price = product.getPrice(); | ||
| this.imageUrl = product.getImageUrl(); | ||
| } | ||
|
|
||
| public static List<ProductResponse> listOf(Iterable<Product> list) { | ||
| ArrayList<ProductResponse> productResponseArrayList = new ArrayList<>(); | ||
| for (Product product : list) { | ||
| productResponseArrayList.add(new ProductResponse(product)); | ||
| } | ||
| return productResponseArrayList; | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package com.codesoom.assignment.product.infra.api.web.v1; | ||
|
|
||
| import com.codesoom.assignment.common.dto.ErrorResponse; | ||
| import com.codesoom.assignment.product.application.InvalidProductRequest; | ||
| import com.codesoom.assignment.product.application.ProductNotFoundException; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.web.bind.annotation.ControllerAdvice; | ||
| import org.springframework.web.bind.annotation.ExceptionHandler; | ||
| import org.springframework.web.bind.annotation.ResponseBody; | ||
| import org.springframework.web.bind.annotation.ResponseStatus; | ||
|
|
||
| @ControllerAdvice | ||
| public class ProductAdvice { | ||
|
|
||
| @ExceptionHandler(ProductNotFoundException.class) | ||
| @ResponseStatus(HttpStatus.NOT_FOUND) | ||
| @ResponseBody | ||
| public ErrorResponse productNotFoundExceptionHandler(ProductNotFoundException e) { | ||
|
|
||
| return e.toErrorResponse(); | ||
| } | ||
|
|
||
| @ExceptionHandler(InvalidProductRequest.class) | ||
| @ResponseStatus(HttpStatus.BAD_REQUEST) | ||
| @ResponseBody | ||
| public ErrorResponse invalidProductExceptionHandler(InvalidProductRequest e) { | ||
| return e.toErrorResponse(); | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package com.codesoom.assignment.product.infra.api.web.v1; | ||
|
|
||
| import com.codesoom.assignment.product.application.ProductCreator; | ||
| import com.codesoom.assignment.product.domain.dto.ProductRequest; | ||
| import com.codesoom.assignment.product.domain.dto.ProductResponse; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.web.bind.annotation.*; | ||
|
|
||
| @RestController | ||
| @CrossOrigin | ||
| @RequiredArgsConstructor | ||
| public class ProductCreateController { | ||
| private final ProductCreator productCreator; | ||
|
|
||
| @PostMapping("/products") | ||
| @ResponseStatus(HttpStatus.CREATED) | ||
| public ProductResponse createProduct(@RequestBody ProductRequest productRequest) { | ||
| productRequest.validate(); | ||
| return new ProductResponse(productCreator.createProduct(productRequest)); | ||
| } | ||
| } |
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