-
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 1 commit
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 |
|---|---|---|
| @@ -1,12 +1,13 @@ | ||
| 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 jakarta.servlet.http.HttpSession; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
|
|
@@ -17,43 +18,31 @@ public class CartService { | |
| private final CartRepository cartRepository; | ||
| private final ProductRepository productRepository; | ||
|
|
||
| private final HttpSession session; | ||
|
|
||
| public CartService(CartRepository cartRepository, ProductRepository productRepository, HttpSession session) { | ||
| public CartService(CartRepository cartRepository, ProductRepository productRepository) { | ||
| this.cartRepository = cartRepository; | ||
| this.productRepository = productRepository; | ||
| this.session = session; | ||
| } | ||
|
|
||
| public void addItemToCart(ProductId productId, ProductOption option, int quantity) { | ||
| Cart cart = getCart(); | ||
| 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 IllegalArgumentException("상품이 존재하지 않습니다."); | ||
| throw new ProductNotFoundException(productId); | ||
| } | ||
| cart.addProduct(productId, option, quantity); | ||
| } | ||
|
|
||
| public void removeLineItem(LineItemId lineItemId) { | ||
| Cart cart = getCart(); | ||
| public void removeLineItem(String userId, LineItemId lineItemId) { | ||
| Cart cart = getCart(userId); | ||
| cart.removeLineItem(lineItemId); | ||
| } | ||
|
|
||
| public void clearCart() { | ||
| Cart cart = getCart(); | ||
| public void clearCart(String userId) { | ||
| Cart cart = getCart(userId); | ||
| cart.clearItems(); | ||
| } | ||
|
|
||
| public Cart getCart() { | ||
| Long cartId = (Long) session.getAttribute("CART_ID"); | ||
|
|
||
| if (cartId != null) { | ||
| return cartRepository.findById(cartId) | ||
| .orElseThrow(() -> new IllegalArgumentException("장바구니가 존재하지 않습니다.")); | ||
| } | ||
|
|
||
| Cart cart = new Cart(); | ||
| Cart savedCart = cartRepository.save(cart); | ||
| session.setAttribute("CART_ID", savedCart.getId()); | ||
| return savedCart; | ||
| public Cart getCart(String userId) { | ||
| return cartRepository.findByUserId(userId) | ||
| .orElseThrow(CartNotFoundException::new); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,29 +4,37 @@ | |
| 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; | ||
| } | ||
|
|
||
| private final CartService cartService; | ||
|
|
||
| @GetMapping | ||
| public CartResponseDto getCart() { | ||
| 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) { | ||
|
|
||
| Cart cart = cartService.getCart(); | ||
| 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); | ||
|
|
||
| return new CartResponseDto( | ||
| CartResponseDto responseDto = new CartResponseDto( | ||
| cart.getTotalQuantity(), | ||
| cart.getLineItems().stream() | ||
| .map(lineItem -> new LineItemResponseDto( | ||
|
|
@@ -37,12 +45,21 @@ public CartResponseDto getCart() { | |
| ) | ||
| ).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() { | ||
| cartService.clearCart(); | ||
| 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 |
|---|---|---|
| @@ -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("인증이 필요합니다."); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.example.demo.util; | ||
|
|
||
| import com.example.demo.exception.UnauthorizedException; | ||
|
|
||
| public class AuthorizationUtils { | ||
|
|
||
| private AuthorizationUtils() { | ||
| } | ||
|
|
||
| public static String extractUserIdFromAuthorization(String authorization) { | ||
| if (authorization == null || !authorization.startsWith("Bearer ")) { | ||
| throw new UnauthorizedException(); | ||
| } | ||
| return authorization.substring(7); | ||
| } | ||
|
|
||
| } |

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를 잡아주세요.