스레드란

스레드란 하나하나의 실행 단위를 말하며, 흔히 프로세스를 차를 만드는 공장에 비유한다면 스레드를 바퀴를 만드는 일꾼, 모터를 만드는 일꾼 등에 비유하곤 합니다.

 

 

이 스레드는 커널 공간에서 구현하는 커널 스레드와 유저 공간에서 구현하는 유저 스레드 두 종류로 나눌 수 있습니다.

 

 

커널 스레드 (Kernel Threads)

: 운영 체제의 "커널"이 관리하는 스레드입니다. 프로세스가 생성되면 커널은 하나의 커널 스레드를 만드는데, "프로세스"가 아닌 이 "커널 스레드"가 실질적인 스케쥴링 대상이 되며 하나의 프로세스는 하나 이상의 커널 스레드를 가집니다.  

 

 

유저 스레드 (Kernel Threads)

: 유저 공간에서 라이브러리에 의해 관리되는 스레드입니다. 커널은 유저 스레드의 존재를 모르며, 모든 유저 스레드의 관리는 유저 공간에서 이루어집니다. 유저 스레드의 실질적인 수행은 자신이 속한 프로세스의 커널 스레드와 연결돼서 수행됩니다.

 

 

좀 더 이해하기 쉽도록, A라는 프로세스에 대해 커널 스레드를 2개 만들 때와 유저 스레드를 2개 만들 때를 도식화해서 보겠습니다.

 

스레드를 커널 스레드로 해서 2개 만든 경우
스레드를 유저 스레드로 해서 2개 만든 경우

 

이때, 실질적으로 스케쥴링 대상이 되는 것은 커널 스레드이며, 커널은 유저 스레드의 존재를 모른다고 했습니다.  따라서 프로세스 A, B가 있는 상황에서 A에 커널 스레드만 2개 있는 경우와 A에 유저 스레드가 2개 있는 경우는 다음과 같이 스케쥴링됩니다.

 

프로세스 A에 커널 스레드가 2개인 경우

 

프로세스 A에 유저 스레드가 2개인 경우

 

프로세스 A에 커널 스레드만 2개인 경우, A의 2개 커널 스레드 각각이 스케쥴링 대상이 됩니다. 따라서 A의 0번 스레드와 1번 스레드, 프로세스 B(얘도 B의 커널 스레드라고 볼 수 있겠죠)가 스케쥴링되는 걸 볼 수 있습니다. 그러나 프로세스 A에 유저 스레드가 2개인 경우, 커널은 유저 스레드를 모르며 오직 커널 스레드만이 스케쥴링 대상이 되기 때문에 A의 커널 스레드와 B의 커널 스레드만이 스케쥴링 대상이 됩니다. 이 때 A의 유저 스레드가 2개인 상황에서 A의 커널 스레드가 실행될 때, 0번 스레드(유저 스레드임)와 1번 스레드(유저 스레드임)를 어떻게 배분할지는 스레드 라이브러리가 책임집니다. 즉 유저 스레드는 자신이 속한 프로세스의 커널 스레드가 CPU에 올라왔을 때, 그 커널 스레드와 연결돼서 수행되는 스레드라고 볼 수 있는 것이죠.

 

"커널 스레드가 실질적인 스케쥴링 대상이다"로 비롯되는 다른 차이점은 어떤 것들이 있을까요? 우선 커널 스레드의 경우 같은 프로세스의 커널 스레드라 할지라도 다른 프로세서에 할당할 수 있는 반면, 같은 프로세스의 유저 스레드들은 다른 프로세서로 할당될 수 없을 수도 있습니다(커널 스레드가 CPU에 올라갔을 때 연결돼서 수행되기 때문). 즉 커널 스레드는 멀티프로세서 시스템에서 유저 스레드보다 더 효율적으로 작동한다고 볼 수 있죠. 그리고 커널 스레드가 시스템콜을 호출하면 해당 스레드만 블로킹되고, 다른 커널 스레드들은 계속 실행될 수 있습니다. 반면 유저 스레드는 하나의 유저 스레드가 시스템콜을 호출하면 같은 프로세스의 다른 유저 스레드들도 블로킹될 수 있습니다. 커널은 그 유저 스레드에 연결된 커널 스레드가 시스템콜을 호출한 거라고 여기기 때문에, 해당 커널 스레드가 블로킹되면서 물려있던다른 유저 스레드들도 블로킹될 여지가 있다는 거죠. 컨텍스트 스위칭 비용의 경우, 커널 스레드의 컨텍스트 스위칭이 유저 스레드의 컨텍스트 스위칭보다 가볍습니다.

 

표로 정리하면 다음과 같겠습니다.

  커널 스레드 유저 스레드
관리 주체 커널 유저 수준 라이브러리
생성 및 관리 비용 상대적으로 높음 상대적으로 낮음
블로킹 시스템콜 한 스레드가 블로킹되어도 다른 스레드에 영향 없음 한 스레드가 블로킹되면 전체가 블로킹될 수 있음
멀티프로세서 지원 효율적 비효율적
컨텍스트 스위칭 비용 상대적으로 높음 상대적으로 낮음

 

 

 

그럼 이제 유저 스레드가 커널 스레드와 연결되는 3가지 스레드 모델을 살펴보겠습니다.

 

 

1. One-to-One Model

 

One-to-One Model

 

각 유저 스레드가 하나의 커널 스레드와 연결되는 모델입니다. 각 커널 스레드는 독립적으로 스케쥴링되므로, 같은 프로세스 내의 유저 스레드들은 각각 다른 프로세서에서 병렬로 실행될 수 있으며 하나의 유저 스레드에서 시스템콜을 호출해도 다른 유저 스레드가 블로킹되지 않습니다. 그러나 유저 스레드의 컨텍스트 스위칭 이점을 볼 수 없는 모델이기도 합니다.

 

 

2. Many-to-One Model

Many-To-One Model

 

여러 유저 스레드가 하나의 커널 스레드와 연결되는 모델입니다. 따라서 같은 프로세스의 유저 스레드들이 서로 다른 프로세서에서 병렬로 실행될 수 없으며, 하나의 유저 스레드에서 시스템콜을 호출한 경우 다른 유저 스레드들도 블로킹될 수 있습니다. 그러나 유저 스레드의 컨텍스트 스위칭 이점을 얻을 수 있는 모델입니다.

 

 

3. Many-to-Many Model

 

Many-To-Many Model

 

여러 유저 스레드가 여러 커널 스레드와 연결되는 모델로 One-to-One과 Many-to-One을 짬뽕한 모델입니다. 같은 프로세스의 유저 스레드가 여러 프로세서에서 병렬로 실행될 수 있으며, 한 유저 스레드가 시스템콜을 호출해도 다른 커널 스레드에 연결된 유저 스레드들이 있으니 유저 스레드들이 모두 블로킹되는 불상사를 막을 수 있습니다. 유저 스레드의 컨텍스트 스위칭 이점도 챙길 수 있으나, 설계와 구현이 매우 복잡하며 유저 스레드와 커널 스레드 간 연결 관계의 관리 등에 오버헤드가 들어감을 감안해야 합니다.

 

 

 

참고로 자바에서 21버전부터 Virtual Thread가 도입됐는데요. 원래는 자바에서 유저 스레드를 만들면 그에 연결되는 커널 스레드도 함께 만들어졌습니다. 즉 기존에는 One-to-One 모델을 사용했던 것이죠. 그러나 Virtual Thread는 Many-to-One과 비슷한 컨셉을 차용해서 컨텍스트 스위칭 비용을 낮췄습니다. 관심있는 분은 다음 글을 보면 될 것 같습니다.

 

https://techblog.woowahan.com/15398/

 

Java의 미래, Virtual Thread | 우아한형제들 기술블로그

JDK21에 공식 feature로 추가된 Virtual Thread에 대해 알아보고, Thread, Reactive Programming, Kotlin coroutines와 비교해봅니다.

techblog.woowahan.com

 

 

 

 

 

'CS > 운영체제' 카테고리의 다른 글

피터슨의 알고리즘  (0) 2024.06.22

피터슨의 알고리즘

컴퓨터 시스템에서 여러 프로세스가 동시에 실행될 때, 공유 자원에 대해 여러 프로세스에서 동시 접근이 가능하다면 데이터 불일치 등의 문제가 발생할 수 있습니다. 이 때 공유 자원에 대해 동시 접근이 발생 가능한 영역을 임계 영역(Critical section)이라 부르며, 이 임계 영역에서 비롯되는 문제들을 막으려면 다음과 같은 요구사항을 만족해야 합니다.

 

1. 상호 배제 (Mutual Exclusion)

: 한 번에 하나의 프로세스만이 임계 구역에 들어갈 수 있어야 합니다.

 

2. 진행 (Progress)

: 임계 구역에 들어가려는 프로세스가 없을 때, 어느 프로세스도 임계 구역에 들어갈 수 있어야 합니다.

 

3. 한정 대기 (Bounded Waiting)

: 각 프로세스는 임계 구역에 들어가기 위해 무한정 기다리지 않아야 합니다.

 

피터슨의 알고리즘(Peterson's Algorithm)은 위 요구사항들을 만족하는 고전적인 알고리즘으로, 두 프로세스 간의 상호 배제를 위해 만들어졌습니다. 1980년대에 게리 피터슨(Gary L. Peterson)에 의해 제안됐으며, 상호 배제 문제를 간단하고 효율적으로, 이해하기 쉽고 최소한의 변수만을 사용하여 구현할 수 있다는 장점이 있습니다.

 

피터슨의 알고리즘은 flag 배열과 turn 변수를 사용하여 두 프로세스가 임계 영역에 동시에 진입(즉 경쟁 상태 발생)하는 것을 방지합니다. flag 배열은 각 프로세스가 임계 구역에 들어가기를 원하는지를 표시하며, turn 변수는 임계 구역에 들어갈 차례인 프로세스를 나타내는데요. 이 간단한 구조 덕에 직관적이고 구현하기 쉬운 상호 배제 해결책으로 널리 알려져 있습니다.

 

 

피터슨의 알고리즘 동작 순서

피터슨의 알고리즘은 다음과 같은 순서로 각 작업이 순서대로 임계 영역에 들어가게 합니다. 

 

 

1. 임계 구역 진입 요청

: 프로세스가 임계 구역(critical section)에 들어가고자 할 때, 자신의 flag를 true로 설정하여 진입 의사를 표시합니다.

 

2. 양보 설정

: 프로세스는 turn 변수를 상대방 프로세스의 번호로 설정하고, 이를 통해 상대방 프로세스에게 양보할 의사를 나타냅니다.

 

3. 대기 상태

: 프로세스는 상대방 프로세스의 flag가 true이고, turn이 상대방 프로세스의 번호인 동안 계속해서 기다립니다(상대방 프로세스가 임계 구역에 들어가고자 할 경우 상대방에게 우선권을 주기 위함)

 

4. 임계 구역 진입

: 상대방 프로세스가 임계 구역에 들어가고자 하지 않거나, turn이 자신의 번호로 설정될 때까지 기다린 후 임계 구역에 진입합니다.

 

5. 임계 구역 종료

: 임계 구역에서의 작업을 마치면, 자신의 flag를 false로 설정하여 임계 구역에서 나왔음을 알립니다.

 

6. 비임계 구역 작업

: 비임계 구역에서 다른 작업을 수행합니다.

 

 

코드로 나타내면 다음과 같습니다.

// 변수 선언
boolean flag[2] = {false, false};
int turn;

function process0() {
    while (true) {
        // 1. 임계 구역 진입 요청
        flag[0] = true;
        
        // 2. 양보 설정
        turn = 1;
        
        // 3. 대기 상태
        while (flag[1] == true && turn == 1) {
            // busy-wait
        }
        
        // 4. 임계 구역 진입
        // 임계 구역에서 수행할 작업
        print("Process 0 in critical section")

        // 5. 임계 구역 종료
        flag[0] = false;

        // 6. 비임계 구역 작업
        // 비임계 구역에서 수행할 작업
        print("Process 0 in non-critical section")
    }
}

function process1() {
    while (true) {
        // 1. 임계 구역 진입 요청
        flag[1] = true;
        
        // 2. 양보 설정
        turn = 0;
        
        // 3. 대기 상태
        while (flag[0] == true && turn == 0) {
            // busy-wait
        }

        // 4. 임계 구역 진입
        // 임계 구역에서 수행할 작업
        print("Process 1 in critical section")

        // 5. 임계 구역 종료
        flag[1] = false;

        // 6. 비임계 구역 작업
        // 비임계 구역에서 수행할 작업
        print("Process 1 in non-critical section")
    }
}

 

 

피터슨의 알고리즘 문제점

1. 두 개 프로세스에서만 적용 가능

: 피터슨의 알고리짐은 기본적으로 두 개의 프로세스 사이에서만 상호 배제를 보장합니다. 2개보다 많은 프로세스를 다룰 경우 알고리즘을 확장해야 하지만, 코드가 복잡해지고 효율성이 떨어질 수 있습니다.

 

2. Busy waiting

: 피터슨의 알고리즘은 다른 프로세스가 임계 영역에 들어가있는 동안 다른 프로세스는 무한 루프를 통해 대기(Busy waiting)하는데, 이는 CPU 자원의 낭비 및 효율성 저하의 원인이 됩니다. 또한 실시간 시스템인 경우, Busy waiting은 일종의 지연 현상을 초래할 수도 있습니다.

 

 

 

 

 

'CS > 운영체제' 카테고리의 다른 글

커널 스레드 vs 유저 스레드(사용자 스레드)  (0) 2024.06.23

알아보기 전, Polymorphism에 대해 알고 가면 좋다.

Polymorphism ?

우리 말로는 '다형성'이라고 하며, 똑같은 이름을 가졌지만 실제 수행 시엔 다른 결과를 내는 것을 말한다. 부모로부터 상속받은 메소드를 자식 단에서 오버라이딩했을 때를 그 중 하나의 예시로 들 수 있고 strategy pattern에서 이를 사용한다.

다음과 같은 관계를 갖는 아이들이 있다고 하자.

Dog와 Cat은 부모 Pet으로 받은 메소드 cry()를 각각 bark(), meow()를 호출하는 함수로 오버라이딩했다고 하자. 그리고 다음과 같이 자바 언어로 간단한 프로그램을 작성했다 하자.

public class PetMain(){
    public static void main(String[] args){
        Pet myPet;
        myPet = new Dog();
        myPet.cry();
        myPet = new Cat();
        myPet.cry();
    }
}

myPet의 cry()는 두 번 호출됐지만, 각각 다른 결과를 보일 것이다! 똑같은 이름을 가졌지만 실제 수행 시엔 다른 결과를 내는 것이며, 그 이유는 코드에서 드러나있듯이 첫 번째 cry를 할 땐 Dog인스턴스, 두 번째 cry를 할 땐 Cat인스턴스에 관한 것이기 때문. 

 

 

strategy pattern

결론부터 말하면 메소드, 알고리즘 등(객체들이 할 수 있는 '행위'라고 생각하면 됨)을 캡슐화하여 행위를 유연하게 확장하는 패턴을 말한다. 

 

다음과 같은 관계를 갖는 아이들이 있다고 하자.

부모 Duck을 상속한 RedHatDuck, MallardDuck이 있는 상황. 이 오리들은 fly, swim, quack을 할 수 있는데, display를 하면 화면에 서로 다른 모습으로 그려진다고 하자. 그래서 fly, swim, quack은 부모에 그냥 뒀지만 display는 추상 메소드로 둬서 자식 단에서 구현하는 모습을 확인할 수 있다.

 

이 상황에서, RubberDuck을 만드는데 얘도 Duck을 상속받아 만들려고 한다고 하자. 문제는 이 RubberDuck은 fly를 할 수 없고, quack(꽥!!)역시 살아있는 오리들의 꽥이 아니라 모형오리를 누르면 나오는 squack이라고 하자. 

 

...어떻게 이 상황을 처리할 수 있을까?

 

뭐 우선 RubberDuck이 자신의 메소드로 squack을 가지게 하고..

fly가 문젠데, 이 녀석을 오버라이딩하는 방법이 있을 것이다!

뭐 그러면 다음과 같은 꼴이 된다

.그러나 애당초 상속을 하는 이유가 뭐였는가? 코드의 재사용 떄문이다. 러버덕부터 시작해 이런 식으로 오버라이딩을 통해 해결한다면 결국 어느 순간에 이르러서는 상속의 의미가 없어지게 된다..(암튼 뭐 이 방법은 객체지향적인 관점에서 썩 좋은 방법은 아니라는 얘기)

 

 

그렇다면 어떻게 할 수 있을까..

 

메소드, 행위 자체를 인터페이스 클래스로 바꾼다면 어떨까! 기존 부모에 있는 fly, quack을 따로 빼서 flyable, quackable이란 클래스로 만들고 나중에 fly가 가능한 애들만 flyable을 상속받고 quack이 가능한 애들만 quackable을 상속받게 하는 것이다.

후..draw.io에서 그리다가 화딱지나서 파워포인트로 그렸다..

 

 

RubberDuck입장에선 쓰지도 않는 fly와 quack을 상속받지 않으니 으흠 깔끔하게 만들었군!이란 생각이 든다.

 

그러나...이 방법은 Multiple Inheritance, 즉 다중 상속이 많아지는 방식이므로 구조가 매우 복잡해질 수 있는 단점이 있기 때문에 역시나 완전 좋은 방법은 아니다.

 

방금 방법에서 메소드와 행위 자체를 인터페이스 클래스로 분리시켰음에 주목하자. 부모클래스 Duck이 이 메소드클래스와  association을 갖게 하면 어떨까? 다음과 같이 작성할 수 있다!

객체가 할 있는 행위들 중 바뀌는 행위들을 따로 클래스로 만든 모습을 볼 수 있다. 아까 Pet 인터페이스를 자식 Dog, Cat이 구현하고 Pet변수에 Dog객체, Cat객체를 대입하여 같은 메소드지만 서로 다른 일이 일어나게 했었는데(다형성!), 똑같이 여기서도 Duck에 멤버변수로 FlyBehavior변수, QuackBehavior변수를 둔 다음 이 변수들에 서로 다른 구현객체(FlyWithWings..)를 대입하여 서로 다른 동작이 일어나게 할 수 있다. 이를 테면,

 

public class MallardDuck extends Duck(){
    MallardDuck(){
        flyableBehavior = new FlyWithWings();
        quackableBehavior = new AnimalQuack();
    }
    // .. 생략
}

 

이런 식이다. 만약 나중에 새로운 나는 방식(예를 들어 로켓으로 난다든지..)을 도입해야 한다면 단순히 FlyBehavior를 구현하는 클래스 FlyWithRocket을 만들면 끝이다. 기존 코드를 건드리지 않으면서 확장이 가능한 셈이다.

 

이렇듯, 전략패턴(strategy pattern)은 메소드, 알고리즘, 행위 등을 정의하고 캡슐화하여 독립적으로 변경할 수 있게끔 하는 패턴이다. 

  일반적으로 서비스를 요청하는 측을 클라이언트, 제공하는 측을 서버라고 한다. 웹 브라우저, 메일 프로그램 등은 사용지 측에서 사용하는 클라이언트 애플리케이션이고, 웹 서버 프로그램이나 메일 서버 프로그램 등은 서버 측에서 쓰는 서버 애플리케이션이라 볼 수 있다. 이러한 애플리케이션은 모두 응용 계층(application layer)에서 동작한다.

 

  물리 계층, 데이터링크 계층, 네트워크 계층, 전송 계층을 통해 상대방에게 데이터를 정확하게 전달할 수 있다는 것을 알게 됐다. 응용 계층은 애플리케이션과 데이터를 주고받기 위해 필요한 부분으로, 클라이언트의 요청(request)를 전달하기 위해 통신 대상(서버 등)이 이해할 수 있는 메세지(데이터)로 변환하고 이를 전송 계층으로 전달하는 역할을 한다. 웹 브라우저, 메일 프로그램 등과 같은 클라이언트 측 애플리케이션이 서버 측 애플리케이션과 통신하려면 응용 계층의 프로토콜을 사용해야 한다. 웹 사이트를 볼 땐 HTTP, 파일을 전송할 땐 FTP, 메일을 보낼 땐 SMTP, 메일을 받을 때는 POP3라는 프로토콜을 사용한다. 암튼 결국, 응용 계층은 각각의 애플리케이션에 대응되는 데이터를 전송 계층으로 전달하는 역할을 한다. 

 

HTTP란?

  클라이언트(웹 브라우저)는 웹 사이트를 보기 위해 서버(웹 서버 프로그램)의 80번 포트를 사용해 HTTP통신을 한다. 클라이언트 측에서 HTTP request를 보내고, 서버 측에서 HTTP response를 반환하는 식이다. 지금은 HTTP/2나 HTTP/1.1버전을 사용하지만 옛날에 쓰던 HTTP/1.0버전에서는 요청을 보낼 때마다 연결했다가 응답이 오면 연결을 끊고, 요청을 또 보내게 되면 새롭게 연결하는 방식을 사용했으나 HTTP/1.1버전부터는 keepalive라는 기능이 추가되면서 연결이 한 번 되면 데이터 교환을 마칠 때까지 연결을 유지하는 방식이 가능하게 됐다. HTTP/1.1버전은 요청을 보낸 순서대로 응답을 반환한다는 특징이 있어서 이전 요청을 처리하는 시간이 길어지면 다음 요청이 밀리는 단점이 있었는데. 2.0버전부터는 순서대로 응답을 반환할 필요가 없어져서 콘텐츠를 더욱 빠르게 표시할 수 있게 됐다.

 

DNS?

  특정 사이트를 보고싶으면 특정 IP주소를 통해 웹 서버에 접속해 웹 사이트를 보는게 맞다. 네이버에 가고 싶으면 주소창에 www.naver.com을 쳐서 웹사이트를 본다. ..? 뭔가 이상하지 않은가? 네이버의 IP주소가 아니라 다른 걸 입력했는데 네이버의 웹사이트를 보고 있는 셈이니 말이다. 이것이 DNS덕분이다. 간단히 말해 DNS는 URL을 IP주소로 변환하는 시스템이다. www로 시작하는 저 url대신에 실제 네이버의 IP주소를 통해서도 물론 웹 서버에 요청을 보낼 수 있지만 네이버, 다음, 구글 등의 IP주소를 하나하나 기억하는 것은 쉬운 것이 아니다. 예를 들어 뭐..친구들 전화번호 다 외우고 있는 사람 없을 것이다. 다 전화번호부에 이름으로 저장해서 그 이름에 전화걸지..일일이 전화번호 외워서 전화거는 사람은 별로 없을 거란 얘기. DNS는 즉 일종의 전화번호부라고 생각할 수도 있겠다. 아무튼 특정 url을 사용해 웹 서버에 접속하도록 돕는 것을 DNS의 이름 해석이라고 말하며, 이는 내가 원하는 url의 IP주소를 알려주는 것을 의미한다. url을 입력하면 컴퓨터가 먼저 DNS서버에게 이 url의 IP주소를 알려달라고 요청하고, DNS서버는 컴퓨터에게 그 url의 IP주소를 반환한다. 이를 이용해 그 IP주소로 접속할 수 있는 것.

 

메일 서버의 구조

  메일을 송수신하기 위해서는 클라이언트 측의 메일 프로그램과 서버 측의 메일 서버 프로그램 간에 통신을 해야 한다. 이 때 사용되는 프로토콜의 종류에는 두 가지가 있는데 첫째는 메일을 보내는데 사용되는 SMTP고 둘째는 메일을 받는데 사용되는 POP3다. SMTP는 포트번호로 25번을, POP3는 포트 번호로 110번을 사용한다. 좀 더 정확히 설명하자면 컴퓨터 A, B와 메일 서버1, 메일 서버 2가 있을 때 SMTP를 이용해 A에서 메일서버1로 메일을 보내고, 마찬가지로 SMTP를 이용해 메일서버1에서 메일서버2로 메일을 보낸다음 POP3를 이용해 메일서버2에서 B로 메일을 보낸다. 

 

SMTP에 의한 메일 송신

  다음과 같은 순서로 진행된다

 

1) 세션 시작 통지

2) 송신자의 메일 주소 통지

3) 목적지 메일 주소 통지

4) 메일 본문 전송 통지

5) 메일 본문 송신

6) 세션 종료 통지

 

각 과정마다 응답을 받은 후 다음 과정이 진행되는 식. 방금 살펴본 것이 컴퓨터 A에서 메일서버1로 보내는 과정이었고, 메일서버1에서도 SMTP를 이용해 메일 서버2로 메일을 보낸다.

 

POP3에 의한 메일 수신

  메일 서버에는 메일 박스라고 불리는 메일을 보관해 주는 기능이 있다. 메일 서버2는 POP3를 사용해 메일 서버2의 메일박스에서 메일을 가져와 컴퓨터 B로 전송한다. B입장에서 메일 서버2로부터 메일을 수신할 때는 사용자 이름과 비밀번호를 이용한 사용자 인증이 필요하다. POP3 프로토콜로 110번 포트를 사용한다. POP3는 다음과 같은 순서로 진행된다. B가 메일서버2에게

 

1) 세션 시작 통지 -> 메일서버2가 확인응답 보냄

2) 수신자의 사용자이름 통지 -> 메일서버2가 확인응답 보냄

3) 수신자의 비밀번호 통지  -> 메일서버2가 확인응답 보냄

4) 메일을 확인  -> 메일서버2가 "있음"이라는 확인응답 보냄

5) 메일의 전송 요청 -> 메일서버2가 메일내용 전송

6) 세션 종료 요청

  네트워크 계층을 설명할 때 다른 네트워크로 데이터를 전송하려면 라우터가 필요하고, 라우터의 라우팅 기능을 사용해 전송할 수 있다고 했다. 문제는 이렇게 데이터를 전송하더라고 목적지에 잘 도착하지 않을 수도 있다는 것! 우리가 편지를 보낼 때도 예기치 못한 사고로 인해 배송이 지연되거나 중간에 분실되거나 할 수 있는 것처럼 말이다. 물리 계층, 데이터 링크 계층, 네트워크 계층을 통해서 목적지에 데이터를 보낼 수는 있지만, 배송 과정에서 데이터가 손상되거나 유실되더라도 이들은 아무것도 해주지 않는다 즉 책임을 지지 않는다. 참으로 괘씸하기 짝이 없다.  이 때 전송 계층(transport layer)는 목적지에 신뢰할 수 있는 데이터를 전달하기 위해 필요한 계층으로, 오류를 점검하는 기능이 있다! 이를 통해 오류가 발생하면 데이터의 재전송을 요청하기도 한다. 다시 말하면 네트워크 계층이 목적지까지 데이터를 전달하는 역할을 한다면 전송 계층에서는 데이터가 제대로 잘 갔는지 확인하는 역할을 하는 것이다. 

  

  전송 계층은 오류 점검말고도 한 가지 역할을 더 수행한다. 건물로 편지를 잘 보냈지만 그 건물 어느 호실에 편지를 전해야 할 지 모른다면 어떠겠는가? 마찬가지로 컴퓨터가 데이터를 받아도 어떤 애플리케이션에 전달해야 하는지 모른다면 곤란하게 된다. 전송계층에서는 전송된 데이터의 목적지가 어떤 애플리케이션인지 식별하는 역할 역시 수행한다.

 

  전송 계층의 특징을 간단히 설명하면 신뢰성/정확성효율성으로 구분할 수 있다. 신뢰성/정확성은 데이터를 목적지에 문제없이 전달하는 것을, 효율성은 데이터를 빠르고 효율적으로 전달하는 것을 의미한다. 이 때 신뢰할 수 있고 정확한 데이터를 전달하는 통신을 연결형 통신이라고 하고, 효율적으로 데이터를 전달하는 것을 비연결형 통신이라고 한다. 좀 더 설명하자면 연결형 통신은 상대편(수신자)와 하나하나 확인하면서 데이터를 전송하는 방식이고, 비연결형 통신은 일방적으로 데이터를 전송하는 방식이다.

 

ex) 연결형 통신

A : 보내도 돼?

B : 네, 보내세요

A : 보냅니다!

B : 받았슴다~!

A  : 확인~

 

ex) 비연결형 통신A : 보낸다~수고~

 

이는 당연한 얘기지만 연결형 통신은 신뢰성/정확성을 우선으로 하는 통신이고, 비연결형 통신은 효율성을 우선으로 하는 통신방식이기 때문에 그렇다. 그럼 여기서 궁금한 점이 생길 수 있다. 신뢰성과 정확성이 보장되지 않는 저 상도덕 없이 일방적으로만 보내는 비연결형통신을 사용하는 경우가 있는가? 정답은 당연히 Yes, 예를 들면 동영상을 시청할 때다. 동영상은 아무래도 빠른 전송이 필요하기 때문! 정확하게 하나하나 확인하면서 한다고 하면 데이터가 늦게 도착해 화면이 버벅거리는 영상을 보게 될텐데, 그것보다는 좀 유실되더라도 원활하게 보는 것이 낫기 때문이다. 이 연결형 통신의 프로토콜로는 그 유명한 TCP가 쓰이고, 비연결형 통신 프로토콜로는 UDP가 사용된다.

 

 

TCP의 구조 [1] 3-way handskake

  다른 계층과 마찬가지로 전송계층에서 TCP프로토콜로 데이터를 전송할 때도 데이터에 TCP헤더를 붙여서 덩어리를 만들어 보내며, 이 덩어리를 세그먼트라고 한다. 이 TCP헤더에는 출발지 포트 번호, 목적지 포트 번호, 일련번호, 확인 응답 번호..등과 같은 다양한 정보들이 담긴다. 앞서 말했듯 TCP는 꼼꼼하게 상대방을 확인하면서 데이터를 보내는 연결형 통신이기 때문에 사실 데이터를 전송하기 전에 해야 하는 작업이 있다. 그건 바로 데이터를 전송하기 위해 연결(connection)이라는 가상의 우리 둘만의♡(즉 독점적인) 통신로를 확보하는 것이다. 이 연결을 확보한 후에 데이터를 전송할 수 있다.

 

  이 연결을 그럼 어떻게 확보하는가? TCP헤더를 보면 107 ~ 112비트까지의 총 6비트로 구성된 '코드 비트'라는 정보가 있다. 이 코드 비트에 연결의 제어 정보가 기록되는데, 6개 비트가 각 비트별로 역할이 있다. 6개 각 비트들에는 URG, ACK등 각각을 칭하는 이름이 있으며 이들의 초깃값은 0이고 활성화되면 1이 된다. 이 때 우리가 원하는 '연결'을 확보하려면 이 중 SYNACK가 필요하다. SYN은 연결 요청, ACK는 확인 응답을 뜻한다. 이 두 녀석을 활용해 '연결'은 다음과 같이 3-way handshake로 확립된다.

 

ex)컴퓨터 A, B가 있고 A가 B랑 연결을 확보하고 싶은 상황.

 

1) A가 먼저 B에게 허가를 받아야 하므로 연결 확보 허가를 받기 위한 요청(SYN)을 보낸다2) B는 A가 보낸 요청을 받은 후 허가한다는 응답을 회신하기 위해 ACK를 A에게 보낸다. 이와 함께 B 역시 A에게 데이터 전송 허가를 받기 위해 연결 확보 허가를 받기 위한 요청(SYN)을 보낸다.3) B의 요청을 받은 A는 허가한다는 응답(ACK)를 B에게 보낸다.

 

그림으로 나타내면 다음과 같다.

데이터를 보내기 전에 연결을 확보하기 위해 패킷 요청을 총 3번 교환하므로 3-way handshake라고 부른다. 네트워크 분야에서 이 핸드셰이크(handshake)는 사람들이 상대방을 확인하고 악수하는 것처럼 데이터 통신에서도 확실하게 데이터가 전송됐는지 확인하며 이루어지는 통신 수단을 말한다. 데이터를 다 전송한 후엔(서로 볼 일 다 봤을 땐) 바로 손절하는게 아니라 젠틀하게 연결을 끊기 위한 요청을 교환하는데, 이 때는 FIN과 ACK를 사용한다.

 

TCP의 구조 [2] 일련번호와 확인 응답 번호의 구조

  3-way handshake를 했다면 이제 데이터 전송을 해야 된다. 데이터 전송을 할 땐 이 땐 TCP헤더에서 일련번호와 확인응답번호가 쓰인다(둘 다 32비트). TCP는 데이터를 분할해서 보내게 되는데 일련번호는 순서를 가진 연속된 번호로 송신측에서 수신측에게 "이 데이터가 몇 번째 데이터야"라고 알려줄 때 쓰인다. 이를 통해 수신자는 이 데이터가 원래 데이터의 몇 번째 데이터인지 알 수 있다. 확인 응답 번호는 수신자가 송신자에게 몇 번째 데이터를 수신했는지 알려줄 때 쓰이는 번호로, 10번 데이터를 받았다면 송신자에세 확인 응답 번호로 11번을 보낸다. 뭐 굳이 뜻을 붙이자면 "저는 10번 데이터를 받았으니, 그 다음 번호인 11번을 주시면 됩니다" 하는 것이다. 이 작업을 '확인 응답'이라고 부른다.

 

  좀 더 구체적으로 설명하자면, 일련번호는 내가 보내는 데이터의 첫 번째 바이트 번호이다. 3001번 ~ 3200번까지의 200바이트짜리 데이터를 보낸다고 하면 일련번호는 3001이 되는 것. 이를 받는 쪽은 3001 ~ 3200번까지의 200바이트 데이터를 받았으니 다음엔 3201번부터 받을 차례이므로 확인 응답 번호로 3201을 보낸다. 이를 통해 내가 보낸 데이터가 잘 갔는지 확인이 가능하다. 데이터가 항상 올바르게 전달되는 것은 아니므로 일련 번호와 확인 응답 번호를 사용해 데이터가 손상되거나 유실된 경우가 확인되면 데이터를 재전송할 수 있는 것! 이를 재전송 제어라고 한다.

 

  이때까지 설명한 것은 세그먼트 하나를 보낼 때마다 확인 응답을 한 번 반환하는 통신이었다. 그러나 이 방법은 당연히 효율이 좋지 않다. 애시당초 연결형 통신 자체가 신뢰성/정확성을 우선시하는 통신이긴 하지만 그럼에도 불구하고 너무나도 극악의 효율이라는 것이다. 그러나 매번 하나 보내고 확인 응답 기다리고..를 반복하는 것보단 세그먼트를 연속해서 촤라락 보내고 난 다음에 확인 응답을 반환한다면 어떨까? 효율이 더 높아질 것이다! 그러나 이 경우 상대방 쪽에선 세그먼트가 점점 쌓일 것이라는 문제를 제기할 수 있는데, 받은 세그먼트를 일시적으로 보관할 수 있는 장소인 버퍼(buffer)가 있어서 괜찮다. 이 버퍼 덕분에 송신자 측에서 세그먼트를 연달아 보내도 수신 측은 대응이 가능하고 확인 응답의 효율이 높아진다. 

 

  그러나 버퍼의 크기는 정해져있기 마련이므로 너무 대량의 세그먼트를 받으면 처리하지 못하는 경우도 생긴다. 버퍼가 넘치게 되는 현상을 오버플로(overflow)라고 부르며, 이 오버플로가 발생하지 않도록 버퍼의 한계 크기를 알고 있어야 한다. 이것이 TCP헤더의 윈도우 크기 정보에 해당하는 부분이다. 윈도우 크기는 버퍼가 얼마나 많은 용량의 데이터를 저장해 둘 수 있는지에 대한, 즉 확인 응답을 일일이 하지 않고 연속해서 송수신 가능한 데이터 크기를 말한다. 이 윈도우 크기의 초깃값은 3-way handshake를 할 때 판단하며, 보내는 쪽의 윈도우 크기를 담아서 보내게 된다. 

 

ex) 컴퓨터 A가 B에게 SYN을 보낼 때 자신의 윈도우 크기를 담아서 보내고, B는 이를 통해 A의 윈도우 크기가 몇이구나를 알게 됨

 

이렇게 상대방의 윈도우 크기를 알게 됐다면 상대방 버퍼의 한계치를 알게 된 것이니, 세그먼트를 오버플로되지 않도록 보내면 되는 것!

 

TCP의 구조 [3] 포트 번호의 구조

  지금까지 연결 확보, 재전송 제어, 윈도우 크기에 대해 알아봤고 이들을 통해 TCP는 정확한 데이터 전송이 가능했다. 앞서 설명했듯 TCP는 이것 말고도 한 가지 역할을 더 하는데, 그건 바로 수신한 데이터가 어떤 애플리케이션에 가야 하는지 구분하는 역할이었다. 이를 위해 TCP헤더에는 출발지 포트 번호와 목적지 포트 번호에 대한 정보가 있고, 이를 통해 어떤 애플리케이션으로 가야 하는지 구분할 수 있다. 둘 다 16비트를 갖는  녀석으로 0 ~ 65535번까지의 포트번호를 쓸 수 있다고 생각할 수 있지만, 0 ~ 1023번 포트는 주요 프르토콜이 사용하도록 예약된 포트이다. 1024번은 예약되어 있지만 안 쓰는 포트고, 1025번 이상부터 랜덤 포트라고 부르며 클라이언트 측의 손싱 포트로 사용된다. 0 ~ 1023번 포트는 주로 서버 측 애플리케이션에서 사용된다.

 

  아무튼, 컴퓨터 상에서 애플리케이션들은 각각 포트 번호가 있어서 다른 애플리케이션과 구분되며, 데이터를 전송할 땐 상대방의 IP주소가 필요하지만 어떤 애플리케이션에게 줄 지를 결정하기 위해서 TCP는 꼭 포트 번호가 필요하다. 즉 일종의 창구 역할을 하는 것이 포트라고 생각할 수 있는 것. 

 

 

UDP의 구조

  UDP는 비연결형 통신이기 때문에 데이터를 전송할 때 TCP처럼 시간이 걸리는 확인 작업을 일일이 하지 않는다. 왜 why? 얘는 효율성을 더 우선시하는 통신방법이니까. 때문에 UDP의 장점은 데이터를 효율적으로 빠르게 보내는 것에 있기 때문에.

  

  TCP에서는 TCP헤더를 붙인 덩어리를 세그먼트라고 불렀던 반면 UDP는 UDP헤더를 붙인 덩어리를 UDP 데이터그램이라고 부른다. 이 녀석도 출발지 포트 번호, 목적지 포트 번호 등의 정보를 가진다. TCP와의 또 한 가지 차이점은 TCP는 번거롭게 여러 번 확인 응답을 보내며 전송하던 반면 UDP는 상대방을 확인하지 않고 연속해서 데이터를 보낸다! 즉 확인 응답 하지 않고 A쪽에서 B쪽으로 와다다다 데이터를 보내기만 하는 구조이다. 또한 UDP를 사용하면 랜에 있는 컴퓨터나 네트워크 장비에 데이터를 일괄로 보낼 수 있는데 이를 브로드캐스트(broadcast)라고 한다.

 

 

지금까지 전송계층에 대해 얘기했다. 다시 한 번 정리하자면, 전송 계층은 신뢰할 수 있는 데이터를 순차적으로 전달하는 역할을 하므로 상위 계층들이 데이터 전달의 유효성 등을(잘 전달됐을까..?) 신경스지 않도록 만들어준다! 상사한테 "신경쓰지 마십쇼. 제 선에서 처리하겠슴다"하면서 자기가 일일이 확인해주는 녀석.

 

 

  

+ Recent posts