티스토리 뷰
개요
현재 프로젝트로 웹 애플리케이션 서버를 구현 중이며, Connector 구조는 BIO 기반으로 설계되어 있다. 이 구조는 기본적인 클라이언트 요청 처리, 서버 소켓 관리, 스레드풀 운용 등을 정상적으로 수행하고 있다.
다만, 향후 다양한 I/O 모델(NIO 등)을 지원하도록 시스템을 확장하기 위해서는 현재 구조에 일부 한계가 존재한다. 특히 NIO Connector 구조를 추가하기에 앞서, 보다 확장성과 유연성을 갖춘 설계로 리팩토링을 진행하고자 한다.
(Connector에 대해 궁금하면 링크 참고 바랍니다.)
오픈소스 분석 - Tomcat 소켓 I/O 동작 방식 파헤쳐보기(BIO, NIO Connector)
개요 이전 글에서는 C10K 문제를 출발점으로 서버 아키텍처와 커널 I/O의 발전 과정(멀티 프로세스 → 멀티 스레드 → 멀티플렉싱)을 살펴보고, 자바의 멀티플렉싱 지원 컴포넌트인 Selector에 대해
ego2-1.tistory.com
핵심 문제점
현재 Connector의 구조는 UML 이미지처럼 구성되어 있다. 다양한 I/O모델(BIO, NIO, APR)을 지원해야하나, Connector에서 구현체인 BioProtocolHandler를 직접 의존하고 있다. 향후 NIO로 변경할 때 Connector의 소스코드를 수정해야하는 문제점이 존재한다.

public class Connector {
private static final Logger log = LoggerFactory.getLogger(Connector.class);
private final ProtocolHandler protocolHandler;
public Connector(StandardContext context, int port) {
this.protocolHandler = new BioProtocolHandler(context, port); // 구현체 의존(유연성 부족)
}
public void start() throws IOException {
log.info("Starting BioConnector...");
protocolHandler.start();
}
}
또한, 다른 I/O 모델이 추가될 경우에도 일부 공통 로직이 존재할 수 있다. 그러나 현재는 이러한 공통 골격을 담당하는 상위 추상 클래스가 정의되어 있지 않아, 추후 공통 로직임에도 불구하고 각 구현체에 중복으로 작성되어 유지보수에 어려움이 생길 가능성이 있다.
더불어, 소켓 생성부터 커넥션 핸들러 생성, 스레드 시작까지 모든 초기화 로직이 하나의 메서드 내부에서 순차적으로 실행되고 있다. 이로 인해 서버 소켓 바인딩(bind), 커넥션 수락(accept), 비즈니스 요청 처리 등 각 단계가 분리되어 관리되지 않으며, 결과적으로 전체 시스템의 라이프사이클이 명확히 구분되지 않는 문제가 있다.
public class BioEndpoint {
private final ExecutorService executor = Executors.newFixedThreadPool(200);
private final StandardContext context;
private final int port;
public BioEndpoint(StandardContext context, int port) {
this.context = context;
this.port = port;
}
// 하나의 메서드로 관리되는 문제
public void start() {
try {
ServerSocket serverSocket = new ServerSocket(port);
BioConnectionHandler handler = new BioConnectionHandler(context);
Thread acceptor = new Thread(new Acceptor(serverSocket, executor, handler));
acceptor.start();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
구조 개선
현재 구조가 확장성을 확보하기 위해서는 디자인 패턴 기반의 리팩토링이 필요하다. Connector가 ProtocolHandler의 구체 구현체에 의존하고 있으므로, 이를 제거하고 전략 패턴(Strategy Pattern)을 적용하여 외부에서 ProtocolHandler를 주입할 수 있도록 개선한다.
이렇게 하면 BIO, NIO, APR등 다양한 I/O모델 전략을 손쉽게 교체할 수 있을 것이고, 추후 server.xml을 파싱하여 런타임 시점에 ProtocolHandler을 선택하도록 확장해도 Connector코드는 수정할 필요가 없게 된다. Connector는 클라이언트에서 주입 받은 ProtocolHandler(전략)에 대해 실행하기만 하면 된다.

public class WebServer {
... 생략 ...
public static void main(String[] args) throws Exception {
... 생략 ...
// 2. 프로토콜 핸들러 선택(Http11BioProtocol, Http11NioProtocol 등)
ProtocolHandler handler = new Http11BioProtocol(DEFAULT_PORT);
// 3. Connector 구성
Connector connector = new Connector(handler, standardContext); // 프로토콜 핸들러 주입
}
}
public class Connector implements Lifecycle {
private static final Logger log = LoggerFactory.getLogger(Connector.class);
private final ProtocolHandler protocolHandler;
public Connector(ProtocolHandler protocolHandler, StandardContext context) {
this.protocolHandler = protocolHandler;
this.protocolHandler.setContext(context);
}
@Override
public void init() throws Exception {
log.info("Initializing ProtocolHandler...");
protocolHandler.initProtocol();
}
... 생략 ...
}
또한, 전송 프로토콜에 관계없이 공통적으로 반복되는 라이프사이클 흐름은 상위 추상 클래스를 두어 템플릿 메서드(Template Method) 패턴으로 정의한다. 이 추상 클래스에서 전체 처리의 뼈대 로직을 작성하고, 하위 클래스에서는 각 I/O 모델의 특성에 맞는 세부 동작을 Hook 메서드 오버라이딩을 통해 구현하도록 분리한다.
이를 통해 공통 로직과 개별 구현 로직을 명확히 분리하고, 새로운 프로토콜이 추가되더라도 최소한의 중복으로 일관된 구조를 유지할 수 있다.

[ProtocolHandler 인터페이스 정의]
public interface ProtocolHandler {
void initProtocol() throws Exception;
void startProtocol() throws Exception;
void stopProtocol() throws Exception;
void destroyProtocol() throws Exception;
void setContext(StandardContext context);
}
[AbstractProtocol: 템플릿 메서드 패턴 적용]
public abstract class AbstractProtocol implements ProtocolHandler {
protected StandardContext context;
protected AbstractEndpoint endpoint;
protected int port;
protected AbstractProtocol(int port) {
this.port = port;
}
@Override
public void setContext(StandardContext context) {
this.context = context;
}
@Override
public final void initProtocol() throws Exception {
endpoint.bind(port); // 공통 작업
initInternal(); // hook, 서브클래스에서 구현할 초기화 작업
}
@Override
public final void startProtocol() throws Exception {
endpoint.startEndpoint(context);
startInternal();
}
@Override
public final void stopProtocol() throws Exception {
endpoint.stopEndpoint();
stopInternal();
}
@Override
public final void destroyProtocol() throws Exception {
destroyInternal();
}
/* 서브클래스에서 구현해야 하는 hook 메소드들 */
protected abstract void initInternal() throws Exception;
protected abstract void startInternal() throws Exception;
protected abstract void stopInternal() throws Exception;
protected abstract void destroyInternal() throws Exception;
}
[BIO 모델 구현체: Http11BioProtocol]
public class Http11BioProtocol extends AbstractProtocol {
public Http11BioProtocol(int port) {
super(port);
this.endpoint = new BioEndpoint();
}
@Override
protected void initInternal() throws Exception {
// BIO 모델에 특화된 동작 구현
}
@Override
protected void startInternal() throws Exception {
// BIO 모델에 특화된 동작 구현
}
@Override
protected void stopInternal() throws Exception {
// BIO 모델에 특화된 동작 구현
}
@Override
protected void destroyInternal() throws Exception {
// BIO 모델에 특화된 동작 구현
}
}
EndPoint에 대해서도 동일하게 템플릿 메소드 패턴을 적용하면 최종 UML은 다음과 같다.

디자인 패턴 적용 과정에서 인사이트
디자인 패턴을 도입하는 과정에서 느낀 점은, 객체 간 협력 구조의 복잡도와 유연성, 확장성 간의 균형을 항상 고민해야 한다는 것이다. 패턴을 적용하면서 응집도를 높이고 결합도를 낮추는 장점이 있지만, 반대로 설계 자체가 과도하게 복잡해질 위험도 존재한다.(오버엔지니어링)
따라서 단순히 패턴을 적용하는 것이 목적이 아니라, 해당 패턴이 현재 문제를 해결하는데 실질적인 필요성과 근거가 있는지 명확히 검토하는 과정이 중요하다는 점을 다시 한 번 체감할 수 있었다.
'Java' 카테고리의 다른 글
| 자바 리플렉션 API에 대해 (2) | 2025.07.08 |
|---|---|
| 오픈소스 분석 - Tomcat 소켓 I/O 동작 방식 파헤쳐보기(BIO, NIO Connector) (0) | 2025.05.28 |
| 자바에서는 멀티플렉싱을 어떻게 지원할까? – Java NIO의 Selector (0) | 2025.05.15 |
| 접두사 기반 URL 탐색 구조 성능 측정 및 최적화 (0) | 2025.05.05 |
| Servlet과 Servlet Container 동작 과정의 이해 (3) | 2025.04.09 |