Java로 작은 요구사항 구현하기: 주문 집계와 페이지네이션

Alchemist·5일 전

KT AIVLE

목록 보기
104/130

기본문법을 안다고 바로 프로그램을 설계할 수 있는 것은 아니다. 실제 요구사항은 "어떤 입력을 받아 무엇을 반환해야 하는가"부터 결정해야 한다. 이번에는 주문 데이터를 요약하고 페이지 단위로 조회하는 작은 기능을 작성하면서 문제를 코드로 바꾸는 과정을 연습해 보자.

먼저 요구사항을 명확하게 만들자

간단한 주문 관리 기능의 조건을 다음처럼 정했다.

  • 주문은 주문 ID, 사용자 ID, 결제 금액, 상태를 가진다.
  • PAID 상태의 주문만 매출 합계에 포함한다.
  • 목록은 한 페이지에 정해진 수만큼 보여 준다.
  • 잘못된 페이지 번호와 크기를 받으면 오류를 알린다.

규칙을 먼저 적는 이유는 메서드의 책임과 테스트할 경계 조건을 쉽게 찾기 위해서다.

데이터 모델과 처리 로직 작성

Java 16 이상에서는 record로 간단한 데이터 전달 객체를 만들 수 있다. 다음 코드는 Java 17 이상에서 실행된다.

// OrderReportExample.java
import java.util.List;

public class OrderReportExample {
    enum Status {
        CREATED, PAID, CANCELLED
    }

    record Order(String id, String userId, int amount, Status status) {
    }

    static long calculatePaidAmount(List<Order> orders) {
        return orders.stream()
                .filter(order -> order.status() == Status.PAID)
                .mapToLong(Order::amount)
                .sum();
    }

    static int totalPages(int itemCount, int pageSize) {
        if (itemCount < 0 || pageSize <= 0) {
            throw new IllegalArgumentException("항목 수와 페이지 크기를 확인하세요.");
        }
        return itemCount / pageSize + (itemCount % pageSize == 0 ? 0 : 1);
    }

    static <T> List<T> getPage(List<T> items, int page, int pageSize) {
        if (page < 1 || pageSize <= 0) {
            throw new IllegalArgumentException("페이지 번호는 1 이상, 크기는 양수여야 합니다.");
        }
        long start = (long) (page - 1) * pageSize;
        if (start >= items.size()) {
            return List.of();
        }
        int from = (int) start;
        int to = (int) Math.min(start + pageSize, items.size());
        return List.copyOf(items.subList(from, to));
    }

    public static void main(String[] args) {
        List<Order> orders = List.of(
                new Order("O-201", "U-1", 18_000, Status.PAID),
                new Order("O-202", "U-2", 9_000, Status.CREATED),
                new Order("O-203", "U-1", 32_000, Status.PAID),
                new Order("O-204", "U-3", 7_000, Status.CANCELLED),
                new Order("O-205", "U-4", 15_000, Status.PAID)
        );

        System.out.println("매출 합계: " + calculatePaidAmount(orders));
        System.out.println("전체 페이지: " + totalPages(orders.size(), 2));
        System.out.println("두 번째 페이지: " + getPage(orders, 2, 2));
    }
}

실행 결과의 앞 두 줄은 다음과 같다.

매출 합계: 65000
전체 페이지: 3

마지막 줄은 두 번째 페이지에 해당하는 O-203, O-204 두 주문의 record 표현을 출력한다.

핵심 메서드가 나뉜 이유

calculatePaidAmount는 상태에 따라 필터링하고 합계만 구한다. 주문 금액 타입을 실무에서 정의할 때는 통화, 최소 단위, 범위, 환불 금액 등을 고려해야 한다. 예제에서는 이해를 위해 int를 사용하고 합계에서는 long으로 변환했다.

totalPages는 전체 항목 수와 한 페이지 크기를 받아 페이지 수를 계산한다. 예를 들어 항목 5개를 한 페이지에 2개씩 표시하면 3페이지가 필요하다. 항목이 0개일 때는 0을 반환하는 정책이다. 실제 UI에서는 빈 목록을 1페이지로 표현하는 등 제품 요구에 맞는 정책을 선택하면 된다.

getPage는 1부터 시작하는 페이지 번호를 0 기반 리스트 인덱스로 바꾼다. 범위를 벗어난 페이지는 빈 리스트를 반환한다. 오버플로를 방지하기 위해 시작 위치 계산은 long으로 수행했다.

텍스트 데이터는 어떻게 정리할까?

검색 입력이나 사용자 로그를 다룰 때는 단순한 문자열 처리도 자주 필요하다.

// TextCleanupExample.java
public class TextCleanupExample {
    static String normalizeKeyword(String raw) {
        if (raw == null) {
            return "";
        }
        return raw.strip().replaceAll("\\s+", " ").toLowerCase(java.util.Locale.ROOT);
    }

    public static void main(String[] args) {
        System.out.println(normalizeKeyword("  Java    API  ")); // java api
    }
}

strip()은 앞뒤 공백을 제거한다. replaceAll("\\s+", " ")은 연속된 정규표현식 공백 문자를 하나로 바꾼다. 단, 자바 정규표현식의 \s는 설정에 따라 모든 유니코드 공백을 의미하지 않으므로 국제화된 텍스트에서는 요구사항을 더 정확하게 정의해야 한다.

테스트할 때는 정상 사례보다 경계값이 중요하다

다음처럼 표를 만들어 예상 결과를 먼저 정리하면 코드 검증이 쉬워진다.

입력 조건예상 동작
주문 0개, 크기 5총 페이지 수 0
주문 5개, 크기 2총 페이지 수 3
페이지 4, 주문 5개, 크기 2빈 리스트
페이지 0IllegalArgumentException
페이지 크기 0IllegalArgumentException
미결제 주문 포함결제 완료 주문만 합산

실무에서는 JUnit 같은 테스트 프레임워크로 이런 사례를 자동화한다. 빠른 학습 단계에서도 main에서 메서드별 예상 출력을 확인할 수 있다.

문제 해결력을 높이는 습관

  1. 입력과 출력, 예외 조건을 한 문장으로 정의한다.
  2. 데이터를 표현할 타입을 고른다.
  3. 한 메서드가 한 가지 중요한 일을 하도록 나눈다.
  4. 정상 입력뿐 아니라 빈 목록, 음수, 초과 인덱스를 검사한다.
  5. 예외 메시지에 원인을 적되 비밀번호·토큰 등 민감정보는 남기지 않는다.
  6. 반복되는 요구사항을 만나면 그때 구조 개선을 고려한다.

AI 도구로 예제나 테스트 초안을 생성하는 것도 좋지만, 나온 코드가 해당 Java 버전에서 컴파일되는지와 요구사항의 경계 조건을 만족하는지는 직접 검증해야 한다.

핵심 정리

  • 프로그램의 출발점은 문법이 아니라 입력·출력·정책 정의다.
  • record는 간단한 불변 데이터 전달 모델을 표현할 때 편리하다.
  • enum으로 허용 가능한 상태값을 타입으로 제한할 수 있다.
  • Stream은 필터링과 집계를 명료하게 표현한다.
  • 페이지네이션은 페이지 번호·크기의 유효성 검사가 중요하다.
  • 빈 데이터와 범위 초과 등 경계 조건을 먼저 테스트한다.
  • 작은 메서드로 역할을 나누면 재사용과 검증이 쉬워진다.
profile
html_programming_language

0개의 댓글