AutoMapper 사용법: 객체 간 매핑 자동화의 신세계
AutoMapper는 C# 개발자들이 객체 간 데이터를 손쉽게 변환할 수 있도록 도와주는 강력한 라이브러리입니다. 이 글에서는 AutoMapper의 기본 개념부터 고급 기능, 그리고 최신 개발 환경에서의 DI(의존성 주입) 처리까지 다양한 예제를 통해 자세하게 알아보겠습니다.
참고: 최근 AutoMapper.Extensions.Microsoft.DependencyInjection 패키지는 더 이상 활발하게 유지·관리되지 않고 있습니다. 따라서 본 글에서는 순수 AutoMapper를 이용한 DI 등록 방법과 함께, 필요한 경우 Mapster와 같은 대체 라이브러리도 소개합니다.
1. AutoMapper란 무엇인가?
AutoMapper는 서로 다른 클래스 간에 이름과 타입이 일치하는 프로퍼티들을 자동으로 매핑해 주어, 수동으로 일일이 복사 코드를 작성하는 번거로움을 덜어줍니다. 예를 들어, 데이터베이스 엔티티와 DTO(Data Transfer Object) 또는 ViewModel 간의 변환을 쉽게 구현할 수 있습니다.
저는 개인적으로 AutoMapper를 사용하면서, "반복되는 단순 변환 코드를 줄여준다"는 점에서 매우 큰 생산성 향상을 경험했습니다. 물론, 모든 상황에 AutoMapper를 무조건 사용해야 하는 건 아니지만, 특히 복잡한 계층 구조의 데이터 변환이 필요한 경우에 매우 유용하다고 생각합니다.
2. AutoMapper 설치 및 기본 설정
2.1. NuGet 패키지 설치
Visual Studio의 NuGet 패키지 매니저, 패키지 관리자 콘솔 또는 .NET CLI를 통해 설치할 수 있습니다.
- Visual Studio NuGet 매니저:
프로젝트를 우클릭 → NuGet 패키지 관리 → AutoMapper 검색 후 설치
- 패키지 관리자 콘솔:
- Install-Package AutoMapper
- .NET CLI:
- dotnet add package AutoMapper
2.2. 기본 매핑 프로파일 작성
먼저, 객체 간 매핑 규칙을 한 곳에서 관리할 수 있도록 프로파일(Profile) 클래스를 작성합니다.
예시로, 아래와 같이 UserEntity와 UserDto 간의 매핑을 설정합니다.
// 도메인 엔티티
public class UserEntity
{
public int Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
public DateTime BirthDate { get; set; }
}
// DTO
public class UserDto
{
public int Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
// BirthDate는 없고 대신 Age 계산
public int Age { get; set; }
}
// 매핑 프로파일
using AutoMapper;
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<UserEntity, UserDto>()
.ForMember(dest => dest.Age,
opt => opt.MapFrom(src => DateTime.Now.Year - src.BirthDate.Year))
.ReverseMap();
}
}
이렇게 작성하면, UserEntity 객체를 UserDto로 변환할 때 자동으로 Age가 계산되어 매핑됩니다. 이처럼 매핑 규칙을 한 곳에 모아두면 유지보수와 코드 관리가 훨씬 용이해집니다.
3. AutoMapper 기본 사용법
3.1. 단일 객체 매핑
작성한 프로파일을 사용하여 실제 매핑을 수행하는 방법은 아래와 같습니다.
// MapperConfiguration 생성 및 프로파일 등록
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile<MappingProfile>();
});
// IMapper 인스턴스 생성
IMapper mapper = config.CreateMapper();
// UserEntity 객체 생성
var user = new UserEntity
{
Id = 1,
Username = "honggildong",
Email = "[email protected]",
BirthDate = new DateTime(1990, 1, 1)
};
// UserDto로 매핑
UserDto userDto = mapper.Map<UserDto>(user);
Console.WriteLine($"Id: {userDto.Id}, Username: {userDto.Username}, Email: {userDto.Email}, Age: {userDto.Age}");
실행 결과는 다음과 같이 나타납니다:
Id: 1, Username: honggildong, Email: [email protected], Age: 35
Tip: ReverseMap()을 사용하면 양방향 매핑도 자동으로 구성되어, DTO에서 엔티티로 다시 변환할 때도 매우 유용합니다.
4. ASP.NET Core에서 AutoMapper와 DI 연계하기
4.1. AutoMapper.Extensions.Microsoft.DependencyInjection 없이 등록하기
최근 해당 확장 패키지가 유지보수 중단되었으므로, 순수 AutoMapper를 이용한 DI 등록 방법을 사용하는 것이 좋습니다.
using AutoMapper;
using Microsoft.Extensions.DependencyInjection;
using YourProject.Mappings; // MappingProfile이 정의된 네임스페이스
// Program.cs 혹은 Startup.cs 내에서
var mappingConfig = new MapperConfiguration(cfg =>
{
cfg.AddProfile<MappingProfile>();
});
IMapper mapper = mappingConfig.CreateMapper();
services.AddSingleton(mapper); // DI 컨테이너에 등록
이렇게 등록하면, 컨트롤러나 서비스에서 생성자 주입을 통해 IMapper 인스턴스를 받아 사용할 수 있습니다. 제 개인적인 경험상 DI와 AutoMapper를 잘 연계하면, 코드의 의존성을 효과적으로 관리할 수 있어 전체 아키텍처가 훨씬 깔끔해집니다.
5. AutoMapper 고급 기능
AutoMapper는 기본 사용법 외에도 매우 다양한 고급 기능을 제공합니다. 아래에서는 몇 가지 대표적인 예시와 제 의견을 덧붙여 보겠습니다.
5.1. 커스텀 값 변환기 (Custom Value Converter)
특정 속성의 변환 로직이 필요할 경우, IValueConverter를 사용하여 변환기를 구현할 수 있습니다.
public class DateTimeToStringConverter : IValueConverter<DateTime, string>
{
public string Convert(DateTime sourceMember, ResolutionContext context)
{
return sourceMember.ToString("yyyy-MM-dd");
}
}
// 매핑 프로파일 내에서 사용 예시
CreateMap<UserEntity, UserDto>()
.ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email)) // 기본 매핑
.ForMember(dest => dest.Age, opt => opt.ConvertUsing(new DateTimeToStringConverter(), src => src.BirthDate));
주의: 변환 결과 타입과 대상 속성 타입이 일치해야 합니다. 이 기능은 날짜나 숫자 등의 포맷 변경 시 매우 유용합니다.
5.2. 커스텀 값 리졸버 (Custom Value Resolver)
복잡한 로직을 통한 값 변환이 필요할 때는 IValueResolver를 구현할 수 있습니다. 예를 들어, 두 개의 속성을 합쳐서 새로운 값을 생성하는 경우입니다.
public class FullNameResolver : IValueResolver<PersonEntity, PersonDto, string>
{
public string Resolve(PersonEntity source, PersonDto destination, string destMember, ResolutionContext context)
{
return $"{source.FirstName} {source.LastName}";
}
}
public class PersonEntity
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class PersonDto
{
public string FullName { get; set; }
}
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<PersonEntity, PersonDto>()
.ForMember(dest => dest.FullName, opt => opt.MapFrom<FullNameResolver>());
}
}
이 방식은 나중에 비즈니스 로직이 복잡해졌을 때도 유지보수가 용이하도록 해줍니다.
5.3. 조건부 매핑
특정 조건에 따라 매핑할 속성을 제어할 수 있습니다. 예를 들어, 이메일 값이 비어있지 않을 때만 매핑하도록 할 수 있습니다.
CreateMap<UserEntity, UserDto>()
.ForMember(dest => dest.Email,
opt => opt.Condition(src => !string.IsNullOrEmpty(src.Email)));
이와 같은 조건부 매핑은 데이터 정합성을 확보하는 데 도움이 됩니다.
5.4. 컬렉션 매핑과 ProjectTo
Entity Framework Core와 함께 사용할 때, ProjectTo<T>()를 이용하면 IQueryable 단계에서 필요한 데이터만 선별하여 조회할 수 있습니다.
// Repository 내에서
public IQueryable<UserDto> GetUserDtos()
{
return _context.Users.ProjectTo<UserDto>(_mapper.ConfigurationProvider);
}
이 방식은 데이터베이스 조회 성능을 최적화하는 데 매우 유용합니다.
6. 시니어 개발자를 위한 고급 사용 사례
6.1. 상속 관계 매핑
객체 지향 프로그래밍에서 상속 관계에 있는 클래스 간의 매핑도 AutoMapper는 손쉽게 처리할 수 있습니다.
public class Animal { public string Name { get; set; } }
public class Dog : Animal { public string Breed { get; set; } }
public class AnimalDto { public string Name { get; set; } }
public class DogDto : AnimalDto { public string Breed { get; set; } }
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<Animal, AnimalDto>()
.Include<Dog, DogDto>();
CreateMap<Dog, DogDto>();
}
}
이와 같이 설정하면 상속 관계에 있는 모든 클래스 간의 매핑이 자동으로 처리됩니다.
6.2. AfterMap 액션
매핑 후 추가 처리가 필요할 때 AfterMap() 메서드를 활용하면 좋습니다. 예를 들어, 이메일을 소문자로 변환하는 로직을 추가할 수 있습니다.
CreateMap<UserEntity, UserDto>()
.AfterMap((src, dest) => {
dest.Email = dest.Email?.ToLower();
});
이 방식은 매핑 완료 후 데이터 후처리가 필요한 경우에 매우 유용합니다.
7. 결론 및 최신 개발 환경에서의 활용 방안
최신 AutoMapper 버전은 단순한 객체 매핑을 넘어 복잡한 데이터 변환, 조건부 로직 처리, 상속 및 컬렉션 매핑 등 다양한 기능을 제공합니다. 또한, AutoMapper.Extensions.Microsoft.DependencyInjection 패키지가 중단된 현재 상황에서는 순수 AutoMapper를 이용한 DI 등록 방법을 활용하는 것이 좋으며, 성능 최적화와 코드 간결화를 동시에 이룰 수 있습니다.
핵심 정리:
- 기본 사용법: CreateMap<TSource, TDestination>()와 ReverseMap()을 통해 간편하게 매핑 규칙을 작성합니다.
- DI 연계: 최신 환경에서는 MapperConfiguration을 직접 생성하고 IMapper를 Singleton으로 DI 컨테이너에 등록합니다.
- 고급 기능: 커스텀 값 변환기, 조건부 매핑, 상속 및 컬렉션 매핑 등 다양한 고급 기능을 활용해 복잡한 로직도 깔끔하게 처리할 수 있습니다.
- EF Core와 ProjectTo: 데이터베이스 조회 최적화를 위해 ProjectTo()를 사용해 필요한 데이터만 선별 조회할 수 있습니다.
제 개인적인 의견으로는, AutoMapper를 도입할 때 가장 중요한 것은 팀 내에서 매핑 규칙을 일관되게 관리하는 것입니다. 초기 설정은 다소 복잡해 보일 수 있지만, 한 번 잘 구성해두면 이후 유지보수가 매우 편리해집니다. 또한, 복잡한 매핑 로직이 필요한 경우 커스텀 리졸버나 조건부 매핑을 적절히 활용하여 코드의 가독성을 높이는 것이 좋습니다.
마지막으로, 만약 AutoMapper가 제공하는 기능보다 더 빠른 성능이나 컴파일 타임 검증이 필요하다면, Mapster와 같은 대체 라이브러리도 고려해볼 만합니다. 하지만 대부분의 일반적인 개발 환경에서는 AutoMapper가 충분히 강력한 도구임을 다시 한 번 강조드립니다.
AutoMapper를 통해 여러분의 코드베이스를 더욱 깔끔하게 유지하고, 반복되는 변환 코드를 줄여 생산성을 극대화해 보세요. 최신 버전의 AutoMapper와 이 글의 예제들이 여러분의 프로젝트에 큰 도움이 되기를 바랍니다.
참고 자료: