MSA 구현하기(13) - 상품 카테고리 기능 추가 (feat. SQL)
Skala 과정에서 마이크로 서비스 아키텍처 구조에 대해 새롭게 알게 되었습니다.
배운 것을 실제로 구현해보기 위해 이 여정을 시작하기로 했습니다.
처음 글부터 보러가기
DB 시간에 온라인 쇼핑몰을 예시로 ERD 및 테이블을 생성해보는 실습 시간이 있었는데
제 프로젝트에도 카테고리를 추가하는 것이 좋을 것 같아서 해보기로 했습니다!
SQL로 직접 테이블 추가하기
R2DBC를 사용하고 있기 때문에 Spring에서 엔티티 클래스를 작성한다고 해도
자동으로 테이블을 생성해주지 않습니다!
사실 그래서 해봐야겠다는 생각이 들었습니다 ㅎㅎ
item 테이블에 category_id 열 추가
1
2
ALTER TABLE category
CHANGE COLUMN sub_category_id parent_id INT;
우선 item 테이블에 category_id 열을 새로 추가해주었습니다.
category 테이블 생성
1
2
3
4
5
6
7
8
CREATE TABLE `category` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL,
`parent_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `FK_category` (`parent_id`),
CONSTRAINT `FK_category` FOREIGN KEY (`parent_id`) REFERENCES `category` (`id`)
)
category 테이블은 셀프 조인을 통해 계속해서 세부 카테고리를 생성할 수 있도록 만들었습니다.
DBeaver로 확인해보면 이렇게 셀프 조인을 하고 있는 것을 확인할 수 있습니다!
category 내 데이터 삽입
1
2
3
4
5
6
7
INSERT INTO `category` (name, parent_id)
VALUES ('FOOD', null);
.
.
.
INSERT INTO `category` (name, parent_id)
VALUES ('FROZEN FOOD', 1);
INSERT 를 통해 category 안에 샘플 데이터를 몇 개 넣어주었습니다.
id가 자동으로 증가되도록 설정되어 있기 때문에 행을 삽입할 때 id를 명시해주지 않아도 자동으로 1씩 증가되며 삽입됩니다.
item의 category_id에 값 넣어주기
1
UPDATE item SET category_id = 10 WHERE id = 1;
임시로 생성해놓았던 상품들 중 하나에 값을 넣어봤습니다!
잘 삽입되어 있는 것을 확인할 수 있습니다 ☺️
Spring 코드로 테이블 연결하기
R2DBC 기반으로 되어 있기 때문에 잘 연결이 될 지 불안했지만..
생각보다 굉장히 수월하게 연결되었습니다!
Category.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Table
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class Category {
@Id
private Long id;
private String name;
private Long parentId;
public Category(String name) {
this.name = name;
this.parentId = null;
}
public Category(String name, Long parentId) {
this.name = name;
this.parentId = parentId;
}
}
대분류 카테고리의 경우 parentId가 없이 추가되니, 생성자도 여러개 만들어주었습니다!
@Table과@Id모두org.springframework.data에서 import 되도록 해주세요
CategoryRepository.java
1
2
3
4
public interface CategoryRepository extends R2dbcRepository<Category, Long> {
Flux<Category> findByParentIdIsNull();
Flux<Category> findByParentId(Long parentId);
}
대분류만 찾기 위해 findByParentIdIsNull() 함수를 선언하고
대분류 및 세부 카테고리에 대해 깊이가 1인 카테고리 목록들만 불러올 수 있게 findByParentId() 함수를 선언해주었습니다.
CategoryService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@Service
@Slf4j
@RequiredArgsConstructor
public class CategoryService {
private final CategoryRepository categoryRepository;
public Flux<AllCategoryResponse> allCategories() {
return categoryRepository.findByParentIdIsNull()
.flatMap(this::getAllCategory);
}
public Mono<CategoryDetailsResponse> getCategoryDetails(Long id) {
return categoryRepository.findByParentId(id)
.map(Category::getName)
.collectList()
.map(CategoryDetailsResponse::new);
}
public Mono<Void> register(
CategoryRegisterRequest request,
Long userId,
UserRole role
) {
log.info("user({}) registering new category: {}", userId, request.getName());
RoleCheck.isAdmin(role);
Mono<Category> categoryMono;
if (request.getParentId() == null) {
categoryMono = Mono.just(new Category(request.getName()));
} else {
categoryMono = categoryRepository.findById(request.getParentId())
.switchIfEmpty(Mono.error(new CategoryNotFoundException()))
.map(parent -> new Category(request.getName(), parent.getId()));
}
return categoryMono
.flatMap(categoryRepository::save)
.switchIfEmpty(Mono.error(new CategoryRegisterException()))
.then();
}
private Mono<AllCategoryResponse> getAllCategory(Category category) {
return categoryRepository.findByParentId(category.getId())
.map(Category::getName)
.collectList()
.map(children -> new AllCategoryResponse(category.getName(), children));
}
}
서비스 클래스는 위와 같이 작성해주었습니다.
세부 로직은 각자 상황에 맞게 구성하면 되고, WebFlux 환경에서 원활하게 동작할 수 있도록 Mono나 Flux를 반환하도록 만들어주었습니다!
CategoryController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@RestController
@RequestMapping("/category")
@Slf4j
@RequiredArgsConstructor
public class CategoryController {
private final CategoryService categoryService;
/** [권한 제한 없음]
* 전체 카테고리 조회
* @return 전체 카테고리 이름 및 깊이 1 세부 카테고리 목록
*/
@GetMapping
public Flux<AllCategoryResponse> allLargeCategories() {
return categoryService.allCategories();
}
/** [권한 제한 없음]
* 개별 카테고리 별 세부 카테고리 조회
* @param id 개별 카테고리 Id
* @return 개별 카테고리 별 깊이 1 세부 카테고리 목록
*/
@GetMapping("/{categoryId}")
public Mono<CategoryDetailsResponse> categoryDetails(
@PathVariable("categoryId") Long id
) {
return categoryService.getCategoryDetails(id);
}
/** [관리자]
* 카테고리 등록(추가)
* @param request 카테고리 등록 Request
* @param userId 로그인 된 사용자 User Id
* @param role 로그인 된 사용자 Role
* @return Void
*/
@PostMapping("/register")
public Mono<Void> registerCategory(
@RequestBody CategoryRegisterRequest request,
@RequestHeader("x-user-id") Long userId,
@RequestHeader("x-user-role") UserRole role
) {
return categoryService.register(request, userId, role);
}
}
컨트롤러 클래스도 위와 같이 작성해주었습니다.
마치며
결과 화면
이제 백엔드 부분은 버그가 있지 않는 이상 더 건드리지 않고, 수업 시간에 배운 프론트나 AI 부분을 적용시켜보고자 합니다.
나중에 다시 보자 스프링아,,,
다음 글부터는 화면 구성에 대해 다뤄보겠습니다!
👻🛒
참고 자료
Skala 교육 자료!





