이번 프로젝트를 하면서 예외 처리에 대해서 고민 많았고
실제로 작업하면서 몇번씩 수정을 거치게 되었다.
일단 예외가 발생했을 때 발생원인이 명확한 경우 구체적인 예외 사유를 알 수 있게 customException을 만들어서 던지기로 했다.
public class ProductCustomException extends Exception{
private String data;
private ProductExceptionMsg msg;
public ProductCustomException(ProductExceptionMsg msg) {
this.msg = msg;
}
public ProductCustomException(String data, ProductExceptionMsg msg) {
this.data = data;
this.msg = msg;
}
}
ProductCustomException은
ProductExceptionMsg를 가지고 있다.
public enum ProductExceptionMsg implements ProductMsgInterface{
FOUND_NO_MEMBER, FOUND_NO_OPTIONSET,FOUND_NO_OPTIONS, FOUND_NO_PRODUCT,
FOUND_NO_CATEGORY, FOUND_NO_RECENTVIEW,FOUND_NO_INTEREST,
ALREADY_EXISTS_RECENTVIEW,ALREADY_EXISTS_INTEREST, IS_NOT_INTERESTEDD, NEED_LOGIN, WRONG_PARAMETER
}
ProductMsgInterface를 상속한 ExceptionMsg는 예외명들을 가지고 있다.
이렇게 하면 예외가 발생했을 때 ResponseDto에 ExceptionMsg를 보낼 수도 있고
예외를 던지거나 잡아야할 때 Msg를 확인하고 Msg에 맞게 처리할 수 있다.
그리고 ProductCustomException을 상속한 커스텀 익셉션을 만들고
내부 클래스로 그 커스텀을 구체적으로 나눴다.
FoundNoObjectException만 만들고 ExceptionMsg로 발생한 부분을 구분할 수도 있지만 클래스명으로
직관적으로 알 수 있게 클래스단위로 분리하였다.
public class FoundNoObjectException extends ProductCustomException {
public FoundNoObjectException(ProductExceptionMsg msg) {
super(msg);
}
public FoundNoObjectException(String data, ProductExceptionMsg msg) {
super(data,msg);
}
public static class FoundNoMemberException extends FoundNoObjectException{
public FoundNoMemberException(String data) {
super(data,ProductExceptionMsg.FOUND_NO_MEMBER);
}
public FoundNoMemberException() {
super(ProductExceptionMsg.FOUND_NO_MEMBER);
}
}
public static class FoundNoOptionSetException extends FoundNoObjectException{
public FoundNoOptionSetException(String data, ProductExceptionMsg msg) {
super(data, ProductExceptionMsg.FOUND_NO_OPTIONSET);
}
public FoundNoOptionSetException() {
super(ProductExceptionMsg.FOUND_NO_OPTIONSET);
}
}
...
}
그리고 dao와 service ,controller 단에서 들어오는 데이터를 검증하고 객체를 찾지 못하거나 로그인이 필요하거나 이미 존재하는 등 체크해서 알맞은 예외를 던져주었다.
@Override
public void delete(Long id) throws FoundNoCategoryException {
Category find= repository.findById(id).orElseThrow(() -> new FoundNoCategoryException());
repository.deleteById(find.getId());
}
예를들어
카테고리를 삭제할 때 삭제할 카테고리를 찾지 못하면 FoundNoCategoryExcetpion을 던져주었다.
public ResponseDto<?> deleteHeart(@Valid InterestDto dto) throws FoundNoMemberException, FoundNoOptionSetException {
try {
if(interestDao.isInterested(dto)) {
interestDao.delete(dto);
}else {
return ResponseDto.builder().msg(ProductExceptionMsg.IS_NOT_INTERESTEDD).build();
}
} catch (FoundNoInterestException e) {
e.printStackTrace();
return ResponseDto.builder().msg(e.getMsg()).build();
}
return ResponseDto.builder().msg(ProductSuccessMsg.UNTAP_HEART).build();
}
파라메터로 들어오는 InterestDto에 @Valid 어노테이션으로 검증을 하고
**
implementation 'org.springframework.boot:spring-boot-starter-validation'
검증을 위한 validation gradle 추가
deleteHeart 서비스는 제품의 하트를 눌렀을 때 관심상품에 추가하고 삭제할 때 삭제하는 서비스인데
일단 관심상품으로 등록되어있는지 확인해야한다.
비동기 방식으로 요청을 보내기 때문에 응답이 가기전에 여러번 중복하여 요청이 오는 경우
관심상품이 이미 삭제되었는데 삭제 요청이 올 수도 있기 때문에
if문으로 확인하는 작업을 거치고
그렇지 않으면 500번 대신 명확한 메시지를 담은 ResponseDto를 보내준다.
public ResponseDto<?> showOptionNameValues(Long categoryId) {
List<OptionNamesValues> optionNameValue = optionDao.findOptionNameValueMapByCategoryId(categoryId);
if(optionNameValue==null||optionNameValue.isEmpty()) {
log.warn("no children categories found");
return ResponseDto.builder().msg(ProductExceptionMsg.FOUND_NO_OPTIONS).build();
}
Map<String, Set<String>> dto = optionNameValue.stream().collect(Collectors.groupingBy(
OptionNamesValues::getName, Collectors.mapping(OptionNamesValues::getValue, Collectors.toSet())));
List<Map<String, Set<String>>> data = new ArrayList<>();
data.add(dto);
return ResponseDto.<Map<String, Set<String>>>builder().data(data).msg(ProductSuccessMsg.FIND_OPTION_NAME_VALUES).build();
}
showOptionNameValues 서비스는 카테고리를 선택했을 때 비동기방식으로 요청을 보내
카테고리의 제품들이 보유한 모든 옵션을 가져오는 서비스인데
optionNameValue가 없다는건 카테고리에 해당하는 제품이 없거나 옵션이 없다는 뜻이다.
만약 이런 상황이 발생할 때 예외 페이지로 보내거나 클라이언트가 다른 요청을 보내는데 지장이 생기면 안된다.
카테고리에 해당하는 제품이 없다거나 옵션이 없다면 그 카테고리나 제품에 문제가 있는 것일 수도 있고
원래 옵셥이 없는 제품들만 있는 카테고리일 수도 있는 것이다.
여기서 예외를 처리할 수 있는 최선의 방법은 log로 남기고 나중에 log를 확인하고 제품이나 카테고리를 확인하는 작업을 하는 것이고
만약 진짜 옵션이 없는 상황이라면 그 카테고리를 선택했을 때 선택할 옵션이 없다는 표시를 해주는 것이 최선이라고 생각한다.
컨트롤러까지와서 예외를 잡지 못하거나 발생한 예외에 대해 일관된 처리가 필요한 경우는
ExceptionHandler로 처리했다.
https://youarethebestcoding.tistory.com/24
Spring Boot Exception Handler
Local Exception Controller Controller 내에서 발생하는 Exception만 처리한다. @Controller public class LocalExceptionController { @GetMapping("/business1") public String business_method() throws BusinessException1 { boolean b = true; if(b) { thro
youarethebestcoding.tistory.com
restController에서 발생한 예외는 클라이언트 쪽에서 처리하기 위해 상태코드와 메세지를 담아 응답을 보내줬다.
@RestControllerAdvice
public class ExceptionRestController {
@ResponseBody
@ExceptionHandler(value = {NeedLoginException.class,FoundNoOptionSetException.class,FoundNoMemberException.class,MethodArgumentNotValidException.class})
protected ResponseEntity<?> defaultRestException(Exception e) {
ProductExceptionMsg errorMsg=null;
if (e instanceof NeedLoginException) {
errorMsg = ((NeedLoginException) e).getMsg();
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ResponseDto.builder().msg(errorMsg).build());
} else if (e instanceof FoundNoObjectException.FoundNoMemberException) {
errorMsg = ((FoundNoObjectException.FoundNoMemberException) e).getMsg();
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ResponseDto.builder().msg(errorMsg).build());
} else if (e instanceof FoundNoOptionSetException) {
errorMsg = ((FoundNoOptionSetException) e).getMsg();
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ResponseDto.builder().msg(errorMsg).build());
} else if (e instanceof MethodArgumentNotValidException) {
errorMsg = ProductExceptionMsg.WRONG_PARAMETER;
}
return ResponseEntity.badRequest().body(ResponseDto.builder().msg(errorMsg).build());
}
}
예외가 발생하면
ResponseEntity로 다시 클라이언트에 보내면
fetch(options.url, options).then((response) => {
if (response.status === 200 || response.status === 201) {
return response.json();
} else if (response.status === 401) {
alert('로그인이 필요한 서비스입니다.');
window.location.href = "/member_login_form";// redirect
} else if (response.stataus === 404) {
window.location.href = "/404.html";
} else if (response.msg == 'WRONG_PARAMETER') {
alert('잘못된 요청입니다. 입력값을 확인해주세요.');
} else {
Promise.reject(response);
throw Error(response);
}
})
fetch 또는 ajax 등 비동기 방식으로 요청하고 받은
response의 status나 msg로 예외를 구분하고
그 예외에 대한 처리를 정의할 수 있다.
그리고 컨트롤러에서 발생한 예외는 404 페이지로 보내거나 로그인 폼으로 보냈다.
@ControllerAdvice
public class ExceptionController {
@ExceptionHandler(value = {FoundNoObjectException.FoundNoOptionSetException.class,NeedLoginException.class
,FoundNoMemberException.class,NoSuchElementException.class})
protected String defaultException(Exception e, HttpSession session) {
if (e instanceof FoundNoOptionSetException) {
return "redirect:404"; //없는 상품 조회
} else if (e instanceof NoSuchElementException) {
return "redirect:404";
} else if (e instanceof NeedLoginException) {
return "redirect:member_login_form";
} else if (e instanceof FoundNoMemberException) {
return "redirect:member_login_form";
}
return "redirect:404"; //없는 상품 조회
}
@ExceptionHandler(Exception.class)
@ResponseStatus
protected String exception (Exception e) {
return "redirect:404";
}
}
'Java > Project' 카테고리의 다른 글
스프링부트 프로젝트 CI/CD 환경 구축 (with.AWS + GIthub Actions) (0) | 2023.11.24 |
---|---|
스프링부트 프로젝트 JPA 활용 (0) | 2023.11.21 |
스프링부트 프로젝트 계층에 따른 데이터 전송 형태 (0) | 2023.11.21 |
다나가 쇼핑몰 프로젝트 ER diagram과 Entity (0) | 2023.11.20 |
쇼핑몰 웹사이트 제작 프로젝트 (0) | 2023.11.09 |