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
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