Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
fba6fbd
[Feat] Requirement 1
ParkSeongGeun Sep 27, 2025
d90c6bb
[Feat] Add HTTP header and body parsing to RequestHandler
ParkSeongGeun Sep 27, 2025
48235ce
[Feat] Add helper for sending HTTP 302 redirect response
ParkSeongGeun Sep 29, 2025
148c4b3
[Feat] Requirement 2
ParkSeongGeun Sep 29, 2025
71da5b0
[Fix] Change Get -> Post
ParkSeongGeun Sep 29, 2025
8fc5f74
[Feat] Requirement 3: POST SignUp
ParkSeongGeun Sep 29, 2025
6ba9cfa
[Fix] HTTP Header Handler method fix
ParkSeongGeun Sep 29, 2025
913652b
[Feat] Requirement 5 - Add login handling and improve redirect logic
ParkSeongGeun Sep 29, 2025
8e2a33e
[Feat] Requirement 6 - Add login check for /user/userList route
ParkSeongGeun Sep 29, 2025
626e901
[Feat] Requirement 7 - getContentType
ParkSeongGeun Sep 29, 2025
6e06134
[Feat] Assignment2 - Requirement 1: Make Enums
ParkSeongGeun Sep 29, 2025
0a5ca5b
[Feat] Assignment2 - Requirement 1: Using Enums in RequestHandler
ParkSeongGeun Sep 29, 2025
1db727e
[Feat] Assignment2 Requirement 2: Add HTTP request parsing classes
ParkSeongGeun Sep 29, 2025
d3b3d15
[Feat] Assignment2 Requirement 2: Add HttpRequest parsing unit tests
ParkSeongGeun Sep 29, 2025
6a62fc7
[Feat] Assignment2 Requirement 2: Refactor RequestHandler
ParkSeongGeun Sep 29, 2025
c0b73a0
[Feat] Assignment2 Requirement 3: Add HttpResponse class
ParkSeongGeun Sep 29, 2025
070db5f
[Feat] Assignment2 Requirement 3: Add HttpResponse unit tests and tes…
ParkSeongGeun Sep 29, 2025
1a43fbe
[Chore] Assignment2 Requirement 3: Move request test files to request…
ParkSeongGeun Sep 29, 2025
afc5b7b
[Feat] Assignment2 Requirement 3: Refactor RequestHandler to use Http…
ParkSeongGeun Sep 29, 2025
3cbbf7a
[Feat] Assignment2 Requirement 4: Create Controller Interface
ParkSeongGeun Sep 29, 2025
d91bd20
[Chore] add mockito dependency
ParkSeongGeun Sep 29, 2025
81328f3
[Feat] Assignment2 Requirement 4: Create UserSignupControllerTest
ParkSeongGeun Sep 29, 2025
af032a3
[Feat] Assignment2 Requirement 4: Create UserSignupController
ParkSeongGeun Sep 29, 2025
766093a
[Refact] Assignment2 Requirement 4: Refact RequestHandler to using Us…
ParkSeongGeun Sep 29, 2025
201e646
[Feat] Assignment2 Requirement 4: Create UserLoginControllerTest
ParkSeongGeun Sep 29, 2025
4d4c1da
[Feat] Assignment2 Requirement 4: Create UserLoginController
ParkSeongGeun Sep 29, 2025
9cad2f7
[Refact] Assignment2 Requirement 4: Refact RequestHandler to using Us…
ParkSeongGeun Sep 29, 2025
b77b479
[Feat] Assignment2 Requirement 4: Create UserListController
ParkSeongGeun Sep 29, 2025
5b96d7b
[Feat] Assignment2 Requirement 4: Create UserListController
ParkSeongGeun Sep 29, 2025
25ed0b9
[Refact] Assignment2 Requirement 4: Refact RequestHandler to using Us…
ParkSeongGeun Sep 29, 2025
53f75b6
[Feat] Assignment2 Requirement 4: Create StaticFileControllerTest
ParkSeongGeun Sep 29, 2025
4e926f5
[Feat] Assignment2 Requirement 4: Create StaticFileController
ParkSeongGeun Sep 29, 2025
24aaf94
[Refact] Assignment2 Requirement 4: Refact RequestHandler to using St…
ParkSeongGeun Sep 29, 2025
cd5e736
[Chore] Change StaticFileController File Name to ForwardController
ParkSeongGeun Sep 29, 2025
822abc5
[Refact] Refact RequestHandler
ParkSeongGeun Sep 29, 2025
2b6e292
[Feat] Assignment2 Requirement 5: Create RequestMapper
ParkSeongGeun Sep 29, 2025
6a36007
[Refact] Assignment2 Requirement 5: Refact RequestHandler to using Re…
ParkSeongGeun Sep 29, 2025
6db6245
[Feat] Assignment2 Requirement 5: Create RequestMapperTest
ParkSeongGeun Sep 29, 2025
75f58e3
[Refact] Refactor controller mapping to WebConfig class
ParkSeongGeun Sep 30, 2025
48e86d8
[Fix] CodeRabbit Review
ParkSeongGeun Oct 3, 2025
0788b13
[Fix] Code Review
ParkSeongGeun Oct 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ repositories {
dependencies {
testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'

testImplementation 'org.mockito:mockito-junit-jupiter:5.12.0'
}

test {
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/controller/Controller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package controller;

import http.HttpRequest;
import http.HttpResponse;

import java.io.IOException;

public interface Controller {
void execute(HttpRequest request, HttpResponse response) throws IOException;
}
41 changes: 41 additions & 0 deletions src/main/java/controller/ForwardController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package controller;

import http.HttpRequest;
import http.HttpResponse;
import http.enums.RequestPath;

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

public class ForwardController implements Controller {
private static final Logger log = Logger.getLogger(ForwardController.class.getName());

@Override
public void execute(HttpRequest request, HttpResponse response) throws IOException {
String path = request.getPath();

// 1. 루트 경로 ("/") 처리 - 기본 페이지로 변경
if (path.equals(RequestPath.ROOT.getValue())) {
path = RequestPath.INDEX.getValue();
}

// 2. 보안 검증 - ../ 과 같은 디렉토리 traversal 공격 방지
if (path.contains("..")) {
log.log(Level.WARNING, "Directory traversal attack detected: " + path);
response.notFound();
return;
}

try {
// 3. 파일 forward
response.forward(path);
log.log(Level.INFO, "Static file served successfully: " + path);

} catch (IOException fileException) {
// 4. 파일이 없거나 읽기 실패시 404 에러 응답
log.log(Level.WARNING, "File not found or read error: " + path);
response.notFound();
}
}
}
29 changes: 29 additions & 0 deletions src/main/java/controller/UserListController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package controller;

import http.HttpRequest;
import http.HttpResponse;
import http.enums.RequestPath;

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

public class UserListController implements Controller {
private static final Logger log = Logger.getLogger(UserListController.class.getName());

@Override
public void execute(HttpRequest request, HttpResponse response) throws IOException {
// Cookie에서 로그인 상태 확인
String logined = request.getCookie("logined");

if (logined != null) {
// 로그인된 사용자: user/list.html 파일 forward
log.log(Level.INFO, "Logged in user accessing user list");
response.forward(RequestPath.USER_LIST_HTML.getValue());
} else {
// 비로그인 상태: 로그인 페이지로 리다이렉트
log.log(Level.INFO, "Non-logged user redirected to login page");
response.redirect(RequestPath.USER_LOGIN_HTML.getValue());
}
}
}
51 changes: 51 additions & 0 deletions src/main/java/controller/UserLoginController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package controller;

import db.MemoryUserRepository;
import http.HttpRequest;
import http.HttpResponse;
import http.enums.RequestPath;
import model.User;
import model.UserField;

import java.io.IOException;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

public class UserLoginController implements Controller {
private static final Logger log = Logger.getLogger(UserLoginController.class.getName());

@Override
public void execute(HttpRequest request, HttpResponse response) throws IOException {
Map<String, String> params = request.getParameters();

String userId = params.get(UserField.USER_ID.getValue());
String password = params.get(UserField.PASSWORD.getValue());

if (userId != null && !userId.isEmpty()) {
log.log(Level.INFO, "Login attempt: " + userId);
}

// 파라미터 유효성 검사
if (userId == null || userId.isEmpty() || password == null || password.isEmpty()) {
log.log(Level.WARNING, "Login failed: missing parameters");
Comment on lines +29 to +31
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

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

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

response.redirect(RequestPath.USER_LOGIN_FAILED.getValue());
return;
}

// MemoryUserRepository에서 사용자 조회
MemoryUserRepository repository = MemoryUserRepository.getInstance();
User user = repository.findUserById(userId);

// 인증 검증
if (user != null && user.getPassword().equals(password)) {
// 로그인 성공: Cookie 설정 + 메인페이지로 리다이렉트
log.log(Level.INFO, "Login successful: " + userId);
response.redirectWithCookie(RequestPath.INDEX.getValue(), "logined=true");
} else {
// 로그인 실패: 에러페이지로 리다이렉트
log.log(Level.WARNING, "Login failed: " + userId);
response.redirect(RequestPath.USER_LOGIN_FAILED.getValue());
}
}
}
50 changes: 50 additions & 0 deletions src/main/java/controller/UserSignupController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package controller;

import db.MemoryUserRepository;
import http.HttpRequest;
import http.HttpResponse;
import http.enums.HttpMethod;
import http.enums.RequestPath;
import model.User;
import model.UserField;

import java.io.IOException;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

public class UserSignupController implements Controller {
private static final Logger log = Logger.getLogger(UserSignupController.class.getName());

@Override
public void execute(HttpRequest request, HttpResponse response) throws IOException {
// 파라미터 추출
Map<String, String> params = request.getParameters();
log.log(Level.INFO, "POST Signup params: " + params);

// 필수 필드 검증
String userId = params.get (UserField.USER_ID.getValue());
String password = params.get (UserField. PASSWORD.getValue());
String name = params.get (UserField.NAME.getValue());
String email = params.get (UserField.EMAIL.getValue());

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;
}
Comment on lines +31 to +37

Choose a reason for hiding this comment

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

Bad Request에 대해 TODO로 남아있는데 어떻게 처리되는걸까요??
+) if문안에 Valid 처리가 많은데, 별도의 메서드나 클래스 분리는 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

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

이 부분은 이전 파트장님의 코멘트를 통해 확인할 수 있습니다..

  • 귀찮음 이슈로 하지 않은 레전드 상황발생

Choose a reason for hiding this comment

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

이전 코멘트의 성근님과 파트장님의 의견에도 동의합니다만! 서버가 누군가(?)의 요청을 믿어야 하는 상황이 만들어지는거 같아요!

정확한 문서화를 바탕으로 한 개발을 진행하거나 특별한 사유(유저 플로우 등)에 의해서 유효성 검사 위치를 정하는게 아니라면?
서버 입장에서 들어오는 요청은 블랙박스로 취급하는 것이 더 적절하지 않을까 하는 개인적인 생각입니다~😁


// User 객체 생성
User newUser = new User(userId, password, name, email);

// 메모리 저장소에 저장
MemoryUserRepository repository = MemoryUserRepository.getInstance();
repository.addUser(newUser);
log.log(Level.INFO, "New User Registered: " + newUser.getUserId());

// 302 리다이렉트로 메인 페이지로 이동
response.redirect(RequestPath.INDEX.getValue());
}
}
56 changes: 56 additions & 0 deletions src/main/java/http/HttpHeaders.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package http;

import http.enums.HttpHeader;

import java.io.BufferedReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class HttpHeaders {
private final Map<String, String> headers;

public HttpHeaders(Map<String, String> headers) {
this.headers = new HashMap<>(headers);
}

public static HttpHeaders from(BufferedReader br) throws IOException {
Map<String, String> headerMap = new HashMap<>();

String line;
while ((line = br.readLine()) != null && !line.isEmpty()) {
int colonIndex = line.indexOf(":");
if (colonIndex != -1) {
String key = line.substring(0, colonIndex).trim().toLowerCase();
String value = line.substring(colonIndex + 1).trim();
headerMap.put(key, value);
}
}

return new HttpHeaders(headerMap);
}

public String getHeader(HttpHeader header) {
return headers.get(header.getValue().toLowerCase());
}

public String getHeader(String headerName) {
return headers.get(headerName.toLowerCase());
}

public int getContentLength() {
String contentLength = getHeader(HttpHeader.CONTENT_LENGTH);
if (contentLength == null) {
return 0;
}
try {
return Integer.parseInt(contentLength);
} catch (NumberFormatException e) {
return 0;
}
}

public String getCookie() {
return getHeader(HttpHeader.COOKIE);
}
}
107 changes: 107 additions & 0 deletions src/main/java/http/HttpRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package http;

import http.enums.HttpMethod;
import http.util.HttpRequestUtils;
import http.util.IOUtils;

import java.io.BufferedReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class HttpRequest {
private final HttpStartLine startLine;
private final HttpHeaders headers;
private final String body;

private HttpRequest(HttpStartLine startLine, HttpHeaders headers, String body) {
this.startLine = startLine;
this.headers = headers;
this.body = body;
}

public static HttpRequest from(BufferedReader br) throws IOException {
String requestLine = br.readLine();
if (requestLine == null || requestLine.isEmpty()) {
throw new IllegalArgumentException("Request line cannot be null or empty");

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.

이부분 핸들링은 requestHandler에서 이뤄집니당

}

HttpStartLine startLine = HttpStartLine.from(requestLine);
HttpHeaders headers = HttpHeaders.from(br);

String body = null;
if (startLine.getMethod() == HttpMethod.POST && headers.getContentLength() > 0) {
body = IOUtils.readData(br, headers.getContentLength());
}

return new HttpRequest(startLine, headers, body);
}

public HttpMethod getMethod() {
return startLine.getMethod();
}

public String getUrl() {
return startLine.getPath();
}

public String getPath() {
return startLine.getPathWithoutQuery();
}

public String getQueryString() {
return startLine.getQueryString();
}

public String getVersion() {
return startLine.getVersion();
}

public String getHeader(String headerName) {
return headers.getHeader(headerName);
}

public String getCookie() {
return headers.getCookie();
}

public String getCookie(String key) {
String cookieHeader = headers.getCookie();
if (cookieHeader == null) {
return null;
}

for (String pair : cookieHeader.split(";\\s*")) {
String[] kv = pair.split("=", 2);
if (kv.length == 2 && kv[0].equals(key)) {
return kv[1];
}
}
return null;
}

public int getContentLength() {
return headers.getContentLength();
}

public String getBody() {
return body;
}

public HttpStartLine getStartLine() {
return startLine;
}

public HttpHeaders getHeaders() {
return headers;
}

public Map<String, String> getParameters() {
if (startLine.getMethod() == HttpMethod.GET && getQueryString() != null) {
return HttpRequestUtils.parseQueryParameter(getQueryString());
} else if (startLine.getMethod() == HttpMethod.POST && body != null) {
return HttpRequestUtils.parseQueryParameter(body);
}
return new HashMap<>();
}
}
Loading