이 프로젝트는 **학습용 경량 WAS(Web Application Server)**로서, 실제 웹 서버의 핵심 설계 원칙을 구현합니다.
- HTTP 연결 관리 및 Keep-Alive 지원
- 요청 파이프라인 구축 (필터 → 디스패처 → 서블릿)
- 동적 라우팅 및 메서드 핸들링
- 정적 자원 제공 분리
- 최소한의 DI(Dependency Injection) 컨테이너 구현
- 언어: Java 17+
- 데이터베이스: H2 (인메모리/파일 기반)
- HTTP 파싱: 직접 구현
- 라우팅: 직접 구현
- DI: 반사(Reflection) 기반 간단한 컨테이너
┌─────────────────────────────────────────────────────────────┐
│ 1. 클라이언트 요청 │
└────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. ConnectionManager: 소켓 수명 및 연결 풀 관리 │
│ - Keep-Alive 관리 │
│ - 버퍼 닫기 시점 처리 │
└────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. RequestHandler: HTTP 요청 파싱 │
│ - 요청 라인, 헤더, 본문 분석 │
│ - 요청 메서드별 본문 처리 (Form, Multipart 등) │
└────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. FilterChain: 교차 관심사 처리 (Chain of Responsibility) │
│ - AuthFilter: 인증 확인 │
│ - LoginFilter: 로그인 처리 │
│ - 기타 공통 필터들 │
└────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 5. Dispatcher (Front Controller) │
│ - AppServlet 또는 StaticServlet으로 라우팅 │
└────────────────────┬────────────────────────────────────────┘
│
┌───────────┴───────────┐
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ AppServlet │ │ StaticServlet │
│ (동적 요청) │ │ (정적 자원) │
└──────────────────┘ └──────────────────┘
│ │
▼ │
┌─────────────────────────────────┐
│ AppRouteConfig: 라우트 매핑 │
│ RouteKey(Path, Method) → │
│ Facade → MethodHandler │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ MethodHandler: │
│ 1. ArgumentMapper로 요청 매핑 │
│ 2. AuthInjector로 인증 정보 주입│
│ 3. 핸들러 함수 실행 │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Facade (UseCase 진입점) │
│ - HomeFacade │
│ - ArticleFacade │
│ - UserFacade │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ UseCase (비즈니스 로직) │
│ - RegisterUseCase │
│ - PostingUseCase │
│ - UpdateUserUseCase │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ DAO / DataSource │
│ - UserDao │
│ - ArticleDao │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ H2 Database │
└─────────────────────────────────┘
위치: container/DIContainer.java, annotation/Singleton.java
설계 의도:
- 의존성 자동 주입으로 객체 결합도 낮추기
- 싱글톤 애노테이션으로 전역 상태 관리 및 메모리 효율화
- 리플렉션 기반으로 구성 파일 없이 자동 의존성 주입
특징:
@Singleton // 애노테이션으로 싱글톤 표시
public class DataSource { ... }
// 자동 의존성 주입
DIContainer.getInstance(Dispatcher.class); // 생성자 매개변수도 자동 주입이점:
- ✅ 테스트 용이: Mock 객체 교체 가능
- ✅ 순환 의존성 감지: ThreadLocal 사용
- ✅ 초기화 안전성: 생성 중 상태 추적
위치: filter/Filter.java, filter/FilterChain.java
설계 의도:
- 요청 처리 단계를 순차적 체인으로 구성
- 각 필터가 독립적으로 요청을 가로채기(intercept)
- 새로운 필터 추가 시 기존 코드 수정 불필요
구현 흐름:
AuthFilter → LoginFilter → ... → Dispatcher
│ │
└──────────────────────────────┘
(필터 체인 거쳐야 함)
핵심 클래스:
// Filter 인터페이스: 단일 책임 - 한 가지 요청 처리만
public interface Filter {
HttpResponse doFilter(HttpRequest request, FilterChain chain);
}
// FilterChain: 다음 필터로 전파
public class FilterChain {
public HttpResponse doChain(HttpRequest request) {
if (index < filters.size()) {
return filters.get(index++).doFilter(request, this);
}
return dispatcher.dispatch(request); // 마지막에 디스패처 호출
}
}이점:
- ✅ 관심사 분리: 인증, 로깅, 캐싱 등을 독립 필터로 구현
- ✅ 동적 필터 추가/제거 가능
- ✅ 필터 순서 조정 용이
위치: router/Dispatcher.java, router/servlet/AppServlet.java
설계 의도:
- 모든 동적 요청이 단일 진입점(Dispatcher)을 지나감
- 동적/정적 요청을 분리해 처리
- 중앙 집중식 요청 분배로 공통 로직 처리 용이
구현:
@Singleton
public class Dispatcher {
public HttpResponse dispatch(HttpRequest request) {
try {
// 1차: 동적 요청 처리 시도
Optional<HttpResponse> response = appServlet.service(request);
if (response.isPresent()) return response.get();
// 2차: 정적 자원 제공
response = staticServlet.service(request);
return response.orElseGet(HttpResponseFactory::notFound);
} catch (Exception e) {
return HttpResponseFactory.internalServerError(e.getMessage());
}
}
}이점:
- ✅ 요청 흐름 추적 용이
- ✅ 예외 처리 중앙 관리
- ✅ 요청 전처리/후처리 일관성
위치: application/router/MethodHandler.java, router/AppRouteConfig.java
설계 의도:
- 각 경로/메서드별로 다른 처리 전략(핸들러 함수) 설정
- 런타임에 경로에 따라 핸들러 동적 선택
- 새 엔드포인트 추가 시 전체 시스템 수정 불필요
라우트 등록 방식:
// RouteKey = (Path, HttpMethod) 조합
Map<RouteKey, MethodHandler<?>> routeMap = new HashMap<>();
routeMap.put(
new RouteKey(new Path("/register"), HttpMethod.POST),
new MethodHandler<>(mapper, handler, authInjector)
);이점:
- ✅ 엔드포인트 추가가 간단한 맵 수정으로 끝남
- ✅ 다양한 핸들러 전략 동시 운영 가능
- ✅ 테스트할 때 핸들러 Mock 주입 가능
위치: application/facade/Facade.java (상위 클래스)
HomeFacade.java- 홈, 회원가입 처리ArticleFacade.java- 게시글 처리UserFacade.java- 사용자 정보 처리
설계 의도:
- 여러 UseCase를 단일 Facade로 묶기
- 컨트롤러 레이어와 비즈니스 로직 간 결합도 낮추기
- 경로별 Facade를 조합하여 전체 라우팅 구성
구현 방식:
@Singleton
public class HomeFacade extends Facade {
private final HomePage homePage;
private final RegisterUseCase registerUseCase;
protected void createRouteMap() {
// 경로별로 ArgumentMapper와 핸들러를 등록
registerRoute(
"/",
HttpMethod.GET,
new VoidMapper<>(GeneralRequest.class),
req -> homePage.render(...) // 홈 페이지 렌더링
);
registerRoute(
"/register",
HttpMethod.POST,
new FormDataMapper<>(UserRequest.class),
req -> registerUseCase.execute(...) // 회원가입 실행
);
}
}이점:
- ✅ 기능별 Facade로 코드 응집도 향상
- ✅ 경로 기반 라우팅이 직관적
- ✅ Facade 추가만으로 새 엔드포인트 집합 추가 가능
위치: application/router/mapper/
FormDataMapper- form-urlencoded 요청MultipartMapper- multipart/form-data 요청QueryParameterMapper- URL 쿼리 매개변수VoidMapper- 매개변수 없음- 기타 커스텀 매퍼들
설계 의도:
- 요청 형식에 따라 다른 매핑 전략 적용
- 리플렉션을 활용한 자동 매핑으로 보일러플레이트 코드 최소화
- 요청 타입별로 독립적인 매퍼로 확장성 확보
사용 예:
// FormDataMapper: application/x-www-form-urlencoded
registerRoute(
"/login",
HttpMethod.POST,
new FormDataMapper<>(LoginRequest.class), // 자동 매핑
loginHandler
);
// MultipartMapper: multipart/form-data (파일 업로드)
registerRoute(
"/article",
HttpMethod.POST,
new MultipartMapper<>(ArticleRequest.class), // 이미지 + 데이터
postingHandler
);이점:
- ✅ 요청 형식별 복잡한 파싱 로직 캡슐화
- ✅ 새 요청 타입 추가 시 새 Mapper 구현만 필요
- ✅ 핸들러는 매핑된 DTO로 깔끔하게 작업
위치: application/usecase/rendering/
HomePage- 홈 페이지 렌더링ArticlePage- 게시글 상세 페이지 렌더링WritingPage- 글쓰기 페이지 렌더링
설계 의도:
- HTML 템플릿 기반 렌더링 프로세스 통일
- 데이터를 템플릿에 바인딩
- 페이지별 렌더링 로직 분리
예시:
// ArticlePage: 글 상세 조회 후 템플릿에 바인딩
public class ArticlePage {
public HttpResponse render(Article article, LoginUser user) {
String html = TemplateEngine.render("article/template.html", {
"writer": article.getWriterId(),
"title": article.getTitle(),
"image": article.getImageUrl(),
"content": article.getContent(),
});
return HttpResponseFactory.html(html);
}
}이점:
- ✅ 렌더링 로직과 비즈니스 로직 분리
- ✅ 템플릿만 변경하면 렌더링 변경 가능
- ✅ 자동 HTML 이스케이핑 및 보안 강화 가능
위치: application/db/
UserDao- 사용자 CRUDArticleDao- 게시글 CRUDDataSource- H2 연결 관리
설계 의도:
- 데이터베이스 접근을 캡슐화
- SQL 쿼리를 DAO에 집중
- 향후 데이터베이스 변경 시 DAO만 수정
예시:
@Singleton
public class UserDao {
private final DataSource dataSource;
public void insert(User user) throws SQLException {
String sql = "INSERT INTO USERS (USER_ID, PASSWORD, ...) VALUES (?, ?, ...)";
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, user.getUserId());
// ... 매개변수 설정
stmt.executeUpdate();
}
}
}이점:
- ✅ 비즈니스 로직과 데이터 접근 분리
- ✅ 데이터베이스 마이그레이션 용이
- ✅ 테스트할 때 Mock DAO 주입 가능
┌────────────────────────────────────────┐
│ Presentation (HTTP 요청/응답) │
│ - Filter, Dispatcher, Servlet │
└────────────────────────────────────────┘
▲
│ HttpRequest/Response
▼
┌────────────────────────────────────────┐
│ Control/Routing (요청 라우팅) │
│ - RouteKey, MethodHandler, Facade │
│ - ArgumentMapper │
└────────────────────────────────────────┘
▲
│ DTO (Data Transfer Object)
▼
┌────────────────────────────────────────┐
│ Application/Business Logic (UseCase) │
│ - RegisterUseCase, PostingUseCase │
│ - AuthInjector, AuthSession │
└────────────────────────────────────────┘
▲
│ Domain Model
▼
┌────────────────────────────────────────┐
│ Domain (비즈니스 엔티티) │
│ - User, Article, Cookie, LoginUser │
└────────────────────────────────────────┘
▲
│ CRUD 메서드
▼
┌────────────────────────────────────────┐
│ Data Access (DAO) │
│ - UserDao, ArticleDao, DataSource │
└────────────────────────────────────────┘
▲
│ SQL, Connection
▼
┌────────────────────────────────────────┐
│ Persistence (H2 Database) │
│ - USERS, ARTICLE 테이블 │
└────────────────────────────────────────┘
1. 클라이언트 GET 요청
↓
2. ConnectionManager: 소켓 관리
↓
3. RequestHandler: HTTP 요청 파싱
↓
4. FilterChain 통과
- AuthFilter: 인증 체크 (홈은 오픈 경로)
↓
5. Dispatcher: 동적/정적 분류
→ AppServlet으로 라우팅 (앱 요청이 아님)
↓
6. StaticServlet: index.html 제공
↓
7. HttpResponse 반환 (200 OK + HTML)
1. 클라이언트 POST 요청 (form-urlencoded)
↓
2. FilterChain 통과
- AuthFilter: 인증 불필요 (오픈 경로)
↓
3. Dispatcher → AppServlet
↓
4. AppRouteConfig: 라우트 맵에서 매칭
RouteKey("/register", POST) 찾기
↓
5. MethodHandler 실행
a) FormDataMapper.map(request)
→ UserRequest 객체로 매핑
b) AuthInjector.injectIfNeed(dto, request)
→ 인증 정보 필요 없음 (null)
c) registerHandler.apply(dto)
→ RegisterUseCase.execute(userRequest)
↓
6. UseCase 비즈니스 로직
a) 중복 아이디 검사 (UserDao.find)
b) 비밀번호 암호화 (EncryptUtil)
c) 사용자 저장 (UserDao.insert)
↓
7. HttpResponse (201 Created or 400 Bad Request)
1. 클라이언트 POST 요청
↓
2. FilterChain
- AuthFilter: 오픈 경로
- LoginFilter: 로그인 시도 감시
↓
3. AppServlet → /login 라우트
↓
4. LoginUseCase.execute(loginRequest)
a) UserDao.find(userId) - DB 조회
b) 비밀번호 검증 (EncryptUtil.verify)
↓
5. 로그인 성공 시
- AuthSession.addSession(sid) - 세션 저장
- Cookie("sid", sid) 응답
- 302 Redirect → /
↓
6. 로그인 실패 시
- 302 Redirect → /login?failure=true
1. 클라이언트 POST 요청 (multipart/form-data, 쿠키 포함)
↓
2. FilterChain
- AuthFilter: 인증 체크
a) request.cookieValue("sid") 추출
b) AuthSession.isValid(sid) 확인
c) 미인증 → 302 /login?forbidden=true
↓
3. AppServlet → /article 라우트
↓
4. MethodHandler 실행
a) MultipartMapper.map(request)
→ ArticleRequest (title, content, image 포함)
b) AuthInjector.injectIfNeed(dto, request)
→ LoginUser 객체 주입 (세션에서 추출)
→ dto의 optional<LoginUser> 필드 채우기
c) postingHandler.apply(dto)
→ PostingUseCase.execute(articleRequest, loginUser)
↓
5. UseCase
a) 제목/내용 검증
b) 이미지 저장 (multipart body에서 추출)
c) Article 객체 생성
d) ArticleDao.insert(article) - DB 저장
↓
6. HttpResponse (201 Created or 400 Bad Request)
1단계: Facade 생성 또는 기존 Facade 수정
@Singleton
public class CommentFacade extends Facade {
private final CommentUseCase commentUseCase;
public CommentFacade(AuthInjector authInjector, CommentUseCase usecase) {
super(authInjector);
this.commentUseCase = usecase;
}
@Override
public String basePath() {
return "/comment";
}
@Override
protected void createRouteMap() {
registerRoute(
"/create", // 최종 경로: /comment/create
HttpMethod.POST,
new FormDataMapper<>(CommentRequest.class),
request -> {
HttpResponse response = commentUseCase.execute(request);
return response;
}
);
}
}2단계: ApiModule 목록에 Facade 추가
// router/AppRouteConfig에 CommentFacade 등록
List<ApiModule> apiModules = Arrays.asList(
homeFacade,
articleFacade,
userFacade,
new CommentFacade(authInjector, commentUseCase) // 추가
);3단계: UseCase 구현
public class CommentUseCase {
private final CommentDao commentDao;
public HttpResponse execute(CommentRequest request) {
// 비즈니스 로직
Comment comment = new Comment(...);
commentDao.insert(comment);
return HttpResponseFactory.created();
}
}4단계: DAO 및 데이터베이스 테이블 추가
@Singleton
public class CommentDao {
private final DataSource dataSource;
public void insert(Comment comment) throws SQLException {
// SQL 쿼리 실행
}
}
// DataSource.initSchema()에 테이블 생성 쿼리 추가
CREATE TABLE COMMENTS (
ID BIGINT AUTO_INCREMENT PRIMARY KEY,
ARTICLE_ID BIGINT NOT NULL,
WRITER_ID VARCHAR(50) NOT NULL,
CONTENT TEXT,
CREATED_AT TIMESTAMP,
FOREIGN KEY (ARTICLE_ID) REFERENCES ARTICLE(ID),
FOREIGN KEY (WRITER_ID) REFERENCES USERS(USER_ID)
);5단계: 요청 DTO 추가
public record CommentRequest(
Long articleId,
Optional<LoginUser> loginUser, // AuthInjector가 자동 주입
String content
) { }위치: application/exception/
client/- 클라이언트 오류 (4xx)server/- 서버 오류 (5xx)
설계:
// 커스텀 예외
public class IllegalContentTypeException extends RuntimeException { }
public class WhoRUException extends RuntimeException { } // 인증 오류
// FilterChain 또는 AppServlet에서 통일 예외 처리
try {
// ... 요청 처리
} catch (IllegalContentTypeException e) {
return HttpResponseFactory.badRequest(e.getMessage());
} catch (Exception e) {
return HttpResponseFactory.internalServerError(e.getMessage());
}위치: application/auth/
설계:
AuthSession: 세션 저장소 (ConcurrentHashMap)AuthFilter: 경로별 인증 필요 여부 판단AuthInjector: 핸들러에 인증 정보 주입Cookie: HTTP 쿠키 표현
흐름:
1. 로그인 성공
→ AuthSession에 (sid) 저장
→ 쿠키에 sid 담아 반환
2. 다음 요청
→ 쿠키에서 sid 추출
→ AuthFilter: AuthSession.isValid(sid) 확인
→ 유효 → 요청 계속
→ 무효 → /login으로 리다이렉트
3. 핸들러 실행
→ AuthInjector가 LoginUser 정보 추출
→ DTO의 Optional<LoginUser> 필드 채우기
→ 핸들러에서 현재 사용자 정보 사용 가능
StaticServlet vs AppServlet
public class Dispatcher {
public HttpResponse dispatch(HttpRequest request) {
// 1차: 동적 요청 (앱 로직)
Optional<HttpResponse> response = appServlet.service(request);
if (response.isPresent()) return response.get();
// 2차: 정적 자원
response = staticServlet.service(request);
return response.orElseGet(HttpResponseFactory::notFound);
}
}이점:
- ✅ 정적 자원 캐싱 가능 (향후 개선)
- ✅ 정적/동적 파일 처리 로직 분리
- ✅ 성능 최적화 선택적 적용
- ✅ 마이크로서비스 분리 용이 (CDN 등으로 정적 전담)
| 원칙 | 적용 부분 | 효과 |
|---|---|---|
| S (단일 책임) | Filter, Facade, UseCase | 각 컴포넌트가 한 가지만 담당 |
| O (열림-닫힘) | Mapper, Filter, Facade | 확장에는 열려있고 수정에는 닫혀있음 |
| L (리스코프 치환) | Filter 인터페이스 | 모든 Filter가 동일하게 대체 가능 |
| I (인터페이스 분리) | ArgumentMapper, ApiModule | 필요한 메서드만 정의 |
| D (의존성 역전) | DIContainer | 고수준 모듈이 저수준에 의존하지 않음 |
ArgumentMapper: 요청 매핑 로직 중앙화HttpResponseFactory: 응답 생성 일관성FilterChain: 공통 처리 일관화- `Template**: HTML 렌더링 템플릿 재사용
HTTP 처리 ← RequestHandler, ConnectionManager
필터링 ← FilterChain, Filter들
라우팅 ← Dispatcher, AppRouteConfig
요청 변환 ← ArgumentMapper
비즈니스 로직 ← UseCase, Facade
데이터 접근 ← DAO
문제: DIContainer가 A 생성 중 B 필요 → B 생성 중 A 필요
해결:
ThreadLocal<Map<Class<?>, Boolean>> underConstruction = ...;
// 생성 중인 클래스 추적 → 순환 의존 감지문제: 하나의 소켓으로 여러 HTTP 요청 처리
해결:
// ConnectionManager에서 소켓 재사용 관리
// BufferedReader/Writer 닫기 시점 명확히 처리문제: Form 데이터와 파일을 함께 처리
해결:
// MultipartMapper에서 경계(boundary) 파싱
// 파일 바이너리와 폼 필드 분리이 프로젝트는 경량이지만 견고한 웹 서버의 설계 원칙을 보여줍니다.
핵심 교훈:
- 패턴이 코드를 단순하게 만든다
- 관심사 분리로 유지보수성 극대화
- 확장성 있는 설계는 비용 절감
- DI와 팩토리로 결합도 최소화
이러한 설계 원칙은 Spring, Django, Express.js 등 실무 프레임워크에서도 동일하게 적용되고 있습니다.