출처: https://brownbears.tistory.com/289
커넥션을 관리하는 주체
(JDBC에서는 드라이버 매니저 외에 DataSource를 이용해서 커넥션을 연결 가능)
DataBase Connection Pool(DBCP)
매번 커넥션을 생성 -> close하면 많은 자원이 소모된다.
커넥션 풀은 커넥션을 미리 만들어서 풀에 저장.
필요 시마다 가져와서 사용한 뒤 반환하는 방법이다.
커넥션 풀은 DataSource가 관리하고 있다.
pom.xml에 spring-boot-starter-jdbc
의존성을 추가하면 HikariCP를 포함한 많은 라이브러리들이 추가된다.
비교 포인트
Driver manager
를 이용한 CRUD코드를 작성했을 때는
getConnection()
에 url, user, password를 입력해야했다.
HikariCP를 이용하면 Connection Pool에서 Connection을 가져오기 때문에
Dependency injection을 사용해서 구현체를 바꿔 동작할 수 있다.
@Repository // 컴포넌트 대상이 되기 위해 @Repository 추가
public class CustomerJDBCRepository implements CusotomerRepository {
private static final Logger logger = LoggerFactory.getLogger(JdbcCustomerRepository.class);
private final DataSource dataSource;
// 생성자 주입을 통해 주입
public CustomerJDBCRepository(DataSource datasource) {
this.dataSource = datasource;
}
// SELECT
@Override
public List<Customer> findAll() {
List<Customer> allCustomers = new ArrayList<>();
try (
var connection = dataSource.getConnection();
var statement = connection.prepareStatement("select * from customers");
var resultSet = statement.executeQuery()
) {
while (resultSet.next()) {
mapToCustomer(allCustomers, resultSet);
}
} catch (SQLException throwable) {
logger.error("Got error while closing connection", throwable);
throw new RuntimeException(throwable); // RuntimeException으로 반환
}
return allCustomers;
}
// SELECT
@Override
public Optional<Customer> findById(UUID customerId) {
List<Customer> allCustomers = new ArrayList<>();
try (
var connection = dataSource.getConnection();
var statement = connection.prepareStatement("select * from customers WHERE customer_id = UUID_TO_BIN(?)");
) {
statement.setBytes(1, customerId.toString().getBytes());
try (var resultSet = statement.executeQuery()) {
while (resultSet.next()) {
mapToCustomer(allCustomers, resultSet);
}
}
} catch (SQLException throwable) {
logger.error("Got error while closing connection", throwable);
throw new RuntimeException(throwable); // RuntimeException으로 반환
}
return allCustomers.stream().findFirst();
}
// SELECT
@Override
public Optional<Customer> findByName(String name) {
List<Customer> allCustomers = new ArrayList<>();
try (
var connection = dataSource.getConnection();
var statement = connection.prepareStatement("select * from customers WHERE name = ? ");
) {
statement.setString(1, name);
logger.info("statement -> {}", statement);
try (var resultSet = statement.executeQuery()) {
while (resultSet.next()) {
mapToCustomer(allCustomers, resultSet);
}
}
} catch (SQLException throwable) {
logger.error("Got error while closing connection", throwable);
throw new RuntimeException(throwable); // RuntimeException으로 반환
}
return allCustomers.stream().findFirst();
}
// SELECT
@Override
public Optional<Customer> findByEmail(String email) {
List<Customer> allCustomers = new ArrayList<>();
try (
var connection = dataSource.getConnection();
var statement = connection.prepareStatement("select * from customers WHERE email = ? ");
) {
statement.setString(1, email);
logger.info("statement -> {}", statement);
try (var resultSet = statement.executeQuery()) {
while (resultSet.next()) {
mapToCustomer(allCustomers, resultSet);
}
}
} catch (SQLException throwable) {
logger.error("Got error while closing connection", throwable);
throw new RuntimeException(throwable); // RuntimeException으로 반환
}
return allCustomers.stream().findFirst();
}
// INSERT
@Override
public Customer insert(Customer customer) {
try (
var connection = dataSource.getConnection();
var statement = connection.prepareStatement("INSERT INTO customers(customer_id, name, email, created_at) VALUES (UUID_TO_BIN(?), ?, ?, ?)");
) {
statement.setBytes(1, customer.getCustomerId().toString().getBytes());
statement.setString(2, customer.getName());
statement.setString(3, customer.getEmail());
statement.setTimestamp(4, Timestamp.valueOf(customer.getCreatedAt()));
var executeUpdate = statement.executeUpdate();
if (executeUpdate != 1) { // 추가 여부 확인
throw new RuntimeException("Nothing was inserted");
}
return customer;
} catch (SQLException throwable) {
logger.error("Got error while closing connection", throwable);
throw new RuntimeException(throwable); // RuntimeException으로 반환
}
}
// DELETE
@Override
public void deleteAll() {
try (
var connection = dataSource.getConnection();
var statement = connection.prepareStatement("DELETE FROM customers");
) {
statement.executeUpdate();
} catch (SQLException throwable) {
logger.error("Got error while closing connection", throwable);
throw new RuntimeException(throwable); // RuntimeException으로 반환
}
}
// UPDATE
@Override
public Customer update(Customer customer) {
try (
var connection = dataSource.getConnection();
var statement = connection.prepareStatement("UPDATE customers SET name = ?, email = ?, last_login_at = ? WHERE customer_id = UUID_TO_BIN(?)");
) {
statement.setString(1, customer.getName());
statement.setString(2, customer.getEmail());
statement.setTimestamp(3, customer.getLastLoginAt() != null ? Timestamp.valueOf(customer.getLastLoginAt()) : null);
statement.setBytes(4, customer.getCustomerId().toString().getBytes());
var executeUpdate = statement.executeUpdate();
if (executeUpdate != 1) { // 업데이트 여부 확인
throw new RuntimeException("Nothing was updated");
}
return customer;
} catch (SQLException throwable) {
logger.error("Got error while closing connection", throwable);
throw new RuntimeException(throwable); // RuntimeException으로 반환
}
}
// 테이블의 행을 select해서 List에 추가하는 메소드
private void mapToCustomer(List<Customer> allCustomers, ResultSet resultSet) throws SQLException {
var customerName = resultSet.getString("name");
var email = resultSet.getString("email");
var customerId = toUUID(resultSet.getBytes("customer_id"));
var lastLoginAt = resultSet.getTimestamp("last_login_at") != null ?
resultSet.getTimestamp("last_login_at").toLocalDateTime() : null;
var createdAt = resultSet.getTimestamp("created_at").toLocalDateTime();
allCustomers.add(new Customer(customerId, customerName, email, lastLoginAt, createdAt));
}
static UUID toUUID(byte[] bytes){
var byteBuffer = ByteBuffer.wrap(bytes);
return new UUID(byteBuffer.getLong(), byteBuffer.getLong());
}
}
DataSource 사용 시 connection 생성과 예외처리 부분이 반복된다.
스프링에서는 이렇게 반복되는 코드와 변경되는 부분을 Jdbc Template을 이용하여 제거할 수 있다.
template callback 패턴을 이용한다.
dataSource 필요
import javax.sql.DataSource;
@Repository // 컴포넌트 대상이 되기 위해 @Repository 추가
public class CustomerJDBCRepository implements CusotomerRepository {
private static final Logger logger = LoggerFactory.getLogger(JdbcCustomerRepository.class);
private final DataSource dataSource;
private final JdbcTemplate jdbcTemplate;
//
private static RowMapper<Customer> customerRowMapper = (resultSet, i) -> {
// RowNum의 이름없는 메소드 구현: (resultSet, 인덱스) return Customer
var customerName = resultSet.getString("name");
var email = resultSet.getString("email");
var customerId = toUUID(resultSet.getBytes("customer_id"));
var lastLoginAt = resultSet.getTimestamp("last_login_at") != null ?
resultSet.getTimestamp("last_login_at").toLocalDateTime() : null;
var createdAt = resultSet.getTimestamp("created_at").toLocalDateTime();
return new Customer(customerId, customerName, email, lastLoginAt, createdAt);
};
public CustomerJDBCRepository(DataSource datasource, JdbcTemplate jdbcTemplate) {
this.dataSource = datasource;
this.jdbcTemplate = jdbcTemplate;
}
// COUNT
@Override
public int count() {
return jdbcTemplate.queryForObject("select count(*) from customers", Integer.class); // count()함수의 return타입을 설정 가능
}
// SELECT
@Override
public List<Customer> findAll() {
// jdbcTemplate.query(sql문, RowMapper) return List<>
return jdbcTemplate.query("select * from customers", customerRowMapper);
}
// SELECT
@Override
public Optional<Customer> findById(UUID customerId) {
try {
// jdbcTemplate.queryForObject(sql문, RowMapper, sql문에 치환될 파라미터 값) return 단일객체
return Optional.ofNullable(jdbcTemplate.queryForObject("select * from customers WHERE customer_id = UUID_TO_BIN(?)",
customerRowMapper,
customerId.toString().getBytes())); // '?' 에 들어가는 파라미터 호출 가능
} catch (EmptyResultDataAccessException e) {
logger.error("Got empty result", e);
return Optional.empty();
}
}
// SELECT
@Override
public Optional<Customer> findByName(String name) {
List<Customer> allCustomers = new ArrayList<>();
try {
return Optional.ofNullable(jdbcTemplate.queryForObject("select * from customers WHERE name = ?",
customerRowMapper,
name));
} catch (EmptyResultDataAccessException e) {
logger.error("Got empty result", e);
return Optional.empty();
}
}
// SELECT
@Override
public Optional<Customer> findByEmail(String email) {
List<Customer> allCustomers = new ArrayList<>();
try {
return Optional.ofNullable(jdbcTemplate.queryForObject("select * from customers WHERE email = ?",
customerRowMapper,
email));
} catch (EmptyResultDataAccessException e) {
logger.error("Got empty result", e);
return Optional.empty();
}
}
// INSERT
@Override
public Customer insert(Customer customer) {
// jdbcTemplate.update(sql문, sql문에 치환될 파라미터 값) return 단일객체
var update = jdbcTemplate.update("INSERT INTO customers(customer_id, name, email, created_at) VALUES (UUID_TO_BIN(?), ?, ?, ?)",
customer.getCustomerId().toString().getBytes(),
customer.getName(),
customer.getEmail(),
Timestamp.valueOf(customer.getCreatedAt()));
if (update != 1) { // 추가 여부 확인
throw new RuntimeException("Nothing was inserted");
}
return customer;
}
// UPDATE
@Override
public Customer update(Customer customer) {
var update = jdbcTemplate.update("UPDATE customers SET name = ?, email = ?, last_login_at = ? WHERE customer_id = UUID_TO_BIN(?)",
customer.getName(),
customer.getEmail(),
customer.getLastLoginAt() != null ? Timestamp.valueOf(customer.getLastLoginAt()) : null,
customer.getCustomerId().toString().getBytes()
);
if (update != 1) { // 업데이트 여부 확인
throw new RuntimeException("Nothing was updated");
}
return customer;
}
// DELETE
@Override
public void deleteAll() {
jdbcTemplate.update("DELETE FROM customers");
}
static UUID toUUID(byte[] bytes) {
var byteBuffer = ByteBuffer.wrap(bytes);
return new UUID(byteBuffer.getLong(), byteBuffer.getLong());
}
}
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
…
@SpringJUnitConfig
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS) // 인스턴스가 하나만 생성
class CustomerJDBCRepositoryTest {
@Configuration
@ComponentScan(
basePackages = {"org.prgrms.kdt.customer"}
)
static class Config {
@Bean
public DataSource dataSource() {
var dataSource = DataSourceBuilder.create()
.url("jdbc:mysql://localhost/order_mgmt")
.username("root")
.password("root1234!")
.type(HikariDataSource.class) // (기본) HikariDataSource가 pool에 10개의 connection을 채워넣는다.
.build();
dataSource.setMaximumPoolSize(1000); // connection 사이즈를 1000으로 설정
dataSource.setMinimumIdle(100); // 기본 connection을 100개로 설정
return dataSource;
}}
@Autowired
CustomerJDBCRepository customerJDBCRepository;
@Autowired
DataSource dataSource; // 등록된 bean
Customer newCustomer;
@BeforeAll
void setup() {
newCustomer = new Customer(UUID.randomUUID(), "test-user", "test1-user@gmail.com", LocalDateTime.now());
customerJDBCRepository.deleteAll();
}
@Test
@Order(1)
public void testHikariConnectionPool() {
assertThat(dataSource.getClass().getName(), is("com.zaxxer.hikari.HikariDataSource"));
}
@Test
@Order(2)
@DisplayName("고객을 추가할 수 있다.")
public void testInsert() {
customerJDBCRepository.insert(newCustomer);
var retrievedCustomer = customerJDBCRepository.findById(newCustomer.getCustomerId());
assertThat(retrievedCustomer.isEmpty(), is(false));
assertThat(retrievedCustomer.get(), samePropertyValuesAs(newCustomer));
}
@Test
@Order(3)
@DisplayName("전체 고객을 조회할 수 있다.")
public void testFindAll() {
var customers = customerJDBCRepository.findAll();
assertThat(customers.isEmpty(), is(false));
}
@Test
@Order(4)
@DisplayName("이름으로 고객을 조회할 수 있다.")
public void testFindByName() {
var customer = customerJDBCRepository.findByName(newCustomer.getName());
assertThat(customer.isEmpty(), is(false));
var unknown = customerJDBCRepository.findByName("unknown-user"); // 알 수 없는 고객 조회
assertThat(unknown.isEmpty(), is(true));
}
@Test
@Order(5)
@DisplayName("이메일로 고객을 조회할 수 있다.")
public void testFindByEmail() {
var customer = customerJDBCRepository.findByName(newCustomer.getEmail());
assertThat(customer.isEmpty(), is(false));
var unknown = customerJDBCRepository.findByName("unknown-user@gmail.com"); // 알 수 없는 고객 조회
assertThat(unknown.isEmpty(), is(true));
}
@Test
@Order(6)
@DisplayName("고객을 수정할 수 있다.")
public void testUpdate() {
newCustomer.changeName("updated-user");
customerJDBCRepository.update(newCustomer);
var all = customerJDBCRepository.findAll();
assertThat(all, hasSize(1));
assertThat(all, everyItem(samePropertyValuesAs(newCustomer)));
// 전체 데이터의 정확성 테스트
var retrievedCustomer = customerJDBCRepository.findById(newCustomer.getCustomerId());
assertThat(retrievedCustomer.isEmpty(), is(false));
assertThat(retrievedCustomer.get(), samePropertyValuesAs(newCustomer));
}
}
static class Config {
@Bean
public DataSource dataSource() {
var dataSource = DataSourceBuilder.create()
.url("jdbc:mysql://localhost/order_mgmt")
.username("root")
.password("root1234!")
.type(HikariDataSource.class) // (기본) HikariDataSource가 pool에 10개의 connection을 채워넣는다.
.build();
dataSource.setMaximumPoolSize(1000); // connection 사이즈를 1000으로 설정
dataSource.setMinimumIdle(100); // 기본 connection을 100개로 설정
return dataSource;
}
// JdbcTemplate 사용을 위한 Bean 설정
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
테스트 코드는 테스트 코드에 나열한 순서대로 실행되지 않는다.
@Order()
@SpringJUnitConfig
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) // @Order를 보장
@TestInstance(TestInstance.Lifecycle.PER_CLASS) // 인스턴스가 하나만 생성
class CustomerJDBCRepositoryTest {
@Test
@Order(1)
테스트 코드
…
@Test
@Order(n)
테스트 코드
}
@TestInstance(Lifecycle.PER_CLASS)
를 선언한 클래스는 클래스 단위 생명주기를 가진다.@BeforeAll
이나 @AfterAll
를 사용할 수 있다.@Nested
클래스에서 @BeforeAll
이나 @AfterAll
메서드를 사용할 수 있다.새로 알게 된 용어
- callback 함수
- 다른 함수의 인자로써 이용되는 함수.
- 어떤 이벤트에 의해 호출되어지는 함수.
출처: https://satisfactoryplace.tistory.com/18 [만족:티스토리]
코드 작성 시 팁
- 어떤 필드에
final
키워드가 적합한지 고민해봐야 한다.
(어떤 필드의 값이 변하지 않을 것인지?)- setter는 만들지 않는다
(setter 역할의 메소드를 따로 정의)- domain클래스 생성 시 정의된 비즈니스룰을 잘 작성하는 게 중요하다.
- 항상 Optional 사용을 고려하라.
rf
더 공부해보면 좋을 자료 (@TestMethodOrder)
참고한 블로그: yshjft님의 벨로그
[Spring Boot] JUnit 5 (5) - 테스트 인스턴스 (@TestInstance)
JUnit 5 (5)