💋 인트로
이 포스팅은 우아한테크코스 5기 깃짱이 톰캣 구현하기 미션 후 작성했습니다.
SpringBoot를 사용하게 되면 내장된 톰캣
을 사용한다.
물론 알빠 NO 하고 사용해도 되지만, 어떻게 작동하는지 알 수 있다면 더 섬세하게 튜닝하고 디버깅할 수 있을 것이다.
이번 미션에서는 HTTP Server가 어떤 일을 하는지 알아보기 위해서 톰캣 서버의 역할 중 일부를 직접 구현해 보았다.
💋 코드 저장소
최종 코드: https://github.com/woowacourse/jwp-dashboard-http/tree/eunkeeee
1, 2단계: https://github.com/woowacourse/jwp-dashboard-http/pull/342
3단계: https://github.com/woowacourse/jwp-dashboard-http/pull/404
4단계: https://github.com/woowacourse/jwp-dashboard-http/pull/468
미션을 하면서 진행한 순서로 서술하겠다!
💋 HTTP Server란?
✔️ 개념
- HTTP Server는 클라이언트의 HTTP 요청을 받아들이고, 해당 요청을 처리한 후 HTTP 응답을 반환하는 서버
- 클라이언트가 웹 브라우저를 통해 웹 페이지나 리소스를 요청
- HTTP Server는 해당 요청을 받아들여 필요한 작업을 수행한 후, 클라이언트에게 요청한 데이터를 응답
쉽게 말해서 HTTP 요청을 처리하고, HTTP 응답을 반환하는 서버다.
✔️ 동작 과정
스프링 부트에서 HTTP Server의 역할을 하는 톰캣은 아래와 같이 작동한다.
💋 HTTP Server의 역할
✔️ HTTP 요청 메세지 파싱 & HTTP 응답 메세지 생성
HTTP 요청 메시지를 개발자가 직접 파싱해서 사용해도 되지만, 매우 불편할 것이다.
아래는 HTTP 요청 메세지의 예시인데, 아래 내용은 뭉쳐져 있는 텍스트 형태로 들어오지만, 애플리케이션에서 처리하기 위해서는 텍스트 속의 의미를 파악하기 위해서 각 부분을 START LINE, HTTP 메소드, URL, 쿼리 스트링, 스키마, 프로토콜 헤더, 바디 등으로 직접 나눠야만 할 것이다.
POST /save HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
username=kim&age=20
서블릿은 개발자가 HTTP 요청 메시지를 편리하게 사용할 수 있도록 개발자 대신에 HTTP 요청 메시지를 파싱한다. 그리고 그 결과를 HttpServletRequest 객체에 담아서 제공한다.
아래는 내가 만든 HttpReqeust 클래스로, 내부에 파싱에 관한 로직을 모두 포함한다.
package org.apache.coyote.http11.request;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
@Getter
@RequiredArgsConstructor
public class HttpRequest {
private static final Logger log = LoggerFactory.getLogger(HttpRequest.class);
private final HttpRequestStartLine httpRequestStartLine;
private final HttpRequestHeader httpRequestHeader;
private final HttpRequestBody httpRequestBody;
public static HttpRequest from(BufferedReader bufferedReader) throws IOException {
HttpRequestStartLine httpRequestStartLine = parseRequestStartLine(bufferedReader);
HttpRequestHeader httpRequestHeader = parseRequestHeaders(bufferedReader);
HttpRequestBody httpRequestBody = parseRequestBody(httpRequestHeader.contentLength(), bufferedReader);
return new HttpRequest(httpRequestStartLine, httpRequestHeader, httpRequestBody);
}
private static HttpRequestStartLine parseRequestStartLine(BufferedReader bufferedReader) throws IOException {
String requestStartLine = bufferedReader.readLine();
if (requestStartLine == null) {
log.info("잘못된 형태의 요청!");
throw new IllegalArgumentException();
}
return HttpRequestStartLine.from(requestStartLine);
}
private static HttpRequestHeader parseRequestHeaders(BufferedReader bufferedReader) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
String line = bufferedReader.readLine();
while (!"".equals(line)) {
stringBuilder.append(line).append("\r\n");
line = bufferedReader.readLine();
}
return HttpRequestHeader.from(stringBuilder.toString());
}
private static HttpRequestBody parseRequestBody(String contentLength, BufferedReader bufferedReader) throws IOException {
if (contentLength == null) {
return HttpRequestBody.none();
}
int length = Integer.parseInt(contentLength);
char[] httpRequestBody = new char[length];
bufferedReader.read(httpRequestBody, 0, length);
return HttpRequestBody.from(new String(httpRequestBody));
}
}
마찬가지로, 발생한 응답을 HTTP 프로토콜에서 약속된 형태로 생성하는 역할을 담당해준다.
아래는 내가 만든 HttpResponse 객체로, 응답 메세지를 생성한다.
package org.apache.coyote.http11.response;
import lombok.Builder;
import lombok.Getter;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.util.Map;
import java.util.stream.Collectors;
import static org.apache.coyote.http11.common.HttpProtocolVersion.HTTP11;
@Getter
public class HttpResponse {
private static final String CRLF = "\r\n";
private static final String BLANK_LINE = "";
private static final String BLANK_SPACE = " ";
private HttpResponseStatusLine httpResponseStatusLine;
private HttpResponseHeaders httpResponseHeaders;
private HttpResponseBody httpResponseBody;
public HttpResponse() {
this(null, null, null);
}
@Builder
public HttpResponse(HttpResponseStatusLine httpResponseStatusLine, HttpResponseHeaders httpResponseHeaders, HttpResponseBody httpResponseBody) {
this.httpResponseStatusLine = httpResponseStatusLine;
this.httpResponseHeaders = httpResponseHeaders;
this.httpResponseBody = httpResponseBody;
}
public void modifyResponse(ResponseEntity responseEntity) throws IOException {
this.httpResponseStatusLine = HttpResponseStatusLine.of(HTTP11, responseEntity.getHttpStatus());
this.httpResponseHeaders = generateHttpResponseHeaders(responseEntity, getOrGenerateResponseBody(responseEntity));
this.httpResponseBody = getOrGenerateResponseBody(responseEntity);
}
public static HttpResponse from(ResponseEntity responseEntity) throws IOException {
HttpStatus httpStatus = responseEntity.getHttpStatus();
HttpResponseBody responseBody = getOrGenerateResponseBody(responseEntity);
HttpResponseHeaders headers = generateHttpResponseHeaders(responseEntity, responseBody);
return HttpResponse.builder()
.httpResponseStatusLine(HttpResponseStatusLine.of(HTTP11, httpStatus))
.httpResponseHeaders(headers)
.httpResponseBody(responseBody)
.build();
}
private static HttpResponseBody getOrGenerateResponseBody(ResponseEntity responseEntity) throws IOException {
String location = responseEntity.getLocation();
HttpResponseBody responseBody = responseEntity.getResponseBody();
if (responseBody == null) {
responseBody = findResponseBodyFrom(location);
}
return responseBody;
}
private static HttpResponseHeaders generateHttpResponseHeaders(ResponseEntity responseEntity, HttpResponseBody responseBody) {
HttpStatus httpStatus = responseEntity.getHttpStatus();
String location = responseEntity.getLocation();
if (httpStatus == HttpStatus.FOUND) {
return new HttpResponseHeaders()
.location(location)
.setCookie(responseEntity.getHttpCookie());
}
return new HttpResponseHeaders()
.contentType(responseEntity.getContentType())
.contentLength(responseBody);
}
private static HttpResponseBody findResponseBodyFrom(String location) throws IOException {
URL resource = ClassLoader.getSystemClassLoader().getResource("static" + location);
File file = new File(resource.getFile());
return HttpResponseBody.from(new String(Files.readAllBytes(file.toPath())));
}
public String getResponse() {
String responseStatusLine = formatResponseStatusLine(httpResponseStatusLine);
String responseHeaders = formatResponseHeaders(httpResponseHeaders);
String responseBody = formatResponseBody();
return String.join(
CRLF,
responseStatusLine,
responseHeaders,
BLANK_LINE,
responseBody
);
}
private String formatResponseStatusLine(HttpResponseStatusLine httpResponseStatusLine) {
return String.format("%s %s %s ",
httpResponseStatusLine.getHttpProtocolVersion().getName(),
httpResponseStatusLine.getHttpStatus().getStatusCode(),
httpResponseStatusLine.getHttpStatus().name()
);
}
private String formatResponseHeaders(HttpResponseHeaders headers) {
return headers.getHeaders().entrySet().stream()
.map(this::convertHeader)
.collect(Collectors.joining(CRLF));
}
private String convertHeader(Map.Entry<String, String> entry) {
return String.format("%s: %s ", entry.getKey(), entry.getValue());
}
private String formatResponseBody() {
return httpResponseBody.getBody();
}
}
✔️ Java Servlet API
- 요청 메세지 파싱, 응답 메세지 생성에 사용되는 HttpServletRequest, HttpServletResponse 객체는 Java Servlet API의 일부분
- 서블릿 컨테이너인 톰캣 뿐만 아니라 다른 웹 애플리케이션 서버에서도 사용됨
- 클라이언트의 HTTP 요청에 대한 정보를 담고 있으며, 웹 애플리케이션 개발에서 많이 사용됨.
[개인적 궁금증] Java Servlet API는 어디 코드에 정의되어 있을까?
자바 서블릿 API는 웹 애플리케이션 개발을 위한 자바 표준 API입니다.
서블릿은 javax.servlet 패키지에 포함되어 있으며, HttpServlet 클래스를 상속받아 구현합니다.
이를 통해 HTTP 프로토콜에 맞는 요청과 응답을 처리할 수 있습니다.
Java Servlet API는 JavaEE에 포함되어 있던 코드입니다.
자바EE는 기업급 애플리케이션을 개발하기 위한 플랫폼입니다.
자바 서블릿 API는 자바EE의 웹 컴포넌트 중 하나로서, 동적인 웹 콘텐츠를 생성하고 처리하기 위한 기능을 제공합니다.
💋 HTTP Response는 크게 단순 텍스트, HTML, JSON으로 분류할 수 있다.
- 단순 텍스트 (Plain Text)
- 단순 텍스트는 가장 기본적인 형식으로, 사람이 읽을 수 있는 텍스트 형태로 반환됩니다.
- 주로 간단한 메시지, 오류 메시지, 로깅 정보 등을 전달합니다.
- Content-Type 헤더는 "text/plain"으로 설정됩니다.
- HTML (Hypertext Markup Language)
- 서버는 HTML 형식으로 응답하여 클라이언트에게 웹 페이지를 전송합니다.
- 웹 브라우저는 HTML을 해석하여 사용자에게 보여줍니다.
- 서버 사이드 렌더링 (Server-side Rendering, SSR)에서 자주 사용되는 형태입니다.
- Content-Type 헤더는 "text/html"로 설정됩니다.
- JSON (JavaScript Object Notation)
- JSON은 데이터를 효율적으로 표현하기 위한 경량의 데이터 교환 형식입니다.
- 서버는 JSON 형식으로 응답하여 클라이언트에게 데이터를 전송합니다.
- 주로 API 요청에 대한 응답으로 사용되며, 클라이언트는 이를 파싱하여 데이터를 활용합니다.
- Content-Type 헤더는 "application/json"으로 설정됩니다.
💋 특정 경로의 정적 파일을 읽어올 수 있다.
위에서 말한 경우 중 HTML 파일을 반환하는 경우에, Servlet은 정적인 리소스를 읽어오기 위해서 File을 통한 탐색을 사용한다.
따라서 학습 테스트를 통해서 먼저 정적 파일을 읽어오는 것에 대해서 학습했다.
참고 링크에 들어가보면, 자바에서 파일을 읽는 방법에 대해서 굉장히 자세히 나와있다!
참고: https://www.baeldung.com/reading-file-in-java
✔️ File, Files, Path, Paths 클래스
File
: File 클래스는 파일이나 디렉토리의 경로, 이름, 속성 등을 다루는 기본적인 파일 시스템 연산을 제공합니다. 이 클래스는 Java 6 이전부터 사용되었으며, 상대적으로 간단한 파일 작업에 적합합니다.Files
: Files 클래스는 Java 7에서 도입된 java.nio.file 패키지의 일부로, 파일 시스템 작업을 수행하는 다양한 메서드를 제공합니다. 이 클래스는 File 클래스보다 더 풍부한 기능을 제공하며, 파일 복사, 파일 이동, 파일 삭제 등의 작업을 수행할 수 있습니다.Path
: Path 인터페이스는 파일이나 디렉토리의 경로를 나타냅니다. 파일 시스템에서 식별자로 사용되며, 파일 또는 디렉토리에 대한 작업을 수행하는 데 사용됩니다. Path 인터페이스는 파일 시스템에 독립적이며, 특정 파일 시스템에서 사용되는 구체적인 경로를 나타내는 구현 클래스인 Paths를 생성할 수 있습니다.Paths
: Paths 클래스는 Path 인터페이스의 구현 클래스를 생성하는 데 사용됩니다. 이 클래스는 파일 시스템에 특정한 구현을 사용하여 경로를 나타내는 Path 객체를 생성할 수 있습니다. 예를 들어, 파일 시스템의 기본 경로 구현을 사용하여 경로를 생성하거나, 다른 파일 시스템 구현을 사용하여 경로를 생성할 수도 있습니다.
참고로, Files 클래스와 Path 인터페이스는 Java 7 이후에 추가되었으며, 이전의 File 클래스보다 더 유연하고 향상된 파일 시스템 작업을 제공합니다.
참고: https://www.baeldung.com/java-io-file
✔️ Resource 디렉토리 경로 찾기
/**
* resource 디렉터리 경로 찾기
* <p>
* File 객체를 생성하려면 파일의 경로를 알아야 한다.
* 자바 애플리케이션은 resource 디렉터리에 HTML, CSS 같은 정적 파일을 저장한다.
* resource 디렉터리의 경로는 어떻게 알아낼 수 있을까?
*/
@Test
void resource_디렉터리에_있는_파일의_경로를_찾는다() {
final String fileName = "nextstep.txt";
// todo
URL resource = getClass()
.getClassLoader()
.getResource(fileName);
final String actual = resource.getFile();
assertThat(actual).endsWith(fileName);
}
✔️ 파일 내용 읽기
/**
* 파일 내용 읽기
* <p>
* 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다.
* File, Files 클래스를 사용하여 파일의 내용을 읽어보자.
*/
@Test
void 파일의_내용을_읽는다() throws IOException {
final String fileName = "nextstep.txt";
// todo
URL resource = getClass()
.getClassLoader()
.getResource(fileName);
final Path path = new File(resource.getPath()).toPath();
// todo
final List<String> actual = Files.readAllLines(path);
assertThat(actual).containsOnly("nextstep");
}
✔️ I/O Stream에 대한 학습 테스트
/**
* 자바는 스트림(Stream)으로부터 I/O를 사용한다.
* 입출력(I/O)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다.
* <p>
* InputStream은 데이터를 읽고, OutputStream은 데이터를 쓴다.
* FilterStream은 InputStream이나 OutputStream에 연결될 수 있다.
* FilterStream은 읽거나 쓰는 데이터를 수정할 때 사용한다. (e.g. 암호화, 압축, 포맷 변환)
* <p>
* Stream은 데이터를 바이트로 읽고 쓴다.
* 바이트가 아닌 텍스트(문자)를 읽고 쓰려면 Reader와 Writer 클래스를 연결한다.
* Reader, Writer는 다양한 문자 인코딩(e.g. UTF-8)을 처리할 수 있다.
*/
@DisplayName("Java I/O Stream 클래스 학습 테스트")
class IOStreamTest {
/**
* OutputStream 학습하기
* <p>
* 자바의 기본 출력 클래스는 java.io.OutputStream이다.
* OutputStream의 write(int b) 메서드는 기반 메서드이다.
* <code>public abstract void write(int b) throws IOException;</code>
*/
@Nested
class OutputStream_학습_테스트 {
/**
* OutputStream은 다른 매체에 바이트로 데이터를 쓸 때 사용한다.
* OutputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 쓰기 위해 write(int b) 메서드를 사용한다.
* 예를 들어, FilterOutputStream은 파일로 데이터를 쓸 때,
* 또는 DataOutputStream은 자바의 primitive type data를 다른 매체로 데이터를 쓸 때 사용한다.
* <p>
* write 메서드는 데이터를 바이트로 출력하기 때문에 비효율적이다.
* <code>write(byte[] data)</code>와 <code>write(byte b[], int off, int len)</code> 메서드는
* 1바이트 이상을 한 번에 전송 할 수 있어 훨씬 효율적이다.
*/
@Test
void OutputStream은_데이터를_바이트로_처리한다() throws IOException {
final byte[] bytes = {110, 101, 120, 116, 115, 116, 101, 112};
final OutputStream outputStream = new ByteArrayOutputStream(bytes.length);
/**
* todo
* OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다
*/
String nextstep = "nextstep";
outputStream.write(nextstep.getBytes());
final String actual = outputStream.toString();
assertThat(actual).isEqualTo(nextstep);
outputStream.close();
}
/**
* 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다.
* BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다.
* <p>
* 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자.
* flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다.
* Stream은 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면
* 데드락(deadlock) 상태가 되기 때문에 flush로 해제해야 한다.
*/
@Test
void BufferedOutputStream을_사용하면_버퍼링이_가능하다() throws IOException {
final OutputStream outputStream = mock(BufferedOutputStream.class);
/**
* todo
* flush를 사용해서 테스트를 통과시킨다.
* ByteArrayOutputStream과 어떤 차이가 있을까?
*/
outputStream.flush();
verify(outputStream, atLeastOnce()).flush();
outputStream.close();
}
/**
* 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다.
* 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다.
*/
@Test
void OutputStream은_사용하고_나서_close_처리를_해준다() throws IOException {
final OutputStream outputStream = mock(OutputStream.class);
/**
* todo
* try-with-resources를 사용한다.
* java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다.
*/
try (outputStream) {
}
verify(outputStream, atLeastOnce()).close();
}
}
/**
* InputStream 학습하기
* <p>
* 자바의 기본 입력 클래스는 java.io.InputStream이다.
* InputStream은 다른 매체로부터 바이트로 데이터를 읽을 때 사용한다.
* InputStream의 read() 메서드는 기반 메서드이다.
* <code>public abstract int read() throws IOException;</code>
* <p>
* InputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 읽기 위해 read() 메서드를 사용한다.
*/
@Nested
class InputStream_학습_테스트 {
/**
* read() 메서드는 매체로부터 단일 바이트를 읽는데, 0부터 255 사이의 값을 int 타입으로 반환한다.
* int 값을 byte 타입으로 변환하면 -128부터 127 사이의 값으로 변환된다.
* 그리고 Stream 끝에 도달하면 -1을 반환한다.
*/
@Test
void InputStream은_데이터를_바이트로_읽는다() throws IOException {
byte[] bytes = {-16, -97, -92, -87};
final InputStream inputStream = new ByteArrayInputStream(bytes);
/**
* todo
* inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까?
*/
final String actual = new String(inputStream.readAllBytes());
assertThat(actual).isEqualTo("🤩");
assertThat(inputStream.read()).isEqualTo(-1);
inputStream.close();
}
/**
* 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다.
* 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다.
*/
@Test
void InputStream은_사용하고_나서_close_처리를_해준다() throws IOException {
final InputStream inputStream = mock(InputStream.class);
/**
* todo
* try-with-resources를 사용한다.
* java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다.
*/
try (inputStream) {
}
verify(inputStream, atLeastOnce()).close();
}
}
/**
* FilterStream 학습하기
* <p>
* 필터는 필터 스트림, reader, writer로 나뉜다.
* 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다.
* reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된 텍스트를 처리하는 데 사용된다.
*/
@Nested
class FilterStream_학습_테스트 {
/**
* BufferedInputStream은 데이터 처리 속도를 높이기 위해 데이터를 버퍼에 저장한다.
* InputStream 객체를 생성하고 필터 생성자에 전달하면 필터에 연결된다.
* 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까?
*/
@Test
void 필터인_BufferedInputStream를_사용해보자() throws IOException {
final String text = "필터에 연결해보자.";
final InputStream inputStream = new ByteArrayInputStream(text.getBytes());
final InputStream bufferedInputStream = new BufferedInputStream(inputStream);
final byte[] actual = bufferedInputStream.readAllBytes();
assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class);
assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes());
}
}
/**
* 자바의 기본 문자열은 UTF-16 유니코드 인코딩을 사용한다.
* 문자열이 아닌 바이트 단위로 처리하려니 불편하다.
* 그리고 바이트를 문자(char)로 처리하려면 인코딩을 신경 써야 한다.
* reader, writer를 사용하면 입출력 스트림을 바이트가 아닌 문자 단위로 데이터를 처리하게 된다.
* 그리고 InputStreamReader를 사용하면 지정된 인코딩에 따라 유니코드 문자로 변환할 수 있다.
*/
@Nested
class InputStreamReader_학습_테스트 {
/**
* InputStreamReader를 사용해서 바이트를 문자(char)로 읽어온다.
* 읽어온 문자(char)를 문자열(String)로 처리하자.
* 필터인 BufferedReader를 사용하면 readLine 메서드를 사용해서 문자열(String)을 한 줄 씩 읽어올 수 있다.
*/
@Test
void BufferedReader를_사용하여_문자열을_읽어온다() throws IOException {
final String emoji = String.join("\r\n",
"😀😃😄😁😆😅😂🤣🥲☺️😊",
"😇🙂🙃😉😌😍🥰😘😗😙😚",
"😋😛😝😜🤪🤨🧐🤓😎🥸🤩",
"");
final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes());
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
List<String> readLines = new ArrayList<>();
String line = bufferedReader.readLine();
while (line != null) {
readLines.add(line);
line = bufferedReader.readLine();
}
final String actual = String.join("\r\n", readLines) + "\r\n";
assertThat(actual).hasToString(emoji);
}
}
}
💋 톰캣의 패키지 네이밍
catalina
패키지- 톰캣의 서블릿 컨테이너
coyote
패키지- 웹 서버 Connector
- HTTP 처리하는 역할
- 패키지 이름은 그냥 강아지 이름 붙여주듯 붙인 이름임.
- 개인적으로, 이런 그냥 지어준 이름으로 지으면 신입 들어올 때마다 변경해야 하기 때문에 이름을 지어주는 것은 비추천
💋 회고
- 레벨4 들어와서 첫 미션이었는데, 어렴풋이 알고있던 HTTP Server의 역할을 직접 만들어 보면서 생각보다 편의성이 강하다는 생각이 들었다. 어쩌면 나도 라이브러리 만들 수 있을지도...?!!
- 이번에 내가 만든 기능은 톰캣이 제공하는 모든 기능은 아니지만, 핵심 기능인 요청 메세지 파싱, 응답 메세지 생성, 그리고 세션 관리를 해냈다.
- 세션 관리도 사실 굉장히 모호했었는데, 직접 로그인이 성공했을 때 랜덤 세션 아이디인 uuid를 생성해서 Set-Cookie 응답 헤더를 통해 보내주면, 브라우저가 알아서 저장한 뒤에 이후 모든 요청에서 보내준다는 것이 확 체감이 되었다.
- 유일한 단점이라고는 세션 데이터베이스를 관리해야 하고, 모든 요청마다 쿠키에 들어있는 JSESSIONID를 빼내서, 데이터베이스에서 찾아야 하는 번거로움...!
- 그렇담 모든 정보를 다 담아서, 암호화 빡시게 해서 토큰으로 사용하면 되지 않나에서 출발한게 토큰이라는 개념인데!
- 액세스 토큰 재발급을 위한 Refresh Token을 사용하게 되면 결국에는 리프레쉬 토큰을 데이터베이스에 저장해야 한다.
- 또 로그아웃 구현을 위해서도 이미 발급한 토큰을 별도로 '이 계정은 로그아웃했어요'라는 의미르 지닌 블랙리스트(그냥 이름인듯) 테이블에 저장하는 방법을 많이들 사용한다는데, 이러면 토큰을 사용해도 결국에는 세션 정보를 저장하는 것과 같지 않을까?
- 근데 구구 코치가 말해준 거로는, 일단 리프레쉬 토큰은 한 번 저장하면 이후에 토큰 재발급을 위해 사용하기 전까지는 데이터베이스를 뒤질 일이 없다.
- 또, 로그아웃을 하지 않고 계속 로그인이 유지된 상태를 가정한다면 계속해서 데이터베이스를 뒤질 일이 없다.
- 이정도만 되더라도, 모든 요청마다 데이터베이스를 뒤질 일은 없으니, JWT 토큰 좀 가치 있을 수도..?
- 근데 보통 JWT 토큰 복호화한 뒤에 안에 들어있는 내용 맞는지 확인을 데이터베이스에서 하지 않나??? ㅎㅎㅎㅎㅎㅎㅎㅎ 그냥 세션을 쓰는 거랑 똑같은걸까????
- 우테코 내부에서 자잘한 코드 수정으로 tomcat의 컨트리뷰터가 된 크루들이 속출했는데, 생각보다 contribution이 멀지 않다는 생각도 들었다. 오픈소스라는게 확 와닿으면서, 이렇게 일관성 없는 여러 사람들이 각각 작성한 코드가 모여서 소프트웨어가 되는게 신기했다.
- 우리 스탬프크러쉬 서비스가 좀 허술한 부분이 있는데, 아무리 사용자가 많고 복잡한 톰캣이더라도 개발자들이 불안하게 생각하는 부분이 분명히 있다. 사람 하는 일은 다 비슷하다는 생각이 들었다.
- 나도 한 번 톰캣 코드를 살펴볼까 했다가 너무 복잡하게 지저분하게 생겨서 포기했닼ㅋㅋ
💋 참고자료
- https://clean-nutria-44b.notion.site/1-b15e4c19c2dc43868a8358dc0a90487b
- https://parkadd.tistory.com/113
도움이 되었다면, 공감/댓글을 달아주면 깃짱에게 큰 힘이 됩니다!🌟
'우아한테크코스5기' 카테고리의 다른 글
[우테코] Domain Driven Development(도메인 주도 개발)이란? (3) | 2023.10.17 |
---|---|
[우테코] 점진적 리팩터링: 달리는 기차의 바퀴를 갈아끼우기 (0) | 2023.09.19 |
[우테코] 레벨3 레벨인터뷰: 인터뷰 실제 대화 스크립트, 피드백 (0) | 2023.08.29 |
[우테코] 레벨로그: 레벨3 동안 공부한 내용들을 정리하며 (2) | 2023.08.29 |
[우테코] 장바구니 협업 미션 회고: 협업을 잘 하기 위한 노력 (1) | 2023.06.07 |