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

๐ŸŒ HTTP ์„œ๋ฒ„

HTTP ์„œ๋ฒ„๋Š” Tomcat์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์„ ์žฌํ˜„ํ•œ Sprout์˜ ์ž„๋ฒ ๋””๋“œ ์„œ๋ฒ„ ๊ตฌํ˜„์ฒด์ž…๋‹ˆ๋‹ค. Blocking I/O (BIO)์™€ Non-blocking I/O (NIO)๋ฅผ ๋ชจ๋‘ ์ง€์›ํ•˜๋ฉฐ, ํ”Œ๋žซํผ ์Šค๋ ˆ๋“œ ๋˜๋Š” ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ ์˜ต์…˜์„ ์ œ๊ณตํ•˜๋Š” ์œ ์—ฐํ•˜๊ณ  ์„ค์ • ๊ฐ€๋Šฅํ•œ ์„œ๋ฒ„ ์ธํ”„๋ผ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๊ฐœ์š”โ€‹

Sprout์˜ HTTP ์„œ๋ฒ„๋Š” ๋‹ค์Œ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค:

  • NIO ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ๋ฃจํ”„: Java NIO Selector๋ฅผ ์‚ฌ์šฉํ•œ ๊ณ ์„ฑ๋Šฅ ๋…ผ๋ธ”๋กœํ‚น I/O
  • ํ•˜์ด๋ธŒ๋ฆฌ๋“œ BIO/NIO ๋ชจ๋“œ: ํ”„๋กœํ† ์ฝœ ํ•ธ๋“ค๋Ÿฌ๋ณ„ ์œ ์—ฐํ•œ I/O ์ „๋žต ์„ ํƒ
  • ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ ์ง€์›: Java 21์˜ ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ๋ฅผ ํ™œ์šฉํ•œ ํ˜„๋Œ€์ ์ธ ๋™์‹œ์„ฑ ์ฒ˜๋ฆฌ
  • ํ”Œ๋žซํผ ์Šค๋ ˆ๋“œ ํ’€: ํ˜ธํ™˜์„ฑ์„ ์œ„ํ•œ ์ „ํ†ต์ ์ธ ์Šค๋ ˆ๋“œ ํ’€
  • ํ”„๋กœํ† ์ฝœ ๊ฐ์ง€: HTTP/WebSocket ํ”„๋กœํ† ์ฝœ ์ž๋™ ์‹๋ณ„
  • ํ”Œ๋Ÿฌ๊ทธ์ธํ˜• ํ•ธ๋“ค๋Ÿฌ ์•„ํ‚คํ…์ฒ˜: ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ํ”„๋กœํ† ์ฝœ ํ•ธ๋“ค๋Ÿฌ ์‹œ์Šคํ…œ

์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜โ€‹

ํ•ต์‹ฌ ์ปดํฌ๋„ŒํŠธโ€‹

HTTP ์„œ๋ฒ„๋Š” ๋‹ค์Œ์˜ ์ฃผ์š” ์ปดํฌ๋„ŒํŠธ๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค:

  • HttpServer: ๋ผ์ดํ”„์‚ฌ์ดํด ๊ด€๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฉ”์ธ ์„œ๋ฒ„ ํŒŒ์‚ฌ๋“œ
  • ServerStrategy: ํ”Œ๋Ÿฌ๊ทธ์ธํ˜• ์„œ๋ฒ„ ์ „๋žต ์ธํ„ฐํŽ˜์ด์Šค (NIO ์ด๋ฒคํŠธ ๋ฃจํ”„)
  • ConnectionManager: ์—ฐ๊ฒฐ ์ˆ˜๋ฝ ๋ฐ ํ”„๋กœํ† ์ฝœ ๋ผ์šฐํŒ…
  • ProtocolDetector: ์ดˆ๊ธฐ ๋ฐ”์ดํŠธ๋กœ๋ถ€ํ„ฐ ํ”„๋กœํ† ์ฝœ ์‹๋ณ„
  • ProtocolHandler: ํ”„๋กœํ† ์ฝœ๋ณ„ ์š”์ฒญ ์ฒ˜๋ฆฌ
  • RequestExecutorService: ์Šค๋ ˆ๋“œ ๊ด€๋ฆฌ ์ถ”์ƒํ™”

์„œ๋ฒ„ ์ดˆ๊ธฐํ™” ํ”„๋กœ์„ธ์Šคโ€‹

public class SproutApplication {
public static void run(Class<?> primarySource) throws Exception {
// 1. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ปจํ…์ŠคํŠธ ์ƒ์„ฑ
ApplicationContext ctx = new SproutApplicationContext(packages);
ctx.refresh();

// 2. HttpServer ๋นˆ ๊ฐ€์ ธ์˜ค๊ธฐ
HttpServer server = ctx.getBean(HttpServer.class);

// 3. ์„œ๋ฒ„ ์‹œ์ž‘
int port = server.start(8080);
System.out.println("Server started on port " + port);
}
}

์Šค๋ ˆ๋“œ ์‹คํ–‰ ๋ชจ๋“œโ€‹

๊ฐ€์ƒ ์Šค๋ ˆ๋“œ ๋ชจ๋“œ (๊ธฐ๋ณธ๊ฐ’)โ€‹

๊ฐ€์ƒ ์Šค๋ ˆ๋“œ๋Š” ๋†’์€ ์ฒ˜๋ฆฌ๋Ÿ‰์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์œ„ํ•œ ๊ฒฝ๋Ÿ‰ ๋™์‹œ์„ฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค:

@Configuration
public class ServerConfiguration {
@Bean
public RequestExecutorService executorService(
AppConfig appConfig,
List<ContextPropagator> contextPropagators) {

String threadType = appConfig.getStringProperty(
"server.thread-type", "virtual"
);

if (threadType.equals("virtual")) {
return new VirtualRequestExecutorService(contextPropagators);
}
return new RequestExecutorPoolService(
appConfig.getIntProperty("server.thread-pool-size", 100)
);
}
}

VirtualRequestExecutorService ๊ตฌํ˜„โ€‹

public class VirtualRequestExecutorService implements RequestExecutorService {
private final ExecutorService pool =
Executors.newVirtualThreadPerTaskExecutor();
private final List<ContextPropagator> propagators;

@Override
public void execute(Runnable task) {
// ํ˜„์žฌ ์ปจํ…์ŠคํŠธ๋ฅผ ์บก์ฒ˜ํ•˜๊ณ  ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ๋กœ ์ „ํŒŒ
final ContextSnapshot snapshot = new ContextSnapshot(propagators);
pool.execute(snapshot.wrap(task));
}
}

์ฃผ์š” ๊ธฐ๋Šฅ:

  • ์ž‘์—…๋‹น ์ƒˆ๋กœ์šด ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ ์ƒ์„ฑ
  • ์ตœ์†Œํ•œ์˜ ๋ฉ”๋ชจ๋ฆฌ ๊ณต๊ฐ„ (~1KB per thread)
  • ์ž์‹ ์Šค๋ ˆ๋“œ๋กœ์˜ ์ž๋™ ์ปจํ…์ŠคํŠธ ์ „ํŒŒ
  • ์ˆ˜๋ฐฑ๋งŒ ๊ฐœ์˜ ๋™์‹œ ์—ฐ๊ฒฐ์— ์ ํ•ฉ

ํ”Œ๋žซํผ ์Šค๋ ˆ๋“œ ํ’€ ๋ชจ๋“œโ€‹

ํ˜ธํ™˜์„ฑ์„ ์œ„ํ•œ ์ „ํ†ต์ ์ธ ๊ณ ์ • ํฌ๊ธฐ ์Šค๋ ˆ๋“œ ํ’€:

public class RequestExecutorPoolService implements RequestExecutorService {
private final ExecutorService pool;

public RequestExecutorPoolService(int threadPoolSize) {
this.pool = Executors.newFixedThreadPool(threadPoolSize);
}

@Override
public void execute(Runnable task) {
pool.execute(task);
}
}

์„ค์ •:

# application.properties
server.thread-type=platform
server.thread-pool-size=200

์ฃผ์š” ๊ธฐ๋Šฅ:

  • ๊ณ ์ •๋œ ์ˆ˜์˜ ํ”Œ๋žซํผ ์Šค๋ ˆ๋“œ
  • ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ
  • ๋ชจ๋“  Java ๋ฒ„์ „๊ณผ ํ˜ธํ™˜
  • ์ ๋‹นํ•œ ์ˆ˜์ค€์˜ ๋™์‹œ์„ฑ ์š”๊ตฌ์— ์ ํ•ฉ

I/O ์‹คํ–‰ ๋ชจ๋“œโ€‹

ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๋ชจ๋“œ (BIO with NIO Accept)โ€‹

์—ฐ๊ฒฐ ์ˆ˜๋ฝ์„ ์œ„ํ•œ NIO์™€ ์š”์ฒญ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ BIO๋ฅผ ๊ฒฐํ•ฉ:

public class BioHttpProtocolHandler implements AcceptableProtocolHandler {
private final RequestDispatcher dispatcher;
private final HttpRequestParser parser;
private final RequestExecutorService requestExecutorService;

@Override
public void accept(SocketChannel channel, Selector selector,
ByteBuffer initialBuffer) throws Exception {
// 1. NIO selector์—์„œ ๋ถ„๋ฆฌ
detachFromSelector(channel, selector);

// 2. ๋ธ”๋กœํ‚น ๋ชจ๋“œ๋กœ ์ „ํ™˜
channel.configureBlocking(true);
Socket socket = channel.socket();

// 3. ์›Œ์ปค ์Šค๋ ˆ๋“œ์— ์œ„์ž„
requestExecutorService.execute(() -> {
try (InputStream in = socket.getInputStream();
BufferedWriter out = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream()))) {

// 4. ์™„์ „ํ•œ ์š”์ฒญ ์ฝ๊ธฐ (๋ธ”๋กœํ‚น)
String rawRequest = HttpUtils.readRawRequest(initialBuffer, in);

// 5. ํŒŒ์‹ฑ ๋ฐ ๋””์ŠคํŒจ์น˜
HttpRequest<?> req = parser.parse(rawRequest);
HttpResponse res = new HttpResponse();
dispatcher.dispatch(req, res);

// 6. ์‘๋‹ต ์“ฐ๊ธฐ (๋ธ”๋กœํ‚น)
writeResponse(out, res.getResponseEntity());
} catch (Exception e) {
e.printStackTrace();
}
});
}
}

ํ๋ฆ„:

  1. NIO Selector๊ฐ€ ์—ฐ๊ฒฐ ์ˆ˜๋ฝ
  2. ์ดˆ๊ธฐ ๋ฐ”์ดํŠธ๋ฅผ ์ฝ์–ด ํ”„๋กœํ† ์ฝœ ๊ฐ์ง€
  3. ์ฑ„๋„์„ ๋ธ”๋กœํ‚น ๋ชจ๋“œ๋กœ ์ „ํ™˜
  4. Selector์—์„œ ๋ถ„๋ฆฌํ•˜๊ณ  ์›Œ์ปค ์Šค๋ ˆ๋“œ์— ์œ„์ž„
  5. ์›Œ์ปค ์Šค๋ ˆ๋“œ์—์„œ ๋ธ”๋กœํ‚น I/O๋กœ ์š”์ฒญ/์‘๋‹ต ์ฒ˜๋ฆฌ

์žฅ์ :

  • ๊ฐ„๋‹จํ•œ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ชจ๋ธ (๋ธ”๋กœํ‚น I/O)
  • ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ์™€ ์ž˜ ์ž‘๋™
  • ์ˆœ์ˆ˜ NIO๋ณด๋‹ค ๋‚ฎ์€ ๋ณต์žก๋„
  • ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ executor์™€ ํ•จ๊ป˜ ์ข‹์€ ์ฒ˜๋ฆฌ๋Ÿ‰

์„ค์ •:

server.execution-mode=hybrid
server.thread-type=virtual

์ˆœ์ˆ˜ NIO ๋ชจ๋“œโ€‹

์ตœ๋Œ€ ํ™•์žฅ์„ฑ์„ ์œ„ํ•œ ์™„์ „ํ•œ ๋…ผ๋ธ”๋กœํ‚น I/O:

public class NioHttpProtocolHandler implements AcceptableProtocolHandler {
private final RequestDispatcher dispatcher;
private final HttpRequestParser parser;
private final RequestExecutorService requestExecutorService;

@Override
public void accept(SocketChannel channel, Selector selector,
ByteBuffer initialBuffer) throws Exception {
// 1. ์ƒํƒœ๋ฅผ ๊ฐ€์ง„ ์—ฐ๊ฒฐ ํ•ธ๋“ค๋Ÿฌ ์ƒ์„ฑ
HttpConnectionHandler handler = new HttpConnectionHandler(
channel, selector, dispatcher, parser,
requestExecutorService, initialBuffer
);

// 2. ํ•ธ๋“ค๋Ÿฌ๋ฅผ attachment๋กœ ํ•˜์—ฌ READ ์ด๋ฒคํŠธ ๋“ฑ๋ก
channel.register(selector, SelectionKey.OP_READ, handler);

// 3. ์ดˆ๊ธฐ ์ฝ๊ธฐ ํŠธ๋ฆฌ๊ฑฐ
handler.read(channel.keyFor(selector));
}
}

HttpConnectionHandler ์ƒํƒœ ๋จธ์‹ โ€‹

public class HttpConnectionHandler implements ReadableHandler, WritableHandler {
private final SocketChannel channel;
private final Selector selector;
private final ByteBuffer readBuffer = ByteBuffer.allocate(8192);
private volatile ByteBuffer writeBuffer;
private HttpConnectionStatus currentState = HttpConnectionStatus.READING;

@Override
public void read(SelectionKey key) throws Exception {
if (currentState != HttpConnectionStatus.READING) return;

// 1. ๋…ผ๋ธ”๋กœํ‚น ์ฝ๊ธฐ
int bytesRead = channel.read(readBuffer);
if (bytesRead == -1) {
closeConnection(key);
return;
}

readBuffer.flip();

// 2. ์š”์ฒญ์ด ์™„๋ฃŒ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
if (HttpUtils.isRequestComplete(readBuffer)) {
currentState = HttpConnectionStatus.PROCESSING;
key.interestOps(0); // ์ด๋ฒคํŠธ ๊ฐ์ง€ ์ค‘์ง€

// 3. ์š”์ฒญ ์ถ”์ถœ
byte[] requestBytes = new byte[readBuffer.remaining()];
readBuffer.get(requestBytes);
String rawRequest = new String(requestBytes, StandardCharsets.UTF_8);

// 4. ์›Œ์ปค ์Šค๋ ˆ๋“œ์—์„œ ์ฒ˜๋ฆฌ
requestExecutorService.execute(() -> {
try {
HttpRequest<?> req = parser.parse(rawRequest);
HttpResponse res = new HttpResponse();
dispatcher.dispatch(req, res);

// 5. ์‘๋‹ต ์ค€๋น„ ๋ฐ WRITING ์ƒํƒœ๋กœ ์ „ํ™˜
this.writeBuffer = HttpUtils.createResponseBuffer(
res.getResponseEntity()
);
this.currentState = HttpConnectionStatus.WRITING;

// 6. WRITE ์ด๋ฒคํŠธ ๋“ฑ๋ก
key.interestOps(SelectionKey.OP_WRITE);
selector.wakeup();

} catch (Exception e) {
closeConnection(key);
e.printStackTrace();
}
});

readBuffer.clear();
}
}

@Override
public void write(SelectionKey key) throws IOException {
if (currentState != HttpConnectionStatus.WRITING || writeBuffer == null)
return;

// ๋…ผ๋ธ”๋กœํ‚น ์“ฐ๊ธฐ
channel.write(writeBuffer);

if (!writeBuffer.hasRemaining()) {
// ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์ „์†ก ์™„๋ฃŒ
currentState = HttpConnectionStatus.DONE;
closeConnection(key);
}
// ๋ฐ์ดํ„ฐ๊ฐ€ ๋‚จ์•„์žˆ์œผ๋ฉด, selector๊ฐ€ ์ค€๋น„๋˜์—ˆ์„ ๋•Œ ๋‹ค์‹œ write ํŠธ๋ฆฌ๊ฑฐ
}
}

์ƒํƒœ ๋จธ์‹ :

READING โ†’ PROCESSING โ†’ WRITING โ†’ DONE
โ†‘ โ†“
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ (close/reset) โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

์žฅ์ :

  • ์ตœ๋Œ€ ํ™•์žฅ์„ฑ (๋‹จ์ผ ์Šค๋ ˆ๋“œ๊ฐ€ ์ˆ˜์ฒœ ๊ฐœ์˜ ์—ฐ๊ฒฐ ์ฒ˜๋ฆฌ)
  • ์ตœ์†Œํ•œ์˜ ์Šค๋ ˆ๋“œ ์ปจํ…์ŠคํŠธ ์Šค์œ„์นญ
  • ํšจ์œจ์ ์ธ ๋ฆฌ์†Œ์Šค ํ™œ์šฉ
  • ๋†’์€ ๋™์‹œ์„ฑ ์‹œ๋‚˜๋ฆฌ์˜ค์— ์ตœ์ 

์„ค์ •:

server.execution-mode=nio
server.thread-type=platform
server.thread-pool-size=100

NIO ์ด๋ฒคํŠธ ๋ฃจํ”„ ์•„ํ‚คํ…์ฒ˜โ€‹

NioHybridServerStrategyโ€‹

๋ฉ”์ธ ์ด๋ฒคํŠธ ๋ฃจํ”„ ๊ตฌํ˜„:

@Component
public class NioHybridServerStrategy implements ServerStrategy {
private final ConnectionManager connectionManager;
private volatile boolean running = true;
private Selector selector;
private ServerSocketChannel serverChannel;

@Override
public int start(int port) throws Exception {
// 1. NIO selector ์ดˆ๊ธฐํ™”
selector = Selector.open();
serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(port));
serverChannel.configureBlocking(false);

// 2. ACCEPT ์ด๋ฒคํŠธ ๋“ฑ๋ก
serverChannel.register(selector, SelectionKey.OP_ACCEPT);

// 3. ์ด๋ฒคํŠธ ๋ฃจํ”„ ์Šค๋ ˆ๋“œ ์‹œ์ž‘
running = true;
Thread t = new Thread(this::eventLoop, "sprout-nio-loop");
t.setDaemon(false);
t.start();

return ((InetSocketAddress) serverChannel.getLocalAddress()).getPort();
}

private void eventLoop() {
while (running) {
selector.select(); // ์ด๋ฒคํŠธ๊ฐ€ ์ค€๋น„๋  ๋•Œ๊นŒ์ง€ ๋ธ”๋กœํ‚น

for (Iterator<SelectionKey> it = selector.selectedKeys().iterator();
it.hasNext();) {
SelectionKey key = it.next();
it.remove();

if (!key.isValid()) {
cleanupConnection(key);
continue;
}

try {
// ์ƒˆ๋กœ์šด ์—ฐ๊ฒฐ ์ˆ˜๋ฝ
if (key.isAcceptable()) {
connectionManager.acceptConnection(key, selector);
}

Object attachment = key.attachment();

// ์ฝ๊ธฐ ๊ฐ€๋Šฅ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
if (key.isReadable() && attachment instanceof ReadableHandler rh) {
rh.read(key);
}

// ์“ฐ๊ธฐ ๊ฐ€๋Šฅ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
if (key.isWritable() && attachment instanceof WritableHandler wh) {
wh.write(key);
}
} catch (IOException ioe) {
cleanupConnection(key);
} catch (Exception e) {
e.printStackTrace();
cleanupConnection(key);
}
}
}
}
}

์ด๋ฒคํŠธ ๋ฃจํ”„ ์ฑ…์ž„:

  • ConnectionManager๋ฅผ ํ†ตํ•œ ์ƒˆ๋กœ์šด ์—ฐ๊ฒฐ ์ˆ˜๋ฝ
  • ReadableHandler๋กœ READ ์ด๋ฒคํŠธ ์œ„์ž„
  • WritableHandler๋กœ WRITE ์ด๋ฒคํŠธ ์œ„์ž„
  • ์˜ค๋ฅ˜ ์‹œ ์—ฐ๊ฒฐ ์ •๋ฆฌ

ํ”„๋กœํ† ์ฝœ ๊ฐ์ง€ ๋ฐ ๋ผ์šฐํŒ…โ€‹

์—ฐ๊ฒฐ ์ˆ˜๋ฝ ํ๋ฆ„โ€‹

@Component
public class DefaultConnectionManager implements ConnectionManager {
private final List<ProtocolDetector> detectors;
private final List<ProtocolHandler> handlers;

@Override
public void acceptConnection(SelectionKey selectionKey, Selector selector)
throws Exception {
// 1. ์—ฐ๊ฒฐ ์ˆ˜๋ฝ
ServerSocketChannel serverChannel =
(ServerSocketChannel) selectionKey.channel();
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);

// 2. ํ”„๋กœํ† ์ฝœ ๊ฐ์ง€๋ฅผ ์œ„ํ•œ ์ดˆ๊ธฐ ๋ฐ”์ดํŠธ ์ฝ๊ธฐ
ByteBuffer buffer = ByteBuffer.allocate(2048);
int bytesRead = clientChannel.read(buffer);

if (bytesRead <= 0) {
clientChannel.close();
return;
}

buffer.flip();

// 3. ํ”„๋กœํ† ์ฝœ ๊ฐ์ง€
String detectedProtocol = "UNKNOWN";
for (ProtocolDetector detector : detectors) {
detectedProtocol = detector.detect(buffer);
if (!"UNKNOWN".equals(detectedProtocol)) {
break;
}
}

if ("UNKNOWN".equals(detectedProtocol)) {
clientChannel.close();
return;
}

// 4. ์ ์ ˆํ•œ ํ•ธ๋“ค๋Ÿฌ๋กœ ๋ผ์šฐํŒ…
for (ProtocolHandler handler : handlers) {
if (handler.supports(detectedProtocol)) {
if (handler instanceof AcceptableProtocolHandler) {
((AcceptableProtocolHandler) handler)
.accept(clientChannel, selector, buffer);
return;
}
}
}
}
}

HTTP ํ”„๋กœํ† ์ฝœ ๊ฐ์ง€โ€‹

@Component
public class HttpProtocolDetector implements ProtocolDetector {
private static final Set<String> HTTP_METHODS = Set.of(
"GET ", "POST ", "PUT ", "DELETE ", "HEAD ",
"OPTIONS ", "PATCH ", "TRACE "
);

@Override
public String detect(ByteBuffer buffer) throws Exception {
// ๋ฒ„ํผ ์œ„์น˜ ์ €์žฅ
buffer.mark();

// ์ฒ˜์Œ 8๋ฐ”์ดํŠธ ์ฝ๊ธฐ
int readLimit = Math.min(buffer.remaining(), 8);
byte[] headerBytes = new byte[readLimit];
buffer.get(headerBytes);

// ๋ฒ„ํผ ์œ„์น˜ ๋ณต์›
buffer.reset();

String prefix = new String(headerBytes, StandardCharsets.UTF_8);

// HTTP ๋ฉ”์„œ๋“œ ํ™•์ธ
if (HTTP_METHODS.stream().anyMatch(prefix::startsWith)) {
return "HTTP/1.1";
}

return "UNKNOWN";
}
}

๊ฐ์ง€ ํ”„๋กœ์„ธ์Šค:

  1. ์—ฐ๊ฒฐ์—์„œ ์ฒ˜์Œ ๋ฐ”์ดํŠธ ์ฝ๊ธฐ (๋น„ํŒŒ๊ดด์ )
  2. HTTP ๋ฉ”์„œ๋“œ ํ‚ค์›Œ๋“œ ํ™•์ธ
  3. ๊ฐ์ง€๋œ ํ”„๋กœํ† ์ฝœ ๋˜๋Š” "UNKNOWN" ๋ฐ˜ํ™˜
  4. ํ›„์† ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด ๋ฒ„ํผ ๋ณด์กด

์š”์ฒญ ์™„๋ฃŒ ๊ฐ์ง€โ€‹

HTTP ์š”์ฒญ ํŒŒ์‹ฑโ€‹

public final class HttpUtils {
public static boolean isRequestComplete(ByteBuffer buffer) {
// 1. ํ—ค๋” ๋ ์ฐพ๊ธฐ (\r\n\r\n)
byte[] arr = new byte[buffer.remaining()];
buffer.get(arr);
String content = new String(arr, StandardCharsets.UTF_8);
int headerEnd = content.indexOf("\r\n\r\n");

if (headerEnd < 0) {
return false; // ํ—ค๋” ๋ถˆ์™„์ „
}

String headers = content.substring(0, headerEnd);

// 2. Content-Length ๋˜๋Š” Transfer-Encoding ํ™•์ธ
int contentLength = parseContentLength(headers);
boolean isChunked = isChunked(headers);

int bodyStart = headerEnd + 4;
int totalLength = content.length();

if (isChunked) {
// ์ฒญํฌ ์ธ์ฝ”๋”ฉ: 0\r\n\r\n ํ™•์ธ
String body = content.substring(bodyStart);
return isChunkedBodyComplete(body);
} else if (contentLength >= 0) {
// Content-Length: ๋ณธ๋ฌธ ํฌ๊ธฐ ํ™•์ธ
int bodyReceived = totalLength - bodyStart;
return bodyReceived >= contentLength;
} else {
// ๋ณธ๋ฌธ ์—†์Œ (GET ์š”์ฒญ)
return true;
}
}
}

์™„๋ฃŒ ๊ธฐ์ค€:

  • ํ—ค๋”: \r\n\r\n ํฌํ•จํ•ด์•ผ ํ•จ
  • Content-Length: ๋ณธ๋ฌธ ๋ฐ”์ดํŠธ๊ฐ€ ์„ ์–ธ๋œ ๊ธธ์ด์™€ ์ผ์น˜ํ•ด์•ผ ํ•จ
  • Chunked: ๋งˆ์ง€๋ง‰ ์ฒญํฌ๋Š” 0\r\n\r\n์ด์–ด์•ผ ํ•จ
  • ๋ณธ๋ฌธ ์—†์Œ: ํ—ค๋” ์ดํ›„ ์™„๋ฃŒ

๋ชจ๋“œ ๋น„๊ตโ€‹

ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๋ชจ๋“œ (BIO + ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ)โ€‹

์ตœ์  ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค:

  • ๊ฐ„๋‹จํ•œ ์ฝ”๋“œ๋กœ ๋†’์€ ์ฒ˜๋ฆฌ๋Ÿ‰
  • Java 21+ ํ”„๋กœ์ ํŠธ
  • ์ ๋‹นํ•œ ์š”์ฒญ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„์„ ๊ฐ€์ง„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜

์•„ํ‚คํ…์ฒ˜:

[NIO Selector] โ†’ Accept โ†’ [Detect Protocol] โ†’ [Switch to BIO]
โ†“
[Virtual Thread]
โ†“
[Blocking Read/Write]

์žฅ์ :

  • ๊ฐ„๋‹จํ•˜๊ณ  ์ฝ๊ธฐ ์‰ฌ์šด ์ฝ”๋“œ
  • ์ž๋™ ๋ฐฑํ”„๋ ˆ์…”
  • ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ์™€ ์ž˜ ์ž‘๋™
  • ๋””๋ฒ„๊น… ์šฉ์ด

๋‹จ์ :

  • ์ˆœ์ˆ˜ NIO๋ณด๋‹ค ์—ฐ๊ฒฐ๋‹น ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ๋†’์Œ
  • ์Šค๋ ˆ๋“œ ์ „ํ™˜ ์˜ค๋ฒ„ํ—ค๋“œ

์ˆœ์ˆ˜ NIO ๋ชจ๋“œโ€‹

์ตœ์  ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค:

  • ์ตœ๋Œ€ ํ™•์žฅ์„ฑ
  • ๋‚ฎ์€ ์ง€์—ฐ์‹œ๊ฐ„ ์š”๊ตฌ์‚ฌํ•ญ
  • ๋ฆฌ์†Œ์Šค๊ฐ€ ์ œํ•œ๋œ ํ™˜๊ฒฝ

์•„ํ‚คํ…์ฒ˜:

[NIO Selector] โ†’ Accept โ†’ [Detect Protocol] โ†’ [Register READ]
โ†‘ โ†“
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€[Write Complete]โ”€โ”€โ”€[Process in Pool Thread]

์žฅ์ :

  • ๋‹จ์ผ ์Šค๋ ˆ๋“œ๊ฐ€ ์ˆ˜์ฒœ ๊ฐœ์˜ ์—ฐ๊ฒฐ ์ฒ˜๋ฆฌ
  • ์ตœ์†Œ ๋ฉ”๋ชจ๋ฆฌ ๊ณต๊ฐ„
  • ๋‚ฎ์€ ์ง€์—ฐ์‹œ๊ฐ„

๋‹จ์ :

  • ๋ณต์žกํ•œ ์ƒํƒœ ๋จธ์‹ 
  • ๋””๋ฒ„๊น… ์–ด๋ ค์›€
  • ์„ธ์‹ฌํ•œ ๋ฒ„ํผ ๊ด€๋ฆฌ ํ•„์š”

์„ค์ • ๊ฐ€์ด๋“œโ€‹

๊ถŒ์žฅ ์„ค์ •โ€‹

๋†’์€ ์ฒ˜๋ฆฌ๋Ÿ‰ API ์„œ๋ฒ„ (Java 21+)โ€‹

server.execution-mode=hybrid
server.thread-type=virtual

์ตœ๋Œ€ ํ™•์žฅ์„ฑ (์—ฐ๊ฒฐ ์ง‘์•ฝ์ )โ€‹

server.execution-mode=nio
server.thread-type=platform
server.thread-pool-size=200

๋ ˆ๊ฑฐ์‹œ ํ˜ธํ™˜์„ฑ (Java 11/17)โ€‹

server.execution-mode=hybrid
server.thread-type=platform
server.thread-pool-size=500

๋ชจ๋ฒ” ์‚ฌ๋ก€โ€‹

1. ์˜ฌ๋ฐ”๋ฅธ ๋ชจ๋“œ ์„ ํƒโ€‹

// ๋Œ€๋ถ€๋ถ„์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ (Java 21+)
server.execution-mode=hybrid
server.thread-type=virtual

// ๊ทนํ•œ์˜ ํ™•์žฅ์„ฑ ํ•„์š” ์‹œ
server.execution-mode=nio
server.thread-type=platform

2. ์ปจํ…์ŠคํŠธ ์ „ํŒŒโ€‹

๊ฐ€์ƒ ์Šค๋ ˆ๋“œ executor๋Š” ์ž๋™์œผ๋กœ ์ปจํ…์ŠคํŠธ๋ฅผ ์ „ํŒŒํ•ฉ๋‹ˆ๋‹ค:

public class VirtualRequestExecutorService {
@Override
public void execute(Runnable task) {
// ํƒœ์Šคํฌ ์ œ์ถœ ์ „์— ์ปจํ…์ŠคํŠธ ์บก์ฒ˜
final ContextSnapshot snapshot = new ContextSnapshot(propagators);
pool.execute(snapshot.wrap(task));
}
}

3. ์šฐ์•„ํ•œ ์ข…๋ฃŒโ€‹

@Component
public class ServerShutdownHook {
private final HttpServer server;

@PreDestroy
public void shutdown() throws Exception {
server.stop();
}
}

4. ์Šค๋ ˆ๋“œ ์‚ฌ์šฉ๋Ÿ‰ ๋ชจ๋‹ˆํ„ฐ๋งโ€‹

// ํ”Œ๋žซํผ ์Šค๋ ˆ๋“œ: ์Šค๋ ˆ๋“œ ํ’€ ํฌํ™”๋„ ๋ชจ๋‹ˆํ„ฐ๋ง
// ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ: ๋ฉ”๋ชจ๋ฆฌ ๋ฐ CPU ์‚ฌ์šฉ๋Ÿ‰ ๋ชจ๋‹ˆํ„ฐ๋ง

์„ฑ๋Šฅ ํŠน์„ฑโ€‹

๊ฐ€์ƒ ์Šค๋ ˆ๋“œ ๋ชจ๋“œโ€‹

  • ํ™•์žฅ์„ฑ: ๋†’์€ ์—ฐ๊ฒฐ ์ˆ˜์— ํƒ์›”

ํ”Œ๋žซํผ ์Šค๋ ˆ๋“œ ํ’€ ๋ชจ๋“œโ€‹

  • ํ™•์žฅ์„ฑ: ์Šค๋ ˆ๋“œ ํ’€ ํฌ๊ธฐ์— ์˜ํ•ด ์ œํ•œ๋จ

NIO vs ํ•˜์ด๋ธŒ๋ฆฌ๋“œโ€‹

  • NIO: ๋‚ฎ์€ ๋ฉ”๋ชจ๋ฆฌ, ๋†’์€ ๋ณต์žก๋„
  • ํ•˜์ด๋ธŒ๋ฆฌ๋“œ: ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ์™€ ํ•จ๊ป˜ ๋†’์€ ์ฒ˜๋ฆฌ๋Ÿ‰, ๊ฐ„๋‹จํ•œ ์ฝ”๋“œ
  • ํ•˜์ด๋ธŒ๋ฆฌ๋“œ + ๊ฐ€์ƒ: ํ˜„๋Œ€์ ์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์ตœ์ ์˜ ๊ท ํ˜•

ํ™•์žฅ ํฌ์ธํŠธโ€‹

์ปค์Šคํ…€ ํ”„๋กœํ† ์ฝœ ํ•ธ๋“ค๋Ÿฌโ€‹

@Component
public class CustomProtocolHandler implements AcceptableProtocolHandler {
@Override
public void accept(SocketChannel channel, Selector selector,
ByteBuffer buffer) throws Exception {
// ์ปค์Šคํ…€ ํ”„๋กœํ† ์ฝœ ์ฒ˜๋ฆฌ ๋กœ์ง
}

@Override
public boolean supports(String protocol) {
return "CUSTOM/1.0".equals(protocol);
}
}

์ปค์Šคํ…€ ํ”„๋กœํ† ์ฝœ ๊ฐ์ง€๊ธฐโ€‹

@Component
public class CustomProtocolDetector implements ProtocolDetector {
@Override
public String detect(ByteBuffer buffer) throws Exception {
// ๋ฒ„ํผ๋ฅผ ๊ฒ€์‚ฌํ•˜๊ณ  ํ”„๋กœํ† ์ฝœ ์ด๋ฆ„ ๋ฐ˜ํ™˜
return "CUSTOM/1.0";
}
}