[스터디] 프로세스의 구조

Jun_k·2026년 4월 24일

CS

목록 보기
7/16

정적변수와 지역변수는 무엇이 다른가

지역변수

  • 메서드 실행 중에만 존재
  • 메서드 끝나면 사라진다.
  • 초기화는 필수적으로 해줘야 한다.
    • 메서드 호출할 때마다 새로 만들어지고,
      스택에 할당되는데 스택은 이전 데이터로 오염되어 있다.
      그래서 초기화 안 하면 쓰레기값 남아있을 수 있기에
      자바는 안전을 위해 반드시 초기화하도록 강제한다.
  • 저장 위치는 스택 영역에 저장된다.
class Counter {
    void count() {
        int num = 0;  // 초기화 필수
        num++;
        System.out.println(num);  // 1 출력
    }  // ← num은 여기서 사라진다.
}

public class Main {
    public static void main(String[] args) {
        Counter c = new Counter();
        c.count();  // 1 출력
        c.count();  // 1 출력 (새로운 num)
        c.count();  // 1 출력 (여전히 새로운 num)
    }
}
  • 매번 메서드를 호출할 때마다 num이 새로 만들어진다.
    당연히 이전 값은 사라진다.

정적변수

  • 프로그램 시작 ~ 종료까지 존재한다.
  • 메서드가 끝나도 값을 기억한다.
  • 초기값은 0으로 자동 초기화 된다.
    • 프로그램 시작 시 메모리 영역에 한 번만 할당 된다.
      메모리 영역은 프로그램이 깔끔하게 초기화 해준다.
      따라서 초기화 생략이 가능하다.
  • 저장 위치는 프로세스가 점유하고 있는
    메모리의 데이터 영역에 저장된다. (C/C++)
    자바는 정적변수가 메서드 영역에 저장된다.
class Counter {
    static int num = 0;  // 프로그램 시작 시 한 번만 초기화
    
    void count() {
        num++;
        System.out.println(num);
    }  // num은 메서드가 끝나도 사라지지 않는다.
}

public class Main {
    public static void main(String[] args) {
        Counter c = new Counter();
        c.count();  // 1 출력
        c.count();  // 2 출력 (이전값 기억함)
        c.count();  // 3 출력 (계속 기억함)
    }
}
  • static int num은 처음 한 번만 만들어지고, 이후로는 그 값을 계속 기억한다.

JVM 메모리 구조

메서드 영역 (Method Area)

  • 코드 + 데이터 영역 (클래스 정보, static 변수, 전역변수 등)
  • 메서드의 이름, 반환 타입, 매개변수 정보
  • 실제 실행될 메서드의 바이트코드 (.class 파일의 내용)

힙 (Heap)

  • 객체 인스턴스 (new로 만든 것들)

스택 (Stack)

  • 지역변수, 메서드 호출, 콜스택

class MyClass {
    static int global = 10;  // 메서드 영역 (= C의 데이터 영역)
    
    public static void main(String[] args) {
        int local = 5;                    // 스택 (지역변수)
        Integer obj = new Integer(20);    // obj는 참조 변수라 스택, 객체는 힙영역
    }
}

64비트 컴퓨터 기준으로 최대 256테라바이트까지 사용할 수 있다?

책 내용을 보니깐 64비트 컴퓨터 기준으로 최대 256테라바이트까지 사용 가능하다고,
나와 있는데 여기서 생긴 의문점이 실제 물리 메모리가 256테라바이트가 안되는데
어떻게 가능하다는 것인지 궁금하여 찾아보게 되었다.

  • 일단 이론적으로 사용할 수 있는 주소의 범위가 256TB 라는 것이고,
    (가상의 주소 공간)
    실제로는 물리 메모리(RAM)와 디스크(Swap)의 공간에는 한계가 있기에
    실제로는 RAM(16GB) + Swap(약 2TB) 정도만 사용 가능하다는 뜻이다.

  • 이런식으로 설계한 이유는 운영체제가 가상 메모리 시스템을 사용하기 때문이다.
    프로그래머는 256TB의 주소공간이 있다고 생각하고,
    프로그램을 만들면 되고, 실제로 메모리가 부족해지면 운영체제가
    알아서 자동으로 디스크(Swap)를 메모리처럼 사용하여 처리해준다.

    따라서 프로그래머는 메모리 크기를 깊게 고민할 필요 없이
    필요한 만큼 할당하면 운영체제가 알아서 관리해주는 식이다.

  • 프로그래머의 입장: "256TB 주소공간 있으니 마음껏 사용해 봐야지"

  • 운영체제의 입장: "일단 가상 주소는 할당해줄게
    실제 메모리는 RAM + Swap으로 관리할게"

  • 실제 동작

    • RAM으로 빠르게 처리 (가능하면)
    • RAM 부족 - Swap 사용 (느려짐)
    • 둘 다 부족 -> 할당 실패 (에러)

힙 영역의 주소 증가, 스택영역의 주소 감소

힙 (Heap)

  • 낮은 주소에서 높은 주소로 증가한다. = 주소 값이 계속 커진다.
  • 버스 좌석 예시로 힙은 낮은 번호로 시작하는 앞에서부터 좌석을 채운다.
  • 예: 0x1000 → 0x2000 → 0x3000 (증가)

스택 (Stack)

  • 높은 주소에서 낮은 주소로 감소한다. = 주소 값이 계속 작아진다.
    버스 좌석 예시로 스택은 높은 번호로 시작하는 뒤에서부터 좌석을 채운다.
  • 예: 0x7000 → 0x6000 → 0x5000 (감소)

다중 스레드 vs 다중 프로세스 + IPC

다중 스레드(Multi-Thread)

  • 같은 프로세스 내의 모든 스레드는 같은 메모리 공간(코드, 데이터, 힙) 공유

  • 스레드 A와 스레드 B가 같은 변수에 접근 가능

  • 운영체제가 격리하지 않음

  • 스레드 생성 비용: 낮음 (수 마이크로초 ~ 수백 마이크로초)

    • 메모리 공간은 이미 있음
    • 스택만 추가로 할당 (스택 영역만 개별로 가지기 때문)
    • 컨텍스트 스위칭도 빠름

다중 프로세스

  • 각 프로세스는 독립적인 메모리 공간을 가진다.

  • 프로세스 A의 메모리 공간 ≠ 프로세스 B의 메모리 공간

  • 운영체제가 완전히 격리

  • 프로세스 생성 비용: 높음 (수 ms ~ 수십 ms)

    • 새로운 메모리 공간 할당
    • 운영체제가 개별 관리
    • 컨텍스트 스위칭 비용도 큼

왜 이 차이가 생기는가?

  • 메모리 격리는 보안과 안정성을 위함
  • 한 프로세스의 오류가 다른 프로세스에 영향을 주지 않음
  • 스레드는 빠른 통신이 필요해서 공유 메모리 선택

IPC가 왜 필요한가?

기본 상황: 프로세스는 격리되어 있다.

프로세스 A                    프로세스 B
┌──────────────┐             ┌──────────────┐
│ 메모리 공간  │             │ 메모리 공간  │
│ data = "hi"  │  격리됨      │ data = ???   │
└──────────────┘             └──────────────┘

문제: 프로세스 A"hi"를 프로세스 B가 못 받음
해결책: IPC 필요

IPC 없으면 어떻게 되나?

// 프로세스 A
class ProcessA {
    public static void main(String[] args) {
        String message = "안녕하세요";
        // 프로세스 B에 이 메시지를 보낼 방법이 없다.
    }
}

// 프로세스 B
class ProcessB {
    public static void main(String[] args) {
        // 프로세스 A의 메시지를 받을 방법이 없다.
    }
}

IPC의 4가지(+ 파일) 방식

방식 0: 파일 (부수적인 추가사항)

  • 원래 목적이 통신이 아니라 데이터 저장이다.
    통신을 위해 만들어진 도구가 아니라
    저장된 데이터를 공유하는 부수적인 효과를 이용하는 것이기 때문에
    기존 IPC 메커니즘 분류에서는 빠지는 경우가 많다.
  • 가장 단순하고, 이해하기 쉽다.
  • 속도는 디스크 접근하기에 느리다.
  • 어떤 언어든 가능하다.
  • 프로세스들이 자주 실행 및 종료될 때 혹은
    간단한 일회성 데이터 전달에 사용된다.
// 프로세스 A: 파일에 메시지 쓰기
class ProcessA {
    public static void main(String[] args) throws IOException {
        String message = "프로세스 B에게 전달";
        Files.write(
            Paths.get("shared_message.txt"), 
            message.getBytes()
        );
        System.out.println("메시지 저장됨");
    }
}

// 프로세스 B: 파일에서 메시지 읽기
class ProcessB {
    public static void main(String[] args) throws IOException {
        Thread.sleep(1000);  // A가 쓸 때까지 대기
        
        String message = new String(
            Files.readAllBytes(Paths.get("shared_message.txt"))
        );
        System.out.println("받은 메시지: " + message);
    }
}

방식 1: 공유 메모리

  • 일반적으로 운영체제는 프로세스 간의 독립성을 보장하기 위해
    각 프로세스에게 독립적인 가상 메모리 공간을 할당한다.
    즉, 프로세스 A는 프로세스 B의 메모리를 절대 볼 수 없다.
    (이를 메모리 보호라고 합니다.)

  • 공유 메모리는 이 "담장"의 일부분을 허무는 기술이다.

  • 메커니즘: OS 커널이 물리 메모리(RAM)의 특정 영역을 할당한다.

  • 매핑: 프로세스 A와 프로세스 B가 각자의 가상 주소 공간을
    이 동일한 물리 메모리 영역에 연결한다.

  • 결과: 프로세스 A가 특정 주소에 값을 쓰면,
    프로세스 B는 자신의 주소 공간에서 그 값을 즉시 읽을 수 있다.

방식 2: 파이프 (Pipe) - 단방향 통신

  • 한 방향만 통신
  • 파이프 생성할 때부터 "누가 쓰고, 누가 읽을지"를 정한다.
  • 누가 무엇을 할지 명확하고, 되돌릴 방법이 없으니 실수할 여지가 적다.
  • 부모-자식 프로세스 간 통신에 최적
  • OS 레벨에서 지원
상황 2: 영화관 매표소
┌─────────────┐  표 넘김  ┌──────────┐
│ 관객        │ ────→     │ 관객     │
│ (표 받으러) │ (표 제출) │ (표 받음)│
└─────────────┘           └──────────┘

관객은 표를 넘길 수만 있음
스태프는 표를 건네줄 수만 있음

방식 3: 소켓 (Socket) - 네트워크 통신

프로세스 A                           프로세스 B
(전화 받는 사람)                     (전화 거는 사람)
    ↓                                    ↓
[전화기 대기]                       [전화 걸기]
    ↑ ← ← ← ← ← ← ← 신호 ← ← ← ← ←   ↑
    ↓                                    ↓
[통화 중 (양방향)]                  [통화 중 (양방향)]
    ↑ ↔ ↔ ↔ ↔ ↔ ↔ 음성 신호 ↔ ↔ ↔   ↑
  • 전화는 두 사람이 동시에 이야기 할 수 있다. (양방향)
  • 한 명이 먼저 걸고, 다른 한 명이 받아야 함.
  • 연결되면 자유롭게 주고받을 수 있다.

통신 과정

1단계: 대기
프로세스 A가 "5000번 포트"에서 기다림

2단계: 연결 신청
프로세스 B가 "프로세스 A에 연결해달라"고 요청

3단계: 연결 수락
프로세스 A가 "좋아, 연결됨"

4단계: 자유로운 대화
서로 원하는 대로 주고받음

5단계: 연결 끊기
누가 끊으면 끝

방식 4: 메시지 큐 (Message Queue) - 비동기 통신

프로세스 A          메시지 큐          프로세스 B
(메시지 발송)      (중간 저장소)       (메시지 처리)

메시지 넣음 →      [msg1]
메시지 넣음 →      [msg2]  →  메시지 꺼냄
메시지 넣음 →      [msg3]  →  메시지 꺼냄
(떠남)            [msg4]  →  (나중에) 메시지 꺼냄

왜 "비동기"인가?

동기 (소켓 - 전화)
A: "안녕?" 
B: (전화 받을 때까지 기다림)
B: "응, 뭐해?"
A: (대답할 때까지 기다림)
→ 둘이 동시에 움직여야 함

비동기 (메시지 큐 - 우편함)
A: 편지 쓰고 우편함에 넣음
A: (기다리지 않고 다른 일 함)
B: (나중에 와서 편지 꺼냄)
→ 시간차 있어도 괜찮음

메시지 큐가 왜 필요한가?

상황 1: 소켓(전화)의 문제점

  • 고객이 상담사에게 전화
  • 고객: 안녕하세요.
  • 상담사: 응답없음 (퇴근함)
  • 고객: 기다림
  • 결과: 전화 끊김, 고객 불만

상황 2: 메시지 큐의 해결책

  • 고객이 상담 요청을 메시지 큐에 넣음
  • 고객: "상담 요청" -> 큐에 저장
  • 고객: 기다리지 않고 나감 -> 시간 경과
  • 상담사: 출근해서 큐에서 읽는다 -> "상담 요청"
  • 상담사: 처리한다.
  • 결과적으로 고객은 기다릴 필요가 없고,
    상담사가 없어도 요청하고자 한 메시지는 안전하게 보관된다.
    여러 고객의 요청도 한 번에 처리 가능하다.

장점과 단점

장점

  • 발신자가 기다릴 필요없이 시간차를 두고, 처리가 가능하다.
  • 메시지가 손실되지않고, 시스템이 부하 견디기가 쉽다.
  • 처리자를 늘리면 더 빨리 처리 가능하고, 부하 분산이 가능하다.

단점

  • 실시간성이 부족하다. (즉시 처리 되지 않음, 약간의 지연 발생)
  • 큐가 커질수록 메모리 증가 하기에 관리가 필요하다.
profile
개발을 즐겨보자.

0개의 댓글