diff --git a/.gitignore b/.gitignore
index 63d2f1a..edaf5a0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,3 +34,4 @@ out/
### VS Code ###
.vscode/
+*.yml
diff --git a/README.md b/README.md
index 8ebd4ff..62642bb 100644
--- a/README.md
+++ b/README.md
@@ -57,19 +57,46 @@
사용자 정보를 바탕으로 로그인 기능을 구현한다.
-1. 로그인 기능을 구현한다.
-- 세션을 이용해 구현한다.
-- 세션은 애플리케이션 내부에 저장/관리한다.
- - 세션 유지 시간을 제한 한다.
- - [선택] 최근 로그인 기록과 아이피를 식별할 수 있도록 한다.
-
-2. 개인 정보 상세 조회 기능을 개발한다.
-- [선택] 이미지 업로드 기능을 구현한다.
+- [x] 로그인 기능을 구현한다.
+ - 세션을 이용해 구현한다.
+ - 세션은 애플리케이션 내부에 저장/관리한다.
+ - 세션 유지 시간을 제한 한다.
+ - [선택] 최근 로그인 기록과 아이피를 식별할 수 있도록 한다.
+
+- [x] 개인 정보 상세 조회 기능을 개발한다.
+
### 학습 목표
-- HTTP 특징에 대해 학습한다.
+
+1. HTTP 특징에 대해 학습한다.
- 쿠키/세션에 대해 학습한다.
- - 세션 관리 방법에 대해 학습한다.
+
+2. 세션 관리 방법에 대해 학습한다.
+
+
+
+
+
+
+
+
+## Step3. 데이터베이스를 교체한다.
+
+애플리케이션 내부에 저장하던 데이터를 외부 데이터베이스에 저장한다.
+
+1. 데이터베이스 종류는 자유롭게 선택 한다.
+ - RDB, Redis 등
+2. JDBC 템플릿을 구현한다.
+
+
+
+
+### 학습 목표
+
+1. 추상화에 대해 이해한다.
+2. 데이터베이스 통신 과정에 대해 이해한다.
+3. 각 데이터베이스의 특징에 대해 이해한다.
+4. 트랜잭션에 대해 학습한다.
diff --git a/app/README.md b/app/README.md
new file mode 100644
index 0000000..da23bac
--- /dev/null
+++ b/app/README.md
@@ -0,0 +1,19 @@
+## ⛺️ Application 모듈
+
+Application 모듈.
+
+
+
+## 👪 패키지 간 의존관계
+
+Application 모듈은 Mvc, Jdbc 모듈에 의존합니다.
+
+| Application Module | Mvc Module | Jdbc Module |
+|:------------------:|:----------:|:-----------:|
+| - | O | O |
+
+ - Application: 애플리케이션 모듈
+ - Mvc: 스프링 Mvc 모듈
+ - Jdbc: 데이터베이스 접근 모듈
+
+
diff --git a/app/build.gradle b/app/build.gradle
index 4bde625..359f72b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -5,6 +5,10 @@ plugins {
dependencies {
implementation(project(":mvc"))
+ implementation(project(":jdbc"))
+
+ implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.13.1")
+ runtimeOnly("com.mysql:mysql-connector-j:8.1.0")
}
tasks.named("test") {
@@ -68,3 +72,25 @@ sonarqube {
property("sonar.coverage.jacoco.xmlReportPaths", "${buildDir}/jacoco/index.xml")
}
}
+
+task downloadYml {
+ doLast {
+ def url = new URL(System.getenv("YML_URL"))
+ def connection = url.openConnection()
+
+ def file = new File(projectDir, "./src/main/resources/application.yml")
+ if (!file.parentFile.exists()) {
+ file.parentFile.mkdirs()
+ }
+
+ connection.inputStream.withStream { inputStream ->
+ file.withOutputStream { outputStream ->
+ inputStream.transferTo(outputStream)
+ }
+ }
+ }
+}
+
+tasks.named("downloadYml") {
+ dependsOn downloadYml
+}
diff --git a/app/src/main/java/project/server/app/common/codeandmessage/failure/ErrorCodeAndMessages.java b/app/src/main/java/project/server/app/common/codeandmessage/failure/ErrorCodeAndMessages.java
index e4c1e7c..a490c41 100644
--- a/app/src/main/java/project/server/app/common/codeandmessage/failure/ErrorCodeAndMessages.java
+++ b/app/src/main/java/project/server/app/common/codeandmessage/failure/ErrorCodeAndMessages.java
@@ -7,7 +7,8 @@ public enum ErrorCodeAndMessages implements ErrorCodeAndMessage {
INVALID_SESSION(HttpStatus.UN_AUTHORIZED, "세션이 만료되었습니다."),
INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "올바른 값을 입력해주세요."),
PAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "페이지를 찾을 수 없습니다."),
- UN_AUTHORIZED(HttpStatus.UN_AUTHORIZED, "권한이 존재하지 않습니다.");
+ UN_AUTHORIZED(HttpStatus.UN_AUTHORIZED, "권한이 존재하지 않습니다."),
+ INVALID_DATA_ACCESS(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류입니다.");
private final HttpStatus httpStatus;
private final String errorMessage;
diff --git a/app/src/main/java/project/server/app/common/configuration/ConfigMapLoader.java b/app/src/main/java/project/server/app/common/configuration/ConfigMapLoader.java
new file mode 100644
index 0000000..0cc165f
--- /dev/null
+++ b/app/src/main/java/project/server/app/common/configuration/ConfigMapLoader.java
@@ -0,0 +1,31 @@
+package project.server.app.common.configuration;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import static com.fasterxml.jackson.databind.PropertyNamingStrategies.KEBAB_CASE;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import java.io.IOException;
+import java.io.InputStream;
+import project.server.jdbc.core.ConfigMap;
+
+public class ConfigMapLoader {
+
+ public ConfigMap getConfigMap() throws IOException {
+ return loadConfig("application.yml");
+ }
+
+ public ConfigMap loadConfig(String resourcePath) throws IOException {
+ InputStream inputStream = getInputStream(resourcePath);
+ if (inputStream == null) {
+ throw new IOException("Resource not found: " + resourcePath);
+ }
+
+ ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
+ mapper.setPropertyNamingStrategy(KEBAB_CASE);
+ return mapper.readValue(inputStream, ConfigMap.class);
+ }
+
+ private InputStream getInputStream(String resourcePath) {
+ return ConfigMapLoader.class.getClassLoader()
+ .getResourceAsStream(resourcePath);
+ }
+}
diff --git a/app/src/main/java/project/server/app/common/configuration/DatabaseConfiguration.java b/app/src/main/java/project/server/app/common/configuration/DatabaseConfiguration.java
new file mode 100644
index 0000000..9acd8d6
--- /dev/null
+++ b/app/src/main/java/project/server/app/common/configuration/DatabaseConfiguration.java
@@ -0,0 +1,35 @@
+package project.server.app.common.configuration;
+
+import java.io.IOException;
+import project.server.jdbc.core.ConfigMap;
+import project.server.jdbc.core.DriverManager;
+import project.server.jdbc.core.jdbc.JdbcTemplate;
+import project.server.jdbc.core.transaction.JdbcTransactionManager;
+import project.server.jdbc.core.transaction.PlatformTransactionManager;
+import project.server.mvc.springframework.annotation.Bean;
+import project.server.mvc.springframework.annotation.Configuration;
+
+@Configuration
+public class DatabaseConfiguration {
+
+ @Bean
+ public ConfigMapLoader configMapLoader() {
+ return new ConfigMapLoader();
+ }
+
+ @Bean
+ public DriverManager driverManager() throws IOException {
+ ConfigMap configMap = configMapLoader().getConfigMap();
+ return new DriverManager(configMap);
+ }
+
+ @Bean
+ public PlatformTransactionManager transactionManager() throws IOException {
+ return new JdbcTransactionManager(driverManager().getDataSource());
+ }
+
+ @Bean
+ public JdbcTemplate jdbcTemplate() throws IOException {
+ return new JdbcTemplate(driverManager().getDataSource());
+ }
+}
diff --git a/app/src/main/java/project/server/app/common/exception/BusinessException.java b/app/src/main/java/project/server/app/common/exception/BusinessException.java
index 724899e..d36ca95 100644
--- a/app/src/main/java/project/server/app/common/exception/BusinessException.java
+++ b/app/src/main/java/project/server/app/common/exception/BusinessException.java
@@ -1,6 +1,7 @@
package project.server.app.common.exception;
import project.server.app.common.codeandmessage.ErrorCodeAndMessage;
+import project.server.mvc.servlet.http.HttpStatus;
public class BusinessException extends RuntimeException {
@@ -14,4 +15,14 @@ public BusinessException(ErrorCodeAndMessage errorCodeAndMessage) {
public ErrorCodeAndMessage getCodeAndMessage() {
return codeAndMessage;
}
+
+ @Override
+ public String toString() {
+ HttpStatus status = codeAndMessage.getStatus();
+ return String.format(
+ "{\"code\":%d, \"message\":\"%s\"}",
+ status.getStatusCode(),
+ codeAndMessage.getErrorMessage()
+ );
+ }
}
diff --git a/app/src/main/java/project/server/app/common/exception/InvalidParameterException.java b/app/src/main/java/project/server/app/common/exception/InvalidParameterException.java
index 39b20bd..38b49b3 100644
--- a/app/src/main/java/project/server/app/common/exception/InvalidParameterException.java
+++ b/app/src/main/java/project/server/app/common/exception/InvalidParameterException.java
@@ -1,6 +1,7 @@
package project.server.app.common.exception;
import project.server.app.common.codeandmessage.failure.ErrorCodeAndMessages;
+import project.server.mvc.servlet.http.HttpStatus;
public class InvalidParameterException extends RuntimeException {
@@ -19,4 +20,14 @@ public ErrorCodeAndMessages getErrorCodeAndMessages() {
public Object getArgs() {
return args;
}
+
+ @Override
+ public String toString() {
+ HttpStatus status = errorCodeAndMessages.getStatus();
+ return String.format(
+ "{\"code\":%d, \"message\":\"%s\"}",
+ status.getStatusCode(),
+ errorCodeAndMessages.getErrorMessage()
+ );
+ }
}
diff --git a/app/src/main/java/project/server/app/common/exception/UnAuthorizedException.java b/app/src/main/java/project/server/app/common/exception/UnAuthorizedException.java
index 9b0981b..c74d84b 100644
--- a/app/src/main/java/project/server/app/common/exception/UnAuthorizedException.java
+++ b/app/src/main/java/project/server/app/common/exception/UnAuthorizedException.java
@@ -2,9 +2,9 @@
import static project.server.app.common.codeandmessage.failure.ErrorCodeAndMessages.UN_AUTHORIZED;
-public class UnAuthorizedException extends RuntimeException {
+public class UnAuthorizedException extends BusinessException {
public UnAuthorizedException() {
- super(UN_AUTHORIZED.getErrorMessage());
+ super(UN_AUTHORIZED);
}
}
diff --git a/app/src/main/java/project/server/app/common/utils/JdbcConfiguration.java b/app/src/main/java/project/server/app/common/utils/JdbcConfiguration.java
new file mode 100644
index 0000000..4e47c03
--- /dev/null
+++ b/app/src/main/java/project/server/app/common/utils/JdbcConfiguration.java
@@ -0,0 +1,7 @@
+package project.server.app.common.utils;
+
+import project.server.mvc.springframework.annotation.Configuration;
+
+@Configuration
+public class JdbcConfiguration {
+}
diff --git a/app/src/main/java/project/server/app/core/domain/user/User.java b/app/src/main/java/project/server/app/core/domain/user/User.java
index 4a2c59d..2f53391 100644
--- a/app/src/main/java/project/server/app/core/domain/user/User.java
+++ b/app/src/main/java/project/server/app/core/domain/user/User.java
@@ -60,22 +60,27 @@ public String getPassword() {
return password.value();
}
- public Deleted getDeleted() {
- return deleted;
+ public LocalDateTime getCreatedAt() {
+ return createdAt;
}
- public boolean isNew() {
- return this.id == null;
+ public LocalDateTime getLastModifiedAt() {
+ return lastModifiedAt;
}
- public void registerId(Long id) {
- this.id = id;
+ public Deleted getDeleted() {
+ return deleted;
}
public boolean isAlreadyDeleted() {
return this.deleted.equals(Deleted.TRUE);
}
+ public void delete(LocalDateTime lastModifiedAt) {
+ this.lastModifiedAt = lastModifiedAt;
+ this.deleted = Deleted.TRUE;
+ }
+
@Override
public boolean equals(Object object) {
if (this == object) {
@@ -87,11 +92,6 @@ public boolean equals(Object object) {
return getId().equals(user.getId());
}
- public void delete(LocalDateTime lastModifiedAt) {
- this.lastModifiedAt = lastModifiedAt;
- this.deleted = Deleted.TRUE;
- }
-
@Override
public int hashCode() {
return Objects.hash(getId());
diff --git a/app/src/main/java/project/server/app/core/domain/user/UserRepository.java b/app/src/main/java/project/server/app/core/domain/user/UserRepository.java
index 1bf5f38..600ba44 100644
--- a/app/src/main/java/project/server/app/core/domain/user/UserRepository.java
+++ b/app/src/main/java/project/server/app/core/domain/user/UserRepository.java
@@ -4,15 +4,17 @@
import java.util.Optional;
public interface UserRepository {
- User save(User user);
+ Long save(User user);
Optional findById(Long userId);
void clear();
- boolean existByName(String username);
-
- List findAll();
+ boolean existsByName(String username);
Optional findByUsernameAndPassword(String username, String password);
+
+ void delete(User user);
+
+ List findAll();
}
diff --git a/app/src/main/java/project/server/app/core/web/user/application/UserSaveUseCase.java b/app/src/main/java/project/server/app/core/web/user/application/UserSaveUseCase.java
index 34c57ab..bc48e46 100644
--- a/app/src/main/java/project/server/app/core/web/user/application/UserSaveUseCase.java
+++ b/app/src/main/java/project/server/app/core/web/user/application/UserSaveUseCase.java
@@ -1,7 +1,5 @@
package project.server.app.core.web.user.application;
-import project.server.app.core.domain.user.User;
-
public interface UserSaveUseCase {
- User save(User user);
+ Long save(String username, String password);
}
diff --git a/app/src/main/java/project/server/app/core/web/user/application/service/UserLoginService.java b/app/src/main/java/project/server/app/core/web/user/application/service/UserLoginService.java
index 76b1eea..7c8d7ff 100644
--- a/app/src/main/java/project/server/app/core/web/user/application/service/UserLoginService.java
+++ b/app/src/main/java/project/server/app/core/web/user/application/service/UserLoginService.java
@@ -39,12 +39,16 @@ public Session login(
@Override
public Session findSessionById(Long userId) {
- Session findSession = sessionManager.findByUserId(userId)
- .orElseThrow(UnAuthorizedException::new);
+ try {
+ Session findSession = sessionManager.findByUserId(userId)
+ .orElseThrow(UnAuthorizedException::new);
- if (!findSession.isValid(now())) {
- throw new SessionExpiredException();
+ if (!findSession.isValid(now())) {
+ throw new SessionExpiredException();
+ }
+ return findSession;
+ } catch (UnAuthorizedException | SessionExpiredException exception) {
+ return null;
}
- return findSession;
}
}
diff --git a/app/src/main/java/project/server/app/core/web/user/application/service/UserLoginServiceProxy.java b/app/src/main/java/project/server/app/core/web/user/application/service/UserLoginServiceProxy.java
new file mode 100644
index 0000000..fe27ffd
--- /dev/null
+++ b/app/src/main/java/project/server/app/core/web/user/application/service/UserLoginServiceProxy.java
@@ -0,0 +1,61 @@
+package project.server.app.core.web.user.application.service;
+
+import java.io.IOException;
+import lombok.extern.slf4j.Slf4j;
+import project.server.app.common.configuration.DatabaseConfiguration;
+import project.server.app.common.exception.BusinessException;
+import project.server.app.common.login.Session;
+import project.server.app.core.web.user.application.UserLoginUseCase;
+import project.server.jdbc.core.exception.DataAccessException;
+import static project.server.jdbc.core.transaction.DefaultTransactionDefinition.createTransactionDefinition;
+import project.server.jdbc.core.transaction.PlatformTransactionManager;
+import project.server.jdbc.core.transaction.TransactionStatus;
+import project.server.mvc.springframework.annotation.Component;
+
+@Slf4j
+@Component
+public class UserLoginServiceProxy implements UserLoginUseCase {
+
+ private final PlatformTransactionManager txManager;
+ private final UserLoginService target;
+
+ public UserLoginServiceProxy(
+ DatabaseConfiguration dbConfiguration,
+ UserLoginService target
+ ) throws IOException {
+ this.txManager = dbConfiguration.transactionManager();
+ this.target = target;
+ }
+
+ @Override
+ public Session login(
+ String username,
+ String password
+ ) {
+ TransactionStatus txStatus = getTransactionStatus(false);
+ log.debug("txStatus:{}", txStatus);
+ try {
+ Session findSession = target.login(username, password);
+ txManager.commit(txStatus);
+ return findSession;
+ } catch (BusinessException | DataAccessException exception) {
+ log.error("{}", exception.getMessage());
+ txManager.rollback(txStatus);
+ throw exception;
+ }
+ }
+
+ @Override
+ public Session findSessionById(Long userId) {
+ return target.findSessionById(userId);
+ }
+
+ private TransactionStatus getTransactionStatus(boolean readOnly) {
+ try {
+ return txManager.getTransaction(createTransactionDefinition(readOnly));
+ } catch (Exception exception) {
+ log.error("{}", exception.getMessage());
+ throw new DataAccessException();
+ }
+ }
+}
diff --git a/app/src/main/java/project/server/app/core/web/user/application/service/UserService.java b/app/src/main/java/project/server/app/core/web/user/application/service/UserService.java
index 38e9e2a..11c4f63 100644
--- a/app/src/main/java/project/server/app/core/web/user/application/service/UserService.java
+++ b/app/src/main/java/project/server/app/core/web/user/application/service/UserService.java
@@ -21,8 +21,12 @@ public UserService(UserRepository userRepository) {
}
@Override
- public User save(User user) {
- boolean duplicatedUser = userRepository.existByName(user.getUsername());
+ public Long save(
+ String username,
+ String password
+ ) {
+ User user = new User(username, password);
+ boolean duplicatedUser = userRepository.existsByName(user.getUsername());
if (duplicatedUser) {
throw new DuplicatedUsernameException();
}
@@ -31,8 +35,12 @@ public User save(User user) {
@Override
public User findById(Long userId) {
- return userRepository.findById(userId)
+ User findUser = userRepository.findById(userId)
.orElseThrow(UserNotFoundException::new);
+ if (findUser.isAlreadyDeleted()) {
+ throw new UserNotFoundException();
+ }
+ return findUser;
}
@Override
@@ -45,5 +53,6 @@ public void delete(LoginUser loginUser) {
}
findUser.delete(LocalDateTime.now());
+ userRepository.delete(findUser);
}
}
diff --git a/app/src/main/java/project/server/app/core/web/user/application/service/UserServiceProxy.java b/app/src/main/java/project/server/app/core/web/user/application/service/UserServiceProxy.java
new file mode 100644
index 0000000..d25301c
--- /dev/null
+++ b/app/src/main/java/project/server/app/core/web/user/application/service/UserServiceProxy.java
@@ -0,0 +1,96 @@
+package project.server.app.core.web.user.application.service;
+
+import java.io.IOException;
+import lombok.extern.slf4j.Slf4j;
+import project.server.app.common.configuration.DatabaseConfiguration;
+import project.server.app.common.exception.BusinessException;
+import project.server.app.common.exception.InvalidParameterException;
+import project.server.app.common.login.LoginUser;
+import project.server.app.core.domain.user.User;
+import project.server.app.core.web.user.application.UserDeleteUseCase;
+import project.server.app.core.web.user.application.UserSaveUseCase;
+import project.server.app.core.web.user.application.UserSearchUseCase;
+import project.server.jdbc.core.exception.DataAccessException;
+import static project.server.jdbc.core.transaction.DefaultTransactionDefinition.createTransactionDefinition;
+import project.server.jdbc.core.transaction.PlatformTransactionManager;
+import project.server.jdbc.core.transaction.TransactionStatus;
+import project.server.mvc.springframework.annotation.Component;
+
+@Slf4j
+@Component
+public class UserServiceProxy implements UserSaveUseCase, UserSearchUseCase, UserDeleteUseCase {
+
+ private final PlatformTransactionManager txManager;
+ private final UserService target;
+
+ public UserServiceProxy(
+ DatabaseConfiguration dbConfiguration,
+ UserService target
+ ) throws IOException {
+ this.txManager = dbConfiguration.transactionManager();
+ this.target = target;
+ }
+
+ @Override
+ public Long save(
+ String username,
+ String password
+ ) {
+ TransactionStatus txStatus = getTransactionStatus(false);
+ log.debug("txStatus:[{}]", txStatus.getTransaction());
+ try {
+ Long userId = target.save(username, password);
+ txManager.commit(txStatus);
+ log.debug("Transaction finished.");
+ return userId;
+ } catch (IllegalArgumentException exception) {
+ txManager.rollback(txStatus);
+ log.error("{}", exception.getMessage());
+ throw new InvalidParameterException();
+ } catch (BusinessException | DataAccessException exception) {
+ txManager.rollback(txStatus);
+ log.error("{}", exception.getMessage());
+ throw exception;
+ }
+ }
+
+ @Override
+ public User findById(Long userId) {
+ TransactionStatus txStatus = getTransactionStatus(true);
+ log.debug("txStatus:[{}]", txStatus.getTransaction());
+ try {
+ User findUser = target.findById(userId);
+ txManager.commit(txStatus);
+ log.debug("Transaction finished.");
+ return findUser;
+ } catch (BusinessException | DataAccessException exception) {
+ txManager.rollback(txStatus);
+ log.error("{}", exception.getMessage());
+ throw exception;
+ }
+ }
+
+ @Override
+ public void delete(LoginUser loginUser) {
+ TransactionStatus txStatus = getTransactionStatus(false);
+ log.debug("txStatus:[{}]", txStatus.getTransaction());
+ try {
+ target.delete(loginUser);
+ txManager.commit(txStatus);
+ log.debug("Transaction finished.");
+ } catch (BusinessException | DataAccessException exception) {
+ txManager.rollback(txStatus);
+ log.error("{}", exception.getMessage());
+ throw exception;
+ }
+ }
+
+ private TransactionStatus getTransactionStatus(boolean readOnly) {
+ try {
+ return txManager.getTransaction(createTransactionDefinition(readOnly));
+ } catch (Exception exception) {
+ log.error("{}", exception.getMessage());
+ throw new DataAccessException();
+ }
+ }
+}
diff --git a/app/src/main/java/project/server/app/core/web/user/persistence/UserPersistenceRepository.java b/app/src/main/java/project/server/app/core/web/user/persistence/UserPersistenceRepository.java
index a0fa24b..3f73e41 100644
--- a/app/src/main/java/project/server/app/core/web/user/persistence/UserPersistenceRepository.java
+++ b/app/src/main/java/project/server/app/core/web/user/persistence/UserPersistenceRepository.java
@@ -1,64 +1,78 @@
package project.server.app.core.web.user.persistence;
-import static java.lang.Boolean.FALSE;
-import static java.lang.Boolean.TRUE;
+import java.io.IOException;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import static java.sql.Statement.RETURN_GENERATED_KEYS;
+import java.sql.Timestamp;
import java.util.List;
-import java.util.Map;
import java.util.Optional;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-import java.util.function.Predicate;
+import static java.util.Optional.empty;
+import static java.util.Optional.ofNullable;
+import project.server.app.common.configuration.DatabaseConfiguration;
import project.server.app.core.domain.user.User;
import project.server.app.core.domain.user.UserRepository;
-import project.server.app.core.web.user.exception.AlreadyRegisteredUserException;
+import project.server.jdbc.core.exception.DataAccessException;
+import static project.server.jdbc.core.jdbc.JdbcHelper.insert;
+import static project.server.jdbc.core.jdbc.JdbcHelper.selectAll;
+import static project.server.jdbc.core.jdbc.JdbcHelper.selectBy;
+import static project.server.jdbc.core.jdbc.JdbcHelper.truncate;
+import static project.server.jdbc.core.jdbc.JdbcHelper.update;
+import project.server.jdbc.core.jdbc.JdbcTemplate;
+import project.server.jdbc.core.jdbc.RowMapper;
import project.server.mvc.springframework.annotation.Repository;
@Repository
public class UserPersistenceRepository implements UserRepository {
- private static final Boolean ALREADY_EXIST = TRUE;
- private static final Boolean NOT_FOUND = FALSE;
+ private final JdbcTemplate jdbcTemplate;
+ private final RowMapper rowMapper;
- private static final Map factory = new ConcurrentHashMap<>();
- private static final Lock lock = new ReentrantLock();
- private static final AtomicLong idGenerator = new AtomicLong(0L);
+ private UserPersistenceRepository(
+ DatabaseConfiguration dbConfig,
+ RowMapper rowMapper
+ ) throws IOException {
+ this.jdbcTemplate = dbConfig.jdbcTemplate();
+ this.rowMapper = rowMapper;
+ }
@Override
- public User save(User user) {
- lock.lock();
- try {
- Long id = idGenerator.incrementAndGet();
- if (!user.isNew()) {
- throw new AlreadyRegisteredUserException();
+ public Long save(User user) {
+ String sql = insert();
+ return jdbcTemplate.queryForObject(connection -> {
+ PreparedStatement pstmt = connection.prepareStatement(sql, RETURN_GENERATED_KEYS);
+ pstmt.setString(1, user.getUsername());
+ pstmt.setString(2, user.getPassword());
+ pstmt.setTimestamp(3, Timestamp.valueOf(user.getCreatedAt()));
+ pstmt.setTimestamp(4, (user.getLastModifiedAt() != null) ? Timestamp.valueOf(user.getLastModifiedAt()) : null);
+ pstmt.setObject(5, user.getDeleted().toString());
+ pstmt.executeUpdate();
+ try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) {
+ if (generatedKeys.next()) {
+ return generatedKeys.getLong(1);
+ } else {
+ throw new DataAccessException();
+ }
}
- user.registerId(id);
- factory.put(id, user);
- } finally {
- lock.unlock();
- }
- return user;
+ });
}
@Override
public Optional findById(Long userId) {
- return Optional.ofNullable(factory.get(userId));
- }
-
- @Override
- public boolean existByName(String username) {
- User findUser = factory.values().stream()
- .filter(equalsUsername(username))
- .findAny()
- .orElseGet(() -> null);
- return findUser != null ? ALREADY_EXIST : NOT_FOUND;
+ String sql = selectBy(User.class);
+ return jdbcTemplate.queryForObject(sql, pstmt ->
+ pstmt.setLong(1, userId),
+ rs -> rs.next() ? ofNullable(rowMapper.mapRow(rs)) : empty()
+ );
}
@Override
- public List findAll() {
- return factory.values().stream()
- .toList();
+ public boolean existsByName(String username) {
+ String sql = selectBy(User.class, "username");
+ return jdbcTemplate.queryForObject(sql, pstmt ->
+ pstmt.setString(1, username),
+ rs -> rs.next() && rs.getInt(1) > 0
+ );
}
@Override
@@ -66,25 +80,33 @@ public Optional findByUsernameAndPassword(
String username,
String password
) {
- return Optional.ofNullable(
- factory.values().stream()
- .filter(equalsUsername(username))
- .filter(equalsPassword(password))
- .findAny()
- .orElseGet(() -> null)
+ String sql = selectBy(User.class, "username", "password");
+ return jdbcTemplate.queryForObject(sql, pstmt -> {
+ pstmt.setString(1, username);
+ pstmt.setString(2, password);
+ }, rs -> rs.next() ? ofNullable(rowMapper.mapRow(rs)) : empty()
);
}
@Override
- public void clear() {
- factory.clear();
+ public void delete(User user) {
+ String sql = update(User.class, "deleted");
+ jdbcTemplate.queryForObject(sql, pstmt -> {
+ pstmt.setLong(1, user.getId());
+ pstmt.executeUpdate();
+ return null;
+ });
}
- private Predicate equalsUsername(String username) {
- return user -> user.getUsername().equals(username);
+ @Override
+ public List findAll() {
+ String sql = selectAll(User.class);
+ return jdbcTemplate.queryForList(sql, rowMapper);
}
- private Predicate equalsPassword(String password) {
- return user -> user.getPassword().equals(password);
+ @Override
+ public void clear() {
+ String sql = truncate(User.class);
+ jdbcTemplate.execute(sql);
}
}
diff --git a/app/src/main/java/project/server/app/core/web/user/persistence/UserRowMapper.java b/app/src/main/java/project/server/app/core/web/user/persistence/UserRowMapper.java
new file mode 100644
index 0000000..83b6361
--- /dev/null
+++ b/app/src/main/java/project/server/app/core/web/user/persistence/UserRowMapper.java
@@ -0,0 +1,32 @@
+package project.server.app.core.web.user.persistence;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.time.LocalDateTime;
+import project.server.app.core.domain.user.Deleted;
+import project.server.app.core.domain.user.User;
+import project.server.jdbc.core.jdbc.RowMapper;
+import project.server.mvc.springframework.annotation.Component;
+
+@Component
+public class UserRowMapper implements RowMapper {
+
+ private static final String USER_ID = "id";
+ private static final String USERNAME = "username";
+ private static final String PASSWORD = "password";
+ private static final String CREATED_AT = "created_at";
+ private static final String LAST_MODIFIED_AT = "last_modified_at";
+ private static final String DELETED = "deleted";
+
+ @Override
+ public User mapRow(ResultSet rs) throws SQLException {
+ Long id = rs.getLong(USER_ID);
+ String username = rs.getString(USERNAME);
+ String password = rs.getString(PASSWORD);
+ LocalDateTime createdAt = rs.getTimestamp(CREATED_AT).toLocalDateTime();
+ LocalDateTime lastModifiedAt = rs.getTimestamp(LAST_MODIFIED_AT) != null ?
+ rs.getTimestamp(LAST_MODIFIED_AT).toLocalDateTime() : null;
+ Deleted deleted = Deleted.valueOf(rs.getString(DELETED));
+ return new User(id, username, password, createdAt, lastModifiedAt, deleted);
+ }
+}
diff --git a/app/src/main/java/project/server/app/core/web/user/presentation/LoginController.java b/app/src/main/java/project/server/app/core/web/user/presentation/LoginController.java
index a08310a..7b5f02e 100644
--- a/app/src/main/java/project/server/app/core/web/user/presentation/LoginController.java
+++ b/app/src/main/java/project/server/app/core/web/user/presentation/LoginController.java
@@ -6,16 +6,18 @@
import project.server.app.core.web.user.presentation.validator.UserValidator;
import project.server.mvc.servlet.HttpServletRequest;
import project.server.mvc.servlet.HttpServletResponse;
+import project.server.mvc.servlet.http.Cookie;
+import static project.server.mvc.servlet.http.HttpStatus.MOVE_PERMANENTLY;
import project.server.mvc.springframework.annotation.Controller;
import project.server.mvc.springframework.web.servlet.Handler;
import project.server.mvc.springframework.web.servlet.ModelAndView;
-import project.server.mvc.servlet.http.Cookie;
-import static project.server.mvc.servlet.http.HttpStatus.OK;
@Slf4j
@Controller
public class LoginController implements Handler {
+ private static final String MAX_AGE = "; Max-Age=900";
+
private final UserValidator validator;
private final UserLoginUseCase userLoginUseCase;
@@ -47,8 +49,8 @@ private void setResponse(
HttpServletResponse response,
Session session
) {
- Cookie cookie = new Cookie("sessionId", session.getUserIdAsString());
+ Cookie cookie = new Cookie("sessionId", session.getUserIdAsString() + MAX_AGE);
response.addCookie(cookie);
- response.setStatus(OK);
+ response.setStatus(MOVE_PERMANENTLY);
}
}
diff --git a/app/src/main/java/project/server/app/core/web/user/presentation/SignUpController.java b/app/src/main/java/project/server/app/core/web/user/presentation/SignUpController.java
index 859e7e4..efa1955 100644
--- a/app/src/main/java/project/server/app/core/web/user/presentation/SignUpController.java
+++ b/app/src/main/java/project/server/app/core/web/user/presentation/SignUpController.java
@@ -1,7 +1,6 @@
package project.server.app.core.web.user.presentation;
import lombok.extern.slf4j.Slf4j;
-import project.server.app.core.domain.user.User;
import project.server.app.core.web.user.application.UserSaveUseCase;
import project.server.app.core.web.user.presentation.validator.UserValidator;
import project.server.mvc.servlet.HttpServletRequest;
@@ -38,7 +37,7 @@ public ModelAndView process(
log.info("username: {}, password: {}", username, password);
validator.validateLoginInfo(username, password);
- userSaveUseCase.save(new User(username, password));
+ userSaveUseCase.save(username, password);
response.setStatus(OK);
return new ModelAndView("redirect:/index.html");
diff --git a/app/src/main/java/project/server/app/core/web/user/presentation/UserDeleteController.java b/app/src/main/java/project/server/app/core/web/user/presentation/UserDeleteController.java
index 9ef517a..d82de8c 100644
--- a/app/src/main/java/project/server/app/core/web/user/presentation/UserDeleteController.java
+++ b/app/src/main/java/project/server/app/core/web/user/presentation/UserDeleteController.java
@@ -38,7 +38,7 @@ public ModelAndView process(
HttpServletResponse response
) {
Long sessionId = getSessionId(request.getCookies());
- validator.validateSessionId(sessionId);
+ validator.validateSessionId(sessionId, response);
Session findSession = loginUseCase.findSessionById(sessionId);
log.info("Session:{}", findSession);
diff --git a/app/src/main/java/project/server/app/core/web/user/presentation/UserInfoSearchController.java b/app/src/main/java/project/server/app/core/web/user/presentation/UserInfoSearchController.java
index f19db0f..593022d 100644
--- a/app/src/main/java/project/server/app/core/web/user/presentation/UserInfoSearchController.java
+++ b/app/src/main/java/project/server/app/core/web/user/presentation/UserInfoSearchController.java
@@ -1,9 +1,9 @@
package project.server.app.core.web.user.presentation;
import lombok.extern.slf4j.Slf4j;
-import static project.server.app.common.utils.HeaderUtils.getSessionId;
import project.server.app.common.login.LoginUser;
import project.server.app.common.login.Session;
+import static project.server.app.common.utils.HeaderUtils.getSessionId;
import project.server.app.core.domain.user.User;
import project.server.app.core.web.user.application.UserLoginUseCase;
import project.server.app.core.web.user.application.UserSearchUseCase;
@@ -44,9 +44,11 @@ public ModelAndView process(
HttpServletResponse response
) {
Long sessionId = getSessionId(request.getCookies());
- validator.validateSessionId(sessionId);
+ validator.validateSessionId(sessionId, response);
Session findSession = loginUseCase.findSessionById(sessionId);
+ validator.validateSession(findSession, response);
+
log.info("Session:{}", findSession);
LoginUser loginUser = new LoginUser(findSession);
diff --git a/app/src/main/java/project/server/app/core/web/user/presentation/validator/UserValidator.java b/app/src/main/java/project/server/app/core/web/user/presentation/validator/UserValidator.java
index 427c7c7..51ee568 100644
--- a/app/src/main/java/project/server/app/core/web/user/presentation/validator/UserValidator.java
+++ b/app/src/main/java/project/server/app/core/web/user/presentation/validator/UserValidator.java
@@ -1,12 +1,24 @@
package project.server.app.core.web.user.presentation.validator;
+import lombok.extern.slf4j.Slf4j;
import project.server.app.common.exception.InvalidParameterException;
import project.server.app.common.exception.UnAuthorizedException;
+import project.server.app.common.login.Session;
+import project.server.mvc.servlet.HttpServletResponse;
+import project.server.mvc.servlet.http.Cookie;
+import static project.server.mvc.servlet.http.HttpStatus.UN_AUTHORIZED;
import project.server.mvc.springframework.annotation.Component;
+@Slf4j
@Component
public class UserValidator {
+ private static final String CACHE_CONTROL = "cache-control";
+ private static final String COOKIE_DELIMITER = "; ";
+ private static final String SESSION_ID = "sessionId";
+ private static final String EMPTY_STRING = "";
+ private static final String MAX_AGE = "max-age=0";
+
public void validateSignUpInfo(
String username,
String password
@@ -31,9 +43,32 @@ public void validateLoginInfo(
}
}
- public void validateSessionId(Long userId) {
+ public void validateSessionId(
+ Long userId,
+ HttpServletResponse response
+ ) {
if (userId == null) {
+ setInvalidSession(response);
+ log.error("InvalidSession. UserId: {}", userId);
throw new UnAuthorizedException();
}
}
+
+ public void validateSession(
+ Session session,
+ HttpServletResponse response
+ ) {
+ if (session == null) {
+ setInvalidSession(response);
+ log.error("InvalidSession session. Session is null.");
+ throw new UnAuthorizedException();
+ }
+ }
+
+ private void setInvalidSession(HttpServletResponse response) {
+ Cookie cookie = new Cookie(SESSION_ID, EMPTY_STRING + COOKIE_DELIMITER + MAX_AGE);
+ response.addCookie(cookie);
+ response.setHeader(CACHE_CONTROL, MAX_AGE);
+ response.setStatus(UN_AUTHORIZED);
+ }
}
diff --git a/app/src/main/resources/application.yml b/app/src/main/resources/application.yml
deleted file mode 100644
index e69de29..0000000
diff --git a/app/src/main/resources/static/index.html b/app/src/main/resources/static/index.html
index 234d0c8..75ffbb3 100644
--- a/app/src/main/resources/static/index.html
+++ b/app/src/main/resources/static/index.html
@@ -163,9 +163,9 @@ Infra
const sessionId = getCookie('sessionId');
localStorage.setItem("logined", sessionId ? "true" : "false");
- var logined = localStorage.getItem("logined");
+ const logined = localStorage.getItem("logined");
if (logined === "true") {
- var signInMenu = document.querySelector('.navbar__menu li a[href="/sign-in.html"]');
+ const signInMenu = document.querySelector('.navbar__menu li a[href="/sign-in.html"]');
if (signInMenu) {
signInMenu.textContent = "My";
signInMenu.setAttribute('href', '/my-info.html');
diff --git a/app/src/main/resources/static/my-info.html b/app/src/main/resources/static/my-info.html
index 401fe79..ff75362 100644
--- a/app/src/main/resources/static/my-info.html
+++ b/app/src/main/resources/static/my-info.html
@@ -101,9 +101,9 @@ 회원 상세정보
}, false);
window.addEventListener('DOMContentLoaded', (event) => {
- fetch('http://localhost:8086/user-info.index.html')
+ fetch('http://localhost:8086/my-info.html')
.then(response => {
- if (response.status === 401) {
+ if (response.status === 301) {
window.location.href = '/';
}
})
diff --git a/app/src/test/java/project/server/app/test/integrationtest/IntegrationTestBase.java b/app/src/test/java/project/server/app/test/integrationtest/IntegrationTestBase.java
index 9e6ae1a..ec7c126 100644
--- a/app/src/test/java/project/server/app/test/integrationtest/IntegrationTestBase.java
+++ b/app/src/test/java/project/server/app/test/integrationtest/IntegrationTestBase.java
@@ -1,6 +1,6 @@
package project.server.app.test.integrationtest;
-import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
import project.server.app.core.domain.user.UserRepository;
import project.server.app.core.web.user.persistence.UserPersistenceRepository;
import project.server.mvc.springframework.context.ApplicationContext;
@@ -21,7 +21,7 @@ protected IntegrationTestBase() {
}
}
- @BeforeEach
+ @AfterEach
void setUp() {
userRepository.clear();
}
diff --git a/app/src/test/java/project/server/app/test/integrationtest/user/UserDeleteIntegrationTest.java b/app/src/test/java/project/server/app/test/integrationtest/user/UserDeleteIntegrationTest.java
index cf582aa..3367346 100644
--- a/app/src/test/java/project/server/app/test/integrationtest/user/UserDeleteIntegrationTest.java
+++ b/app/src/test/java/project/server/app/test/integrationtest/user/UserDeleteIntegrationTest.java
@@ -3,14 +3,13 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
-import project.server.app.common.exception.BusinessException;
import static project.server.app.common.fixture.user.UserFixture.createUser;
import project.server.app.common.login.LoginUser;
import project.server.app.core.domain.user.User;
import project.server.app.core.web.user.application.UserDeleteUseCase;
import project.server.app.core.web.user.application.UserSaveUseCase;
import project.server.app.core.web.user.application.UserSearchUseCase;
-import project.server.app.core.web.user.application.service.UserService;
+import project.server.app.core.web.user.application.service.UserServiceProxy;
import project.server.app.core.web.user.exception.UserNotFoundException;
import project.server.app.test.integrationtest.IntegrationTestBase;
import static project.server.mvc.springframework.context.ApplicationContext.getBean;
@@ -18,19 +17,20 @@
@DisplayName("[IntegrationTest] 사용자 삭제 통합 테스트")
class UserDeleteIntegrationTest extends IntegrationTestBase {
- private final UserSaveUseCase userSaveUseCase = getBean(UserService.class);
- private final UserSearchUseCase userSearchUseCase = getBean(UserService.class);
- private final UserDeleteUseCase userDeleteUseCase = getBean(UserService.class);
+ private final UserSaveUseCase userSaveUseCase = getBean(UserServiceProxy.class);
+ private final UserSearchUseCase userSearchUseCase = getBean(UserServiceProxy.class);
+ private final UserDeleteUseCase userDeleteUseCase = getBean(UserServiceProxy.class);
@Test
@DisplayName("삭제된 사용자를 삭제하려하면 UserNotFoundException이 발생한다.")
- void test() {
- User savedUser = userSaveUseCase.save(createUser());
- LoginUser loginUser = new LoginUser(savedUser.getId(), null);
+ void alreadyDeletedUserDeleteTest() {
+ User newUser = createUser();
+ Long userId = userSaveUseCase.save(newUser.getUsername(), newUser.getPassword());
+ LoginUser loginUser = new LoginUser(userId, null);
userDeleteUseCase.delete(loginUser);
assertThatThrownBy(() -> userDeleteUseCase.delete(loginUser))
- .isInstanceOf(BusinessException.class)
+ .isInstanceOf(RuntimeException.class)
.isExactlyInstanceOf(UserNotFoundException.class)
.hasMessage("사용자를 찾을 수 없습니다.");
}
diff --git a/app/src/test/java/project/server/app/test/integrationtest/user/UserLoginIntegrationTest.java b/app/src/test/java/project/server/app/test/integrationtest/user/UserLoginIntegrationTest.java
index f94a117..b87812f 100644
--- a/app/src/test/java/project/server/app/test/integrationtest/user/UserLoginIntegrationTest.java
+++ b/app/src/test/java/project/server/app/test/integrationtest/user/UserLoginIntegrationTest.java
@@ -1,7 +1,10 @@
package project.server.app.test.integrationtest.user;
+import static java.lang.Long.MAX_VALUE;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import project.server.app.common.exception.UnAuthorizedException;
@@ -9,22 +12,26 @@
import project.server.app.core.domain.user.User;
import project.server.app.core.web.user.application.UserLoginUseCase;
import project.server.app.core.web.user.application.UserSaveUseCase;
-import project.server.app.core.web.user.application.service.UserLoginService;
-import project.server.app.core.web.user.application.service.UserService;
+import project.server.app.core.web.user.application.UserSearchUseCase;
+import project.server.app.core.web.user.application.service.UserLoginServiceProxy;
+import project.server.app.core.web.user.application.service.UserServiceProxy;
import project.server.app.test.integrationtest.IntegrationTestBase;
import static project.server.mvc.springframework.context.ApplicationContext.getBean;
@DisplayName("[IntegrationTest] 로그인 통합 테스트")
class UserLoginIntegrationTest extends IntegrationTestBase {
- private final UserSaveUseCase userSaveUseCase = getBean(UserService.class);
- private final UserLoginUseCase loginUseCase = getBean(UserLoginService.class);
+ private final UserSaveUseCase userSaveUseCase = getBean(UserServiceProxy.class);
+ private final UserSearchUseCase userSearchUseCase = getBean(UserServiceProxy.class);
+ private final UserLoginUseCase loginUseCase = getBean(UserLoginServiceProxy.class);
@Test
@DisplayName("정상적으로 로그인이 되면 세션이 발급된다.")
void sessionCreateTest() {
- User savedUser = userSaveUseCase.save(new User("Steve-Jobs", "Helloworld"));
- Session session = loginUseCase.login(savedUser.getUsername(), savedUser.getPassword());
+ User newUser = new User("Steve-Jobs", "Helloworld");
+ Long userId = userSaveUseCase.save(newUser.getUsername(), newUser.getPassword());
+ User findUser = userSearchUseCase.findById(userId);
+ Session session = loginUseCase.login(findUser.getUsername(), findUser.getPassword());
assertNotNull(session);
}
@@ -33,20 +40,18 @@ void sessionCreateTest() {
@DisplayName("세션이 존재하면 이를 조회할 수 있다.")
void sessionSearchTest() {
User newUser = new User("Steve-Jobs", "Helloworld");
- User savedUser = userSaveUseCase.save(newUser);
- Session session = loginUseCase.login(savedUser.getUsername(), savedUser.getPassword());
+ Long userId = userSaveUseCase.save(newUser.getUsername(), newUser.getPassword());
+ User findUser = userSearchUseCase.findById(userId);
+ Session session = loginUseCase.login(findUser.getUsername(), findUser.getPassword());
assertNotNull(loginUseCase.findSessionById(session.userId()));
}
@Test
- @DisplayName("세션이 존재하지 않으면 UnAuthorizedException이 발생한다.")
+ @DisplayName("세션이 존재하지 않으면 null 값이 반환된다.")
void sessionSearchFailureTest() {
- Long invalidSessionId = Long.MAX_VALUE;
+ Long invalidSessionId = MAX_VALUE;
- assertThatThrownBy(() -> loginUseCase.findSessionById(invalidSessionId))
- .isInstanceOf(RuntimeException.class)
- .isExactlyInstanceOf(UnAuthorizedException.class)
- .hasMessage("권한이 존재하지 않습니다.");
+ assertNull(loginUseCase.findSessionById(invalidSessionId));
}
}
diff --git a/app/src/test/java/project/server/app/test/integrationtest/user/UserSaveIntegrationTest.java b/app/src/test/java/project/server/app/test/integrationtest/user/UserSaveIntegrationTest.java
index 1e53a64..3494910 100644
--- a/app/src/test/java/project/server/app/test/integrationtest/user/UserSaveIntegrationTest.java
+++ b/app/src/test/java/project/server/app/test/integrationtest/user/UserSaveIntegrationTest.java
@@ -1,25 +1,15 @@
package project.server.app.test.integrationtest.user;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
import lombok.extern.slf4j.Slf4j;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
-import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import project.server.app.common.exception.BusinessException;
import project.server.app.core.domain.user.User;
-import project.server.app.core.domain.user.UserRepository;
import project.server.app.core.web.user.application.UserSaveUseCase;
-import project.server.app.core.web.user.application.service.UserService;
-import project.server.app.core.web.user.exception.AlreadyRegisteredUserException;
+import project.server.app.core.web.user.application.service.UserServiceProxy;
import project.server.app.core.web.user.exception.DuplicatedUsernameException;
-import project.server.app.core.web.user.persistence.UserPersistenceRepository;
import project.server.app.test.integrationtest.IntegrationTestBase;
import static project.server.mvc.springframework.context.ApplicationContext.getBean;
@@ -27,28 +17,27 @@
@DisplayName("[IntegrationTest] 사용자 저장 통합 테스트")
class UserSaveIntegrationTest extends IntegrationTestBase {
- private final UserSaveUseCase userSaveUseCase = getBean(UserService.class);
- private final UserRepository userRepository = getBean(UserPersistenceRepository.class);
+ private final UserSaveUseCase userSaveUseCase = getBean(UserServiceProxy.class);
@Test
@DisplayName("사용자가 저장되면 PK가 생성된다.")
void userSaveTest() {
User newUser = new User("Steve-Jobs", "helloworld");
- User savedUser = userSaveUseCase.save(newUser);
+ Long userId = userSaveUseCase.save(newUser.getUsername(), newUser.getPassword());
- assertNotNull(savedUser.getId());
+ assertNotNull(userId);
}
@Test
@DisplayName("사용자가 이미 저장 돼 있다면 AlreadyRegisteredUserException이 발생한다.")
void userSaveFailureTest() {
User newUser = new User("Steve-Jobs", "helloworld");
- userSaveUseCase.save(newUser);
+ userSaveUseCase.save(newUser.getUsername(), newUser.getPassword());
- assertThatThrownBy(() -> userRepository.save(newUser))
+ assertThatThrownBy(() -> userSaveUseCase.save(newUser.getUsername(), newUser.getPassword()))
.isInstanceOf(BusinessException.class)
- .isExactlyInstanceOf(AlreadyRegisteredUserException.class)
- .hasMessage("이미 가입된 사용자 입니다.");
+ .isExactlyInstanceOf(DuplicatedUsernameException.class)
+ .hasMessage("중복된 아이디 입니다.");
}
@Test
@@ -56,38 +45,10 @@ void userSaveFailureTest() {
void duplicatedUsernameSaveTest() {
User newUser = new User("Steve-Jobs", "helloworld");
User duplicatedUser = new User("Steve-Jobs", "helloworld");
- userSaveUseCase.save(newUser);
+ userSaveUseCase.save(newUser.getUsername(), newUser.getPassword());
- assertThatThrownBy(() -> userSaveUseCase.save(duplicatedUser))
+ assertThatThrownBy(() -> userSaveUseCase.save(duplicatedUser.getUsername(), duplicatedUser.getPassword()))
.isInstanceOf(BusinessException.class)
.isExactlyInstanceOf(DuplicatedUsernameException.class);
}
-
- @Test
- @DisplayName("동시에 1_000 명의 사용자가 가입해도 PK값은 유일하다.")
- void userSaveSynchronizedTest() throws InterruptedException {
- int fixedUserCount = 1_000;
- ExecutorService executorService = Executors.newFixedThreadPool(32);
- CountDownLatch latch = new CountDownLatch(fixedUserCount);
-
- Set userIds = ConcurrentHashMap.newKeySet();
- for (int index = 1; index <= fixedUserCount; index++) {
- User newUser = new User("Username" + index, "Password" + index);
- executorService.submit(() -> {
- try {
- User savedUser = userSaveUseCase.save(newUser);
- userIds.add(savedUser.getId());
- } finally {
- latch.countDown();
- }
- });
- }
-
- latch.await();
-
- List findAllUsers = userRepository.findAll();
- log.info("findAllUsers: {}명", findAllUsers.size());
-
- assertEquals(findAllUsers.size(), userIds.size());
- }
}
diff --git a/app/src/test/java/project/server/app/test/integrationtest/user/UserSearchIntegrationTest.java b/app/src/test/java/project/server/app/test/integrationtest/user/UserSearchIntegrationTest.java
index 6a76d20..4d51288 100644
--- a/app/src/test/java/project/server/app/test/integrationtest/user/UserSearchIntegrationTest.java
+++ b/app/src/test/java/project/server/app/test/integrationtest/user/UserSearchIntegrationTest.java
@@ -1,13 +1,12 @@
package project.server.app.test.integrationtest.user;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import project.server.app.core.domain.user.User;
import project.server.app.core.web.user.application.UserSaveUseCase;
import project.server.app.core.web.user.application.UserSearchUseCase;
-import project.server.app.core.web.user.application.service.UserService;
+import project.server.app.core.web.user.application.service.UserServiceProxy;
import project.server.app.core.web.user.exception.UserNotFoundException;
import project.server.app.test.integrationtest.IntegrationTestBase;
import static project.server.mvc.springframework.context.ApplicationContext.getBean;
@@ -15,17 +14,14 @@
@DisplayName("[IntegrationTest] 사용자 조회 통합 테스트")
class UserSearchIntegrationTest extends IntegrationTestBase {
- private final UserSaveUseCase userSaveUseCase = getBean(UserService.class);
- private final UserSearchUseCase userSearchUseCase = getBean(UserService.class);
+ private final UserSaveUseCase userSaveUseCase = getBean(UserServiceProxy.class);
+ private final UserSearchUseCase userSearchUseCase = getBean(UserServiceProxy.class);
@Test
@DisplayName("사용자가 저장되면 PK로 조회할 수 있다.")
void userSearchTest() {
User newUser = new User("Steve-Jobs", "helloworld");
- User savedUser = userSaveUseCase.save(newUser);
-
- User findUser = userSearchUseCase.findById(savedUser.getId());
- assertNotNull(findUser);
+ userSaveUseCase.save(newUser.getUsername(), newUser.getPassword());
}
@Test
diff --git a/app/src/test/java/project/server/app/test/unittest/user/UserUnitTest.java b/app/src/test/java/project/server/app/test/unittest/user/UserUnitTest.java
index 5952b23..1bdc915 100644
--- a/app/src/test/java/project/server/app/test/unittest/user/UserUnitTest.java
+++ b/app/src/test/java/project/server/app/test/unittest/user/UserUnitTest.java
@@ -1,5 +1,6 @@
package project.server.app.test.unittest.user;
+import java.time.LocalDateTime;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
@@ -7,6 +8,8 @@
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static project.server.app.common.fixture.user.UserFixture.createUser;
+import static project.server.app.core.domain.user.Deleted.FALSE;
+import static project.server.app.core.domain.user.Deleted.TRUE;
import project.server.app.core.domain.user.User;
@DisplayName("[UnitTest] 사용자 단위 테스트")
@@ -18,6 +21,34 @@ void userCreateTest() {
assertNotNull(createUser());
}
+ @Test
+ @DisplayName("사용자가 최초로 생성되면 삭제 칼럼 값이 FALSE다.")
+ void userInitDeletedTest() {
+ User user = createUser();
+
+ assertEquals(FALSE, user.getDeleted());
+ }
+
+ @Test
+ @DisplayName("사용자를 삭제하면 deleted 값이 TRUE가 된다.")
+ void userDeleteTest() {
+ User user = createUser();
+ LocalDateTime now = LocalDateTime.now();
+ user.delete(now);
+
+ assertEquals(TRUE, user.getDeleted());
+ }
+
+ @Test
+ @DisplayName("사용자를 삭제하면 최종 수정한 날짜가 현재로 바뀐다.")
+ void userDeleteLastModifiedAtTest() {
+ User user = createUser();
+ LocalDateTime now = LocalDateTime.now();
+ user.delete(now);
+
+ assertEquals(now, user.getLastModifiedAt());
+ }
+
@Test
@DisplayName("equals를 재정의 했을 때, 값이 같다면 같은 객체로 인식한다.")
void userEqualsTest() {
diff --git a/app/src/test/java/project/server/app/test/unittest/user/UserValidatorUnitTest.java b/app/src/test/java/project/server/app/test/unittest/user/UserValidatorUnitTest.java
index e9e0f32..9559bac 100644
--- a/app/src/test/java/project/server/app/test/unittest/user/UserValidatorUnitTest.java
+++ b/app/src/test/java/project/server/app/test/unittest/user/UserValidatorUnitTest.java
@@ -2,6 +2,8 @@
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -10,6 +12,9 @@
import project.server.app.common.exception.InvalidParameterException;
import project.server.app.common.exception.UnAuthorizedException;
import project.server.app.core.web.user.presentation.validator.UserValidator;
+import project.server.mvc.servlet.HttpServletResponse;
+import project.server.mvc.servlet.Response;
+import project.server.mvc.servlet.http.HttpStatus;
@DisplayName("[UnitTest] 사용자 검증 단위 테스트")
class UserValidatorUnitTest {
@@ -42,14 +47,29 @@ void passwordNullOrBlank(String parameter) {
@Test
@DisplayName("사용자 아이디가 null이면 UnAuthorizedException이 발생한다.")
void sessionIdNullTest() {
- assertThatThrownBy(() -> validator.validateSessionId(null))
+ HttpServletResponse response = new Response();
+ assertThatThrownBy(() -> validator.validateSessionId(null, response))
.isInstanceOf(RuntimeException.class)
.isExactlyInstanceOf(UnAuthorizedException.class);
}
+ @Test
+ @DisplayName("사용자 아이디가 null이면 상태가 UnAuthorizedException이 된다.")
+ void sessionIdNullStatusTest() {
+ HttpServletResponse response = new Response();
+
+ assertThrows(UnAuthorizedException.class, () -> {
+ validator.validateSessionId(null, response);
+ }
+ );
+
+ assertEquals(HttpStatus.UN_AUTHORIZED, response.getStatus());
+ }
+
@Test
@DisplayName("사용자 아이디가 null이 아니라면 에러가 발생하지 않는다.")
void sessionIdValidateTest() {
- assertDoesNotThrow(() -> validator.validateSessionId(1L));
+ HttpServletResponse response = new Response();
+ assertDoesNotThrow(() -> validator.validateSessionId(1L, response));
}
}
diff --git a/jdbc/README.md b/jdbc/README.md
new file mode 100644
index 0000000..133fb4e
--- /dev/null
+++ b/jdbc/README.md
@@ -0,0 +1,19 @@
+## ⛺️ Jdbc 모듈
+
+데이터베이스 접근 모듈.
+
+
+
+## 👪 패키지 간 의존관계
+
+Jdbc 모듈은 다른 모듈에 의존하지 않습니다.
+
+| Application Module | Mvc Module | Jdbc Module |
+|:------------------:|:----------:|:-----------:|
+| X | X | - |
+
+ - Application: 애플리케이션 모듈
+ - Mvc: 스프링 Mvc 모듈
+ - Jdbc: 데이터베이스 접근 모듈
+
+
diff --git a/jdbc/build.gradle b/jdbc/build.gradle
new file mode 100644
index 0000000..60edeb5
--- /dev/null
+++ b/jdbc/build.gradle
@@ -0,0 +1,3 @@
+dependencies {
+ implementation("org.apache.commons:commons-dbcp2:2.10.0")
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/ConfigMap.java b/jdbc/src/main/java/project/server/jdbc/core/ConfigMap.java
new file mode 100644
index 0000000..8a97de9
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/ConfigMap.java
@@ -0,0 +1,90 @@
+package project.server.jdbc.core;
+
+public class ConfigMap {
+
+ private SpringConfig spring;
+
+ public SpringConfig getSpring() {
+ return spring;
+ }
+
+ public String getDriverClassName() {
+ return spring.getDriverClassName();
+ }
+
+ public String getUrl() {
+ return spring.getUrl();
+ }
+
+ public String getUsername() {
+ return spring.getUsername();
+ }
+
+ public String getPassword() {
+ return spring.getPassword();
+ }
+
+ static class SpringConfig {
+ private DatasourceConfig datasource;
+
+ public DatasourceConfig getDatasource() {
+ return datasource;
+ }
+
+ public String getDriverClassName() {
+ return datasource.getDriverClassName();
+ }
+
+ public String getUrl() {
+ return datasource.getUrl();
+ }
+
+ public String getUsername() {
+ return datasource.getUsername();
+ }
+
+ public String getPassword() {
+ return datasource.getPassword();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("dataSource:%s", datasource);
+ }
+
+ static class DatasourceConfig {
+ private String driverClassName;
+ private String url;
+ private String username;
+ private String password;
+
+ public String getDriverClassName() {
+ return driverClassName;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "driverClassName:%s, url:%s, username:%s, password:%s", driverClassName, url, username, password
+ );
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(spring);
+ }
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/ConnectionHolder.java b/jdbc/src/main/java/project/server/jdbc/core/ConnectionHolder.java
new file mode 100644
index 0000000..54b047a
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/ConnectionHolder.java
@@ -0,0 +1,43 @@
+package project.server.jdbc.core;
+
+import java.sql.Connection;
+import java.util.Objects;
+
+public class ConnectionHolder {
+
+ private String uuid;
+ private Connection connection;
+
+ public Connection getConnection() {
+ return connection;
+ }
+
+ public String getId() {
+ return uuid;
+ }
+
+ public void setId(String uniqueId) {
+ this.uuid = uniqueId;
+ }
+
+ public void setConnection(Connection connection) {
+ this.connection = connection;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object == null || getClass() != object.getClass()) {
+ return false;
+ }
+ ConnectionHolder that = (ConnectionHolder) object;
+ return uuid.equals(that.uuid);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(uuid);
+ }
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/DataSourceUtils.java b/jdbc/src/main/java/project/server/jdbc/core/DataSourceUtils.java
new file mode 100644
index 0000000..3e823d4
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/DataSourceUtils.java
@@ -0,0 +1,38 @@
+package project.server.jdbc.core;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import javax.sql.DataSource;
+import lombok.extern.slf4j.Slf4j;
+import static project.server.jdbc.core.transaction.TransactionSynchronizationManager.releaseResource;
+
+@Slf4j
+public final class DataSourceUtils {
+
+ public static void releaseConnection(
+ DataSource dataSource,
+ Connection connection
+ ) {
+ try {
+ doReleaseConnection(connection);
+ } catch (SQLException exception) {
+ log.error("SQLException.", exception);
+ } catch (Throwable exception) {
+ log.error("Exception.", exception);
+ } finally {
+ releaseResource(dataSource);
+ }
+ }
+
+ public static void doReleaseConnection(Connection connection) throws SQLException {
+ if (connection == null) {
+ return;
+ }
+ doCloseConnection(connection);
+ }
+
+ public static void doCloseConnection(Connection connection) throws SQLException {
+ connection.close();
+ log.debug("Connection closed.");
+ }
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/DriverManager.java b/jdbc/src/main/java/project/server/jdbc/core/DriverManager.java
new file mode 100644
index 0000000..f3b1746
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/DriverManager.java
@@ -0,0 +1,26 @@
+package project.server.jdbc.core;
+
+import javax.sql.DataSource;
+import org.apache.commons.dbcp2.BasicDataSource;
+
+public class DriverManager {
+
+ private final DataSource dataSource;
+
+ public DriverManager(ConfigMap configMap) {
+ this.dataSource = createDataSource(configMap);
+ }
+
+ private DataSource createDataSource(ConfigMap configMap) {
+ BasicDataSource dataSource = new BasicDataSource();
+ dataSource.setDriverClassName(configMap.getDriverClassName());
+ dataSource.setUrl(configMap.getUrl());
+ dataSource.setUsername(configMap.getUsername());
+ dataSource.setPassword(configMap.getPassword());
+ return dataSource;
+ }
+
+ public DataSource getDataSource() {
+ return dataSource;
+ }
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/exception/DataAccessException.java b/jdbc/src/main/java/project/server/jdbc/core/exception/DataAccessException.java
new file mode 100644
index 0000000..fccc71a
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/exception/DataAccessException.java
@@ -0,0 +1,19 @@
+package project.server.jdbc.core.exception;
+
+public class DataAccessException extends RuntimeException {
+
+ private static final String INTERNAL_SERVER_ERROR = "서버 내부 오류입니다.";
+
+ public DataAccessException() {
+ super(INTERNAL_SERVER_ERROR);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "{\"code\":%d, \"message\":\"%s\"}",
+ 500,
+ INTERNAL_SERVER_ERROR
+ );
+ }
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/exception/InvalidDataAccessException.java b/jdbc/src/main/java/project/server/jdbc/core/exception/InvalidDataAccessException.java
new file mode 100644
index 0000000..d72a348
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/exception/InvalidDataAccessException.java
@@ -0,0 +1,4 @@
+package project.server.jdbc.core.exception;
+
+public class InvalidDataAccessException extends RuntimeException {
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/jdbc/ConnectionCallback.java b/jdbc/src/main/java/project/server/jdbc/core/jdbc/ConnectionCallback.java
new file mode 100644
index 0000000..c12b537
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/jdbc/ConnectionCallback.java
@@ -0,0 +1,9 @@
+package project.server.jdbc.core.jdbc;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+
+@FunctionalInterface
+public interface ConnectionCallback {
+ T doInConnection(Connection connection) throws SQLException;
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/jdbc/InitializingBean.java b/jdbc/src/main/java/project/server/jdbc/core/jdbc/InitializingBean.java
new file mode 100644
index 0000000..d9bb842
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/jdbc/InitializingBean.java
@@ -0,0 +1,5 @@
+package project.server.jdbc.core.jdbc;
+
+public interface InitializingBean {
+ void afterPropertiesSet() throws Exception;
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcAccessor.java b/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcAccessor.java
new file mode 100644
index 0000000..90a7495
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcAccessor.java
@@ -0,0 +1,18 @@
+package project.server.jdbc.core.jdbc;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import javax.sql.DataSource;
+
+public abstract class JdbcAccessor implements InitializingBean {
+
+ private DataSource dataSource;
+
+ public JdbcAccessor(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public Connection getConnection() throws SQLException {
+ return dataSource.getConnection();
+ }
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcHelper.java b/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcHelper.java
new file mode 100644
index 0000000..a259bfd
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcHelper.java
@@ -0,0 +1,63 @@
+package project.server.jdbc.core.jdbc;
+
+import java.util.Arrays;
+import static java.util.stream.Collectors.joining;
+
+public final class JdbcHelper {
+
+ private static final String ENGLISH_ALPHA = "([a-z])([A-Z]+)";
+ private static final String REPLACEMENT_A_TO_B = "$1_$2";
+
+ private JdbcHelper() {
+ throw new AssertionError("올바른 방식으로 생성자를 호출해주세요.");
+ }
+
+ public static String insert() {
+ return "INSERT INTO user "
+ + "(username, password, created_at, last_modified_at, deleted) "
+ + "VALUES (?, ?, ?, ?, ?)";
+ }
+
+ public static String selectBy(Class> clazz) {
+ return "SELECT * FROM "
+ + convertCamelToSnake(clazz.getSimpleName())
+ + " WHERE id = ? AND deleted = 'FALSE'";
+ }
+
+ public static String selectBy(
+ Class> clazz,
+ String... fieldNames
+ ) {
+ String tableName = convertCamelToSnake(clazz.getSimpleName());
+ String whereClause = Arrays.stream(fieldNames)
+ .map(fieldName -> convertCamelToSnake(fieldName) + " = ?")
+ .collect(joining(" AND "));
+ return "SELECT * FROM " + tableName + " WHERE " + whereClause;
+ }
+
+ public static String update(
+ Class> clazz,
+ String fieldName
+ ) {
+ return "UPDATE "
+ + convertCamelToSnake(clazz.getSimpleName())
+ + " SET "
+ + convertCamelToSnake(fieldName)
+ + " = 'TRUE' WHERE id = ?";
+ }
+
+ public static String selectAll(Class> clazz) {
+ return "SELECT * FROM "
+ + convertCamelToSnake(clazz.getSimpleName());
+ }
+
+ public static String truncate(Class> clazz) {
+ return "TRUNCATE TABLE "
+ + convertCamelToSnake(clazz.getSimpleName());
+ }
+
+ public static String convertCamelToSnake(String value) {
+ return value.replaceAll(ENGLISH_ALPHA, REPLACEMENT_A_TO_B)
+ .toLowerCase();
+ }
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcOperations.java b/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcOperations.java
new file mode 100644
index 0000000..0c7c50f
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcOperations.java
@@ -0,0 +1,15 @@
+package project.server.jdbc.core.jdbc;
+
+import java.util.List;
+
+public interface JdbcOperations {
+ T queryForObject(ConnectionCallback action);
+
+ T queryForObject(String sql, PreparedStatementCallback action);
+
+ T queryForObject(String sql, PreparedStatementSetter pss, ResultSetExtractor rse);
+
+ List queryForList(String sql, RowMapper rowMapper, Object... params);
+
+ void execute(String sql);
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcTemplate.java b/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcTemplate.java
new file mode 100644
index 0000000..13458e2
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcTemplate.java
@@ -0,0 +1,111 @@
+package project.server.jdbc.core.jdbc;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import javax.sql.DataSource;
+import lombok.extern.slf4j.Slf4j;
+import project.server.jdbc.core.exception.DataAccessException;
+
+@Slf4j
+public class JdbcTemplate extends JdbcAccessor implements JdbcOperations {
+
+ public JdbcTemplate(DataSource dataSource) {
+ super(dataSource);
+ }
+
+ @Override
+ public T queryForObject(ConnectionCallback action) {
+ try (Connection conn = getConnection()) {
+ return action.doInConnection(conn);
+ } catch (SQLException exception) {
+ log.error("SQLException: {}", exception.getMessage());
+ throw new DataAccessException();
+ }
+ }
+
+ @Override
+ public T queryForObject(
+ String sql,
+ PreparedStatementCallback action
+ ) {
+ return queryForObject(conn -> {
+ try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
+ return action.doInPreparedStatement(pstmt);
+ } catch (SQLException exception) {
+ log.error("SQLException: {}", exception.getMessage());
+ throw new DataAccessException();
+ }
+ });
+ }
+
+ @Override
+ public T queryForObject(
+ String sql,
+ PreparedStatementSetter pss,
+ ResultSetExtractor rse
+ ) {
+ return queryForObject(sql, pstmt -> {
+ pss.setValues(pstmt);
+ try (ResultSet rs = pstmt.executeQuery()) {
+ return rse.extractData(rs);
+ } catch (SQLException exception) {
+ log.error("SQLException: {}", exception.getMessage());
+ throw new DataAccessException();
+ }
+ });
+ }
+
+ @Override
+ public List queryForList(
+ String sql,
+ RowMapper rowMapper,
+ Object... params
+ ) {
+ return queryForObject(
+ sql, pstmt ->
+ setParameters(pstmt, params), rs -> {
+ List results = new ArrayList<>();
+ while (rs.next()) {
+ results.add(rowMapper.mapRow(rs));
+ }
+ return results;
+ }
+ );
+ }
+
+ @Override
+ public void execute(String sql) {
+ try (
+ Connection conn = getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(sql)
+ ) {
+ pstmt.executeUpdate();
+ } catch (SQLException exception) {
+ log.error("SQLException: {}", exception.getMessage());
+ throw new DataAccessException();
+ }
+ }
+
+ private void setParameters(
+ PreparedStatement pstmt,
+ Object... params
+ ) {
+ try {
+ for (int index = 0; index < params.length; index++) {
+ pstmt.setObject(index + 1, params[index]);
+ }
+ } catch (SQLException exception) {
+ log.error("SQLException: {}", exception.getMessage());
+ throw new DataAccessException();
+ }
+ }
+
+ @Override
+ public void afterPropertiesSet() {
+ log.info("AfterProperties.");
+ }
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/jdbc/PreparedStatementCallback.java b/jdbc/src/main/java/project/server/jdbc/core/jdbc/PreparedStatementCallback.java
new file mode 100644
index 0000000..64a5d1f
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/jdbc/PreparedStatementCallback.java
@@ -0,0 +1,9 @@
+package project.server.jdbc.core.jdbc;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+
+@FunctionalInterface
+public interface PreparedStatementCallback {
+ T doInPreparedStatement(PreparedStatement pstmt) throws SQLException;
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/jdbc/PreparedStatementSetter.java b/jdbc/src/main/java/project/server/jdbc/core/jdbc/PreparedStatementSetter.java
new file mode 100644
index 0000000..78ad86a
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/jdbc/PreparedStatementSetter.java
@@ -0,0 +1,9 @@
+package project.server.jdbc.core.jdbc;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+
+@FunctionalInterface
+public interface PreparedStatementSetter {
+ void setValues(PreparedStatement pstmt) throws SQLException;
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/jdbc/ResultSetExtractor.java b/jdbc/src/main/java/project/server/jdbc/core/jdbc/ResultSetExtractor.java
new file mode 100644
index 0000000..332384c
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/jdbc/ResultSetExtractor.java
@@ -0,0 +1,9 @@
+package project.server.jdbc.core.jdbc;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+@FunctionalInterface
+public interface ResultSetExtractor {
+ T extractData(ResultSet resultSet) throws SQLException;
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/jdbc/RowMapper.java b/jdbc/src/main/java/project/server/jdbc/core/jdbc/RowMapper.java
new file mode 100644
index 0000000..82892ae
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/jdbc/RowMapper.java
@@ -0,0 +1,9 @@
+package project.server.jdbc.core.jdbc;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+@FunctionalInterface
+public interface RowMapper {
+ T mapRow(ResultSet rs) throws SQLException;
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/transaction/AbstractPlatformTransactionManager.java b/jdbc/src/main/java/project/server/jdbc/core/transaction/AbstractPlatformTransactionManager.java
new file mode 100644
index 0000000..4a28793
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/transaction/AbstractPlatformTransactionManager.java
@@ -0,0 +1,24 @@
+package project.server.jdbc.core.transaction;
+
+import java.sql.SQLException;
+
+public abstract class AbstractPlatformTransactionManager implements PlatformTransactionManager {
+
+ @Override
+ public final TransactionStatus getTransaction(TransactionDefinition definition) throws SQLException {
+ Object transaction = doGetTransaction();
+ return startTransaction(definition, transaction);
+ }
+
+ private TransactionStatus startTransaction(
+ TransactionDefinition definition,
+ Object transaction
+ ) throws SQLException {
+ doBegin(transaction, definition);
+ return new DefaultTransactionStatus(transaction, true, false);
+ }
+
+ abstract void doBegin(Object transaction, TransactionDefinition definition) throws SQLException;
+
+ protected abstract Object doGetTransaction();
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/transaction/AbstractTransactionStatus.java b/jdbc/src/main/java/project/server/jdbc/core/transaction/AbstractTransactionStatus.java
new file mode 100644
index 0000000..a316c9b
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/transaction/AbstractTransactionStatus.java
@@ -0,0 +1,6 @@
+package project.server.jdbc.core.transaction;
+
+import project.server.jdbc.core.transaction.TransactionStatus;
+
+public abstract class AbstractTransactionStatus implements TransactionStatus {
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/transaction/DataSourceTransactionManager.java b/jdbc/src/main/java/project/server/jdbc/core/transaction/DataSourceTransactionManager.java
new file mode 100644
index 0000000..63b7b5e
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/transaction/DataSourceTransactionManager.java
@@ -0,0 +1,79 @@
+package project.server.jdbc.core.transaction;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import javax.sql.DataSource;
+import lombok.extern.slf4j.Slf4j;
+import static project.server.jdbc.core.DataSourceUtils.releaseConnection;
+import project.server.jdbc.core.exception.DataAccessException;
+import static project.server.jdbc.core.transaction.TransactionSynchronizationManager.bindResource;
+
+@Slf4j
+public class DataSourceTransactionManager extends AbstractPlatformTransactionManager {
+
+ private final DataSource dataSource;
+
+ public DataSourceTransactionManager(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ @Override
+ void doBegin(
+ Object transaction,
+ TransactionDefinition definition
+ ) throws SQLException {
+ DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
+ Connection connection = dataSource.getConnection();
+ log.info("Get connection.");
+
+ txObject.setConnection(definition.getId(), connection);
+ connection.setAutoCommit(false);
+ log.info("Set auto-commit false.");
+ prepareConnectionForTransaction(connection, definition);
+
+ bindResource(dataSource, txObject.getConnectionHolder());
+ }
+
+ private void prepareConnectionForTransaction(
+ Connection connection,
+ TransactionDefinition definition
+ ) throws SQLException {
+ if (definition != null && definition.isReadOnly()) {
+ connection.setReadOnly(true);
+ }
+ }
+
+ @Override
+ public void commit(TransactionStatus status) {
+ DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
+ Connection connection = txObject.getConnection();
+
+ try {
+ connection.commit();
+ } catch (Exception exception) {
+ throw new DataAccessException();
+ } finally {
+ releaseConnection(dataSource, connection);
+ }
+ }
+
+ @Override
+ public void rollback(TransactionStatus status) {
+ DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
+ Connection connection = txObject.getConnection();
+
+ try {
+ connection.rollback();
+ } catch (Exception exception) {
+ throw new RuntimeException();
+ } finally {
+ releaseConnection(dataSource, connection);
+ log.debug("Rollback, transaction failed.");
+ }
+ }
+
+ @Override
+ protected Object doGetTransaction() {
+ return new DataSourceTransactionObject();
+ }
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/transaction/DataSourceTransactionObject.java b/jdbc/src/main/java/project/server/jdbc/core/transaction/DataSourceTransactionObject.java
new file mode 100644
index 0000000..50bd498
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/transaction/DataSourceTransactionObject.java
@@ -0,0 +1,38 @@
+package project.server.jdbc.core.transaction;
+
+import java.sql.Connection;
+import project.server.jdbc.core.ConnectionHolder;
+
+public class DataSourceTransactionObject {
+
+ private final ConnectionHolder connectionHolder;
+
+ public DataSourceTransactionObject() {
+ this.connectionHolder = new ConnectionHolder();
+ }
+
+ public String getId() {
+ return connectionHolder.getId();
+ }
+
+ public Connection getConnection() {
+ return connectionHolder.getConnection();
+ }
+
+ public ConnectionHolder getConnectionHolder() {
+ return connectionHolder;
+ }
+
+ public void setConnection(
+ String uniqueId,
+ Connection connection
+ ) {
+ this.connectionHolder.setId(uniqueId);
+ this.connectionHolder.setConnection(connection);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("id: %s", connectionHolder.getId());
+ }
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/transaction/DefaultTransactionDefinition.java b/jdbc/src/main/java/project/server/jdbc/core/transaction/DefaultTransactionDefinition.java
new file mode 100644
index 0000000..c7537f5
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/transaction/DefaultTransactionDefinition.java
@@ -0,0 +1,28 @@
+package project.server.jdbc.core.transaction;
+
+import static java.util.UUID.randomUUID;
+
+public class DefaultTransactionDefinition implements TransactionDefinition {
+
+ private final String id;
+ private final boolean readOnly;
+
+ public DefaultTransactionDefinition() {
+ this.id = randomUUID().toString();
+ this.readOnly = false;
+ }
+
+ public DefaultTransactionDefinition(boolean readOnly) {
+ this.id = randomUUID().toString();
+ this.readOnly = readOnly;
+ }
+
+ public static DefaultTransactionDefinition createTransactionDefinition(boolean readOnly) {
+ return new DefaultTransactionDefinition(readOnly);
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/transaction/DefaultTransactionStatus.java b/jdbc/src/main/java/project/server/jdbc/core/transaction/DefaultTransactionStatus.java
new file mode 100644
index 0000000..af090fe
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/transaction/DefaultTransactionStatus.java
@@ -0,0 +1,28 @@
+package project.server.jdbc.core.transaction;
+
+public class DefaultTransactionStatus extends AbstractTransactionStatus {
+
+ private final Object transaction;
+ private final boolean newTransaction;
+ private final boolean readOnly;
+
+ public DefaultTransactionStatus(
+ Object transaction,
+ boolean newTransaction,
+ boolean readOnly
+ ) {
+ this.transaction = transaction;
+ this.newTransaction = newTransaction;
+ this.readOnly = readOnly;
+ }
+
+ @Override
+ public Object getTransaction() {
+ return transaction;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s, newTransaction:%s, readOnly:%s", transaction, newTransaction, readOnly);
+ }
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/transaction/JdbcTransactionManager.java b/jdbc/src/main/java/project/server/jdbc/core/transaction/JdbcTransactionManager.java
new file mode 100644
index 0000000..58b4db0
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/transaction/JdbcTransactionManager.java
@@ -0,0 +1,10 @@
+package project.server.jdbc.core.transaction;
+
+import javax.sql.DataSource;
+
+public class JdbcTransactionManager extends DataSourceTransactionManager {
+
+ public JdbcTransactionManager(DataSource dataSource) {
+ super(dataSource);
+ }
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/transaction/PlatformTransactionManager.java b/jdbc/src/main/java/project/server/jdbc/core/transaction/PlatformTransactionManager.java
new file mode 100644
index 0000000..1911fcd
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/transaction/PlatformTransactionManager.java
@@ -0,0 +1,11 @@
+package project.server.jdbc.core.transaction;
+
+import java.sql.SQLException;
+
+public interface PlatformTransactionManager extends TransactionManager {
+ TransactionStatus getTransaction(TransactionDefinition definition) throws SQLException;
+
+ void commit(TransactionStatus status);
+
+ void rollback(TransactionStatus status);
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/transaction/TransactionDefinition.java b/jdbc/src/main/java/project/server/jdbc/core/transaction/TransactionDefinition.java
new file mode 100644
index 0000000..c8bf64e
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/transaction/TransactionDefinition.java
@@ -0,0 +1,9 @@
+package project.server.jdbc.core.transaction;
+
+public interface TransactionDefinition {
+ String getId();
+
+ default boolean isReadOnly() {
+ return false;
+ }
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/transaction/TransactionManager.java b/jdbc/src/main/java/project/server/jdbc/core/transaction/TransactionManager.java
new file mode 100644
index 0000000..b3fcc94
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/transaction/TransactionManager.java
@@ -0,0 +1,4 @@
+package project.server.jdbc.core.transaction;
+
+public interface TransactionManager {
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/transaction/TransactionStatus.java b/jdbc/src/main/java/project/server/jdbc/core/transaction/TransactionStatus.java
new file mode 100644
index 0000000..26808d2
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/transaction/TransactionStatus.java
@@ -0,0 +1,5 @@
+package project.server.jdbc.core.transaction;
+
+public interface TransactionStatus {
+ Object getTransaction();
+}
diff --git a/jdbc/src/main/java/project/server/jdbc/core/transaction/TransactionSynchronizationManager.java b/jdbc/src/main/java/project/server/jdbc/core/transaction/TransactionSynchronizationManager.java
new file mode 100644
index 0000000..d7f28a3
--- /dev/null
+++ b/jdbc/src/main/java/project/server/jdbc/core/transaction/TransactionSynchronizationManager.java
@@ -0,0 +1,38 @@
+package project.server.jdbc.core.transaction;
+
+import java.util.HashMap;
+import java.util.Map;
+import javax.sql.DataSource;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public final class TransactionSynchronizationManager {
+
+ private static final ThreadLocal> factory = new ThreadLocal<>();
+
+ private TransactionSynchronizationManager() {
+ throw new AssertionError("올바른 방식으로 생성자를 호출해주세요.");
+ }
+
+ public static void bindResource(
+ DataSource key,
+ Object value
+ ) {
+ Map transactionMap = getTransactionMap();
+ transactionMap.put(key, value);
+ log.debug("Put {} into threadlocal.", key);
+ }
+
+ private static Map getTransactionMap() {
+ if (factory.get() == null) {
+ factory.set(new HashMap<>());
+ }
+ return factory.get();
+ }
+
+ public static void releaseResource(DataSource key) {
+ Map transactionMap = factory.get();
+ transactionMap.remove(key);
+ log.debug("Remove {} from threadlocal.", key);
+ }
+}
diff --git a/mvc/README.md b/mvc/README.md
new file mode 100644
index 0000000..29c4355
--- /dev/null
+++ b/mvc/README.md
@@ -0,0 +1,19 @@
+## ⛺️ Mvc 모듈
+
+스프링 Mvc 모듈.
+
+
+
+## 👪 패키지 간 의존관계
+
+Mvc 모듈은 다른 모듈에 의존하지 않습니다.
+
+| Application Module | Mvc Module | Jdbc Module |
+|:------------------:|:----------:|:-----------:|
+| X | - | X |
+
+ - Application: 애플리케이션 모듈
+ - Mvc: 스프링 Mvc 모듈
+ - Jdbc: 데이터베이스 접근 모듈
+
+
diff --git a/mvc/build.gradle b/mvc/build.gradle
index 5e29569..34a1707 100644
--- a/mvc/build.gradle
+++ b/mvc/build.gradle
@@ -1,3 +1,4 @@
dependencies {
implementation("org.reflections:reflections:${reflectionsVersion}")
+ implementation("com.fasterxml.jackson.core:jackson-databind:2.16.1")
}
diff --git a/mvc/src/main/java/project/server/mvc/Acceptor.java b/mvc/src/main/java/project/server/mvc/Acceptor.java
index 1ce1a67..79b8a37 100644
--- a/mvc/src/main/java/project/server/mvc/Acceptor.java
+++ b/mvc/src/main/java/project/server/mvc/Acceptor.java
@@ -3,25 +3,33 @@
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
+import static java.nio.ByteBuffer.allocate;
import java.nio.channels.SelectionKey;
+import static java.nio.channels.SelectionKey.OP_READ;
+import static java.nio.channels.SelectionKey.OP_WRITE;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
-import lombok.SneakyThrows;
+import java.util.concurrent.ExecutorService;
+import static java.util.concurrent.Executors.newFixedThreadPool;
import lombok.extern.slf4j.Slf4j;
-import project.server.mvc.tomcat.AsyncRequest;
+import project.server.mvc.servlet.HttpServletResponse;
+import static project.server.mvc.servlet.http.HttpStatus.INTERNAL_SERVER_ERROR;
+import project.server.mvc.tomcat.AbstractEndpoint;
import project.server.mvc.tomcat.Nio2EndPoint;
+import project.server.mvc.tomcat.NioSocketWrapper;
@Slf4j
public class Acceptor implements Runnable {
- private static final int FINISHED = -1;
+ private static final int FIXED_THREAD_COUNT = 32;
private static final int BUFFER_CAPACITY = 1024;
private final Selector selector;
- private final Nio2EndPoint nio2EndPoint;
+ private final AbstractEndpoint nio2EndPoint;
+ private final ExecutorService service = newFixedThreadPool(FIXED_THREAD_COUNT);
public Acceptor(
int port,
@@ -40,22 +48,25 @@ private void initContext(int port) throws IOException {
}
@Override
- @SneakyThrows
public void run() {
while (true) {
- selector.select();
- Set selectedKeys = selector.selectedKeys();
- Iterator keys = selectedKeys.iterator();
-
- while (keys.hasNext()) {
- SelectionKey key = keys.next();
-
- if (key.isAcceptable()) {
- acceptSocket(key, selector);
- } else if (key.isReadable()) {
- read(key);
+ try {
+ selector.select();
+ Set selectedKeys = selector.selectedKeys();
+ Iterator keys = selectedKeys.iterator();
+ while (keys.hasNext()) {
+ SelectionKey key = keys.next();
+ if (key.isAcceptable()) {
+ acceptSocket(key, selector);
+ } else if (key.isReadable()) {
+ read(key);
+ } else if (key.isWritable()) {
+ write(key);
+ }
+ keys.remove();
}
- keys.remove();
+ } catch (IOException exception) {
+ throw new RuntimeException(exception);
}
}
}
@@ -63,25 +74,48 @@ public void run() {
private void acceptSocket(
SelectionKey key,
Selector selector
- ) throws IOException {
+ ) {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
- SocketChannel socketChannel = serverChannel.accept();
- socketChannel.configureBlocking(false);
- socketChannel.register(selector, SelectionKey.OP_READ);
+ try {
+ SocketChannel socketChannel = serverChannel.accept();
+ socketChannel.configureBlocking(false);
+ socketChannel.register(selector, OP_READ);
+ } catch (IOException exception) {
+ throw new RuntimeException(exception);
+ }
}
- private void read(SelectionKey key) throws Exception {
- SocketChannel socketChannel = (SocketChannel) key.channel();
- ByteBuffer buffer = ByteBuffer.allocate(BUFFER_CAPACITY);
- int bytesRead = socketChannel.read(buffer);
- if (bytesRead == FINISHED) {
- socketChannel.close();
- log.info("Connection closed by client.");
- return;
+ private void read(SelectionKey key) {
+ log.debug("READ");
+ NioSocketWrapper socketWrapper = null;
+ try {
+ SocketChannel socketChannel = (SocketChannel) key.channel();
+ ByteBuffer buffer = allocate(BUFFER_CAPACITY);
+ socketWrapper = new NioSocketWrapper(socketChannel, buffer);
+ socketWrapper.flip();
+
+ socketChannel.register(selector, OP_WRITE);
+ key.attach(socketWrapper);
+ } catch (IOException exception) {
+ if (socketWrapper != null) {
+ HttpServletResponse response = socketWrapper.getResponse();
+ response.setStatus(INTERNAL_SERVER_ERROR);
+ }
}
+ }
- buffer.flip();
- new AsyncRequest(socketChannel, buffer)
- .run();
+ private void write(SelectionKey key) {
+ log.debug("WRITE");
+ SocketChannel socketChannel = (SocketChannel) key.channel();
+ service.submit((Runnable) key.attachment());
+ try {
+ socketChannel.register(selector, OP_READ);
+ } catch (IOException exception) {
+ Object object = key.attachment();
+ if (object != null) {
+ HttpServletResponse response = (HttpServletResponse) object;
+ response.setStatus(INTERNAL_SERVER_ERROR);
+ }
+ }
}
}
diff --git a/mvc/src/main/java/project/server/mvc/servlet/HttpServletResponse.java b/mvc/src/main/java/project/server/mvc/servlet/HttpServletResponse.java
index 44a42d3..996542a 100644
--- a/mvc/src/main/java/project/server/mvc/servlet/HttpServletResponse.java
+++ b/mvc/src/main/java/project/server/mvc/servlet/HttpServletResponse.java
@@ -1,19 +1,24 @@
package project.server.mvc.servlet;
+import java.io.IOException;
import java.nio.channels.SocketChannel;
import project.server.mvc.servlet.http.Cookie;
import project.server.mvc.servlet.http.HttpStatus;
public interface HttpServletResponse extends ServletResponse {
- String getStatusAsString();
+ String getHttpHeaderLine();
+
+ HttpStatus getStatus();
void setStatus(HttpStatus status);
void addCookie(Cookie cookie);
- String getCookiesAsString();
+ void setHeader(String key, String value);
- SocketChannel getSocketChannel();
+ void setBody(String body);
- HttpStatus getStatus();
+ void write(String data) throws IOException;
+
+ void write(byte[] data) throws IOException;
}
diff --git a/mvc/src/main/java/project/server/mvc/servlet/Request.java b/mvc/src/main/java/project/server/mvc/servlet/Request.java
index a0628e8..161006b 100644
--- a/mvc/src/main/java/project/server/mvc/servlet/Request.java
+++ b/mvc/src/main/java/project/server/mvc/servlet/Request.java
@@ -119,6 +119,6 @@ public String getHeader(String key) {
@Override
public String toString() {
- return String.format("%s%s\r\n%s", requestLine, headers, requestBody);
+ return String.format("%s%s%s", requestLine, headers, requestBody);
}
}
diff --git a/mvc/src/main/java/project/server/mvc/servlet/Response.java b/mvc/src/main/java/project/server/mvc/servlet/Response.java
index 498aa00..e10fa85 100644
--- a/mvc/src/main/java/project/server/mvc/servlet/Response.java
+++ b/mvc/src/main/java/project/server/mvc/servlet/Response.java
@@ -1,46 +1,49 @@
package project.server.mvc.servlet;
-import java.io.OutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
+import static java.nio.charset.StandardCharsets.UTF_8;
import project.server.mvc.servlet.http.Cookie;
import project.server.mvc.servlet.http.HttpHeaders;
import project.server.mvc.servlet.http.HttpStatus;
-import static project.server.mvc.servlet.http.HttpStatus.OK;
+import project.server.mvc.servlet.http.ResponseBody;
+import project.server.mvc.servlet.http.StatusLine;
public class Response implements HttpServletResponse {
- private HttpStatus status;
+ private final StatusLine statusLine;
private final HttpHeaders headers;
+ private final ResponseBody responseBody;
private final SocketChannel socketChannel;
- private final OutputStream outputStream;
- public Response(
- SocketChannel socketChannel,
- OutputStream outputStream
- ) {
- this.socketChannel = socketChannel;
- this.status = OK;
+ public Response() {
+ this.statusLine = new StatusLine();
+ this.socketChannel = null;
this.headers = new HttpHeaders();
- this.outputStream = outputStream;
+ this.responseBody = new ResponseBody();
}
- public Response(OutputStream outputStream) {
- this(null, outputStream);
+ public Response(SocketChannel socketChannel) {
+ this.statusLine = new StatusLine();
+ this.socketChannel = socketChannel;
+ this.headers = new HttpHeaders();
+ this.responseBody = new ResponseBody();
}
@Override
- public OutputStream getOutputStream() {
- return outputStream;
+ public String getHttpHeaderLine() {
+ return String.format("%s%s", statusLine, headers);
}
@Override
- public String getStatusAsString() {
- return status.getStatus();
+ public HttpStatus getStatus() {
+ return statusLine.getHttpStatus();
}
@Override
public void setStatus(HttpStatus status) {
- this.status = status;
+ statusLine.setStatus(status);
}
@Override
@@ -49,17 +52,36 @@ public void addCookie(Cookie cookie) {
}
@Override
- public String getCookiesAsString() {
- return headers.getCookiesAsString();
+ public void setHeader(
+ String key,
+ String value
+ ) {
+ headers.addHeader(key, value);
}
@Override
- public SocketChannel getSocketChannel() {
- return socketChannel;
+ public void setBody(String body) {
+ this.responseBody.setBody(body);
}
@Override
- public HttpStatus getStatus() {
- return status;
+ public void write(String data) throws IOException {
+ ByteBuffer headerBuffer = ByteBuffer.wrap(data.getBytes(UTF_8));
+ while (headerBuffer.hasRemaining()) {
+ socketChannel.write(headerBuffer);
+ }
+ }
+
+ @Override
+ public void write(byte[] data) throws IOException {
+ ByteBuffer buffer = ByteBuffer.wrap(data);
+ while (buffer.hasRemaining()) {
+ socketChannel.write(buffer);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s%s%s", statusLine, headers, responseBody);
}
}
diff --git a/mvc/src/main/java/project/server/mvc/servlet/ServletResponse.java b/mvc/src/main/java/project/server/mvc/servlet/ServletResponse.java
index 370e3ed..7d89ae9 100644
--- a/mvc/src/main/java/project/server/mvc/servlet/ServletResponse.java
+++ b/mvc/src/main/java/project/server/mvc/servlet/ServletResponse.java
@@ -1,8 +1,4 @@
package project.server.mvc.servlet;
-import java.io.IOException;
-import java.io.OutputStream;
-
public interface ServletResponse {
- OutputStream getOutputStream() throws IOException;
}
diff --git a/mvc/src/main/java/project/server/mvc/servlet/http/Cookies.java b/mvc/src/main/java/project/server/mvc/servlet/http/Cookies.java
index 9ec3ea8..d4ffa7c 100644
--- a/mvc/src/main/java/project/server/mvc/servlet/http/Cookies.java
+++ b/mvc/src/main/java/project/server/mvc/servlet/http/Cookies.java
@@ -6,11 +6,13 @@
public class Cookies {
+ private static final String EMPTY_STRING = "";
private static final int KEY = 0;
private static final int VALUE = 1;
private static final String DELIMITER = "=";
private static final String COOKIE_DELIMITER = "; ";
- private static final String CARRIAGE_RETURN = "\r\n";
+ private static final String SET_COOKIE_DELIMITER = ": ";
+ private static final String SET_COOKIE = "Set-Cookie";
private final Map cookiesMap;
public static final Cookies emptyCookies = new Cookies();
@@ -56,15 +58,20 @@ public void add(Cookie cookie) {
@Override
public String toString() {
+ if (cookiesMap.isEmpty()) {
+ return EMPTY_STRING;
+ }
+
StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append(SET_COOKIE)
+ .append(SET_COOKIE_DELIMITER);
+
for (String key : cookiesMap.keySet()) {
- if (!stringBuilder.isEmpty()) {
- stringBuilder.append(COOKIE_DELIMITER);
- }
stringBuilder.append(key)
.append(DELIMITER)
- .append(cookiesMap.get(key).value());
+ .append(cookiesMap.get(key).value())
+ .append(COOKIE_DELIMITER);
}
- return stringBuilder.toString().trim() + CARRIAGE_RETURN;
+ return stringBuilder.toString().trim();
}
}
diff --git a/mvc/src/main/java/project/server/mvc/servlet/http/HttpHeaders.java b/mvc/src/main/java/project/server/mvc/servlet/http/HttpHeaders.java
index def2c27..019a0c2 100644
--- a/mvc/src/main/java/project/server/mvc/servlet/http/HttpHeaders.java
+++ b/mvc/src/main/java/project/server/mvc/servlet/http/HttpHeaders.java
@@ -2,18 +2,18 @@
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import static java.util.Collections.emptyList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.stream.Collectors;
+import static java.util.stream.Collectors.joining;
public class HttpHeaders {
private static final int KEY = 0;
private static final int VALUE = 1;
private static final String HEADER_DELIMITER = ": ";
+ private static final String HEADER_JOINING_DELIMITER = ", ";
private static final String MULTI_VALUE_DELIMITER = ",";
private static final String CARRIAGE_RETURN = "\r\n";
private static final String COOKIE = "Cookie";
@@ -94,38 +94,38 @@ public void addCookie(Cookie cookie) {
this.cookies.add(cookie);
}
- public String getCookiesAsString() {
- return cookies.toString();
- }
-
public Cookies getCookies() {
return cookies;
}
+ public void addHeader(
+ String key,
+ String value
+ ) {
+ List values = this.headers
+ .getOrDefault(key, new ArrayList<>());
+ values.add(new HttpHeader(key, value));
+ this.headers.put(key, values);
+ }
+
@Override
public String toString() {
- StringBuilder stringBuilder = new StringBuilder();
-
List keys = new ArrayList<>(this.headers.keySet());
- Collections.sort(keys);
-
+ StringBuilder stringBuilder = new StringBuilder();
for (String key : keys) {
- if (COOKIE.equals(key)) {
- stringBuilder.append(key)
- .append(HEADER_DELIMITER)
- .append(cookies);
- continue;
- }
stringBuilder.append(key)
.append(HEADER_DELIMITER)
.append(joiningValues(headers.get(key)));
}
+
+ stringBuilder.append(cookies)
+ .append(CARRIAGE_RETURN);
return stringBuilder.toString();
}
private String joiningValues(List headers) {
return headers.stream()
.map(HttpHeader::getValue)
- .collect(Collectors.joining(", ")) + CARRIAGE_RETURN;
+ .collect(joining(HEADER_JOINING_DELIMITER)) + CARRIAGE_RETURN;
}
}
diff --git a/mvc/src/main/java/project/server/mvc/servlet/http/HttpStatus.java b/mvc/src/main/java/project/server/mvc/servlet/http/HttpStatus.java
index 50c4500..7f5e42c 100644
--- a/mvc/src/main/java/project/server/mvc/servlet/http/HttpStatus.java
+++ b/mvc/src/main/java/project/server/mvc/servlet/http/HttpStatus.java
@@ -1,11 +1,14 @@
package project.server.mvc.servlet.http;
+import java.util.Arrays;
+import java.util.function.Predicate;
+
public enum HttpStatus {
OK("200 OK", 200),
NO_CONTENT("204 No Content", 204),
MOVE_PERMANENTLY("301 Moved Permanently", 301),
BAD_REQUEST("400 Bad Request", 400),
- NOT_FOUND("NOT_FOUND", 404),
+ NOT_FOUND("404 NOT_FOUND", 404),
UN_AUTHORIZED("401 Unauthorized", 401),
INTERNAL_SERVER_ERROR("500 Internal Server Error", 500);
@@ -20,6 +23,17 @@ public enum HttpStatus {
this.statusCode = statusCode;
}
+ public static HttpStatus findByCode(int code) {
+ return Arrays.stream(values())
+ .filter(equals(code))
+ .findAny()
+ .orElseThrow(IllegalStateException::new);
+ }
+
+ private static Predicate equals(int code) {
+ return statusCode -> statusCode.getStatusCode() == code;
+ }
+
public String getStatus() {
return status;
}
diff --git a/mvc/src/main/java/project/server/mvc/servlet/http/RequestBody.java b/mvc/src/main/java/project/server/mvc/servlet/http/RequestBody.java
index 9d938c2..9bb79a7 100644
--- a/mvc/src/main/java/project/server/mvc/servlet/http/RequestBody.java
+++ b/mvc/src/main/java/project/server/mvc/servlet/http/RequestBody.java
@@ -5,6 +5,8 @@
public class RequestBody {
+ private static final String CARRIAGE_RETURN = "\r\n";
+ private static final String EMPTY_STRING = "";
private static final String VALUE_DELIMITER = "=";
private static final String VALUES_DELIMITER = "&";
private static final int KEY = 0;
@@ -36,6 +38,6 @@ public String getAttribute(String key) {
@Override
public String toString() {
- return String.format("%s", attributes);
+ return String.format("%s", attributes == null ? EMPTY_STRING + CARRIAGE_RETURN : attributes + CARRIAGE_RETURN);
}
}
diff --git a/mvc/src/main/java/project/server/mvc/servlet/http/ResponseBody.java b/mvc/src/main/java/project/server/mvc/servlet/http/ResponseBody.java
index 384b27d..6647588 100644
--- a/mvc/src/main/java/project/server/mvc/servlet/http/ResponseBody.java
+++ b/mvc/src/main/java/project/server/mvc/servlet/http/ResponseBody.java
@@ -5,20 +5,22 @@
public class ResponseBody {
private static final String EMPTY_BODY = "";
+ private static final String CARRIAGE_RETURN = "\r\n";
+
private String body;
public ResponseBody() {
this.body = EMPTY_BODY;
}
- public ResponseBody(String body) {
- this.body = body;
- }
-
public String getBody() {
return body;
}
+ public void setBody(String body) {
+ this.body = body;
+ }
+
@Override
public boolean equals(Object object) {
if (this == object) {
@@ -38,6 +40,9 @@ public int hashCode() {
@Override
public String toString() {
- return body;
+ if (body.isBlank()) {
+ return "";
+ }
+ return body + CARRIAGE_RETURN;
}
}
diff --git a/mvc/src/main/java/project/server/mvc/servlet/http/StatusLine.java b/mvc/src/main/java/project/server/mvc/servlet/http/StatusLine.java
index 8fa67a3..461839e 100644
--- a/mvc/src/main/java/project/server/mvc/servlet/http/StatusLine.java
+++ b/mvc/src/main/java/project/server/mvc/servlet/http/StatusLine.java
@@ -2,6 +2,7 @@
public class StatusLine {
+ private static final String CARRIAGE_RETURN = "\r\n";
private static final HttpVersion basicProtocolVersion = HttpVersion.HTTP_1_1;
private String protocolVersion;
@@ -9,6 +10,7 @@ public class StatusLine {
public StatusLine() {
this.protocolVersion = basicProtocolVersion.getValue();
+ this.httpStatus = HttpStatus.OK;
}
public StatusLine(HttpStatus httpStatus) {
@@ -23,11 +25,16 @@ public HttpStatus getHttpStatus() {
return httpStatus;
}
+ public String getHttpStatusAsString() {
+ return httpStatus.getStatus();
+ }
+
public void setStatus(HttpStatus status) {
this.httpStatus = status;
}
- public String getHttpStatusAsString() {
- return httpStatus.getStatus();
+ @Override
+ public String toString() {
+ return String.format("%s %s", protocolVersion, httpStatus.getStatus()) + CARRIAGE_RETURN;
}
}
diff --git a/mvc/src/main/java/project/server/mvc/springframework/context/ApplicationContext.java b/mvc/src/main/java/project/server/mvc/springframework/context/ApplicationContext.java
index 696106d..43ff168 100644
--- a/mvc/src/main/java/project/server/mvc/springframework/context/ApplicationContext.java
+++ b/mvc/src/main/java/project/server/mvc/springframework/context/ApplicationContext.java
@@ -3,23 +3,32 @@
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
+import static java.lang.reflect.Modifier.isStatic;
import java.util.Collection;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Map;
import java.util.Set;
+import java.util.function.Predicate;
import org.reflections.Reflections;
-import org.reflections.scanners.SubTypesScanner;
-import org.reflections.scanners.TypeAnnotationsScanner;
-import org.reflections.util.ClasspathHelper;
+import static org.reflections.scanners.Scanners.SubTypes;
+import static org.reflections.scanners.Scanners.TypesAnnotated;
+import static org.reflections.util.ClasspathHelper.forPackage;
import org.reflections.util.ConfigurationBuilder;
import project.server.mvc.springframework.annotation.Bean;
import project.server.mvc.springframework.annotation.Component;
import project.server.mvc.springframework.annotation.Configuration;
public class ApplicationContext {
- private static final Map, Object> beans = new HashMap<>();
- private static final Map, Object> dependenciesInjectedBeans = new HashMap<>();
- private static final Map dependenciesInjectedBeansByName = new HashMap<>();
+
+ private static final int FIRST_CONSTRUCTOR = 0;
+ private static final String PROXY = "Proxy";
+ private static final String DATASOURCE = "Datasource";
+
+ private static final Set> allBeans = new HashSet<>();
+ private static final Map nameKeyBeans = new HashMap<>();
+ private static final Map, Object> clazzKeyBeans = new HashMap<>();
+ private static final Map, Object> dependencyInjectedBeans = new HashMap<>();
private static Reflections reflections;
@@ -32,6 +41,14 @@ public ApplicationContext(String... packages) throws Exception {
processConfigurations(configurations);
}
+ private Reflections getReflections(String... packages) {
+ ConfigurationBuilder configurationBuilder = createConfigurationBuilder();
+ for (String packageName : packages) {
+ configurationBuilder.addUrls(forPackage(packageName));
+ }
+ return new Reflections(configurationBuilder);
+ }
+
private void componentScan(Set> components) throws Exception {
for (Class> component : components) {
if (isInstance(component)) {
@@ -41,7 +58,7 @@ private void componentScan(Set> components) throws Exception {
for (Class> instance : components) {
if (isInstance(instance)) {
- injectDependencies(instance);
+ put(instance);
registerNamedInstance(instance);
}
}
@@ -59,99 +76,126 @@ private void processConfigurations(Set> configurations) throws Exceptio
}
}
+ private boolean isInstance(Class> clazz) {
+ return !clazz.isAnnotation()
+ && !clazz.isInterface()
+ && !Modifier.isAbstract(clazz.getModifiers());
+ }
+
private Object createInstance(Class> clazz) throws Exception {
Constructor> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
return constructor.newInstance();
}
- private void processBeanMethod(Object configInstance, Method method) throws Exception {
+ private void processBeanMethod(
+ Object configInstance,
+ Method method
+ ) throws Exception {
Class> returnType = method.getReturnType();
- if (!Modifier.isStatic(method.getModifiers()) && returnType != void.class) {
+ if (!isStatic(method.getModifiers()) && isNotVoid(returnType)) {
Object bean = method.invoke(configInstance);
- beans.put(returnType, bean);
- dependenciesInjectedBeansByName.put(method.getName(), bean);
+ clazzKeyBeans.put(returnType, bean);
+ nameKeyBeans.put(method.getName(), bean);
}
}
- private Reflections getReflections(String... packages) {
- ConfigurationBuilder configurationBuilder = new ConfigurationBuilder()
- .setScanners(new SubTypesScanner(), new TypeAnnotationsScanner());
- for (String packageName : packages) {
- configurationBuilder.addUrls(ClasspathHelper.forPackage(packageName));
- }
- return new Reflections(configurationBuilder);
+ private boolean isNotVoid(Class> returnType) {
+ return returnType != void.class;
}
- private boolean isInstance(Class> clazz) {
- return !clazz.isAnnotation() && !clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers());
+ private ConfigurationBuilder createConfigurationBuilder() {
+ return new ConfigurationBuilder()
+ .setScanners(SubTypes, TypesAnnotated);
}
private void registerNamedInstance(Class> clazz) {
- Object instance = dependenciesInjectedBeans.get(clazz);
+ Object instance = dependencyInjectedBeans.get(clazz);
if (instance != null) {
- dependenciesInjectedBeansByName.put(clazz.getSimpleName(), instance);
+ nameKeyBeans.put(clazz.getSimpleName(), instance);
}
}
public static T getBean(String beanName) {
@SuppressWarnings("unchecked")
- T bean = (T) dependenciesInjectedBeansByName.get(beanName);
+ T bean = (T) ApplicationContext.nameKeyBeans.get(beanName);
return bean;
}
private void add(Class> clazz) throws Exception {
- if (beans.containsKey(clazz)) {
+ if (clazzKeyBeans.containsKey(clazz) || allBeans.contains(clazz)) {
return;
}
+ allBeans.add(clazz);
if (clazz.isInterface()) {
+ if (isDataSource(clazz)) {
+ return;
+ }
+
@SuppressWarnings("unchecked")
Set> implementations = reflections.getSubTypesOf((Class) clazz);
if (implementations.isEmpty()) {
throw new IllegalStateException("No implementation found for interface: " + clazz.getName());
}
- Class> subTypeClass = implementations.iterator().next();
- add(subTypeClass);
- beans.put(clazz, beans.get(subTypeClass));
+ Class> concreteClass = implementations.stream()
+ .filter(containsName(PROXY))
+ .findAny()
+ .orElse(implementations.iterator().next());
+
+ add(concreteClass);
+ clazzKeyBeans.put(clazz, clazzKeyBeans.get(concreteClass));
return;
}
- Constructor> constructor = clazz.getDeclaredConstructors()[0];
+ Object instance = injectDependency(clazz);
+ clazzKeyBeans.put(clazz, instance);
+ dependencyInjectedBeans.put(clazz, instance);
+ }
+
+ private boolean isDataSource(Class> clazz) {
+ return DATASOURCE.equals(clazz.getSimpleName());
+ }
+
+ private Object injectDependency(Class> clazz) throws Exception {
+ Constructor> constructor = clazz.getDeclaredConstructors()[FIRST_CONSTRUCTOR];
constructor.setAccessible(true);
Class>[] paramTypes = constructor.getParameterTypes();
Object[] params = new Object[paramTypes.length];
for (int index = 0; index < paramTypes.length; index++) {
Class> parameterType = paramTypes[index];
- if (!beans.containsKey(parameterType)) {
+ if (!clazzKeyBeans.containsKey(parameterType)) {
add(parameterType);
}
- params[index] = beans.get(parameterType);
+ params[index] = clazzKeyBeans.get(parameterType);
}
- Object instance = constructor.newInstance(params);
- beans.put(clazz, instance);
+ return constructor.newInstance(params);
+ }
+
+ private Predicate> containsName(String name) {
+ return clazz -> clazz.getSimpleName().contains(name);
}
- private void injectDependencies(Class> clazz) {
- if (dependenciesInjectedBeans.containsKey(clazz)) {
+ private void put(Class> clazz) {
+ if (dependencyInjectedBeans.containsKey(clazz)) {
return;
}
- Object instance = beans.get(clazz);
+ Object instance = clazzKeyBeans.get(clazz);
if (instance == null) {
throw new IllegalStateException("Instance not found: " + clazz.getName());
}
- dependenciesInjectedBeans.put(clazz, instance);
+ dependencyInjectedBeans.put(clazz, instance);
}
public static T getBean(Class clazz) {
- return clazz.cast(dependenciesInjectedBeans.get(clazz));
+ return clazz.cast(dependencyInjectedBeans.get(clazz));
}
public static Collection getAllDependencyInjectedInstances() {
- return dependenciesInjectedBeansByName.values();
+ return nameKeyBeans.values();
}
}
diff --git a/mvc/src/main/java/project/server/mvc/springframework/exception/ErrorResponse.java b/mvc/src/main/java/project/server/mvc/springframework/exception/ErrorResponse.java
new file mode 100644
index 0000000..d4e3497
--- /dev/null
+++ b/mvc/src/main/java/project/server/mvc/springframework/exception/ErrorResponse.java
@@ -0,0 +1,39 @@
+package project.server.mvc.springframework.exception;
+
+public class ErrorResponse {
+
+ private int code;
+ private String message;
+
+ private ErrorResponse() {
+ }
+
+ public ErrorResponse(
+ int code,
+ String message
+ ) {
+ this.code = code;
+ this.message = message;
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setCode(int code) {
+ this.code = code;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("code:%s, message:%s", code, message);
+ }
+}
diff --git a/mvc/src/main/java/project/server/mvc/springframework/handler/RequestHandler.java b/mvc/src/main/java/project/server/mvc/springframework/handler/RequestHandler.java
deleted file mode 100644
index b00e47d..0000000
--- a/mvc/src/main/java/project/server/mvc/springframework/handler/RequestHandler.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package project.server.mvc.springframework.handler;//package project.server.mission.server;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.net.Socket;
-import java.nio.charset.StandardCharsets;
-import lombok.extern.slf4j.Slf4j;
-import project.server.mvc.servlet.HttpServletRequest;
-import project.server.mvc.servlet.HttpServletResponse;
-import project.server.mvc.servlet.Request;
-import project.server.mvc.servlet.Response;
-import project.server.mvc.servlet.Servlet;
-import static project.server.mvc.springframework.context.ApplicationContextProvider.getBean;
-
-@Slf4j
-public final class RequestHandler extends Thread {
-
- private final Socket connection;
- private final Servlet dispatcherServlet;
-
- public RequestHandler(Socket connectionSocket) {
- this.connection = connectionSocket;
- this.dispatcherServlet = getBean("dispatcherServlet");
- }
-
- @Override
- public void run() {
- log.info("Connected IP : {}, Port : {}", connection.getInetAddress(), connection.getPort());
- try (
- InputStream in = connection.getInputStream();
- OutputStream out = connection.getOutputStream();
- BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))
- ) {
- HttpServletRequest request = new Request(bufferedReader);
- HttpServletResponse response = new Response(out);
- dispatcherServlet.service(request, response);
- } catch (IOException exception) {
- log.error(exception.getMessage());
- } catch (Exception exception) {
- throw new RuntimeException(exception);
- }
- }
-}
diff --git a/mvc/src/main/java/project/server/mvc/springframework/ui/ModelMap.java b/mvc/src/main/java/project/server/mvc/springframework/ui/ModelMap.java
index 1333466..7e67279 100644
--- a/mvc/src/main/java/project/server/mvc/springframework/ui/ModelMap.java
+++ b/mvc/src/main/java/project/server/mvc/springframework/ui/ModelMap.java
@@ -1,11 +1,12 @@
package project.server.mvc.springframework.ui;
+import java.util.InvalidPropertiesFormatException;
import java.util.LinkedHashMap;
import java.util.Map;
public class ModelMap {
- private final Map map = new LinkedHashMap();
+ private final Map map = new LinkedHashMap<>();
public void put(
String attributeName,
@@ -17,4 +18,11 @@ public void put(
public Object getAttribute(String key) {
return map.get(key);
}
+
+ public String[] keys() {
+ if (!map.isEmpty()) {
+ return map.keySet().toArray(new String[0]);
+ }
+ return new String[0];
+ }
}
diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/FrameworkServlet.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/FrameworkServlet.java
index e5d6498..b935917 100644
--- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/FrameworkServlet.java
+++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/FrameworkServlet.java
@@ -11,7 +11,8 @@
public abstract class FrameworkServlet extends HttpServletBean {
private static final HandlerMethod staticResourceHandlerMethod = new HandlerMethod(new ResourceHttpRequestHandler());
- private static final String STATIC_RESOURCE = ".";
+ private static final String EMPTY_STRING = "";
+ private static final List staticResources = List.of(".js", ".css", ".favicon", ".jpg", ".jpeg", ".png");
private static final List excludeStaticResources = List.of("my-info.html");
@Override
@@ -39,19 +40,20 @@ public void doGet(
private boolean isStaticResource(HttpServletRequest request) {
String uri = request.getRequestUri();
String[] parsedUri = uri.split("/");
- boolean uriContainsExclude = false;
for (String eachUri : parsedUri) {
- if (".css".equals(eachUri) || ".png".equals(eachUri) || ".favicon".equals(eachUri)) {
+ if (EMPTY_STRING.equals(eachUri)) {
+ continue;
+ }
+ if (staticResources.contains(eachUri)) {
return true;
}
}
for (String eachUri : parsedUri) {
if (excludeStaticResources.contains(eachUri)) {
- uriContainsExclude = true;
- break;
+ return false;
}
}
- return uri.contains(STATIC_RESOURCE) && !uriContainsExclude;
+ return true;
}
private void processStaticRequest(
diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/GlobalExceptionHandler.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/GlobalExceptionHandler.java
index 060ed4b..936f20f 100644
--- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/GlobalExceptionHandler.java
+++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/GlobalExceptionHandler.java
@@ -1,41 +1,33 @@
package project.server.mvc.springframework.web.servlet;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import project.server.mvc.servlet.HttpServletResponse;
import project.server.mvc.servlet.http.HttpStatus;
-import static project.server.mvc.servlet.http.HttpStatus.UN_AUTHORIZED;
+import static project.server.mvc.servlet.http.HttpStatus.INTERNAL_SERVER_ERROR;
import project.server.mvc.springframework.annotation.Component;
+import project.server.mvc.springframework.exception.ErrorResponse;
@Slf4j
@Component
public class GlobalExceptionHandler {
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
public void resolveException(
HttpServletResponse response,
Exception exception
- ) {
+ ) throws JsonProcessingException {
Throwable cause = exception.getCause();
- HttpStatus findStatus = getHttpStatus(cause.getMessage());
- log.error("{code:{}, message:{}}", findStatus.getStatusCode(), cause.getMessage());
- response.setStatus(findStatus);
- }
-
- public HttpStatus getHttpStatus(String message) {
- if ("중복된 아이디 입니다.".equals(message)) {
- return HttpStatus.BAD_REQUEST;
- }
- if ("올바른 값을 입력해주세요.".equals(message)) {
- return HttpStatus.BAD_REQUEST;
+ if (cause == null) {
+ response.setStatus(INTERNAL_SERVER_ERROR);
+ return;
}
- if ("이미 가입된 사용자 입니다.".equals(message)) {
- return HttpStatus.BAD_REQUEST;
- }
- if ("사용자를 찾을 수 없습니다.".equals(message)) {
- return HttpStatus.NOT_FOUND;
- }
- if ("권한이 존재하지 않습니다.".equals(message)) {
- return UN_AUTHORIZED;
- }
- return HttpStatus.INTERNAL_SERVER_ERROR;
+ String stringJson = cause.toString();
+ ErrorResponse errorResponse = objectMapper.readValue(stringJson, ErrorResponse.class);
+
+ HttpStatus findStatus = HttpStatus.findByCode(errorResponse.getCode());
+ response.setStatus(findStatus);
}
}
diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/MyInfoView.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/MyInfoView.java
index c9ba5ae..3dfa7a9 100644
--- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/MyInfoView.java
+++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/MyInfoView.java
@@ -3,22 +3,21 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.nio.ByteBuffer;
-import java.nio.channels.SocketChannel;
+import static java.lang.String.valueOf;
import static java.nio.charset.StandardCharsets.UTF_8;
import project.server.mvc.servlet.HttpServletRequest;
import project.server.mvc.servlet.HttpServletResponse;
+import project.server.mvc.servlet.http.HttpStatus;
+import static project.server.mvc.servlet.http.HttpStatus.UN_AUTHORIZED;
import project.server.mvc.springframework.ui.ModelMap;
public class MyInfoView implements View {
- private static final String CARRIAGE_RETURN = "\r\n";
private static final int BASE_OFFSET = 0;
private static final int DEFAULT_BUFFER_SIZE = 1_024;
private static final int EMPTY = -1;
- private static final String CONTENT_LENGTH = "Content-Length: ";
- private static final String CONTENT_TYPE = "Content-Type: ";
- private static final String DELIMITER = " ";
+ private static final String CONTENT_TYPE = "Content-Type";
+ private static final String CONTENT_LENGTH = "Content-Length";
private static final String MASKING = "*";
private static final String STATIC_PREFIX = "static";
@@ -28,72 +27,64 @@ public void render(
HttpServletRequest request,
HttpServletResponse response
) throws Exception {
+ HttpStatus httpStatus = response.getStatus();
+ if (UN_AUTHORIZED.equals(httpStatus)) {
+ response.setStatus(UN_AUTHORIZED);
+ return;
+ }
+
ModelMap modelMap = modelAndView.getModelMap();
InputStream inputStream = getInputStream(STATIC_PREFIX + request.getRequestUri());
- byte[] buffer = readStream(inputStream, modelMap);
+ byte[] buffer = getBuffer(inputStream, modelMap);
+
setResponseHeader(request, response, buffer.length);
- responseBody(response, buffer);
+ setResponseBody(response, buffer);
}
private InputStream getInputStream(String path) {
- return getClass().getClassLoader().getResourceAsStream(path);
- }
-
- private void responseBody(
- HttpServletResponse response,
- byte[] body
- ) throws IOException {
- SocketChannel channel = response.getSocketChannel();
- ByteBuffer buffer = ByteBuffer.wrap(body);
- while (buffer.hasRemaining()) {
- channel.write(buffer);
- }
+ return getClass().getClassLoader()
+ .getResourceAsStream(path);
}
private void setResponseHeader(
HttpServletRequest request,
HttpServletResponse response,
int lengthOfBodyContent
- ) throws IOException {
- SocketChannel channel = response.getSocketChannel();
- String header = request.getHttpVersion() + DELIMITER + getStatus(response) + CARRIAGE_RETURN
- + CONTENT_TYPE
- + request.getContentType()
- + CARRIAGE_RETURN
- + CONTENT_LENGTH
- + lengthOfBodyContent
- + CARRIAGE_RETURN
- + CARRIAGE_RETURN;
- ByteBuffer headerBuffer = ByteBuffer.wrap(header.getBytes(UTF_8));
- while (headerBuffer.hasRemaining()) {
- channel.write(headerBuffer);
- }
+ ) {
+ response.setHeader(CONTENT_TYPE, request.getContentType());
+ response.setHeader(CONTENT_LENGTH, valueOf(lengthOfBodyContent));
}
- private static String getStatus(HttpServletResponse response) {
- return response.getStatusAsString() + DELIMITER;
+ private void setResponseBody(
+ HttpServletResponse response,
+ byte[] buffer
+ ) {
+ response.setBody(new String(buffer));
}
- private byte[] readStream(
+ private byte[] getBuffer(
+ InputStream inputStream,
+ ModelMap modelMap
+ ) throws IOException {
+ String convertedHtml = getConvertedHtml(inputStream, modelMap);
+ return convertedHtml.getBytes(UTF_8);
+ }
+
+ private static String getConvertedHtml(
InputStream inputStream,
ModelMap modelMap
) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
+
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != EMPTY) {
byteArrayOutputStream.write(buffer, BASE_OFFSET, bytesRead);
}
- String convertedHtml = getConvertedHtml(modelMap, byteArrayOutputStream);
- return convertedHtml.getBytes(UTF_8);
- }
- private static String getConvertedHtml(
- ModelMap modelMap,
- ByteArrayOutputStream byteArrayOutputStream
- ) {
- Object username = modelMap.getAttribute("username");
- Object password = modelMap.getAttribute("password");
+ String[] keys = modelMap.keys();
+ Object username = modelMap.getAttribute(keys[0]);
+ Object password = modelMap.getAttribute(keys[1]);
String htmlPage = byteArrayOutputStream.toString(UTF_8);
String replacedUsernameHtml = htmlPage.replace(MASKING, username.toString());
diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/RedirectView.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/RedirectView.java
index f948dff..1f42b5e 100644
--- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/RedirectView.java
+++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/RedirectView.java
@@ -1,22 +1,29 @@
package project.server.mvc.springframework.web.servlet;
+import java.io.BufferedReader;
import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.channels.SocketChannel;
-import java.nio.charset.StandardCharsets;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import static java.lang.String.valueOf;
+import static java.nio.charset.StandardCharsets.UTF_8;
import java.util.HashMap;
import java.util.Map;
import project.server.mvc.servlet.HttpServletRequest;
import project.server.mvc.servlet.HttpServletResponse;
+import static project.server.mvc.servlet.http.HttpStatus.MOVE_PERMANENTLY;
public class RedirectView implements View {
- private static final String CARRIAGE_RETURN = "\r\n";
private static final String PROTOCOL = "http://";
- private static final String DELIMITER = " ";
- private static final String LOCATION_DELIMITER = "Location: ";
+ private static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
+ private static final String ALL_ORIGIN = "*";
+ private static final String LOCATION_DELIMITER = "Location";
private static final String REDIRECT_LOCATION = "redirect:/index.html";
- private static final String HOME = "/index.html";
+ private static final String CONTENT_TYPE = "Content-Type";
+ private static final String CONTENT_LENGTH = "Content-Length";
+ private static final String INDEX_HTML = "/index.html";
+ private static final String SIGN_IN_HTML = "static/sign-in.html";
+ private static final String TEXT_HTML = "text/html";
public RedirectView() {
Map views = new HashMap<>();
@@ -29,41 +36,51 @@ public void render(
HttpServletRequest request,
HttpServletResponse response
) throws Exception {
- response(request, response);
+ response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, ALL_ORIGIN);
+ if (MOVE_PERMANENTLY.equals(response.getStatus())) {
+ response.setHeader(LOCATION_DELIMITER, getRedirectLocation(request));
+ return;
+ }
+ InputStream inputStream = getInputStream();
+ byte[] buffer = getBuffer(inputStream);
+ setResponseHeader(response, buffer.length);
+ setResponseBody(response, buffer);
}
- private void response(
- HttpServletRequest request,
- HttpServletResponse response
- ) throws IOException {
- setResponseHeader(request, response);
+ private String getRedirectLocation(HttpServletRequest request) {
+ return String.format("%s%s%s", PROTOCOL, request.getHost(), INDEX_HTML);
}
- private void setResponseHeader(HttpServletRequest request, HttpServletResponse response) throws IOException {
- SocketChannel channel = response.getSocketChannel();
- String header = getStartLine(request, response)
- + LOCATION_DELIMITER + getRedirectLocation(request) + CARRIAGE_RETURN
- + "Access-Control-Allow-Origin: *" + CARRIAGE_RETURN
- + "Set-Cookie: " + response.getCookiesAsString() + CARRIAGE_RETURN
- + CARRIAGE_RETURN;
- ByteBuffer headerBuffer = ByteBuffer.wrap(header.getBytes(StandardCharsets.UTF_8));
- while (headerBuffer.hasRemaining()) {
- channel.write(headerBuffer);
- }
+ private InputStream getInputStream() {
+ return getClass().getClassLoader()
+ .getResourceAsStream(SIGN_IN_HTML);
}
- private String getStartLine(
- HttpServletRequest request,
- HttpServletResponse response
- ) {
- return String.format("%s%s%s%s", request.getHttpVersion(), DELIMITER, getStatus(response), CARRIAGE_RETURN);
+ private byte[] getBuffer(InputStream inputStream) throws IOException {
+ StringBuilder stringBuilder = new StringBuilder();
+ InputStreamReader inputStreamReader = new InputStreamReader(inputStream, UTF_8);
+ try (BufferedReader reader = new BufferedReader(inputStreamReader)) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ stringBuilder.append(line).append("\n");
+ }
+ }
+ return stringBuilder.toString()
+ .getBytes(UTF_8);
}
- private static String getStatus(HttpServletResponse response) {
- return String.format("%s%s", response.getStatusAsString(), DELIMITER);
+ private void setResponseHeader(
+ HttpServletResponse response,
+ long lengthOfBodyContent
+ ) {
+ response.setHeader(CONTENT_TYPE, TEXT_HTML);
+ response.setHeader(CONTENT_LENGTH, valueOf(lengthOfBodyContent));
}
- private String getRedirectLocation(HttpServletRequest request) {
- return String.format("%s%s%s", PROTOCOL, request.getHost(), HOME);
+ private void setResponseBody(
+ HttpServletResponse response,
+ byte[] buffer
+ ) {
+ response.setBody(new String(buffer));
}
}
diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/StaticView.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/StaticView.java
index 66f0c11..65be0be 100644
--- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/StaticView.java
+++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/StaticView.java
@@ -4,23 +4,19 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
-import java.nio.ByteBuffer;
-import java.nio.channels.SocketChannel;
import static java.nio.charset.StandardCharsets.UTF_8;
import project.server.mvc.servlet.HttpServletRequest;
import project.server.mvc.servlet.HttpServletResponse;
import project.server.mvc.servlet.http.HttpStatus;
+import static project.server.mvc.servlet.http.HttpStatus.MOVE_PERMANENTLY;
import static project.server.mvc.servlet.http.HttpStatus.UN_AUTHORIZED;
public class StaticView implements View {
- private static final String CARRIAGE_RETURN = "\r\n";
- private static final String INVALID_COOKIE = "Set-Cookie=; Max-Age=0; Path=/";
- private static final String DELIMITER = " ";
- private static final String CONTENT_TYPE = "Content-Type: ";
+ private static final String CONTENT_TYPE = "Content-Type";
private static final String TEXT_HTML = "text/html";
- private static final String HTML_REQUEST_LINE = CONTENT_TYPE + TEXT_HTML + CARRIAGE_RETURN + CARRIAGE_RETURN;
- private static final String INDEX_HTML = "static/index.html";
+ private static final String LOCATION = "Location";
+ private static final String ROOT = "/";
private static final String NEXT_LINE = "\n";
@Override
@@ -29,62 +25,23 @@ public void render(
HttpServletRequest request,
HttpServletResponse response
) throws Exception {
- response(request, response);
- }
-
- private void response(
- HttpServletRequest request,
- HttpServletResponse response
- ) throws IOException {
- setResponseHeader(request, response);
- }
-
- private void setResponseHeader(
- HttpServletRequest request,
- HttpServletResponse response
- ) throws IOException {
- SocketChannel channel = response.getSocketChannel();
HttpStatus httpStatus = response.getStatus();
- String header = getHeader(request, response);
-
- ByteBuffer headerBuffer = ByteBuffer.wrap(header.getBytes(UTF_8));
- channel.write(headerBuffer);
-
if (UN_AUTHORIZED.equals(httpStatus)) {
- InputStream inputStream = getInputStream();
- String htmlContent = readInputStream(inputStream);
- ByteBuffer contentTypeBuffer = ByteBuffer.wrap(HTML_REQUEST_LINE.getBytes(UTF_8));
- channel.write(contentTypeBuffer);
-
- ByteBuffer htmlBuffer = ByteBuffer.wrap(htmlContent.getBytes(UTF_8));
- channel.write(htmlBuffer);
+ response.setStatus(MOVE_PERMANENTLY);
+ response.setHeader(LOCATION, ROOT);
+ return;
}
- }
-
- private InputStream getInputStream() {
- return getClass().getClassLoader()
- .getResourceAsStream(INDEX_HTML);
- }
- private String getHeader(
- HttpServletRequest request,
- HttpServletResponse response
- ) {
- HttpStatus httpStatus = response.getStatus();
+ InputStream inputStream = getInputStream(request.getRequestUri());
+ String html = readInputStream(inputStream);
- StringBuilder headerBuilder = new StringBuilder();
- headerBuilder.append(request.getHttpVersion())
- .append(DELIMITER)
- .append(httpStatus.getStatusCode())
- .append(DELIMITER)
- .append(httpStatus.getStatus())
- .append(CARRIAGE_RETURN);
+ setResponseHeader(response);
+ setResponseBody(response, html);
+ }
- if (UN_AUTHORIZED.equals(httpStatus)) {
- headerBuilder.append(INVALID_COOKIE)
- .append(CARRIAGE_RETURN);
- }
- return headerBuilder.toString();
+ private InputStream getInputStream(String path) {
+ return getClass().getClassLoader()
+ .getResourceAsStream(path);
}
private String readInputStream(InputStream inputStream) throws IOException {
@@ -98,4 +55,15 @@ private String readInputStream(InputStream inputStream) throws IOException {
}
return stringBuilder.toString();
}
+
+ private void setResponseHeader(HttpServletResponse response) {
+ response.setHeader(CONTENT_TYPE, TEXT_HTML);
+ }
+
+ private void setResponseBody(
+ HttpServletResponse response,
+ String html
+ ) {
+ response.setBody(html);
+ }
}
diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java
index a0519d6..df3698c 100644
--- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java
+++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java
@@ -24,7 +24,8 @@ private String initLookupPath(HttpServletRequest request) {
}
private String getRequestPath(HttpServletRequest request) {
- return request.getRequestUri();
+ return request.getRequestUri()
+ .replace(".html", "");
}
private HandlerMethod lookupHandlerMethod(
@@ -52,7 +53,7 @@ public MappingRegistry() {
new MappingRegistration(new HandlerMethod(homeController))
);
registry.put(
- new RequestMappingInfo(HttpMethod.GET, "/my-info.html"),
+ new RequestMappingInfo(HttpMethod.GET, "/my-info"),
new MappingRegistration(new HandlerMethod(userInfoController))
);
registry.put(
diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/mvc/method/AbstractHandlerMethodAdapter.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/mvc/method/AbstractHandlerMethodAdapter.java
index fd026af..53011a5 100644
--- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/mvc/method/AbstractHandlerMethodAdapter.java
+++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/mvc/method/AbstractHandlerMethodAdapter.java
@@ -2,12 +2,14 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import lombok.extern.slf4j.Slf4j;
import project.server.mvc.servlet.HttpServletRequest;
import project.server.mvc.servlet.HttpServletResponse;
import project.server.mvc.springframework.web.method.HandlerMethod;
import project.server.mvc.springframework.web.servlet.HandlerAdapter;
import project.server.mvc.springframework.web.servlet.ModelAndView;
+@Slf4j
public abstract class AbstractHandlerMethodAdapter implements HandlerAdapter {
@Override
@@ -34,7 +36,9 @@ private ModelAndView invokeHandlerMethod(
Object instance = handlerMethod.getHandler();
Object[] args = new Object[]{request, response};
+ log.info("args: {}", args);
Object result = method.invoke(instance, args);
+ log.info("result: {}", result);
return (ModelAndView) result;
}
}
diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/resource/ResourceHttpRequestHandler.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/resource/ResourceHttpRequestHandler.java
index 9ce2f32..900bcf5 100644
--- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/resource/ResourceHttpRequestHandler.java
+++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/resource/ResourceHttpRequestHandler.java
@@ -5,7 +5,6 @@
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
-import static java.nio.charset.StandardCharsets.UTF_8;
import project.server.mvc.servlet.HttpServletRequest;
import project.server.mvc.servlet.HttpServletResponse;
import static project.server.mvc.servlet.http.HttpStatus.NOT_FOUND;
@@ -13,13 +12,12 @@
public class ResourceHttpRequestHandler implements HttpRequestHandler {
- private static final String CARRIAGE_RETURN = "\r\n";
private static final int BASE_OFFSET = 0;
private static final int START_INDEX = 1;
private static final int EMPTY = -1;
- private static final String CONTENT_LENGTH = "Content-Length: ";
- private static final String CONTENT_TYPE = "Content-Type: ";
- private static final String DELIMITER = " ";
+ private static final int BUFFER_CAPACITY = 1_024;
+ private static final String CONTENT_LENGTH = "Content-Length";
+ private static final String CONTENT_TYPE = "Content-Type";
private static final String HTML = ".html";
private static final String STATIC_PREFIX = "static";
@@ -60,7 +58,8 @@ private String getFile(String uri) {
}
private InputStream getInputStream(String path) {
- return getClass().getClassLoader().getResourceAsStream(path);
+ return getClass().getClassLoader()
+ .getResourceAsStream(path);
}
private void response(
@@ -68,24 +67,20 @@ private void response(
HttpServletResponse response,
InputStream inputStream
) throws IOException {
- byte[] buffer = readStream(inputStream);
+ byte[] buffer = readInputStream(inputStream);
setResponseHeader(request, response, buffer.length);
- responseBody(response, buffer);
+ setResponseBody(response, buffer);
}
private void responsePageNotFound(HttpServletResponse response) {
response.setStatus(NOT_FOUND);
}
- private void responseBody(
+ private void setResponseBody(
HttpServletResponse response,
byte[] body
) throws IOException {
- SocketChannel channel = response.getSocketChannel();
- ByteBuffer buffer = ByteBuffer.wrap(body);
- while (buffer.hasRemaining()) {
- channel.write(buffer);
- }
+ response.write(body);
}
private void setResponseHeader(
@@ -93,24 +88,17 @@ private void setResponseHeader(
HttpServletResponse response,
int lengthOfBodyContent
) throws IOException {
- SocketChannel channel = response.getSocketChannel();
- String header = request.getHttpVersion() + DELIMITER + getStatus(response) + CARRIAGE_RETURN +
- CONTENT_TYPE + request.getContentType() + CARRIAGE_RETURN +
- CONTENT_LENGTH + lengthOfBodyContent + CARRIAGE_RETURN +
- CARRIAGE_RETURN;
- ByteBuffer headerBuffer = ByteBuffer.wrap(header.getBytes(UTF_8));
- while (headerBuffer.hasRemaining()) {
- channel.write(headerBuffer);
- }
- }
+ response.getHttpHeaderLine();
+ response.setHeader(CONTENT_TYPE, request.getContentType());
+ response.setHeader(CONTENT_LENGTH, String.valueOf(lengthOfBodyContent));
- private static String getStatus(HttpServletResponse response) {
- return response.getStatusAsString() + DELIMITER;
+ String header = response.getHttpHeaderLine();
+ response.write(header);
}
- private byte[] readStream(InputStream inputStream) throws IOException {
+ private byte[] readInputStream(InputStream inputStream) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
- byte[] buffer = new byte[1024];
+ byte[] buffer = new byte[BUFFER_CAPACITY];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != EMPTY) {
byteArrayOutputStream.write(buffer, BASE_OFFSET, bytesRead);
diff --git a/mvc/src/main/java/project/server/mvc/tomcat/AbstractEndpoint.java b/mvc/src/main/java/project/server/mvc/tomcat/AbstractEndpoint.java
index 5fe0057..cb6f207 100644
--- a/mvc/src/main/java/project/server/mvc/tomcat/AbstractEndpoint.java
+++ b/mvc/src/main/java/project/server/mvc/tomcat/AbstractEndpoint.java
@@ -4,11 +4,13 @@
import lombok.extern.slf4j.Slf4j;
@Slf4j
-public abstract class AbstractEndpoint {
+public abstract class AbstractEndpoint {
private final ExecutorService executorService;
public AbstractEndpoint(ExecutorService executorService) {
this.executorService = executorService;
}
+
+ protected abstract boolean setSocketOptions(U socket);
}
diff --git a/mvc/src/main/java/project/server/mvc/tomcat/AsyncRequest.java b/mvc/src/main/java/project/server/mvc/tomcat/AsyncRequest.java
deleted file mode 100644
index 50a2fa3..0000000
--- a/mvc/src/main/java/project/server/mvc/tomcat/AsyncRequest.java
+++ /dev/null
@@ -1,92 +0,0 @@
-package project.server.mvc.tomcat;
-
-import java.io.OutputStream;
-import java.nio.ByteBuffer;
-import java.nio.channels.SocketChannel;
-import java.util.ArrayList;
-import java.util.List;
-import lombok.extern.slf4j.Slf4j;
-import project.server.mvc.servlet.HttpServletRequest;
-import project.server.mvc.servlet.HttpServletResponse;
-import project.server.mvc.servlet.Request;
-import project.server.mvc.servlet.Response;
-import project.server.mvc.servlet.http.HttpHeaders;
-import project.server.mvc.servlet.http.RequestBody;
-import project.server.mvc.servlet.http.RequestLine;
-import static project.server.mvc.springframework.context.ApplicationContextProvider.getBean;
-import project.server.mvc.springframework.web.servlet.DispatcherServlet;
-
-@Slf4j
-public class AsyncRequest implements Runnable {
-
- private static final int START_OFFSET = 0;
- private static final int START_LINE = 0;
- private static final String CARRIAGE_RETURN = "\r\n";
-
- private final SocketChannel socketChannel;
- private final ByteBuffer buffer;
- private final DispatcherServlet dispatcherServlet;
-
- public AsyncRequest(
- SocketChannel socketChannel,
- ByteBuffer buffer
- ) {
- this.socketChannel = socketChannel;
- this.buffer = buffer;
- this.dispatcherServlet = getBean("dispatcherServlet");
- }
-
- @Override
- public void run() {
- try {
- String httpMessage = new String(buffer.array(), START_OFFSET, buffer.limit());
- String[] lines = httpMessage.split(CARRIAGE_RETURN);
-
- int index = 1;
- List headerLines = new ArrayList<>();
- while (index < lines.length && !lines[index].isEmpty()) {
- headerLines.add(lines[index]);
- index++;
- }
-
- String requestBody = getRequestBodyBuilder(lines, index);
-
- try (SocketChannel channel = this.socketChannel;
- OutputStream outputStream = channel.socket().getOutputStream()) {
-
- HttpServletRequest request = createHttpServletRequest(lines, headerLines, requestBody);
- HttpServletResponse response = new Response(channel, outputStream);
- dispatcherServlet.service(request, response);
- }
-
- } catch (Exception exception) {
- log.error("message: {}", exception.getMessage());
- }
- }
-
- private String getRequestBodyBuilder(
- String[] lines,
- int index
- ) {
- StringBuilder requestBodyBuilder = new StringBuilder();
- if (index < lines.length) {
- for (int subIndex = index + 1; subIndex < lines.length; subIndex++) {
- requestBodyBuilder.append(lines[subIndex])
- .append(CARRIAGE_RETURN);
- }
- }
- return requestBodyBuilder.toString();
- }
-
- private HttpServletRequest createHttpServletRequest(
- String[] lines,
- List headerLines,
- String requestBody
- ) {
- return new Request(
- new RequestLine(lines[START_LINE]),
- new HttpHeaders(headerLines),
- new RequestBody(requestBody)
- );
- }
-}
diff --git a/mvc/src/main/java/project/server/mvc/tomcat/Nio2EndPoint.java b/mvc/src/main/java/project/server/mvc/tomcat/Nio2EndPoint.java
index 404f532..1638682 100644
--- a/mvc/src/main/java/project/server/mvc/tomcat/Nio2EndPoint.java
+++ b/mvc/src/main/java/project/server/mvc/tomcat/Nio2EndPoint.java
@@ -1,5 +1,6 @@
package project.server.mvc.tomcat;
+import java.nio.channels.SocketChannel;
import java.util.Objects;
import java.util.Queue;
import java.util.UUID;
@@ -7,7 +8,7 @@
import java.util.concurrent.ExecutorService;
import java.util.function.Predicate;
-public class Nio2EndPoint extends AbstractEndpoint {
+public class Nio2EndPoint extends AbstractJsseEndpoint {
private final Poller poller;
@@ -16,13 +17,21 @@ public Nio2EndPoint(ExecutorService executorService) {
this.poller = new Poller();
}
+ @Override
+ public boolean setSocketOptions(SocketChannel socket) {
+// PollerEvent pollerEvent = new PollerEvent(socket);
+ return false;
+ }
+
class Poller implements Runnable {
private static final Queue events = new ConcurrentLinkedQueue<>();
@Override
public void run() {
-
+ while (true) {
+ break;
+ }
}
public void register(NioSocketWrapper socketWrapper) {
diff --git a/mvc/src/main/java/project/server/mvc/tomcat/NioSocketWrapper.java b/mvc/src/main/java/project/server/mvc/tomcat/NioSocketWrapper.java
index 1f4b284..93c4f7d 100644
--- a/mvc/src/main/java/project/server/mvc/tomcat/NioSocketWrapper.java
+++ b/mvc/src/main/java/project/server/mvc/tomcat/NioSocketWrapper.java
@@ -1,4 +1,107 @@
package project.server.mvc.tomcat;
-public class NioSocketWrapper {
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import java.util.ArrayList;
+import java.util.List;
+import lombok.extern.slf4j.Slf4j;
+import project.server.mvc.servlet.HttpServletRequest;
+import project.server.mvc.servlet.HttpServletResponse;
+import project.server.mvc.servlet.Request;
+import project.server.mvc.servlet.Response;
+import project.server.mvc.servlet.http.HttpHeaders;
+import project.server.mvc.servlet.http.RequestBody;
+import project.server.mvc.servlet.http.RequestLine;
+import static project.server.mvc.springframework.context.ApplicationContextProvider.getBean;
+import project.server.mvc.springframework.web.servlet.DispatcherServlet;
+
+@Slf4j
+public class NioSocketWrapper implements Runnable {
+
+ private static final int START_OFFSET = 0;
+ private static final int START_LINE = 0;
+ private static final String CARRIAGE_RETURN = "\r\n";
+
+ private final SocketChannel socketChannel;
+ private final ByteBuffer buffer;
+ private final DispatcherServlet dispatcherServlet = getBean("dispatcherServlet");
+ private HttpServletResponse response;
+
+ public NioSocketWrapper(
+ SocketChannel socketChannel,
+ ByteBuffer buffer
+ ) throws IOException {
+ this.socketChannel = socketChannel;
+ this.buffer = buffer;
+ socketChannel.read(buffer);
+ }
+
+ @Override
+ public void run() {
+ try {
+ String httpMessage = new String(buffer.array(), START_OFFSET, buffer.limit());
+ String[] lines = httpMessage.split(CARRIAGE_RETURN);
+
+ int index = 1;
+ List headerLines = new ArrayList<>();
+ while (index < lines.length && !lines[index].isEmpty()) {
+ headerLines.add(lines[index]);
+ index++;
+ }
+
+ String requestBody = getRequestBodyBuilder(lines, index);
+
+ try (SocketChannel channel = this.socketChannel) {
+ HttpServletRequest request = createHttpServletRequest(lines, headerLines, requestBody);
+ HttpServletResponse response = new Response(channel);
+
+ this.response = response;
+ dispatcherServlet.service(request, response);
+
+ ByteBuffer headerBuffer = ByteBuffer.wrap(response.toString().getBytes(UTF_8));
+ while (headerBuffer.hasRemaining()) {
+ channel.write(headerBuffer);
+ }
+ }
+ } catch (Exception exception) {
+ log.error("message: {}", exception.getMessage());
+ }
+ }
+
+ private String getRequestBodyBuilder(
+ String[] lines,
+ int index
+ ) {
+ StringBuilder requestBodyBuilder = new StringBuilder();
+ if (index < lines.length) {
+ for (int subIndex = index + 1; subIndex < lines.length; subIndex++) {
+ requestBodyBuilder.append(lines[subIndex])
+ .append(CARRIAGE_RETURN);
+ }
+ }
+ return requestBodyBuilder.toString();
+ }
+
+ private HttpServletRequest createHttpServletRequest(
+ String[] lines,
+ List headerLines,
+ String requestBody
+ ) {
+ return new Request(
+ new RequestLine(lines[START_LINE]),
+ new HttpHeaders(headerLines),
+ new RequestBody(requestBody)
+ );
+ }
+
+ public HttpServletResponse getResponse() {
+ return response;
+ }
+
+ public void flip() {
+ buffer.flip();
+ }
}
diff --git a/settings.gradle b/settings.gradle
index 5816a77..2adb816 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,2 +1,2 @@
rootProject.name = "spring-server"
-include("app", "mvc")
+include("app", "mvc", "jdbc")