코드 가독성 개선
코드 가독성이란 일반적으로 코드를 다른사람도 쉽게 이해할 수 있는 정도라고 생각하면 될 것 같다.
즉, 코드 가독성을 개선한다는 것은 우리가 구현한 코드를 다른 사람이 쉽게 이해하고 유지보수 할 수 있게 만드는 것을 의미한다.
코드의 가독성을 높이려면 코드의 문서화를 잘하고, 표준 코딩 규칠을 준수하는 등의 노력을 기울여야 한다.
익명 클래스를 람다 표현식으로 리팩터링 하기
하나의 추상 메서드를 구현하는 익명 클래스는 람다 표현식으로 리팩터링 할 수 있다.
// 익명 클래스 사용
Runnable r1 = new Runnable() {
public void run() {
System.out.println("Run!!");
}
}
// 람다 표현식 사용
Runnable r2 = () -> System.out.println("Run!!");
하지만 모든 익명 클래스를 람다 표현식으로 변환할 수 있는 것은 아니다.
- 익명 클래스에서 사용한 this와 super는 람다 표현식에서 다른 의미를 갖는다.
익명 클래스에서 this는 자신을 가리키지만 람다에서 this는 람다를 감싸는 클래스를 가리킨다. - 익명 클래스는 감싸고 있는 클래스의 변수를 가릴 수 있다.(shadow variable)
// 익명 클래스 사용
Runnable r1 = new Runnable() {
public void run() {
int a = 2;
System.out.println(a);
}
}
// 람다 표현식 사용
Runnable r2 = () -> {
int a = 2; // 컴파일 에러
System.out.println(a);
}
익명 클래스는 인스턴스화할 때 명시적으로 형식이 정해지는 반면, 람다의 형식은 콘텍스트에 따라 달라지기 때문에 익명 클래스를 람다 표현식으로 바꾸면 콘텍스트 오버로딩에 따른 모호함이 초래된다.
만약 Runnable과 Task 인터페이스를 인수로 받는 메서드가 오버로딩되어있다고 가정해보자.
interface Task {
pubilc void execute();
}
public static void doSomething(Runnable r) { r.run(); }
public static void doSomething(Task t) { t.execute(); }
Task를 구현하는 익명 클래스는 아래처럼 전달이 가능하다.
doSomething(new Task() {
public void execute() {
System.out.println("execute!!");
}
});
하지만 람다 표현식으로 바꾸면 메서드를 호출할 때 Runnable과 Task중 어느 것을 가리지는지 알 수 없는 모호함이 생긴다.
doSomething(() -> System.out.println("what method!?"));
이럴때는 명시적 형변환을 이용해야 한다.
그리고 IDE에서는 리팩터링 기능을 이용하면 이와 같은 문제를 쉽게 해결할 수 있다.
doSomething((Task)() -> System.out.println("what method!?"));
람다 표현식을 메서드 참조로 리팩터링하기
람다 표현식은 쉽게 전달할 수 있는 짧은 코드이지만 람다 표현식 대신 메서드 참조를 이용하면 가독성을 높일 수 있다.
public class Food {
private String name;
private Integer calories;
}
List<Food> foods = List.of(
new Food("셀러드", 200),
new Food("치킨", 1000),
new Food("피자", 1300)
);
// 람다 표현식
Map<CaloricLevel, List<Food>> foodsByCaloricLevel = foods.stream()
.collect(Collectors.groupingBy(food -> {
if (food.getCalories() <= 400) return CaloricLevel.DIET;
else if (food.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
}));
메서드 참조를 이용하면 메서드명으로 코드의 의도를 명확하게 알릴 수 있다.
public class Food {
private String name;
private Integer calories;
public CaloricLevel getCaloricLevel() {
if (this.getCalories() <= 400) return CaloricLevel.DIET;
else if (this.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
}
}
// 메서드 참조
Map<CaloricLevel, List<Food>> foodsByCaloricLevel = foods.stream()
.collect(Collectors.groupingBy(Food::getCaloricLevel));
명령형 데이터 처리를 스트림으로 리팩터링하기
이론적으로는 반복자를 이용한 기존의 모든 컬렉션 처리 코드를 스트림 API로 바꿔야 한다.
스트림 API는 데이터 처리 파이프라인의 의도를 더 명확하게 보여주고 최적화뿐 아니라 멀티고어 아키텍처를 활용할 수 있는 지름길을 제공한다.
List<String> foodNames = new ArrayList<>();
for (Food food: foods) {
if (food.getCalories() > 300) {
foodNames.add(food.getName());
}
}
위 코드처럼 작성한다면 전체 코드를 살펴봐야 코드의 의도를 이해할 수 있을 것이다. 게다가 병렬로 실행시키는 것은 매우 어렵다.
스트림 API를 이용하면 더 직접적으로 기술할 수 있을 뿐 아니라 쉽게 병렬화가 가능하다.
List<String> foodNames = foods.parallelStream()
.filter(food -> food.getCalories > 300)
.map(Food::getName)
.collect(toList());
명령형 코드의 break, continue, return 등의 제어 흐름문을 모두 분석해서 같은 기능을 수행하는 스트림 연산으로 유추해야 하므로 며령형 코드를 스트림 API로 바꾸는 것은 쉬운일이 아니다.
코드 유연성 개선
람다 표현식을 이용하면 동작 파라미터화를 쉽게 구현할 수 있다.
즉, 다양한 람다를 전달해서 다양한 동작을 표현할 수 있다. 따라서 변화하는 요구사항에 대응할 수 있는 코드를 구현할 수 있다.
함수형 인터페이스 적용
먼저 람다 표현식을 이용하려면 함수형 인터페이스가 필요하다. 따라서 함수형 인터페이스를 코드에 추가해야 한다.
조건부 연기 실행
실제 작업을 처리하는 코드 내부에 제어 흐름문이 복잡하게 얽힌 코드를 흔히 볼 수 있다.
흔히 보안 검사나 로깅 관련 코드가 이처럼 사용된다. 다음은 내장 자바 Logger 클래스를 사용하는 예제다.
if (logger.isLoggable(Log.FINER)) {
logger.finer(" ... ");
}
위 코드의 문제는 logger의 상태가 isLoggable이라는 메서드에 의해 클라이언트 코드로 노출된다.
그리고 메세지를 로깅할 때마다 logger 객체 상태를 매번 확인하고 있다.
다음처럼 메시지를 로깅하기 전에 logger 객체가 적절한 수준으로 설정되었는지 내부적으로 확인하는 log 메서드를 사용하는 것이 바람직하다.
logger.log(Level.FINER, " ... ");
덕분에 불필요한 if문을 제거할 수 있으며 looger의 상태를 노출할 필요도 없으므로 위 코드가 더 바람직한 구현이다.
하지만 logger가 활성화되어 있지 않더라도 항상 로깅 메시지를 남기게 된다.
람다를 이용하면 이 문제를 쉽게 해결할 수 있다.
특정 조건에서만 로그를 남길수 있도록 생성 과정을 연기할 수 있어야 한다.
다음은 새로 추가된 log 메서드의 시크니처다.
public void log(Level level, Supplier<Strng> msgSupplier);
다음처럼 log 메서드를 호출할 수 있다.
logger.log(Level.FINER, () -> " ... ");
// 내부 구현 코드
public void log(Level level, Supplier<String> msgSupplier) {
if (logger.isLoggable(level)) {
log(level, msgSupplier.get();
}
}
만일 클라이언트 코드에서 객체 상태를 자주 확이노하거나 객체의 일부 메서드를 호출하는 상황이라면 내부적으로 객체의 상태를 확인한 다음에 메서드를 호출하도록 구현하는 것이 가독성과 캡슐화에 도움이 된다.
실행 어라운드
매번 같은 준비, 종료 과정을 반복적으로 수행하는 코드가 있다면 이를 람다로 변환할 수 있다.
public interface AddNumberProcessor {
int process(int a, int b);
}
public int addNumber(int a, int b, AddNumberProcessor p) {
System.out.println("addNumber!!");
return p.process(a, b);
}
int result1 = addNumber(1, 1, (a, b) -> (a + b))
System.out.println("result1 : " + result1);
// 출력 결과 :
// addNumber!!
// result1 : 2
int result2 = addNumber(1, 1, (a, b) -> (a + b) + (a + b));
System.out.println("result2 : " + resuresult2lt1);
// 출력 결과 :
// addNumber!!
// result2 : 4
위 코드를 보면 addNumber!!를 출력하는 코드를 함께 사용하고 있다.
이처럼 준비, 종료 과정을 처리하는 로직을 재사용함으로써 코드 중복을 줄일 수 있다.
'Java' 카테고리의 다른 글
[모던 자바] 람다 표현식과 스트림 테스팅과 디버깅 (0) | 2022.03.14 |
---|---|
[모던 자바] 디자인 패턴 소개 및 람다를 이용해 리팩터링 하기 (0) | 2022.03.11 |
[모던 자바] 컬렉션 팩토리(Collection Factory) 소개 및 사용법 (0) | 2022.03.10 |
[모던 자바] Spliterator 인터페이스란 무엇인가? (0) | 2022.03.10 |
[모던 자바] 포크/조인 프레임워크란 무엇인가? (0) | 2022.03.10 |