Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
b6dc20d
test : 상품 등록 테스트 작성
tmxhsk99 Jul 24, 2023
b7fab18
feat : 상품 등록 기능 작성
tmxhsk99 Jul 24, 2023
7a652f1
refactor: getter 롬복으로 수정
tmxhsk99 Jul 24, 2023
7f83bbb
refactor: 불필요한 import 삭제
tmxhsk99 Jul 24, 2023
ce8b6da
refactor: final 추가 및 사용하지 않는 생성자 삭제
tmxhsk99 Jul 24, 2023
f152427
refactor: final 추가 및 사용하지 않는 생성자 삭제
tmxhsk99 Jul 24, 2023
c89696c
test : 상품리스트 조회 테스트 작성
tmxhsk99 Jul 24, 2023
a3e15d3
feat : 상품 리스트 조회 기능 작성
tmxhsk99 Jul 24, 2023
a8be822
feat : 단일 상품 조회 테스트 작성
tmxhsk99 Jul 24, 2023
9fb095d
feat : 단일 상품 조회 기능 작성
tmxhsk99 Jul 24, 2023
6987f97
test : 단일 상품 조회 실패시 테스트 작성
tmxhsk99 Jul 24, 2023
3fcdf27
feat : product 관련 조회관련 에러처리 로직 추가 Error Advice 예외처리 추가
tmxhsk99 Jul 24, 2023
2629f0c
test : 상품 수정 테스트 작성
tmxhsk99 Jul 24, 2023
6e5c2a9
feat : 상품 수정 기능 작성
tmxhsk99 Jul 24, 2023
feb3230
test : 상품 삭제 테스트 추가
tmxhsk99 Jul 24, 2023
07895af
feat : 상품 삭제 기능 추가
tmxhsk99 Jul 24, 2023
a2ab229
test : 상품 삭제 예외 발생 테스트 추가
tmxhsk99 Jul 24, 2023
cf1fbd0
feat: cors 관련 설정 적용
tmxhsk99 Jul 24, 2023
e8cafd3
feat: Transactional 추가
tmxhsk99 Jul 24, 2023
c9a9e37
test : ProductService 테스트 작성
tmxhsk99 Jul 24, 2023
921019e
refactor : 서비스 레이어와 웹 레이어 간의 의존성 제거
tmxhsk99 Jul 25, 2023
377cd7f
chore : value 삭제
tmxhsk99 Jul 25, 2023
b0f3227
refactor : 계층형 패키지 스타일로 수정
tmxhsk99 Jul 25, 2023
c693974
test : 기존 ProductServiceTest 삭제 및 BDD 형식 ProductCreateTest 추가
tmxhsk99 Jul 25, 2023
1ba2012
test : ProductReader 테스트 작성
tmxhsk99 Jul 26, 2023
f2c036d
test : getProduct 테스트 추가
tmxhsk99 Jul 26, 2023
d5e7b72
test : ProductUpdater,ProductDeleter 테스트추가
tmxhsk99 Jul 26, 2023
d72292b
chore : 메서드명 camelCase로 수정
tmxhsk99 Jul 27, 2023
6134110
chore : 코드 통일성을위해 AssertJ 사용으로 수정
tmxhsk99 Jul 27, 2023
908298d
chore : 문맥상의 일의 구분을 위한 개행처리
tmxhsk99 Jul 27, 2023
95e3bbd
test : 기본 셋업 객체 생성 로직 셋업 메서드로 이동
tmxhsk99 Jul 27, 2023
b2395a3
refactor : Iterable -> List 로 반환하도록 수정
tmxhsk99 Jul 27, 2023
227d922
refactor : repository 에서 productReader 사용하여 상품을 찾도록 수정
tmxhsk99 Jul 28, 2023
d237689
test : deleterTest 의존성 수정
tmxhsk99 Jul 28, 2023
07dc77c
feat : crudRepository -> JpaRepository로 수정
tmxhsk99 Jul 28, 2023
22dbd7e
refactor : contorller 분리
tmxhsk99 Jul 29, 2023
7357651
chore : 클래스명 수정
tmxhsk99 Jul 29, 2023
0e31033
chore : lombok getter에서 일반 getter로 수정
tmxhsk99 Jul 29, 2023
2a2513b
feat : ProductRequest 검증 로직 추가 및 InvalidProductRequest 커스텀 에러 추가
tmxhsk99 Jul 29, 2023
a95ec89
test : InvalidProductRequest 에러 발생 테스트 추가
tmxhsk99 Jul 29, 2023
5a564ea
test : InvalidProductRequest 발생 테스트 케이스별 추가
tmxhsk99 Jul 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
Copy link
Contributor

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 철저한 에러 피드백 반환하기

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
Copy link
Contributor

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.

하지만 추상화와 일관성을 위해서 명시적으로 작성하는 것도 좋다는 의견도 있으니 참고하시면 좋을 것 같습니다.

참고

}
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));
}
}
Loading