Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ dependencies {
implementation 'ch.qos.logback:logback-classic:1.2.3'
testImplementation 'org.assertj:assertj-core:3.16.1'

runtimeOnly 'com.h2database:h2:2.2.224'

testImplementation 'org.mockito:mockito-core:4.11.0'
testImplementation('org.mockito:mockito-junit-jupiter:4.11.0') {
exclude group: 'org.junit.jupiter'
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/app/db/Database.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class Database {

public static void addUser(User user) {
long id = sequentialId.getAndIncrement();
user.setUserId(id);
user.setId(id);
users.put(id, user);
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/app/handler/LoginWithPost.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public HandlerResponse handle(QueryParameters params) {
}

SessionEntity session = sessionManager.create(
user.getUserId(),
user.getId(),
user.getUserRole(),
user.getNickname());

Expand Down
4 changes: 2 additions & 2 deletions src/main/java/app/handler/RegisterWithGet.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import web.filter.authentication.UserRole;
import web.handler.SingleArgHandler;
import web.response.HandlerResponse;
import web.response.StaticViewResponse;
import web.response.RedirectResponse;

public class RegisterWithGet extends SingleArgHandler<QueryParameters> {
private static final Logger log = LoggerFactory.getLogger(RegisterWithGet.class);
Expand All @@ -28,6 +28,6 @@ public HandlerResponse handle(QueryParameters params) {
String nickname = params.getQueryValue("nickname").orElseThrow(()-> new ServiceException(ErrorCode.MISSING_REGISTER_TOKEN, "nickname required"));
Database.addUser(new User(password, nickname, email, UserRole.MEMBER.toString()));
log.info("Registered - password:{}, nickname:{}, email:{}", password, nickname, email);
return StaticViewResponse.of("/login");
return RedirectResponse.to("/login");
}
}
12 changes: 6 additions & 6 deletions src/main/java/app/model/User.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package app.model;

public class User {
private Long userId;
private Long id;
private String password;
private String nickname;
private String email;
Expand All @@ -14,12 +14,12 @@ public User(String password, String nickname, String email, String userRole) {
this.userRole = userRole;
}

public Long getUserId() {
return userId;
public Long getId() {
return id;
}

public void setUserId(Long userId){
this.userId = userId;
public void setId(Long id){
this.id = id;
}

public String getPassword() {
Expand All @@ -40,6 +40,6 @@ public String getUserRole() {

@Override
public String toString() {
return "User [userId=" + userId + ", password=" + password + ", name=" + nickname + ", email=" + email + "]";
return "User [userId=" + id + ", password=" + password + ", name=" + nickname + ", email=" + email + "]";
}
}
3 changes: 3 additions & 0 deletions src/main/java/bootstrap/WebServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.util.concurrent.Executors;

import config.AppConfig;
import config.DatabaseConfig;
import config.FilterConfig;
import config.SecurityConfig;
import org.slf4j.Logger;
Expand All @@ -18,6 +19,7 @@ public class WebServer {
private static final AppConfig LOADER = new AppConfig();
private static final SecurityConfig securityConfig = new SecurityConfig();
private static final FilterConfig filterConfig = new FilterConfig();
private static final DatabaseConfig databaseConfig = new DatabaseConfig();
private static final ExecutorService executor = Executors.newFixedThreadPool(32);

public static void main(String args[]) throws Exception {
Expand Down Expand Up @@ -53,5 +55,6 @@ public static void main(String args[]) throws Exception {
private static void config(){
securityConfig.config();
filterConfig.config();
databaseConfig.config();
}
}
20 changes: 20 additions & 0 deletions src/main/java/config/AppConfig.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package config;

import app.handler.*;
import database.ConnectionManager;
import database.H2DbManager;
import exception.ExceptionHandlerMapping;
import exception.handler.ErrorExceptionHandler;
import exception.handler.ServiceExceptionHandler;
Expand Down Expand Up @@ -94,6 +96,7 @@ public List<WebHandler> webHandlerList() {
);
}

// ===== Handler =====
public StaticContentHandler staticContentHandler() {
return getOrCreate(
"staticContentHandler",
Expand Down Expand Up @@ -298,4 +301,21 @@ public SessionStorage sessionStorage() {
return getOrCreate("sessionStorage",
SessionStorage::new);
}

/**
* ===== DB =====
*/
public ConnectionManager connectionManager(){
return h2DbManager();
}

public H2DbManager h2DbManager(){
return getOrCreate(H2DbManager.class.getSimpleName(), H2DbManager::new);
}

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

28 changes: 28 additions & 0 deletions src/main/java/config/DatabaseConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package config;

import app.model.User;
import java.util.List;

public class DatabaseConfig {
private final AppConfig appConfig = new AppConfig();

Choose a reason for hiding this comment

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

AppConfig 불필요한 인스턴스화: DatabaseConfig에서 new AppConfig()를 생성하면 AppConfig의 싱글톤 인스턴스들이 다시 만들어질 가능성이 있습니다. WebServer에서 이미 만든 AppConfig 인스턴스를 주입받도록 리팩토링하세요.


public static final List<Class<?>> ENTITY_CLASSES = List.of(
User.class
);

public static final List<String> RESOLVED_WORD = List.of(
"user"
);

Choose a reason for hiding this comment

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

하드코딩된 환경 설정: 데이터베이스 URL, 사용자명이 소스 코드에 하드코딩되어 있습니다. 운영 환경마다 다른 DB 주소를 사용해야 하므로 환경 변수나 설정 파일에서 읽도록 변경하세요. 특히 파일 시스템 절대 경로(/Users/apple/h2/testdb)는 개발 환경에만 동작합니다.

public static final String H2_DB_URL = "jdbc:h2:tcp://localhost//Users/apple/h2/testdb";
public static final String H2_DB_USER = "sa";
public static final String H2_DB_PASSWORD = "";

public static final boolean CREATE_TABLES = false;
public static final boolean DROP_IF_EXISTS = false;

public void config(){
DdlGenerator ddlGenerator = appConfig.ddlGenerator();
if(CREATE_TABLES) ddlGenerator.generateTables();
}
}
111 changes: 111 additions & 0 deletions src/main/java/config/DdlGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package config;

import database.ConnectionManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.LocalDateTime;
import java.util.List;

public class DdlGenerator {
private static final Logger log = LoggerFactory.getLogger(DdlGenerator.class);
private final List<String> resolvedWord = DatabaseConfig.RESOLVED_WORD;
private final ConnectionManager connectionManager;
private final boolean dropIfExists = DatabaseConfig.DROP_IF_EXISTS;
private final List<Class<?>> entityClasses = DatabaseConfig.ENTITY_CLASSES;

public DdlGenerator(ConnectionManager connectionManager) {
this.connectionManager = connectionManager;
}

public void generateTables() {
try (Connection conn = connectionManager.getConnection();
Statement stmt = conn.createStatement()) {

for (Class<?> entityClass : entityClasses) {
String tableName = toTableName(entityClass);
String ddl = buildDdlForEntity(entityClass, tableName);
log.info("DDL for {}:\n{}", tableName, ddl);
stmt.execute(ddl);
}

Comment on lines +31 to +36

Choose a reason for hiding this comment

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

SQL 인젝션 취약점: tableName이 사용자 입력에서 영향을 받을 수 있는 경우 위험합니다. 현재는 고정된 ENTITY_CLASSES에서만 가져오므로 괜찮지만, 향후 동적 테이블명 지원 시 주의가 필요합니다. 또한 stmt.execute(ddl)은 여러 SQL문을 한 번에 실행할 수 있어 DDL이 예상과 다르게 파싱될 수 있습니다.

} catch (SQLException e) {
throw new RuntimeException("DDL 생성/실행 중 오류", e);
}
Comment on lines +37 to +39

Choose a reason for hiding this comment

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

예외 처리 미흡: SQLException 발생 시 원본 예외 정보(원인, SQL 상태 코드 등)를 버리고 일반적인 RuntimeException으로 변환합니다. 디버깅을 어렵게 만들므로 throw new RuntimeException("DDL 생성/실행 중 오류", e);를 유지하되, 더 자세한 context 정보(현재 테이블명, 시도한 DDL 문)를 포함하면 좋습니다.

}

private String buildDdlForEntity(Class<?> entityClass, String tableName) {
StringBuilder ddl = new StringBuilder();

if (dropIfExists) {
ddl.append("DROP TABLE IF EXISTS ").append(tableName).append(";\n");
}

ddl.append("CREATE TABLE ").append(tableName).append(" (\n");

Field[] fields = entityClass.getDeclaredFields();

boolean firstColumn = true;

for (Field field : fields) {
if (Modifier.isStatic(field.getModifiers())) {
continue;
}

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

if (!firstColumn) {
ddl.append(",\n");
}
firstColumn = false;

if ("id".equals(columnName) && (fieldType == Long.class || fieldType == long.class)) {
ddl.append(" id BIGINT AUTO_INCREMENT PRIMARY KEY");
} else {
String sqlType = toSqlType(fieldType);
ddl.append(" ").append(columnName).append(" ").append(sqlType);
}
}

ddl.append("\n);");

return ddl.toString();
}

private String toSqlType(Class<?> fieldType) {
if (fieldType == Long.class || fieldType == long.class) {
return "BIGINT";
} else if (fieldType == Integer.class || fieldType == int.class) {
return "INT";
} else if (fieldType == String.class) {
return "VARCHAR(255)";
} else if (fieldType == Boolean.class || fieldType == boolean.class) {
return "BOOLEAN";
} else if (fieldType == LocalDateTime.class ||
fieldType == java.util.Date.class ||
fieldType == java.sql.Timestamp.class) {
return "TIMESTAMP";
} else if (fieldType == Double.class || fieldType == double.class) {
return "DOUBLE";
} else if (fieldType == Float.class || fieldType == float.class) {
return "FLOAT";
}
return "VARCHAR(255)";
}

private String toTableName(Class<?> clazz) {
String name = clazz.getSimpleName().toLowerCase();

for(String word : resolvedWord){
if(word.equals(name))
return "`" + word + "`";
}
return name;
}
}
7 changes: 7 additions & 0 deletions src/main/java/database/ConnectionManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package database;

import java.sql.Connection;

public interface ConnectionManager {
Connection getConnection();
}
Loading