NHN Acadmey에서 인증과정 중 제가 구현한 파트에 대해 정리하고자 글을 씁니다.
인터넷 도서 사이트를 제작하는 프로젝트였고 그 중 카테고리, 주문 조회, 결제, 캐싱처리를 담당하였습니다.
첫번째로는 카테고리입니다.
카테고리는 2단으로 구현을 했습니다. 부모 카테고리 - 자식 카테고리로 구현 하였고 자기 참조 방식을 사용하였습니다.
해당 카테고리를 등록, 수정, 삭제, 순서 변경까지 가능하도록 구현하였습니다.
아래는 카테고리의 엔티티 맵핑 관련한 코드입니다.
- JPA에서 entity를 생성할때, 기본 생성자(NoArgs)를 protected 까지 허용해주기 때문에 롬복 어노테이션을 사용하여 선언하였습니다.
- 카테고리는 static한 id를 사용하기로 정책으로 정했습니다. 고로 부모 카테고리는 10000씩, 자식 카테고리는 100씩 증가하여 생성됩니다.
- soft delete (실제 삭제가 아님)를 위해 isDeleted라고 하는 필드를 사용합니다.
- 자기 참조를 위해 depth와 parent(Category)를 사용합니다.
- @ManyToOne : 부모 카테고리는 자식 카테고리를 여러개 가질 수 있습니다.
- 편의성을 위해 children이라고하는 자식 카테고리 List를 양방향으로 선언했습니다.
- @OneToMany : mappedBy를 통해 Fk주인이 아님을 명시, Lazy fetch를 통해 일단 N+1 방지
- cascade : soft delete 정책으로 인해 delete에 대한 side effect를 고려하지는 않았지만 persist, merge를 사용해야만 getChildren()을 선언시 쿼리문이 1번만 나감. (더 공부 필요)
- verfiyChange()는 JPA EntityManger의 특성 중 하나인 dirty check(변경 감지)를 사용하기 위해 선언하였습니다.
@Getter
@Builder
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Table(name = "categories")
@Entity
public class Category {
public static final int DEPTH_PARENT = 0;
public static final int DEPTH_CHILD = 1;
public static final long TERM_OF_PARENT_ID = 10000L;
public static final long TERM_OF_CHILD_ID = 100L;
@Id
private Long id;
@Column(length = 30, nullable = false)
private String name;
@Builder.Default
@Column(name = "is_shown", nullable = false)
private boolean isShown = true;
@Column(name = "`order`")
private Integer order;
@Builder.Default
@Column(name = "depth", nullable = false)
private int depth = 0;
// soft delete를 위해 사용
@Builder.Default
@Column(name = "disable", nullable = false)
private boolean isDisable = false;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Category parent;
@ToString.Exclude
@Builder.Default
@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<Category> children = new ArrayList<>();
/**
* entity의 변경감지를 위해 사용
*
* @param name
* @param isShown
* @param order
*/
public void verifyChange(
String name,
Boolean isShown,
Integer order
) {
if (Objects.nonNull(name)) {
this.name = name;
}
if (Objects.nonNull(isShown)) {
this.isShown = isShown;
}
if (Objects.nonNull(order)) {
this.order = order;
}
}
}
다음은 카테고리 생성 관련한 dto와 서비스 코드(비즈니스 코드) 입니다.
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class CategoryRequestDto {
@NotBlank
private String name;
@NotNull
private Boolean isShown;
private Integer order;
private Long parentId;
/**
* 카테고리 수정을 위해 해당 dto 를 Category 로 변환
*
* @param id 카테고리 id
* @param depth 2차 카테고리일 경우 '0', 1차 카테고리일 경우 '1'
* @param parent 2차 카테고리일 경우 사용, 1차 카테고리일 경우 null 입력
* @return
*/
public Category toEntity(Long id, int depth, Category parent) {
return Category.builder()
.id(id)
.name(this.name)
.isShown(this.isShown)
.order(this.order)
.depth(depth)
.parent(parent)
.build();
}
public void setOrder(Integer order) {
this.order = order;
}
}
- 카테고리를 생성하는 2가지의 경우에 따라 코드를 구성하였습니다.
- 부모 카테고리(=1차 카테고리)를 생성 : parentId가 null
- 자식 카테고리(=2차 카테고리)를 생성 : parentId가 notNull
- 우선 id가 자동 증가가 아니기때문에 최근에 생성된 id를 조회해 그 id에 10000 or 100을 더합니다.
- 또한 순서도 가장 마지막 순서의 카테고리 뒤 (제일 마지막 번호)에 배정합니다.
- 그리고는 Jpa에서 제공하는 save 메서드를 통해 데이터 베이스에 저장합니다.
....
@Transactional
@Override
public CategoryResponseDto create(CategoryRequestDto createRequest) {
if (Objects.nonNull(createRequest.getParentId())) {
return saveCategoryByAddingChildId(createRequest);
}
return saveCategoryByAddingId(createRequest);
}
/**
* 1차 카테고리인 경우,카테고리 생성시 id에 10000L씩 더해서 id 배정
*
* @param createRequest
* @return CategoryResponseDto
*/
private CategoryResponseDto saveCategoryByAddingId(CategoryRequestDto createRequest) {
CategoryOnlyIdDto onlyParentId = queryCategoryRepository.getLatestIdByDepth(Category.DEPTH_PARENT);
createRequest.setOrder(
queryCategoryRepository.getLatestOrderByDepth(Category.DEPTH_PARENT) + 1);
Category category = commandCategoryRepository.save(createRequest.toEntity(
onlyParentId.getId() + Category.TERM_OF_PARENT_ID, Category.DEPTH_PARENT, null));
return CategoryResponseDto.fromEntity(category);
}
/**
* 2차 카테고리 인 경우, 카테고리 생성시 id에 100L씩 더해서 id 배정
*
* @param createRequest
* @return CategoryResponseDto
*/
private CategoryResponseDto saveCategoryByAddingChildId(CategoryRequestDto createRequest) {
Long parentId = createRequest.getParentId();
CategoryOnlyIdDto onlyChildId = queryCategoryRepository.getLatestChildIdByDepthAndParentId(
Category.DEPTH_CHILD,
parentId
);
Category parentCategory = queryCategoryRepository.findById(parentId)
.orElseThrow(() -> new ClientException(
ErrorCode.CATEGORY_NOT_FOUND,
"Category not found with id : " + parentId
));
createRequest.setOrder(queryCategoryRepository.getLatestChildOrderByDepthAndParentId(
Category.DEPTH_CHILD,
parentId
) + 1);
Category category = commandCategoryRepository.save(createRequest.toEntity(
onlyChildId.getId() + Category.TERM_OF_CHILD_ID,
Category.DEPTH_CHILD,
parentCategory
));
return CategoryResponseDto.fromEntity(category);
}
다음은 컨트롤러 코드입니다.
- Rest API는 버전을 명시하고 계층구조를 지키기 위해 '/v1/categories'를 사용하였습니다.
- 생성을 위해 POST를 선정하였고 응답은 201 created를 사용하였습니다.
- @Valid + BindingResult 를 사용하여 Request에 대하여 검증합니다.
- @Valid 어노테이션은 Dispatcher Servlet에서 HandlerAdapter를 통해 적절한 컨트롤러를 찾아 요청을 전달하는 과정 가운데 ArgumnetResolver에서 검증합니다.
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1/categories")
public class CommandCategoryController {
private final CommandCategoryService commandCategoryService;
/**
* 카테고리를 생성하기위해 Post 요청을 처리하는 기능
*
* @param categoryRequest 생성시 필요한 이름, 노출 여부, 상위 카테고리 id가 존재
* @return ResponseEntity로 카테고리 생성 성공시 생성된 카테고리의 일부 데이터를 반환
*/
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ResponseDto<CategoryResponseDto> createCategory(
@Valid @RequestBody CategoryRequestDto categoryRequest,
BindingResult bindingResult
) {
checkRequestValidation(bindingResult);
CategoryResponseDto categoryResponseDto = commandCategoryService.create(categoryRequest);
return ResponseDto.<CategoryResponseDto>builder()
.success(true)
.status(HttpStatus.CREATED)
.data(categoryResponseDto)
.build();
}
...
}
위 예시는 생성만 보여드렸지만 수정, 삭제 등 다양한 기능을 구현하였습니다.
코드 참고는 아래 깃헙에서 하시면 됩니다.
반응형
'개발 > Spring' 카테고리의 다른 글
[Spring boot 프로젝트 정리 2] 관리자페이지 도서 카테고리 관리 (0) | 2023.04.04 |
---|---|
[Spring Cache] Spring boot 환경에서 Redis Cache 사용하기 (0) | 2023.02.15 |
[Spring] Spring MVC 정리 (0) | 2022.12.23 |
[Spring] 스프링 코어 정리 2 - 어노테이션 (0) | 2022.10.31 |
[Spring] 스프링 코어 정리 1 (0) | 2022.10.12 |