diff --git a/build.gradle b/build.gradle index c7b2ba3f1..4e5ee18de 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/src/main/java/app/db/Database.java b/src/main/java/app/db/Database.java index 8a95d06ac..333135d8d 100644 --- a/src/main/java/app/db/Database.java +++ b/src/main/java/app/db/Database.java @@ -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); } diff --git a/src/main/java/app/handler/LoginWithPost.java b/src/main/java/app/handler/LoginWithPost.java index 6f64f319d..987ad8160 100644 --- a/src/main/java/app/handler/LoginWithPost.java +++ b/src/main/java/app/handler/LoginWithPost.java @@ -38,7 +38,7 @@ public HandlerResponse handle(QueryParameters params) { } SessionEntity session = sessionManager.create( - user.getUserId(), + user.getId(), user.getUserRole(), user.getNickname()); diff --git a/src/main/java/app/handler/RegisterWithGet.java b/src/main/java/app/handler/RegisterWithGet.java index 2d9ab46f7..fa0740b57 100644 --- a/src/main/java/app/handler/RegisterWithGet.java +++ b/src/main/java/app/handler/RegisterWithGet.java @@ -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 { private static final Logger log = LoggerFactory.getLogger(RegisterWithGet.class); @@ -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"); } } diff --git a/src/main/java/app/model/User.java b/src/main/java/app/model/User.java index 550178337..936b915e6 100644 --- a/src/main/java/app/model/User.java +++ b/src/main/java/app/model/User.java @@ -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; @@ -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() { @@ -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 + "]"; } } diff --git a/src/main/java/bootstrap/WebServer.java b/src/main/java/bootstrap/WebServer.java index 95f524e1f..51c7811d1 100644 --- a/src/main/java/bootstrap/WebServer.java +++ b/src/main/java/bootstrap/WebServer.java @@ -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; @@ -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 { @@ -53,5 +55,6 @@ public static void main(String args[]) throws Exception { private static void config(){ securityConfig.config(); filterConfig.config(); + databaseConfig.config(); } } diff --git a/src/main/java/config/AppConfig.java b/src/main/java/config/AppConfig.java index 64c8413d1..7944b2cbf 100644 --- a/src/main/java/config/AppConfig.java +++ b/src/main/java/config/AppConfig.java @@ -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; @@ -94,6 +96,7 @@ public List webHandlerList() { ); } + // ===== Handler ===== public StaticContentHandler staticContentHandler() { return getOrCreate( "staticContentHandler", @@ -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())); + } } + diff --git a/src/main/java/config/DatabaseConfig.java b/src/main/java/config/DatabaseConfig.java new file mode 100644 index 000000000..70e72a47c --- /dev/null +++ b/src/main/java/config/DatabaseConfig.java @@ -0,0 +1,28 @@ +package config; + +import app.model.User; +import java.util.List; + +public class DatabaseConfig { + private final AppConfig appConfig = new AppConfig(); + + public static final List> ENTITY_CLASSES = List.of( + User.class + ); + + public static final List RESOLVED_WORD = List.of( + "user" + ); + + 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(); + } +} diff --git a/src/main/java/config/DdlGenerator.java b/src/main/java/config/DdlGenerator.java new file mode 100644 index 000000000..3dfb27dbf --- /dev/null +++ b/src/main/java/config/DdlGenerator.java @@ -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 resolvedWord = DatabaseConfig.RESOLVED_WORD; + private final ConnectionManager connectionManager; + private final boolean dropIfExists = DatabaseConfig.DROP_IF_EXISTS; + private final List> 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); + } + + } catch (SQLException e) { + throw new RuntimeException("DDL 생성/실행 중 오류", e); + } + } + + 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; + } +} diff --git a/src/main/java/database/ConnectionManager.java b/src/main/java/database/ConnectionManager.java new file mode 100644 index 000000000..1b1b06278 --- /dev/null +++ b/src/main/java/database/ConnectionManager.java @@ -0,0 +1,7 @@ +package database; + +import java.sql.Connection; + +public interface ConnectionManager { + Connection getConnection(); +} diff --git a/src/main/java/database/CrudRepository.java b/src/main/java/database/CrudRepository.java new file mode 100644 index 000000000..55c67cfe4 --- /dev/null +++ b/src/main/java/database/CrudRepository.java @@ -0,0 +1,314 @@ +package database; + +import config.DatabaseConfig; +import exception.ErrorException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.sql.*; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class CrudRepository { + private static final Logger log = LoggerFactory.getLogger(CrudRepository.class); + private final List resolvedWord = DatabaseConfig.RESOLVED_WORD; + private final ConnectionManager connectionManager; + private final Class type; + private final String tableName; + + public CrudRepository(ConnectionManager connectionManager, Class type) { + this.connectionManager = connectionManager; + this.type = type; + this.tableName = toTableName(type); + } + + // ========== CREATE ========== + public T save(T entity) { + try (Connection conn = connectionManager.getConnection()) { + + Field[] allFields = type.getDeclaredFields(); + + StringBuilder sqlBuilder = new StringBuilder(); + StringBuilder placeholder = new StringBuilder(); + + sqlBuilder.append("INSERT INTO ").append(tableName).append(" ("); + + List insertFields = new ArrayList<>(); + + for (Field field : allFields) { + if (Modifier.isStatic(field.getModifiers())) { + continue; + } + if ("id".equals(field.getName())) { + continue; + } + sqlBuilder.append(field.getName()).append(", "); + placeholder.append("?, "); + insertFields.add(field); + } + + if (!insertFields.isEmpty()) { + sqlBuilder.setLength(sqlBuilder.length() - 2); + placeholder.setLength(placeholder.length() - 2); + } + + sqlBuilder.append(") VALUES (").append(placeholder).append(")"); + + String sql = sqlBuilder.toString(); + + try (PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + + int index = 1; + for (Field field : insertFields) { + field.setAccessible(true); + Object value = field.get(entity); + setParameter(pstmt, index++, value); + } + + pstmt.executeUpdate(); + + try (ResultSet resultSet = pstmt.getGeneratedKeys()) { + if (resultSet.next()) { + long generatedId = resultSet.getLong(1); + setId(entity, generatedId); + } + } + } + + log.info("{} created: {}", tableName, sql); + return entity; + } catch (SQLException | IllegalAccessException e) { + throw new ErrorException("엔티티 저장 중 오류", e); + } + } + + // ========== READ ========== + public Optional findById(Long id) { + String sql = "SELECT * FROM " + tableName + " WHERE id = ?"; + + try (Connection conn = connectionManager.getConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql)) { + + pstmt.setLong(1, id); + + try (ResultSet resultSet = pstmt.executeQuery()) { + if (resultSet.next()) { + log.info("{} queried: {}", tableName, sql); + return Optional.of(mapRow(resultSet)); + } + return Optional.empty(); + } + + } catch (SQLException e) { + throw new ErrorException("엔티티 조회 중 오류 (id=" + id + ")", e); + } + } + + public List findAll() { + String sql = "SELECT * FROM " + tableName; + + try (Connection conn = connectionManager.getConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql); + ResultSet resultSet = pstmt.executeQuery()) { + + List result = new ArrayList<>(); + while (resultSet.next()) { + result.add(mapRow(resultSet)); + } + log.info("{} queried({} results): {}", tableName, result.size(), sql); + return result; + + } catch (SQLException e) { + throw new ErrorException("엔티티 전체 조회 중 오류", e); + } + } + + public List findByColumn(String columnName, Object value) { + String sql = "SELECT * FROM " + tableName + " WHERE " + columnName + " = ?"; + + try (Connection conn = connectionManager.getConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql)) { + + setParameter(pstmt, 1, value); + + try (ResultSet resultSet = pstmt.executeQuery()) { + List result = new ArrayList<>(); + while (resultSet.next()) { + result.add(mapRow(resultSet)); + } + log.info("{} queried({} results): {}", tableName, result.size(), sql); + return result; + } + + } catch (SQLException e) { + throw new ErrorException("엔티티 조회 중 오류 (column=" + columnName + ", value=" + value + ")", e); + } + } + + // ========== UPDATE ========== + public void update(T entity) { + Long id = getId(entity); + if (id == null) { + throw new ErrorException("id가 null인 엔티티는 업데이트할 수 없습니다."); + } + + Field[] allFields = type.getDeclaredFields(); + + StringBuilder sql = new StringBuilder(); + sql.append("UPDATE ").append(tableName).append(" SET "); + + List updateFields = new ArrayList<>(); + + for (Field field : allFields) { + if (Modifier.isStatic(field.getModifiers())) { + continue; + } + if ("id".equals(field.getName())) { + continue; + } + sql.append(field.getName()).append(" = ?, "); + updateFields.add(field); + } + + if (!updateFields.isEmpty()) { + sql.setLength(sql.length() - 2); + } + + sql.append(" WHERE id = ?"); + + String finalSql = sql.toString(); + + try (Connection conn = connectionManager.getConnection(); + PreparedStatement pstmt = conn.prepareStatement(finalSql)) { + + int index = 1; + + for (Field field : updateFields) { + field.setAccessible(true); + Object value = field.get(entity); + setParameter(pstmt, index++, value); + } + + pstmt.setLong(index, id); + + pstmt.executeUpdate(); + + log.info("{} updated: {}", tableName, sql); + } catch (SQLException | IllegalAccessException e) { + throw new ErrorException("엔티티 업데이트 중 오류", e); + } + } + + // ========== private methods ========== + private void setParameter(PreparedStatement pstmt, int index, Object value) throws SQLException { + if (value == null) { + pstmt.setObject(index, null); + return; + } + + if (value instanceof Long) { + pstmt.setLong(index, (Long) value); + } else if (value instanceof Integer) { + pstmt.setInt(index, (Integer) value); + } else if (value instanceof String) { + pstmt.setString(index, (String) value); + } else if (value instanceof Boolean) { + pstmt.setBoolean(index, (Boolean) value); + } else if (value instanceof LocalDateTime) { + pstmt.setTimestamp(index, Timestamp.valueOf((LocalDateTime) value)); + } else if (value instanceof java.util.Date) { + pstmt.setTimestamp(index, new Timestamp(((java.util.Date) value).getTime())); + } else { + pstmt.setObject(index, value); + } + } + + private void setId(T entity, Long id) { + try { + Field idField = type.getDeclaredField("id"); + idField.setAccessible(true); + idField.set(entity, id); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new ErrorException("id 필드 설정 중 오류", e); + } + } + + private Long getId(T entity) { + try { + Field idField = type.getDeclaredField("id"); + idField.setAccessible(true); + Object value = idField.get(entity); + if (value == null) { + return null; + } + if (value instanceof Long) { + return (Long) value; + } + throw new ErrorException("id 필드 타입이 Long이 아닙니다: " + value.getClass()); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new ErrorException("id 필드 조회 중 오류", e); + } + } + + private T mapRow(ResultSet resultSet) { + try { + T instance = type.getDeclaredConstructor().newInstance(); + + Field[] fields = type.getDeclaredFields(); + for (Field field : fields) { + if (Modifier.isStatic(field.getModifiers())) { + continue; + } + + String columnName = field.getName(); + field.setAccessible(true); + + Class fieldType = field.getType(); + + Object value; + + if (fieldType == Long.class || fieldType == long.class) { + long v = resultSet.getLong(columnName); + value = resultSet.wasNull() ? null : v; + } else if (fieldType == Integer.class || fieldType == int.class) { + int v = resultSet.getInt(columnName); + value = resultSet.wasNull() ? null : v; + } else if (fieldType == String.class) { + value = resultSet.getString(columnName); + } else if (fieldType == Boolean.class || fieldType == boolean.class) { + boolean v = resultSet.getBoolean(columnName); + value = resultSet.wasNull() ? null : v; + } else if (fieldType == LocalDateTime.class) { + Timestamp ts = resultSet.getTimestamp(columnName); + value = (ts != null) ? ts.toLocalDateTime() : null; + } else if (fieldType == java.util.Date.class) { + Timestamp ts = resultSet.getTimestamp(columnName); + value = (ts != null) ? new java.util.Date(ts.getTime()) : null; + } else { + value = resultSet.getObject(columnName); + } + + field.set(instance, value); + } + + return instance; + + } catch (Exception e) { + throw new ErrorException("ResultSet → 엔티티 매핑 중 오류", e); + } + } + + private String toTableName(Class clazz) { + String name = clazz.getSimpleName().toLowerCase(); + + for(String word : resolvedWord){ + if(word.equals(name)) + return "`" + word + "`"; + } + return name; + } +} diff --git a/src/main/java/database/H2DbManager.java b/src/main/java/database/H2DbManager.java new file mode 100644 index 000000000..03f74d4e3 --- /dev/null +++ b/src/main/java/database/H2DbManager.java @@ -0,0 +1,28 @@ +package database; + +import config.DatabaseConfig; +import exception.ErrorException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public class H2DbManager implements ConnectionManager{ + private static final String DB_URL = DatabaseConfig.H2_DB_URL; + private static final String DB_USER = DatabaseConfig.H2_DB_USER; + private static final String DB_PASSWORD = DatabaseConfig.H2_DB_PASSWORD; + private static final Logger log = LoggerFactory.getLogger(H2DbManager.class); + + @Override + public Connection getConnection() { + try { + return DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD); + } catch (SQLException e){ + log.error(e.fillInStackTrace().toString()); + log.error(e.getSQLState()); + throw new ErrorException("DB error"); + } + } +} diff --git a/src/test/java/web/dispatch/adapter/SingleArgHandlerAdapterTest.java b/src/test/java/web/dispatch/adapter/SingleArgHandlerAdapterTest.java index 56737345e..e0b0536e2 100644 --- a/src/test/java/web/dispatch/adapter/SingleArgHandlerAdapterTest.java +++ b/src/test/java/web/dispatch/adapter/SingleArgHandlerAdapterTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import web.handler.WebHandler; +import web.response.RedirectResponse; import web.response.StaticViewResponse; import web.response.HandlerResponse; @@ -25,7 +26,7 @@ void test(){ HandlerResponse response = adapter.handle(request, handler); assertThat(response).isNotNull(); - assertThat(response).isInstanceOf(StaticViewResponse.class); + assertThat(response).isInstanceOf(RedirectResponse.class); } } \ No newline at end of file