ProductServiceImpl 살펴보기
제품에 관한 비즈니스 로직을 처리하는 클래스이다.
- Service 클래스이며 ProductService 인터페이스를 상속한다.
- ProductRepository와 OrderItemRepository를 생성자의 매개변수로 주입 받는다.
- findOne, findAll, findAllWithFilter, save, update, delete 메서드가 구현되었다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ProductServiceImpl implements ProductService {
private final ProductRepository productRepository;
private final OrderItemRepository orderItemRepository;
@Override
public ProductInfoDto findOne(Long productId) {
Product found = productRepository.findById(productId)
.orElseThrow(PRODUCT_NOT_FOUND::exception);
return convertInfoDto(found);
}
@Override
public Page<ProductInfoDto> findAll(Pageable pageable) {
return productRepository.findAll(pageable).map(this::convertInfoDto);
}
@Override
public Page<ProductInfoDto> findAllWithFilter(Pageable pageable, String code, String search) {
Page<Product> found;
if (!search.isEmpty() && !code.isEmpty()) {
found = productRepository.findAllByCodeContainsAndNameContains(pageable, code, search);
} else if (search.isEmpty() && code.isEmpty()) {
found = productRepository.findAll(pageable);
} else if (search.isEmpty()) {
found = productRepository.findAllByCodeContains(pageable, code);
} else {
found = productRepository.findAllByNameContains(pageable, search);
}
return found.map(this::convertInfoDto);
}
@Transactional
@Override
public Long save(ProductSaveDto dto) {
productRepository.findByName(dto.getName())
.ifPresent(p -> {throw PRODUCT_NAME_DUPLICATE.exception();});
return productRepository.save(dto.toEntity(saveImgFile(dto)))
.getProductId();
}
@Transactional
@Override
public Long update(ProductUpdateDto dto) {
Product found = productRepository.findById(dto.getProductId())
.orElseThrow(PRODUCT_NOT_FOUND::exception);
productRepository.findByName(dto.getName())
.ifPresent(p -> {
if (!p.getProductId().equals(dto.getProductId())) {
throw PRODUCT_NAME_DUPLICATE.exception();
}
});
deleteDirectory(found.getDirectoryPath());
found.updateProduct(dto, saveImgFile(dto));
return found.getProductId();
}
@Transactional
@Override
public void delete(Long productId) {
Product found = productRepository.findById(productId)
.orElseThrow(PRODUCT_NOT_FOUND::exception);
if (orderItemRepository.existsByProductId(productId))
throw PRODUCT_REFERRED.exception();
productRepository.deleteById(productId);
deleteDirectory(found.getDirectoryPath());
}
private String saveImgFile(ProductDto dto) {
try {
saveFiles(dto.getImgFiles(), dto.getViewPath());
return saveFile(dto.getDetailInfo(), dto.getDirectoryPath());
} catch (NullPointerException e) {
throw PRODUCT_IMAGE_EMPTY.exception();
}
}
private ProductInfoDto convertInfoDto(Product product) {
return new ProductInfoDto(
product,
getAccessUrls(product.getImgFolderPath()),
getAccessUrl(product.getDetailInfo())
);
}
}
ProductServiceImpl 분석하기
1. Anntation
- @Service
기능 : 스프링 빈에 등록하기 위한 Annotation으로 @Component와 거의 다르지 않다.
분석 : 해당 클래스가 서비스 계층에 속하며 비즈니스 로직을 처리하기에 적절하다.
- @Transactional(readOnly = ture)
기능 : 클래스에 선언하는 경우 클래스에 포함된 모든 메서드에 적용되며 메서드에 선언하는 경우 해당 메서드에 대해서만 적용된다. 트랜잭션은 데이터 저장 및 수정 처리 중 문제가 발생했을 때 DB의 상태를 수행 이전 상태로 되돌린다. readOnly 옵션을 설정해 좀 더 효율적인 DB 커넥션 사용이 가능해진다.
분석 : 서비스 계층에선 작업 도중 실패시 데이터의 무결성을 위해 트랜잭션이 수행되어야 한다. 현재 단계에선 메서드 단위로 적용하는 형태지만 트랜잭션의 효율성을 올리기 위해 범위 적용에 대해 고민할 필요가 있다.
- @RequiredArgsConstructor
기능 : 클래스 내 final로 선언된 필드에 대해 생성자를 자동 생성하여 값을 주입한다.
분석 : @Autowired를 이용한 생성자 주입의 경우 주입받는 매개변수에 따라 코드가 길어진다. 하지만 해당 Annotation을 이용해 코드를 줄임으로써 코드 가독성이 향상된다.
2. Field
- private final ProductRepository productRepository
기능 : 스프링 컨테이너로부터 주입받은 ProductRepository를 저장한다.
분석 : Product 서비스 로직을 처리하기 위해 필요하다.
- private final OrderItemRepository orderItemRepository
기능 : 스프링 컨테이너로부터 주입받은 OrderItemRepository를 저장한다.
분석 : Product 서비스 로직을 처리하기 위해 필요하다.
3. Method
- public ProductInfoDto findOne(Long productId)
기능 : 매개변수로 받은 productId를 이용해 Product Entity를 조회한다.
분석 : productId가 존재하지 않는 경우 예외처리가 필요하다. Entity를 반환 타입인 ProductInfoDto 변환하기위해 Entity 내에 구현한 toInfoDto 메서드를 사용하는 것이 적절하다.
- public Page<ProductInfoDto> findAll(Pageable pageable)
기능 : 매개변수로 받은 Pageable을 이용해 반환할 page 번호, page 당 개수, 정렬 기준 등을 적용하여 해당하는 모든 데이터를 조회한다.
분석 :
- 현재 findAllWithFilter와 기능이 중복된다고 할 수 있다. Spring Data Jpa가 아닌 JPA를 사용하는 방식을 이용하게 되면 이후 제거될 수 있다.
- convertInfoDto가 아닌 toInfoDto 메서드를 적용해야 한다.
- public Page<ProductInfoDto> findAllWithFilter(Pageable pageable, String code, String search)
기능 : code와 search 입력 상황에 따라 적절한 필터 조회 방식을 선택하여 해당하는 모든 데이터를 조회한다.
분석 :
- 현재 구현된 상태는 조건문이 많아 코드가 난잡하다. JPA를 사용하는 방식을 이용하게 되면 이후 더 간결한 코드 작성이 가능해질 것이다.
- convertInfoDto가 아닌 toInfoDto 메서드를 적용해야 한다.
- public Long save(ProductSaveDto)
기능 : 제품 이름의 중복 여부를 체크하고 유효한 경우 이미지 파일을 로컬에 저장한 뒤 DB에 제품 정보를 저장한다.
분석 : 유일한 경로를 위해 제품 이름의 중복 체크는 필수적이며, 더 이상 toEntity 메서드의 매개변수로 생성된 파일명을 전달하지 않아도 된다.
- public Long update(ProductUpdateDto)
기능 : 매개변수로 받은 ProductUpdate의 데이터로 관련 Product Entity의 데이터를 수정한다.
분석 :
- productId, name에 대한 유효성 검증을 통해 imgPath에 대한 유일성을 보장하였다.
- 영속성 컨텍스트 수정 과정에서 문제가 발생할 수 있기에 deleteDirectory 메서드의 위치를 updateProduct 이후로 변경하는 것이 적절하다. 이 경우 수정되기 전의 imgPath 값을 저장해둘 필요가 있다.
- public void delete(Long productId)
기능 : 제품의 기본키에 해당하는 Entity를 DB에서 제거한다.
분석 :
- 제품과 관련된 주문이 하나라도 존재하는 이상 제품을 삭제할 수 없기에 productId을 이용해 기본키의 유효성 검증과 관련 oderItem의 여부를 판단하는 로직을 작성하였다. 관련 주문이 있는 경우 처리를 하게된다면 비활성화 상태를 나타내는 컬럼을 추가하고 이를 수정하는 방식을 사용할 것이다.
- 제품 삭제시 로컬에 저장된 이미지도 삭제되어야 하지만 문제가 발생해 롤백되는 경우를 대비해 로컬 이미지의 삭제 순서를 가장 마지막으로 정하였다.
- deleteDirectory 메서드의 매개변수로 새로 추가한 imgPath를 전달하는 것이 적절하다.
- private String saveImgFile(ProductDto dto)
기능 : imgFiles와 detailInfo를 로컬에 저장한다.
분석 : 이미지 저장 도중 발생하는 예외에 대해 Product 관련 예외로 변경하여 처리하였다. 현재 쓰인 getViewPath, getDirectoryPah는 imgPath로 대체하는 것이 적절하다.
- public ProductInfoDto convertInfoDto(Product product)
기능 : Product Entity를 ProductInfoDto로 변환한다.
분석 : Product 내에 toInfoDto 메서드를 구현함에 따라 더 이상 사용하지 않아 제거하는 것이 적절하다.
ProductServiceImpl 리팩토링
수정된 경우 ■ 색, 추가된 경우 ■ 색, 삭제된 경우 ■ 색으로 표시하였다. 수정된 코드는 다음과 같다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ProductServiceImpl implements ProductService {
private final ProductDao productDao;
private final OrderItemRepository orderItemRepository;
@Override
public ProductInfoDto findOne(Long productId) {
return productDao
.findById(productId)
.orElseThrow(PRODUCT_NOT_FOUND::exception)
.toInfoDto();
}
@Override
public Page<ProductInfoDto> findAllByFilter(Pageable pageable, String code, String search) {
return productDao
.findAllByFilter(pageable, code, search)
.map(Product::toInfoDto);
}
@Transactional
@Override
public Long save(ProductSaveDto dto) {
if (productDao.existsByName(dto.getName())) {
throw PRODUCT_NAME_DUPLICATE.exception();
}
Long savedId = productDao.save(dto.toEntity());
saveImgFile(dto);
return savedId;
}
@Transactional
@Override
public Long update(ProductUpdateDto dto) {
Product found = productDao
.findById(dto.getProductId())
.orElseThrow(PRODUCT_NOT_FOUND::exception);
if (productDao.existsByNameAndProductIdNot(dto.getProductId(), dto.getName())) {
throw PRODUCT_NAME_DUPLICATE.exception();
}
String preImgPath = found.getImgPath();
found.updateProduct(dto);
deleteDirectory(preImgPath);
saveImgFile(dto);
return found.getProductId();
}
@Transactional
@Override
public void delete(Long productId) {
Product found = productDao
.findById(productId)
.orElseThrow(PRODUCT_NOT_FOUND::exception);
if (orderItemRepository.existsByProductId(productId)) {
throw PRODUCT_REFERRED.exception();
}
productDao.deleteById(productId);
deleteDirectory(found.getImgPath());
}
private void saveImgFile(ProductDto dto) {
try {
saveFiles(dto.getImgFiles(), dto.getProductImgSavePath());
saveFile(dto.getDetailInfo(), dto.getDetailInfoImgSavePath());
} catch (NullPointerException e) {
throw PRODUCT_IMAGE_EMPTY.exception();
}
}
}
1. Annotaion
2. Field
3. Method
- public Page<ProductInfoDto> findAll(Pageable pageable)
이제 findAllByFilter 메서드로 대체가 가능해져 제거하였다.
- public Page<ProductInfoDto> findAllByFilter(Pageable pageable, String code, String search)
findAllWithFilter에서 findAllByFilter로 명칭을 수정하였다. 기존 조건문을 이용한 분기 과정을 제거하고 ProductRepository에게 책임을 넘겼다.
- public Long save(ProductSaveDto)
ProductRepository에서 save는 productId를 반환하는 형태로 수정되었다. 따라서 getProductId 메서드를 사용할 필요가 없어졌다. 만약 save 과정에서 실패하게되면 기존에 로컬 영역에 저장한 이미지 파일은 그대로 남아있게 된다. 따라서 saveImgFile 메서드를 save 메서드 이후로 옮기는 것이 적절하다고 판단하였다.
- public Long update(ProductUpdateDto)
롤백 시점을 고려하여 deleteDirectroy 메서드의 위치를 updateProduct 메서드 이후로 옮겼으며 이전 저장 경로에 대해선 preImgPath변수에 저장하는 방식을 사용하였다. ProductDao에 새로 구현한 existsByNameAndProductIdNot 메서드를 이용해 name에 대한 중복에서 같은 Entity의 name 중복은 제외하였다.
- public void delete(Long productId)
deleteDirectroy 매개변수는 getImgPath을 통해 전달하도록 수정하였다.
- private void saveImgFile(ProductDto dto)
더 이상 생성된 파일명이 필요없기에 반환 타입을 void로 수정하는 것이 적절하다. 저장 경로에 대해선 imgFiles의 경우 "/view'라는 하위 폴더를, detailInfo의 경우 "/info"라는 하위 폴더를 경로에 추가하여 저장한다.
- public ProductInfoDto convertInfoDto(Product product)
Product Entity의 toInfoDto로 대체 가능하기에 제거하였다.
//ProductController
//삭제
@GetMapping
public Page<ProductInfoDto> searchAll(Pageable pageable) {
return productService.findAll(pageable);
}
//메서드명 & 호출 메서드명 수정
@GetMapping("/filter")
public Page<ProductInfoDto> searchAllByFilter(@RequestParam(name = "code", defaultValue = "") String code,
@RequestParam(name = "search", defaultValue = "") String search,
Pageable pageable) {
return productService.findAllByFilter(pageable, code, search);
}
'개발서 > ToyProject-Smart' 카테고리의 다른 글
| [Toy - Smart] PathConverter 구현 (0) | 2023.02.28 |
|---|---|
| [Toy - Smart] CustomFileUtils 리팩토링 (0) | 2023.02.28 |
| [Toy - Smart] ProductDao 구현 (0) | 2023.02.25 |
| [Toy - Smart] ProductInfoDto 리팩토링 (0) | 2023.02.25 |
| [Toy - Smart] ProductUpdateDto 리팩토링 (0) | 2023.02.25 |
댓글