Guide_Accessing data with R2DBC

Dev.Hammy·2024년 1월 31일
0

Spring Guides

목록 보기
43/46

이 가이드는 반응형 데이터베이스 드라이버를 사용하여 관계형 데이터베이스에 데이터를 저장하고 검색하기 위해 Spring Data R2DBC를 사용하는 애플리케이션을 구축하는 과정을 안내합니다.


R2DBC는 "Reactive Relational Database Connectivity"의 약자로, 반응형 프로그래밍 스타일을 지원하는 비동기적인 자바 프레임워크입니다. 이 프레임워크는 관계형 데이터베이스와의 상호 작용을 위한 비동기 API를 제공하여, 기존의 JDBC(Java Database Connectivity)의 블로킹 방식 대신에 비동기적인 방식으로 데이터베이스와 통신할 수 있게 합니다. 이는 대규모 또는 고성능 애플리케이션에서 더 효율적인 I/O 처리를 가능하게 합니다. 이러한 비동기 접근 방식은 대규모 동시 요청을 처리할 때 성능 및 확장성을 향상시킬 수 있습니다.


무엇을 구축할 것인가

메모리 기반 데이터베이스에 Customer POJO(Plain Old Java Objects)를 저장하는 애플리케이션을 구축합니다.

build.gradle

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.2.2'
	id 'io.spring.dependency-management' version '1.1.4'
}

group = 'guide'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '17'
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
	runtimeOnly 'com.h2database:h2'
	runtimeOnly 'io.r2dbc:r2dbc-h2'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'io.projectreactor:reactor-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

Define a Schema

이 예에서는 각각 R2DBC 엔터티로 주석(annotated)이 달린 Customer 개체를 저장합니다. 다음 목록은 SQL 스키마 클래스(src/main/resources/schema.sql에 있음)를 보여줍니다.

CREATE TABLE IF NOT EXISTS customer (id SERIAL PRIMARY KEY, first_name VARCHAR(255), last_name VARCHAR(255));

여기에는 id, first_namelast_name이라는 세 개의 열이 있는 customer 테이블이 있습니다. id 열은 자동으로 증가(auto-incremented)하며, 다른 열은 기본 스네이크 케이스 명명 체계를 따릅니다. 나중에 데이터베이스 스키마를 초기화하기 위해 애플리케이션 시작 중에 schema.sql 파일을 선택(pick up)하려면 ConnectionFactoryInitializer를 등록해야 합니다. H2 드라이버가 클래스 경로에 있고 연결 URL을 지정하지 않았기 때문에 Spring Boot는 내장된 H2 데이터베이스를 시작합니다.


H2 데이터베이스가 클래스 경로에 있는 경우와 ConnectionFactoryInitializer를 등록하지 않으면 schema.sql을 선택하지 못하는 이유는 스프링 부트의 자동 설정 메커니즘과 관련이 있습니다.

스프링 부트는 클래스 경로에 특정 데이터베이스 드라이버가 존재하면 해당 데이터베이스를 사용할 수 있는 것으로 간주합니다. 따라서 H2 데이터베이스 드라이버가 클래스 경로에 있으면 스프링 부트는 내장 H2 데이터베이스를 사용하도록 설정됩니다.

그러나 ConnectionFactoryInitializer를 등록하지 않으면 스프링 부트는 자동으로 데이터베이스 스키마 초기화를 수행하지 않습니다. ConnectionFactoryInitializer는 스프링 데이터 R2DBC에서 제공하는 기능 중 하나로, 애플리케이션 시작 시 데이터베이스 스키마를 초기화하기 위해 사용됩니다. 이를 통해 schema.sql 파일을 실행하여 데이터베이스의 초기 스키마를 설정할 수 있습니다.

따라서 ConnectionFactoryInitializer를 등록하면 스프링 부트는 내장 H2 데이터베이스를 사용하고, schema.sql 파일을 찾아서 초기화할 수 있습니다. 반면에 ConnectionFactoryInitializer를 등록하지 않으면 스프링 부트는 데이터베이스 스키마 초기화를 수행하지 않으므로 schema.sql 파일이 사용되지 않습니다.


Define a Simple Entity

이 예에서는 각각 R2DBC 엔터티로 주석이 달린 Customer 개체를 저장합니다. 다음 목록은 Customer 클래스(src/main/java/com/example/accessingdatar2dbc/Customer.java에 있음)를 보여줍니다.

package com.example.accessingdatar2dbc;

import org.springframework.data.annotation.Id;

public class Customer {

    @Id
    private Long id;

    private final String firstName;

    private final String lastName;

    public Customer(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public Long getId() {
        return this.id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getFirstName() {
        return this.firstName;
    }

    public String getLastName() {
        return this.lastName;
    }

    @Override
    public String toString() {
        return String.format(
            "Customer[id=%d, firstName='%s', lastName='%s']",
            id, firstName, lastName);
    }
}

여기에는 id, firstNamelastName의 세 가지 속성이 있는 Customer 클래스가 있습니다. Customer 클래스에는 최소한의 주석(annotated)이 추가됩니다. id 속성에는 @Id라는 주석이 달려 있어 Spring Data R2DBC가 기본 키를 식별할 수 있습니다. 기본적으로 기본 키는 INSERT 시 데이터베이스에 의해 생성되는 것으로 가정됩니다.

다른 두 속성인 firstNamelastName은 주석이 추가되지 않은 상태로 유지됩니다. 속성 자체와 동일한 이름을 공유하는 열에 매핑된다고 가정합니다.

편리한 toString() 메소드는 고객의 속성을 인쇄합니다.

Create Simple Queries

Spring Data R2DBC는 R2DBC를 관계형 데이터베이스에 데이터를 저장하는 기본 기술로 사용하는 데 중점을 둡니다. 가장 강력한 기능은 저장소 인터페이스에서 런타임에 저장소 구현을 생성하는 기능입니다.

이것이 어떻게 작동하는지 보려면 다음 목록(src/main/java/com/example/accessingdatar2dbc/CustomerRepository.java)에 표시된 대로 고객 엔터티와 작동하는 저장소 인터페이스를 만듭니다.

import org.springframework.data.r2dbc.repository.Query;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;

public interface CustomerRepository extends ReactiveCrudRepository<Customer, Long> {

    @Query ("SELECT * FROM customer WHERE last_name = :lastname")
    Flux<Customer> findByLastName(String lastName);
}

CustomerRepositoryReactiveCrudRepository 인터페이스를 확장합니다. 작업하는 엔터티 유형과 ID인 CustomerLongReactiveCrudRepository의 generic 매개변수에 지정됩니다. ReactiveCrudRepository를 확장함으로써 CustomerRepository는 reactive 유형을 사용하여 Customer 엔터티를 저장, 삭제 및 찾는 방법을 포함하여 Customer persistence 작업을 위한 여러 방법을 상속합니다.

Spring Data R2DBC를 사용하면 @Query로 주석을 달아 다른 쿼리 메서드를 정의할 수도 있습니다. 예를 들어 CustomerRepository에는 findByLastName() 메서드가 포함되어 있습니다.

일반적인 Java 애플리케이션에서는 CustomerRepository를 구현하는 클래스를 작성할 것으로 예상할 수 있습니다. 그러나 이것이 Spring Data R2DBC를 매우 강력하게 만드는 이유입니다. 저장소 인터페이스의 구현을 작성할 필요가 없습니다. Spring Data R2DBC는 애플리케이션을 실행할 때 구현을 생성합니다.

이제 이 예제를 연결하고 어떻게 보이는지 확인할 수 있습니다!

Create an Application Class

Spring initializr는 애플리케이션을 위한 간단한 클래스를 생성합니다. 다음 목록은 이 예제를 위해 Initializr가 생성한 클래스(src/main/java/com/example/accessingdatar2dbc/AccessingDataR2dbcApplication.java)를 보여줍니다.

package com.example.accessingdatar2dbc;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AccessingDataR2dbcApplication {

	public static void main(String[] args) {
		SpringApplication.run(AccessingDataR2dbcApplication.class, args);
	}

}

이제 Initializr가 생성한 간단한 클래스를 수정해야 합니다. (이 예에서는 콘솔로) 출력을 얻으려면 로거를 설정해야 합니다. 그런 다음 초기화 프로그램을 설정하여 스키마와 일부 데이터를 설정하고 이를 사용하여 출력을 생성해야 합니다. 다음 목록은 완성된 AccessingDataR2dbcApplication 클래스(src/main/java/com/example/accessingdatar2dbc/AccessingDataR2dbcApplication.java에 있음)를 보여줍니다.

package com.example.accessingdatar2dbc;

import io.r2dbc.spi.ConnectionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer;
import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator;

import java.time.Duration;
import java.util.Arrays;

@SpringBootApplication
public class AccessingDataR2dbcApplication {

    private static final Logger log = LoggerFactory.getLogger(AccessingDataR2dbcApplication.class);

    public static void main(String[] args) {
        SpringApplication.run(AccessingDataR2dbcApplication.class, args);
    }

    @Bean
    public CommandLineRunner demo(CustomerRepository repository) {

        return (args) -> {
            // save a few customers
            repository.saveAll(Arrays.asList(new Customer("Jack", "Bauer"),
                new Customer("Chloe", "O'Brian"),
                new Customer("Kim", "Bauer"),
                new Customer("David", "Palmer"),
                new Customer("Michelle", "Dessler")))
                .blockLast(Duration.ofSeconds(10));

            // fetch all customers
            log.info("Customers found with findAll():");
            log.info("-------------------------------");
            repository.findAll().doOnNext(customer -> {
                log.info(customer.toString());
            }).blockLast(Duration.ofSeconds(10));

            log.info("");

            // fetch an individual customer by ID
			repository.findById(1L).doOnNext(customer -> {
				log.info("Customer found with findById(1L):");
				log.info("--------------------------------");
				log.info(customer.toString());
				log.info("");
			}).block(Duration.ofSeconds(10));


            // fetch customers by last name
            log.info("Customer found with findByLastName('Bauer'):");
            log.info("--------------------------------------------");
            repository.findByLastName("Bauer").doOnNext(bauer -> {
                log.info(bauer.toString());
            }).blockLast(Duration.ofSeconds(10));;
            log.info("");
        };
    }

}

AccessingDataR2dbcApplication 클래스에는 몇 가지 테스트를 통해 CustomerRepository를 배치하는 main() 메서드가 포함되어 있습니다. 먼저 Spring 애플리케이션 컨텍스트에서 CustomerRepository를 가져옵니다. 그런 다음 소수의 Customer 개체를 저장하고 save() 메서드를 시연하고 사용할 일부 데이터를 설정합니다. 다음으로 findAll()을 호출하여 데이터베이스에서 모든 Customer 개체를 가져옵니다. 그런 다음 findById()를 호출하여 해당 ID로 단일 Customer를 가져옵니다. 마지막으로 findByLastName()을 호출하여 성이 "Bauer"인 모든 고객을 찾습니다.

R2DBC는 반응형(reactive) 프로그래밍 기술입니다. 동시에 우리는 동기화(synchronized)된 명령(imperative) 흐름에서 이를 사용하고 있으므로 각 호출을 block(…) 메서드 변형으로 동기화해야 합니다. 일반적인 반응형 애플리케이션에서 결과 Mono 또는 Flux는 호출 스레드를 차단하지 않고 반응 시퀀스를 구독하는 웹 컨트롤러 또는 이벤트 프로세서로 다시 전달되는 연산자의 파이프라인을 나타냅니다.

기본적으로 Spring Boot는 R2DBC 저장소 지원을 활성화하고 @SpringBootApplication이 있는 패키지(및 해당 하위 패키지)를 찾습니다. 구성에 표시되지 않는 패키지에 R2DBC 저장소 인터페이스 정의가 있는 경우 @EnableR2dbcRepositories 및 해당 유형 안전 basePackageClasses=MyRepository.class 매개변수를 사용하여 대체 패키지를 가리킬 수 있습니다.

Test

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.r2dbc.DataR2dbcTest;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.r2dbc.dialect.H2Dialect;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;

import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(SpringExtension.class)
@DataR2dbcTest
class CustomerRepositoryTests {

	@Autowired
	private DatabaseClient databaseClient;

	@Autowired
	private CustomerRepository customers;

	@Test
	public void testFindByLastName() {
		Customer customer = new Customer("first", "last");
		R2dbcEntityTemplate template = new R2dbcEntityTemplate(databaseClient, H2Dialect.INSTANCE);
		template.insert(Customer.class).using(customer).then().as(StepVerifier::create).verifyComplete();

		Flux<Customer> findByLastName = customers.findByLastName(customer.getLastName());

		findByLastName.as(StepVerifier::create).assertNext(actual -> {
			assertThat(actual.getFirstName()).isEqualTo("first");
			assertThat(actual.getLastName()).isEqualTo("last");
		}).verifyComplete();
	}

}

이 코드는 스프링 부트와 리액티브 스프링 데이터 R2DBC를 사용하여 데이터베이스 작업을 테스트하는 코드입니다. 각 부분을 순서대로 분석해보겠습니다.

  1. @ExtendWith(SpringExtension.class): 이 어노테이션은 JUnit 5에서 스프링 테스트 확장을 지정합니다. 이것은 스프링 애플리케이션 컨텍스트를 테스트에 사용할 수 있도록 확장을 활성화합니다.

  2. @DataR2dbcTest: 이 어노테이션은 리액티브 R2DBC 데이터베이스 테스트를 위한 스프링 부트 테스트 슬라이스를 활성화합니다. 이것은 테스트를 실행하는 동안 리액티브 R2DBC 관련 빈들을 자동으로 구성하고 데이터베이스 연결을 설정합니다.

  3. DatabaseClient는 스프링 데이터 R2DBC에서 제공하는 리액티브한 데이터베이스 클라이언트입니다. 이를 사용하여 리액티브한 방식으로 데이터베이스에 대한 쿼리 및 작업을 실행할 수 있습니다.

  4. @Autowired를 사용할 수 있는 기준은 해당 필드나 메서드가 스프링 애플리케이션 컨텍스트에서 자동으로 주입될 수 있는 빈으로 등록되어 있어야 합니다. 이는 주로 스프링 컨테이너가 해당 빈을 찾을 수 있고, 타입에 맞는 빈을 주입할 수 있는 경우에 해당합니다.

코드에서 DatabaseClient를 주입하기 위해 @Autowired 어노테이션을 사용했는데, 이것은 스프링 부트에서 기본적으로 리액티브 R2DBC를 지원하는 데이터베이스에 대한 클라이언트를 자동으로 설정합니다. 때문에 DatabaseClient 빈은 프로젝트에 명시적으로 정의되어 있지 않더라도 스프링 부트의 자동 구성으로 인해 사용 가능합니다.

스프링 부트는 클래스 패스에 있는 라이브러리와 기본적인 설정을 바탕으로 여러 가지 자동 구성을 제공합니다. 이에 따라 리액티브 R2DBC를 사용하는 경우, 스프링 부트가 기본적인 리액티브 데이터베이스 클라이언트를 자동으로 구성하여 사용할 수 있습니다. 따라서 DatabaseClient와 같은 리액티브 데이터베이스 관련 빈을 명시적으로 정의하지 않아도 됩니다.

  1. H2Dialect.Instance는 H2 데이터베이스를 위한 R2DBC 데이터베이스 다이얼렉트(Dialect)의 인스턴스를 의미합니다. 이것은 H2 데이터베이스에 특화된 SQL 문법이나 데이터베이스 기능을 사용하기 위해 사용됩니다.

  2. StepVerifier는 리액티브한 코드를 테스트하기 위한 리액티브 테스트 도구입니다. 리액티브 스트림의 각 이벤트를 검증하고 예상된 결과를 확인할 수 있습니다.

  3. R2dbcEntityTemplate template = new R2dbcEntityTemplate(databaseClient, H2Dialect.INSTANCE): R2dbcEntityTemplate을 생성하고 H2 데이터베이스에 대한 R2DBC 연결을 설정합니다.

  4. template.insert(Customer.class).using(customer).then().as(StepVerifier::create).verifyComplete(): 새로운 Customer 객체를 데이터베이스에 삽입합니다.

  5. Flux<Customer> findByLastName = customers.findByLastName(customer.getLastName()): 리포지토리를 사용하여 마지막 이름에 해당하는 Customer를 찾습니다.

  6. findByLastName.as(StepVerifier::create).assertNext(actual -> { ... }).verifyComplete(): findByLastName Flux를 구독하고 검증합니다. 예상된 결과와 일치하는지 확인합니다.

이 코드는 리액티브 스프링 데이터 R2DBC를 사용하여 리액티브하게 데이터베이스 작업을 테스트하는 전형적인 방법을 보여줍니다.

Build a executable JAR

Gradle을 사용하는 경우 ./gradlew bootRun을 사용하여 애플리케이션을 실행할 수 있습니다. 또는 다음과 같이 ./gradlew build를 사용하여 JAR 파일을 빌드한 후 JAR 파일을 실행할 수 있습니다.

java -jar build/libs/{project_id}-0.1.0.jar

축하해요! 구체적인 저장소 구현을 작성하지 않고도 Spring Data R2DBC를 사용하여 데이터베이스에 객체를 저장하고 데이터베이스에서 가져오는 간단한 애플리케이션을 작성했습니다.

0개의 댓글