새로운 컬렉션 API의 기능
1. 작은 리스트, 집합, 맵을 쉽게 만들 수 있도록 자바 9에 새로 추가된 컬 렉션 팩토리를 살펴본다.
2. 자바 8의 개선 사항으로 리스트와 집합에서 요소를 삭제하 거나 바꾸는 관용 패턴을 적용하는 방법을 배운다.
3. 맵 작업과 관련해 추가된 새로 운 편리 기능을 살펴본다.
ConcurrentHashMap 클래스는 멀티스레드 환경에서 안정성과 성능을 모두 고려한 해시맵.
동시성 친화적이며 최신 기술을 반영한 HashMap 버전. ConcurrentHashap은 내부 자료구조의 특정 부분만 잠궈 동시 추가, 갱신 작업을 허용한다. 따라서 동기화된 Hashtable 버전에 비해 읽기 쓰기 연산 성능이 월등하다(참고로, 표준 HashMap은 비동기로 동작함).
forEach
, reduce
, search
등의 메서드요약
// # 1. 익명 클래스를 람다 표현식으로 리팩토링
// 같은 시그니처를 갖는 함수형 인터페이스를 선언 시,
// 람다 표현식으로는 어떤 인터페이스를 사용하는지 알 수 없다.
doSomeThing(() -> System.out.println("Danger danger!!"));
// 명시적 형변환을 이용해서 모호함을 제거할 수 있다.
doSomeThing((Task)() -> System.out.println("Danger danger!!"));
// # 2. 람다 표현식을 메서드 참조로 리팩토링
.collect( groupingBy(dish -> {
if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
.collect( groupingBy(dish::getCaloricLevel));
// # 3. 명령형 데이터 처리를 스트림으로 리팩토링
List<String> dishNames = new ArrayList<>();
for (Dish dish : menu) {
if (dish.getCalories() > 300) {
dishNames.add(dish.getName());
}
}
// 스트림 API로 변환하면 더 직접적으로 기술 & 쉽게 병렬화 가능
menu.stream()
.filter(d -> d.getCalories() > 300)
.map(Dish::getName)
.collect(toList());
조건부 연기 실행 - 코드 가독성, 캡슐화
// 1. 행위 캡슐화: Supplier<String> 인터페이스로 로그 생성 로직을 감싸기
logger.log(Level.FINER, () -> "Problem : " + generateDiagnostic()); // 실제 실행 지연
public void log(Level level, Supplier<String> msgSupplier){
if(logger.isLoggable(level)){ // 2. 조건 검증: 로그 레벨 활성화 상태 확인
// 3. 필요시 실행: 조건 충족 시에만 get() 호출로 실제 로그 생성
log(level, msgSupplier.get()); // 람다 실행
}
}
실행 어라운드 - 재사용성
String oneLine = processFile((BufferedReader b) -> b.readline()); //람다 전달
public static String processFile(BufferedReaderProcessor p) throws IOException {
try(BufferedReader br = new BufferedReader(new fileReader("data.txt"))) {
return p.process(br); //인수로 전달된 BufferedReaderProcessor 실행
}
}
public interface BufferedReaderProcessor {
string process(BufferedReader b) throws IOException;
}
람다 표현식과 함수형 인터페이스를 이용하면 일부 디자인 패턴은 더 간결하게 표현 가능
ValidationStrategy
가 함수형 인터페이스이므로 람다로 간결하게 대체 가능 // 기존
public interface ValidationStrategy {
boolean execute(String s);
}
public class IsNumeric implements ValidationStrategy {
public boolean execute(String s) {
return s.matches("\\d+");
}
}
public class Validator {
private final ValidationStrategy strategy;
public Validator(ValidationStrategy strategy) {
this.strategy = strategy;
}
public boolean validate(String s) {
return strategy.execute(s);
}
}
// 람다로 리팩토링
Validator numericValidator = new Validator(s -> s.matches("\\d+"));
// 기존
abstract class OnlineBanking {
public void processCustomer(int id) {
Customer c = Database.getCustomerWithId(id);
makeCustomerHappy(c);
}
abstract void makeCustomerHappy(Customer c);
}
// 람다로 리팩토링
public class OnlineBankingLambda {
public void processCustomer(int id, Consumer<Customer> makeCustomerHappy) {
Customer c = Database.getCustomerWithId(id);
makeCustomerHappy.accept(c);
}
}
new OnlineBankingLambda().processCustomer(1337,
c -> System.out.println("Hello " + c.getName()));
// 기존 구현
interface Observer {
void notify(String tweet);
}
class NYTimes implements Observer {
public void notify(String tweet) {
if(tweet != null && tweet.contains("money")) {
System.out.println("Breaking news in NY! " + tweet);
}
}
}
// Subject 인터페이스의 정의다.
interface Subject {
void registerObserver(Observer o);
void notifyObservers(String tweet);
}
class Feed implements Subject {
private final List<Observer> observers = new ArrayList<>();
// 새로운 옵저를 등록한다.
public void registerObserver(Observer o) {
this.observers.add(o);
}
// 트윗을 등록한 옵저들에게 알린다.
public void notifyObservers(String tweet) {
observers.forEach(o -> o.notify(tweet));
}
}
Feed f = new Feed();
f.registerObserver(new NYTimes());
f.notifyObservers("The queen said her favorite book is Java 8 in Action!");
// 람다로 리팩토링
Feed f = new Feed();
feed.registerObserver(tweet -> {
if (tweet.contains("money")) {
System.out.println("Breaking news in NY! " + tweet);
}
});
Function
의 andThen
메서드를 활용하면 체인 구조를 명확하게 표현Supplier
를 활용하면 조건문 없이 동적으로 객체를 생성할 수 있다.// 기존
public class ProductFactory {
public static Product createProduct(String name) {
switch (name) {
case "loan": return new Loan();
case "stock": return new Stock();
case "bond": return new Bond();
default: throw new RuntimeException("No such product " + name);
}
}
}
// 람다로 리팩토링
Map<String, Supplier<Product>> productMap = new HashMap<>();
productMap.put("loan", Loan::new);
productMap.put("stock", Stock::new);
productMap.put("bond", Bond::new);
람다 자체를 직접 테스트하지 않고 람다가 사용된 메서드/함수의 결과를 검증하는 것이 핵심
public static List<Point> moveAllPointsRightBy(List<Point> points, int x) {
return points.stream().map(p -> new Point(p.getX() + x, p.getY())).collect(toList());
}
함수를 인자로 받는 메서드는 다양한 람다 케이스로 검증하자
// 필터 조건을 달리한 여러 테스트 케이스
filter(numbers, i -> i%2==0) // 짝수 검증
filter(numbers, i -> i<3) // 3 미만 검증
람다 표현식과 스트림은 기존 디버깅 무시
stacktrace 확인
정보 logging
List<Integer> result = numbers.stream()
.peek(x -> System.out.println("From stream: " + x))
.map(x -> x + 17)
.peek(x -> System.out.println("after map: " + x))
.filter(x -> x % 2 == 0)
.peek(x -> System.out.println("after filter: " + x))
.lmit(3)
.peek(x -> System.out.println("after limit: " + x))
.collect(toList());
// result
from stream: 2
after map: 19
from stream: 3
after map: 20
after filter: 20
after limit: 20
from stream: 4
after map: 21
from stream: 5
after map: 22
after filter: 22
after limit: 22
프로그래밍에서 "코드가 마치 문장처럼 읽히면 좋겠다"는 바람.
개발팀과 도메인 전문가가 이해할 수 있는 코드는 생산성과 직결되기 때문에 코드는 읽기 쉽고 이해하기 쉬워야 한다.
=> 도메인 전용 언어(Domain-Specific Language)**는 이런 요구를 해결하기 위한 방법 중 하나
장단점
구분 | 내부 DSL | 외부 DSL | 다중 DSL |
---|---|---|---|
의존성 | 호스트 언어에 종속 (예: Java/Kotlin) | 독립적인 문법 체계 (예: SQL) | JVM 호환 언어 혼용 (예: Scala+Java) |
구현 | 호스트 언어의 API 확장 | 별도 파서/컴파일러 필요 | 언어 간 인터페이스 브리징 |
예시 | Querydsl, Kotlin DSL | SQL, HTML/CSS | Scala+Java 조합 |
// 외부 DSL (SQL)
SELECT name FROM users WHERE age > 18;
// 내부 DSL (JOOQ)
DSLContext create = DSL.using(SQLDialect.POSTGRES);
Result<Record> result = create.select(USER.NAME)
.from(USER)
.where(USER.AGE.gt(18))
.fetch();
// Stream API
List<String> highCaloricDishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.map(Dish::getName)
.collect(toList());
// DateTimeFormatter
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
.appendText(ChronoField.DAY_OF_MONTH)
.appendLiteral(". ")
.appendText(ChronoField.MONTH_OF_YEAR)
.appendLiteral(" ")
.appendText(ChronoField.YEAR)
.toFormatter();
마치 미니 언어를 자바 위에 얹은 듯한 효과를 준다.
각 메서드가 자신을 반환하면 연속 호출이 가능합니다.
builder.add("milk").add("sugar").add("coffee");
객체 생성 과정을 단계적으로 표현할 수 있습니다.
Order order = new OrderBuilder()
.forCustomer("BigBank")
.buy(100).stock("IBM").on("NYSE").at(125.00)
.sell(50).stock("GOOGLE").on("NASDAQ").at(375.00)
.end();
람다로 계층 구조를 표현할 수 있습니다.
Order order = order(o -> {
o.forCustomer("BigBank");
o.buy(t -> {
t.quantity(100);
t.stock("IBM");
t.market("NYSE");
t.price(125.00);
});
});
Order order = order(
forCustomer("BigBank"),
buy(100, stock("IBM", on("NYSE")), at(125.00)),
sell(50, stock("GOOGLE", on("NASDAQ")), at(375.00))
);
assertThat(frodo.getName()).isEqualTo("Frodo");
assertThat(frodo.getAge()).isBetween(30, 40);
// 추가로
// RestAssured DSL 스타일
given()
.param("query", "dsl")
.when()
.get("/search")
.then()
.statusCode(200)
.body("results.size()", greaterThan(0));
자바는 원래 DSL을 만들기에는 문법적 제약이 많은 언어였지만 자바 8부터는 람다, 메서드 참조, 스트림 API, 메서드 체이닝 등 덕분에 DSL 스타일의 API 작성이 훨씬 수월해졌다.
DSL은 다음과 같은 상황에서 유용하다:
자바에서 DSL을 구현할 때는 유연성과 가독성의 균형을 신중히 고려해야한다. 너무 복잡하게 설계하면 오히려 코드가 읽기 어려워질 수 있기 때문.