用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/puk0806/gugbab-claude --skill lombok-mapstruct-modelmapper命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
DDD(Domain-Driven Design) 아키텍처 핵심 패턴 - 유비쿼터스 언어, 서브도메인, 바운디드 컨텍스트, Aggregate, Entity/VO, 도메인 서비스/이벤트, 레이어드 아키텍처
대규모 React/Next.js 프로젝트를 layer-first(types/·utils/·hooks/·api/·components/ 밑에 도메인이 반복되는 구조)에서 domain-first(feature/도메인 우선) 구조로 전환하는 설계 기준과 절차. Feature-Sliced Design 2.1 정본(layers 6종·slices·segments·import 규칙·@x 크로스임포트·public API), FSD를 쓰지 않는 경량 대안(features + shared 2~3계층 + ESLint import/no-restricted-paths), Next.js App Router 공존 전략(route group `()`·private folder `_`·colocation), Turborepo/Nx 모노레포에서 폴더↔패키지 승격 기준, colocation과 배럴 파일 성능 트레이드오프, 도메인 경계 역추출(import 그래프·change coupling·용어 클러스터), 전환 실패 패턴(shared 비대화·entities 남용·순환 의존·도메인=라우트 착각·조기 추상화). 도메인 개념 자체(바운디드 컨텍스트·유비쿼터스 언어)는 `architecture/ddd` 스킬을 참조한다.
소스 파일 수천 개 규모 프론트엔드 코드베이스를 멈추지 않고 점진 재구조화하는 실행 전략 - Strangler Fig / Branch by Abstraction / Parallel Change, ts-morph·jscodeshift codemod, PR 분할·검증 게이트·되돌리기, 테스트 없는 코드의 안전망, 작업 순서 설계와 위반 수 기반 진행 추적
基于 SOC 职业分类
正在显示 SKILL.md
| name | lombok-mapstruct-modelmapper |
| description | Lombok + MapStruct + ModelMapper 통합 가이드 - Spring Boot에서 DTO 변환과 보일러플레이트 제거 패턴 |
소스: https://projectlombok.org/features/ | https://mapstruct.org/documentation/stable/reference/html/ | https://modelmapper.org/getting-started/ 검증일: 2026-04-22
주의: 본 문서는 Lombok 1.18.44, MapStruct 1.6.3, ModelMapper 3.2.x 기준. Spring Boot 2.5+ / 3.x 양쪽에서 동일하게 적용 가능하나, Spring Boot 3.x는 Java 17 이상이 필요하다.
| 상황 | 선택 |
|---|---|
| 컴파일타임 안전성·대용량 트래픽·성능 우선 | MapStruct |
| 빠른 프로토타입·단순 1:1 매핑·러닝커브 최소화 | ModelMapper |
| 보일러플레이트(getter/setter/builder) 제거 | Lombok (DTO·설정 빈) |
| 불변 VO / Record | Java Record 또는 Lombok @Value |
| JPA 엔티티 | Lombok은 제한적 사용 (@Data 금지, @EqualsAndHashCode 주의) |
팀 내에서는 MapStruct / ModelMapper 중 하나를 주력으로 통일한다. 두 라이브러리를 혼재시키면 동일한 DTO가 서로 다른 방식으로 매핑되어 유지보수가 어려워진다.
| 어노테이션 | 생성되는 것 |
|---|---|
@Getter / @Setter | 모든 필드의 getter/setter |
@NoArgsConstructor | 인자 없는 생성자 |
@AllArgsConstructor | 모든 필드 인자 생성자 |
@RequiredArgsConstructor | final 또는 @NonNull 필드만 인자로 받는 생성자 |
@Builder | Builder 패턴 (내부 빌더 클래스 + 정적 builder() 메서드) |
@ToString | toString() |
@EqualsAndHashCode | equals() / hashCode() |
@Data | @Getter + @Setter + @ToString + @EqualsAndHashCode + @RequiredArgsConstructor |
@Value | 불변 버전 @Data — 모든 필드 private final, 클래스 final, setter 없음 |
@Slf4j | SLF4J Logger log 필드 자동 생성 |
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserDto {
private Long id;
private String username;
private String email;
}
// 사용
UserDto dto = UserDto.builder()
.id(1L)
.username("alice")
.email("a@example.com")
.build();
@Valueimport lombok.Value;
import lombok.Builder;
@Value
@Builder
public class UserDto {
Long id; // 자동으로 private final
String username;
String email;
}
// 클래스가 final이 되고 setter가 없으므로 값 객체로 안전
@Value는 다음을 합친 것과 같다:final @ToString @EqualsAndHashCode @AllArgsConstructor @FieldDefaults(makeFinal=true, level=PRIVATE) @Getter
@RequiredArgsConstructorimport lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final UserMapper userMapper;
// Lombok이 모든 final 필드를 인자로 받는 생성자 자동 생성
// Spring이 이 생성자로 주입 수행
}
@Slf4jimport lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class OrderService {
public void process(Long orderId) {
log.info("Processing order {}", orderId);
}
}
1. @Data를 JPA 엔티티에 사용 금지
@Data는 @Setter를 포함 → 불변성이 중요한 엔티티·VO에 부적합@Data가 생성하는 equals()/hashCode()는 모든 필드를 사용 → JPA 엔티티에서 다음 문제 발생:
HashSet 내 엔티티 동일성 깨짐equals 재귀 호출@Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) 권장)2. @EqualsAndHashCode(callSuper=...) 명시
@EqualsAndHashCode를 쓰면 Lombok은 기본값(skip)으로 동작하며 경고를 출력한다.@EqualsAndHashCode(callSuper = true)로 명시한다.@Getter
@EqualsAndHashCode(callSuper = true) // 또는 false 명시
public class AdminUser extends User { ... }
3. @SneakyThrows 사용 금지
Gradle (Spring Boot 2.5 / 3.x 공통)
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.44'
annotationProcessor 'org.projectlombok:lombok:1.18.44'
// 테스트 코드에도 필요할 때
testCompileOnly 'org.projectlombok:lombok:1.18.44'
testAnnotationProcessor 'org.projectlombok:lombok:1.18.44'
}
compileOnly + annotationProcessor 양쪽 모두 필수.lombok.jar를 IDE 설치 디렉터리에 주입해야 한다 (java -jar lombok.jar).컴파일타임 코드 생성 방식. annotation processor가 인터페이스 구현체를 target/generated-sources에 생성한다.
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
public interface UserMapper {
@Mapping(source = "username", target = "name")
UserDto toDto(User user);
@Mapping(source = "name", target = "username")
User toEntity(UserDto dto);
List<UserDto> toDtoList(List<User> users); // 컬렉션은 선언만 해도 자동 변환
}
componentModel = "spring" → Spring Bean으로 등록되어 @Autowired / 생성자 주입 가능.@Mapping 없이 자동 매핑.@Mapper(uses = OtherMapper.class)로 위임 가능.@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final UserMapper userMapper;
public UserDto findById(Long id) {
return userMapper.toDto(
userRepository.findById(id).orElseThrow()
);
}
}
@Mapper(componentModel = "spring")
public interface OrderMapper {
@Mapping(source = "customer.name", target = "customerName")
@Mapping(source = "customer.address.city", target = "customerCity")
OrderDto toDto(Order order);
}
@MappingTarget기존 엔티티 필드를 DTO 값으로 덮어쓸 때 사용. 반환 타입은 void 또는 target 타입.
@Mapping(target = "id", ignore = true) // ID는 변경 금지
@Mapping(target = "createdAt", ignore = true)
void updateEntity(UserUpdateDto dto, @MappingTarget User user);
User user = userRepository.findById(id).orElseThrow();
userMapper.updateEntity(dto, user); // user 필드가 in-place로 수정됨
qualifiedByName — 이름 기반 커스텀 변환@Mapper(componentModel = "spring")
public interface UserMapper {
@Mapping(source = "patients", target = "numPatients", qualifiedByName = "countPatients")
DoctorDto toDto(Doctor doctor);
@Named("countPatients")
default int countPatients(List<Patient> patients) {
return patients == null ? 0 : patients.size();
}
}
expression — 인라인 Java 코드 (주의해서 사용)@Mapping(target = "fullName",
expression = "java(user.getFirstName() + \" \" + user.getLastName())")
UserDto toDto(User user);
expression과qualifiedByName은 동시 사용 불가. 복잡한 로직은qualifiedByName쪽이 리팩토링·테스트에 유리하다.
@AfterMapping — 후처리 훅@Mapper(componentModel = "spring")
public abstract class UserMapper {
public abstract UserDto toDto(User user);
@AfterMapping
protected void afterToDto(User source, @MappingTarget UserDto target) {
target.setDisplayName(source.getFirstName() + " " + source.getLastName());
}
}
@BeforeMapping도 동일한 방식으로 쓸 수 있다. @AfterMapping은 mapping 메서드의 마지막 문장으로 호출된다.
public record UserDto(Long id, String name, int age) {}
@Mapper(componentModel = "spring")
public interface UserMapper {
UserDto toDto(User user); // Record 대상 매핑은 생성자를 통해 이루어짐
User toEntity(UserDto dto);
}
주의: Record의 접근자는
name()형태 (JavaBeangetName()아님). MapStruct 1.5+는 이를 자동 인식한다. Record는 불변이므로@MappingTarget업데이트 매핑의 target으로는 사용할 수 없다.
Gradle (Groovy DSL — build.gradle)
ext {
lombokVersion = '1.18.44'
mapstructVersion = '1.6.3'
lombokMapstructBinding = '0.2.0'
}
dependencies {
implementation "org.mapstruct:mapstruct:${mapstructVersion}"
compileOnly "org.projectlombok:lombok:${lombokVersion}"
// 순서가 매우 중요: mapstruct → lombok → binding
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
annotationProcessor "org.projectlombok:lombok-mapstruct-binding:${lombokMapstructBinding}"
}
Gradle (Kotlin DSL — build.gradle.kts)
val lombokVersion = "1.18.44"
val mapstructVersion = "1.6.3"
val lombokMapstructBinding = "0.2.0"
dependencies {
implementation("org.mapstruct:mapstruct:$mapstructVersion")
compileOnly("org.projectlombok:lombok:$lombokVersion")
// 순서가 매우 중요: mapstruct → lombok → binding
annotationProcessor("org.mapstruct:mapstruct-processor:$mapstructVersion")
annotationProcessor("org.projectlombok:lombok:$lombokVersion")
annotationProcessor("org.projectlombok:lombok-mapstruct-binding:$lombokMapstructBinding")
}
lombok-mapstruct-binding이 필요한 이유
Unmapped target properties: ... 에러 발생.lombok-mapstruct-binding은 MapStruct가 Lombok 처리 완료를 기다리게 만든다.Maven
<properties>
<lombok.version>1.18.44</lombok.version>
<mapstruct.version>1.6.3</mapstruct.version>
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<>
org.apache.maven.plugins
maven-compiler-plugin
org.mapstruct
mapstruct-processor
${mapstruct.version}
org.projectlombok
lombok
${lombok.version}
org.projectlombok
lombok-mapstruct-binding
${lombok-mapstruct-binding.version}
상세 레퍼런스 (예제·고급 패턴·흔한 실수) →
references/REFERENCE.md