๋ณธ๋ฌธ์œผ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ

๐Ÿ’ฌ HTTP Message Parsing

๊ฐœ์š”โ€‹

์ด ๋ฌธ์„œ๋Š” Sprout Framework์˜ HTTP ๋ฉ”์‹œ์ง€ ํŒŒ์‹ฑ ์‹œ์Šคํ…œ์— ๋Œ€ํ•œ ๊ธฐ์ˆ ์  ๋ถ„์„์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์›์‹œ HTTP ์š”์ฒญ ํ…์ŠคํŠธ๋ฅผ ๊ตฌ์กฐํ™”๋œ HttpRequest ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ํŒŒ์‹ฑ ํŒŒ์ดํ”„๋ผ์ธ์˜ ๋‚ด๋ถ€ ๊ตฌ์กฐ, ์•Œ๊ณ ๋ฆฌ์ฆ˜, ์„ค๊ณ„ ๊ฒฐ์ •์„ ๊ฒ€ํ† ํ•ฉ๋‹ˆ๋‹ค.

ํŒŒ์‹ฑ ํŒŒ์ดํ”„๋ผ์ธ ์•„ํ‚คํ…์ฒ˜โ€‹

์ „์ฒด ํŒŒ์‹ฑ ํ๋ฆ„โ€‹

์›์‹œ HTTP ํ…์ŠคํŠธ โ†’ HttpRequestParser โ†’ ๊ตฌ์กฐํ™”๋œ HttpRequest ๊ฐ์ฒด
โ†“
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ RequestLineParser โ†’ HttpHeaderParser โ†’ QueryStringParser โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๊ตฌ์„ฑ ์š”์†Œ๋ณ„ ์ฑ…์ž„ ๋ถ„๋ฆฌโ€‹

๊ฐ ํŒŒ์„œ๋Š” HTTP ๋ฉ”์‹œ์ง€์˜ ํŠน์ • ๋ถ€๋ถ„์„ ๋‹ด๋‹นํ•˜๋Š” ๋‹จ์ผ ์ฑ…์ž„ ์›์น™์„ ๋”ฐ๋ฅด๋„๋ก ์„ค๊ณ„ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

  • HttpRequestParser: ์ „์ฒด ์กฐ์ • ๋ฐ ๋ฉ”์‹œ์ง€ ๋ถ„ํ• 
  • RequestLineParser: ์š”์ฒญ ๋ผ์ธ (๋ฉ”์„œ๋“œ, ๊ฒฝ๋กœ, HTTP ๋ฒ„์ „)
  • HttpHeaderParser: HTTP ํ—ค๋” ํŒŒ์‹ฑ
  • QueryStringParser: URL ์ฟผ๋ฆฌ ์ŠคํŠธ๋ง ํŒŒ๋ผ๋ฏธํ„ฐ

ํ•ต์‹ฌ ๊ตฌ์„ฑ ์š”์†Œ ๋ถ„์„โ€‹

1. HttpRequestParser: ๋งˆ์Šคํ„ฐ ์กฐ์ •์žโ€‹

๋ฉ”์‹œ์ง€ ๋ถ„ํ•  ์•Œ๊ณ ๋ฆฌ์ฆ˜

private String[] split(String raw) {
// 1. CRLF ๊ตฌ๋ถ„์ž ์šฐ์„  ํƒ์ƒ‰ (\r\n\r\n)
int delimiterIdx = raw.indexOf("\r\n\r\n");
int delimiterLen = 4;

// 2. CRLF ์—†์œผ๋ฉด LF ๊ตฌ๋ถ„์ž ํƒ์ƒ‰ (\n\n)
if (delimiterIdx == -1) {
delimiterIdx = raw.indexOf("\n\n");
delimiterLen = 2;
}

// 3. ๊ตฌ๋ถ„์ž ๊ธฐ์ค€์œผ๋กœ ํ—ค๋”/๋ฐ”๋”” ๋ถ„ํ• 
if (delimiterIdx != -1) {
return new String[]{
raw.substring(0, delimiterIdx), // ํ—ค๋” + ์š”์ฒญ๋ผ์ธ
raw.substring(delimiterIdx + delimiterLen) // ๋ฐ”๋””
};
}

// 4. ๊ตฌ๋ถ„์ž ์—†์œผ๋ฉด ๋ฐ”๋””๋Š” ๋นˆ ๋ฌธ์ž์—ด
return new String[]{ raw, "" };
}

์„ค๊ณ„ ๊ฒฐ์ • ๋ถ„์„

  1. ๊ด€๋Œ€ํ•œ ๊ตฌ๋ถ„์ž ์ฒ˜๋ฆฌ: CRLF์™€ LF ๋ชจ๋‘ ์ง€์›ํ•˜์—ฌ ๋‹ค์–‘ํ•œ ํด๋ผ์ด์–ธํŠธ์™€์˜ ํ˜ธํ™˜์„ฑ ํ™•๋ณด
  2. ์กฐ๊ธฐ ์‹คํŒจ ๋ฐฉ์ง€: ๊ตฌ๋ถ„์ž๊ฐ€ ์—†์–ด๋„ ์ „์ฒด ์š”์ฒญ์„ ํ—ค๋”๋กœ ์ฒ˜๋ฆฌ
  3. ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์„ฑ: substring() ์‚ฌ์šฉ์œผ๋กœ ๋ถˆํ•„์š”ํ•œ ๋ณต์‚ฌ ์ตœ์†Œํ™”

ํŒŒ์‹ฑ ์กฐ์ • ๋กœ์ง

public HttpRequest<?> parse(String raw) {
String[] parts = split(raw);
String headerAndRequestLinePart = parts[0];
String bodyPart = parts[1];

// ์ฒซ ๋ฒˆ์งธ ๋ผ์ธ ์ถ”์ถœ (์š”์ฒญ ๋ผ์ธ)
String firstLine = headerAndRequestLinePart.split("\r?\n", 2)[0];

// ๊ฐ ํŒŒ์„œ์— ์œ„์ž„
var rl = lineParser.parse(firstLine);
var query = qsParser.parse(rl.rawPath());

// ํ—ค๋” ๋ถ€๋ถ„ ์ถ”์ถœ ๋ฐ ํŒŒ์‹ฑ
String rawHeadersOnly = extractHeaders(headerAndRequestLinePart);
Map<String, String> headers = headerParser.parse(rawHeadersOnly);

return new HttpRequest<>(rl.method(), rl.cleanPath(), bodyPart, query, headers);
}

2. RequestLineParser: HTTP ์š”์ฒญ ๋ผ์ธ ํŒŒ์‹ฑโ€‹

ํŒŒ์‹ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜

public RequestLine parse(String line) {
String[] parts = line.trim().split(" ", 3); // ์ตœ๋Œ€ 3๊ฐœ ๋ถ€๋ถ„์œผ๋กœ ๋ถ„ํ• 

if (parts.length < 2) {
throw new BadRequestException(ExceptionMessage.BAD_REQUEST, ResponseCode.BAD_REQUEST);
}

HttpMethod method = HttpMethod.valueOf(parts[0].toUpperCase());
String rawPath = parts[1];
String cleanPath = rawPath.split("\\?")[0]; // ์ฟผ๋ฆฌ ์ŠคํŠธ๋ง ์ œ๊ฑฐ

return new RequestLine(method, rawPath, cleanPath);
}

ํ•ต์‹ฌ ์„ค๊ณ„ ํŠน์ง•

  1. ์ œํ•œ๋œ ๋ถ„ํ• : split(" ", 3)๋กœ HTTP ๋ฒ„์ „์— ๊ณต๋ฐฑ์ด ์žˆ์–ด๋„ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ
  2. ๋Œ€์†Œ๋ฌธ์ž ์ •๊ทœํ™”: HTTP ๋ฉ”์„œ๋“œ๋ฅผ ๋Œ€๋ฌธ์ž๋กœ ํ†ต์ผ
  3. ๊ฒฝ๋กœ ์ „์ฒ˜๋ฆฌ: ์ฟผ๋ฆฌ ์ŠคํŠธ๋ง์„ ๋ถ„๋ฆฌํ•˜์—ฌ clean path ์ƒ์„ฑ
  4. ์œ ํšจ์„ฑ ๊ฒ€์ฆ: ์ตœ์†Œ ์š”๊ตฌ์‚ฌํ•ญ(๋ฉ”์„œ๋“œ, ๊ฒฝ๋กœ) ํ™•์ธ

์„ฑ๋Šฅ ์ตœ์ ํ™”

  • ํ•œ ๋ฒˆ์˜ split ํ˜ธ์ถœ๋กœ ๋ชจ๋“  ๋ถ€๋ถ„ ์ถ”์ถœ
  • trim()์œผ๋กœ ์„ ํ–‰/ํ›„ํ–‰ ๊ณต๋ฐฑ ์ œ๊ฑฐ
  • ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ํ†ตํ•œ ์กฐ๊ธฐ ์‹คํŒจ

3. HttpHeaderParser: HTTP ํ—ค๋” ํŒŒ์‹ฑโ€‹

์ •๊ทœ์‹ ๊ธฐ๋ฐ˜ ํŒŒ์‹ฑ

private static final Pattern HEADER_PATTERN = Pattern.compile("^([^:]+):\\s*(.*)$");

public Map<String, String> parse(String rawHeaders) {
Map<String, String> headers = new HashMap<>();
String[] lines = rawHeaders.split("\r?\n"); // ๊ด€๋Œ€ํ•œ ๋ผ์ธ ๋ถ„๋ฆฌ

for (String line : lines) {
if (line.isBlank()) continue; // ๋นˆ ๋ผ์ธ ๊ฑด๋„ˆ๋›ฐ๊ธฐ

Matcher matcher = HEADER_PATTERN.matcher(line);
if (matcher.matches()) {
String key = matcher.group(1).trim();
String value = matcher.group(2).trim();
headers.put(key, value);
} else {
// ์œ ํšจํ•˜์ง€ ์•Š์€ ํ—ค๋” ํ˜•์‹ ๊ฒฝ๊ณ 
System.err.println("Warning: Invalid header format: " + line);
}
}
return headers;
}

์ •๊ทœ์‹ ํŒจํ„ด ๋ถ„์„

  • ^([^:]+): ์ฝœ๋ก  ์ด์ „์˜ ๋ชจ๋“  ๋ฌธ์ž (ํ—ค๋” ์ด๋ฆ„)
  • :\\s*: ์ฝœ๋ก ๊ณผ ์„ ํƒ์  ๊ณต๋ฐฑ
  • (.*)$: ๋‚˜๋จธ์ง€ ๋ชจ๋“  ๋ฌธ์ž (ํ—ค๋” ๊ฐ’)

์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ์ „๋žต

  1. ์œ ์—ฐํ•œ ๋ผ์ธ ์ข…๋ฃŒ: \r?\n๋กœ CRLF/LF ๋ชจ๋‘ ์ง€์›
  2. ๋นˆ ๋ผ์ธ ๋ฌด์‹œ: HTTP ์ŠคํŽ™์— ๋”ฐ๋ฅธ ๋นˆ ๋ผ์ธ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
  3. ๋ถ€๋ถ„์  ์‹คํŒจ ํ—ˆ์šฉ: ์ž˜๋ชป๋œ ํ—ค๋”๊ฐ€ ์žˆ์–ด๋„ ํŒŒ์‹ฑ ๊ณ„์†
  4. ๋””๋ฒ„๊น… ์ง€์›: ์ž˜๋ชป๋œ ํ˜•์‹์— ๋Œ€ํ•œ ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€

4. QueryStringParser: URL ํŒŒ๋ผ๋ฏธํ„ฐ ํŒŒ์‹ฑโ€‹

URL ๋””์ฝ”๋”ฉ๊ณผ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”์ถœ

public Map<String,String> parse(String rawPath) {
Map<String,String> out = new HashMap<>();
String[] parts = rawPath.split("\\?", 2); // ๊ฒฝ๋กœ์™€ ์ฟผ๋ฆฌ์ŠคํŠธ๋ง ๋ถ„๋ฆฌ

if (parts.length == 2) {
for (String token : parts[1].split("&")) { // ํŒŒ๋ผ๋ฏธํ„ฐ๋ณ„ ๋ถ„ํ• 
String[] kv = token.split("=", 2); // ํ‚ค=๊ฐ’ ๋ถ„ํ• 

if (kv.length == 2) {
// ์ •์ƒ์ ์ธ ํ‚ค=๊ฐ’ ์Œ
out.put(
URLDecoder.decode(kv[0], StandardCharsets.UTF_8),
URLDecoder.decode(kv[1], StandardCharsets.UTF_8)
);
} else if (kv.length == 1 && !kv[0].isEmpty()) {
// ๊ฐ’ ์—†๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ (์˜ˆ: ?flag&other=value)
out.put(URLDecoder.decode(kv[0], StandardCharsets.UTF_8), "");
}
}
}
return out;
}

URL ๋””์ฝ”๋”ฉ ์ฒ˜๋ฆฌ

  • ๋ฌธ์ž ์ธ์ฝ”๋”ฉ: UTF-8 ๊ฐ•์ œ ์‚ฌ์šฉ์œผ๋กœ ์ผ๊ด€์„ฑ ๋ณด์žฅ
  • ์•ˆ์ „ํ•œ ๋””์ฝ”๋”ฉ: URLDecoder.decode() ์‚ฌ์šฉ
  • ๋นˆ ๊ฐ’ ์ฒ˜๋ฆฌ: ๊ฐ’์ด ์—†๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ๋„ ์ง€์›

์—ฃ์ง€ ์ผ€์ด์Šค ์ฒ˜๋ฆฌ

  1. ์ฟผ๋ฆฌ์ŠคํŠธ๋ง ์—†์Œ: split ๊ฒฐ๊ณผ๊ฐ€ 1๊ฐœ์ผ ๋•Œ ๋นˆ ๋งต ๋ฐ˜ํ™˜
  2. ๋นˆ ํŒŒ๋ผ๋ฏธํ„ฐ: ๋นˆ ํ‚ค๋Š” ๋ฌด์‹œ, ๋นˆ ๊ฐ’์€ ํ—ˆ์šฉ
  3. ํŠน์ˆ˜ ๋ฌธ์ž: URL ์ธ์ฝ”๋”ฉ๋œ ๋ฌธ์ž๋“ค ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋””์ฝ”๋”ฉ

๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์™€ ํƒ€์ž… ์•ˆ์ „์„ฑโ€‹

RequestLine Record ํ™œ์šฉโ€‹

public record RequestLine(HttpMethod method, String rawPath, String cleanPath) {}

Record ์‚ฌ์šฉ์˜ ์žฅ์ 

  1. ๋ถˆ๋ณ€์„ฑ: ์ƒ์„ฑ ํ›„ ์ˆ˜์ • ๋ถˆ๊ฐ€๋Šฅํ•œ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ
  2. ์ž๋™ ๊ตฌํ˜„: equals(), hashCode(), toString() ์ž๋™ ์ƒ์„ฑ
  3. ํƒ€์ž… ์•ˆ์ „์„ฑ: ์ปดํŒŒ์ผ ์‹œ๊ฐ„์— ํƒ€์ž… ๊ฒ€์ฆ
  4. ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์„ฑ: ์ตœ์†Œํ•œ์˜ ๋ฉ”๋ชจ๋ฆฌ ์˜ค๋ฒ„ํ—ค๋“œ

HttpRequest ๊ตฌ์กฐํ™”โ€‹

new HttpRequest<>(rl.method(), rl.cleanPath(), bodyPart, query, headers)

๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ์˜ ์ด์ 

  • ํƒ€์ž… ์•ˆ์ „์„ฑ: ๊ฐ ํ•„๋“œ๊ฐ€ ์ ์ ˆํ•œ ํƒ€์ž…์œผ๋กœ ๋งคํ•‘
  • ์ ‘๊ทผ ํŽธ์˜์„ฑ: ๊ตฌ์กฐํ™”๋œ ์ ‘๊ทผ์ž๋ฅผ ํ†ตํ•œ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ
  • ํ™•์žฅ ๊ฐ€๋Šฅ์„ฑ: ์ œ๋„ค๋ฆญ ํƒ€์ž…์œผ๋กœ ๋ฐ”๋”” ํƒ€์ž… ์œ ์—ฐ์„ฑ

์„ฑ๋Šฅ ๋ถ„์„โ€‹

์‹œ๊ฐ„ ๋ณต์žก๋„โ€‹

HttpRequestParser

  • ์ „์ฒด ํŒŒ์‹ฑ: O(n) (n = ์›์‹œ ๋ฉ”์‹œ์ง€ ๊ธธ์ด)
  • ๋ฉ”์‹œ์ง€ ๋ถ„ํ• : O(n)
  • ๊ฐ ์„œ๋ธŒํŒŒ์„œ ํ˜ธ์ถœ: O(k) (k = ๊ฐ ์„น์…˜ ๊ธธ์ด)

๊ฐœ๋ณ„ ํŒŒ์„œ๋“ค

  • RequestLineParser: O(1) (๊ณ ์ •๋œ ๋ถ„ํ•  ํšŸ์ˆ˜)
  • HttpHeaderParser: O(h) (h = ํ—ค๋” ๋ผ์ธ ์ˆ˜)
  • QueryStringParser: O(p) (p = ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆ˜)

๊ณต๊ฐ„ ๋ณต์žก๋„โ€‹

๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ ํŒจํ„ด

  • ์›์‹œ ๋ฌธ์ž์—ด: ์ž…๋ ฅ ํฌ๊ธฐ๋งŒํผ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ
  • ์ค‘๊ฐ„ ๋ฐฐ์—ด๋“ค: split() ์—ฐ์‚ฐ์œผ๋กœ ์ž„์‹œ ๋ฐฐ์—ด ์ƒ์„ฑ
  • ์ตœ์ข… ๊ฐ์ฒด: ํŒŒ์‹ฑ๋œ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์ฒด๋“ค

์ตœ์ ํ™” ๊ธฐ๋ฒ•

  • substring() ์‚ฌ์šฉ์œผ๋กœ ๋ฌธ์ž์—ด ๋ณต์‚ฌ ์ตœ์†Œํ™”
  • ์ •๊ทœ์‹ ์ปดํŒŒ์ผ ์บ์‹ฑ (static Pattern)
  • ์กฐ๊ธฐ ์ข…๋ฃŒ๋กœ ๋ถˆํ•„์š”ํ•œ ์ฒ˜๋ฆฌ ๋ฐฉ์ง€

๋ฉ”๋ชจ๋ฆฌ ํ• ๋‹น ํŒจํ„ดโ€‹

// ํšจ์œจ์ ์ธ ๋ฌธ์ž์—ด ์ฒ˜๋ฆฌ
String[] parts = rawPath.split("\\?", 2); // ์ตœ๋Œ€ 2๊ฐœ๋กœ ์ œํ•œ
String cleanPath = rawPath.split("\\?")[0]; // ์ฒซ ๋ฒˆ์งธ ๋ถ€๋ถ„๋งŒ ์‚ฌ์šฉ

// ์ •๊ทœ์‹ ์žฌ์‚ฌ์šฉ
private static final Pattern HEADER_PATTERN = Pattern.compile("...");

์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๋ฐ ๋ณต์›๋ ฅโ€‹

๊ณ„์ธตํ™”๋œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌโ€‹

๋ ˆ๋ฒจ 1: ๊ตฌ๋ฌธ ์˜ค๋ฅ˜ (Syntax Errors)

if (parts.length < 2) {
throw new BadRequestException(ExceptionMessage.BAD_REQUEST, ResponseCode.BAD_REQUEST);
}

๋ ˆ๋ฒจ 2: ํ˜•์‹ ๊ฒฝ๊ณ  (Format Warnings)

System.err.println("Warning: Invalid header format detected: " + line);

๋ ˆ๋ฒจ 3: ๊ด€๋Œ€ํ•œ ์ฒ˜๋ฆฌ (Lenient Processing)

if (line.isBlank()) continue;  // ๋นˆ ๋ผ์ธ ๋ฌด์‹œ

๋ณต์›๋ ฅ ์žˆ๋Š” ํŒŒ์‹ฑโ€‹

  1. ๋ถ€๋ถ„์  ์‹คํŒจ ํ—ˆ์šฉ: ์ผ๋ถ€ ํ—ค๋”๊ฐ€ ์ž˜๋ชป๋˜์–ด๋„ ๋‚˜๋จธ์ง€ ํŒŒ์‹ฑ ๊ณ„์†
  2. ๊ธฐ๋ณธ๊ฐ’ ์ œ๊ณต: ๋ˆ„๋ฝ๋œ ์š”์†Œ์— ๋Œ€ํ•œ ํ•ฉ๋ฆฌ์  ๊ธฐ๋ณธ๊ฐ’
  3. ๋‹ค์ค‘ ํ˜•์‹ ์ง€์›: CRLF/LF ๋ผ์ธ ์ข…๋ฃŒ ๋ชจ๋‘ ์ง€์›
  4. ์ธ์ฝ”๋”ฉ ์•ˆ์ „์„ฑ: UTF-8 ๊ฐ•์ œ ์‚ฌ์šฉ์œผ๋กœ ๋ฌธ์ž ์ธ์ฝ”๋”ฉ ๋ฌธ์ œ ๋ฐฉ์ง€

HTTP ์ŠคํŽ™ ์ค€์ˆ˜๋„โ€‹

RFC 7230/7231 ์ค€์ˆ˜ ์‚ฌํ•ญโ€‹

์š”์ฒญ ๋ผ์ธ ์ฒ˜๋ฆฌ:

  • HTTP ๋ฉ”์„œ๋“œ ๋Œ€์†Œ๋ฌธ์ž ์ฒ˜๋ฆฌ โœ“
  • URI ๊ฒฝ๋กœ ์ถ”์ถœ โœ“
  • HTTP ๋ฒ„์ „ ๋ฌด์‹œ (๋‹จ์ˆœํ™”) โš ๏ธ

ํ—ค๋” ์ฒ˜๋ฆฌ:

  • ํ•„๋“œ๋ช…:๊ฐ’ ํ˜•์‹ โœ“
  • ์„ ํ–‰/ํ›„ํ–‰ ๊ณต๋ฐฑ ์ œ๊ฑฐ โœ“
  • ๋นˆ ๋ผ์ธ์œผ๋กœ ํ—ค๋” ์ข…๋ฃŒ โœ“
  • ํ—ค๋” ์ค‘๋ณต ์ฒ˜๋ฆฌ (๋งˆ์ง€๋ง‰ ๊ฐ’ ์‚ฌ์šฉ) โš ๏ธ

๋ฉ”์‹œ์ง€ ๋ฐ”๋””:

  • Content-Length ๊ฒ€์ฆ ์—†์Œ โš ๏ธ
  • Transfer-Encoding ๋ฏธ์ง€์› โš ๏ธ

๋‹จ์ˆœํ™”๋œ ๊ตฌํ˜„โ€‹

Sprout์€ ๊ต์œก/ํ•™์Šต ๋ชฉ์ ์˜ ํ”„๋ ˆ์ž„์›Œํฌ๋กœ์„œ HTTP ์ŠคํŽ™์˜ ํ•ต์‹ฌ ๋ถ€๋ถ„๋งŒ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

์ง€์› ๊ธฐ๋Šฅ

  • ๊ธฐ๋ณธ์ ์ธ ์š”์ฒญ ๋ผ์ธ ํŒŒ์‹ฑ
  • ํ‘œ์ค€ ํ—ค๋” ํ˜•์‹
  • URL ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ
  • UTF-8 ์ธ์ฝ”๋”ฉ

์˜๋„์  ์ œ์™ธ

  • HTTP/2, HTTP/3 ์ง€์›
  • ์ฒญํฌ ์ „์†ก ์ธ์ฝ”๋”ฉ
  • ๋ณต์žกํ•œ ์ธ์ฆ ํ—ค๋”
  • ๋‹ค์ค‘๊ฐ’ ํ—ค๋” ์ฒ˜๋ฆฌ

ํ™•์žฅ์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑโ€‹

ํŒŒ์„œ ๊ต์ฒด ๊ฐ€๋Šฅ์„ฑโ€‹

์˜์กด์„ฑ ์ฃผ์ž…์„ ํ†ตํ•ด ๊ฐ ํŒŒ์„œ๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ๊ต์ฒด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

public HttpRequestParser(RequestLineParser lineParser, 
QueryStringParser qsParser,
HttpHeaderParser headerParser) {
// ๊ฐ ํŒŒ์„œ๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ์ฃผ์ž…
}

์ƒˆ๋กœ์šด ํŒŒ์‹ฑ ๊ธฐ๋Šฅ ์ถ”๊ฐ€โ€‹

ํ™•์žฅ ํฌ์ธํŠธ

  1. ์ƒˆ๋กœ์šด ํŒŒ์„œ ์ถ”๊ฐ€: ๋ฐ”๋”” ํŒŒ์„œ, ์ธ์ฝ”๋”ฉ ํŒŒ์„œ ๋“ฑ
  2. ํŒŒ์‹ฑ ์ „๋žต ๋ณ€๊ฒฝ: ์ •๊ทœ์‹ ๋Œ€์‹  ์ƒํƒœ ๋จธ์‹  ํŒŒ์„œ
  3. ๊ฒ€์ฆ ๊ทœ์น™ ์ถ”๊ฐ€: ๋” ์—„๊ฒฉํ•œ HTTP ์ŠคํŽ™ ์ค€์ˆ˜
  4. ์„ฑ๋Šฅ ์ตœ์ ํ™”: ์ŠคํŠธ๋ฆฌ๋ฐ ํŒŒ์„œ, ์ œ๋กœ์นดํ”ผ ํŒŒ์‹ฑ

ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑโ€‹

๊ฐ ํŒŒ์„œ๊ฐ€ ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ์ˆœ์ˆ˜ ํ•จ์ˆ˜๋กœ ์„ค๊ณ„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

// ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ
@Test
void testValidRequestLine() {
RequestLine result = parser.parse("GET /path HTTP/1.1");
assertEquals(HttpMethod.GET, result.method());
assertEquals("/path", result.cleanPath());
}

๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญโ€‹

์ž…๋ ฅ ๊ฒ€์ฆโ€‹

ํ˜„์žฌ ๋ณด์•ˆ ๋ฉ”์ปค๋‹ˆ์ฆ˜

  1. ์ž…๋ ฅ ํฌ๊ธฐ ์ œํ•œ ์—†์Œ โš ๏ธ: DoS ๊ณต๊ฒฉ ๊ฐ€๋Šฅ์„ฑ
  2. URL ๋””์ฝ”๋”ฉ ์•ˆ์ „: ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ
  3. ์ •๊ทœ์‹ ๋ณด์•ˆ: ๊ฐ„๋‹จํ•œ ํŒจํ„ด์œผ๋กœ ReDoS ์œ„ํ—˜ ๋‚ฎ์Œ

๋ณด์•ˆ ๊ฐœ์„  ์‚ฌ์•ˆ

// ๊ถŒ์žฅ: ์ž…๋ ฅ ํฌ๊ธฐ ์ œํ•œ
public HttpRequest<?> parse(String raw) {
if (raw.length() > MAX_REQUEST_SIZE) {
throw new RequestTooLargeException();
}
// ... ๊ธฐ์กด ๋กœ์ง
}

์ธ์ ์…˜ ๊ณต๊ฒฉ ๋ฐฉ์ง€โ€‹

ํ—ค๋” ์ธ์ ์…˜

  • ํ˜„์žฌ: ๊ธฐ๋ณธ์ ์ธ ํ˜•์‹ ๊ฒ€์ฆ๋งŒ ์ˆ˜ํ–‰
  • ๊ฐœ์„ : CRLF ์ธ์ ์…˜ ๊ฒ€์ฆ ํ•„์š”

๊ฒฝ๋กœ ์กฐ์ž‘

  • ํ˜„์žฌ: ๊ธฐ๋ณธ์ ์ธ URL ๋””์ฝ”๋”ฉ
  • ๊ฐœ์„ : ๊ฒฝ๋กœ ์ˆœํšŒ ๊ณต๊ฒฉ ๋ฐฉ์ง€ ํ•„์š”

Sprout์˜ HTTP ํŒŒ์‹ฑ ์‹œ์Šคํ…œ์€ ๊ต์œก์  ๋ชฉ์ ์— ์ ํ•ฉํ•œ ๋ช…ํ™•ํ•˜๊ณ  ์ดํ•ดํ•˜๊ธฐ ์‰ฌ์šด ๊ตฌ์กฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ํŒŒ์„œ์˜ ๋‹จ์ผ ์ฑ…์ž„ ์›์น™ ์ค€์ˆ˜, ์˜์กด์„ฑ ์ฃผ์ž…์„ ํ†ตํ•œ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ, ๊ทธ๋ฆฌ๊ณ  ํ•ฉ๋ฆฌ์ ์ธ ์„ฑ๋Šฅ ํŠน์„ฑ์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

๋งŒ์•ฝ ๋ณด์•ˆ ๋ฐ ์ŠคํŽ™ ์ค€์ˆ˜์— ๋Œ€ํ•ด ๊ธฐ์—ฌ๋ฅผ ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, ์ด์Šˆ ๋“ฑ๋ก ํ›„ ์ž‘์—… ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.