JVM 은 Java 코드 및 Application을 동작시킬 수 있도록 런타임 환경을 제공해주는 Java Engine 이다.

 

JVM 은 JRE의 일부분이며 Java 개발자로서 JVM 의 동작을 이해하는 것은 Java 구동 환경과 JRE(Java Runtime Environment) 을 이해하는 데 있어서 아주 중요한 부분이다.

 

Java 가 추구하는 가치는 WORA(Write Once Run Anywhere) 이며, 이는 모든 자바의 코드는 JVM 이라는 환경 위에서 동작하면서 어느곳이든 이식이 가능하다는 뜻을 포함하고 있다.

 

먼저 JVM 의 아키텍처는 다음과 같다.

 

하나하나 동작을 살펴보도록 하자.

 

Java Application 의 동작은 다음과 같은 순서로 이루어진다.

 

 

(1) Java JIT Compiler 가 Java 코드를 해석한다. Java Compiler 는 해석한 .java 코드를 .class 파일 형태로 해석결과로 만든다.

 

(2) 클래스 로더에 의해 .class 파일들은 다음 단계들을 거치며 해석된다.

 - Loading : 클래스 파일에서 클래스 이름, 상속관계, 클래스의 타입(class, interface, enum) 정보, 메소드 & 생성자 & 멤버변수 정보, 상수 등에 대한 정보를 로딩해서 Binary 데이터로 변경한다.

 

 - Linking : Verification 과 Preparation, Resolution 단계를 거치면서 바이트코드를 검증하고 필요한 만큼의 메모리를 할당한다. Resolution 과정에서는 Symbolic Reference 를 Direct Reference 등으로 바꿔준다.

 

 - Initialization : static block 의 초기화 및 static 데이터들을 할당한다. Top->Bottom 방식으로 클래스들을 해석한다.

참조 : (https://jins-dev.tistory.com/entry/Java-%ED%81%B4%EB%9E%98%EC%8A%A4%EB%A1%9C%EB%8D%94ClassLoader%EC%97%90-%EB%8C%80%ED%95%9C-%EC%9D%B4%ED%95%B4)

 

(3) 컴파일된 바이트코드는 Execution Engine 에 의해 머신별로 해석된다. 이제 Host System 과 Java Source 는 Byte Code 를 이용해 중계된다. JVM 이 할당된 메모리 영역을 관리하게 된다.

Execution Engine 은 3가지 파트로 구성된다.

 

 - Interpreter : 바이트코드를 Line by Line 으로 해석하고 실행시킨다. 동일 메서드가 여러번 Call 되더라도 매번 Interperter 가 호출된다.

 

 - JIT compiler(Just In Time Compiler) : Interpreter 의 효율성을 증가시켜준다. 모든 Byte 코드를 한번에 Compile 해놓고 Direct 로 변환할 수 있게 해줌으로써 Re-interpret 의 발생을 낮춰준다.

 

 - Garbage Collector : Un-referenced Object 들을 제거해주는 가비지 컬렉팅을 수행한다.

 

 

이처럼 일반적인 C 프로그램과 다르게 Java 는 ByteCode 로 해석하고 변환하는 단계가 있기 때문에 느릴 수 밖에 없다.

- Dynamic Linking : Java 의 Linking 은 런타임에 동적으로 일어난다.

- Run-time Interpreter : Byte Code 의 머신코드로의 컨버팅 역시 Runtime 에 일어난다. 

 

물론 최신 Java 버전은 병목현상을 효과적으로 제거했기 때문에 구버전의 자바와는 비교가 안될정도의 퍼포먼스를 갖는다. :)

 

참조 : https://www.guru99.com/java-virtual-machine-jvm.html

 

Java Virtual Machine (JVM) & its Architecture

Java String has three types of Replace method replace replaceAll replaceFirst. With the help of...

www.guru99.com

https://www.geeksforgeeks.org/jvm-works-jvm-architecture/

 

How JVM Works - JVM Architecture? - GeeksforGeeks

JVM(Java Virtual Machine) acts as a run-time engine to run Java applications. JVM is the one that actually calls the main method present in a… Read More »

www.geeksforgeeks.org

 

 

Lazy Evaluation 이란 프로그래밍이 동작하는 데 사용되는 계산 전략의 한 종류를 말한다.

 

일반적인 프로그래밍에서 계산식이 즉발적으로 수행되는데 비해 Lazy Evaluation 은 해당 계산식의 실제 수행을 필요할 때까지 늦춘다.

이렇게 되면 장점으로는 필요하지 않은 시점에서 해당 식의 "불필요한 수행" 을 막을 수 있으며 어떤 경우에는 실제 수행되는 시점에조차 수행 과정에서의 불필요한 동작들을 최적화시킬 수 있다.

다음의 예시를 확인해보자.

 

위의 코드에서 일반적인 수행이라면 checked 의 값은 무조건 먼저 계산이 된다.

foo 함수가 얼마만큼의 비용을 수반하더라도, foo(val) 의 Return 값이 checked 가 가져야할 값이므로 일반적인 수행에서 checked 값은 foo 함수의 결과를 저장하며 그에 따라 아래의 조건식을 수행하게 된다.

 

만약 위의 코드가 Lazy 하게 동작한다면 흐름은 조금 다르다.

먼저 변수 checked 는 foo 메서드가 val 을 argument 로 전달받아서 수행하게된 "어떤 값" 이라고 간주되고, 이를 이후 필요한 시점, 즉 조건문을 체크하는 아래 로직에서 실제로 사용하게 된다.

 

첫번째 조건문인 if(true && checked) 조건문에서 foo 함수는 호출되게 된다. checked 가 true 여야지만 해당 조건이 참이되기 때문이다.

하지만 두번째 조건문에서는 foo 함수는 호출되지 않는다. 알다시피 true 가 이미 or 의 조건문에 포함되어 있기 때문에 compiler 는 최적화를 통해 이를 수행하지 않는다.

 

<참고로 위의 예제코드는 이해를 돕기 위해 작성한 것일 뿐 실제로 Lazy 하게 동작하지 않음을 주의한다.>

 

이러한 Lazy Evaluation 은 프로그래밍에서 종종 사용되는 개념이며 실제로 많은 경우에 퍼포먼스의 향상을 가져올 수 있다.

 

Java에서도 Lazy Evaluation 을 사용할 수 있으며 보통 Java8 의 Lambda 를 이용해 이를 지원한다.

 

이제 위의 메서드를 Lambda 식을 이용해서 Lazy Evaluation 형태로 바꾸어 보고 비교해보자.

 

 

위의 예제를 변형한 코드이다.

코드에서는 Lazy Evaluation 을 함수형 인터페이스의 Supplier 메서드를 이용해 구현한 모습으로,

위의 main 동작에서 가장 마지막 조건문의 수행 시, foo 함수는 호출되지 않는다.

 

이와 같은 Lazy Evaluation 을 이용하면 사소한 부분이지만 만약 foo 함수의 비용이 막대한 경우에 큰 성능 상 이점을 가져올 수 있다.

 

특히 Lambda 가 대중화되고 Lazy Evaluation 과 같은 처리가 익숙해진 최근의 추세에서 확실히 개념을 이해하고 사용하는 것이 중요하겠다.

 



Java9 에서 새로 추가된 JShell 을 이용하면 마치 Java를 파이썬을 사용하듯이 손쉽게 프로토타이핑할 수 있다.


JShell 이란 Command Line Interface 로 제공되는 자바 코드 작성을 위한 도구이다.


당연히 복잡한 종류의 모듈을 JShell 을 이용해 제작하는 건 무리지만, 간단히 자바 코드를 실행하고, 모듈을 작성해보거나, 


Java Library 의 기능을 테스트해보아야 한다면 실무에서도 유용한 도구이다.



JShell 을 이용하는 방법은 다음과 같다.


1. Java9 를 설치한다.



2. 환경변수 Path 에 설치된 경로를 지정한다.




3. 커맨드 라인에서 Jshell 을 실행한다.




Jshell 을 이용하면, 커맨드라인에서 즉시 자바 명령어를 실행할 수 있으며, 다음과 같이 모듈의 Import 및 사용 역시 가능하다.


이렇게 실행하는 Java Statements, Definition, Expression 등을 스니펫(Snippet) 이라고 하며, 스니펫을 실행하면 JShell 에서 즉각적인 피드백(Feedback)을 제공해준다.






또한 JShell 은 위의 그림에 마지막에 작성된 스니펫인 /edit 을 이용하면, 별도의 Edit pad 를 제공하는데, 


이를 이용하면 JShell 만을 이용해서도 마치 IDE 를 사용하는 듯 하게 에디터를 이용해 작업을 해볼 수 있다.





실행시킨 Jshell 은 /exit 명령어를 이용해서 종료할 수도 있다.


실무에서도 JShell 은 간단한 코드를 프로젝트 수정없이 테스트할 경우 굉장히 유용하며, 본인은 때때로 알고리즘을 테스트할 때 JShell 을 이용하기도 한다.


따라서... 이제는 더 이상 간단한 자바 코드의 Prototyping 을 위해 이클립스를 켜지 않아도 된다!


이처럼 Java 는 버전을 올려감에 따라서 함수형 언어의 특징을 받아들이면서 좀 더 Modern 한 언어의 특징을 위해 업그레이드되어 가고 있다.


좀 더 자세한 사용은 아래 블로그에 잘 정리되어있으니 참고해보는 것도 좋을 듯 하다.


http://taewan.kim/post/trans_jshell/




Java 에서 사용하는 Thread Local 이란 간단히 설명해서 쓰레드 지역 변수이다.


즉, Thread Local 로 정의된 객체는 같은 Thread 라는 Scope 내에서 공유되어 사용될 수 있는 값으로 다른 쓰레드에서 공유변수를 접근할 시 발생할 수 있는 동시성 문제의 예방을 위해 만들어졌다.


한 쓰레드 내에서만 사용되는 변수라더라도 전역변수처럼 State 값을 부여해서 사용하게 되므로 가능한 가공이 없는 참조용 객체의 경우가 사용되며, 지나친 사용은 재사용성을 떨어트리고 부작용을 발생시킬 수 있다.


아래의 그림을 보자.




위의 그림에서 Thread A 와 Thread B는 각 쓰레드 내부에서 사용되는 ThreadLocal 을 갖고 있으며 이 객체는 해당 Thread 내에서 일정하며 다른 Thread 로부터 격리되어 있다.


Java 에서는 이 값을 Set 하고 Get 함으로써 전역변수처럼 사용이 가능하다.


(1) Thread Local 객체 생성


(2) Set 을 통해 Thread Local 에 값 세팅


(3) Get 을 통해 Thread Local 로부터 값 조회


(4) Remove 를 통해 Thread Local 의 값 해제



주로 쓰레드 로컬은 서버에서 클라이언트 요청들에 대해 각 쓰레드에서 처리하게 될 경우, 해당 유저의 인증 및 세션정보나 참조 데이터를 저장하는 데 사용된다.


이런 경우 Spring 등에서는 Interceptor 를 이용해서 쓰레드 로컬의 작업을 제어하는 경우가 많고, 그에 따라 처리할 수 있는 정보와 처리할 수 없는 정보로 디자인을 나누는 것이 중요하다.

실무에서 Use case 를 통해 활용 방법을 학습하는 것이 중요하다.





Java 11 이 나오는 와중에 아직 Java9 와 Java10을 정리하지 못했다.(반성;;)

본 포스팅에서는 Java8 에 비해 달라진 Java9 의 Feature 들에 대해 정리해보았다.


1. Java Platform Module System (Jigsaw Project)

Java8 의 주특징이 Lambda 와 Stream 을 중심의 함수형 프로그래밍 도구들이었다면, Java9 의 가장 특징적인 부분 중 하나는 모듈(Module) 의 등장이다.


Java9 부터 사용할 수 있는 모듈은 Class 나 Interface, Package 등을 포함할 수 있는 캡슐화 도구이다.


가령 모듈은 다음과 같이 정의할 수 있다.



module sample {
    exports com.myproject.module;
    requires commons;
}

module commons {
    requires java.base;
    requires java.xml;
}



위의 예시에서 module sample 은 commons 모듈을 import 하고, commons 모듈은 java.base 모듈과 java.xml 모듈을 import 하면서 com.myproject.module 로 export 되게 된다.


여기서 requires 로 지정된 모듈 각각은 서로 dependency 를 가진다. commons 모듈을 통해 java.base 모듈과 java.xml 모듈을 import할 수 있다.


모듈이 등장한 이유는 그 이전까지의 Java 버전이 사용했던 JAR 배포에 있다. 


JAR 파일은 JVM 에 의해 로딩되는 File Archive 로 dependency 에 대한 컨셉을 갖고있지 않다.

Module 은 이를 개선하려 하며, 로딩하는 과정에서 Dependency 를 정의하고자 한다.


또한 Module 은 JAR 파일이 지원하지않는 Encapsulation 에 대한 메커니즘을 지원한다. 포함된 package 의 컬렉션 각각에 대해 필요한 곳과 불필요한 곳을 적절히 정의하고 분류하며


Java의 모듈들을 보기 위해서는 CMD를 이용해서 다음 커맨드를 사용하면 된다.


> java --list-modules



2. JShell 의 지원


이제 Java를 별도의 컴파일 과정이나 main 함수 작성을 위한 Project Structure 의 구성없이 작동시킬 수 있는 REPL 도구인 JShell 이 제공된다.


REPL(Read - Eval - Print - Loop)이란 대화식 언어도구로, JShell 은 이를 지원하는 콘솔로 보면 된다.


대략 다음과 같이 JShell 을 이용할 수 있다.


<예시>


마치 Python 의 Shell 을 흉내낸듯한 CLI 를 통해 보다 쉽게 Java 언어를 구현하고 테스트해볼 수 있다.



3. JavaDoc 이 업그레이드되었다.


JavaDoc 을 사용하면 HTML 형식의 API Document 를 작성할 수 있는데, HTML5 가 지원되도록 추가되었다.

추가로 Java Lint 기능과 Module 에 대한 지원도 되었다.



4. Stream 및 Optional API 지원이 강화되었다.


몇몇 쓰기 불편했던 Java8 의 Stream API 들이 개선되었고, Optional 에 대한 상호 지원이 추가되었다.


iterate(), takeWhile(), dropWhile() 등과 같은 Stream 연산자들은 이제 Stream 의 Laziness 를 강화하며, 이를 이용하면 마치 Python 의 Yield 처럼의 동작도 구현 가능하다.


가령 takeWhile() 연산을 이용하면 스트림을 해당 조건을 만족하는 부분까지 Process 한 후 멈춰둘 수 있다. 이는 Stream Pipeline 을 좀 더 유연하게 사용할 수 있는 흘륭한 도구이다.


가령 Stream 에 Optional에 대한 지원이 추가됨으로써 Nullable 한 상황에 대한 Stream 레벨에서 핸들링이 가능해졌다. 

이전까지 ifPresent 로 조건 체크만 가능했다면, ifPresentOrElse() 와 같은 메서드는 Stream 내에서 Optional 을 지원함으로써 흐름의 분기도 구현이 가능하다.


마찬가지로 Optional 에도 Stream 지원이 추가되어 보다 복잡한 Stream Pipeline 을 구성할 수 있게 되었다.



5. Collection Factory methods


각종 Java Collection 들을 기존까지는 "생성 후 add" 방식으로 이용했다면, 이제는 Factory Method 로 손쉽게 초기화가 가능하다.


가령 다음과 같이 하면 손쉽게 Collection 을 만들 수 있다.



List<String> texts = List.of("hello", "world");



6. Private Interface method


Java 의 인터페이스가 private 메서드를 제공한다. 이제 Interface 구현시에도 외부에 공개할 필요없는 method 를 생성함으로써 캡슐화를 유지할 수 있다.


단, Private method 의 default 구현은 public 이기 때문에 사용에 있어 주의하자.



7. 익명클래스에 대한 Diamond Operator


이제는 <>(Diamond Operator) 가 허용되며, 제약없이도 익명클래스의 사용이 가능하다.



8. HTTP/2 


Java 의 HTTPURLConnection 이 HTTP/2 와 Websocket 등을 지원한다.

또한 Multiplexing 을 통한 다중 요청 처리 후 순서에 따른 응답이 가능해졌고, Push 기능을 지원한다.


그 외에도 JAR 의 다중 Release 가 가능해져서 프로젝트 관리가 쉬워진 부분, Language 지원, 시스템 프로세스 접근의 용이성을 위한 Process API 의 지원 등 기능이 추가되었다.




자바의 클래스로더는 자바의  Class  들을 런타임에 JVM으로 로딩하는 역할을 한다.
클래스로더는 JRE(Java Runtime Environment)의 한 부분이다.


JVM은 클래스로더 덕분에 파일의 구조나 파일 시스템을 고려하지 않을 수 있으면서 자바 프로그램을 작동시킬 수 있다.

이는 C/C++언어의 결과처럼 자바 프로그램이 하나의 Single Executable 파일을 생성하는 것이 아니라 여러 개의 자바 클래스들이 JAR의 형태로 묶여서 관리되다가 JVM에 의해 메모리에 올라가서 바이너리 형태로 동작하기 때문에 생기는 차이점이라 할 수 있다.

자바 클래스들은 한번에 모든 클래스가 메모리에 올라가지 않는다. 각 클래스들은 필요할 때 어플리케이션에 올라가게 되며, 이 작업을 클래스로더가 해주게 된다.

 

Class Loader 는 다음과 같은 역할을 해준다. 


 - Loading : 클래스 파일에서 클래스 이름, 상속관계, 클래스의 타입(class, interface, enum) 정보, 메소드 & 생성자 & 멤버변수 정보, 상수 등에 대한 정보를 로딩해서 Binary 데이터로 변경한다.


 - Linking : Verification 과 Preparation, Resolution 단계를 거치면서 바이트코드를 검증하고 필요한 만큼의 메모리를 할당한다. Resolution 과정에서는 Symbolic Reference 를 Direct Reference 등으로 바꿔준다.


 - Initialization : static block 의 초기화 및 static 데이터들을 할당한다. Top->Bottom 방식으로 클래스들을 해석한다.


(자세한 기능 동작은 다음을 참조

https://jins-dev.tistory.com/entry/JVMJava-Virtual-Machine-%EC%9D%B4-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%98%EB%8A%94-%EC%A7%80-How-JVM-Works)


자바의 클래스로더는 3가지의 서로다른 클래스로더로 구성되어 있다.

 

 (1) Bootstrap Class Loader
가령 java.lang 이나 ArrayList 같이 java.lang.ClassLoader 가 로드할 수 없는 bootstrap 또는 primordial 클래스들에 대해서는 JRE의 Bootstrap ClassLoader 가 이 역할을 해준다.
Bootstrap ClassLoader 는 native 언어로 만들어진 JVM 코어의 일부분으로, 일반 클래스 로더가 불러올 수 없는 클래스 들의 로딩을 담당해준다.

 

 (2) Extension Class Loader
Extension Class Loader는 bootstrap Class loader 의 자식클래스로 일반적인 Core Java Class 들의 로딩을 담당한다.

 

 (3) System Class Loader
System Class Loader는 시스템 환경변수 또는 클래스 변수 등과 같은 환경에 종속된 클래스들에 대한 로딩을 담당한다.


클래스로더는 런타임에 모든 클래스들에 대한 정의를 로딩한다. 이때 만약 클래스 매핑에 실패한다면, NoClassDefFoundError 를 만나보게 된다.

 

Java 의 클래스로더는 3가지 원칙을 지키며 동작한다.

 

1. Delegation :  Java의 클래스는 필요할 때 로딩되어야 한다는 원칙이다.

 

2. Visibility : Child 클래스는 Parent 클래스로더에 의해 로딩된 클래스들을 확인할 수 있지만, 그 역은 불가능하다는 원칙이다.

 

3. Uniqueness : 한번 클래스로더에 의해 로딩된 클래스는 재로딩되지 않는다. 가령, Parent 클래스로더에 의해 이미 로딩된 클래스는 Child 클래스로더가 다시 로드하지 못한다.

 

Java의 ClassLoader 클래스를 상속하면, 사용자정의 클래스로더를 구현할 수도 있으며, 언제든 findClass() 와 loadClass() 메서드를 이용해 동적으로 로딩이 가능하다.




Java8 에서는 기존의 Java 에 비해 다양한 기능들이 접목되었다. 

Optional 과 Lambda, Stream API 와 같은 기능들이 그것인데, 이는 새로이 대세로 떠오르고 있는 함수형 프로그래밍의 메타를 적용한 Java 의 새로운 진화라 할 수 있겠다.


Java8 에 대한 전반적인 소개는 다음 링크를 참조하자.

(http://jins-dev.tistory.com/entry/Java8-%EC%97%90%EC%84%9C-%EC%83%88%EB%A1%9C-%EC%83%9D%EA%B2%A8%EB%82%9C-API-%EB%93%A4?category=760006)


본 포스팅에서 집중해볼 것은 실무에서도 이제 널리 사용되고 있는 Stream API 에 대한 내용과 간단한 사용방법들이다.


 대부분의 이전 Java는 데이터를 처리하기 위해 컬렉션을 사용하고 이는 전통적인 반복문과 조건문 사용에 기인한다. 

컬렉션을 사용하여 할 수 있는 기능은 무한하고 강력하지만, 정작 데이터를 처리하는 패턴은 SQL에서 데이터를 찾는것과 비슷하게 데이터를 찾거나, 묶는것 정도에 국한된다.


 Java8의 Stream API 는 컬렉션을 이용했던 기존의 코드를 좀 더 깔끔하게 구현하고 병렬처리에 있어서도 이점을 가질 수 있도록 하기 위하여 제공된다. 

Stream API의 기본이 되는 원형 stream() 메서드는 모든 컬렉션 타입에 대해 제공되며, 컬렉션 내의 Element 들에 대해 하나씩 구분한 Stream 결과를 나타낸다. 


먼저 Stream API 가 어떻게 구성되는지 알아보자.

Stream API 는 생성연산, 중간연산, 최종연산으로 Flow 가 구성되어 있다. 생성연산은 스트림의 생성을, 중간 연산은 스트림을 통한 데이터 가공 및 변환을 담당하고 최종 연산은 Stream 의 사용을 담당한다. 다음은 연산별로 Stream API 를 정리한 내용이다.



순서대로 스트림 생성연산, 중간연산, 최종연산을 분류하였다.

Stream API 를 사용하기 위해서는 반드시 위의 Pipeline 을 순서대로 따라야 하며, 여러개의 중간연산을 붙이는 것도 가능하다. 다만, 생성연산과 중간연산들은 Stream 을 반환해야만 추가 연산이 가능하다는 점을 잊지 말자.

이를 이용해서 다음 리스트에서 원하는 정보를 추출해보자.


Stream API 를 이용하면 위의 리스트에서 70 이상의 숫자들을 정렬해서 다음과 같이 추출해낼 수 있다.



결과도 다음과 같이 확인해볼 수 있다.



코드 역시 상당히 직관적으로 이해할 수 있다. 코드상에서 우리는 numList 의 Stream 을 생성하여, 70 이상인 요소들만 필터링하여 정렬 후 Array 형태로 반환하고 있다.


이처럼 Stream API 를 사용하면 만들어진 스트림을 별도의 저장이나 가공없이 filter(속성에 맞게 필터링), sorted(정렬), map(정보를 추출), collect(정보를 가공) 할 수 있으며 이 메서드들은 collect에 의해 연속되어 처리된 결과를 리턴하거나 형변환, 또는 Stream 연산의 결과를 취합하여 반환한다. 


다음은 몇가지 자주 사용되는 연산에 대한 설명이다.

(1) map
함수 형태 : Stream<R> map(Function<? super T, ? extends R> mapper)
설명 :  T 타입 객체를 입력받아 R 타입을 반환하는 스트림 생성. 가장 많이 쓰이는 함수 중 하나이며, Stream 을 다른 형태의 Stream 으로 매핑시키는 역할을 한다.

(2) filter
함수 형태 : Stream<T> filter(Predicate<? super T> predicate)
설명 : Predicate(T 를 입력으로 받아 Boolean 을 반환하는 조건 식) 람다식이 true를 반환하는 새로운 스트림 생성

(3) flatMap
함수 형태 : Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
설명 : T 타입을 입력으로 받아, R 타입의 객체를 1:N 으로 매핑하는 스트림을 생성. 말그대로 평평하게(Flat) 만드는 함수이다.

(4) peek
함수 형태 : Stream<T> peek(Consumer<? super T> action)
설명 : T 타입의 매개변수를 입력받아 void 를 반환하는 함수를 실행하고 Stream 을 그대로 반환.

(5) limit
함수 형태 : Stream<T> limit(long maxSize)
설명 : maxSize까지의 Element 들만 반환하는 스트림 생성

(6) sorted
함수 형태 : sorted(), sorted(Comparator<T> comparator)
설명 : 정렬된 스트림을 생성. Integer 라면 자동정렬하지만, T 자료형에 대해 직접 Comparator 구현 필요하다. 
입력으로 주어지는 전체 스트림의 요소를 한꺼번에 정렬하기 때문에, 연속적으로 적용이 불가능하다.

(7) distinct
함수 형태 : distinct()
설명 : 값은 값을 갖는 요소를 중복해서 발생하지 않는 스트림 생성

8번부터는 Terminal Operation 들이다.

(8) forEach
함수 형태 : void forEach(Consumer<? super T> consumer)
설명 : T 타입을 입력으로 받아 void 를 수행하고 void 를 반환한다. for문과 동일하지만, 동작은 동일하지 않다. 
(forEach 는 Stream API 의 일부라는 점을 명심하자. 일반 for 구문과 다르게 PipeLine 을 충실히 따른다.)

(9) count
함수 형태 : count()
설명 : Stream 의 요소의 갯수를 반환한다.

(10) anyMatch
함수 형태 : boolean anyMatch(Predicate<? super T> predicate)
설명 : T를 입력받아 boolean 을 반환하는 predicate 람다식을 만족하는 항목이 Stream 에 하나라도 존재하는지를 반환한다.

(11) noneMatch
함수 형태 : boolean noneMatch(Predicate<? super T> predicate)
설명 : anyMatch 와 동일하지만 람다식을 만족하는 항목이 없는지를 반환한다.

(12) collect
함수 형태 : <R,A> R collect(Collector<? super T,A,R> collector)
설명 : Stream 을 Collector 형태로 치환한다. List 나 Map 등의 Collection 으로 변환하는 함수가 들어간다.


Stream API 를 사용할 때 꼭 알아두어야 할 점은 Stream은 요소들을 보관하지 않으며 요소들은 하부의 컬렉션에 보관되거나 필요할 때 생성된다는 점이다. 또한 원본을 변경하지 않는 대신 결과를 담은 새로운 스트림을 반환한다. 

스트림 연산은 가능하면 지연(Lazy) 처리 되기 때문에 결과가 필요하기 전에는(최종 연산 이전) 실행되지 않는다. 

따라서 최종연산 이전의 연산의 순서는 보장할 수 없다. (가령, Stream 연산에 2개 이상의 sorted 가 있을 경우 어떤 sorted 가 먼저 수행될지는 불분명하다.)


이 말을 다시한번 정리해보자. 스트림은 최대한 연산을 지연하며, 그에 따라 최종연산에 오기 전까지는 연산이 실제로 수행되지 않는다는 것이다.


이를 통해 잘 알아두어야 할 점은, 각 연산별 Pipeline 은 
절차적이지 않다는 점이다. 가령, stream 에 peek 을 통해 연산을 수행한 뒤 collect 로 취합하더라도 루프를 중첩해서 돌지 않는다. 
Stream 은 최종 연산 수행시 내부 파이프라인을 통해 최적화된 연산을 수행한다.

이처럼 Stream 은 각 요소 단위로 연산이 수행되는데 지연(Lazy) 처리 특성에 따라 최종연산에서 한꺼번에 처리가 되므로 최종연산이 선언되지 않은 체인 스트림에서 동작을 수행할 경우 이는 반영되지 않는다. 즉, 다음과 같은 코드는 아무 동작도 하지 않는다.



위의 연산은 peek 이라는 중개 연산을 마지막으로 구문을 마치고 있기 때문에 결과적으로 Stream 연산이 수행되지 않는다. 따라서 출력도 되지 않는다.


이러한 Stream의 특성을 Lazy & ShortCircuit 이라 하며 이는 Stream 이 Collection 과 다르게 그 자체만으로 자료구조가 아닌 연산을 위한 자료구조인 특성을 반영한다 


Stream 을 이용하면 가독성이 뛰어나고, 잘 사용하면 성능적으로도 뛰어난 결과를 가져올 수 있으며 쉽게 병렬 처리 환경으로 이식도 가능하다.

Stream 의 응용은 무궁무진하며 사용하기 위해서는 익숙해지는 것이 중요하다. 많이 사용해보고 적절하게 응용해보도록 하자.







 가비지컬렉터 의 동작을 이해하는 건 Java 개발자의 가장 필수적인 요건이다. 나름 Java 를 할 줄 안다고 자만하고 있었는데, 이 부분을 공부하면서 깊이 반성하였다.


 C/C++ 개발자의 경우 객체를 관리하는데 있어서 메모리는 순전히 개발자의 몫이다. 필요한 시점에 생성하고 불필요한 시점에 반환해야 하며 올바른 메모리 주소를 참조할 수 있도록 관리해주어야 한다. 

반면 Java 의 경우 개발자는 이 부분에 있어서 신경을 덜 써도 되며 이는 Java 언어가 제공하는 Garbage Collector 가 이 일을 해주기 때문이다. Garbage Collector 는 항상 background 에서 데몬 쓰레드로 돌아가면서 접근 불가능한(Unreachable) 상태가 된 객체들의 메모리를 정리해준다.



 먼저 Garbage Collectors (이하 GC) 의 동작을 이해하기 위해서는 JVM 의 메모리 관리에 대해 알아야 한다. JVM에는 일반적으로 Young Generation / Old Generation 이라는 두가지의 물리적 공간이 존재한다.


 GC는 2가지 전제를 갖고 있다. 대부분의 객체가 금방 Unreachable 한 상태가 된다는 것과 Old 객체에서 Young 객체로의 참조가 적다는 점이다. 다음은 각 물리 공간에 GC 가 어떻게 동작하는지를 설명한다.


- Young Generation 영역 : 새롭게 생성한 객체가 위치한다. 많은 객체가 이 영역에 생성되었다 사라지며 이를 Minor GC라고 한다.


- Old Generation 영역 : 접근불가능한 상태가 되지않아 Young 영역에서 살아남은 객체가 이 영역으로 복사된다. Young 영역보다 크게 할당되며 GC는 적게 발생한다. 이 영역에서 객체가 사라질 때 Major GC 또는 Full GC 가 발생한다. 

Old 영역에서는 Card table이라는 512 byte의 Chunk 가 존재하며 Old영역의 객체 중 Young 영역의 객체를 참조하는 객체의 정보들을 저장한다.


 Young 영역은 Eden 영역과 2개의 Survivor 영역으로나뉜다. New 를 이용해서 객체를 생성하면 이는 Eden 영역에 위치하게 된다. Eden 영역에서 GC가 한번 발생 후 살아남은 객체는 Survivor 영역 중 하나로 이동한다. 이 때 객체가 Eden 영역에서 Survivor1, Survivor2 영역으로 이동할 때 Minor GC 가 수행된다.


지속적인 Eden 영역에서의 GC 이후 Survivor 영역으로 객체가 계속 쌓이고 Survivor 영역 내 빈곳으로 살아남은 객체들이 이동한다. 이러한 과정을 계속 반복 후 Survivor 영역들이 가득차게 되면, 남은 객체가 Old 영역으로 이동한다.


 2개의 Survivor 영역에 모두 데이터가 존재하거나 모두 사용량이 0이면 시스템은 비정상이다. Old 영역은 기본적으로 데이터가 가득차면 GC를 수행하며 GC 방식은 JDK 7 기준으로 5가지가 있다.


(1) Serial GC : Mark-sweep-compact 알고리즘이 사용된다. Old 영역에 살아있는 객체를 Mark 하고 Heap의 앞부분부터 살아있는 객체를 Sweep한 뒤, 힙의 앞부분부터 객체를 쌓는다.(Compact) 메모리와 CPU 코어수가 적을 때 좋다.


(2) Parallel GC : Serial GC와 기본적인 알고리즘은 같지만, GC를 처리하는 Thread의 개수가 여러 개다. 메모리와 CPU 코어 수가 많을수록 좋다.


(3) Parallel Old GC : Parallel GC와 같지만 Old 영역의 GC 알고리즘만 다르다. Mark-Summary-Compact 알고리즘이며 조금 더 복잡하다.


(4) CMS GC : Stop-the-world 이후 Initial marking 시 살아있는 객체만 찾는다. 이후 concurrent mark 단계에서 참조를 따라가며 새로 추가되거나 참조가 끊긴 객체들을 remark 한다. 모든 작업이 멀티스레드 환경에서 동시진행되기 때문에 stop-the-world 시간이 매우 짧은 대신 memory와 CPU 를 많이 사용하고 compaction 단계가 제공되지 않는다.


(5) GI GC : Young 영역과 Old 영역이 없이 매우 빠르게 객체를 할당하고 GC한다.



 GC가 실행되기 전 JVM은 Application의 실행을 멈춘다. 이를 Stop-the-World 라 하며 GC 쓰레드 이외의 쓰레드들의 작업이 멈춘다. 이 때 이 Stop the world 를 줄이는 작업을 GC 튜닝이라 한다.


GC 튜닝이 모든 Java Application 에 필수적인 것은 아니지만, 크리티컬한 요청을 담당하는 서버나 코어 엔진의 경우 GC 튜닝이 필요하다. GC 튜닝을 위해 지켜야할 기본적인 원칙은 GC 튜닝이 로직에 영향을 미치지 않도록 가능한 늦게 수행하고, 객체 생성을 최소화하는 것이다.


 즉, GC 튜닝은 Java 코드 최적화와 맞물려 있는 영역이라고도 할 수 있다. 가령 String 의 append 시, + 연산으로 2개 이상의 String 을 더하는 대신, StringBuilder 등을 쓰는 것도 일종의 메모리 튜닝이라고 할 수 있다. 그 외에는 설정적인 부분으로 JVM 옵션으로 메모리 크기를 조절하고 GC 방식을 옵션으로 지정해주는 등이 있다.


(1) 메모리 관련 GC 튜닝을 위한 JVM 옵션

 -Xms : JVM 시작 시 힙 영역의 크기

 -Xmx : 최대 힙 영역 크기

 -XX:NewRatio : New 영역과 Old 영역의 비율. (New 영역의 비율을 전달한다.)

 -XX:NewSize : New 영역의 크기

 -XX:SurvivorRatio : Eden 영역과 Survivor 영역의 비율 (Survivor 영역의 비율을 전달한다.)


(2) GC 방식 관련 GC 튜닝을 위한 JVM 옵션

 - Serial GC :

-XX:+UseSerialGC 


 - Parallel GC : 

-XX:+UseParallelGC

-XX:ParallelGCThreads=value


 - Parallel Compacting GC : 

-XX:+UseParallelOldGC


 - CMS GC :   

-XX:+UseConcMarkSweepGC 

-XX:+UseParNewGC

-XX:+CMSParallelRemarkEnabled

-XX:CMSInitiatingOccupancyFraction=value 

-XX:+UseCMSInitiatingOccupancyOnly

 - G1 : 

-XX:+UnlockExperimentalVMOptions

-XX:+UseG1GC


위와 같은 옵션을 적용한다고 해도 GC 의 튜닝이 비약적인 시스템의 성능 향상을 가져오게 되리라고 보장할 수는 없다. 결국 GC 튜닝은 계속적인 모니터링을 통해서 이루어지는 것으로, 복잡한 시스템의 메모리 구조를 공식처럼 맞춰서 설정하기 보다는 지속적인 모니터링과 성능 향상을 위한 노력이 필요하다.




기본이되는 내용이지만 짚고 가야할 내용이라 정리해두었다.



 일반적으로 Primitive Type 대해서 final 선언되면 해당 객체를 바꿀 없지만, Object 경우 final 선언되면 해당 객체 혹은 해당 객체 멤버의 내용은 바꿀 있다.


 Object 앞에 선언되는 final 의미는 해당 객체를 가리키는 포인터를 바꿀 없게 하는 의미로, 해당 이름으로 객체의 선언 / 재할당을 막는 다는 의미가 된다.


(ex)

Public foo(Obj_1 Obj1, final Obj_2 obj2){

             Obj1 = new Obj_1();      //가능

             Obj2 = new Obj_2();      //불가능

             Obj1.setId(12345);         //가능

             Obj2.setId(12345);         //가능

}


 시스템이 굉장히 복잡하다고 가정했을 , 특히 멀티 쓰레드 환경에서 Thread Safe 하지 않은 객체에 대해 자바 가상 머신의 예기치 못한 동작으로 특정 메모리 영역의 객체는 얼마든지 다른 객체로 변할 여지가 있다.


이는 개발자 입장에서는 버그이지만 JVM입장에서는 버그가 아닌 동작일 있다. 그런 경우를 방지하기 위해 메모리를 참조하는 주소를 final 키워드를 이용해서 고정시켜놓으면 그러한 불상사를 예방할 있다.


 Java8에서는 Lambda 식과 같은 함수형 프로그래밍을 지원하기 위한 API를 포함해서 이를 위한 특징적인 API 들이 많이 생겨났다.

그 중에서도 이 포스팅에서는 핵심적인 3가지, Stream, Optional, Lambda 에 대해 정리하고자 한다.


 먼저 컬렉션을 다루기위한 API 중에는 Stream API 가 존재한다.

Java 에서 컬렉션은 데이터를 관리하고 제어하기 위한 좋은 컨테이너이지만 구현의 인터페이스 적인 측면에 있어서는 깔끔하다고 보기는 힘들다.


특히 이것은 SQL 문과 비교해보면 이해가 쉽다. 학생 정보를 담고 있는 데이터 컬렉션에서 특정 성적 이상을 가진 학생들을 찾을 때 Collection을 이용하면 다음과 같이 구현할 수 있다.


List<Student> students = getStudentList();
List<Student> List = new ArrayList();
For(Student s : students)
{
    If(s.getGrade() >= 80)
        List.add(s);
}


반면, Sql 문을 사용한다면 이는 조건절이 포함된 SELECT 구문 하나로 처리가 가능하다.

Java8의 Stream API는 이러한 복잡한 구조의 데이터 처리를 간단하게 해주어 복잡도를 낮추는 설계가 가능하게 한다. 또한 이런 종류의 Lazy collection 은 멀티 스레드 및 병렬처리 환경에서도 개발자에게 상당히 유연한 선택지를 갖게 한다. 또한 한번만 소비 가능한 형태의 컬렉션이기 때문에 메모리 관리 차원에서의 이점도 있다.



List<Student> list = students.stream().filter(t->t.getGrade()>=80).collect(Collectors.toList());


Stream API 는 위와 같이 Collection 의 각 요소들을 따로따로 주어진 함수구문(위에서는 람다식)에 따라 처리하며 내부 버퍼를 통해 주어진 형태로 반환해준다.


Stream API 에는 위에 적용된 filter 나 collector 와 같은 메서드를 포함해서, map, flatMap, reduce, peek 등 다양한 메서드를 제공한다.

Stream API 의 자세한 사용에 있어서는 추가로 포스팅하고자 한다.


 새로 추가된 Optional API 는 Java8 에서 제공하는 Null이 포함되지 않는 Collection이다.

NullPointerException 을 걱정하지 않아도 되며, ifpresent(함수식) 기능을 통해 무결성 검증과 동시에 함수식을 수행할 수도 있다. 

가령 NULL 이 될 수 있는 객체에 대해 다음과 같이 핸들링 할 수 있다. 



public Student getStudentById(String studentId) {
// Stream API 의 findFirst() 는 그 자체로 Optional 을 return 하기 때문에 사실 이는 좀 어색한 함수 구현이다.
return getStudentList().stream()
.filter(student->student.getStudentId().equals(studentId)).findFirst().get();
}

public void foo() {

Student student = getStudentById("철수");

if(Optional.ofNullable(student).isPresent()) {
System.out.println("철수라는 학생이 존재합니다.");
} else {
System.out.println("철수라는 학생은 없습니다.");
}
}


위와 같이 Optional 을 사용하면 Null 을 핸들링할 수 있는 객체를 다룰 수 있다. if(student == null) 과 같은 형태 대신 좀 더 직관적으로 다양한 이용이 가능하다.


마지막으로 Java8의 람다식은 함수형 인터페이스를 제공함으로써 지원이 된다.

 함수형 인터페이스를 만드는 방법은 @FunctionalInterface 어노테이션을 이용해 명시적으로 생성도 가능하지만, method가 하나 존재하는 인터페이스를 선언함으로써 생성이 가능하다.

다음의 예시를 참조할 수 있다.



Public interface Foo
{
    Int calc(int a, int b);
}
{
    Foo add = (int a, int b) -> { return a+b; };
    Foo minus = (int a, int b) -> { return a-b; };
    Int addv = add.calc(3, 5);      //8
    Int minusv = minus.calc(5, 3);  //2
}


위의 예시는 직접 FunctionalInterface 를 구현하여 Lambda 식을 적용해본 것이고, 실제 많이 사용되는 것은 Java8 에서 제공하는 API 들이다. 다음은 그 몇가지 종류이다.


Function<T, R> : T 타입의 입력파라미터를 받아 R 타입을 리턴한다.


Supplier<T> : void 타입의 파라미터를 입력받아 T 타입을 리턴한다.


Consumer<T> : T 타입의 파라미터를 받아 void 를 리턴한다.


Predicate<T> : T 타입의 입력을 받아 boolean 을 리턴한다.


BiPredicate<T, U> : T와 U 타입을 입력받아 boolean 을 리턴한다.


UnaryOperator<T, T> : T 타입 2개를 입력받아 T 타입을 리턴한다.


이 외에도 많은 종류의 FunctionalInterface 를 제공하며, 잘 사용하면 함수형 프로그래밍의 장점을 누릴 수 있다.


+ Recent posts