티스토리 뷰

개요 

현재 프로젝트로 웹 애플리케이션 서버를 구현 중이며, 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의 소스코드를 수정해야하는 문제점이 존재한다.

현재 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(전략)에 대해 실행하기만 하면 된다.

Strategy Pattern

 

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 메서드 오버라이딩을 통해 구현하도록 분리한다.

 

이를 통해 공통 로직과 개별 구현 로직을 명확히 분리하고, 새로운 프로토콜이 추가되더라도 최소한의 중복으로 일관된 구조를 유지할 수 있다.

Template Method

[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은 다음과 같다.

개선 후 Connector 아키텍처

 

디자인 패턴 적용 과정에서 인사이트

디자인 패턴을 도입하는 과정에서 느낀 점은, 객체 간 협력 구조의 복잡도 유연성, 확장성 간의 균형을 항상 고민해야 한다는 것이다. 패턴을 적용하면서 응집도를 높이고 결합도를 낮추는 장점이 있지만, 반대로 설계 자체가 과도하게 복잡해질 위험도 존재한다.(오버엔지니어링)

 

따라서 단순히 패턴을 적용하는 것이 목적이 아니라, 해당 패턴이 현재 문제를 해결하는데 실질적인 필요성과 근거가 있는지 명확히 검토하는 과정이 중요하다는 점을 다시 한 번 체감할 수 있었다.

 
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/12   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
글 보관함