프로젝트 발표 피드백으로 DTO와 Entity 간의 변환 로직이 불필요하게 중복되었다는 말을 들었다. 그에 대한 대안으로 MapStruct라는 것을 소개 받았는데 어떻게 적용할 수 있는지 간단하게 알아보려고 한다.
MapStruct
MapStruct는 Java 빈 타입 간의 매핑하는 코드를 자동으로 생성해주는 코드생성기이다. MapStruct는 컴파일 시점에 Java Bean 매핑 코드를 컴파일 시점에 생성하기 때문에 높은 성능을 제공하며, 개발자에게 빠른 피드백을 제공하고 철저한 에러 검사를 수행한다.
User 엔티티
회원가입 API에서 회원 정보를 받아 User Entity로 변환하고 저장된 Entity를 가지고 Response로 변환하는 것을 MapStruct를 적용해보겠다.
@Getter
@Entity
@Table(name = "p_users")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
@AllArgsConstructor
public class User extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(length = 10, nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Column(length = 10, nullable = false)
private String name;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private UserRole role;
@Column(length = 20, nullable = false)
private String slackId;
}
MapStruct 적용 전
public record SignupRequest(
@NotBlank String username,
@NotBlank String password,
@NotBlank String name,
@NotNull UserRole role,
@NotBlank String slackId
) {
// RequestDto -> Entity 변환 메서드
public User toEntity(String encodedPassword){
return User.builder()
.username(username)
.password(encodedPassword)
.name(name)
.role(role)
.slackId(slackId)
.build();
}
}
public record SignupResponse(
UUID id
) {
// Entity -> ResponseDto 변환 메서드
public static SignupResponse from(User user){
return new SignupResponse(user.getId());
}
}
회원가입 서비스 로직
@Transactional
public SignupResponse signup(SignupRequest signupRequest) {
checkDuplicateUsername(signupRequest.username());
User savedUser = userRepository.save(signupRequest.toEntity(passwordEncoder.encode(signupRequest.password())));
return SignupResponse.from(savedUser);
}
MapStruct 적용 후
의존성 추가
dependencies {
implementation 'org.mapstruct:mapstruct:1.6.3'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3'
}
Mapper 인터페이스 및 구현체
@Mapper
public interface UserMapper {
// UserMapper 인스턴스를 생성 (MapStruct가 자동 생성한 구현체를 반환)
UserMapper USER_MAPPER = Mappers.getMapper(UserMapper.class);
// target: 반환 클래스의 필드명
// source: 매개변수로 전달된 클래스의 필드명
// target 클래스와 source 클래스 필드명이 같다면 자동으로 매핑
@Mapping(target = "id", source = "id")
SignupResponse userToSignupResponse (User user);
@Mapping(target = "password", source = "encodedPassword")
User signupRequestToEntity (SignupRequest signupRequest, String encodedPassword);
}
- 타겟 클래스와 소스 클래스의 필드명이 같다면 자동으로 매핑되지만 필드명이 다를 경우, @Mapping 어노테이션을 사용해 매핑을 지정해야 합니다.
import com.nangman.user.application.dto.request.SignupRequest;
import com.nangman.user.application.dto.response.SignupResponse;
import com.nangman.user.domain.entity.User;
import java.util.UUID;
import javax.annotation.processing.Generated;
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2024-12-20T01:01:46+0900",
comments = "version: 1.6.3, compiler: IncrementalProcessingEnvironment from gradle-language-java-8.11.1.jar, environment: Java 17.0.7 (Azul Systems, Inc.)"
)
public class UserMapperImpl implements UserMapper {
@Override
public SignupResponse userToSignupResponse(User user) {
if ( user == null ) {
return null;
}
UUID id = null;
id = user.getId();
SignupResponse signupResponse = new SignupResponse( id );
return signupResponse;
}
@Override
public User signupRequestToEntity(SignupRequest signupRequest, String encodedPassword) {
if ( signupRequest == null && encodedPassword == null ) {
return null;
}
User.UserBuilder user = User.builder();
if ( signupRequest != null ) {
user.username( signupRequest.username() );
user.name( signupRequest.name() );
user.role( signupRequest.role() );
user.slackId( signupRequest.slackId() );
}
user.password( encodedPassword );
return user.build();
}
}
- 내가 작성한 Mapper 인터페이스를 바탕으로 MapStruct가 생성한 변환 코드이다.
회원가입 서비스 로직
import static com.nangman.user.application.mapper.UserMapper.USER_MAPPER;
...
@Transactional
public SignupResponse signup(SignupRequest signupRequest) {
checkDuplicateUsername(signupRequest.username());
User savedUser = userRepository.save(USER_MAPPER.signupRequestToEntity(signupRequest, passwordEncoder.encode(signupRequest.password())));
return USER_MAPPER.userToSignupResponse(savedUser);
}
요청 DTO
public record SignupRequest(
@NotBlank String username,
@NotBlank String password,
@NotBlank String name,
@NotNull UserRole role,
@NotBlank String slackId
) {}
응답 DTO
public record SignupResponse(
UUID id
) {}
MapStruct를 사용하면 DTO와 Entity 간의 매핑 로직을 Mapper에 위임함으로써 DTO와 Entity 간의 결합도가 낮아지고, 책임이 명확히 분리되어, 코드의 가독성과 유지보수성이 향상된다
'Spring 단기심화 2기' 카테고리의 다른 글
TIL_MapStruct Mapping 오류_250102 (1) | 2025.01.03 |
---|---|
TIL_Annotation_241221 (1) | 2024.12.21 |
TIL_JVM 구조와 동작 원리_241208 (3) | 2024.12.09 |
TIL_HTTP 메서드의 멱등성_241205 (0) | 2024.12.05 |
TIL_대규모 스트림 처리에서의 데이터 일관성 유지_241205 (0) | 2024.12.05 |