질문, 피드백 등 모든 댓글 환영합니다.
건국대학교 교내 IT 동아리 KUIT에서 진행한 3주차 미션에 관한 내용입니다.
서블릿, 스프링 등 서버 개발 프레임워크/라이브러리 등을 사용하지 않고 순수 자바코드로만 간단한 웹 서버를 개발합니다.
해당 과정을 통해 웹 서버 애플리케이션 핵심 동작 원리를 파악하는 것을 목표로 합니다.
전체 코드 : github
메인 클래스로, Executors, ExecutorService 으로 쓰레드풀을 생성하여 여러 요청을 병렬적으로 처리함.
ServerSocket(환영소켓)과 WelcomeSocker(연결소켓)을 사용하여 요청을 받음.
int port = DEFAULT_PORT;
ExecutorService service = Executors.newFixedThreadPool(DEFAULT_THREAD_NUM);
// TCP 환영 소켓
try (ServerSocket welcomeSocket = new ServerSocket(port)){
// 연결 소켓
Socket connection;
while ((connection = welcomeSocket.accept()) != null) {
// 스레드에 작업 전달
service.submit(new RequestHandler(connection));
}
}
InputStream, OutputStream 을 사용하여 HttpRequest, HttpResponse 객체 생성.
FrontController로 HttpRequest, HttpResponse 전달.
Buffer I/O 에 대한 작업 자체만으로 큰 책임과 역할을 가진다. 때문에 해당 작업은 HttpRequest, HttpResponse 객체에 위임하고 RequestHandler 에서는 해당 객체를 생성하여 FrontController로 넘겨주었다.
Socket connection;
private static final Logger log = Logger.getLogger(RequestHandler.class.getName());
public RequestHandler(Socket connection) {
this.connection = connection;
}
@Override
public void run() {
log.log(Level.INFO, "[RequestHandler] New Client Connect! Connected IP : " + connection.getInetAddress() + ", Port : " + connection.getPort());
try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()){
BufferedReader br = new BufferedReader(new InputStreamReader(in));
DataOutputStream dos = new DataOutputStream(out);
HttpRequest httpRequest = HttpRequest.from(br);
HttpResponse httpResponse = new HttpResponse(dos);
FrontController frontController = FrontController.getInstance();
frontController.service(httpRequest, httpResponse);
} catch (IOException e) {
log.log(Level.SEVERE,e.getMessage());
}
}
RequestMapper를 통해 HTTP 요청을 처리할 수 있는 컨트롤러 조회 및 실행.
요청을 처리할 컨트롤러를 매핑하여 관리하고 찾아내는 작업만으로 충분한 책임을 가진다. 때문에 해당 작업은 RequestMapper 에 위임하였고 덕분에 FrontController는 단순히 요청에 맞는 컨트롤러를 실행하는 역할만 수행할 수 있다.
public void service(HttpRequest request, HttpResponse response) throws IOException {
getController(request).process(request, response);
}
private Controller getController(HttpRequest request) {
return RequestMapper.getController(request.getRequestUri());
}
등록된 컨트롤러를 관리하고 요청에 따른 컨트롤러를 반환.
여러 개의 if문을 통해 찾아내는 작업은 가독성 측면에서나 유지보수 관점에서 매우 좋지 못한 코드다. 컨트롤러들은 모두 Controller를 implement 하므로, Map 자료구조로 관리하도록 하였고 단순 정적 리소스에 대한 요청일 경우 ForwardController로 대응하였다.
private static final Map<String, Controller> handlerMappingMap = new HashMap<>();
static {
handlerMappingMap.put(SIGNUP.getValue(), new SignUpController());
handlerMappingMap.put(LOGIN.getValue(), new LoginController());
handlerMappingMap.put(LIST.getValue(), new UserListController());
}
public static Controller getController(String path) {
Controller controller = handlerMappingMap.get(path);
return controller != null ? controller :
new ForwardController();
}
모든 컨트롤러는 인터페이스로 정의된 Controller
를 상속받는다.
public interface Controller {
void process(HttpRequest request, HttpResponse response) throws IOException;
}
정적 리소스에 관한 요청은 ForwardController
가 책임진다.
"/" 경로로 요청이 발생하면 index.html
을 응답한다.
public class ForwardController implements Controller {
@Override
public void process(HttpRequest request, HttpResponse response) throws IOException {
if (request.getRequestUri().equals("/")) {
response.forward(Http.INDEX.getValue());
return;
}
response.forward(request.getRequestUri());
}
}
POST 방식의 로그인 요청을 처리한다.
파라미터로 넘어온 loginid, passward 값을 비교하여 로그인 성공 시 쿠키값을 설정하고 첫 화면으로 redirect 시키고, 실패 시 로그인 실패 화면으로 redirect 시킨다.
주의
정밀한 로그인 로직을 구현하는 것이 목표가 아니므로 쿠키값을 임의의 값을 설정하였습니다. 로그인 로직은 보안에 매우 민감하므로 세션, 토큰 등의 방식을 고민해야 합니다.public class LoginController implements Controller {
private final MemoryUserRepository repository;
public LoginController() {
repository = MemoryUserRepository.getInstance();
}
@Override
public void process(HttpRequest request, HttpResponse response) throws IOException {
String paramUserId = request.getParamValue("userId");
String paramPassword = request.getParamValue("password");
User findById = repository.findUserById(paramUserId);
if (!passwordCheck(paramPassword, findById)) {
response.redirect(USER_LOGIN_FAILED.getValue());
return;
}
response.addHeader(SET_COOKIE.getValue(), LOGINED_TRUE.getValue());
response.redirect(INDEX.getValue());
}
private boolean passwordCheck(String paramPassword, User findById) {
return findById != null && findById.getPassword().equals(paramPassword);
}
}
유저 목록을 조회하는 화면은 쿠키값을 비교하여 로그인 회원만 접근이 가능하도록한다. 비로그인 사용자는 로그인 화면으로 redirect 시킨다.
@Override
public void process(HttpRequest request, HttpResponse response) throws IOException {
String cookieValue = request.getHeader(COOKIE.getValue());
if (cookieValue == null || !cookieValue.equals(LOGINED_TRUE.getValue())) {
response.redirect(LOGIN_FORM.getValue());
return;
}
response.forward(request.getRequestUri());
}