entity
@Entity
@Getter
public class Menu {
@Comment("식별자")
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Comment("메뉴 이름")
@Column(name = "menu_name", nullable = false)
private String menuName;
@Comment("메뉴 순서")
@Column(name = "sort_order", nullable = false)
private int sortOrder;
@Comment("사용 여부")
@Column(name = "is_use", nullable = false)
private Boolean isUse;
@Comment("부모 메뉴")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_menu_id")
private Menu parent;
@Comment("자식 메뉴")
@OneToMany(mappedBy = "parent")
private List<Menu> children = new ArrayList<>();
}
DTO
@Getter
@Setter
public class MenuResponse {
private Long id;
private String menuName;
private int sortOrder;
private Boolean isUse;
private Long parentId;
private List<MenuResponse> children;
}
service
public List<MenuResponse> findMenuList() {
List<Menu> menuEntity = menuRepository.findAll();
return menuMapper.toDtoList(menuEntity);
}
mapper
package com.example.admin_project.menu.mapper;
import com.example.admin_project.menu.dto.MenuResponse;
import com.example.admin_project.menu.entity.Menu;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import java.util.List;
@Mapper(componentModel = "spring") // (1)
public interface MenuMapper {
@Mapping(source = "parent.id", target = "parentId") // (2)
@Mapping(target = "children", qualifiedByName = "mapChildren") // (3)
MenuResponse toDto(Menu menu);
List<MenuResponse> toDtoList(List<Menu> menuList);
// 자식 메뉴를 재귀적으로 변환
@Named("mapChildren") // (4)
default List<MenuResponse> mapChildren(List<Menu> children) {
return children != null ? toDtoList(children) : null;
}
}
| 주석 | 설명 |
| (1) | Mapper 구현체를 Spring Bean으로 등록하기 위한 설정 -> MapStruct은 컴파일 시 자동으로 MenuMapperImpl라는 구현 클래스를 생성 |
| (2) | Menu 엔티티 객체의 parent.id 값을 MenuResponse DTO의 parentId 필드로 매핑 |
| (3) | 자식 메뉴 필드를 mapChildren 메서드를 사용해 재귀적으로 매핑 -> qualifiedByName = "mapChildren"는 @Named("mapChildren")로 지정된 메서드를 사용하겠다는 의미입니다. |
| (4) | MapStruct가 사용할 수 있도록 mapChildren 메서드를 이름으로 지정 |
@Mapping(source = "children", target = "children")
MenuResponse toDto(Menu menu);
- source: 원본 객체(예: Menu 엔티티)의 필드 이름
- target: 대상 객체(예: MenuResponse DTO)의 필드 이름
toDtoList()만 쓰는데 왜 toDto()가 필요??
-> toDtoList()는 내부적으로 리스트 요소 하나하나에 toDto()를 호출하기 때문

(전체 흐름)
1. toDto(Menu) 호출
- 상위 메뉴 하나를 변환 하기 위해 toDto(Menu)가 호출됨
2. MapStruct가 children 필드에 접근시
- @Mapping(target = "children", qualifiedByName = "mapChildren") 설정 덕분에
- Menu의 children 필드는 자동 매핑하지 않고, 위에 설정에 맞는 mapChildren() 메서드를 호출
3. mapChildren() 호출
@Named("mapChildren")
default List<MenuResponse> mapChildren(List<Menu> children) {
return children != null ? toDtoList(children) : null;
}
- children이 null이 아니면 toDtoList()로 변환
4. toDtoList()가 리스트 반복
- 리스트 안의 각각의 Menu를 toDto()로 다시 변환함 (재귀 호출)
- 이 안에서도 또 children이 있다면?
- 다시 mapChildren() 호출됨
'어드민 프로젝트' 카테고리의 다른 글
| 어드민 프로젝트 로그 파일 읽어서 DB저장(스케줄러) - 3 (0) | 2025.07.09 |
|---|---|
| 어드민 프로젝트 사용자 행동 로그파일 저장(AOP) - 2 (0) | 2025.06.06 |
| 프로젝트 하면서 찾아본 내용 (0) | 2025.05.16 |