Ver3.0 ๐ฏ AutoMapper๋ก ์ฝ๊ฒ ๋ฐฐ์ฐ๋ ๊ฐ์ฒด ๋งคํ๊ณผ DTO ํจํด ์๋ฒฝ ๊ตฌํ ์ ๋ต

๐ฏ AutoMapper๋ก ์ฝ๊ฒ ๋ฐฐ์ฐ๋ ๊ฐ์ฒด ๋งคํ๊ณผ DTO ํจํด ์๋ฒฝ ๊ตฌํ ์ ๋ต
C# ๊ฐ๋ฐ์๋ผ๋ฉด ๋ฐ๋์ ์์์ผ ํ ๊ฐ์ฒด ๋ณํ์ ๋ชจ๋ ๊ฒ
๐ ์๋
! ์ค๋์ C# ๊ฐ๋ฐํ๋ฉด์ ์ ๋ง ์์ฃผ ๋ง์ฃผ์น๋ ๋ฌธ์ ํ๋๋ฅผ ๊ฐ์ด ํด๊ฒฐํด๋ณผ ๊ฑฐ์ผ. ๋ฐ๋ก "๊ฐ์ฒด๋ฅผ ๋ค๋ฅธ ๊ฐ์ฒด๋ก ๋ณํํ๋ ์์
"์ด์ง. ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๊ฐ์ ธ์จ ์ํฐํฐ๋ฅผ API ์๋ต์ฉ DTO๋ก ๋ฐ๊พธ๊ฑฐ๋, ํด๋ผ์ด์ธํธ์์ ๋ฐ์ ์์ฒญ ๊ฐ์ฒด๋ฅผ ๋๋ฉ์ธ ๋ชจ๋ธ๋ก ๋ณํํ๋ ์ผ ๋ง์ด์ผ.
์ฒ์์ "๊ทธ๋ฅ ์ผ์ผ์ด ๋งคํํ๋ฉด ๋์ง ๋ญ~" ํ๊ณ ์๊ฐํ ์ ์์ด. ๊ทผ๋ฐ ํ๋ก์ ํธ๊ฐ ์ปค์ง๋ฉด์ ์ด๋ฐ ๋งคํ ์ฝ๋๊ฐ ์๋ฐฑ ๊ฐ์ฉ ์์ด๋ฉด? ๐ฑ ์ ์ง๋ณด์ ์ง์ฅ์ด ๋ฐ๋ก ์์ง. ๊ทธ๋์ ๋ฑ์ฅํ ๊ฒ ๋ฐ๋ก AutoMapper์ผ!
์ด ๊ธ์์๋ AutoMapper์ ๊ธฐ๋ณธ ๊ฐ๋
๋ถํฐ ์ค์ ํ์ฉ๋ฒ, ๊ทธ๋ฆฌ๊ณ DTO ํจํด์ ์ ๋๋ก ๊ตฌํํ๋ ์ ๋ต๊น์ง ์น๊ตฌํํ
์ค๋ช
ํ๋ฏ์ด ์ฝ๊ฒ ํ์ด๋ณผ๊ฒ. ๐
๐ค ์ ๊ฐ์ฒด ๋งคํ์ด ํ์ํ ๊น?
๋ณธ๊ฒฉ์ ์ผ๋ก AutoMapper๋ฅผ ๋ค๋ฃจ๊ธฐ ์ ์, ์ ๊ฐ์ฒด ๋งคํ์ด๋ผ๋ ๊ฒ ํ์ํ์ง๋ถํฐ ์ด์ผ๊ธฐํด๋ณด์. ์ค์ ๊ฐ๋ฐ ํ์ฅ์์ ๊ฒช๋ ์ํฉ์ ์๋ก ๋ค์ด๋ณผ๊ฒ.
๋๊ฐ ์ผํ๋ชฐ API๋ฅผ ๋ง๋ ๋ค๊ณ ์๊ฐํด๋ด. ๋ฐ์ดํฐ๋ฒ ์ด์ค์๋ User ํ
์ด๋ธ์ด ์๊ณ , ์ฌ๊ธฐ์ ์ฌ์ฉ์์ ๋ชจ๋ ์ ๋ณด๊ฐ ๋ค์ด์์ด. ๋น๋ฐ๋ฒํธ ํด์, ๋ด๋ถ ๊ด๋ฆฌ์ฉ ID, ์์ฑ์ผ์, ์์ ์ผ์, ์ญ์ ์ฌ๋ถ ๋ฑ๋ฑ... ๐
๊ทธ๋ฐ๋ฐ ํด๋ผ์ด์ธํธ(ํ๋ก ํธ์๋)์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ณด๋ผ ๋ ์ด ๋ชจ๋ ์ ๋ณด๋ฅผ ๋ค ์ค์ผ ํ ๊น? ๋น์ฐํ ์๋์ง! ๋น๋ฐ๋ฒํธ ํด์ ๊ฐ์ ๋ฏผ๊ฐํ ์ ๋ณด๋ ์ ๋ ๋
ธ์ถ๋๋ฉด ์ ๋๊ณ , ๋ด๋ถ ๊ด๋ฆฌ์ฉ ํ๋๋ค๋ ํด๋ผ์ด์ธํธ๋ ์ ํ์๊ฐ ์์ด.
๊ทธ๋์ ์ฐ๋ฆฌ๋ DTO(Data Transfer Object)๋ผ๋ ๊ฑธ ๋ง๋ค์ด. API ์๋ต์ ๋ฑ ํ์ํ ์ ๋ณด๋ง ๋ด์ ๊น๋ํ ๊ฐ์ฒด์ง. ์ด๊ฒ ๋ฐ๋ก ๊ด์ฌ์ฌ์ ๋ถ๋ฆฌ(Separation of Concerns)์ผ.
โ๏ธ Entity vs DTO ๋น๊ต
| ๊ตฌ๋ถ | Entity (์ํฐํฐ) | DTO (๋ฐ์ดํฐ ์ ์ก ๊ฐ์ฒด) |
|---|---|---|
| ๋ชฉ์ | ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ ์ด๋ธ๊ณผ 1:1 ๋งคํ | ๊ณ์ธต ๊ฐ ๋ฐ์ดํฐ ์ ์ก |
| ์์น | ๋๋ฉ์ธ ๊ณ์ธต, ๋ฐ์ดํฐ ๊ณ์ธต | ํ๋ ์ ํ ์ด์ ๊ณ์ธต, API ๊ณ์ธต |
| ๋ณ๊ฒฝ ๋น๋ | DB ์คํค๋ง ๋ณ๊ฒฝ ์ | API ์๊ตฌ์ฌํญ ๋ณ๊ฒฝ ์ |
| ํฌํจ ์ ๋ณด | ๋ชจ๋ ํ๋, ๊ด๊ณ, ๋ฉํ๋ฐ์ดํฐ | ํ์ํ ์ ๋ณด๋ง ์ ๋ณ |
| ๋น์ฆ๋์ค ๋ก์ง | ํฌํจ ๊ฐ๋ฅ | ์ผ๋ฐ์ ์ผ๋ก ์์ |
โ ๏ธ ์ด๋ณด ๊ฐ๋ฐ์๋ค์ด ์์ฃผ ํ๋ ์ค์
"Entity๋ฅผ ๊ทธ๋๋ก API ์๋ต์ผ๋ก ๋ณด๋ด๋ฉด ์ ๋๋์?" - ์ด๋ ๊ฒ ๋ฌผ์ด๋ณด๋ ๊ฒฝ์ฐ๊ฐ ๋ง์. ๊ธฐ์ ์ ์ผ๋ก๋ ๊ฐ๋ฅํ์ง๋ง, ์ ๋ ๊ถ์ฅํ์ง ์์! ์ด์ ๋:
1. ๋ณด์ ๋ฌธ์ : ๋ฏผ๊ฐํ ์ ๋ณด๊ฐ ๋
ธ์ถ๋ ์ ์์ด
2. ์ฑ๋ฅ ๋ฌธ์ : ๋ถํ์ํ ๋ฐ์ดํฐ๊น์ง ์ง๋ ฌํ๋์ด ๋คํธ์ํฌ ๋ถํ ์ฆ๊ฐ
3. ์ํ ์ฐธ์กฐ: Entity ๊ฐ ๊ด๊ณ ๋๋ฌธ์ JSON ์ง๋ ฌํ ์ ๋ฌดํ ๋ฃจํ ๋ฐ์ ๊ฐ๋ฅ
4. ๊ฒฐํฉ๋ ์ฆ๊ฐ: DB ์คํค๋ง ๋ณ๊ฒฝ์ด API ์คํ ๋ณ๊ฒฝ์ผ๋ก ์ง๊ฒฐ๋จ
๐ง AutoMapper๊ฐ ๋ญ๊ธธ๋?
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก AutoMapper๋ฅผ ์์๋ณด์! ๐ฏ
AutoMapper๋ .NET ์ํ๊ณ์์ ๊ฐ์ฅ ์ ๋ช
ํ ๊ฐ์ฒด-๊ฐ์ฒด ๋งคํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ. 2009๋
์ Jimmy Bogard๊ฐ ๋ง๋ค์๊ณ , ์ง๊ธ๊น์ง๋ ํ๋ฐํ๊ฒ ์ ์ง๋ณด์๋๊ณ ์์ด. ๊ธฐ๋ณธ ์์ด๋์ด๋ ๊ฐ๋จํด: "๊ท์น ๊ธฐ๋ฐ์ผ๋ก ํ ๊ฐ์ฒด์ ์์ฑ์ ๋ค๋ฅธ ๊ฐ์ฒด์ ์์ฑ์ผ๋ก ์๋ ๋ณต์ฌ"ํ๋ ๊ฑฐ์ง.
์๋ฅผ ๋ค์ด๋ณผ๊ฒ. AutoMapper ์์ด ์๋์ผ๋ก ๋งคํํ๋ฉด ์ด๋ ๊ฒ ํด์ผ ํด:
public UserDto ConvertToDto(User user)
{
return new UserDto
{
Id = user.Id,
Name = user.Name,
Email = user.Email,
Age = user.Age,
PhoneNumber = user.PhoneNumber,
Address = user.Address,
City = user.City,
Country = user.Country,
PostalCode = user.PostalCode,
// ... ์์ฑ์ด 20๊ฐ, 30๊ฐ๋ผ๋ฉด? ๐ฑ
};
}
์์ฑ์ด ๋ช ๊ฐ ์ ๋๋ฉด ๊ด์ฐฎ์๋ฐ, ์ค๋ฌด์์๋ ์์ฑ์ด ์์ญ ๊ฐ์ธ ๊ฒฝ์ฐ๋ ๋ง์. ๊ฒ๋ค๊ฐ ์ด๋ฐ ๋ณํ ๋ฉ์๋๊ฐ ํ๋ก์ ํธ ์ ์ฒด์ ์๋ฐฑ ๊ฐ์ฉ ์๋ค๋ฉด? ์ฝ๋ ์ค๋ณต๋ ์ฌํ๊ณ , ๋์ค์ ์์ฑ ํ๋ ์ถ๊ฐํ๋ ค๋ฉด ๋ชจ๋ ๊ณณ์ ๋ค ์์ ํด์ผ ํด. ๐ญ
AutoMapper๋ฅผ ์ฌ์ฉํ๋ฉด ์ด๋ ๊ฒ ๋ฐ๋์ด:
// ์ค์ (ํ ๋ฒ๋ง)
var config = new MapperConfiguration(cfg => {
cfg.CreateMap<User, UserDto>();
});
var mapper = config.CreateMapper();
// ์ฌ์ฉ (๋งค์ฐ ๊ฐ๋จ!)
UserDto dto = mapper.Map<UserDto>(user);
์์ฐ! โจ ๋จ ํ ์ค๋ก ๋ชจ๋ ์์ฑ์ด ์๋์ผ๋ก ๋งคํ๋ผ. AutoMapper๋ ์์ฑ ์ด๋ฆ์ด ๊ฐ์ผ๋ฉด ์๋์ผ๋ก ์ฐ๊ฒฐํด์ฃผ๊ฑฐ๋ . ์ด๊ฒ ๋ฐ๋ก Convention over Configuration (์ค์ ๋ณด๋ค ๊ด๋ก) ์์น์ด์ผ.
๐ก AutoMapper์ ํต์ฌ ์ฅ์
โข ์์ฐ์ฑ ํฅ์: ๋ฐ๋ณต์ ์ธ ๋งคํ ์ฝ๋ ์์ฑ ์๊ฐ ๋ํญ ๊ฐ์
โข ์ ์ง๋ณด์์ฑ: ๋งคํ ๋ก์ง์ด ํ ๊ณณ์ ์ง์ค๋์ด ๊ด๋ฆฌ ์ฉ์ด
โข ํ
์คํธ ์ฉ์ด์ฑ: ๋งคํ ์ค์ ์์ฒด๋ฅผ ํ
์คํธํ ์ ์์
โข ๋ฆฌํฉํ ๋ง ์์ ์ฑ: ์์ฑ๋ช
๋ณ๊ฒฝ ์ ์ปดํ์ผ ํ์์ ์ค๋ฅ ๊ฐ์ง
โข ์ผ๊ด์ฑ: ํ ์ ์ฒด๊ฐ ๋์ผํ ๋งคํ ๋ฐฉ์ ์ฌ์ฉ
๐ฆ AutoMapper ์ค์น ๋ฐ ๊ธฐ๋ณธ ์ค์
์, ์ด์ ์ค์ ์ผ๋ก ๋ค์ด๊ฐ๋ณด์! ๋จผ์ AutoMapper๋ฅผ ํ๋ก์ ํธ์ ์ค์นํด์ผ๊ฒ ์ง? ๐ ๏ธ
Step 1: NuGet ํจํค์ง ์ค์น
Visual Studio์ ํจํค์ง ๊ด๋ฆฌ์ ์ฝ์์์ ๋ค์ ๋ช ๋ น์ด๋ฅผ ์คํํด:
Install-Package AutoMapper
Install-Package AutoMapper.Extensions.Microsoft.DependencyInjection
๋๋ .NET CLI๋ฅผ ์ฌ์ฉํ๋ค๋ฉด:
dotnet add package AutoMapper
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
๋ ๋ฒ์งธ ํจํค์ง๋ ASP.NET Core์ ์์กด์ฑ ์ฃผ์ (DI) ์ปจํ ์ด๋์ AutoMapper๋ฅผ ํตํฉํ๊ธฐ ์ํ ๊ฑฐ์ผ. ์์ฆ ๋๋ถ๋ถ์ ํ๋ก์ ํธ๊ฐ DI๋ฅผ ์ฌ์ฉํ๋๊น ๊ฑฐ์ ํ์๋ผ๊ณ ๋ณด๋ฉด ๋ผ. ๐
Step 2: ํ๋กํ(Profile) ํด๋์ค ์์ฑ
AutoMapper์ ๋งคํ ์ค์ ์ Profile ํด๋์ค๋ฅผ ์์๋ฐ์ ์ ์ํด. ์ด๋ ๊ฒ ํ๋ฉด ๋งคํ ์ค์ ์ ๋ ผ๋ฆฌ์ ์ผ๋ก ๊ทธ๋ฃนํํ ์ ์์ด.
using AutoMapper;
public class UserProfile : Profile
{
public UserProfile()
{
// User ์ํฐํฐ๋ฅผ UserDto๋ก ๋งคํ
CreateMap<User, UserDto>();
// ์๋ฐฉํฅ ๋งคํ์ด ํ์ํ๋ฉด
CreateMap<UserDto, User>();
// ๋ค๋ฅธ ๋งคํ๋ค๋ ์ฌ๊ธฐ์ ์ถ๊ฐ
CreateMap<Product, ProductDto>();
CreateMap<Order, OrderDto>();
}
}
Profile ํด๋์ค๋ ๊ด๋ จ๋ ๋งคํ๋ค์ ๋ฌถ์ด์ฃผ๋ ์ญํ ์ ํด. ์๋ฅผ ๋ค์ด UserProfile, ProductProfile, OrderProfile ์ด๋ฐ ์์ผ๋ก ๋๋ฉ์ธ๋ณ๋ก ๋๋ ์ ์์ง. ๐
Step 3: ASP.NET Core์ ๋ฑ๋ก
Program.cs (๋๋ Startup.cs)์์ AutoMapper๋ฅผ DI ์ปจํ
์ด๋์ ๋ฑ๋กํด:
// Program.cs (.NET 6 ์ด์)
var builder = WebApplication.CreateBuilder(args);
// AutoMapper ๋ฑ๋ก - ์ด์
๋ธ๋ฆฌ ์ค์บ ๋ฐฉ์
builder.Services.AddAutoMapper(typeof(Program));
// ๋๋ ํน์ Profile ํด๋์ค ์ง์
// builder.Services.AddAutoMapper(typeof(UserProfile));
var app = builder.Build();
// ... ๋๋จธ์ง ์ค์
AddAutoMapper(typeof(Program))์ Program ํด๋์ค๊ฐ ์๋ ์ด์
๋ธ๋ฆฌ๋ฅผ ์ค์บํด์ ๋ชจ๋ Profile ํด๋์ค๋ฅผ ์๋์ผ๋ก ์ฐพ์ ๋ฑ๋กํด์ค. ์ ๋ง ํธ๋ฆฌํ์ง? ๐
Step 4: ์ปจํธ๋กค๋ฌ์์ ์ฌ์ฉ
์ด์ ์ปจํธ๋กค๋ฌ๋ ์๋น์ค์์ IMapper๋ฅผ ์ฃผ์
๋ฐ์ ์ฌ์ฉํ๋ฉด ๋ผ:
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly IMapper _mapper;
private readonly IUserService _userService;
public UsersController(IMapper mapper, IUserService userService)
{
_mapper = mapper;
_userService = userService;
}
[HttpGet("{id}")]
public async Task<ActionResult<UserDto>> GetUser(int id)
{
var user = await _userService.GetByIdAsync(id);
if (user == null)
return NotFound();
// ๋งคํ ์ํ!
var userDto = _mapper.Map<UserDto>(user);
return Ok(userDto);
}
[HttpGet]
public async Task<ActionResult<List<UserDto>>> GetAllUsers()
{
var users = await _userService.GetAllAsync();
// ์ปฌ๋ ์
๋ ํ ๋ฒ์ ๋งคํ ๊ฐ๋ฅ!
var userDtos = _mapper.Map<List<UserDto>>(users);
return Ok(userDtos);
}
}
๋ณด๋ค์ํผ _mapper.Map<TDestination>(source) ํํ๋ก ์ฌ์ฉํ๋ฉด ๋ผ. ์ปฌ๋ ์
(List, Array ๋ฑ)๋ ์๋์ผ๋ก ์ฒ๋ฆฌํด์ฃผ๋๊น ์ ๋ง ํธํด! ๐
๐จ ๋งคํ ์ปค์คํฐ๋ง์ด์ง - ๊ธฐ๋ณธ๋ถํฐ ๊ณ ๊ธ๊น์ง
AutoMapper์ ์ง์ง ๊ฐ๋ ฅํจ์ ์ปค์คํฐ๋ง์ด์ง์ ์์ด. ๋จ์ํ ๊ฐ์ ์ด๋ฆ์ ์์ฑ๋ง ๋งคํํ๋ ๊ฒ ์๋๋ผ, ๋ณต์กํ ๋ณํ ๋ก์ง๋ ์ฐ์ํ๊ฒ ์ฒ๋ฆฌํ ์ ์๊ฑฐ๋ . ํ๋์ฉ ์ดํด๋ณด์! ๐ฏ
1๏ธโฃ ์์ฑ๋ช ์ด ๋ค๋ฅผ ๋ - ForMember
๊ฐ์ฅ ํํ ์ผ์ด์ค์ผ. ์์ค์ ๋์์ ์์ฑ๋ช ์ด ๋ค๋ฅผ ๋ ์ด๋ป๊ฒ ๋งคํํ ์ง ๋ช ์์ ์ผ๋ก ์ง์ ํ๋ ๊ฑฐ์ง.
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
}
public class UserDto
{
public int UserId { get; set; } // Id์ ์ด๋ฆ์ด ๋ค๋ฆ
public string FullName { get; set; } // FirstName + LastName ์กฐํฉ
public int Age { get; set; } // BirthDate๋ก๋ถํฐ ๊ณ์ฐ
}
// Profile ์ค์
public class UserProfile : Profile
{
public UserProfile()
{
CreateMap<User, UserDto>()
.ForMember(dest => dest.UserId,
opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.FullName,
opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}"))
.ForMember(dest => dest.Age,
opt => opt.MapFrom(src => CalculateAge(src.BirthDate)));
}
private int CalculateAge(DateTime birthDate)
{
var today = DateTime.Today;
var age = today.Year - birthDate.Year;
if (birthDate.Date > today.AddYears(-age)) age--;
return age;
}
}
ForMember๋ ๋์(destination) ์์ฑ์ ์ง์ ํ๊ณ , MapFrom์ผ๋ก ์์ค์์ ์ด๋ป๊ฒ ๊ฐ์ ๊ฐ์ ธ์ฌ์ง ์ ์ํด. ๋๋ค ํํ์์ ์ฌ์ฉํ๋๊น ๋ณต์กํ ๋ก์ง๋ ์์ ๋กญ๊ฒ ์์ฑํ ์ ์์ด! โจ
2๏ธโฃ ํน์ ์์ฑ ๋ฌด์ํ๊ธฐ - Ignore
๋๋ก๋ ํน์ ์์ฑ์ ๋งคํํ๊ณ ์ถ์ง ์์ ๋๊ฐ ์์ด. ์๋ฅผ ๋ค์ด DTO์๋ ์์ง๋ง ์๋์ผ๋ก ์ค์ ํด์ผ ํ๋ ์์ฑ์ด๋ผ๋ ์ง.
public class CreateUserDto
{
public string Username { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public DateTime CreatedAt { get; set; } // ์๋ฒ์์ ์๋ ์ค์
}
public class UserProfile : Profile
{
public UserProfile()
{
CreateMap<CreateUserDto, User>()
.ForMember(dest => dest.CreatedAt, opt => opt.Ignore())
.ForMember(dest => dest.Id, opt => opt.Ignore());
}
}
3๏ธโฃ ์กฐ๊ฑด๋ถ ๋งคํ - Condition
ํน์ ์กฐ๊ฑด์ ๋ง์กฑํ ๋๋ง ๋งคํํ๊ณ ์ถ๋ค๋ฉด Condition์ ์ฌ์ฉํด:
CreateMap<User, UserDto>()
.ForMember(dest => dest.Email, opt =>
{
opt.Condition(src => !string.IsNullOrEmpty(src.Email));
opt.MapFrom(src => src.Email);
});
4๏ธโฃ Null ๊ฐ ์ฒ๋ฆฌ - NullSubstitute
์์ค ๊ฐ์ด null์ผ ๋ ๊ธฐ๋ณธ๊ฐ์ ์ง์ ํ ์ ์์ด:
CreateMap<User, UserDto>()
.ForMember(dest => dest.PhoneNumber,
opt => opt.NullSubstitute("๋ฒํธ ์์"));
5๏ธโฃ ์ค์ฒฉ ๊ฐ์ฒด ๋งคํ - Nested Mapping
๊ฐ์ฒด ์์ ๋ ๋ค๋ฅธ ๊ฐ์ฒด๊ฐ ์๋ ๊ฒฝ์ฐ๋ ์๋์ผ๋ก ์ฒ๋ฆฌ๋ผ. ๋จ, ํด๋น ํ์ ์ ๋ํ ๋งคํ ์ค์ ์ด ์์ด์ผ ํด:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public Address Address { get; set; } // ์ค์ฒฉ ๊ฐ์ฒด
}
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string Country { get; set; }
}
public class UserDto
{
public int Id { get; set; }
public string Name { get; set; }
public AddressDto Address { get; set; }
}
public class AddressDto
{
public string Street { get; set; }
public string City { get; set; }
public string Country { get; set; }
}
// Profile ์ค์
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<User, UserDto>(); // User ๋งคํ
CreateMap<Address, AddressDto>(); // Address ๋งคํ๋ ์ ์
}
}
AutoMapper๊ฐ User.Address๋ฅผ ๋งคํํ ๋ ์๋์ผ๋ก Address โ AddressDto ๋งคํ์ ์ฐพ์์ ์ ์ฉํด์ค. ๋๋ํ์ง? ๐ง
6๏ธโฃ Flattening (ํํํ)
์ด๊ฑด AutoMapper์ ์ ๋ง ๋ฉ์ง ๊ธฐ๋ฅ ์ค ํ๋์ผ! ์ค์ฒฉ๋ ๊ฐ์ฒด๋ฅผ ํํํ๊ฒ ํผ์น ์ ์์ด:
public class Order
{
public int Id { get; set; }
public Customer Customer { get; set; }
}
public class Customer
{
public string Name { get; set; }
public string Email { get; set; }
}
public class OrderDto
{
public int Id { get; set; }
public string CustomerName { get; set; } // Customer.Name
public string CustomerEmail { get; set; } // Customer.Email
}
// Profile ์ค์ - ์ถ๊ฐ ์ค์ ํ์ ์์!
CreateMap<Order, OrderDto>();
๋๋๊ฒ๋ CustomerName์ด๋ผ๋ ์์ฑ๋ช
์ ๋ณด๊ณ AutoMapper๊ฐ ์๋์ผ๋ก Customer.Name์ ์ฐพ์์ ๋งคํํด์ค! ์ด๊ฒ ๋ฐ๋ก PascalCase ๋ถํ ๊ท์น์ด์ผ. Customer + Name์ ์ธ์ํ๋ ๊ฑฐ์ง. ๐
๐ก Flattening ๊ท์น
AutoMapper๋ ๋ค์ ํจํด๋ค์ ์๋์ผ๋ก ์ธ์ํด:
โข CustomerName โ Customer.Name
โข CustomerAddressCity โ Customer.Address.City
โข GetCustomerName() โ Customer.Name (๋ฉ์๋๋ ๊ฐ๋ฅ!)
์ด ๊ธฐ๋ฅ ๋๋ถ์ ๋ณต์กํ ๊ฐ์ฒด ๊ตฌ์กฐ๋ฅผ ๊ฐ๋จํ DTO๋ก ์ฝ๊ฒ ๋ณํํ ์ ์์ด.
7๏ธโฃ Reverse Mapping (์ญ๋ฐฉํฅ ๋งคํ)
์๋ฐฉํฅ ๋งคํ์ด ํ์ํ ๋ ReverseMap()์ ์ฌ์ฉํ๋ฉด ๋ผ:
CreateMap<User, UserDto>()
.ForMember(dest => dest.FullName,
opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}"))
.ReverseMap()
.ForMember(dest => dest.FirstName,
opt => opt.MapFrom(src => src.FullName.Split(' ')[0]))
.ForMember(dest => dest.LastName,
opt => opt.MapFrom(src => src.FullName.Split(' ')[1]));
ReverseMap() ์ดํ์ ForMember๋ ์ญ๋ฐฉํฅ(UserDto โ User) ๋งคํ์ ์ ์ฉ๋ผ. ํธ๋ฆฌํ์ง? ๐
๐๏ธ DTO ํจํด ์ค๊ณ ์ ๋ต
AutoMapper ์ฌ์ฉ๋ฒ์ ์์์ผ๋, ์ด์ DTO๋ฅผ ์ด๋ป๊ฒ ์ค๊ณํด์ผ ํ๋์ง ์ด์ผ๊ธฐํด๋ณด์. DTO ํจํด์ ๋จ์ํด ๋ณด์ด์ง๋ง, ์ ๋๋ก ์ค๊ณํ์ง ์์ผ๋ฉด ์คํ๋ ค ๋ณต์ก๋๋ง ์ฆ๊ฐํ ์ ์์ด. ๐จ
๐ DTO ๋ช ๋ช ๊ท์น
์ผ๊ด๋ ๋ช ๋ช ๊ท์น์ ์ฝ๋ ๊ฐ๋ ์ฑ์ ํฌ๊ฒ ๋์ฌ์ค. ์ค๋ฌด์์ ๋ง์ด ์ฌ์ฉํ๋ ํจํด๋ค์ ์๊ฐํ ๊ฒ:
์ฉ๋๋ณ DTO ๋ค์ด๋ฐ
// 1. ์กฐํ์ฉ (Read)
public class UserDto { }
public class UserDetailDto { } // ์์ธ ์ ๋ณด
public class UserSummaryDto { } // ์์ฝ ์ ๋ณด
public class UserListItemDto { } // ๋ชฉ๋ก์ฉ
// 2. ์์ฑ์ฉ (Create)
public class CreateUserDto { }
public class CreateUserRequest { }
public class UserCreateCommand { }
// 3. ์์ ์ฉ (Update)
public class UpdateUserDto { }
public class UpdateUserRequest { }
public class UserUpdateCommand { }
// 4. ์๋ต์ฉ (Response)
public class UserResponse { }
public class CreateUserResponse { }
public class ApiResponse<T> { }
// 5. ๊ฒ์/ํํฐ์ฉ
public class UserSearchDto { }
public class UserFilterDto { }
public class UserQueryParameters { }
ํ๋ง๋ค ์ ํธํ๋ ์คํ์ผ์ด ๋ค๋ฅผ ์ ์์ง๋ง, ์ค์ํ ๊ฑด ์ผ๊ด์ฑ์ด์ผ. ํ ํ๋ก์ ํธ ๋ด์์๋ ํ๋์ ๊ท์น์ ์ ํด์ ์ญ ๋ฐ๋ผ๊ฐ๋ ๊ฒ ์ข์. ๐
๐ฏ DTO ๋ถ๋ฆฌ ์ ๋ต
"DTO๋ฅผ ์ผ๋ง๋ ์ธ๋ถํํด์ผ ํ ๊น?"๋ ์์ฃผ ๋ฐ๋ ์ง๋ฌธ์ด์ผ. ์ ๋ต์ ์์ง๋ง, ๋ช ๊ฐ์ง ๊ฐ์ด๋๋ผ์ธ์ ์ค๊ฒ:
์์น 1: ๋จ์ผ ์ฑ ์ ์์น (SRP)
ํ๋์ DTO๋ ํ๋์ ๋ช ํํ ๋ชฉ์ ๋ง ๊ฐ์ ธ์ผ ํด. ์๋ฅผ ๋ค์ด:
// โ ๋์ ์: ๋ชจ๋ ์ฉ๋๋ฅผ ํ๋๋ก
public class UserDto
{
public int Id { get; set; }
public string Username { get; set; }
public string Password { get; set; } // ์กฐํ ์ ๋
ธ์ถ๋๋ฉด ์ ๋จ!
public string Email { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
// ... ์์ญ ๊ฐ์ ์์ฑ
}
// โ
์ข์ ์: ์ฉ๋๋ณ๋ก ๋ถ๋ฆฌ
public class UserProfileDto // ํ๋กํ ์กฐํ์ฉ
{
public int Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
public string ProfileImageUrl { get; set; }
}
public class CreateUserDto // ํ์๊ฐ์
์ฉ
{
public string Username { get; set; }
public string Password { get; set; }
public string Email { get; set; }
}
public class UpdateUserProfileDto // ํ๋กํ ์์ ์ฉ
{
public string Email { get; set; }
public string ProfileImageUrl { get; set; }
// Id๋ Username์ ์์ ๋ถ๊ฐํ๋ฏ๋ก ์ ์ธ
}
์์น 2: ์ต์ ๊ถํ ์์น
ํด๋ผ์ด์ธํธ์๊ฒ ํ์ํ ์ต์ํ์ ์ ๋ณด๋ง ๋ ธ์ถํด:
// ๊ด๋ฆฌ์์ฉ DTO
public class AdminUserDto
{
public int Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime LastLoginAt { get; set; }
public bool IsActive { get; set; }
public string IpAddress { get; set; }
public List<string> Roles { get; set; }
}
// ์ผ๋ฐ ์ฌ์ฉ์์ฉ DTO (์๊ธฐ ์ ๋ณด ์กฐํ)
public class MyProfileDto
{
public string Username { get; set; }
public string Email { get; set; }
public DateTime CreatedAt { get; set; }
}
// ๊ณต๊ฐ ํ๋กํ DTO (๋ค๋ฅธ ์ฌ์ฉ์๊ฐ ๋ณผ ๋)
public class PublicUserDto
{
public string Username { get; set; }
public string ProfileImageUrl { get; set; }
// ์ด๋ฉ์ผ, ์์ฑ์ผ ๋ฑ ๋ฏผ๊ฐ์ ๋ณด ์ ์ธ
}
๐ DTO ์์๊ณผ ์กฐํฉ
DTO ๊ฐ์ ๊ณตํต ์์ฑ์ด ๋ง๋ค๋ฉด ์์์ด๋ ์กฐํฉ์ ๊ณ ๋ คํด๋ณผ ์ ์์ด:
// ๊ธฐ๋ณธ DTO
public class BaseUserDto
{
public int Id { get; set; }
public string Username { get; set; }
}
// ์์์ ํตํ ํ์ฅ
public class UserDetailDto : BaseUserDto
{
public string Email { get; set; }
public string PhoneNumber { get; set; }
public AddressDto Address { get; set; }
}
// ์กฐํฉ์ ํตํ ๊ตฌ์ฑ
public class OrderDto
{
public int Id { get; set; }
public DateTime OrderDate { get; set; }
public UserSummaryDto Customer { get; set; } // ์กฐํฉ
public List<OrderItemDto> Items { get; set; }
}
ํ์ง๋ง ์ฃผ์ํ ์ ! ์์์ ๋จ์ฉํ๋ฉด ์คํ๋ ค ๋ณต์กํด์ง ์ ์์ด. ๋ช ํ์ฑ์ด ์ฌ์ฌ์ฉ์ฑ๋ณด๋ค ์ค์ํ๋ค๋ ๊ฑธ ๊ธฐ์ตํด. โ ๏ธ
๐ฆ DTO ํด๋ ๊ตฌ์กฐ
ํ๋ก์ ํธ๊ฐ ์ปค์ง๋ฉด DTO ํ์ผ๋ ์์ฒญ ๋ง์์ ธ. ์ฒด๊ณ์ ์ธ ํด๋ ๊ตฌ์กฐ๊ฐ ํ์ํด:
๐ MyProject.Api
๐ DTOs
๐ Users
๐ UserDto.cs
๐ CreateUserDto.cs
๐ UpdateUserDto.cs
๐ UserProfileDto.cs
๐ Products
๐ ProductDto.cs
๐ CreateProductDto.cs
๐ ProductListItemDto.cs
๐ Orders
๐ OrderDto.cs
๐ OrderDetailDto.cs
๐ CreateOrderDto.cs
๐ Common
๐ PagedResultDto.cs
๐ ApiResponseDto.cs
๐ Mappings
๐ UserProfile.cs
๐ ProductProfile.cs
๐ OrderProfile.cs
๋๋ฉ์ธ๋ณ๋ก ํด๋๋ฅผ ๋๋๊ณ , ๊ณตํต์ผ๋ก ์ฌ์ฉํ๋ DTO๋ Common ํด๋์ ๋ชจ์๋๋ ๊ฑฐ์ง. AutoMapper Profile๋ ๋ณ๋ ํด๋๋ก ๊ด๋ฆฌํ๋ฉด ์ฐพ๊ธฐ ์ฌ์! ๐
โก ์ฑ๋ฅ ์ต์ ํ ํ
AutoMapper๋ ํธ๋ฆฌํ์ง๋ง, ์๋ชป ์ฌ์ฉํ๋ฉด ์ฑ๋ฅ ๋ฌธ์ ๊ฐ ์๊ธธ ์ ์์ด. ์ค์ ์์ ๊ผญ ์์์ผ ํ ์ต์ ํ ํ๋ค์ ๊ณต์ ํ ๊ฒ! ๐
1๏ธโฃ ProjectTo ์ฌ์ฉํ๊ธฐ
Entity Framework Core์ ํจ๊ป ์ฌ์ฉํ ๋ ๊ฐ์ฅ ์ค์ํ ์ต์ ํ์ผ. ProjectTo๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ ๋จ๊ณ์์ ํ์ํ ์ปฌ๋ผ๋ง ์ ํํด:
// โ ๋์ ์: ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ๋ฉ๋ชจ๋ฆฌ๋ก ๊ฐ์ ธ์จ ํ ๋งคํ
var users = await _context.Users.ToListAsync();
var userDtos = _mapper.Map<List<UserDto>>(users);
// โ
์ข์ ์: ์ฟผ๋ฆฌ ๋จ๊ณ์์ ํ์ํ ์ปฌ๋ผ๋ง ์ ํ
var userDtos = await _context.Users
.ProjectTo<UserDto>(_mapper.ConfigurationProvider)
.ToListAsync();
ProjectTo๋ฅผ ์ฌ์ฉํ๋ฉด SQL ์ฟผ๋ฆฌ๊ฐ ์ด๋ ๊ฒ ๋ฐ๋์ด:
-- ProjectTo ์ฌ์ฉ ์
SELECT * FROM Users; -- ๋ชจ๋ ์ปฌ๋ผ
-- ProjectTo ์ฌ์ฉ ํ
SELECT Id, Username, Email FROM Users; -- ํ์ํ ์ปฌ๋ผ๋ง
๋ฐ์ดํฐ๊ฐ ๋ง์์๋ก ์ฑ๋ฅ ์ฐจ์ด๊ฐ ์์ฒญ๋! ํนํ ์ปฌ๋ผ์ด ์์ญ ๊ฐ์ธ ํ ์ด๋ธ์์๋ ํ์์ผ. โก
โ ๏ธ ProjectTo ์ฌ์ฉ ์ ์ฃผ์์ฌํญ
โข Include()๋ ThenInclude()์ ํจ๊ป ์ฌ์ฉํ์ง ๋ง. ProjectTo๊ฐ ์์์ ํ์ํ ์กฐ์ธ์ ์ฒ๋ฆฌํด.
โข ๋ณต์กํ ๊ณ์ฐ ๋ก์ง์ด ์์ผ๋ฉด SQL๋ก ๋ณํ๋์ง ์์ ์ ์์ด. ์ด ๊ฒฝ์ฐ AsEnumerable() ํ ๋งคํ.
โข ํญ์ ConfigurationProvider๋ฅผ ์ ๋ฌํด์ผ ํด.
2๏ธโฃ ๋งคํ ์ค์ ๊ฒ์ฆํ๊ธฐ
๋ฐํ์์ ๋งคํ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ฉด ๋๋ฒ๊น ์ด ์ด๋ ค์. ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ ์ค์ ์ ๊ฒ์ฆํ์:
// Program.cs
var app = builder.Build();
// ๊ฐ๋ฐ ํ๊ฒฝ์์๋ง ๊ฒ์ฆ (์ฑ๋ฅ ์ํฅ ์์)
if (app.Environment.IsDevelopment())
{
var mapper = app.Services.GetRequiredService<IMapper>();
mapper.ConfigurationProvider.AssertConfigurationIsValid();
}
app.Run();
AssertConfigurationIsValid()๋ ๋ชจ๋ ๋งคํ ์ค์ ์ ๊ฒ์ฌํด์ ๋ฌธ์ ๊ฐ ์์ผ๋ฉด ์์ธ๋ฅผ ๋์ ธ. ๋ฐฐํฌ ์ ์ ๊ผญ ํ์ธํ์! โ
3๏ธโฃ ์ปฌ๋ ์ ๋งคํ ์ต์ ํ
๋๋์ ๋ฐ์ดํฐ๋ฅผ ๋งคํํ ๋๋ ์ปฌ๋ ์ ํ์ ์ ํ์ด ์ค์ํด:
// List๋ณด๋ค IEnumerable์ด ๋ ํจ์จ์ ์ผ ์ ์์
public class OrderDto
{
public int Id { get; set; }
public IEnumerable<OrderItemDto> Items { get; set; } // List ๋์
}
// ์ฝ๊ธฐ ์ ์ฉ์ด๋ฉด IReadOnlyCollection ์ฌ์ฉ
public class ProductDto
{
public int Id { get; set; }
public IReadOnlyCollection<string> Tags { get; set; }
}
4๏ธโฃ ๋ถํ์ํ ๋งคํ ํผํ๊ธฐ
๋ชจ๋ ๊ฑธ ๋งคํํ ํ์๋ ์์ด. ๊ฐ๋จํ ๊ฒฝ์ฐ๋ ์๋ ๋งคํ์ด ๋ ๋น ๋ฅผ ์ ์์ด:
// ์์ฑ์ด 1~2๊ฐ๋ฉด ์๋ ๋งคํ๋ ๊ณ ๋ ค
public class SimpleDto
{
public int Id { get; set; }
public string Name { get; set; }
}
// ์ด ์ ๋๋ AutoMapper ์์ด๋ OK
var dto = new SimpleDto
{
Id = entity.Id,
Name = entity.Name
};
AutoMapper๋ ๋ฆฌํ๋ ์ ์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ์ฝ๊ฐ์ ์ค๋ฒํค๋๊ฐ ์์ด. ์ฑ๋ฅ์ด ๊ทน๋๋ก ์ค์ํ ํซํจ์ค(hot path)์์๋ ์๋ ๋งคํ์ ๊ณ ๋ คํด๋ด. ๐ฅ
5๏ธโฃ ๋งคํ ์บ์ฑ ํ์ฉ
AutoMapper๋ ๋ด๋ถ์ ์ผ๋ก ๋งคํ ๊ณํ์ ์บ์ฑํด. ํ์ง๋ง ๋์ ์ผ๋ก ๋งคํ์ ์์ฑํ๋ฉด ์บ์ ํจ๊ณผ๊ฐ ๋จ์ด์ ธ:
// โ ๋์ ์: ๋งค๋ฒ ์๋ก์ด ๋งคํ ์์ฑ
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<User, UserDto>();
});
var mapper = config.CreateMapper(); // ๋งค๋ฒ ์์ฑํ์ง ๋ง!
// โ
์ข์ ์: DI ์ปจํ
์ด๋์์ ์ฑ๊ธํค์ผ๋ก ์ฃผ์
๋ฐ๊ธฐ
public class UserService
{
private readonly IMapper _mapper; // ์ฌ์ฌ์ฉ
public UserService(IMapper mapper)
{
_mapper = mapper;
}
}
๐ก๏ธ ์ค์ ์์ : ์์ ํ CRUD API ๊ตฌํ
์ด๋ก ์ ์ถฉ๋ถํ ๋ฐฐ์ ์ผ๋, ์ด์ ์ค์ ์์ ๋ฅผ ๋ง๋ค์ด๋ณด์! ๊ฐ๋จํ ๋ธ๋ก๊ทธ ์์คํ ์ ๊ฒ์๊ธ(Post) ๊ด๋ฆฌ API๋ฅผ AutoMapper์ DTO ํจํด์ผ๋ก ๊ตฌํํด๋ณผ๊ฒ. ๐ป
๐ Step 1: ์ํฐํฐ ์ ์
// Domain/Entities/Post.cs
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public string Slug { get; set; }
public int AuthorId { get; set; }
public User Author { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public DateTime? PublishedAt { get; set; }
public bool IsPublished { get; set; }
public bool IsDeleted { get; set; }
public int ViewCount { get; set; }
public List<Comment> Comments { get; set; }
public List<Tag> Tags { get; set; }
}
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
public string PasswordHash { get; set; }
public string DisplayName { get; set; }
public string AvatarUrl { get; set; }
}
public class Comment
{
public int Id { get; set; }
public string Content { get; set; }
public int AuthorId { get; set; }
public User Author { get; set; }
public DateTime CreatedAt { get; set; }
}
public class Tag
{
public int Id { get; set; }
public string Name { get; set; }
}
๐ฆ Step 2: DTO ์ ์
// DTOs/Posts/PostDto.cs
public class PostDto
{
public int Id { get; set; }
public string Title { get; set; }
public string Slug { get; set; }
public string Excerpt { get; set; } // Content์ ์์ฝ
public AuthorDto Author { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? PublishedAt { get; set; }
public int ViewCount { get; set; }
public int CommentCount { get; set; }
public List<string> Tags { get; set; }
}
// DTOs/Posts/PostDetailDto.cs
public class PostDetailDto
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public string Slug { get; set; }
public AuthorDto Author { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public DateTime? PublishedAt { get; set; }
public int ViewCount { get; set; }
public List<CommentDto> Comments { get; set; }
public List<string> Tags { get; set; }
}
// DTOs/Posts/CreatePostDto.cs
public class CreatePostDto
{
[Required]
[StringLength(200)]
public string Title { get; set; }
[Required]
public string Content { get; set; }
public List<string> Tags { get; set; }
public bool PublishImmediately { get; set; }
}
// DTOs/Posts/UpdatePostDto.cs
public class UpdatePostDto
{
[StringLength(200)]
public string Title { get; set; }
public string Content { get; set; }
public List<string> Tags { get; set; }
}
// DTOs/Common/AuthorDto.cs
public class AuthorDto
{
public int Id { get; set; }
public string Username { get; set; }
public string DisplayName { get; set; }
public string AvatarUrl { get; set; }
}
// DTOs/Comments/CommentDto.cs
public class CommentDto
{
public int Id { get; set; }
public string Content { get; set; }
public AuthorDto Author { get; set; }
public DateTime CreatedAt { get; set; }
}
๐บ๏ธ Step 3: AutoMapper Profile ์ค์
// Mappings/PostProfile.cs
public class PostProfile : Profile
{
public PostProfile()
{
// Post โ PostDto
CreateMap<Post, PostDto>()
.ForMember(dest => dest.Excerpt,
opt => opt.MapFrom(src => CreateExcerpt(src.Content, 150)))
.ForMember(dest => dest.CommentCount,
opt => opt.MapFrom(src => src.Comments.Count))
.ForMember(dest => dest.Tags,
opt => opt.MapFrom(src => src.Tags.Select(t => t.Name).ToList()));
// Post โ PostDetailDto
CreateMap<Post, PostDetailDto>()
.ForMember(dest => dest.Tags,
opt => opt.MapFrom(src => src.Tags.Select(t => t.Name).ToList()));
// CreatePostDto โ Post
CreateMap<CreatePostDto, Post>()
.ForMember(dest => dest.Slug,
opt => opt.MapFrom(src => GenerateSlug(src.Title)))
.ForMember(dest => dest.CreatedAt,
opt => opt.MapFrom(src => DateTime.UtcNow))
.ForMember(dest => dest.IsPublished,
opt => opt.MapFrom(src => src.PublishImmediately))
.ForMember(dest => dest.PublishedAt,
opt => opt.MapFrom(src => src.PublishImmediately ? DateTime.UtcNow : (DateTime?)null))
.ForMember(dest => dest.Tags, opt => opt.Ignore()) // ๋ณ๋ ์ฒ๋ฆฌ
.ForMember(dest => dest.Id, opt => opt.Ignore())
.ForMember(dest => dest.AuthorId, opt => opt.Ignore())
.ForMember(dest => dest.ViewCount, opt => opt.Ignore())
.ForMember(dest => dest.Comments, opt => opt.Ignore());
// UpdatePostDto โ Post
CreateMap<UpdatePostDto, Post>()
.ForMember(dest => dest.UpdatedAt,
opt => opt.MapFrom(src => DateTime.UtcNow))
.ForMember(dest => dest.Slug,
opt => opt.MapFrom((src, dest) =>
!string.IsNullOrEmpty(src.Title) ? GenerateSlug(src.Title) : dest.Slug))
.ForMember(dest => dest.Tags, opt => opt.Ignore())
.ForAllMembers(opts => opts.Condition((src, dest, srcMember) => srcMember != null));
// User โ AuthorDto
CreateMap<User, AuthorDto>();
// Comment โ CommentDto
CreateMap<Comment, CommentDto>();
}
private string CreateExcerpt(string content, int maxLength)
{
if (string.IsNullOrEmpty(content))
return string.Empty;
var plainText = System.Text.RegularExpressions.Regex.Replace(content, "<.*?>", string.Empty);
return plainText.Length <= maxLength
? plainText
: plainText.Substring(0, maxLength) + "...";
}
private string GenerateSlug(string title)
{
if (string.IsNullOrEmpty(title))
return string.Empty;
return title.ToLower()
.Replace(" ", "-")
.Replace("?", "")
.Replace("!", "")
.Replace(".", "");
}
}
๐ฎ Step 4: ์๋น์ค ๋ ์ด์ด
// Services/PostService.cs
public interface IPostService
{
Task<PagedResult<PostDto>> GetPostsAsync(int page, int pageSize);
Task<PostDetailDto> GetPostByIdAsync(int id);
Task<PostDetailDto> GetPostBySlugAsync(string slug);
Task<PostDetailDto> CreatePostAsync(CreatePostDto dto, int authorId);
Task<PostDetailDto> UpdatePostAsync(int id, UpdatePostDto dto);
Task<bool> DeletePostAsync(int id);
}
public class PostService : IPostService
{
private readonly ApplicationDbContext _context;
private readonly IMapper _mapper;
public PostService(ApplicationDbContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public async Task<PagedResult<PostDto>> GetPostsAsync(int page, int pageSize)
{
var query = _context.Posts
.Include(p => p.Author)
.Include(p => p.Tags)
.Include(p => p.Comments)
.Where(p => !p.IsDeleted && p.IsPublished)
.OrderByDescending(p => p.PublishedAt);
var totalCount = await query.CountAsync();
// ProjectTo ์ฌ์ฉ์ผ๋ก ์ฑ๋ฅ ์ต์ ํ!
var posts = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ProjectTo<PostDto>(_mapper.ConfigurationProvider)
.ToListAsync();
return new PagedResult<PostDto>
{
Items = posts,
TotalCount = totalCount,
Page = page,
PageSize = pageSize
};
}
public async Task<PostDetailDto> GetPostBySlugAsync(string slug)
{
var post = await _context.Posts
.Include(p => p.Author)
.Include(p => p.Tags)
.Include(p => p.Comments)
.ThenInclude(c => c.Author)
.FirstOrDefaultAsync(p => p.Slug == slug && !p.IsDeleted);
if (post == null)
return null;
// ์กฐํ์ ์ฆ๊ฐ
post.ViewCount++;
await _context.SaveChangesAsync();
return _mapper.Map<PostDetailDto>(post);
}
public async Task<PostDetailDto> CreatePostAsync(CreatePostDto dto, int authorId)
{
var post = _mapper.Map<Post>(dto);
post.AuthorId = authorId;
// ํ๊ทธ ์ฒ๋ฆฌ
if (dto.Tags != null && dto.Tags.Any())
{
post.Tags = new List<Tag>();
foreach (var tagName in dto.Tags)
{
var tag = await _context.Tags
.FirstOrDefaultAsync(t => t.Name == tagName);
if (tag == null)
{
tag = new Tag { Name = tagName };
_context.Tags.Add(tag);
}
post.Tags.Add(tag);
}
}
_context.Posts.Add(post);
await _context.SaveChangesAsync();
// ์์ฑ๋ ๊ฒ์๊ธ ๋ค์ ์กฐํ (๊ด๊ณ ํฌํจ)
var createdPost = await _context.Posts
.Include(p => p.Author)
.Include(p => p.Tags)
.Include(p => p.Comments)
.FirstOrDefaultAsync(p => p.Id == post.Id);
return _mapper.Map<PostDetailDto>(createdPost);
}
public async Task<PostDetailDto> UpdatePostAsync(int id, UpdatePostDto dto)
{
var post = await _context.Posts
.Include(p => p.Tags)
.FirstOrDefaultAsync(p => p.Id == id && !p.IsDeleted);
if (post == null)
return null;
// AutoMapper๋ก ๋ณ๊ฒฝ์ฌํญ ์ ์ฉ
_mapper.Map(dto, post);
// ํ๊ทธ ์
๋ฐ์ดํธ
if (dto.Tags != null)
{
post.Tags.Clear();
foreach (var tagName in dto.Tags)
{
var tag = await _context.Tags
.FirstOrDefaultAsync(t => t.Name == tagName);
if (tag == null)
{
tag = new Tag { Name = tagName };
_context.Tags.Add(tag);
}
post.Tags.Add(tag);
}
}
await _context.SaveChangesAsync();
// ์
๋ฐ์ดํธ๋ ๊ฒ์๊ธ ๋ค์ ์กฐํ
var updatedPost = await _context.Posts
.Include(p => p.Author)
.Include(p => p.Tags)
.Include(p => p.Comments)
.FirstOrDefaultAsync(p => p.Id == id);
return _mapper.Map<PostDetailDto>(updatedPost);
}
public async Task<bool> DeletePostAsync(int id)
{
var post = await _context.Posts.FindAsync(id);
if (post == null || post.IsDeleted)
return false;
// ์ํํธ ์ญ์
post.IsDeleted = true;
await _context.SaveChangesAsync();
return true;
}
}
๐ Step 5: API ์ปจํธ๋กค๋ฌ
// Controllers/PostsController.cs
[ApiController]
[Route("api/[controller]")]
public class PostsController : ControllerBase
{
private readonly IPostService _postService;
public PostsController(IPostService postService)
{
_postService = postService;
}
/// <summary>
/// ๊ฒ์๊ธ ๋ชฉ๋ก ์กฐํ
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(PagedResult<PostDto>), 200)]
public async Task<ActionResult<PagedResult<PostDto>>> GetPosts(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10)
{
if (page < 1 || pageSize < 1 || pageSize > 100)
return BadRequest("Invalid pagination parameters");
var result = await _postService.GetPostsAsync(page, pageSize);
return Ok(result);
}
/// <summary>
/// ๊ฒ์๊ธ ์์ธ ์กฐํ (Slug ๊ธฐ๋ฐ)
/// </summary>
[HttpGet("{slug}")]
[ProducesResponseType(typeof(PostDetailDto), 200)]
[ProducesResponseType(404)]
public async Task<ActionResult<PostDetailDto>> GetPost(string slug)
{
var post = await _postService.GetPostBySlugAsync(slug);
if (post == null)
return NotFound();
return Ok(post);
}
/// <summary>
/// ๊ฒ์๊ธ ์์ฑ
/// </summary>
[HttpPost]
[Authorize]
[ProducesResponseType(typeof(PostDetailDto), 201)]
[ProducesResponseType(400)]
public async Task<ActionResult<PostDetailDto>> CreatePost([FromBody] CreatePostDto dto)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
// ํ์ฌ ๋ก๊ทธ์ธํ ์ฌ์ฉ์ ID ๊ฐ์ ธ์ค๊ธฐ
var authorId = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier)?.Value);
var post = await _postService.CreatePostAsync(dto, authorId);
return CreatedAtAction(
nameof(GetPost),
new { slug = post.Slug },
post);
}
/// <summary>
/// ๊ฒ์๊ธ ์์
/// </summary>
[HttpPut("{id}")]
[Authorize]
[ProducesResponseType(typeof(PostDetailDto), 200)]
[ProducesResponseType(404)]
public async Task<ActionResult<PostDetailDto>> UpdatePost(
int id,
[FromBody] UpdatePostDto dto)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
var post = await _postService.UpdatePostAsync(id, dto);
if (post == null)
return NotFound();
return Ok(post);
}
/// <summary>
/// ๊ฒ์๊ธ ์ญ์
/// </summary>
[HttpDelete("{id}")]
[Authorize]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public async Task<IActionResult> DeletePost(int id)
{
var result = await _postService.DeletePostAsync(id);
if (!result)
return NotFound();
return NoContent();
}
}
์! ๊ฝค ๊ธด ์ฝ๋์์ง? ๐ ํ์ง๋ง ์ด๊ฒ ์ค์ ํ๋ก์ ํธ์์ ์ฌ์ฉํ๋ ํจํด์ด์ผ. ๊ฐ ๊ณ์ธต์ด ๋ช ํํ ๋ถ๋ฆฌ๋์ด ์๊ณ , AutoMapper ๋๋ถ์ ๋งคํ ์ฝ๋๊ฐ ๊น๋ํ๊ฒ ์ ๋ฆฌ๋์ด. โจ
๐ฏ ์ด ์์ ์์ ๋ฐฐ์ด ํต์ฌ ํฌ์ธํธ
โข ๊ณ์ธต ๋ถ๋ฆฌ: Controller โ Service โ Repository ๊ตฌ์กฐ
โข DTO ๋ถ๋ฆฌ: ๋ชฉ๋ก์ฉ, ์์ธ์ฉ, ์์ฑ์ฉ, ์์ ์ฉ DTO๋ฅผ ๊ฐ๊ฐ ์ ์
โข ProjectTo ํ์ฉ: ๋ชฉ๋ก ์กฐํ ์ ์ฑ๋ฅ ์ต์ ํ
โข ๋ณต์กํ ๋งคํ: ์ค์ฒฉ ๊ฐ์ฒด, ์ปฌ๋ ์
, ๊ณ์ฐ ์์ฑ ์ฒ๋ฆฌ
โข ๋น์ฆ๋์ค ๋ก์ง: Slug ์์ฑ, ์กฐํ์ ์ฆ๊ฐ, ํ๊ทธ ๊ด๋ฆฌ ๋ฑ
โข ์ํํธ ์ญ์ : IsDeleted ํ๋๊ทธ ์ฌ์ฉ
๐จ ํํ ์ค์์ ํด๊ฒฐ ๋ฐฉ๋ฒ
AutoMapper๋ฅผ ์ฌ์ฉํ๋ค ๋ณด๋ฉด ๋๊ตฌ๋ ํ ๋ฒ์ฏค ๊ฒช๋ ์ค์๋ค์ด ์์ด. ๋ฏธ๋ฆฌ ์๊ณ ์์ผ๋ฉด ์ฝ์ง์ ์ค์ผ ์ ์์ง! ๐ก
โ ์ค์ 1: ์ํ ์ฐธ์กฐ ๋ฌธ์
์ฆ์: JSON ์ง๋ ฌํ ์ ๋ฌดํ ๋ฃจํ ๋ฐ์
์์ธ: Entity ๊ฐ ์๋ฐฉํฅ ๊ด๊ณ๊ฐ ์์ ๋ AutoMapper๊ฐ ๋์์ด ๋งคํ์ ์๋ํจ
// ๋ฌธ์ ์ํฉ
public class Post
{
public int Id { get; set; }
public User Author { get; set; }
}
public class User
{
public int Id { get; set; }
public List<Post> Posts { get; set; } // ์ํ ์ฐธ์กฐ!
}
ํด๊ฒฐ ๋ฐฉ๋ฒ:
// ๋ฐฉ๋ฒ 1: DTO์์ ์ํ ์ฐธ์กฐ ์ ๊ฑฐ
public class PostDto
{
public int Id { get; set; }
public AuthorDto Author { get; set; } // User ์ ์ฒด๊ฐ ์๋ ์์ฝ ์ ๋ณด๋ง
}
public class AuthorDto
{
public int Id { get; set; }
public string Name { get; set; }
// Posts ์์ฑ ์์!
}
// ๋ฐฉ๋ฒ 2: MaxDepth ์ค์
CreateMap<Post, PostDto>()
.ForMember(dest => dest.Author, opt => opt.MaxDepth(1));
โ ์ค์ 2: ๋งคํ ์ค์ ๋๋ฝ
์ฆ์:
AutoMapperMappingException: Missing type map configuration ์๋ฌ์์ธ: ํด๋น ํ์ ์ ๋ํ
CreateMap ์ค์ ์ ์ ํ์ํด๊ฒฐ ๋ฐฉ๋ฒ:
// Profile์ ๋งคํ ์ถ๊ฐ
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<Source, Destination>();
// ์ค์ฒฉ ๊ฐ์ฒด๋ ์์ง ๋ง๊ณ !
CreateMap<NestedSource, NestedDestination>();
}
}
// ๋๋ AssertConfigurationIsValid๋ก ๋ฏธ๋ฆฌ ๊ฒ์ฆ
mapper.ConfigurationProvider.AssertConfigurationIsValid();
โ ์ค์ 3: ์ปฌ๋ ์ ๋งคํ ์ null ์ฒ๋ฆฌ ์ ํจ
์ฆ์: NullReferenceException ๋ฐ์
์์ธ: ์์ค ์ปฌ๋ ์
์ด null์ผ ๋ ์ฒ๋ฆฌ ์ ํจ
ํด๊ฒฐ ๋ฐฉ๋ฒ:
CreateMap<Post, PostDto>()
.ForMember(dest => dest.Tags, opt =>
{
opt.PreCondition(src => src.Tags != null);
opt.MapFrom(src => src.Tags.Select(t => t.Name).ToList());
})
// ๋๋ null ๋์ ๋น ๋ฆฌ์คํธ
.ForMember(dest => dest.Tags, opt =>
opt.MapFrom(src => src.Tags != null
? src.Tags.Select(t => t.Name).ToList()
: new List<string>()));
โ ์ค์ 4: ๊ธฐ์กด ๊ฐ์ฒด ์ ๋ฐ์ดํธ ์ ์ค์
์ฆ์: PATCH ์์ฒญ ์ null ๊ฐ์ด ๊ธฐ์กด ๋ฐ์ดํฐ๋ฅผ ๋ฎ์ด์
์์ธ: ๋ชจ๋ ์์ฑ์ ๋ฌด์กฐ๊ฑด ๋งคํํจ
ํด๊ฒฐ ๋ฐฉ๋ฒ:
// ๋ฐฉ๋ฒ 1: ForAllMembers + Condition
CreateMap<UpdateUserDto, User>()
.ForAllMembers(opts =>
opts.Condition((src, dest, srcMember) => srcMember != null));
// ๋ฐฉ๋ฒ 2: ์๋์ผ๋ก null ์ฒดํฌ
public async Task UpdateUserAsync(int id, UpdateUserDto dto)
{
var user = await _context.Users.FindAsync(id);
if (!string.IsNullOrEmpty(dto.Name))
user.Name = dto.Name;
if (!string.IsNullOrEmpty(dto.Email))
user.Email = dto.Email;
// ... ๋๋จธ์ง ์์ฑ
}
โ ์ค์ 5: ์ฑ๋ฅ ๊ณ ๋ ค ์์ด Include ๋จ๋ฐ
์ฆ์: ์ฟผ๋ฆฌ๊ฐ ๋๋ ค์ง๊ณ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ์ฆ๊ฐ
์์ธ: ํ์ ์๋ ๊ด๊ณ๊น์ง ๋ชจ๋ ๋ก๋ํจ
ํด๊ฒฐ ๋ฐฉ๋ฒ:
// โ ๋์ ์
var posts = await _context.Posts
.Include(p => p.Author)
.ThenInclude(a => a.Posts) // ๋ถํ์!
.Include(p => p.Comments)
.ThenInclude(c => c.Author)
.ThenInclude(a => a.Posts) // ๋์ฑ ๋ถํ์!
.ToListAsync();
// โ
์ข์ ์: ProjectTo ์ฌ์ฉ
var posts = await _context.Posts
.ProjectTo<PostDto>(_mapper.ConfigurationProvider)
.ToListAsync();
// AutoMapper๊ฐ ํ์ํ ๊ฒ๋ง ์๋์ผ๋ก ์กฐ์ธ!
๐งช ํ ์คํธ ์์ฑํ๊ธฐ
AutoMapper ์ค์ ๋ ํ ์คํธ๊ฐ ํ์ํด! ๋งคํ์ด ์ ๋๋ก ๋๋์ง ํ์ธํ๋ ํ ์คํธ๋ฅผ ์์ฑํด๋ณด์. ๐ฌ
1๏ธโฃ ๋งคํ ์ค์ ๊ฒ์ฆ ํ ์คํธ
using AutoMapper;
using Xunit;
public class MappingProfileTests
{
private readonly IMapper _mapper;
public MappingProfileTests()
{
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile<PostProfile>();
cfg.AddProfile<UserProfile>();
});
_mapper = config.CreateMapper();
}
[Fact]
public void Configuration_Should_Be_Valid()
{
// AutoMapper ์ค์ ์ด ์ ํจํ์ง ๊ฒ์ฆ
_mapper.ConfigurationProvider.AssertConfigurationIsValid();
}
[Fact]
public void Should_Map_Post_To_PostDto()
{
// Arrange
var post = new Post
{
Id = 1,
Title = "Test Post",
Content = "This is a test content",
Author = new User
{
Id = 1,
Username = "testuser",
DisplayName = "Test User"
},
CreatedAt = DateTime.UtcNow,
ViewCount = 100,
Comments = new List<Comment>
{
new Comment { Id = 1, Content = "Comment 1" },
new Comment { Id = 2, Content = "Comment 2" }
},
Tags = new List<Tag>
{
new Tag { Id = 1, Name = "C#" },
new Tag { Id = 2, Name = "AutoMapper" }
}
};
// Act
var dto = _mapper.Map<PostDto>(post);
// Assert
Assert.Equal(post.Id, dto.Id);
Assert.Equal(post.Title, dto.Title);
Assert.Equal(post.Author.DisplayName, dto.Author.DisplayName);
Assert.Equal(2, dto.CommentCount);
Assert.Equal(2, dto.Tags.Count);
Assert.Contains("C#", dto.Tags);
Assert.NotNull(dto.Excerpt);
}
[Fact]
public void Should_Generate_Slug_When_Creating_Post()
{
// Arrange
var createDto = new CreatePostDto
{
Title = "Hello World Test!",
Content = "Content here",
PublishImmediately = true
};
// Act
var post = _mapper.Map<Post>(createDto);
// Assert
Assert.Equal("hello-world-test", post.Slug);
Assert.True(post.IsPublished);
Assert.NotNull(post.PublishedAt);
}
[Fact]
public void Should_Create_Excerpt_From_Content()
{
// Arrange
var longContent = new string('a', 200);
var post = new Post
{
Id = 1,
Title = "Test",
Content = longContent,
Author = new User { Id = 1, Username = "test" }
};
// Act
var dto = _mapper.Map<PostDto>(post);
// Assert
Assert.True(dto.Excerpt.Length <= 153); // 150 + "..."
Assert.EndsWith("...", dto.Excerpt);
}
}
2๏ธโฃ ํตํฉ ํ ์คํธ
public class PostServiceIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly IMapper _mapper;
public PostServiceIntegrationTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
_mapper = _factory.Services.GetRequiredService<IMapper>();
}
[Fact]
public async Task CreatePost_Should_Return_MappedDto()
{
// Arrange
using var scope = _factory.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var service = new PostService(context, _mapper);
var createDto = new CreatePostDto
{
Title = "Integration Test Post",
Content = "This is an integration test",
Tags = new List<string> { "Test", "Integration" },
PublishImmediately = true
};
// Act
var result = await service.CreatePostAsync(createDto, 1);
// Assert
Assert.NotNull(result);
Assert.Equal(createDto.Title, result.Title);
Assert.NotNull(result.Slug);
Assert.Equal(2, result.Tags.Count);
Assert.NotNull(result.Author);
}
}
ํ ์คํธ๋ฅผ ์์ฑํ๋ฉด ๋ฆฌํฉํ ๋งํ ๋ ์์ฌํ๊ณ ์ฝ๋๋ฅผ ์์ ํ ์ ์์ด. ํนํ ๋งคํ ๋ก์ง์ด ๋ณต์กํ ์๋ก ํ ์คํธ๊ฐ ์ค์ํด! ๐ก๏ธ
๐ ๊ณ ๊ธ ๊ธฐ๋ฒ๊ณผ ํ
๊ธฐ๋ณธ์ ๋ง์คํฐํ์ผ๋, ์ด์ ์ข ๋ ๊ณ ๊ธ ๊ธฐ๋ฒ๋ค์ ์์๋ณด์! ๐
1๏ธโฃ Value Resolver ์ฌ์ฉํ๊ธฐ
๋ณต์กํ ๋งคํ ๋ก์ง์ ๋ณ๋ ํด๋์ค๋ก ๋ถ๋ฆฌํ๋ฉด ์ฌ์ฌ์ฉ์ฑ๊ณผ ํ ์คํธ ์ฉ์ด์ฑ์ด ๋์์ ธ:
// Value Resolver ์ ์
public class FullNameResolver : IValueResolver<User, UserDto, string>
{
public string Resolve(User source, UserDto destination, string destMember, ResolutionContext context)
{
return $"{source.FirstName} {source.MiddleName} {source.LastName}".Trim();
}
}
// Profile์์ ์ฌ์ฉ
public class UserProfile : Profile
{
public UserProfile()
{
CreateMap<User, UserDto>()
.ForMember(dest => dest.FullName,
opt => opt.MapFrom<FullNameResolver>());
}
}
// DI ๋ฑ๋ก
builder.Services.AddTransient<FullNameResolver>();
2๏ธโฃ Type Converter ํ์ฉ
ํน์ ํ์ ๊ฐ ๋ณํ ๋ก์ง์ ์ ์ํ ์ ์์ด:
// DateTime์ ํน์ ํฌ๋งท ๋ฌธ์์ด๋ก ๋ณํ
public class DateTimeToStringConverter : ITypeConverter<DateTime, string>
{
public string Convert(DateTime source, string destination, ResolutionContext context)
{
return source.ToString("yyyy-MM-dd HH:mm:ss");
}
}
// Profile์ ๋ฑ๋ก
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<DateTime, string>().ConvertUsing<DateTimeToStringConverter>();
// ์ด์ DateTime โ string ๋งคํ์ด ์๋์ผ๋ก ์ด ์ปจ๋ฒํฐ ์ฌ์ฉ
CreateMap<Post, PostDto>(); // CreatedAt์ด ์๋ ๋ณํ๋จ
}
}
3๏ธโฃ Before/After Map ํ
๋งคํ ์ ํ์ ์ถ๊ฐ ๋ก์ง์ ์คํํ ์ ์์ด:
CreateMap<CreatePostDto, Post>()
.BeforeMap((src, dest) =>
{
// ๋งคํ ์ ์คํ
Console.WriteLine($"Creating post: {src.Title}");
})
.AfterMap((src, dest) =>
{
// ๋งคํ ํ ์คํ
dest.Slug = dest.Slug?.ToLower();
Console.WriteLine($"Post created with slug: {dest.Slug}");
});
4๏ธโฃ ์กฐ๊ฑด๋ถ ๋งคํ ๊ณ ๊ธ ํ์ฉ
CreateMap<User, UserDto>()
// ์กฐ๊ฑด์ ๋ฐ๋ผ ๋ค๋ฅธ ๊ฐ ๋งคํ
.ForMember(dest => dest.Status, opt => opt.MapFrom(src =>
src.IsActive ? "ํ์ฑ" :
src.IsSuspended ? "์ ์ง" :
src.IsDeleted ? "์ญ์ " : "์ ์ ์์"))
// ํน์ ์กฐ๊ฑด์ผ ๋๋ง ๋งคํ
.ForMember(dest => dest.Email, opt =>
{
opt.Condition(src => src.EmailVerified);
opt.MapFrom(src => src.Email);
});
5๏ธโฃ ๋์ ๋งคํ (Runtime์ ๊ฒฐ์ )
// ์ฌ์ฉ์ ์ญํ ์ ๋ฐ๋ผ ๋ค๋ฅธ DTO๋ก ๋งคํ
public async Task<object> GetUserProfile(int userId, string role)
{
var user = await _context.Users.FindAsync(userId);
return role switch
{
"Admin" => _mapper.Map<AdminUserDto>(user),
"User" => _mapper.Map<UserDto>(user),
"Guest" => _mapper.Map<PublicUserDto>(user),
_ => throw new ArgumentException("Invalid role")
};
}
6๏ธโฃ ๊ธ๋ก๋ฒ ์ค์
var config = new MapperConfiguration(cfg =>
{
// ๋ชจ๋ ๋งคํ์ ์ ์ฉ๋๋ ๊ธ๋ก๋ฒ ์ค์
cfg.AllowNullCollections = true; // null ์ปฌ๋ ์
ํ์ฉ
cfg.AllowNullDestinationValues = true; // null ๋์ ๊ฐ ํ์ฉ
// ๋ช
๋ช
๊ท์น ์ค์
cfg.SourceMemberNamingConvention = new LowerUnderscoreNamingConvention();
cfg.DestinationMemberNamingConvention = new PascalCaseNamingConvention();
// ํ๋กํ ์ถ๊ฐ
cfg.AddProfile<UserProfile>();
cfg.AddProfile<PostProfile>();
});
๐ ์ค๋ฌด ๋ฒ ์คํธ ํ๋ํฐ์ค
์ง๊ธ๊น์ง ๋ฐฐ์ด ๋ด์ฉ์ ๋ฐํ์ผ๋ก, ์ค๋ฌด์์ ๊ผญ ์ง์ผ์ผ ํ ๋ฒ ์คํธ ํ๋ํฐ์ค๋ฅผ ์ ๋ฆฌํด๋ณผ๊ฒ! ๐
โ DO (ํด์ผ ํ ๊ฒ๋ค)
1. Profile์ ๋๋ฉ์ธ๋ณ๋ก ๋ถ๋ฆฌํ๋ผ
ํ๋์ ๊ฑฐ๋ํ Profile๋ณด๋ค UserProfile, ProductProfile ๋ฑ์ผ๋ก ๋๋๋ ๊ฒ ๊ด๋ฆฌํ๊ธฐ ์ฌ์.
2. DTO ๋ช
๋ช
๊ท์น์ ์ผ๊ด๋๊ฒ ์ ์งํ๋ผ
ํ ์ ์ฒด๊ฐ ๋์ผํ ๋ค์ด๋ฐ ์ปจ๋ฒค์
์ ๋ฐ๋ผ์ผ ํด. ๋ฌธ์ํํด๋๋ ๊ฒ๋ ์ข์.
3. ProjectTo๋ฅผ ์ ๊ทน ํ์ฉํ๋ผ
EF Core์ ํจ๊ป ์ฌ์ฉํ ๋๋ ๊ฑฐ์ ํ์์ผ. ์ฑ๋ฅ ์ฐจ์ด๊ฐ ์์ฒญ๋๊ฑฐ๋ .
4. ๋งคํ ์ค์ ์ ํ
์คํธํ๋ผ
AssertConfigurationIsValid()์ ๋จ์ ํ
์คํธ๋ก ๊ฒ์ฆํด.
5. ๋ณต์กํ ๋ก์ง์ Value Resolver๋ก ๋ถ๋ฆฌํ๋ผ
Profile์ด ๋๋ฌด ๋ณต์กํด์ง๋ฉด ์ ์ง๋ณด์๊ฐ ์ด๋ ค์์ ธ.
6. ๋ฌธ์ํ๋ฅผ ์ฒ ์ ํ ํ๋ผ
ํนํ ์ปค์คํ
๋งคํ ๋ก์ง์ ์ฃผ์์ผ๋ก ์ค๋ช
ํด๋ฌ.
7. ๋ฒ์ ๊ด๋ฆฌ๋ฅผ ๊ณ ๋ คํ๋ผ
API ๋ฒ์ ์ด ์ฌ๋ผ๊ฐ๋ฉด DTO๋ ๋ฒ์ ๋ณ๋ก ๊ด๋ฆฌํด. ์: UserDtoV1, UserDtoV2
โ DON'T (ํ์ง ๋ง์์ผ ํ ๊ฒ๋ค)
1. Entity๋ฅผ ์ง์ ๋ฐํํ์ง ๋ง๋ผ
ํญ์ DTO๋ฅผ ๊ฑฐ์ณ์ ๋ฐํํด. ๋ณด์๊ณผ ์ ์ง๋ณด์๋ฅผ ์ํด์์ผ.
2. ์๋ฐฉํฅ ๋งคํ์ ๋จ์ฉํ์ง ๋ง๋ผ
ReverseMap()์ ํธํ์ง๋ง, ์๋ํ์ง ์์ ๋งคํ์ด ์๊ธธ ์ ์์ด.
3. ๋ชจ๋ ์์ฑ์ ๋งคํํ๋ ค ํ์ง ๋ง๋ผ
ํ์ํ ๊ฒ๋ง ๋งคํํด. "ํน์ ๋ชฐ๋ผ์"๋ ์ข์ง ์์ ์ด์ ์ผ.
4. ๋งคํ ๋ก์ง์ ๋น์ฆ๋์ค ๋ก์ง์ ๋ฃ์ง ๋ง๋ผ
๋งคํ์ ๋จ์ ๋ณํ๋ง. ๋ณต์กํ ๋ก์ง์ ์๋น์ค ๊ณ์ธต์์ ์ฒ๋ฆฌํด.
5. Include๋ฅผ ProjectTo์ ํจ๊ป ์ฌ์ฉํ์ง ๋ง๋ผ
ProjectTo๊ฐ ์์์ ํ์ํ ์กฐ์ธ์ ์ฒ๋ฆฌํด์ค.
6. ์ฑ๋ฅ ์ธก์ ์์ด ์ต์ ํํ์ง ๋ง๋ผ
"๋๋ฆด ๊ฒ ๊ฐ์์"๊ฐ ์๋๋ผ ์ค์ ๋ก ์ธก์ ํ ํ ์ต์ ํํด.
7. AutoMapper๋ฅผ ๋ชจ๋ ๊ณณ์ ์ฌ์ฉํ์ง ๋ง๋ผ
๊ฐ๋จํ ๊ฒฝ์ฐ๋ ์๋ ๋งคํ์ด ๋ ๋ช
ํํ ์ ์์ด.
๐ก ํ๋ก์ ํธ ๊ตฌ์กฐ ์์
๐ MyProject.Solution
๐ MyProject.Domain
๐ Entities
๐ User.cs
๐ Post.cs
๐ Comment.cs
๐ MyProject.Application
๐ DTOs
๐ Users
๐ UserDto.cs
๐ CreateUserDto.cs
๐ UpdateUserDto.cs
๐ Posts
๐ PostDto.cs
๐ PostDetailDto.cs
๐ CreatePostDto.cs
๐ Mappings
๐ UserProfile.cs
๐ PostProfile.cs
๐ Services
๐ IUserService.cs
๐ UserService.cs
๐ MyProject.Api
๐ Controllers
๐ UsersController.cs
๐ PostsController.cs
๐ Program.cs
๐ MyProject.Tests
๐ Mappings
๐ UserProfileTests.cs
๐ PostProfileTests.cs
๐ Services
๐ UserServiceTests.cs
๐ ์ฌ๋ฅ๋ท์์ ๋ฐฐ์ฐ๊ณ ๋๋๊ธฐ
์ฌ๊ธฐ๊น์ง AutoMapper์ DTO ํจํด์ ๋ํด ๊น์ด ์๊ฒ ์์๋ดค์ด! ๐
์ด๋ฐ ๊ธฐ์ ๋ค์ ํผ์ ๊ณต๋ถํ๋ ๊ฒ๋ ์ข์ง๋ง, ๋ค๋ฅธ ๊ฐ๋ฐ์๋ค๊ณผ ๊ฒฝํ์ ๋๋๋ฉด ํจ์ฌ ๋ ๋นจ๋ฆฌ ์ฑ์ฅํ ์ ์์ด. ์ฌ๋ฅ๋ท์์๋ C# ๊ฐ๋ฐ ๊ด๋ จ ๋ค์ํ ์ฌ๋ฅ์ ๊ฑฐ๋ํ๊ณ ์์ด. AutoMapper ํ์ฉ๋ฒ์ ๋ ๋ฐฐ์ฐ๊ณ ์ถ๊ฑฐ๋, ๋ฐ๋๋ก ๋ค๊ฐ ๊ฐ์ง ๋
ธํ์ฐ๋ฅผ ๋ค๋ฅธ ์ฌ๋๋ค์๊ฒ ์๋ ค์ฃผ๊ณ ์ถ๋ค๋ฉด ์ฌ๋ฅ๋ท์ ํ์ฉํด๋ณด๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ! ๐ค
์ค๋ฌด ํ๋ก์ ํธ์์ AutoMapper๋ฅผ ๋์
ํ๋ ค๋๋ฐ ๋ง๋งํ๋ค๋ฉด, ๊ฒฝํ ๋ง์ ๊ฐ๋ฐ์์๊ฒ ์ปจ์คํ
์ ๋ฐ์ ์๋ ์๊ณ , ์ฝ๋ ๋ฆฌ๋ทฐ๋ฅผ ๋ฐ์ ์๋ ์์ง. ๋ฐ๋๋ก ๋ค๊ฐ AutoMapper ์ ๋ฌธ๊ฐ๋ผ๋ฉด ๋ค๋ฅธ ๊ฐ๋ฐ์๋ค์ ๋์์ฃผ๋ฉด์ ์์ต๋ ์ฐฝ์ถํ ์ ์์ด!
๐ฌ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์, ์ ๋ง ๊ธด ์ฌ์ ์ด์์ง? ๐
AutoMapper์ DTO ํจํด์ ์ฒ์์ "์ด๊ฒ ๊ผญ ํ์ํ๊ฐ?" ์ถ์ ์ ์์ด. ํ์ง๋ง ํ๋ก์ ํธ๊ฐ ์ปค์ง๊ณ ํ์ด ์ปค์ง์๋ก ๊ทธ ์ง๊ฐ๋ฅผ ๋ฐํํ์ง.
ํต์ฌ์ ๋ค์ ํ๋ฒ ์ ๋ฆฌํ๋ฉด:
1. DTO ํจํด์ ๊ณ์ธต ๊ฐ ๋ฐ์ดํฐ ์ ์ก์ ์ํ ๊ฐ์ฒด๋ก, ๊ด์ฌ์ฌ๋ฅผ ๋ถ๋ฆฌํ๊ณ ๋ณด์์ ๊ฐํํด.
2. AutoMapper๋ ๊ฐ์ฒด ๊ฐ ๋งคํ์ ์๋ํํด์ ๋ฐ๋ณต์ ์ธ ์ฝ๋๋ฅผ ์ค์ด๊ณ ์์ฐ์ฑ์ ๋์ฌ์ค.
3. ProjectTo๋ฅผ ์ฌ์ฉํ๋ฉด EF Core์ ํจ๊ป ์ฌ์ฉํ ๋ ์ฑ๋ฅ์ ํฌ๊ฒ ํฅ์์ํฌ ์ ์์ด.
4. ๋ณต์กํ ๋งคํ์ Value Resolver๋ Type Converter๋ก ๋ถ๋ฆฌํด์ ๊ด๋ฆฌํด.
5. ํ
์คํธ๋ ํ์! ๋งคํ ์ค์ ์ด ์ฌ๋ฐ๋ฅธ์ง ํญ์ ๊ฒ์ฆํด.
6. ๋ฒ ์คํธ ํ๋ํฐ์ค๋ฅผ ๋ฐ๋ฅด๋, ํ๋ก์ ํธ ์ํฉ์ ๋ง๊ฒ ์ ์ฐํ๊ฒ ์ ์ฉํด.
์ฒ์์ ์ค์ ํ ๊ฒ ๋ง์์ ๋ณต์กํด ๋ณด์ผ ์ ์์ง๋ง, ํ๋ฒ ์ ๋๋ก ์ธํ
ํด๋๋ฉด ์ ๋ง ํธํด. ํนํ ๋๊ท๋ชจ ํ๋ก์ ํธ์์๋ AutoMapper ์์ด๋ ์์ํ๊ธฐ ํ๋ค ์ ๋์ผ! ๐
์ด ๊ธ์ด AutoMapper์ DTO ํจํด์ ์ดํดํ๋ ๋ฐ ๋์์ด ๋์ผ๋ฉด ์ข๊ฒ ์ด. ๊ถ๊ธํ ์ ์ด ์๊ฑฐ๋ ๋ ๊น์ด ์๋ ๋ด์ฉ์ด ํ์ํ๋ค๋ฉด, ์ง์ ํ๋ก์ ํธ์ ์ ์ฉํด๋ณด๋ฉด์ ๊ฒฝํ์ ์์๊ฐ๋ ๊ฒ ์ต๊ณ ์ผ. ๊ทธ๋ฆฌ๊ณ ๋งํ๋ ๋ถ๋ถ์ด ์๋ค๋ฉด ๊ฐ๋ฐ์ ์ปค๋ฎค๋ํฐ๋ ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์ ํ์ฉํด์ ๋์์ ๋ฐ๋ ๊ฒ๋ ์์ง ๋ง! ๐ช
ํดํผ ์ฝ๋ฉ! ๐จโ๐ป๐ฉโ๐ป
๐ฏ ํต์ฌ ์์ฝ
AutoMapper๋ ๊ฐ์ฒด ๊ฐ ๋งคํ์ ์๋ํํ๋ ๊ฐ๋ ฅํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
DTO ํจํด์ผ๋ก ๊ณ์ธต์ ๋ถ๋ฆฌํ๊ณ ๋ณด์์ ๊ฐํ
ProjectTo๋ก EF Core ์ฟผ๋ฆฌ ์ฑ๋ฅ ์ต์ ํ
Profile๋ก ๋งคํ ์ค์ ์ ์ฒด๊ณ์ ์ผ๋ก ๊ด๋ฆฌ
ํ
์คํธ์ ๋ฒ ์คํธ ํ๋ํฐ์ค๋ฅผ ์์ง ๋ง์!
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.