개발자의 끄적끄적

[java/spring] RESTful 서비스 설계와 개발 - 메시지와 예외 처리 [펌] 본문

개발/java & jsp

[java/spring] RESTful 서비스 설계와 개발 - 메시지와 예외 처리 [펌]

효벨 2020. 9. 28. 01:00
728x90
반응형

[java/spring] RESTful 서비스 설계와 개발 - 메시지와 예외 처리 [펌]

 

결과 메시지 설계 

Response Message 구성 

fieldtypeDescription

code int

결과 코드, 정상인 경우 200

status

boolean

정상, 성공인 경우 true

timestamp

Date

시간

data

Map 처리 결과 데이터들
message

String

결과 메시지

error Set 에러 발생시 에러 내용 

error : Set

fieldtypeDescription

code int

결과 코드, 정상인 경우 200

errorMessage String

상세 에러 메시지

referedUrl

String

요청 url 

HTTP 응답상태코드 (rfc2616 참고)

성공

200 : OK, 요청 정상 처리 

201 : Created, 생성 요청 성공

202 : Accepted, 비동기 요청 성공

204 : No Content, 요청 정상 처리, 응답 데이터 없음. 

실패

400 : Bad Request, 요청이 부적절 할 때, 유효성 검증 실패, 필수 값 누락 등. 

401 : Unauthorized, 인증 실패, 로그인하지 않은 사용자 또는 권한 없는 사용자 처리

402 : Payment Required

403 : Forbidden, 인증 성공 그러나 자원에 대한 권한 없음. 삭제, 수정시 권한 없음. 

404 : Not Found, 요청한 URI에 대한 리소스 없을 때 사용. 

405 : Method Not Allowed, 사용 불가능한 Method를 이용한 경우. 

406 : Not Acceptable, 요청된 리소스의 미디어 타입을 제공하지 못할 때 사용.

408 : Request Timeout

409 : Conflict, 리소스 상태에 위반되는 행위 시 사용.

413 : Payload Too Large

423 : Locked

428 : Precondition Required

429 : Too Many Requests

 

500 : 서버 에러 

 

* 응답 상태 코드 중 사용할 만한 것만 선별한 것입니다. 

 

참고 소스 (스프링 기반)

ResponseMessage.java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

package eblo.common.domain;

 

import java.util.Date;

import java.util.HashMap;

import java.util.Map;

 

import org.springframework.http.HttpStatus;

 

import eblo.common.exception.AbstractEbloBaseException;

import lombok.Getter;

 

@Getter

public class ResponseMessage extends BaseObject {

 

    private static final long serialVersionUID = 1L;

 

    private static final String DEFAULT_KEY = "result";

    private int code;

    private boolean status;

    private String message;

    private Date timestamp;

    private Map<String, Object> data;

    private ErrorMessage error;

 

    public ResponseMessage() {

        this(HttpStatus.OK);

    }

    

    public ResponseMessage(HttpStatus httpStatus) {

        this.data = new HashMap<>();

        this.code = httpStatus.value();

        this.status = (httpStatus.isError())? false:true;

        this.message = httpStatus.getReasonPhrase();

        this.timestamp = new Date();

    }

 

    public ResponseMessage(AbstractEbloBaseException ex, String referedUrl) {

        HttpStatus httpStatus = ex.getHttpStatus();

        this.data = new HashMap<>();

        this.code = httpStatus.value();

        this.status = (httpStatus.isError())? false:true;

        this.message = httpStatus.getReasonPhrase();

        this.error = new ErrorMessage(code, ex.getMessage(), referedUrl);

        this.timestamp = new Date();

    }

    

    public ResponseMessage(HttpStatus status, Object result) {

        this(status);

           this.data.put(DEFAULT_KEY, result);

    }

    

    public void add(String key, Object result) {

        this.data.put(key, result);

    }

    

    public void remove(String key) {

        this.data.remove(key);

    }

}

 

Colored by Color Scripter

cs

 

ErrorMessage.java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

@Getter

@Setter

public class ErrorMessage extends BaseObject{

 

    private static final long serialVersionUID = 1L;

 

    private int code;

    private String errorMessage;

    private String referedUrl;

    public ErrorMessage(int code, String errorMessage, String referedUrl) {

        super();

        this.code = code;

        this.errorMessage = errorMessage;

        this.referedUrl = referedUrl;

    }

}

 

Colored by Color Scripter

cs

 

Test

1

2

3

4

5

6

7

8

9

10

11

    @GetMapping("/api/users")

    public ResponseMessage users(User user) {

        List<User> users = new ArrayList<>();

        for(int i=0; i<10;i++) {

            users.add(new User("user_"+i, "사용자_"+i, "facebook"));

        }

 

        ResponseMessage message = new ResponseMessage(HttpStatus.OK);

        message.add("users", users);

        return message;

    }

Colored by Color Scripter

cs

 

결과 

{

  • "code":200,
  • "status":true,
  • "message":"OK",
  • "timestamp":1550117593498,
  • "data":{
    • "users":[
      1. {
        • "userId":"user_0",
        • "userPwd":null,
        • "name":"사용자_0",
        • "authType":"facebook"
        },
      2. {
        • "userId":"user_1",
        • "userPwd":null,
        • "name":"사용자_1",
        • "authType":"facebook"
        },
      3. {
        • "userId":"user_2",
        • "userPwd":null,
        • "name":"사용자_2",
        • "authType":"facebook"
        },
      4. {
        • "userId":"user_3",
        • "userPwd":null,
        • "name":"사용자_3",
        • "authType":"facebook"
        },
      5. {
        • "userId":"user_4",
        • "userPwd":null,
        • "name":"사용자_4",
        • "authType":"facebook"
        },
      6. {
        • "userId":"user_5",
        • "userPwd":null,
        • "name":"사용자_5",
        • "authType":"facebook"
        },
      7. {
        • "userId":"user_6",
        • "userPwd":null,
        • "name":"사용자_6",
        • "authType":"facebook"
        },
      8. {
        • "userId":"user_7",
        • "userPwd":null,
        • "name":"사용자_7",
        • "authType":"facebook"
        },
      9. {
        • "userId":"user_8",
        • "userPwd":null,
        • "name":"사용자_8",
        • "authType":"facebook"
        },
      10. {
        • "userId":"user_9",
        • "userPwd":null,
        • "name":"사용자_9",
        • "authType":"facebook"
        }
      ]
    },
  • "error":null

}

 

예외 생성(스프링 기반) 

에러를 어떻게 처리할지를 먼저 결정하고 그에 따라 Exception을 설계합니다. 

Exception 발생시 Respons status는 Exception에 정의한 HttpStatus를 활용할거며 에러 메시지와 HttpStatus의 value를 반환할 것입니다. 

AbstractEbloBaseException.java 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

import org.springframework.http.HttpStatus;

 

public abstract class AbstractEbloBaseException extends RuntimeException {

 

    private static final long serialVersionUID = 1L;

 

    public AbstractEbloBaseException() {

        super();

    }

 

    public AbstractEbloBaseException(String msg) {

        super(msg);

    }

 

    public AbstractEbloBaseException(Throwable e) {

        super(e);

    }

 

    public AbstractEbloBaseException(String errorMessge, Throwable e) {

        super(errorMessge, e);

    }

 

    public abstract HttpStatus getHttpStatus();

 

}

 

Colored by Color Scripter

cs

 

EbloNotFoundException.java 

not found exception

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

import org.springframework.http.HttpStatus;

 

public class EbloNotFoundException extends AbstractEbloBaseException {

 

    private static final long serialVersionUID = 1L;

 

    public EbloNotFoundException() {

        super();

    }

 

    public EbloNotFoundException(Throwable e) {

        super(e);

    }

 

    public EbloNotFoundException(String errorMessge) {

        super(errorMessge);

    }

 

    public EbloNotFoundException(String errorMessge, Throwable e) {

        super(errorMessge, e);

    }

 

    public HttpStatus getHttpStatus() {

        return HttpStatus.NOT_FOUND;

    }

}

Colored by Color Scripter

cs

 

EbloInvalidRequestException.java 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

import org.springframework.http.HttpStatus;

 

public  class EbloInvalidRequestException extends AbstractEbloBaseException {

 

    private static final long serialVersionUID = 1L;

 

    public EbloInvalidRequestException() {

        super();

    }

 

    public EbloInvalidRequestException(Throwable e) {

        super(e);

    }

 

    public EbloInvalidRequestException(String errorMessge) {

        super(errorMessge);

    }

 

    public EbloInvalidRequestException(String errorMessge, Throwable e) {

        super(errorMessge, e);

    }

 

    public HttpStatus getHttpStatus() {

        return HttpStatus.BAD_REQUEST;

    }

}

Colored by Color Scripter

cs

 

이외 EbloForbiddenException, EbloUnauthorizedException, EbloUnknownException, EbloSystemException 등 공통 Exception 생성. 

에러 코드는 스프링의 HttpStatus 이용합니다. 

 

Exception 처리 

스프링에서는 Controller based Exception Handling과 Global Exception Handling 두가지 형태로 처리 할 수 있습니다. 

 

참고 

Exception Handling in Spring MVC : https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc

 

저는 서비스에 따라 각각 처리할 때 공통 컨트롤러를 만들고 그것을 상속 받아서 처리하게 만들었습니다.

AbstractBaseRestController. java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

@Slf4j

@RestController

public class AbstractBaseRestController extends AbstractBaseController{

 

    @ExceptionHandler(AbstractEbloBaseException.class)

    public ResponseMessage abstractBaseException(HttpServletRequest req, HttpServletResponse res, final AbstractEbloBaseException exception){

        log.error("AbstractEbloBaseException : "+exception.getMessage());

        res.setStatus(exception.getHttpStatus().value());

        return new ResponseMessage(exception, req.getRequestURL().toString());

    }

    

    @ExceptionHandler(IllegalArgumentException.class)

    @ResponseStatus(value=HttpStatus.BAD_REQUEST)

    public ResponseMessage illegalArgumentException(HttpServletRequest req, final IllegalArgumentException exception){

        log.error("IllegalArgumentException : "+exception.getMessage());

        return new ResponseMessage(new EbloInvalidRequestException(exception.getMessage(), exception), req.getRequestURL().toString());

    }

    

    @ExceptionHandler(Exception.class)

    @ResponseStatus(value=HttpStatus.INTERNAL_SERVER_ERROR, reason="Internal server error")

    public ResponseMessage exception(HttpServletRequest req, final Exception exception){

        log.error("Exception : "+exception.getMessage());

        return new ResponseMessage(new EbloUnknownException(exception.getMessage(), exception), req.getRequestURL().toString());

    }

 

    @ExceptionHandler(BindException.class

    @ResponseStatus(value=HttpStatus.BAD_REQUEST)

    public ResponseMessage bindingErrors(final HttpServletRequest req, final BindException exception) {

        log.error("BindException : "+exception.getMessage());

        List<FormError> formErrors = bindingResultHandler.getErrors(exception.getBindingResult());

        StringBuilder msg = new StringBuilder();

        for(FormError err : formErrors) {

            msg.append(String.format(bindingErrorMsgFormat, err.getInputName(), err.getMessage()));

        }

        return new ResponseMessage(new BindingException(exception.getMessage(), exception), req.getRequestURL().toString());

    }

 

}

Colored by Color Scripter

cs

Rest 서비스에 대한 컨트롤러입니다. 

각 서비스, 컨트롤러에서 예외를 던지면 @ExceptionHandler를 통해 예외를 자동 처리 해주게 됩니다. 

예외에 따라 Response status를 결정하고 에러 코드와 메시지를 생성합니다. 

이렇게 예외를 처리하게 되면 불필요한 코드를 다 제거 할 수 있게 되고 소스의 가독성 역시 엄청나게 향샹 됩니다. 

 

* 이외 일반 컨트롤러에서 에러 발생시 에러 페이지로 연결하는 공통 컨트롤러를 만들어서 사용하게 되면 코딩이 한결 수월해 집니다. 

 

컨트롤러 예제 

UserAuthRestController.java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

@RestController

@Slf4j

public class UserAuthRestController extends AbstractBaseRestController{ 

 

    @GetMapping("/api/users")

    public ResponseMessage users(User user) {

        Assert.notNull(user.getUserId(), "요청 사용자 id는 필수 입니다.");

        List<User> users = new ArrayList<>();

        for(int i=0; i<10;i++) {

            users.add(new User("user_"+i, "사용자_"+i, "facebook"));

        }

 

        ResponseMessage message = new ResponseMessage(HttpStatus.OK);

        message.add("users", users);

        return message;

    }

}

Colored by Color Scripter

cs

 

결과

{

  • "code":400,
  • "message":"Bad Request",
  • "error":{
    • "code":400,
    • "errorMessage":"요청 사용자 id는 필수 입니다.",
    • "referedUrl":"https://localhost:8080/api/users"
    },
  • "status":false

}

 

 

 

마무리

REST 서비스에서 사용하는 공통 MESSAGE와 EXCEPTION에 대한 예제를 만들어 보았습니다. 

이중에 Exception은 매우 중요하다고 생각합니다.  Rest 서비스가 아니더라도 exception 만큼은 잘 알고 코딩을 했으면 좋겠습니다. 

고급 개발자 또는 잘하는 사람의 코딩을 보면 굉장히 간결하고 짧게 코딩이 되어 있는 것을 볼 수 있습니다. 

코드를 간결하고 쉽게 만드는 비결 중 하나는 얼마만큼 exception을 다룰 수 있는가 하는 능력이라고 봅니다. 

코드의 반은 try-catch이고 나머지 반은 조건문인 경우가 많습니다. 이런 부분만 해소하면 코드 품질이 엄청나게 향상될거라 생각합니다.  

 

 

출처 : eblo.tistory.com/48

반응형
Comments