이 글은 기존 운영했던 WordPress 블로그인 PyxisPub: Development Life (pyxispub.uzuki.live) 에서 가져온 글 입니다. 모든 글을 가져오지는 않으며, 작성 시점과 현재 시점에는 차이가 많이 존재합니다.
작성 시점: 2019-11-28
대부분의 경우에는 ORM (서버라면 JPA, 안드로이드는 ObjectBox) 를 사용했었지만, 이번 개인 프로젝트를 진행하면서 ORM을 사용하지 않게 되었다.
그 이유는, 안드로이드의 경우에는 한번 쯤은 Room을 제대로 써보고 싶다는 생각이 들었기도 했고, 서버에서는 비교적 쿼리가 들어가기 때문이었다.
물론, 쿼리의 복잡성 때문에 ORM을 선택하지 않았다는 이유는 아니고, 다른 오픈소스 프로젝트를 Spring Boot로 포팅하면서 기능을 덧붙이는 방식으로 진행했었기 때문에, DB 구조 등을 그대로 가져왔기 때문이다.
DB 구조는 표현하면 다음과 같다.
작품 테이블 - 작품 ID, 작품의 정보, 작가 등
태그 테이블 - 태그 ID, 태그 이름
작품ID-태그ID 테이블 - 작품 ID, 태그 ID (1:N)
그리고 클라이언트가 원하는 자료에는 태그 정보가 당연하게도 필요했었고, 검색하는 필터에도 태그 정보가 당연하게 들어가게 된다.
이를 위해 가능하기 위해 태그 정보까지 검색하고, 태그 정보까지 불러올 수 있는 기능을 최소한의 LEFT JOIN과 INNER JOIN을 사용해서 하려고 했었고, 결국에는 Spring Boot에서 query string를 구성해서 실행하는 방식으로 하기로 결정했다.
그렇게 되면, Spring Boot에서 ORM을 사용하지 않고 바로 데이터 소스에 연결해서 사용해야 하는데, 그 때 JDBC Template를 사용하면 비교적 쉽게 할 수 있다.
따라서 본 글에서는 간단하게나마 Spring Boot 2.1 + JDBC Template 기반으로 쿼리를 실행하고 데이터를 받을 수 있도록 다뤄보려 한다. 참고로 본 예제에서 사용한 DB는 MariaDB이다.
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
implementation("org.springframework.boot:spring-boot-starter-jdbc")
runtimeOnly("mysql:mysql-connector-java")
MariaDB를 connector로 사용할 것이므로 mysql-connector-java도 같이 둔다.
여기서 모델이란 DB 결과를 파싱해서 보여줄 수 있는 클래스 객체이며, ORM의 Entity와는 다르게 어떠한 부가정보도 필요로 하지 않는다.
단, 이 클래스의 구현은 RowMapper와 관련이 있는데, 만일 쿼리의 SELECT 로 가져오는 Column들에 대해서 같은 이름으로 클래스의 프로퍼티를 만들어주는 것이 가능하다면, BeanPropertyRowMapper 라고 하는 Mapper로 쓸 수 있기 때문이다.
즉, 쿼리에서 나올 항목이 id, name 이라면 해당 클래스는 똑같이 id, name를 담고 있으면 된다.
## 데이터 소스 접속 정보 설정
spring.datasource.url=jdbc:mysql://{ADDRESS}/{DB_NAME}?characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username={USERNAME}
spring.datasource.password={PASSWORD}
## 로깅 설정
logging.path=logs
logging.level.com.tutorial.springboot=DEBUG
## HikariCP에서 사용되는 타임아웃 시간 정의
spring.datasource.hikari.idleTimeout=40000
spring.datasource.hikari.connection-timeout=5000
spring.datasource.hikari.validation-timeout=10000
spring.datasource.hikari.maxLifetime=580000
Spring Boot 2.0 부터 HikariCP가 기본 Connection Pool 관리 도구로 선정되었으므로, HikariCP의 설정을 포함하면서도 기본 데이터 소스에 대한 접속 정보를 설정한다.
상기된 것 처럼 여기에서는 MariaDB를 사용할 것이므로 jdbc:mysql://
를 사용했다.
쿼리는 Repository에서 접근하는 구조로 간다고 가정해본다.
Repository에서는 JdbcTemplate
라는 클래스를 Autowired
로 주입받게 되면, SpringBootApplication
이 실행될 때 applications.properties 에 기재된 접속 정보를 가지고 DataSource 객체를 생성해서 JdbcTemplate 객체로 관리할 수 있게 된다.
@Autowired
private lateinit var jdbcTemplate: JdbcTemplate
또는, 전통적인 ?
를 대신해 파라미터 이름을 사용할 수 있게 하는 NamedParameterJdbcTemplate
도 사용이 가능하다.
@Autowired
private lateinit var jdbcTemplate: NamedParameterJdbcTemplate
쿼리는 jdbcTemplate.query(String, RowMapper)
로 실행이 가능하고, NamedParameterJdbcTemplate 한정으로 Param을 넣을 수 있는 jdbcTemplate.query(String, Map, RowMapper)
사용이 가능하다.
가령, 유저의 id와 이름을 가져오는 query를 실행한다면, 아래와 구현할 수 있다.
@Repository
class JdbcUserRepository : UserRepository {
@Autowired
private lateinit var jdbcTemplate: NamedParameterJdbcTemplate
override fun findAll(): List<User> {
val query = "SELECT id, name FROM users"
return jdbcTemplate.query(query, BeanPropertyRowMapper(User::class.java))
}
}
전 챕터에서 잠깐 언급되었던 BeanPropertyRowMapper 는 주어진 클래스 객체를 가지고 결과를 파싱할 수 있게 도와주는 클래스로, 편의성을 위해 사용된다. (성능을 고려한다면, 직접 RowMapper 클래스를 구현하여 사용하도록 권장되고 있다.)
만일 RowMapper를 직접 구현한다면 아래와 같다.
@Repository
class JdbcUserRepository : UserRepository {
@Autowired
private lateinit var jdbcTemplate: NamedParameterJdbcTemplate
override fun findAll(): List<User> {
val query = "SELECT id, name FROM users"
return jdbcTemplate.query(query) { rs, rowNum ->
User().apply {
name = rs.getString("name")
id = rs.getString("id")
}
}
}
}
그 외에도 가변 인자를 사용할 수 있는 SimpleJdbcTemplate 등도 있으나 더 언급하지는 않을 예정이다.
먼저, JdbcTemplate는 JDBC 사용을 단순화하고 일반적인 오류를 피하는 데에 큰 도움을 줄 수 있다는 것이다. 위의 코드를 보면 단순히 개발자가 한 것은 접속 정보를 설정하고 Repository에서 @Autowired 어노테이션으로 통해 인스턴스를 주입받은 것 밖에는 존재하지 않는다.
이는 나머지 처리에 대해 JdbcTemplate가 적절하게 처리해준다는 것을 보여주기도 하며, org.springframework.dao 패키지에 정의된 것 보다 유익한 계층 구조로 반환해줄 수 있다는 의미이다.
또한, JdbcTemplate의 인스턴스는 한번 메모리에 로드되면 Thrad-Safe 하게 된다.
즉, 한번 생성된 인스턴스는 SpringBootApplications 내부에 있는 Repository들에게 공유된 참조를 보내줄 수 있는데, 이는 JdbcTemplate가 DataSource에 대한 상태는 가지고 있지만 이 상태는 대화 상태(Conversational state) 가 아니다는 특성을 가지고 있기 때문이다.
물론 이러한 특성을 가진 탓에 실행하는 쿼리에 maxResult를 설정하는 것이 중요한데, 이는 하나의 Repository에서 하는 작업이 다른 Repository에 영향을 줄 수 있기 때문이다.