학습 자료

main 이 끝나면 프로그램은 죽는다 — 서버는 왜 안 죽는가


Article

자바 편에서 우리가 만든 프로그램은 전부 이렇게 생겼었다.

public class Hello {
    public static void main(String[] args) {
        System.out.println("안녕");
    }
}
javac -encoding UTF-8 Hello.java
java Hello
터미널
  • 안녕
  • $

"안녕"을 찍고, 프롬프트가 돌아왔다. 프로그램이 죽었다는 뜻이다.

이건 실패가 아니다. 정상이다. main 의 마지막 줄이 끝나면 프로그램은 할 일을 다 한 것이고, JVM 은 짐을 싸서 나간다. 자바 편 2강에서 JVM 이 main 을 찾아 들어간다고 했다. 들어갔으면 나오기도 한다.

그런데 웹사이트는 그러지 않는다. 새벽 세 시에 주소창에 주소를 쳐도 답이 온다. 어제도 답했고 오늘도 답한다. 누군가 죽지 않고 계속 기다리고 있다는 뜻이다.

이번 강의는 Spring 을 쓰지 않는다. 설치도 하지 않는다. 대신 지금까지 배운 자바만으로 서버 비슷한 것을 하나 만들어보고, 그 다음부터 이걸 다시는 안 만들게 되는 이유를 본다.

우리 프로그램에는 없던 두 가지

서버가 우리가 만들던 프로그램과 다른 점은 둘이다.

지금까지의 프로그램 vs 서버

java Hello

자바 편에서 만든 것

  • 할 말을 하고 죽는다. main 이 끝나면 끝
  • 밖에서 아무것도 받지 않는다. 값은 전부 코드 안에 적혀 있었다
  • 결과는 System.out.println 으로 나에게만 보인다

서버

지금부터 만들 것

  • 안 죽는다. 끝내지 않고 기다린다
  • 밖에서 들어오는 것을 받는다. 누가, 무엇을 달라고 하는지
  • 결과를 다른 컴퓨터에게 돌려준다

두 번째 항목을 한 번 더 짚고 싶다. 자바 편 스물한 강을 통틀어, 우리 프로그램은 바깥에서 무언가를 받아본 적이 단 한 번도 없다. 더할 숫자도, 담을 책 제목도 전부 코드에 직접 적었다. 프로그램은 자기가 아는 것만 말하고 죽었다.

지금부터는 반대다. 무엇을 달라고 할지 모르는 채로 켜두고, 물어보면 그때 답한다. 이 시리즈는 사실상 그 한 문장을 스물네 강에 걸쳐 푸는 것이다.

안 죽는 프로그램

죽지 않으려면 main 이 끝나지 않으면 된다. 방법은 간단하다.

while (true) {
    // 여기서 뭔가 기다린다
}

문제는 "뭔가"다. 그냥 while (true) { } 라고 쓰면 프로그램은 안 죽지만, 대신 CPU 를 100% 태우면서 아무 일도 안 한다. 우리에게 필요한 건 아무 일도 안 하면서 조용히 멈춰 서서 기다리다가, 누가 오면 깨어나는 것이다.

자바는 그 도구를 표준으로 갖고 있다. ServerSocket 이다.

서버가 하는 일은 결국 이 네 가지다
  1. 포트를 연다8080 번 문에 자리를 잡는다
  2. 기다린다accept() 에서 멈춰 선다
  3. 받는다누가 무엇을 달라는지 읽는다
  4. 답한다글자를 돌려주고 연결을 끊는다
그리고 다시 2번으로 돌아간다. 이 고리가 끊기지 않는 한 서버는 죽지 않는다.

포트가 뭔가

한 컴퓨터에는 프로그램이 여러 개 떠 있다. 밖에서 편지가 왔을 때, 그게 누구에게 온 편지인지 구분해야 한다.

포트는 그 구분에 쓰는 번호다. 컴퓨터가 건물이라면 포트는 호실 번호다. 주소(IP)로 건물을 찾고, 포트로 호실을 찾는다.

8080 은 아무 뜻 없는 숫자다. 개발할 때 관습적으로 쓰는 번호일 뿐이고, 9999 로 해도 된다. 다만 이미 다른 프로그램이 쓰고 있는 번호를 고르면 자리를 못 잡는다. 그 얘기는 잠시 뒤에 한다.

직접 만들어본다

아래가 전부다. 자바 편에서 배운 것만 썼다 — 클래스, main, while, try 없는 throws, 그리고 String.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws Exception {
        ServerSocket server = new ServerSocket(8080);
        System.out.println("8080 포트에서 기다린다");

        while (true) {
            Socket client = server.accept();
            System.out.println("누가 왔다");

            BufferedReader in = new BufferedReader(
                    new InputStreamReader(client.getInputStream(), "UTF-8"));
            String first = in.readLine();
            System.out.println("첫 줄: " + first);

            String body = "안녕";
            byte[] bodyBytes = body.getBytes("UTF-8");

            String head = "HTTP/1.1 200 OK\r\n"
                    + "Content-Type: text/plain; charset=utf-8\r\n"
                    + "Content-Length: " + bodyBytes.length + "\r\n"
                    + "\r\n";

            OutputStream out = client.getOutputStream();
            out.write(head.getBytes("US-ASCII"));
            out.write(bodyBytes);
            out.flush();
            client.close();
        }
    }
}

컴파일하고 실행한다.

javac -encoding UTF-8 Server.java
java Server
터미널
  • 8080 포트에서 기다린다
  • 커서가 깜빡인다. 프롬프트가 돌아오지 않는다.

프롬프트가 안 돌아온다. Hello 를 실행했을 때와 결정적으로 다른 점이다. 프로그램이 안 죽었다.

죽지 않은 이유는 while (true) 때문이 아니다. server.accept() 때문이다. 이 메서드는 누가 접속할 때까지 그 자리에서 멈춰 선다. CPU 를 태우지 않고, 그냥 잠들어서 기다린다. 이걸 블로킹이라고 부른다.

자바 편에서 배운 메서드는 전부 불렀다가 곧바로 돌아왔다. accept() 는 안 돌아온다. 누가 오기 전까지는.

말을 걸어본다

터미널을 하나 더 연다. 서버는 켜둔 채로 둔다.

curl 은 주소로 요청을 보내고 답을 받아 찍어주는 도구다. -i 를 붙이면 답의 머리 부분까지 보여준다.

curl -i http://localhost:8080/
두 번째 터미널
  • HTTP/1.1 200 OK
  • Content-Type: text/plain; charset=utf-8
  • Content-Length: 6
  • 안녕

그리고 서버를 켜둔 첫 번째 터미널을 보면, 없던 줄이 늘어 있다.

서버를 켜둔 터미널
  • 8080 포트에서 기다린다
  • 누가 왔다
  • 첫 줄: GET / HTTP/1.1

여기서 두 가지를 확인했다.

하나. accept() 가 깨어났다. 잠들어 있다가 curl 이 접속하자 일어나서 Socket 하나를 돌려줬다. 그리고 답을 보낸 뒤 while 을 한 바퀴 돌아 다시 accept() 에서 잠들었다. 서버는 여전히 살아 있다. 한 번 더 curl 을 치면 또 답한다.

둘. 우리 프로그램이 생애 처음으로 바깥에서 글자를 받았다. 그 글자가 이것이다.

GET / HTTP/1.1

우리는 이 문장을 해석하지 않았다. readLine() 으로 한 줄 읽어서 그대로 화면에 찍었을 뿐이다. 무슨 뜻인지도 모른 채, 누가 오기만 하면 무조건 "안녕"을 돌려줬다.

우리가 만든 것은 서버가 아니다

방금 만든 것은 서버 흉내다. 진짜 서버라면 해야 하는데 안 하는 일이 널려 있다.

빠진 것들
해야 하는 일우리 코드는어디서 배우나
GET / HTTP/1.1 이 무슨 뜻인지 안다읽고 버린다2강
/books/users 를 다르게 대접한다전부 "안녕"3강 · 9강
가져가기(GET)와 새로 만들기(POST)를 구분한다구분 안 한다3강
없는 것을 달라고 하면 없다고 답한다무조건 200 OK3강 · 21강
글자가 아니라 객체를 주고받는다손으로 조립한 글자10강 · 11강
이걸 전부 손으로 채우면 프레임워크 하나를 새로 쓰게 된다. 그래서 안 쓴다.

Content-Length: 6 을 손으로 계산해서 넣은 것을 보라. "안녕"이 6바이트라서 6이다. 한 글자만 늘려도 이 숫자를 고쳐야 하고, 안 고치면 응답이 잘린다. 답 하나 돌려주는 데 이런 걸 신경 써야 한다면, 정작 하고 싶었던 일에 쓸 힘이 남지 않는다.

우리가 하고 싶은 일은 책 목록을 돌려주는 것이지, 바이트 수를 세는 게 아니다.

같은 포트로 하나 더 띄웠을 때
  • Exception in thread "main" java.net.BindException: Address already in use: bind
  • at java.base/sun.nio.ch.Net.bind0(Native Method)
  • at java.base/java.net.ServerSocket.<init>(ServerSocket.java:167)
  • at Server.main(Server.java:9)

스택 트레이스는 위가 터진 곳, 아래가 시작한 곳이라고 자바 편 18강에서 배웠다. 그러니 맨 아래를 보면 된다. Server.java 의 9번째 줄, new ServerSocket(8080) 에서 터졌다.

그래서 Spring 을 쓴다

Spring Boot 가 하는 일을 한 줄로 줄이면 이렇다.

while (true)accept() 를 대신 돌려주고, 우리에게는 "무엇을 답할지"만 적게 한다.

같은 일을 하는 두 코드

직접 만든 서버

방금 짠 것

  • ServerSocketnew 한다
  • while 을 돌린다
  • accept() 로 기다린다
  • 요청을 손으로 읽는다
  • 응답 헤더를 손으로 조립한다
  • "안녕"이라는 글자는 딱 한 줄이다

Spring Boot

9강에서 실제로 쓴다

  • @RestController 를 붙인다
  • 메서드 하나를 적는다
  • return "안녕";
  • 나머지는 전부 프레임워크가 한다

여기서 이 시리즈 전체를 관통하는 첫 번째 질문이 나온다.

우리는 방금 new ServerSocket(8080) 이라고 썼다. new 를 우리 손으로 쳤다. 자바 편에서 객체는 언제나 우리가 만들었다. 그런데 Spring 을 쓰기 시작하면 이상한 일이 벌어진다. 어디에도 new 가 없는데 객체들이 만들어져서 서로 연결되어 있다.

누가 만들었나. 그 질문에 5강부터 답한다.

정리하면

서버는 죽지 않고 기다리는 프로그램이고, 우리는 그 기다리는 코드를 직접 쓰지 않게 된다.

다시 짚어보기
  1. 01

    main 이 끝나면 프로그램은 죽는다

    자바 편에서 만든 프로그램은 전부 할 말을 하고 죽었다. 그게 정상이다.

  2. 02

    서버는 accept() 에서 잠들어 기다린다

    CPU 를 태우지 않고 멈춰 서 있다가, 누가 접속하면 깨어난다. 이걸 블로킹이라고 한다.

    Socket client = server.accept();   // 여기서 멈춘다
  3. 03

    포트는 컴퓨터 안의 호실 번호다

    이미 누가 살고 있으면 BindException 이 난다. 앞으로 가장 자주 볼 에러다.

  4. 04

    우리 프로그램이 처음으로 바깥의 글자를 받았다

    GET / HTTP/1.1 을 받아서 그대로 찍었다. 무슨 뜻인지는 아직 모른다. 2강에서 본다.

  5. 05

    new 를 우리가 쳤다는 것을 기억해둘 것

    new ServerSocket(8080). 앞으로 이 new 가 사라진다. 누가 대신 하는지가 5강의 주제다.

    ServerSocket server = new ServerSocket(8080);

다음 강의에서는 방금 읽고 버린 그 한 줄을 제대로 본다.

GET / HTTP/1.1

세 덩어리로 되어 있고, 각각 이름이 있다. 그리고 우리가 손으로 조립해서 돌려준 HTTP/1.1 200 OK 도 마찬가지다. HTTP 는 어렵지 않다. 사람이 읽을 수 있는 글자 몇 줄이 전부다. 2강에서 확인한다.

main 이 끝나면 프로그램은 죽는다 — 서버는 왜 안 죽는가 — 디코드랩(DCODELAB)