.NET Core MVC + Dapper
들어가며
Dapper는 Stack Overflow 팀이 만든 Micro ORM 이다. Entity Framework처럼 무거운 ORM 대신, SQL을 직접 작성하면서도 객체 매핑은 자동으로 해주는 가벼운 도구다.
Java 개발자 관점에서 보면, Spring JDBC Template 또는 MyBatis 와 비슷한 위치에 있다. JPA처럼 SQL을 추상화하지 않고, 개발자가 직접 쿼리를 작성한다.
이 글에서는 .NET Core MVC 프로젝트에서 Dapper를 사용해 기본적인 CRUD를 구현하는 과정을 정리한다. Java 경험이 있는 개발자가 C#으로 넘어올 때 참고할 수 있도록, 두 언어의 개념을 비교하며 설명한다.
프로젝트 구조
최종적으로 완성되는 프로젝트 구조는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
DapperMvcDemo/
├── Data/
│ └── DapperDbContext.cs ← DB 연결 관리
├── Models/
│ └── ProductModel.cs ← 엔티티
├── Repositories/
│ ├── IProductRepository.cs ← 인터페이스
│ └── ProductRepository.cs ← Dapper 쿼리 실행
├── Controllers/
│ └── ProductsController.cs ← 요청 처리
├── Views/Products/
│ ├── Index.cshtml ← 목록
│ ├── Create.cshtml ← 등록
│ ├── Edit.cshtml ← 수정
│ └── Delete.cshtml ← 삭제
├── Program.cs ← DI 등록
└── appsettings.json ← 연결 문자열
Java Spring 프로젝트와 비교하면 이렇다.
| C# .NET Core | Java Spring |
|---|---|
| Models/ | domain/ 또는 entity/ |
| Repositories/ | repository/ |
| Controllers/ | controller/ |
| Views/ | templates/ (Thymeleaf) |
| Program.cs | @Configuration 클래스 |
| appsettings.json | application.properties |
환경 설정
프로젝트 생성 및 패키지 설치
1
2
3
4
dotnet new mvc -n DapperMvcDemo
cd DapperMvcDemo
dotnet add package Dapper
dotnet add package Microsoft.Data.SqlClient
Java로 치면 build.gradle이나 pom.xml에 의존성을 추가하는 것과 같다.
연결 문자열 설정 (appsettings.json)
1
2
3
4
5
{"ConnectionStrings":{"DefaultConnection":"Server={SSMS 연결할때 쓴 본인 서버이름};Database=SampleDB;Trusted_Connection=true;TrustServerCertificate=true;"}}
SSMS에서 SSL 인증서 오류가 발생하면 연결 옵션에서 서버 인증서 신뢰 를 체크하면 된다.
DbContext - 연결 관리 클래스
DapperDbContext.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
public class DapperDbContext
{
private readonly IConfiguration _configuration;
private readonly string _connectionString;
public DapperDbContext(IConfiguration configuration)
{
_configuration = configuration;
_connectionString = configuration.GetConnectionString("DefaultConnection");
}
public IDbConnection CreateConnection() => new SqlConnection(_connectionString);
}
이 클래스의 역할은 단순하다. 연결 문자열을 보관하고, 필요할 때 DB 연결 객체를 생성 해주는 것이다.
Java로 비유하면 DataSource 또는 ConnectionFactory에 해당한다. CreateConnection() 메서드는 dataSource.getConnection()과 같은 역할을 한다.
Q. 왜 이렇게 분리하는가?
연결 문자열을 한 곳에서 관리하기 위해서다. 여러 Repository에서 각자 연결 문자열을 하드코딩하면 유지보수가 어려워진다.
Repository 패턴
인터페이스 정의 (IProductRepository.cs)
1
2
3
4
5
6
7
8
public interface IProductRepository
{
Task<IEnumerable<ProductModel>> GetAll();
Task<ProductModel> GetById(Guid id);
Task<ProductModel> Create(ProductModel model);
Task<ProductModel> Update(ProductModel model);
Task Delete(Guid id);
}
Java의 JpaRepository 인터페이스를 직접 정의한다고 생각하면 된다.
여기서 낯선 키워드들이 보인다.
| C# | Java 대응 | 설명 |
|---|---|---|
Task<T> |
CompletableFuture<T> |
비동기 결과를 담는 래퍼 |
IEnumerable<T> |
List<T> / Iterable<T> |
컬렉션 인터페이스 |
Guid |
UUID |
고유 식별자 |
구현 클래스 (ProductRepository.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
29
30
31
32
33
34
35
36
37
38
39
40
public class ProductRepository : IProductRepository
{
private readonly DapperDbContext _context;
public ProductRepository(DapperDbContext context)
{
_context = context;
}
public async Task<IEnumerable<ProductModel>> GetAll()
{
var sql = "SELECT * FROM Products";
using var connection = _context.CreateConnection();
return await connection.QueryAsync<ProductModel>(sql);
}
public async Task<ProductModel> GetById(Guid id)
{
var sql = "SELECT * FROM Products WHERE ProductId = @Id";
using var connection = _context.CreateConnection();
return await connection.QueryFirstOrDefaultAsync<ProductModel>(sql, new { Id = id });
}
public async Task<ProductModel> Create(ProductModel model)
{
model.ProductId = Guid.NewGuid();
model.CreatedOn = DateTime.Now;
var sql = @"INSERT INTO Products
(ProductId, ProductName, Price, ProductDescription, CreatedOn)
VALUES
(@ProductId, @ProductName, @Price, @ProductDescription, @CreatedOn)";
using var connection = _context.CreateConnection();
await connection.ExecuteAsync(sql, model);
return model;
}
// Update, Delete도 동일한 패턴...
}
Dapper 메서드 패턴
모든 메서드가 다음과 같은 비슷한 패턴을 따른다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public async Task<리턴타입> 메서드명(파라미터)
{
// 1. SQL 작성
var sql = "쿼리문";
// 2. 연결 생성 (자동 종료)
using var connection = _context.CreateConnection();
// 3. 실행
await connection.XXXAsync(sql, 파라미터);
// 4. 리턴
return 결과;
}
using var는 Java의 try-with-resources 와 같다. 블록이 끝나면 자동으로 connection.Dispose()가 호출되어 연결이 종료된다.
작업별 Dapper 메서드
| 작업 | Dapper 메서드 | 리턴 타입 |
|---|---|---|
| SELECT 여러 개 | QueryAsync<T> |
IEnumerable<T> |
| SELECT 하나 | QueryFirstOrDefaultAsync<T> |
T 또는 null |
| INSERT/UPDATE/DELETE | ExecuteAsync |
int (영향받은 행 수) |
Controller
ProductsController.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
29
public class ProductsController : Controller
{
private readonly IProductRepository _repository;
public ProductsController(IProductRepository repository)
{
_repository = repository;
}
public async Task<IActionResult> Index()
{
var products = await _repository.GetAll();
return View(products);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(ProductModel model)
{
if (ModelState.IsValid)
{
await _repository.Create(model);
return RedirectToAction(nameof(Index));
}
return View(model);
}
// Edit, Delete도 동일한 패턴...
}
IActionResult는 Controller 메서드가 반환할 수 있는 모든 응답 타입의 인터페이스 다. Java Spring의 ResponseEntity<?>와 비슷하다.
| C# 메서드 | 역할 | Java Spring 대응 |
|---|---|---|
View() |
뷰 페이지 반환 | return "viewName" |
RedirectToAction() |
리다이렉트 | redirect:/path |
NotFound() |
404 응답 | ResponseEntity.notFound() |
의존성 주입 (DI) 등록
Program.cs
1
2
3
4
5
6
7
8
9
10
var builder = WebApplication.CreateBuilder(args);
// 서비스 등록
builder.Services.AddSingleton<DapperDbContext>();
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddControllersWithViews();
var app = builder.Build();
// ...
Java Spring에서는 @Repository, @Service 어노테이션만 붙이면 자동으로 빈이 등록된다. 하지만 C#에서는 직접 등록 해야 한다.
1
2
builder.Services.AddScoped<IProductRepository, ProductRepository>();
// ↑ 인터페이스 ↑ 구현 클래스
이 등록을 빠뜨리면 다음 에러가 발생한다.
1
2
InvalidOperationException: Unable to resolve service for type
'IProductRepository' while attempting to activate 'ProductsController'.
DI 등록을 잊으면 Controller에서 Repository를 주입받을 수 없다. 에러 메시지가 길지만, 핵심은 "해당 타입을 찾을 수 없다" 는 것이다.
결과
상품목록 뷰 및 DB
상품등록 뷰
상품등록 후 목록 및 DB
상품상세 뷰
상품수정 뷰
수정 후
삭제
트러블슈팅
1. SSMS SSL 인증서 오류
1
신뢰되지 않은 기관에서 인증서 체인을 발급했습니다
해결: 연결 창 → 옵션 → 연결 속성 → 서버 인증서 신뢰 체크
2. SQL 문법 오류
코드 작성 시 문자열을 적는것과 같은 판정이므로 오타나 빨간줄이 보이지 않아 주의해서 적어야한다! 여기서 오타나 문법 오류가 생기면 찾기 상당히 힘들다.
1
var sql = @"";
2. 파라미터 전달 오류
1
2
3
4
5
// ❌ 잘못된 예
await connection.ExecuteAsync(sql, new { model });
// ✅ 올바른 예
await connection.ExecuteAsync(sql, model);
new { model }로 감싸면 model이라는 이름의 프로퍼티를 가진 익명 객체가 되어버린다. Dapper는 객체의 프로퍼티 이름과 SQL의 @파라미터명을 매칭하므로, 객체를 직접 전달해야 한다.
마치며
Dapper + .NET Core MVC로 CRUD를 구현하는 과정을 정리했다. 핵심 원칙을 요약하면 다음과 같다.
- DbContext 는 연결 문자열을 관리하고 연결 객체를 생성하는 역할이다
- Repository 패턴 은 SQL 작성 → 연결 생성 → 실행 → 리턴의 일관된 구조를 따른다
using var는 Java의 try-with-resources와 같다. 연결 자동 종료를 보장한다async/await는 비동기 처리다. DB 응답을 기다리는 동안 스레드를 블로킹하지 않는다- DI 등록을 잊지 말 것.
Program.cs에서 서비스를 등록해야 Controller에서 주입받을 수 있다
Java Spring에 익숙하다면 개념 자체는 비슷하다. 문법과 네이밍 컨벤션만 다를 뿐, Repository 패턴, DI, MVC 구조 모두 동일한 철학을 공유한다.