본문 바로가기
Programming/Languages (Java, etc)

[Java] Java 8 사용하기 : 가독성과 유연성, 람다와 Streams API

by kghworks 2023. 8. 16.

목차

  • Java 8로 향상되는 가독성과 유연성
  • 익명 클래스 -> 람다 표현식
  • 람다 표현식 -> 메서드 참조
  • 명령적 데이터 가공 -> Streams API
  • 함수형 인터페이스

 

 Java 8 이상의 버전을 사용하면서 legacy를 Java 8로 리팩터링 하는 방법을 정리한다. 같은 주제로 2개 이상의 포스팅을 할 건데, 처음에는 Java 8에 추가된 스펙으로 코드 수준에서 어떻게 리팩터링 해내는지 본다. 이후 포스팅에는 디자인 패턴, 테스트, 디버깅 등의 부분에서 Java 8로 개선해 보겠다. 

 

 이 포스팅에서는 Java 8의 주요 스펙에 대해 문법적인 수준의 설명을 생략한다. 필요하다면 oracle java 문서나, 책 modern java in action을 추천한다. 

 

https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html

 

Lambda Expressions (The Java™ Tutorials > Learning the Java Language > Classes and Objects)

The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Java Language Changes for a summary of updated

docs.oracle.com

https://www.oracle.com/technical-resources/articles/java/ma14-java-se-8-streams.html

 

Processing Data with Java SE 8 Streams, Part 1

Use stream operations to express sophisticated data processing queries. What would you do without collections? Nearly every Java application makes and processes collections. They are fundamental to many programming tasks: they let you group and process dat

www.oracle.com

https://product.kyobobook.co.kr/detail/S000001810171

 

모던 자바 인 액션 | 라울-게이브리얼 우르마 - 교보문고

모던 자바 인 액션 | 자바 1.0이 나온 이후 18년을 통틀어 가장 큰 변화가 자바 8 이후 이어지고 있다. 자바 8 이후 모던 자바를 이용하면 기존의 자바 코드 모두 그대로 쓸 수 있으며, 새로운 기능

product.kyobobook.co.kr

 


Java 8로 향상되는 가독성과 유연성

 

 

 코드에서 가독성이라는 것은 주관적이나, 일반적으로 다음과 같은 의미로 해석될 수 있을 것 같다.

 

코드를 작성하지 않은 다른 개발자가 읽었을 때 얼마나 쉽게 이해가능한가

 

Java 8 부터 추가되는 스펙들은 다음과 같은 방법으로 코드 가독성과 유연성을 늘려줄 수 있게 한다.

Java 8 이전 Java 8
익명 클래스 (anonymous class) 람다 표현식 (lamda expressions)
- 람다 표현식 (lamda expressions)
-> 메서드 참조 (method reference)
명령적 데이터 가공 (imperative data processing) Streams API

 

익명 클래스 -> 람다 표현식

 

 익명 클래스(anonymous class)란, 1개의 추상 메서드 (abstract method)를 가지는 클래스이다. client에서 객체 사용 시 익명 객체로 구현하여 한번 쓰고 버려지는 클래스이다. 익명 클래스는 코드 부피가 커지고, 중복 코드가 많아지는 단점을 가진다. 이러한 단점은 아래처럼 lamda로 간단하게 대체가 된다.

 

// anonymous class
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello World");
    }
};

// lamda
Runnable r2 = () -> System.out.println("Hello World");

 

 단, lamda로 대체가 어려운 경우가 있는데, 다음과 같다.

 

  • 익명 클래스 구현체 내부에서 this, super 키워드 사용 (익명클래스의 this는 자기 자신, lamda에선 enclosing class)
  • enclosing class에서 사용한 변수와 동일한 변수 사용
  • overloading
int a = 10;
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        int a = 20;
        System.out.println("Hello World! : "+ a); // Hello World! : 20
    }
};

Runnable r2 = () -> {
    int a = 30; // compile error : Variable 'a' is already defined in the scope
    System.out.println("Hello World");
};

public interface Task {
    public void execute();
}

public static void doSomething(Runnable r) { r.run(); } // overloading
public static void doSomething(Task a) { a.execute(); } // overloading

// anonymous class
doSomething(new Task() {
    @Override
    public void execute() {
        System.out.println("Danger danger!!");
    }
});

// lamda
doSomething(() -> System.out.println("Danger danger!!")); // compile error : reference to doSomething is ambiguous
doSomething((Task)() -> System.out.println("Danger danger!!")); // OK

람다 표현식 -> method reference

 

 method reference는 람다 표현식을 한번 더 간단하게 만든다. 이 때 helper static method (comparint(), maxBy())와 built-in helper method (summingInt(), maxBy(), averagingInt())를 사용할 수 있다.

// lamda
Map<Member.AgeLevel, List<Member>> byTeam = memberList.stream()
    .collect(groupingBy(member -> {
        member.getAgeLevel();
    }));
    
// method reference
Map<Member.AgeLevel, List<Member>> byTeam = memberList.stream()
	.collect(groupingBy(Member::getAgeLevel));
    
// lamda
memberList.sort((Member m1, Member m2) -> m1.getAge().compareTo(m2.getAge()));

// method reference
memberList.sort(comparing(Member::getAge));

// lamda
int totalAge = memberList.stream()
	.map(Member::getAge).reduce(0, (a, b) -> a + b);
  
// method reference + reduction operation + built-in helper method
int totalAge = memberList.stream()
	.collect(summingInt(Member::getAge));

 


명령적 데이터 가공 - > Streams API

 

 개인적으로 Java 8 스펙에서 가장 마음에 드는 부분이다. 기존에는 명령적으로 데이터를 가공했다면, 이제는 Streams API를 사용하여 어떻게 데이터를 가공할지 (how)만 집중한다. 명령적 데이터 가공 (imperative data processing)은 생소한 개념일지라도, Java 개발자라면 당연히 가공하던 아래와 같은 프로세스다. 

// imperative data processing
List<String> memberAespa = new ArrayList<>();
for(Member member : memberList) {
    if(member.getTeam().equals(Aespa)) {
        memberAespa.add(member.getName());
    }
}

 

 데이터 가공시 무엇을 (what)에 집중하게 되고 이러다 보니, 어떻게 가공해야 하는지 (how)보다 what에 해당하는 분의 코드가 장황해지고 중복이 많아지게 된다. Streams API로 작성해보면 아래와 같다.

// Streams API
memberList.stream()
    .filter(member -> member.getTeam().equals(Aespa))
    .map(Member::getName)
    .collect(toList());

 

 for-each문 external-iterative는 사라졌고, 이미 Java API에 구현한 Streams API를 사용해서 간단하게 구현되었다. 성능 최적화가 이미 되어있는 API를 사용한다는 것은 대단히 매력적이고, 안정적이다. 내부적으로 short-circuiting, lazy evaluation, parallel processing을 구현해 두었기 때문에 개발자는 API를 적절하게 사용하면 그만이다. 

 

 다만, Streams API는 lamda 표현식으로 사용하기 때문에, 그 안에서 제어 흐름이 어려운 단점이 있다. (continue(), break(), return)

 


함수형 인터페이스 (Functional interface)

 

 함수형 인터페이스 (funcitonal interface)란 하나의 메서드만을 가진 인터페이스를 말하는데, 람다 표현식에서 객체가 되기 위해 작성된 클래스를 말한다. (@FunctionalInterface 생략 가능) 이미 Java 8에서 많은 함수형 인터페이스들을 추가해 두었고, 개발자는 사용하면 되거나, 필요할 때 직접 만들어 쓰면 된다. 

 

@FunctionalInterface
public interface MemberProcessor {
    String process(Member member);
}

 

 Execute Around란 주요 실행 코드 앞뒤로 중복으로 발생하는 코드 패턴을 말하는데 주로 resource를 열고 닫거나, DB Connection 코드 등에서 많이 보이는 패턴이다. 이 패턴의 앞뒤 중복 코드를 하나의 메서드에 만들고, 주요 실행 코드를 함수형 인터페이스 (MemberProcessor)를 인자로 받아 실행하게 할 수 있다.

@FunctionalInterface
public interface MemberProcessor {
    String process(Member member);
}

public static String processMember(MemberProcessor p) {
    Member member = memberService.getFavoriteMember(); // DB Connection
    return p.process(member);
}

String nameWtihTeam = processMember((Member member) -> member.getName() + " | " + member.getTeam());
String nameWithAge = processMember((Member member) -> member.getName() + " | " + member.getAge());
String nameWIthTeanAndAge = processMember((Member member) -> member.getName() + " | " + member.getTeam() + " | " + member.getAge());

 

 또한 Supplier<String> 를 사용하면 다음과 같이 logging 코드가 간단하고 성능이 우수해질 수 있다.

// 문제점 : client code에서 logging 조건 확인 (코드 중복)
if(logger.isLoggable(Log.FINER)) {
    logger.finer("Problem: " + generateDiagnostic());
}

// 문제점 : logging하지 않아도, generateDiagnostic() 메서드는 실행
logger.log(Level.FINER, "Problem: " + generateDiagnostic());

// 내부적으로 conditional deferred execution
logger.log(Level.FINER, () -> "Problem: " + generateDiagnostic()); // Supplier<String>를 인자로 받음

정리

 코드 수준에서 Java 8 스펙을 사용하여 리팩터링 하는 것을 정리했다. 부분마다 주의사항, 제약사항이 있는데, Java 8을 제대로 구현하려면 반드시 알아두어야 하는 부분이다. 이 포스팅에서는 간단하게 언급하거나 생략한 부분이 많으므로 문서나, 책을 통해 정확히 습득할 필요가 있다.  서두에 언급했듯, Oracle 사이트나, 책 Modern Java in ACTION을 추천한다. 

댓글