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

부산대 BE_정재빈_step3 #206

Open
wants to merge 56 commits into
base: jaebin2019
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
98f3a17
feat(global): create global controller
JaeBin2019 Jun 26, 2024
e114c0e
docs(global): summerize list of features
JaeBin2019 Jun 25, 2024
5b37775
feat(product): create product controller
JaeBin2019 Jun 25, 2024
2b52d04
feat(product): create product entity
JaeBin2019 Jun 25, 2024
f54b33a
fix(product): add column id into product entity
JaeBin2019 Jun 25, 2024
f376876
feat(product): add method get product by id in controller
JaeBin2019 Jun 25, 2024
0ded7d3
feat(product): create product request record
JaeBin2019 Jun 25, 2024
dc565f4
feat(product): add constructor to initialize Product from ProductRequest
JaeBin2019 Jun 25, 2024
5507db1
fix(product): change RequestMapping url error
JaeBin2019 Jun 25, 2024
fa94538
feat(product): add create product method
JaeBin2019 Jun 25, 2024
3028213
fix(product): change endpoint product to products
JaeBin2019 Jun 25, 2024
eedb420
feat(product): add update product method
JaeBin2019 Jun 25, 2024
c3c5908
feat(product): add delete product method
JaeBin2019 Jun 25, 2024
317a4ba
feat(product): create http request file
JaeBin2019 Jun 25, 2024
5d21ad1
refactor(global): combine common parts of API URLs
JaeBin2019 Jun 26, 2024
c42e117
feat(product): add get all product method
JaeBin2019 Jun 26, 2024
225aaf7
chore(product): rename global controller to view controller
JaeBin2019 Jun 26, 2024
ebc23b2
fix(global): change get products list api url
JaeBin2019 Jun 26, 2024
bf7afcd
chore(view): delete unnecssary parameter
JaeBin2019 Jun 27, 2024
ecc4ff9
feat(view): create product management UI
JaeBin2019 Jun 27, 2024
199c894
feat(product): create ServiceDto
JaeBin2019 Jun 27, 2024
a38cc6f
feat(product): add toServiceDto method to convert ProductRequest
JaeBin2019 Jun 27, 2024
4c57bf1
feat(product): change all method to use productService
JaeBin2019 Jun 27, 2024
20d14b7
feat(product): create Product Not Found Exception
JaeBin2019 Jun 27, 2024
dd3d0e5
feat(product): create Service Dto
JaeBin2019 Jun 27, 2024
fc49319
feat(product): create Product Service and methods
JaeBin2019 Jun 27, 2024
a944475
feat(product): add isNew method in product entity
JaeBin2019 Jun 28, 2024
56f5f57
chore(global): add data jdbc dependency
JaeBin2019 Jun 28, 2024
29cf8c4
feat(product): create product repository and methods
JaeBin2019 Jun 28, 2024
714baf2
feat(global): setting global exception handler
JaeBin2019 Jun 28, 2024
902f597
feat(global): setting db properties
JaeBin2019 Jun 28, 2024
71fb6ed
feat(product): create input initial data sql
JaeBin2019 Jun 28, 2024
b00ee6f
feat(product): create schema sql to make table products
JaeBin2019 Jun 28, 2024
1d4f2bb
fix(product): add default constructor and change directory entity to …
JaeBin2019 Jun 28, 2024
199a7ff
feat(global): create result code enum
JaeBin2019 Jun 28, 2024
b388095
feat(product): set result code of product api
JaeBin2019 Jun 28, 2024
8a22e26
feat(global): create result response
JaeBin2019 Jun 28, 2024
9410cc5
fix(global): change result code status type String to int
JaeBin2019 Jun 28, 2024
c3736c5
fix(global): change result response private to public
JaeBin2019 Jun 28, 2024
7bac451
chore(global): change ResultResponse name to ResultResponseDto
JaeBin2019 Jun 28, 2024
0759818
fix(http): update url for get all products
JaeBin2019 Jun 28, 2024
7e61e41
feat(global): create simple result response dto
JaeBin2019 Jun 28, 2024
3ef0273
chore(global): move global exception handler package response to handler
JaeBin2019 Jun 28, 2024
1d83a57
feat(product): change all method response to use ResponseEntity
JaeBin2019 Jun 28, 2024
c300531
fix(product): remove return value from create and update methods
JaeBin2019 Jun 28, 2024
77076be
feat(global): create response helper util
JaeBin2019 Jun 28, 2024
fd89619
refactor(product): apply ResponseHelper for response creation in Prod…
JaeBin2019 Jun 28, 2024
8fdd8ea
fix(product): change method name from isNew to checkNew to fix JSON s…
JaeBin2019 Jun 28, 2024
ebee7f4
fix(view): update index.html to use response.data for request parameter
JaeBin2019 Jun 28, 2024
d512a90
refactor(global): change resultCode variable to code in ResultCode enum
JaeBin2019 Jun 28, 2024
bedd0c2
feat(global): create ErrorCode enum
JaeBin2019 Jun 28, 2024
9d897c4
feat(global): create ErrorResponseDto record
JaeBin2019 Jun 28, 2024
39cddd4
fix(global): add private constructor to ResponseHelper class
JaeBin2019 Jun 28, 2024
18efb9c
refactor(global): update error handling to return ErrorResponseDto in…
JaeBin2019 Jun 28, 2024
025b774
refactor(product): rename ProductRequest to ProductRequestDto
JaeBin2019 Jun 28, 2024
52a232e
feat(view): add search product by productId
JaeBin2019 Jun 28, 2024
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
47 changes: 46 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,46 @@
# spring-gift-product
# spring-gift-product

---

## Step1

**요구 사항**<br>
아래와 같이 http 메세지를 받도록 구현한다

```http request
GET /api/products HTTP/1.1
```

```http request
HTTP/1.1 200
Content-Type: application/json

[
{
"id": 8146027,
"name": "아이스 카페 아메리카노 T",
"price": 4500,
"imageUrl": "https://st.kakaocdn.net/product/gift/product/20231010111814_9a667f9eccc943648797925498bdd8a3.jpg"
}
]
```


**필요 조건**<br>
상품 데이터 관리
현재는 별도의 데이터베이스가 없으므로 적절한 컬렉션을 이용하여 메모리에 저장한다.
```java
public class ProductController {
private final Map<Long, Product> productMap = new HashMap<>();
}
```
----
## 구현 기능
- product 조회
- product 추가
- product 수정
- product 삭제




1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ repositories {

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
runtimeOnly 'com.h2database:h2'
Expand Down
31 changes: 31 additions & 0 deletions src/main/http/product/Product.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
### get product list
GET http://localhost:8080/api/products
Content-Type: application/json

### get product
GET http://localhost:8080/api/products/1
Content-Type: application/json

### create product
POST http://localhost:8080/api/products
Content-Type: application/json

{
"name": "아메리카노",
"price": "4500",
"imageUrl": "http://hello"
}

### update product
PUT http://localhost:8080/api/products/1
Content-Type: application/json

{
"name": "아메리카노2",
"price": "5000",
"imageUrl": "http://hello"
}

### delete product
DELETE http://localhost:8080/api/products/1
Content-Type: application/json
21 changes: 21 additions & 0 deletions src/main/java/gift/global/exception/BusinessException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package gift.global.exception;

import gift.global.response.ErrorCode;

public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;

public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}

public ErrorCode getErrorCode() {
return errorCode;
}

@Override
public String toString() {
return errorCode.toString() + " : " + super.toString();
}
}
16 changes: 16 additions & 0 deletions src/main/java/gift/global/handler/GlobalExceptionHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package gift.global.handler;

import gift.global.exception.BusinessException;
import gift.global.response.ErrorResponseDto;
import gift.global.utils.ResponseHelper;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice

Choose a reason for hiding this comment

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

Advice랑 Handler 까지 구현하셨군요... 빠르시네요

Copy link
Author

Choose a reason for hiding this comment

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

혹시 ResultCode 에서 현재는 status 값을 int 로 관리하고 있는데,
이 값을 HttpStatus 로 관리하는 편이 더 좋을까요?

현재 status 값이 사용되는 곳은 ResponseEntity.status(resultCode.getStatus()) 이고

    public static BodyBuilder status(HttpStatusCode status) {
        Assert.notNull(status, "HttpStatusCode must not be null");
        return new DefaultBuilder(status);
    }

    public static BodyBuilder status(int status) {
        return new DefaultBuilder(status);
    }
------
     public DefaultBuilder(int statusCode) {
            this(HttpStatusCode.valueOf(statusCode));
        }

        public DefaultBuilder(HttpStatusCode statusCode) {
            this.headers = new HttpHeaders();
            this.statusCode = statusCode;
        }

------
    static HttpStatusCode valueOf(int code) {
        Assert.isTrue(code >= 100 && code <= 999, () -> {
            return "Status code '" + code + "' should be a three-digit positive integer";
        });
        HttpStatus status = HttpStatus.resolve(code);
        return (HttpStatusCode)(status != null ? status : new DefaultHttpStatusCode(code));
    }

과정을 거쳐서 ResponseEntity 가 생성되고 있습니다

  • int 로 관리하는 경우
  1. 코드 값의 역할에 대한 직관성이 떨어질 수 있다
  2. 불필요한 valueOf 작업이 필요하다
  • HttpStatus 로 관리하는 경우
  1. 코드 값에 대한 직관성이 분명하다
  2. 불필요한 함수 호출이 없다

=> 따라서, HttpStatus 로 수정하는 것이 좋다. 고 생각합니다

혹시, 이에 대해서는 어떻게 생각하시나요?

Choose a reason for hiding this comment

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

이 서비스 기준으로 정답은 사실 다양합니다. 왜냐하면 서비스가 크지 않은 상태에서 포맷팅은 쓸데 없이 코드가 복잡해지는 건 사실이거든요.
다만 현업 기준으로 가면 HttpStatus가 맞습니다. 메소드가 천 개가 넘어가는 서비스 안에서 HTTP Code를 int로 관리하면 문제가 많을 겁니다. 예를 들어 외부 통신 에러 ExternalException을 400으로 설정했다고 합니다. 만약 이 때 팀 차원에서 이것을 500으로 격상시킨다면 어떻게 될까요? 아 물론 예시가 적절치 않았을 수도 있지만, 기본적인 원칙은 그 메소드만 보고도 판단을 할 수 있게 한다. 를 기준으로 삼으면 좋을 거 같습니다.

Copy link
Author

Choose a reason for hiding this comment

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

넵 감사합니다

public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponseDto> handleBusinessException(BusinessException e) {
return ResponseHelper.createErrorResponse(e.getErrorCode());
}
}
28 changes: 28 additions & 0 deletions src/main/java/gift/global/response/ErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package gift.global.response;

public enum ErrorCode {
// Product
PRODUCT_NOT_FOUND_ERROR(400, "EP001", "Product Not Found Error"),
;
private final int status;
private final String code;
private final String message;

ErrorCode(int status, String code, String message) {
this.status = status;
this.code = code;
this.message = message;
}

public int getStatus() {
return status;
}

public String getCode() {
return code;
}

public String getMessage() {
return message;
}
}
7 changes: 7 additions & 0 deletions src/main/java/gift/global/response/ErrorResponseDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package gift.global.response;

public record ErrorResponseDto(String code, String message) {
public ErrorResponseDto(ErrorCode errorCode) {
this(errorCode.getCode(), errorCode.getMessage());
}
}
35 changes: 35 additions & 0 deletions src/main/java/gift/global/response/ResultCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package gift.global.response;

public enum ResultCode {
// Product
GET_ALL_PRODUCTS_SUCCESS(200, "P001", "모든 제품 조회 성공"),
GET_PRODUCT_BY_ID_SUCCESS(200, "P002", "단일 제품 조회 성공"),
CREATE_PRODUCT_SUCCESS(200, "P003", "제품 추가 성공"),
UPDATE_PRODUCT_SUCCESS(200, "P002", "제품 수정 성공"),
DELETE_PRODUCT_SUCCESS(200, "P002", "제품 삭제 성공"),

;

// status 를 HttpStatus 로 관리하는 것이 좋을까, 아니면 int로 관리하는 것이 좋을까?
private final int status;
private final String code;
private final String message;

ResultCode(int status, String code, String message) {
this.status = status;
this.code = code;
this.message = message;
}

public int getStatus() {
return status;
}

public String getCode() {
return code;
}

public String getMessage() {
return message;
}
}
8 changes: 8 additions & 0 deletions src/main/java/gift/global/response/ResultResponseDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package gift.global.response;

public record ResultResponseDto<T>(String code, String message, T data) {
// 자바 record 에서 생성자를 만들 경우, 반드시 canonical(표준) 생성자를 사용해야 한다!!
public ResultResponseDto(ResultCode resultCode, T data) {
this(resultCode.getCode(), resultCode.getMessage(), data);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package gift.global.response;

public record SimpleResultResponseDto(String code, String message) {
public SimpleResultResponseDto(ResultCode resultCode) {
this(resultCode.getCode(), resultCode.getMessage());
}
}
26 changes: 26 additions & 0 deletions src/main/java/gift/global/utils/ResponseHelper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package gift.global.utils;

import gift.global.response.*;
import org.springframework.http.ResponseEntity;

public class ResponseHelper {
private ResponseHelper() {}

public static <T> ResponseEntity<ResultResponseDto<T>> createResponse(ResultCode resultCode, T data) {
ResultResponseDto<T> resultResponseDto = new ResultResponseDto<>(resultCode, data);
return org.springframework.http.ResponseEntity.status(resultCode.getStatus())
.body(resultResponseDto);
}

public static ResponseEntity<SimpleResultResponseDto> createSimpleResponse(ResultCode resultCode) {
var resultResponseDto = new SimpleResultResponseDto(resultCode);
return ResponseEntity.status(resultCode.getStatus())
.body(resultResponseDto);
}

public static ResponseEntity<ErrorResponseDto> createErrorResponse(ErrorCode errorCode) {
ErrorResponseDto errorResponseDto = new ErrorResponseDto(errorCode);
return ResponseEntity.status(errorCode.getStatus())
.body(errorResponseDto);
}
}
53 changes: 53 additions & 0 deletions src/main/java/gift/product/controller/ProductController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package gift.product.controller;

import gift.global.response.ResultCode;
import gift.global.response.ResultResponseDto;
import gift.global.response.SimpleResultResponseDto;
import gift.global.utils.ResponseHelper;
import gift.product.dto.ProductRequestDto;
import gift.product.domain.Product;
import gift.product.service.ProductService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;

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

@GetMapping("")
public ResponseEntity<ResultResponseDto<List<Product>>> getAllProducts() {
List<Product> products = productService.getAllProducts();
return ResponseHelper.createResponse(ResultCode.GET_ALL_PRODUCTS_SUCCESS, products);
}

@GetMapping("/{id}")
public ResponseEntity<ResultResponseDto<Product>> getProductById(@PathVariable(name = "id") Long id) {
Product product = productService.getProductById(id);
return ResponseHelper.createResponse(ResultCode.GET_PRODUCT_BY_ID_SUCCESS, product);
}

@PostMapping("")
public ResponseEntity<SimpleResultResponseDto> createProduct(@RequestBody ProductRequestDto productRequestDto) {
productService.createProduct(productRequestDto.toServiceDto());
return ResponseHelper.createSimpleResponse(ResultCode.CREATE_PRODUCT_SUCCESS);
}

@PutMapping("/{id}")
public ResponseEntity<SimpleResultResponseDto> updateProduct(@PathVariable(name = "id") Long id, @RequestBody ProductRequestDto productRequestDto) {
productService.updateProduct(productRequestDto.toServiceDto(id));
return ResponseHelper.createSimpleResponse(ResultCode.UPDATE_PRODUCT_SUCCESS);
}

@DeleteMapping("/{id}")
public ResponseEntity<SimpleResultResponseDto> deleteProduct(@PathVariable(name = "id") Long id) {
productService.deleteProduct(id);
return ResponseHelper.createSimpleResponse(ResultCode.DELETE_PRODUCT_SUCCESS);
}
}
52 changes: 52 additions & 0 deletions src/main/java/gift/product/domain/Product.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package gift.product.domain;

import gift.product.dto.ProductRequestDto;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;

@Table("products")
public class Product {
@Id
private Long id;
private String name;
private int price;
private String imageUrl;

// JDBC 에서 엔티티 클래스를 인스턴스화할 때 반드시 기본 생성자와 파라미터 생성자가 필요하다
public Product() {}

public Product(Long id, String name, int price, String imageUrl) {
this.id = id;
this.name = name;
this.price = price;
this.imageUrl = imageUrl;
}

public Product(Long id, ProductRequestDto productRequestDto) {
this.id = id;
this.name = productRequestDto.name();
this.price = productRequestDto.price();
this.imageUrl = productRequestDto.imageUrl();
}


public Long getId() {
return id;
}

public String getName() {
return name;
}

public int getPrice() {
return price;
}

public String getImageUrl() {
return imageUrl;
}

public boolean checkNew() {
return id == null;
}
}
18 changes: 18 additions & 0 deletions src/main/java/gift/product/dto/ProductRequestDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package gift.product.dto;

import java.util.Objects;

public record ProductRequestDto(String name, int price, String imageUrl) {
public ProductRequestDto {
Objects.requireNonNull(name);
Objects.requireNonNull(imageUrl);
}

public ServiceDto toServiceDto() {
return new ServiceDto(null, this.name, this.price, this.imageUrl);
}

public ServiceDto toServiceDto(Long id) {
return new ServiceDto(id, this.name, this.price, this.imageUrl);
}
}
9 changes: 9 additions & 0 deletions src/main/java/gift/product/dto/ServiceDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package gift.product.dto;

import gift.product.domain.Product;

public record ServiceDto(Long id, String name, int price, String imageUrl) {
public Product toProduct() {
return new Product(id, name, price, imageUrl);
}
}
10 changes: 10 additions & 0 deletions src/main/java/gift/product/exception/ProductNotFoundException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package gift.product.exception;

import gift.global.exception.BusinessException;
import gift.global.response.ErrorCode;

public class ProductNotFoundException extends BusinessException {
public ProductNotFoundException() {
super(ErrorCode.PRODUCT_NOT_FOUND_ERROR);
}
}
Loading