[Go] 러닝 Go 후기
Go언어 기초부분을 알려주는데, 고급스럽게 알려준다.
구조체와 nullable을 다루는 부분, 포인터, 스택의 예시가 너무 인상깊었다.
Go를 공부한거보다, CS를 공부한느낌...🥹
`JVM 밑바닥까지 파헤치기`책을 읽기전에 이 내용을 먼저 알았으면 좋았을 거 같다.
Go에 대해 공부해보게된 계기는 K8S와 도커가 Go로 작성되었다는 것 때문이다.
언어가 확실히 심플하고, 모던한게 느껴졌다.
코틀린과도 비슷하고, 덕타이핑도 지원하고, 고루틴도 있고, 포인터도 있고..
기초를 배우는데는 정말 금방 배울 수 있으니, 한 번쯤 시도해보는 것을 추천한다.
자바에서의 클래스(정적영역(method,static)), 인스턴스(힙영역), 함수 호출(스택영역)와 비교해서 차이점을 알 수 있는것이 가장 기억에 남는다.
여태까지 자바를 사용하면서 참조의 개념을 제대로 모르고 사용했던것이 너무나 충격이다.
자바와 Go의 공통점
- 자바와 Go 모두 파라미터와 리턴은 복사로 이루어진다.
- 두 언어 모두 함수 호출 시 스택에 스택 프레임이 할당되고, 종료 시 제거된다.
Go에서 메모리 관리
- 함수에서 포인터를 반환할 경우, 컴파일러의 Escape Analysis를 통해 데이터가 스택에 남아 있을지, 힙으로 이동할지를 결정한다. 함수 종료 후에도 참조되는 데이터라면 힙에 할당된다.
- 구조체는 데이터가 연속적으로 메모리에 배치되며, 기본적으로 스택에 저장된다. 하지만 Escape Analysis 결과에 따라 힙에 할당될 수도 있다.
- 배열은 고정 크기의 경우 스택에 저장될 수 있지만, 크기가 크거나 동적으로 생성된 경우 힙에 할당된다.
- 따라서 스택에 저장된 데이터들은 함수 종료시 자동으로 제거되며, GC의 작동히 필요하지 않다.
자바에서 메모리 관리
- 자바에서 참조 타입 변수는 스택에 저장되며, 참조하는 객체는 힙에 저장된다.
- 함수 호출이 끝난 후에도 힙에 저장된 객체는 GC(가비지 컬렉션)에 의해 정리되기 전까지 접근 가능하다.
- 따라서 자바에서는 인스턴스가 힙에 저장되기 때문에, GC의 작동이 필요하다.
- 스택이 종료되면서 Young Region에 있던 객체가 참조되지 않으면 Minor GC로 바로 제거될 수 있다.
정리
- 즉, 스택에서 정리되는 데이터들은 GC의 대상이 아니기 때문에, 최적화의 효과를 낸다.
- 포인터는 가능한 드물게 사용하는 것이 합리적일 수 있으며, 로직의 흐름을 단방향으로 만들고 복잡도를 낮출 수 있다.
- Go에서 데이터의가 1메가보다 작은 경우, 포인터 전달이 더 느릴 수 있어, 성능에 큰 영향을 끼치지 않는다.
코틀린 코루틴과 고루틴의 차이점에 대해서도 말해보고자 한다.
코틀린 코투린과 고루틴의 공통점
- 둘 다 비동기 프로그래밍을 위해서 설계되었으며, 코루틴으로써 경량 쓰레드로 불린다.
- OS 쓰레드보다 훨씬 적은 메모리를 사용하며, 하나의 쓰레드에서 수천 개의 루틴을 처리할 수 있다.(쓰레드를 수천개 생성한다면, 컨텍스트 스위칭으로 인한 오버헤드가 발생한다)
- 둘다 동시성 프로그래밍을 지원하며, I/O작업이나 병렬 처리에 사용될 수 있다.
- 비동기 작업을 동기식 코드 스타일로 작성할 수 있어, 간결하고 읽기 쉽게 표현한다.
코틀린 코루틴과 고루틴의 차이점
코루틴은 Kotlin 표준 라이브러리에서 제공되며, Coroutine Dispatcher를 통해 동작된다.
고루틴은 Go런타임에서 관리된다. 실행 스케줄링 및 자원관리는 Go런타임이 전담한다.
코루틴은 협력적 스케줄링으로 명시적인 컨텍스트 스위칭이 필요하며, 개발자가 컨트롤을 가져간다.
고루틴은 선점형 스케줄링으로 자동으로 스케줄링되며, Go런타임이 필요한 작업을 관리한다.
코루틴은 스레드 풀 위에서 동작하며, 하나의 스레드에 여러 코루틴이 매핑된다.
고루틴은 Go의 M:N 스케줄러를 사용하여 다수의 고루틴을 OS 쓰레드에 매핑된다.
왜 경량쓰레드인가?
두 루틴 모두, 메인 루틴에서 동시적으로 작동이 필요한 코드들을 사용하도록하는데
이를 순차적으로 하지않고 다른 실행흐름에서 실행하도록한다.
전통적인 쓰레드 기반 작업은 컨텍스트 스위칭(Context Switching) 과정에서 다음과 같은 오버헤드가 발생한다.
- 레지스터 저장 및 복원
- OS 스케줄러 호출
- 스택 메모리 재배치
그러나 코루틴을 통한 경량쓰레드를 통해, 하나의 스레드가 계속 작업을 수행하며 작업청크들이 수행된다.
이처럼 경량 쓰레드 방식 덕분에, 수천~수만 개의 루틴을 동시에 실행할 수 있는 것이다.
데이터가 도착하게되면 쓰레드풀에서 놀고있는 쓰레드에게 일을 시킨다.
그런데 모든 쓰레드가 일을 하고있다면?? 쓰레드를 양보해야한다.
여기서 코루틴의 정지가능한 함수가 사용된다.
함수 실행중에 함수를 정지시키고 다른 일을 처리한뒤 해당 함수를 재게시키면 된다.
그러면 함수를 어떻게 정지시킬까? 여기서 선점형과 비선점형 두 종류가 있다.
선점형과 비선점형?
선점형
- 하나의 프로세스가 실행 중이어도, 운영체제가 강제로 프로세서를 회수하여 다른 프로세스에 할당할 수 있다. 즉, 실행 중인 작업을 중단시키고 다른 작업이 실행되도록 한다
비선점형
- 하나의 프로세스가 실행을 끝내기 전까지는 다른 프로세스가 CPU를 사용할 수 없다. 작업이 스스로 제어권을 넘기지 않으면 다른 작업은 대기해야 한다.
코틀린 코루틴은 비선점형으로 작동한다.
즉, 현재 실행 중인 코루틴이 명시적으로 제어권을 넘겨주기(suspend) 전까지는 다른 코루틴이 실행되지 않는다.
이 비선점형 특성은 코루틴의 동작 방식과 이어진다.
코틀린 코루틴은 컴파일 타임에 상태 머신으로 변환된다.
- 이는 코루틴이 suspend와 resume을 통해 중단되었다가 이어지는 동작을 한다는 것이다.
- 코루틴이 중단(suspend)되는 순간에만 다른 코루틴이 실행될 수 있다.
- 따라서 비선점형 환경에서는 작업이 적절히 suspend되지 않으면, 다른 작업이 실행되지 못할 수 있다.
이것 때문에, 코투린의 Dispatcher.IO는 블로킹 작업에서 사용된다.
- 블로킹 작업은 제어권을 넘기는 데 시간이 오래 걸리므로, 이러한 작업에 충분한 쓰레드를 제공하기 위해 쓰레드 풀이 많도록 설계되어 있다.
- 블로킹 작업 중 다른 코루틴이 실행되지 못하더라도, 여러 쓰레드가 존재하므로 작업 병렬 처리가 가능하다.
Go언어의 고루틴은 자기만의 콜 스택을 가지고 있고,
이를 통해 함수의 어떤 지점에서든 현재 실행 상태를 저장하고 일시정지 시킬 수 있다.
마치 쓰레드와 똑같지 않은가?
WebFlux(Reactor)와의 차이
WebFlux(또는 Reactor)는 논블로킹 API를 사용하며, 작업의 흐름이 코루틴과 다르다.
- 논블로킹 작업은 I/O 작업 중에도 제어권을 즉시 반환하여 다른 작업이 바로 실행될 수 있도록 설계되어있다.
- 이 때문에 WebFlux는 쓰레드 풀이 적어도 충분히 효율적으로 동작한다.
작업을 많이하는 블로킹작업은 쓰레드풀이 많은곳에서 실행된다.
- CPU 연산이 많은 작업은 Schedulers.parallel()와 같은 쓰레드풀에서 처리
- 블로킹 I/O 작업은 Schedulers.boundedElastic()과 같은 쓰레드풀에서 실행
이벤트루프
현재 일하고 있는 각 코루틴들이 I/O를 실행할때, 무작정 대기(블로킹)하게 할 필요가 있을까?
이벤트 루프는 비동기 API의 핵심 구성 요소로, 작업을 관리하고 실행한다.
- 작업이 I/O 대기 상태에 들어가면, 해당 작업을 대기 큐에 넣고 즉시 다른 작업을 실행한다.
- 대기 상태가 완료되면, 이벤트 루프는 해당 작업을 다시 실행하도록 스케줄링한다.
이 과정에서 대기 시간이 제거되므로, 동일 쓰레드에서 다수의 작업을 효과적으로 처리할 수 있는 것
WebFlux의 Java NIO
WebFlux에서는 Java의 NIO를 사용한다.
- 네트워크 요청이 있을 때, 소켓을 논블로킹 모드로 설정하여 데이터가 도착하기 전까지 쓰레드를 차단하지 않고, 데이터가 준비되면 콜백을 통해 이벤트루프에 알린다.
- Java NIO는 운영체제의 epoll(Linux) 같은 시스템 호출을 활용하여 논블로킹 소켓을 모니터링한다.
SpringBoot 혹은 Android에서 논블로킹 처리
이처럼 논블로킹의 핵심은 이벤트루프이다.
따라서 단순히 RestTemplete, OkHttp를 사용하게 된다면 블로킹 API를 사용하게되어 요청을 처리하고 있는 쓰레드는 블로킹된다.
(OkHttp3는 suspend함수를 지원하나, 이는 Dispather.IO에서 수행된다. 이벤트루프 방식이 아니다.)
따라서 클라이언트측에서 IO가 필요한 API 호출시에 완전한 비동기처리를 위해서는
reacter를 사용하는 이벤트루프가 필요하다.
SpringBoot의 WebClient, Ktor Client, R2DBC와 같은 API를 사용하면 된다.
(CPU 코어수*2의 쓰레드풀에서 작동한다고 기억하는데, 확인해보기를 바란다)
많은 것들을 배울 수 있는 좋은 책이였다.