Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
37 changes: 13 additions & 24 deletions src/main/java/com/example/demo/application/CartService.java
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;

Expand All @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

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

본격적으로 하실 거면 UserId VO를 잡아주세요.

Cart cart = getCart(userId);
Copy link
Member

Choose a reason for hiding this comment

The 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);
}
}
29 changes: 23 additions & 6 deletions src/main/java/com/example/demo/controllers/CartController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The 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);
Copy link
Member

Choose a reason for hiding this comment

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

이 부분은 인증을 도입하지 않은 상태에서 어정쩡하게 가져가는 게 문제입니다.

  1. Bearer엔 Access Token이 들어가지 User ID가 들어가지 않습니다.
  2. 인증 처리가 먼저 이뤄지고, Controller에선 이렇게 복잡하게 처리하지 않습니다.

실제로 인증 처리를 하고 싶다면 인증을 넣어야 하고, 그게 아니면 설계를 고려해서 임의의 코드를 써줘야 합니다. 제가 기대했던 건 후자에 대해 다음 둘 중 하나를 하는 거였습니다.

  1. UI Layer에서 User ID를 하드코딩.
  2. Application Layer에서 Cart ID를 하드코딩.

애매하게 UI Layer에서 Cart ID를 하드코딩하면 나중에 진짜 구현이 들어왔을 때 다 갈아엎어야 해서 그 부분을 이야기하고 싶었어요.

마찬가지로, 지금 코드는 인증이 들어오면 싹 다 갈아엎어야 합니다. 앞으로 변화에 대해 최소한의 수정으로 대응하는 것, 유지보수성이 높아지는 것, 이런 게 관심사의 분리가 주는 혜택이죠.

Copy link
Member

Choose a reason for hiding this comment

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

인증이 없는 상태에서는 그 상황을 고려해서 코드를 써야 하는데, 이 부분은 대개 Filter나 Intercept를 써서 처리하고, Contoroller 코드는 이런 모양이 됩니다. 꼭 이 모양이어야 하는 건 아니고, 여러 방법이 있지만 그냥 눈에 띄는 거 하나 아무거나 가져온 겁니다.

Screenshot

다시 강조하지만, 여기서 인증 처리가 필요한 건 아닙니다. 각 Layer의 책임 범위를 명확히 하고, 유지보수성이 높은 설계와 코드를 작성하는 게 필요하다는 겁니다. 하드코딩을 하더라도 최소한의 수정으로 처리하려면, 올바르게 배치 = 설계하는 게 필요합니다. 구현은 그 다음 문제죠.

구현부터 가면 계속 늪으로 빠질 수 있으니 고민하고 있는 부분을 먼저 공유해 주세요. 이미 많은 사람들이 만든 베스트 프랙티스가 많기 때문에 독자연구로 빠질 필요가 없습니다.

Cart cart = cartService.getCart(userId);

return new CartResponseDto(
CartResponseDto responseDto = new CartResponseDto(
cart.getTotalQuantity(),
cart.getLineItems().stream()
.map(lineItem -> new LineItemResponseDto(
Expand All @@ -37,12 +45,21 @@ public CartResponseDto getCart() {
)
).toList()
);

return new ResponseEntity<>(responseDto, HttpStatus.OK);
Copy link
Member

Choose a reason for hiding this comment

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


}
19 changes: 16 additions & 3 deletions src/main/java/com/example/demo/controllers/LineItemController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

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;
Expand All @@ -11,10 +13,13 @@
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")
Expand All @@ -29,9 +34,13 @@ public LineItemController(CartService cartService) {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void create(
@Valid @RequestBody LineItemRequestDto requestDto) {
@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());
Expand All @@ -40,10 +49,14 @@ public void create(

@DeleteMapping("/{lineItemId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteLineItem(@PathVariable String lineItemId) {
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,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("인증이 필요합니다.");
}
}
14 changes: 12 additions & 2 deletions src/main/java/com/example/demo/model/Cart.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.example.demo.model;

import com.example.demo.exception.CartQuantityLimitException;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
Expand All @@ -20,6 +22,9 @@ public class Cart {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private String userId;

@OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true)
private List<LineItem> lineItems = new ArrayList<>();

Expand All @@ -28,7 +33,12 @@ public class Cart {
public Cart() {
}

public Cart(List<LineItem> lineItems) {
public Cart(String userId) {
this.userId = userId;
}

public Cart(String userId, List<LineItem> lineItems) {
this.userId = userId;
this.lineItems = new ArrayList<>(lineItems);
updateTotalQuantity();
}
Expand Down Expand Up @@ -66,7 +76,7 @@ private void updateTotalQuantity() {
.mapToInt(LineItem::getQuantity)
.sum();
if (totalQuantity > 20) {
throw new IllegalArgumentException("담을수 있는 수량을 초과했습니다.");
throw new CartQuantityLimitException();
}
}

Expand Down
5 changes: 1 addition & 4 deletions src/main/java/com/example/demo/model/LineItem.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@
import jakarta.persistence.Embedded;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;

@Entity
@Table(name="line_items")
@Table(name = "line_items")
public class LineItem {

@EmbeddedId
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/example/demo/repository/CartRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@
import com.example.demo.model.Cart;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface CartRepository extends JpaRepository<Cart, Long> {
Optional<Cart> findByUserId(String userId);
}
17 changes: 17 additions & 0 deletions src/main/java/com/example/demo/util/AuthorizationUtils.java
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);
}

}
Loading