์ง๋ ํฌ์คํ ์ ์ด์ด ์ด์ ์ค์ง์ ์ผ๋ก ์ด๋ป๊ฒ ์ฝ๋๋ฅผ ์ง ๊ฑด์ง ์ ๋ฆฌํด๋ณด๋๋ก ํ๊ฒ ๋ค.
์ฐ์ , ๊ฐ๋ฐํ๊ฒฝ์ JAVA 17, gradle, Spring 3.2.1 ์ด๋ฌํ๋ค.
1. ํ์ํ API ๋ชฉ๋ก ์ ๋ฆฌ
A. ์ ์ฒด ์นดํ ๊ณ ๋ฆฌ ๋ชฉ๋ก ์กฐํ API
์ฒซ ๋ฉ์ธ ํ๋ฉด์ ์ง์ ํ๋ฉด ๊ฐ์ฅ ๋จผ์ ์ ์ฒด ์นดํ ๊ณ ๋ฆฌ๋ฅผ ์กฐํํ๊ฒ ๋๋ค.
B. ํน์ ์นดํ ๊ณ ๋ฆฌ์ ํ์ ์นดํ ๊ณ ๋ฆฌ ๋ชฉ๋ก ์กฐํ API
๋ง์ฝ "์๋ฅ" ์นดํ ๊ณ ๋ฆฌ๋ผ๋ฉด ๊ทธ ํ์์ ์กด์ฌํ๋ ํฐ์ ์ธ , ๋งจํฌ๋งจ/ํ๋ํฐ ๋ฑ๋ฑ์ ํ์ ์นดํ ๊ณ ๋ฆฌ ๋ชฉ๋ก์ด ํ์ํ๋ค.
2. ์ ์ฒด ์นดํ ๊ณ ๋ฆฌ ๋ชฉ๋ก ์กฐํ API ๊ตฌํ
์นดํ ๊ณ ๋ฆฌ ๋ถ๋ถ ์ค์ ๊ตฌํํ ๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ์ด๋ค.
A. REQ/RES ํํ ์ ์
GET ์์ฒญ์ผ๋ก /category/total ์ด ๋ค์ด์จ๋ค๋ฉด,
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 614
{
"code" : 200,
"status" : "OK",
"message" : "OK",
"data" : [ {
"categoryId" : 1,
"parentId" : 1,
"level" : 1,
"name" : "TEST CATE1",
"childCategories" : [ {
"categoryId" : 2,
"parentId" : 1,
"level" : 2,
"name" : "TEST CATE1",
"childCategories" : [ {
"categoryId" : 3,
"parentId" : 2,
"level" : 3,
"name" : "TEST CATE1",
"childCategories" : [ ]
}, {
"categoryId" : 4,
"parentId" : 2,
"level" : 3,
"name" : "TEST CATE1",
"childCategories" : [ ]
} ]
} ]
} ]
}
์ด๋ฐ ์์ ํํ๋ก ๋ฐํํ ๊ณํ์ด๋ค.
์ ๋ฆฌํ์๋ฉด, childCategory๋ผ๋ ๊ฐ์ ๋ฆฌ์คํธ ์์ ํ์ ์นดํ ๊ณ ๋ฆฌ ์ ๋ณด๋ค์ด ์๊ณ ์ด๋ฐ ํํ์ด๋ค.
B. ์นดํ ๊ณ ๋ฆฌ ๋๋ฉ์ธ ์ํฐํฐ ์ค์
@Entity
@Getter
@NoArgsConstructor
public class Category extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "category_id")
private Long id;
@ManyToOne()
@JsonIgnore
@OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "parent_id",referencedColumnName = "category_id",insertable=false, updatable=false)
private Category parent;
@Column(name = "parent_id")
private Long parentId;
private Long level; //๋๋ถ๋ฅ 1L, ์ค๋ถ๋ฅ 2L, ์๋ถ๋ฅ 3L.
private String name;
@Builder
private Category(Long id, Long parentId, Long level, String name, Category parent) {
this.id = id;
this.parentId = parentId;
this.level = level;
this.name = name;
this.parent = parent;
}
public void setParentId(Category category){
this.parent = category;
}
}
1. ํ๋์ ์์ ์นดํ ๊ณ ๋ฆฌ๋ ์ฌ๋ฌ๊ฐ์ ํ์ ์นดํ ๊ณ ๋ฆฌ๋ฅผ ๊ฐ์ง๊ณ ์์ ์ ์๋ค.
์ํฐํฐ ๊ด๊ณ ๊ด์ ์ผ๋ก ๋ณด๋ฉด ์์ ์ํฐํฐ๋ถํฐ ์กด์ฌํ๊ณ ๊ฑฐ๊พธ๋ก ๋ถ๋ชจ ์นดํ ๊ณ ๋ฆฌ์์ ์ฐ๊ด ๊ด๊ณ๋ฅผ ์ฌ์ด์ฃผ๋ ์์๊ฐ ๋๋ค.
๋ฐ๋ผ์ ManyToOne์ผ๋ก ๊ฐ์ ํ์ ์ ๋ณ์๋ฅผ parent๋ก์ ์ ์ธํด์ค๋ค.
parentId๊ฐ ๋ฐ๋ก ๋ ์กด์ฌํ๋ ์ด์ ๋ ๋ค์ ๊ฐ์ ์ค๋ช ํ๋๋ก ํ๊ฒ ๋ค.
2. Builder ํจํด์ ํ์ฉํด์ ์ํฐํฐ ์์ฑ์ ํ๋ผ๋ฏธํฐ ์์๋ฅผ ๊ณ ๋ คํ์ง ์์๋ ๋๋๋ก ๊ตฌํ
๊ฐ์ฒด๋ฅผ ์์ฑํ ๋ ์์ฑ์๋ฅผ ํตํด ์์ฑ์ ํ๊ฒ ๋๋๋ฐ, ์ด๋ ์์ฑ ๋ฉ์๋ ํ๋ผ๋ฏธํฐ์ ์์๋ฅผ ์ง์ผ์ผํ๋ค.
๊ทผ๋ฐ builder ์ด๋ ธํ ์ด์ ์ ๋ถ์ฌ์ builder ํจํด์ ์ฌ์ฉํ๊ฒ ๋๋ฉด ์์๋ฅผ ๊ณ ๋ คํ์ง ์๊ณ , ๋ณ์ ์ด๋ฆ์ผ๋ก ๋งค์นญํ์ฌ ์์ฑํ ์ ์๋ค.
request / response ๊ณผ์ ์์ ์ฝ๋์ ๊ฐ๊ฒฐํจ๊ณผ ์ฌ์ฌ์ฉ์ฑ์ ์ํด Dto๋ฅผ ์ด์ฉํด์ ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ์๋ค.
์ด๋๋ Dto๋ฅผ ๋น๋ํจํด์ผ๋ก ์์ฑํ๊ฒ ๊ตฌํํ๋๋ฐ,
public static CategoryDto of(Category category){
return CategoryDto.builder()
.categoryId(category.getId())
.parentId(category.getParentId())
.level(category.getLevel())
.name(category.getName())
.build();
}
์ด๋ฐ์์ผ๋ก ์ฌ์ฉ๋ ์ ์๋ค.
3. BaseEntity ์์์ผ๋ก ์ฝ๋ ์ฌ์ฌ์ฉ์ฑ ์ฆ๊ฐ
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public abstract class BaseEntity {
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
private LocalDateTime deletedAt;
}
DB์ ๊ฐ ํ ์ด๋ธ๋ง๋ค ์์ฑ์ผ์, ์์ ์ผ์, ์ญ์ ์ผ์๊ฐ ๋ค์ด๊ฐ๊ฒ ๋๋๋ฐ, ์ฝ๋ ์ฌ์ฌ์ฉ์ฑ๊ณผ ์ ์ง๋ณด์๋ฅผ ์ํด BaseEntity๋ก ๋ฌถ๊ณ , ํด๋น ํด๋์ค๋ฅผ ์์๋ฐ๋๋ก ์ฒ๋ฆฌํ๋ค.
C. Dto ์ค์
@NoArgsConstructor
@Getter
public class CategoryDto {
private Long categoryId;
private Long parentId;
private Long level;
private String name;
private List<CategoryDto> childCategories;
@Builder
public CategoryDto(Long categoryId, Long parentId, Long level, String name) {
this.categoryId = categoryId;
this.parentId = parentId;
this.level = level;
this.name = name;
this.childCategories = new ArrayList<>(); // ๋น ๋ฐฐ์ด๋ก ์ด๊ธฐํ
}
public static CategoryDto of(Category category){
return CategoryDto.builder()
.categoryId(category.getId())
.parentId(category.getParentId())
.level(category.getLevel())
.name(category.getName())
.build();
}
public void addChildCategories(CategoryDto categoryDto){
childCategories.add(categoryDto);
}
public void addChildCategories(List<CategoryDto> categoryDto){
childCategories.addAll(categoryDto);
}
}
DB์ ๋งค์นญ๋๋ ๋๋ฉ์ธ ์ํฐํฐ ํ ์ด๋ธ๋ง์ผ๋ก ์ฌ์ฉํ๊ฒ ๋๋ฉด ์ฝ๋๋จ์์ ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ์ ๋๋ง๋ค ๋ณํ์ํค๋ ์์ ์ด ํ์ํ๊ฒ ๋๋ค.
1. ์ ์ง๋ณด์์ ์ฝ๋ ์ฌ์ฌ์ฉ์ฑ์ ์ํด CategoryDto ์ฌ์ฉ
- Category ์ํฐํฐ ํ ์ด๋ธ์ ์ง์ ๊ฑด๋ค์ง ์๊ณ , Dto๋ฅผ ํตํด์ ์ ๊ทผํ๊ฑฐ๋, ๋ณํํ๋๋ก ์ค๊ณํ์๋ค.
- CategoryDto์ of ๋ฉ์๋๋ฅผ ์ถ๊ฐํ์ฌ Category์ํฐํฐ์ ๊ตฌ์ฑ์์๋ฅผ ํ๋ํ๋ ์์ง ๋ชปํ๋๋ผ๋ ๊ฐ์ฒด๋ง ์ง์ด ๋ฃ์ผ๋ฉด ๋ฐ๋ก Dto๊ฐ์ฒด๋ก ๋ณํ์์ผ์ฃผ๋๋ก ์ค๊ณํ์๋ค.
2. ๋ฐํ ๋ฐ์ดํฐ ๊ท๊ฒฉ ๊ธฐ์ค์ผ๋ก Dto ๋ณ์ ์ ์ธ
๋ฐํ ๋ฐ์ดํฐ๋ฅผ ์ค์ ํ ๊ธฐ์ค๋๋ก ๋ณ์๋ฅผ ์ ์ธํ๊ณ , List<CategoryDto> ํ์ ์ผ๋ก ์ฌ๊ท์ ์ธ ๊ตฌ์กฐ๋ฅผ ๊ตฌํํ์๋ค.
D. controller ๊ตฌํ
@RestController
@RequiredArgsConstructor
@RequestMapping("/category")
public class CategoryController {
@Autowired
private final CategoryService categoryService;
//์กฐํ
@UserAuthorize
@GetMapping("/total")
public ApiResponse<List<CategoryDto>> getTotalCategories() {
return ApiResponse.ok(categoryService.getTotalCategories());
}
}
1. ์ฝ๋ ์ฌ์ฌ์ฉ์ฑ๊ณผ ์ ์ง๋ณด์์ ํธ์๋ฅผ ์ํด ApiResponse ๊ฐ์ฒด ์ฌ์ฉ
@Getter
public class ApiResponse<T> {
private int code;
private HttpStatus status;
private String message;
private T data;
@Builder
private ApiResponse(HttpStatus status, String message, T data) {
this.code = status.value();
this.status = status;
this.message = message;
this.data = data;
}
public static <T> ApiResponse<T> of(HttpStatus httpStatus, String message, T data) {
return new ApiResponse<>(httpStatus, message, data);
}
public static <T> ApiResponse<T> of(HttpStatus httpStatus, T data) {
return of(httpStatus, httpStatus.name(), data);
}
public static <T> ApiResponse<T> of(HttpStatus httpStatus) {
return of(httpStatus, null);
}
public static <T> ApiResponse<T> ok(T data) {
return of(HttpStatus.OK, data);
}
public static <T> ApiResponse<T> create() {
return of(HttpStatus.CREATED);
}
}
๊ธฐ๋ณธ์ ์ธ code, status, message, data์ ๋ณ์๋ก ๊ตฌ์ฑ๋๊ฒ ์ค์ ํ์๋ค.
๋ฐ๋ผ์ service๋จ์์ ๋์ด์จ ๋ฐ์ดํฐ ๊ฒฐ๊ณผ๋ฅผ ok ์ํ๊ฐ๊ณผ ํจ๊ป List<CategoryDto>ํ์ ์ ๊ฐ์ data ๋ณ์์์ ๋ฃ์ด์ ๋ฐํํ๋ ๊ฒ์ด๋ค.
E. CategoryService ๊ตฌํ
@Service
@Transactional(readOnly = true)
public class CategoryService {
private final CategoryRepository categoryRepository;
private final CategoryCustomRepository categoryCustomRepository;
@Builder
public CategoryService(CategoryRepository categoryRepository, CategoryCustomRepository categoryCustomRepository) {
this.categoryRepository = categoryRepository;
this.categoryCustomRepository = categoryCustomRepository;
}
public List<CategoryDto> getTotalCategories() {
List<Category> categoryList = categoryCustomRepository.findAllOrderByParentIdAscNullsFirstCategoryIdAsc();
return CategoryDto.toDtoList(categoryList);
}
}
- ์ต๋ํ Service์๋ ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง๋ง์ ๋ด๊ณ ์ถ์๋ค.
- Service ๋จ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ๊ณตํ๋ ๋ฑ(๋จ์ํ ๊ฐ์ฒด -> ๊ฐ์ฒด ๋ฆฌ์คํธ๋ก ๋ณํํ๋ ์์ )์ ํ์๋ฅผ ํ๊ฒ ๋๋ฉด ์ ์ง๋ณด์ ์ธก๋ฉด์์ ์ข์ง ์๋ค๊ณ ์๊ฐํ๊ธฐ ๋๋ฌธ์ด๋ค.
- ๋ด๊ฐ ์๊ฐํ ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง์ด๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ๋ณ๊ฒฝ๋๊ฑฐ๋ Req,Res ๊ท๊ฒฉ์ด ๋ณ๊ฒฝ๋์ด๋ ๋ณํ์ง ์๋ ๋ก์ง์ด๋ค.
์๋ฅผ ๋ค์๋ฉด ์๋ฆฌ๋ฅผ ํ๋ค๊ณ ์ณค์๋, ์๋ฆฌ์ ํ์ํ ํ์ ๊ณผ์ ๋ง์ ๋์ดํ๋ ๊ฒ์ด๋ค.
์ ์๊ฐ ๋ฐ๋๋ , ์๋ฆฌ์ฌ๊ฐ ๋ฐ๋๋ , ๋ ์คํ ๋์ด ๋ฐ๋๋ ๋ฑ์ ์ํฉ์๋ ์ด์จ๋ ์๋ฆฌ ๊ณผ์ ์ ๋ณํจ ์์ ๊ฒ์ด๋ค.
F. Repository ๊ตฌํ
1. Spring JPA๋ฅผ ์ฌ์ฉํ๋ ๋ ํฌ์งํ ๋ฆฌ, ๊ทธ ์ธ ์ปค์คํ ํด์ ์ฌ์ฉํ๋ ๋ ํฌ์งํ ๋ฆฌ๋ฅผ ๋๋์๋ค.
- ๊ฐ๊ฐ interface๋ฅผ ์์ฑํ๊ณ , ๊ทธ๊ฒ๋ค์ ๊ตฌํํ๋ impl ํด๋์ค๋ฅผ ๋ง๋ค์ด์ฃผ์๋ค.
- port ๋๋ ํ ๋ฆฌ ํ์์ ์ธํฐํ์ด์ค๋ ๋ค๋ฅธ ๋ชจ๋(์ฃผ๋ฌธ, ํ์ ๊ธฐํ ๋ฑ๋ฑ...)์์ ๊ฐ์ ธ๋ค ์ฐ๋ผ๊ณ ๊ฐ์ฒด์งํฅ์ 5์์น ์ค ๊ฐ๋ฐฉ-ํ์ ์์น์ ์ ์ฉํ์ฌ ์ค๊ณํ ๊ฒ์ด๋ค.
-> ์ด๋ฐ ์ค๊ณ๋ฅผ ํตํด ๋ค๋ฅธ ํํธ๋ฅผ ๋ด๋นํ๋ ์ฌ๋๋ค์ ๋ด๋ถ์ ์ผ๋ก ์ธ์ธํ ๋์ ๋ฐฉ๋ฒ๊น์ง ์์ง ๋ชปํด๋ ๊ฐ๋จํ๊ฒ ๊ฐ์ ธ๋ค ์ธ ์ ์๊ฒ ๋๋ค.
- Spring JPA๋ฅผ ์ฌ์ฉํ๋ ๋ ํฌ์งํ ๋ฆฌ์ธ CategoryRepositoryImpl ํด๋์ค๋ CategoryRepository๋ฅผ ๊ตฌํํ๊ณ , CategoryJpaRepository ์ธํฐํ์ด์ค๋ ํฌํจ๊ด๊ณ๋ก ๊ฐ์ง๊ณ ์๋ ํํ๋ก ๊ตฌํํ์๋ค.
@Repository
@RequiredArgsConstructor
public class CategoryRepositoryImpl implements CategoryRepository {
private final CategoryJpaRepository categoryJpaRepository;
@Override
public Category findById(Long id) {
return categoryJpaRepository.findById(id)
.orElseThrow(() -> new JpaObjectRetrievalFailureException(new EntityNotFoundException("์นดํ
๊ณ ๋ฆฌ๊ฐ ์กด์ฌํ์ง ์์ต๋๋ค.")));
}
@Override
public void save(Category category) {
categoryJpaRepository.save(category);
}
@Override
public void saveAll(List<Category> categories) {
categoryJpaRepository.saveAll(categories);
}
@Override
public Category findByName(String name) {
return categoryJpaRepository.findByName(name).orElseGet(() -> Category.builder().name("EMPTY").build());
}
@Override
public List<Category> findAllByParentId(Long parentId) {
return categoryJpaRepository.findAllByParentId(parentId).orElseGet(ArrayList::new);
}
}
- CategoryJpaRepository์ ๋ฐํ๊ฐ์ Optional๋ก ์ค์ ํด์ NPE(Null Point Error)์ ์ํฉ์ ๋ฐฉ์งํด์ฃผ์๋ค.
public interface CategoryJpaRepository extends JpaRepository<Category, Long> {
Optional<Category> findByName(String name);
Optional<Category> findById(Long categoryId);
@Query(value = "SELECT c FROM Category c WHERE c.parentId=:parentId ")
Optional<List<Category>> findAllByParentId(@Param("parentId") Long parentId);
}
์์์ parentId๊ฐ ๋ฐ๋ก ๋ณ์๋ก ์กด์ฌํ๋ ์ด์ ๊ฐ ๋ฐ๋ก ํ์ ์นดํ ๊ณ ๋ฆฌ๋ฅผ ์กฐํํ๋ JPQL์ ์ฌ์ฉํ๋ ๋ฉ์๋์์
๋ฐ๋ก ๋ณ์๋ฅผ ์ ์ธํ์ง ์์ผ๋ฉด ํ๋ผ๋ฏธํฐ๋ก ๋ฐ์ ์๊ฐ ์๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํด์ ๋ฐ๋ก ๋ณ์๋ฅผ ์ ์ธํด์ฃผ์๋ค.
์ ์ด๋ ๊ฒ ํด์ผํ๋ ์ง๋ ๋ชจ๋ฅด๊ฒ ๋ค.
2. QueryDsl์ ์ฌ์ฉํด์ ์ฌ๊ท์ฟผ๋ฆฌ๋ฅผ ๊ตฌํ
@Repository
@RequiredArgsConstructor
public class CategoryCustomRepositoryImpl implements CategoryCustomRepository {
private final JPAQueryFactory queryFactory;
@Override
public List<Category> findAllOrderByParentIdAscNullsFirstCategoryIdAsc() {
QCategory c1 = new QCategory("c1");
QCategory c2 = new QCategory("c2");
List<Category> result = queryFactory
.select(Projections.fields(Category.class,
c1.id,
c2.as("parent"),
c1.level,
c1.name
)
)
.from(c1)
.leftJoin(c2).on(c2.eq(c1.parent),c1.id.ne(c1.parent.id))
.orderBy(c2.id.asc(),c1.id.asc())
.fetch();
if (result == null){
return new ArrayList<>();
}
return result;
}
}
- ์ฅ์ 1 : ๋ฌธ์๊ฐ ์๋ ์ฝ๋๋ฅผ ์์ฑํ๋ฏ๋ก ์ปดํ์ผ ์์ ์ ๋ฌธ๋ฒ ์ค๋ฅ๋ฅผ ์ฝ๊ฒ ํ์ธ ๊ฐ๋ฅํ๋ค
- ์ฅ์ 2 : IDE์ ๋์์ ๋ฐ์์ ์ฝ๋ ์์ฑ์ ์๋ ์์ฑ์ด ๋๋ ๋ฑ์ ํธ๋ฆฌํจ์ด ์๋ค.
- ์ฅ์ 3 : ๋์ ์ธ ์ฟผ๋ฆฌ ์์ฑ์ด ํธํ๋ค.
๋ฐ๋ก Category ๊ฐ์ฒด ๋ฆฌ์คํธ๋ก ๋ฐํํ๋๋ก ๊ตฌํํ์๊ณ , ๊ฒฐ๊ณผ๊ฐ null์ผ ๋๋ ๋น ๋ฐฐ์ด์ ๋ฐํํจ์ผ๋ก์จ NPE๋ฅผ ๋ฐฉ์งํ๋ค.
G. ๊ตฌํ ๋ง๋ฌด๋ฆฌ (์์ฒด helper ๊ตฌํ)
public List<CategoryDto> getTotalCategories() {
List<Category> categoryList = categoryCustomRepository.findAllOrderByParentIdAscNullsFirstCategoryIdAsc();
return CategoryDto.toDtoList(categoryList);
}
๋ค์ ์๋น์ค ์ฝ๋๋ฅผ ๋ณด๋ฉด, Dto๊ฐ์ฒด์ toDtoList ๋ฉ์๋๋ฅผ ํตํด Category ๊ฐ์ฒด ๋ฆฌ์คํธ๊ฐ ๋ณํ๋์ด ๋ฐํ๋๊ณ ์๋ค.
public static List<CategoryDto> toDtoList(List<Category> categories){
NestedConvertHelper helper = NestedConvertHelper.newInstance(
categories,
c -> CategoryDto.of(c),
c -> c.getParent(),
c -> c.getId(),
d -> d.getChildCategories()
);
return helper.convert();
}
์ด ์ฝ๋๋ฅผ Dto ํด๋์ค์ ์ถ๊ฐ ํด์ฃผ์๋ค.
1. ์์ฒด helper
- ํ์ฌ ๋จ์์๋ ์์ ๊ฐ Category ๊ฐ์ฒด ๋ฆฌ์คํธ <-> CategoryDto ๊ฐ์ฒด ๋ฆฌ์คํธ์ ํ๋ณํ์ด๋ค.
- QueryDsl์ ํตํด ๋ฐํ๋ Category๊ฐ์ฒด ๋ฆฌ์คํธ๋ ์ฐ๊ด๊ด๊ณ๊ฐ ๋ค ๋งตํ๋์ด์๋ ์ํ์ด๋ค.
- ์ฐ๊ด๊ด๊ณ๊ฐ ๋งตํ๋ ์ํ ๊ทธ๋๋ก CategoryDto๊ฐ์ฒด์ childCategories์ ๋ฃ์ด์ฃผ๋ ์์ ์ด ํ์ํ๋ค.
public class NestedConvertHelper<K, E, D> {
private List<E> entities;
private Function<E, D> toDto;
private Function<E, E> getParent;
private Function<E, K> getKey; // id ๊ฐ
private Function<D, List<D>> getChildren;
public static <K, E, D> NestedConvertHelper newInstance(List<E> entities, Function<E, D> toDto, Function<E, E> getParent, Function<E, K> getKey, Function<D, List<D>> getChildren) {
return new NestedConvertHelper<K, E, D>(entities, toDto, getParent, getKey, getChildren);
}
private NestedConvertHelper(List<E> entities, Function<E, D> toDto, Function<E, E> getParent, Function<E, K> getKey, Function<D, List<D>> getChildren) {
this.entities = entities;
this.toDto = toDto;
this.getParent = getParent;
this.getKey = getKey;
this.getChildren = getChildren;
}
public List<D> convert() {
try {
return convertInternal();
} catch (NullPointerException e) {
throw new CannotConvertNestedStructureException(e.getMessage());
}
}
private List<D> convertInternal() {
Map<K, D> map = new HashMap<>();
List<D> roots = new ArrayList<>();
for (E e : entities) {
// entity Dto ๋ณํ
D dto = toDto(e);
map.put(getKey(e), dto);
if (hasParent(e)) {
E parent = getParent(e);
K parentKey = getKey(parent);
D parentDto = map.get(parentKey);
// ๋ถ๋ชจ DTO์ children ๊ฐ์ ์ง๊ธ dto ์ถ๊ฐํด์ค
getChildren(parentDto).add(dto);
} else {
// ๋ถ๋ชจ๊ฐ ์์ผ๋ฉด root ์ํฐํฐ
roots.add(dto);
}
}
return roots;
}
private boolean hasParent(E e) {
// ๋ถ๋ชจ๊ฐ null์ด ์๋๊ฑฐ๋ null์ด ์๋ ์กฐ๊ฑด ์ค์ ๋ถ๋ชจ id๋ ์์ ์ id๊ฐ ๋ค๋ฅด๋ฉด ๋ถ๋ชจ๊ฐ ์๋๊ฑฐ์
AtomicBoolean b = new AtomicBoolean(false);
if (getParent(e) != null && (getKey(getParent(e)) != getKey(e))) b.set(true);
else {
b.set(false);
}
return b.get();
}
private E getParent(E e) {
return getParent.apply(e);
}
private D toDto(E e) {
return toDto.apply(e);
}
private K getKey(E e) {
return getKey.apply(e);
}
private List<D> getChildren(D d) {
return getChildren.apply(d);
}
}
์ฝ๊ฒ ๋งํด, ๊ณ์ธตํ ๊ตฌ์กฐ๋ก ๋ฐ๊ฟ์ ๋ฐํํด์ฃผ๋ helper์ด๋ค.
๋ฐ๋ก ํด๋์ค๋ฅผ ๋นผ๋์ด ์ฌ์ฌ์ฉ์ฑ์ ๋์ด๊ณ , ์ ์ง๋ณด์์ ํธ์์ฑ์ ๋์๋ค.
3. ํน์ ์นดํ ๊ณ ๋ฆฌ์ ํ์ ์นดํ ๊ณ ๋ฆฌ ๋ชฉ๋ก ์กฐํ API
-> ๋ถ๋ชจ ์นดํ ๊ณ ๋ฆฌ idํ๋๋ฅผ ๊ฐ์ง๊ณ ํ์ ์นดํ ๊ณ ๋ฆฌ๋ฅผ ์กฐํํ๋ API๋ ์์ฝ ์ค๋ช ์ผ๋ก ๋ง๋ฌด๋ฆฌ ํ๊ฒ ๋ค.
1. ํ๋ผ๋ฏธํฐ๋ก ๋ถ๋ชจ ์นดํ ๊ณ ๋ฆฌ id๋ฅผ ๋ฐ์์ ํด๋น ์์ด๋์ ๋ ๋ฒจ ์ ๋ณด๋ฅผ ๋ฐ์์จ๋ค
2. 3๋ ๋ฒจ์ด๋ผ๋ฉด ๊ทธ๋ฅ Dto๋ก ๋ณํํด์ ๋ฐํํ๋ค.
3. 3๋ ๋ฒจ์ด ์๋๋ผ๋ฉด JPA ๋ ํฌ์งํ ๋ฆฌ๋ฅผ ํ์ฉํ์ฌ findAllByParentId๋ก ์กฐํํด์ DtoList๋ก ๋ณํํ์ฌ ๋ฐํํ๋ค.