Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 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.HashMap;
import java.util.Map;

public abstract class baseException extends RuntimeException {
public final Map<String, String> validation = new HashMap<>();

public baseException() {
}

public baseException(String message) {
super(message);
}

public abstract int getStatusCode();

public void addValidation(String fieldName, String message) {
validation.put(fieldName, message);
}

public ErrorResponse toErrorResponse() {
return new ErrorResponse(
String.valueOf(getStatusCode()),
getMessage(),
validation
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.codesoom.assignment.common.dto;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.util.HashMap;
import java.util.Map;

@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 Map<String, String> validation;

public ErrorResponse(String code, String message, Map<String, String> validation) {
this.code = code;
this.message = message;
this.validation = validation != null ? validation : new HashMap<>();
}

public void addValidation(String field, String message) {
validation.put(field, message);
}

}
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,47 @@
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.domain.dto.ProductResponse;
import com.codesoom.assignment.product.infra.persistence.ProductRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.util.List;

@Slf4j
@Service
public class ProductService {
Copy link
Contributor

Choose a reason for hiding this comment

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

상품에 대한 여러가지 일을 하다보니 서비스의 이름에 Service가 붙을 수 밖에 없었던 것 같아요. 여러가지 일을 할 수록 보편적인 이름을 붙일 수 밖에 없게 돼죠. 서비스가 하나의 일만 하도록 나눠보는 것은 어떨까요?

See also

private final ProductRepository productRepository;

public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}

public ProductResponse createProduct(ProductRequest product) {
Product savedProduct = productRepository.save(product.toProductEntity());
return new ProductResponse(savedProduct);
}

public List<ProductResponse> getProductList() {
return ProductResponse.listOf(productRepository.findAll());
}

public ProductResponse getProduct(Long id) {
Product product = productRepository.findById(id).orElseThrow(ProductNotFoundException::new);
return new ProductResponse(product);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

도메인 객체를 바로 반환하지 않으려고 하신 것 같아요. ProductResponse를 Controller에서 응답을 결정하는 객체인 것 같네요. 서비스 레이어에서 웹 컨트롤러 레이어에서 사용되는 개념에 의존하고 있네요.

이러면 안쪽 레이어에서 바깥 쪽 레이어에 의존하게 돼요. 클린 아키텍처는 의존성의 방향이 자주 바뀌는 것에서 덜 바뀌는 쪽으로, 덜 중요한 곳에서 더 중요한 곳으로 향해야해요. 만약 응답을 다르게 해야된다고 했을 때, 서비스 레이어에서 영향을 받을 수 있을 것 같아요.

따라서 서비스 레이어에서 반환하는 것과 웹 컨트롤러에서 응답하는 것을 분리하는 것이 좋아 보입니다.

See also


@Transactional
public ProductResponse updateProduct(Long id, ProductRequest productRequest) {
Product product = productRepository.findById(id).orElseThrow(ProductNotFoundException::new);
product.update(productRequest.toProductEntity());
return new ProductResponse(product);
}

public void deleteProduct(Long id) {
Product product = productRepository.findById(id).orElseThrow(ProductNotFoundException::new);
productRepository.delete(product);
}
}
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,29 @@
package com.codesoom.assignment.product.domain.dto;

import com.codesoom.assignment.product.domain.Product;
import lombok.Getter;
import lombok.ToString;

@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();
}
}
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,21 @@
package com.codesoom.assignment.product.infra.api;

import com.codesoom.assignment.common.dto.ErrorResponse;
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(value = HttpStatus.NOT_FOUND)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
@ResponseStatus(value = HttpStatus.NOT_FOUND)
@ResponseStatus(HttpStatus.NOT_FOUND)

@ResponseBody
public ErrorResponse ProductNotFoundExceptionHandler(ProductNotFoundException e) {
return e.toErrorResponse();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.codesoom.assignment.product.infra.api;

import com.codesoom.assignment.product.application.ProductService;
import com.codesoom.assignment.product.domain.dto.ProductRequest;
import com.codesoom.assignment.product.domain.dto.ProductResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@CrossOrigin
@Slf4j
public class ProductController {

private final ProductService productService;

public ProductController(ProductService productService) {
this.productService = productService;
}

@PostMapping("/products")
@ResponseStatus(HttpStatus.CREATED)
public ProductResponse createProduct(@RequestBody ProductRequest productRequest) {
log.info("Product request: {}", productRequest);
return productService.createProduct(productRequest);
}

@GetMapping("/products")
public List<ProductResponse> getProductList() {
return productService.getProductList();
}

@GetMapping("/products/{id}")
public ProductResponse getProduct(@PathVariable Long id) {
return productService.getProduct(id);
}

@PatchMapping("/products/{id}")
public ProductResponse updateProduct(@PathVariable Long id, @RequestBody ProductRequest productRequest) {
return productService.updateProduct(id, productRequest);
}

@DeleteMapping("/products/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.codesoom.assignment.product.infra.persistence;

import com.codesoom.assignment.product.domain.Product;
import org.springframework.data.repository.CrudRepository;


public interface ProductRepository extends CrudRepository<Product, Long> {
}
Loading