개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

기초개념 › 구조와 운영

프로세스

프로세스는 무엇이고, 프로그램을 두 번 실행하면 무엇이 따로인가

프로그램을 컴파일해 만든 Counter.class는 디스크에 저장된 파일입니다. 이 파일을 java Counter 명령으로 실행하면, 실행 중인 것이 하나 생깁니다. 이것이 프로세스입니다. 운영체제는 프로세스마다 번호(PID)를 붙이고, 그 프로세스만 쓰는 메모리를 내줍니다. 코드의 static int count는 그 메모리에 있는 변수이고, add()는 count를 3 올린 뒤 이 프로세스의 PID와 count를 출력합니다. 프로세스 안에서 코드를 실행하는 흐름은 스레드라고 부릅니다. 이 실습은 같은 Counter.class를 터미널 두 곳에서 실행하거나, 끝난 뒤 다시 실행하거나, 한 번만 실행하고 그 안에서 스레드가 차례로 add를 실행하게 하며 실제로 실행해, 프로세스가 몇 개 생기는지와 각자 본 count가 얼마인지를 봅니다. (웹 서버 같은 프로그램도 java 명령으로 실행하면 프로세스 하나입니다. static은 이 실습에서 다루지 않습니다.)

실행으로 대조 · JDK 21에서 여섯 가지 조합을 javac와 java 프로세스로 실제로 실행해 대조

이 실습에 나오는 개념

프로그램과 프로세스
프로그램은 디스크에 저장된 파일이고, 프로세스는 그 파일을 실행한 것이다. 자바에서 javac로 만든 Counter.class는 프로그램 파일이고, java Counter 명령을 쓰면 운영체제가 프로세스를 하나 만들어 그 파일을 실행한다. 파일은 1개여도 실행할 때마다 프로세스가 하나씩 생기므로, 같은 파일을 두 번 실행하면 프로세스가 둘이 된다. 프로세스는 실행하는 동안만 있고, 끝나면 사라진다. 이 프로세스는 자기만의 실행 환경을 가진다.
PID(프로세스 번호)
PID는 운영체제가 프로세스에 붙이는 식별 번호이다. 자바에서는 ProcessHandle.current().pid()로 지금 실행 중인 프로세스의 PID를 읽는다. 같은 때에 살아 있는 프로세스는 서로 PID가 다르다. 한 프로세스 안의 스레드는 같은 프로세스이므로 PID가 하나이다. 운영체제는 프로세스가 끝난 뒤 그 번호를 다시 쓸 수 있고, 다시 쓰일 때까지의 시간은 정해져 있지 않다.
프로세스의 메모리
Java 튜토리얼(The Java Tutorials)은 각 프로세스가 자기만의 메모리 공간을 가진다고 설명한다. 프로세스 안의 변수, 예를 들어 static int count는 그 프로세스의 메모리에 있으므로, 같은 프로그램을 두 번 실행해도 프로세스마다 count가 따로 있고 한쪽이 바꾼 값은 다른 쪽에서 보이지 않는다. 프로세스가 끝나면 그 메모리의 값은 더는 쓸 수 없으며, 같은 프로그램을 다시 실행해도 새 프로세스의 count는 처음 값에서 시작한다. 반대로 한 프로세스 안의 스레드들은 그 프로세스의 메모리를 같이 쓴다.
프로세스와 스레드
프로세스는 실행 중인 프로그램으로, 운영체제로부터 독립된 메모리 공간을 할당받는다. 스레드는 프로세스 안에서 실제로 작업을 수행하는 실행 흐름으로, 같은 프로세스의 스레드들은 메모리(힙, 코드, 데이터)를 공유하고 스택만 따로 가진다. 그래서 스레드 간에는 데이터를 주고받기 쉽지만 동시에 같은 데이터를 건드리는 문제가 생긴다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. 터미널에서 javac Counter.java 명령을 쓰면 javac가 Counter.java를 읽어 Counter.class 파일을 만듭니다. 이 파일은 디스크에 저장된 파일일 뿐이고, 지금 실행 중인 것은 아무것도 없습니다. 실행 중인 프로그램을 프로세스라고 부르는데, 프로세스는 이 파일을 실행해야 생깁니다.
  2. 터미널 1에서 java Counter 명령을 쓰면, 운영체제가 java(JVM) 프로세스를 하나 만들고, 그 JVM이 Counter.class를 읽어 실행합니다. 이 프로세스의 번호(PID)는 4120입니다. 운영체제는 이 프로세스에 메모리를 내주고, 우리가 쓴 count 변수는 그 메모리 안에서 0으로 시작합니다. 프로세스를 만든 것은 우리가 쓴 코드가 아니라 java 명령과 운영체제입니다.
  3. main이 add()를 부르면 add 안의 count++ 세 줄이 실행되어, 이 프로세스의 메모리에서 count가 3이 됩니다. println이 "PID 4120 count 3"을 출력합니다. 이어서 main은 Thread.sleep(2000)에서 2초 기다리므로, 이 프로세스는 아직 끝나지 않았습니다.
  4. 앞의 프로세스가 아직 실행 중일 때 터미널 2에서 java Counter 명령을 한 번 더 씁니다. 파일은 같은 Counter.class 하나인데, 운영체제는 프로세스를 하나 더 만들고 새 번호를 붙입니다. 이 프로세스의 PID는 7352입니다. 운영체제는 이 프로세스에도 메모리를 따로 내주어서, 이 프로세스의 count도 0에서 시작합니다.
  5. 이 프로세스도 add()를 실행합니다. count++ 세 줄이 올리는 것은 이 프로세스의 메모리에 있는 count이므로 0에서 3이 되고, "PID 7352 count 3"이 출력됩니다. 앞 프로세스의 count는 그대로 3입니다. 한 프로세스가 올린 값은 다른 프로세스에서 보이지 않습니다.
  6. 2초가 지나면 각 프로세스의 main이 끝나고 프로세스가 모두 끝납니다. 끝난 프로세스의 count는 더는 쓸 수 없습니다. 프로세스는 2개 만들어졌지만 Counter.class 파일은 처음 그대로 1개입니다.

실행 결과

코드

# Counter.java
public class Counter {
    // 이 프로세스의 메모리에 있는 변수
    static int count = 0;

    // count를 3 올리고, 이 프로세스의 PID와 count를 출력한다
    static void add() {
        count++;
        count++;
        count++;
        System.out.println("PID " + ProcessHandle.current().pid()
                + " count " + count);
    }

    public static void main(String[] args) throws Exception {
        add();
        // 실행이 2초 이어지게 한다
        Thread.sleep(2000);
    }
}

# 터미널
# 컴파일: Counter.class 파일이 만들어진다
javac Counter.java
# 터미널 1에서 실행한다
java Counter
# 터미널 2에서, 앞의 실행이 끝나기 전에 실행한다
java Counter

이 모형의 전제

실행 환경은 JDK 21.0.12.1(javac 21, java 21)이고 운영체제는 Windows입니다. 여섯 가지 조합을 모두, 화면의 코드와 같은 글(# Counter.java 구역과 # 터미널 구역)을 조합마다 빈 폴더에 만들고, 그 글에서 소스 파일과 명령을 읽어 실제 javac 프로그램과 java 프로그램을 프로세스로 띄워 실행한 결과와 대조했습니다. 터미널 구역의 java Counter 줄은 바로 윗줄 주석을 읽어, "끝난 뒤에"가 있으면 앞의 프로세스가 모두 끝나기를 기다린 뒤, 아니면 기다리지 않고 바로 시작했습니다. 터미널 여러 곳은 터미널을 여러 개 열지 않고 프로세스를 이어서 시작하는 것으로 대신했습니다. 대조한 것은 실제로 시작한 프로세스 수, 프로세스별로 출력된 count, 한 프로세스가 3보다 큰 count를 본 적이 있는지(메모리를 같이 쓰는지), 프로세스를 시작할 때마다 센 살아 있는 프로세스 수의 최댓값, 출력한 PID가 실행한 쪽이 본 프로세스 번호와 같은지, 함께 떠 있던 프로세스의 PID가 서로 다른지, 종료 코드와 표준 오류, 실행해도 Counter.class가 1개 그대로인지입니다. 화면에 나오는 PID 4120, 7352, 9081은 예시 숫자이고 실제 번호는 실행할 때마다 다릅니다. 끝난 뒤 다시 실행한 프로세스의 PID는 이 PC에서는 앞과 모두 달랐지만, 운영체제는 끝난 프로세스의 번호를 다시 쓸 수 있고 언제 다시 쓰는지는 정해져 있지 않다고 Java SE 21 API 문서의 ProcessHandle(pid() 설명)이 밝히고 있어, 화면은 "다시 실행한 프로세스의 번호는 항상 다르다"고 말하지 않았습니다. 웹 서버 같은 프로그램도 java 명령으로 실행하면 프로세스 하나라는 것은, 요청을 받는 작은 서버 프로그램(JDK의 com.sun.net.httpserver)을 java 명령으로 실행해 요청을 두 번 보내 확인했습니다. 두 응답의 PID는 같고 실행한 쪽이 본 프로세스 번호와 같았으며 count는 1, 2로 이어졌습니다. 이 서버는 화면에 쓰지 않았고, 톰캣 같은 실제 서버 제품은 실행하지 않았습니다. 문서는 직접 열어 확인했습니다. The Java Tutorials의 "Processes and Threads"에서 프로세스가 자기만의 실행 환경과 메모리 공간을 가진다는 것(Processes 절), 스레드가 프로세스 안에 있고 메모리를 포함한 프로세스의 자원을 함께 쓴다는 것(Threads 절), 자바 가상 머신 구현은 대부분 프로세스 하나로 실행된다는 것을 따랐습니다. 프로그램 파일과 프로세스의 구분(파일은 디스크에 있고 프로세스는 그것을 실행한 것)은 이 문서가 정의한 것이 아니라 위의 실행 결과와 일반적인 용어를 따른 설명입니다. 화면의 코드는 프로세스 안의 변수 count가 프로세스의 메모리에 있다는 것을 보이려고 static 변수로 썼고, 스레드 2개 이상이 동시에 count를 올릴 때 생기는 문제(경쟁 상태)를 피하려고 join()으로 스레드를 차례로 실행하게 했습니다. Thread.sleep(2000)은 프로세스가 곧바로 끝나지 않아 같은 때에 떠 있게 하려고 넣은 것이며, 실제 프로그램에는 필요하지 않습니다. 화면의 코드는 import가 필요 없는 모양이어서 줄인 것이 없습니다. 이 실습은 한 컴퓨터에서 운영체제가 프로세스에 메모리를 따로 내준다는 것까지만 다루었고, 프로세스끼리 값을 주고받는 방법, 프로세스를 만드는 비용, 프로세스가 죽었을 때 다른 프로세스에 미치는 영향은 다루지 않았습니다. 이것은 "세부 동작 실습: 프로세스와 스레드"에서, 프로세스를 격리해 묶어 실행하는 컨테이너는 "세부 동작 실습: 컨테이너와 가상 머신"에서 다룹니다. 문서를 열어 확인하지 못한 것: 프로세스 번호(PID)를 운영체제가 어떻게 정하는지, 프로세스가 끝나면 운영체제가 메모리를 어떻게 거두는지는 문서를 열지 않았고 화면에도 쓰지 않았습니다.

앞 실습: 입력과 인젝션 · 다음 실습: 다른 서버 부르기

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요