스프링을 이용하여 관계형 데이터에 접근하는 연습
스프링 JdbcTemplate
으로 관계형 데이터베이스에 저장된 데이터를 사용하는 애플리케이션을 빌드합니다
스프링 이니셜라이저를 이용하여 JDBC API와 H2 Database를 의존성으로 추가합니다
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.0'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'guides'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
customer
Object작업하게 될 간단한 데이터 액세스 논리는 고객의 이름과 성을 관리합니다. 애플리케이션 수준에서 이 데이터를 나타내려면 다음 목록(src/main/java/guides/relationaldataaccess/Customer.java)에 표시된 대로 Customer
클래스를 만듭니다.
package guides.relationaldataaccess;
public class Customer {
private long id;
private String firstName, lastName;
public Customer(long id, String firstName, String lastName) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public String toString() {
return String.format(
"Customer[id=%d, firstName='%s', lastName='%s']",
id, firstName, lastName);
}
// getters & setters omitted for brevity
}
Spring은 SQL 관계형 데이터베이스 및 JDBC 작업을 쉽게 해주는 JdbcTemplate
이라는 템플릿 클래스를 제공합니다. 대부분의 JDBC 코드는 코드가 달성하려는 목표와 전혀 관련이 없는 리소스 획득, 연결 관리, 예외 처리 및 일반 오류 검사에 빠져 있습니다. JdbcTemplate
이 이 모든 것을 처리해 줍니다. 당신이 해야 할 일은 당면한 일에 집중하는 것뿐입니다. 다음 목록(src/main/java/guidese/relationaldataaccess/RelationalDataAccessApplication.java)은 JDBC를 통해 데이터를 저장하고 검색할 수 있는 클래스를 보여줍니다.
package guides.relationaldataaccess;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@SpringBootApplication
public class RelationalDataAccessApplication implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(RelationalDataAccessApplication.class);
public static void main(String args[]) {
SpringApplication.run(RelationalDataAccessApplication.class, args);
}
@Autowired
JdbcTemplate jdbcTemplate;
@Override
public void run(String... strings) throws Exception {
log.info("Creating tables");
jdbcTemplate.execute("DROP TABLE customers IF EXISTS");
jdbcTemplate.execute("CREATE TABLE customers(" +
"id SERIAL, first_name VARCHAR(255), last_name VARCHAR(255))");
// Split up the array of whole names into an array of first/last names
List<Object[]> splitUpNames = Arrays.asList("John Woo", "Jeff Dean", "Josh Bloch", "Josh Long").stream()
.map(name -> name.split(" "))
.collect(Collectors.toList());
// Use a Java 8 stream to print out each tuple of the list
splitUpNames.forEach(name -> log.info(String.format("Inserting customer record for %s %s", name[0], name[1])));
// Uses JdbcTemplate's batchUpdate operation to bulk load data
jdbcTemplate.batchUpdate("INSERT INTO customers(first_name, last_name) VALUES (?,?)", splitUpNames);
log.info("Querying for customer records where first_name = 'Josh':");
jdbcTemplate.query(
"SELECT id, first_name, last_name FROM customers WHERE first_name = ?",
(rs, rowNum) -> new Customer(rs.getLong("id"), rs.getString("first_name"), rs.getString("last_name")), "Josh")
.forEach(customer -> log.info(customer.toString()));
}
}
Spring Boot는 H2(인메모리 관계형 데이터베이스 엔진)를 지원하고 자동으로 연결을 생성합니다. spring-jdbc
를 사용하기 때문에 Spring Boot는 자동으로 JdbcTemplate
을 생성합니다. @Autowired JdbcTemplate
필드는 이를 자동으로 로드하여 사용 가능하게 만듭니다.
이 Application
클래스는 Spring Boot의 CommandLineRunner
를 구현합니다. 즉, 애플리케이션 컨텍스트가 로드된 후 run()
메서드를 실행합니다.
첫째, JdbcTemplate
의 execute
메소드를 사용하여 일부 DDL을 설치합니다.
둘째, 문자열 목록을 가져와 Java 8 스트림을 사용하여 firstname/lastname 쌍으로 분할한 Java 배열을 생성합니다.
그런 다음 JdbcTemplate
의 batchUpdate
메서드를 사용하여 새로 생성된 테이블에 일부 레코드를 설치합니다. 메서드 호출의 첫 번째 인수는 쿼리 문자열입니다. 마지막 인수(Object
인스턴스 배열)는 ?
글자가 있는 쿼리로 대체될 변수를 보유합니다.
단일 insert 문에서는
insert
메소드를 사용하지만 다중 insert문에서는batchUpdate
를 사용하는 편이 좋습니다.
?
를 인수를 사용하여 JDBC가 변수에 바인딩 함으로써 SQL injection 공격을 회피하도록 합니다.
마지막으로 query
메소드를 사용하여 기준과 일치하는 레코드를 테이블에서 검색합니다. 다시 ?
인수를 사용하여, 호출할 때 실제 값을 전달할 쿼리 매개변수를 생성합니다. 마지막 인수는 각 결과 행을 새 Customer
개체로 변환하는 데 사용되는 Java 8 람다입니다.
Java 8 람다는 Spring의
RowMapper
와 같은 단일 메서드 인터페이스에 잘 매핑됩니다. Java 7 이하를 사용하는 경우 익명 인터페이스 구현을 연결하고 메서드 본문을 람다 식의 본문과 동일하게 만들 수 있습니다.
CommandLineRunner는 스프링 부트에서 제공하는 인터페이스이며 다음과 같이 정의되어 있습니다.
@FunctionalInterface
public interface CommandLineRunner {
void run(String... args) throws Exception;
}
CommandLineRunner
인터페이스의 run
메서드는 문자열 가변인수(String... strings
)를 매개변수로 가지고 있습니다. 그래서 일반적으로 run
메서드의 매개변수는 문자열의 배열로 받아들이는 것이 일반적입니다.
run
메서드는 단일 스레드로 실행되며 명령행에서 전달되는 인수를 받는 데 사용됩니다. 만약 멀티스레드로 여러 요청이 들어와서 미리 예측할 수 없는 요청을 처리해야 한다면 CommandLineRunner
를 사용하는 것은 적합하지 않을 수 있습니다.
일반적으로 CommandLineRunner
는 스프링 부트 애플리케이션이 시작될 때 한 번 실행되며, 명령행 인수를 처리하는 데 사용됩니다. 만약 여러 요청을 비동기적으로 처리하고자 하거나, 요청이 들어올 때마다 실행되어야 한다면 CommandLineRunner
를 사용하기보다는 다른 방법을 고려해야 합니다.
이번 실습에서는 CommandLineRunner
를 RelationalDataAccessApplication
으로 implement
하고 run
메서드를 @Override
했습니다.
@SpringBootApplication
public class RelationalDataAccessApplication implements CommandLineRunner {
//...중략
@Autowired
JdbcTemplate jdbcTemplate;
@Override
public void run(String... strings) throws Exception {
// 생략
}
}
RESTful 웹 서비스를 사용하기에서는 CommandLineRunner
가 어떻게 사용되었는지 다시 살펴봅시다.
@SpringBootApplication
public class ConsumingRestApplication {
//... 중략
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
@Bean
@Profile("!test")
public CommandLineRunner run(RestTemplate restTemplate) throws Exception {
return args -> {
Quote quote = restTemplate.getForObject(
"http://localhost:8080/api/random",
Quote.class
);
log.info(quote.toString());
};
}
}
@Bean
어노테이션을 사용하는 경우에는 해당 빈의 생성과 초기화를 Spring 컨테이너에게 위임하게 됩니다. 그리고 @Bean
어노테이션으로 설정된 메서드가 리턴하는 객체는 해당 빈의 인스턴스가 됩니다. 이 방법을 사용하면, 런타임 시에 필요한 의존성을 주입하여 인스턴스를 얻을 수 있게 됩니다.
CommandLineRunner
인터페이스를 직접 구현하는 대신, @Bean
어노테이션과 메서드를 사용하여 해당 인터페이스를 구현한 객체를 반환하였고 이는 스프링의 IoC(Inversion of Control)을 활용하는 것입니다. 인터페이스의 시그니처와 맞지 않지만, 스프링 IoC 컨테이너가 관리하는 빈을 매개변수로 받아 사용하는 것이 가능합니다.
Creating tables
부터 차례로 로그를 통해 데이터베이스에 insert되고 query 되는 것을 확인할 수 있습니다.
Connection pool(연결 풀)은 데이터베이스 연결을 관리하기 위한 기술입니다. 일반적으로 데이터베이스와의 연결은 비용이 많이 드는 작업 중 하나입니다. 따라서 매번 데이터베이스에 새로운 연결을 맺고 끊는 것은 시간과 자원을 많이 소비하게 됩니다.
연결 풀은 이러한 연결 비용을 절감하기 위해 미리 일정 수의 데이터베이스 연결을 생성해 두고, 애플리케이션이 데이터베이스 연결을 요청할 때마다 이 풀에서 사용 가능한 연결을 대여하고, 사용이 끝나면 다시 풀에 반환하는 방식으로 동작합니다.
일반적으로 연결 풀은 다음과 같은 장점을 제공합니다:
Spring Boot는 이러한 연결 풀을 다루기 위한 다양한 설정을 제공하며, 이를 통해 데이터베이스 연결 관리와 관련된 많은 문제를 쉽게 해결할 수 있도록 지원합니다.
일반적으로 연결 풀을 관리하는 것은 데이터베이스에 연결하는 연산 자체를 말합니다. 예를 들어, DataSource
를 설정하고 이를 통해 데이터베이스와의 연결을 확립하는 것이 연결 풀을 관리하는 것입니다.
JdbcTemplate
은 실제로 이러한 연결 풀을 내부적으로 관리하고 있어서, 개발자가 직접적으로 연결을 생성하거나 닫을 필요는 없습니다. 그러나 JdbcTemplate
을 사용하는 경우에도 스프링 부트는 설정에 따라 내부적으로 데이터베이스와의 연결을 효율적으로 관리합니다.
따라서 CommandLineRunner
를 사용하여 JdbcTemplate
을 실행하는 것은 데이터베이스 연산을 수행하는 것이지만, 직접적인 연결 풀의 생성 또는 관리는 JdbcTemplate
이나 스프링 부트에서 이루어지는 것입니다.