SpringBoot Part2 (3)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
DataSource
connection을 매번 생성하고 close하면 그 과정에서 많은 리소스가 허비된다.
이러한 문제를 해결하기 위해 connection pool 개념이 등장.
JDBC에서는 driver maneger 외에 dataSource를 이용해 커넥션을 가져올 수 있고, dataSource가 connection을 관리하는 객체가된다.
DataBase Connection Pool(DBCP)
dataSource를 통해 데이터를 얻거나 반납할 수 있다.
close를 하면 실제 물리적으로 연결을 끊는게 아니라 풀에 반납하는 것
DBCP를 구현한 다양한 라이브러리가 있다.
(simple driver data sourse → pool이 아닌 driver에서 가져와 테스트 용으로 사용)
HikariCP
톰켓에서 제공하는 DBCP를 많이 사용했으나 springboot 2.0이상부터 HikariCP를 기본저그올 사용한다.
빠르고 안정적인 DBCP 오픈소스
→ 별다른 설치 없이 spring-boot-starter-jdbc 에 라이브러리가 포함되어있다.
CustomerRepository
1. Customer 객체 생성
: setter을 무분별하게 만들지 않도록 유의한다.
: 메소드 이름을 명확하게 한다. (ex - changeName)
2. CustomerRopository 구현
기존 DriverManager 방식
public List<String> findByNames(String name){
List<String> names = new ArrayList<>();
try(
var connection = DriverManager.getConnection("jdbc:mysql://localhost/order_mgmt","root","dudwl0804!");
var statement = connection.prepareStatement(SELECT_ALL_SQL);
var resultSet = statement.executeQuery();
) {
while(resultSet.next()){
var customerName = resultSet.getString("name");
var customerId = toUUID(resultSet.getBytes("customer_id"));
var createdAt = resultSet.getTimestamp("created_at").toLocalDateTime();
names.add(customerName);
}
} catch (SQLException e) {
logger.error("Got error while closing connection", e);
}
return names;
}
Datasource 사용
private final DataSource dataSource; // 생성자로 주입받기
public List<Customer> findAll() {
List<Customer> customers = new ArrayList<>();
try(
var connection = dataSource.getConnection();
var statement = connection.prepareStatement("select * from customers");
var resultSet = statement.executeQuery();
) {
while(resultSet.next()){
var customerName = resultSet.getString("name");
var customerId = UUID.nameUUIDFromBytes(resultSet.getBytes("customer_id"));
var email = resultSet.getString("email");
var createdAt = resultSet.getTimestamp("created_at").toLocalDateTime();
var lastLoginAt = resultSet.getTimestamp("last_login_at") != null ?
resultSet.getTimestamp("last_login_at").toLocalDateTime() :null;
customers.add(new Customer(customerId,customerName,email,lastLoginAt,createdAt));
}
} catch (SQLException e) {
logger.error("Got error while closing connection", e);
throw new RuntimeException(e);
}
return customers;
}
- (추가) connection pool에 connection 늘리기
public DataSource dataSource(){
HikariDataSource dataSource = DataSourceBuilder.create()
.url("jdbc:mysql://localhost/order_mgmt")
.username(id)
.password(password)
.type(HikariDataSource.class) // datasource 만들 구현체 타입 지정
.build();
// connection pool은 기본적으로 10개의 connection을 만든다.
// 100개로 늘리고 싶다면
dataSource.setMaximumPoolSize(1000);
dataSource.setMinimumIdle(100);
return dataSource;
}
show status like "%Thread%" 쿼리를 통해 connection 개수 확인가능
TestInstance Lifecycle
@BeforeAll
→ static으로 선언되어야한다.
→ static 메소드만 호출이 가능하다. (안좋음)
→ @TestInstance 을 이용해 Lifecycle을 PER_CLASS 로 지정하면 class 마다 인스턴스가 하나만 생성된다.
@BeforeAll이 static 메소드로 선언되어야하는 이유?
LifeCycle은 기본으로 PRE_METHOD로 설정되어있다. 즉, 각각의 테스트 메소드를 실행할때마다 새로운 class인스턴스를 생성한다. 한번만 실행되어야하는 BeforeAll 경우 기본적으로 static 메소드로 선언되어야한다.
@TestMethodOrder
→ 테스트 메소드의 동작 순서를 보장해준다.
@SpringJUnitConfig
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS) // PER_CLASS : class 마다 인스턴스가 하나
class CustomerJdbcRepositoryTest {
@Configuration
@ComponentScan(
basePackages = {"org.prgrms.kdt.customer"}
)
static class Config {
@Bean
public DataSource dataSource() {
HikariDataSource dataSource = DataSourceBuilder.create()
.url("jdbc:mysql://localhost/order_mgmt")
.username(id)
.password(pw)
.type(HikariDataSource.class) // datasource 만들 구현체 타입 지정
.build();
return dataSource;
}
}
@Autowired
CustomerJdbcRepository customerJdbcRepository;
@Autowired
DataSource dataSource;
Customer newCustomers; // 전역변수로 등록하여 하드코딩하지 않고 getter를 이용한다.
@BeforeAll
void setup(){
customerJdbcRepository.deleteAll();
newCustomers = new Customer(UUID.randomUUID(), "test-user", "test_user@gmail.com", LocalDateTime.now());
}
@Test
@Order(1)
@DisplayName("Hikari 연결 확인")
public void testHikariConnectionPool() {
assertThat(dataSource.getClass().getName(), is("com.zaxxer.hikari.HikariDataSource"));
}
@Test
@Order(2)
@DisplayName("고객을 추가할 수 있다.")
public void testInsert() {
customerJdbcRepository.insert(newCustomers);
var retrievedCustomer = customerJdbcRepository.findById(newCustomers.getCustomerId());
assertThat(retrievedCustomer.isEmpty(), is(false));
assertThat(retrievedCustomer.get(), samePropertyValuesAs(newCustomers));
// samePropertyValuesAs -> 안에 있는 property들을 다 검사해준다.
}
@Test
@Order(3)
@DisplayName("전체 고객을 조회할 수 있다.")
public void testFindAll() {
var customers = customerJdbcRepository.findAll();
assertThat(customers.isEmpty(), is(false));
}
}
추가
samePropertyValuesAs
→ 안에 있는 property들을 전부 검사해준다.
→ LocatDateTime의 정밀도가 운영체제 마다 달라 오류가 발생할 수 있어 정밀도를 맞추는 사용 방식을 추천한다.
// Mac은 정밀도가 마이크로(6자리), Window는 정밀도가 밀리(3자리)
var newCustomer = new Customer(UUID.randomUUID(), "test-user", "test-user@gmail.com", LocalDateTime.now());
// Window는 정밀도가 밀리(3자리)
var newCustomer = new Customer(UUID.randomUUID(), "test-user", "test-user@gmail.com", LocalDateTime.now().truncatedTo(ChronoUnit.MILLIS));
JDBC Template
계속해서 반복되는 부분
- connection 연결
- 예외처리
- 데이터를 map하는 로직
⇒ Spring은 반복되는 부분을 탬플릿을 만들어 놓고 Temlpate Callback Pattern을 이용해서 JDBC 탬플릿을 제공한다.
// jdbcTemplate bean 등록
@Bean
public JdbcTemplate jdbcTemplate (DataSource dataSource){
return new JdbcTemplate(dataSource);
}
// 데이터를 map하는 로직을 static final RowMapper<Customer> 변수로 선언
private static final RowMapper<Customer> customerRowMapped = new RowMapper<Customer>() {
@Override
public Customer mapRow(ResultSet rs, int rowNum) throws SQLException { //resultSet, index
var customerName = rs.getString("name");
var customerId = toUUID(rs.getBytes("customer_id"));
var email = rs.getString("email");
var createdAt = rs.getTimestamp("created_at").toLocalDateTime();
var lastLoginAt = rs.getTimestamp("last_login_at") != null ?
rs.getTimestamp("last_login_at").toLocalDateTime() : null;
return new Customer(customerId, customerName, email, lastLoginAt, createdAt);
}
};
private final JdbcTemplate jdbcTemplate;
@Override
public List<Customer> findAll() {
return jdbcTemplate.query("select * from customers", customerRowMapped); //sql,row mapper 전달
}
@Override
public Optional<Customer> findById(UUID customerId) {
try {
return Optional.ofNullable(jdbcTemplate.queryForObject("select * from customers WHERE customer_id = UUID_TO_BIN(?)", customerRowMapper,customerId.toString().getBytes()));
// queryForObject -> 하나만 꺼내기 값이 없으면 ㅇㅖ외발생
}catch (EmptyResultDataAccessException e){
return Optional.empty();
}
}
@Override
public int count() {
return jdbcTemplate.queryForObject("select count(*) from customers",Integer.class);
}
@Override
public Customer insert(Customer customer) {
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;
}
@Override
public void deleteAll() {
jdbcTemplate.update("DELETE FROM customers");
}
Template Debug
JDBC Query
: result에서 null 체크를 해준다.
: list 반환
: 내부에 templete, callback
→ 토비의 스프링 추가 학습 추천
→ 오픈 소스 들여다보기,,, 디버그해보기,,,
evaluate experssion
출처 - 해리 : SpringBoot Part1
'Back-end 데브코스 > week 03 - 05 TIL (Spring)' 카테고리의 다른 글
[TIL] 221111 - SpringBoot Part2 : 트랜잭션과 AoP (1) | 2022.11.22 |
---|---|
[TIL] 221110 - SpringBoot Part2 : Embedded DB, Named Parameter Template, 트랜잭션 (0) | 2022.11.11 |
[TIL] 221108 - SpringBoot Part2 : JDBC (0) | 2022.11.08 |
[TIL] 221107 - SpringBoot Part2 : Spring Test 시작하기 (0) | 2022.11.08 |
[TIL] 221104 - SpringBoot Part1 : logging, SpringBoot (2) | 2022.11.06 |
댓글