쾌락코딩

백준 10451번 (순열사이클) with Java

|

문제

11003 문제 링크

profile

접근법

정답 비율이 60%가 넘을 만큼 간단하다. 그러나 이 문제는 주어진 숫자를 입력 받아서 그래프를 찾아내는 문제이기 때문에 그래프에 대한 이해와 bfs, 혹은 dfs에 대한 이해가 없으면 어려울 수도 있다.

문제 자체는 깔끔하고 이해하기가 쉽다. 문제를 다 읽고나면, 주어지는 숫자 하나하나의 인덱스와 주어진 값을 각각 노드로 생각해서 그 둘을 이어주면 될 것이라는 생각이 든다. 예를 들어 처음으로 3인 숫자가 들어왔다면, 인덱스가 1이니 노드1을 하나 만들어서 노드3를 향하게 하면된다. 이와 동일하게 두 번째로 2가 들어왔다. 이때 인덱스는 2다(두번째 값이니까). 따라서 노드2를 만들고, 방금 들어온 값인 노드2를 향하게 한다. 이 경우 노드2는 자기 자신을 가르키게 되는 그래프가 된다.

그래프는, 코드로 아래와 같이 구현하면 된다.

[
    1 : [ 3 ],
    2 : [ 2 ],
    3 : [ 7 ],
    4 : [ 8 ],
    5 : [ 1 ],
    6 : [ 4 ],
    7 : [ 5 ],
    8 : [ 6 ]
]

앞에 있는 key 값이 출발지 노드이고, value 배열 안에 들어있는 값들이 도착지 노드이다. 즉, 노드1은 노드3을 향하고, 노드3은 노드7을 향하는 것이다. 나는 개인적으로 이런 그래프 문제를 풀때 항상 노드를 class로 분리하는 것을 좋아하기 때문에, 별 내용은 없겠지만 Node 클래스를 만들었다. profile 이 노드는 자신이 향하고 있는 노드들을 child라는 리스트에 담을 것이다.

저런 그래프 관계를 구현하는건 단순한 작업이다. profile 이렇게 처음 빈 list에 입력 받은 n+1개의 크기만큼 노드를 생성한다. 이 노드에는 아직 어떤 child도 포함되어있지 않기 때문에 그냥 초기화라고 생각해도 된다. 이후 값을 하나씩 받아 들일 때 마다 그래프를 조금씩 채워나가는 것이다.

이제 1부터 N까지 모든 노드를 시작점으로 해서 bfs를 수행할 것이다. N개 만큼 bfs를 실행하는 것이다. 생각해보자, 1번 노드를 시작으로 bfs를 수행하면 1번 노드와 연결된 노드들 만이 선택(출력)될 것이다. 그말은 1번에서 시작된 bfs가 끝이난다면 그 순간 순열 사이클을 한개 찾은 것이다. 이제 2번 노드를 시작으로 bfs를 수행하자. 2번 역시 2번 노드와 연결된 노드들 만이 선택(출력)될 것이다. 3번도 마찬가지이다. 너무 간단하지않은가.

그런데 이 방법은 비효율적이다. 예시를 보아도 알겠지만 1번 노드에서 bfs를 시작하게 되면, 분명히 3번, 7번, 5번 노드를 거치게 된다. 즉, 3번 7번 5번 노드는 1번노드와 함께 같은 순열 사이클을 이루고 있다. 우리가 구하고자 하는 답은 순열 사이클의 갯수이기 때문에 3번 7번 5번을 시작으로 또 bfs를 돌 필요가 없다(5번에서 시작하나 1번에서 시작하나 결국 그것은 한개의 순열 사이클이기 때문).

3번 5번 7번 노드를 시작점으로 하는 bfs를 수행하지 않기 위해서 visited[] 배열을 사용하면 된다. 즉, 1번 노드를 시작으로 bfs를 돌면서 3,7,5번을 만날 때마다 3,7,5를 방문한 노드라고 표시해 두는 것이다.

글을 모두 읽었다면 아래 코드를 이해하는데 훨씬 수월할 것 같다.

import java.util.*;

public class B10451 {
    final static Scanner scanner = new Scanner(System.in);

    public static void main(String[] args) {
        int testCase = scanner.nextInt();
        for (int i = 0; i < testCase; i++) {
            List<Node> list = new ArrayList<>();
            int n = scanner.nextInt();
            int[] visited = new int[n + 1];
            Arrays.fill(visited, -1); // 방문한 노드는 1로 표기하자.
            int answer = 0;
            for (int k = 0; k < n + 1; k++) {
                list.add(new Node());
            }
            for (int j = 1; j <= n; j++) {
                int val = scanner.nextInt();
                list.get(j).child.add(val);
            }
            for (int j = 1; j <= n; j++) {
                // 이미 방문한 노드라면 bfs를 할 필요가 없다.
                if (visited[j] == -1) {
                    answer += bfs(list, visited, j);
                }
            }
            System.out.println(answer);
        }
    }

    public static int bfs(List<Node> list, int [] visited, int start) {
        Queue<Integer> q = new LinkedList<>();
        q.offer(start);
        visited[start] = 1;
        while (!q.isEmpty()) {
            int index = q.poll();
            for (int val : list.get(index).child) {
                if(visited[val] == -1) {
                    q.offer(val);
                }
                visited[val] = 1;
            }
        }
        return 1;
    }

    static class Node {
        List<Integer> child = new ArrayList<>();
    }
}

아차!

이문제는 굳이 Node를 따로 빼낼 필요는 없다. 어차피 이 Node에는 ArrayList하나만 들어있으므로 그냥 ArrayList로 곧장 사용해도 된다. 이 문제는 그게 더 편할지도 모른다. 그러나 여러 문제를 풀다보면 이 문제처럼 Node가 간단한게 아니라 Node만이 가지는 특성들이 필요한 경우도 많기 때문에 Node를 빼냈다. 정답은 없다.

백준 11003번 문제 (최솟값 찾기) with Java

|

문제

11003 문제

profile

접근법

슬라이딩 윈도우 기법을 이용해 접근했다. 우선 문제를 이해해 보면, 주어진 L 값 만큼을 범위로하여 최솟값을 찾는 문제이다. 예를 들어 예제의 경우 L이 3일 때, 0 0 1 중에 가장 작은 값, 0 1 5 중에 가장 작은 값, 1 5 2 중에 가장 작은 값, 5 2 3 중에 가장 작은 값, 2 3 6 중에 가장 작은 값(이하 생략)이렇게 총 세가지 숫자를 범위로하여 각각 최솟값을 찾는 문제다.

우선은 값을 딱 L개 정도 저장하고 있을 자료구조가 필요하다. 어차피 딱 L개의 숫자를 비교하고 그때 그때 출력하면 되니까 딱 L개 정도만 저장할 수 있으면 된다. 이 문제의 경우 Deque가 적당하다. 직관적으로 보아도 주어진 N개의 수를 차례대로대로 deque에 넣고, deque에 값이 L개 이상 찼으면 먼저 넣어진 값은 앞에서 빼주며 나중에 들어올 값은 뒤에서 넣어주면 유용하게 쓰일 것 같은 느낌이 온다.

이제 deque를 더 효율적으로 사용할 방법이 필요하다. 한 가지 컨셉이 필요한데, deque의 맨 앞에는 작은 값이오고, 뒤로 갈 수록 더 큰 숫자가 오게 하는 것이다. 이렇게 할 경우 해당 최솟값은 매번 deque의 제일 앞에 있는 숫자가 된다.

일단 한번 무식하게 생각을 해보았을 때는 아래와 같은 방법이 떠오른다. profile

여기서 왼쪽으로 갈수록 작은 값이 오게 하기위해, 매번 새로운 값이 들어올 때 마다 이미 들어있는 값과 비교해서 정렬을 한다. 그럼 인덱스 3 때는 아래와 같은 상황이 될 것이다. profile

잠시 여기서 생각해보자. 일단 여기서 하나가 더 들어올 경우 인덱스는 3일 테고, 그림은 아래와 같을 것이다. profile

먼저 현재 인덱스가 3이라서 인덱스 3인 박스가 들어왔다. 따라서 우리가 찾는 최솟값은 인덱스가 1,2,3 인것 중에서 찾아야 하니까 제일 먼저 들어온 인덱스가 0인 것은 빼준다. 그럼 나머지 것들 중에서 맨앞에 있는 것이 최소 값이다. 따라서 2가 최소값이다.

그런데 이 방법은, 맞긴 맞지만 루프를 돌아 새로운 값을 추가 할 때마다 매번 정렬을 해야하고, 빠져나가야 할 박스를 찾기 위해 가장 작은 index를 가진 박스를 찾아 제거해야 한다. 효율적이지 못하고, 제한시간 내에 문제를 풀지 못한다.

여기서 조금의 응용이 필요하다. 매번 정렬을 하지 말고, 그냥 새로들어올 값을 deque에 들어있는 값들과 뒤에서부터 비교해서 자신 보다 큰 값들은 뺀 후, 새로 들어올 값을 넣어도 되지 않을까? 왜냐하면, 어차피 deque에 들어갈 값이라면 deque에 함께 있을 다른 값들 보다 수명이 더 길테니까. 즉, 새로들어올 값보다 큰 값은 절대로 쓸모가 없기 때문이다.

그림을 다시 보자. profile 현재 deque는 이 상황이다. 이제 {값:3, 인덱스:3} 인 박스가 들어올 것인데, 정렬을 하지 말고 차례대로 넣자. 대신 자신보다 값이 큰 값이 있다면 그 것을 pop해주고 자신을 넣자. 왜냐하면, 값이 5인 박스는 무조건 뒤에 들어올 값이 3인 박스보다 먼저 소멸된다. 즉, deque에 값1, 값5인 값 두개만 있다면 값: 1인 박스가 사라질 경우 최솟값이 값 : 5가 될 수 도 있겠지만, 값 5가 deque에 존재하는 상황에서 새로들어오는 박스의 값이 5보다 작다면 5는 절대 답이 될 수 없기 때문이다. 값 :5 인 박스가 아무리 지지고 볶아도 값:5인 박스와 값:3인 박스는 함께 deque에 존재한다(게다가 값:5의 인덱스가 빠르니까 값:5 박스가 먼저 사라진다).

결국 새로들어올 값보다 더 큰 값들을 삭제하고나서 새로운 값을 넣으면, deque는 자연스럽게 정렬이 된 상태일 것이다. 아래 그림처럼. profile

새로운 값이 들어올 때 마다 이 알고리즘을 반복하면서 deque에 있는 가장 먼저 들어온 값, 즉 가장 먼저 들어온 값인 deque[0]의 index를 보고, 이 박스가 이제 L의 범위를 벗어나 필요없다고 판단되면 빼준다.

코드

import java.io.*;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Scanner;
import java.util.StringTokenizer;

public class B11003 {
    public static final Scanner scanner = new Scanner(System.in);

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        int n = Integer.parseInt(st.nextToken());
        int l = Integer.parseInt(st.nextToken());

        st = new StringTokenizer(br.readLine());

        Deque<Node> deque = new LinkedList<>();
        for (int i = 0; i<n; i++) {
            int temp = Integer.parseInt(st.nextToken());

            // 새로들어올 박스의 값보다 더 큰 값이 있다면 pop해주자. 
            while(!deque.isEmpty() && deque.getLast().value > temp) {
                deque.removeLast();
            }
            deque.addLast(new Node(temp, i));
            // 너무 오래되서 빼야할 박스들을 빼는 부분
            if (deque.getFirst().index <= i -l) {
                deque.removeFirst();
            }
            bw.write(deque.getFirst().value + " ");
        }
        bw.flush();
        bw.close();
    }

    static class Node {
        public int value;
        public int index;

        Node(int value, int index) {
            this.value = value;
            this.index = index;
        }
    }
}

브라우저에 URL을 입력하면 일어나는 일 (2)

|

이전 장에서 브라우저가 os의 프로토콜 스택으로 http request 메시지를 전달해 주는 것 까지 알아보았다. 이제 프로토콜은 전달 받은 메시지를 어떻게 처리하는지 알아볼 차례다.

프로토콜 스택

프로토콜 스택이 무엇인지 알기 전에, 프로토콜은 어디서 정보를 얻어오고, 또 어디로 요청을 하는지, 그 흐름을 그려보았다.

protocol_stack

큰 흐름은 위에서 아래로 흐른다. 프로토콜 스택은 app(브라우저)에서 메시지를 전달 받는다. 메시지를 아주 작은 데이터인 패킷들로 나누고, 수신처 주소등의 제어 정보를 덧붙인다. 그리고 프로토콜 스택과 LAN어댑터가 연대하여 패킷을 전기나 빛의 신호로 변환하고, LAN어댑터가 실제로 송수신 동작, 즉 케이블에 대해 신호를 송수신하는 동작을 실행한다.

결과적으로 프로토콜 스택이란, TCP 프로토콜을 사용하여 데이터 송수신을 담당하는 부분과, UDP를 사용하여 송수신을 담당하는 부분, 또 IP를 사용하는 부분의 집합에 다른 부가적인 기능이 보함된 것이라고 볼 수 있을 것 같다.

서버에 접속 (통신 가능한 상태로 만들기- 통로 만들기)

브라우저에서 프로토콜 스택으로 메시지를 전달하면 프로토콜 스택은 여러가지 부가정보를 메모리에 저장한다. 부가적인 정보란, 통신 동작을 제어하기 위한 제어 정보인데, 예를 들면 통신 상대의 IP주소, 포트 번호, 오류 유무, 전송 시간, 데이터 오프셋 등등 꽤 많은 정보가 있다. 프로토콜 스택은 이 제어 정보를 참조하면서 오류를 제어하거나, 재송신하거나 등등의 일을 한다.

부가적인 정보는 누가 어떻게 저장하는가?

프로토콜 스택이 tcp 통신을 의뢰받았다면 부가적인 정보들은 위의 그림에서 tcp박스가 담당한다. 정확히 말하자면 부가적인 정보들을 담을 메모리 확보 절차를 소켓 작성이라고 한다. tcp박스, 더 큰 개념으로 프로토콜 스택이 소켓을 작성을 완료하면 브라우저에게 이 사실을 알리고, 언제든 이 소켓을 사용해 통신할 수 있게끔 일종의 리모콘(?)을 준다.

부가적인 제어 정보는 매번 전송된다.

tcp_header

그림에서 보듯이 통신을 위한 부가적인 제어 정보는 포트 번호, 시퀀스 번호(송신 데이터의 일련 번호), ACK 번호(수신 데이터의 일련 번호) 등 많은 정보가 있다. 이런 정보는 클라이언트와 서버가 대화할 때 마다 늘 필요하다. 따라서 클라이언트와 서버 사이에 주고받는 패킷의 맨 앞부분에 추가하는데, 이 추가하는 영역을 헤더 영역이라고 한다. 물론 ip영역에서도 ip나름의 헤더가 붙게된다.

3-way-handshake

클라이언트 컴퓨터의 tcp 부분에서 헤더를 만들고나면 tcp역시 ip 부분에 헤더를 건네주며 송신을 의뢰한다. IP부분이 패킷 송신 동작을 실행하고 네트워크를 통해 패킷이 서버에 도착하면 서버측의 IP부분이 이것을 받아 서버측의 TCP 부분으로 넘겨주는 방식이다.

TCP통신 방식은, 최초 통신때 이런 방식으로 서버와 클라이언트 사이에 파이프와 같은 연결 통로를 확보해야 한다. 파이프가 연결되어야 데이터를 마음껏 주고 받을 수 있기 때문인데, 이 초기 설정 과정에 3-way-handshake 방식이 사용된다.

쉽게 설명하자면 아래와 같이 3단계를 거친다.

  1. 클라이언트 : 서버야, 너와 통신하고 싶어! 너가 이 메세지를 받았다면 잘 받았다고 나한테 알려줘. 그럼 이만!

  2. 서버 : 그래 클라이언트야. 네가 보낸 메세지 잘 받았어 우리 통신하자! 그나저나 내 메세지도 잘 전달 될지 모르겠네? 너도 내 메세지를 정상적으로 받았다면 나한테 다시 알려줘~

  3. 클라이언트 : 메세지 잘 받았어! 이제 서로 통신이 잘 된다는 것을 알았으니 마음편히 통신하자 :smile:

이렇게 연결이 되고 나면 둘중 한 측이 close를 호출하여 연결을 끊을 때까지 연결이 계속 존재한다.

더 깊고 자세하고 세련되고 훌륭한 내용은 한재엽님의 좋은 자료를 참조하자.

TCP가 신뢰성있게 데이터를 송수신하는 원리

tcp헤더에는 sequence number와 ack가 있다. tcp를 통해 신뢰성 있는 전송을 하기 위해서는 sequence number와 ack가 상당히 중요하다.

  • Sequence Number : tcp는 애플리케이션(브라우저)에서 넘겨 받는 데이터가 크다면 이를 여러개의 패킷(세그먼트)으로 쪼개어 서버로 전송한다. 이때 세그먼트들의 순서를 의미하는 것이 sequence number라고 볼 수 있다. 예를 들어 3000byte 짜리 파일을 전송한다고 하자. 이때 한번에 보낼 수 있는 데이터가 100byte라면 tcp는 30개의 세그먼트로 쪼개어 서버로 전송할 것이다. 이럴 경우, 각 세그먼트의 sequence number field에 순서를 의미하는 번호를 붙인다. 1,2,3 이렇게 나타내는게 아니라 byte형식으로 나타낸다. 즉, 첫 번째 세그먼트가 0이라면, 두 번째는 100, 세 번째는 200. 이런 식으로 Sequence Number를 할당하여 서버측으로 함께 보낸다.

  • ACK : 받은 것에 대한 응답이다. 즉, 클라이언트에서 요청을 받은 후, 다시 클라이언트로 보내는 값이다. 어떤 값을 다시 클라이언트로 보낼까? 서버는 클라이언트로부터 앞으로 받아야 할 다음 데이터의 squence number를 ACK로 설정하고 보낸다. 즉, 클라이언트로 부터 sequence number가 0인 세그먼트를 받았다면, 서버는 이에 대한 응답으로 ack : 100 을 응답한다. 0번을 받았으니 이제 다음으로 받고 싶은 sequence number 100를 ack로 보내는 것이다.

이렇게 할 경우, 클라이언트 측이 서버로부터 받을 것이라고 기대했던 ack값이 오지 않을 경우 tcp가 다시 해당 세그먼트를 전송하기 때문에 신뢰가 있다. 자세히 들어가면 더 복잡한 방법으로 신뢰적이지만 여기서는 여기까지만 포스팅 하겠다.

NEXT

앞에서 언급했지만 결국 TCP도 IP에게 의뢰한다. TCP에 대해서 얕게 알아보았으니 IP에 대해서도 어느 정도 알아보겠다.

fetch사용시 유의 사항 (json() 함수 사용하기)

|

js 개발자들은 network request 요청이 필요할 경우 대부분 axios.js나 기타 다른 라이브러리를 쓰는 것 같다. js에서 기본적으로 제공하는 fetch라는 함수가 있지만, fetch를 사용할 경우 응답받은 body데이터의 form을 직접 변환해야 하는데, 이 단계를 자동으로 해주기 때문이고, 에러 핸들링도 편하기 때문이다.

나 역시도 axios나 기타 HTTP requests libarary를 다루는 방법은 잘 알고있다. 그러나 vanilla JS로 사이드 프로젝트를 하면서 fetch를 사용하게 되었는데, axios가 개발자 몰래 자동으로 해준 한 단계가 무엇인지 몰라서 막힌 부분이 있었다.

fetch로는 데이터를 바로 사용할 수 없다.

fetch를 사용할 땐 두 단계를 거쳐야 한다. 먼저 올바른 url로 요청을 보내야 하고, 바로 뒤에오는 응답에 대해 json()을 해줘야 하는 것이다(axios는 json()과정을 자동으로 해주는 셈이다).

fetch(`https://api.openweathermap.org/data/2.5/weather?lat=${lat}
&lon=${lon}&appid=${API_KEY}`)
    .then(res => console.log(res));

위의 코드는 openweathermap라는 사이트에서 현재 위치의 날씨를 가져오는 코드이다. 실행하면 아래와 같은 결과가 나온다.

fetch

그러나 이것은 내가 원했던 정보가 아니다. openweathermap에 의하면 내가 반환 받아야 할 데이터는 아래와 같다.

{"coord":{"lon":139.01,"lat":35.02},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"base":"stations","main":{"temp":285.514,"pressure":1013.75,"humidity":100,"temp_min":285.514,"temp_max":285.514,"sea_level":1023.22,"grnd_level":1013.75},"wind":{"speed":5.52,"deg":311},"clouds":{"all":0},"dt":1485792967,"sys":{"message":0.0025,"country":"JP","sunrise":1485726240,"sunset":1485763863},"id":1907296,"name":"Tawarano","cod":200}

res의 body에 담겨있을 것 같지만 그렇지 않았다. 이 body는 스트림이고, 데이터가 완전히 다 받아진 상태가 아닌것이다. 이 걸로는 날씨 데이터를 받아와 데이터를 뿌려줄 수가 없다. 그래서 res 객체의 json()이라는 메서드를 사용한다. json()은 Response 스트림을 가져와 스트림이 완료될때까지 읽는다. 그리고 다 읽은 body의 텍스트를 Promise형태로 반환한다.

따라서 서버가 주는 json데이터를 사용하고 싶다면 아래와 같은 코드를 작성해야 한다.

fetch(`https://api.openweathermap.org/data/2.5/weather?
lat=${lat}&lon=${lon}&appid=${API_KEY}&units=metric`)
    .then((res) => {
        return res.json(); //Promise 반환
    })
    .then((json) => {
        console.log(json); // 서버에서 주는 json데이터가 출력 됨
    });

반면 axios를 사용할 경우 res.json()단계를 넘어가도 좋다. 그러나 axios로 받아오는 데이터는 서버에서 넘겨주는 body데이터 외에 부가적인 정보도 포함되어 있기 때문에 원하는 data만 골라서 사용해야 한다. 이부분은 axios 공식문서를 보는편이 낫겠다.

참고자료

브라우저에 URL을 입력하면 일어나는 일 (1)

|

어느 사이트에선가 “브라우저에 URL을 입력한 후 원하는 화면이 보일 때까지 어떤 일이 일어나는지 아는가?” 라는 질문을 보았다. 아마 어느 스타트업의 cto님께서 이런 질문에 대답을 할 줄 아는 사람을 원한다고 적은 글이었던 것 같다. 생각해보면 학교에서 네트워크를 배우긴했지만 tcp/ip의 장단점, arp의 역할 등등을 알고만 넘어갔지 실제로 어떻게 통신이 되고, 그것들이 어떻게 쓰이는 몰랐다. 그래서 한번 이 질문에 답변을 해보고자 큰 틀로 정리를 하려고 한다.

in 웹 브라우저

우선은 웹 브라우저에 url을 입력하는 것으로 통신은 시작된다. 그럼 사용자가 url을 입력하고 엔터를 누르면 무슨 일이 일어나는가?

http request 메시지를 만든다

http 요청 메시지는 아래와 같이 되어있다. http_request 위의 그림처럼 어떤 행위(get,post,delete,put 등등)를 할 것인지, uri가 무엇이고 http version이 몇인지, 그리고 부가적인 자세한 정보가 필요한 경우 Header값을 설정할 수 있다. 마지막으로 post와 같이 클라이언트에서 서버로 어떤 데이터를 전달 해야 할 경우 body영역에 데이터를 싣는다. 우리가 url을 입력하면 브라우저는 이를 해독해 그림과 같은 http 메세지를 자동으로 만들어 준다.

Header의 종류도 다양하고, method의 종류도 다양하지만 여기서 다루진 않겠다.

OS에 데이터 송출을 의뢰한다

HTTP 메세지를 만들면 브라우저는 이를 OS에 의뢰한다(정확히는 프로토콜 스택). 브라우저 자체에는 이 메세지를 네트워크에 송출하는 기능이 없기 때문이다. 그런데 OS에 의뢰를 할 때는 도메인 명이 아니라 ip주소로 메세지를 받을 상대를 지정해야 한다. 이 과정에서 DNS서버를 조회해야 한다.

브라우저는 어떻게 도메인 주소를 IP주소로 변경할까?

사실 아주 정확하게는 모르지만, 큰 흐름은 이렇다. socket이라는 라이브러리가 있는데, 이는 네트워크의 기능을 애플리케이션 단에서 호출할 수 있게 하는 표준 라이브러리이다. 브라우저는 이 라이브러리를 사용해서 dns서버로 도메인 주소를 보내면, dns가 ip주소로 응답을 준다. 그런데 결국 socket을 이용해 dns로 어떤 요청을 보낸다는 것 역시 OS 내부에 포함된 프로토콜 스택을 호출하여 의뢰하는 것이다. 프로토콜 스택이란 간단하게 “os 내부에 내장된 네트워크 제어용 tcp/ip 소프트웨어” 라고 생각하면 될 것 같다.

이제 정말 프로토콜 스택에 메세지 송신을 의뢰한다

ip주소를 얻었으면 이제 웹 서버에 메세지를 송신하도록 os내부의 프로토콜 스택에 의뢰한다. 그러나 사실 의뢰한다는 것은 결국 또 socket 라이브러리를 사용하는 것이다. 위에서 프로토콜 스택은 tcp/ip 소프트웨어라고 설명했는데, tcp는 연결형 이라는 말을 많이 들어보았다. 여기서 그 원리가 나온다.

즉, socket 라이브러리를 이용해 의뢰를 하게되면 데이터를 송수신 하는 컴퓨터 사이에 데어터의 통로 같은 것이 생긴다. 클라이언트와 서버에 데이터 전용의 긴 통로가 생기는 것이다. 이 통로를 통해 데이터를 주고 받을 수 있으며 어느 쪽에서 데이터를 보내든 상관은 없다. 그러나 수업시간에 tcp의 단점으로 배웠던게 하나 있는데, 바로 초기 회선(통로) 설정 비용이다. 즉 데이터를 송수신 하기 전에 매번 이 통로를 설정해야 하는 단점이 있다.

결과적으로 클라이언트에도, 서버에도 통로의 입구가 존재하게 된다. 그리고 이 입구를 socket이라고 부른다. 소켓을 만들고 통신하는 과정은 크게 보면 4단계가 있다.

  1. 서버와 클라이언트 모두 소켓을 만든다.
  2. 서버측의 소켓에 클라이언트의 소켓이 다가가서 통로를 연결한다.(접속)
  3. 데이터를 송수신 한다.
  4. 송수신이 끝나면 통로를 분리하고 소켓을 없앤다.

이와 같은 과정이 일반적인데 http는 조금 비효율적인 부분이 있다. 그 이유는, http 특성상 html 문서, 이미지 파일 등의 데이터를 하나하나 별도의 것으로 취급하기 때문에 여러개의 이미지를 요청할 경우 매번 각각의 새로운 통신을 해야 한다는 점이다. (물론 이를 해결하기 위한 방법은 존재한다)

NEXT

다음 포스트에는 프로토콜 스택이 정확히 무엇이고 어떻게 데이터를 전송 하는지에 대해, TCP와 IP가 무슨 역할을 하는지 알아보겠다.