ASP.NET Core를 사용하여 네이티브 모바일 앱용 백 엔드 서비스 만들기

James Montemagno 작성

모바일 앱은 ASP.NET Core 백 엔드 서비스와 통신할 수 있습니다. iOS 시뮬레이터 및 Android 에뮬레이터에서 로컬 웹 서비스를 연결하는 방법에 대한 지침은 iOS 시뮬레이터 및 Android 에뮬레이터에서 로컬 웹 서비스에 연결을 참조하세요.

샘플 백 엔드 서비스 코드 보기 및 다운로드

샘플 네이티브 모바일 앱

이 자습서에서는 네이티브 모바일 앱을 지원하기 위해 ASP.NET Core를 사용하여 백 엔드 서비스를 만드는 방법을 보여 줍니다. Android, iOS 및 Windows에 대한 별도 네이티브 클라이언트를 포함하는 네이티브 클라이언트로 Xamarin.Forms TodoRest 앱을 사용합니다. 연결된 자습서를 따라 네이티브 앱을 만들고(필요한 무료 Xamarin 도구 설치) Xamarin 샘플 솔루션을 다운로드할 수 있습니다. Xamarin 샘플에는 이 문서의 ASP.NET Core 앱이 바꾸는 ASP.NET Core Web API 서비스 프로젝트가 포함되어 있습니다(클라이언트에서 필요한 변경 내용 없이).

To Do Rest application running on an Android smartphone

기능

ToDoREST 앱은 할 일 항목 나열, 추가, 삭제 및 업데이트를 지원합니다. 각 항목에는 ID, 이름, 메모 및 완료되었는지 여부를 나타내는 속성이 있습니다.

위에 표시된 것처럼 항목의 주 보기는 각 항목의 이름을 나열하고 확인 표시로 완료되었는지를 나타냅니다.

+ 아이콘을 누르면 항목 추가 대화 상자를 엽니다.

Add item dialog

기본 목록 화면에서 항목을 누르면 항목의 이름, 메모 및 완료 설정을 수정할 수 있거나 항목을 삭제할 수 있는 편집 대화 상자를 엽니다.

Edit item dialog

컴퓨터에서 실행되는 다음 섹션에서 만든 ASP.NET Core 앱을 직접 테스트하려면 앱의 RestUrl 상수를 업데이트합니다.

Android 에뮬레이터는 로컬 머신에서 실행되지 않으며 루프백 IP(10.0.2.2)를 사용하여 로컬 머신과 통신합니다. Xamarin.Essentials DeviceInfo를 활용하여 올바른 URL을 사용하기 위해 시스템이 실행 중인 운영 체제를 검색합니다.

TodoREST 폴더로 이동하여 Constants.cs 파일을 엽니다. 파일에는 Constants.cs 다음 구성이 포함됩니다.

using Xamarin.Essentials;
using Xamarin.Forms;

namespace TodoREST
{
    public static class Constants
    {
        // URL of REST service
        //public static string RestUrl = "https://YOURPROJECT.azurewebsites.net:8081/api/todoitems/{0}";

        // URL of REST service (Android does not use localhost)
        // Use http cleartext for local deployment. Change to https for production
        public static string RestUrl = DeviceInfo.Platform == DevicePlatform.Android ? "http://10.0.2.2:5000/api/todoitems/{0}" : "http://localhost:5000/api/todoitems/{0}";
    }
}

필요에 따라 Azure와 같은 클라우드 서비스에 웹 서비스를 배포하고 RestUrl을 업데이트할 수 있습니다.

ASP.NET Core 프로젝트 만들기

Visual Studio에서 새 ASP.NET Core 웹 애플리케이션을 만듭니다. Web API 템플릿을 선택합니다. 프로젝트 이름을 TodoAPI로 지정합니다.

New ASP.NET Web Application dialog with Web API project template selected

앱은 모바일 클라이언트에 대한 일반 텍스트 http 트래픽을 포함하여 포트 5000에 대한 모든 요청에 응답해야 합니다. 업데이트 Startup.csUseHttpsRedirection 개발에서 실행되지 않습니다.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        // For mobile apps, allow http traffic.
        app.UseHttpsRedirection();
    }

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

참고 항목

IIS Express가 아닌 앱을 직접 실행합니다. IIS Express는 기본적으로 로컬이 아닌 요청을 무시합니다. 명령 프롬프트에서 dotnet run을 실행하거나 Visual Studio 도구 모음의 디버그 대상 드롭다운에서 앱 이름 프로필을 선택합니다.

할 일 항목을 나타내도록 모델 클래스를 추가합니다. [Required] 특성으로 필수 필드를 표시합니다.

using System.ComponentModel.DataAnnotations;

namespace TodoAPI.Models
{
    public class TodoItem
    {
        [Required]
        public string ID { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        public string Notes { get; set; }

        public bool Done { get; set; }
    }
}

API 메서드에는 데이터를 사용하는 방법이 필요합니다. 원래 Xamarin 샘플에서 사용하는 동일한 ITodoRepository 인터페이스를 사용합니다.

using System.Collections.Generic;
using TodoAPI.Models;

namespace TodoAPI.Interfaces
{
    public interface ITodoRepository
    {
        bool DoesItemExist(string id);
        IEnumerable<TodoItem> All { get; }
        TodoItem Find(string id);
        void Insert(TodoItem item);
        void Update(TodoItem item);
        void Delete(string id);
    }
}

이 샘플의 경우 구현은 항목의 개별 컬렉션을 사용합니다.

using System.Collections.Generic;
using System.Linq;
using TodoAPI.Interfaces;
using TodoAPI.Models;

namespace TodoAPI.Services
{
    public class TodoRepository : ITodoRepository
    {
        private List<TodoItem> _todoList;

        public TodoRepository()
        {
            InitializeData();
        }

        public IEnumerable<TodoItem> All
        {
            get { return _todoList; }
        }

        public bool DoesItemExist(string id)
        {
            return _todoList.Any(item => item.ID == id);
        }

        public TodoItem Find(string id)
        {
            return _todoList.FirstOrDefault(item => item.ID == id);
        }

        public void Insert(TodoItem item)
        {
            _todoList.Add(item);
        }

        public void Update(TodoItem item)
        {
            var todoItem = this.Find(item.ID);
            var index = _todoList.IndexOf(todoItem);
            _todoList.RemoveAt(index);
            _todoList.Insert(index, item);
        }

        public void Delete(string id)
        {
            _todoList.Remove(this.Find(id));
        }

        private void InitializeData()
        {
            _todoList = new List<TodoItem>();

            var todoItem1 = new TodoItem
            {
                ID = "6bb8a868-dba1-4f1a-93b7-24ebce87e243",
                Name = "Learn app development",
                Notes = "Take Microsoft Learn Courses",
                Done = true
            };

            var todoItem2 = new TodoItem
            {
                ID = "b94afb54-a1cb-4313-8af3-b7511551b33b",
                Name = "Develop apps",
                Notes = "Use Visual Studio and Visual Studio for Mac",
                Done = false
            };

            var todoItem3 = new TodoItem
            {
                ID = "ecfa6f80-3671-4911-aabe-63cc442c1ecf",
                Name = "Publish apps",
                Notes = "All app stores",
                Done = false,
            };

            _todoList.Add(todoItem1);
            _todoList.Add(todoItem2);
            _todoList.Add(todoItem3);
        }
    }
}

다음에서 구현을 구성합니다.Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<ITodoRepository, TodoRepository>();
    services.AddControllers();
}

컨트롤러 만들기

프로젝트에 새 컨트롤러, TodoItemsController를 추가합니다. ControllerBase에서 상속되어야 합니다. Route 특성을 추가하여 컨트롤러에서 api/todoitems로 시작하는 경로에 대해 만든 요청을 처리하는 것을 나타냅니다. 경로의 [controller] 토큰은 컨트롤러의 이름으로 대체되며(Controller 접미사 생략) 글로벌 경로에 대해 특히 유용합니다. 라우팅에 대해 자세히 알아봅니다.

컨트롤러는 작동하기 위해 ITodoRepository가 필요합니다. 컨트롤러의 생성자를 통해 이 유형의 인스턴스를 요청합니다. 런타임 시 이 인스턴스는 종속성 주입에 대한 프레임워크의 지원을 사용하여 제공됩니다.

[ApiController]
[Route("api/[controller]")]
public class TodoItemsController : ControllerBase
{
    private readonly ITodoRepository _todoRepository;

    public TodoItemsController(ITodoRepository todoRepository)
    {
        _todoRepository = todoRepository;
    }

이 API는 데이터 원본에서 CRUD(만들기, 읽기, 업데이트, 삭제) 작업을 수행하도록 4개의 다른 HTTP 동사를 지원합니다. 이 중 가장 간단한 것은 HTTP GET 요청에 해당하는 읽기 작업입니다.

항목 읽기

항목의 목록 요청은 List 메서드에 대한 GET 요청으로 수행됩니다. List 메서드의 [HttpGet] 특성은 이 작업이 GET 요청만을 처리해야 함을 나타냅니다. 이 작업에 대한 경로는 컨트롤러에 지정된 경로입니다. 경로의 일부로 작업 이름을 사용할 필요가 없습니다. 각 작업에 고유하고 명확한 경로가 있도록 하기만 하면 됩니다. 라우팅 특성은 특정 경로를 작성하도록 컨트롤러와 메서드 수준 모두에 적용될 수 있습니다.

[HttpGet]
public IActionResult List()
{
    return Ok(_todoRepository.All);
}

List 메서드는 200 정상 응답 코드와 JSON으로 직렬화된 모든 Todo 항목을 반환합니다.

여기에 표시된 Postman과 같은 다양한 도구를 사용하여 새 API 메서드를 테스트할 수 있습니다.

Postman console showing a GET request for todoitems and the body of the response showing the JSON for three items returned

항목 만들기

일반적으로 새 데이터 항목 만들기는 HTTP POST 동사로 매핑됩니다. Create 메서드는 [HttpPost] 특성이 적용되어 있으며 TodoItem 인스턴스를 허용합니다. item 인수는 POST 본문에 전달되기 때문에 이 매개 변수는 [FromBody] 특성을 지정합니다.

메서드 내부에서 유효성 및 데이터 저장소의 이전 존재에 대해 항목이 선택되며 문제가 발생하지 않는 경우 리포지토리를 사용하여 추가됩니다. ModelState.IsValid를 확인하면 모델 유효성 검사를 수행하고, 사용자 입력을 허용하는 모든 API 메서드에서 수행되어야 합니다.

[HttpPost]
public IActionResult Create([FromBody]TodoItem item)
{
    try
    {
        if (item == null || !ModelState.IsValid)
        {
            return BadRequest(ErrorCode.TodoItemNameAndNotesRequired.ToString());
        }
        bool itemExists = _todoRepository.DoesItemExist(item.ID);
        if (itemExists)
        {
            return StatusCode(StatusCodes.Status409Conflict, ErrorCode.TodoItemIDInUse.ToString());
        }
        _todoRepository.Insert(item);
    }
    catch (Exception)
    {
        return BadRequest(ErrorCode.CouldNotCreateItem.ToString());
    }
    return Ok(item);
}

샘플은 모바일 클라이언트에 전달되는 오류 코드를 포함하는 enum을 사용합니다.

public enum ErrorCode
{
    TodoItemNameAndNotesRequired,
    TodoItemIDInUse,
    RecordNotFound,
    CouldNotCreateItem,
    CouldNotUpdateItem,
    CouldNotDeleteItem
}

요청의 본문에 JSON 형식의 새 개체를 제공하는 POST 동사를 선택하여 Postman을 사용하는 새 항목 추가를 테스트합니다. 또한 application/jsonContent-Type을 지정하는 요청 헤더를 추가해야 합니다.

Postman console showing a POST and response

메서드는 응답에서 새로 만든 항목을 반환합니다.

항목 업데이트

레코드 수정은 HTTP PUT 요청을 사용하여 수행됩니다. 이 변경 외에 Edit 메서드는 Create와 거의 동일합니다. 레코드를 찾을 수 없는 경우 Edit 작업은 NotFound(404) 응답을 반환합니다.

[HttpPut]
public IActionResult Edit([FromBody] TodoItem item)
{
    try
    {
        if (item == null || !ModelState.IsValid)
        {
            return BadRequest(ErrorCode.TodoItemNameAndNotesRequired.ToString());
        }
        var existingItem = _todoRepository.Find(item.ID);
        if (existingItem == null)
        {
            return NotFound(ErrorCode.RecordNotFound.ToString());
        }
        _todoRepository.Update(item);
    }
    catch (Exception)
    {
        return BadRequest(ErrorCode.CouldNotUpdateItem.ToString());
    }
    return NoContent();
}

Postman으로 테스트하려면 동사를 PUT으로 변경합니다. 요청의 본문에서 업데이트된 개체 데이터를 지정합니다.

Postman console showing a PUT and response

이 메서드는 성공한 경우 기존 API와의 일관성을 위해 NoContent(204) 응답을 반환합니다.

항목 삭제

레코드 삭제는 서비스에 대해 DELETE 요청을 만들고, 삭제될 항목의 ID를 전달하여 수행됩니다. 업데이트를 사용하면 존재하지 않는 항목에 대한 요청은 NotFound 응답을 수신합니다. 그렇지 않으면 성공한 요청은 NoContent(204) 응답을 받습니다.

[HttpDelete("{id}")]
public IActionResult Delete(string id)
{
    try
    {
        var item = _todoRepository.Find(id);
        if (item == null)
        {
            return NotFound(ErrorCode.RecordNotFound.ToString());
        }
        _todoRepository.Delete(id);
    }
    catch (Exception)
    {
        return BadRequest(ErrorCode.CouldNotDeleteItem.ToString());
    }
    return NoContent();
}

삭제 기능을 테스트할 때 요청의 본문에 아무 것도 필요하지 않습니다.

Postman console showing a DELETE and response

과도한 게시 방지

현재 샘플 앱은 전체 TodoItem 개체를 공개합니다. 일반적으로 프로덕션 앱은 모델의 하위 집합을 사용하여 입력 및 반환되는 데이터를 제한합니다. 이 동작에는 여러 가지 이유가 있으며, 보안이 주요 이유 중 하나입니다. 일반적으로 모델의 하위 집합을 DTO(데이터 전송 개체), 입력 모델 또는 뷰 모델이라고 합니다. 이 문서에서는 DTO를 사용합니다.

DTO는 다음과 같은 용도로 사용할 수 있습니다.

  • 과도한 게시를 방지합니다.
  • 클라이언트에 표시되지 않아야 하는 속성을 숨깁니다.
  • 페이로드 크기를 줄이기 위해 일부 속성을 생략합니다.
  • 중첩된 개체를 포함하는 개체 그래프를 평면화합니다. 클라이언트에는 평면화된 개체 그래프가 더 편리할 수 있습니다.

DTO 방법을 보여 주려면 초과 게시 방지를 참조하세요.

공통 Web API 규칙

앱에 대해 백 엔드 서비스를 개발하는 경우 교차 편집 문제 처리를 위해 일관적인 규칙의 집합 또는 정책을 찾을 수 있습니다. 예를 들어 위에 표시된 서비스에서 발견되지 않았던 특정 레코드에 대한 요청은 BadRequest 응답 대신 NotFound 응답을 받았습니다. 마찬가지로, 모델 바인딩 형식을 전달한 이 서비스에 대해 만든 명령은 항상 ModelState.IsValid를 확인했고 잘못된 모델 유형에 대해 BadRequest를 반환했습니다.

API에 대한 일반적인 정책을 식별했으면 필터에서 캡슐화할 수 있습니다. ASP.NET Core MVC 애플리케이션에서 일반적인 API 정책을 캡슐화하는 방법에 대해 자세히 알아봅니다.

추가 리소스