[Spring Boot] 11. Spring Data

  • Spring-Boot-Starter-JDBC
    • spring-jdbc
    • 스프링부트 자동설정
      • DataSourceAutoConfiguration, JdbcTemplateAutoConfiguration 등이 적용
      • 자동 설정된 빈: DataSource, JdbcTemplate

많이 쓰이는 인메모리 데이터베이스

  • HSQL
  • Derby
  • H2 : 추천, 콘솔떄문?

H2 의존성 추가 : 버전은 스프링 부트가 관리

H2의존성이 클래스 패스에 들어가 있고 아무런 DataSource를 하지 않았다면
스프링부트가 자동으로 인메모리DB가 설정되고 이를 바탕으로 DataSource가 빈 등록되고
이 DataSource를 가지는 JdbcTemplate 빈도 등록이 되서 바로 작업 가능

해당 부분은 org.springframework.boot.autoconfigure.jdbc.DataSourceProperties
를 보면 알 수 있다

  • Hikari CP
    DataSourceProperties에서 DB,user,password 만드는 부분
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    @ConfigurationProperties(prefix = "spring.datasource")
    public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {

    //중간 생략

    //DB 이름 : 따로 정의하지 않으면 testdb
    public String determineDatabaseName() {
    if (this.generateUniqueName) {
    if (this.uniqueName == null) {
    this.uniqueName = UUID.randomUUID().toString();
    }
    return this.uniqueName;
    }
    if (StringUtils.hasLength(this.name)) {return this.name;}
    if (this.embeddedDatabaseConnection != EmbeddedDatabaseConnection.NONE) {
    return "testdb";
    }
    return null;
    }

    //사용자 이름: 따로 정의하지 않으면 sa
    public String determineUsername() {
    if (StringUtils.hasText(this.username)) {
    return this.username;
    }
    if (EmbeddedDatabaseConnection.isEmbedded(determineDriverClassName())) {
    return "sa";
    }
    return null;
    }

    //password: 따로 정의하지않으면 blank 암호
    public String determinePassword() {
    if (StringUtils.hasText(this.password)) {
    return this.password;
    }
    if (EmbeddedDatabaseConnection.isEmbedded(determineDriverClassName())) {
    return "";
    }
    return null;
    }
    }

스프링부트가 자동으로 만들어준 DataSource를 통해서 간단한 테스트를 해본다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@Component
@Slf4j
public class H2Runner implements ApplicationRunner {

@Autowired

DataSource dataSource;

@Autowired
JdbcTemplate jdbcTemplate;

@Override
public void run(ApplicationArguments args) throws Exception {

log.info("URL: " + conn.getMetaData().getURL());
log.info("User: " + conn.getMetaData().getUserName());

//table 생성
try (Statement stmt = conn.createStatement()) {
stmt.executeUpdate("CREATE TABLE USER (ID INTEGER NOT NULL, name VARCHAR(255), PRIMARY KEY (id))");
}


//Data 입력
try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO USER VALUES (?, ?)")) {

stmt.setInt(1, 1);
stmt.setString(2, "Geunchang");
stmt.executeUpdate();

stmt.setInt(1, 2);
stmt.setString(2, "GeunChang2");
stmt.executeUpdate();
}

//전체 조회
try (PreparedStatement stmt = conn.prepareStatement("SELECT * FROM USER ")) {
ResultSet rs = stmt.executeQuery();
ResultSetMetaData rsmd = rs.getMetaData();
log.info("count: "+rsmd.getColumnCount());
while(rs.next()){
log.info("ID: " + rs.getInt(1));
log.info("name: " + rs.getString(2));
}
}


//JdbcTemplate test
log.info("**JdbcTemplate test**");
SqlRowSet sqlRowSet = jdbcTemplate.queryForRowSet("SELECT * FROM USER");
while(sqlRowSet.next()){
log.info("ID: " + sqlRowSet.getInt(1));
log.info("name: " + sqlRowSet.getString(2));
}
}
}
}


/*
출력값

SA
*/
  • 설명
    • @Autowired로 스프링부트가 자동으로 생성한 DataSource를 받아왔다

    • @Autowired로 JdbcTemplate도 받아왔다

    • DataSource로부터 Connection을 만들었으며 커넥션의 getMetaData()를 통해서
      필요한 정보를 getURL(), getUserName()로 가져왔다

      • getURL(): jdbc:h2:mem:testdb
      • getUserName(): SA
    • 테이블 생성후 값 2개 넣고 전체 조회해서 출력하는 간단한 예제

    • ResultSetMetaData를 이용해서 ResultSet의 컬럼수를 쉽게 알 수 있음을 주목

    • jdbcTemplate를 사용해서 바로 위의 Statement 사용과 간결해짐 비교

    • Spring-JDBC가 제공하는 JdbcTemplate를 사용하면 이렇게 간결하게 사용가능
      try-catch를 따로 하지 않아도 리소스 처리가 잘되어있으며 예외처리의 경우에도
      더 가독성이 좋은 메세지 확인 가능

    • 인텔리J Ultimate의 DB의 Datasource 추가로 간단하게 콘솔 접근 가능

    • H2 Console 직접 켜기 위해선 2가지 방법

      • spring-boot-devtools 추가
      • spring.h2.console.enabled=true(applications.properties)
    • 올린후 localhost:8080/h2-console에 접속하면 끝

2. MySql

지원 DBCP

  • HikariCP(default)
  • TomcatCP
  • Commons DBCP2

역시 applications.properties에 해당 DBCP의 설정이 가능하다

  • 예)spring.datasource.hikari.maximum-pool-size=5
  • HikariDataSource는 HikariConfig를 상속하고 있으며 여기에 많은 값들이 정의

요새는 직접 Mysql을 추가하기 보다는 도커로 mysql을 올린다
mysql-connector-java만 의존성에 추가한다(mysql에 대한 datasource 구현체 포함)

pom.xml에 mysql-connector-java를 추가
1
2
3
4
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

이러면 mysql-connector-java-.jar가 의존성에 들어 온다

try (Connection conn = dataSource.getConnection()) {

application.properties에 필요한 정보를 세팅
1
2
3
4
5
6
7
##mysql
ahn.server=192.168.0.79
#ahn.server=localhost
#spring.datasource.hikari.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://${ahn.server}:3306/springdata?serverTimezone=UTC&useSSL=false
spring.datasource.username=ahn
spring.datasource.password=pass
  • 에러처리
    • 타임존
      • 나의 경우에는 우분투를 사용하는 원격 컴퓨터에 도커로 mysql을 쓰고 접속했는데
        타임존이 KST라는 에러가 나와서 타임존을표준인 UTC로 하도록 url쿼리에 설정
    • SSL
    • mysql의 경우 특정버전이상인경우 SSL연결을 강제하는 것으로 보인다
    • 이상적으로는 useSSL=true이고 truststore를 주어서 SSL로 접속할 수 있게끔
    • 테스트시에는 무시

mysql 커뮤니티에디션도 영리목적으로 사용 불가하므로 MariaDB를 쓰자

3. PostgreSQL

pom.xml
1
2
3
4
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
postgresql 로 url수정
1
2
##postgresql
spring.datasource.url=jdbc:postgresql://${ahn.server}:5432/springdata
  • 테스트 시작
    • 테스트에서 썼던 테이블명을 users나 accounts등으로 바꾸자
    • PostgreSQL에서 user는 키워드임
    • spring.datasource-driver-class-name=에 들어가는 값을 스프링 부트가
      datasource의 url을 보고 추측하여 자동으로 넣어준다
      잘 작동하는 것을 볼 수 있다

4. 스프링데이터 JPA

ORM(Object-Relational Mapping)과 JPA (Java Persistence API)

스프링 데이터 JPA

  • Repository 빈 자동 생성
  • 쿼리 메소드 자동 구현
  • @EnableJpaRepositories
    • 스프링에선 애노테이션 사용한뒤 사용해서 설정해야지 본격적으로 사용 가능
    • 스프링 부트 사용시엔 자동으로 설정하기 때문에 안해도 됨
  • Spring Data JPA -> JPA -> Hibernate -> Datasource
  • 의존성
    pom.xml
    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
  • 프로퍼티에는 h2가 test 스코프로, postgresql설정은 application.properties에 설정

먼저 엔티티 작성, Repository작성, 그리고 테스트 작성,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
//entity
import lombok.Data;

@Entity @Data
public class Account {

@Id @GeneratedValue
private Long id;
private String username;
private String password;
}

//Test
@RunWith(SpringRunner.class)
@DataJpaTest
@Slf4j
public class AccountRepositoryTest {

@Autowired
DataSource dataSource;

@Autowired
JdbcTemplate jdbcTemplate;

@Autowired
AccountRepository accountRepository;

//Autowired될 빈들이 잘 등록이 되는지, 이테스트는 무사히 실행되는지
@Test
public void blankTest() throws SQLException {
try(Connection conn = dataSource.getConnection()) {
DatabaseMetaData metaData = conn.getMetaData();
log.info(metaData.getURL());
log.info(metaData.getDriverName());
log.info(metaData.getUserName());
log.info(metaData.getDatabaseProductName());
log.info(metaData.getDatabaseProductVersion());

}
}

@Test
public void insertTest() throws SQLException {
try(Connection conn = dataSource.getConnection()) {
Account account = new Account();
account.setUsername("GeunChang");
account.setPassword("pass");

Account newAccount = accountRepository.save(account);

assertThat(newAccount). isNotNull();

}
}

@Test
public void searchTest() throws SQLException {
try(Connection conn = dataSource.getConnection()) {
Account account = new Account();
account.setUsername("GeunChang");
account.setPassword("pass");

Account newAccount = accountRepository.save(account);

assertThat(newAccount). isNotNull();

Account existingAccount = accountRepository.findByUsername(newAccount.getUsername());
assertThat(existingAccount).isNotNull();

Account nonExistingAccount = accountRepository.findByUsername("no_name_like_this");
assertThat(nonExistingAccount).isNull();
}
}
}

//Repo
import org.springframework.data.jpa.repository.JpaRepository;

public interface AccountRepository extends JpaRepository<Account, Long> {
Account findByUsername(String username); //이렇게만 해도 만들어준다
}
  • 설명
    • Entity는 Lombok을 써서 간결하게 하였다 , 아이디부분 잘 볼것
    • @SpringBootTest를 사용하면 integration Test가 되서 application.properties가
      적용되서 postgresql이 되고 @DataJpaTest를 사용하면 무조건 인메모리 DB사용
    • JpaRepository를 상속받은 “가장 세련된 방법”을 사용하였다
      기억안나면 예전 JPA공부하던것 다시 볼 것
    • DataSource, JdbcTemplate 말고 AccountRepository까지 @Autowired 할 수 있음
    • findById같은 것은 자동설정되나 findByUsername는 없는데 그냥 quick fix를 통해
      바로 만들면 된다-아규먼트 타입과 리턴 타입만 한번 쓱 보고 끝

sql 사용할때도 사실 JPA를 통해서 사용할 수 있다

  • JpaRepository의 메소드(여기서는 findByUsername)에 @Query 붙여서 가능
  • 기본적으론 JPQL문법을 써야하는데 nativeQuery=true하면 native쿼리가 가능
public interface AccountRepository extends JpaRepository<Account, Long> {
    @Query(nativeQuery = true, value = "select * from account where username= :username")
    Optional<Account> findByUsername(String username);
}

위에서 Optional형태의 리턴으로 바꿨다
이경우에는 테스트를 empty 유무로 바꿔야한다

  • assertThat(existingAccount).isNotEmpty();

  • assertThat(nonExistingAccount).isEmpty();

  • tip : 전에 JPA공부할때 알았는데 또 까먹

    • sql 보이게 하는 방법: spring.jpa.show-sql=true
    • sql 이쁘게 보이는 방법: spring.jpa.properties.hibernate.format_sql=true
    • ?값 보이게 하는 법: logging.level.org.hibernate.type.descriptor=trace

5. DB 초기화

초기화 방법

  • JPA를 사용한 DB 초기화 : DDL 자동생성 및 실행기능 -> 2개의 외부 속성

    • spring.jpa.hibernate.ddl-auto(enum):하이버네이트 특성
      • none: 자동 DDL 생성안함
      • create: 하이버네이트 SessionFactory가 시작할떄마다 항상 다시 생성,
        이미 존재하면 드롭후 다시 생성
      • create-drop: SessionFactory 시작시 생성하고 종료할때 드롭
      • update : SessionFactory 시작시 엔티티 클래스와 DB에 생성된 스키마를 비교해서
        DB에 반영이 안된 테이블이나 컬럼이 있으면 생성, 기생성된 부분은 건들지 않음
      • validate: update처럼 비교하되 변경은 하지 않고 다르다면 예외 발생
      • hsqldb, h2, derby등의 in-memory DB는 create-drop이 기본값이며
        나머지는 none이 기본값
    • spring.jpa.generate-dll(boolean)
      • DB벤더에 종속적이지 않는다
    • 운영시엔 generate-dll=false에 ddl-auto=validate로 하면 된다
  • SQL 스크립트를 사용한 DB 초기화

    • schema.sql 또는 schema-${platform}.sql -> table 생성쪽
    • data.sql 또는 data-${platform}.sql -> data 입력쪽
    • ${platform}값은 spring.datasource.platform으로 설정가능
      • 예를 들어서 에디션별, 버전별
  • 개발시에는 update로 쓰다가 배포시점이 가까워지면 테스트실행후
    나오는 sql문들로 schema.sql을 만들면 깔끔한 sql을 생성할 수 있다

DB와 DB데이타를 체계적으로 관리하고 싶다면 DB 마이그레이션 툴을 사용하는게 좋다
스프링 부트는 DB 마이 그레이션 툴도 지원한다

6. DB Migration tool

Flyway와 Liquibase가 대표적

Related POST

공유하기