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 애플리케이션을 실행시키면 해당 테이블에 버전 히스토리가 기록된다.

 

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

spring acturator를 통해서 스프링 애플리케이션의 작동여부등을 체크해보자.

 

설정


우선 gradle 라이브러리를 추가한다.

compile 'org.springframework.boot:spring-boot-starter-actuator'

그리고 기존에는 application.properties나 yml에 아래 옵션을 설정해줘야 했지만 기본적으로 설정이 되어있다.

endpoints.health.enabled=true

하지만 이는 Spring boot 2.0에서 다음으로 변경되었다. (https://stackoverflow.com/questions/48900892/how-to-enable-all-endpoints-in-actuator-spring-boot-2-0-0-rc1)

 

How to enable all endpoints in actuator (Spring Boot 2.0.0 RC1)

I moved to Spring Boot 2.0.0 RC1 from 1.5.10 and I am stuck with actuator in the latest version. How can I enable expose and enable all actuator endpoints? The only endpoints that get exposed are:...

stackoverflow.com

각 health, info등으로 적어도 되지만 귀찮으면 asterisk로 모두 포함해도 된다. 대표적인 엔드포인트에 대한 설명은 아래 기재해 놓았다.

management:
  endpoints:
    web:
      exposure:
        include: "*"

endpoint는 다음과 같이 지정해줄수 있다.

management:
  endpoints:
    web:
      base-path: /application

 

기본적인 정보를 보여주는 Endpoint 


Actuator에서 사용할 수 있는 기본적인 엔드포인트는 다음과 같고 더 자세하게 다른 정보를 볼 수 있는 api들도 제공한다.

 

/health

 앱의 대한 건강 정보를 보여준다. 


/info 

전체적인 앱에 대한 정보를 보여준다.


/metrics 

앱에 대한 통계정보를 보여준다. (카운터 등등) 

우선적으로 볼 수 있는 파라미터정보가 나오고 상세히 보고 싶은경우 다음 경로에 추가해서 조회하면 볼 수있다.


/httptrace

앱에 대한 상세 요청정보를 보여준다.

 

Spring Acturator의 데이터는 모두 휘발성으로 데이터를 저장하고 있지는 않다. 이를 저장하는 방법은 따로 있는지는 확인해보지는 않았다. 나주에 기회되면 확인해보고 정리해봐야겠다. 그리고 health endpoint를 내가 원하는대로 커스텀할 수 있다. 이 또한 당장 필요성이 없어 상세하게 알아보지는 않았지만 크게 어렵지는 않은 것 같다. 

모니터링 할 수 있는 Acturator가 생각보다 괜찮은 것 같다. 나중에 실무에서 써볼 수 있으면 써봐야겠다.

 

Sprint 5에서 리액티브 프로그래밍을 지원하는 웹 애플리케이션을 만들 수 있다.

리액티브 프로그램이란?
이전시간에 정리했었지만 스프링 리액티브 프로그래밍을 들어가기 전에 간단하게 정리해 보자. 일반적으로 리액티브 프로그래밍은 비동기, evnet-driven 방식으로 non-blocking 하는 프로그래밍으로써 일반적인 시스템 보다 작은 작은 쓰레드 수가 필요하다.

그럼 왜?
비동기-논블록킹 리액티브 개발이 가능. 기존 멀티쓰레드 블로킹 방식과는 다르게 서버의 응답에 의지하지 않는 효율적 개발이 가능. 서버간에 호출이 잦은 마이크로 서비스에서 사용됨

Spring Web Reactive Module (Servlet 3.1 이상부터 지원)
Spring Framework 5는 spring-web-reactive module을 포함한다. 이 module은 REST, HTML browser 그리고 웹 소켓에서 상호작용을 할 수 있도록 reactive HTTP와 Web Socket을 지원한다.


Annotation-based 프로그래밍 모델
기존 Spring MVC와 동일하게 @Controller를 사용이 가능하다. 주요한 차이점은 HandlerMapping과 HandlerAdapter등의 내용이 비동기인지 차이이고 리액티브에서는 HttpServletRequest, HttpServletResponse 보다 ServerHttpRequest, ServerHttpResponse에서 동작한다.

exeample)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RestController
public class PersonController {
 
    private final PersonRepository repository;
 
    public PersonController(PersonRepository repository) {
        this.repository = repository;
    }
 
    @PostMapping("/person")
    Mono<Void> create(@RequestBody Publisher<Person> personStream) {
        return this.repository.save(personStream).then();
    }
 
    @GetMapping("/person")
    Flux<Person> list() {
        return this.repository.findAll();
    }
 
    @GetMapping("/person/{id}")
    Mono<Person> findById(@PathVariable String id) {
        return this.repository.findOne(id);
    }
}
cs


함수형 프로그래밍 모델
함수형 프로그래밍 모델을 사용하여 routing과 request를 handling 할 수있다. 가장 주요로 사용하는 함수형 인터페이스는 RouterFunction과 HandlerFunction이다. 이 둘을 사용하면 웹 애플리케이션에서 손쉽게 block을 생성할 수 있다.

아래 예는 person에 대한 데이터를 router로 request handling 하는 것을 보여준다.
- endpoint, accept type, response 등등

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
 
/**
 * reactive
 *
 * @author wedul
 * @since 2019-01-12
 **/
@Configuration
@EnableWebFlux
public class PersonRouterFunction implements WebFluxConfigurer {
 
  @Bean
  public RouterFunction<ServerResponse> routes(FunctionHandler handler) {
    return RouterFunctions.route(GET("/person"), handler::functionHandler);
  }
 
}
 
@Component
class FunctionHandler {
  public Mono<ServerResponse> functionHandler(ServerRequest req) {
    Mono<Person> person = Mono.just(new Person("wedul"));
    return ServerResponse.ok().body(person, Person.class);
  }
}
cs


여기서 route에서 받는 파라미터는 RouterFuction 인터페이스로써 Mono<HandlerFunction<T>> route(ServerRequest request) 함수를 정의해서 전달해야한다. 그래서 route에 미리 정의해놓은 fuctionHandler를 전달한다. 이렇게 정의해서 사용하는 Handler를 route에 무조건 적용한다면 복잡해지고 다른곳에서 공통으로 사용할 수 없으니 위에 처럼 따로 구분해서 사용할 것.

MVC annotation으로만 사용하던 나에게는 불편해보이지만 함수형 프로그램을 할 수 있다는 장점이 있는거 같다.


웹 클라이언트
선언된 컨트롤러에서 Flux 또는 Mono를 읽고 구독을 진행해서 데이터를 읽는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * reactive
 *
 * @author wedul
 * @since 2019-01-12
 **/
public class PersonClient {
 
  WebClient client = WebClient.create("https://wedul.pos:8080");
 
  public void Test() {
    Flux<Person> personMono = client.get().uri("/person").retrieve().bodyToFlux(Person.class);
 
    personMono.subscribe(System.out::println);
  }
 
}
cs


예제 github

https://github.com/weduls/reactive

지금까지 Spring5에서 추가되었던 리액트 프로그램을 사용하여 간단한 프로그램을 만들어 봤지만 정확하게 Mono와 Flux에 차이와 정의를 정리하지 못한 것 같다. 이번기회에 두 개의 정확한 차이와 사용방법등을 정리해보자.


리액티브 프로그래밍
비동기 블록킹 프로세스로 동작하는 애플리케이션을 논블록킹 프로세스로 동작하기 위해서 지원하는 프로그래밍. (현재 node.js의 동작방식과 유사)


기존 Spring 블록킹 방식
웹에서 서버에 요청이 왔을때 서버는 요청에 대한 적절한 응답을 보내야 하는데 만약 작업이 오래 걸릴 경우에는 요청에 대한 응답이 모두 종료될 때까지 블록킹된다. Spring에서는 그래서 동시 요청 처리를 위해서 멀티 thread를 지원한다. 그러면 하나의 작업이 thread에서 진행되고 다른 thread가 다른 요청을 할당받아서 처리한다. 하지만 이렇게 결국 thread가 늘어나게 되는 경우에는 thread 할당에 필요한 리소스가 늘어나게 되어 비 효율적이 될 수도 있다.


Spring5의 Non blocking
Spring 5가 도입 되면서 클라이언트에 요청에 별도의 thread를 생성하지 않고 buffer를 사용해서 요청을 받고 뒤에서 처리하는 처리하는 thread는 여러개를 두어서 처리한다. 결국 node.js의 싱글스레드 논블로킹을 따라가는 것 같다.

그럼 왜 블로킹 방식을 지원하던 스프링에서 왜 논블로킹 방식을 생각하게 된걸까? 만약에 수천개의 스트림 데이터가 초당 계속 업데이트 되는 시스템이고 적절하게 응답을 해줘야할 때 기존의 블로킹 방식에 경우 상당한 부담을 받게 된다. 그래서 이런 부담을 효율적으로 처리하기 위해서 도입되었다.


Mono와 Flux
Mono는 0-1개의 결과만을 처리하기 위한 Reactor 객체
Flux는 0-N개의 결과물을 처리하기 위한 Reactor 객체

보통 여러 스트림을 하나의 결과를 모아줄 때 Mono를 쓰고 각각의 Mono를 합쳐서 하나의 여러 개의 값을 여러개의 값을 처리할 떄 Flux를 사용한다.

근데 이 부분에서 의문이 있다. 왜 그럼 Flux를 사용하면 되는거지 한개까지만 데이터를 처리할 수 있는 Mono라는 타입이 있는걸까? Mono와 Flux는 같은 Publisher 인터페이스를 구현해서 만들어졌다. 하지만 어떤 시스템에서는 Multi Result가 아닌 하나의 결과셋만 있는 경우가 있다. 그럴경우에는 Mono를 사용한다. 예를 들어 우리가 보통 자바에서 하나의 결과 또는 결과가 없는경우에 List를 사용해서 결과를 받지 않는다. 그와 동일한 개념이라고 생각하면 좋다.

그럼 Mono와 Flux를 사용해서 리액티브 프로그래밍을 하는 방식을정리해보자.

리액티브 스트림

  • 비동기 스트림 처리를 위한 표준으로써 next는 다음신호를 담고 complete는 신호가 끝난것 그리고 error은 신호보내는 도중 에러가 발생한 것을 의미한다.
  • Publisher가 전송하면 데이터는 sequence 대로 전송한다. 그러면 Subscriber가 데이터를 수신한다.
  • next, complete, error 신호를 발생시킨다.


기본적인 설명

1
2
3
4
5
6
// Integer 값을 발생하는 Flux 생성
Flux<Integer> seq = Flux.just(4, 5, 6); 
 
// 구독
seq.subscribe(System.out::println); 
 
cs


Flux.just(1, 2, 3);
--1-2-3-|→ 이처럼 1, 2, 3 세개의 next신호를 발생하고 마지막에 complete 신호를 발생시켜 시퀀스를 끝낸다.

Flux.just();
아무런 sequence가 없는 경우에는 complete 신호만 발생시킨다.

Mono.just(1);
--1-|→ Mono와 Flux의 차이는 Mono는 최대 발생할 수 있는 값이 1개이다.


구독과 신호 발생
sequence는 바로 신호를 발생하지 않는다. 구독을 하는 시점에 신호를 발생하기 시작한다.

1
2
3
4
Flux.just(1, 2, 3)
 .doOnNext(i -> System.out.println("호출: " + i))
 .subscribe(i -> System.out.println("출력 결과: " + i));
 
cs

-> doOnNext메소드는 consumer로부터 구독이 일어났을때 실행된다. 그래서 위에 메시지에 next 신호가 발생했을때 다음과 같은 결과가 발생한다.

호출: 1
출력 결과: 1
호출: 2
출력 결과: 2
호출: 3
출력 결과: 3


Subscriber 인터페이스 메서드 사용방법 정의
subscriber에서 제공하는 메소드는 다음과 같고 구독이 발생하면 onSubscribe가 호출되고 다음 값을 요청하면 onNext 오류가 발생하면 onError 모든 데이터 요청이 끝나면 onComplete가 호출된다.

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
Flux<Integer> seq = Flux.just(1, 2, 3);
 
seq.subscribe(new Subscriber<>() {
    private Subscription subscription;
    @Override
    public void onSubscribe(Subscription s) {
        // 구독 시작
        this.subscription = s;
        this.subscription.request(1);
    }
 
    @Override
    public void onNext(Integer i) {
        System.out.println("Costomer가 Publisher에게 데이터 요청: " + i);
        this.subscription.request(1);
    }
 
    @Override
    public void onError(Throwable t) {
        System.out.println("Subscriber.onError: " + t.getMessage());
    }
 
    @Override
    public void onComplete() {
        System.out.println("Subscriber.onComplete");
    }
});
cs

-> seq.subscribe 메서드에서 전달한 임의 Subscriber 객체를 onSubscribe 메서드에서 인자로 받아서 이를 필드로 저장하여 사용한다. request(1)은 한개의 데이터를 요청한다는 뜻이다. 만약 모든 데이터를 한번에 받고 싶다면 다음과 같이 지정하면 된다

1
2
3
4
5
6
@Override
public void onSubscribe(Subscription s) {
    System.out.println("Subscriber.onSubscribe");
    this.subscription = s;
    this.subscription.request(Long.MAX_VALUE);
}
cs


콜드 시퀀스와 핫 시퀀스
시퀀스는 구독 시점부터 데이터를 새로 생성하는 Cold sequence와 구독하는 customer와 상관 없이 데이터를 생성하는 hot sequence가 존재한다.

앞 예제 Flux.just()로 생성한 시퀀스가 콜드 시퀀스이다.
콜드 시퀀스는 위에 보면 알겠지만 subscribe가 발생하지 않는다.

Flux<Integer> seq = Flux.just(1, 2, 3);
seq.subscribe(v -> System.out.println(“첫번 째 요청: " + v)); // 구독
seq.subscribe(v -> System.out.println("두번 째 요청: " + v)); // 구독

-> 이 코드를 보면 알겠지만 seq 시퀀스는 구독을 두번한다.이 결과를 seq 시퀀스는 각 구독마다 데이터를 새롭게 생성한다. 마치 API에서 호출하는 것 처럼 매 호출마다 새로운 응답을 만들어 낸다.
첫번 째 요청: 1
첫번 째 요청: 2
첫번 째 요청: 3
두번 째 요청: 1
두번 째 요청: 2
두번 째 요청: 3

핫 시퀀스는 구독여부에 상관없이 데이터가 생성된다. 구독을 하면 구독한 시점이후에 발생하는 데이터부터 신호를 받는다.
-> 예전부터 있었던 데이터를 똑같은 응답을 받는게 아니라 구독을 시작한 부분부터 받는다.


다음 시간에는 스프링으로 직접 만들어보자.



Spring reactor
https://ahea.wordpress.com/2017/02/15/spring-reactive/

단계별 설명
http://wiki.sys4u.co.kr/pages/viewpage.action?pageId=8552586


spring에서 사용하는 의존성 주입에 경우 @Autowired만을 사용하면 나중에 테스트 케이스를 만들때 어려움이 생긴다. 


그래서 생성자 주입을 사용하는데 잘 정돈된 사이트가 있어 공유한다.


http://multifrontgarden.tistory.com/214

+ Recent posts