프로그램 언어를 해석하고 실행시키는 대표적인 방법으로 Compile 과 Interpret 방식이 있다.

Compile 작업은 Compiler 에 의해 실행되고, Interpret 작업은 Interpreter 에 의해 실행되는데, 두 컨셉이 명확하게 다르기 때문에 

많은 프로그래밍 언어들은 둘 중 한가지 방식을 통해 언어를 실행하도록 설계된다. (Java 와 같이 두가지를 모두 채용하는 경우도 있다!)

그렇기 때문에 Compiler 와 Interpreter 를 이해하는 것은 어떤 언어를 배우던지간에 해당 언어의 구동원리를 배울 수 있는 중요한 선행학습이라할 수 있겠다.


컴파일 (Compile)

프로그래밍 언어를 Runtime 이전에 기계어로 해석하는 작업 방식이다.
이때 원래의 소스를 원시 코드, 바뀐 코드를 목적 코드(Object Code) 라 한다.

런타임 이전에 Assembly 언어로 변환하기 때문에 구동 시간이 오래걸리지만, 구동된 이후는 하나의 패키지로 매우 빠르게 작동하게 된다.
구동시에 코드와 함께 시스템으로부터 메모리를 할당받으며 할당받은 메모리를 사용하게 된다.

런타임 이전에 이미 해석을 마치고 대게 컴파일 결과물이 바로 기계어로 전환되기 때문에 OS 및 빌드 환경에 종속적이다.
그러므로 OS 환경에 맞게 호환되는 라이브러리와 빌드환경을 구분해서 구축해줘야 한다.

Compile 언어의 대표격으로 C / C++ 와 같은 언어들을 들 수 있으며, Java 역시 Byte Code 로 바꾸기 위한 과정에서 컴파일을 수행한다.


인터프릿 (Interpret)

런타임 이전에 기계어로 프로그래밍 언어를 변환하는 컴파일 방식과 다르게, 런타임 이후에 Row 단위로 해석(Interpret) 하며 프로그램을 구동시키는 방식이다.

프로그래밍 언어를 기계어로 바로 바꾸지않고 중간 단계를 거친 뒤, 런타임에 즉시 해석하기 때문에 바로 컴팩트한 패키지 형태로 Binary 파일을 뽑아낼 수 있는 Compile 방식에 비해 낮은 퍼포먼스를 보이게 된다.

런타임에 직접 코드를 구동시키는 특징이 있기 때문에 실제 실행시간은 느리며, 대신 런타임에 실시간 Debugging 및 코드 수정이 가능하다.

또한 메모리를 별도로 할당받아 수행되지 않으며, 필요할 때 할당하여 사용한다. 이와 관련되어 코드의 흐름 자체도 실제 필요할 때, 실제 수행되어야하는 시점에 수행되기 때문에 덕타이핑(Duck Typing) 이 가능한 측면이 있으나, 반대로 정적 분석이 되지않는 Trade off 를 갖고 있다.

 

대표적인 Interpreter 언어로는 Javascript 와 같은 스크립팅 언어들이 있다. 하지만, 스크립트 언어 뿐 아니라 컴파일 이후의 동작에서 Interpret 을 수행하는 언어들도 많이 존재한다.


많은 프로그래밍 언어들의 인터프리터는 해석을 위한 Virtual Machine 을 두고, Machine 위에서 Interpret 을 수행하게 되는데, 이 때 해석의 기반이 되는 머신들이 OS 환경들을 지원해줌으로써, 해당 방식으로 인터프리터는 OS 및 플랫폼 에 종속되지않는 프로그램 구동이 가능하게 된다.
(이런 특징을 지닌 Interpreter 는 Java 의 JVM 과 Python 의 Analyzer 가 있겠다.)


컴파일러와 인터프리터의 차이는 잘 이해하고 언어와 환경을 파악하는데 활용하는 것이 중요하다.

 

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 과 같은 처리가 익숙해진 최근의 추세에서 확실히 개념을 이해하고 사용하는 것이 중요하겠다.

 


메서드와 함수의 차이점은 간단하면서도 기초적인 내용이지만 자주 되새겨지는 개념이 아닌데다가, 비슷하게 혼용되어 사용되다보니 많이 잊게 되는 내용이다.


차이점 먼저 서술을 하자면, Method 는 "객체" 에 대한 코드를 말하고 동작(Operation)의 결과로 동작을 수행한 객체가 영향을 주거나 받는다.


즉, Operation 은 해당 메서드를 소유한 객체 중심으로 발생한다.


반면, Function 은 특정 형태의 Data 를 받아서 내부 동작(Operation)을 수행한 후 특정 형태의 Output Data 를 반환한다.


즉, Operation 은 객체와 무관한 독립적인 그 자체의 코드 조각으로의 의미를 지닌다.



- 정리


: Method - Member. 객체에 대한 Operation 을 수행하는 코드 조각.


: Function - Free. 객체와 독립적인 Operation 을 수행하는 코드 조각.



C 에서 사용되는 Logic 들은 모두 함수(Function) 이며 Java 에서 사용되는 Logic 들은 모두 메서드(Method) 이다.






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 를 통해 활용 방법을 학습하는 것이 중요하다.




컴파일 과정은 사람이 이해할 수 있는 High Level Programming Language 로 구성된 소스코드를 기계가 이해할 수 있는 Lower Level Language 로 바꾸는 과정이다.


컴파일러는 다음과 같은 과정을 통해 컴파일을 수행한다.


[출처 : https://www.programcreek.com/2011/02/how-compiler-works/]


위의 그림은 컴파일의 단계를 간략하게 설명한다. 다음은 그림에 대한 설명이다.


(1) Lexical Analysis 

소스코드를 Token 으로 분할한다. 모든 키워드와 Parenthesis, 변수들 및 괄호들을 분리해낸다.


(2) Syntax Analysis

앞선 단계에서의 스캔으로 만들어진 토큰들(Token Stream)의 문법을 분석하기 위한 자료구조료 변형한다.

이렇게 만들어지는 자료구조를 Parse Tree 라고 한다.

이 단계에서는 Token 이 Valid 한지 검출하지 못하며, Token 이 사용되기 이전에 정의 또는 초기화되어있는 지 등 정적분석은 불가능하다.


또한 이단계에서 파싱이 일어난다. Parsing 작업은 Top-Down, Bottom-Up 두가지 방식으로 나뉜다.

Top-Down Parsing 은 Parse Tree 의 윗쪽부터 파싱을 수행하며, Bottom-Up Parsing 은 트리의 아래쪽부터 파싱이 수행된다.


유명한 파서의 종류로 Top-Down 방식의 Non-Backtracking Predictive Parser 인 LL Parser와 Bottom-Up 방식의 LR Parser 가 존재한다.


(3) Semantic Analysis

각 기호들 및 구문들을 의미있는 값들로 변경한다. 가령 기호 < 는 bool 을 Return 하는 Operand 함수로,

While 과 같은 키워드는 반복 구조를 이루는 void 함수로, 각 변수는 메모리로 치환한다.

이 단계에서 Type 의 Mismatch 나, 변수의 미정의, 파리미터 미정의 등 문법적 요소들이 검증된다.


(4) IR Generation

구문 분석으로 이루어진 자료를 중간 언어로 변경하는 작업을 수행한다.

IR 은 Intermediate Representation 의 약어로 소스코드에 근접한 기계어인 High Level IR, 타겟 머신에 종속적으로 디자인된 Low Level IR 이 존재한다.

컴파일러는 소스코드를 High Level IR -> Low Level IR 로 변경한 뒤 Target Machine Code 로 해석한다.


(5) IR Optimization

중간 언어를 최적화한다. 불필요한 루프를 없애거나, 사용하지 않는 변수의 정리 등이 수행된다.


(6) Code Generation

Syntax Analyzer 및 Semantic Analyzing 된 출력값을 Low level Code 로 해석한다.

이 단계를 거쳐서 Assembly Code 등의 Object Code 로 번역된다.


이 단계에서 수행되는 작업들은 다음과 같다.

 - Instruction Selection : 어떤 명령어를 사용할 것인지

 - Instruction Scheduling : 어떤 명령어를 먼저 실행할지 (최적화)

 - Register Allocation : 변수들을 프로세서 레지스터에 할당

 - Debug data generation : 디버그 모드인 경우 디버그를 위한 코드를 생성


(7) Optimization

위의 과정에서 생성된 코드를 한단계 더 최적화한다.

이 단계의 최적화는 두단계로, Machine 에 종속되지 않은 일반적인 형태의 최적화와 Machine 에 종속된 최적화가 이루어진다.

이 과정을 거치면서 중복 제거, 메모리 확보, 코드 정리, 루프 최적화, Control Flow 개선이 일어난다.


위의 단계들을 거치면 High Level Source Code 는 Machine Code 로 변환된다.

Generating 되는 코드는 대게 머신별로 다르게 되며, 세부로직 및 최적화의 과정 역시 컴파일러에 따라 차이가 존재하게 된다.




본 포스팅은 컴파일러에 대한 간략한 소개를 다루고 있으며 더 자세한 내용은 다음 링크들을 참조한다.

(이해해야할 분량이 많다.)


참조

https://www.tutorialspoint.com/compiler_design/index.htm

https://www.programcreek.com/2011/02/how-compiler-works/



해외 사이트의 좋은 글을 퍼와서 번역한 내용입니다. :)

특히 중요한 부분 위주로 변역하여 올린 내용으로, 이제 막 개발에 흥미를 붙이신 분들이라면 도움이 될 만한 습관들과 방법들을 정리해보았습니다.

(원 출처 : http://studyorcrytrying.tumblr.com/post/141574889807/general-always-comment-your-code-commenting-your)



좋은 개발자가 되기 위한 습관들


항상 코드에 주석을 달자.

코드를 만들면서 주석을 다는 것은 머릿속으로 생각을 정리하는 것 뿐 아니라, 다른 사람들이 코드를 읽었을 때 의미를 좀 더 분명히 할 수 있는 좋은 방법이 됩니다. 물론 줄마다 주석을 달 필요는 없지만, 특히 복잡한 로직이 포함될 경우에 주석은 코드를 파악하기 위한 좋은 습관입니다.


메소드와 함수들에 대해서는 기록하세요.
당신이 정의하고 만든 메서드에 대해 문서화하는 것은 프로그램 전체 구조를 추적하는데 있어 좋은 방법이 됩니다. 메소드 이름들을 리스팅하고, 어떤 인자를 Argument 로 갖는지, 어떤 동작을 통해 어떤 값을 반환하는 지는 프로그램 구조를 생성할 때, 각 작업 단위를 명확히 하는데 큰 도움을 줍니다.  또한 이는 프로젝트에 새로운 멤버가 프로젝트를 빨리 파악하는 데 큰 도움이 되기도 합니다.


당신만의 코딩 스타일을 확립하세요.

아마 처음 코딩을 시작할 때, 많은 코딩 스타일들이 레퍼런스 등을 통해 당신에게 유입될 것입니다. 괄호는 중구난방일 것이고, 불필요한 공간을 만들거나 변수 네이밍이 제각각일 수 있습니다. 경험을 통해 자신만의 코딩 스타일을 찾게 된다면, 코드를 빨리 파악하고 작성하는데 큰 도움이 될 것입니다.


사용하는 언어에 대한 Official 사이트를 알아두세요.

당신이 사용하는 C++, Android, Java 등등 언어에 대한 공식 웹 사이트를 알고 있다면 꼭 참조해서 알아보는 것이 중요합니다. 새로운 정보를 지속적으로 습득하는 것 뿐 아니라, 학습 자체를 하는 것에 있어서도 공식사이트가 갖는 공신력은 큰 영향을 가집니다.


당신만의 코드를 실험하세요.

이 부분은 코딩 스타일의 확립과도 일치되는 내용입니다. 만약 궁금하거나 실험해보고 싶은 부분이 있다면 얼마든지 도전해보세요! 새로운 기술을 적용하고 당신만의 프로그램을 당신만의 방법으로 최적화 해보는 것은 코딩 실력향상에 있어서 큰 도움을 줍니다. 심지어 당신의 선생님 / 사수가 당신을 최고의 코드를 만들게 인도하는 것보다도 더 큰 향상을 이루어낼 수도 있습니다.


당신이 한 작업을 디버깅하세요.

문제가 생겼을 시, 당신이 만들어낸 코드의 작업을 쭉 따라가면서 디버깅을 해보는 것은 좋은 습관입니다. 중간중간 작업의 단위를 나누어 디버깅을 하는 습관을 들인다면, 디버깅의 단위가 커져서 감당할 수 없게 되거나, 전체 모듈을 다시 코딩하는 불상사를 방지할 수 있습니다.




좋은 개발자가 되기 위한 초보 개발자들의 과제


출력 결과를 읽는 시간을 충분히 가지세요.

중요한 부분이나 중요한 결과가 도출되는 부분에서 만들어내는 프로그램의 출력 결과를 무시하는 경우가 많습니다. 이 부분을 읽는 것을 불필요하다고 여기지 마시고 충분히 관찰해본다면 내부 구동 원리를 파악하고 학습에 도움이 될 수 있습니다.


코드 상단에 당신의 문서라는 걸 명시해보세요. 
많은 IDE 들은 author와 문서의 description 을 기입하는 기능을 제공하고, 이는 프로젝트 협업 시 어떤 파일에서 어떤 의도로 당신이 설계했는지 파악하는데 도움이 될 수 있습니다.

변수 들에 대한 리스트 만들기
당신이 사용하는 중요한 변수들이 있다면 리스트를 만드세요. 프로그래밍은 복잡한 작업이고, 시간이 지날 수록 예전에 사용하던 당신의 코드, 그 중에서도 변수들은 까먹기 쉽습니다. 훌륭하게 네이밍 하는 것은 이를 도와주지만, 훨씬 더 효과적인 방법은 Document 화하여 기록해보는 것입니다. 구식적인 방법이라고 볼 수도 있겠지만 초보 개발자에게는 프로젝트를 진행하는 데 있어서 큰 도움이 될 수 있습니다.

코드를 복잡하게 만들지 마세요.

초보 개발자 티를 조금 벗은 개발자들이 많이 하는 실수입니다. 할당받은 양이 있다면 그 만큼에 대해서만 코드를 작성하세요. 현금 결제 시스템을 만드는 데 있어서 신용카드나 보너스 등까지 고려할 필요는 없습니다. 


좋은 개발자가 있다면 언제든 도움을 구하세요.

Stack Overflow 에 의존하는 것은 좋은 방법이지만, 실제 더 나은 개발자가 있다면 직접 배우는 것이 더 도움이 될 수 있습니다. 문제를 해결해주는 답변에 당신은 좋아요를 누르겠지만 이해했다고 확신할 수 있나요?


페어 프로그래밍은 좋은 코드 향상 방법입니다. 하지만...

페어 프로그래밍은 좋은 협업 방법이지만 당신이 당신의 몫을 충분히 할 때 의미가 있습니다. 그렇지 않고서는 이는 지루한 1대 1 코딩 과외가 될 뿐입니다. 파트너에게 코드 작성을 기대지말고 스스로 독자적인 코드를 만들고 토론할 수 있을 때 페어 프로그래밍은 의의를 갖습니다. 


마지막으로 강조하지만 당신만의 코드를 만들어보세요.

깃허브나 다른 훌륭한 파트너에 의존하지말고, 구글에서 코드를 복사하지말고 당신만의 코드를 만들어보세요.

어려우면 어려운대로 처음부터 코드를 작성해보는 것이 굉장히 중요합니다. 남의 코드를 가져다만 쓰는 것은 결국 한계를 드러내기 마련입니다.



상당히 좋은 내용들이 많습니다. :) 

방문해주시는 분들 읽고 도움이 될 수 있었으면 좋겠습니다.



 가비지컬렉터 의 동작을 이해하는 건 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 키워드를 이용해서 고정시켜놓으면 그러한 불상사를 예방할 있다.



 마틴 파울러가 재창한 두개의 개념으로, 비즈니스 로직을 처리하는 두가지 패턴을 말한다.

이는 책임지는 쪽이 Domain Level이냐 Script Level이냐에 따라 구분된다. 가령 다음과 같은 예시가 있다고 가정해보자.


먼저 트랜잭션 스크립트란, 은행의 계좌 이체 서비스처럼 하나의 트랜잭션 안에서 필요한 모든 로직을 수행하는 패턴이다. 

그래서 이름도 Transaction Script 로 부른다. 구현이 매우 쉽고 단순하지만 구조가 복잡해질 수록 모듈화의 복잡도 역시 높아진다.

다만, 하나의 강건한 로직이 해당 모듈에서만 구현되어야할 경우 side effect 를 예방할 수 있고, 좀 더 효율적인 코드를 작성할 수 있다.


도메인 모델은 우리가 흔히 객체 지향의 예제로 많이 배우는 형태의 모델로, 각 객체에 객체가 수행해야 하는 업무를 분담시키는 것이다. 

주요 특징은 데이터와 프로세스가 같이 존재한다는 점이며, 객체간 관계를 맺을 수 있어, 제약하거나 로직의 단순화에 도움이 된다.


또한 주로 같이 사용되는 상속이나 다양한 패턴들이 객체를 중심으로 얽히면서 개발자가 선택할 수 있는 유연성이 높아지고 모듈화가 간단해진다. 유지보수가 편리하며 재사용성이 뛰어나다.


 하지만 객체들간의 설계가 수반되어야하기 때문에 모델 구축이 쉽지 않고, 객체간의 관계를 잘 풀어나가야 한다. 특히 객체간의 dependency 는 반대로 말하자면 제약사항이기 때문에 설계가 매우 중요하다.


도메인 모델과 트랜잭션 스크립트 패턴의 차이를 보기 위해 다음 예제를 확인해보자.



    Emp emp = loadEmp("나직원");
    Dept dept = loadDept("A부서");



이 때 이를 비교 하기 위한 Domain Model 방식과 Transaction Script 방식의 차이는 다음과 같다.



    //Transaction Script 방식
    if(emp.getDeptName() == dept.getDeptName())
        return true;

    //Domain Model 방식
    if(emp.isBelongTo(dept))
        return true;



즉, Function의 책임 소재를 어디에 두는가, 사용자가 작성하는 코드 로직에 두는가(Transaction Script 방식) 아니면 객체 자체의 모델링 자체에 두는가(Domain Model 방식)의 차이이다.


참조

http://javacan.tistory.com/entry/94

http://lorenzo-dee.blogspot.com/2014/06/quantifying-domain-model-vs-transaction-script.html




 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