Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions src/main/java/app/db/UserRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package app.db;

import app.model.User;
import database.ConnectionManager;
import database.CrudRepository;

public class UserRepository extends CrudRepository<User> {
public UserRepository(ConnectionManager connectionManager) {
super(connectionManager, User.class);
}
}
54 changes: 47 additions & 7 deletions src/main/java/app/handler/RegisterWithPost.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package app.handler;

import app.db.Database;
import app.db.UserRepository;
import app.model.User;
import config.VariableConfig;
import exception.ErrorCode;
import exception.ServiceException;
import http.HttpMethod;
Expand All @@ -14,19 +15,58 @@
import web.response.RedirectResponse;

public class RegisterWithPost extends SingleArgHandler<QueryParameters> {
private static final String EMAIL = "email";
private static final String NICKNAME = "nickname";
private static final String PASSWORD = "password";

private static final Logger log = LoggerFactory.getLogger(RegisterWithPost.class);

public RegisterWithPost() {
private final UserRepository userRepository;

public RegisterWithPost(UserRepository userRepository) {
super(HttpMethod.POST, "/user/create");
this.userRepository = userRepository;
}

@Override
public HandlerResponse handle(QueryParameters params) {
String email = params.getQueryValue("email").orElseThrow(()-> new ServiceException(ErrorCode.MISSING_REGISTER_TOKEN, "email required"));
String nickname = params.getQueryValue("nickname").orElseThrow(()-> new ServiceException(ErrorCode.MISSING_REGISTER_TOKEN, "nickname required"));
String password = params.getQueryValue("password").orElseThrow(()-> new ServiceException(ErrorCode.MISSING_REGISTER_TOKEN, "password required"));
Database.addUser(new User(password, nickname, email, UserRole.MEMBER.toString()));
log.info("Registered - password:{}, nickname:{}, email:{}", password, nickname, email);
String email = getRequired(params, EMAIL);
String nickname = getRequired(params, NICKNAME);
String password = getRequired(params, PASSWORD);

validate(email, nickname, password);

User saved = userRepository.save(
new User(password, nickname, email, UserRole.MEMBER.toString()));

log.info("Registered id:{}, email:{}, nickname:{}, password:{}",
saved.getId(), email, nickname, password);
return RedirectResponse.to("/login");
}

private void validate(String email, String nickname, String password) {
validateDuplicate(email, nickname);
validateLength(email, VariableConfig.EMAIL_MIN, VariableConfig.EMAIL_MAX, ErrorCode.EMAIL_LENGTH_INVALID);
validateLength(nickname, VariableConfig.NICKNAME_MIN, VariableConfig.NICKNAME_MAX, ErrorCode.NICKNAME_LENGTH_INVALID);
validateLength(password, VariableConfig.PASSWORD_MIN, VariableConfig.PASSWORD_MAX, ErrorCode.PASSWORD_LENGTH_INVALID);
}

private String getRequired(QueryParameters params, String key) {
return params.getQueryValue(key)
.orElseThrow(() -> new ServiceException(ErrorCode.MISSING_REGISTER_TOKEN, key + " required"));
}
Comment on lines +55 to +57

Choose a reason for hiding this comment

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

중복검사 성능 문제: findByColumn()을 각각 호출할 때마다 데이터베이스 쿼리가 2번 발생합니다. 비용이 큰 작업이므로 검사 순서를 조정하거나 길이 검증을 먼저 수행한 후 중복 검사를 하는 것이 좋습니다(유효하지 않은 데이터로 불필요한 DB 쿼리 방지).

또한 findByColumn(EMAIL, email) 호출 시 문자열 리터럴 "email"을 사용하는데, 이는 EMAIL 상수와 불일치할 위험이 있습니다. 상수 값이 실제 DB 컬럼명과 정확히 일치하는지 확인이 필요합니다.


private void validateLength(String value, int min, int max, ErrorCode code) {
int len = value.length();
if (len < min || len > max)
throw new ServiceException(code);
}

private void validateDuplicate(String email, String nickname) {
if (!userRepository.findByColumn(EMAIL, email).isEmpty())
throw new ServiceException(ErrorCode.EMAIL_ALREADY_EXISTS);

if (!userRepository.findByColumn(NICKNAME, nickname).isEmpty())
throw new ServiceException(ErrorCode.NICKNAME_ALREADY_EXISTS);
}
}
3 changes: 3 additions & 0 deletions src/main/java/app/model/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ public User(String password, String nickname, String email, String userRole) {
this.userRole = userRole;
}

public User() {
}
Comment on lines +17 to +18

Choose a reason for hiding this comment

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

ORM 패턴 일관성: 기본 생성자를 추가했는데, 이는 보통 ORM(Hibernate 등)의 리플렉션 기반 인스턴스화를 위해 필요합니다. 다른 엔티티도 동일하게 기본 생성자를 가지고 있는지 확인하세요. 불완전한 상태로 객체가 생성되지 않도록 접근 제어(private 또는 package-private)를 고려하세요.


public Long getId() {
return id;
}
Expand Down
22 changes: 12 additions & 10 deletions src/main/java/config/AppConfig.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package config;

import app.db.UserRepository;
import app.handler.*;
import database.ConnectionManager;
import database.H2DbManager;
Expand Down Expand Up @@ -120,8 +121,7 @@ public RegisterWithGet registerWithGet() {
public RegisterWithPost registerWithPost() {
return getOrCreate(
"registerWithPost",
RegisterWithPost::new
);
() -> new RegisterWithPost(userRepository()));
}

public LoginWithPost loginWithPost() {
Expand Down Expand Up @@ -233,8 +233,8 @@ public QueryParamsResolver queryParamsResolver() {
}

public MultipartFormResolver multipartFormResolver(){
return getOrCreate("multipartFormResolver", () ->
new MultipartFormResolver(multipartFormParser()));
return getOrCreate("multipartFormResolver",
() -> new MultipartFormResolver(multipartFormParser()));
}

public MultipartFormParser multipartFormParser(){
Expand All @@ -250,10 +250,7 @@ public ExceptionHandlerMapping exceptionHandlerMapping() {
List.of(
serviceExceptionHandler(),
errorExceptionHandler(),
unhandledErrorHandler()
)
)
);
unhandledErrorHandler())));
}

public ServiceExceptionHandler serviceExceptionHandler() {
Expand Down Expand Up @@ -325,8 +322,13 @@ public H2DbManager h2DbManager(){
}

public DdlGenerator ddlGenerator(){
return getOrCreate(DdlGenerator.class.getSimpleName(), () ->
new DdlGenerator(connectionManager()));
return getOrCreate(DdlGenerator.class.getSimpleName(),
() -> new DdlGenerator(connectionManager()));
}

public UserRepository userRepository(){
return getOrCreate(UserRepository.class.getSimpleName(),
()-> new UserRepository(connectionManager()));
}
}

7 changes: 6 additions & 1 deletion src/main/java/config/DdlGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ private String buildDdlForEntity(Class<?> entityClass, String tableName) {
continue;
}

String columnName = field.getName();
String columnName = toColumnName(field.getName());
Class<?> fieldType = field.getType();

if (!firstColumn) {
Expand Down Expand Up @@ -108,4 +108,9 @@ private String toTableName(Class<?> clazz) {
}
return name;
}

private String toColumnName(String str){
String snake = str.replaceAll("(?<!^)([A-Z])", "_$1");
return snake.toLowerCase();
}
}
7 changes: 7 additions & 0 deletions src/main/java/config/VariableConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,11 @@ public class VariableConfig {

public static final long IDLE_MS = 30*60*100;
public static final long ABSOLUTE_MS = 180*60*100;

public static final int EMAIL_MAX = 50;
public static final int EMAIL_MIN = 4;
public static final int NICKNAME_MAX = 12;
public static final int NICKNAME_MIN = 4;
public static final int PASSWORD_MAX = 16;
public static final int PASSWORD_MIN = 4;
}
Comment on lines +15 to 22

Choose a reason for hiding this comment

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

하드코딩된 검증 값: 길이 제한이 상수로 고정되어 있습니다. 향후 변경 시 코드 수정이 필요하고, 외부 설정으로 관리되지 않습니다. 가능하면 데이터베이스나 설정 파일에서 로드하는 방식을 고려하세요.

19 changes: 12 additions & 7 deletions src/main/java/database/CrudRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ public T save(T entity) {
if (Modifier.isStatic(field.getModifiers())) {
continue;
}
if ("id".equals(field.getName())) {
if ("id".equals(toColumnName(field.getName()))) {

Choose a reason for hiding this comment

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

논리 오류: 필드명 "id"를 snake_case로 변환한 결과와 비교하고 있지만, "id"는 변환해도 "id"이므로 실제로는 정상 동작합니다. 다만 명확성을 위해 변환 전 원본 필드명으로 비교하는 것이 낫습니다: if ("id".equals(field.getName())) 사용 권장.

continue;
}
sqlBuilder.append(field.getName()).append(", ");
sqlBuilder.append(toColumnName(field.getName())).append(", ");
placeholder.append("?, ");
insertFields.add(field);
}
Expand Down Expand Up @@ -128,7 +128,7 @@ public List<T> findAll() {
}

public List<T> findByColumn(String columnName, Object value) {
String sql = "SELECT * FROM " + tableName + " WHERE " + columnName + " = ?";
String sql = "SELECT * FROM " + tableName + " WHERE " + toColumnName(columnName) + " = ?";
Comment on lines 130 to +131

Choose a reason for hiding this comment

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

SQL 인젝션 위험: 사용자 입력인 columnName 파라미터를 toColumnName()을 거쳐서 SQL에 직접 삽입하고 있습니다. toColumnName() 검증만으로는 충분하지 않습니다. 특수 문자 필터링이나 허용 목록 검증(whitelist)을 추가하거나, 더 안전한 메커니즘을 사용하세요.


try (Connection conn = connectionManager.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
Expand All @@ -145,7 +145,7 @@ public List<T> findByColumn(String columnName, Object value) {
}

} catch (SQLException e) {
throw new ErrorException("엔티티 조회 중 오류 (column=" + columnName + ", value=" + value + ")", e);
throw new ErrorException("엔티티 조회 중 오류 (column=" + toColumnName(columnName) + ", value=" + value + ")", e);
}
}

Expand All @@ -167,10 +167,10 @@ public void update(T entity) {
if (Modifier.isStatic(field.getModifiers())) {
continue;
}
if ("id".equals(field.getName())) {
if ("id".equals(toColumnName(field.getName()))) {
continue;
}
sql.append(field.getName()).append(" = ?, ");
sql.append(toColumnName(field.getName())).append(" = ?, ");
updateFields.add(field);
}

Expand Down Expand Up @@ -264,7 +264,7 @@ private T mapRow(ResultSet resultSet) {
continue;
}

String columnName = field.getName();
String columnName = toColumnName(field.getName());
field.setAccessible(true);

Class<?> fieldType = field.getType();
Expand Down Expand Up @@ -311,4 +311,9 @@ private String toTableName(Class<?> clazz) {
}
return name;
}

private String toColumnName(String str){
String snake = str.replaceAll("(?<!^)([A-Z])", "_$1");
return snake.toLowerCase();
}
}
7 changes: 7 additions & 0 deletions src/main/java/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ public enum ErrorCode {
HttpStatus.BAD_REQUEST, "400_MISSING_REGISTER_TOKEN", "회원가입에 필요한 토큰이 누락되었습니다."),
LOGIN_FAILED(
HttpStatus.BAD_REQUEST, "400_LOGIN_FAILED", "아이디 혹은 비밀번호가 잘못되었습니다."),
EMAIL_LENGTH_INVALID(HttpStatus.BAD_REQUEST, "400_EMAIL_LENGTH_INVALID", "이메일은 4 ~ 50글자 사이여야합니다."),
NICKNAME_LENGTH_INVALID(HttpStatus.BAD_REQUEST, "400_NICKNAME_LENGTH_INVALID", "닉네임은 4 ~ 12글자 사이여야합니다."),
PASSWORD_LENGTH_INVALID(HttpStatus.BAD_REQUEST, "400_PASSWORD_LENGTH_INVALID", "비밀번호는 4 ~ 16글자 사이여야합니다."),


EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "409_EMAIL_ALREADY_EXISTS", "이미 가입된 Email입니다."),
NICKNAME_ALREADY_EXISTS(HttpStatus.CONFLICT, "409_NICKNAME_ALREADY_EXISTS", "이미 사용중인 닉네임입니다."),

/* Internal Error */
INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "500_INTERNAL", "서버 내부 오류가 발생했습니다."),
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/exception/handler/ErrorExceptionHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ public boolean support(Throwable e) {
@Override
public void handle(Throwable t, Socket connection) {
ErrorException error = (ErrorException) t;
logger.debug(error.getThrowable().toString());
logger.debug("{} - {}", error.getMessage(), error.getThrowable().toString());
ErrorCode errorCode = error.getErrorCode();
HttpStatus status = errorCode.getStatus();

Choose a reason for hiding this comment

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

응답 일관성 문제: ErrorCode.getMessage()로 변경했으나, 이전에 error.getMessage()에서 반환되던 값은 무엇인지 확인 필요합니다. 호출자가 원래 전달한 메시지를 의도했다면 정책 변경이 문제가 될 수 있습니다. 항상 ErrorCode 메시지만 반환할 것인지 명확히 정의하세요.

String body = toJson(errorCode.getCode(), error.getMessage());
String body = toJson(errorCode.getCode(), errorCode.getMessage());
byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8);

StringBuilder sb = new StringBuilder();
Expand Down
Loading