상품
상품 상세 보기 Mock
GET http://localhost:8080/products/:id
JSON
{
"success": true,
"response": {
"id": 1,
"productName": "기본에 슬라이딩 지퍼백 크리스마스/플라워에디션 에디션 외 주방용품 특가전",
"description": "",
"image": "/images/1.jpg",
"price": 1000,
"starCount": 5,
"options": [
{
"id": 1,
"optionName": "01. 슬라이딩 지퍼백 크리스마스에디션 4종",
"price": 10000
},
{
"id": 2,
"optionName": "02. 슬라이딩 지퍼백 플라워에디션 5종",
"price": 10900
},
{
"id": 3,
"optionName": "고무장갑 베이지 S(소형) 6팩",
"price": 9900
},
{
"id": 4,
"optionName": "뽑아쓰는 키친타올 130매 12팩",
"price": 16900
},
{
"id": 5,
"optionName": "2겹 식빵수세미 6매",
"price": 8900
}
]
},
"error": null
}
상품 상세 정보 DTO와 option DTO 2개가 필요하다.
상품 상세 보기 DTO
@Getter @Setter
public class ProductRespFindByIdDTO {
private int id;
private String productName;
private String description;
private String image;
private int price;
private int starCount; // 0~5
private List<ProductOptionDTO> options;
@Builder
public ProductRespFindByIdDTO(int id, String productName, String description, String image, int price, int starCount, List<ProductOptionDTO> options) {
this.id = id;
this.productName = productName;
this.description = description;
this.image = image;
this.price = price;
this.starCount = starCount;
this.options = options;
}
}
optionDTO
@Getter @Setter
public class ProductOptionDTO {
private int id;
private String optionName;
private int price;
@Builder
public ProductOptionDTO(int id, String optionName, int price) {
this.id = id;
this.optionName = optionName;
this.price = price;
}
}
ProductController
@RestController
public class ProductRestController {
@GetMapping("/products/{id}")
public ResponseEntity<?> findById(@PathVariable int id) {
// 상품을 담을 DTO 생성
ProductRespFindByIdDTO responseDTO = null;
if(id == 1) {
List<ProductOptionDTO> optionDTOList = new ArrayList<>();
optionDTOList.add(
ProductOptionDTO.builder()
.id(1)
.optionName("01. 슬라이딩 지퍼백 크리스마스에디션 4종")
.price(10000)
.build());
optionDTOList.add(ProductOptionDTO.builder()
.id(2)
.optionName("02. 슬라이딩 지퍼백 플라워에디션 5종")
.price(10900)
.build());
optionDTOList.add(ProductOptionDTO.builder()
.id(3)
.optionName("고무장갑 베이지 S(소형) 6팩")
.price(9900)
.build());
optionDTOList.add(ProductOptionDTO.builder()
.id(4)
.optionName("뽑아쓰는 키친타올 130매 12팩")
.price(16900)
.build());
optionDTOList.add(ProductOptionDTO.builder()
.id(5)
.optionName("2겹 식빵수세미 6매")
.price(8900)
.build());
responseDTO = ProductRespFindByIdDTO.builder()
.id(1)
.productName("기본에 슬라이딩 지퍼백 크리스마스/플라워에디션 에디션 외 주방용품 특가전")
.description("")
.image("/images/1.jpg")
.price(1000)
.starCount(5)
.options(optionDTOList)
.build();
}else if(id == 2){
List<ProductOptionDTO> optionDTOList = new ArrayList<>();
optionDTOList.add(ProductOptionDTO.builder()
.id(6)
.optionName("22년산 햇단밤 700g(한정판매)")
.price(9900)
.build());
optionDTOList.add(ProductOptionDTO.builder()
.id(7)
.optionName("22년산 햇단밤 1kg(한정판매)")
.price(14500)
.build());
optionDTOList.add(ProductOptionDTO.builder()
.id(8)
.optionName("밤깎기+다회용 구이판 세트")
.price(5500)
.build());
responseDTO = ProductRespFindByIdDTO.builder()
.id(1)
.productName("[황금약단밤 골드]2022년산 햇밤 칼집밤700g외/군밤용/생율")
.description("")
.image("/images/2.jpg")
.price(2000)
.starCount(5)
.options(optionDTOList)
.build();
}else { //id가 1,2가 아닐 경우
return ResponseEntity.badRequest().body(ApiUtils.error("해당 상품을 찾을 수 없습니다 : " + id, HttpStatus.BAD_REQUEST));
}
return ResponseEntity.ok(ApiUtils.success(responseDTO));
}
}
가짜 데이터를 넣어 응답을 생성한다.
Builder 패턴으로 수행했다.
Mock Test
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
public class ProductRestControllerTest {
@Autowired
private MockMvc mvc;
@Test
@DisplayName("개별 상품 상세 조회")
public void findById_test() throws Exception {
// given
int id = 1;
// when
ResultActions resultActions = mvc.perform(
get("/products/" + id)
);
String responseBody = resultActions.andReturn().getResponse().getContentAsString();
System.out.println("테스트 : " + responseBody);
// verify
resultActions.andExpect(jsonPath("$.success").value("true"));
resultActions.andExpect(jsonPath("$.response.id").value(1));
resultActions.andExpect(jsonPath("$.response.productName").value("기본에 슬라이딩 지퍼백 크리스마스/플라워에디션 에디션 외 주방용품 특가전"));
resultActions.andExpect(jsonPath("$.response.description").value(""));
resultActions.andExpect(jsonPath("$.response.image").value("/images/1.jpg"));
resultActions.andExpect(jsonPath("$.response.price").value(1000));
resultActions.andExpect(jsonPath("$.response.options[0].id").value(1));
resultActions.andExpect(jsonPath("$.response.options[0].optionName").value("01. 슬라이딩 지퍼백 크리스마스에디션 4종"));
resultActions.andExpect(jsonPath("$.response.options[0].price").value(10000));
resultActions.andExpect(jsonPath("$.response.options[1].id").value(2));
resultActions.andExpect(jsonPath("$.response.options[1].optionName").value("02. 슬라이딩 지퍼백 플라워에디션 5종"));
resultActions.andExpect(jsonPath("$.response.options[1].price").value(10900));
}
}
장바구니(cart)
장바구니 목록 보기(조회) Mock
GET http://localhost:8080/carts
JSON
{
"success": true,
"response": {
"products": [
{
"id": 1,
"productName": "기본에 슬라이딩 지퍼백 크리스마스/플라워에디션 에디션 외 주방용품 특가전",
"carts": [
{
"id": 33,
"option": {
"id": 1,
"optionName": "01. 슬라이딩 지퍼백 크리스마스에디션 4종",
"price": 10000
},
"quantity": 5,
"price": 50000
},
{
"id": 34,
"option": {
"id": 2,
"optionName": "02. 슬라이딩 지퍼백 플라워에디션 5종",
"price": 10900
},
"quantity": 5,
"price": 54500
}
]
},
{
"id": 2,
"productName": "[황금약단밤 골드]2022년산 햇밤 칼집밤700g외/군밤용/생율",
"carts": [
{
"id": 44,
"option": {
"id": 6,
"optionName": "22년산 햇단밤 700g(한정판매)",
"price": 9900
},
"quantity": 8,
"price": 79200
},
{
"id": 45,
"option": {
"id": 7,
"optionName": "22년산 햇단밤 1kg(한정판매)",
"price": 14500
},
"quantity": 5,
"price": 72500
}
]
}
],
"totalPrice": 434300
},
"error": null
}
구조는 다음과 같다.
- products 리스트
- id
- productName
- carts 리스트
- id
- option
- id
- optionName
- price
- quantity
- price
- totalPrice
크게 4개의 dto로 이루어져있다. 바깥에서부터 4개의 dto만 만들면 된다.
CartRespFindAllDTO.java
@Getter @Setter
public class CartRespFindAllDTO {
private List<ProductDTO> products;
private int totalPrice;
@Builder
public CartRespFindAllDTO(List<ProductDTO> products, int totalPrice) {
this.products = products;
this.totalPrice = totalPrice;
}
맨 바깥의 dto이다.
ProductDTO.java
@Getter @Setter
public class ProductDTO {
private int id;
private String productName;
private List<CartItemDTO> cartItems;
@Builder
public ProductDTO(int id, String productName, List<CartItemDTO> cartItems) {
this.id = id;
this.productName = productName;
this.cartItems = cartItems;
}
}
✅ JSON에서 carts이지만 cartItems로 바뀌었다.
CartItemDTO.java
@Getter @Setter
public class CartItemDTO {
private int id;
private ProductOptionDTO option;
private int quantity;
private int price;
@Builder
public CartItemDTO(int id, ProductOptionDTO option, int quantity, int price) {
this.id = id;
this.option = option;
this.quantity = quantity;
this.price = price;
}
}
ProductOptionDTO.java
@Getter @Setter
public class ProductOptionDTO {
private int id;
private String optionName;
private int price;
@Builder
public ProductOptionDTO(int id, String optionName, int price) {
this.id = id;
this.optionName = optionName;
this.price = price;
}
}
CartRestController
@RestController
public class CartRestController {
@GetMapping("/carts")
public ResponseEntity<?> findAll() {
// 카트 아이템 리스트 만들기
List<CartItemDTO> cartItemDTOList = new ArrayList<>();
// 카트 아이템 리스트에 담기
CartItemDTO cartItemDTO1 = CartItemDTO.builder()
.id(4)
.quantity(5)
.price(50000)
.build();
cartItemDTO1.setOption(ProductOptionDTO.builder()
.id(1)
.optionName("01. 슬라이딩 지퍼백 크리스마스에디션 4종")
.price(10000)
.build());
cartItemDTOList.add(cartItemDTO1);
CartItemDTO cartItemDTO2 = CartItemDTO.builder()
.id(5)
.quantity(5)
.price(54500)
.build();
cartItemDTO2.setOption(ProductOptionDTO.builder()
.id(1)
.optionName("02. 슬라이딩 지퍼백 크리스마스에디션 5종")
.price(10900)
.build());
cartItemDTOList.add(cartItemDTO2);
// productDTO 리스트 만들기
List<ProductDTO> productDTOList = new ArrayList<>();
// productDTO 리스트에 담기
productDTOList.add(
ProductDTO.builder()
.id(1)
.productName("기본에 슬라이딩 지퍼백 크리스마스/플라워에디션 에디션 외 주방용품 특가전")
.cartItems(cartItemDTOList)
.build()
);
CartRespFindAllDTO responseDTO = new CartRespFindAllDTO(productDTOList, 104500);
return ResponseEntity.ok(ApiUtils.success(responseDTO));
}
}
안에서부터 바깥까지 데이터를 차곡차곡 집어넣는다. (builder 패턴 이용)
안 <----------------------------------------------------------------> 밖
ProductOptionDTO / CartItemDTO.java / ProductDTO / CartRespFindAllDTO
😀 위 코드에서는 CartItemDTO를 먼저 만들고 ProductOptionDTO를 만들어 집어넣었다. 그 이후 순서는 순서대로 만들어 집어넣었다.
CartRestControllerTest
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
public class CartRestControllerTest {
@Autowired
private MockMvc mvc;
@Test
@WithMockUser
@DisplayName("장바구니 조회")
public void findAll_test() throws Exception {
// when
ResultActions resultActions = mvc.perform(
get("/carts")
);
String responseBody = resultActions.andReturn().getResponse().getContentAsString();
System.out.println("테스트 : " + responseBody);
// verify
resultActions.andExpect(jsonPath("$.success").value("true"));
resultActions.andExpect(jsonPath("$.response.totalPrice").value(104500));
resultActions.andExpect(jsonPath("$.response.products[0].id").value(1));
resultActions.andExpect(jsonPath("$.response.products[0].productName").value("기본에 슬라이딩 지퍼백 크리스마스/플라워에디션 에디션 외 주방용품 특가전"));
resultActions.andExpect(jsonPath("$.response.products[0].cartItems[0].id").value(4));
resultActions.andExpect(jsonPath("$.response.products[0].cartItems[0].option.id").value(1));
resultActions.andExpect(jsonPath("$.response.products[0].cartItems[0].option.optionName").value("01. 슬라이딩 지퍼백 크리스마스에디션 4종"));
resultActions.andExpect(jsonPath("$.response.products[0].cartItems[0].option.price").value(10000));
resultActions.andExpect(jsonPath("$.response.products[0].cartItems[0].quantity").value(5));
resultActions.andExpect(jsonPath("$.response.products[0].cartItems[0].price").value(50000));
}
}
예상데이터와 일치하는지 비교한다.
API별로 DTO 분리
API에서 각각의 기능별로 패키지를 만들어 DTO를 생성하였다.
위 사진을 보면 cart/response/ProductOptionDTO와 product/response/ProductOptionDTO가 있는데,
두 DTO는 같은 코드이다. 하지만 후에 달라질 수 있으므로 중복되더라도 분리하여 생성한다.
✳️ DTO 중복을 줄이는 방법
DTO를 Inner Static Class로 만드는 방법이다.
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
public class Product {
@Getter
@AllArgsConstructor
@Builder
public static class Info {
private int id;
private String name;
private int price;
}
@Getter
@Setter
public static class Request {
private String name;
private int price;
}
@Getter
@AllArgsConstructor
public static class Response {
private Info info;
private int returnCode;
private String returnMessage;
}
}
한 클래스 파일로 DTO 여러개를 묶어 구성할 수 있다.
장바구니 담기
JSON Parse 오류
HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `co m.example.kakaoshop.cart.CartRestController$AddCartDTO ` from Array value (token `JsonToken.START_ARRAY`); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException:Cannot deserialize value of type `co m.example.kakaoshop.cart.CartRestController$AddCartDTO ` from Array value (token `JsonToken.START_ARRAY`)\n at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 1]\n\tat org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType
JSON으로 받은 데이터를 해당 객체로 변환(역직렬화)할 수 없어 발생한 오류이다.
위 오류의 원인은 요청으로 보낸 데이터에 있다.
[
{
"optionId":1,
"quantity":5
},
{
"optionId":2,
"quantity":5
}
]
요청으로 보낸 데이터는 List인데, @RequestBody는 기본적으로 JSON 데이터를 받기때문에 오류가 발생한 것이다.
RequestBody에서 List로 요청보내는 방법
List 타입 데이터를 받기 위해서는 객체를 List로 묶어 List<DTO> 요청을 받으면 된다.
@PostMapping("/add") // /carts/add
public ResponseEntity<?> addCart(@RequestBody List<CartDTO> request) { //리스트 형식으로 요청받음
return ResponseEntity.ok(ApiUtils.success(null));
}
또한 리스트로 요청 데이터를 보낼때는 Content-Type을 application/json 타입으로 해도 요청이 잘 되는 것을 볼 수 있다.
DTO + Controller
@RestController
@RequestMapping("/carts")
public class CartRestController {
//장바구니 담기
@PostMapping("/add") // /carts/add
public ResponseEntity<?> addCart(@RequestBody List<CartDTO> request) { //리스트 형식으로 요청받음
return ResponseEntity.ok(ApiUtils.success(null));
}
@Data
static class CartDTO{
private int optionId;
private int quantity;
@Builder
public CartDTO(int optionId, int quantity) {
this.optionId = optionId;
this.quantity = quantity;
}
}
}
✅ dto를 Inner static class로 작성하여 dto가 너무 많아지는 것을 방지했다.
✅ 생성자 레벨 Builder를 사용했다.
@Builder 클래스 레벨 생성 vs 생성자 레벨 생성
클래스 레벨 Builder : 클래스 위에 @Builder를 붙이며, 클래스에서 가능한 모든 필더에 대해 빌더 메서드 생성
생성자 레벨 Builder : 생성자의 파라미터 필드에 대해서만 빌더 메서드 생성 - 생성자에 존재하지 않은 필드는 빌더 메서드 생성X
생성자 레벨 Builder를 추천한다.
생성자 레벨 Builder를 추천하는 이유
클래스 레벨 Builder는 @AllArgsConstructor와 같은 효과(모든 멤버 필드에 대해서 매개변수를 받는 기본 생성자)를 준다.
=> 빌더로 생성되지 않아야 할 매개변수들도 빌더에 노출될 수 있다.
=> 생성자의 접근 레벨이 default가 되어 동일 패키지 내에서 생성자가 호출될 수 있다.
생성자 레벨 Builder를 추천하는 이유
클래스 레벨 Builder는 @AllArgsConstructor(모든 멤버필드에 대해 매개변수를 받는 기본 생성자)와 같은 효과를 발생시킨다.
즉, Builder로 생성되어야하지 않을 매개변수(ex) timestamp)들도 builder에 노출된다.
또한, 생성자의 접근 레벨이 default가 되므로 동일 패키지 내에서 생성자가 호출될 수 있다.
테스트 코드
@Test
@WithMockUser
@DisplayName("장바구니 담기")
public void addCart_test() throws Exception {
//given
List<CartRestController.CartDTO> cartDTOList = new ArrayList<>();
CartRestController.CartDTO cartDTO1 = new CartRestController.CartDTO(1,5);
CartRestController.CartDTO cartDTO2 = new CartRestController.CartDTO(2,5);
cartDTOList.add(cartDTO1);
cartDTOList.add(cartDTO2);
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(cartDTOList);
System.out.println(requestData);
// when
ResultActions resultActions = mvc.perform(
post("/carts/add")
.contentType(MediaType.APPLICATION_JSON)
.content(requestData)
);
String responseBody = resultActions.andReturn().getResponse().getContentAsString();
System.out.println("테스트 : " + responseBody);
// verify
resultActions.andExpect(jsonPath("$.success").value("true"));
resultActions.andExpect(jsonPath("$.response").doesNotExist()); //null인지 확인
resultActions.andExpect(jsonPath("$.error").doesNotExist());
}
null 검증 코드
//resultActions.andExpect(jsonPath("$.error").value("null")); //null 비교 에러
resultActions.andExpect(jsonPath("$.response").doesNotExist()); //null인지 확인
resultActions.andExpect(jsonPath("$.error").doesNotExist());
null인지 확인하기위해 jsonPath의 반환값인 JsonPathResultMatchers의 value함수를 이용하여 검증하면 아래와 같은 오류가 발생한다.
그러므로 null인지 검증하기 위해 JsonPathResultMatchers의 doesNotExist()를 사용하자.
doesNotExist()
null이 아닌 값이 존재하지 않는지 검증한다. (즉 null만 존재하면 통과)
장바구니 수정
DTO + Controller
@RestController
@RequestMapping("/carts")
public class CartRestController {
//장바구니 수정
@PostMapping("/update") // /carts/update
public ResponseEntity<?> updateCart(@RequestBody List<CartUpdateRequestDTO> request) {
// 카트 Info 리스트 만들기
List<CartInfoDTO> cartInfoDTOList = new ArrayList<>();
// 카트 Info 리스트에 담기
CartInfoDTO cartInfoDTO1 = CartInfoDTO.builder()
.cartId(4)
.optionId(1)
.optionName("01. 슬라이딩 지퍼백 크리스마스에디션 4종")
.quantity(10)
.price(100000)
.build();
cartInfoDTOList.add(cartInfoDTO1);
CartInfoDTO cartInfoDTO2 = CartInfoDTO.builder()
.cartId(5)
.optionId(2)
.optionName("02. 슬라이딩 지퍼백 플라워에디션 5종")
.quantity(10)
.price(109000)
.build();
cartInfoDTOList.add(cartInfoDTO2);
//responseDTO 만들기
CartUpdateResponseDTO responseDTO = CartUpdateResponseDTO.builder()
.carts(cartInfoDTOList)
.totalPrice(209000)
.build();
return ResponseEntity.ok(ApiUtils.success(responseDTO));
}
@Data
static class CartUpdateRequestDTO{
private int cartId;
private int quantity;
@Builder
public CartUpdateRequestDTO(int cartId, int quantity) {
this.cartId = cartId;
this.quantity = quantity;
}
}
@Data
static class CartUpdateResponseDTO{
private List<CartInfoDTO> carts;
private int totalPrice;
@Builder
public CartUpdateResponseDTO(List<CartInfoDTO> carts, int totalPrice) {
this.carts = carts;
this.totalPrice = totalPrice;
}
}
@Data
static class CartInfoDTO{
private int cartId;
private int optionId;
private String optionName;
private int quantity;
private int price;
@Builder
public CartInfoDTO(int cartId, int optionId, String optionName, int quantity, int price) {
this.cartId = cartId;
this.optionId = optionId;
this.optionName = optionName;
this.quantity = quantity;
this.price = price;
}
}
}
Mock Test
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@DisplayName("장바구니 수정")
public class CartRestControllerTest {
@Autowired
private MockMvc mvc;
@Test
@WithMockUser
// 장바구니 수정
public void updateCart_test() throws Exception {
//given
List<CartRestController.CartUpdateRequestDTO> cartList = new ArrayList<>();
CartRestController.CartUpdateRequestDTO cartDTO1 = new CartRestController.CartUpdateRequestDTO(4,10);
CartRestController.CartUpdateRequestDTO cartDTO2 = new CartRestController.CartUpdateRequestDTO(5,10);
cartList.add(cartDTO1);
cartList.add(cartDTO2);
//JSON 문자열로 변환
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(cartList);
// when
ResultActions resultActions = mvc.perform(
post("/carts/update")
.contentType(MediaType.APPLICATION_JSON)
.content(requestData)
);
String responseBody = resultActions.andReturn().getResponse().getContentAsString();
System.out.println("테스트 : " + responseBody);
// verify
resultActions.andExpect(jsonPath("$.success").value("true"));
resultActions.andExpect(jsonPath("$.response.totalPrice").value(209000));
resultActions.andExpect(jsonPath("$.response.carts[0].cartId").value(4));
resultActions.andExpect(jsonPath("$.response.carts[0].optionId").value(1));
resultActions.andExpect(jsonPath("$.response.carts[0].optionName").value("01. 슬라이딩 지퍼백 크리스마스에디션 4종"));
resultActions.andExpect(jsonPath("$.response.carts[0].quantity").value(10));
resultActions.andExpect(jsonPath("$.response.carts[0].price").value(100000));
resultActions.andExpect(jsonPath("$.response.carts[1].cartId").value(5));
resultActions.andExpect(jsonPath("$.response.carts[1].optionId").value(2));
resultActions.andExpect(jsonPath("$.response.carts[1].optionName").value("02. 슬라이딩 지퍼백 플라워에디션 5종"));
resultActions.andExpect(jsonPath("$.response.carts[1].quantity").value(10));
resultActions.andExpect(jsonPath("$.response.carts[1].price").value(109000));
resultActions.andExpect(jsonPath("$.error").doesNotExist());
}
}
주문(Order)
1. 주문 결과 확인
DTO+Controller
@RestController
@RequestMapping("/orders")
public class OrderRestController {
@GetMapping("/{id}")
public ResponseEntity<?> findById(@PathVariable int id) {
// 주문을 담을 DTO 생성
OrderRespFindByIdDTO responseDTO = null;
if(id == 1) {
//ItemInfo 담을 리스트 생성
List<ItemInfoDTO> itemInfoDTOList = new ArrayList<>();
//ItemInfo 리스트에 담기
ItemInfoDTO itemInfoDTO1 = ItemInfoDTO.builder()
.id(4)
.optionName("01. 슬라이딩 지퍼백 크리스마스에디션 4종")
.quantity(10)
.price(100000)
.build();
itemInfoDTOList.add(itemInfoDTO1);
ItemInfoDTO itemInfoDTO2 = ItemInfoDTO.builder()
.id(5)
.optionName("02. 슬라이딩 지퍼백 플라워에디션 5종")
.quantity(10)
.price(109000)
.build();
itemInfoDTOList.add(itemInfoDTO2);
//ProductItem 리스트 만들기
List<ProductItemDTO> productItemDTOList = new ArrayList<>();
ProductItemDTO productItemDTO1 = ProductItemDTO.builder()
.productName("기본에 슬라이딩 지퍼백 크리스마스/플라워에디션 에디션 외 주방용품 특가전")
.items(itemInfoDTOList)
.build();
//ProductItem 리스트에 담기
productItemDTOList.add(productItemDTO1);
//응답할 dto 생성
responseDTO = OrderRespFindByIdDTO.builder()
.id(2)
.products(productItemDTOList)
.totalPrice(209000)
.build();
}
else { //id가 1이 아닌 경우
return ResponseEntity.badRequest().body(ApiUtils.error("해당 주문을 찾을 수 없습니다 : " + id, HttpStatus.BAD_REQUEST));
}
return ResponseEntity.ok(ApiUtils.success(responseDTO));
}
@Data
static class OrderRespFindByIdDTO{
private int id;
private List<ProductItemDTO> products;
private int totalPrice;
@Builder
public OrderRespFindByIdDTO(int id, List<ProductItemDTO> products, int totalPrice) {
this.id = id;
this.products = products;
this.totalPrice = totalPrice;
}
}
@Data
static class ProductItemDTO{
private String productName;
private List<ItemInfoDTO> items;
@Builder
public ProductItemDTO(String productName, List<ItemInfoDTO> items) {
this.productName = productName;
this.items = items;
}
}
@Data
static class ItemInfoDTO{
private int id;
private String optionName;
private int quantity;
private int price;
@Builder
public ItemInfoDTO(int id, String optionName, int quantity, int price) {
this.id = id;
this.optionName = optionName;
this.quantity = quantity;
this.price = price;
}
}
}
Mock Test
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@DisplayName("주문 결과 확인")
class OrderRestControllerTest {
@Autowired
private MockMvc mvc;
@Test
@WithMockUser //인증된 사용자 생성
// 주문 결과 확인
public void findById_test() throws Exception {
// given
int id = 1;
// when
ResultActions resultActions = mvc.perform(
get("/orders/" + id)
);
String responseBody = resultActions.andReturn().getResponse().getContentAsString();
System.out.println("테스트 : " + responseBody);
// verify
resultActions.andExpect(jsonPath("$.success").value("true"));
resultActions.andExpect(jsonPath("$.response.id").value(2));
resultActions.andExpect(jsonPath("$.response.totalPrice").value(209000));
resultActions.andExpect(jsonPath("$.response.products[0].productName").value("기본에 슬라이딩 지퍼백 크리스마스/플라워에디션 에디션 외 주방용품 특가전"));
resultActions.andExpect(jsonPath("$.response.products[0].items[0].id").value(4));
resultActions.andExpect(jsonPath("$.response.products[0].items[0].optionName").value("01. 슬라이딩 지퍼백 크리스마스에디션 4종"));
resultActions.andExpect(jsonPath("$.response.products[0].items[0].quantity").value(10));
resultActions.andExpect(jsonPath("$.response.products[0].items[0].price").value(100000));
resultActions.andExpect(jsonPath("$.response.products[0].items[1].id").value(5));
resultActions.andExpect(jsonPath("$.response.products[0].items[1].optionName").value("02. 슬라이딩 지퍼백 플라워에디션 5종"));
resultActions.andExpect(jsonPath("$.response.products[0].items[1].quantity").value(10));
resultActions.andExpect(jsonPath("$.response.products[0].items[1].price").value(109000));
}
}
주문은 회원만 가능하므로, @WithMockUser를 사용하여 인증된 상태로 테스트하도록 했다.
Q&A
💙 Q1. 행위에 가까운 API에 대한 Restful 설계
- 로그인, 로그아웃, 회원가입은 자원보다는 행위에 가까운 API인데, Restful하게 API를 설계하는 방법이 궁금합니다.
- /login, /logout은 동사인데 이렇게 URI를 작성하는 것이 Restful한지 궁금합니다.
멘토님 👨💻 :
Restful 하다는 것은 REST를 잘 지킨다는 의미이지, 반드시 모든 규칙을 따를 필요는 없습니다.
- REST의 목적은 리소스를 명확하게 인식하고 직관적으로 파악하기 쉬운 uri를 만드는 것입니다. 그러므로 REST 규칙을 어기더라도 좀 더 자원을 명확하게 정의할 수 있다면 써도 됩니다.
- login과 join 등은 단어 자체로 직관적으로 파악하기 쉬우므로 사용하는 것이 좋습니다.
- 규칙을 지키는 것보다 어겨서 얻는 이득이 더 크다면(tradeoff), 그렇게 사용해도 됩니다.
💙 Q2. 화면설계 - 톡딜가
- 톡딜가 부분은 구현을 하지 않는다고 들었는데, 백엔드 설계시 JSON 응답과 데이터베이스 product table에 price로 포함되어있습니다.
멘토님 👨💻 :
- 프론트에서 톡딜가를 띄우도록 만들었기 때문에, price를 통해 가격을 설정해준 것입니다. 톡딜가가 어떤 로직으로 설정되는지 등은 신경쓰지 않아도 되므로 price에는 임의로 가격을 넣어주면 될 것 같습니다.
💙 Q3. 같은 Entity에 대한 DTO 생성 반복
- API 명세에 맞춰 DTO를 생성하다보니, 같은 Entity로 부터 조금 달라도 DTO를 여러 개 생성해야 했습니다. 이 방향이 맞는지 궁금합니다.
멘토님 👨💻 :
- DTO를 생성하는 이유는 응답 형태에 맞는 적절한 응답을 생성하고, Entity에 접근하지 않기 위해서입니다. 개인적인 시각으로는 DTO가 많아지는 게 나쁘지 않다고 생각합니다.
- 하지만 생성한 DTO들을 상황에 따라 분리하는 것이 필요합니다.
참고
'Spring > 카테캠 - TIL' 카테고리의 다른 글
TIL[0712] : 3주차 강의 (DTO, 스트림, HTTP, JDBC, JPA) (0) | 2023.07.10 |
---|---|
TIL[0706] : 2주차 과제 - Mock Controller 3(마무리) (0) | 2023.07.07 |
TIL [0705] : 2주차 과제 -1. Restful API, 2. Mock Controller 일부 (0) | 2023.07.05 |
카테캠 : 1주차 코드리뷰 (0) | 2023.07.04 |
TIL [0704] - 2주차 강의 2 : Spring Security (0) | 2023.07.04 |