Spring 개발일지

[대규모 AI 시스템 프로젝트] 페이징/검색/정렬 구현 및 추가 리팩토링

김둘리 2026. 4. 3. 10:05

1. DTO 길이 제한 추가

코드래빗 리뷰에서 DTO에 길이 제한이 없으면 DB 컬럼 제약조건 위반이 flush 시점에 늦게 발생한다는 지적이 있었다. 요청 단계에서 미리 검증하도록 @Size 어노테이션을 추가했다.

// CompanyCreateRequest.java
@NotBlank
@Size(max = 200, message = "업체명은 200자 이하여야 합니다.")
private String name;

@NotBlank
@Size(max = 255, message = "주소는 255자 이하여야 합니다.")
private String address;

// ProductCreateRequest.java
@NotBlank
@Size(max = 200, message = "상품명은 200자 이하여야 합니다.")
private String name;

2. MSA 간 통신 구조 이해

주문 서비스에서 상품 재고 차감이 필요한 상황에 대해 팀원과 논의했다.

결론: 주문 서비스에서 FeignClient로 상품 서비스의 재고 차감 API를 호출하는 방식으로 연동한다.

주문 생성 → FeignClient → POST /products/{productId}/stock/decrease

상품 서비스는 재고 차감 API만 제공하면 되고, 호출은 주문 도메인 담당자가 구현한다.


3. 검색/정렬/페이징 구현

3-1. Repository 수정

Spring Data JPA의 메서드 이름 기반 쿼리를 활용해 검색과 페이징을 구현했다.

// CompanyRepository.java
Page<Company> findByNameContainingAndDeletedAtIsNull(String name, Pageable pageable);
Page<Company> findByHubIdAndNameContainingAndDeletedAtIsNull(UUID hubId, String name, Pageable pageable);

// ProductRepository.java
Page<Product> findByNameContainingAndDeletedAtIsNull(String name, Pageable pageable);
Page<Product> findByCompanyIdAndNameContainingAndDeletedAtIsNull(UUID companyId, String name, Pageable pageable);

Containing 키워드를 사용하면 LIKE '%값%' 쿼리가 자동으로 생성된다.

 

3-2. Service 수정

페이징 단위를 10/30/50으로 제한하는 로직을 추가했다.

@Transactional(readOnly = true)
public Page<Product> search(UUID companyId, String name, int page, int size, String sortBy) {
    // 페이징 단위 기본 10, 30이나 50으로 변경 가능
    size = List.of(10, 30, 50).contains(size) ? size : 10;

    Sort sort = Sort.by(Sort.Direction.DESC, sortBy);
    Pageable pageable = PageRequest.of(page, size, sort);

    if (companyId != null) {
        return productRepository.findByCompanyIdAndNameContainingAndDeletedAtIsNull(companyId, name, pageable);
    }
    return productRepository.findByNameContainingAndDeletedAtIsNull(name, pageable);
}

 

3-3. Controller 수정

기존 findAll() 메서드를 검색/정렬/페이징을 지원하는 방식으로 교체했다. 허브별/업체별 조회도 같은 엔드포인트에서 통합 처리한다.

// CompanyController.java
@GetMapping
public ResponseEntity<APIResponse<Page<CompanyResponse>>> findAll(
        @RequestParam(required = false) UUID hubId,
        @RequestParam(required = false, defaultValue = "") String name,
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "10") int size,
        @RequestParam(defaultValue = "createdAt") String sortBy) {

    Page<CompanyResponse> responses = companyService.search(hubId, name, page, size, sortBy)
            .map(CompanyResponse::from);

    return ResponseEntity.ok(APIResponse.success(responses));
}

3-4. API 사용 예시

# 전체 조회
GET /products

# 상품명 검색
GET /products?name=테스트

# 업체별 조회 + 검색
GET /products?companyId={uuid}&name=테스트

# 페이징 단위 변경
GET /products?size=30

# 정렬
GET /products?sortBy=name
GET /products?sortBy=createdAt

# 모두 적용
GET /products?companyId={uuid}&name=테스트&size=10&page=0&sortBy=createdAt

4. 트러블슈팅 — 한글 정렬 순서 이슈

sortBy=name으로 정렬 시 가나다 순서가 아닌 이상한 순서로 정렬되는 현상이 발생했다.

원인: PostgreSQL이 한글을 유니코드 코드포인트 기준으로 정렬하기 때문에 우리가 기대하는 가나다 순서와 다르게 동작한다.

결론: 버그가 아니라 PostgreSQL collation 특성이다. 가나다 순서가 필요하다면 LC_COLLATE 설정 변경이 필요하지만 현재 단계에서는 넘어간다.


5. build.gradle 의존성 관리 개선

팀 회의를 통해 멀티모듈 프로젝트의 build.gradle 관리 방식을 개선했다.

원칙:

  • 루트 — lombok, test 등 모든 모듈 공통 의존성만 관리
  • common — 공유 도메인 의존성 독립 관리
  • 각 모듈 — common 의존 + 해당 모듈에서만 필요한 의존성만 선언
 
// 루트 build.gradle subprojects
dependencies {
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

// common/build.gradle
bootJar { enabled = false }
jar { enabled = true }

// company/build.gradle
dependencies {
    implementation project(':common')
    implementation 'org.springframework.boot:spring-boot-starter-validation'
}