Spring Boot에서 완벽한 CRUD 구현하기: MySelectShop 프로젝트 분석
Spring Boot를 사용한 MySelectShop 프로젝트를 통해 완벽한 CRUD(Create, Read, Update, Delete) 구현 방법을 살펴보겠습니다. 이 프로젝트는 사용자가 관심 상품을 등록하고 관리할 수 있는 쇼핑 애플리케이션으로, 실제 운영 환경에서 사용할 수 있는 완성도 높은 CRUD 예제입니다.
프로젝트 개요
MySelectShop은 다음과 같은 주요 기능을 제공합니다:
- 상품 관리: 관심 상품 등록, 조회, 수정
- 폴더 기능: 상품을 분류할 수 있는 폴더 생성 및 관리
- 사용자 인증: JWT 기반 로그인 시스템
- 가격 모니터링: 네이버 쇼핑 API를 통한 자동 가격 업데이트
핵심 CRUD 구현 분석
1. Create (생성) - 상품 등록
Controller (ProductController.java:28-31)
@PostMapping("/api/products")
public ProductResponseDto createProduct(@RequestBody ProductRequestDto requestDto,
@AuthenticationPrincipal UserDetailsImpl userDetails) {
return productService.createProduct(requestDto, userDetails.getUser());
}
Service (ProductService.java:34-38)
public ProductResponseDto createProduct(ProductRequestDto requestDto, User user) {
Product product = productRepository.save(new Product(requestDto, user));
return new ProductResponseDto(product);
}
핵심 포인트:
- @RequestBody로 JSON 데이터를 DTO로 자동 변환
- 인증된 사용자 정보를 통해 권한 검증
- Entity 생성자를 통한 깔끔한 객체 생성
2. Read (조회) - 상품 목록 조회
Controller (ProductController.java:38-47)
@GetMapping("/api/products")
public Page<ProductResponseDto> getProducts(
@RequestParam("page") int page,
@RequestParam("size") int size,
@RequestParam("sortBy") String sortBy,
@RequestParam("isAsc") boolean isAsc,
@AuthenticationPrincipal UserDetailsImpl userDetails) {
return productService.getProducts(userDetails.getUser(), page - 1, size, sortBy, isAsc);
}
Service (ProductService.java:69-87)
@Transactional(readOnly = true)
public Page<ProductResponseDto> getProducts(User user, int page, int size, String sortBy, boolean isAsc) {
Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC;
Sort sort = Sort.by(direction, sortBy);
Pageable pageable = PageRequest.of(page, size, sort);
UserRoleEnum userRoleEnum = user.getRole();
Page<Product> productList;
if (userRoleEnum == UserRoleEnum.USER) {
productList = productRepository.findAllByUser(user, pageable);
} else {
productList = productRepository.findAll(pageable);
}
return productList.map(ProductResponseDto::new);
}
핵심 포인트:
- 페이지네이션: Pageable을 사용한 효율적인 데이터 처리
- 동적 정렬: 사용자가 원하는 기준으로 정렬
- 권한별 조회: 일반 사용자는 본인 상품만, 관리자는 전체 상품 조회
- @Transactional(readOnly = true)로 성능 최적화
3. Update (수정) - 상품 정보 수정
Controller (ProductController.java:33-36)
@PutMapping("/products/{id}")
public ProductResponseDto updateProduct(@PathVariable Long id,
@RequestBody ProductMypriceRequestDto requestDto) {
return productService.updateProduct(id, requestDto);
}
Service (ProductService.java:40-67)
@Transactional
public ProductResponseDto updateProduct(Long id, ProductMypriceRequestDto requestDto) {
int myprice = requestDto.getMyprice();
if (myprice < MIN_MY_PRICE) {
throw new IllegalArgumentException(
messageSource.getMessage(
"below.min.my.price",
new Integer[] {MIN_MY_PRICE},
"Wrong Price",
Locale.getDefault()
)
);
}
Product product = productRepository.findById(id).orElseThrow(() ->
new ProductNotFoundException(messageSource.getMessage(
"not,found,product",
null,
"Not Found Product",
Locale.getDefault()
))
);
product.update(requestDto);
return new ProductResponseDto(product);
}
Entity의 Update 메서드 (Product.java:55-57)
public void update(ProductMypriceRequestDto requestDto) {
this.myprice = requestDto.getMyprice();
}
핵심 포인트:
- 유효성 검증: 비즈니스 로직에 맞는 데이터 검증
- 예외 처리: 존재하지 않는 상품에 대한 명확한 에러 메시지
- 국제화 지원: MessageSource를 통한 다국어 에러 메시지
- 더티 체킹: JPA의 변경 감지로 자동 UPDATE
4. Delete (삭제)
암시적 삭제 이 프로젝트에서는 직접적인 DELETE 엔드포인트는 없지만,
폴더에서 상품 제거 등의 연관관계 관리를 통해 삭제 개념을 구현합니다.
고급 CRUD 기능들
1. 연관관계 관리 - 폴더에 상품 추가
Controller (ProductController.java:49-56)
@PostMapping("/products/{productId}/folder")
public void addFolder(
@PathVariable Long productId,
@RequestParam Long folderId,
@AuthenticationPrincipal UserDetailsImpl userDetails
) {
productService.addFolder(productId, folderId, userDetails.getUser());
}
Service (ProductService.java:97-118)
public void addFolder(Long productId, Long folderId, User user) {
Product product = productRepository.findById(productId).orElseThrow(
() -> new NullPointerException("해당 상품이 존재하지 않습니다.")
);
Folder folder = folderRepository.findById(folderId).orElseThrow(
() -> new NullPointerException("해당 폴더가 존재하지 않습니다.")
);
if (!product.getUser().getId().equals(user.getId())
|| !folder.getUser().getId().equals(user.getId())) {
throw new IllegalArgumentException("회원님의 관심상품이 아니거나, 회원님의 폴더가 아닙니다.");
}
Optional<ProductFolder> overlapFolder = productFolderRepository.findByProductAndFolder(product, folder);
if (overlapFolder.isPresent()) {
throw new IllegalArgumentException("중복된 폴더입니다.");
}
productFolderRepository.save(new ProductFolder(product, folder));
}
2. 복잡한 조회 - 폴더별 상품 조회
폴더 CRUD 구현
폴더 생성 (Create)
Controller (FolderController.java:22-26)
@PostMapping("/folders")
public void addFolders(@RequestBody FolderRequestDto folderRequestDto,
@AuthenticationPrincipal UserDetailsImpl userDetails) {
List<String> folderNames = folderRequestDto.getFolderNames();
folderService.addFolders(folderNames, userDetails.getUser());
}
폴더 조회 (Read)
Controller (FolderController.java:28-31)
@GetMapping("/folders")
public List<FolderResponseDto> getFolders(@AuthenticationPrincipal UserDetailsImpl userDetails) {
return folderService.getFolders(userDetails.getUser());
}
CRUD 구현의 Best practice
1. 계층화된 아키텍처
Controller → Service → Repository → Entity
- 각 계층의 책임이 명확히 분리
- 비즈니스 로직은 Service 계층에 집중
2. DTO 패턴 활용
- Request DTO: 클라이언트로부터 받는 데이터 검증
- Response DTO: 클라이언트에게 전달할 데이터 가공
3. 예외 처리 전략
Product product = productRepository.findById(id).orElseThrow(() ->
new ProductNotFoundException("상품을 찾을 수 없습니다.")
);
4. 보안 고려사항
- @AuthenticationPrincipal로 인증된 사용자만 접근
- 권한별 데이터 접근 제어
- 소유자 검증을 통한 보안 강화
5. 성능 최적화
- @Transactional(readOnly = true) 활용
- 페이지네이션으로 대용량 데이터 처리
- Lazy Loading으로 필요한 데이터만 로딩
6. 데이터 무결성
- 중복 데이터 검증
- 비즈니스 규칙 검증 (최소 가격 등)
- 연관관계 무결성 보장
마무리
MySelectShop 프로젝트를 통해 살펴본 CRUD 구현은 단순한 데이터 조작을 넘어서 실제 서비스에서 요구되는 다양한 요구사항들을 충족합니다:
- 확장 가능한 구조: 새로운 기능 추가가 용이
- 보안성: 인증/인가 시스템 완비
- 성능: 페이지네이션과 최적화된 쿼리
- 사용자 경험: 명확한 에러 메시지와 유효성 검증
