[ASP.NET Core] SPA NetCore - API 결과 리턴 공통화 0.5
API의 결과를 줄 때 성공하면 약속된 모델을 전달하고 실패하면 실패에 약속된 모델을 전달하는 것이 일반적인 방법입니다.
이 방법의 문제는 성공했을 때와 실패했을 때 구분이 매번 똑같을 수 없으니 프론트엔드(Front-end)에서는 이것을 구분하기 위한 작업을 그때그때 해야 합니다.
그래서 API결과를 알리기 위해 고정된 값을 하나 전달해주는 것이 좋습니다.
API 결과용 베이스를 만들어 API를 리턴할때는 이 베이스를 상속받은 모델을 리턴하는 것이죠.
1. 필요한 정보
API결과는 다음과 같은 경우가 있습니다.
성공 : 의도대로 API가 성공한 경우
실패 : 서버에서 실패로 판단하는 경우. 백엔드(Back-end)와 협의된 정보를 전달한다.
캐치 가능한 오류 : 서버에서 캐치한 오류(try~catch)
알 수 없는 오류 : 예측하지 못한 오류(500 에러)
프론트 엔드에서는 '알 수 없는 오류'를 제외하면 모두 성공(success)으로 전달하여 적절한 동작을 해야 합니다.
'알 수 없는 오류'는 백엔드에서 잘못한 것이기 때문에 백엔드 개발자에게 전달해야 합니다.
2. 공통 모델
이제 '1.'에서 이야기한 정보를 전달하기 위한 모델들을 만들어야 합니다.
2-1. 베이스(Base)
정보코드가 "0"이면 프론트엔드에서는 성공으로 판단하여 처리합니다.
전달할 메시지(Message)는 프론트엔드 개발자에게 전달하는 메시지입니다.
이것은 선택사항이므로 직접 모델을 만드는 경우 제외해도 됩니다.
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 | /// <summary> /// API 결과 공통 베이스. /// </summary> public class ApiResultBaseModel { /// <summary> /// 실패시 전달한 코드 /// 0 : 성공. /// 다른 값은 모두 실패 /// </summary> public string InfoCode { get; set; } /// <summary> /// 전달할 메시지 /// </summary> public string Message { get; set; } /// <summary> /// 기본 생성. /// InfoCode가 "0"로 초기화됨 /// </summary> public ApiResultBaseModel() { this.InfoCode = "0"; this.Message = string.Empty; } /// <summary> /// 인포코드와 메시지를 넣고 생성 /// </summary> /// <param name="sInfoCode"></param> /// <param name="sMessage"></param> public ApiResultBaseModel(string sInfoCode, string sMessage) { this.InfoCode = sInfoCode; this.Message = sMessage; } } | cs |
기본값은 인포코드는 "0"입니다. (성공)
2-2. 실패용 모델
실패하면 베이스를 그냥 전달해도 됩니다.
그런데도 모델을 따로 만드는 건 별도 작업이 필요할 수 있어서 미리 생성해 둔 것입니다.
직접 만들 때는 필요 없으면 실패용 모델은 작성하지 않아도 됩니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | /// <summary> /// api 실패시 전달할 모델(자바스크립 전달용) /// </summary> public class ApiResultFailModel: ApiResultBaseModel { /// <summary> /// 기본 초기화 /// </summary> public ApiResultFailModel() : base() { } /// <summary> /// 인포코드와 메시지를 넣고 생성 /// </summary> /// <param name="sInfoCode"></param> /// <param name="sMessage"></param> public ApiResultFailModel(string sInfoCode, string sMessage) :base(sInfoCode, sMessage) { } } | cs |
2-3. 모델 작성 없이 사용하는 모델
특히 값을 하나만 전달하면 될 때나 내부에서 사용하는 모델을 그대로 리턴할때 마다 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 38 39 40 41 | /// <summary> /// 스웨거에 노출시키지 않고 모델을 리턴할때 사용한다. /// 테스트용으로 사용해도된다.(매번 새로운 모델을 만들기 힘들기 때문) /// </summary> public class ApiResultObjectModel : ApiResultBaseModel { /// <summary> /// 전달할 오브젝트 /// </summary> public object ResultObject { get; set; } /// <summary> /// 기본 생성 /// </summary> public ApiResultObjectModel() : base() { } /// <summary> /// 리턴할 모델 지정하여 생성 /// </summary> /// <param name="objResult"></param> public ApiResultObjectModel(object objResult) : base() { this.ResultObject = objResult; } /// <summary> /// 인포코드와 메시지를 넣고 생성 /// </summary> /// <param name="sInfoCode"></param> /// <param name="sMessage"></param> public ApiResultObjectModel(string sInfoCode, string sMessage) : base(sInfoCode, sMessage) { } } | cs |
'ResultObject'에 원하는 데이터나 모델을 넣어 전달하면 됩니다.
2-4. 결과 처리 클래스
API가 시작되면 해당 API의 성공 여부를 해당 함수에서 체크하기 위해
1) 지역변수처럼 선언해놓고
2) 상태가 변할 때 이 인스턴스에 저장해둡니다.
3) 'ToResult'를 호출하여 저장된 상태 값을 기준으로 만든 'ApiResultBaseModel'를 상속받은 모델을 전달받아 리턴해줍니다.
이런 처리를 해주는 클래스입니다.
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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | /// <summary> /// api요청을 처리할때 요청결과처리를 공통화 하는 클래스. /// ApiResultFailModel를 공통으로 리턴하기 위해 베이스를 가지고 있다. /// 결과 출력용으로 데이터는 외부로 부터 받아야 한다. /// 외부에서는 ToResult를 이용하여 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(null, null) { this.ThisCB = cbThis; this.StatusCode = StatusCodes.Status200OK; } /// <summary> /// API끝에서 호출하여 'ObjectResult'를 생성하여 리턴해 준다. /// 만들어지는 결과의 ApiResultBaseModel데이터는 this 기준이다. /// </summary> /// <param name="objResultData">전달할 모델</param> /// <returns></returns> public ObjectResult ToResult(object objResultData) { ObjectResult orReturn = null; if (null == objResultData) {//오브젝트가 없다. //없으면 ApiResultBaseModel로 초기화 해준다. objResultData = new ApiResultBaseModel(); } if (StatusCode == StatusCodes.Status200OK) {//성공 if(null != base.InfoCode) {//베이스에 데이터가 있다. //베이스에 있는 데이터를 사용한다. //결과에 있는 코드와 메시지를 결과용 모델에 저장한다. ((ApiResultBaseModel)objResultData).InfoCode = base.InfoCode; ((ApiResultBaseModel)objResultData).Message = base.Message; } else {//베이스에 데이터가 없으면 들어온 데이터를 그대로 사용한다. } //성공은 전달받은 오브젝트를 준다, 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 |
'ToResult'를의 매개변수를 'null'로 주면 자동으로 'ApiResultBaseModel'를 리턴합니다.
매개변수가 'null'이 아닐 때는 들어온 모델이 리턴되게 됩니다.
'StatusCode'는 'Microsoft.AspNetCore.Http.StatusCodes'에 선언된 값을 주면 됩니다.
'StatusCode'가 '200'(성공)이 아니면 실패로 봅니다.
'200'이 아니면 기본값을 '500'(서버 오류)로 봅니다만.....
여기 정상적으로 들어왔다는 것 자체가 예상 가능한 에러이므로 '200'으로 처리해주고 'ApiResultFailModel'모델을 전달해 줍니다.
그렇다면 결국 'StatusCode'와 상관없이 같은 값이 나간다는 뜻입니다.
이것은 나중에 별도의 작업이 필요할 때(예> 서버에 로그 남기기) 사용하기 위해 그렇습니다.
3. 사용하기
이제 '2'에서 만든 모델을 사용해 보겠습니다.
3-1. 리턴이 별도로 없는 경우
먼저 API를 생성하고 'ApiResultReadyModel'인스턴스를 만들어줍니다.
'ApiResultReadyModel'인스턴스에 결과를 저장합니다.
'ApiResultBaseModel'의 변수들은 'null'로 초기화되기 때문에 값을 필수로 넣어줘야 합니다.
'ToResult'를 호출할 때 'null'을 전달합니다.
1 2 3 4 5 6 7 8 9 10 11 | [HttpGet] public ActionResult<ApiResultBaseModel> Call() { //리턴 보조 ApiResultReadyModel armResult = new ApiResultReadyModel(this); armResult.InfoCode = "0"; armResult.Message = "성공"; return armResult.ToResult(null); } | cs |
테스트하면 'ApiResultBaseModel'이 넘어오는 것을 알 수 있습니다.
3-2. 모델을 만들어서 사용
리턴용 모델을 명시하면 스웨거 같은 자동 문서화 툴에서 해당 모델의 정보를 확인 할 수 있습니다.
리턴용 모델은 'ApiResultBaseModel'를 상속받아야 합니다.
(참고 : SPA_NetCore_Foundation/SPA_NetCore_Foundation/SPA_NetCore_Foundation07/Model/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 | /// <summary> /// 테스트용 01 /// </summary> public class TestModel01 : ApiResultBaseModel { /// <summary> /// /// </summary> public int nTest { get; set; } /// <summary> /// /// </summary> public string sTest { get; set; } /// <summary> /// /// </summary> public TestModel01() : base() { this.nTest = 0; this.sTest = string.Empty; } } | cs |
API에서 리턴을 결과용 모델로 명시해주고 사용합니다.
'ApiResultBaseModel'와 결과용 모델의 인스턴스를 만들고
모든 정보는 결과용 모델의 인스턴스에 저장합니다.
'StatusCode'의 변경은 필수가 아닙니다.
어차피 내부적으로 '200'으로 바꿔주고 가지고 있는 데이터를 리턴하기때문입니다.
그래도 가능하면 상황에 맞는 상태 코드를 넣어주는 게 좋습니다.
(참고 : SPA_NetCore_Foundation/SPA_NetCore_Foundation07/Controllers/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 | [HttpGet] 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); } | cs |
테스트하면 아까 만든 결과용 모델이 리턴된 것을 알 수 있습니다.
3-3. 오브젝트 전달
전달할 데이터가 'null'이 아닌데 굳이 전달용 모델을 만들 필요가 없다면 'ApiResultObjectModel'를 사용하면 됩니다.
(참고 : SPA_NetCore_Foundation/SPA_NetCore_Foundation07/Controllers/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 | [HttpGet] public ActionResult<ApiResultObjectModel> Test03(int nData, string sData) { //리턴 보조 ApiResultReadyModel armResult = new ApiResultReadyModel(this); //리턴용 모델 ApiResultObjectModel tmResult = new ApiResultObjectModel(); List<string> listReturn = new List<string>(); if (0 <= nData) {//양수다. listReturn.Add(nData.ToString()); listReturn.Add(sData); tmResult.ResultObject = listReturn; } else { armResult.StatusCode = StatusCodes.Status500InternalServerError; armResult.InfoCode = "1"; armResult.Message = "'nData'에 음수가 입력되었습니다."; } return armResult.ToResult(tmResult); } | cs |
사용 방법은 결과용 모델을 만들어 쓰는 것과 비슷합니다.
단지 'ResultObject'에 전달할 정보를 넣어준다는 것이 차이가 있죠.
마무리
사실 이 포스팅에서 가장 중요한 건 1번 항목입니다.
설계에 따라 의도대로 되는 게 아니면 죄다 '200'외의 상태 값을 던지는 프로젝트가 있을 수도 있고
캐치 가능한 오류도 500에러로 처리할 수도 있죠.
하지만 전 캐치 가능한 오류는 서버 오류로 보질 않기 때문에 이렇게 설계가 된 것이죠.
각자에게 맞게 만들어 쓰시면 됩니다.