Skip to content

3주차 미션 / 서버 2조 박성근 #1

Open
ParkSeongGeun wants to merge 41 commits intoKonkuk-KUIT:mainfrom
ParkSeongGeun:week3
Open

3주차 미션 / 서버 2조 박성근 #1
ParkSeongGeun wants to merge 41 commits intoKonkuk-KUIT:mainfrom
ParkSeongGeun:week3

Conversation

@ParkSeongGeun
Copy link

@ParkSeongGeun ParkSeongGeun commented Sep 29, 2025

전체 흐름

  • Java로 HTTP 웹 서버를 구현하고, 리팩토링을 통해 Spring MVC와 유사한 프레임워크 구조로 발전하는 과정입니다.

과제1

  • 요구사항 1-3: 회원가입 (GET → POST)
    • HTTP 요청 파싱 및 정적 파일 서빙
    • 쿼리 스트링 파싱으로 회원가입 구현
    • GET → POST 전환 (보안 개선)
  • 요구사항 5-6: 로그인 및 인가
    • 쿠키 기반 로그인 구현
    • 로그인 체크를 통한 접근 제어
  • 요구사항 7: Content-Type
    • MIME 타입에 따른 동적 Content-Type 설정

과제 2

  • Enum 도입
  • HttpRequest 추상화
  • HttpResponse 추상화
  • Controller 패턴
  • RequestMapper (라우팅)

최종 아키텍처

  RequestHandler → RequestMapper → Controller
                                      ↓
                              HttpRequest/HttpResponse

Summary by CodeRabbit

  • 신기능
    • HTTP 요청/응답 파싱, 헤더·쿠키·본문 접근 API 추가
    • 경로·메서드 기반 라우팅과 컨트롤러 실행 도입
    • 정적 파일 포워드, 리다이렉트, 404 응답 처리
    • 회원가입(POST), 로그인(POST), 사용자 목록(쿠키 기반) 기능 추가
  • 리팩터
    • 요청 처리 흐름을 공통 매퍼로 위임해 구조 단순화
  • 테스트
    • 요청/응답, 라우팅, 컨트롤러 전반의 단위/통합 테스트 대거 추가
  • 작업
    • 테스트 의존성 및 IDE 검사 프로파일 추가

- 요구사항 1: index.html 반환하기
@jyun-KIM
Copy link
Member

jyun-KIM commented Oct 3, 2025

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Oct 3, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Oct 3, 2025

Walkthrough

HTTP 요청/응답 파싱·생성, 라우팅 구성(RequestMapper/WebConfig) 및 컨트롤러(Forward/UserSignup/UserLogin/UserList)들이 추가되고 RequestHandler가 이를 위임하도록 변경되었습니다. 관련 단위 테스트, 테스트 리소스, Gradle 테스트 의존성, HTML 폼 메서드 변경 및 IntelliJ 검사 프로필이 포함됩니다.

Changes

Cohort / File(s) Summary of changes
Build & IDE
​.idea/inspectionProfiles/Project_Default.xml, build.gradle
IntelliJ 검사 프로필 추가(자원 닫기 METHOD_MATCHER_CONFIG). 테스트 의존성 org.mockito:mockito-junit-jupiter:5.12.0 추가.
HTTP Core (classes & enums)
src/main/java/http/HttpHeaders.java, src/main/java/http/HttpRequest.java, src/main/java/http/HttpResponse.java, src/main/java/http/HttpStartLine.java, src/main/java/http/enums/*
HTTP 요청 시작선/헤더/본문 파싱 도입; HttpResponse에 forward/redirect/redirectWithCookie/notFound 구현; 여러 HTTP 관련 enum(Headers/Method/Status/ContentType/RequestPath) 추가.
Controllers
src/main/java/controller/Controller.java, src/main/java/controller/ForwardController.java, src/main/java/controller/UserSignupController.java, src/main/java/controller/UserLoginController.java, src/main/java/controller/UserListController.java
Controller 인터페이스 및 구현 추가: 정적 리소스 포워딩, 회원가입 처리(POST), 로그인 처리(쿠키 설정 포함), 로그인 기반 사용자 목록 접근 제어.
Webserver Routing
src/main/java/webserver/RequestHandler.java, src/main/java/webserver/RequestMapper.java, src/main/java/webserver/WebConfig.java
RequestHandler가 BufferedReader→HttpRequest, OutputStream→HttpResponse를 생성하고 RequestMapper로 위임; WebConfig에서 경로(+메서드)→Controller 매핑 구성; 매칭 없을 땐 ForwardController 사용.
Model
src/main/java/model/UserField.java
사용자 입력 필드( userId/password/name/email )를 enum으로 추가.
Tests - Core
src/test/java/HttpRequestTest.java, src/test/java/HttpResponseTest.java
HttpRequest/HttpResponse 동작 검증용 단위 테스트 추가(요청 파싱, 헤더/쿠키/바디, forward/redirect/notFound).
Tests - Controllers & Routing
src/test/java/controller/*, src/test/java/webserver/RequestMapperTest.java
각 컨트롤러 및 RequestMapper에 대한 Mockito 기반 단위 테스트 추가(경로/메서드/쿠키 시나리오).
Test Resources
src/test/resources/request/get_request.txt, src/test/resources/request/post_request.txt, src/test/resources/response/test.html
원시 HTTP 요청 샘플(GET/POST) 및 테스트용 HTML 리소스 추가.
Webapp
webapp/user/form.html
회원가입 폼 제출 메서드 변경: GETPOST.
IDE VCS
.idea/vcs.xml
프로젝트 VCS 매핑(Git) 파일 추가.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as 클라이언트
  participant RH as RequestHandler
  participant RM as RequestMapper
  participant WC as WebConfig
  participant CT as Controller
  participant R as HttpResponse
  Note over RH,RM: 파싱 → 라우팅 → 컨트롤러 실행 → 응답 전송
  C->>RH: TCP 연결 및 HTTP 요청
  RH->>RH: BufferedReader로 요청 읽음
  RH->>RH: HttpRequest.from(...)
  RH->>R: new HttpResponse(OutputStream, version)
  RH->>RM: RequestMapper.getController(path, method)
  RM->>WC: (매핑 조회)
  alt 매칭된 컨트롤러 있음
    RM->>CT: CT.execute(request, response)
  else 매칭 없음
    RM->>CT: ForwardController.execute(request, response)
  end
  CT-->>R: forward / redirect / notFound
  R-->>C: 상태라인, 헤더, 바디 전송
Loading
sequenceDiagram
  autonumber
  participant C as 클라이언트
  participant ULC as UserLoginController
  participant Repo as MemoryUserRepository
  participant R as HttpResponse
  Note over ULC: 로그인 처리(인증 및 쿠키 설정)
  C->>ULC: POST /user/login (userId, password)
  ULC->>Repo: findById(userId)
  alt 사용자 존재 및 비밀번호 일치
    ULC->>R: redirectWithCookie("/index.html", "logined=true")
  else 인증 실패
    ULC->>R: redirect("/user/login_failed.html")
  end
Loading
sequenceDiagram
  autonumber
  participant C as 클라이언트
  participant ULCt as UserListController
  participant R as HttpResponse
  Note over ULCt: 쿠키 기반 접근 제어
  C->>ULCt: GET /user/userList (Cookie)
  alt Cookie에 logined=true 포함
    ULCt->>R: forward("/user/list.html")
  else 비로그인
    ULCt->>R: redirect("/user/login.html")
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

새 길을 파는 토끼가 말하네,
요청이 와요 찰칵 파싱해요,
라우터에 쓱 보내면 컨트롤러가 척척,
쿠키 하나로 로그인 찰칵, 파일은 포워드 휙휙.
깡충토끼 축하해요! 🐇🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive 제목이 PR에서 수행된 HTTP 서버 구현, 컨트롤러 패턴 도입, HttpRequest/HttpResponse 추상화 등의 주요 변경 사항을 전혀 설명하지 않고 과제명과 작성자 정보만 포함하여 구체적인 의미를 전달하지 않습니다. 주요 변경 내용을 한 문장으로 요약하는 설명적인 제목으로 수정해 주세요. 예를 들어 “Java HTTP 서버 및 Spring MVC 유사 프레임워크 구현”과 같이 변경 사항을 명확히 나타내는 제목이 필요합니다.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jyun-KIM
Copy link
Member

jyun-KIM commented Oct 3, 2025

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Oct 3, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (16)
src/test/java/controller/UserSignupControllerTest.java (1)

60-80: 두 번째 회원가입 테스트가 첫 번째 테스트와 중복됩니다.

동일한 로직을 다른 데이터로 반복하는 테스트입니다. 테스트 커버리지는 높이지만, @ParameterizedTest를 사용하면 중복을 제거하고 더 많은 시나리오를 쉽게 추가할 수 있습니다.

@ParameterizedTest로 리팩토링 고려:

@ParameterizedTest
@CsvSource({
    "testuser,1234,홍길동,test@example.com",
    "getuser,5678,김철수,get@example.com"
})
@DisplayName("POST 요청으로 유효한 사용자 정보를 전송하면 회원가입이 성공해야 한다")
public void signupSuccessWithPostRequest(String userId, String password, String name, String email) throws IOException {
    // given
    when(mockRequest.getMethod()).thenReturn(HttpMethod.POST);
    when(mockRequest.getBody()).thenReturn(
        String.format("userId=%s&password=%s&name=%s&email=%s", userId, password, name, email)
    );
    when(mockRequest.getQueryString()).thenReturn(null);

    // when
    controller.execute(mockRequest, mockResponse);

    // then
    verify(mockResponse).redirect(RequestPath.INDEX.getValue());
    
    User savedUser = repository.findUserById(userId);
    assertNotNull(savedUser, "사용자가 저장되어야 한다");
    assertEquals(userId, savedUser.getUserId());
    assertEquals(password, savedUser.getPassword());
    assertEquals(name, savedUser.getName());
    assertEquals(email, savedUser.getEmail());
}
src/main/java/controller/UserListController.java (1)

14-28: 쿠키 파싱 로직을 유틸리티로 분리하는 것을 권장합니다.

쿠키 파싱 로직이 여러 컨트롤러에서 반복될 가능성이 높습니다. HttpRequestUtils와 같은 유틸리티 클래스로 추출하여 재사용성을 높이세요.

예시:

// HttpRequestUtils에 추가
public static Map<String, String> parseCookies(String cookieHeader) {
    Map<String, String> cookies = new HashMap<>();
    if (cookieHeader == null) return cookies;
    for (String pair : cookieHeader.split(";\\s*")) {
        String[] kv = pair.split("=", 2);
        if (kv.length == 2) {
            cookies.put(kv[0], kv[1]);
        }
    }
    return cookies;
}

그런 다음 컨트롤러에서:

Map<String, String> cookies = HttpRequestUtils.parseCookies(request.getCookie());
if ("true".equals(cookies.get("logined"))) {
    // ...
}
src/test/java/controller/ForwardControllerTest.java (2)

26-29: MockitoExtension 사용을 권장합니다.

MockitoAnnotations.openMocks(this)를 수동으로 호출하는 대신 @ExtendWith(MockitoExtension.class)를 사용하면 Mockito 생명주기 관리가 자동화되고 코드가 간결해집니다.

다음과 같이 변경하세요:

+import org.mockito.junit.jupiter.MockitoExtension;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@ExtendWith(MockitoExtension.class)
 public class ForwardControllerTest {
 
     @Mock
     private HttpRequest mockRequest;
 
     @Mock
     private HttpResponse mockResponse;
 
     private ForwardController controller;
 
     @BeforeEach
     public void setUp() {
-        MockitoAnnotations.openMocks(this);
         controller = new ForwardController();
     }

73-85: 존재하지 않는 파일 테스트 로직을 명확히 하세요.

Line 77에서 forward가 IOException을 던지도록 설정했지만, 실제 ForwardController 구현에서는 예외를 catch하고 notFound를 호출합니다. 테스트는 이 동작을 검증하지만, forwardnotFound 호출 순서가 명확하지 않을 수 있습니다.

호출 순서를 명시적으로 검증하려면:

         // then
         verify(mockResponse).forward("/nonexistent.html");
         verify(mockResponse).notFound();
+        
+        // 또는 InOrder로 순서 검증
+        InOrder inOrder = inOrder(mockResponse);
+        inOrder.verify(mockResponse).forward("/nonexistent.html");
+        inOrder.verify(mockResponse).notFound();
src/main/java/controller/UserSignupController.java (1)

37-39: 중복 사용자 체크를 추가하세요.

동일한 userId로 여러 번 회원가입을 시도할 경우, 기존 사용자를 덮어쓰거나 중복 데이터가 저장될 수 있습니다.

다음과 같이 중복 체크를 추가하세요:

         MemoryUserRepository repository = MemoryUserRepository.getInstance();
+        if (repository.findUserById(userId) != null) {
+            log.log(Level.WARNING, "User already exists: " + userId);
+            response.redirect("/user/signup?error=duplicate");
+            return;
+        }
         repository.addUser(newUser);
         log.log(Level.INFO, "New User Registered: " + newUser.getUserId());
src/main/java/controller/ForwardController.java (1)

24-28: 디렉토리 트래버설 방어를 강화하는 것을 권장합니다.

현재 contains("..")는 기본적인 공격을 차단하지만, URL 인코딩된 경로(%2e%2e)나 정규화되지 않은 경로(/./, //)를 우회할 수 있습니다.

다음과 같이 경로를 정규화한 후 검증하세요:

+        // 2. 보안 검증 - 경로 정규화 및 디렉토리 traversal 공격 방지
-        if (path.contains("..")) {
+        try {
+            String normalizedPath = Paths.get(path).normalize().toString();
+            if (normalizedPath.contains("..") || !normalizedPath.startsWith("/")) {
+                log.log(Level.WARNING, "Directory traversal attack detected: " + path);
+                response.notFound();
+                return;
+            }
+        } catch (Exception e) {
             log.log(Level.WARNING, "Directory traversal attack detected: " + path);
             response.notFound();
             return;
         }
src/main/java/http/enums/HttpStatus.java (1)

3-6: 추가 HTTP 상태 코드 지원을 고려하세요.

현재 3개의 상태 코드만 정의되어 있지만, 향후 확장을 위해 다음 상태 코드를 추가하는 것을 고려하세요:

  • 400 Bad Request (잘못된 요청 검증용)
  • 401 Unauthorized (인증 실패용)
  • 500 Internal Server Error (서버 오류용)
     OK(200, "HTTP/1.1 200 OK"),
+    CREATED(201, "HTTP/1.1 201 Created"),
+    BAD_REQUEST(400, "HTTP/1.1 400 Bad Request"),
+    UNAUTHORIZED(401, "HTTP/1.1 401 Unauthorized"),
     FOUND(302, "HTTP/1.1 302 Found"),
     NOT_FOUND(404, "HTTP/1.1 404 Not Found");
+    INTERNAL_SERVER_ERROR(500, "HTTP/1.1 500 Internal Server Error");
src/main/java/http/enums/HttpMethod.java (2)

23-24: TODO: 커스텀 예외 구현을 고려하세요.

TODO 주석이 있는 대로, 프레임워크 일관성을 위해 IllegalArgumentException 대신 커스텀 예외(예: UnsupportedHttpMethodException)를 사용하는 것을 고려하세요. 이렇게 하면 HTTP 프로토콜 관련 예외를 더 명확하게 처리할 수 있습니다.

다음과 같은 커스텀 예외를 생성할 수 있습니다:

public class UnsupportedHttpMethodException extends RuntimeException {
    public UnsupportedHttpMethodException(String method) {
        super("Unsupported HTTP method: " + method);
    }
}

그런 다음 from 메서드에서:

-        // todo: Custom Exception
-        throw new IllegalArgumentException("Unknown HTTP method: " + method);
+        throw new UnsupportedHttpMethodException(method);

이 작업을 수행하기 위해 새 이슈를 생성하시겠습니까?


17-25: from 메서드 최적화를 선택적으로 고려하세요.

현재 선형 검색은 2개의 enum 값에 대해서는 충분히 빠르지만, 향후 HTTP 메서드가 추가될 경우 정적 Map을 사용하여 O(1) 조회를 수행하는 것이 좋습니다.

+    private static final Map<String, HttpMethod> METHOD_MAP = 
+        Arrays.stream(values()).collect(Collectors.toMap(HttpMethod::getValue, m -> m));
+
     public static HttpMethod from(String method) {
-        for (HttpMethod httpMethod : HttpMethod.values()) {
-            if (httpMethod.getValue().equals(method)) {
-                return httpMethod;
-            }
-        }
+        HttpMethod httpMethod = METHOD_MAP.get(method);
+        if (httpMethod == null) {
+            throw new IllegalArgumentException("Unknown HTTP method: " + method);
+        }
-        throw new IllegalArgumentException("Unknown HTTP method: " + method);
+        return httpMethod;
     }
src/main/java/webserver/WebConfig.java (2)

20-20: null 메서드의 의미를 명확히 하세요.

Line 20과 Line 29에서 methodnull로 전달하고 있지만, 이것이 "모든 HTTP 메서드를 허용"하는 것인지 "GET만 허용"하는 것인지 불분명합니다. 주석이나 명시적인 상수로 의도를 명확히 하세요.

다음 중 하나를 선택하세요:

옵션 1: 명시적으로 GET 지정

-        controllers.put(createKey(RequestPath.ROOT.getValue(), null), new ForwardController());
+        controllers.put(createKey(RequestPath.ROOT.getValue(), HttpMethod.GET), new ForwardController());

옵션 2: 주석으로 의도 명확화

-        // 루트 경로
+        // 루트 경로 (모든 HTTP 메서드 허용)
         controllers.put(createKey(RequestPath.ROOT.getValue(), null), new ForwardController());

옵션 3: 상수 도입

private static final HttpMethod ANY_METHOD = null; // 모든 HTTP 메서드 허용
controllers.put(createKey(RequestPath.ROOT.getValue(), ANY_METHOD), new ForwardController());

Also applies to: 29-29


16-32: 중복 경로 등록 검증을 추가하는 것을 고려하세요.

현재 구현에서는 실수로 동일한 경로+메서드 조합을 두 번 등록하면 나중 것이 이전 것을 조용히 덮어씁니다. 개발 중 버그를 조기에 발견하기 위해 중복 검증을 추가하는 것을 고려하세요.

     public static Map<String, Controller> configureControllers() {
         Map<String, Controller> controllers = new HashMap<>();
 
         // 루트 경로
-        controllers.put(createKey(RequestPath.ROOT.getValue(), null), new ForwardController());
+        putController(controllers, RequestPath.ROOT.getValue(), null, new ForwardController());
 
         // 회원가입
-        controllers.put(createKey(RequestPath.USER_SIGNUP.getValue(), HttpMethod.POST), new UserSignupController());
+        putController(controllers, RequestPath.USER_SIGNUP.getValue(), HttpMethod.POST, new UserSignupController());
         
         // ... 나머지도 동일하게
 
         return controllers;
     }
+    
+    private static void putController(Map<String, Controller> controllers, 
+                                      String path, HttpMethod method, Controller controller) {
+        String key = createKey(path, method);
+        if (controllers.containsKey(key)) {
+            throw new IllegalStateException("Duplicate route registration: " + key);
+        }
+        controllers.put(key, controller);
+    }
src/test/java/controller/UserLoginControllerTest.java (1)

1-19: MockitoExtension 사용을 권장합니다.

현재 MockitoAnnotations.openMocks(this)를 수동으로 호출하고 있습니다. JUnit Jupiter와 Mockito 5.x에서는 @ExtendWith(MockitoExtension.class)를 사용하는 것이 권장됩니다.

다음 diff를 적용하여 개선할 수 있습니다:

 package controller;
 
 import db.MemoryUserRepository;
 import http.HttpRequest;
 import http.HttpResponse;
 import http.enums.HttpMethod;
 import http.enums.RequestPath;
 import model.User;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.jupiter.MockitoExtension;
 
 import java.io.IOException;
 
 import static org.mockito.Mockito.*;
 
+@ExtendWith(MockitoExtension.class)
 public class UserLoginControllerTest {

그리고 setUp 메서드에서 다음 라인을 제거하세요:

     @BeforeEach
     public void setUp() {
-        MockitoAnnotations.openMocks(this);
         controller = new UserLoginController();
src/main/java/http/HttpStartLine.java (1)

45-57: 쿼리 스트링 파싱이 단순하지만 안전합니다.

현재 구현은 첫 번째 ?를 기준으로 파싱하며, 대부분의 경우 문제없이 동작합니다.

만약 더 방어적인 구현을 원한다면, 다음과 같이 개선할 수 있습니다:

     public String getPathWithoutQuery() {
-        if (path.contains("?")) {
-            return path.substring(0, path.indexOf("?"));
-        }
-        return path;
+        int queryIndex = path.indexOf("?");
+        return queryIndex != -1 ? path.substring(0, queryIndex) : path;
     }
 
     public String getQueryString() {
-        if (path.contains("?")) {
-            return path.substring(path.indexOf("?") + 1);
-        }
-        return null;
+        int queryIndex = path.indexOf("?");
+        return queryIndex != -1 ? path.substring(queryIndex + 1) : null;
     }
src/main/java/http/enums/HttpHeader.java (1)

22-29: 대소문자 구분 없는 헤더 매칭을 고려하세요.

HTTP 헤더는 대소문자를 구분하지 않습니다(RFC 7230). 현재 구현은 정확히 일치해야만 매칭되므로, Content-lengthContent-Length를 다르게 처리합니다.

다음과 같이 개선할 수 있습니다:

     public static HttpHeader from(String headerName) {
         for (HttpHeader header : values()) {
-            if (header.headerName.equals(headerName)) {
+            if (header.headerName.equalsIgnoreCase(headerName)) {
                 return header;
             }
         }
         return null; // 알려지지 않은 헤더는 null return
     }
src/test/java/HttpRequestTest.java (1)

18-20: 리소스 관리 책임을 명확히 하세요.

bufferedReaderFromFile()이 반환하는 BufferedReader를 호출자가 닫아야 합니다. 현재 테스트에서는 HttpRequest.from()BufferedReader를 소비하지만 닫지 않아 리소스 누수가 발생할 수 있습니다.

테스트 메서드에서 try-with-resources를 사용하도록 권장합니다:

     @Test
     @DisplayName("POST 요청을 파싱할 때 메서드, URL, 헤더, 바디가 올바르게 파싱되어야 한다")
     public void parsePostRequestWithBodyAndHeaders() throws IOException {
         // given
-        BufferedReader br = bufferedReaderFromFile(TEST_DIRECTORY + "request/post_request.txt");
-        
-        // when
-        HttpRequest httpRequest = HttpRequest.from(br);
+        try (BufferedReader br = bufferedReaderFromFile(TEST_DIRECTORY + "request/post_request.txt")) {
+            // when
+            HttpRequest httpRequest = HttpRequest.from(br);
         
-        // then
-        assertEquals(HttpMethod.POST, httpRequest.getMethod());
-        ...
+            // then
+            assertEquals(HttpMethod.POST, httpRequest.getMethod());
+            ...
+        }
     }
src/main/java/http/HttpRequest.java (1)

73-79: 내부 객체 노출이 의도적인지 확인하세요.

getStartLine()getHeaders()가 내부 객체를 직접 반환합니다. HttpStartLine은 불변이고 HttpHeaders의 접근자도 방어적 복사를 하므로 현재는 안전하지만, API 사용자가 이 객체들을 직접 조작하려 할 수 있습니다.

만약 내부 구조를 완전히 숨기고 싶다면, 이 메서드들을 제거하고 필요한 접근자만 HttpRequest에서 제공하는 것을 고려하세요. 아니면 현재처럼 유지하되, 이것이 의도된 설계임을 문서화하세요.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dba7017 and 75f58e3.

📒 Files selected for processing (31)
  • .idea/inspectionProfiles/Project_Default.xml (1 hunks)
  • build.gradle (1 hunks)
  • src/main/java/controller/Controller.java (1 hunks)
  • src/main/java/controller/ForwardController.java (1 hunks)
  • src/main/java/controller/UserListController.java (1 hunks)
  • src/main/java/controller/UserLoginController.java (1 hunks)
  • src/main/java/controller/UserSignupController.java (1 hunks)
  • src/main/java/http/HttpHeaders.java (1 hunks)
  • src/main/java/http/HttpRequest.java (1 hunks)
  • src/main/java/http/HttpResponse.java (1 hunks)
  • src/main/java/http/HttpStartLine.java (1 hunks)
  • src/main/java/http/enums/ContentType.java (1 hunks)
  • src/main/java/http/enums/HttpHeader.java (1 hunks)
  • src/main/java/http/enums/HttpMethod.java (1 hunks)
  • src/main/java/http/enums/HttpStatus.java (1 hunks)
  • src/main/java/http/enums/RequestPath.java (1 hunks)
  • src/main/java/model/UserField.java (1 hunks)
  • src/main/java/webserver/RequestHandler.java (2 hunks)
  • src/main/java/webserver/RequestMapper.java (1 hunks)
  • src/main/java/webserver/WebConfig.java (1 hunks)
  • src/test/java/HttpRequestTest.java (1 hunks)
  • src/test/java/HttpResponseTest.java (1 hunks)
  • src/test/java/controller/ForwardControllerTest.java (1 hunks)
  • src/test/java/controller/UserListControllerTest.java (1 hunks)
  • src/test/java/controller/UserLoginControllerTest.java (1 hunks)
  • src/test/java/controller/UserSignupControllerTest.java (1 hunks)
  • src/test/java/webserver/RequestMapperTest.java (1 hunks)
  • src/test/resources/request/get_request.txt (1 hunks)
  • src/test/resources/request/post_request.txt (1 hunks)
  • src/test/resources/response/test.html (1 hunks)
  • webapp/user/form.html (1 hunks)
🔇 Additional comments (23)
src/test/resources/response/test.html (1)

1-10: 테스트 리소스가 적절하게 작성되었습니다.

HttpResponse 테스트를 위한 간단하고 유효한 HTML 구조입니다. DOCTYPE, head, body 요소가 올바르게 구성되어 있습니다.

build.gradle (1)

16-16: Mockito 의존성 추가가 적절합니다.

mockito-junit-jupiter 5.12.0은 JUnit Jupiter와의 통합을 지원하며, 컨트롤러 단위 테스트에서 사용되는 @mock 및 MockitoAnnotations와 호환됩니다. Based on learnings

webapp/user/form.html (1)

58-58: 회원가입 폼의 보안 개선이 적절합니다.

GET에서 POST로 변경하여 사용자 정보가 URL에 노출되지 않도록 개선되었습니다. 이는 PR 목표인 "GET→POST transition for security"와 일치하며, UserSignupController의 POST 처리 로직과 정확히 연결됩니다.

src/test/resources/request/get_request.txt (1)

1-6: GET 요청 테스트 리소스가 적절합니다.

쿼리 파라미터, 헤더, 쿠키를 포함한 유효한 HTTP/1.1 GET 요청 구조입니다. HttpRequestTest의 파싱 테스트에 적합합니다.

src/main/java/controller/Controller.java (1)

8-10: 컨트롤러 인터페이스 설계가 깔끔합니다.

단일 책임 원칙을 따르는 간결한 인터페이스입니다. HttpRequest/HttpResponse를 인자로 받는 execute 메서드 시그니처가 적절하며, RequestMapper를 통한 다형적 호출 패턴을 지원합니다.

src/test/java/controller/UserSignupControllerTest.java (1)

38-58: 첫 번째 회원가입 테스트 로직이 정확합니다.

POST 요청 모킹, 컨트롤러 실행, 리다이렉트 검증, 그리고 저장소 상태 확인까지 모든 단계가 적절하게 구현되어 있습니다.

src/main/java/model/UserField.java (1)

3-18: 사용자 필드 enum이 잘 설계되었습니다.

매직 스트링을 제거하고 타입 안전성을 제공하는 깔끔한 enum 설계입니다. UserSignupController에서 폼 파라미터 추출 시 사용되어 코드 가독성과 유지보수성을 향상시킵니다.

src/main/java/controller/UserSignupController.java (1)

42-42: 리다이렉트 로직이 적절합니다.

POST 요청 후 302 리다이렉트로 메인 페이지로 이동하는 것은 PRG(Post-Redirect-Get) 패턴을 따르며, 브라우저 새로고침 시 중복 제출을 방지합니다.

src/main/java/controller/ForwardController.java (1)

30-39: 예외 처리 로직이 적절합니다.

IOException을 catch하여 404 응답을 반환하고, 적절한 로그 레벨(WARNING)로 기록하는 것은 정상적인 에러 흐름입니다. 파일이 없는 것은 클라이언트 오류이므로 스택 트레이스를 기록할 필요가 없습니다.

src/main/java/http/enums/HttpStatus.java (1)

8-22: Enum 설계가 잘 구현되었습니다.

불변 필드와 getter 메서드를 사용한 깔끔한 enum 구현입니다. 상태 코드와 상태 라인을 함께 관리하여 일관성을 보장합니다.

src/main/java/webserver/WebConfig.java (1)

34-39: createKey 로직이 명확합니다.

경로와 메서드를 조합하여 고유한 키를 생성하는 로직이 간단하고 명확합니다. null 처리도 적절합니다.

src/main/java/http/enums/RequestPath.java (1)

1-31: 경로 상수 관리가 잘 되어 있습니다.

RequestPath enum으로 경로를 중앙 관리하면 오타 방지, 리팩토링 용이성, IDE 자동완성 지원 등의 이점이 있습니다. 전체적으로 깔끔한 설계입니다.

src/test/java/controller/UserLoginControllerTest.java (1)

41-95: 테스트 케이스 구조와 커버리지가 우수합니다.

Given-When-Then 패턴을 일관되게 사용하고, 성공 케이스와 다양한 실패 케이스(잘못된 비밀번호, 존재하지 않는 사용자, 빈 파라미터)를 모두 커버하고 있습니다. @DisplayName으로 테스트 의도가 명확하게 표현되어 있습니다.

src/main/java/http/HttpStartLine.java (1)

10-31: 요청 라인 파싱 로직이 정확합니다.

null/empty 검증과 형식 검증(3개 파트)이 적절하며, HttpMethod.from으로 메서드 파싱을 위임하는 설계가 깔끔합니다.

src/main/java/http/enums/HttpHeader.java (1)

1-20: HTTP 헤더 enum 구조가 명확합니다.

필수 헤더들이 잘 정의되어 있고, getValue()로 실제 헤더 이름을 가져오는 구조가 적절합니다.

src/main/java/http/HttpHeaders.java (1)

50-60: 헤더 존재 확인 메서드가 올바르게 구현되었습니다.

getAllHeaders()가 방어적 복사본을 반환하는 것도 좋습니다. 단, 대소문자 처리 문제는 앞서 언급한 수정으로 함께 해결됩니다.

src/test/java/HttpRequestTest.java (3)

22-39: POST 요청 파싱 테스트가 포괄적입니다.

메서드, URL, 버전, 헤더, Content-Length, 바디 존재 여부 및 내용까지 모두 검증하고 있어 테스트 커버리지가 우수합니다.


41-58: GET 요청 테스트가 쿼리 스트링과 쿠키를 잘 검증합니다.

쿼리 파라미터 파싱과 쿠키 추출, 그리고 GET 요청에서 바디가 null인지 확인하는 등 GET 요청의 특성을 정확하게 테스트하고 있습니다.


60-78: 예외 처리 테스트가 적절합니다.

잘못된 형식과 빈 요청 라인에 대해 IllegalArgumentException이 발생하는지 검증하여 에러 처리를 확인하고 있습니다.

src/main/java/http/HttpResponse.java (3)

17-24: 생성자 설계가 깔끔합니다.

기본 webapp 경로를 제공하는 동시에 테스트를 위한 커스터마이징이 가능한 오버로드된 생성자 구조가 좋습니다.


38-49: 리다이렉트 구현이 올바릅니다.

302 상태 코드와 Location 헤더 사용이 정확하며, redirectWithCookie()도 Set-Cookie 헤더를 적절히 추가하고 있습니다.


51-77: 404 응답과 헬퍼 메서드 구현이 깔끔합니다.

응답 작성 로직이 잘 분리되어 있고, writeBody()에서 적절히 flush하고 있습니다.

src/main/java/http/HttpRequest.java (1)

14-35: HTTP 요청 파싱 로직이 견고합니다.

불변 설계, 적절한 검증, 그리고 POST 요청에 대해서만 Content-Length가 있을 때 바디를 읽는 조건부 로직이 정확합니다. HttpStartLineHttpHeaders로 파싱을 위임하는 것도 책임이 잘 분리되어 있습니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/main/java/controller/UserSignupController.java (1)

38-40: 검증 실패 시 HTTP 에러 응답이 누락되었습니다.

입력 검증이 실패하면 return만 수행하여 클라이언트에게 응답을 보내지 않습니다. 이는 연결이 멈추거나 타임아웃될 수 있어 사용자 경험을 저해합니다.

다음과 같이 400 Bad Request 응답을 추가하세요:

         if (userId == null || userId.trim().isEmpty() ||
                 password == null || password.length() < 4 ||
                 name == null || name.trim().isEmpty() ||
                 email == null || !email.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$")) {
-            // TODO: Bad Request
+            response.badRequest("Invalid input parameters");
             return;
         }

참고: HttpResponsebadRequest() 메서드 추가가 필요합니다.

🧹 Nitpick comments (2)
src/main/java/controller/UserSignupController.java (1)

38-39: TODO: 검증 실패 시 적절한 응답을 구현하세요.

현재 검증 실패 시 조기 반환만 하고 클라이언트에 응답을 보내지 않습니다. 사용자에게 검증 실패 이유를 알려주는 400 Bad Request 응답이 필요합니다.

다음과 같이 개선할 수 있습니다:

         if (userId == null || userId.trim().isEmpty() ||
                 password == null || password. length() < 4 ||
                 name == null || name.trim().isEmpty() ||
                 email == null || !email.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$")) {
-            // TODO: Bad Request
-            return;
+            response.badRequest("Invalid input parameters");
+            return;
         }

HttpResponsebadRequest() 메서드 추가가 필요합니다. 구현 코드 생성을 도와드릴까요?

src/test/java/controller/UserLoginControllerTest.java (1)

30-39: 테스트 격리 제약 사항을 인지하고 계십니다.

개발자께서 이미 싱글톤 Repository의 clear() 메서드 부재로 인한 테스트 격리 문제를 인지하고 계신 것으로 확인됩니다. 현재 프로젝트 제약 사항 내에서 적절한 선택입니다.

향후 테스트 간 상태 공유로 인한 문제 발생 시 Repository 인터페이스에 clear() 추가를 고려하시기 바랍니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75f58e3 and 48e86d8.

📒 Files selected for processing (12)
  • .idea/vcs.xml (1 hunks)
  • src/main/java/controller/UserListController.java (1 hunks)
  • src/main/java/controller/UserLoginController.java (1 hunks)
  • src/main/java/controller/UserSignupController.java (1 hunks)
  • src/main/java/http/HttpHeaders.java (1 hunks)
  • src/main/java/http/HttpRequest.java (1 hunks)
  • src/main/java/http/HttpResponse.java (1 hunks)
  • src/main/java/http/enums/ContentType.java (1 hunks)
  • src/main/java/http/enums/RequestPath.java (1 hunks)
  • src/test/java/HttpRequestTest.java (1 hunks)
  • src/test/java/controller/UserLoginControllerTest.java (1 hunks)
  • src/test/resources/request/post_request.txt (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • .idea/vcs.xml
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/main/java/controller/UserLoginController.java
  • src/main/java/http/HttpResponse.java
  • src/main/java/http/HttpHeaders.java
  • src/main/java/http/enums/ContentType.java
  • src/test/resources/request/post_request.txt
🧰 Additional context used
🧬 Code graph analysis (5)
src/main/java/controller/UserSignupController.java (5)
src/main/java/db/MemoryUserRepository.java (1)
  • MemoryUserRepository (9-35)
src/main/java/http/HttpRequest.java (1)
  • HttpRequest (9-80)
src/main/java/http/HttpResponse.java (1)
  • HttpResponse (13-91)
src/main/java/http/util/HttpRequestUtils.java (1)
  • HttpRequestUtils (8-20)
src/main/java/model/User.java (1)
  • User (5-46)
src/main/java/http/HttpRequest.java (3)
src/main/java/http/util/IOUtils.java (1)
  • IOUtils (6-21)
src/main/java/http/HttpStartLine.java (1)
  • HttpStartLine (5-58)
src/main/java/http/HttpHeaders.java (1)
  • HttpHeaders (10-68)
src/test/java/HttpRequestTest.java (1)
src/main/java/http/HttpRequest.java (1)
  • HttpRequest (9-80)
src/test/java/controller/UserLoginControllerTest.java (4)
src/main/java/db/MemoryUserRepository.java (1)
  • MemoryUserRepository (9-35)
src/main/java/http/HttpRequest.java (1)
  • HttpRequest (9-80)
src/main/java/http/HttpResponse.java (1)
  • HttpResponse (13-91)
src/main/java/model/User.java (1)
  • User (5-46)
src/main/java/controller/UserListController.java (2)
src/main/java/http/HttpRequest.java (1)
  • HttpRequest (9-80)
src/main/java/http/HttpResponse.java (1)
  • HttpResponse (13-91)
🔇 Additional comments (19)
src/main/java/controller/UserListController.java (3)

20-20: 과거 리뷰의 보안 취약점이 올바르게 수정되었습니다.

contains() 대신 정확한 키-값 파싱을 사용하도록 변경되어 부분 문자열 매칭 문제가 해결되었습니다. Objects.equals를 사용한 null-safe 비교도 적절합니다.


31-39: 쿠키 파싱 로직이 올바르게 구현되었습니다.

세미콜론과 공백으로 쌍을 분리하고, =로 키-값을 분할하며, 키가 일치하는 값을 반환하는 구현이 정확합니다.


20-20: 쿠키 검증 로직이 올바르게 개선되었습니다.

이전 리뷰에서 제기된 부분 문자열 매칭 취약점이 parseCookieValue 헬퍼 메서드를 통한 정확한 키-값 파싱으로 해결되었습니다. Objects.equals를 사용한 null-safe 비교도 적절합니다.

Also applies to: 31-39

src/main/java/http/enums/RequestPath.java (2)

23-30: 과거 리뷰의 일관성 문제가 올바르게 해결되었습니다.

이제 HttpMethod.from()과 동일하게 알 수 없는 경로에 대해 IllegalArgumentException을 던지므로, null 체크 누락으로 인한 NPE 위험이 제거되었습니다.


23-30: from() 메서드가 일관성 있게 개선되었습니다.

이전 리뷰에서 제기된 null 반환 문제가 IllegalArgumentException 발생으로 변경되어 HttpMethod.from()과 일관된 예외 처리 방식을 따르게 되었습니다. 알 수 없는 경로에 대한 명확한 오류 메시지도 제공됩니다.

src/main/java/controller/UserSignupController.java (2)

28-40: 입력 검증이 올바르게 추가되었습니다.

과거 리뷰에서 요청한 필수 필드 검증, 비밀번호 길이 확인, 이메일 형식 검증이 모두 구현되었습니다. NPE 방지와 데이터 무결성이 크게 개선되었습니다.


28-37: 입력 검증 로직이 올바르게 추가되었습니다.

이전 리뷰에서 제기된 NPE 및 유효하지 않은 데이터 처리 문제가 필수 필드 검증, 비밀번호 길이 검증, 이메일 형식 검증을 통해 해결되었습니다. 보안과 안정성이 향상되었습니다.

src/test/java/controller/UserLoginControllerTest.java (3)

30-39: setUp 메서드가 올바르게 구현되었습니다.

Mock 초기화와 테스트용 사용자 등록이 적절히 수행됩니다. 싱글톤 Repository의 상태 공유 문제는 개발자가 인지하고 있으며, clear() 메서드가 없는 제약사항으로 확인되었습니다.


41-95: 테스트 케이스가 포괄적으로 작성되었습니다.

성공 케이스(올바른 인증), 실패 케이스(잘못된 비밀번호, 존재하지 않는 사용자, 빈 파라미터)를 모두 검증하며, Mockito verify를 사용한 동작 검증도 적절합니다.


41-95: 로그인 테스트 커버리지가 충분합니다.

성공 케이스, 잘못된 비밀번호, 존재하지 않는 사용자, 빈 파라미터 등 주요 시나리오를 모두 검증하고 있습니다. 테스트 로직과 어설션이 정확합니다.

src/test/java/HttpRequestTest.java (4)

22-39: POST 요청 파싱 테스트가 올바르게 작성되었습니다.

메서드, URL, 버전, 헤더, 바디를 포함한 모든 요소를 검증하며, 테스트 리소스 파일을 사용한 접근 방식이 실제 HTTP 요청과 유사하여 좋습니다.


41-58: GET 요청 파싱 테스트가 포괄적입니다.

쿼리 파라미터와 쿠키 파싱을 검증하고, POST와 달리 body가 null임을 확인하는 등 GET 요청의 특성을 정확히 테스트합니다.


60-78: 에러 케이스 테스트가 적절합니다.

잘못된 형식과 빈 요청 라인에 대한 예외 처리를 assertThrows로 검증하여, 파싱 견고성을 확인합니다.


22-78: HTTP 요청 파싱 테스트가 포괄적입니다.

POST/GET 요청의 정상 파싱, 쿼리 파라미터/쿠키 처리, 잘못된 형식/빈 요청 라인에 대한 예외 처리 등 핵심 시나리오를 모두 검증하고 있습니다. 테스트 구조가 명확하고 어설션이 정확합니다.

src/main/java/http/HttpRequest.java (5)

20-35: HTTP 요청 파싱 팩토리 메서드가 올바르게 구현되었습니다.

요청 라인 검증, 헤더 파싱, POST 요청에 대한 조건부 바디 읽기가 적절히 처리됩니다. IOUtils를 통한 Content-Length 기반 바디 읽기도 정확합니다.


41-47: 과거 리뷰의 메서드 중복 문제가 올바르게 해결되었습니다.

getUrl()은 쿼리 스트링을 포함한 전체 경로를, getPath()는 쿼리 없는 경로를 반환하도록 명확히 구분되었습니다. 이는 과거 리뷰의 Option 2 권장사항과 일치합니다.


37-80: 접근자 메서드들이 올바르게 구현되었습니다.

모든 HTTP 요청 요소(메서드, URL, 헤더, 쿠키, 바디)에 대한 접근자가 적절히 위임 패턴을 사용하여 구현되었으며, 캡슐화가 잘 유지됩니다.


41-47: getUrl()getPath() 메서드가 명확하게 구분되었습니다.

이전 리뷰에서 제기된 중복 문제가 해결되었습니다:

  • getUrl(): 쿼리 스트링을 포함한 전체 경로 반환
  • getPath(): 쿼리 스트링을 제외한 경로만 반환

각 메서드가 명확하고 구분된 역할을 수행합니다.


20-35: HTTP 요청 파싱 로직이 올바르게 구현되었습니다.

팩토리 메서드가 다음을 적절하게 처리합니다:

  • 요청 라인 검증 (null/empty 체크)
  • HttpStartLine 및 HttpHeaders로의 파싱 위임
  • POST 요청의 Content-Length 기반 바디 읽기

파싱 로직이 견고하고 명확합니다.

Comment on lines +30 to +32
// 파라미터 유효성 검사
if (userId == null || userId.isEmpty() || password == null || password.isEmpty()) {
log.log(Level.WARNING, "Login failed: missing parameters");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 예외처리 부분이 너무 포괄적이라는 생각도 드네요
나중에 프론트에 에러 던져줄 거 생각해보면 "bad request"보다는 "userId가 비었습니다" 같은 문구가 원인 파악하기에 좋으니까 하나씩 빼서 다른 log 찍도록 처리하면 좋을것 같습니다!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우선은 코드래빗 리뷰대로만 수정했습니다... <- 추석 이슈 입니다..

[이전에 프론트만 했덥 입장으로..]

백엔드에서 처리할
파라미터 유효성 검사에 대해서 많이 생각을 해봤는데...

이런 부분은 프론트에서 처리하는 게 맞지 않을까요?-?

서버에서 유효성 검사를 진행하는 게 맞을까요?-? @jyun-KIM

이럴 때 파트장님의 생각은 어떠하신지가 궁금하네요.

제 생각에는 예외처리 부분에서는 userId - password가 매칭이 안되는 것? 과 같이 유효성 검사를 다 뚫고 오는 것들만 처리해주는 게 맞는 거 같아서요...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ParkSeongGeun

네 맞습니다. 성근님이 더 잘 아시겠지만 실제 프로젝트에서는 ID나 비밀번호 값이 비어있는 예외는 프론트엔드에서 처리해주는 것이 일반적이죠. 그리고 백엔드에서는 말씀하신 대로 ID를 받아 사용자가 존재하는지 확인하는 것부터 시작하고요.

다만 지금은 학습하는 과정에 있으니, 발생할 수 있는 예외 상황을 백엔드에서 하나하나 명시적으로 처리하는 연습을 해보는 것이 중요하다고 생각했습니다. 이런 경험이 나중에 안정적인 서비스를 만드는 데 좋은 밑거름이 될 것이라 생각해서 자세하게 말씀드렸습니다!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jyun-KIM

넵! 반영하도록 하겠습니다!

피드백 감사드려요~!~!

Copy link

@rlagkswn00 rlagkswn00 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

슥 지나갑니다~

Comment on lines +3 to +7
public enum UserField {
USER_ID("userId"),
PASSWORD("password"),
NAME("name"),
EMAIL("email");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UserField enum 클래스로 지정하신 이유가 있나용?
다른 enum클래스와 다르게 특정 종류에 대한 것이 아니라고 느껴지는 내용인 듯 합니다

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

단순히 매직 스트링 방지 때문이었습니다!

  • 상수 클래스로도 할까 고민을 했었는데, UserField로 입력될 수 있는 부분에 대한 관리로 사용했습니다.

} catch (IOException e) {
} catch (Exception e) {
log.log(Level.SEVERE, e.getMessage());
System.out.println(Arrays.toString(e.getStackTrace()));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사소하지만 에러를 "로깅"하기 위한 목적이라면 logger를 쓰는게 더 적절할거 같습니다!(바로 위처럼!)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아직 Logger에 대해 익숙치 않아, 그냥 확인하는 방향으로는 sout을 썼었네요.. 👎

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그렇다면 단순한 입출력과 로깅의 차이에 대해 한번 고민해보시면 좋을거 같아요~!🙌

Comment on lines +12 to +20
private final HttpRequest httpRequest;
private final HttpResponse httpResponse;

private static final Map<String, Controller> CONTROLLERS = WebConfig.configureControllers();

public RequestMapper(HttpRequest httpRequest, HttpResponse httpResponse) {
this.httpRequest = httpRequest;
this.httpResponse = httpResponse;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RequestMapper가 HttpRequest와 HttpResponse 전체 객체를 필드로 갖고 있어야 할까요??
"Mapper"라는 이름에 맞게 "요청"과 "컨트롤러"를 매핑하는게 주 목적이라면 없어도 충분할거 같아요!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mapper는 매핑만 하면 되는데 실행에 필요한 객체까지 들고 있네요...

HttpRequest, HttpResponse를 사용하는 것이 아니라

param으로 들어온 path, method를 통해

String key = WebConfig.createKey(path, method);

를 통해 Controller만 찾아주면 되는.. 것이었네요

Comment on lines +22 to +32
public void proceed() throws IOException {
String path = httpRequest.getPath();
HttpMethod method = httpRequest.getMethod();

Controller controller = getController(path, method);
if (controller == null) {
controller = new ForwardController();
}

controller.execute(httpRequest, httpResponse);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RequestMapper의 역할이 어떤 것인지 한번 더 고민해보시면 이 메서드가 필요 없을 수도 있을거 같아요!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mapper에서 매핑 + 실행 까지 모두 하고 있었네요. 없어져도 될 거 같습니다!

Comment on lines +10 to +13
/**
* Controller 매핑 설정을 담당하는 클래스
* Spring의 @Configuration과 유사한 역할
*/

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호 @configuration와 어떤 유사한 역할일까요???

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Controller 인스턴스를 생성하고 URL 매핑을 해준다는 점에서 유사하다고 생각하였습니다.

  • Bean 등록을 하는 부분과 유사하다고 생각했습니다.


public class HttpResponse {
private final DataOutputStream dos;
private final String webappPath;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사용자가 하나의 요청을 하면 하나의 요청,응답 객체를 만드는 구조로 이해했어요!
이때 어떠한 이유에서 매 응답객체마다 webappPath가 필요한 건가요?
각 응답마다 다른 webappPath를 설정해 주어야 하는 걸까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아닙니다..아닙니다...아니에요... ㅠㅠㅠ

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static으로 변경하는 게 가장 간단히 수정할 수 있는 방법 같아, 수정했습니다!

Comment on lines +27 to +31
// 경로 정규화 및 검증
if (path.contains("..")) {
notFound();
return;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요청, 응답에서 모두 이 디렉토리 리스팅에 관한 검사가 진행되는게 인상깊네요!
응답 같은 경우 개발자가 의도한 바대로 진행될텐데 그럼에도 이 부분을 고려한 이유가 있을까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

휴먼 에러 방지용이었는데, 크게 의미는 없었습니다... ㅎㅎ

Comment on lines +4 to +6
OK(200, "HTTP/1.1 200 OK"),
FOUND(302, "HTTP/1.1 302 Found"),
NOT_FOUND(404, "HTTP/1.1 404 Not Found");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

응답 Http 버전이 항상 1.1인걸까요??
변경될 여지는 없을까요?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 요청 시에 들어오는 Http 버전과 같은 의미를 띄고 있지는 않을까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

당연히 변경됩니다... HTTP 응답 버전은 요청 버전과 동일해야 하는 게 맞습니다....

@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this);
controller = new ForwardController();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

매 테스트 마다 FowardController를 생성해야할 이유가 있나요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

허허 없습니다! ㅎㅎ stateless이니, 인스턴스 재사용만 하면 될 거 같습니다 ㅎㅎ..

Comment on lines +36 to +38
// 테스트용 사용자 미리 등록
User testUser = new User("testuser", "1234", "홍길동", "test@example.com");
repository.addUser(testUser);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BeforeEach에 의해 동일한 계정정보를 가진 사용자가 매 테스트마다 1개, 2개, 3개... 쌓이는거 같은데 이러면 테스트 중에 문제가 생길 수도 있을거 같은데 어떻게 생각하시나요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이것은 기존 repository에 clearAll과 같은 메서드가 없어 그대로 두었습니다...

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그렇다면 성근님 고-수시기 떄문에 쉽게 만드시지 않을까🤣

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/test/java/controller/UserLoginControllerTest.java (1)

30-39: 테스트 격리 개선을 권장합니다.

UserSignupControllerTest와 동일한 테스트 격리 문제가 있습니다. 각 테스트 실행 전에 동일한 사용자를 저장소에 추가하므로, Map.put이 기존 값을 덮어쓰긴 하지만 테스트 간 상태 공유가 발생합니다.

저장소에 clear() 메서드를 추가하고 @BeforeEach에서 먼저 호출하여 깨끗한 상태에서 테스트를 시작하는 것을 권장합니다.

src/main/java/http/HttpResponse.java (1)

28-51: 경로 순회 취약점이 여전히 존재합니다.

Line 30의 ".." 체크는 부분적인 개선이지만 여전히 여러 공격 벡터에 취약합니다:

  • URL 인코딩 우회: %2e%2e%2f 또는 ..%2f
  • 절대 경로: /etc/passwd
  • 다중 슬래시: ....//
  • 대소문자 변형 (Windows): ..\

또한 다음 문제들이 있습니다:

  • 파일 I/O 예외 처리 누락: Line 43의 Files.readAllBytes()에서 IOException이 발생하면 스트림이 불완전한 상태로 남을 수 있습니다.
  • 파일 타입 검증 누락: Line 38의 Files.exists()는 디렉토리도 통과시킵니다.

다음과 같이 개선하세요:

     public void forward(String path) throws IOException {
-        // 경로 정규화 및 검증
-        if (path.contains("..")) {
-            notFound();
-            return;
-        }
-        
-        String filePath = WEBAPP_PATH + path;
-        
-        // 파일 존재 확인
-        if (!Files.exists(Paths.get(filePath))) {
-            notFound();
-            return;
-        }
-        
-        byte[] fileContent = Files.readAllBytes(Paths.get(filePath));
-        String contentType = ContentType.fromFileExtension(filePath).getValue();
+        try {
+            // 경로 정규화 및 검증
+            Path normalizedPath = Paths.get(WEBAPP_PATH, path).normalize();
+            Path webappBase = Paths.get(WEBAPP_PATH).toAbsolutePath().normalize();
+            
+            // webapp 디렉토리 외부 접근 차단
+            if (!normalizedPath.toAbsolutePath().startsWith(webappBase)) {
+                notFound();
+                return;
+            }
+            
+            // 파일 존재 및 타입 확인
+            if (!Files.exists(normalizedPath) || !Files.isRegularFile(normalizedPath)) {
+                notFound();
+                return;
+            }
+            
+            byte[] fileContent = Files.readAllBytes(normalizedPath);
+            String contentType = ContentType.fromFileExtension(normalizedPath.toString()).getValue();
         
-        writeStatusLine(HttpStatus.OK);
-        writeHeader(HttpHeader.CONTENT_TYPE, contentType);
-        writeHeader(HttpHeader.CONTENT_LENGTH, String.valueOf(fileContent.length));
-        writeEndOfHeaders();
-        writeBody(fileContent);
+            writeStatusLine(HttpStatus.OK);
+            writeHeader(HttpHeader.CONTENT_TYPE, contentType);
+            writeHeader(HttpHeader.CONTENT_LENGTH, String.valueOf(fileContent.length));
+            writeEndOfHeaders();
+            writeBody(fileContent);
+        } catch (IOException e) {
+            notFound();
+        }
     }

주요 개선사항:

  1. Path.normalize()startsWith() 검증으로 모든 경로 조작 시도 차단
  2. Files.isRegularFile() 추가로 디렉토리 접근 방지
  3. try-catch로 파일 I/O 예외 처리
🧹 Nitpick comments (3)
src/test/java/controller/UserSignupControllerTest.java (1)

31-36: 테스트 격리를 개선하는 것을 권장합니다.

MemoryUserRepository 싱글톤을 사용하면서 테스트 간 초기화가 없어 상태가 누적됩니다. 현재는 각 테스트가 서로 다른 userId를 사용하므로 실질적인 충돌은 적지만, 테스트 실행 순서에 의존하게 되어 유지보수성이 저하됩니다.

저장소에 clear() 메서드를 추가하고 @BeforeEach 또는 @AfterEach에서 호출하여 테스트 격리를 보장하는 것을 고려해 보세요.

src/main/java/webserver/RequestHandler.java (1)

23-36: 추상화 통합이 잘 되었습니다.

HttpRequestHttpResponse 추상화를 통해 요청 처리 로직이 명확해졌고, RequestMapper와의 통합도 깔끔합니다. 요청 버전으로 응답을 생성하는 부분도 올바릅니다.

예외 처리를 더 구체화하는 것을 고려해 보세요. 현재 모든 Exception을 포괄적으로 잡고 있어 복구 가능한 오류와 치명적인 오류를 구분하기 어렵습니다. 향후 특정 예외 타입별로 다른 처리 전략을 적용할 수 있도록 개선하면 좋습니다.

src/main/java/http/HttpResponse.java (1)

19-26: 스트림 생명주기 문서화를 권장합니다.

현재 HttpResponse는 전달받은 OutputStream을 닫지 않습니다. 이는 호출자가 스트림을 소유하고 관리한다는 의도로 보이는데, 이를 명시적으로 문서화하면 사용자의 혼란을 줄일 수 있습니다.

클래스 또는 생성자에 다음과 같은 JavaDoc을 추가하는 것을 고려해보세요:

/**
 * HTTP 응답을 생성하고 전송하는 유틸리티 클래스.
 * <p>
 * 참고: 이 클래스는 전달받은 OutputStream을 닫지 않습니다.
 * 호출자가 스트림의 생명주기를 관리할 책임이 있습니다.
 * </p>
 */
public class HttpResponse {
    // ...
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 48e86d8 and 0788b13.

📒 Files selected for processing (16)
  • src/main/java/controller/UserListController.java (1 hunks)
  • src/main/java/controller/UserLoginController.java (1 hunks)
  • src/main/java/controller/UserSignupController.java (1 hunks)
  • src/main/java/http/HttpHeaders.java (1 hunks)
  • src/main/java/http/HttpRequest.java (1 hunks)
  • src/main/java/http/HttpResponse.java (1 hunks)
  • src/main/java/http/HttpStartLine.java (1 hunks)
  • src/main/java/http/enums/HttpStatus.java (1 hunks)
  • src/main/java/webserver/RequestHandler.java (2 hunks)
  • src/main/java/webserver/RequestMapper.java (1 hunks)
  • src/test/java/HttpResponseTest.java (1 hunks)
  • src/test/java/controller/ForwardControllerTest.java (1 hunks)
  • src/test/java/controller/UserListControllerTest.java (1 hunks)
  • src/test/java/controller/UserLoginControllerTest.java (1 hunks)
  • src/test/java/controller/UserSignupControllerTest.java (1 hunks)
  • src/test/java/webserver/RequestMapperTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/main/java/controller/UserListController.java
  • src/main/java/http/HttpStartLine.java
  • src/test/java/controller/ForwardControllerTest.java
  • src/main/java/http/HttpRequest.java
  • src/main/java/http/HttpHeaders.java
  • src/test/java/HttpResponseTest.java
  • src/main/java/controller/UserLoginController.java
🧰 Additional context used
🧬 Code graph analysis (6)
src/main/java/webserver/RequestHandler.java (2)
src/main/java/http/HttpRequest.java (1)
  • HttpRequest (12-107)
src/main/java/http/HttpResponse.java (1)
  • HttpResponse (13-93)
src/test/java/controller/UserListControllerTest.java (2)
src/main/java/http/HttpRequest.java (1)
  • HttpRequest (12-107)
src/main/java/http/HttpResponse.java (1)
  • HttpResponse (13-93)
src/test/java/controller/UserLoginControllerTest.java (4)
src/main/java/db/MemoryUserRepository.java (1)
  • MemoryUserRepository (9-35)
src/main/java/http/HttpRequest.java (1)
  • HttpRequest (12-107)
src/main/java/http/HttpResponse.java (1)
  • HttpResponse (13-93)
src/main/java/model/User.java (1)
  • User (5-46)
src/main/java/controller/UserSignupController.java (4)
src/main/java/db/MemoryUserRepository.java (1)
  • MemoryUserRepository (9-35)
src/main/java/http/HttpRequest.java (1)
  • HttpRequest (12-107)
src/main/java/http/HttpResponse.java (1)
  • HttpResponse (13-93)
src/main/java/model/User.java (1)
  • User (5-46)
src/main/java/webserver/RequestMapper.java (1)
src/main/java/webserver/WebConfig.java (1)
  • WebConfig (14-40)
src/test/java/controller/UserSignupControllerTest.java (4)
src/main/java/db/MemoryUserRepository.java (1)
  • MemoryUserRepository (9-35)
src/main/java/http/HttpRequest.java (1)
  • HttpRequest (12-107)
src/main/java/http/HttpResponse.java (1)
  • HttpResponse (13-93)
src/main/java/model/User.java (1)
  • User (5-46)
🔇 Additional comments (11)
src/main/java/http/enums/HttpStatus.java (1)

3-27: 구현이 깔끔합니다!

HTTP 상태 코드를 enum으로 관리하는 것은 타입 안전성을 제공하는 좋은 접근입니다. 현재 구현된 세 가지 상태 코드(OK, FOUND, NOT_FOUND)는 기본 기능에 충분합니다.

src/test/java/webserver/RequestMapperTest.java (1)

22-90: 매퍼 테스트 범위가 적절합니다.

RequestMapper의 단위 테스트로서 올바른 컨트롤러 인스턴스를 반환하는지 검증하는 것은 적절한 범위입니다. 컨트롤러의 실행 동작은 각 컨트롤러의 개별 테스트에서 검증되고 있으므로 책임 분리가 잘 되어 있습니다.

src/main/java/webserver/RequestMapper.java (1)

8-24: 매핑 책임이 명확합니다!

RequestMapper가 라우팅 로직에만 집중하고 실행은 컨트롤러에 위임하는 설계가 깔끔합니다. 2단계 조회(path+method → path만) 및 기본 폴백 전략도 합리적입니다.

src/test/java/controller/UserListControllerTest.java (1)

17-84: 접근 제어 시나리오가 잘 커버되었습니다.

쿠키 기반 인가 로직의 주요 경로(로그인됨/안됨)를 명확하게 테스트하고 있으며, 테스트 이름도 의도를 잘 드러냅니다.

src/main/java/controller/UserSignupController.java (1)

19-49: 전반적인 구현이 견고합니다.

컨트롤러 패턴을 잘 따르고 있으며, 입력 검증, 저장소 상호작용, 리다이렉션 흐름이 명확합니다. 유효성 검증 실패 시 응답 처리만 보완하면 프로덕션 수준에 가까워질 것입니다.

src/main/java/http/HttpResponse.java (6)

1-12: 임포트 구성이 적절합니다.

필요한 모든 의존성이 올바르게 임포트되었습니다.


13-26: 클래스 구조가 적절합니다.

WEBAPP_PATH를 static으로 변경한 것은 이전 피드백을 잘 반영한 개선입니다. 생성자 오버로딩도 합리적입니다.


53-57: 리다이렉트 구현이 적절합니다.

302 Found 상태 코드와 Location 헤더를 사용한 표준적인 리다이렉트 구현입니다.


59-64: 쿠키와 함께 리다이렉트하는 구현이 적절합니다.

Set-Cookie를 먼저, Location을 나중에 설정하는 것은 일반적인 관례를 따르고 있습니다.


66-75: 404 응답 구현이 적절합니다.

간단하지만 기능적으로 올바른 404 Not Found 응답입니다.


77-92: 헬퍼 메서드들이 HTTP 프로토콜을 정확히 구현하고 있습니다.

CRLF(\r\n) 사용과 flush 처리가 적절합니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants