프로젝트 끝 bb
오늘은 코드의 리팩토링을 진행했다.
JPA에서 제공하는 SQL을 추상화한 객체 지향 쿼리 언어
테이블을 대상으로 쿼리 하는 것이 아닌 엔티티 객체를 대상으로 쿼리한다.
또한, JPQL은 SQL을 추상화했기 때문에 특정 데이터베이스 SQL에 의존하지 않는 장점이 있다.
JPQL은 SQL과 문법이 유사하며, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN을 지원한다.
이러한 JPQL은 기본 문자열로 작성되기 때문에 컴파일 시 에러를 발생하지 않는다 -> 문제가 있음에도 불구하고 정상적으로 작동하여 배포 시 문제가 발생할 수 있음
난 [주문 취소하기] API를 도메인 로직으로 구현했었다.
Before
boolean isOwner = userDetails.getRole().equals(UserRoleEnum.OWNER)
&& order.getRestaurant().getOwner().getUserId().equals(userDetails.getUserId());
boolean isCustomer = userDetails.getRole().equals(UserRoleEnum.CUSTOMER)
&& order.getUser().getUserId().equals(userDetails.getUserId());
if (isOwner || UserRoleEnum.MANAGER.equals(userDetails.getRole())) {
//가게 주인이거나 매니저인 경우 취소 가능
order.orderCancel();
} else if (isCustomer) {
//주문 고객인 경우 5분 이내에만 취소 가능
if (order.getCreatedAt().isBefore(LocalDateTime.now().minusMinutes(5))) {
throw new CustomApiException(OrderException.INVALID_ORDER_CANCEL);
} else {
order.orderCancel();
}
}
return OrderResponseDto.fromEntity(order);
코테만 공부하다보니 이 코드가 최선이였다.....
체이닝이 많이 되어있는 코드는 가독성이 떨어지고 에러가 발생하면 어디서 생긴건지 파악하기 어렵다. 그렇기에 JPQL로 수정하였다.
After
[orderService]
//주문 취소하기
@Transactional
public OrderResponseDto cancelOrder(UUID orderId, CancelOrderRequestDto requestDto, UserDetailsImpl userDetails) {
Order order = orderRepository.findByOrderIdAndDeletedAtIsNull(orderId)
.orElseThrow(() -> new CustomApiException(OrderException.INVALID_ORDER_ID));
String role = userDetails.getRole().name();
UUID userId = userDetails.getUserId();
LocalDateTime minTime = LocalDateTime.now().minusMinutes(5);
OrderStatus cancelStatus = OrderStatus.CANCELED;
if (orderRepository.cancelOrder(orderId, role, userId, minTime, cancelStatus) == 0) {
throw new CustomApiException(OrderException.INVALID_ORDER_CANCEL);
}
return OrderResponseDto.fromEntity(order);
}
[orderRepository]
//주문 취소
@Modifying
@Query("UPDATE Order o " +
"SET o.orderStatus = :cancelStatus " +
"WHERE o.orderId = :orderId " +
"AND ( " +
" (:role = 'MANAGER') " +
" OR (:role = 'OWNER' AND o.restaurant.owner.userId = :userId) " +
" OR (:role = 'CUSTOMER' AND o.user.userId = :userId AND o.createdAt > :minTime) " +
")")
int cancelOrder(@Param("orderId") UUID orderId,
@Param("role") String role,
@Param("userId") UUID userId,
@Param("minTime") LocalDateTime minTime,
@Param("cancelStatus") OrderStatus cancelStatus);
-> int cancelOrder은 업데이트 쿼리 실행 후 업데이트된 행의 수를 반환한다.
만약 조건을 만족하는 주문이 업데이트되면, 업데이트된 행의 수가 1 이상이 될 것이다.
그러므로 0은 Exception 처리
가독성도 좋고 확장성도 고려한 코드 작성 방식을 배웠다.
@WithMockCustomUser(userId = "d2ed72d8-090a-4efb-abe4-7acbdce120e1", role = "CUSTOMER")
@DisplayName("주문 상세 조회 성공 200")
@Test
void searchOrderDetailByUser_200() throws Exception {
// given
UUID orderId = UUID.fromString("d8ef5ca7-2b3c-49bb-9c6d-425c85036de1"); // 테스트용 주문 ID
// when, then
mockMvc.perform(get("/api/orders/{order_id}", orderId)
.header("Authorization", "Bearer {ACCESS_TOKEN}")
)
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.result.orderId").value(orderId.toString()))
.andExpect(jsonPath("$.result.orderType").value("DELIVERY"))
.andExpect(jsonPath("$.result.orderResTotal").value(25500))
.andDo(print());
}
커스텀된 Authentication을 사용할 수 있는 어노테이션이다. 또한 중복되는 정보 세팅도 줄여준다.
그럼 여기서 드는 의문
@WithMockUser와 @WithMockCustomUser 차이는 무엇인고
@WithMockUser -> 기본 사용자 이름(username="user"), 역할(role="CUSTOMER") 등 기본 정보만 제공한다.
@WithMockCustomUser -> 추가로 사용자 ID나 프로젝트에서 정의한 커스텀 필드들을 설정할 수 있도록 한다.
=> 확장된 사용자 정보를 제공하기 위해 WithMockCustomUser 채택
프로젝트 기간이 어떻게 흘러간건지 모르겠다. 하지만 확실히 정말 많이 배웠다. 코테 공부할때도 코드가 좀 지저분해도 정답이면 넘어갔었는데 앞으로는 효율성과 시간 복잡도를 고려해서 코드를 작성해야겠다고 다짐했다.
어렵지만 많이 얻어갔던 프로젝트였고 좋은 팀이였다 암샼!