유닉스

미키오·2024년 11월 6일

Secure

목록 보기
5/5
post-thumbnail

유닉스의 역사

유닉스(Unix)는 1960년대 후반에 AT&T의 벨 연구소에서 개발된 운영 체제이다.
켄 톰슨(Ken Thompson), 데니스 리치(Dennis Ritchie), 그리고 동료들은 다중 사용자 시스템의 필요성을 느끼고, 소형 컴퓨터에서도 동작하는 간결하고 효율적인 운영 체제를 만들기 위해 유닉스를 개발했다.

유닉스는 본래 MIT, 벨 연구소, 제너럴 일렉트릭이 협력하여 개발한 멀틱스(Multics) 프로젝트에서 비롯되었다. 멀틱스의 복잡성과 크기에 실망한 벨 연구소의 연구진은 이를 대체할 새로운 운영 체제를 만들기로 결심했으며, 그 결과로 탄생한 것이 유닉스이다. 초기에는 단일 작업만 가능한 운영 체제였으나, C 언어의 발전과 함께 유닉스는 이식성이 높아졌고, 다중 작업다중 사용자 기능을 추가하여 진정한 타임셰어링 시스템으로 발전하였다.

1990년대에 들어서면서 유닉스는 슈퍼컴퓨터부터 워크스테이션, 서버에 이르기까지 다양한 컴퓨팅 환경에서 주 운영 체제로 자리 잡았다.

또한, BSD리눅스, MacOS 같은 유닉스 계열 시스템들이 등장하면서 유닉스 철학을 따르는 운영 체제들이 점차 발전하게 되었으며, 이는 오늘날에도 여전히 큰 영향을 미치고 있다.

유닉스의 현재 사용

현재 유닉스와 유닉스 계열 운영 체제들은 서버, 워크스테이션, 모바일 장치 등 다양한 환경에서 사용되고 있다. 예를 들어, 애플의 macOS는 유닉스 기반의 운영 체제이며, 리눅스는 많은 서버와 클라우드 인프라에서 널리 사용되고 있다.

특히 유닉스 철학을 따르는 운영 체제들은 모듈화된 설계와 강력한 커맨드 라인 도구들로 인해 시스템 안정성, 이식성, 그리고 확장성이 뛰어나다는 평가를 받고 있다.

POSIX와 유닉스의 표준화

이후 다양한 버전의 유닉스들이 탄생하며 발전의 기회를 제공했지만, 동시에 호환성 문제를 초래했다. 이를 해결하기 위해 IEEE에서는 유닉스 운영 체제의 표준을 정의한 POSIX(Portable Operating System Interface)를 만들었다.

POSIX는 유닉스 계열 운영 체제들이 공통적으로 따라야 할 인터페이스와 명령어들을 규정함으로써, 개발자들이 다양한 플랫폼에서도 일관된 방식으로 소프트웨어를 개발할 수 있도록 도와준다.

주요 유닉스 운영 체제로는 HP-UX, AIX, 솔라리스 등이 있으며, 리눅스와 같은 유닉스 계열 운영 체제들도 유닉스의 철학을 이어받아 사용되고 있다.

POSIX 표준

파일 시스템 접근: 파일과 디렉터리를 다루는 표준 API를 제공하여, 파일 생성, 삭제, 열기, 읽기, 쓰기 등의 작업을 일관되게 수행할 수 있다.


int main() {
    int fd = open("example.txt", O_RDONLY);
    if (fd == -1) {
        return 1;
    }
    char buffer[100];
    ssize_t bytesRead = read(fd, buffer, sizeof(buffer));
    close(fd);
    return 0;
}

POSIX API 중 open(), read(), write(), close()와 같은 함수들은 파일을 열고 읽고 쓰고 닫는 작업을 수행한다.

프로세스 제어: 프로세스를 생성하고 종료하는 방법, 프로세스 간 통신(IPC), 신호(signal) 처리 등을 포함한다. 이를 통해 다양한 플랫폼에서 동일한 방식으로 프로세스를 제어할 수 있다.

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        // 자식 프로세스 코드
        printf("Child process");
} else if (pid > 0) {
// 부모 프로세스 코드
printf("Parent process
");
kill(pid, SIGTERM); // 자식 프로세스 종료
}
return 0;
}

스레드(Thread) 관리: POSIX 스레드는 멀티스레드 프로그래밍을 위한 표준 API를 제공하며, 이를 통해 다양한 운영 체제에서 멀티스레드 프로그램을 일관되게 작성할 수 있다.

void* print_message(void* arg) {
    printf("Hello from thread
");
    return NULL;
}

int main() {
    pthread_t thread;
    pthread_create(&thread, NULL, print_message, NULL);
    pthread_join(thread, NULL);
    return 0;
}

입출력 표준화: 파일, 네트워크 소켓, 장치 간의 입출력을 표준화하여 개발자가 플랫폼에 구애받지 않고 네트워크와 파일 입출력을 구현할 수 있게 한다.

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    write(sockfd, "GET / HTTP/1.1", 18);
    
char response[1024];
read(sockfd, response, sizeof(response));
close(sockfd);
return 0;
}

파일과 소켓의 입출력을 POSIX API를 통해 일관되게 처리할 수 있다. 위 예시처럼, read()와 write() 함수는 파일뿐만 아니라 네트워크 소켓에도 사용될 수 있다.

유틸리티 및 명령어: awk, sed, grep, ls 등과 같은 기본 유닉스 유틸리티의 동작을 표준화하여, 다양한 유닉스 계열 시스템에서 동일한 명령어를 사용할 수 있도록 한다.

ls | grep "kids" | wc -l

타이머 및 클럭: 시간 지연, 주기적 타이머 설정, 시스템 클럭 접근 등과 관련된 API를 제공하여 시간 관련 작업을 일관되게 처리할 수 있다.

cron을 사용해 특정 시간마다 작업을 실행할 수 있다. 예를 들어, 매일 자정에 스크립트를 실행하려면 다음과 같은 cron 설정을 사용할 수 있다.

0 0 * * * /path/to/script.sh

이러한 POSIX 표준을 통해 개발자들은 다양한 유닉스 계열 시스템에서 호환성 문제 없이 소프트웨어를 개발하고 배포할 수 있다.

유닉스 철학

유닉스는 작은 도구들이 결합되어 강력한 기능을 수행하는 철학을 가지고 있다. 유닉스 철학은 크게 "한 가지 일을 잘하라"와 "프로그램들이 텍스트 스트림을 이용해 서로 통신할 수 있도록 하라"라는 원칙으로 요약할 수 있다. 마치 SOLID의 단일 책임 원칙(SRP), 인터페이스 분리 원칙(ISP)와도 연관이 있다는 생각이 들었다.

이러한 유닉스 철학은 단순하고 모듈화된 시스템을 지향하며, 복잡한 문제를 해결하기 위해 여러 작은 프로그램을 결합하는 방식을 선호한다. 이런 접근 방식은 개발자에게 확장 가능하고 재사용 가능한 코드를 작성할 수 있는 기회를 제공한다. 그러므로 유닉스 철학을 공부한다는 것은 곧 개발 철학을 공부한다는 것과 맞닿아있다.

한 가지 일을 잘하라 (Do One Thing and Do It Well)

유닉스 도구들은 한 가지 특정 작업을 전문적으로 수행하도록 설계되어 있다.

이 원칙은 프로그램이 복잡한 기능을 여러 가지 제공하는 대신 하나의 기능을 잘 수행하도록 개발되도록 유도한다. 이를 통해 각 도구는 더 간단해지고, 문제가 발생했을 때 디버깅이 쉬워진다.

ex) grep은 텍스트에서 특정 문자열을 검색하는 도구이다. grep은 다른 기능을 실행하지 않고 오직 문자열 검색에만 집중하여 이를 매우 효율적으로 수행한다.

텍스트 기반 인터페이스 사용 (Use Text Streams as a Universal Interface)

유닉스에서는 프로그램 간의 데이터 교환을 텍스트 스트림을 통해 처리하는 것을 선호한다.

텍스트는 사람이 읽을 수 있는 형태이며, 이를 통해 다양한 프로그램들이 서로의 출력을 받아들여 처리하는 데 용이하다. 이는 프로그램들 간의 호환성을 높여주며, 쉽게 수정 및 조합할 수 있도록 해준다.

ex) 예시: 유닉스 명령어인 ls는 디렉터리의 파일 목록(list)을 출력한다.

도구들을 연결하여 파이프라인을 만들라 (Build a Prototype as a Chain of Tools)

유닉스 철학의 중요한 부분은 작은 도구들을 파이프(|)로 연결하여 더 복잡한 작업을 수행하는 것이다.

이를 통해 각 도구는 개별적으로 동작하지만, 조합하여 더욱 강력한 기능을 구현할 수 있다. 예를 들어, ls로 파일 목록을 출력하고, grep으로 특정 파일을 필터링한 뒤, wc로 라인 수를 계산하는 식으로 조합할 수 있다.

ls | grep "kids" | wc -l

이 명령어는 현재 디렉터리에서 "kids"라는 단어가 포함된 파일들의 개수를 출력한다. ls로 파일 목록을 얻고, grep으로 파일명을 필터링하고, wc -l로 그 개수를 세는 과정을 파이프라인으로 연결한 것이다.

빠르게 프로토타입을 만들라 (Build Prototypes Rapidly)

유닉스 철학에서는 작업을 빠르게 반복하여 개선하는 것을 권장한다.

간단한 도구들을 사용해 프로토타입을 빠르게 만들고, 문제를 해결하기 위해 이를 발전시켜 나가는 방식이 효율적이라고 본다. 이렇게 하면 복잡한 시스템도 작고 독립적인 도구들의 조합으로 구성되므로, 유지보수성과 확장성이 높아진다.

ex) 리액트에서도 빠르게 프로토타입을 만들고 사용자 피드백을 통해 개선하는 방식이 권장된다.

const PrototypeComponent = () => {
  const [items, setItems] = useState([]);

  useEffect(() => {
    // 간단한 데이터를 불러와서 프로토타입을 빠르게 구성
    setItems(["Item 1", "Item 2", "Item 3"]);
  }, []);

  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
};
export default PrototypeComponent;

필자도 우선적으로 items에 임의의 값을 넣은 후 레이아웃을 구성한 뒤 백엔드가 구축이 되면 후에 코드를 개선하는 과정을 수행한다.

명확하고 간단하게 설계하라 (Design for Simplicity and Clarity)

유닉스 철학은 코드와 도구의 설계가 간단하고 명확해야 한다는 것을 강조한다.

복잡한 기능은 도구를 결합하여 구현하도록 하며, 개별 도구는 최대한 단순하게 설계되어야 한다. 이를 통해 프로그램의 이해와 유지보수가 쉬워지고, 오류의 가능성도 줄어든다.

즉, 가독성, 재사용성, 유지보수성을 높이기 위해 작은 컴포넌트 혹은 모듈을 구현하여 이를 조합하여 처리하는 것을 추구한다.

마무리

유닉스의 역사를 알아보면서, 이 운영 체제가 어떻게 컴퓨터 세계에서 중요한 위치를 차지하게 되었는지 깨달을 수 있었다.

특히 "한 가지 일을 잘하라"는 유닉스 철학은 오늘날의 개발 패러다임에도 큰 영향을 미치고 있음을 느끼게 되었고 은연중에 실제 코드 구현 시에도 실천하려고 노력하는 부분이다.

웹 개발에서도 작은 컴포넌트를 조합하여 더 큰 기능을 구현하는 원칙은 여전히 중요한 가치로 자리 잡고 있다. 이러한 철학이 지금의 개발 문화와 실천으로 이어졌다는 점은 왜 유닉스에 대해 개발자로서 인지해야하는지를 시사한다.

다음 글에서는 유닉스의 다양한 시스템과 도구들을 기반으로 한 보안과 해킹 기법에 대해 깊이 있게 다룰 예정이다. 특히, 솔라리스, 텔넷, SSH, RPC와 같은 유닉스 환경에서 발생할 수 있는 보안 문제와 해킹 기법들에 대해 살펴보고자 한다. 계속 탐구해 나가자! 🚀

📚 Bibliography

profile
교육 전공 개발자 💻

0개의 댓글