MapStruct 는 Java Bean Mapper 라이브러리다.
DTO ↔ Entity 같은 객체 간 변환 코드를 자동으로 생성해주는 도구이다. 구현하다 보면 매핑할 일이 많은데 간단하게 구현할 수 있게 도와준다.
userDto.setId(userEntity.getId());
userDto.setName(userEntity.getName());
userDto.setEmail(userEntity.getEmail());
필드가 많아지면 매번 이렇게 매핑 코드를 쓰는 게 엄청 귀찮고 실수도 많이 나온다. MapStruct 는 이런 매핑을 자동으로 만들어 주는 도움을 준다.
자동 코드 생성
반복되는 setter/getter 매핑 코드를 줄일 수 있다.
우리가 직접 안 써도 되고 MapStruct 가 빌드 타임에 컴파일러가 이해할 수 있는 Java 코드로 변환 코드를 만들어 준다.
컴파일 타임 체크
매핑할 필드명이 잘못되면 컴파일 시점에 에러를 내준다.
(ModelMapper 같은 라이브러리는 런타임에 매핑 실패를 알 수 있는데 MapStruct 는 빌드할 때 잡아줘서 더 안전하다.
커스텀 매핑 가능
매핑 시키는 엔티티들은 대부분 필드명이 같은데 필드명이 다른 경우는 @Mapping 을 써서 직접 선언해 줄 수도 있다.
@Mapping(target = "status", qualifiedByName = "status")
@Mapping(target = "remainingCount", source = "bulkPassEntity.count")
@Named("status")
default PassStatus status(BulkPassStatus status) {
return PassStatus.READY;
}

public interface PassModelMapper {
PassModelMapper INSTANCE = Mappers.getMapper(PassModelMapper.class);
}
interface PassModelMapper 만 작성해서 사용하고 있지만 실제적으로는 컴파일 시점에 MapStruct 가 내부에서 자동으로 구현 클래스를 만들어주고 있다. 이름은 보통 PassModelMapperImpl 로 생성된다. 안에 toPassEntity() 같은 메서드의 실제 변환 로직이 들어가고 Mappers.getMapper(PassModelMapper.class)는 그 자동으로 생성된 구현체를 찾아서 리턴해준다.
INSTANCE 는 사실 PassModelMapperImpl 객체를 가지고 있다. 우리가 따로 new PassModelMapperImpl() 해서 만들 필요 없이 이 한 줄로 PassModelMapper 를 그냥 쓸 수 있고 toPassEntity(어쩌구저쩌구) 구현체에 있는 변환 로직을 바로 사용하는게 가능한거다.
PassEntity passEntity = PassModelMapper.INSTANCE.toPassEntity(bulkPassEntity, userId);
필드명이 같으면 자동이지만 필드명이 다르면 @Mapping 으로 “어디 → 어디” 인지 명시적으로 알려줘야 한다.
source: 원본 객체 필드
target: 변환될 객체 필드
// Entity
public class PackageEntity {
private Long id;
private String packageName;
private int totalCount;
}
// DTO
public class Package {
private Long id;
private String name;
private int count;
}
요렇게!
@Mapper
public interface PackageMapper {
@Mapping(source = "packageName", target = "name")
@Mapping(source = "totalCount", target = "count")
Package map(PackageEntity entity);
}
점(.) 표기법 기억하면 됨
// Entity
class PackageEntity {
private Long id;
private CategoryEntity category;
}
class CategoryEntity {
private String code;
}
// DTO
class Package {
private Long id;
private String categoryCode;
}
요렇게!
@Mapper
public interface PackageMapper {
@Mapping(source = "category.code", target = "categoryCode")
Package map(PackageEntity entity);
}
@Mapping(target = "status", constant = "ACTIVE")
항상 status = "ACTIVE"
ignore
@Mapping(target = "createdAt", ignore = true)
전역 설정
@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE)
커스텀 변환
@Mapping(
source = "status",
target = "status",
qualifiedByName = "statusToString"
)
@Named("statusToString")
default String statusToString(Status status) {
return status.getCode();
}