파트 10: ASP.NET Core MVC - Details 메서드 및 Delete 메서드 살펴보기

등록일시: 2016-07-15 08:00,  수정일시: 2016-07-18 23:47
조회수: 5,040
이 문서는 ASP.NET Core MVC 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
본 자습서 시리즈의 마지막 파트인 본문에서는 스캐폴딩 엔진이 생성한 Details 메서드와 Delete 메서드의 코드를 살펴봅니다.

먼저 Movie 컨트롤러를 열고 Details 메서드부터 살펴보겠습니다:

// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.SingleOrDefaultAsync(m => m.ID == id);
    if (movie == null)
    {
        return NotFound();
    }

    return View(movie);
}

이 액션 메서드의 주석에는 MVC 스캐폴딩 엔진이 코드를 생성하면서 메서드를 호출할 수 있는 한 가지 HTTP 요청 사례를 기입해 놓았습니다. Movies 컨트롤러와 Details 메서드, 그리고 id 값, 이렇게 세 가지 URL 세그먼트들로 구성된 GET 요청이 그것입니다. 대부분 기억하시겠지만 이 세그먼트들은 Startup.cs 파일에서 설정됩니다.

app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}");
});

Code First를 사용하면 SingleOrDefaultAsync 메서드를 통해서 손쉽게 원하는 데이터를 검색할 수 있습니다. Details 메서드에 보안과 관련해서 구현된 주목할 만한 특징 한 가지는, 영화 정보를 이용해서 무언가 작업을 수행하기 전에 항상 먼저 검색 메서드의 결과가 존재하는지 여부부터 확인한다는 점입니다. 만약 이 검사를 수행하지 않으면, 악의적인 사용자가 목록 페이지의 링크를 통해서 얻을 수 있는 URL인 http://localhost:xxxx/Movies/Details/1http://localhost:xxxx/Movies/Details/12345 같은 URL로 변조해서 (즉, 실제로 존재하지 않는 유효하지 않은 영화 정보를 지정해서) 사이트에 오류를 만들어낼 수 있습니다. 따라서 영화 정보가 null인지 확인하지 않는다면 응용 프로그램이 예외를 던질 것입니다.

계속해서 이번에는 Delete 메서드와 DeleteConfirmed 메서드를 살펴봅니다.

// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.SingleOrDefaultAsync(m => m.ID == id);
    if (movie == null)
    {
        return NotFound();
    }

    return View(movie);
}

// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
    var movie = await _context.Movie.SingleOrDefaultAsync(m => m.ID == id);
    _context.Movie.Remove(movie);
    await _context.SaveChangesAsync();
    return RedirectToAction("Index");
}

이 코드의 HTTP GET Delete 메서드는 지정된 영화 정보를 삭제하는 것이 아니라, 단지 삭제 작업(HttpPost)을 승인할 수 있는 영화 정보 뷰를 반환할 뿐이라는 점에 주의하시기 바랍니다. GET 요청의 응답으로 삭제 작업을 수행하면 (또는 수정 작업이나 생성 작업을 비롯한 데이터 변경이 발생하는 모든 유형의 작업을 수행하면) 보안상의 취약점이 발생합니다.

그리고 데이터를 삭제하는 [HttpPost] 메서드의 이름이 DeleteConfirmed로 지정되어 있는 것을 볼 수 있는데, 이는 HTTP POST 메서드에 고유한 시그니처 및 이름을 부여하기 위한 것입니다. 두 메서드의 시그니처를 비교해보면 다음과 같습니다:

// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)

// POST: Movies/Delete/
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)

공통 언어 런타임(CLR, Common Language Runtime)에서 오버로드 된 메서드의 시그니처는 유일해야합니다 (즉, 메서드의 이름이 같을 수는 있지만 매개변수들의 목록은 달라야 합니다). 그러나 현재 상황은 두 가지 Delete 메서드(GET 버전과 POST 버전)가 모두 필요한 경우임에 반해, 공교롭게도 두 메서드의 매개변수 시그니처가 동일합니다. (즉, 두 메서드 모두 하나의 정수형 매개변수를 받습니다.)

이 문제점을 해결할 수 있는 방법은 두 가지가 있습니다. 첫 번째 방법은 두 메서드의 이름을 서로 다르게 지정하는 것입니다. 본문의 예제에서 스캐폴딩 메커니즘 역시 이 방식을 따르고 있음을 확인할 수 있습니다. 그러나 여전히 사소한 문제점이 존재하는데, ASP.NET이 URL의 세그먼트들을 액션 메서드와 매핑할 때 메서드의 이름을 기준으로 처리하기 때문에, 지금처럼 메서드 이름을 변경해버리면 라우팅이 적절한 액션 메서드를 찾을 수 없게 됩니다. 이 문제의 해결방법은 본문의 예제 코드에서 볼 수 있는 것처럼 ActionName("Delete") 어트리뷰트를 DeleteConfirmed 메서드에 지정하는 것입니다. 이 어트리뷰트를 적용하면 /Delete/ 세그먼트가 포함된 URL에 대한 POST 요청을 라우팅 시스템을 통해서 DeleteConfirmed 메서드로 원활하게 매핑할 수 있습니다.

또는 동일한 이름과 시그니처를 갖고 있는 메서드의 문제점을 해결할 수 있는 또 다른 일반적인 방법은 아예 POST 메서드의 시그니처에 인위적으로 사용하지 않는 추가적인 매개변수를 추가하는 것입니다. 검색 기능 추가하기 파트에서 notUsed 매개변수를 추가했던 이유가 바로 이 때문으로, [HttpPost] Delete 메서드도 동일한 방식으로 처리할 수 있습니다:

[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id, bool notUsed)
{
    var movie = await _context.Movie.SingleOrDefaultAsync(m => m.ID == id);
    _context.Movie.Remove(movie);
    await _context.SaveChangesAsync();
    return RedirectToAction("Index");
}