반응형
1. Stream API 테스트 전략
1.1 단위 테스트 작성
Stream API를 사용하는 코드의 효과적인 테스트 방법을 살펴보겠습니다.
@Test
class SalesAnalyzerTest {
public class SalesAnalyzer {
public Map<String, SalesStats> analyzeSalesByCategory(List<Sale> sales) {
return sales.stream()
.collect(Collectors.groupingBy(
Sale::getCategory,
Collectors.collectingAndThen(
Collectors.toList(),
this::calculateStats
)
));
}
private SalesStats calculateStats(List<Sale> sales) {
return new SalesStats(
sales.stream()
.map(Sale::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add),
sales.size()
);
}
}
@Test
@DisplayName("카테고리별 판매 통계 계산 테스트")
void analyzeSalesByCategory() {
// Given
List<Sale> sales = Arrays.asList(
new Sale("전자제품", new BigDecimal("1000000")),
new Sale("전자제품", new BigDecimal("1500000")),
new Sale("의류", new BigDecimal("500000")),
new Sale("의류", new BigDecimal("300000"))
);
SalesAnalyzer analyzer = new SalesAnalyzer();
// When
Map<String, SalesStats> result = analyzer.analyzeSalesByCategory(sales);
// Then
assertAll(
() -> assertEquals(2, result.size()),
() -> assertEquals(
new BigDecimal("2500000"),
result.get("전자제품").getTotalAmount()
),
() -> assertEquals(
2,
result.get("전자제품").getCount()
),
() -> assertEquals(
new BigDecimal("800000"),
result.get("의류").getTotalAmount()
),
() -> assertEquals(
2,
result.get("의류").getCount()
)
);
}
@Test
@DisplayName("빈 리스트에 대한 처리 테스트")
void analyzeSalesWithEmptyList() {
// Given
List<Sale> sales = Collections.emptyList();
SalesAnalyzer analyzer = new SalesAnalyzer();
// When
Map<String, SalesStats> result = analyzer.analyzeSalesByCategory(sales);
// Then
assertTrue(result.isEmpty());
}
}
1.2 복잡한 Stream 연산 테스트
public class ComplexStreamProcessor {
public List<TransactionSummary> processTransactions(
List<Transaction> transactions) {
return transactions.stream()
.filter(this::isValidTransaction)
.collect(Collectors.groupingBy(
Transaction::getCustomerId,
Collectors.collectingAndThen(
Collectors.toList(),
this::summarizeTransactions
)))
.values()
.stream()
.sorted(Comparator.comparing(TransactionSummary::getTotalAmount)
.reversed())
.collect(Collectors.toList());
}
@Test
@DisplayName("복잡한 거래 처리 테스트")
void testComplexTransactionProcessing() {
// Given
List<Transaction> transactions = createTestTransactions();
ComplexStreamProcessor processor = new ComplexStreamProcessor();
// When
List<TransactionSummary> result = processor.processTransactions(transactions);
// Then
assertAll(
// 결과 크기 확인
() -> assertEquals(3, result.size()),
// 정렬 순서 확인
() -> assertTrue(
result.get(0).getTotalAmount()
.compareTo(result.get(1).getTotalAmount()) >= 0
),
// 특정 고객의 거래 요약 확인
() -> {
TransactionSummary firstSummary = result.get(0);
assertEquals("CUST001", firstSummary.getCustomerId());
assertEquals(new BigDecimal("1500000"),
firstSummary.getTotalAmount());
assertEquals(3, firstSummary.getTransactionCount());
}
);
}
}
2. 디버깅 전략
2.1 peek()를 활용한 디버깅
public class StreamDebugger {
private static final Logger log = LoggerFactory.getLogger(StreamDebugger.class);
public List<ProcessedData> processWithDebugLogging(List<RawData> dataList) {
return dataList.stream()
.peek(data -> log.debug("Before filtering: {}", data))
.filter(this::isValid)
.peek(data -> log.debug("After filtering: {}", data))
.map(this::transform)
.peek(data -> log.debug("After transformation: {}", data))
.filter(this::meetsCriteria)
.peek(data -> log.debug("Final result: {}", data))
.collect(Collectors.toList());
}
// 디버깅을 위한 중간 결과 수집기
public static <T> Collector<T, ?, List<T>> debugCollector(String label) {
return Collectors.collectingAndThen(
Collectors.toList(),
list -> {
log.debug("{}: {}", label, list);
return list;
}
);
}
}
2.2 일반적인 문제와 해결 방법
public class CommonStreamIssues {
// 문제 1: 스트림 재사용 시도
public void streamReuseIssue() {
Stream<String> stream = getDataStream();
// 첫 번째 사용 - 정상
List<String> list1 = stream.collect(Collectors.toList());
// 두 번째 사용 - IllegalStateException 발생
// List<String> list2 = stream.collect(Collectors.toList());
// 해결방법: 새로운 스트림 생성
List<String> list2 = getDataStream().collect(Collectors.toList());
}
// 문제 2: 무한 스트림 처리
public void infiniteStreamIssue() {
// 잘못된 방법
// Stream.iterate(1, n -> n + 1)
// .collect(Collectors.toList()); // OutOfMemoryError 발생
// 올바른 방법
List<Integer> numbers = Stream.iterate(1, n -> n + 1)
.limit(100)
.collect(Collectors.toList());
}
// 문제 3: 병렬 스트림에서의 상태 공유
public void parallelStreamStateIssue() {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 잘못된 방법
List<Integer> result1 = new ArrayList<>();
numbers.parallelStream()
.map(n -> n * 2)
.forEach(result1::add); // 동시성 문제 발생
// 올바른 방법
List<Integer> result2 = numbers.parallelStream()
.map(n -> n * 2)
.collect(Collectors.toList());
}
}
3. 베스트 프랙티스
3.1 코드 가독성 향상
public class StreamBestPractices {
// 1. 적절한 메소드 추출
public List<Employee> getHighPerformers(List<Employee> employees) {
return employees.stream()
.filter(this::isHighPerformer)
.filter(this::isEligibleForPromotion)
.sorted(this::compareByPerformance)
.collect(Collectors.toList());
}
private boolean isHighPerformer(Employee emp) {
return emp.getPerformanceScore() >= 4.5;
}
private boolean isEligibleForPromotion(Employee emp) {
return emp.getYearsOfService() >= 2;
}
private int compareByPerformance(Employee e1, Employee e2) {
return Double.compare(
e2.getPerformanceScore(),
e1.getPerformanceScore()
);
}
// 2. 복잡한 조건의 분리
public List<Transaction> getValidTransactions(List<Transaction> transactions) {
Predicate<Transaction> validAmount = t ->
t.getAmount().compareTo(BigDecimal.ZERO) > 0;
Predicate<Transaction> validDate = t ->
t.getDate().isAfter(LocalDate.now().minusDays(30));
Predicate<Transaction> validStatus = t ->
t.getStatus() != TransactionStatus.CANCELLED;
return transactions.stream()
.filter(validAmount.and(validDate).and(validStatus))
.collect(Collectors.toList());
}
}
3.2 성능 고려사항
public class StreamPerformanceGuidelines {
// 1. 적절한 데이터 구조 선택
public Map<String, List<Transaction>> groupTransactions(
Collection<Transaction> transactions) {
// ArrayList보다 HashSet 사용이 검색에 효율적
return new HashSet<>(transactions).stream()
.collect(Collectors.groupingBy(Transaction::getCustomerId));
}
// 2. 불필요한 박싱/언박싱 피하기
public double calculateAverage(List<Integer> numbers) {
// 잘못된 방법
// double avg1 = numbers.stream()
// .mapToDouble(Integer::doubleValue).average().orElse(0.0);
// 올바른 방법
return numbers.stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0.0);
}
// 3. 적절한 병렬화 결정
public BigDecimal calculateTotalAmount(List<Order> orders) {
return orders.size() > 10000 ?
orders.parallelStream()
.map(Order::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add)
:
orders.stream()
.map(Order::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
4. 실전 적용 가이드라인
- 스트림 체인의 적절한 길이 유지
- 너무 긴 체인은 가독성을 해침
- 중간 결과를 의미 있는 변수로 추출 고려
- 명확한 네이밍
- 스트림 연산의 목적을 명확히 표현하는 메소드명 사용
- 중간 결과를 저장하는 변수명도 의미있게 지정
- 예외 처리
- 스트림 내부의 예외는 try-catch로 감싸서 처리
- 필요한 경우 Optional을 활용하여 null 처리
- 단위 테스트 작성
- 각 스트림 연산의 결과를 검증하는 테스트 케이스 작성
- 경계 조건에 대한 테스트 포함
- 성능 모니터링
- 대용량 데이터 처리 시 성능 측정
- 병렬 스트림 사용 여부 결정
이러한 가이드라인을 따르면서 Stream API를 사용하면, 더 유지보수하기 쉽고 효율적인 코드를 작성할 수 있습니다.
반응형