사전 실행
개념

실행

gabury1고친 사람 github-actions[bot]

실행은 적어 둔 일을 컴퓨터가 실제로 해내는 일입니다. 코드는 적어 두기만 해서는 아무 일도 일어나지 않습니다. 실행해야 값이 계산되고 데이터가 바뀝니다. 실행 전에는 적힌 것을 읽고 뜻을 알아내는 단계가 섭니다.

쉽고 빠른 이해

실행은 적어 둔 일을 실제로 해내는 일입니다. 데이터베이스에 질의를 보내면 표에서 행을 골라 돌려줍니다. 행을 골라 내는 그 일이 실행입니다.

실행하지 않으면 적어 둔 코드는 아무 일도 하지 않습니다. 실행해야 값이 계산되고 데이터가 바뀝니다.

적는 일과 해내는 일을 갈라 두는 데는 까닭이 있습니다. 해내기 전에 적힌 것을 먼저 읽어 보면 틀린 지시를 걸러낼 수 있습니다. 데이터를 바꾸기 시작한 뒤에 틀린 곳을 찾으면 되돌리기 어렵습니다.

어떻게 도나:

  1. 적힌 것을 읽어 옵니다
  2. 무슨 일인지 알아내고 문제가 없는지 봅니다
  3. 알아낸 대로 해냅니다. 이 단계가 실행입니다

대가도 있습니다. 먼저 읽어 보는 것만으로는 걸러지지 않는 문제가 있습니다. 0 으로 나누기가 그렇습니다. 그리고 실행한 일은 결과를 남깁니다. 데이터를 바꾸는 지시라면 실행하는 순간 데이터가 바뀝니다.

상세

식당 주방에 주문서가 들어옵니다. 주방장은 주문서를 먼저 읽고 무슨 요리인지, 재료가 있는지 봅니다. 그다음에야 불을 켜고 요리를 합니다. 주문서에 적힌 것을 접시 위의 요리로 만들어 내는 일이 실행에 해당합니다.

실행은 적어 둔 동작을 실제로 해내는 일입니다. 예를 들어 total = price * count 라고 적은 코드는 실행해야 곱셈이 일어나고 total 에 값이 담깁니다. 해낸다는 것은 값을 계산하고, 데이터를 읽거나 쓰고, 다음에 할 일을 정하는 것입니다.

적어 둔 것은 프로그램 하나일 수도 있습니다. 데이터베이스에 보내는 질의 하나일 수도 있습니다. 계산 장치가 처리하는 명령어 하나일 수도 있습니다.

실행 앞에는 적힌 것을 알아내는 단계가 섭니다. 이 편에서는 그 단계들을 묶어 「앞 단계」라고 부르겠습니다. 앞 단계는 적힌 것을 읽어 와서 무슨 일인지 알아냅니다. 문제가 없는지 보고, 곧바로 해낼 수 있게 갖춰 두기도 합니다.

flowchart TD
    A["적어 둔 것<br/>프로그램 · 질의 · 명령어"] --> B["앞 단계<br/>읽어 와서 무슨 일인지 알아낸다"]
    B --> C{"문제가 있나"}
    C -->|있다| D["실행 없이 끝난다<br/>오류를 알린다"]
    C -->|없다| E["실행<br/>계산하고 읽고 쓴다"]
    E --> F["결과"]

그림에서 실행은 앞 단계가 통과시킨 것만 받습니다. 앞 단계에서 걸리면 실행은 대개 시작되지 않고 오류만 남습니다.

실행이 한 번에 해내는 단위는 맥락마다 크기가 다릅니다. 그래도 뜻은 하나입니다. 크기만 다릅니다. 앞 단계를 부르는 이름도 맥락마다 다릅니다. 그 이름은 아래 「갈래」 절에서 하나씩 봅니다.

「실행 파일」·「실행 중」 같은 말도 같은 뜻에 낱말이 붙은 것입니다. 실행 파일은 곧바로 실행할 수 있게 갖춰 둔 파일입니다. 실행 중은 해내는 도중을 가리킵니다.

배경

코드를 적는 때와 그 코드가 일을 해내는 때는 다릅니다. 개발자는 코드를 한 번 적습니다. 그 코드는 나중에 여러 번, 매번 다른 입력을 받아 돕니다. 적힌 글만 보고는 어느 입력에서 무엇이 일어날지 다 알 수 없습니다.

그래서 문제가 두 가지로 갈립니다. 문법이 틀렸거나 없는 이름을 부른 것은 적힌 것만 읽어도 알 수 있어서 해내기 전에 걸러낼 수 있습니다. 0 으로 나누기나 없는 파일 열기는 값을 넣어 돌려 봐야 드러납니다. 데이터를 바꾸는 지시라면 미리 걸러야 이미 바뀐 데이터를 되돌릴 일이 생기지 않습니다.

그래서 해내는 쪽에 따로 이름을 붙였습니다. 해내는 일이 실행입니다. 실행하는 때는 실행 시점 또는 런타임이라고 부릅니다.

실행 도중에야 드러나는 오류는 런타임 오류라고 합니다. 반대편에는 코드를 실행하지 않고 읽기만 해서 문제를 찾는 정적 분석이 있습니다.

예시

실행이 시작됐는지가 결과에 드러나는 사례 둘을 봅니다. 하나는 웹 서버가 쓰는 질의 언어입니다. 다른 하나는 데이터베이스입니다.

GraphQL 응답의 data 와 errors

GraphQL 은 클라이언트가 원하는 데이터의 모양을 적어 보내면 서버가 그 모양대로 채워 돌려주는 질의 언어입니다. 요청에 적는 데이터 항목 하나하나를 필드라고 합니다.

서버는 요청을 받으면 문법을 검사하는 파싱과 내용을 검사하는 검증을 거칩니다. 검증은 보통 실행 바로 앞에 둡니다. 모든 검증 규칙을 통과한 요청만 실행하게 되어 있습니다. 전에 검증해 둔 요청이 그대로라면 다시 검증하지 않고 실행해도 됩니다.

아래는 영웅 한 명의 이름과 그 친구들의 id·name 을 묻는 요청입니다. $episode 에는 요청과 함께 보낸 값이 채워집니다.

graphql
{
  hero(episode: $episode) {
    name
    heroFriends: friends {
      id
      name
    }
  }
}

id 가 1002 인 친구의 이름을 가져오다 실패하면 응답은 이렇게 올 수 있습니다.

JSON
{
  "errors": [
    {
      "message": "Name for character with ID 1002 could not be fetched.",
      "locations": [{ "line": 6, "column": 7 }],
      "path": ["hero", "heroFriends", 1, "name"]
    }
  ],
  "data": {
    "hero": {
      "name": "R2-D2",
      "heroFriends": [
        {
          "id": "1000",
          "name": "Luke Skywalker"
        },
        {
          "id": "1002",
          "name": null
        },
        {
          "id": "1003",
          "name": "Leia Organa"
        }
      ]
    }
  }
}

data 가 있다는 것은 이 요청이 실행까지 갔다는 뜻입니다. 실행 도중 필드 하나에서 오류가 나도 실행은 이어 가려 합니다. 실패한 name 하나만 null 로 채우고 나머지는 돌려줍니다.

무엇이 잘못됐는지는 errors 에 따로 담깁니다. path 는 오류가 난 필드까지 내려가는 길입니다. 1 은 친구 목록의 둘째 항목입니다. 0 부터 셉니다. 클라이언트는 이 길을 보고 null 이 원래 비어 있던 값인지 오류로 생긴 값인지 가립니다. locations 는 요청 글의 6번째 줄 7번째 칸, 곧 친구의 name 을 가리킵니다.

서버에는 내줄 수 있는 데이터의 종류와 모양을 적은 정의가 있습니다. 이것을 스키마라고 합니다. 스키마에서 name 을 null 이 될 수 없는 필드로 정했다면 응답이 달라집니다. name 에 null 을 담을 수 없으니 null 이 그 위로 올라가 둘째 친구 항목 전체가 null 이 됩니다. errors 의 내용은 같습니다.

파싱이나 검증에서 걸린 요청은 사정이 다릅니다. 이런 요청은 실행이 시작되지 않습니다. 그래서 응답에 data 키가 없습니다. errors 만 옵니다. data 가 있느냐 없느냐로 실행이 시작됐는지를 가릴 수 있습니다.

PostgreSQL EXPLAIN 과 EXPLAIN ANALYZE

PostgreSQL 은 SQL(Structured Query Language) 질의를 받아 처리하는 관계형 데이터베이스입니다. 질의 앞에 EXPLAIN 을 붙이면 그 질의를 어떻게 처리할지 세운 계획을 보여 줍니다. 이 계획을 실행 계획이라고 합니다.

아래는 정수 열 하나에 행이 10000개인 표 foo 를 읽는 질의의 계획입니다.

EXPLAIN SELECT * FROM foo;

                       QUERY PLAN
---------------------------------------------------------
 Seq Scan on foo  (cost=0.00..155.00 rows=10000 width=4)
(1 row)

Seq Scan 은 표를 처음부터 차례로 읽겠다는 계획입니다. rows=10000 은 읽어 낼 것으로 내다본 행 수입니다. 이 출력에 있는 숫자는 전부 계획을 세우며 어림한 값입니다.

ANALYZE 를 붙이면 계획만 세우지 않고 질의를 실행합니다. 그리고 실행하며 잰 값을 계획 옆에 붙여 보여 줍니다. 아래는 다른 질의의 예입니다. PREPARE 로 이름을 붙여 둔 질의 query 에 값 100 과 200 을 넣어 EXECUTE 로 돌렸습니다.

PREPARE query(int, int) AS SELECT sum(bar) FROM test
    WHERE id > $1 AND id < $2
    GROUP BY foo;

EXPLAIN ANALYZE EXECUTE query(100, 200);

                                                       QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------
 HashAggregate  (cost=10.77..10.87 rows=10 width=12) (actual time=0.043..0.044 rows=10.00 loops=1)
   Group Key: foo
   Batches: 1  Memory Usage: 24kB
   Buffers: shared hit=4
   ->  Index Scan using test_pkey on test  (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99.00 loops=1)
         Index Cond: ((id > 100) AND (id < 200))
         Index Searches: 1
         Buffers: shared hit=4
 Planning Time: 0.244 ms
 Execution Time: 0.073 ms
(10 rows)

괄호가 둘로 늘었습니다. 앞 괄호는 어림한 값입니다. actual 로 시작하는 뒤 괄호가 실행하며 잰 값입니다.

색인으로 읽는 줄은 rows=99 로 어림했습니다. 재 보니 99행이 나왔습니다. 마지막 두 줄은 계획을 세운 시간 Planning Time 과 실행한 시간 Execution Time 을 따로 적습니다.

이 숫자들은 표에 든 데이터에 따라 달라집니다. PostgreSQL 판이 바뀌면 숫자와 고른 처리 방법까지 달라질 수 있습니다.

갈래

갈래를 가르는 기준은 실행이 한 번에 해내는 단위의 크기입니다. 작은 쪽부터 명령어 하나, 질의 하나, 프로그램 하나입니다. 단위마다 앞 단계의 이름과 실행이 하는 일이 다릅니다.

명령어 하나

CPU(Central Processing Unit, 중앙처리장치)는 명령어 하나를 몇 단계로 나눠 처리합니다. Cortex-M3(Arm 이 만든 프로세서의 제품 이름)는 단계가 셋입니다. 인출(Fetch)은 메모리에서 명령어를 읽어 옵니다. 해독(Decode)은 읽어 온 명령어가 무슨 일인지 알아냅니다. 마지막이 실행(Execute) 단계입니다.

실행 단계는 해독이 알아낸 일을 합니다. 덧셈 같은 산술과 AND 같은 논리 계산은 ALU(Arithmetic Logic Unit, 산술 논리 장치)라는 회로가 맡습니다. 곱셈·나눗셈, 메모리와 데이터를 주고받는 일, 분기할지 정하는 일도 이 단계에 들어 있습니다.

단계를 나눠 두면 한 명령어가 실행되는 동안 다음 명령어를 해독하고 그다음 명령어를 인출할 수 있습니다. 이렇게 겹쳐 처리하는 방식을 파이프라인이라고 합니다. 미리 꺼내 온 명령어가 꼭 실행되는 것은 아닙니다. 앞 명령어가 분기여서 명령어 흐름이 내다본 것과 달라지면 미리 꺼내 둔 명령어는 쓰이지 않습니다. 그동안 실행 단계는 할 일 없이 빕니다.

질의 하나

PostgreSQL 에서 질의는 파서 → 재작성 → 플래너 → 실행기 순서로 지납니다. 파서는 문법을 검사하고 질의를 트리 꼴로 바꿉니다. 재작성은 그 트리에 적용할 규칙이 있으면 규칙이 적은 대로 트리를 바꿉니다. 플래너는 같은 결과로 가는 길을 여럿 세워 비용을 어림합니다. 그중 가장 싼 길로 계획을 짭니다.

실행기(executor)는 그 계획 트리를 따라 내려가며 행을 꺼냅니다. 계획의 각 노드는 불릴 때마다 행을 하나 더 내놓거나, 다 내놨다고 알립니다. 표를 읽고, 정렬하고, 조인하는 일이 이 단계에서 일어납니다. SELECT·INSERT·UPDATE·DELETE·MERGE 다섯 질의가 모두 같은 실행기를 거칩니다.

GraphQL 서버도 실행 앞에 파싱과 검증을 둡니다. 검증은 보통 실행 바로 앞에 섭니다. 읽기 요청인 쿼리는 맨 바깥에 적은 필드들을 보통 병렬로 실행해도 됩니다. 쓰기 요청인 뮤테이션은 그 필드들이 데이터를 바꾸는 부작용을 낸다고 봅니다. 그래서 적힌 순서대로 하나씩 실행해 부작용끼리 끼어드는 경쟁 상태를 막습니다.

프로그램 하나

운영체제에서 프로그램을 실행하면 프로세스가 그 프로그램을 돌리기 시작합니다. 프로세스는 실행 중인 프로그램 하나를 운영체제가 관리하는 단위입니다. 프로세스가 메모리에 올려 둔 코드와 데이터는 프로세스 이미지라고 합니다. POSIX(Portable Operating System Interface)의 exec 계열 함수는 지금 프로세스의 이미지를 실행 파일로 만든 새 이미지로 갈아 끼웁니다.

exec 가 성공하면 호출한 쪽으로 돌아오지 않습니다. 호출한 쪽의 이미지가 새 이미지로 덮였기 때문입니다. 돌아왔다면 실패한 것입니다. 이때 반환값은 -1 입니다. C 프로그램이면 새 이미지는 main(argc, argv) 함수 호출로 시작합니다.

자바 가상 머신은 클래스를 적재하고, 링크하고, 초기화한 다음에야 main 메서드를 부릅니다. 적재는 클래스의 이진 표현을 찾아 들이는 일입니다. 링크는 적재한 클래스를 초기화할 수 있는 상태로 만드는 단계입니다. 그 안의 검증은 적재한 코드가 형식에 맞는지, 자바 언어와 가상 머신의 규칙을 지키는지 봅니다. 초기화가 하는 일은 자료에 없습니다(미확인).

세 단위를 나란히 놓으면 이렇습니다.

단위 앞 단계 실행이 하는 일
명령어 하나 인출 · 해독 계산하고, 메모리와 데이터를 주고받고, 분기를 정한다
질의 하나 파싱 · 검증 · 재작성 · 계획 계획이나 요청의 모양을 따라 값을 구해 모은다
프로그램 하나 적재 · 링크 · 초기화 첫 함수부터 프로그램을 돌린다

경계

EXPLAIN 은 질의를 실행하는가. 아닙니다. EXPLAIN ANALYZE 는 실행합니다.

EXPLAIN 만 붙이면 PostgreSQL 은 계획을 세워 보여 주는 데서 멈춥니다. 질의에 적힌 동작은 해내지 않으니 실행이 아닙니다. 예시 절의 첫 출력에 어림한 값만 있고 잰 값이 없는 까닭입니다.

ANALYZE 를 붙이면 질의를 실행합니다. SELECT 가 돌려줄 행은 내보내지 않고 버립니다. 그 밖의 효과는 평소처럼 일어납니다. 그래서 INSERT·UPDATE·DELETE 에 걸면 데이터에 영향이 갑니다.

데이터를 건드리지 않고 쓰기 질의의 실행을 재려면 트랜잭션으로 감싸고 되돌립니다. 트랜잭션은 여러 작업을 한 묶음으로 확정하거나 한꺼번에 되돌리는 단위입니다.

SQL
BEGIN;
EXPLAIN ANALYZE ...;
ROLLBACK;

ROLLBACK 이 묶음을 되돌리는 명령입니다. 이때도 실행은 일어났습니다. 실행이 남긴 결과만 거둬들인 것입니다. 실행인지 가르는 기준은 명령의 이름이 아니라 적힌 동작을 해냈는가입니다.

관련 항목

프로그램을 실행하는 데 관여하는 구성 요소

프로세스 · 운영체제 · 시스템 콜 · exec · 로더 · 링커 · 클래스 로더 · 런타임 · 인터프리터 · 컴파일러 · 실행 파일

CPU 에서 실행 앞뒤에 서는 처리 단계

인출 · 해독 · 결과 쓰기 · 파이프라인 · 프리페치 · 프로그램 카운터 · 레지스터 · ALU · CPU · 메모리

질의 처리에서 실행 앞뒤에 서는 처리 단계

파싱 · 파서 · 검증 · 쿼리 플래너 · 실행 계획 · 리졸버 · 직렬화 · 스키마

실행 단계를 따로 두는 질의 언어 · 데이터베이스 · 명령

GraphQL · 쿼리 · 뮤테이션 · PostgreSQL · SQL · EXPLAIN

실행이 남기는 효과와 그것을 거둬들이는 수단

부작용 · 경쟁 상태 · 트랜잭션 · 롤백

실행 도중에야 드러나는 오류

런타임 오류 · 예외 · 트랩 · 인터럽트

실행 시점과 맞세우는 개념

컴파일 타임 · 로드 타임 · 정적 분석 · 지연 실행 · 즉시 실행

다른 이름: execution · execute · 수행