ETL์ด๋ ์ถ์ถ(Extraction), ๋ณํ(Transformation), ์ ์ฌ(Load)์ ์ฝ์ด์ด๋ค.
Database์ ์๋ ๋ฐ์ดํฐ๋ฅผ ์ถ์ถํ์ฌ, ์ํ๋ ํํ๋ก ๋ณํํ๊ณ ๋ค๋ฅธ Database์ ์ ์ฌํ๋ ์์
์ด๋ค.
๋ฐ์ดํฐ๋ถ์์์๋ ๋น ์ง์ง์๋ ์ค์ํ ๊ฐ๋ ์ด๋ค.
๋ณธ ์๋ฆฌ์ฆ์์๋ ETL์ ์ ์ฒด๋ฅผ ๋ค๋ฃจ๋ฉฐ ๋ค์์ ์ฌ์ฉํ๋ค.
MariaDB์ Jenkins๋ docker๋ฅผ ํ์ฉํ์ฌ ์๋น์คํ๋ค.
๋จผ์ ETL์ ์ํ ๊ธฐ๋ณธํ๊ฒฝ๊ตฌ์ฑ์ ํ๋ค.
mariadb container๋ฅผ ์ด 2๊ฐ๋ฅผ ๋์ด๋ค.
db-source์ ๋ฐ์ดํฐ๋ฅผ ๋ณํํ์ฌ db-target์ ์ ์ฌํ ๊ฒ์ด๋ค.
MariaDB๋ sudo su๋ฅผ ํตํด ์ ์ํ๊ฑฐ๋ mysql -u root -p๋ก ๋ฃจํธ ํจ์ค์๋๋ฅผ ์
๋ ฅํ๊ณ ์ ์ํด์ผํ๋ค.
container๋ก DB๋ฅผ ์ฌ๋ฆฌ๋ฉด MYSQL_ROOT_PASSWORD๋ผ๋ ํ๊ฒฝ๋ณ์๋ฅผ ์ค์ ํ๊ณ ์ด๋ฅผ ๋ก๊ทธ์ธํ ๋ ์
๋ ฅํด์ผ ํ๋ค.
์ด๊ธฐ ๊ตฌ๋ ํ ํด๋น ํ๊ฒฝ๋ณ์๋ฅผ ์ง์ด ํ ์ฌ๊ธฐ๋ํ๋ค.
docker-compose.yamlmkdir -p ~/etl-sample
touch ~/etl-sample/docker-compose.yaml
docker-compose.yamlversion: '3.1'
services:
db-source:
container_name: db-source
image: mariadb:latest
restart: always
ports:
- 3306:3306
volumes:
- /data/db-source/data:/var/lib/mysql
- /data/db-source/config:/etc/mysql/conf.d
environment:
TZ: Asia/Seoul
# mysql์ ์ฒ์ ์๋น์คํ ๋์ root password๊ฐ ํ์ํ๋ค.
# ์๋์ ํ๊ฒฝ๋ณ์๋ฅผ ํฌํจํ์ฌ ์ฌ๋ฆฐ ํ ์ง์์ค๋ค.
# MYSQL_ROOT_PASSWORD: ${PASSWORD}
db-target:
container_name: db-target
image: mariadb:latest
restart: always
ports:
- 3307:3306
volumes:
- /data/db-target/data:/var/lib/mysql
- /data/db-target/config:/etc/mysql/conf.d
environment:
TZ: Asia/Seoul
# MYSQL_ROOT_PASSWORD: ${PASSWORD}
cd ~/etl-sample
# ์ด๊ธฐ ๊ตฌ๋
docker-compose up -d
# ์ฌ์์
## docker-compose downd์ docker rm์ ํฌํจํ๊ณ ์๊ธฐ์ volume์ค์ ์ ํ์ธํ์.
docker-compose down
docker-compose up -d
db-source: Create table & Insert sample datadb-source์ ์ํ๋ฐ์ดํฐ๋ฅผ ๋ฃ๋ ์์
์ด๋ค.
docker exec -it db-source bash
mysql -u root -p
# ํจ์ค์๋ ์
๋ ฅ
CREATE DATABASE etl;
USE etl;
CREATE TABLE product (
id bigint not null auto_increment,
name varchar(255),
price bigint,
created datetime,
primary key (id)
) engine = InnoDB;
INSERT INTO product (name, price, created) VALUES ('product1', 1000, '2021-05-04 00:00:00');
INSERT INTO product (name, price, created) VALUES ('product2', 2000, '2021-05-04 00:00:00');
INSERT INTO product (name, price, created) VALUES ('product3', 3000, '2021-05-04 00:00:00');
INSERT INTO product (name, price, created) VALUES ('product4', 4000, '2021-05-04 00:00:00');
db-source: Create Metadata tableSpring batch๊ฐ ์๋ํ๊ธฐ ์ํด ํ์ํ ํ
์ด๋ธ๋ค์ด ์ ํด์ ธ์๋ค.
์ด๋ฅผ ๋ฉํ๋ฐ์ดํฐ ํ
์ด๋ธ์ด๋ผ๊ณ ๋ถ๋ฅธ๋ค.
H2์ ๊ฐ์ ์ธ๋ฉ๋ชจ๋ฆฌ DB๋ฅผ ์ฌ์ฉํ๋ฉด Spring-boot์์ ์๋์ผ๋ก ๋ง๋ค์ด์ฃผ์ง๋ง, MySQL๊ณผ ๊ฐ์ด ๋ณ๊ฐ์ DB๋ฅผ ์ฌ์ฉํ๋ฉด ์๋์ผ๋ก ๋ง๋ค์ด์ผ ํ๋ค.
์ฌ์ฉํ๋ Database๋ณ๋ก ์คํค๋ง๊ฐ ์ ํด์ ธ ์๋ค. ์คํค๋ง ๋งํฌ
๋ณธ ๋ฌธ์์์๋ MySQL์ ์ฌ์ฉํ๋ฏ๋ก schema-mysql.sql์ ์ฌ์ฉํ๋ค.
docker exec -it db-source bash
mysql -u root -p
# Enter password
use etl;
<!-- Paste schema-mysql.sql -->
show tables;
db-source: Set the authorityetl ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ฌ์ฉ์๋ฅผ ๋ง๋ค๊ณ ๊ถํ์ ๋ถ์ฌํ๋ค.
docker exec -it db-source bash
mysql -u root -p
# ํจ์ค์๋ ์
๋ ฅ
# $USER์๊ฒ etl db์ ์ ๊ทผ ๊ถํ์ ๋ถ์ฌํ๋ค.
# ๋ง์ผ, $USER๊ฐ ์๋ค๋ฉด ์๋ก ์์ฑํ๋ค.
# grant all privileges on $DATABASE.* to $USER@'%' identified by '$PASSWORD';
grant all privileges on etl.* to rivernine@'%' identified by 'rivernine';
exit
# ํ์ธ
mysql -u rivernine -p
# rivernine
show databases;
db-target: Create tableCREATE DATABASE etl;
USE etl;
CREATE TABLE trans_product (
id bigint not null auto_increment,
name varchar(255),
price bigint,
created datetime,
discount boolean,
primary key (id)
) engine = InnoDB;
grant all privileges on etl.* to rivernine@'%' identified by 'rivernine';
db-target: Set the authorityetl ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ฌ์ฉ์๋ฅผ ๋ง๋ค๊ณ ๊ถํ์ ๋ถ์ฌํ๋ค.
docker exec -it db-target bash
mysql -u root -p
# ํจ์ค์๋ ์
๋ ฅ
grant all privileges on etl.* to rivernine@'%' identified by 'rivernine';
exit
# ํ์ธ
mysql -u rivernine -p
# rivernine
show databases;
ETL์ ์ฌ์ฉํ๊ธฐ ์ํด Spring-boot ํ๊ฒฝ์ค์ ์ ํ๋ค.
์ฐ์ , build.gradle, application.yaml์ ์์ ํ๊ณ DTO๋ฅผ ์ ์ํ ํ Job์ ์์ฑํ๋ค.
๋ณด๋ค ์์ธํ ๋ด์ฉ์ ์ด์ ๊ธ์์ ๋ค๋ฃจ์๋ค.
build.gradle์ ETL์ ํ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ถ๊ฐํ๋ค.
build.gradledependencies {
implementation (
'org.springframework.boot:spring-boot-starter-batch', // Batch
'org.springframework.boot:spring-boot-starter-jdbc', // Jdbc
'org.projectlombok:lombok', // Lombok
'mysql:mysql-connector-java', // Mysql
)
annotationProcessor 'org.projectlombok:lombok'
// test
// testImplementation 'org.springframework.boot:spring-boot-starter-test'
// testImplementation 'org.springframework.batch:spring-batch-test'
}
Product.java@ToString
@Getter
@Setter
@NoArgsConstructor
@Entity
public class Product {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Long price;
private LocalDateTime created;
public Product(String name, Long price, String created) {
this.price = price;
this.name = name;
this.created = LocalDateTime.parse(created, FORMATTER);
}
public Product(Long id, String name, Long price, String created) {
this.id = id;
this.price = price;
this.name = name;
this.created = LocalDateTime.parse(created, FORMATTER);
}
}
TransProduct.java@ToString
@Getter
@Setter
@NoArgsConstructor
public class TransProduct {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
private Long id;
private String name;
private Long price;
private LocalDateTime created;
private boolean discount;
public TransProduct(String name, Long price, String created, boolean discount) {
this.price = price;
this.name = name;
this.created = LocalDateTime.parse(created, FORMATTER);
this.discount = discount;
}
public TransProduct(String name, Long price, LocalDateTime created, boolean discount) {
this.price = price;
this.name = name;
this.created = created;
this.discount = discount;
}
public TransProduct(Long id, String name, Long price, String created, boolean discount) {
this.id = id;
this.price = price;
this.name = name;
this.created = LocalDateTime.parse(created, FORMATTER);
this.discount = discount;
}
}
db-source์ db-target์ ๋์ ์ ๊ทผํ๊ธฐ ์ํด์๋ datasource์์ ์ด ํ์ํ๋ค.
application.yml๋จผ์ , application.yml์ db-source์ db-target์ datasource ์ ๋ณด๋ฅผ ์
๋ ฅํ๋ค.
application.ymlspring:
profiles:
active: mysql
batch.job.names: ${job.name:NONE}
---
spring:
profiles: mysql
datasource-dbsource:
hikari:
# MYSQL_ADDR:PORT/DATABASE
jdbc-url: jdbc:mysql://192.168.56.103:3306/etl
username: rivernine
password: rivernine
# mysql์ฉ jdbc๋๋ผ์ด๋ฒ
driver-class-name: com.mysql.jdbc.Driver
datasource-dbtarget:
hikari:
jdbc-url: jdbc:mysql://192.168.56.103:3307/etl
username: rivernine
password: rivernine
driver-class-name: com.mysql.jdbc.Driver
DataSourceConfiguration.javaconfig/DataSourceConfiguration.java๋ฅผ ์์ฑํ๊ณ ์์ ํ๋ค.
DataSourceConfiguration.java@Configuration
public class DataSourceConfiguration {
@Bean(name = "dataSource-dbsource")
@Primary
@ConfigurationProperties(prefix="spring.datasource-dbsource.hikari")
public DataSource dataSourceDbSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "dataSource-dbtarget")
@ConfigurationProperties(prefix="spring.datasource-dbtarget.hikari")
public DataSource dataSourceDbTarget() {
return DataSourceBuilder.create().build();
}
}
์ด์ ์ฌ์ฉํ๊ณ ์ ํ๋ ํด๋์ค ๋ด๋ถ์ ๋ค์๊ณผ ๊ฐ์ด ์ ์ธํ๊ณ ์ฌ์ฉํ๋ค.
@Qualifier("dataSource-dbsource")
@Autowired
private DataSource dataSourceDbSource;
@Qualifier("dataSource-target")
@Autowired
private DataSource dataSourceDbTarget;
๋ชจ๋ ์์ค๋ Github์ ์ฌ๋ ค๋์๋ค.