Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JDBC 라이브러리 구현하기 - 2단계] 도이(유도영) 미션 제출합니다. #421

Merged
merged 11 commits into from
Oct 6, 2023
Merged
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dependencies {
implementation "org.apache.commons:commons-lang3:3.13.0"
implementation "com.fasterxml.jackson.core:jackson-databind:2.15.2"
implementation "com.h2database:h2:2.2.220"
implementation 'com.zaxxer:HikariCP:5.0.1'
Copy link

Choose a reason for hiding this comment

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

👍어떻게 보면 무리한 요구일 수 도 있었는데 직접 해보시고 좋은 경험을 하신 것 같아서 다행입니다 ㅎㅎ


testImplementation "org.assertj:assertj-core:3.24.2"
testImplementation "org.junit.jupiter:junit-jupiter-api:5.7.2"
Expand Down
18 changes: 10 additions & 8 deletions app/src/main/java/com/techcourse/config/DataSourceConfig.java
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
package com.techcourse.config;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.util.Objects;
import org.h2.jdbcx.JdbcDataSource;

public class DataSourceConfig {

private static javax.sql.DataSource INSTANCE;

public static javax.sql.DataSource getInstance() {
if (Objects.isNull(INSTANCE)) {
INSTANCE = createJdbcDataSource();
INSTANCE = createHikariDataSource();
}
return INSTANCE;
}

private static JdbcDataSource createJdbcDataSource() {
final var jdbcDataSource = new JdbcDataSource();
jdbcDataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;");
jdbcDataSource.setUser("");
jdbcDataSource.setPassword("");
return jdbcDataSource;
private static HikariDataSource createHikariDataSource() {
final var hikariConfig = new HikariConfig();
hikariConfig.setJdbcUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;");
hikariConfig.setUsername("");
hikariConfig.setPassword("");

return new HikariDataSource(hikariConfig);
}

private DataSourceConfig() {}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
package com.techcourse.support.jdbc.init;

import com.techcourse.config.DataSourceConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.springframework.jdbc.CannotGetJdbcConnectionException;
import org.springframework.jdbc.core.ConnectionManager;

public class DataSourceConnectionManager implements ConnectionManager {
public class PooledDataSourceConnectionManager implements ConnectionManager {

private final HikariDataSource dataSource;

public PooledDataSourceConnectionManager() {
this.dataSource = (HikariDataSource) DataSourceConfig.getInstance();
}

@Override
public Connection getConnection() throws CannotGetJdbcConnectionException {
try {
DataSource dataSource = DataSourceConfig.getInstance();
return dataSource.getConnection();
final var connection = dataSource.getConnection();
return connection;
} catch (final SQLException exception) {
throw new CannotGetJdbcConnectionException("Datasource connection error:" + exception.getMessage());
}
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/resources/logback.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>

<logger name="com.zaxxer.hikari" level="DEBUG">
Copy link

Choose a reason for hiding this comment

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

Logback 설정 좋네요! 혹시 로그 설정으로 어떤 정보를 얻으셨을까요?

Copy link
Author

Choose a reason for hiding this comment

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

image 해당 로그 설정으로, app 모듈의 Application을 실행했을 때 위 이미지와 같이 총 10개의 Connection이 생성되는 것을 확인할 수 있었어요. (그런데 왜 각 로그가 두번씩 출력되는지는 모르겠어요..) 그리고 주기적으로 아래와 같은 로그가 출력되는데, 계속해서 풀을 채워야 하는지 확인하는 것 같아요.
Fill pool skipped, pool has sufficient level or currently being filled (queueDepth=0).

그런데 connection을 가져가고 반납하는 내용은 해당 설정에서는 확인이 어려웠네요..!

<appender-ref ref="STDOUT"/>
</logger>
</configuration>
4 changes: 2 additions & 2 deletions app/src/test/java/com/techcourse/dao/UserDaoTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import com.techcourse.config.DataSourceConfig;
import com.techcourse.domain.User;
import com.techcourse.support.jdbc.init.DataSourceConnectionManager;
import com.techcourse.support.jdbc.init.PooledDataSourceConnectionManager;
import com.techcourse.support.jdbc.init.DatabasePopulatorUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand All @@ -18,7 +18,7 @@ class UserDaoTest {
void setup() {
DatabasePopulatorUtils.execute(DataSourceConfig.getInstance());

userDao = new UserDao(new JdbcTemplate(new DataSourceConnectionManager()));
userDao = new UserDao(new JdbcTemplate(new PooledDataSourceConnectionManager()));
final var user = new User("gugu", "password", "[email protected]");
userDao.insert(user);
}
Expand Down
4 changes: 2 additions & 2 deletions app/src/test/java/com/techcourse/service/UserServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import com.techcourse.dao.UserDao;
import com.techcourse.dao.UserHistoryDao;
import com.techcourse.domain.User;
import com.techcourse.support.jdbc.init.DataSourceConnectionManager;
import com.techcourse.support.jdbc.init.PooledDataSourceConnectionManager;
import com.techcourse.support.jdbc.init.DatabasePopulatorUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
Expand All @@ -23,7 +23,7 @@ class UserServiceTest {

@BeforeEach
void setUp() {
this.jdbcTemplate = new JdbcTemplate(new DataSourceConnectionManager());
this.jdbcTemplate = new JdbcTemplate(new PooledDataSourceConnectionManager());
this.userDao = new UserDao(jdbcTemplate);

DatabasePopulatorUtils.execute(DataSourceConfig.getInstance());
Expand Down
1 change: 1 addition & 0 deletions jdbc/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dependencies {
implementation "org.reflections:reflections:0.10.2"
implementation "org.apache.commons:commons-lang3:3.13.0"
implementation "ch.qos.logback:logback-classic:1.2.12"
implementation "com.h2database:h2:2.2.220"

testImplementation "org.assertj:assertj-core:3.24.2"
testImplementation "org.junit.jupiter:junit-jupiter-api:5.7.2"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.springframework;
package org.springframework.jdbc;

public class SqlQueryException extends RuntimeException {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.springframework.jdbc.core;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

@FunctionalInterface
public interface ConnectionCallback<T> {

T doInConnection(Connection connection, PreparedStatement preparedStatement) throws SQLException;

}
78 changes: 48 additions & 30 deletions jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,64 +8,82 @@
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.SqlQueryException;
import org.springframework.jdbc.SqlQueryException;

public class JdbcTemplate {

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

private final ConnectionManager connectionManager;

public JdbcTemplate(ConnectionManager connectionManager) {
public JdbcTemplate(final ConnectionManager connectionManager) {
this.connectionManager = connectionManager;
}

public void executeUpdate(String query, Object... parameters) {
try (final Connection connection = connectionManager.getConnection();
final PreparedStatement preparedStatement = connection.prepareStatement(query)) {
log.info("query: {}", query);
setParameters(preparedStatement, parameters);
preparedStatement.executeUpdate();
} catch (SQLException exception) {
throw new SqlQueryException(exception.getMessage(), query);
}
public int executeUpdate(final String query, final Object... parameters) {
return execute(query, (connection, preparedStatement) -> preparedStatement.executeUpdate(), parameters);
}

public <T> T executeQueryForObject(String query, RowMapper<T> rowMapper, Object... parameters) {
try (final Connection connection = connectionManager.getConnection();
final PreparedStatement preparedStatement = connection.prepareStatement(query);
final ResultSet resultSet = executePreparedStatementQuery(preparedStatement, parameters)) {
log.info("query: {}", query);
public <T> T executeQueryForObject(
final String query,
final RowMapper<T> rowMapper,
final Object... parameters
) {
final ResultSetExtractor<T> resultSetExtractor = resultSet -> {
if (resultSet.next()) {
return rowMapper.mapRow(resultSet);
}
return null;
} catch (SQLException exception) {
throw new SqlQueryException(exception.getMessage(), query);
}
};

return executeQuery(query, resultSetExtractor, parameters);
}

public <T> List<T> executeQueryForList(String query, RowMapper<T> rowMapper, Object... parameters) {
try (final Connection connection = connectionManager.getConnection();
final PreparedStatement preparedStatement = connection.prepareStatement(query);
final ResultSet resultSet = executePreparedStatementQuery(preparedStatement, parameters)) {
log.info("query: {}", query);
public <T> List<T> executeQueryForList(
final String query,
final RowMapper<T> rowMapper,
final Object... parameters
) {
final ResultSetExtractor<List<T>> resultSetExtractor = resultSet -> {
final List<T> results = new ArrayList<>();
while (resultSet.next()) {
results.add(rowMapper.mapRow(resultSet));
}
return results;
};

return executeQuery(query, resultSetExtractor, parameters);
}

public <T> T executeQuery(
final String query,
final ResultSetExtractor<T> resultSetExtractor,
final Object... parameters
) {
return execute(query, (connection, preparedStatement) -> {
try (final ResultSet resultSet = preparedStatement.executeQuery()) {
return resultSetExtractor.extract(resultSet);
}
}, parameters);
}

private <T> T execute(
final String query,
final ConnectionCallback<T> callback,
final Object... parameters
) {
try (final Connection connection = connectionManager.getConnection();
final PreparedStatement preparedStatement = connection.prepareStatement(query)) {
log.info("query: {}", query);
setParameters(preparedStatement, parameters);
return callback.doInConnection(connection, preparedStatement);
Copy link

Choose a reason for hiding this comment

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

어마어마한 리팩토링이네요! 힌트를 안보고 진행하셨다고 하셨는데 그러면 직접 코드를 보신건가요? 아니면 공식문서를 보신건가요? 각 메서드별로 기능들이 확실하게 나눠져있고, 그 나눠진 로직을 람다를 이용해 깔끔하게 리팩토링하신게 인상적입니다.. 특히 callback.doInConnection은 아직도 이해를 잘 못한거 같은데 이 메서드를 통해 반환 값을 사용하는쪽에서 정의하도록 한 거죠?

Copy link
Author

@yoondgu yoondgu Oct 6, 2023

Choose a reason for hiding this comment

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

헉 감사합니다 !
이전에 JdbcTemplate가 템플릿 콜백 메서드 패턴을 사용한다는 것을 접했고, JdbcTemplate을 사용할 때 RowMapper나 ResultSetExtractor를 사용했던 경험을 기반으로 진행했습니다.
그 과정에서 JdbcTemplate이 사용하는 콜백 인터페이스 중 ConncectionCallback 인터페이스의 시그니처를 보고 제 코드에 적용시켜봤어요.

특히 callback.doInConnection은 아직도 이해를 잘 못한거 같은데 이 메서드를 통해 반환 값을 사용하는쪽에서 정의하도록 한 거죠?

네 맞습니다! 사실 헷갈리셨을 것 같은게,
제출 후 코드를 다시 보니 제가 doInConnection에서 connection, preparedStatement를 받고 있지만 사실 connection은 사용하지 않고 있더라구요. (그래서 적절한 시그니처와 클래스 네이밍은 아니었던 것 같네용..ㅎㅎ 3단계에서 수정하려고 합니다)
에코가 말씀해주신 것처럼 preparedStatement를 사용해 반환값을 정의하는 역할만 하고 있다고 보시면 될 것 같아요.

} catch (SQLException exception) {
throw new SqlQueryException(exception.getMessage(), query);
}
}

private ResultSet executePreparedStatementQuery(PreparedStatement preparedStatement, Object... parameters) throws SQLException {
setParameters(preparedStatement, parameters);
return preparedStatement.executeQuery();
}

private void setParameters(PreparedStatement preparedStatement, Object... parameters) throws SQLException {
private void setParameters(final PreparedStatement preparedStatement, final Object... parameters)
throws SQLException {
for (int index = 1; index <= parameters.length; index++) {
preparedStatement.setString(index, String.valueOf(parameters[index - 1]));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.springframework.jdbc.core;

import java.sql.ResultSet;
import java.sql.SQLException;


public interface ResultSetExtractor<T> {

T extract(ResultSet resultSet) throws SQLException;

}
115 changes: 115 additions & 0 deletions jdbc/src/test/java/nextstep/jdbc/JdbcTemplateTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,120 @@
package nextstep.jdbc;

import static org.assertj.core.api.Assertions.assertThat;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import org.h2.jdbcx.JdbcDataSource;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.CannotGetJdbcConnectionException;
import org.springframework.jdbc.core.ConnectionManager;
import org.springframework.jdbc.core.JdbcTemplate;

class JdbcTemplateTest {

private JdbcTemplate jdbcTemplate;

@BeforeEach
void setUp() {
TestConnectionManager connectionManager = new TestConnectionManager();
this.jdbcTemplate = new JdbcTemplate(connectionManager);
try (Connection conn = connectionManager.getConnection()) {
conn.setAutoCommit(true);
Copy link

Choose a reason for hiding this comment

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

혹시 AutoCommit을 true로 했다가 false로 하신 이유가 있을까요?!

Copy link
Author

Choose a reason for hiding this comment

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

앗 사실 이 부분은 테스트 환경 설정을 빠르게 하려고
학습 테스트 PoolingVsNoPoolingTest 코드를 가져왔는데요..ㅎㅎ
DDL을 실행하는 부분만 AutoCommit으로 처리하고 나머지 테스트 내용에서는 AutoCommit으로 할 필요가 없어서(테스트 내용에 따라 직접 커밋/롤백을 하는 게 나아서) 이렇게 했다고 이해했어요.

try (Statement stmt = conn.createStatement()) {
stmt.execute("DROP TABLE IF EXISTS users;");
stmt.execute(
"CREATE TABLE IF NOT EXISTS users (id INT AUTO_INCREMENT PRIMARY KEY, email VARCHAR(100) NOT NULL)");
stmt.executeUpdate("INSERT INTO users (email) VALUES ('[email protected]')");
conn.setAutoCommit(false);
}
} catch (SQLException e) {
throw new AssertionError(e);
}
}

@Test
@DisplayName("쓰기 작업(생성, 수정, 삭제)을 하는 쿼리를 실행한다.")
void executeUpdate() {
// given
failIfExceptionThrownBy(() -> {
// when
final var updated = jdbcTemplate.executeUpdate("insert into users (email) values (?)", "[email protected]");

// then
assertThat(updated).isOne();
});
}

@Test
@DisplayName("단건 레코드를 조회하는 쿼리를 실행한다.")
void executeQueryForObject() {
// given
failIfExceptionThrownBy(() -> {
// when
User user = jdbcTemplate.executeQueryForObject(
"select email from users where id = ?",
resultSet -> new User(resultSet.getString(1)), 1L
);

// then
assertThat(user).usingRecursiveComparison()
.comparingOnlyFields("[email protected]");
});
}

@Test
@DisplayName("다건 레코드를 조회하는 쿼리를 실행한다.")
void executeQueryForList() {
// given
jdbcTemplate.executeUpdate("insert into users (email) values (?)", "[email protected]");

failIfExceptionThrownBy(() -> {
// when
List<User> users = jdbcTemplate.executeQueryForList(
"select email from users",
resultSet -> new User(resultSet.getString(1))
);

// then
assertThat(users).extracting("email")
.containsExactlyInAnyOrder("[email protected]", "[email protected]");
});
}

private void failIfExceptionThrownBy(Runnable test) {
try {
test.run();
} catch (Exception exception) {
Assertions.fail(exception);
}
}

private static class User {

private final String email;

public User(final String email) {
this.email = email;
}

}

private static class TestConnectionManager implements ConnectionManager {

@Override
public Connection getConnection() throws CannotGetJdbcConnectionException {
try {
final var jdbcDataSource = new JdbcDataSource();
jdbcDataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;");
return jdbcDataSource.getConnection();
} catch (SQLException sqlException) {
throw new AssertionError();
}
}
}
}
Loading