어제에 이어 오늘도 람다와 스트림을 중심으로 실습 위주 학습을 진행했다.
여기에 Comparable / Comparator, Optional 까지 이어지면서 개념이 빠르게 확장되는 느낌이었다.
오늘도 복기를 위한 정리를 진행해본다.
1. Stream 복습 & 실습
스트림 핵심 정리
- 스트림은 일회성이다 (한 번 소비하면 재사용 불가)
- 중간 연산은 지연 처리된다 (최종 연산 전까지 실행되지 않음)
- 최종 연산이 실행되어야 파이프라인이 동작한다
Product 리스트 실습
List<Product> productList = Arrays.asList(
new Product("potato", 18, 101),
new Product("coke", 20, 102),
new Product("granola", 55, 103),
new Product("orange", 18, 104),
new Product("melon", 32, 105)
);
가격 합계 구하기 (3가지 방식)
같은 결과라도 “상황에 따라 더 읽기 좋은 방식”이 달라서 여러 방법이 존재한다.
int sum1 = productList.stream()
.mapToInt(Product::getPrice)
.sum();
int sum2 = productList.stream()
.map(Product::getPrice)
.reduce(0, Integer::sum);
int sum3 = productList.stream()
.collect(Collectors.summingInt(Product::getPrice));
mapToInt().sum(): 기본형 스트림(IntStream)을 활용해서 가장 직관적reduce(): 누적 연산의 원리를 보여주는 느낌 (학습용으로 좋음)Collectors.summingInt(): collect 흐름 안에서 “합계”를 표현할 때 깔끔함
평균 + 통계 정보
double avg = productList.stream()
.collect(Collectors.averagingDouble(Product::getPrice));
DoubleSummaryStatistics stats = productList.stream()
.mapToDouble(Product::getPrice)
.summaryStatistics();
summaryStatistics()는 count/sum/min/max/avg를 한 번에 뽑아줘서 유용하다.
groupingBy + mapping
Map<Integer, List<String>> grouped = productList.stream()
.collect(Collectors.groupingBy(
Product::getProductCode,
Collectors.mapping(Product::getName, Collectors.toList())
));
groupingBy: 기준으로 묶기mapping: 묶되, 객체 전체 말고 필요한 값만 뽑아서 담기
2. 스트림은 어떻게 실행될까?
스트림은 “전체를 한 번에 처리”하는 느낌이 아니라, 요소 하나를 흘려보내며 단계별로 처리한다.
그리고 중간 연산은 최종 연산이 호출되기 전까지는 실행되지 않는다(지연 처리).
lectureList.stream()
.filter(v -> {
System.out.println("filter");
return v.contains("a");
})
.map(v -> {
System.out.println("map");
return v;
})
.collect(Collectors.toList());
출력 결과를 통해 확인한 점:
- 최종 연산(
collect)이 호출되어야 전체 파이프라인이 실행된다 - 각 요소는
filter → map순서로 처리되고, 다음 요소로 넘어간다
(즉, filter를 전부 돌리고 map을 도는 구조가 아니다)
3. Comparable vs Comparator
객체 리스트를 정렬하려다 보면 가장 먼저 마주치는 개념이 Comparable과 Comparator이다.
두 인터페이스의 차이는 “누가 정렬 기준을 가지고 있느냐”로 정리하면 명확하다.
Comparable — 객체 스스로의 기본 정렬 기준
Comparable은 객체 자신이 기본 정렬 기준을 알고 있을 때 사용한다.
public interface Comparable<T> {
int compareTo(T o);
}
- 객체 내부에 기본 정렬 기준을 정의
- 기본 정렬 기준은 보통 1개로 고정되는 편
Comparator.naturalOrder(),Comparator.reverseOrder()와 함께 잘 맞는다
public class Product implements Comparable<Product> {
private int productCode;
@Override
public int compareTo(Product o) {
return this.productCode - o.productCode;
}
}
productList.sort(Comparator.naturalOrder());
productList.sort(Comparator.reverseOrder());
Comparator — 외부에서 비교 기준을 주입
Comparator는 객체 외부에서 정렬 기준을 정의한다.
public interface Comparator<T> {
int compare(T o1, T o2);
}
- 객체 수정 없이 정렬 로직 분리 가능
- 정렬 기준이 다양할 때 유리
- 실무에서 더 자주 보이는 방식
Comparator<Product> byCodeDesc =
(a, b) -> b.getProductCode() - a.getProductCode();
productList.sort(byCodeDesc);
스트림에서도 동일하게 사용 가능하다.
productList.stream()
.sorted((a, b) -> b.getProductCode() - a.getProductCode())
.forEach(System.out::println);
언제 무엇을 쓸까?
- “이 객체의 기본 정렬은 이거다”가 명확하면 →
Comparable - 정렬 기준이 바뀔 수 있거나 여러 기준이 필요하면 →
Comparator
보통은
👉 Comparator 중심 + 필요하면 Comparable 보조로 사용한다고 한다.
4. Optional
Optional이 등장한 배경
자바에서 null은 너무 자유롭고, 그만큼 위험하다.
obj.method(); // obj가 null이면 NPE
Optional은 값이 없을 수도 있음을 명시적으로 표현하기 위한 클래스다.
Optional 생성
Optional.empty(); // 빈 Optional
Optional.of(value); // null ❌
Optional.ofNullable(value); // null ⭕
값 꺼내기
opt.ifPresent(v -> ...);
opt.orElse("default");
opt.orElseGet(() -> "default"); // lazy, 권장
opt.orElseThrow(() -> new Exception());
orElse vs orElseGet
opt.orElse(expensive()); // 항상 실행
opt.orElseGet(() -> expensive()); // 필요할 때만 실행
👉 기본적으로 orElseGet이 안전하다.
Optional + map
Optional<String> opt = Optional.ofNullable("optional");
Optional<Integer> len = opt.map(String::length);
// Optional[8]
Optional Best Practice
Optional<User> user = Optional.empty(); // OK
Optional<User> user = null; // ❌
- Optional을 필드/파라미터/생성자에 쓰는 건 보통 비권장
- 반환값에서 “없을 수 있음”을 표현할 때 가장 깔끔하다
- 컬렉션을 Optional로 감싸는 것도 보통 피한다
(Optional<List<T>>대신Collections.emptyList()같은 방식)
5. Optional + MVC 개선 적용
Model은 핵심 로직에 집중하고, 예외 처리는 Controller가 담당하도록 정리해봤다.
Model — 조회 실패 시 예외 던지기
return studentList.stream()
.filter(Objects::nonNull)
.filter(s -> s.getStudentNo() == studentNo)
.findFirst()
.orElseThrow(() -> new Exception("일치하는 학생이 없습니다."));
Controller — 예외 처리
try {
Student student = StudentModel.getStudentByStudentNo(1001);
EndView.printStudent(student);
} catch (Exception e) {
EndView.printResult(e.getMessage());
}
- Model: 조회 로직/규칙
- Controller: 흐름 제어 + 예외 처리
'Learning Log' 카테고리의 다른 글
| [멋사 클라우드 5기] Day 9 - Enum 활용, BigNumber, Lombok, 객체 설계 (1) | 2026.02.03 |
|---|---|
| [멋사 클라우드 5기] Day 8 - 입출력, 직렬화, 스레드, Enum (0) | 2026.02.02 |
| [멋사 클라우드 5기] Day 6 - 자료구조, Lambda, Stream (0) | 2026.01.29 |
| [멋사 클라우드 5기] Day 5 - Java 기본 라이브러리 & 컬렉션 정리 (0) | 2026.01.28 |
| equals가 있는데 hashCode는 왜 필요할까? (0) | 2026.01.27 |
