BaseEntity 구현
엔티티의 공통 속성으로 createdDateTime, modifiedDateTime 등과 함께 soft delete를 위한 deletedAt, deletedBy 필드를 정의했습니다.
또한, softDelete(String userName) 메서드에서 deletedAt에 LocalDateTime.now()를, deletedBy에 전달받은 사용자명을 설정하도록 했습니다.
Service 계층의 deleteCompany 메서드
@Transactional(readOnly = false)
public void deleteCompany(UUID id) {
Company company = companyRepository.findByIdAndDeletedAtIsNull(id)
.orElseThrow(() -> new BaseException(Code.COMPANY_FIND_ERROR));
company.softDelete("temp");
companyRepository.save(company);
}
@Transactional(readOnly = false) 설정을 통해, 엔티티 변경사항이 DB에 반영되도록 했습니다.
문제 상황
PATCH 요청을 사용하여 삭제 API를 호출했지만, 로그 상에서 softDelete로 인한 SQL UPDATE가 실행되지 않았습니다.
가설:
PATCH 요청을 처리하는 로직에서 클라이언트의 요청 본문을 기반으로 엔티티를 병합(merge)하는 과정이 있었고, 이 과정에서 softDelete로 설정한 값들이 덮어써지는 문제가 발생했을 가능성이 있었습니다.
해결 시도:
전용 엔드포인트 설계:
PATCH 대신에 soft delete 전용의 PUT 엔드포인트를 생성하여, 병합 로직 없이 삭제 관련 필드만 업데이트하도록 함.
Controller 코드 예시:
@RestController
@RequestMapping("/api/v1/companies")
@RequiredArgsConstructor
public class CompanyController {
private final CompanyService companyService;
@PutMapping("/{id}/soft-delete")
public ResponseEntity<?> softDeleteCompany(@PathVariable("id") UUID id) {
companyService.deleteCompany(id);
return ResponseEntity.ok(ApiResponseData.success(Code.COMPANY_DELETE));
}
}
이로써 클라이언트는 soft delete 기능만을 호출할 수 있어, 기존의 엔티티 병합 이슈를 회피할 수 있었습니다.
오류 로그:
java.lang.ClassCastException: class java.util.ArrayList cannot be cast to class org.springframework.data.domain.Page
원인 분석:
CompanyRepository의 findAllByDeletedAtIsNull(Pageable pageable) 메서드가 List를 반환하도록 선언되어 있었으나, 서비스 계층에서 이를 Page로 캐스팅하려 했습니다.해결 방법:
리포지토리 메서드의 반환 타입을 List에서 Page로 변경하도록 수정했습니다.
public interface CompanyRepository extends JpaRepository<Company, UUID> {
Optional<Company> findById(UUID id);
Optional<Company> findByIdAndDeletedAtIsNull(UUID id);
List<Company> findAll();
// 반환 타입 수정
Page<Company> findAllByDeletedAtIsNull(Pageable pageable);
@Query("SELECT c FROM Company c " +
"WHERE (:keyword IS NULL OR :keyword = '' OR " +
" cast(c.id as string) LIKE concat('%', :keyword, '%') OR " +
" cast(c.hub_id as string) LIKE concat('%', :keyword, '%') OR " +
" lower(c.name) LIKE lower(concat('%', :keyword, '%')) OR " +
" lower(c.type) LIKE lower(concat('%', :keyword, '%')) OR " +
" lower(c.address) LIKE lower(concat('%', :keyword, '%')))")
Page<Company> searchCompanies(@Param("keyword") String keyword, Pageable pageable);
}
이로 인해 ClassCastException 문제가 해결되었습니다.
이러한 과정을 통해 soft delete 구현 시 발생할 수 있는 다양한 문제들을 파악하고, 각 문제에 대한 해결 방안을 적용해 보았습니다.