Intro

이번 Jungle CS 스터디의 주제는 “System Call”입니다. 저는 이번 개인 주제로서, JVM에서 System Call이 어떻게 동작하는지에 대해 소개해보겠습니다. 글의 개요는 아래와 같습니다.

개요

  • System Call의 배경과 필요성
  • System Call의 동작 원리
  • JVM과 System Call
  • JVM 기반 언어 vs. Native 언어(C)의 System Call 접근 방식 비교

 

System Call의 배경과 필요성

우리는 컴퓨터 하나에서 동시에 많은 프로그램을 사용할 수 있습니다. 이는 운영체제가 각 프로세스 별로 공정하게 하드웨어 자원을 분배하는 역할을 하기 때문이죠. 수많은 프로세스는 결국 유한한 하드웨어를 공유하며 동작하고 있는 것입니다. 그렇다면 만약 하나의 프로세스가 다른 프로세스의 허락 없이 데이터를 읽고 수정할 수 있다면 어떻게 될까요? 또는 어떤 프로세스가 모든 프로세스가 공유하는 하드웨어 자원의 사용을 못하게 망가트린다면? 심각한 데이터 유출이나 발생하거나 컴퓨터 사용에 큰 지장이 생길 것입니다.

따라서 하드웨어 자원을 추상화하고 효율적인 운영에 책임이 있는 OS는, 프로세스의 하드웨어 자원 사용을 엄격하게 제한할 필요가 있습니다. 이를 위해 OS는 최소 두가지 실행 모드를 제공합니다.

  • 사용자 모드(User Mode): 제한된 명령어만 실행할 수 있으며, 하드웨어 자원에 직접 접근할 수 없습니다. 일반 애플리케이션은 이 모드에서 실행됩니다.
  • 커널 모드(Kernel Mode): 모든 하드웨어 자원에 직접 접근이 가능하며, 모든 CPU 명령어 실행 권한을 가집니다. 운영체제의 핵심 부분인 커널이 이 모드에서 실행됩니다.

프로세스(User Mode)는 하드웨어 자원에 대한 접근이 필요하다면, 반드시 OS에게 하드웨어 사용을 요청해 OS가 Kernel Mode에서 하드웨어를 사용할 수 있게해야 합니다. System Call이란, 위 과정에서 프로세스가 OS에게 하드웨어 접근 사용을 허가 받기 위한 요청을 의미합니다. 조금더 자세히 System Call의 역할에 대해 알아봅시다.

System Call의 역할

System Call은 사용자 모드 프로그램과 커널 모드 운영체제 사이의 인터페이스를 제공합니다. 이는 다음과 같은 역할을 수행합니다:

  1. 보호 경계(Protection Boundary) 유지: 사용자 프로그램이 운영체제나 다른 프로그램의 중요한 부분에 직접 접근하는 것을 방지합니다.
  2. 제어된 인터페이스 제공: 애플리케이션이 하드웨어와 시스템 자원에 접근할 수 있는 안전하고 일관된 방법을 제공합니다.
  3. 권한 검증(Privilege Checking): 요청된 작업을 수행할 권한이 있는지 확인하여 보안을 강화합니다.
  4. 추상화 계층(Abstraction Layer) 제공: 하드웨어의 복잡성을 감추고 일관된 API를 제공하여 애플리케이션 개발을 단순화합니다.

System Call을 어렵게 생각할건 없습니다. 우리는 이미 밥먹듯이 System Call을 쓰고 있거든요. 파일을 읽고 실행하고 종료하는 것도 System Call 없이는 불가능합니다. print() 문을 사용하거나, scanner로 사용자 입력을 받는 것도 System Call이 필요하지요. 이렇게 핵심적인 역할을 하는 System Call은 그렇다면 어떻게 동작하는 걸까요?

System Call의 동작 원리

System Call의 핵심은 사용자 모드(User Mode)에서 커널 모드(Kernel Mode)로의 안전한 전환입니다. 이 전환 과정은 일종의 '문지기'가 있는 출입문을 통과하는 것과 비슷합니다. 프로그램은 마음대로 커널 영역에 들어갈 수 없고, 반드시 정해진 절차와 방법을 통해서만 접근이 가능합니다.

트랩(Trap) 메커니즘과 인터럽트 처리

System Call이 작동하는 핵심 메커니즘은 '트랩(Trap)'이라고 불리는 특별한 형태의 인터럽트입니다. 인터럽트란 CPU에게 "잠깐만, 지금 하던 일을 멈추고 이것을 처리해야 해!"라고 알리는 신호입니다.

Interrupt에는 크게 두 종류가 있습니다:

(정확히는, interrupt, trap, exception, fault, and abort 등의 단어는 명확한 정의는 없다고 합니다)

  1. Hardware Interrupt
    1. 디스크, 키보드, 네트워크 카드, DMA 등의 장치가 발생
  2. Software Interrupt
    1. Exception(예외)
      • 0으로 나누기, 접근 권한 위반 등 프로그램 실행 중 오류가 발생할 때 생성
    2. Trap(트랩)
      • 프로그램이 의도적으로 발생시키는 인터럽트로, System Call이 여기에 해당합니다.

트랩은 특별한 명령어(int, syscall, svc 등 아키텍처마다 다름)를 통해 의도적으로 발생시키는 인터럽트입니다. 이 명령어가 실행되면 다음과 같은 일이 발생합니다:

  1. CPU는 현재 실행 중인 프로그램의 상태(레지스터 값, 프로그램 카운터 등)를 저장합니다.
  2. CPU 모드가 사용자 모드에서 커널 모드로 전환됩니다.
  3. 제어권이 커널의 인터럽트 핸들러로 넘어갑니다.

이것은 마치 비행기를 타기 위해 보안 검색대를 통과하는 것과 비슷합니다. 여러분의 신원(프로그램 상태)이 확인되고, 제한 구역(커널 모드)으로 들어갈 수 있는 허가를 받는 과정입니다.

유저 모드에서 커널 모드로의 전환 과정

System Call의 전체 과정을 단계별로 살펴보겠습니다:

  1. 애플리케이션 요청: 프로그램이 System Call 라이브러리 함수를 호출합니다. 예를 들어 C 프로그램에서 printf()를 호출하면, 이는 내부적으로 write() System Call을 사용합니다.
  2. 라이브러리 래퍼(Wrapper): 라이브러리 함수는 필요한 매개변수를 준비하고, CPU 레지스터에 System Call 번호와 매개변수를 설정합니다.
  3. 트랩 명령어 실행: syscall(x86-64), svc(ARM) 등의 특별한 명령어를 실행하여 트랩을 발생시킵니다.
  4. 모드 전환: CPU가 사용자 모드에서 커널 모드로 전환됩니다. 이때 중요한 점은 프로그램의 실행 흐름이 바뀐다는 것입니다. 프로그램의 다음 명령어가 아닌, 커널의 System Call 핸들러로 점프합니다.
  5. System Call 핸들러: 커널은 요청된 System Call 번호를 확인하고, 해당하는 커널 함수를 호출합니다.
  6. 권한 검사: 커널은 프로세스가 요청한 작업을 수행할 권한이 있는지 확인합니다. 예를 들어, 파일을 열려면 해당 파일에 대한 접근 권한이 있어야 합니다.
  7. 요청 처리: 권한 검사를 통과하면, 커널은 요청된 작업(파일 읽기, 네트워크 패킷 전송 등)을 수행합니다.
  8. 결과 준비: 작업이 완료되면 결과 값(성공/실패 코드, 읽은 데이터 등)을 사용자 프로그램에 반환할 준비를 합니다.
  9. 사용자 모드 복귀: 커널은 다시 사용자 모드로 전환하고, 제어권을 System Call을 호출한 프로그램의 다음 명령어로 돌려줍니다. 결과 값은 보통 레지스터를 통해 전달됩니다.

쉽게 얘기해서 음식점에서 손님이 칵테일을 주문하는 것과 비슷합니다. 손님(process)가 직원(OS)를 불러서, 먹고 싶은 칵테일의 번호(Trap No.)를 불러주며 주문합니다(System Call). 그럼 직원은 손님이 성인인지 확인하고(권한검사), 바 테이블로 가서(커널모드 전환) 칵테일을 제조하죠(커널이 작업 수행). 그리고 칵테일이 완성되면 바를 나가(사용자 모드 복귀), 손님에게 주문한 칵테일을 대접하는거죠!

컨텍스트 스위칭의 비용

우리는 칵테일을 한 잔씩 주문할 때마다 직원에게 돈을 내야해죠. 그것처럼 System Call을 통해 User Mode → Kernel Mode로 전환(Context Swiching) 하는데에도 비용이 필요합니다. 이러한 비용을 Context Swiching Cost라고 하죠.

  1. CPU 상태 저장: 현재 실행 중인 프로그램의 상태(레지스터 값, 프로그램 카운터 등)를 저장해야 합니다.
  2. CPU 캐시 영향: 모드가 전환되면 CPU 캐시의 효율성이 떨어질 수 있습니다. 커널 코드와 데이터가 캐시를 차지하게 되어, 사용자 프로그램의 캐시 데이터가 밀려날 수 있습니다.
  3. TLB(Translation Lookaside Buffer) 플러시: 일부 아키텍처에서는 모드 전환 시 TLB를 플러시(비우기)해야 하며, 이는 메모리 접근 성능을 일시적으로 저하시킵니다.
  4. 파이프라인 비우기: 현대 CPU의 명령어 파이프라인이 비워져야 할 수도 있습니다.

일반적인 함수 호출은 몇 나노초 정도 소요되는 반면, System Call은 수백 나노초에서 마이크로초 단위의 시간이 걸릴 수 있습니다. 이는 100~1000배 더 느릴 수 있다는 의미입니다!

이런 비용 때문에, 성능이 중요한 애플리케이션에서는 System Call의 사용을 최소화하는 기법을 사용합니다.

  1. 버퍼링(Buffering): 작은 데이터를 여러 번 쓰는 대신, 큰 버퍼에 모았다가 한 번에 쓰는 방식을 사용합니다. 이는 write() System Call의 호출 횟수를 줄여줍니다.
  2. 메모리 매핑(Memory Mapping): mmap() System Call을 사용하여 파일을 메모리에 매핑하면, 이후 파일 접근이 일반 메모리 접근으로 처리되어 추가적인 System Call이 필요 없게 됩니다.
  3. 배치 처리(Batching): 최신 Linux 커널의 io_uring과 같은 인터페이스는 여러 I/O 작업을 한 번의 System Call로 처리할 수 있게 해줍니다.

휴 System Call 개념을 설명하는 것만 해도 힘이 드네요. 드디어 본론인 JVM에서 System Call은 어떤 과정으로 동작하는지에 대해 알아봅시다.

JVM과 System Call

저번 게시물에도 언급했듯이, 자바의 철학은 “Write Once, Run Anywhere”입니다. 이러한 철학에 대한 구현이 바로 java와 운영체제 사이에 존재하는 JVM(Java Virtual Machine)이구요. 저번에는 JVM의 메모리 구조에 집중했습니다. 이번 주제인 System Call이 JVM에서 사용되는 과정을 이해하기 위해선 JVM의 전체 구조 중, Java Native Method Interface(JNI)와 Native Method Libraries를 집중해서 보겠습니다.

Native Method

먼저, Native Method란 무엇일까요? Java에서 네이티브 메소드란, Java가 아닌 다른 언어(주로 C/C++)로 작성된 메소드로써, native keyword로 선언된 메소드를 의미합니다. Java 자체만으로는 접근하기 어려운 하드웨어 자원 제어, 운영체제 특정 기능 활용, 또는 성능상의 이유로 다른 언어로 구현된 기능 등이 있죠. 이 메소드들은 native 키워드로 선언되며, 실제 구현은 Native Method Libraries에 있습니다.

public class FileExample {
    // 네이티브 메소드 선언
    private native int open(String path, int flags);
    private native int write(int fd, byte[] buffer, int size);
    private native int close(int fd);
    
    // 네이티브 라이브러리 로드
    static {
        System.loadLibrary("fileio");
    }
    
    // Java 메소드에서 네이티브 메소드 호출
    public void writeToFile(String path, String content) {
        int fd = open(path, 1); // 1은 쓰기 모드를 의미
        byte[] data = content.getBytes();
        write(fd, data, data.length);
        close(fd);
    }
}

이 예제에서 open(), write(), close()는 Native Method로, 실제 구현은 'fileio'라는 네이티브 라이브러리에 있습니다. 그리고 Native Method에 대한 구현이 바로 Native Method Libraries에 있는 Native Code인 것이죠. 즉 요약하자면,

  • Native Method: Java 코드 내에 선언된 인터페이스이며, 실제 동작은 Native Code에 위임합니다.
  • Native Code: JVM 외부에서 실행되는 실제 구현체이며, 운영체제와 직접 상호작용하여 Native Method가 요청한 작업을 수행합니다. System Call이 실행되죠(System Call이 필요할 때만)

즉 Java에서 System Call은 Native Method를 통해 Native Code가 실행되며 실행되는 것입니다. 그리고 이를 가능케 하는 중간자로 JNI(Java Native Method Interface)가 있습니다.

Java Native Method Interface(JNI)의 역할

JNI(Java Native Method Interface)는 Java 코드와 네이티브 코드(C, C++ 등) 간의 다리 역할을 하는 인터페이스입니다. JNI를 통해 Java는:

  1. Native Method를 호출할 수 있습니다.
  2. Java 객체를 네이티브 코드에 전달할 수 있습니다.
  3. Native Code에서 Java 객체의 필드를 읽고 쓸 수 있습니다.
  4. Native Code에서 Java 메소드를 호출할 수 있습니다.

JNI는 Java의 플랫폼 독립성을 해치지 않으면서도 운영체제의 네이티브 기능을 활용할 수 있게 해주는 중요한 메커니즘입니다. JNI는 통역사와 비슷한 역할을 하는거라 할 수 있죠. 서로 다른 언어를 쓰는 외국인들끼리 만나도, 통역사를 통해 의사소통이 가능한 거죠.

JNI를 사용하는 네이티브 코드의 예를 살펴보겠습니다:

#include <jni.h>
#include <fcntl.h>
#include <unistd.h>

// Java_클래스명_메소드명 형식으로 함수 이름 지정
JNIEXPORT jint JNICALL Java_FileExample_open
  (JNIEnv *env, jobject obj, jstring path, jint flags) {

// Java 문자열을 C 문자열로 변환
    const char *nativePath = (*env)->GetStringUTFChars(env, path, NULL);

// open() 시스템 콜 호출
    int fd = open(nativePath, flags);

// 자원 해제
    (*env)->ReleaseStringUTFChars(env, path, nativePath);

    return fd;
}

// write(), close() 메소드도 비슷한 방식으로 구현

이 C 코드는 Java의 네이티브 메소드에 대응하는 네이티브 구현입니다. 여기서 open() 함수는 운영체제의 System Call을 직접 호출합니다.

자 이제 JVM 내부에서 System Call이 호출되는 과정을 이해하기 위한 개념 정리는 다 했습니다. 전체 과정을 살펴봅시다.

JVM 내부의 System Call 호출 과정

Java 프로그램에서 System Call이 호출되는 전체 과정을 살펴보겠습니다:

  1. Java 코드 실행: Java 애플리케이션이 표준 라이브러리 메소드(예: FileOutputStream.write())를 호출합니다.
  2. Java API 처리: 표준 라이브러리는 이 호출을 처리하고, 필요한 경우 JVM의 네이티브 메소드를 호출합니다.
  3. 네이티브 메소드 호출: JVM은 JNI를 통해 네이티브 라이브러리의 함수를 호출합니다.
  4. System Call 호출: 네이티브 함수는 운영체제의 System Call을 호출합니다.(By native code, …)
  5. 커널 처리: 커널은 System Call을 처리하고 결과를 반환합니다.
  6. 결과 전달: 결과가 네이티브 함수 → JVM → Java API → Java 애플리케이션으로 전달됩니다.

개념들을 알고나니 실행 과정을 이해하는게 어렵지 않죠? 그렇다면 System Call을 사용하는 Java Standard Libraries의 기능 예시를 몇가지만 살펴봅시다.

Java 표준 라이브러리와 System Call 매핑

Java 표준 라이브러리의 많은 기능들은 내부적으로 System Call에 의존합니다. 주요 예를 살펴보겠습니다:

파일 입출력

FileOutputStream fos = new FileOutputStream("test.txt");
fos.write("Hello, World!".getBytes());
fos.close();

사용 System Calls:

  • open(): 파일 생성/열기
  • write(): 데이터 쓰기
  • close(): 파일 닫기

네트워크 통신

Socket socket = new Socket("example.com", 80);
OutputStream out = socket.getOutputStream();
out.write("GET / HTTP/1.1\\r\\n\\r\\n".getBytes());

사용 System Calls:

  • socket(): 소켓 생성
  • connect(): 서버에 연결
  • write(): 데이터 전송

스레드 관리

Thread thread = new Thread(() -> {
    System.out.println("Hello from thread!");
});
thread.start();

사용 System Calls:

  • clone() 또는 pthread_create(): 새 스레드 생성
  • futex(): 스레드 동기화 (Linux의 경우)

이 외에도, 하드웨어 자원 사용이나 실행 흐름과 관련된 기능 등 매우 많은 기능들이 System Call을 사용합니다. System Call에 대한 이해 없이 언어의 동작 과정을 이해하는 건 불가능에 가깝죠. 하지만 Java는 System Call을 몰라도 편하게 사용할 수 있게 잘 추상화시켜놨습니다.

Java의 System Call 추상화 계층

Java는 운영체제의 System Call을 직접 노출하는 대신, 여러 계층의 추상화를 통해 이를 감춥니다:

  1. 고수준 API: Files, Paths, Channels 등의 현대적인 API
  2. 중간 수준 API: InputStream, OutputStream 등의 전통적인 API
  3. 저수준 JVM 네이티브 메소드: 내부 구현에 사용되는 메소드
  4. 운영체제별 네이티브 라이브러리: 각 OS에 맞는 구현
  5. 운영체제 System Call: 실제 커널 기능

이러한 계층 구조 덕분에 Java 개발자는 운영체제의 차이점을 신경 쓰지 않고도 코드를 작성할 수 있습니다. 같은 Java 코드가 Windows, Linux, macOS에서 동일하게 동작하는 것은 JVM이 각 운영체제에 맞는 네이티브 코드와 System Call을 알아서 처리해주기 때문입니다

JVM 기반 언어 vs. Native 언어(C)의 System Call 접근 방식 비교

JVM 기반 언어(Java, Kotlin, Scala …)는 패키지 여행과 같아 현지 언어와 문화(OS)를 몰라도 가이드가 모든 것을 처리해주죠. 반면 Native 언어는 가이드 없이 가는 자유여행이라 더 자유롭고 내 마음대로 여행할 수 있지만, 현지 언어와 문화(OS)에 맞게 개인이 일일이 준비를 해야합니다. 두 진영의 System Call에 대한 접근 방식도 이러한 차이에서 비롯합니다.

크로스 플랫폼 지원

플랫폼 이식성은 두 접근 방식의 가장 큰 차이점 중 하나입니다:

  1. 네이티브 언어:
    • 각 운영체제별로 소스 코드를 수정하거나 조건부 컴파일이 필요합니다.
    • 각 플랫폼별로 다시 컴파일해야 합니다.
    • System Call의 차이를 개발자가 직접 처리해야 합니다.
    #ifdef _WIN32
    // Windows 시스템 콜 사용
        HANDLE file = CreateFile("test.txt", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
        WriteFile(file, "Hello, World!", 13, &bytesWritten, NULL);
        CloseHandle(file);
    #else
    // UNIX/Linux 시스템 콜 사용
        int fd = open("test.txt", O_WRONLY | O_CREAT, 0644);
        write(fd, "Hello, World!", 13);
        close(fd);
    #end
    
  2. JVM 기반 언어:
    • "Write Once, Run Anywhere" - 한 번 작성하면 어디서나 실행 가능합니다.
    • JVM이 각 플랫폼의 차이를 추상화합니다.
    • 개발자는 운영체제 차이를 거의 신경 쓰지 않아도 됩니다.
    // 어떤 운영체제에서도 동일하게 작동
    FileOutputStream fos = new FileOutputStream("test.txt");
    fos.write("Hello, World!".getBytes());
    fos.close();
    

이는 마치 네이티브 언어는 각 나라의 현지 언어로 대화해야 하는 것과 같고, JVM 언어는 어디서나 영어로 대화할 수 있는 것과 비슷합니다.

플랫폼 특화 기능 접근성

반면, 각 플랫폼의 고유 기능을 활용하는 측면에서는:

  1. 네이티브 언어:
    • 운영체제의 모든 특화 기능에 쉽게 접근할 수 있습니다.
    • 최신 System Call이나 API를 즉시 활용할 수 있습니다.
  2. JVM 기반 언어:
    • 플랫폼 특화 기능은 JNI를 통해 접근해야 합니다.
    • JVM이 새로운 System Call을 지원하기까지 시간이 걸릴 수 있습니다.
    • 일부 저수준 기능은 표준 API로 제공되지 않을 수 있습니다.

따라서 특정 운영체제의 고유 기능이 필요한 애플리케이션(예: 시스템 드라이버, 저수준 도구)은 네이티브 언어가 유리하고, 다양한 플랫폼에서 일관되게 동작해야 하는 애플리케이션(예: 엔터프라이즈 소프트웨어, 웹 애플리케이션)은 JVM 기반 언어가 유리합니다.

오버헤드의 차이

두 접근 방식의 가장 큰 차이점 중 하나는 성능 오버헤드입니다:

  1. 네이티브 언어: System Call로의 전환만 오버헤드로 발생합니다. 즉, 사용자 모드 → 커널 모드 전환 비용만 지불합니다.
  2. JVM 기반 언어: JVM 추상화, JNI 변환, 그리고 사용자 모드 → 커널 모드 전환 등 여러 단계의 오버헤드가 발생합니다.

실제 벤치마크에 따르면, 단순한 System Call(예: 파일 열기/닫기)의 경우 JVM 언어는 네이티브 언어보다 2~10배 더 느릴 수 있습니다. 그러나 이 차이는 아래와 같은 상황에서 줄어듭니다:

  • 실제 작업(I/O, 연산 등)이 오래 걸리는 경우
  • JIT 컴파일러가 코드를 최적화한 경우
  • 반복적인 호출로 JVM이 최적화 기회를 얻은 경우

이는 마치 택시와 버스의 차이와 비슷합니다. 짧은 거리에서는 택시(네이티브)가 훨씬 빠르지만, 장거리에서는 고속버스(JVM)도 비슷한 시간이 걸릴 수 있습니다.

메모리 사용과 가비지 컬렉션

System Call 성능에 영향을 미치는 또 다른 중요한 요소는 메모리 관리 방식입니다:

  1. 네이티브 언어:
    • 명시적인 메모리 관리(malloc/free)로 인해 개발자가 완전히 제어할 수 있습니다.
    • 가비지 컬렉션으로 인한 지연이 없습니다.
    • 그러나 메모리 누수나 버퍼 오버플로우 위험이 있습니다.
  2. JVM 기반 언어:
    • 가비지 컬렉션이 자동으로 메모리를 관리합니다.
    • 가비지 컬렉션 중 일시적인 지연(Stop-the-World)이 발생할 수 있습니다.
    • 메모리 안전성이 더 높지만, 세밀한 제어는 어렵습니다.

System Call이 많은 애플리케이션에서는 가비지 컬렉션으로 인한 지연이 실시간 성능에 영향을 줄 수 있습니다. 이는 마치 고속도로에서 주행 중에 갑자기 연료(메모리)를 채우기 위해 정차하는 것과 비슷합니다.

결론: 각자의 장단점

JVM 기반 언어와 네이티브 언어의 System Call 접근 방식은 각각 고유한 장단점을 가지고 있습니다:

네이티브 언어의 강점:

  • System Call에 대한 직접적이고 효율적인 접근
  • 더 적은 런타임 오버헤드와 메모리 사용량
  • 운영체제와 하드웨어의 모든 기능에 완전한 접근

JVM 기반 언어의 강점:

  • 뛰어난 플랫폼 이식성과 "Write Once, Run Anywhere" 특성
  • 자동화된 메모리 관리와 향상된 안전성
  • 높은 수준의 추상화와 개발자 생산성

어떤 접근 방식이 "더 좋다"고 단정할 수는 없으며, 기술적으로 해결해야 하는 비즈니스 문제에 따라 적절한 진영을 선택하면 되겠습니다.

후기

후… 글 정리하는데 정말 힘들었습니다. 특히 적절한 비유를 생각해내는게 쉽지 않았는데 CS 개념을 나만의 비유를 만들어 설명하는 방식이 개념을 이해하는데 크게 도움이 되는 것 같았습니다.

또한 java는 언어와 운영체제 사이의 JVM 위에서 동작하지만, 결국 밑으로 들어가면 OS에서 배운 것과 똑같이 Native하게 동작한다는 것을 다시한번 느꼈습니다. 결국 OS 지식 없이는 언어의 동작 원리 무엇하나 이해할 수 없는 것이지요… Krafton Jungle에서 CS 중심의 학습을 하지 않았다면 이런 글을 도저히 적지 못했을 겁니다. 그리고 이번 Jungle CS Study를 통해 복습할 수 있어서 너무 좋네요. 추가로 공부해보고 싶은 내용도 마구마구 생각나구요! 다음 주제는 다른 스터디원 분들이 원하는 것으로 하기로 해 무엇이 될지 모르겠지만, 어떻게라도 자바와 엮어 준비해보겠습니다! 기다려주세용

출처

https://product.kyobobook.co.kr/detail/S000001868716

https://m.yes24.com/Goods/Detail/93738334

https://product.kyobobook.co.kr/detail/S000001550352

https://brewagebear.github.io/java-syscall-and-io/

https://inpa.tistory.com/entry/JAVA-☕-JVM-내부-구조-메모리-영역-심화편?category=976278

https://hongong.hanbit.co.kr/운영체제란-커널의-개념-응용-프로그램-실행을-위한/

 

 

'JAVA' 카테고리의 다른 글

Process Memory 구조와 JVM의 Memory 구조  (1) 2025.04.22

Intro

Jungle CS Study의 첫 주제는 “Process 개념과 관련지어, 자신이 사용 중인 기술 스택의 동작 원리에 대해 설명하자”이다.

상세 주제를 정하기 전에, Process란 무엇인가 복습해보자.

 

Process는,
OS가 Disk에 있던 Program Code를 읽어 RAM(Main memory)에 Load하여, 실행시킨 프로그램이다 (e.g., class와 instance의 관계)

 

프로세스가 thread, memory, system call 등을 모두 포함하는 워낙 포괄적인 개념이다 보니 구체적으로 어떤 주제를 잡을지 고민이 됐다… 그래도 요즘 포트폴리오로 만든 주변시위 Now라는 Spring App의 성능 테스트를 진행하면서 Prometheus와 Grafana로 JVM Memory metric을 접했기 때문에, “Memory”를 주제로 정했다! JVM도 하나의 Process니까, JVM의 메모리 구조를 까보는 것도 괜찮겠지?

Prometheus + Grafana를 통해 확인할 수 있는 JVM Memory 전체 구조
JVM Heap/Non-Heap Memory 구성요소. 각 요소들은 무엇을 뜻할까?

 

글의 목표

  • 일반적인 Virtual Memory 구조와 비교하여, JVM Memory 구조(Runtime Data Area)를 이해한다
  • JVM Memory 구조(Runtime Data Area)에서, 세부 구성 요소를 이해한다
    • Heap Area
      • Eden, Survivor Space
    • Non-heap Area
      • Meta Space

일반적인 Virtual Memory Segments

먼저, 흔히 배우는 가상 메모리 구조는 아래와 같다.

Code(Text) Segment

  • 실행 가능한 프로그램 코드(이진수로 된 기계어)가 저장되는 영역
  • 읽기 전용으로 설정되어 있어 프로그램이 자신의 코드를 수정할 수 없음
  • CPU의 Program Counter Register가 해당 영역의 주소를 저장한다
  • 프로그램이 메모리에 로드될 때 영역의 크기가 정해짐

Data Segment

  • Global 변수나, Static 변수 등 프로그램 전체에서 사용되는 데이터가 저장되는 공간
  • Code 영역과 마찬가지로, 프로그램 시작 시 할당되고 종료 시 해제됨
  • 크게 2가지의 Data 영역이 있다
    1. Initialized Data Segment
      1. Read-Only Data Segment
        • 상수와 문자열 리터럴이 저장됨
        • 실행 중에 수정이 불가능한 영역
        • 메모리 보호 기능으로 쓰기 시도 시 세그먼테이션 오류 발생
        • 예: const int MAX_VALUE = 100;, 문자열 상수 "Hello, World"
      2. GVAR(Global Variables) Segment
        • 초기값이 있는 전역 변수와 정적 변수 저장
        • Read/Write 모두 가능
        • 예: int globalCounter = 0;, static int count = 10;
    2. UnInitialized Data Segmnet
      1. BSS(Block Started by Symbol) Segment
        • 초기화되지 않은 전역 변수와 정적 변수가 저장
        • 프로그램 로드 시 0으로 초기화됨
        • Read/Write 모두 가능

힙(Heap) 영역

  • 동적으로 할당된 메모리를 관리하는 영역
  • 프로그램 실행 중 크기가 변할 수 있음
  • 메모리 할당(malloc/new)과 해제(free/delete)가 명시적으로 이루어짐
  • 낮은 주소에서 높은 주소 방향으로 증가

스택(Stack) 영역

  • 함수 호출 정보, 지역 변수, 매개변수 등이 저장
  • 함수가 호출될 때 스택 프레임이 생성되고 반환될 때 제거됨
  • 높은 주소에서 낮은 주소 방향으로 증가
  • 각 스레드는 독립적인 스택을 가짐

JVM Memory Segments(Runtime Data Area)

이제 JVM의 메모리 구조를 살펴보자.

JVM의 메모리 구조는 크게 모든 JVM 쓰레드가 공유하는 영역(Shared Memory)와 쓰레드마다 개별적으로 가지는 영역으로 구분된다. 그리고 일반적인 memory 구조에는 없는 영역들도 있는데, 하나하나 뭐 하는 녀석들인지 살펴보자.

Heap Memory

  • 모든 JVM 스레드가 공유하는 메모리 영역
  • new() 연산자로 생성한 객체와 배열이 저장되는 곳
  • 전체 크기는 한정되어 있지만, 사용 크기는 앱 실행 중에 동적으로 증가하거나 감소함
  • Garbage Collector(GC)에 의해 자동 관리됨
  • 종류
    • Young Generation
      • Eden Space
        • 새로 생성된 대부분의 객체가 처음 위치하는 영역
      • Survivor Spaces - S0, S1
        • 가비지 컬렉션 후 살아남은 객체가 이동하는 두 개의 영역
        • ‘From’, ‘To’ 영역이라 부르기도 함
    • Old Generation
      • Young Generation에서 오랫동안 살아남은 객체들이 이동하는 영역
      • 보통 더 큰 메모리 공간을 차지하며, Full GC의 대상이 됨

Metaspace

  • JVM 내에서 실행될 수 있는 byte code인 class file format(.class)의 정보를 저장(class에 대한 meta data)
    • constant pool, static variables, member variables/methods에 대한 정보 등
    • Java 8 이전과 이후로 포함 영역이 다름!

  • Java 8 이전: Permanent Generation로 구현
  • Java 8 이후: 네이티브 메모리에 있는 메타스페이스로 대체

 

코드 캐시(Code Cache)

  • JIT(Just-In-Time) 컴파일러가 네이티브 코드를 저장하는 영역
  • 자주 실행되는 메서드의 컴파일된 코드가 저장됨

JVM 스택(JVM Stack)

  • 각 스레드마다 독립적인 스택 생성. Stack 안에 “Frame”이란 것이 함께 만들어진다
  • Frame
    • frame은 메소드가 호출될 때마다 만들어지먀, 메소드의 상태 정보를 저장함
    • frame의 3요소: 1) Local Variables 2) Operand Stack 3) Frame Data
    • Stack Frame의 크기는 Compile time에 정의됨

Program Counter Register

  • 각 스레드마다 생성되는 레지스터
  • 현재 실행 중인 JVM 명령의 주소를 가리킴. Native 명령어의 주소값은 저장하지 않음
  • PC Registers는 Register-base로 구동되는 방식이 아니라 Stack-base로 작동

Native Method Stack

  • java code와 상호작용하는 native methods(C/C++ …)의 실행을 관리하기 위한 스택
  • C Stack이라고도 불림
  • 고정 크기 또는 동적 확장/축소 가능

일반적인 Memory 구조와 JVM의 메모리 구조 비교

이제 JVM의 메모리 구조에 대해서도 살펴보았다. 위 구조를 한 눈에 비교해서 보자

일반적인 Memory 구조
JVM 메모리 구조

주요한 차이 요약은 아래와 같다.

특성  일반 프로세스 JVM
코드 저장 Code Segment(기계어) Metaspace(바이트코드)
전역 데이터 Data Segment/BSS Metaspace, Heap
동적 메모리 Heap(명시적 관리) Heap(자동 GC 관리, 세대별 구조)
지역 변수 Stack JVM Stack(with Frame)
메모리 관리 프로그래머 책임 자동(GC)
메모리 안전성 낮음(버퍼 오버플로우 등 가능) 높음(범위 검사, 타입 안전성)
메모리 모델 운영체제 관리 JVM 관리
추가 구조 단순 PC Register, C Stack 등
플랫폼 의존성 높음 낮음(WORA - Write Once Run Anywhere)
성능 특성 직접 접근으로 빠름, 오버헤드 적음 GC 일시 정지, 추가 추상화 레이어로 오버헤드 있음

이제 상세한 비교 내용을 보자.

Code Segment 비교

  • 실행될 프로세스의 명령어 집합이 저장되는 곳에 대한 비교를 해보자

일반: Code Segment

  • 기계어로 컴파일된 실행 파일의 코드가 직접 저장됨
  • 읽기 전용으로 설정되어 수정 불가능(보안 강화)
  • 예: C 프로그램의 함수 코드는 컴파일되어 기계어로 변환된 후 이 영역에 로드됨

JVM: Metaspace, Code Cache

Metaspace

  • 클래스 파일의 바이트코드가 저장됨
  • 클래스 구조, 메서드, 필드, 상수 풀 정보 포함
  • 예: Java 클래스의 정적 메서드, 상수, 클래스 메타데이터가 이 영역에 저장됨
  • 네이티브 메모리(OS가 관리하는 메모리)에 할당됨

Code Cache

  • 자주 실행되는 메서드의 JIT 컴파일된 코드가 저장됨

주요 차이점:

  • 일반 프로세스는 기계어 코드를 직접 실행하지만, JVM은 바이트코드를 해석하거나 JIT 컴파일을 통해 실행
  • 일반 프로세스의 코드 세그먼트는 일반적으로 프로세스 수명 동안 고정되지만, JVM의 Metaspace 영역은 런타임에 클래스 로딩/언로딩에 따라 크기 변화가 가능

데이터 영역 비교

  • 전역/정적 변수들이 저장/관리 되는 방식은 어떻게 다를까?

일반: Data Segment

  • 초기화된 데이터 세그먼트: 명시적으로 초기화된 전역/정적 변수 저장
  • BSS: 초기화되지 않은 전역/정적 변수 저장(0으로 초기화됨)
  • 프로그램의 전체 실행 기간 동안 존재
  • 예: int global_var = 100; (초기화된 데이터), static int uninitialized_var; (BSS)

JVM: Metaspace, Heap

  • 클래스의 정적 변수는 Metaspace에 저장
  • 참조 타입 정적 변수의 실제 객체는 JVM Heap에 저장됨
  • 예: static int counter = 0;는 metaspace에 저장, static Student student = new Student();에서 참조는 metaspace에, 실제 Student 객체는 힙에 저장

주요 차이점:

  • 일반 프로세스는 데이터 타입에 따라 직접 값을 저장하지만, JVM은 참조 타입의 경우 참조만 저장하고 실제 객체는 항상 힙에 저장
  • JVM에서는 클래스 로딩 시 정적 필드가 초기화되며, 클래스 언로딩 시 제거될 수 있음

Heap Segment 비교

  • Reference type이 주로 저장되는 Heap 영역에는 어떠한 차이가 있는지 알아보자.

일반: Heap

  • malloc(), calloc(), realloc() 등의 함수를 통해 명시적으로 메모리 할당
  • free() 함수를 통해 명시적으로 메모리 해제 필요(메모리 관리는 프로그래머 책임)
  • 할당과 해제가 반복되면서 메모리 단편화 발생 가능
  • 범위를 벗어난 메모리 접근, 해제된 메모리 접근, 메모리 누수 등의 문제 발생 가능
  • 예:
  • int* array = (int*)malloc(10 * sizeof(int)); // 사용 후 free(array);

JVM: Heap

  • 모든 객체 인스턴스와 배열이 저장됨
  • 자동 메모리 관리(가비지 컬렉션)을 통해 더 이상 참조되지 않는 객체 자동 제거
  • 세대별 가비지 컬렉션을 위해 Young/Old 영역으로 구분
  • Young 영역은 다시 Eden과 두 개의 Survivor 공간으로 나뉨
  • 예:
  • ArrayList<String> list = new ArrayList<>();// 힙에 할당 list = null;// 참조 제거, GC 대상이 됨

주요 차이점:

  • JVM은 자동 메모리 관리(GC)를 제공하여 메모리 누수, 댕글링 포인터 문제 감소
  • JVM 힙은 세대별 구조로 최적화되어 단기 객체와 장기 객체를 효율적으로 관리
  • 일반 프로세스는 메모리 할당 크기를 직접 지정하지만, JVM은 객체 크기를 자동으로 계산
  • 일반 프로세스는 메모리 해제 시점을 프로그래머가 결정하지만, JVM은 가비지 컬렉터가 결정

Stack Segment 비교

일반: Stack

  • 함수 호출 정보(활성화 레코드/스택 프레임)와 지역 변수 저장
  • LIFO(Last In First Out) 구조로 작동
  • 컴파일 시간에 크기가 결정되는 지역 변수는 스택에 할당
  • 함수 호출 시마다 새로운 스택 프레임 생성
  • 재귀 호출이 너무 깊거나 지역 변수가 너무 많으면 스택 오버플로우 발생 가능
  • 예:
  • void function() { int local_var = 10;// 스택에 할당// ... }// 함수 종료 시 스택 프레임 제거

JVM: Stack(with Frame)

  • 각 스레드마다 별도의 JVM 스택 생성
  • 메서드 호출마다 Stack Frame 생성
  • 스택 프레임은 로컬 변수 배열, 피연산자 스택, 프레임 데이터 포함
  • 원시 타입 변수와 객체 참조값은 스택에 저장(객체 자체는 힙에 저장)
  • 예:
  • void method() { int x = 10;// 스택에 저장 Object obj = new Object();// 참조는 스택에, 객체는 힙에 저장 }// 메서드 종료 시 스택 프레임 제거

주요 차이점:

  • 일반 프로세스 스택은 주로 함수 실행 상태와 지역 변수 저장에만 집중한다
  • 반면, JVM Stack은 기본적은 stack의 기능과 더불어 method 호출 단위의 Frame이란 개념이 추가되며, byte code 실행과 관련된 기능을 수행하기도 한다
    • JVM 스택은 스레드마다 독립적으로 생성됨
    • JVM 스택 프레임은 피연산자 스택을 포함하여 바이트코드 실행을 지원
    • JVM 스택은 참조 타입의 경우 참조만 저장하고 실제 객체는 항상 힙에 저장됨
    • JVM 스택은 현재 실행 중인 바이트코드에 대한 정보를 포함

추가 JVM 메모리 구조

  • 일반적인 memory 구조에는 없는, JVM에만 있는 memory 영역 또한 존재한다

PC 레지스터

  • 각 스레드마다 별도의 PC 레지스터 존재
  • 현재 실행 중인 JVM 명령어 주소(바이트코드 오프셋) 저장
  • 네이티브 메서드 실행 시 undefined 값 가짐

네이티브 메서드 스택

  • JNI(Java Native Interface)를 통해 호출된 네이티브 메서드를 위한 스택
  • C 스택이라고도 불림
  • 일반 프로세스의 스택과 유사하게 작동

주요 차이점:

  • JVM은 ‘Write Once Run Anywhere’라는 철학으로 만들어졌다. 따라서 특정 OS나 Hardware에 종속되지 않고자 위와 같은 요소를 메모리 구조에 추가하였다

후기

오랜만에 CS 공부를 하니 시간 가는 줄 모르고 정말 재미있게 했다. 특히 요즘 prometheus로 JVM Metric을 수집하는데, JVM 관련 지식에 목말랐던 터라 이번 JVM의 메모리 구조에 대해 공부하는게 더욱 흥미로웠다. 이런 식으로 일반적인 CS 개념이 내가 요즘 궁금해하고 사용하는 기술과 비교하여 공부하니 학습 효과도 더욱 뛰어난 것 같다. 다만 아쉬운 점도 있다. JVM의 Memory 구조에는 ‘Write Once Run Anywhere’라는 java의 철학을 반영하기 위해 존재하는 영역이나 방식이 많아, Java가 이러한 철학을 지키기 위한 전체적인 흐름(JRE, Java Runtime Environment)에 대해 알고 있었다면 더욱 빠른 학습과 깊이 있는 이해가 가능했을 것 같다. 그래도 이번 주제를 열심히 준비한 덕분에 java의 실행 방식에 대해 전체적인 그림을 익힐 수 있었다.

또 이번 글을 준비하며 다음과 같은 주제에 흥미가 생겼다

  • java의 전체적인 동작 흐름(compile부터 종료까지)
  • java의 system call 동작 과정
  • JVM Memory 최적화
  • JIT Compile

앞으로 스터디 할 날이 많으니, 차차 다뤄봐야겠다.

 

출처

https://hongsii.github.io/2018/12/20/jvm-memory-structure/

다이어그램

https://jaemunbro.medium.com/java-metaspace에-대해-알아보자-ac363816d35e

https://johngrib.github.io/wiki/java8-why-permgen-removed/

https://johngrib.github.io/wiki/jvm-stack/

https://loosie.tistory.com/846

https://help.sap.com

https://kotlinworld.com/308

https://velog.io/@lucius__k/JIT-컴파일러와-코드-캐시

'JAVA' 카테고리의 다른 글

System Call과 JVM  (2) 2025.04.28
정글에서 살아남기

 

 

지나온 과거에 대한 성찰

  입대 전까지는 개발의 근간에 대해 고민하지 않고 그저 앞에 닥친 문제를 해결하기 위해 애쓰는 하루의 연속이었다. 나름 애쓰다 보니 눈 앞의 문제들은 어느정도 해결할 수 있었다. 하지만 일을 하면 할 수록 내가 만든 것들의 동작에 대한 이해 없이는 더 레벨업하기 힘들겠다는 것이 느껴졌다. 그러던 와중 갑작스런 입대를 하게 됐다.

 

  군복무 하며 이런 나의 부족한 cs 지식을 채우고 싶었다. 하지만 달마다 1~2주의 훈련이나 휴가를 다녀오면 공부의 맥락이 끊기기 일수였다. 아무리 책을 붙들어도 연속성이 떨어져 머리에 남는게 없었다. 공부 내용과 방법에 대해 시행착오를 하느라 많은 시간이 소비되었다. 한창 사회활동 할 시기에 군복무 중인 것도 너무 시간 아까운데 공부마저 내 마음대로 안되자 배움에 대한 아쉬움은 더욱 커져만 갔다.

 

5개월 동안 얻어가고 싶은 것

  그러던 중 정말 감사하게도 크래프톤 정글 6기 과정을 시작하게 됐다. 이번 과정을 통해 얻고 싶은 것은 다음과 같다.

- Algorithm, Data Structure, OS, Network, DB와 같은 핵심 CS 지식

- 하루 12시간 이상 주 6일 꾸준히 공부하는 습관

- 앞으로의 개발 인생에서 서로 믿고 의지할 수 있는 동료

 

임하고 싶은 자세

  목표로 하는 것을 얻기 위해선 정말 많은 노력이 필요하다. 배울 것은 산더미고 아무리 공부를 많이 한다고 한들 주어진 시간은 충분하지 않다. 그렇다고 포기할 수는 없다. 목표를 크게 가져야 그 일부라도 달성한다. 또 아무리 거대한 목표라도 그것을 최대한 잘게 sub goals로 쪼개고, 그것을 달성하는데 하루하루 집중하다 보면 어느새 수많은 계단을 올라와 있을거라 믿는다.

 

  공부에는 끝이 없다. 마라톤을 스프린트로 뛰면 결코 완주할 수 없다. 5개월의 정글 과정 중에도, 끝난 후에도 꾸준히 하기 위한 시간 관리와 체력 안배 또한 매우 중요하다. 공부만큼 휴식과 운동 또한 잘 챙겨보는 것이 목표다. 강렬한 빛을 내지만 짧게 끝나고 마는 초신성보다, 밝기는 상대적으로 약할 수 있어도 끊임없이 빛나는 항성이 되고 싶다.

 

정글이 끝난 후 나의 모습

  본 과정이 끝난 후 원하는 모습은 간단하다. 위에서 언급한 5개월 동안 얻어가고 싶은 것을 모두 가진 모습이다. 본 과정이 내 모든 문제를 해결해줄거라 기대 하지 않는다. 적어도 이 곳에서의 목표는 모두 이루고, 그것을 양분 삼아 더 빠르게, 더 오래 달릴 수 있는 기초체력이 완성되기를 바란다. 과정 이후로도 만들고 싶은 프로덕트를 만들며, 이를 바탕으로 나를 다음 단계로 성장시킬 수 있는 조직에서 내 몫의 역할을 하며 사회에 기여하겠다.

 

 

 

유튜브 영상 controller는 약 3초 동안 사용자의 움직임이 없으면 영상 controller를 숨김.

이를 react hook으로 간단히 만들어 봄

 

import { useEffect, useRef, useState } from 'react';

export default function useVideoControllers(playing: boolean) {
  const [showControllers, setShowControllers] = useState(true);

  const timeout = useRef<number | null>();

  useEffect(() => {
    const showControllers = () => {
      setShowControllers(true);
      if (timeout.current) window.clearTimeout(timeout.current);
      timeout.current = window.setTimeout(() => {
        setShowControllers(false);
      }, 3500);
    };
    window.addEventListener('mousemove', showControllers);
    return () => window.removeEventListener('mousemove', showControllers);
  }, []);

  return { showControllers };
}

 

알게된 것

  • state가 바뀌면 그냥 local 변수는 값의 재할당이 일어난다
    • re-render와 상관없이 data를 유지하고 싶으면 ref를 꼭 쓰자!!! useRef 쓸 일이 생각보다 정말 많네
  • 등록한 setTimeout은 clearTimeout으로 없앨 수 있다
  • setTimeout의 return 값은 number 형식의 id 이다

문제 상황

회사 일로 react-sortable-tree를 쓰는데, 'Cannot have two HTML5 backends at the same time.'라는 에러가 떴음

문제는 말 내가 한 페이지 내에서 두개 이상의 SortableTree를 사용하기 때문이었음

https://github.com/frontend-collective/react-sortable-tree/issues/177

해결 시도

찾아보니, SortableTreeWithoutDndContext를 쓰고, dnd를 내 컴포넌트에서 주입해주면 된다고 함. 

https://github.com/frontend-collective/react-sortable-tree/issues/511

그렇게 했지만, 렌더는 잘됐던게 아무것도 안뜸. 서치를 더 해보니 밖에서 주입해야하는 dnd가 적용이 안되서 그럼. 분명 안될리가 없는데 안됐음

 

해결

이슈를 계속 뒤적여보니, react-dnd가 내 package와 sortableTree 두 군데서 따로 load되어 사용되기 때문이라는 코멘트가 보였음

나를 살린 한 문장

https://github.com/frontend-collective/react-sortable-tree/issues/665

그래서 npm dedupe --legacy-peer-deps를 적용하니 문제가 해결됨!!

 

npm dedupe

여러 패키지에서 공통으로 사용하는 패키지를 정리해줌

https://outofbedlam.gitbooks.io/npm-handbook/content/cli/npm-dedupe.html

Next.js에서 window 등 client 객체 사용하기

  • Next.js는 기본적으로 SSR임
  • 따라서 평소처럼 window, document 등 client에서 쓰는 객체를 평범하게 사용하려고 하면 반드시 error가 남
  • 위와 같은 객체들은 꼭 useEffect를 통해 mount 되고 사용하는 것을 추천함

Next.js에서 window 객체를 사용해 화면 크기를 구하는 hook 예제

https://stackoverflow.com/questions/63406435/how-to-detect-window-size-in-next-js-ssr-using-react-hook

 

How to detect window size in Next.js SSR using react hook?

I am building an app using Next.js and react-dates. I have two component DateRangePicker component and DayPickerRangeController component. I want to render DateRangePicker when the window's width is

stackoverflow.com

 

tailwind css의 mobile first

  • tailwind는 mobile first임
  • 따라서 반응형으로 스타일링을 할 때, mobile을 default로 하고, pc를 옵션으로 설정해야 함(mobile을 타겟으로 하는 것들 sm:...등은 사용하지 않아야 함

https://tailwindcss.com/docs/responsive-design#mobile-first

 

Responsive Design - Tailwind CSS

Using responsive utility variants to build adaptive user interfaces.

tailwindcss.com

 

1. 좋은 개발자, 좋은 개발 팀이 되기 위한 조언

1) 남을 가르치는 기회를 가져보라 - 내부 컨퍼런스 등

  • 남을 가르치려면 내가 알고 있는 정보 정리를 잘 해야 함
  • 교육을 준비할 때 내가 무엇을 알고 모르는지 정확히 알 수 있음

 

2) 코드리뷰를 하라

  • 하나의 기능을 구현하는 방법은 수백가지임
  • 반면 한 명의 개발자가 생각해낼 수 있는 방법은 소수에 불과함
  • 코드리뷰를 통해 다른 사람의 코드를 참고하고 서로 대화하면, 최적의 구현법을 찾는데 매우 큰 도움이 됨
  • 코드리뷰 프로세스는 개발자 개인의 성장을 위해 매우 필요함
  • 하지만, 코드리뷰가 자리 잡기 위해선 조직적인 합의가 필수임
  • 리뷰 초반에는, 아무리 바쁘더라도 퇴근 전 하루 한 번은 꼭 코드리뷰 하는 것을 추천함

 

3) 테스트 코드를 작성하라

  • 테스트 코드는 경제적임
  • 서비스 / 비즈니스에 따라 적절한 테스트 커버리지가 다름
  • 테스트 코드는 새로운 개발자 landing하는 데에도 좋음

 

4) Software Architecture 문서를 작성하라

  • 비즈니스 / 팀의 상황과 특성에 따라 중요한 특성(Quality Attribute)이 다름
  • Software Architecture 문서는 Quality Attribute 등 “개발 철학”을 정의한 문서임

(Software architecture in practice 책 추천)

 

 

5) 개발 스터디를 하라

  • 공부에는 강제성과 관성이 필요함
  • 혼자하는 공부는 위 두 요소를 충족시키기 어려움 → 지속하기 힘듦
  • 29살 때는 2주 간격의 스터디를 6개 정도 진행함
  • 지금까지도 개발 스터디 하는 중. 아래의 책으로 진행하고 있음

 

6) 문서 작성 또한 중요하다

  • 최상의 엔지니어는 스펙을 정의하고 정리하는 사람임
  • Use Case Specification 문서 정도는 만들어 보는 것을 추천함

 

7) 해외 개발 컨퍼런스에 참가하라

  • 해외 컨퍼런스에는, 유명한 언어 / 프레임 워크의 창시자들을 직접 만나고 질문할 수 있음
  • 그러기 위해 영어공부는 매우 필수임!

 

8) 이슈 트래킹 툴 도입 추천

  • 숫자로 이슈와 관련된 내용들을 트래킹 하는 것이 중요함
  • 개발 조직이 크기 위해 체계와 체계를 실천하기 위한 이슈 트래킹 툴과 같은 도구가 필요함

 

2. Q&A

Q. CS 지식은 언제, 어떻게 공부하는 것이 좋은가?

A. 필요할 때 공부하는게 제일 좋다. 공부한 내용을 체화하려면 피부에 와닿는 현실에서의 이슈가 필요하다

 

Q. 좋은 코드, 좋은 개발자란?

A. 아래와 같이 생각한다

  • 좋은 코드 - simple & solid한 코드. 오랜 세월 계속 사용할 수 있는, 높은 커버리지, 유연함 등
  • 좋은 개발자 - 편견이 없는 유연한 개발자. 다양한 언어 / 소프트웨어의 철학을 이해하고 적절히 조합하여 사용할 수 있는 개발자

 

3. 기타

  • 추천하는 책 목록
    • Design Pattern
    • Refactoring
    • Optimal Java
    • Clean Architecture
  • 성장하지 못하는 개발자들은 난관에 부딪혔을 때 고민 없이 바로 물어봄
    • 훌륭한 개발자가 되기 위해선, 충분히 혼자 고민을 해야 함
    • 고민을 바탕으로 자신만의 질문을 정교화하고, 그 질문에 대한 자신만의 가설 or 결론을 세워야 함
    • 그 뒤에 질문을 해야 함
  • 2030 개발자들에게 꼭 추천하는 것!
    • 연애를 하세요… (40대 이상의 개발 독거노인들 많다..)
    • 영어공부 열심히 하세요
    • 계속 꾸준히 공부하세요. 혼자 힘들면 스터디를 하세요

 

1. input에서 한 글자 입력 후 focus를 잃는 현상

react로 input을 사용하다보면, 가끔 한 글자 입력 후 input의 focus가 사라지는 현상이 발생한다.

이러한 현상의 원인은 100% state의 변경 등으로 인해 해당 Input이 re-render 되기 때문이다

 

State 변경으로 인한 re-render

1. input만 함수 or 컴포넌트로 따로 묶는 경우

export default function Editor() {
	
	const [name, setName] = useState("");
    
    const onChange = (e) => {
    	const {value} = e.target;
        setName(value);
    }
    
    const Input = () => (<input name="name" value={name} onChange={onChange} />)

	// 한 글자 입력할 때마다 state가 변경 -> Input 컴포넌트가 re-render됨 -> focus 잃음
    // input을 아래 return에 바로 넣어줘야 함
	return (
    	<div>
        	<Input/>
        </div>
    )
}

https://stackoverflow.com/questions/42573017/in-react-es6-why-does-the-input-field-lose-focus-after-typing-a-character

 

In React ES6, why does the input field lose focus after typing a character?

In my component below, the input field loses focus after typing a character. While using Chrome's Inspector, it looks like the whole form is being re-rendered instead of just the value attribute of...

stackoverflow.com

 

2. map의 key 값에 함수 값을 주는 경우

key 값으로 nanoid() 등을 주는 경우, state가 변경될 때마다 nanoid()가 새롭게 실행되어 input이 새롭게 re-render 됨 → focus 잃음

 

3. input이 있는 styled-component 객체를 jsx 컴포넌트 안에 선언하는 경우

state가 변경될 때마다 styled-component 변수를 새롭게 선언함 → focus 잃음

https://www.inflearn.com/questions/121968

 

input 한글자 입력후 focus사라지는 현상 - 인프런 | 질문 & 답변

어...아래 에러는 해결했는데  로그인할때 input창에서 한글자 입력하면 focus를 잃어서 다시 입력하려면 input창을 클릭해야하는 현상이 발생하는데  이건 어디서 문제가 발생한 걸까요 제가 보이

www.inflearn.com

 

2. string으로 js object 값에 접근하기 - reduce 사용

const obj = {
  lego: {
    name: "hello",
  },
};

const string = "lego.name";
const result = string.split(".").reduce((acc, cur) => acc[cur], obj); // hello

 

1. 브라우져별 지원하는 동영상 format이 다를 수 있다

  • 아이폰으로 촬영된 영상의 포맷은 HEVC임
  • 하지만 해당 포맷은 크롬에서 지원하지 않기 때문에 재생이 안됨

1. react-query 특정 조건일 때 fetch하도록 하기

  • 이전부터 찾아 해매던 기능... 역시 영어로 stack overflow 보는게 최고 빠르다...
  • document 예제와 같이, query option으로 enabled(boolean) 값을 주면 됨!! 
    • enabled로 준 값이 true일 때만 fetch함
    •  // Get the user
       const { data: user } = useQuery(['user', email], getUserByEmail)
       
       const userId = user?.id
       
       // Then get the user's projects
       const { isIdle, data: projects } = useQuery(
         ['projects', userId],
         getProjectsByUser,
         {
           // The query will not execute until the userId exists
           enabled: !!userId,
         }
       )
       
       // isIdle will be `true` until `enabled` is true and the query begins to fetch.
       // It will then go to the `isLoading` stage and hopefully the `isSuccess` stage :)

https://react-query.tanstack.com/guides/dependent-queries

 

Dependent Queries

Subscribe to our newsletter The latest TanStack news, articles, and resources, sent to your inbox.

react-query.tanstack.com

https://stackoverflow.com/questions/63397534/conditionally-calling-an-api-using-react-query-hook

 

Conditionally calling an API using React-Query hook

I am using react-query to make API calls, and in this problem case I want to only call the API if certain conditions are met. I have an input box where users enter a search query. When the input va...

stackoverflow.com

 

1. PM을 내려놓으며 배운 것들

회사에서 일을 하면서 대표님과의 의견 충돌로 맡고 있던 프로젝트의 PM을 내려놓게 됐다.

위 과정에서 배운 것들을 간단히 정리해보자.

  • 사람은 결국 따라와주길 바라는 자기만의 규칙이 있다
    • 아무리 상대가 규칙이 없이 자유롭게 해봐라 해도, 사람인 이상 따라줬으면 하는 규칙이 분명 있다
    • 그러니 정말 자유롭게 했을 때 상대가 뭐라고 해도 속상해하지 말자. 사람인 이상 어쩔 수 없다
    • 그 규칙을 어떻게하면 귀찮게 안하면서 잘 파악할 수 있을지 고민해보자
  • 그릇이 큰 사람이 되자
    • 상대가 나의 기분을 상하게 했더라도, 나도 똑같이 나가며 들이받지 말자(정말 부당하고 나쁜 일 제외)
    • 들이받을 당시에는 통쾌할 수 있으나, 나중에 분명 후회한다
    • 상대의 잘못을 지적하거나 따지려 하지 말고, 나의 부족한 점에 집중하자
    • 상대방을 바꾸려 하지 말고, 내가 스스로를 바꿀 수 있는 것에 집중하자
    • 상대가 어떠한 자세로 나오든, 나는 끊임 없이 낮은 자세로 상대방을 대하자
  • 일과 나를 동치시키지 말자
    • 사회에서 그룹으로 하는 모든 일은, 분명 내 마음대로 안되고 좌절하게 된다
    • 일과 나를 동치시킨다면, 그때마다 상처입고 괴로운 시간을 보내게 된다
    • 하지만 이런 결과가 일을 더욱 잘하게 되는데 큰 도움은 되지 않는 것 같다
    • 오히려 일과 나를 분리시키고, 객관적인 관점에서 일을 잘되게 하는데 집중하는게 더욱 도움이 많이 되는 듯 하다
  • 사람을 그 사람이 한 말이 아니라, 그 사람이 한 행동으로 이해하자
    • 말과 행동이 일치하는 것은 정말 어렵고, 나도 잘 지키지 못한다
    • 하지만 이 어려운 것을 상대방이 지킬 것이라 생각하고, 말로 그 사람을 이해한다면 충돌하는 행동들에 상처받고 이해하지 못하게 된다
    • 말에 현혹되지 말고 그 사람의 행동으로 이해하자
  • 회사 분의 말씀 중 가장 위로가 되었던 말
    • "저는 어떤 일이 잘못되었을 때, 개인이 아니라 팀 전체의 문제라고 생각해요. 누군가 개인을 탓한다면 그 탓하는 사람이 부족한 것 같아요. 개인을 탓한다고 변하는건 없어요. 무슨 일이 잘못된다면, 그것을 예방할 수 있는 시스템을 고민하고 개선하는게 맞아요"
      • 내가 자책하고 다른 사람들이 나를 부족한 사람이라 여길 것이라 생각했던건, 어찌보면 나 스스로가 회사에서 문제가 생긴다면 문제를 일으키는 개인을 탓하는 마음을 갖고 있어 그런 것 같다. 
      • 나부터 개인을 탓하는 마음을 가지지 않도록 그릇을 키워야겠다

 

1. Custom Hook 사용 조건

  • Rules of Hooks를 지켜야 함
  • 다른 컴포넌트에서 hook을 동시에 여러군데서 사용해도 side effect가 없어야 함
  • 디버깅하기 용이해야 함

https://medium.com/finda-tech/%ED%95%80%EB%8B%A4%EC%97%90%EC%84%9C-%EC%93%B0%EB%8A%94-react-custom-hooks-1a732ce949a5

 

핀다에서 쓰는 React Custom Hooks

+ Custom Hooks로 적합한 것과 그렇지 않은 것

medium.com

 

2. React: Too many re-renders Error

  • setState 함수가 react component function에서 useEffect나 closure로 감싸져 있지 않아서 생기는 문제
  • 컴포넌트 render → setState 초기화 → 컴포넌트 render → setState 초기화 → so on....

 

 

1. return type 'Element[]' is not a valid jsx element

  • valid한 JSX Element는 Signle Root를 return 해야함!

https://stackoverflow.com/questions/62702485/component-cannot-be-used-as-a-jsx-component-its-return-type-element-is-not

 

Component cannot be used as a JSX component. Its return type 'Element[]' is not a valid JSX element

I'm currently getting the following error on the Todos component inside TodoApp.tsx: 'Todos' cannot be used as a JSX component. Its return type 'Element[]' is not a valid JSX element. Type 'Element...

stackoverflow.com

 

 

1. Nullish Operator

  • The Nullish Coalescing Operator
    • function myFn(variable1, variable2) {
        let var2 = variable2 ?? "default value"
        return variable1 + var2
      }
      
      myFn("this has", " no default value") //returns "this has no default value"
      myFn("this has no") //returns "this has no default value"
      myFn("this has no", 0) //returns "this has no 0"
  • Logical Nullish Operator
    • function myFn(variable1, variable2) {
        variable2 ??= "default value"
        return variable1 + variable2
      }
      
      myFn("this has", " no default value") //returns "this has no default value"
      myFn("this has no") //returns "this has no default value"
      myFn("this has no", 0) //returns "this has no 0"

 

 

 

 

1. TS에서 enum 대신 const를 활용한 union type을 쓰자

  • enum을 사용하는 것을 자제하는게 좋은 이유
    • TypeScript에서 enum을 사용하면 Tree-shaking이 되지 않음
      • Tree-shaking = 사용하지 않는 코드를 삭제하는 기능
    • const enum은 긴 문자열을 할당할 경우 효과적이지 않음
      • ts file을 js로 transfile할 경우
  • 모든 것을 고려했을 때, 아래와 같이 const를 활용해 type화 하는 것이 좋음
const MOBILE_OS = {
  IOS: 'iOS',
  Android: 'Android'
} as const;

type MOBILE_OS = typeof MOBILE_OS[keyof typeof MOBILE_OS]; // 'iOS' | 'Android'

https://engineering.linecorp.com/ko/blog/typescript-enum-tree-shaking/

 

TypeScript enum을 사용하지 않는 게 좋은 이유를 Tree-shaking 관점에서 소개합니다. - LINE ENGINEERING

안녕하세요. LINE Growth Technology UIT 팀의 Keishima(@pittanko_pta)입니다. 이번 글에서는 TypeScript의 enum을 사용하지 않는 편이 좋은 이유를 Tree-shaking 관점에서 소개하겠습니다.

engineering.linecorp.com

 

+ Recent posts