기타 프로젝트/SPA NetCore

[ASP.NET Core] .NET Core로 구현한 SPA(Single Page Applications)(3) - API 결과 공통 처리

당근천국 2019. 11. 25. 15:30

WebAPI의 리턴용 모델을 공통화하여 결과를 출력하기 위한 모델을 관리하겠습니다.


이미 기존 예제에도 포함되어 있는 내용입니다.

순서를 좀 더 앞에서 다뤘어야 한다는 생각이 심각하게 드네요 ㅎㅎㅎㅎ




1. WebAPI 공통화 필요성

WebAPI를 만들때마다 어떻게 해야 프론트엔드가 편하게 사용할지 생각합니다.

그럴 때마다 나오는 결론이 고정된 리턴 값이 있어서 이것을 가지고 1차 판단을 해야 한다는 것입니다.


이 고정된 리턴값을 베이스로 만들고 리턴용 모델들은 이 베이스를 상속하여 작성해야 합니다.


WebAPI는의 Http 상태 코드는

200 : 성공

4xx : 인증 관련 오류

500 : 서버 내부 오류


하지만 예측할 수 있는 오류(예를 들면 비밀번호가 틀렸다)는 200으로 리턴하고

프론트 엔드가 구분할 수 있는 값을 전달해야 합니다.

이것에 고정된 리턴값이 실어서 보내게 되는 것이죠.


2. 모델 작성

'Model'폴더를 만들고 그 아래 'ApiModel'폴더를 만듭니다.

이 폴더는 API의 공통 처리되는 모델들을 넣어 둡니다.



2-1. 베이스 만들기
'ApiResultBaseModel.cs'를 생성합니다.

이 클래스는 모든 API리턴용 모델이 상속하여 사용해야 합니다.



아래와 같이 작성합니다.


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
/// <summary>
/// API 결과 공통 베이스.
/// 자바스크립트에도 전달해야 하므로 소문자로 시작한다.
/// </summary>
public class ApiResultBaseModel
{
    /// <summary>
    /// 실패시 전달한 코드
    /// 0 : 성공.
    /// 다른 값은 모두 실패
    /// </summary>
    public string infoCode { get; set; }
    /// <summary>
    /// 전달할 메시지
    /// </summary>
    public string message { get; set; }
 
    public ApiResultBaseModel()
    {
        this.infoCode = "0";
        this.message = string.Empty;
    }
 
    public ApiResultBaseModel(string sInfoCode, string sMessage)
    {
        this.infoCode = sInfoCode;
        this.message = sMessage;
    }
}
cs



프론트 엔드는 'infoCode'값을 가지고 우선 판단합니다.

0이면 성공이고 나머지 값은 각 모델에 따라 약속된 값을 전달해야 합니다.


예를 들면 아이디가 없으면 '1'

비밀번호가 틀렸으면 '2'

이런 식으로 말이죠.


'message'는 뭔가 전달할 메시나 정보가 있다면 문자열 형태로 전달하기 위해서 사용합니다.


2-2. 예측 할 수 있는 오류 모델
예측 할 수 있는 오류가 발생한 경우 리턴될 모델입니다.

별다른 처리 없이 'infoCode'와 'message'만 전달하면 됩니다.


'ApiResultFailModel.cs'를 생성하고 아래와 같이 작성합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// <summary>
/// api 실패시 전달할 모델(자바스크립 전달용)
/// </summary>
public class ApiResultFailModel: ApiResultBaseModel
{
    public ApiResultFailModel()
        : base()
    {
            
    }
 
    public ApiResultFailModel(string sInfoCode, string sMessage)
        :base(sInfoCode, sMessage)
    {
    }
}
cs




2-3. 공통 처리용 모델

API가 성공하면 각자 정해진 모델을 전달하게 됩니다.


1) API가 시작될때 선언되고 

2) 개발자가 필요한 정보들을 입력한 후

3) 결과모델을 받아 고정된 리턴값을 추가한다음  'ObjectResult'로 만들어 리턴해 줍니다.


'ApiResultReadyModel.cs'를 생성하고 아래와 같이 작성합니다.


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
/// <summary>
/// api요청을 처리할때 요청결과를 임시로 저장해두고
/// 결과용 
/// </summary>
public class ApiResultReadyModel: ApiResultBaseModel
{
    /// <summary>
    /// 컨트롤러베이스의 기능을 쓰기위한 개체
    /// </summary>
    private ControllerBase ThisCB { get; set; }
 
    /// <summary>
    /// 스테이터스 코드
    /// </summary>
    public int StatusCode { get; set; }
 
    /// <summary>
    /// API의 처음부분에서 선언한다.
    /// </summary>
    /// <param name="cbThis">컨트롤러 기능을 사용하기위한 인스턴스</param>
 
    public ApiResultReadyModel(ControllerBase cbThis)
        : base()
    {
        this.ThisCB = cbThis;
 
        this.StatusCode = StatusCodes.Status200OK;
    }
 
    /// <summary>
    /// API끝에서 호출하여 'ObjectResult'를 생성하여 리턴해 준다.
    /// </summary>
    /// <param name="objResultData">전달할 모델</param>
    /// <returns></returns>
    public ObjectResult ToResult(object objResultData)
    {
        ObjectResult orReturn = null;
 
        if (StatusCode == StatusCodes.Status200OK)
        {//성공
            //성공은 전달받은 오브젝트를 준다,
            orReturn = this.ThisCB.StatusCode(this.StatusCode, objResultData);
        }
        else
        {//실패
            //실패는 500 에러를 기본으로 전달해야 한다.
            ApiResultFailModel afm = new ApiResultFailModel(base.infoCode, base.message);
 
            //여기에 들어왔다는건 예측 가능한 오류가 났다는 의미다.
            //예측가능한 오류는 200으로 바꿔준다.
            orReturn = this.ThisCB.StatusCode(StatusCodes.Status200OK, afm);
            //여기서 예측가능한 오류를 200으로 바꾸지 않으려면 이 코드를 사용한다.
            //orReturn = this.ThisCB.StatusCode(this.StatusCode, afm);
        }
 
        return orReturn;
    }
}
cs



22번 라인의 'public ApiResultReadyModel(ControllerBase cbThis)'에서 컨트롤 베이스는 API 단에서 'this'를 의미합니다.

이것은 'StatusCode()'를 사용하기 위해서 전달받아야 합니다.


API의 리턴용 'ObjectResult'를 만들기 위해 'ToResult(object objResultData)'를 호출합니다.

API가 성공했다면 'objResultData'는 정상적인 모델이 전달될 것입니다.

API가 실패했다면 위에서 만든 'ApiResultFailModel'를 'ObjectResult'로 만들어 리턴합니다.


49번 라인은 예측 가능한 오류를 200으로 내보낼지 다른 오류로 내보낼지에 따라 코드가 달라집니다.

전 예측 가능한 오류는 성공(200)으로 내보내려고 위와 같이 만들었습니다.



3. 공통 모델 사용하기

이제 위에서 만든 공통모델을 사용하기 위한 컨트롤러와 모델을 작성합니다.



3-1. 테스트용 모델 생성

모델 폴더에 'TestModel.cs'를 생성하고 아래 코드를 넣어 줍니다.


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
/// <summary>
/// 테스트용 01
/// </summary>
public class TestModel01 : ApiResultBaseModel
{
    public int nTest { get; set; }
    public string sTest { get; set; }
 
    public TestModel01() : base()
    {
        this.nTest = 0;
        this.sTest = string.Empty;
    }
}
 
/// <summary>
/// 테스트용 02
/// </summary>
public class TestModel02 : ApiResultBaseModel
{
    public int nTest001 { get; set; }
    public string sTest002 { get; set; }
 
    public TestModel02() : base()
    {
        this.nTest001 = 0;
        this.sTest002 = string.Empty;
    }
}
cs



3-2. 테스트용 컨트롤러 생성
'TestController.cs'를 생성합니다.


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
62
63
64
65
[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
    [HttpGet]
    [Route("Call")]
    public ActionResult<ObjectResult> Call()
    {
        ObjectResult apiresult = new ObjectResult(200);
 
        apiresult = StatusCode(200"성공!");
 
        return apiresult;
    }
 
    [HttpGet]
    [Route("Test01")]
    public ActionResult<TestModel01> Test01(int nData, string sData)
    {
        //리턴 보조
        ApiResultReadyModel armResult = new ApiResultReadyModel(this);
        //리턴용 모델
        TestModel01 tmResult = new TestModel01();
 
        if(0 <= nData)
        {//양수다.
            tmResult.nTest = nData;
            tmResult.sTest = sData;
        }
        else
        {
            armResult.StatusCode = StatusCodes.Status500InternalServerError;
 
            armResult.infoCode = "1";
            armResult.message = "'nData'에 음수가 입력되었습니다.";
        }
 
        return armResult.ToResult(tmResult);
    }
 
    [HttpGet]
    [Route("Test02")]
    public ActionResult<TestModel02> Test02(int nData)
    {
        //리턴 보조
        ApiResultReadyModel armResult = new ApiResultReadyModel(this);
        //리턴용 모델
        TestModel02 tmResult = new TestModel02();
 
        if (0 <= nData)
        {//양수다.
            tmResult.nTest001 = nData;
            tmResult.sTest002 = "성공 했습니다!";
        }
        else
        {
            armResult.StatusCode = StatusCodes.Status500InternalServerError;
 
            armResult.infoCode = "1";
            armResult.message = "'nData'에 음수가 입력되었습니다.";
        }
 
        return armResult.ToResult(tmResult);
    }
}
cs



4. 테스트

이제 테스트를 위해 함수를 만들고 API를 호출하는 함수를 만듭시다.


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
/**
 * 데이터 바인드 테스트
 * @param {int} nData 전달할 값
 */
Test02.prototype.Test01 = function (nData)
{
    var objThis = this;
 
    AA.get(FS_Api.Test_Test01
        , { nData: nData, sData: "테스트 01" }
        , function (data) {
            console.log(data);
 
            if ("0" === data.infoCode)
            {//에러 없음
                objThis.divOutput.html("nTest : " + data.nTest + " sTest : " + data.sTest );
            }
            else
            {//에러 있음
                //아웃풋 지우기
                objThis.divOutput.html("");
                alert("error code : " + data.infoCode + "\n"
                    + "내용 : " + data.message);
            }
        }
        , function (error) {
            console.log(error);
 
            //예측가능한 에러를 200으로 처리하지 않았다면 아래와 같이 만들어야 한다.
            //if (error.responseJSON && error.responseJSON.infoCode)
            //{
            //    alert("실패코드 : " + error.responseJSON.infoCode
            //        + "\n " + error.responseJSON.message);
            //}
 
        });
};
cs



호출이 성공하면 'infoCode'가 '0'인지 확인하고

'0'이 아니면 약속된 동작을 하면 됩니다.


만약 예측 가능한 에러를 '성공(200)'으로 처리하지 않았다면

Ajax에러에서 'infoCode'를 확인하여 약속된 동작을 하면 됩니다.



이제 API를 호출해 봅시다.




성공했을 때는 지정된 모델이 json형태로 오는 것을 확인 할 수 있습니다.

실패했을 때는 'ApiResultFailModel'이 넘어오고 'infoCode'가 '0' 이외의 값이 오는 것을 확인 할 수 있습니다.



마무리

API를 공통화하는 건 선택사항입니다.

하지만 이것 하나만으로도 프론트 엔드와 소통이 편해집니다.

무조건 'infoCode'는 넘어오니까요.

클라이언트 쪽 문제가 생겼을 때도 추적도 편하죠.


저는 예측 가능한 요소는 http 상태 코드를 성공으로 보내야 하는 것이 맞다고 보기 때문에 

500에러보다는 200성공으로 보내는 것을 추천합니다.


원래 500에러는 서버 쪽에서 처리되지 않은 오류가 있을 때 발생시켜야 합니다.

그런데 예측 가능한 에러는 처리된 오류이므로 500에러로 리턴할 필요가 없죠.

그리고 프론트 엔드 입장에서도 처리된 오류와 처리되지 않은 오류를 구분할 필요가 없어집니다.


물론 어떤 선택을 할지는 설계자가 결정하는 내용입니다. ㅎㅎㅎ