MVC 패턴
MVC(Model-View-Controller)는 소프트웨어 아키텍처 패턴 중 하나로,
애플리케이션을 세 가지 주요 구성 요소로 분리하여 개발하는 방법입니다.
MVC 패턴을 식당에 비유하면:
- Model: 식재료와 레시피 (데이터와 비즈니스 로직)
- View: 완성된 요리의 플레이팅 (사용자에게 보여지는 화면)
- Controller: 요리사 (사용자 요청을 처리하고 Model과 View를 연결)
Java Spring MVC와 동일한 개념으로, C#에서도 같은 구조로 애플리케이션을 설계합니다.
왜 MVC 패턴을 사용할까?
MVC 패턴의 장점
- 관심사의 분리: 각 역할이 명확히 구분되어 코드 관리가 쉬움
- 재사용성: Model과 Controller는 다른 View에서도 재사용 가능
- 유지보수성: 한 부분의 변경이 다른 부분에 미치는 영향 최소화
- 협업 효율성: 팀원들이 각자 담당 영역에 집중할 수 있음
- 테스트 용이성: 각 구성 요소를 독립적으로 테스트 가능
Model - 데이터와 비즈니스 로직의 핵심
Model은 애플리케이션의 데이터와 비즈니스 로직을 담당합니다.
기본 Model 클래스 예제
// Models/Student.cs
using System;
using System.ComponentModel.DataAnnotations;
namespace SchoolApp.Models
{
public class Student
{
// 기본 속성들
public int StudentId { get; set; }
[Required(ErrorMessage = "이름은 필수입니다.")]
[StringLength(50, ErrorMessage = "이름은 50자를 초과할 수 없습니다.")]
public string Name { get; set; }
[Required(ErrorMessage = "이메일은 필수입니다.")]
[EmailAddress(ErrorMessage = "올바른 이메일 형식이 아닙니다.")]
public string Email { get; set; }
[Range(18, 100, ErrorMessage = "나이는 18세 이상 100세 이하여야 합니다.")]
public int Age { get; set; }
public DateTime EnrollmentDate { get; set; }
// 생성자
public Student()
{
EnrollmentDate = DateTime.Now;
}
// 비즈니스 로직 메서드
public bool IsAdult()
{
return Age >= 18;
}
public int GetStudyYears()
{
return DateTime.Now.Year - EnrollmentDate.Year;
}
public string GetDisplayName()
{
return $"{Name} ({Age}세)";
}
}
}
코드 해석:
- [Required], [EmailAddress] 등: 데이터 검증을 위한 어트리뷰트
- public int StudentId { get; set; }: 자동 속성 (Java의 getter/setter와 동일)
- IsAdult(), GetStudyYears(): 비즈니스 로직을 담은 메서드
복잡한 Model - 여러 엔티티 관계
// Models/Course.cs
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace SchoolApp.Models
{
public class Course
{
public int CourseId { get; set; }
[Required]
[StringLength(100)]
public string CourseName { get; set; }
[StringLength(500)]
public string Description { get; set; }
public int Credits { get; set; }
// 다대다 관계 - 한 과목에 여러 학생이 등록 가능
public List<Student> Students { get; set; }
public Course()
{
Students = new List<Student>();
}
// 비즈니스 로직
public int GetStudentCount()
{
return Students?.Count ?? 0;
}
public bool HasStudents()
{
return GetStudentCount() > 0;
}
}
}
ViewModel - View를 위한 특별한 Model
// ViewModels/StudentViewModel.cs
using System.Collections.Generic;
using SchoolApp.Models;
namespace SchoolApp.ViewModels
{
public class StudentViewModel
{
// 학생 목록 표시용
public List<Student> Students { get; set; }
// 검색 조건
public string SearchName { get; set; }
public int? SearchAge { get; set; }
// 페이징 정보
public int CurrentPage { get; set; }
public int TotalPages { get; set; }
public int PageSize { get; set; }
// 통계 정보
public int TotalStudentCount { get; set; }
public int AdultStudentCount { get; set; }
public StudentViewModel()
{
Students = new List<Student>();
PageSize = 10;
CurrentPage = 1;
}
// ViewModel 전용 로직
public bool HasNextPage => CurrentPage < TotalPages;
public bool HasPreviousPage => CurrentPage > 1;
}
}
ViewModel을 사용하는 이유:
- View에 필요한 여러 데이터를 하나로 묶음
- Model과 View 사이의 중간 역할
- 페이징, 검색 조건 등 화면 전용 데이터 포함
Controller - 요청 처리의 중심
Controller는 사용자의 요청을 받아서 적절한 Model을 조작하고 결과를 View로 전달합니다.
기본 Controller 구조
// Controllers/StudentController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using SchoolApp.Models;
using SchoolApp.ViewModels;
namespace SchoolApp.Controllers
{
public class StudentController : Controller
{
// 실제로는 데이터베이스나 서비스를 사용하지만,
// 예제를 위해 메모리 리스트 사용
private static List<Student> _students = new List<Student>
{
new Student { StudentId = 1, Name = "홍길동", Email = "hong@example.com", Age = 20 },
new Student { StudentId = 2, Name = "김철수", Email = "kim@example.com", Age = 22 },
new Student { StudentId = 3, Name = "이영희", Email = "lee@example.com", Age = 19 }
};
// GET: Student/Index - 학생 목록 조회
public ActionResult Index(string searchName = "", int page = 1)
{
const int pageSize = 5;
// 검색 로직
var filteredStudents = _students.AsQueryable();
if (!string.IsNullOrEmpty(searchName))
{
filteredStudents = filteredStudents.Where(s =>
s.Name.Contains(searchName));
}
// 페이징 처리
var totalCount = filteredStudents.Count();
var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
var students = filteredStudents
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList();
// ViewModel 구성
var viewModel = new StudentViewModel
{
Students = students,
SearchName = searchName,
CurrentPage = page,
TotalPages = totalPages,
PageSize = pageSize,
TotalStudentCount = _students.Count,
AdultStudentCount = _students.Count(s => s.IsAdult())
};
return View(viewModel);
}
// GET: Student/Details/5 - 특정 학생 상세 조회
public ActionResult Details(int id)
{
var student = _students.FirstOrDefault(s => s.StudentId == id);
if (student == null)
{
return HttpNotFound("학생을 찾을 수 없습니다.");
}
return View(student);
}
// GET: Student/Create - 학생 생성 폼 표시
public ActionResult Create()
{
return View();
}
// POST: Student/Create - 학생 생성 처리
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Student student)
{
if (ModelState.IsValid)
{
// 새로운 ID 생성
student.StudentId = _students.Max(s => s.StudentId) + 1;
student.EnrollmentDate = DateTime.Now;
_students.Add(student);
// 성공 메시지와 함께 리다이렉트
TempData["SuccessMessage"] = "학생이 성공적으로 등록되었습니다.";
return RedirectToAction("Index");
}
// 유효성 검사 실패 시 폼 다시 표시
return View(student);
}
// GET: Student/Edit/5 - 학생 수정 폼 표시
public ActionResult Edit(int id)
{
var student = _students.FirstOrDefault(s => s.StudentId == id);
if (student == null)
{
return HttpNotFound();
}
return View(student);
}
// POST: Student/Edit/5 - 학생 수정 처리
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(Student student)
{
if (ModelState.IsValid)
{
var existingStudent = _students.FirstOrDefault(s => s.StudentId == student.StudentId);
if (existingStudent != null)
{
existingStudent.Name = student.Name;
existingStudent.Email = student.Email;
existingStudent.Age = student.Age;
TempData["SuccessMessage"] = "학생 정보가 성공적으로 수정되었습니다.";
return RedirectToAction("Index");
}
}
return View(student);
}
// POST: Student/Delete/5 - 학생 삭제 처리
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Delete(int id)
{
var student = _students.FirstOrDefault(s => s.StudentId == id);
if (student != null)
{
_students.Remove(student);
TempData["SuccessMessage"] = "학생이 성공적으로 삭제되었습니다.";
}
else
{
TempData["ErrorMessage"] = "삭제할 학생을 찾을 수 없습니다.";
}
return RedirectToAction("Index");
}
// AJAX용 API 메서드
[HttpPost]
public JsonResult GetStudentInfo(int id)
{
var student = _students.FirstOrDefault(s => s.StudentId == id);
if (student == null)
{
return Json(new { success = false, message = "학생을 찾을 수 없습니다." });
}
return Json(new
{
success = true,
data = new
{
student.StudentId,
student.Name,
student.Email,
student.Age,
DisplayName = student.GetDisplayName(),
StudyYears = student.GetStudyYears(),
IsAdult = student.IsAdult()
}
});
}
}
}
Controller의 주요 개념:
- ActionResult: 컨트롤러 메서드의 반환 타입
- View(): View를 반환
- RedirectToAction(): 다른 액션으로 리다이렉트
- Json(): JSON 데이터 반환
- HttpNotFound(): 404 에러 반환
- HTTP 동사 구분: [HttpPost], [HttpGet] 등으로 요청 방식 지정
- ModelState: 유효성 검사 결과를 담는 객체
- TempData: 한 번의 요청-응답 사이클 동안만 유지되는 데이터
실전 예제: 성적 관리 시스템
Grade Model
// Models/Grade.cs
using System;
using System.ComponentModel.DataAnnotations;
namespace SchoolApp.Models
{
public class Grade
{
public int GradeId { get; set; }
[Required]
public int StudentId { get; set; }
[Required]
public int CourseId { get; set; }
[Range(0, 100, ErrorMessage = "점수는 0-100 사이여야 합니다.")]
public int Score { get; set; }
public DateTime GradeDate { get; set; }
// 네비게이션 속성
public Student Student { get; set; }
public Course Course { get; set; }
// 비즈니스 로직
public string GetLetterGrade()
{
if (Score >= 90) return "A";
if (Score >= 80) return "B";
if (Score >= 70) return "C";
if (Score >= 60) return "D";
return "F";
}
public bool IsPassing()
{
return Score >= 60;
}
public string GetGradeStatus()
{
return IsPassing() ? "합격" : "불합격";
}
}
}
Grade Controller
// Controllers/GradeController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using SchoolApp.Models;
namespace SchoolApp.Controllers
{
public class GradeController : Controller
{
// 예제용 데이터
private static List<Grade> _grades = new List<Grade>();
private static List<Student> _students = new List<Student>
{
new Student { StudentId = 1, Name = "홍길동", Email = "hong@example.com", Age = 20 },
new Student { StudentId = 2, Name = "김철수", Email = "kim@example.com", Age = 22 }
};
private static List<Course> _courses = new List<Course>
{
new Course { CourseId = 1, CourseName = "C# 프로그래밍", Credits = 3 },
new Course { CourseId = 2, CourseName = "데이터베이스", Credits = 3 }
};
// GET: Grade/Index
public ActionResult Index(int? studentId = null)
{
var grades = _grades.AsQueryable();
// 특정 학생의 성적만 조회
if (studentId.HasValue)
{
grades = grades.Where(g => g.StudentId == studentId.Value);
}
// 성적 데이터에 학생, 과목 정보 매핑
var gradeList = grades.ToList();
foreach (var grade in gradeList)
{
grade.Student = _students.FirstOrDefault(s => s.StudentId == grade.StudentId);
grade.Course = _courses.FirstOrDefault(c => c.CourseId == grade.CourseId);
}
// ViewBag으로 드롭다운용 데이터 전달
ViewBag.Students = new SelectList(_students, "StudentId", "Name", studentId);
ViewBag.SelectedStudentId = studentId;
return View(gradeList);
}
// GET: Grade/Create
public ActionResult Create()
{
ViewBag.Students = new SelectList(_students, "StudentId", "Name");
ViewBag.Courses = new SelectList(_courses, "CourseId", "CourseName");
return View();
}
// POST: Grade/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Grade grade)
{
if (ModelState.IsValid)
{
// 중복 체크
var existingGrade = _grades.FirstOrDefault(g =>
g.StudentId == grade.StudentId && g.CourseId == grade.CourseId);
if (existingGrade != null)
{
ModelState.AddModelError("", "해당 학생의 이 과목 성적이 이미 존재합니다.");
ViewBag.Students = new SelectList(_students, "StudentId", "Name", grade.StudentId);
ViewBag.Courses = new SelectList(_courses, "CourseId", "CourseName", grade.CourseId);
return View(grade);
}
grade.GradeId = _grades.Count + 1;
grade.GradeDate = DateTime.Now;
_grades.Add(grade);
TempData["SuccessMessage"] = "성적이 성공적으로 등록되었습니다.";
return RedirectToAction("Index");
}
ViewBag.Students = new SelectList(_students, "StudentId", "Name", grade.StudentId);
ViewBag.Courses = new SelectList(_courses, "CourseId", "CourseName", grade.CourseId);
return View(grade);
}
// GET: Grade/Statistics
public ActionResult Statistics()
{
var statistics = new
{
TotalGrades = _grades.Count,
AverageScore = _grades.Any() ? _grades.Average(g => g.Score) : 0,
PassingRate = _grades.Any() ?
(double)_grades.Count(g => g.IsPassing()) / _grades.Count * 100 : 0,
GradeDistribution = _grades.GroupBy(g => g.GetLetterGrade())
.Select(group => new { Grade = group.Key, Count = group.Count() })
.OrderBy(x => x.Grade)
.ToList()
};
return View(statistics);
}
// AJAX: 학생별 평균 점수 조회
[HttpPost]
public JsonResult GetStudentAverage(int studentId)
{
var studentGrades = _grades.Where(g => g.StudentId == studentId).ToList();
var student = _students.FirstOrDefault(s => s.StudentId == studentId);
if (student == null)
{
return Json(new { success = false, message = "학생을 찾을 수 없습니다." });
}
var result = new
{
success = true,
studentName = student.Name,
totalCourses = studentGrades.Count,
averageScore = studentGrades.Any() ? studentGrades.Average(g => g.Score) : 0,
passingCourses = studentGrades.Count(g => g.IsPassing()),
grades = studentGrades.Select(g => new
{
courseName = _courses.FirstOrDefault(c => c.CourseId == g.CourseId)?.CourseName,
score = g.Score,
letterGrade = g.GetLetterGrade(),
status = g.GetGradeStatus()
}).ToList()
};
return Json(result);
}
}
}
MVC 패턴의 데이터 흐름
사용자 요청 → Controller → Model 조작 → Controller → View 반환 → 사용자
실제 동작 과정 예시:
- 사용자가 "학생 목록" 페이지 요청
- URL: /Student/Index
- Controller에서 요청 처리
- public ActionResult Index() { // Model에서 데이터 조회 var students = _students.ToList(); // View에 데이터 전달 return View(students); }
- Model에서 데이터 제공
- 학생 리스트 반환
- 비즈니스 로직 실행 (예: 성인 여부 판단)
- View에서 화면 렌더링
- HTML 생성하여 사용자에게 반환
초보자가 헷갈리기 쉬운 포인트
1. Model vs ViewModel 차이점
// Model - 실제 데이터베이스 엔티티
public class Student
{
public int StudentId { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
// ViewModel - 화면 표시용 데이터 조합
public class StudentListViewModel
{
public List<Student> Students { get; set; } // Model 포함
public string SearchKeyword { get; set; } // 화면 전용 데이터
public int CurrentPage { get; set; } // 페이징 정보
}
2. ActionResult 반환 타입들
// View 반환
return View(model);
// 다른 액션으로 리다이렉트
return RedirectToAction("Index");
// JSON 데이터 반환 (AJAX용)
return Json(data);
// 404 에러
return HttpNotFound();
// 파일 다운로드
return File(fileBytes, "application/pdf", "document.pdf");
3. HTTP 동사별 사용법
// GET - 데이터 조회 (기본값)
public ActionResult Index() { }
// POST - 데이터 생성/수정
[HttpPost]
public ActionResult Create(Student student) { }
// PUT - 전체 데이터 수정
[HttpPut]
public ActionResult Update(Student student) { }
// DELETE - 데이터 삭제
[HttpDelete]
public ActionResult Delete(int id) { }
정리 및 실무 활용 팁
MVC 패턴 구현 시 주의사항
- 단일 책임 원칙: 각 클래스는 하나의 역할만 담당
- 의존성 주입: Controller에서 직접 데이터 접근 대신 서비스 계층 사용
- 유효성 검사: Model에서 데이터 어트리뷰트로 검증 규칙 정의
- 에러 처리: try-catch 블록과 적절한 에러 메시지 제공
- 보안: [ValidateAntiForgeryToken] 등 보안 관련 어트리뷰트 활용
실무에서 자주 사용하는 패턴
// Repository 패턴과 함께 사용
public class StudentController : Controller
{
private readonly IStudentRepository _studentRepository;
public StudentController(IStudentRepository studentRepository)
{
_studentRepository = studentRepository;
}
public ActionResult Index()
{
var students = _studentRepository.GetAll();
return View(students);
}
}
핵심 포인트:
- MVC는 관심사의 분리를 통한 코드 구조화 패턴
- Model은 데이터와 비즈니스 로직, Controller는 흐름 제어
- 각 구성 요소의 역할을 명확히 구분하여 유지보수성 향상
- 실무에서는 Repository, Service 패턴과 함께 사용
정리
| 구분 | 역할 | 주요 책임 | 예시 |
| Model | 데이터 + 비즈니스 로직 | 데이터 구조 정의, 유효성 검사, 비즈니스 규칙 | Student.cs, Grade.cs |
| Controller | 요청 처리 + 흐름 제어 | 사용자 입력 처리, Model 조작, View 선택 | StudentController.cs |
| ViewModel | View 전용 데이터 모델 | 화면 표시용 데이터 조합, 페이징, 검색 조건 | StudentListViewModel.cs |
| ActionResult | Controller 반환 타입 | View, Redirect, JSON 등 다양한 응답 타입 | View(), RedirectToAction() |
| 데이터 흐름 | 요청→처리→응답 | 사용자 요청을 받아 적절한 응답 생성 | URL → Controller → Model → View |