1
build.gradle 에 implementation 'org.springframework.boot:spring-boot-starter-validation' 가 중복 선언 되어 있습니다.
해결
여러 사람이 동시에 validation 을 추가하고 merge 하는 과정에서 두 커밋 모두를 반영해서 생긴 문제인듯 해서 삭제 처리
2
코드 중복이 보였습니다. 해당 코드가 Admin Service 와 Order Service 에서 완전히 동일하게 존재합니다.
public void isActiveAdmin(Admin admin){
//isLoginable 은 활성(Active) 상태에서만 true 니까 활성상태가 아니라면 throw
if(!admin.getStatus().isLoginable()){
switch (admin.getStatus()) {
case PENDING -> throw new ServiceException(ErrorCode.ADMIN_PENDING); // "계정 승인대기 중"
case REJECTED -> throw new ServiceException(ErrorCode.ADMIN_REJECTED); // "계정 신청 거부됨"
case STOPPED -> throw new ServiceException(ErrorCode.ACCOUNT_STOPPED); // "계정 정지됨"
case INACTIVE -> throw new ServiceException(ErrorCode.ACCOUNT_INACTIVE); // "계정 비활성화됨"
default -> throw new ServiceException(ErrorCode.FORBIDDEN_ADMIN);
}
}
}
해결
해당 부분은 사실 고민을 많이 했는데 조별과제다 보니까 새로 클래스를 만들어서 이제 해당 클래스를 DI 해서 사용하기 애매해서 그냥 모두 코드에 넣었다... 튜터님께는 비밀이지만 사실 comments 를 제외한 모든 곳에 해당 함수를 넣어뒀었다 ㅎㅎ...
admin service 로 옮기거나 admin entity 에 확인하는 로직을 넣는 방법을 생각해봤다.
admin entity 는 건드리지 않는 게 좋다고 해서 ( by 캡틴길중) service 를 새로 만들기로 했다

새로 service 를 만들고 아래처럼 가져와서 사용해줬다

중복코드 삭제 완료!
추가로 getUser 등등... 의 내용도 사실 계속 중복이라 가져와서 쓰고싶었다...
객체를 받아오는 부분을 서비스계층에서 작성해서 가져와서 쓴다면 한 Service 가 해당하는 Repository 만 접근할 수 있으니까 더 편할듯 싶었는데 MSA 는 그런느낌으로 접근하는게 아니라는 얘기를 들었기 때문에..... 조금 더 고민해봐야 할 것 같다.
3
Error Code 에 중복된 코드 값이 상당 수 존재 합니다. E013 과 E018, E009 가 그 값들인데,
에러 코드는 목적이 어떤 에러인지 클라이언트가 정확하게 식별하게 해주는 것이므로, 유니크한 코드를 가지는게 일반적입니다.
기능 적으로 문제는 없겠으나 아쉬움이 남습니다.
해결
이 부분 역시 여러 사람이 동시에 에러코드를 추가하고 merge 하는 과정에서 뭔가 하나를 삭제할 수 없어서 errorCode 가 같더라도 그냥 다 merge 한 결과물인듯 하다..... ㅎ
사용되지 않는 에러코드는 삭제하고, 순서를 맞추고 정리했다

깔끔하긴 한데 현재 INVALID STATUS 가 너무많은 오류를 담당하고 있어서 다른 오류들을 잘게 나눈 의미가 없어지는 것 같아서 아쉽다... 일단 할 수 있는대로 Invalid_Status 에 모호한 의미가 있다면 바꿔줬다

원래 계정관련 오류코드는 Admin 용으로만 사용했었는데 정지, 비활성 고객을 체크하는 부분이 있어서 해당 부분에 계정관련 오류를 새로 넣었다

Cancel 부분도 FORBIDDEN 오류로 수정
수정하고나니까 여전히 InvalidStatus 에 너무 많은 값이 들어가있지만 다 찾아봐도 ... 정말 Invalid Status 오류가 떠야할 것만 같은... 애들이라서 그냥 뒀다
4
주문 상태 전환에 관해서, SHIPPING 으로 전환하는 메서드나 엔드포인트가 없습니다.
public void deliverCompleted(Long orderId, ...) {
order.updateStatus(OrderStatus.DELIVERED);
}
현재 이렇게 만 구현이 되어 있는데요, 해당 코드는 이전 상태 값을 확인하지 않고, PREPARING 에서도 바로 DELIVERED 로 변경 될 수 있게 구현되어 있습니다.
해결
만들기
# 컨트롤러 코드
@PreAuthorize("hasAnyRole('SUPER_ADMIN', 'OP_ADMIN', 'CS_ADMIN')")
@PatchMapping("/admins/orders/{id}/shipping")
ResponseEntity<CommonResponseDTO<String>> deliverShipping(
@PathVariable("id") Long orderId,
@AuthenticationPrincipal UserPrincipal userPrincipal){
orderService.deliverShipping(orderId, userPrincipal);
return CommonResponseHandler.success(SuccessCode.DATA_UPDATED, "배송이 시작되었습니다.");
}
# 서비스 코드
# 배송중 처리
@Transactional
public void deliverShipping(Long orderId, UserPrincipal userPrincipal) {
a.isActiveAdmin(getAdminById(userPrincipal.getId()));
Order order = getOrderById(orderId);
order.updateStatus(OrderStatus.SHIPPING);
}
배송 완료 처리와 정말 정말 유사해서 금방 만들 수 있었다
생각해보니까 DeliverStatusUpdate 로 묶는게 더 좋을 것 같아서 다시 수정
@Getter
public class UpdateDelieverStatusRequest {
@NotBlank(message = "업데이트 될 상태값은 필수입니다.")
private String orderStatus;
}
request DTO 를 하나 만들고
@Transactional
public void deliverStatusUpdate(
Long orderId, UserPrincipal userPrincipal, UpdateDelieverStatusRequest request) {
a.isActiveAdmin(getAdminById(userPrincipal.getId()));
Order order = getOrderById(orderId);
OrderStatus status = OrderStatus.from(request.getOrderStatus());
if (order.getOrderStatus() == status){
# 이미 해당 상태면 변경할 필요가 없음
throw new ServiceException(ErrorCode.INVALID_STATUS);
}
if (order.getOrderStatus().getLevel()>status.getLevel()){
# 더 이전 단계로 변경할 수 없음
throw new ServiceException(ErrorCode.INVALID_STATUS);
}
order.updateStatus(status);
}
service 코드에서 가능한 값으로의 변경인지 체크
PREPARING("준비",1),
SHIPPING("배송",2),
DELIVERED("배송 완료",3),
CANCELED("취소됨",0);
이전 상태로 돌아갈 수 없도록 Enum 값에도 level 을 추가
Controller 도 requestbody 를 받도록 수정해줬다
하면서 약간 후회했던 점은... 상품 상태 관련한 Enum 은 더 추가될 것 같지 않은데 그냥 처음 Shipping 으로 바꾸기, Delivered 로 바꾸기를 유지해도 되었을 것 같다
불필요한 DTO 가 생성되고 String 을 enum 타입으로 바꾸고 해당 상태인지 확인하고, 바꿀 수 있는 상태인지도 또 확인하는 과정이 번거롭게 느껴졌다. 물론 상태확인은 위 코드에서도 해줘야하는데 빼먹은 감이 있지만...!!!
알아서 사용하면 될 듯 싶다
5
주문 에 관해서, Order Entity의 cancel method 를 보면, INVALID_STATUS 로 처리되고 있습니다.
ErrorCoder 를 보면 주문을 취소할 수 없는 상황에 대하여 CANCEL_FORBIDDEN 이 좀 더 정확한 에러 코드였을 것으로 보입니다.
해결
Order order = getOrderById(orderId);
if (order.getOrderStatus() != OrderStatus.PREPARING){
throw new ServiceException(ErrorCode.CANCEL_FORBIDDEN);
}
수정 완료!
6
주문 취소에서 method 이름이 cancelByAdmin 인데 고객과 관리자가 공용으로 사용하고 있습니다. method 이름 수정이 필요해 보입니다.
해결
생각해보니 cancel 에서 열심히 관리자인지 고객인지 구분하는 메서드를 넣었는데 왜 메서드 이름은 안 고쳤지? ㅎ...
수정해줬다

7
NullPointerException 이 발생할 수 있는 에러 처리가 있습니다.
GlobalExceptionHandler 의 38번째 줄을 보면
String errorMessage = e.getBindingResult().getFieldError().getDefaultMessage();
이러한 부분이 있는데,
@Valid 유효성 검사 실패 중 필드 에러가 아닌 경우 (ex/ 클래스 레벨 @AssertTrue, 타입 변환 실패 등)에는 getFieldError()가 null을 반환합니다.
이 경우 .getDefaultMessage()에서 NPE가 터지고, 의도했던 400 응답 대신 500 응답이 클라이언트에게 내려갑니다.
8
DataIntegrityViolationException 메시지가 과일반화 되었는데요,
@ExceptionHandler(org.springframework.dao.DataIntegrityViolationException.class)
public ResponseEntity<ErrorResponse> handleDataIntegrityViolationException(
org.springframework.dao.DataIntegrityViolationException e, HttpServletRequest request) {
log.warn("DataIntegrityViolationException : {}", e.getMessage());
// "해당 작업을 수행할 수 없는 상태입니다(E017)" 코드를 사용합니다.
ErrorCode errorCode = ErrorCode.UNABLE_TO_WORK_STATUS;
return ResponseEntity
.status(errorCode.getStatus())
.body(buildErrorResponse(
errorCode,
"주문 내역이 존재하는 상품은 삭제할 수 없습니다. 대신 단종 처리를 이용해 주세요.",
request.getRequestURI()
));
}
DataIntegrityViolationException은 FK 위반 뿐 아니라 unique 제약 조건 위반, not null 위반 등 다양한 DB 오류에서 모두 발생합니다. 예를 들어 이메일 중복 체크를 서비스에서 하더라도 동시 요청 상황에서는 DB 레벨에서 unique 위반이 발생해 이 핸들러가 잡을 수 있는데, 그때 "주문 내역이 존재하는 상품은 삭제할 수 없습니다"라는 맥락에 맞지 않는 메시지가 나갑니다.
해결
찾아봤는데 DataIntegrityViolation Exception 부분을 안 가져온 것 같다...
이후 조원이 피드백을 보고 수정했다고 들었는데 수정 이후의 코드를 가져온듯해서 이론만 정리해두기로 했다
일단 튜터님이 하는 말은 DataIntegrityViolationException 이 엄청 넓은 범위의 오류를 잡는 예외인데 메시지가 주문내역이 존재하는 상품은 삭제할 수 없다는 메시지를 보내는 것 같다
즉 발생할 수 있는 여러 오류 중 특정 상황 하나만 설정하고 있어서 다른 DB 오류가 발생해도 주문내역과 관련된 메시지만 출력
-> 예외 범위는 넓은데 메시지가 특정 상황에만 맞게 작성
메시지를 수정할 수 있다면 아래처럼 수정하면 될 것 같다
"주문 내역이 존재하는 상품은 삭제할 수 없습니다. 대신 단종 처리를 이용해 주세요."
->
"데이터 무결성 제약조건 위반"
9
Access Denied Exception 에 관한 충돌이 보입니다.
SecurityConfig와 GlobalExceptionHandler가 모두 AccessDeniedException을 처리합니다.
그런데, 다른 형식으로 응답이 나가게 되고 있습니다.
같은 "권한 없음" 에러이므로 응답 형식도 일치 시켜주면 좋을 듯 합니다.
해결
private ErrorResponse buildErrorResponse(ErrorCode errorCode, String message, String path) {
return ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(errorCode.getStatus().value())
.error(errorCode.getStatus().name())
.code(errorCode.getCode())
.message(message)
.path(path)
.build();
}
buildErrorResponse 를 SecurityConfig class 에도 생성하고 내부에서 buildErrorResponse 형식에 맞게 반환하도록 설정
.exceptionHandling(exception -> exception
.authenticationEntryPoint((request, response, authException) -> {
ErrorCode errorCode = ErrorCode.BEFORE_LOGIN;
ErrorResponse errorResponse = buildErrorResponse(
errorCode,
errorCode.getMessage(),
request.getRequestURI()
);
response.setStatus(errorCode.getStatus().value());
response.setContentType("application/json;charset=UTF-8");
objectMapper.writeValue(response.getWriter(), errorResponse);
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
ErrorCode errorCode = ErrorCode.ADMIN_FORBIDDEN;
ErrorResponse errorResponse = buildErrorResponse(
errorCode,
errorCode.getMessage(),
request.getRequestURI()
);
response.setStatus(errorCode.getStatus().value());
response.setContentType("application/json;charset=UTF-8");
objectMapper.writeValue(response.getWriter(), errorResponse);
})
)
Spring Security 에서는 직접 "\n 같은 형식을 통해 작성하다가 이제 ErrorResponse 를 사용하도록 바꿨고
objectMapper 를 통해 JSON 형태까지 챙겨서 GlobalException Handler 에서 사용하던 형식을 Spring Security 에서도 사용할 수 있도록 수정
10
9번에서 말씀드린 부분 뿐 아니라, 현재 에러 처리 경로가 3군데인데,
Global Exception Handler , Security Config, JwtFilter 에서 각각 에러 상황에 대해 다른 형태의 응답이 나갑니다. 이렇게 되면 프론트엔드에서 에러 파싱이 어려워집니다.
해결
3개의 에러처리 경로에서 모두 같은 형식을 사용하도록 수정
private void sendErrorResponse(HttpServletRequest request,
HttpServletResponse response,
ErrorCode errorCode,
String message) throws IOException {
ErrorResponse errorResponse = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(errorCode.getStatus().value())
.error(errorCode.getStatus().getReasonPhrase())
.code(errorCode.getCode())
.message(message)
.path(request.getRequestURI())
.build();
response.setStatus(errorCode.getStatus().value());
response.setContentType("application/json;charset=UTF-8");
objectMapper.writeValue(response.getWriter(), errorResponse);
}

사용하는 곳에서도 바꿔주었다
-> 이제 어느 부분에서 오류가 발생하더라도 (Controller, Filter.. 등등) 똑같은 ErrorResponse 포맷으로 결과값이 출력된다
11
500 에러 발생 시 스택 트레이스가 로그에 안 찍히는 부분이 있습니다.
log.error("Exception : {}", e.getMessage());
500 에러에 대해서 이런 식으로 로그를 출력하는데요,
예상치 못한 500 에러가 발생했을 때 e.getMessage()만 로깅하면 어느 파일 몇 번째 줄에서 에러가 났는지 전혀 알 수 없습니다.
실제 디버깅이 거의 불가능한 수준입니다. 아래처럼 예외 객체 자체를 전달해야 스택 트레이스가 출력 됩니다.
log.error("Unhandled exception", e);
해결
튜터님이 알려주신대로 수정!

회고
이번 과제를 진행하면서 API 명세서와 팀원 간 소통의 중요성을 크게 느낄 수 있었다...
특히 내가 작성하지 않은 코드들에 대해 이해를 할 수 없었고
수정하려고 할 때 마다 의도나 흐름을 파악하는데 시간소요가 너무 컸다 (코드를 직접 작성하는 것 보다 오래 걸린듯 하다)
ErrorCode 등 여러 조원이 함께 쓰는 클래스의 요소나 build.gradle 같은 공통적으로 사용하는 클래스에 대해서 임의로 변경하거나 수정했을 때 수정하기 번거로웠기 때문에 해당 부분에 대해서도 팀 회의때 정해두는 게 좋을 것 같다고 느낌
이번 프로젝트에서는 JWT와 Filter 관련 기능을 다른 팀원이 주로 담당했는데
해당 코드들을 이해하고 수정하는 과정에서 애초에 배우지 않은 부분이 많이 들어가서 이해하기 많이 어려웠다.
이후 심화 Spring 주차에서 JWT 관련 내용을 다시 정리하고 학습해보면 좋겠다고 생각했다.
그래두 뿌듯한 첫 조별과제 끝
'SPARTA 과제 > TEAM | commerce' 카테고리의 다른 글
| JWT 트러블슈팅 (0) | 2026.03.15 |
|---|---|
| 팀) 커머스 - Soft Delete (0) | 2026.02.27 |
| 팀) 커머스 트러블슈팅 (3) (0) | 2026.02.27 |
| 팀) 커머스 - DB 생성 시 값 넣기 (0) | 2026.02.27 |
| 팀) 커머스 - 리스트 출력 (0) | 2026.02.22 |