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

 



CQRS(Command and Query Responsibility Segregation) 란 .Net 기반으로 발전되고 있는 설계 방법론으로 명령과 쿼리의 역할을 구분하는 것이다. 


이는 데이터에 대한 조작 Create, Insert, Delete 와 데이터에 대한 조회 Select 를 구분하는 것에서 출발한다.


어플리케이션을 개발할 때, 컨텐츠를 위한 데이터 모델은 계속해서 복잡도가 올라가게 된다.




특히 주로 사용되는 위의 모델처럼 데이터 변경과 조회는 보통 하나의 데이터모델을 사용하게 되는데, 어플리케이션의 복잡도가 증가할 수록 각 API 기능의 책임이 어떤 데이터 모델에 있는지는 불분명해진다.


이는 설계에 있어서 초기 의도를 지워버리는 역할을 하며 많은 경우의 레거시 코드들이 이런 기반으로 생겨나게 된다.


CQRS 는 이러한 고민에서 출발하며, 데이터에 대한 조회(Query) 와 데이터에 대한 조작(Command) 을 분리함으로써 이 문제를 해결하고자 한다.


기본적으로 CQRS 를 적용하기 위해서는 Command 을 위한 도메인 모델과 Query 를 위한 도메인 모델을 분리한다.



분리된 각각의 도메인 모델을 DB에 적용하는 방안으로는 몇가지가 있다.


(1) Simple


 : 같은 Scheme 을 가진 DB를 사용하며, Command / Query 시에 데이터에 대한 Converting 을 거친 후 DB에 CRUD 에 대한 작업을 수행한다.

 이 경우에는 일반 어플리케이션과 같으며 도메인 모델만 분리한 상태로 개발이 쉽고 적용이 간단하다.



(2) Premium


 : Command 를 위한 DB와 Query 를 위한 DB를 분리하는 형식으로, 데이터의 정합성을 위한 RDB를 Command 용 DB로 분리하고 Query 가 간편한 NoSQL 을 Query 용 DB로 주로 사용한다.

 이렇게 동일한 데이터에 대해 다수의 저장소를 운용하는 방식을 Polyglot Storage 라 하며 이 경우 용도에 맞는 저장소를 골라서 좀 더 알맞게 사용이 가능하다.

 하지만 분리된 저장소 각각에 대한 데이터 동기화 이슈를 Broker 등을 이용해 처리해주어야 하는 점은 이슈가 되며 책임의 소재나 로깅 등에 있어 신뢰도 확보를 위한 작업이 필요하다.



(3) Event Sourcing


 : Event Sourcing 이란 Application 내의 설계를 컨텐츠 기반이 아닌 기능 기반으로 하면서 이러한 "이벤트(Event)" 자체를 DB에 저장하는 방식을 말한다.

 이렇게 함으로써 이벤트에서 사용하는 도메인 모델은 컨텐츠를 위한 DB에 Write 되고 Query 시에는 이벤트를 저장한 DB로부터 해당 컨텐츠를 바탕으로 데이터를 만들어서 가져온다.

 도메인 모델에 대한 Command 가 따로 저장되고, Query 를 위한 도메인 모델은 Event DB로부터 불러오는 방식 때문에

 Event Sourcing 의 Architecture를 적용함에 있어서 CQRS 는 필수적인 설계 방식이 된다.

 CQRS 를 적용하는 데 있어서도 가장 큰 시너지를 낼 수 있는 Architecture 의 하나이다.


<향후 Event Sourcing Architecture에 대해서는 추가로 정리한다.>


CQRS 가 실무에 적용되는 데 있어 아직은 국내외적으로 불확실성이 있는 듯 하지만, 주목해볼만한 패턴인 것은 틀림없다.


(참고자료 : https://docs.microsoft.com/en-us/previous-versions/msp-n-p/dn568103(v=pandp.10))



단일 프로세스 & 쓰레드를 갖는 프로그램을 개발할 경우에는 신경쓸 일이 거의 없지만 여러 작업루틴이 동시에 수행되며, 공유 리소스에 접근하게 되는 동시성 프로그래밍을 할 경우에는 반드시 신경써주어야 할 부분이 바로


Mutual Exclusion (상호배제) 문제이다. 이는 공유 불가능한 자원을 동시에 사용하게 될 경우 발생할 수 있는 충돌을 방지하기 위해서 Critical Section 을 만들고, 해당 영역에서 데이터를 사용하게끔 하는 방법을 사용한다.


(1) 세마포어(Semaphore)


이렇게 공유자원에 대한 동시성 문제가 발생하였을 때, 즉 여러 개의 프로세스 또는 쓰레드가 동시접근하는 문제를 방지하기 위해 고안된 것이 바로 세마포어이다. 


세마포어란 리소스의 상태를 나타내는 카운터를 지정하여 다중 프로세스에서 행동을 조정 및 동기화 시킬 수 있는 기술이다. 

여러 프로세스가 접근 시 여러 개의 Lock 을 할당하여 동시에 허용 가능한 Counter 의 제한을 둔다. 카운터가 1개로 0 / 1의 값을 가질 때 Binary Semaphore 라하고 이는 Mutex와 동작이 같다.


(2) 뮤텍스(Mutex)


뮤텍스는 Lock 을 가지고 있을 때에만 공유자원에 접근이 가능하게끔 하는 로직이다. 

즉, 세마포어가 여러 개의 락을 두어 제한된 리소스 접근을 허용하는데 반해 뮤텍스는 오로지 한 개의 쓰레드/프로세스만 할당한다.



* 세마포어(Semaphore) 와 뮤텍스(Mutex) 의 차이점


- 세마포어는 주로 시스템적 범위에 적용이 되며 뮤텍스는 프로세스 내에서 적용이 된다. 

(물론 세마포어와 뮤텍스는 매커니즘의 개념이기 때문에 국한된다고 할 수는 없다.) 주로 뮤텍스는 프로세스 내 쓰레드간 자원 접근에 대하여 적용이 되며 Lock 한 쓰레드가 Unlock 도 해주어야 한다. 

반면 세마포어는 Lock 을 건 소유주가 아니더라도 Unlock 이 가능하다.


- 세마포어는 동기화 대상이 하나 이상일 때, 뮤텍스는 하나일 때 사용된다.


동기화 처리 로직은 Lock 을 수반하며 이 Lock 이 여러 개의 작업 큐 내에서 걸릴 때 DeadLock 이 생길 우려가 있다.


데드락은 임계 영역 내의 전역 리소스에 대해 복수 개의 Lock에 의해 처리가 지연되는 현상이다. 

대표적으로 Critical Section 에 접근하여 공유 자원을 처리하는 여러개의 로직이 서로에 대한 의존도(Dependency)를 가질 때 발생한다.


다음과 같은 상황을 가정해보자.




위의 그림에서 프로세스1 은 Resource 1을 선점하고 있으며, Resource 2에 대한 작업처리를 요구한다.

Resource 2에 대한 처리를 완료하면 Resource 1을 사용할 수 있도록 Critical Section 바깥으로 반환할 것이다.

반대로 프로세스2 는 Resouce 2를 선점하고 있으며 Resource 1에 대한 작업 처리를 마친 후 Resource 2를 반환할 것이다.


위의 상황에서 두 프로세스는 서로 선점하는 Resource가 다르므로 동시에 Critical Section 진입이 가능하지만, 서로의 자원을 요구하는 탓에 탈출은 불가능하다. 이 상황을 Deadlock이라고 한다.


Deadlock 이 발생하면 자원의 누수 및 동작의 교착상태가 계속되기 때문에 어플리케이션 또는 시스템에 치명적이며 따라서 문제 해결을 위해 다음과 같이 관리 한다.


- 교착 상태의 예방

(1) Mutual Exclusion 조건 제거

(2) 사용할 때에만 해당 자원을 점유하고 사용하지 않을 때에는 해당 자원을 다른 프로세스가 사용할 수 있도록 양도

(3) 선점 가능한 프로토콜 제작

(4) 자원 접근에 대한 순차적 처리 


- 교착 상태의 회피

 : 자원 요청에 대해 Circular Wait를 방지하기 위한 할당 상태를 검사한다.


- 교착 상태의 무시

 : 확률이 낮은 경우 별도의 처리를 하지 않는다.




 시스템을 구성할 때, 특히 서버시스템이라면 장애(Fault) 에 대한 처리는 상당히 중요한 이슈이다. 

오랜시간동안 어떤 종류의 문제가 일어나도 문제가 일어나기 전과 후의 성능이 동일해야하며, 아웃풋 또한 동일해야 한다. 

이런 요건들이 마련되었을 때 해당 시스템을 내구력있는(Tolerant) 시스템이라 한다.


시스템을 구성하는 일부에서 fault / failure 발생해도 정상적 / 부분적으로 기능을 수행할 있는 시스템을 만들기 위해서 수행하는 복구의 종류로는 일반적으로 roll forward 복구, roll back 복구가 존재한다


Role forward 복구는 오류 발생 후의 상태에서 시스템을 복구하고 roll back 복구는 오류 발생 이전 check point 시스템을 복구한다.


Role Forward 복구와 Role Back 복구 모두, 복구 point 에러 발생 지점 간의 멱등성{몇번 실행해도 1 실행과 결과가 동일함이 보장되는 성질} 만족해야 한다.


이를 구현하기 위해서는 다음과 같은 테크닉이 주로 이용된다.



(1) 이중화 시스템

-  Replication :  동일 시스템을 복수로 준비하여 병렬로 실행시켜 다수를 만족한 결과를 올바른 결과로 적용한다.

-  Redundancy : 동일한 시스템을 복수로 준비하여 장애 발생시 보조 시스템으로 전환한다.

-  Diversity : 같은 사양에 다른 H/W 시스템을 복수로 준비하여 운용한다.

( 경우 시스템은 같은 장애를 만들어내지 않는다.)


(2) 동작지속 고장나도 지속적으로 동작해야 하며 복구 작업 성능 간섭이 없어야 한다.


(3) 고장분리 시스템이 고장과 분리되어 정상적인 구성요소에 영향을 주지 말아야 한다.


(4) 고장전염 고장이 전염되지 않게 유의해야 한다.



(1)번과 (3)번 원칙은 주로 설계에 대한 내용들이며, (2)번과 (4)번은 구현에도 직접적인 연관이 있는 편이다. 

서비스마다 차이가 있겠지만, 가능한 발생할 수 있는 "장애"의 단위를 격리시키고, 정상적인 결과를 낼 수 있는 Flow 를 고안하면서(설령 Error 가 Report 되더라도) 동시에 처리할 수 있는 "백신"을 모듈화시키는 것이 중요하다.




정리하기에 앞서 알아두어야할 점은 Synchronous, Asynchronous 와 Blocking, Non-Blocking 은 서로 비교 가능한 개념들이 아니다. 많이들 기술 먼저 습득하다보니(특히 NodeJS), 개념을 먼저 습득하기보다는, 특징을 먼저 배우는 터라 개념이 혼용되어서 쓰이는 경우가 많을 뿐이다.


동기(Sync) 와 비동기(Async)


<Synchronous I/O Model>


Sync와 Async 를 구분하는 기준은 작업 순서이다.

동기식 모델은 모든 작업들이 일련의 순서를 따르며 그 순서에 맞게 동작한다. 즉, A,B,C 순서대로 작업이 시작되었다면 A,B,C 순서로 작업이 끝나야 한다. 설령 여러 작업이 동시에 처리되고 있다고 해도, 작업이 처리되는 모델의 순서가 보장된다면 이는 동기식 처리 모델이라고 할 수 있다. 

많은 자료들이 동기식 처리 모델을 설명할 때, 작업이 실행되는 동안 다음 처리를 기다리는 것이(Wait) Sync 모델이라고 하지만, 이는 잘 알려진 오해이다. 이 Wait Process 때문에 Blocking 과 개념의 혼동이 생기는 경우가 흔하다. 동기 처리 모델에서 알아두어야할 점은 작업의 순서가 보장된다는 점 뿐이다.

동기식 처리 모델은 우리가 만드는 대부분의 프로세스의 로직이며, 특히 Pipeline 을 준수하는 Working Process 에서 매우 훌륭한 모델이다.


<Asynchronous I/O Model>


반면 비동기식 모델은 작업의 순서가 보장되지 않는다. 말그대로 비동기(Asynchronous) 처리 모델로, A,B,C 순서로 작업이 시작되어도 A,B,C 순서로 작업이 끝난다고 보장할 수 없다.

비동기식 처리 모델이 이득을 보는 경우는 각 작업이 분리될 수 있으며, Latency 가 큰 경우이다. 예를들어 각 클라이언트 또는 작업 별로 Latency 가 발생하는 네트워크 처리나 File I/O 등이 훌륭한 적용 예시이다.



블로킹(Blocking) 과 넌블로킹(Non-Blocking)


Blocking 과 Non-Blocking 을 구분하는 기준은 통지 이다.

Blocking 이란 말그대로 작업의 멈춤, 대기(Wait) 을 말한다. 즉, 작업을 시작하고 작업이 끝날때까지 대기하다가 즉석에서 완료 통지를 받는다.

이 때 작업이 멈추는 동안 다른작업이 끼어들수 있는지 없는지는 다른 얘기이다. 이부분이 중요하면서도 헷갈린데, 많은 Blocking 방식의 사례에서 다른 작업의 Interrupt 를 방지하기 때문에, Blocking 은 곧 "순차처리" 로 생각하는 오류가 생긴다. (이렇게 생각해버리면 Blocking 과 Synchronous 모델은 같은 개념이 된다.)

단순히 생각해서 Blocking 은 그저 작업을 수행하는 데 있어서 대기 시간을 갖는다는 의미일 뿐이다.


Non Blocking 이란 작업의 완료를 나중에 통지받는 개념이다. 작업의 시작 이후 완료시까지 대기하지 않고, 완료시킨다. 

즉, 내부 동작에 무관하게 작업에 대한 완료를 처리받는 걸 말한다.

(작업의 예약 부분이 오히려 비동기 설명과 오해를 불러오는 듯 하여 수정했습니다.)

효과적인 작업 상태의 처리를 위해 Non-blocking 에서는 성공, 실패, 일부 성공(partial success) 라는 3가지 패턴이 존재한다.

한 작업에 대해 대기 Queue 와 무관하게 다른 작업을 처리하는 효율적인 알고리즘 처리 시, 각 작업의 완료들의 순서가 보장되지 않는 경우가 많기 때문에 Async 와 헷갈릴 수 있지만, 이들은 앞서 언급했듯이 비교할 수 없는 다른 개념이다.



오해


많은 종류의 소프트웨어에서 동기 처리 방식이 Blocking 이고, 비동기 처리 방식이 Non-Blocking 인 이유는 익숙한 구조이기 때문이다.

일련의 작업들에 대해 순차적으로 하나씩 처리하고 완료하는 방식은 매 작업의 수행마다 Blocking 하는게 작업의 순서를 보장하기 쉬우며,

여러 작업들이 동시에 일어나는 구조에서는 한 작업을 수행하는 동시에 Non-Blocking으로 다른 작업을 받아와서 처리하는 구조가 효율적이다.

그렇기 때문에 같은 개념으로 혼동하기 쉽지만, 동기/비동기와 블로킹/논블로킹은 서로 양립할 수 있는 개념이란걸 기억하자.



정리한 개념을 통해 System I/O 의 모델들에 대해 이해해보자.


- Synchronous Blocking I/O : Application layer 의 계층이 Block을 일으키는 System Call을 호출하고, 이에 따라 User Layer와 Kernel Layer 간의 Context Switching 이 발생한다. 

이 때, Application 은 CPU 를 사용하지 않고 Kernel 의 응답을 기다리게 된다.


- Synchronous Non-blocking I/O : Nonblock Kernel Systemcall 을 사용하기 때문에 더 향상된 것처럼 보이지만, 

User Layer 가 Synchronous 이므로 응답을 기다리는 동안 Kernel 의 System Call을 Polling 하게 된다. 당연히 Context Switching 빈도수가 늘어나기 때문에 더 I/O에 지연이 발생하게 된다.


- Asynchronous Blocking I/O : User Layer의 I/O 가 Non-blocking 이고 Kernel 에서 알림이 블로킹 방식이다. Select System call 이 이 방식의 대표적이며 여러 I/O 를 한번에 수행할 수 있는 모델이다.


- Asynchronous Non-blocking I/O : Kernel I/O 의 개시와 알림 두 차례만 Context Switching 이 발생하고, Kernel 작업이 Non-block 이므로 select() 와 같은 멀티플렉싱 뿐 아니라 다른 프로세싱 자체가 가능하다. 

Kernel level 에서의 응답은 Signal 이나 Thread 기반 Callback 을 통해 user level 로 마치 이벤트처럼 전달된다.


OS 레벨에서 프로세스들의 동작 처리는 비동기 처리 모델 방식으로 진행된다. 각 프로세스 내에서 처리는 작업의 순서가 보장되는 동기 처리 방식이지만, 커널 레벨의 관점에서는 커널의 리소스를 사용하는 모든 작업들의

순서를 지켜줄 수 없으며, 커널 동작의 관점에서 보면 비동기 처리 모델을 사용한다. 

즉, 시스템 프로그래밍 윗 레벨에서의 대부분의 작업 모델은 동기 처리이지만, 커널 레벨에서 프로세스의 운용은 비동기 처리로 볼 수 있다.

물론 각 프로그램들이 동기 / 비동기 중 어떤 방식으로 내부 로직을 처리하는 가는 별도의 문제이다.



추가로 NodeJS 포스팅에서 언급할 예정이지만, NodeJS 는 Async I/O 처리를 위해 libuv 를 사용하는데, 이 엔진은 default thread를 4개 사용하여 Async I/O 를 구현한다. 결국 Async 라 해도 마법은 아니며, 구현의 레벨로 보자면 Scheduling 과 Loop 의 문제이다.


참조 : https://docs.microsoft.com/en-us/windows/desktop/FileIO/synchronous-and-asynchronous-i-o




+ Recent posts