1. 불변 컬렉션

 자바 10에서는 불변 컬렉션(unmodifiable collections)와 관련한 몇 가지 변경 사항들이 있다.

 

1.1 copyOf()

 java.util.List, java.util.Map, java.util.Set 각각 copyOf() 라는 새로운 static 메서드가 생겼다. 이는 각 컬렉션의 복제된 불변 컬렉션을 반환한다.

@Test(expected = UnsupportedOperationException.class)
public void whenModifyCopyOfList_thenThrowsException() {
    List<Integer> copyList = List.copyOf(someIntList);
    copyList.add(4);
}

 

1.2 toUnmodifiable*()

 java.util.stream.Collectors는 Stream을 불변 컬렉션으로 collect하기 위한 추가적인 메서드를 List, Map, Set 별로 각각 제공한다.

@Test(expected = UnsupportedOperationException.class)
public void whenModifyToUnmodifiableList_thenThrowsException() {
    List<Integer> evenList = someIntList.stream()
      .filter(i -> i % 2 == 0)
      .collect(Collectors.toUnmodifiableList());
    evenList.add(4);
}

 위의 코드에서처럼 불변 컬렉션에 대해 변경을 시도하면 java.lang.UnsupportedOperationExceptionruntime이 발생할 것이다.

 

2. Optional*.orElseThrow()

 java.util.Optional, java.util.OptionalDouble, java.util.OptionalIntand java.util.OptionalLong 각각 orElseThrow()라는 새로운 메서드가 생겼다(기존에는 Supplier타입의 매개변수를 가진 단항함수만 존재했다). Optional 객체 내에 어떠한 값도 없을 경우, NoSuchElementException을 발생시킨다.

@Test
public void whenListContainsInteger_OrElseThrowReturnsInteger() {
    Integer firstEven = someIntList.stream()
      .filter(i -> i % 2 == 0)
      .findFirst()
      .orElseThrow();
    is(firstEven).equals(Integer.valueOf(2));
}

 이 메서드는 기존의 get() 메서드와 완전히 동일하며, 더 선호되는 방식이다.

 

3. 컨테이너 인식

 JVM은 이제 도커 컨테이너에서 실행되는 것을 인식하고 운영 체제 자체를 쿼리하는 대신 컨테이너별 구성을 추출한다. 이는 컨테이너에 할당된 CPU의 수 및 총 메모리와 같은 데이터에 적용된다. 그러나 이 기능은 Linux 기반 플랫폼에서만 사용할 수 있다. 이 기능은 기본적으로 활성화되어 있으며 JVM 옵션을 사용하여 명령줄에서 사용하지 않도록 설정할 수 있다. 

-XX:-UseContainerSupport

 

 또한 이 변경 사항은 JVM이 사용할 CPU 수를 지정하는 기능을 제공하는 JVM 옵션을 추가한다.

-XX:ActiveProcessorCount=count

 또한 도커 컨테이너 사용자가 Java 힙에 사용될 시스템 메모리의 양을 보다 세밀하게 제어할 수 있도록 세 가지 새로운 JVM 옵션이 추가되었다.

-XX:InitialRAMPercentage
-XX:MaxRAMPercentage
-XX:MinRAMPercentage

 

4. 사용 안 함 및 제거(Deprecations and Removals)

4.1 커멘드 라인 옵션, 툴

 native 메서드를 구현하기 위해 사용되는 C 헤더와 소스 파일을 생성하기 위해 사용되었던 'javah' 기능이 제거되었다. 대신 'javac -h' 를 사용할 수 있다.

 policytool은 정책 파일 생성 및 관리를 위한 UI 기반 도구인데 제거되었다. 사용자는 간단한 텍스트 편집기를 사용하여 이 작업을 수행할 수 있다.

 Java -Xprofoption이 제거되었다. 이 옵션은 실행 중인 프로그램을 프로파일링하고 프로파일링 데이터를 표준 출력으로 전송하는 데 사용되었다. 사용자는 이제 대신 jmap을 사용해야 한다.

 

4.2 APIs

 Deprecated된 java.security.acl 패키지는 @Deprecated(forRemoval = true)로 표시되었으며, 이는 이후 자바 버전에서 제거될 수 있다. 이것은 java.security.Policy 와 관련된 클래스로 대체되었다.

 비슷하게 java.security.{Certificate,Identity,IdentityScope,Signer} 클래스들도 forRemoval = true로 표시되었다.

 

5. 시간 기반 릴리즈 버전 관리

 Oracle은 Java 10을 시작으로 시간 기반 Java 릴리즈로 전환했다. 여기에는 다음과 같은 뜻이 있다.

 

1) 새로운 Java 릴리즈는 6개월마다 제공된다. 2018년 3월 릴리즈는 JDK 10이라면, 9월 릴리즈는 JDK 11가 되는 식이다. 이러한 방식을 기능 릴리즈라고 하며, 하나 또는 두 개의 중요한 기능이 추가되어야 한다. 

2) 기능 릴리즈는 다음 릴리즈가 나오는 6개월 동안만 지원된다.

3) 장기 지원 릴리즈(Long Term Support)는 LTS라고 표시될 것이다. LTS에 대한 지원 기간은 3년이고 Java 11이 LTS 버전이다.

 

 'java -version' 명령어는 이제 GA Date(General Availability Date)를 표시해줄 것이다. 이는 사용자의 JDK가 얼마나 오래되었는지를 쉽게 확인하는 것을 가능하게 해줄 것이다.

$ java -version
openjdk version "10" 2018-03-20
OpenJDK Runtime Environment 18.3 (build 10+46)
OpenJDK 64-Bit Server VM 18.3 (build 10+46, mixed mode)

 

※ 참조

https://www.baeldung.com/java-10-overview

1. 개요

 이 글에서는 Java 10 릴리스와 함께 제공되는 성능 향상에 대해 설명한다. 이러한 개선 사항은 JDK 10에서 실행되는 모든 응용 프로그램에 적용되므로, 이를 활용하기 위해 코드를 별도로 변경할 필요는 없다.

 

2. G1 가비지 컬렉터를 이용한 병렬 Full GC(Garbage Collection)

 G1 가비지 컬렉터는 JDK 9부터 디폴트 GC이다. 그러나 G1을 이용한 Full GC는 싱글쓰레드 mark-sweep-compact 알고리즘을 사용했다. Java 10에서부터 병렬 mark-sweep-compact 알고리즘을 사용하도록 변경되었으며, 이는 Full GC에서의 정지시간(stop-the-world time)을 효과적으로 줄여주었다.

 

3. 애플리케이션 클래스 데이터 공유

 JDK 5부터 도입된 클래스 데이터 공유 기능을 통해, 일련의 클래스들은 공유되는 아카이브 파일 안에 사전 처리되어 런타임에 메모리 매핑될 수 있다. 이는 스타트업 시간을 줄일 수 있으며, 여러 JVM이 같은 아카이브 파일을 공유할 경우 동적 메모리 공간을 줄일 수 잇다.

 CDS(Class Data Sharing)은 오직 부트스트랩 클래스 로더에만 허용되다보니, 이 기능은 시스템 클래스에만 제한되었다. AppCDS(Application Class Data Sharing)은 기본 제공 시스템 클래스로더(즉, 앱 클래스 로더), 기본 제공 플랫폼 클래스 로더, 커스텀 클래스 로더가 아카이브 파일을 로드하는 것을 가능하게 했다. 따라서 애플리케이션 클래스에 이 기능을 사용할 수 있다. 

 다음 단계에 따라 이 기능을 사용할 수 있다.

 

3.1 아카이빙할 클래스 목록 가져오기

 다음의 명령어는 HelloWorld 애플리케이션에 의해 로드되는 클래스들을 hello.lst 파일 안에 덤프한다. 

$ java -Xshare:off -XX:+UseAppCDS -XX:DumpLoadedClassList=hello.lst \ 
    -cp hello.jar HelloWorld

 

3.2 AppCDS 아카이브 파일 생성

 다음의 명령어는 hello.lst를 통해 hello.jsa를 만든다.

$ java -Xshare:dump -XX:+UseAppCDS -XX:SharedClassListFile=hello.lst \
    -XX:SharedArchiveFile=hello.jsa -cp hello.jar

 

3.3 AppCDS 아카이브 파일 사용

 다음의 명령어는 hello.jsa와 함께 HelloWorld 애플리케이션을 실행한다.

$ java -Xshare:on -XX:+UseAppCDS -XX:SharedArchiveFile=hello.jsa \
    -cp hello.jar HelloWorld

 AppCDS는 오라클의 JDK 8 및 JDK 9용 상용 기능이었으나, 지금은 오픈 소스이며 공개적으로 사용할 수 있다.

 

4. 실험적인 자바 기반의 JIT(Just In Time) 컴파일러

 Graal은 HotSpot JVM과 통합된 Java로 작성된 동적 컴파일러로, 고성능 및 확장성에 중점을 두고 있다. 이것은 또한 JDK 9에 도입된 실험적인 AOT(Ahead Of Time) 컴파일러의 기초이기도 하다.

 JDK 10은 Graal 컴파일러를 리눅스/x64 플랫폼에서 실험적인 JIT 컴파일러로 사용할 수 있게 한다. Graal을 다음 명령어를 통해 JIT 컴파일러로 사용 가능하다.

-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler

 이것은 실험적인 기능이며, 현존하는 JIT 컴파일러보다 항상 더 나은 성능을 보장하지는 않는다는 것을 주의해야한다.

 

 

* 추가적인 공부가 필요해보이는 항목들 (2021/09/27)

- G1 가비지 컬렉터란 무엇인가? 가비지 컬렉팅 프로세스에 대한 이해. mark-sweep-compact 알고리즘? Full GC

- 시스템 클래스 로더와 플랫폼 클래스 로더의 차이는?

- 현존하는 JIT 컴파일러는 어떻게 사용하는 걸까? 이미 JDK에 적용이 되어있는 건가?

 

 

※ 참조

https://www.baeldung.com/java-10-performance-improvements

1. 지역변수 타입 추론

 Java 10에서 가장 눈에 띄는 변화 중 하나는 초기화를 통한 지역변수 타입 추론이다. 자바 9까지는 지역 변수의 타입을 명시해야 했으며, 그것이 오른쪽에 대입되는 객체와 호환이 가능한지를 확인해야 했다.

String message = "Good bye, Java 9";

 자바 10부터는 다음과 같이 지역변수를 선언할 수 있다.

@Test
public void whenVarInitWithString_thenGetStringTypeVar() {
    var message = "Hello, Java 10";
    assertTrue(message instanceof String);
}

 위에서는 message에 타입을 명시하지 않는다. 대신 var로 표시하고, 컴파일러는 오른쪽에 대입되는 객체를 통해 message의 타입을 추론한다. 위의 예시에서 message의 타입은 String일 것이다. 

 

 이 기능은 초기화가 되는 지역 변수에만 사용할 수 있다. 멤버 변수, 메서드 매개 변수, 반환형 등에 사용할 수 없으며, 컴파일러가 타입을 추론할 수 있게 하기 위해 초기화가 필요하다.

 이 기능은 보일러플레이트 코드를 줄이는데 도움을 준다.

Map<Integer, String> map = new HashMap<>(); // Before Java 10
var idToNameMap = new HashMap<Integer, String>(); // Java 10

 이것은 변수의 타입보다 변수명에 더 집중하는데도 도움을 준다.

 

 주의할 점 중 하나는, var는 키워드가 아니라는 것이다. 이는 'var'를 변수명이나 함수명 등에 이미 사용하여 구현된 프로그램들에 대한 하위 호환성을 보장해준다. var는 int와 마찬가지로 예약어이다.

 또한 var을 사용한다고 해서 런타임에 오버헤드가 발생하지 않으며, 자바가 동적 타입 언어가 된 것도 아니라는 것을 기억해야 한다. 변수의 타입은 컴파일 타임에 추론되며, 나중에 바뀔 수 없다.

 

2. var의 잘못된 사용

 이미 언급했듯이, var은 초기화되지 않는 지역 변수에는 사용될 수 없다.

var n; // error: cannot use 'var' on variable without initializer

 

null로 초기화할 수도 없다. null을 통한 타입 추론이 불가능하기 때문이다.

var emptyList = null; // error: variable initializer is 'null'

 

 

 var은 지역변수에만 사용 가능하다. 멤버 변수 등에는 사용이 불가능하다.

class Hello {
    public var = "hello"; // error: 'var' is not allowed here
}

 

 람다 식의 경우, 명시적인 타겟 타입이 필요하다. 따라서 var 사용이 불가능하다.

var p = (String s) -> s.length() > 10; // error: lambda expression needs an explicit target-type

 

 아래와 같이 배열을 사용하는 경우에도 사용할 수 없다.

var arr = { 1, 2, 3 }; // error: array initializer needs an explicit target-type

 

3. var 사용 가이드라인

 var를 사용하는 것이 가능은 하지만, 사용하는 것이 별로 좋은 선택이 아닌 경우가 있다. 예를 들면 가독성을 떨어뜨리는 경우다.

var result = obj.prcoess();

 

 위에서는 process() 메서드에 의해 반환되는 객체의 타입을 이해하는 것이 쉽지 않으므로 가독성이 떨어진다. 

 우리가 이 기능을 어떻게 사용해야할 지에 대한 가이드라인이 있다. 여기를 클릭하면 확인할 수 있다. 

 

 긴 파이프라인을 가진 스트림을 사용할 때도 var 사용을 피해야 한다. 

var x = emp.getProjects.stream()
  .findFirst()
  .map(String::length)
  .orElse(0);

 

 또한 var 사용은 예상하지 못하는 결과를 가져올 수 있다.

 예를 들면, 아래와 같이 다이아몬드 연산자를 써보자.

var empList = new ArrayList<>();

 empList의 타입은 List<Object>가 아닌 ArrayList<Object>가 될 것이다. 만약 ArrayList<Employee>로 명시하고 싶다면 아래와 같이 하면 된다.

var empList = new ArrayList<Employee>();

 

 또한 다음과 같은 경우에도 예상치 못한 에러가 발생할 수 있다.

@Test
public void whenVarInitWithAnonymous_thenGetAnonymousType() {
    var obj = new Object() {};
    assertFalse(obj.getClass().equals(Object.class)); // false
    obj = new Object() // error!!
}

  obj에 new Object()를 대입할 수 없다. 왜냐하면 obj의 타입은 Object가 아니라 Object를 확장한 익명클래스이기 때문이다.

 

※ 참조

https://www.baeldung.com/java-10-local-variable-type-inference

1. 개요

 이전 글들에서 Java 9의 새로운 기능들 중 가장 큰 부분인 모듈에 대해 알아봤다. 이 글에서는 모듈을 제외한 새로운 기능들 중 몇 가지를 소개할 것이다.

 

2. 새로운 Http Client

 오랜 기다림 끝에 드디어 HttpURLConnection을 대체할 수 있는 것이 나왔다.

 새로운 API는 java.net.package 패키지 아래에 있다. HTTP/2 프로토콜과 WebSocket 핸드셰이크를 모두 지원하며, Apache HttpClient, Netty 및 Jetty와도 비슷한 수준의 성능을 가진다.
 간단한 HTTP 요청을 만들어 전송하여 이 새로운 기능을 살펴보자.

2.1 GET Request

 API는 빌더 패턴을 사용하여 다음과 같이 매우 쉽게 사용할 수 있다.

HttpRequest request = HttpRequest.newBuilder()
        .uri(new URI("https://www.naver.com"))
        .GET()
        .build();

HttpResponse<String> response = HttpClient.newHttpClient()
        .send(request, HttpResponse.BodyHandlers.ofString());

 

3. 프로세스 API

 운영 체제 프로세스를 제어하고 관리할 수 있도록 프로세스 API가 개선되었다.

 

3.1 프로세스 정보

 java.lang.ProcessHandle 클래스가 대부분의 새로운 기능들을 가지고 있다.

ProcessHandle self = ProcessHandle.current();
long PID = self.pid();
ProcessHandle.Info procInfo = self.info();

Optional<String[]> arguments = procInfo.arguments();
Optional<String> cmd = procInfo.commandLine();
Optional<Instant> startTime = procInfo.startInstant();
Optional<Duration> cupUsage = procInfo.totalCpuDuration();

ProcessHandle.current().children().forEach(processHandle -> {
    processHandle.destroy();
});

 current() 메서드는 현재 실행 중인 JVM 프로세스를 나타내는 객체를 반환한다. Info 하위 클래스는 프로세스에 대한 세부 정보를 제공한다. 그리고 destroy() 메서드를 통해, 실행 중인 차일드 프로세스를 모두 종료시킬 수 있다.

 

4. 일부 작은 수정 사항들

4.1. Try-With-Resources

 Java 7에서 Try-with-Resource 구문을 사용하려면, 이 구문 내에서 자원에 대한 선언을 해야했다. 
 Java 9부터는, 리소스가 final 이거나 effectively final일 경우, 구문 내에서 선언되지 않아도 된다.

// Before Java 9
try (FileReader fileReader1 = new FileReader("/test.txt")) {
    // do something
}

// Java 9
FileReader fileReader2 = new FileReader("/test.txt");

try (fileReader2) {
    // do something
}

 

4.2 다이아몬드 연산자

 이제 익명객체를 생성할 때도 다이아몬드 연산자를 사용할 수 있다.

Function<Integer, Integer> function = new Function<>() {
    @Override
    public Integer apply(Integer input) {
        return input * 3;
    }
};

 

4.3 인터페이스의 private 메서드

 Java 9에서는 인터페이스의 default 메서드에 대한 분할을 가능하게 하기 위해 private 메서드를 사용할 수 있다.

interface InterfaceWithPrivateMethods {
    
    private static String staticPrivate() {
        return "static private";
    }
    
    private String instancePrivate() {
        return "instance private";
    }
    
    default void check() {
        String result = staticPrivate();
        InterfaceWithPrivateMethods pvt = new InterfaceWithPrivateMethods() {
            // anonymous class
        };
        result = pvt.instancePrivate();
    }
}}

 

5. JShell 커멘드 라인 툴

 JShell은 read(읽고)-eval(해석하고)-print(출력하는)-loop(루프), 줄여서 REPL 이다. 다시 말해, 자바 코드를 라인단위로 해석해주는 대화형 툴이다. 이것은 작은 코드들을 테스트해볼 때 매우 편리하다.

$ jshell 
|  Welcome to JShell -- Version 9
|  For an introduction type: /help intro

jshell> "This is my long string. I want a part of it".substring(8,19);
$1 ==> "my long str"

 JShell은 히스토리와 자동 완성 기능도 제공한다. 또한 파일을 저장하거나 로딩하는 기능도 제공한다.

jshell> System.out.println("Hello World")
Hello World

jshell> /save /home/mslim/test.txt

jshell> /open /home/mslim/test.txt
Hello World

 /open 명령어를 통해 로드된 코드 조각은 실행된다.

 

6. JCMD 커멘드 라인 툴

 jcmd 커멘드 라인 유틸리티의 몇 가지 새로운 하위 명령을 살펴보겠다. 우리는 JVM에 로드된 모든 클래스 및 상속 구조의 목록을 확일할 수 있다. 아래 예제에서는, Eclipse Neon을 실행하고 있는 JVM에 로드된 java.lang.Socket의 계층구조를 볼 수 있다.

jdk-9\bin>jcmd 14056 VM.class_hierarchy -i -s java.net.Socket
14056:
java.lang.Object/null
|--java.net.Socket/null
|  implements java.io.Closeable/null (declared intf)
|  implements java.lang.AutoCloseable/null (inherited intf)
|  |--org.eclipse.ecf.internal.provider.filetransfer.httpclient4.CloseMonitoringSocket
|  |  implements java.lang.AutoCloseable/null (inherited intf)
|  |  implements java.io.Closeable/null (inherited intf)
|  |--javax.net.ssl.SSLSocket/null
|  |  implements java.lang.AutoCloseable/null (inherited intf)
|  |  implements java.io.Closeable/null (inherited intf)

 jcmd 명령의 첫 번째 매개 변수는 명령을 실행할 JVM의 프로세스 ID(PID)이다. 

 또 다른 흥미로운 하위 명령은 set_vmflag이다. JVM 프로세스를 다시 시작하고 시작 매개 변수를 수정할 필요 없이, 온라인으로 일부 JVM 매개 변수를 수정할 수 있다. jcmd <pid> VM.flags -all 명령을 통해 수정 가능한 VM Flag들의 목록을 확인할 수 있다.

 

7. Publish-Subscribe 프레임워크

 java.util.concurrent.Flow 클래스는 Reactive Streams을 지원하는 인터페이스를 제공한다. 이러한 인터페이스는 JVM에서 실행되는 여러 비동기 시스템에서의 상호 운용성을 지원한다. 유틸리티 클래스 SubmissionPublisher를 사용하여 커스텀 컴포넌트를 만들 수 있다.

 

8. 통합 JVM 로깅

 이 기능은 JVM의 모든 구성 요소에 대한 공통 로깅 시스템을 도입한다. 로깅을 수행할 인프라를 제공하지만 모든 JVM 구성 요소에서 실제 로깅을 호출하지는 않는다. 또한 JDK의 Java 코드에 로깅을 추가하지도 않는다.
 로깅 프레임워크는 태그의 집합을 정의한다. 예를 들어 gc, 컴파일러, 스레드 등이다. 커멘드 라인 매개 변수 -Xlog를 사용하여 시작 중에 로깅을 설정할 수 있다.
 'debug' 레벨의 'gc' 태그가 붙은 로그들을 'gc.txt'라는 파일에 기록해보자.

java -Xlog:gc=debug:file=gc.txt:none ...

 -Xlog:help는 가능한 옵션과 예시를 보여줄 것이다. 로깅 설정은 jcmd 명령을 통해 런타임에 수정될 수 있다. 아래에서 gc로그의 레벨을 info로, 로깅 파일을 gc_log로 변경해보자.

jcmd <pid> VM.log output=gc_logs what=gc

 

9. 새로운 API

9.1 불변 컬렉션

 java.util.Set.of(), java.util.List.of(), java.util.Map.of() 메서드를 통해 컬렉션을 좀 더 쉽게 만들 수 있다.

List<String> list = List.of("a", "b", "c");
Set<String> set = Set.of("a", "b", "c");
Map<String, Integer> map = Map.of("a", 1, "b", 2); // key와 value 순서로 넣어야함. 개인적으로 이 함수는 매우 구린 것으로 보인다-_-;

 이 메서드들은 JVM의 내부 클래스인 java.util.ImmutableCollections.ListN / SetN / MapN 객체를 반환하는데, 이는 AbstractCollection을 구현한 것들이며 불변이다. 만약 add(), remove(), put()과 같은 함수를 통해 이 컬렉션들에 변형을 가하려고 할 경우, UnsupportedOperationException이 발생된다. (참고로 코틀린에서는 불변 컬렉션이 디폴트이다.)

 

9.2 Optional.stream()

 java.util.Optional.stream() 메서드가 생김으로써, Optional 객체들을 스트림과 함께 더 편리하게 사용하는 것이 가능해졌다.

List<String> filteredList = listOfOptionals.stream()
  .flatMap(Optional::stream)
  .collect(Collectors.toList());

 

※ 참조

https://www.baeldung.com/new-java-9

1. 개요

 이전 글에서 모듈이란 무엇이고 그것들을 어떻게 사용하는지 알아보았다. 이제 우리가 배운 모든 개념들을 보여줄 간단한 프로젝트를 만들어보자. 여기서는 Maven이나 Gradle을 사용하지 않고 커멘드라인을 사용하여 프로젝트를 빌드할 것이다.

 

1.1 프로젝트 설정

 우선 프로젝트 구조를 잡아야한다. 먼저 프로젝트 폴더를 만들어보자.

mkdir module-project
cd module-project

 이것이 전체 프로젝트의 기반이 되는 디렉토리이다. 따라서 여기에 소스 디렉토리, 리소스 파일 등이 추가될 것이다. 이제 모든 모듈들을 저장할 디렉토리도 만들어보자.

mkdir simple-modules

 전체 프로젝트 구조는 아래와 같다.

module-project
|- simple-modules
  |- hello.modules
    |- com
      |- baeldung
        |- modules
          |- hello
  |- main.app
    |- com
      |- baeldung
        |- modules
          |- main

 

1.2 첫번째 모듈

 이제 기본 구조를 갖추었으니 첫 번째 모듈을 추가해 보자.

 simple-modules 디렉토리 아래에 hello.modules라는 새 디렉토리를 만든다. 우리는 모듈 이름을 어떤 것으로든 정할 수 있으나, 패키지 이름 지정 규칙(예: 단어를 구분하는 마침표 등)을 따르도록 한다. 우리가 원한다면 우리의 메인 패키지의 이름을 모듈 이름으로 사용할 수도 있지만, 우리는 보통 우리가 이 모듈의 JAR을 만들기 위해 사용하는 것과 같은 이름을 쓴다.
 새로운 모듈 아래에서는 원하는 패키지들을 아래와 같이 만들 수 있다.

com.baeldung.modules.hello

 그런 다음 이 패키지에 HelloModules.java라는 새 클래스를 만든다. 코드를 단순하게 유지하도록 한다.

package com.baeldung.modules.hello;

public class HelloModules {
    public static void doSomething() {
        System.out.println("Hello, Modules!");
    }
}

그리고 마침내, hello.modules 루트 디렉토리에 모듈 설명자 파일(module-info.java)을 추가한다.

module hello.modules {
    exports com.baeldung.modules.hello;
}

 

1.3 두번째 모듈

 우리의 첫 번째 모듈은 훌륭하지만, 아무것도 하지 않는다. 이제 우리는 첫 번째 모듈을 사용하는 두 번째 모듈을 만든다.

 simple-modules 디렉토리 아래에 main.app 디렉토리를 만들고 그 안에 모듈 설명자 파일(module-info.java)을 추가한다.

module main.app {
    requires hello.modules;
}

 우리는 어떤 것도 외부에 노출(exports)시킬 필요가 없다. 대신, 우리가 해야 할 일은 우리의 첫 번째 모듈에 의존하는 것이다. 그래서 우리는 그것이 노출(exports)하는 public 클래스에 접근할 수 있다.

 com.baeldung.modules.main 패키지를 만들고 그 안에 MainApp.java라는 새 클래스 파일을 만든다.

package com.baeldung.modules.main;

import com.baeldung.modules.hello.HelloModules;

public class MainApp {
    public static void main(String[] args) {
        HelloModules.doSomething();
    }
}

 이제 다음 단계에서 커멘드 라인을 통해 소스코드를 빌드하고 실행해보자.

 

1.4 모듈 빌드하기

 모듈을 빌드하기 위해서, 간단한 bash 스크립트를 작성하고 그것을 프로젝트 루트에 추가해보자.

 compile-simple-modules.sh라는 파일을 아래와 같이 만들자.

#!/bin/bash
javac -d outDir --module-source-path simple-modules $(find simple-modules -name "*.java")

 이 명령어는 javac, find 두 부분으로 나뉜다. 

 find는 simple-modules 디렉토리 내의 모든 java파일들을 찾는 명령어이다. 이를 통해 자바 파일들의 목록을 직접 자바 컴파일러에게 전달할 수 있다.

 자바 9 이전 버전과 유일하게 다른점은, 모듈을 빌드하기 위해 module-source-path 파라미터를 컴파일러에게 제공해야한다는 것이다.

 이 스크립트를 실행하면, outDir 폴더 내에 두 개의 컴파일된 모듈이 생성된다.

 

1.5 코드 실행하기

 이제 모듈이 정상작동하는지 확인하기 위해 코드를 실행해보자.

 프로젝트 루트에 run-simple-module-app.sh라는 파일을 아래와 같이 만들자.

#!/bin/bash
java --module-path outDir -m main.app/com.baeldung.modules.main.MainApp

 모듈을 실행하기 위해서는, module-path를 메인 클래스에게 제공해야한다. 만약 정상 동작한다면, 아래와 같이 나올 것이다.

$ sh run-simple-module-app.sh 
Hello, Modules!

 

1.6 서비스 추가하기

 이제 모듈을 만드는 방법에 대해 기본적인 이해를 했으니, 좀 더 복잡하게 만들어 보자.

 우리는 'provides…with' 와 'uses' 키워드를 어떻게 사용하는지에 대해 알아볼 것이다.

 먼저 hello.modules 모듈 안에 HelloInterface.java라는 인터페이스를 아래와 같이 만들어 보자.

package com.baeldung.modules.hello;

public interface HelloInterface {
    void sayHello();
}

 

 이제 com.baeldung.modules.hello.impl라는 패키지를 새로 만든 후, 이 인터페이스를 구현하는 클래스를 아래와 같이 만들어보자. 

package com.baeldung.modules.hello.impl;

import com.baeldung.modules.hello.HelloInterface;

public class HelloImpl implements HelloInterface {

    @Override
    public void sayHello() {
        System.out.println("Hello!");
    }
}

 그 다음 아래와 같이 hello.modules 모듈 안의 module-info.java에 추가한다.

module hello.modules {
    exports com.baeldung.modules.hello;
    provides com.baeldung.modules.hello.HelloInterface with com.baeldung.modules.hello.impl.HelloImpl;
}

 위에서는 어떤 클래스가 인터페이스를 구현했는지 명시하고 있다.

 이제 이 서비스를 사용할 차례이다. main.app 모듈의 module-info.java에 다음과 같이 추가한다.

module main.app {
    requires hello.modules;
    uses com.baeldung.modules.hello.HelloInterface;
}

 마지막으로, MainApp 클래스를 아래와 같이 변경한다.

package com.baeldung.modules.main;

import com.baeldung.modules.hello.HelloInterface;
import com.baeldung.modules.hello.HelloModules;

import java.util.ServiceLoader;

public class MainApp {
    public static void main(String[] args) {
        HelloModules.doSomething();

        Iterable<HelloInterface> services = ServiceLoader.load(HelloInterface.class);
        HelloInterface service = services.iterator().next();
        service.sayHello();
    }
}

 다시 컴파일하고 실행하면 아래와 같은 결과를 확인할 수 있다.

$ sh compile-simple-modules.sh
$ sh run-simple-module-app.sh 
Hello, Modules!
Hello!

 우리는 'provides…with' 와 'uses' 키워드를 사용함으로써 우리의 코드가 어떻게 쓰이는지 좀 더 명시적으로 나타낼 수 있다. 또한 인터페이스의 구현은 노출시키지 않은채 인터페이스만 노출시킴으로써, 보다 캡슐화된 구조를 가지게 될 수 있다.

 

* https://www.baeldung.com/java-9-modularity을 기반으로 작성되었으나, 보다 수월한 이해를 위해 일부 소스 코드를 수정하였다. 따라서 소스코드를 위한 별도의 리파지토리를 여기에 제공하겠다. 

 

※ 참조

https://www.baeldung.com/java-9-modularity

1. 개요

 Java 9는 공식적으로 Java Platform Module System (JPMS) 또는 줄여서 '모듈'이라고 알려진 새로운 수준의 추상화를 도입한다. 모듈에는 종속성(dependency)의 개념이 있으며, Public API를 내보내고 구현 세부 정보를 숨김/비공개 상태로 유지할 수 있다.
 모듈형 시스템을 고안하게된 주된 동기 중 하나는, 가용 메모리가 훨씬 적은 장치에서 실행할 수 있는 모듈형 JVM을 제공하는 것이었다. JVM은 애플리케이션에 필요한 모듈 및 API로만 실행될 수 있다. 

 

2. 모듈이란?

 우선, 모듈의 사용법을 이해하기 전에 모듈이 무엇인지 이해해야 한다. 모듈이란 서로 밀접하게 연관된 패키지들과 리소스들의 그룹이다. 즉, "자바 패키지들의 패키지(package of Java Packages)"라고도 볼 수 있는데, 이는 코드의 재사용성을 높이기 위해 추상화 단계를 하나 더 추가한 것이라고 볼 수 있다. 

 

2.1 패키지

 모듈 내의 패키지들은 Java가 시작된 이후 사용해 온 Java 패키지와 동일하다. 모듈을 만들 때, 우리는 이전에 다른 프로젝트에서 했던 것처럼 똑같이 패키지를 구성한다. 코드 구성 외에도 패키지는 모듈 외부에서 공개적으로 접근할 수 있는 코드를 결정하는 데 사용된다. 이 부분에 대해서는 이 글의 뒷부분에서 추가적으로 설명하도록 하겠다.

 

2.2 리소스

 각 모듈은 미디어 또는 configuration 파일과 같은 리소스들을 담당한다. 이전에는 모든 리소스를 프로젝트의 루트 레벨에 넣고 애플리케이션의 각기 다른 부분들에 속한 리소스들을 수동으로 관리했다. 모듈을 사용하면, 모듈과 함께 필요한 이미지와 XML 파일 등을 함께 모아놓을 수 있으므로, 프로젝트를 훨씬 쉽게 관리할 수 있다.

 

2.3 모듈 설명자(Module Descriptor)

 모듈을 만들 때, 모듈의 여러 가지 설정을 정의하는 설명자 파일(module-info.java)을 추가한다. 설정값들에는 다음과 같은 것들이 있다.

 - 이름 : 모듈 이름
 - 종속성 : 이 모듈이 의존하는 다른 모듈 목록
 - 공용 패키지 : 모듈 외부에서 접근할 수 있는 모든 패키지 목록
 - 제공되는 서비스 : 다른 모듈에서 사용할 수 있는 서비스 구현을 제공할 수 있다. (Service Provider)
 - 사용된 서비스 : 현재 모듈이 서비스의 소비자가 될 수 있도록 한다. (Service Consumer)
 - 리플렉션 허용 : 다른 클래스가 리플렉션을 사용하여 패키지의 private 멤버에 접근할 수 있는지의 여부를 명시

 모듈 네이밍 규칙은 패키지의 이름을 지정하는 방법과 유사하다(도트는 허용되고 대시는 허용되지 않음). 프로젝트 스타일(my.module) 또는 리버스 DNS(com.baeldung.mymodule) 스타일 이름을 사용하는 것은 매우 일반적이다.

 

 디폴트로 모든 패키지는 모듈 전용(module private)이므로 공개하려는 모든 패키지들은 명시적으로 나열되어야 한다. 리플랙션 또한 마찬가지로, 디폴트로 다른 모듈에서 모듈의 클래스에 대한 리플랙션을 사용할 수 없다.

 

2.4 모듈 타입

 새 모듈 시스템에는 4가지 유형의 모듈이 있다.

 - 시스템 모듈(System Modules) : list-modules 명령어를 입력했을 때 조회되는 모듈들이다. 여기에는 Java SE 및 JDK 모듈이 포함된다.
 - 애플리케이션 모듈(Application Modules) : 이 모듈은 일반적으로 우리가 모듈을 사용하기로 결정했을 때 빌드하고자 하는 모듈이다. 이들은 JAR에 포함된 컴파일된 module-info.class 파일에 명명되고 정의된다.
 - 자동 모듈(Automatic Modules) : 기존 JAR 파일을 모듈 경로(module path)에 추가하여 비공식 모듈을 포함시킬 수 있다. 모듈 이름은 JAR의 이름에서 파생된다. 자동 모듈은 모듈 경로에 로드된 다른 모든 모듈들에 대한 전체 읽기 액세스 권한을 가지게 된다.
 - 이름 없는 모듈(Unnamed Module) : 클래스 또는 JAR가 모듈 경로(module path)가 아닌 클래스 경로(classpath)에 로드되면, 이름이 지정되지 않은 모듈에 자동으로 추가된다. 이것은 이전에 작성된 Java 코드와의 하위 호환성을 유지하기 위함이다.

 

2.5 배포

 모듈은 JAR 파일 또는 컴파일된 여러 파일들로 이루어진 프로젝트 두 가지 방법 중 하나로 배포될 수 있다. 물론 이는 다른 Java 프로젝트와 동일한 방법이다.

 우리는 "주 애플리케이션"과 여러 라이브러리 모듈로 구성된 다중 모듈 프로젝트를 만들 수 있다. 이때 JAR 파일당 모듈을 하나만 가질 수 있기 때문에 주의해야 한다. 빌드 파일을 설정할 때 프로젝트의 각 모듈을 별도의 JAR로 묶어야 한다.

 

3. 디폴트 모듈

 Java 9를 설치하면 JDK가 새로운 구조를 가지고 있음을 알 수 있다. 원래 패키지들이 모두 새 모듈 시스템으로 옮겨졌다. 커멘드 라인에 아래의 명령어를 입력하면 이러한 모듈들이 무엇인지 알 수 있다.

java --list-modules

 이러한 모듈들은 Java, javafx, jdk 및 Oracle의 네 가지 주요 그룹으로 나뉜다.
 java 모듈은 핵심 Java SE 언어에 대한 구현 클래스이다.
 javafx 모듈은 FX UI 라이브러리이다.
 JDK 자체에 필요한 모든 것은 JDK 모듈에 보관된다.
 마지막으로, 오라클에 특화된(Oracle-specific) 것들은 오라클 모듈에 있다.

 

4. 모듈 선언

 모듈을 설정하려면 module-info.java라는 모듈 설명자 파일을 패키지 루트 레벨에 생성해야 한다. 이 파일에는 모듈을 생성하는데 필요한 모든 데이터가 들어있다. 어떤 설정값도 없는 빈 모듈 설명자 파일을 만들어보자.

module myModuleName {
    // 설정값들은 모두 Optional
}

 'module' 키워드로 모듈 선언을 시작하고, 그다음 모듈 이름을 쓴다. 이러한 선언만으로도 모듈은 동작하지만, 일반적으로 더 많은 정보(설정값들)가 필요하다.

 

4.1 Requires

 첫 번째 키워드는 'requires'이다. 이 키워드는 이 모듈의 종속성을 나타낸다.

module my.module {
    requires module.name;
}

 이제 'my.module'은 'module.name'에 대한 종속성을 갖게 된다(런타임, 컴파일 타임 둘 다). 이제 'my.module'는 'module.name'에서 외부로 공개(exports)하기로 한 모든 public type들에 대한 접근이 가능하다. 

 

4.2 Requires Static

 때때로 우리는 다른 모듈을 참조하는 코드를 작성하지만, 우리 라이브러리의 사용자는 결코 사용을 원하지 않을 수 있다. 예를 들어 다른 로깅 모듈을 사용하여, 내부 상태를 예쁘게 나타내는 유틸리티 함수를 작성할 수 있다. 그러나 우리 라이브러리의 모든 소비자들이 이 기능을 원하는 것은 아니며 추가 로깅 라이브러리가 포함되기를 원치 않을 수 있다.
 이러한 경우 우리는 선택적 종속성(optional dependency)을 사용할 수 있다. 'requires static' 키워드를 사용하여 오직 컴파일 타임에 대한 종속성(compile-time-only dependency)을 갖게 할 수 있다.

module my.module {
    requires static module.name;
}

 

4.3 Requires Transitive

 우리는 어떤 모듈을 우리의 코드에서 사용하려 할 때, 그 모듈이 다른 전이되는 종속성(transitive dependency)을 필요로 할 수 있다는 것을 알아야 한다. 이 종속성을 추가하지 않을 경우, 코드는 정상 동작하지 않을 것이다. 이때 사용할 수 있는 키워드가 'requires transitive'인데, 이를 사용하면 우리 모듈의 사용자들이 전이된 종속성에 대한 접근이 가능하도록 강제할 수 있다.

module my.module {
    requires transitive module.name;
}

 이제 어떤 개발자가 자신의 모듈에 'requires my.module'을 추가한다면, 'requires module.name'은 추가하지 않아도 이에 대한 접근이 가능하다.

 

4.4 Exports

 디폴트로, 모듈은 자신의 API를 다른 모듈에 노출시키지 않는다. 이러한 강력한 캡슐화가 모듈 시스템이 만들어진 주요 동기 중 하나였다. API가 사용될 수 있게 하려면 명시적으로 공개 범위를 나타내야 한다. 모듈 내의 모든 public 멤버들을 외부로 노출시키기 위해 'exports' 키워드를 사용한다.

module my.module {
    exports com.my.package.name;
}

 이제 어떤 개발자가 자신의 모듈에 'requires my.module'을 추가한다면, com.my.package.name 패키지 내의 모든 public 타입에 접근할 수 있다. 하지만 다른 패키지에는 접근할 수 없다.

 

4.5. Exports … To

 특정 모듈에만 API를 노출시키고 싶을 수 있다. 'exports…to' 키워드를 이용하여 모듈의 API를 특정 모듈에만 공개하도록 설정할 수 있다. 

module my.module {
    export com.my.package.name to com.specific.package;
}

 

4.6. Uses

 서비스란 다른 클래스에서 사용할 수 있는 특정 인터페이스 또는 추상 클래스의 구현을 말한다. 'uses' 키워드를 통해 모듈이 사용할 서비스를 지정할 수 있다. 이때 주의할 점은, 'uses' 키워드 뒤에 추가될 것은 구현 클래스가 아니라 서비스의 인터페이스 혹은 추상 클래스여야 한다는 것이다.

module my.module {
    uses class.name.MyInterface;
}

 여기서 우리는 'requires'와 'uses'의 차이점에 대해 유의해야 한다.

 요구된(requires) 모듈의 경우, 우리가 사용할 서비스가 전이된 종속성(transitive dependencies)으로부터 구현된 것일 수도 있다. 이 경우, 모든 전이된 종속성을 모듈에 추가해야 한다. 그렇게 하는 대신, 'uses' 키워드를 이용하여 인터페이스나 추상 클래스만 모듈 경로(module path)에 추가할 수 있다.

 

4.7. Provides … With

 모듈은 다른 모듈이 소비(uses)할 수 있는 서비스 공급자가 될 수 있다. 'provides' 키워드 뒤에는 인터페이스 또는 추상 클래스 이름을 넣는다. 그리고 'with' 키워드 뒤에는 인터페이스를 구현하거나 추상 클래스를 확장한 구현 클래스 이름을 넣는다.

module my.module {
    provides class.name.MyInterface with class.name.MyInterfaceImpl;
}

 

4.8. Open

 앞에서 캡슐화가 모듈 시스템이 생기게 된 주된 원동력이라고 언급했다. 자바 9 이전에는 리플렉션을 사용하여 패키지의 모든 종류와 멤버들에 접근하는 것이 가능했으며, 심지어 private 멤버에도 접근이 가능했다. 실질적으로 캡슐화된 것은 아무것도 없었으며, 이는 많은 문제를 야기시킬 수 있었다.
 Java 9에서는 강력한 캡슐화를 강제하므로, 우리는 다른 모듈들이 우리의 클래스에 대한 리플랙션을 사용할 수 있는지의 여부를 명시적으로 표현해야 한다. 만약 이전 버전의 Java처럼 리플렉션을 모두 허용하려면 전체 모듈을 열면(open) 된다.

open module my.module {
}

 

4.9 Opens

 만약 특정 패키지에 대해서만 리플렉션을 허용하고 싶을 경우, 'opens' 키워드를 아래와 같이 사용한다.

module my.module {
  opens com.my.package;
}

 

4.10. Opens … To

 만약 특정 패키지에 대한 리플랙션을 특정 모듈들에 대해서만 허용하고 싶을 경우, 아래와 같이 모듈들의 목록을 쓰면 된다.

module my.module {
    opens com.my.package to moduleOne, moduleTwo, etc.;
}

 

5. 커멘드 라인 옵션

 Java 9 모듈 시스템에 대한 지원이 Maven과 Gradle에 추가되었으므로, 사실 프로젝트를 수동으로 빌드할 일은 없을 것이다. 그러나 전체 시스템이 어떻게 동작하는지 정확히 이해하기 위해서, 커멘드 라인 옵션에 대한 이해는 필요하다.
 1) module-path : -module-path 옵션을 사용하여 모듈 경로를 지정합니다. 모듈이 포함된 하나 이상의 디렉터리 목록이다.
 2) add-reads : 모듈 설명자 파일(module-info.java)에 의존하지 않고, 커멘드 라인을 통해 'requires'와 동일한 역할을 할 수 있다. : -add-reads
 3) add-exports : 모듈 설명자 파일(module-info.java)에 의존하지 않고, 커멘드 라인을 통해 'exports'와 동일한 역할을 할 수 있다.
 4) add-opens : 모듈 설명자 파일(module-info.java)에 'open' 절을 대체한다.

 5) add-modules : 디폴트 모듈 집합에 모듈 목록을 추가한다.
 6) list-modules : 모든 모듈 및 해당 버전 문자열 목록을 보여준다.
 7) patch-module : 모듈을 추가하거나 오버라이드 한다.
 8) illegal-access=permit|warn|deny : 하나의 글로벌 경고문만을 보여주거나(permit) 혹은 모든 경고문을 보여주면서(warn) 강력한 캡슐화(리플랙션에 대한 제한)를 완화시킬 수 있다. deny의 경우 illegal-access에 대해 에러를 발생시킨다. 디폴트는 permit이다.

 

6. 가시성

 가시성, 혹은 캡슐화에 대한 얘기를 좀 더 해보자. 많은 라이브러리들(JUnit and Spring 등)이 리플랙션에 의존한다. 그러나 Java 9에서는 기본적으로 리플랙션을 사용하여 private 요소들에 접근하고 setAccessible(true)을 호출하더라도 이것들에 대해 접근할 수 없다. 대신 우리는 'open, opens, and opens…to' 를 이용하여 리플렉션에 대한 권한을 런타임 전용으로 부여할 수 있다.

 만약 우리가 특정 모듈에 대한 리플랙션을 사용해야 하는데 그 모듈의 소유자가 아니라면, 커멘드 라인 -add-opens 옵션을 사용하여 리플랙션이 허용되도록 할 수 있다. 여기서 주의해야 할 유일한 점은 모듈을 실행하는 데 사용되는 커멘드 라인 인수에 접근할 수 있어야 한다는 것이다.

 

7. 모듈을 Unnamed Module에 추가하기

 Unnamed Module은 디폴트 패키지와 유사한 개념을 갖는다. 따라서 진짜 모듈이라기보단, 디폴트 모듈로 간주된다.

 만약 어떤 클래스가 named module의 멤버가 아닐 경우, 그것은 자동으로 Unnamed Module의 일부로 간주된다. 

 때때로 특정 플랫폼, 라이브러리 또는 서비스 프로바이더 모듈의 동작을 위해 모듈들을 디폴트 루트 셋에 추가해야할 때가 있다. 예를 들면, Java 9 컴파일러로 Java 8 프로그램을 그대로 실행하려고 할 때 우리는 모듈을 추가해야할 수 있다.

 일반적으로 named module을 디폴트 루트 셋에 추가하는 옵션은 '-add-modules <module>' 이다(<module>은 모듈명이다.). 예를 들어 모든 java.xml.bind 모듈에 대한 접근을 제공해야한다고 가정할 때, 아래와 같이 커멘드 라인 옵션을 추가할 수 있다.

--add-modules java.xml.bind

 메이븐을 사용할 경우, 아래와 같이 플러그인을 사용할 수 있다.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.0</version>
    <configuration>
        <source>9</source>
        <target>9</target>
        <compilerArgs>
            <arg>--add-modules</arg>
            <arg>java.xml.bind</arg>
        </compilerArgs>
    </configuration>
</plugin>

 

※ 참조

https://www.baeldung.com/java-9-modularity

https://www.baeldung.com/new-java-9

0. 서론

 필자는 개발을 자바라는 언어로 처음 시작했고 현재도 자바 개발자로 일하고 있다. '자바의 정석'이라는 책을 통해 공부를 했고 (8 버전까지 제공) 실무에서도 가장 많이 쓰이는 버전이 8 버전이다 보니 나의 자바에 대한 지식은 계속 자바 8 버전에만 멈춰있었다. 그러는 사이 오라클에서는 벌써 자바 17이 LTS 버전으로 공개되었다. 위기의식을 느낀 나는 각 자바 버전의 새로운 기능에 대한 정리를 하기로 마음먹었다. 

 

1. 개요

 이 글은 자바 8의 새로운 기능들 중, 중요하다고 생각되는 몇 가지들(람다식과 함수형 인터페이스, 스트림 API, 인터페이스의 default & static 메서드, 메서드 참조, Optional 클래스)에 대해 다룬다.

 

2. 람다식과 함수형 인터페이스

2.1 람다식

 람다식(Lambda expression)은 간단히 말해서 메서드를 하나의 식으로 표현한 것이다.

int[] arr = new int[5];
Arrays.setAll(arr, (x) -> (int) (Math.random() * 5 + 1));
int[] arr = new int[5];

for (int element : arr) {
    element = method(element);
}

int method(int x) {
    return x + (int) (Math.random() * 5 + 1);
}

 위의 2가지 코드는 모두 같은 로직을 수행한다. 이처럼 람다식을 사용하면 코드를 훨씬 간결하게 표현할 수 있으며, 메서드를 매개변수로 전달하는 것이 가능해진다.

 

2.2 람다식 작성법, 함수형 인터페이스

 함수형 인터페이스란, 인터페이스 내에 오직 한 개의 추상메서드만을 갖고 있어서, 이 인터페이스 타입의 변수에 람다식을 할당하는 것이 가능한 것이다. 지금은 잘 이해가 되지 않을 수도 있으나 계속 읽어나가 보도록 하자. 

 자바에서 모든 메서드는 클래스 내에 포함되어야 하는데, 람다식은 익명 클래스의 객체로 이루어져 있다. 즉 람다식은 익명클래스의 객체로 표현이 가능하다. 아래 예시들에서 익명 객체 내의 함수와 람다식을 비교해가면서 람다식 작성법을 익혀보자.

Runnable runnable;
runnable = () -> System.out.println("Test");
runnable = () -> {
    System.out.println("Test");
};

runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("Test");
    }
};

Consumer<String> consumer;
consumer = a -> System.out.println(a);
consumer = (String a) -> System.out.println(a);
consumer = (a) -> {
    System.out.println(a);
};

consumer = new Consumer<String>() {
    @Override
    public void accept(String a) {
        System.out.println(a);
    }
};

Supplier<Double> supplier;
supplier = () -> Math.random();
supplier = () -> {
    return Math.random();
};

supplier = new Supplier<Double>() {
    @Override
    public Double get() {
        return Math.random();
    }
};

BiFunction<String, String, Integer> biFunction;
biFunction = (a, b) -> Integer.parseInt(a) * Integer.parseInt(b);
biFunction = (String a, String b) -> {
    int aValue = Integer.parseInt(a);
    int bValue = Integer.parseInt(b);

    return aValue * bValue;
};

biFunction = new BiFunction<String, String, Integer>() {
    @Override
    public Integer apply(String a, String b) {
        int aValue = Integer.parseInt(a);
        int bValue = Integer.parseInt(b);

        return aValue * bValue;
    }
};

 위의 예시들에서 유추해볼 수 있듯이 람다식을 함수형 인터페이스 변수에 할당하는 것이 가능하기 위해서는, 인터페이스 내에 오직 하나의 추상 메서드만 정의되어 있어야 한다. 어떤 인터페이스가 함수형 인터페이스인지를 구별하기 위해서 @FunctionalInterface 어노테이션을 붙여준다. 붙이지 않는다고 해도 인터페이스 내의 함수가 1개밖에 없다면 함수형 인터페이스로 사용하는 것이 가능하기는 하나, 향후 유지보수를 위해 붙여주는 것이 좋다.

package java.util.function;

@FunctionalInterface
public interface Function<T, R> {
    R apply(T var1);
}

 

 java.util.function 패키지 내에 있는 표준 함수형 인터페이스들을 사용하면, 대부분의 개발자들의 니즈를 만족시킬 수 있다. 대부분의 라이브러리들도 표준 함수형 인터페이스를 사용하며, 개발자들 간의 효과적이 소통을 위해서도 새로운 함수형 인터페이스를 만드는 것보단 표준 함수형 인터페이스를 사용하는 것을 먼저 고려해봐야 한다.

  

3. 스트림 API

 스트림(java.util.stream)은 Java 8의 주요한 새로운 기능 중 하나로서, 컬렉션이나 배열 등과 같은 요소들의 집합들에 대한 처리를 보다 효과적으로 할 수 있도록 도와준다.

 

3.1 스트림 생성

 스트림은 컬렉션이나 배열 등과 같은 요소들의 집합들로부터 생성될 수 있다.

String[] arr = new String[]{"a", "b", "c"};
Stream<String> stream = Arrays.stream(arr);
stream = Stream.of("a", "b", "c");

 

 아래 예시에서는 Collection interface에 추가된 stream() 함수(인터페이스에 추가된 default method로서, default method 역시 자바 8에 새로 추가된 기능이다. default method에 대해서는 뒤에서 다룰 예정이다.)를 통해 스트림을 생성한다. Collection interface에 추가되었기 때문에 Collection을 상속하는 클래스로 생성된 모든 객체들은 stream()을 통해 스트림을 생성할 수 있다.

List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();

 

3.2 유용한 기능들

 스트림은 다양한 유용한 기능들을 제공하며 코드를 더 간결하게 나타낼 수 있게 도와준다. 아래의 2 코드는 서로 동일한 로직을 수행한다.

for (String a : list) {
    System.out.println(a);
}

list.stream().forEach(a -> System.out.println(a));

 스트림은 이밖에도 다양한 기능들을 람다식과 함께 제공한다. 아래 예시들을 살펴보자.

List<String> list = Arrays.asList("abc", "bac", "ccc", "ccc");
boolean isExist = list.stream().anyMatch(element -> element.contains("a")); // a를 포함한 문자열이 있는지의 여부를 반환
Stream<String> stringStream = list.stream().filter(element -> element.startsWith("a"));// a로 시작하는 문자열만 필터링
Stream<Integer> integerStream = list.stream().map(element -> element.length()); // 문자열의 길이로 이루어진 스트림으로 변환
Stream<String> distinctStream = list.stream().distinct(); // 리스트 내의 중복 제거
list.parallelStream().forEach(System.out::println);  // 멀티쓰레드를 이용한 병렬 처리. 메서드 참조도 사용

 

3.3 지연된 연산

 스트림 연산에서 중요한 점 중 하나는 최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않는다는 것이다. 아래 예시를 살펴보자.

List<String> list = Arrays.asList("abc", "bac", "ccc", "ccc");
List<String> result = list.stream().distinct().limit(5).sorted().collect(Collectors.toList());

 list의 스트림을 생성한 후, distinct(), limit(), sorted(), collect() 라는 총 4가지의 함수를 호출했다. 여기서 마지막 collect()를 제외한 나머지는 모두 '중간 연산'에 속하며 collect()라는 '최종 연산'이 호출되기 전까지는 아무것도 실행되지 않는다. (이는 Kotlin의 Sequence와 동일하다.) 지연된 연산이 없었다면 매 연산이 실행될 때마다 새로운 객체를 생성했을 것이다. 아래는 스트림을 사용하지 않고 동일한 로직을 구현한다고 가정했을 때의 코드이다.

List<String> list = Arrays.asList("abc", "bac", "ccc", "ccc");

List<String> result = new ArrayList<>(new LinkedHashSet<>(list));

for (int i = 5; i < result.size(); i++) {
    result.remove(i);
}

result.sort(Comparator.naturalOrder());

 

4. 인터페이스의 default 메서드와 static 메서드

 자바 8 이전의 interface는 오직 public abstract method만을 가질 수 있었다. 때문에 인터페이스에 하나의 메서드를 추가하려면, 그 인터페이스를 구현한 모든 클래스에 그 메서드를 추가한 후 구현도 해줘야했다. 하지만 default method라는 기능이 자바 8에 생기면서 그럴 필요가 없어졌다. 뿐만 아니라 인터페이스 내에 static method를 추가하는 것도 가능해졌다. 아래는 예시이다.

public interface TestInterface {
    public String method();

    public int multiply(int a, int b);

    default int add(int a, int b) {
        return a + b;
    }

    public static int minus(int a, int b) {
        return a - b;
    }
}

public class TestClass implements TestInterface{

    @Override
    public String method() {
        return "method";
    }

    @Override
    public int multiply(int a, int b) {
        return a * b;
    }
}
public class Main {
    public static void main(String[] args) {
        TestInterface testInterface = new TestClass();
        
        testInterface.add(1, 2);
        TestInterface.minus(2, 1);
    }
}

 

5. 메서드 참조

 메서드 참조란 람다식을 좀 더 가독성이 좋은 짧은 형태로 나타내는 것이다. 항상 가능한 것은 아니고 람다식이 하나의 메서드만 호출하는 경우, 메서드 참조의 형태로 람다식을 간략히 나타낼 수 있다. 총 4가지의 형태가 있다.

 

5.1 Static 메서드

 - syntax : 클래스명::메서드명

List<String> list = Arrays.asList("a", "b", "c");

list.stream().forEach(a -> System.out.println(a));
list.stream().forEach(System.out::println); // 이 둘은 동일한 표현이다.

5.2 Instance 메서드

 - syntax : 변수명::메서드명

String target = "ab";

list.stream().filter(a -> target.contains(a));
list.stream().filter(target::contains); // 이 둘은 동일한 표현이다.

5.3 특정 클래스에 대한 Instance 메서드

 - syntax : 클래스명::메서드명

list.stream().map(a -> a.length());
list.stream().map(String::length); // 이 둘은 동일한 표현이다.

5.4 생성자

 - syntax : 클래스명::new

List<Integer> list = Arrays.asList(1, 2, 3);

list.stream().map(a -> new ArrayList<String>(a));
list.stream().map(ArrayList::new); // 이 둘은 동일한 표현이다.

 

6. Optional<T>

 자바 8 이전의 개발자들은 NullPointerException(NPE)이 발생할 수 있다는 가능성 때문에, 이에 대한 유효성 검사 및 에러 방지를 위한 많은 보일러플레이트 코드를 추가해야만 했다. Optional<T> 클래스는 NPE를 효과적으로 다룰 수 있게해주는 콘테이너 클래스이다. Optional 콘테이너 내부의 객체가 null이 아니라면 그 객체를 반환하고, null일 경우에는 NPE가 발생하는 대신 사전에 정의된 액션을 취한다.

 

6.1 Optional<T> 생성

Optional<String> optional; 
optional = Optional.empty(); // 빈 Optional 객체를 생성
optional = Optional.of("value"); // null이 아닌 객체를 감싼 Optional 객체를 생성

optional = Optional.ofNullable(getString()); 
// getString()이 스트링을 반환할 수도, null을 반환할 수도 있음
// getString()이 null일 경우, 빈 ptional 객체가 생성됨

6.2 Optional<T> 사용

 다음은 Optional<T>를 사용했을 때와 사용하지 않았을 때를 비교하면서, 사용 시에 코드가 얼마나 더 간결해지고 직관적으로 변하는지를 보도록 하자.

List<String> list = getList();
List<String> listOpt1 = list != null ? list : new ArrayList<>(); // Java 8 이전 코드
List<String> listOpt2 = listOpt = getList().orElseGet(() -> new ArrayList<>()); // Optional 사용

User user = getUser();
if (user != null) {
    Address address = user.getAddress();
    if (address != null) {
        String street = address.getStreet();
        if (street != null) {
            return street;
        }
    }
}
return "not specified";
Optional<User> user = Optional.ofNullable(getUser());
String result = user
  .map(User::getAddress)
  .map(Address::getStreet)
  .orElse("not specified");

 위 예제에서 map() 메서드는 getAdress() 메서드의 결과값을 Optional<Address>로 변환하고, getStreet() 메서드의 결과값을 Optional<String>으로 변환한다. 이 get메서드들 중 어느 하나가 null을 반환한다면, map() 메서드는 빈 Optional 객체를 반환할 것이다.

 만약 애초에 이 get메서드들이 Optional<T>을 반환하는 함수였다고 가정한다면, map() 대신 flatMap() 메서드가 아래와 같이 쓰여야 한다.

Optional<OptionalUser> optionalUser = Optional.ofNullable(getOptionalUser());
String result = optionalUser
  .flatMap(OptionalUser::getAddress)
  .flatMap(OptionalAddress::getStreet)
  .orElse("not specified");

String value = null;
String result = "";
try {
    result = value.toUpperCase();
} catch (NullPointerException exception) {
    throw new CustomException();
}
String value = null;
Optional<String> valueOpt = Optional.ofNullable(value);
String result = valueOpt.orElseThrow(CustomException::new).toUpperCase();

 

 

※ 참조

Java의 정석 (저자 : 남궁성 / 도우출판)

https://www.baeldung.com/java-8-streams-introduction

https://www.baeldung.com/java-8-new-features

+ Recent posts