코드 가독성, 코드 유연성
일반적으로 코드가독성이 좋다는 것은 어떤 코드를 다른 사람이 쉽게 이해할 수 있음을 의미한다. 코드 가독성을 높이기 위해서는 코드 문서화를 잘하고, 표준 코딩 규칙을 잘 준수하는 것이 중요하다.
일반적으로 코드유연성은 변화하는 요구사항에 대응할 수 있음을 의미한다. 유연성을 높이기 위해서는 앞에서 살펴본 동작파라미터화 등을 사용할 수 있다.
리팩토링
1. 익명 클래스를 람다 표현식으로 리팩토링
Runnable r1 = new Runnable() {
pubilc void run() {
System.out.println("Hello");
}
}
Runnable r2 = () -> System.out.println("Hello");
위 코드를 보았을 때, 익명 클래스를 람다로 변경하면, 불필요한 코드가 제거되어 간결하고 가독성이 좋은 코드로 변경할 수 있다.
그러나, 익명 클래스와 람다 간에는 차이점이 존재하기 때문에 그 점을 주의하여 리팩토링해야 한다. 첫번째로 this, super의 의미가 다르다. 익명클래스에서 this는 자신을 가리키지만, 람다에서 this는 자신을 감싸는 클래스를 가리킨다. 두번째로는 익명클래스는 감싸고 있는 클래스의 변수를 가릴 수 있지만, 람다 표현식은 가릴 수 없다. 따라서 아래 코드는 에러가 발생한다.
int a = 10;
Runnable r1 = () -> {
int a = 2;
System.out.println(a);
}
또한 람다를 사용할 경우 초래되는 문제점이 있는데, 그는 모호함이다. 람다의 형식은 콘텍스트에 따라서 정해지기 때문이다. 만약에 Runnable과 동일한 시그니처를 가진 다른 Task 함수형 인터페이스를 만든다고 가정하면, 콘텍스트에서 () -> () 대상형식을 만족하는 람다가 있을 때, 둘 중 어떤 형식을 가져야 하는지 모호할 수 있다. 이런 경우에는 명시적 형변환을 통해서 해결할 수 있다.
interface Task {
public void execute();
}
public static void doSomething(Runnable r) {r.run()}
public static void doSomething(Task t) {t.execute()}
doSomething(() -> System.out.prinln("Hello")); // 어떤 함수형 인터페이스 형식인지 모호
doSomething((Task)() -> System.out.prinln("Hello")); // 명시적 형변환
2.람다 표현식을 메서드 참조로 리팩토링
람다를 사용할 때, 간단한 내용은 괜찮지만 함수의 동작이 길어지고 복잡해지면 람다를 사용해도 가독성이 떨어지는 경우가 많다. 그런 경우에는 람다의 내용을 메서드로 추출하고 해당 메서드를 참조하면 코드도 간결해지고 메서드명을 사용하기 때문에 어떤 동작을 하는지 의미전달도 명확해져서 가독성을 높일 수 있다.
Map<CaloricLevel, List<Dish>> dishes= menu.stream().collect(groupingBy(Dish::getCaloricLevel));
sum, maxium과 같이 기본으로 제공되는 내장 헬퍼 메서드를 함께 사용하면 더더욱 가독성을 높일 수 있다. 리듀스를 사용할 때도 스트림을 통해서 하는 것보다 내장 컬렉터를 사용하면 메서드명이 어떤 동작을 하는지 더 잘설명해주어 가독성을 더 높일 수 있다.
int totalCalories = menu.stream().map(Dish::getCalories).reduce(0, (c1, c2) -> c1 + c2);
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
3. 명령형 데이터 처리를 스트림으로 리팩토링하기
명령형 데이터 처리 즉, for-each 반복자를 통한 데이터 처리는 코드의 길이도 길고 복잡해서 한눈에 어떤 동작을 하는 것인지 이해하기가 어렵다. 따라서 이를 스트림으로 변경하면 쇼트서킷과 게으름이라는 특징을 통해서 최적화도 가능하고, 가독성도 좋아진다. 또한 병렬성도 쉽게 얻을 수 있다.
4. 함수형 인터페이스 적용
우리는 변화하는 요구사항에 대응하기 위해서 동작파라미터화를 사용할 수 있는데, 동작파라미터화를 사용하기 위해서는 함수형인터페이스를 선언하는 것이 필요하다. 이를 사용하면 코드유연성을 높일 수 있는데, 앞에서 살펴보았던 실행어라운드 패턴은 파일을 열고 닫는 부분은 이미 선언해두고 그 사이에 실행할 로직만 동작파라미터화하여 유연성을 높였었다. 조건부 연기 실행에서도 이런 특징을 확인할 수 있다.
기존 코드를 보면 로그 레벨을 확인하고 실제 동작을 실행시키는 것을 확인할 수 있다. 기존 코드를 사용하게 되면 로그를 사용할 때마다 로그의 레벨을 확인하고 로깅을 해야 하는 문제점이 있고, logger의 상태가 외부로 노출되는 문제가 있다. 이 문제를 해결하기 위해서 동작파라미터화를 적용하여 구성하게 되면, 로그를 사용할 때마다 로그 레벨을 평가하지 않아도 되고, 필터를 통과한 경우에만 메세지를 생성하기 때문에 효율적이다. 또한 클라이언트 코드에 객체의 상태가 노출되지 않아 안전하다.
// 기존 코드
if (logger.isLoggable(Log.FINER)) {
logger.finer("problem: " + generateDiagnostic());
}
// 동작 파라미터화 적용한 코드
public void log(Level level, Supplier<String> msgSupplier){
if (logger.isLoggable(level)) {
log(level, msgSupplier.get());
}
}
logger.log(Level.FINER, () -> "Problem: " + generateDiagnostic());
람다로 전략패턴 리팩토링하기
전략패턴이란 한 유형의 알고리즘을 보유한 상태에서 런타임에 적합한 알고리즘을 선택하여 사용하는 기법이다. 아래처럼 기준이 되는 인터페이스가 존재하고 이를 구현하는 여러 구현체들이 존재한다. client에서는 런타임에 적합한 구현체를 선택하여 사용한다.

람다를 사용하지 않으면 기본 Strategy interface 클래스, concreteStrategyA, concreteStrategyB 구현체를 모두 구현해야 한다. 하지만 람다를 사용하면 해당 함수형 인터페이스를 구현한 객체의 인스턴스로 간주되기 때문에 concreteStrategyA, concreteStrategyB 구현체를 구현하는 과정을 생략할 수 있다.
private static class Validator {
private final ValidationStrategy strategy;
public Validator(ValidationStrategy v) {
this.strategy = v;
}
public boolean validate(String s) {
return this.strategy.execute(s);
}
}
interface ValidationStrategy {
boolean execute(String var1);
}
private static class IsNumeric implements ValidationStrategy {
private IsNumeric() {
}
public boolean execute(String s) {
return s.matches("\\d+");
}
}
private static class IsAllLowerCase implements ValidationStrategy {
private IsAllLowerCase() {
}
public boolean execute(String s) {
return s.matches("[a-z]+");
}
}
public static void main(String[] args) {
Validator v1 = new Validator(new IsNumeric());
System.out.println(v1.validate("aaaa"));
Validator v2 = new Validator(new IsAllLowerCase());
System.out.println(v2.validate("bbbb"));
Validator v3 = new Validator((s) -> {
return s.matches("\\d+");
});
System.out.println(v3.validate("aaaa"));
Validator v4 = new Validator((s) -> {
return s.matches("[a-z]+");
});
System.out.println(v4.validate("bbbb"));
}
람다로 템플릿메소드패턴 리팩토링하기

템플릿 메소드 패턴이란 여러 클래스에서 공통으로 사용하는 메서드를 템플릿화 하여 상위 클래스에서 정의하고, 하위 클래스마다 세부 동작 사항을 다르게 구현하는 패턴이다.
// 기존 버전
abstract class OnlineBanking {
OnlineBanking() {}
public void processCustomer(int id) {
Customer c = OnlineBanking.Database.getCustomerWithId(id);
this.makeCustomerHappy(c);
}
abstract void makeCustomerHappy(Customer var1);
}
// 람다 사용 버전
public class OnlineBankingLambda {
public OnlineBankingLambda() {}
public void processCustomer(int id, Consumer<Customer> makeCustomerHappy) {
Customer c = OnlineBankingLambda.Database.getCustomerWithId(id);
makeCustomerHappy.accept(c);
}
}
public static void main() {
(new OnlineBankingLambda()).processCustomer(1337, (c) -> {
System.out.println("Hello!");
});
}
람다로 옵저버패턴 리팩토링하기

옵저버 패턴은 어떤 이벤트가 발생했을 때, 특정 객체(주제)가 다른 객체 리스트(옵저버)에 알림을 보내는 상황에 사용되는 패턴이다.
interface Subject {
void registerObserver(Observer var1);
void notifyObservers(String var1);
}
private static class Feed implements Subject {
private final List<Observer> observers;
private Feed() {
this.observers = new ArrayList();
}
public void registerObserver(Observer o) {
this.observers.add(o);
}
public void notifyObservers(String tweet) {
this.observers.forEach((o) -> {
o.inform(tweet);
});
}
}
interface Observer {
void inform(String var1);
}
private static class NYTimes implements Observer {
private NYTimes() {}
public void inform(String tweet) {
if (tweet != null && tweet.contains("money")) {
System.out.println("Breaking news in NY!" + tweet);
}
}
}
public static void main(String[] args) {
Feed f = new Feed();
f.registerObserver(new NYTimes());
f.notifyObservers("The queen said her favourite book is Java 8 & 9 in Action!");
Feed feedLambda = new Feed();
feedLambda.registerObserver((tweet) -> {
if (tweet != null && tweet.contains("money")) {
System.out.println("Breaking news in NY! " + tweet);
}
});
}
람다로 의무체인패턴 리팩토링하기

의무체인패턴은 작업 처리 객체의 체인 동작을 만들 때 사용한다. handle이라는 메소드의 시그니처가 Function과 동일하기 때문에 Function의 andThen을 통해서 체이닝할 수 있다. 즉, 람다를 사용하게 되면 ProcessingObject도 작성할 필요가 없어진다.
private abstract static class ProcessingObject<T> {
protected ProcessingObject<T> successor;
private ProcessingObject() {
}
public void setSuccessor(ProcessingObject<T> successor) {
this.successor = successor;
}
public T handle(T input) {
T r = this.handleWork(input);
return this.successor != null ? this.successor.handle(r) : r;
}
protected abstract T handleWork(T var1);
}
private static class HeaderTextProcessing extends ProcessingObject<String> {
private HeaderTextProcessing() {
super(null);
}
public String handleWork(String text) {
return "From Raoul, Mario and Alan: " + text;
}
}
private static class SpellCheckerProcessing extends ProcessingObject<String> {
private SpellCheckerProcessing() {
super(null);
}
public String handleWork(String text) {
return text.replaceAll("labda", "lambda");
}
}
public static void main(String[] args) {
ProcessingObject<String> p1 = new HeaderTextProcessing();
ProcessingObject<String> p2 = new SpellCheckerProcessing();
p1.setSuccessor(p2);
String result1 = (String)p1.handle("Aren't labdas really sexy?!!");
System.out.println(result1);
UnaryOperator<String> headerProcessing = (text) -> {
return "From Raoul, Mario and Alan: " + text;
};
UnaryOperator<String> spellCheckerProcessing = (text) -> {
return text.replaceAll("labda", "lambda");
};
Function<String, String> pipeline = headerProcessing.andThen(spellCheckerProcessing);
String result2 = (String)pipeline.apply("Aren't labdas really sexy?!!");
System.out.println(result2);
}
람다 테스팅
1. 보이는 람다 표현식의 동작 테스팅
람다는 함수형 인터페이스의 인스턴스를 생성한다. 따라서 우리는 인스턴스의 동작으로 람다 표현식을 테스트할 수 있다.
public class Point {
public final static Comparator<Point> compareByXAndThenY = comparing(Point::getX).thenComparing(Point::getY);
...
}
@Test
public void test comparingTwoPoints() {
Point p1 = new Point(10, 15);
Point p2 = new Point(10, 20);
int reuslt = Point.compareByXAndThenY.compare(p1, p2);
assertTrue(result < 0);
}
2. 람다를 사용하는 메서드의 동작에 집중하라
public static List<Point> moveAllPointsRightBy(List<Point> points, int x){
return points.stream().map(p -> new Points(p.getX() + x, p.getY()).collect(toList());
}
@Test
public void testMoveAllPointsRightBy() {
List<Point> points = Array.asList(new Point(5, 5), new Point(10, 5));
List<Point> expectedPoints = Array.asList(new Point(10, 5), new Point(20, 5));
List<Point> newPoints = Point.moveAllPointsRightBy(points, 10);
assertEquals(expectedPoints, newPoints);
}
우리가 테스트하고 싶은 람다는 p -> new Points(p.getX() + x, p.getY()이지만 이 람다를 직접적으로 테스트하는 것이 아니라 람다를 사용하는 메서드의 동작을 테스트함으로써 간접적으로 람다를 검증한다.
3. 복잡한 람다를 개별 메서드로 분할하기
복잡한 람다를 개별 메서드로 분할하여 일반 메서드를 테스트하듯이 람다 표현식을 테스트한다.
4. 고차원 함수 테스팅
함수를 인수로 받거나, 다른 함수를 반환하는 메서드가 있다. 함수를 인수로 받는 경우에는 다른 람다를 통해서 동작을 테스트한다. 다름 함수를 반환하는 경우는 1번에서 설명한 것처럼 함수형 인터페이스의 인스턴스로 간주하여 테스트하는 방법이 있다.
디버깅
우리는 보통 디버깅을 할 때 스택트레이스와 로깅을 사용한다. 그런데, 람다를 사용하는 경우 이 두가지 모두 사용하기 어렵다는 문제가 있다. 람다는 이름이 없기 때문에 스택트레이스르 보더라도 lambda0이런 식으로 표현이 되기 때문에 어디에서 문제가 발생했는지 확인하기가 어렵다. 이는 메소드 참조에서도 동일하게 나타나는 문제점이다. 그러나, 메소드 참조는 메소드가 선언된 클래스와 동일한 곳에서 참조하는 경우에는 메소드의 이름이 표현된다.
만약 스트림에서 각 요소들을 출력하기 위해서 forEach를 사용하면 스트림이 소비되어 더 이상 동작을 할 수 없다. 이럴 때 peek이라는 연산을 활용할 수 있다. peek은 스트림의 각 요소를 소비한 것처럼 동작하지만 실제로 소비하지는 않는다. peek은 자신이 확인한 요소를 파이프라인의 다음 연산으로 그대로 전달한다. 그래서 스트림 연산 파이프라인 단계마다 peek을 붙이면 스트림API가 어떻게 동작하는지 로깅할 수 있다.
public static void main(String[] args) {
List<Integer> result = (List)Stream.of(2, 3, 4, 5).peek((x) -> {
System.out.println("taking from stream: " + x);
}).map((x) -> {
return x + 17;
}).peek((x) -> {
System.out.println("after map: " + x);
}).filter((x) -> {
return x % 2 == 0;
}).peek((x) -> {
System.out.println("after filter: " + x);
}).limit(3L).peek((x) -> {
System.out.println("after limit: " + x);
}).collect(Collectors.toList());
}'자바' 카테고리의 다른 글
| [모던 자바 인 액션] Optional 클래스 (0) | 2025.05.08 |
|---|---|
| [모던 자바 인 액션] 람다를 이용한 도메인 전용 언어 (0) | 2025.05.02 |
| [모던 자바 인 액션] 컬렉션 API 개선 (1) | 2025.05.02 |
| [모던 자바 인 액션] 병렬 데이터 처리와 성능 (0) | 2025.04.25 |
| [모던 자바 인 액션] 스트림으로 데이터 수집 (0) | 2025.04.25 |