엠소프트웨어 3월 기술세미나의 일환으로, 글을 통해 .NET Core 3.1 환경에서 CRUD기능을 포함한 Web API를 만들고 테스트 및 문서화 하는 일련의 과정을 공유하겠습니다.
꼭 PostgreSQL을 쓰지않고 다른 DB를 사용하셔도 상관 없지만,
이 글은 ASP.NET Core 3.1, PostgreSQL 환경에서 진행됩니다
Visual Studio 2019
PostgreSQL
PG Admin
Postman
Web API를 만들기 전에 API(Application Programming Interface)가 무엇인지 설명하겠습니다
위키백과에서는 API를 이렇게 설명하고 있습니다
API는 응용프로그램에서 사용할 수 있도록, 운영체제나 프로그래밍언어가 제공하는 기능을 제어할 수 있게 만든 인터페이스를 뜻한다.
주로 파일제어, 창 제어, 화상 처리, 문자 제어 등을 위한 인터페이스를 제공한다
정리하자면 API는 다른 응용프로그램이 사용할 수 있게끔 여러 기능과 시스템을 제어하는 기능을 제공하는 Interface라는 뜻입니다
한번 보고 이해가 잘 되지 않아서 예를 들어보겠습니다
A가게, B가게, C가게가 있습니다
처음에는 주변지역을 대상으로 주문과 배달을 했지만,
가게의 홍보가 너무 잘되어서 배달하기 힘든 다른 지역에서도 주문하기 시작했습니다
거리가 너무 멀어 가게들은 배달을 포기하고 지내고 있었습니다
그 때, D라는 배송업체가 나타나
"배송료와 물건 그리고 배달할 위치를 알려주면 우리가 대신 배송할게"
라고 제안합니다
A,B,C 가게는 상품을 더 많이 팔기 위해서 D라는 배송업체와 계약하고 더 많은 상품들을
팔 수 있게 되었습니다
비유가 조잡한 느낌이 들지만, 이 비유에서 각 가게들과 D업체의 관계를 보고 API를 아시겠나요?
A,B,C 가게들이 먼 지역에도 상품판매를 하기 위해 필요한 배송이라는 기능을 D업체가 제공한다는 뜻입니다
이렇게 API는 요청하는 기능을 제어할 수 있게 해주는 인터페이스라고 정리하고 넘어가겠습니다
본격적으로 프로젝트를 생성해 API를 만들겠습니다
Visual Studio 2019를 실행한 후, 새 프로젝트 만들기를 선택합니다
C#, 모든 플랫폼, 웹을 순차적으로 선택한 후 API를 선택합니다
프로젝트 이름, 위치, 솔루션 이름을 설정합니다
프레임워크 버전을 .NET Core 3.1로 선택한 후, 만들기를 선택해 프로젝트를 만듭니다
Web API에서 필요한 데이터를 요청할 때 실제 DB에 있는 데이터까지 다루기 위해 .NET에서 제공하는 ORM인 EF Core를 사용해 DB와 API를 연결하겠습니다
상단 메뉴에서 '도구 < Nuget Package 관리자 < 패키지 관리자 콘솔' 을 선택합니다
하단에 패키지 관리자 콘솔이 생겼다면 아래 코드를 입력해 dotnet-ef cli tool을 설치합니다
dotnet tool install --global dotnet-ef
설치가 완료되었다면 상단 메뉴에서 '도구 < Nuget Package 관리자 < 솔루션용 Nuget 패키지 관리' 를 선택합니다
아래는 솔루션용 패키지 관리를 이용해 설치할 패키지명입니다.
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.Design
Microsoft.EntityFrameworkCore.Tools
Npgsql.EntityFrameworkCore.PostgreSQL
찾아보기 탭에서 'EF Core'를 검색합니다
선택된 패키지들을 설치합니다
설치할 대상 프로젝트와 버전을 확인하고 설치를 진행합니다
검색창에 'EF Npgsql'을 검색한 후, 그림을 따라 설치를 진행합니다
설치가 모두 끝난 후에 솔루션을 빌드합니다.
빌드까지 정상적으로 완료되었다면 PG Admin을 열어 데이터베이스를 생성하겠습니다
암호를 입력해 PG Admin에 접근합니다
Database명을 입력한 후 Save버튼을 클릭해 데이터베이스를 생성합니다
이제 다시 Visual Studio로 돌아와서 마이그레이션을 준비하겠습니다
솔루션 탐색기 창에 있는 프로젝트에 우클릭해 '추가 < 새폴더' 를 선택해 Models폴더를 생성합니다
Models폴더에 우클릭해 '추가 < 클래스' 를 선택해 MyDbContext.cs 라는 이름으로 클래스를 생성합니다
DbContext.cs파일 생성
그 다음 테이블의 구조를 구성하는 역할을 해줄 Model클래스들을 생성합니다
다만, 이 글에서는 한 개의 테이블을 가지고 CRUD를 구성할 예정이므로 Models 폴더 안에 Users.cs 라는 이름의 클래스 하나만 생성하겠습니다
// Users.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace UserManagementApplication.Models
{
public class Users
{
public int UserIdx { get; set; }
public string UserName { get; set; }
public string UserId { get; set; }
public string Password { get; set; }
public string NickName { get; set; }
public bool PublicYn { get; set; }
public DateTime ModifyDate { get; set; }
public DateTime RegistDate { get; set; }
}
}
Model클래스 까지 생성을 했다면 DbContext클래스에 아래 코드를 작성합니다
// MyDbContext.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace UserManagementApplication.Models
{
public class MyDbContext : DbContext
{
public MyDbContext(DbContextOptions<MyDbContext> options)
:base(options)
{
}
public DbSet<Users> Users { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Users>(entity =>
{
entity.HasKey(e => e.UserIdx).HasName("users_pkey");
entity.ToTable("users");
entity.Property(e => e.UserIdx).HasColumnName("useridx").UseIdentityAlwaysColumn();
entity.Property(e => e.UserName).IsRequired().HasColumnName("username").HasMaxLength(50);
entity.Property(e => e.UserId).IsRequired().HasColumnName("userid").HasMaxLength(50);
entity.Property(e => e.Password).IsRequired().HasColumnName("password").HasMaxLength(256);
entity.Property(e => e.NickName).IsRequired().HasColumnName("nickname").HasMaxLength(100);
entity.Property(e => e.PublicYn).IsRequired().HasColumnName("publicyn");
entity.Property(e => e.ModifyDate).IsRequired().HasColumnName("modifydate").HasColumnType("timestamp with time zone");
entity.Property(e => e.RegistDate).IsRequired().HasColumnName("registdate").HasColumnType("timestamp with time zone");
});
}
}
}
다음은 appsettings.json에 DB연결정보를 작성합니다
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"PostgreSQLConnection": "Host=127.0.0.1;Database=데이터베이스명;Username=postgres;Password=비밀번호"
}
}
마지막으로 Startup.cs에 종속성 주입을 하겠습니다
// Startup.cs
using Microsoft.EntityFrameworkCore;
using UserManagementApplication.Models; // using 프로젝트명.Models;
namespace UserManagementApplication
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// PostgreSQL DbContext 종속성 주입
services.AddDbContext<MyDbContext>(options => options.UseNpgsql(dbconnstring));
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
}
}
여기까지 진행했다면 마이그레이션을 시작할 수 있습니다
상단 메뉴에서 '도구 < Nuget Package 관리자 < 패키지 관리자 콘솔' 을 선택해 패키지 관리자 콘솔을 열어 아래 명령어를 순서대로 입력합니다
현재 디렉토리 확인
dir
프로젝트 폴더로 이동
cd 프로젝트 이름
마이그레이션
dotnet ef migrations add 설정할 마이그레이션명
DB에 적용
dotnet ef database update
이렇게 진행하신 후에 위에서 만든 Users클래스가 정상적으로 데이터베이스에 테이블로 생성되었는지 확인합니다
우선 CRUD기능을 만들 API Controller를 만들겠습니다
Controllers 폴더를 우클린 하고 '추가 < 컨트롤러' 를 선택합니다
좌측 메뉴에 있는 API를 선택하고 'API 컨트롤러 - 비어있음' 을 클릭, 컨트롤러 이름까지 정하고 생성합니다.
이 때 Controller의 이름은 반드시 Controller로 끝나게끔 이름을 정해주어야합니다.
ApiController.cs 화면
지금 API Controller를 만들었는데 컨트롤러의 구조를 보시면 방금 만든 Controller가 ControllerBase를 상속받는 구조임을 알 수 있습니다
MVC컨트롤러는 만들게 되면 Controller를 상속받는 구조인데, Controller클래스 또한 ControllerBase를 상속받고 있습니다
지금 만든 컨트롤러와 마찬가지로 MVC가 상속받는 Controller 클래스 또한 ControllerBase를 상속받고 있지만, Controller클래스는 뷰에 대한 지원기능이 추가된 클래스이기 때문에
순수하게 API만 사용하는 컨트롤러에서는 ControllerBase를 상속받는 구조로 컨트롤러를 만들고, MVC의 기능을 사용하는 컨트롤러에서는 Controller 클래스를 상속받는 구조로 컨트롤러를 생성하면 되겠습니다
그리고 Route라는 부분도 있는데 이 부분은 이 컨트롤러의 메소드를 호출할 때 api/컨트롤러명 의 uri로 호출하게끔 설정해 주는 부분입니다
라우팅에 관련해서는 뒷부분에서 더 설명하도록 하겠습니다
우선 Web API를 서비스하려면 CORS(Cross-Origin Resource Sharing)을 프로젝트에 적용해주어야 합니다
웹 브라우저는 보안상의 이유로 다른 도메인에서 리소스를 요청해서 받아오면, 받아온 응답에 대해서 사용하지 않습니다
하지만 웹 서비스를 운영하면서 API를 보여주는 서버와 데이터를 요청하는 웹 서버를 따로 운영하는 경우, 웹 서버에서 필요한 데이터를 API서버로 호출할 때 두 개의 서버의 도메인이 다르기 때문에 API서버가 데이터를 보내주더라도 웹 서버에서 보내준 리소스를 사용하지 않게됩니다
대부분의 경우 WEB서버와 API서버를 따로 운영하기 때문에 CORS를 사용하도록 허용해주어야만 원하는 리소스를 받아올 수 있습니다
그래서 만들고 있는 API 프로젝트에도 CORS를 사용하도록 설정하겠습니다
// startup.cs
public void ConfigureServices(IServiceCollection services)
{
//CORS 설정
services.AddCors(options =>
{
options.AddDefaultPolicy(
builder =>
{
builder.WithOrigins("https://localhost:12345", // 허용할 도메인 입력
"https://www.test.com");
});
});
services.AddControllers();
...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseRouting();
app.UseCors();
app.UseAuthorization();
...
}
이렇게 입력하면 CORS적용이 성공적으로 끝났습니다
이제 CRUD기능을 만들려고 하는데, 만들기 전에 CRUD가 무엇인지 그리고 왜 API를 만들면서 CRUD기능을 메소드로 따로 분리하는지에 대해 설명하겠습니다
우선 마이크로소프트에서는 Web API를 RESTful한 API라고 소개 하고 있습니다
RESTful한 API라는 말은 API가 로이 필딩(Roy Fielding)이 소개한 REST 아키텍쳐에 부합하는 API를 뜻하는 말입니다
그렇다면 REST가 도대체 무엇이길래 우리가 CRUD기능을 만드는데 REST를 알아야할까요?
REST는 웹의 장점을 최대한 활용할 수 있게 만들기위해 소개된 아키텍쳐입니다
REST는 자원(Resource), 행위(Verb), 표현(Representation) 으로 구성되어 있습니다
여기서 자원은 URI, 행위는 HTTP Method, 표현은 서버가 클라이언트에 보내는 응답에 해당됩니다
그리고 REST의 특징 6가지까지 만족할 때, 비로소 우리가 만든 API를 RESTful한 API라고 말할 수 있습니다
CRUD(Create, Read, Update, Delete)기능은 HTTP Method에 POST, GET, PUT(PATCH), DELETE 메소드와 대응됩니다
그래서 우리가 기본적인 CRUD기능을 구현할 때 각각의 목적에 맞게 API를 호출하기 위해서는 기능과 일치하게끔 HTTP Method와 URI를 설정하는게 합당합니다
하지만 RESTful한 API를 설계하고 운영하는 매우 힘들기때문에, 오늘날의 대부분의 REST API라고 말하는 API들은 모든 조건을 완벽하게 만족시키고있지는 않습니다
그렇기때문에 이 글에서 만들게 될 API도 REST API라고 부르기는 어렵지만 최대한 조건에 따라 만들겠습니다
이제 CRUD기능을 만들기 위해서 만든 Controller에 각 CRUD에 대응하는 GET, POST, PUT(PATCH), DELETE 메소드를 만들겠습니다
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using UserManagementApplication.Models;
namespace UserManagementApplication.Controllers
{
[Route("api/user")]
[ApiController]
public class APIController : ControllerBase
{
/// <summary>
/// 유저정보를 조회한다
/// </summary>
/// <param name="uidx">조회대상 회원고유번호</param>
/// <returns></returns>
[HttpGet]
public ActionResult<Users> GetUser(int uidx)
{
return Ok();
}
/// <summary>
/// 유저정보를 등록한다
/// </summary>
/// <param name="user">등록할 유저정보</param>
/// <returns></returns>
[HttpPost]
public ActionResult<Users> PostUser(Users user)
{
return Ok();
}
/// <summary>
/// 유저정보를 수정한다
/// </summary>
/// <param name="user">수정할 유저정보</param>
/// <returns></returns>
[HttpPut]
public ActionResult<Users> PutUser(Users user)
{
return Ok();
}
/// <summary>
/// 유저정보를 삭제한다
/// </summary>
/// <param name="uidx">삭제대상 회원고유번호</param>
/// <returns></returns>
[HttpDelete]
public ActionResult<int> DeleteUser(int uidx)
{
return Ok();
}
}
}
이렇게 코드를 입력해서 각각의 메소드를 만들었지만 실제로 실행시키면 아무것도 바뀌지 않은채로 Ok(200)을 보내게 됩니다.
이제 원하는 기능을 수행하도록 메소드 내부에 코드를 작성하겠습니다
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using UserManagementApplication.Models;
namespace UserManagementApplication.Controllers
{
[Route("api/user")]
[ApiController]
public class APIController : ControllerBase
{
private readonly MyDbContext _dbContext;
public APIController(MyDbContext dbContext)
{
_dbContext = dbContext;
}
/// <summary>
/// 유저정보를 조회한다
/// </summary>
/// <param name="uidx">조회대상 회원고유번호</param>
/// <returns></returns>
[HttpGet]
public ActionResult<Users> GetUser(int uidx)
{
var user = _dbContext.Users.Find(uidx);
return Ok(user);
}
/// <summary>
/// 유저정보를 등록한다
/// </summary>
/// <param name="user">등록할 유저정보</param>
/// <returns></returns>
[HttpPost]
public ActionResult<Users> PostUser(Users user)
{
_dbContext.Users.Add(user);
_dbContext.SaveChanges();
return Ok(user);
}
/// <summary>
/// 유저정보를 수정한다
/// </summary>
/// <param name="user">수정할 유저정보</param>
/// <returns></returns>
[HttpPut]
public ActionResult<Users> PutUser(Users user)
{
_dbContext.Entry(user).State = EntityState.Modified;
return Ok(user);
}
/// <summary>
/// 유저정보를 삭제한다
/// </summary>
/// <param name="uidx">삭제대상 회원고유번호</param>
/// <returns></returns>
[HttpDelete]
public ActionResult<int> DeleteUser(int uidx)
{
var user = _dbContext.Users.Find(uidx);
_dbContext.Users.Remove(user);
return _dbContext.SaveChanges();
}
}
}
유저정보를 읽고 입력하고 수정하고 삭제할 수 있는 기능이 준비가 되었습니다
그럼 실제로 API가 동작하는지 확인하겠습니다
API가 정삭적으로 동작하는지 확인하기 위해서 몇가지 방법이 있는데, 그 중 하나인 Postman이라는 프로그램을 사용해 API의 기능을 확인하려고 합니다
우선 솔루션을 빌드한 후에 실행시켜 API Application을 로컬에서 호출할 수 있게끔 합니다
실행시키면 localhost:port번호/weatherforecast의 url로 웹브라우저가 열렸을텐데 웹브라우저는 이 상태로 두고 Postman을 실행시킵니다
Postman 실행화면
상단 탭에 +버튼을 클릭하고 나온 url입력란에 https://localhost:포트번호/api/user?uidx=1을 입력한 후 Send버튼을 클릭합니다GET 호출 결과화면
지금은 DB에 데이터가 아무것도 없기 때문에 결과가 나오지 않습니다
DB에 데이터를 넣어주기 위해 POST로 호출하도록 하겠습니다
POST로 호출할 때는 POST에서 요구하는 Input Parameter로 작성된 Users의 값을 넣어줘야합니다
{
"userName" : "강구민",
"userId" : "robbie",
"password" : "test1234",
"nickName" : "Robbie",
"publicYn" : false,
"modifyDate" : "2021-03-22",
"registDate" : "2021-03-22"
}
위의 값을 상단 Body탭에 raw JSON을 선택하고 작성한 후 Send버튼을 클릭하면 POST 요청을 할 수 있습니다
POST 호출 결과화면
이제 위에서 했던 방식으로 GET을 호출하면 정상적으로 데이터가 출력되는것을 볼 수 있습니다
PUT과 DELETE도 GET과 POST를 호출했던 것과 마찬가지의 방식으로 테스트를 하면 정상적으로 호출되는지 확인할 수 있습니다
지금까지 만든 API를 Swagger라는 오픈소스 프레임워크를 사용해 자동으로 문서화를 하려고 합니다
이를 위해서 몇 가지 준비가 필요합니다
우선 실행시키고 있던 웹사이트를 종료하고 솔루션을 열어 NugetPackage 관리자('도구 < Nuget Package 관리자 < 솔루션용 Nuget 패키지 관리')를 열겠습니다
그 다음 검색란에 Swashbuckkle.AspNetCore를 검색하고 최신버전을 설치합니다
솔루션용 Nuget Package Manager
설치가 완료되면 솔루션 빌드를 하고 Startup.cs에 아래 코드를 작성합니다
//startup.cs
using Microsoft.OpenApi.Models;
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
...
// 추가할 코드
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("myapi", new OpenApiInfo
{
Version = "v1",
Title = "My API Docs",
Description = "My API Documentation"
});
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseHttpsRedirection();
// 추가할 코드
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/myapi/swagger.json", "My API Docs V1");
});
...
app.UseRouting();
...
}
다시 솔루션을 빌드하고 실행시킨후에 https://localhost:포트번호/swagger로 이동합니다
Swagger 실행화면
정상적으로 실행이 되면 이런식으로 화면이 나오게 됩니다
기본적인 구성은 APIController 아래있는 각 메서드의 HTTP Method와 Route에 따라 정리가 되어 보여지며 클릭했을 때 input값의 구조와 return값의 구조를 확인할 수 있습니다
마지막으로 더 많은 기능과 메소드를 만들고 라우팅합니다
우선 아래 코드를 API Controller에 추가로 작성하겠습니다
/// <summary>
/// 전체유저목록을 조회한다
/// </summary>
/// <returns></returns>
[HttpGet]
public ActionResult<IEnumerable<Users>> GetUserList()
{
var userList = _dbContext.Users.ToList();
return Ok(userList);
}
그리고 Postman을 통해 GET : /api/user 를 호출하면 아래와 같은 화면이 나오게 됩니다
결과화면
이런 오류가 나오게 된것은 동일한 HTTP Method와 URI를 가지고 있는 메소드가 존재하기 때문입니다
RESTful한 API는 HTTP Method와 URI를 가지고 사용하려는 API를 찾기 때문에 명확히 알아볼 수 있도록 Method와 URI를 설정해야합니다
그리고 결국에는 다양한 기능을 API로 만들기 위해서는 Routing을 이용해 메소드의 URI를 분리하는 과정이 필요합니다
이제 아래의 코드처럼 Routing을 하겠습니다
// API Controller
/// <summary>
/// 유저정보를 조회한다
/// GET : /api/users/1
/// </summary>
/// <param name="uidx">조회대상 회원고유번호</param>
/// <returns></returns>
[HttpGet]
[Route("{uidx}")]
public ActionResult<Users> GetUser(int uidx)
{
var user = _dbContext.Users.Find(uidx);
return Ok(user);
}
/// <summary>
/// 전체유저목록을 조회한다
/// GET : /api/users
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("/api/users")]
public ActionResult<IEnumerable<Users>> GetUserList()
{
var userList = _dbContext.Users.ToList();
return Ok(userList);
}
이렇게 수정하고 다시 Postman을 이용해 설정한 URI로 API를 호출하면 두 메소드 다 정상적으로 호출되는 것을 확인할 수 있습니다
글을 통해 .NET Core 3.1 환경에서 Web API를 만드는 방법과 테스트 하는 방법을 소개했습니다
글이 실제 서비스처럼 API를 만들고 있지는 않지만 이 글을 통해 .NET에 처음 접하는 분들이 조금 더 쉽게 입문하는데 도움이 되기를 바랍니다
https://timetodev.co.kr/blog/170 (원글)
https://github.com/mygumi22/MSoftware-TechSeminar-21.03 (소스)