Spring에서 초기 테이블과 데이터 관리를 위해서 data.sql과 schema.sql을 사용하였다. 하지만 테이블 스키마가 변경되거나 필수로 초기에 들어가야하는 데이터들이 추가되거나 수정되었을 때 히스토리 관리가 잘 되지 않았다. 

특히 서로 교류가 잘 되지 않은 경우에서는 컬럼이 추가되거나 무엇이 변경되었는지 알지 못해서 문제를 유발할 수 있기에 이를 관리 할 수 있는 무언가가 필요했다.

그래서 Redgate에서 제공하는 Flyway를 사용해보기로 했다. 우선 내 개인 프로젝트인 timeline에 적용시켜봤다.

 

데이터베이스 버전관리 Flyway

https://flywaydb.org/

동작 방식

Flyway가 버전관리를 하기위해서 테이블이 생성된다. Flyway가 버전관리는 이 테이블에 데이터베이스의 상태를 기록하면서 진행한다. 

Flyway가 시작되면 파일시스템 또는 마이그레이션 대상의 classpath를 스캔해서 Sql 또는 Java로 쓰여진 파일을 찾는다. 이 마이그레이션 작업은 파일에 적혀있는 version number대로 순서대로 진행된다. 그리고 현재 마이그레이션 해야할 파일의 버전과 테이블에 기록된 버전을 확인해보고 같으면 넘어간다.

Flyway에서 사용하는 테이블은 flyway_schema_history로 아래와 같이 구성되어있다.

CREATE TABLE `flyway_schema_history` (
  `installed_rank` int(11) NOT NULL,
  `version` varchar(50) DEFAULT NULL,
  `description` varchar(200) NOT NULL,
  `type` varchar(20) NOT NULL,
  `script` varchar(1000) NOT NULL,
  `checksum` int(11) DEFAULT NULL,
  `installed_by` varchar(100) NOT NULL,
  `installed_on` timestamp NOT NULL DEFAULT current_timestamp(),
  `execution_time` int(11) NOT NULL,
  `success` tinyint(1) NOT NULL,
  PRIMARY KEY (`installed_rank`),
  KEY `flyway_schema_history_s_idx` (`success`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
installed_rank 인덱스
version 버전명 (V나 R 뒤에 붙는 숫자)
description 설명
type SQL 또는 JDBC 
script 스크립트 이름 V1__kdjlkdf.sql
checksum checksum
installed_by 실행 주최자
installed_on 설치된 시간
execution_time 총 실행시간
success 성공여부

 

간단히 말해 변경된 데이터나 테이블 스키마를 적용하기 위해서는 마지막 버전보다 높은 파일을 만들어서 애플리케이션을 구동하면 된다.

 

애플리케이션에 적용

그럼 flyway를 적용하기 위해 gradle에 라이브러리부터 추가해보자.

dependency {
	compile group: "org.flywaydb", name: "flyway-core", version: '5.2.4'
}

그리고 application.yml을 설정하자.

spring:
  flyway:
    enabled: true
    baselineOnMigrate: true
    encoding: UTF-8

그리고 테이블과 데이터를 넣을 sql을 만들자. 

그리고 Springboot 애플리케이션을 실행시키면 해당 테이블에 버전 히스토리가 기록된다.

 

버전관리하기에 좋은거 같다.

엔티티 매핑에서 사용될 컬럼의 필드 유형을 설정하는 매핑 어노테이션을 정리해보자.

@Column
테이블에서 사용 되는 컬럼이라는 필드를 지정해줄때 사용하며 name, nullable(기본이 true) 등의 설정을 해줄 수 있다. 

1
2
@Column(name = "NAME", length = 10, nullable = true)
private String userName;
cs


@Enumerated
자바의 enum 타입을 매핑할 때 사용한다. 속성으로 EnumType.ORDINAL과 EnumType.STRING이 존재하는데 이름 그대로 ORDINAL은 순서를 STRING은 Enum의 이름을 저장한다

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
public class Member {
 
  @Id
  @GeneratedValue
  @Column(name = "ID")
  private String id;
 
  @Column(name = "NAME"length = 10, nullable = true)
  private String userName;
 
  // 매핑 정보가 없는 필드
  private int age;
 
  @Enumerated(EnumType.STRING)
  Gender gender;
 
}
 
enum Gender {
  Men, Women;
 
  private Gender() {
 
  }
}
 
// 위와 같이 설정하면 데이터베이스에 Men으로 들어간다.
member.setGender(Gender.Men);
cs


@Temporal
java.util.Date와 java.util.Calendar 값을 매핑 할 때 사용한다.

1
2
3
4
5
6
7
8
9
10
11
// 2018-04-02 형태 (데이터베이스 DATE와 매핑)
@Temporal(TemporalType.DATE)
private Date birthDate;
 
// 12:11:11 (데이터베이스 TIME과 매핑)
@Temporal(TemporalType.TIME)
private Date birthTime;
 
// 2013-10-21 12:11:11 (데이터베이스 TIME과 매핑)
@Temporal(TemporalType.TIMESTAMP)
private Date birthTimeStamp;
cs


@LOB
데이터베이스 BLOB, CLOB 타입과 매핑 된다. CLOB(String, char[], java.sql.CLOB)은 문자, BLOB(byte[], java.sql.BLOB)은 나머지가 매핑된다.

@Transient
저장 조회에 사용되지도 않고 그냥 단순 값을 가지고 있고 싶을때 사용.

1
2
@Transient
private String tempStr;
cs


@Access
데이터베이스에 엔티티에 값이 저장될 때 필드(AccessType.FIELD)의 값을 직접 접근해서 사용할 것인가 아니면 메서드에 직접(AccessType.PROPERTY) 접근할 것 인가를 설정하는 것이다.

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
@Access(AccessType.FIELD)
public class Member {
 
  @Id
  @GeneratedValue
  @Column(name = "ID")
  private String id;
 
  @Column(name = "NAME"length = 10, nullable = true)
  private String userName;
 
  // 매핑 정보가 없는 필드
  private int age;
 
  @Enumerated(EnumType.STRING)
  Gender gender;
 
  // 2018-04-02 형태 (데이터베이스 DATE와 매핑)
  @Temporal(TemporalType.DATE)
  private Date birthDate;
 
  // 12:11:11 (데이터베이스 TIME과 매핑)
  @Temporal(TemporalType.TIME)
  private Date birthTime;
 
  // 2013-10-21 12:11:11 (데이터베이스 TIME과 매핑)
  @Temporal(TemporalType.TIMESTAMP)
  private Date birthTimeStamp;
 
  @Transient
  private String tempStr;
 
}
cs


- @Access 필드를 생략하고 @Id 필드를 사용하면 AccessType.FIELD로 설정된 것과 같다.
- 나머지 필드는 @Id를 사용하여 AccessType.FIELD로 사용하고 특정 값만 AccessType.PROPERTY로 설정할 수 있다.

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
@Access(AccessType.FIELD)
public class Member {
 
  @Id
  @GeneratedValue
  @Column(name = "ID")
  private String id;
 
  @Column(name = "NAME"length = 10, nullable = true)
  private String userName;
 
  // 매핑 정보가 없는 필드
  private int age;
 
  @Enumerated(EnumType.STRING)
  Gender gender;
 
  // 2018-04-02 형태 (데이터베이스 DATE와 매핑)
  @Temporal(TemporalType.DATE)
  private Date birthDate;
 
  // 12:11:11 (데이터베이스 TIME과 매핑)
  @Temporal(TemporalType.TIME)
  private Date birthTime;
 
  // 2013-10-21 12:11:11 (데이터베이스 TIME과 매핑)
  @Temporal(TemporalType.TIMESTAMP)
  private Date birthTimeStamp;
 
  @Transient
  private String tempStr;
 
  @Access(AccessType.PROPERTY)
  public String getFullName() {
    return "dbsafer" + this.userName;
  }
 
}
cs


테이블에 저장된 데이터를 읽는 방식은 두 가지이다.

Table Full Scan은 해당 테이블에 전체 블록을 읽어서 사용자가 원하는 데이터를 찾는 방식이다. 그리고 Index Range Scan은 인덱스를 이용하여 데이터를 일정부분읽어서 ROWID로 테이블 레코드를 찾아가는 방식이다. ROWID는 테이블 레코드가 디스크 상에 어디 저장됐는지를 가리키는 위치 정보이다.

상당수의 툴(QueryBox, Toad, Orange)에서 데이터를 Full Scan 하는 경우에 실행계획에서 빨간색으로 경고를 보여준다.

그래서 Table Full Scan에 경우 더 느리다는 고정관념이 있으나 모두 그런것은 아니다. Index를 이용한 스캔방식이 더 느린 경우도 있다.

Table Full Scan에 경우 읽고자 하는 데이터의 블록을 Multi Block I/O로 읽기 때문에 프로세스가 데이터를 바로 처리할 수 있으나, Index의 경우 Single Block I/O로 데이터를 읽는다. 그렇기 때문에 데이터를 모두 읽는 I/O Call이 끝날 때까지 정작 프로세스는 대기 상태에 들어가기 때문에 비효율적인 상태가 된다.

그리고 인접한 데이터 500개를 찾을 경우 Single Block I/O를 사용하는 Index의 경우에는 같은 블록을 500번 방문하는 안좋은 경우가 발생할 수 있다.

경우에 따라 Full Scan이 더 좋은 방법일 수 있고, Index Scan이 좋은 방법일 수 있다. 앞으로 더 공부하면서 어떤경우가 더 좋고 나쁜지 알아봐야겠다.

+ Recent posts