-
Notifications
You must be signed in to change notification settings - Fork 0
2주차 금요일 그룹 5
- 요청의 흐름을 처음에 클라이언트 소켓을 받고 스레드를 분리하여 처리하는
ConnectionHandler
가 있음 -
ConnectionHandler
가 각RequestMapping
에서 매핑정보를 불러옵니다. value값으로는 각각의RequestHandler
가 매핑되어 있기 때문에 각 요청을 처리할 수 있습니다. - 각 요청은 대표적은
static
한 파일을 처리하는 핸들러 혹은,경로 매핑
을 처리하는 핸들러로 나뉩니다. - 그래서 해당 핸들러에서 모든 요청을 처리하면 HttpResponse에게 응답을 하도록 요청하면 하나의 요청과 응답이 종료되는 흐름입니다.
- 저는 요청과 응답을 처리하는 부분을 요구사항에 맞춰서 컴팩트하게 작성했는데, 다른분들이 조금 더 고민했던 부분들을 듣게됐습니다. 예를들어 Json을 처리할 수 있는 부분이나, 리플렉션을 활용해 동적으로 처리한 부분들을 보면서 단순히 요구사항을 최적으로 구현하는것도 좋지만, 조금 더 확장적인 방향, 혹은 편의를 위한 기능들을 고민해보고 작성해보는것도 정말 좋은 고민이다라고 느꼈습니다!!
- 두 번째로는 문서화의 중요성을 많이 체감했습니다! 정제님이 발표하실때 단순히 코드 뿐만 아니라 요청의 흐름을 가시화한 흐름도를 같이 참고하니, 확실히 어떤 컨셉인지 어떤 생각으로 이런 흐름을 만들었는지 더 체감할 수 있었습니다! 문서화 저도 열심히 배워서 해보겠슴다~
오늘 코드 리뷰 시간에 제가 만든 HTTP 서버에 대해 설명드렸는데, 생각보다 말씀드릴 내용이 많았습니다. 우선 서버 구조는 메인에서 웹 서버 역할을 하는 클래스를 만들고 start() 메소드로 실행되게 했습니다. 라우터를 사용해서 경로와 핸들러를 매핑하는 방식으로 구현했고요.
소켓 처리는 handleConnection() 메소드에서 하고 있는데, 솔직히 Keep-Alive 구현하려다 좀 어려움을 겪었습니다. 아직도 제대로 동작하지 않네요. 이 부분은 좀 더 공부해봐야 할 것 같습니다.
요청 파싱하는 부분은 꽤 신경 썼습니다. 추상 클래스로 파싱 로직을 나눠놓고 HttpParser에서 모두 조합해서 HttpRequest 객체를 만들게 했거든요. 쿼리 스트링, 헤더, 바디 이런 식으로요. 이 부분은 꽤 마음에 듭니다.
요청 처리는 RequestDispatcher가 라우터에서 알맞은 핸들러를 찾아서 처리하게 했습니다. 핸들러는 인터페이스로 정의하고 추상 클래스로 기본 구현도 해놨는데, 허용되지 않은 HTTP 메소드는 기본적으로 에러 페이지를 반환하게 해놨습니다. 이 방식이 괜찮은 것 같습니다.
응답 만드는 것은 ResponseBuilder를 사용해서 했는데, 상태 코드별로 다른 응답을 만들 수 있게 했습니다. 200, 404, 405, 500 이런 식으로요.
그런데 좀 아쉬운 점이 있습니다. 정적 파일 처리할 때 파일 시스템에서 직접 읽어오게 했는데, JAR로 빌드하니까 문제가 생기더라고요. 급하게 하드코딩으로 해결했는데, 이건 좀 개선이 필요할 것 같습니다. 나중에 꼭 고쳐야겠습니다.
데이터 저장은 간단하게 인메모리 데이터베이스를 만들어서 사용했습니다. 싱글톤으로 관리하게 했고요.
전체적으로 보면 기본적인 HTTP 서버 동작은 구현했지만, 아직 개선할 점이 많습니다. Keep-Alive와 JAR에서 파일 읽는 문제 해결하는 게 가장 시급한 것 같고, 에러 페이지 하드코딩한 것도 수정해야 합니다.
그리고 오늘 리뷰하면서 느낀 건데, 개발 툴이나 단축키 같은 것도 좀 더 익혀야겠습니다. 코드 설명할 때 좀 더 효율적으로 할 수 있을 것 같더라고요.
다른 분들의 코드를 보면서 정말 많이 배웠습니다. 특히 리플렉션을 사용한 JSON 파싱이나 동적 핸들러 할당 부분이 인상적이었습니다. 리플렉션을 사용한 JSON 파싱은 제가 구현한 방식보다 훨씬 유연하고 확장성 있어 보였습니다. 객체의 구조를 동적으로 분석해서 JSON으로 변환하는 과정이 매우 효율적으로 보였습니다. 이 방식을 사용하면 새로운 클래스가 추가되어도 파싱 로직을 수정할 필요가 없어질 것 같아요. 제 코드에도 이런 방식을 적용해보고 싶다는 생각이 들었습니다.
동적 핸들러 할당 부분도 굉장히 인상적이었습니다. 제 코드에서는 핸들러를 정적으로 등록했는데, 동적으로 할당하는 방식을 보니 훨씬 유연한 구조가 가능할 것 같아요. 특히 애노테이션을 사용해서 핸들러를 자동으로 등록하는 방식은 정말 깔끔해 보였습니다. 이렇게 하면 새로운 엔드포인트를 추가할 때 훨씬 편리할 것 같아요.
이런 고급 기술들을 보면서, 제가 아직 배워야 할 게 정말 많다는 걸 느꼈습니다. Java의 리플렉션이나 애노테이션 처리에 대해 더 깊이 공부해야겠다는 생각이 들었어요. 또한, 디자인 패턴이나 객체지향 설계 원칙에 대해서도 더 공부해야 할 것 같습니다.
다른 분들의 코드를 보면서 제 코드가 좀 단순하고 기본적인 수준이라는 걸 깨달았습니다. 하지만 이것이 오히려 동기부여가 되는 것 같아요. 앞으로 이런 고급 기술들을 하나씩 익혀서 제 서버를 더욱 견고하고 유연하게 만들어 가고 싶습니다.
특히 리플렉션을 활용한 부분은 꼭 적용해보고 싶어요. 당장은 어려울 수 있겠지만, 차근차근 공부해서 제 코드에도 적용해볼 생각입니다. 이번 코드 리뷰를 통해 새로운 도전 과제가 생긴 것 같아 오히려 기쁩니다.
그래도 코드 모듈화하고 재사용성을 높이려고 노력한 게 보이니까 뿌듯하긴 합니다. 앞으로 더 공부해서 더 좋은 서버를 만들어봐야겠습니다. 특히 파일 처리와 Keep-Alive 부분은 꼭 제대로 이해하고 구현해보고 싶습니다.
Router
- Http 요청을 처리하는건, 어떤 리소스에 어떤 메서드로 요청을 했냐에 따라 요청 처리가 달라진다고 생각
- 따라서, Http Method와 리소스를 나타내는
EndPoint
객체를 추가
public record EndPoint(HttpMethod method, String path) {}
-
EndPoint
를 키로 하는 Router Map 구현 - 요청을 처리하기 위하기 위한 함수
RouterFunction
지정
@FunctionalInterface
public interface RouterFunction {
Object route(HttpRequest request, HttpResponse response);
}
- 각 요청에 맞는
RouterFunction
을 매핑
Json Body Response
- 라이브러리 사용을 지양해서 Json을 직접 해결해야했음
- Json 형식으로 응답을 보내기 위해
JsonConverter
인터페이스 생성
public interface JsonConverter {
byte[] convertToJsonBytes(Object obj) throws Exception;
}
-
record
class와Map<K,V>
인터페이스의 구현체를 Json형식으로 변환하는 구현체 생성 src/main/java/codesquad/util/jsonconverter/MapJsonConverter.java
src/main/java/codesquad/util/jsonconverter/RecordJsonConverter.java
Endpoint
- path가 정확히 일치하지 않고 pathvariable에 따라 다른 요청 path도 처리할 수 있어야함
- match 메서드 구현
- 아래와 같이 pathvariable이 match함
src/test/java/codesquad/util/EndPointTest.java
@Test
public void testPathVariableMatch() {
EndPoint endPoint = EndPoint.of(HttpMethod.GET, "/users/{id}");
assertTrue(endPoint.matches("GET", "/users/123"));
}
- 이 메서드를
equals
메서드에도 적용해서 Map의 키로 매칭 가능하게 해야할지 고민
지찬우
- 자료구조는
Map<String, List<String>>
사용 - 쿼리 파라미터는 하나의 이름에 여러 값이 존재할 수 있기 때문! -
QueryParameters
객체로 분리 -
QueryParametersParser
구현
HttpRequestProcessor
가 HttpRequest
로 파싱한 후에, 해당 요청을 처리할 수 있는 핸들러를 찾아 핸들러에게 위임.
핸들러가 처리를 완료하면 HttpResponse
반환.
-
Map
자료구조를 활용 - 현재는 GET 메서드만 지원하므로 Request URI만을 key로 갖는 Handler map
Resource
객체가 ResourceType
enum(FILE
과 DIRECTORY
)를 포함하도록 변경하고, Resource
가 DIRECTORY
타입이라면 해당 디렉토리에 index.html
을 찾아 반환하는 로직 추가
이미 HTTP 스펙에 대해 어느정도 알고 있어 이후에 발생할 변경 사항이 예상이 가지만, 해당 부분까지 고려하며 구현하기에는 많은 노력이 필요 → 주어지는 요구사항을 만족하는 정도로만 구현하기로..!
jar 빌드 시 의존성이 포함되지 않아 발생하는 문제. 아래의 스크립트를 build.gradle에 추가해 해결
jar {
from {
configurations.runtimeClasspath.collect {
it.isDirectory() ? it : zipTree(it)
}
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
jar로 빌드하면 리소스 파일의 경로가 달라짐. 리소스 파일은 JAR 파일 내에 포함되어 파일 시스템 경로로 접근 불가능하고, 클래스패스를 통해 접근
try (InputStream inputStream = ResourcesReader.class.getResourceAsStream(path)) {
...
} catch (IOException e) {
...
}
- Main → Processor → handle → api or static → response write → socket close 흐름으로 진행
- 어노테이션 사용하고 리플렉션으로 메서드를 동적으로 가져옴
- 구조에 신경썼음
- 디테일적인 점이 좋았어요. URI 디코딩 하는거나 생각치도 못했는데 해야겠다는 생각을 했어요.
- 구조가 각자 달랐는데 각 구조만의 장점이나 깔끔한게 좋았어요.
- 공식문서를 바탕으로 고민하신 부분들을 보면서 저도 그렇게 해야겠다는 생각을 했어요.
- 공식 문서를 바탕으로 스펙을 더 열심히 봐야겠어요,,