-
Notifications
You must be signed in to change notification settings - Fork 8
[6주차] 장바구니 만들기 #9
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
base: main
Are you sure you want to change the base?
Changes from all commits
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,48 @@ | ||||||||||||||||||||||||||||||||||||
| package com.example.demo.application; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| import com.example.demo.exception.CartNotFoundException; | ||||||||||||||||||||||||||||||||||||
| import com.example.demo.exception.ProductNotFoundException; | ||||||||||||||||||||||||||||||||||||
| import com.example.demo.model.Cart; | ||||||||||||||||||||||||||||||||||||
| import com.example.demo.model.LineItemId; | ||||||||||||||||||||||||||||||||||||
| import com.example.demo.model.ProductId; | ||||||||||||||||||||||||||||||||||||
| import com.example.demo.model.ProductOption; | ||||||||||||||||||||||||||||||||||||
| import com.example.demo.repository.CartRepository; | ||||||||||||||||||||||||||||||||||||
| import com.example.demo.repository.ProductRepository; | ||||||||||||||||||||||||||||||||||||
| import org.springframework.stereotype.Service; | ||||||||||||||||||||||||||||||||||||
| import org.springframework.transaction.annotation.Transactional; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| @Service | ||||||||||||||||||||||||||||||||||||
| @Transactional | ||||||||||||||||||||||||||||||||||||
| public class CartService { | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| private final CartRepository cartRepository; | ||||||||||||||||||||||||||||||||||||
| private final ProductRepository productRepository; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| public CartService(CartRepository cartRepository, ProductRepository productRepository) { | ||||||||||||||||||||||||||||||||||||
| this.cartRepository = cartRepository; | ||||||||||||||||||||||||||||||||||||
| this.productRepository = productRepository; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| public void addItemToCart(String userId, ProductId productId, ProductOption option, int quantity) { | ||||||||||||||||||||||||||||||||||||
| Cart cart = getCart(userId); | ||||||||||||||||||||||||||||||||||||
|
Member
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. 특별한 경우가 아니면 다른 곳과 마찬가지로 Repository에서 바로 얻을 겁니다. 별도의 메서드로 얻을 수 있는 혜택이 적고, 추상화 수준도 안 맞습니다. |
||||||||||||||||||||||||||||||||||||
| if (!productRepository.existsById(productId)) { | ||||||||||||||||||||||||||||||||||||
| throw new ProductNotFoundException(productId); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| cart.addProduct(productId, option, quantity); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+26
to
+32
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. 🛠️ Refactor suggestion 수량(quantity)에 대한 추가 검증이 필요합니다. public void addItemToCart(String userId, ProductId productId, ProductOption option, int quantity) {
Cart cart = getCart(userId);
if (!productRepository.existsById(productId)) {
throw new ProductNotFoundException(productId);
}
+ if (quantity <= 0) {
+ throw new IllegalArgumentException("수량은 0보다 커야 합니다.");
+ }
cart.addProduct(productId, option, quantity);
}📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| public void removeLineItem(String userId, LineItemId lineItemId) { | ||||||||||||||||||||||||||||||||||||
| Cart cart = getCart(userId); | ||||||||||||||||||||||||||||||||||||
| cart.removeLineItem(lineItemId); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+34
to
+37
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. 💡 Verification agent 🧩 Analysis chainLineItem이 존재하지 않을 경우의 예외 처리 확인이 필요합니다. 🏁 Script executed: #!/bin/bash
# LineItemNotFoundException 같은 맞춤 예외가 정의되어 있는지 검색
rg "LineItemNotFoundException"Length of output: 30 위 스크립트의 실행 결과를 공유해 주시거나 수동으로 해당 메서드 내부에 예외 처리를 위한 코드(예: throw 문)가 포함되어 있는지 확인해 주세요. 🏁 Script executed: #!/bin/bash
# Cart 클래스 내의 removeLineItem 메서드 구현부에서 예외 처리(throw 등) 여부를 확인하기 위한 검색
rg -A 15 "public void removeLineItem" -n .Length of output: 1883 예외 처리 추가 필요 현재
도메인 규칙에 따라 LineItem이 존재하지 않을 경우 예외를 발생시켜야 한다면, 해당 메서드에 적절한 예외 처리 로직(예: |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| public void clearCart(String userId) { | ||||||||||||||||||||||||||||||||||||
| Cart cart = getCart(userId); | ||||||||||||||||||||||||||||||||||||
| cart.clearItems(); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| public Cart getCart(String userId) { | ||||||||||||||||||||||||||||||||||||
| return cartRepository.findByUserId(userId) | ||||||||||||||||||||||||||||||||||||
| .orElseThrow(CartNotFoundException::new); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,65 @@ | ||
| package com.example.demo.controllers; | ||
|
|
||
| import com.example.demo.application.CartService; | ||
| import com.example.demo.controllers.dto.CartResponseDto; | ||
| import com.example.demo.controllers.dto.LineItemResponseDto; | ||
| import com.example.demo.model.Cart; | ||
| import com.example.demo.util.AuthorizationUtils; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.DeleteMapping; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.RequestHeader; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| import static com.example.demo.util.AuthorizationUtils.extractUserIdFromAuthorization; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/cart") | ||
| public class CartController { | ||
|
|
||
| private final CartService cartService; | ||
|
|
||
| public CartController(CartService cartService) { | ||
| this.cartService = cartService; | ||
| } | ||
|
|
||
|
|
||
| @GetMapping | ||
| String detail() { | ||
| return ""; | ||
| public ResponseEntity<CartResponseDto> getCart( | ||
| @RequestHeader(name = "Authorization", required = false) | ||
|
Member
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. 아주 특수한 경우가 아니면 Controller에서 ReqeustHeader를 쓸 일이 없습니다. 대개는 더 앞에서 Filter나 Intercept로 처리합니다. 인증/인가는 매우 보편적이라 보다 앞에서 공통으로 처리하는 게 필요합니다. 지금은 임시 구현을 너무 구체적으로 하려고 하는데, 이도 저도 아닌 힘 빼기가 될 위험이 있습니다. 설계에 더 집중해 주세요. 결론보다 과정이 더 중요합니다. 말 없이 코드만 계속 바꾸는 건 안 좋은 신호예요. |
||
| String authorization) { | ||
|
|
||
| String userId = extractUserIdFromAuthorization(authorization); | ||
|
Member
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. 이 부분은 인증을 도입하지 않은 상태에서 어정쩡하게 가져가는 게 문제입니다.
실제로 인증 처리를 하고 싶다면 인증을 넣어야 하고, 그게 아니면 설계를 고려해서 임의의 코드를 써줘야 합니다. 제가 기대했던 건 후자에 대해 다음 둘 중 하나를 하는 거였습니다.
애매하게 UI Layer에서 Cart ID를 하드코딩하면 나중에 진짜 구현이 들어왔을 때 다 갈아엎어야 해서 그 부분을 이야기하고 싶었어요. 마찬가지로, 지금 코드는 인증이 들어오면 싹 다 갈아엎어야 합니다. 앞으로 변화에 대해 최소한의 수정으로 대응하는 것, 유지보수성이 높아지는 것, 이런 게 관심사의 분리가 주는 혜택이죠.
Member
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. 인증이 없는 상태에서는 그 상황을 고려해서 코드를 써야 하는데, 이 부분은 대개 Filter나 Intercept를 써서 처리하고, Contoroller 코드는 이런 모양이 됩니다. 꼭 이 모양이어야 하는 건 아니고, 여러 방법이 있지만 그냥 눈에 띄는 거 하나 아무거나 가져온 겁니다. 다시 강조하지만, 여기서 인증 처리가 필요한 건 아닙니다. 각 Layer의 책임 범위를 명확히 하고, 유지보수성이 높은 설계와 코드를 작성하는 게 필요하다는 겁니다. 하드코딩을 하더라도 최소한의 수정으로 처리하려면, 올바르게 배치 = 설계하는 게 필요합니다. 구현은 그 다음 문제죠. 구현부터 가면 계속 늪으로 빠질 수 있으니 고민하고 있는 부분을 먼저 공유해 주세요. 이미 많은 사람들이 만든 베스트 프랙티스가 많기 때문에 독자연구로 빠질 필요가 없습니다. |
||
| Cart cart = cartService.getCart(userId); | ||
|
|
||
| CartResponseDto responseDto = new CartResponseDto( | ||
| cart.getTotalQuantity(), | ||
| cart.getLineItems().stream() | ||
| .map(lineItem -> new LineItemResponseDto( | ||
| lineItem.getProductId().id(), | ||
| lineItem.getProductOption().color(), | ||
| lineItem.getProductOption().size(), | ||
| lineItem.getQuantity() | ||
| ) | ||
| ).toList() | ||
| ); | ||
|
|
||
| return new ResponseEntity<>(responseDto, HttpStatus.OK); | ||
|
Member
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. ResponseEntity는 영원히 안 써도 됩니다. 아주 옛날에는 특수한 상황에서 쓸 일이 있기도 했지만, 지금은 안 쓰는 걸 강력히 추천합니다. |
||
| } | ||
|
|
||
|
|
||
| @DeleteMapping() | ||
| public ResponseEntity<Void> deleteCart( | ||
| @RequestHeader(name = "Authorization", required = false) | ||
| String authorization) { | ||
|
|
||
| String userId = extractUserIdFromAuthorization(authorization); | ||
|
|
||
| cartService.clearCart(userId); | ||
| return new ResponseEntity<>(HttpStatus.NO_CONTENT); | ||
| } | ||
|
|
||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,62 @@ | ||
| package com.example.demo.controllers; | ||
|
|
||
| import com.example.demo.application.CartService; | ||
| import com.example.demo.controllers.dto.LineItemRequestDto; | ||
| import com.example.demo.exception.UnauthorizedException; | ||
| import com.example.demo.model.Cart; | ||
| import com.example.demo.model.LineItemId; | ||
| import com.example.demo.model.ProductId; | ||
| import com.example.demo.model.ProductOption; | ||
| import jakarta.validation.Valid; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.web.bind.annotation.DeleteMapping; | ||
| import org.springframework.web.bind.annotation.PathVariable; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestBody; | ||
| import org.springframework.web.bind.annotation.RequestHeader; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.ResponseStatus; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| import static com.example.demo.util.AuthorizationUtils.extractUserIdFromAuthorization; | ||
|
|
||
|
|
||
| @RestController | ||
| @RequestMapping("/cart/line-items") | ||
| public class LineItemController { | ||
|
|
||
| public LineItemController(CartService cartService) { | ||
| this.cartService = cartService; | ||
| } | ||
|
|
||
| private final CartService cartService; | ||
|
|
||
| @PostMapping | ||
| @ResponseStatus(HttpStatus.CREATED) | ||
| void create() { | ||
| // | ||
| public void create( | ||
| @Valid @RequestBody LineItemRequestDto requestDto, | ||
| @RequestHeader("Authorization") String authorization) { | ||
|
|
||
| String userId = extractUserIdFromAuthorization(authorization); | ||
|
|
||
| cartService.addItemToCart( | ||
| userId, | ||
| new ProductId(requestDto.productId()), | ||
| new ProductOption(requestDto.color(), requestDto.size()), | ||
| requestDto.quantity()); | ||
|
|
||
| } | ||
|
|
||
| @DeleteMapping("/{lineItemId}") | ||
| @ResponseStatus(HttpStatus.NO_CONTENT) | ||
| public void deleteLineItem( | ||
| @PathVariable String lineItemId, | ||
| @RequestHeader("Authorization") String authorization) { | ||
|
|
||
| String userId = extractUserIdFromAuthorization(authorization); | ||
|
|
||
| cartService.removeLineItem( | ||
| userId, | ||
| new LineItemId(lineItemId)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package com.example.demo.controllers.dto; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public record CartResponseDto( | ||
| int totalQuantity, | ||
| List<LineItemResponseDto> lineItems | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package com.example.demo.controllers.dto; | ||
|
|
||
| import jakarta.validation.constraints.Max; | ||
| import jakarta.validation.constraints.Min; | ||
| import jakarta.validation.constraints.NotBlank; | ||
|
|
||
| public record LineItemRequestDto( | ||
|
|
||
| @NotBlank(message = "상품 ID는 필수입니다.") | ||
| String productId, | ||
|
|
||
| @NotBlank(message = "색상은 필수입니다.") | ||
| String color, | ||
|
|
||
| @NotBlank(message = "사이즈는 필수입니다.") | ||
| String size, | ||
|
|
||
| @Min(value = 1, message = "최소 수량은 1개입니다.") | ||
| @Max(value = 20, message = "최대 수량은 20개입니다.") | ||
| int quantity | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.example.demo.controllers.dto; | ||
|
|
||
| public record LineItemResponseDto( | ||
| String productId, | ||
| String color, | ||
| String size, | ||
| int quantity | ||
| ) { | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.example.demo.exception; | ||
|
|
||
| public class CartNotFoundException extends RuntimeException { | ||
| public CartNotFoundException() { | ||
| super("장바구니를 찾을 수 없습니다."); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.example.demo.exception; | ||
|
|
||
| public class CartQuantityLimitException extends RuntimeException { | ||
| public CartQuantityLimitException() { | ||
| super("장바구니에 담을 수 있는 수량을 초과했습니다."); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.example.demo.exception; | ||
|
|
||
|
|
||
| public record ErrorResponseDto( | ||
| String code, | ||
| String message | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package com.example.demo.exception; | ||
|
|
||
|
|
||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.MediaType; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.ExceptionHandler; | ||
| import org.springframework.web.bind.annotation.RestControllerAdvice; | ||
|
|
||
| @RestControllerAdvice | ||
| public class GlobalExceptionHandler { | ||
|
|
||
| @ExceptionHandler(CartNotFoundException.class) | ||
| public ResponseEntity<ErrorResponseDto> handleCartNotFound(CartNotFoundException e) { | ||
| ErrorResponseDto responseDto = new ErrorResponseDto("CART_NOT_FOUND", e.getMessage()); | ||
| return new ResponseEntity<>(responseDto, HttpStatus.NOT_FOUND); | ||
| } | ||
|
|
||
| @ExceptionHandler(ProductNotFoundException.class) | ||
| public ResponseEntity<ErrorResponseDto> handleProductNotFound(ProductNotFoundException e) { | ||
| ErrorResponseDto responseDto = new ErrorResponseDto("PRODUCT_NOT_FOUND", e.getMessage()); | ||
| return new ResponseEntity<>(responseDto, HttpStatus.NOT_FOUND); | ||
| } | ||
|
|
||
| @ExceptionHandler(CartQuantityLimitException.class) | ||
| public ResponseEntity<ErrorResponseDto> handleCartQuantityLimit(CartQuantityLimitException e) { | ||
| ErrorResponseDto responseDto = new ErrorResponseDto("CART_QUANTITY_LIMIT", e.getMessage()); | ||
| return ResponseEntity | ||
| .status(HttpStatus.BAD_REQUEST) | ||
| .contentType(MediaType.APPLICATION_JSON) | ||
| .body(responseDto); | ||
| } | ||
|
|
||
| @ExceptionHandler(UnauthorizedException.class) | ||
| public ResponseEntity<ErrorResponseDto> handleUnauthorized(UnauthorizedException e) { | ||
| ErrorResponseDto responseDto = new ErrorResponseDto("UNAUTHORIZED", e.getMessage()); | ||
| return new ResponseEntity<>(responseDto, HttpStatus.UNAUTHORIZED); | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package com.example.demo.exception; | ||
|
|
||
| import com.example.demo.model.ProductId; | ||
|
|
||
| public class ProductNotFoundException extends RuntimeException { | ||
| public ProductNotFoundException(ProductId productId) { | ||
| super("상품을 찾을 수 없습니다. ID: " + productId.id()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.example.demo.exception; | ||
|
|
||
| public class UnauthorizedException extends RuntimeException { | ||
| public UnauthorizedException() { | ||
| super("인증이 필요합니다."); | ||
| } | ||
| } |

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.
본격적으로 하실 거면 UserId VO를 잡아주세요.