728x90

이번 프로젝트를 하면서 예외 처리에 대해서 고민 많았고 

실제로 작업하면서 몇번씩 수정을 거치게 되었다. 

 

일단 예외가 발생했을 때 발생원인이 명확한 경우 구체적인 예외 사유를 알 수 있게 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";

}

 

}

 

 

 

+ Recent posts