본문 바로가기
Back-end 데브코스/week 03 - 05 TIL (Spring)

[TIL] 221109 - SpringBoot Part2 : Spring의 JDBC지원

by young-ji 2022. 11. 10.

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

댓글