์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐ŸŽฏ AutoMapper๋กœ ์‰ฝ๊ฒŒ ๋ฐฐ์šฐ๋Š” ๊ฐ์ฒด ๋งคํ•‘๊ณผ DTO ํŒจํ„ด ์™„๋ฒฝ ๊ตฌํ˜„ ์ „๋žต

๐ŸŽฏ AutoMapper๋กœ ์‰ฝ๊ฒŒ ๋ฐฐ์šฐ๋Š” ๊ฐ์ฒด ๋งคํ•‘๊ณผ DTO ํŒจํ„ด ์™„๋ฒฝ ๊ตฌํ˜„ ์ „๋žต

C# ๊ฐœ๋ฐœ์ž๋ผ๋ฉด ๋ฐ˜๋“œ์‹œ ์•Œ์•„์•ผ ํ•  ๊ฐ์ฒด ๋ณ€ํ™˜์˜ ๋ชจ๋“  ๊ฒƒ

๐Ÿ‘‹ ์•ˆ๋…•! ์˜ค๋Š˜์€ C# ๊ฐœ๋ฐœํ•˜๋ฉด์„œ ์ •๋ง ์ž์ฃผ ๋งˆ์ฃผ์น˜๋Š” ๋ฌธ์ œ ํ•˜๋‚˜๋ฅผ ๊ฐ™์ด ํ•ด๊ฒฐํ•ด๋ณผ ๊ฑฐ์•ผ. ๋ฐ”๋กœ "๊ฐ์ฒด๋ฅผ ๋‹ค๋ฅธ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์ž‘์—…"์ด์ง€. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๊ฐ€์ ธ์˜จ ์—”ํ‹ฐํ‹ฐ๋ฅผ API ์‘๋‹ต์šฉ DTO๋กœ ๋ฐ”๊พธ๊ฑฐ๋‚˜, ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ฐ›์€ ์š”์ฒญ ๊ฐ์ฒด๋ฅผ ๋„๋ฉ”์ธ ๋ชจ๋ธ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์ผ ๋ง์ด์•ผ.

์ฒ˜์Œ์—” "๊ทธ๋ƒฅ ์ผ์ผ์ด ๋งคํ•‘ํ•˜๋ฉด ๋˜์ง€ ๋ญ~" ํ•˜๊ณ  ์ƒ๊ฐํ•  ์ˆ˜ ์žˆ์–ด. ๊ทผ๋ฐ ํ”„๋กœ์ ํŠธ๊ฐ€ ์ปค์ง€๋ฉด์„œ ์ด๋Ÿฐ ๋งคํ•‘ ์ฝ”๋“œ๊ฐ€ ์ˆ˜๋ฐฑ ๊ฐœ์”ฉ ์Œ“์ด๋ฉด? ๐Ÿ˜ฑ ์œ ์ง€๋ณด์ˆ˜ ์ง€์˜ฅ์ด ๋”ฐ๋กœ ์—†์ง€. ๊ทธ๋ž˜์„œ ๋“ฑ์žฅํ•œ ๊ฒŒ ๋ฐ”๋กœ AutoMapper์•ผ!

์ด ๊ธ€์—์„œ๋Š” AutoMapper์˜ ๊ธฐ๋ณธ ๊ฐœ๋…๋ถ€ํ„ฐ ์‹ค์ „ ํ™œ์šฉ๋ฒ•, ๊ทธ๋ฆฌ๊ณ  DTO ํŒจํ„ด์„ ์ œ๋Œ€๋กœ ๊ตฌํ˜„ํ•˜๋Š” ์ „๋žต๊นŒ์ง€ ์นœ๊ตฌํ•œํ…Œ ์„ค๋ช…ํ•˜๋“ฏ์ด ์‰ฝ๊ฒŒ ํ’€์–ด๋ณผ๊ฒŒ. ๐Ÿš€

Entity ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ชจ๋ธ โ€ข Id โ€ข Name โ€ข CreatedDate AutoMapper ์ž๋™ ๋ณ€ํ™˜ DTO ๋ฐ์ดํ„ฐ ์ „์†ก ๊ฐ์ฒด โ€ข Identifier โ€ข DisplayName โ€ข Created

๐Ÿค” ์™œ ๊ฐ์ฒด ๋งคํ•‘์ด ํ•„์š”ํ• ๊นŒ?

๋ณธ๊ฒฉ์ ์œผ๋กœ 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 ๋งคํ•‘ ํ”„๋กœ์„ธ์Šค Source (User) ์†์„ฑ๋“ค: Id: 1001 FirstName: "ํ™" LastName: "๊ธธ๋™" Email: "hong@test.com" BirthDate: 1990-01-01 PasswordHash: "***" CreatedAt: DateTime IsDeleted: false Profile ์„ค์ • ๊ธฐ๋ฐ˜ ์ž๋™ ๋งคํ•‘ + ์ปค์Šคํ…€ ๊ทœ์น™ Destination (UserDto) ๋งคํ•‘๋œ ์†์„ฑ๋“ค: Id: 1001 โœ“ FullName: "ํ™๊ธธ๋™" โœ“ (FirstName + LastName) Email: "hong@test.com" โœ“ Age: 34 โœ“ (BirthDate ๊ธฐ๋ฐ˜ ๊ณ„์‚ฐ) ๋ฏผ๊ฐ์ •๋ณด ์ œ์™ธ๋จ ๐Ÿ”’

๐ŸŽจ ๋งคํ•‘ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• - ๊ธฐ๋ณธ๋ถ€ํ„ฐ ๊ณ ๊ธ‰๊นŒ์ง€

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;
    }
}
์„ฑ๋Šฅ ๋น„๊ต: ์ผ๋ฐ˜ ๋งคํ•‘ vs ProjectTo ์ผ๋ฐ˜ ๋งคํ•‘ (๋А๋ฆผ) 1๋‹จ๊ณ„: DB์—์„œ ์ „์ฒด ๋ฐ์ดํ„ฐ ์กฐํšŒ SELECT * FROM Users (๋ชจ๋“  ์ปฌ๋Ÿผ + ๋ถˆํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ) 2๋‹จ๊ณ„: ๋ฉ”๋ชจ๋ฆฌ๋กœ ๋กœ๋“œ ToListAsync() - ํฐ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ 3๋‹จ๊ณ„: AutoMapper ๋งคํ•‘ Map ์ˆ˜ํ–‰ (์ถ”๊ฐ€ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„) โฑ๏ธ ์ด ์‹œ๊ฐ„: ์•ฝ 500ms ProjectTo ์‚ฌ์šฉ (๋น ๋ฆ„) 1๋‹จ๊ณ„: ํ•„์š”ํ•œ ์ปฌ๋Ÿผ๋งŒ ์ฟผ๋ฆฌ SELECT Id, Username, Email FROM Users (ํ•„์š”ํ•œ ์ปฌ๋Ÿผ๋งŒ ์„ ํƒ!) 2๋‹จ๊ณ„: DTO๋กœ ์ง์ ‘ ๋งคํ•‘ ProjectTo ๋‚ด๋ถ€์—์„œ ์ฟผ๋ฆฌ + ๋งคํ•‘ ๋™์‹œ ์ฒ˜๋ฆฌ (๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์ ) โฑ๏ธ ์ด ์‹œ๊ฐ„: ์•ฝ 120ms ๐Ÿš€ ์•ฝ 4๋ฐฐ ๋น ๋ฆ„! ๊ฐœ์„  * 10,000๊ฐœ ๋ ˆ์ฝ”๋“œ ๊ธฐ์ค€ ์ธก์ • ๊ฒฐ๊ณผ (์‹ค์ œ ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์Œ)

๐Ÿ›ก๏ธ ์‹ค์ „ ์˜ˆ์ œ: ์™„์ „ํ•œ 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
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.