사전 이식성
개념

이식성

gabury1고친 사람 github-actions[bot]

이식성은 프로그램을 다른 컴퓨터나 운영체제로 옮길 때 고칠 곳을 줄여 줍니다. 맥 노트북에서 짠 서버 코드를 고치지 않고 리눅스 서버에서 돌리는 것이 그 모습입니다. 개인 데이터를 다른 서비스로 가져가는 일도 같은 낱말로 부릅니다. 이 항목은 프로그램을 옮기는 쪽을 다룹니다.

쉽고 빠른 이해

이식성은 프로그램을 다른 환경으로 옮겨도 적게 고치고 돌게 해 주는 성질입니다. 맥 노트북에서 짠 서버 코드가 리눅스 서버에서도 도는 것이 그렇습니다.

이 성질이 없으면 옮길 때마다 코드를 새로 손봐야 합니다. 운영체제나 칩 종류가 하나 늘 때마다 고칠 코드도 한 벌씩 늡니다.

  1. 여러 환경이 함께 지키는 약속에 맞춰 코드를 짭니다
  2. 환경마다 다른 부분은 한곳에 모아 가려 둡니다
  3. 옮길 때는 그 한곳만 고치거나 새 환경에서 다시 빌드합니다

대가는 환경마다 가진 고유한 기능과 속도를 일부 내준다는 것입니다. 모든 환경이 가진 것만 쓰게 되기 때문입니다.

그래서 여러 환경에 나눠 주는 라이브러리나 도구는 이 대가를 치르고 이식성을 챙깁니다. 한 종류 서버에만 올리는 서비스는 덜 챙깁니다.

상세

요리법을 친구에게 건넨다고 해 봅시다. 「소금 한 작은술, 중불에서 5분」이라고 적으면 친구는 자기 부엌에서 그 글만 보고 따라 합니다. 「우리 집 가스레인지 3단에서 파란 냄비로」라고 적으면 친구는 그 가스레인지도 파란 냄비도 없어서 거기서 멈춥니다.

프로그램도 자기가 도는 환경에 기댑니다. 이식성은 프로그램이 특정 환경에 덜 기대게 짜여 있어서, 다른 환경으로 옮길 때 고칠 곳이 적은 성질입니다.

이식성은 있다 없다로 갈리지 않고 정도로 잽니다. 옮기는 수고가 처음부터 새로 짜는 수고보다 훨씬 적으면 이식성이 높다고 말합니다. 프로그램을 다른 환경으로 옮기는 작업 자체는 포팅이라고 부릅니다.

플랫폼과 그 차이

먼저 프로그램이 기대는 환경이 무엇으로 이루어지는지 보고, 환경이 바뀌면 무엇이 달라지는지를 표로 모읍니다.

프로그램이 올라타서 도는 바탕을 플랫폼이라고 부릅니다. 보통 칩 종류와 운영체제의 짝을 말합니다. 윈도우 노트북과 리눅스 서버는 플랫폼이 다릅니다.

칩 쪽부터 봅니다. CPU(Central Processing Unit, 중앙 처리 장치)는 종류마다 알아듣는 명령이 다릅니다. 한 종류의 CPU 가 알아듣는 명령의 목록을 명령어 집합이라고 부릅니다.

컴파일러가 소스 코드를 번역해 내놓는 결과물이 기계어입니다. 기계어는 한 명령어 집합의 명령으로 적혀 있습니다. 그래서 명령어 집합이 다른 CPU 에서는 돌지 않습니다.

운영체제 쪽도 다릅니다. 운영체제는 파일을 열거나 네트워크로 데이터를 보내는 일을 프로그램 대신 해 줍니다. 프로그램이 운영체제에 이런 일을 맡기는 통로가 시스템 콜입니다. 이 통로의 이름과 모양이 운영체제마다 다릅니다.

그 밖에도 플랫폼마다 갈리는 것이 여럿입니다. 아래 표는 옮길 때 자주 걸리는 차이를 모은 것입니다.

달라지는 것 무엇이 갈리나
명령어 집합 한 CPU 용으로 만든 실행 파일이 다른 CPU 에서 안 돈다
시스템 콜 파일과 프로세스를 다루는 호출의 이름과 인자가 다르다
정수 크기 같은 이름의 정수 타입이 4바이트이기도 8바이트이기도 하다
바이트 순서 여러 바이트짜리 수를 메모리에 어느 끝부터 놓는지가 다르다
파일 경로 경로 구분자가 / 이기도 \ 이기도 하다
줄바꿈 줄 끝을 한 글자로 적기도 두 글자로 적기도 한다

표의 위 둘은 옮기자마자 드러납니다. 빌드가 안 되거나 실행 파일이 안 뜨기 때문입니다. 아래 넷은 빌드도 되고 실행도 됩니다. 특정 값이나 특정 파일을 만났을 때 뒤늦게 틀린 결과로 드러납니다.

소스 이식성과 바이너리 이식성

옮긴다고 할 때 무엇을 들고 가느냐에 따라 길이 갈립니다. 이 소절은 소스 코드를 들고 가는 길, 실행 파일을 들고 가는 길, 그 둘 사이에 중간 언어를 두는 길을 차례로 봅니다.

소스 이식성은 소스 코드를 새 플랫폼으로 가져가 그곳의 컴파일러로 다시 빌드하면 도는 성질입니다. 기계어는 플랫폼마다 새로 나옵니다. 고치지 않고 한 벌로 남는 것은 소스 코드입니다.

인터프리터 언어도 소스 코드를 들고 갑니다. 이쪽은 다시 빌드하지 않습니다. 플랫폼마다 깔린 인터프리터가 그 소스 코드를 읽어 바로 돌립니다. Python 이 이 길입니다.

바이너리 이식성은 이미 빌드한 실행 파일을 다시 빌드하지 않고 다른 컴퓨터에서 돌리는 성질입니다. 이게 되려면 두 컴퓨터의 명령어 집합이 같아야 합니다. 실행 파일이 운영체제·라이브러리와 맞물리는 약속도 같아야 합니다.

그 약속을 ABI(Application Binary Interface, 애플리케이션 바이너리 인터페이스)라고 부릅니다. 실행 파일이 라이브러리 함수나 운영체제를 부를 때 인자를 어떻게 넘기는지, 자료가 메모리에 어떤 모양으로 놓이는지를 정합니다. 소스 코드가 같아도 운영체제·라이브러리와 다른 ABI 로 빌드된 실행 파일은 그 위에서 맞물리지 않습니다.

세 번째 길은 중간 언어를 두는 것입니다. 소스 코드를 특정 CPU 의 기계어가 아니라 바이트코드로 번역합니다. 바이트코드는 실제로 없는 가상의 컴퓨터가 알아듣도록 정한 명령입니다.

플랫폼마다 그 가상의 컴퓨터를 흉내 내는 프로그램이 따로 깔립니다. 이 프로그램을 가상 머신이라고 부릅니다. 바이트코드 파일 한 벌을 들고 가면 가상 머신이 깔린 어느 플랫폼에서든 돕니다.

Java 가 이 길을 택한 대표입니다. 자바 컴파일러가 내놓는 클래스 파일은 JVM(Java Virtual Machine, 자바 가상 머신)이 깔린 곳이면 다시 빌드하지 않고 돕니다. 흔히 「한 번 쓰고 어디서나 돌린다」라고 부르는 성질입니다.

아래 그림은 소스 코드를 들고 가는 길과 바이트코드를 들고 가는 길을 나란히 놓은 것입니다. 무엇을 한 벌만 만들고 무엇을 플랫폼마다 따로 두는지가 다릅니다.

flowchart TD
    subgraph "소스 코드를 들고 간다"
        S["소스 코드 한 벌"] --> C1["리눅스에서 빌드"] --> M1["리눅스용 기계어"]
        S --> C2["윈도우에서 빌드"] --> M2["윈도우용 기계어"]
    end
    subgraph "바이트코드를 들고 간다"
        B["바이트코드 한 벌"] --> V1["리눅스의 가상 머신"]
        B --> V2["윈도우의 가상 머신"]
    end

위쪽은 플랫폼마다 빌드를 한 번씩 합니다. 결과물도 한 벌씩 생깁니다. 아래쪽은 결과물이 한 벌뿐입니다. 대신 플랫폼마다 가상 머신이 먼저 깔려 있어야 합니다.

차이를 한곳에 모으는 방법

이식성을 얻는 방법은 모양이 여럿이지만 생각은 하나입니다. 플랫폼마다 다른 부분을 한 계층에 모아 가리고, 나머지 코드는 그 계층만 보게 합니다. 그런 계층을 만드는 방법 넷을 차례로 보고 끝에 표로 모읍니다.

차이를 가리고 공통된 모양만 내보이는 계층을 추상화 계층이라고 부릅니다. 이 계층 위의 코드는 계층 아래에서 무엇이 바뀌든 모릅니다. 옮길 때는 계층 아래만 새로 맞춥니다.

첫째 방법은 여러 플랫폼이 함께 지키는 표준에 맞춰 짜는 것입니다. C 언어는 언어 규칙과 표준 라이브러리를 문서로 정해 두었습니다. 그 문서가 정한 것만 쓴 코드는 C 컴파일러가 있는 플랫폼이면 다시 빌드해 돕니다.

POSIX(Portable Operating System Interface, 이식 가능 운영체제 인터페이스)는 같은 일을 운영체제 쪽에서 합니다. 파일, 프로세스, 스레드를 다루는 호출의 이름과 동작을 정해 둡니다. 이 표준에 맞춘 프로그램은 유닉스 계열 운영체제 사이를 소스 코드 수준에서 옮겨 다닙니다.

C 로 짜서 옮기는 이 방법의 대표 사례는 운영체제 자신입니다. 운영체제는 원래 기계마다 어셈블리어로 새로 짜던 물건이었습니다. 어셈블리어는 기계어 명령을 사람이 읽는 낱말로 옮겨 적은 언어라서 명령어 집합에 묶입니다.

유닉스는 운영체제 대부분을 C 로 다시 썼습니다. 새 기계로 옮길 때는 그 기계용 C 컴파일러와 기계에 직접 닿는 적은 코드만 새로 만들면 됐습니다.

둘째 방법은 플랫폼마다 달라야 하는 코드를 한 모듈에 모으는 것입니다. 파일 경로를 다루는 코드를 한 모듈에 모아 두면 옮길 때 그 모듈만 새로 짭니다. 나머지 코드는 그 모듈이 내놓는 함수만 부릅니다.

언어의 표준 라이브러리가 이 모듈을 대신 만들어 주기도 합니다. 아래 줄은 경로 두 조각을 플랫폼에 맞는 구분자로 잇습니다.

Python
os.path.join("logs", "a.log")  # logs/a.log

리눅스에서는 위처럼 / 로 잇습니다. 윈도우에서 같은 줄은 logs\a.log 를 냅니다. 코드를 쓰는 쪽은 구분자를 몰라도 됩니다.

C 에서는 빌드할 때 플랫폼에 따라 코드 일부를 넣고 빼는 조건부 컴파일로 이 모듈을 만들기도 합니다. 플랫폼마다 다른 코드가 한 파일 안에서 갈래로 나뉩니다.

셋째 방법은 앞 소절의 가상 머신입니다. 플랫폼 차이를 가상 머신이 전부 떠안습니다. 그 위의 바이트코드는 플랫폼을 모릅니다.

넷째 방법은 프로그램이 기대는 라이브러리와 설정까지 함께 싸서 옮기는 것입니다. 컨테이너 이미지가 이 방식입니다. 이미지 안에 필요한 파일이 다 들어 있어서 옮겨 간 곳에 무엇이 깔려 있는지에 덜 기댑니다.

이 방식이 싸 가지 않는 것이 둘 있습니다. 하나는 운영체제의 핵심부인 커널입니다. 컨테이너는 옮겨 간 곳의 커널을 함께 씁니다. 그래서 리눅스용 이미지는 리눅스 커널 위에서만 돕니다.

다른 하나는 CPU 종류입니다. 이미지 안의 기계어도 한 명령어 집합용입니다. CPU 종류가 다르면 그 종류용 이미지를 따로 만들어야 합니다.

네 방법을 모으면 이렇습니다. 방법마다 플랫폼 차이를 떠안는 쪽이 다릅니다.

방법 차이를 떠안는 쪽 옮길 때 할 일
표준에 맞춰 짜기 그 표준을 구현한 컴파일러와 운영체제 새 플랫폼에서 다시 빌드
차이를 한 모듈에 모으기 그 모듈 그 모듈만 새로 짜기
가상 머신 위에서 돌리기 가상 머신 가상 머신만 깔기
함께 싸서 옮기기 이미지 커널 종류와 CPU 종류가 이미지와 맞는 곳에 올리기

이식성을 깨는 코드

차이를 가리는 계층이 있어도 코드가 그 계층을 건너뛰면 이식성이 깨집니다. 흔한 원인 넷을 차례로 보고, 그중 하나는 C 코드 한 줄로 확인합니다.

첫째는 구현 정의 동작입니다. 언어 규칙이 값을 정하지 않고 컴파일러에게 고르게 맡긴 동작을 말합니다. 컴파일러가 바뀌면 같은 코드가 다른 값을 냅니다.

C 의 long 타입이 대표입니다. long 은 최소 32비트라는 것만 정해져 있습니다. 몇 바이트로 할지는 플랫폼마다 고릅니다.

C
printf("%zu\n", sizeof(long)); // 4 또는 8

이 줄은 어떤 플랫폼에서는 4를, 어떤 플랫폼에서는 8을 찍습니다. long 을 8바이트로 가정하고 파일에 수를 써 두면 4바이트인 플랫폼에서 그 파일을 잘못 읽습니다.

Java 는 반대쪽을 골랐습니다. int 는 어디서나 32비트, long 은 어디서나 64비트로 언어가 못 박습니다. 크기가 플랫폼을 따라가지 않으니 이런 어긋남이 없습니다.

둘째는 정의되지 않은 동작입니다. 언어 규칙이 결과를 아예 정하지 않은 동작입니다. 배열 끝을 넘어 읽는 것이 그 예입니다. 한 플랫폼에서 우연히 맞게 돌던 코드가 다른 컴파일러에서는 다르게 돕니다.

셋째는 바이트 순서입니다. 여러 바이트짜리 수를 메모리에 어느 끝부터 놓느냐를 엔디언이라고 부릅니다. 수 0x01020304 를 어떤 CPU 는 01 02 03 04 순서로 놓고, 어떤 CPU 는 04 03 02 01 순서로 놓습니다.

메모리의 바이트를 손대지 않고 파일이나 네트워크로 내보내면 문제가 생깁니다. 받는 쪽 CPU 의 순서가 다르면 다른 수로 읽습니다. 그래서 밖으로 내보내는 수는 순서를 한쪽으로 정해 두고 씁니다.

넷째는 한 플랫폼에만 있는 호출을 직접 부르는 것입니다. 리눅스에만 있는 epoll 은 네트워크 연결 여럿을 한 번에 지켜보는 시스템 콜입니다. 이것을 부른 코드는 맥OS 에서 빌드부터 안 됩니다.

이식성의 대가와 쓰는 때

이식성을 높이는 일에는 값이 붙습니다. 이 소절은 무엇을 내주는지 보고, 그 값을 언제 치르고 언제 안 치르는지를 봅니다.

첫째로 한 플랫폼만의 기능을 못 씁니다. 모든 플랫폼이 가진 기능만 쓰면 리눅스의 epoll, 맥OS 의 kqueue 처럼 운영체제마다 따로 만든 입출력 방법을 놓칩니다. 모두가 가진 만큼에 맞춘다고 해서 이런 상태를 최소 공통 분모라고 부릅니다.

둘째로 차이를 가리는 계층을 한 번 더 거칩니다. 가상 머신이 바이트코드를 읽어 돌리는 데는 시간이 듭니다. 계층이 많을수록 코드가 기계에 닿기까지 거치는 단계가 늡니다.

셋째로 확인할 조합이 늡니다. 플랫폼 셋을 지원하면 같은 시험을 세 플랫폼에서 돌려야 합니다. 한 플랫폼에서만 나는 버그도 생깁니다.

그래서 이식성은 필요한 만큼만 들입니다. 리눅스 서버 한 종류에만 올리는 사내 서비스는 다른 운영체제로 옮길 일이 적습니다. 그만큼 이식성에 들일 수고도 적습니다.

반대로 여러 운영체제에 나눠 주는 라이브러리나 개발 도구는 이식성이 곧 쓸 수 있는 사람의 범위입니다. 하드웨어가 여러 번 바뀌는 동안 계속 써야 하는 프로그램도 그렇습니다. 옮길 때마다 새로 짤 수는 없기 때문입니다.

백엔드에서는 개발자의 맥 노트북과 운영 서버의 리눅스가 서로 다른 플랫폼입니다. 로컬에서 되던 것이 서버에서 안 되는 일은 이 차이에서 오기도 합니다.

칩도 다를 수 있습니다. Arm 과 x86 은 명령어 집합이 서로 다른 CPU 계열입니다. Arm 칩을 단 맥에서 빌드한 컨테이너 이미지가 x86 칩을 단 리눅스 서버에서 안 뜨는 일이 그 예입니다. 앞에서 본 「CPU 종류가 다르면 이미지를 따로 만든다」가 이것입니다.

헷갈리는 이웃 낱말

이식성과 자주 같이 나오는 낱말이 둘 있습니다. 셋은 무엇이 움직이느냐로 갈립니다.

낱말 묻는 것
이식성 한 프로그램을 다른 플랫폼으로 옮겨도 도나
상호운용성 따로 만든 프로그램끼리 함께 일하나
호환성 밑에 깔린 쪽을 새것으로 바꿔 끼워도 그 위의 것이 도나

이식성에서는 프로그램이 움직입니다. 상호운용성에서는 둘 이상의 프로그램이 서로 마주 봅니다. 호환성에서는 프로그램은 가만히 있고 그 밑이 바뀝니다.

데이터 이식성과 클라우드 이식성

같은 낱말이 다른 분야에서도 쓰입니다. 뜻의 줄기는 같고 움직이는 것이 다릅니다.

개인정보 쪽에서는 사용자가 한 서비스에 쌓은 자기 데이터를 받아서 다른 서비스로 가져가는 일을 데이터 이식성이라고 부릅니다. 여기서 움직이는 것은 프로그램이 아니라 데이터입니다. 옮기는 사람도 개발자가 아니라 사용자입니다.

클라우드 쪽에서는 한 클라우드 업체에서 돌던 서비스를 다른 업체로 옮기는 일을 이식성이라고 부릅니다. 한 업체만의 기능에 깊이 기대 옮기기 어려워진 상태가 그 반대편입니다. 이 상태를 벤더 종속이라고 부릅니다.

관련 항목

이식성이 가려야 하는 플랫폼 차이

플랫폼 · CPU · ARM · x86 · 명령어 집합 · 기계어 · 운영체제 · 커널 · 시스템 콜 · ABI · 엔디언 · 워드 크기 · 줄바꿈 문자 · 파일 경로

이식성을 얻는 수단

추상화 · 표준 · 표준 라이브러리 · 조건부 컴파일 · 가상 머신 · 바이트코드 · 인터프리터 · 컨테이너 이미지 · 하드웨어 추상화 계층 · 크로스 플랫폼

이식성을 설계 목표로 삼은 언어와 시스템

C · C++ · POSIX · 유닉스 · Java · JVM · Python · WebAssembly

이식성을 깨는 동작과 코드

구현 정의 동작 · 정의되지 않은 동작 · 미지정 동작 · 플랫폼 전용 API · 인라인 어셈블리 · 어셈블리어

플랫폼을 옮길 때 거치는 작업

포팅 · 컴파일러 · 크로스 컴파일 · 빌드 · 링커 · 정적 링크 · 동적 링크

이식성을 높이며 치르는 대가

최소 공통 분모 · 성능 · 오버헤드 · 플랫폼 종속 · 벤더 종속

이식성과 헷갈리는 이웃 성질

상호운용성 · 호환성 · 하위 호환 · 데이터 이식성 · 유지보수성 · 품질 속성

다른 이름: portability · 소프트웨어 이식성 · 이식 가능성 · 포터빌리티