Skip to content

Commit

Permalink
[2단계 - JDBC 라이브러리 구현하기] 페드로(류형욱) 미션 제출합니다. (#763)
Browse files Browse the repository at this point in the history
* test: Connection pool 학습 테스트 추가

* test: Transaction 학습 테스트 추가

* style(UserHistoryDao): 쿼리를 대문자로 변경

* refactor(RowMapper): RowMapper 함수형 인터페이스 추가

* refactor(JdbcTemplate): 좀 더 구체적인 예외를 던지도록 변경

* refactor(PreparedStatementSetter): PreparedStatement에 인자를 설정하는 인터페이스 추가

* refactor(PreParedStatementArgumentSetter): PreparedStatement에 인자를 설정하는 구현체를 사용하도록 변경

* test(PreparedStatementArgumentsSetterTest): Argument 설정 로직 테스트 추가

* style(UserDao): 코드 스타일 적용

* refactor(UserHistoryDao): RowMapper 인터페이스를 사용하도록 변경
  • Loading branch information
hw0603 authored Oct 11, 2024
1 parent 4a4acbd commit ed7f467
Show file tree
Hide file tree
Showing 15 changed files with 243 additions and 117 deletions.
35 changes: 13 additions & 22 deletions app/src/main/java/com/techcourse/dao/UserDao.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
package com.techcourse.dao;

import com.techcourse.domain.User;
import com.interface21.jdbc.core.JdbcTemplate;
import java.sql.SQLException;
import com.interface21.jdbc.core.RowMapper;
import com.techcourse.domain.User;
import java.util.List;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.sql.DataSource;
import java.sql.ResultSet;
import java.util.List;

public class UserDao {

private static final Logger log = LoggerFactory.getLogger(UserDao.class);
private static final RowMapper<User> ROW_MAPPER = rs -> new User(
rs.getLong("id"),
rs.getString("account"),
rs.getString("password"),
rs.getString("email")
);

private final JdbcTemplate jdbcTemplate;

Expand All @@ -39,35 +43,22 @@ public void update(final User user) {
public List<User> findAll() {
String sql = "SELECT id, account, password, email FROM users";
logSql(sql);
return jdbcTemplate.query(sql, this::rowMapper);
return jdbcTemplate.query(sql, ROW_MAPPER);
}

public User findById(Long id) {
String sql = "SELECT id, account, password, email FROM users WHERE id = ?";
logSql(sql);
return jdbcTemplate.queryForObject(sql, this::rowMapper, id);
return jdbcTemplate.queryForObject(sql, ROW_MAPPER, id);
}

public User findByAccount(String account) {
String sql = "SELECT id, account, password, email FROM users WHERE account = ?";
logSql(sql);
return jdbcTemplate.queryForObject(sql, this::rowMapper, account);
return jdbcTemplate.queryForObject(sql, ROW_MAPPER, account);
}

private void logSql(String sql) {
log.debug("query : {}", sql);
}

private User rowMapper(ResultSet rs) {
try {
return new User(
rs.getLong("id"),
rs.getString("account"),
rs.getString("password"),
rs.getString("email")
);
} catch (SQLException e) {
throw new IllegalStateException("쿼리 실행 결과가 User 형식과 일치하지 않습니다.", e);
}
}
}
29 changes: 10 additions & 19 deletions app/src/main/java/com/techcourse/dao/UserHistoryDao.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
package com.techcourse.dao;

import com.interface21.jdbc.core.JdbcTemplate;
import com.interface21.jdbc.core.RowMapper;
import com.techcourse.domain.UserHistory;
import java.sql.ResultSet;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class UserHistoryDao {

private static final Logger log = LoggerFactory.getLogger(UserHistoryDao.class);
private static final RowMapper<UserHistory> ROW_MAPPER = rs -> new UserHistory(
rs.getLong("id"),
rs.getLong("user_id"),
rs.getString("account"),
rs.getString("password"),
rs.getString("email"),
rs.getString("created_by")
);

private final JdbcTemplate jdbcTemplate;

Expand All @@ -36,22 +43,6 @@ public void log(UserHistory userHistory) {
public UserHistory findById(Long id) {
String sql = "SELECT id, user_id, account, password, email, created_at, created_by FROM user_history WHERE id = ?";
log.debug("query : {}", sql);

return jdbcTemplate.queryForObject(sql, this::rowMapper, id);
}

private UserHistory rowMapper(ResultSet rs) {
try {
return new UserHistory(
rs.getLong("id"),
rs.getLong("user_id"),
rs.getString("account"),
rs.getString("password"),
rs.getString("email"),
rs.getString("created_by")
);
} catch (SQLException e) {
throw new IllegalStateException("쿼리 실행 결과가 User 형식과 일치하지 않습니다.", e);
}
return jdbcTemplate.queryForObject(sql, ROW_MAPPER, id);
}
}
49 changes: 21 additions & 28 deletions jdbc/src/main/java/com/interface21/jdbc/core/JdbcTemplate.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package com.interface21.jdbc.core;

import com.interface21.dao.DataAccessException;
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 java.util.function.Function;
import java.util.stream.IntStream;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -22,52 +21,46 @@ public JdbcTemplate(final DataSource dataSource) {
this.dataSource = dataSource;
}

public int update(String sql, Object... args) {
public int update(String sql, PreparedStatementSetter psSetter) {
try (Connection connection = dataSource.getConnection();
PreparedStatement ps = connection.prepareStatement(sql)) {
setParameters(ps, args);
psSetter.setValues(ps);
return ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException("Update 실패", e);
throw new DataAccessException("Update 실패", e);
}
}

public <T> T queryForObject(String sql, Function<ResultSet, T> rowMapper, Object... args) {
public int update(String sql, Object... args) {
return update(sql, new PreparedStatementArgumentsSetter(args));
}

public <T> T queryForObject(String sql, RowMapper<T> rowMapper, Object... args) {
List<T> query = query(sql, rowMapper, args);
return query.isEmpty() ? null : query.getLast();
}

public <T> List<T> query(String sql, Function<ResultSet, T> rowMapper, Object... args) {
List<T> results = new ArrayList<>();
public <T> List<T> query(String sql, PreparedStatementSetter psSetter, RowMapper<T> rowMapper) {
try (Connection connection = dataSource.getConnection();
PreparedStatement ps = connection.prepareStatement(sql)) {
setParameters(ps, args);
retrieveRow(rowMapper, ps, results);
psSetter.setValues(ps);
return retrieveRow(rowMapper, ps);
} catch (SQLException e) {
throw new RuntimeException("Query 실패", e);
throw new DataAccessException("Query 실패", e);
}
return results;
}

private <T> void retrieveRow(
Function<ResultSet, T> rowMapper, PreparedStatement ps, List<T> results) throws SQLException {
public <T> List<T> query(String sql, RowMapper<T> rowMapper, Object... args) {
return query(sql, new PreparedStatementArgumentsSetter(args), rowMapper);
}

private <T> List<T> retrieveRow(RowMapper<T> rowMapper, PreparedStatement ps) throws SQLException {
List<T> results = new ArrayList<>();
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
results.add(rowMapper.apply(rs));
results.add(rowMapper.mapRow(rs));
}
}
}

private void setParameters(PreparedStatement ps, Object... args) {
IntStream.range(0, args.length).forEach(i -> setParameterOfIdx(ps, args, i));
}

private void setParameterOfIdx(PreparedStatement ps, Object[] args, int parameterIdx) {
try {
ps.setObject(parameterIdx + 1, args[parameterIdx]);
log.info("Parameter-{} : {}", parameterIdx + 1, args[parameterIdx]);
} catch (SQLException e) {
throw new RuntimeException("파라미터 설정 실패", e);
}
return results;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.interface21.jdbc.core;

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

public class PreparedStatementArgumentsSetter implements PreparedStatementSetter {

private static final int STATEMENT_ARGUMENT_OFFSET = 1;

private final Object[] args;

public PreparedStatementArgumentsSetter(Object... args) {
this.args = args;
}

@Override
public void setValues(PreparedStatement ps) throws SQLException {
if (args == null) {
return;
}

for (int argsIndex = 0; argsIndex < args.length; argsIndex++) {
ps.setObject(argsIndex + STATEMENT_ARGUMENT_OFFSET, args[argsIndex]);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.interface21.jdbc.core;

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

@FunctionalInterface
public interface PreparedStatementSetter {

void setValues(PreparedStatement ps) throws SQLException;
}
10 changes: 10 additions & 0 deletions jdbc/src/main/java/com/interface21/jdbc/core/RowMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.interface21.jdbc.core;

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

@FunctionalInterface
public interface RowMapper<T> {

T mapRow(ResultSet rs) throws SQLException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.interface21.jdbc.core;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

class PreparedStatementArgumentsSetterTest {

private PreparedStatement preparedStatement;

@BeforeEach
void setUp() {
preparedStatement = mock(PreparedStatement.class);
}

@DisplayName("파라미터가 전달되지 않으면 아무런 값도 설정하지 않는다.")
@Test
void setNilArguments() throws SQLException {
// given
PreparedStatementArgumentsSetter argumentSetter = new PreparedStatementArgumentsSetter();

// when
argumentSetter.setValues(preparedStatement);

// then
verify(preparedStatement, never()).setObject(anyInt(), any());
}

@DisplayName("파라미터에 null이 전달되면 아무런 값도 설정하지 않는다.")
@Test
void setNullArguments() throws SQLException {
// given
PreparedStatementArgumentsSetter argumentSetter = new PreparedStatementArgumentsSetter(null);

// when
argumentSetter.setValues(preparedStatement);

// then
verify(preparedStatement, never()).setObject(anyInt(), any());
}

@DisplayName("파라미터 값을 올바르게 설정한다.")
@Test
void setValues() throws SQLException {
// given
Object[] args = {"Hello", ",", " ", "world", "!"};
PreparedStatementArgumentsSetter argumentSetter = new PreparedStatementArgumentsSetter(args);

// when
argumentSetter.setValues(preparedStatement);

// then
for (int i = 0; i < args.length; i++) {
verify(preparedStatement).setObject(i + 1, args[i]);
}
}
}
2 changes: 1 addition & 1 deletion study/src/main/java/aop/repository/UserHistoryDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public UserHistoryDao(final JdbcTemplate jdbcTemplate) {
}

public void log(final UserHistory userHistory) {
final var sql = "insert into user_history (user_id, account, password, email, created_at, created_by) values (?, ?, ?, ?, ?, ?)";
final var sql = "INSERT INTO user_history (user_id, account, password, email, created_at, created_by) VALUES (?, ?, ?, ?, ?, ?)";
jdbcTemplate.update(sql,
userHistory.getUserId(),
userHistory.getAccount(),
Expand Down
4 changes: 2 additions & 2 deletions study/src/main/resources/schema.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# mysql 8.0.30부터는 statement.execute()으로 여러 쿼리를 한 번에 실행할 수 없다.
# 멀티 쿼리 옵션을 url로 전달하도록 수정하는 방법을 찾아서 적용하자.
-- mysql 8.0.30부터는 statement.execute()으로 여러 쿼리를 한 번에 실행할 수 없다.
-- 멀티 쿼리 옵션을 url로 전달하도록 수정하는 방법을 찾아서 적용하자.
CREATE TABLE IF NOT EXISTS users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
account VARCHAR(100) NOT NULL,
Expand Down
4 changes: 3 additions & 1 deletion study/src/test/java/connectionpool/stage2/Stage2Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@ void test() throws InterruptedException {

for (final var thread : threads) {
thread.join();
assertThat(hikariPool.getActiveConnections()).isLessThanOrEqualTo(DataSourceConfig.MAXIMUM_POOL_SIZE);
}

// 동시에 많은 요청이 몰려도 최대 풀 사이즈를 유지한다.
assertThat(hikariPool.getTotalConnections()).isEqualTo(DataSourceConfig.MAXIMUM_POOL_SIZE);
assertThat(hikariPool.getActiveConnections()).isZero(); // join() 했으므로 메인 스레드는 블록됨. 스레드 안에서 try-with-resource로 커넥션을 닫았으므로 active는 0.

// DataSourceConfig 클래스에서 직접 생성한 커넥션 풀.
assertThat(hikariDataSource.getPoolName()).isEqualTo("gugu");
Expand All @@ -65,7 +67,7 @@ private Runnable getConnection() {
log.info("After acquire ");
quietlySleep(500); // Thread.sleep(500)과 동일한 기능
}
} catch (Exception e) {
} catch (Exception ignored) {
}
};
}
Expand Down
Loading

0 comments on commit ed7f467

Please sign in to comment.