728x90

소프트웨어 개발에서 라이브러리는 코드의 재사용성을 높이고, 개발 시간을 단축하는 데 중요한 역할을 함

라이브러리는 크게 정적 라이브러리와 동적 라이브러리로 구분됨

이번 글에서는 정적 라이브러리와 동적 라이브러리의 개념과 차이점, 그리고 라이브러리 생성 및 링크 과정에 대해 정리함

정적 라이브러리(Static Library)란?

정적 라이브러리는 링크 타임에 프로그램에 결합되며, 코드가 실행 파일에 포함되는 라이브러리

 

특징

  • 확장자: .a (Unix 계열), .lib (Windows)
  • 링크 시점: 컴파일 후 링크 타임에 실행 파일에 결합됨
  • 파일 크기: 모든 필요한 라이브러리 코드가 포함되어 실행 파일 크기가 커질 수 있음
  • 독립성: 실행 파일에 라이브러리의 모든 코드가 포함되므로, 실행 시 별도의 라이브러리가 필요하지 않음
  • 업데이트: 라이브러리를 업데이트하려면 실행 파일을 다시 컴파일하고 링크해야 함

로드 방식

  • 링크 타임에 포함: 정적 라이브러리는 프로그램이 링크될 때 실행 파일에 포함됨
    이는 라이브러리의 코드가 실행 파일에 직접 결합되어 독립적인 실행 파일이 생성됨을 의미함
    결과적으로, 실행 시 별도의 라이브러리 파일이 필요하지 않음

동적 라이브러리(Dynamic Library)

동적 라이브러리는 코드가 실행파일에 포함되지 않으며, 일반적으로 실행 시간에 로드되는 라이브러리

 

특징

  • 확장자: .so (Unix 계열), .dll (Windows)
  • 링크 시점: 실행 시간에 라이브러리가 로드됨
  • 파일 크기: 라이브러리 코드가 포함되지 않기 때문에 실행 파일 크기가 정적라이브러리에 비해 작음
  • 공유성: 여러 프로그램이 하나의 라이브러리를 공유하여 메모리 사용량을 줄일 수 있음
  • 업데이트: 라이브러리를 업데이트해도 실행 파일을 다시 컴파일할 필요가 없음

로드 방식

  • 런타임 로드: 일반적으로 프로그램이 실행되는 동안 동적 라이브러리가 로드됨
    이 방식은 라이브러리 파일이 실행 파일과 별도로 유지되며, 필요할 때마다 로드
  • 프리로드: 일부 시스템에서는 프로그램이 실행될 때 특정 동적 라이브러리를 미리 로드할 수 있음
    이를 통해 실행 시간 동안 필요한 라이브러리가 즉시 사용 가능함
    환경 변수(LD_PRELOAD 등)를 사용하여 특정 라이브러리를 미리 로드할 수 있음

라이브러리 생성 과정

라이브러리를 생성하는 과정은 여러 객체 파일을 하나의 라이브러리 파일로 묶는 것을 포함
정적 라이브러리와 동적 라이브러리 생성 과정은 다소 다름

정적 라이브러리 생성

  1. 소스 코드 컴파일
    • 소스 코드를 컴파일하여 객체 파일을 생성함
    • gcc -c libmath.c -o libmath.o
  2. 정적 라이브러리 생성
    • 여러 객체 파일을 하나의 정적 라이브러리 파일로 묶음
    • ar rcs libmath.a libmath.o

동적 라이브러리 생성

  1. 소스 코드 컴파일
    • 동적 라이브러리를 생성할 때는 위치 독립 코드(Position Independent Code)를 생성해야 함
    • gcc -fPIC -c libmath.c -o libmath.o
  2. 동적 라이브러리 생성
    • 객체 파일을 동적 라이브러리로 묶음
    • gcc -shared -o libmath.so libmath.o

라이브러리 링크 과정

생성된 라이브러리를 사용하여 실행 파일을 만드는 과정은 링크 단계에서 이루어짐

 

정적 라이브러리 링크

정적 라이브러리를 포함하여 실행 파일을 생성함
정적 라이브러리의 모든 코드가 실행 파일에 포함

  • gcc -o myprogram myprogram.c -L. -lmath

동적 라이브러리 링크

동적 라이브러리를 포함하여 실행 파일을 생성함
실행 파일에는 동적 라이브러리를 참조하는 코드만 포함되고, 라이브러리는 실행 시 로드됨

  • gcc -o myprogram myprogram.c -L. -lmath

동적 라이브러리 로드 설정

동적 라이브러리를 실행 파일이 실행되는 동안 찾을 수 있도록 환경 변수를 설정

  • export LD_LIBRARY_PATH=.

결론

정적 라이브러리와 동적 라이브러리는 각각의 장단점을 가지고 있으며, 특정 상황에 따라 적절히 선택하여 사용해야 함

라이브러리 생성과 링크는 소프트웨어 개발의 서로 다른 단계이지만, 실행 파일을 만들기 위해 모두 필요함

생성된 라이브러리를 링크 과정에서 사용하여 최종 실행 파일을 만드는 것이 이 두 과정의 주요 역할임

개발 환경과 배포 방식에 따라 적합한 라이브러리 방식을 선택하여 효율적인 소프트웨어 개발을 할 수 있음

728x90

'Computer Science' 카테고리의 다른 글

메모리 단편화  (0) 2024.08.14
운영체제의 역할과 필요성  (0) 2024.08.08
링커의 심벌해석 대상  (3) 2024.07.24
지역 심벌과 전역 심벌  (0) 2024.07.24
링커란?  (0) 2024.07.17
728x90

링커의 심벌 해석 대상은 주로 전역 심벌과 외부 심벌임. 링커는 여러 개의 객체 파일을 결합하여 실행 가능한 프로그램을 생성하는 과정에서 심벌 해석을 수행함

이 과정에서 링커는 각 객체 파일에 포함된 심벌을 분석하고 올바른 메모리 주소를 할당하여 참조를 해결함

링커의 심벌 해석 대상

  1. 전역 심벌(Global Symbols)
    • 정의된 전역 변수와 함수: 각 객체 파일에서 정의된 전역 변수와 함수는 링커에 의해 심벌 테이블에 추가되고, 프로그램 전체에서 참조될 수 있도록 메모리 주소가 할당됨
    • 예시: int g_a = 1; 와 같은 전역 변수나 int func_a(int x, int y); 와 같은 함수 정의
  2. 외부 심벌(External Symbols)
    • 다른 객체 파일에서 정의된 변수와 함수: 한 객체 파일에서 선언되었지만 정의되지 않은 변수나 함수는 외부 심벌로 간주되며, 링커가 다른 객체 파일에서 해당 심벌의 정의를 찾아 연결함
    • 예시: extern int g_e; 와 같은 외부 변수 선언이나 다른 객체 파일에서 정의된 함수 참조

심벌 해석 과정

링커의 심벌 해석 과정은 다음과 같은 단계를 포함함

  1. 심벌 수집(Symbol Collection)
    • 각 객체 파일의 심벌 테이블을 수집하고, 전역 심벌과 외부 심벌을 추출함
  2. 심벌 해석(Symbol Resolution)
    • 전역 심벌과 외부 심벌을 매칭하여 참조를 해결함. 예를 들어, 한 객체 파일에서 extern으로 선언된 심벌을 다른 객체 파일에서 정의된 심벌과 연결함
    • 정의되지 않은 외부 심벌이 발견되면 링커 오류가 발생함
  3. 심벌
    • 에 실제 메모리 주소를 할당함. 이를 통해 각 객체 파일에서 참조된 심벌이 올바른 메모리 위치를 가리키도록 함
  4. 재배치(Relocation)
    • 심벌 해석과 주소 할당 후, 각 객체 파일의 코드와 데이터를 재배치하여 올바른 메모리 위치로 이동함

예시 코드에서의 링커 심벌 해석 대상

int g_a = 1;               // 전역 변수
extern int g_e;           // 외부 변수
int func_a(int x, int y); // 함수 참조

// 함수 구현
int func_b()
{
    int m = g_a + 2;
    return func_a(m + g_e);
}

 

이 예제 코드에서 링커의 심벌 해석 대상은 다음과 같음

  • 전역 변수 g_a: 객체 파일에서 정의된 전역 변수로, 링커는 이 변수를 참조하는 모든 곳에 대해 올바른 메모리 주소를 할당함
  • 외부 변수 g_e: 다른 객체 파일에서 정의된 변수로, 링커는 이 변수를 참조하는 모든 곳에서 정의된 실제 심벌과 연결함
  • 함수 func_a: 다른 객체 파일에서 정의된 함수로, 링커는 이 함수를 참조하는 모든 곳에서 정의된 실제 심벌과 연결함
  • 함수 func_b: 객체 파일에서 정의된 함수로, 링커는 이 함수의 코드와 데이터를 올바른 메모리 위치로 재배치함

이러한 과정을 통해 링커는 프로그램을 실행 가능한 상태로 만들며, 모든 심벌 참조가 올바르게 해결되도록 보장함

728x90

'Computer Science' 카테고리의 다른 글

운영체제의 역할과 필요성  (0) 2024.08.08
정적 라이브러리와 동적 라이브러리  (3) 2024.07.24
지역 심벌과 전역 심벌  (0) 2024.07.24
링커란?  (0) 2024.07.17
컴파일 언어와 인터프리터 언어 비교  (0) 2024.07.10
728x90

프로그래밍에서 변수와 함수는 프로그램의 여러 부분에서 데이터를 저장하고 작업을 수행하는 중요한 역할을 함

이들 변수와 함수는 심벌(symbol)로 표현되며, 그 범위(scope)에 따라 지역 심벌(Local Symbol)과 전역 심벌(Global Symbol)로 구분됨

지역 심벌(Local Symbol)

지역 심벌은 특정 블록이나 함수 내에서 정의되고 사용되는 식별자를 말함

이러한 심벌은 정의된 블록이 종료되면 더 이상 유효하지 않으며, 해당 블록 내에서만 접근할 수 있음

 

특징

  • 정의 위치: 함수나 블록 내부
  • 유효 범위: 정의된 블록 내에서만 유효
  • 메모리 할당: 스택(stack)에 할당되며, 블록이 실행될 때 할당되고 블록이 종료되면 해제됨

전역 심벌(Global Symbol)

전역 심벌은 프로그램 전체에서 접근 가능한 식별자를 말함

이들 심벌은 파일 범위 또는 프로그램 전체 범위에서 정의되며, 모든 함수에서 접근할 수 있음

 

특징

  • 정의 위치: 함수 외부, 보통 파일의 상단에 위치
  • 유효 범위: 프로그램 전체
  • 메모리 할당: 데이터 세그먼트(data segment)에 할당되며, 프로그램이 시작될 때 할당되고 종료될 때 해제됨

예시 코드로 보는 지역 심벌과 전역 심벌

int g_a = 1;               // 전역 변수
extern int g_e;           // 외부 변수
int func_a(int x, int y); // 함수 참조

// 함수 구현
int func_b()
{
    int m = g_a + 2;
    return func_a(m + g_e);
}

이 예제 코드에서 지역 심벌과 전역 심벌을 나눠서 설명하면 다음과 같음

 

전역 심벌

  • g_a
    • 정의: int g_a = 1;
    • 설명: 프로그램 전체에서 접근 가능한 전역 변수
  • g_e
    • 정의: extern int g_e;
    • 설명: 외부 파일에서 정의된 변수로, 링크 단계에서 실제 주소가 결정됨
  • func_a
    • 정의: int func_a(int x, int y);
    • 설명: 함수 프로토타입 선언으로, 실제 구현은 다른 곳에 있어야 함
  • func_b
    • 정의: int func_b() {...}
    • 설명: 프로그램 전체에서 접근 가능한 함수 정의

지역 심벌

  • m
    • 정의: int m = g_a + 2;
    • 설명: func_b 함수 내에서 정의된 지역 변수로, 이 함수 내에서만 유효함
      함수가 호출될 때 스택에 할당되고, 함수가 종료되면 해제됨

결론

지역 심벌과 전역 심벌은 각각의 범위 내에서 서로 다르게 관리되고 사용됨
지역 심벌은 함수나 블록 내에서만 유효하며 스택에 할당되고, 전역 심벌은 프로그램 전체에서 접근 가능하며 데이터 세그먼트에 할당됨
이러한 차이점을 이해함으로써 더 효과적으로 변수를 관리하고 프로그램을 작성할 수 있음

프로그래밍을 할 때, 변수와 함수의 범위를 명확히 이해하고 사용하는 것이 중요함
이를 통해 코드의 가독성과 유지보수성을 높일 수 있음

728x90

'Computer Science' 카테고리의 다른 글

정적 라이브러리와 동적 라이브러리  (3) 2024.07.24
링커의 심벌해석 대상  (3) 2024.07.24
링커란?  (0) 2024.07.17
컴파일 언어와 인터프리터 언어 비교  (0) 2024.07.10
Brute force(브루트 포스)란?  (0) 2024.07.09
728x90

728x90

'diary' 카테고리의 다른 글

이직 후 적응 잘하는 방법  (0) 2024.04.11
728x90

링커란?

링커(linker)는 컴파일러가 만들어낸 하나 이상의 목적 파일을 가져와 이를 단일 실행 프로그램으로 병합하는 프로그램

링커는 소프트웨어 개발 과정에서 필수적인 도구임

컴파일러가 소스 코드를 번역하여 오브젝트 파일을 생성하는 것만으로는 프로그램이 실행될 수 없음

링커는 심벌 해결, 재배치, 라이브러리 포함 등의 작업을 통해 여러 오브젝트 파일을 결합하여 실행 가능한 프로그램을 생성함

따라서 링커의 역할과 동작 원리를 이해하는 것은 효율적인 소프트웨어 개발을 위해 매우 중요함

컴파일 언어의 동작 과정

  1. 소스 코드 작성: 개발자가 소스 코드를 작성
  2. 컴파일 단계: 소스 코드를 오브젝트 파일로 변환함. 각 오브젝트 파일은 독립적으로 존재하며, 단독으로는 실행되지 않음
  3. 링킹 단계: 여러 오브젝트 파일을 결합하여 하나의 실행 파일을 생성

링커의 주요 역할

  1. 심벌 해결(Symbol Resolution)
    • 각 오브젝트 파일에는 함수나 변수가 정의되고 참조됨
    • 링커는 이러한 심벌들을 확인하고 올바른 메모리 주소를 할당함
    • 예를 들어 함수 A가 다른 오브젝트 파일에 정의된 함수 B를 호출할 때, 링커는 함수 B의 실제 메모리 위치를 찾아 함수 A가 올바르게 이를 참조할 수 있도록 함
  2. 재배치(Relocation)
    • 오브젝트 파일에서 코드와 데이터는 고정된 메모리 주소를 가지지 않음
    • 링커는 이들을 적절한 메모리 위치로 이동시키고, 프로그램의 각 부분이 올바르게 동작하도록 재배치함
    • 이는 메모리 주소가 충돌하지 않고 프로그램이 올바르게 실행되도록 보장함
  3. 라이브러리 포함(Library Inclusion)
    • 프로그램이 사용하는 라이브러리를 포함하여 최종 실행 파일을 만듦
    • 예를 들어, 표준 C 라이브러리나 사용자 정의 라이브러리가 프로그램에 필요할 경우, 링커는 해당 라이브러리의 오브젝트 파일들을 결합하여 실행 파일에 포함시킴

링킹 과정

  1. 오브젝트 파일 로드: 컴파일러가 생성한 오브젝트 파일들을 메모리에 로드
  2. 심벌 테이블 생성: 각 오브젝트 파일에 포함된 심벌 정보를 바탕으로 심벌 테이블을 생성
  3. 심벌 해결: 심볼 테이블을 참조하여 각 심벌의 정의와 참조를 해결
  4. 재배치 수행: 각 심볼의 메모리 주소를 계산하고, 코드와 데이터를 적절한 위치로 이동
  5. 라이브러리 결합: 필요한 라이브러리를 오브젝트 파일과 결합하여 최종 실행 파일을 생성

링커의 종류

  • ld: 유닉스 계열 운영체제에서 널리 사용되는 링커
  • gold: GNU 프로젝트에서 개발한 빠른 링커로, ld의 대안으로 사용됨
  • MSVC Linker: 마이크로소프트 비주얼 스튜디오에서 제공하는 링커로, Windows 환경에서 주로 사용됨

 

728x90

'Computer Science' 카테고리의 다른 글

링커의 심벌해석 대상  (3) 2024.07.24
지역 심벌과 전역 심벌  (0) 2024.07.24
컴파일 언어와 인터프리터 언어 비교  (0) 2024.07.10
Brute force(브루트 포스)란?  (0) 2024.07.09
스택(stack)과 힙(Heap)  (0) 2024.03.27
728x90

인터프리터와 컴파일러는 프로그램 소스를 인간이 이해할 수 있는 형태에서 컴퓨터가 실행할 수 있는 형태로 변환하기 위해 필요함

이들은 코드의 정확성을 검증하고, 최적화된 실행을 가능하게 하여 소프트웨어 개발과 실행 효율성을 높임

컴파일러와 인터프리터 언어의 등장 배경

컴퓨터 언어는 컴퓨터 하드웨어가 이해하고 실행할 수 있는 명령어를 작성하기 위해 등장했음

초기 컴퓨터는 기계어(machine language)로 프로그램을 작성했는데, 이는 매우 복잡하고 오류가 발생하기 쉬웠음

이를 개선하기 위해 어셈블리어(assembly language)가 등장했으며, 이는 기계어에 대한 인간의 이해를 돕기 위해 기호(symbols)와 명령어(opcodes)를 사용했음

 

1950년대에 들어서면서 고급 언어(high-level language)가 개발되기 시작했고, 컴파일러(compiler)를 사용하여 이러한 고급 언어를 기계어로 변환함으로써 프로그래밍의 효율성과 생산성이 크게 향상되었음

컴파일러는 소스 코드를 한 번에 모두 번역하여 실행 파일을 생성함으로써 실행 속도가 빠르다는 장점이 있음

 

한편, 1960년대에는 인터프리터(interpreter) 언어가 등장함

인터프리터 언어는 소스 코드를 한 줄씩 읽고 즉시 실행하는 방식으로 동작함

이러한 방식은 디버깅과 테스트 과정에서 유용하며, 프로그램의 즉각적인 피드백을 제공함

그러나 실행 속도 면에서는 컴파일러 언어에 비해 느릴 수 있음

 

결국, 컴파일러와 인터프리터 언어는 각기 다른 용도와 장점을 가지고 발전해왔으며, 현대에는 많은 언어들이 컴파일러와 인터프리터 방식을 혼합하여 사용하고 있음

예를 들어, Java는 바이트코드(bytecode)로 컴파일된 후, JVM(Java Virtual Machine)에서 인터프리터 방식으로 실행됨으로써 두 방식의 장점을 모두 활용함

특징 비교

특징 컴파일 언어 인터프리터 언어
실행 방식 전체 소스 코드를 한 번에 번역하여 실행 파일 생성 한 줄씩 번역하여 실행
실행 주체 운영체제와 CPU 파이썬 가상 기계(Python Virtual Machine, PVM)
속도 상대적으로 빠름 상대적으로 느림
에러 발견 모든 에러가 컴파일 과정에서 한 번에 발견됨 모든 에러를 한 번에 찾을 수는 없음
한 줄씩 실행시킬 수 있어 에러를 발견한 즉시 수정 및 테스트 가능
배포 기계어 실행 파일 배포 소스 코드 그대로 배포
예시 C, C++, Go 파이썬, 자바스크립트

동작 순서

컴파일러 (Go)

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}
  1. 소스 코드 작성
  2. 어휘 분석
    컴파일러는 소스 코드를 읽고, 이를 작은 단위(토큰)로 나눔
    예를 들어, package, main, import, "fmt", func, main, (), {, fmt.Println, ("Hello, World!"), } 등이 토큰이 됨.
  3. 구문 분석
    토큰들을 문법적으로 분석하여 프로그램 구조를 이해함
    예를 들어, func main() { ... }는 함수 정의라는 것을 이해함.
  4. 중간 코드 생성
    소스 코드를 중간 코드 형태로 변환함
    이 중간 코드는 최적화를 위해 사용됨
  5. 컴파일 에러 처리
    어휘 분석, 구문 분석, 중간 코드 생성 단계에서 발생하는 오류는 오류 메시지를 출력하고 컴파일을 중단함
    예를 들어, 문법 오류, 타입 오류, 선언되지 않은 변수 사용 등이 있음
    문법 오류 발생 시 "./main.go:3:2: undefined: fmt"와 같은 메시지를 출력함
  6. 최적화
    중간 코드를 최적화하여 실행 속도를 높임
    불필요한 부분을 제거하거나 더 효율적인 방식으로 변환함
  7. 기계어 생성
    최적화된 중간 코드를 기계어로 변환함
    기계어는 컴퓨터가 직접 이해하고 실행할 수 있는 코드임
  8. 실행 파일 생성
    최종적으로 기계어로 변환된 코드를 실행 파일로 만듦
  9. 실행 파일 실행
    운영 체제가 이 파일을 로드하고, CPU가 기계어 명령을 직접 실행함
  10. 런타임 에러 처리
    프로그램 실행 중에 발생하는 오류는 운영 체제에 의해 감지되고 보고됨
    예를 들어, 메모리 접근 오류, 논리적 오류 등이 있음

인터프리터 (Python)

print("Hello, World!")
  1. 소스 코드 작성
  2. 어휘 분석
    파이썬 인터프리터는 소스 코드를 읽고, 이를 작은 단위(토큰)로 나눔
    예를 들어, print와 "Hello, World!"가 토큰이 됨
  3. 구문 분석
    토큰들을 문법적으로 분석하여 프로그램 구조를 이해함
    예를 들어, print("Hello, World!")는 함수 호출이라는 것을 이해함
  4. 바이트코드 생성
    구문 분석이 끝난 후, 소스 코드를 바이트코드라는 중간 형태로 변환함
    바이트코드는 소스 코드보다 간단하고 최적화된 형태임
    이 단계에서 파이썬 인터프리터는 .pyc 파일을 생성할 수 있음
  5. 바이트코드 실행
    파이썬 가상 기계(Python Virtual Machine, PVM)는 바이트코드를 읽고, 이를 실행함
    print("Hello, World!") 줄을 바이트코드로 변환하면, PVM은 이 명령어를 해석하고 화면에 "Hello, World!"를 출력함
  6. 에러 처리
    실행 도중 에러가 발생하면 인터프리터는 그 즉시 에러를 보고하고, 해당 줄 이후의 코드는 실행하지 않음

인터프리터 언어에서 가상 기계를 사용하는 이유

  • 이식성
    • 가상 기계는 특정 하드웨어나 운영 체제에 종속되지 않아 바이트코드가 여러 플랫폼에서 동일하게 실행될 수 있음
    • 참고 : 컴파일언어는 특정 하드웨어나 운영체제에 종속됨
      하드웨어에 종속되는 예시 → x86 아키텍처용으로 컴파일된 C 프로그램은 ARM 아키텍처의 컴퓨터에서 실행되지 않음
      운영체제에 종속되는 예시 → 리눅스용으로 컴파일된 C 프로그램은 윈도우 운영 체제에서 실행되지 않음
  • 성능 향상
    • 바이트코드는 소스 코드보다 더 간단하고 최적화된 형태임
    • 가상 기계는 바이트코드를 실행하기 위해 설계된 최적화된 환경을 제공함
    • Just-In-Time (JIT) 컴파일러와 같은 기술을 사용하여 바이트코드를 실행 중에 네이티브 기계어로 변환하고, 이를 캐싱하여 반복 실행 시 성능을 향상시킴
  • 보안 및 안정성
    • 가상 기계는 프로그램의 실행을 제어하고 격리된 환경에서 실행할 수 있어 보안성을 높임
    • 가상 기계는 메모리 관리, 예외 처리, 접근 제어 등의 기능을 제공하여 안정성을 보장함
  • 동적 기능 지원
    • 가상 기계는 동적 타이핑, 동적 코드 실행 등의 기능을 지원할 수 있음
    • 이는 인터프리터 언어의 유연성을 극대화하는 데 도움이 됨

인터프리터 언어가 컴파일 언어보다 실행속도가 느린 이유

  • 실시간 해석
    • 해석 비용
      인터프리터는 프로그램의 각 명령어를 실시간으로 해석함
      소스 코드를 읽고, 해석하고, 실행하는 과정을 반복하기 때문에 추가적인 시간이 소요됨
      반면, 컴파일러는 이 과정을 미리 수행하여 기계어로 번역된 실행 파일을 생성함
    • 반복 작업
      인터프리터는 반복적으로 코드를 해석해야 함
      예를 들어, 루프 안에 있는 코드를 여러 번 실행할 때마다 같은 코드를 반복적으로 해석하고 실행해야 함.
  • 최적화 부족
    • 최적화 수준
      컴파일러는 중간 코드 생성 및 최적화 단계를 통해 프로그램을 더 효율적으로 실행할 수 있도록 최적화함
      반면, 인터프리터는 즉시 실행을 목표로 하기 때문에 최적화 과정이 상대적으로 부족함
  • 바이트코드 및 가상 기계
    • 추가 계층
      일부 인터프리터는 소스 코드를 바이트코드로 변환한 후 이를 가상 기계에서 실행함
      이 경우, 바이트코드를 해석하고 실행하는 추가 계층이 필요하기 때문에 실행 속도가 느려짐
      예를 들어, 파이썬은 소스 코드를 바이트코드로 변환한 후 파이썬 가상 기계(PVM)에서 이를 실행함
  • 동적 타입 검사
    • 타입 검사
      인터프리터는 프로그램 실행 중에 변수의 타입을 검사해야 함
      이 동적 타입 검사는 추가적인 계산 작업을 요구하며, 실행 속도를 저하시키는 요인 중 하나임
      예를 들어, 파이썬과 같은 동적 타이핑 언어에서는 변수의 타입을 실행 중에 결정하고 검사함

 

 

 

 

 

728x90

'Computer Science' 카테고리의 다른 글

지역 심벌과 전역 심벌  (0) 2024.07.24
링커란?  (0) 2024.07.17
Brute force(브루트 포스)란?  (0) 2024.07.09
스택(stack)과 힙(Heap)  (0) 2024.03.27
명령형 프로그래밍 vs 선언형 프로그래밍  (0) 2024.03.02
728x90

Brute force(브루트 포스)란 컴퓨터 과학과 암호학에서 사용되는 용어로, 가능한 모든 경우의 수를 하나씩 대입해가며 해답을 찾는 방법을 의미

이 방법은 주로 암호 해독이나 비밀번호 크래킹 등의 분야에서 사용되며, 무차별 대입 공격이라고도 함

 

브루트 포스 공격은 특정 알고리즘이나 시스템의 취약점을 이용하지 않고 단순히 모든 가능한 조합을 시도함으로써 목표를 달성하는 방식임

쉽게 예를 들면 비밀번호를 찾기 위해 모든 가능한 문자 조합을 하나씩 시도하여 맞추는 경우가 있음

 

브루트 포스의 장점은 시스템이나 알고리즘의 구조에 대한 깊은 이해가 필요 없고, 모든 경우의 수를 시도하기 때문에 성공 확률이 높다는 것임

반면, 단점으로는 매우 많은 시간과 자원이 소모된다는 점이 있음

 

현대의 많은 시스템에서는 이러한 브루트 포스 공격을 방어하기 위해 비밀번호의 길이를 길게 하거나, 복잡한 암호화 알고리즘을 사용함

728x90

'Computer Science' 카테고리의 다른 글

지역 심벌과 전역 심벌  (0) 2024.07.24
링커란?  (0) 2024.07.17
컴파일 언어와 인터프리터 언어 비교  (0) 2024.07.10
스택(stack)과 힙(Heap)  (0) 2024.03.27
명령형 프로그래밍 vs 선언형 프로그래밍  (0) 2024.03.02
728x90

service ↔ external 네트워크

  • 외부 트래픽이 LoadBalancer, NodePort, Ingress 등을 통해 쿠버네티스 클러스터에 진입함
  • 각 노드(Node A, Node B)는 호스트 네트워크 네임스페이스를 가지며, 여기에는 호스트 네트워크 인터페이스 카드(NIC)인 eth0가 있음
  • kube-proxy는 iptables/IPVS를 이용하여 네트워크 트래픽을 관리함
  • cni0 브릿지를 통해 veth0, veth1 등의 가상 이더넷 인터페이스가 연결됨
  • 각 파드(Pod)는 자신만의 네트워크 네임스페이스를 가지며, pause 컨테이너를 통해 다른 컨테이너들과 연결됨

쿠버네티스 애플리케이션을 외부 네트워크에 노출하는 방법

Service

쿠버네티스의 서비스(Service)는 NodePort를 설정하거나, 여러 파드(Pod)에 트래픽을 분산하기 위해 여러 기술을 활용함

가장 중요한 기술은 kube-proxy를 중심으로 동작하며, iptables 또는 IPVS를 사용하여 트래픽을 적절히 분산시킴

  • NodePort : 클러스터의 각 노드에서 지정된 포트를 열어 외부 트래픽을 수신
  • LoadBalancer : 클라우드 제공업체의 로드 밸런서를 사용하여 외부 트래픽을 분산
  • ExternalName : 클러스터 외부의 서비스에 대해 DNS 이름을 매핑할 수 있음

Ingress

  • Ingress controller 설치
    • Ingress를 사용하려면 클러스터에 Ingress Controller가 필요함
    • Ingress Controller는 클러스터 내에서 Ingress 리소스를 처리하고, 외부 트래픽을 내부 서비스로 라우팅함
  • 서비스 배포
    • Ingress를 통해 접근할 서비스를 먼저 배포해야 함
    • apiVersion: v1 kind: Service metadata: name: example-service spec: selector: app: nginx ports: - protocol: TCP port: 80 targetPort: 80 type: ClusterIP
  • ingress 리소스 생성
    • Ingress 리소스를 생성하여 특정도메인 example.com으로 오는 외부 트래픽을 요청을 특정 서비스 example-service로 라우팅하는 Ingress 리소스를 생성
    • apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: example-ingress annotations: nginx.ingress.kubernetes.io/rewrite-target: / spec: rules: - host: example.com http: paths: - path: / pathType: Prefix backend: service: name: example-service port: number: 80
  • dns 설정
    • 도메인 이름과 ingress controller의 외부IP 주소를 매핑해야 함
728x90
728x90

서비스의 필요성

pod는 수요에 따라 확장 또는 축소해야 할 수 있고, 애플리케이션 충돌이나 노드 장애가 발생할 경우 다시 생성되기 때문에 매우 동적임

이러한 이벤트로 인해 pod의 IP 주소가 변경되면 네트워킹이 어려워질 수 있음

쿠버네티스는 Service를 사용하여 이 문제를 해결함

  1. 서비스와 연관된 백엔드 Pod를 연결하기 위해 앞단에 정적 가상 IP 주소를 할당
  2. 서비스는 pod의 ip주소를 추적
  3. 서비스는 서비스IP로 전송된 모든 트래픽을 뒷단 pod 세트로 부하 분산
  4. Pod IP 주소가 변경되더라도 클라이언트는 서비스 자체의 정적인 가상 IP 주소로만 직접 연결하기 때문에 Pod에 연결하는 데 아무런 문제가 없음

서비스의 구성 요소

  • Service 객체
    • Service는 Kubernetes 리소스로, API 서버에 정의되어 있음
    • spec.selector 필드를 사용하여 어떤 Pod가 이 서비스에 포함될지를 정의함
    • 예를 들어, app: myapp 라벨을 가진 모든 Pod가 이 서비스의 엔드포인트가 됨
  • Endpoints 객체
    • Service와 연결된 Pod의 IP 주소와 포트 정보를 포함하는 객체
    • Kubernetes 컨트롤러가 자동으로 생성하며, Service의 selector와 일치하는 Pod를 추적함
  • kube-proxy
    • 각 Kubernetes 노드에서 실행되며, Service에 대한 네트워크 트래픽을 적절한 Pod로 라우팅함
    • iptables, IPVS, 또는 유저스페이스 프록시 모드를 사용하여 트래픽을 관리함
      • iptables
        • 이 모드에서 kube-proxy는 API 서버의 변경 사항을 감시함
        • 각 새 서비스에 대해 iptables 규칙을 설치하여 서비스의 clusterIP 및 포트로의 트래픽을 캡처한 다음, 서비스의 백엔드 Pod로 트래픽을 리디렉션
        • Pod는 무작위로 선택됩니다. 이 모드는 안정적이며 Linux Netfilter가 사용자 공간과 커널 공간 사이를 전환할 필요 없이 트래픽을 처리하기 때문에 시스템 오버헤드가 낮음
      • IPVS
        • IPVS는 Netfilter 위에 구축되었으며 전송 계층 부하 분산을 구현함
        • IPVS는 Netfilter 후크 함수를 사용하여 해시 테이블을 기본 데이터 구조로 사용하고 커널 공간에서 작동함
        • IPVS 모드의 kube-proxy는 iptables 모드의 kube-proxy보다 낮은 지연 시간, 높은 처리량 및 더 나은 성능으로 트래픽을 리디렉션함
  • DNS
    • 클러스터 내에서 실행 중인 CoreDNS 또는 kube-dns는 서비스 디스커버리를 위해 Service의 DNS 이름을 관리함
    • 예를 들어, my-service.default.svc.cluster.local 같은 DNS 이름을 사용하여 서비스에 접근할 수 있음

서비스 동작 원리

  1. Service 정의
    • 사용자가 Service 객체를 생성하면 API 서버에 저장됨
    • Service 정의 예시
    • apiVersion: v1 kind: Service metadata: name: my-service spec: selector: app: myapp ports: - protocol: TCP port: 80 targetPort: 9376
  2. Pod와 Endpoints 연계
    • selector 필드에 정의된 라벨을 기준으로, 컨트롤러가 해당 라벨을 가진 모든 Pod를 찾아 Endpoints 객체를 생성함
    • Endpoints 객체는 서비스와 관련된 Pod의 IP 주소와 포트를 포함함
  3. kube-proxy 설정
    • 각 노드에서 실행 중인 kube-proxy는 Service와 Endpoints 정보를 주기적으로 API 서버에서 가져와 로컬 iptables 또는 IPVS 규칙을 설정함
  4. 트래픽 라우팅
    • 클러스터 내의 다른 Pod나 외부 클라이언트가 서비스의 클러스터 IP 주소 또는 DNS 이름을 사용하여 접근할 때, 트래픽이 kube-proxy를 통해 라우팅됨
    • kube-proxy는 로드 밸런싱 알고리즘을 사용하여 트래픽을 Endpoints 리스트에 있는 Pod로 분배함
  5. DNS를 통한 서비스 디스커버리
    • CoreDNS 또는 kube-dns가 클러스터 내에서 서비스 이름을 IP 주소로 변환함
    • 예를 들어, my-service.default.svc.cluster.local로 DNS 요청을 하면 서비스의 클러스터 IP 주소를 반환함
728x90
728x90

네트워크 네임스페이스

  • Host Network Namespace:
    • eth0 (Host NIC): 호스트 시스템의 물리적 네트워크 인터페이스로, 외부 네트워크와 통신함
    • kube-proxy:
      • Kubernetes 컴포넌트로, 클러스터 내의 네트워크 프록시 역할을 수행함
      • iptables 또는 IPVS를 사용하여 클러스터 내의 서비스 트래픽을 적절한 Pod로 라우팅함
    • cni0 (bridge): 호스트 네트워크 네임스페이스에서 사용하는 가상 브리지로, 쿠버네티스 CNI (Container Network Interface) 플러그인에 의해 생성됨. 여러 포드 네트워크 네임스페이스를 연결하는 역할을 함
    • veth0 (veth): 호스트 네트워크 네임스페이스와 포드 네트워크 네임스페이스를 연결하는 가상 이더넷 인터페이스 페어의 한 쪽 끝으로, 호스트 네트워크 네임스페이스의 cni0 브리지와 연결됨
  • Pod Network Namespace
    • Pod A
      • eth0 (veth) : Pod의 네트워크 인터페이스
      • pause container : 네트워크 네임스페이스를 유지하는 용도로 사용됨
      • container1, container2 : Pod A 내부의 실제 애플리케이션 컨테이너
      • localhost : Pod 내부의 컨테이너 간 통신을 위해 사용됨
    • Pod B
      • eth0 : Pod의 네트워크 인터페이스
      • pause container : 네트워크 네임스페이스를 유지하는 용도로 사용됨
      • container1 : Pod B 내부의 실제 애플리케이션 컨테이너

Pod 간 통신하는 방법 : Pod IP 사용

컨테이너 간 네트워킹과 달리 Pod 간 통신은 Pod를 클러스터의 동일한 노드나 다른 노드에 배포하든 실제 IP를 사용하여 이루어짐

쿠버네티스를 사용하면 모든 노드에 Pod에 대한 지정된 CIDR IP 범위가 있음

이를 통해 모든 Pod가 클러스터의 다른 Pod가 볼 수 있는 고유한 IP 주소를 수신함

새 Pod가 생성되면 IP 주소가 겹치지 않음

네트워크 연결 과정

Pod A에서 Pod B로 데이터가 전송될 때의 이벤트 흐름은 다음과 같음

  1. Pod A 트래픽은 eth0를 통해 루트 네트워크 네임스페이스의 가상 인터페이스 veth0로 흐름
  2. 트래픽은 veth0를 거쳐 cni0 가상 브리지로 이동
  3. 트래픽은 cni0 가상 브리지를 통해 veth1로 이동
  4. 트래픽은 veth1을 통해 Pod 2의 eth0 인터페이스에 도달
728x90

+ Recent posts