스프링 부트란 무엇인가
스프링 부트는 스프링 포트폴리오를 신속하게, 미리 정의된 방식으로, 이식성 있게, 실제 서비스 환경에 사용할 수 있도록 조립해 놓은 것이다.
- 신속성: 의존관계를 포함한 여러 요소에 대한 의사결정을 신속히 적용할 수 있도록 해준다.
- 미리 정의된 방식: 스프링 부트를 어떻게 사용할지 구성을 정하면 기본적인 설정값이 자동으로 지정된다.
- 이식성: JDK가 있는 곳이라면 스프링 부트 애플리케이션은 어디에서나 실행할 수 있다.
- 실제 서비스 환경에 사용 가능: 견고한 완성품이다.
리액티브 프로그래밍 소개
- 대규모 사용자가 지속적으로 증가하는 시스템(하이엔드 시스템)은 비동기적으로 인입되는 거의 무제한의 데이터 스트림을 논블로킹 방식으로 처리할 수 있어야 한다.
- 1970 년대에 리액티브 프로그래밍 기술이 나왔으나 이제서야 주목을 받는 이유가 무엇일까?
- 리액티브 프로그래밍을 써야할 정도로 대규모 서비스가 많지 않았기 때문이다.
- 서버를 더 투입하는 방식이 먹히지 않을 정도의 트래픽
- 그렇기 때문에 개발자들은 기존의 자원을 더 효율적이고 일관성 있게 사용하는 방법을 찾으려 했다.
- 그렇게 나온 해법이 리액티브 스트림이다.
- 리액티브 스트림(Reactive Stream)
- 발행자(Publisher)와 구독자(Subscriber)사이의 간단한 계약을 정의하는 명세
- 구독자(Subscriber)가 수요 조절을 할 수 있는 배압(Backpressure)를 적용할 수 있다.
- 프로젝트 리액터는 VMware에서 만든 리액티브 스트림 구현체다.
- 프로젝트 리액터의 특성
- 논블로킹, 비동기 프로그래밍 모델
- 함수형 프로그래밍 스타일
- 스레드를 신경 쓸 필요 없는 동시성
리액터 타입
- 리액티브 스트림은 수요 조절에 기반하고 있다.
- 프로젝트 리액터는 Flux<T>를 사용해서 이러한 수요 조절을 구현한다.
- Flux<T>는 실제 물건을 전달해주는 역할을 하는 플레이스홀더로 서빙 점원과 비슷하다.
class KitchenService {
Flux<Dish> getDishes() {
// You could model a ChefService, but let's just
// hard code some tasty dishes.
returnFlux.just(//
new Dish("Sesame chicken"),//
new Dish("Lo mein noodles, plain"),//
new Dish("Sweet & sour beef"));
}
}
- 위의 예제 코드는 리액터를 활용한 간단한 KitchenService 코드이다.
- 서빙 점원이 호출하게 될 getDishes 함수는 세 가지 요리를 하드코딩으로 반환하고 있다.
- Flux<Dish>안에 포함된 요리는 아직 완성되지 않았고 머지 않아 완성될 것이다.
- 요리가 완성되면 서빙 점원이 행동(act)할 수 있다. 하지만 리액터는 논블로킹 방식으로 동작하기 때문에 서빙 점원은(서버 스레드) 다른 일을 수행할 수 있다.
Flux vs Future
- 결과가 아직 정해지지 않았고 미래 어느 시점이 되어야 알 수 있다는 점에서 둘은 비슷하다.
- Future는 이미 시작되었음을 나타내는 반면 Flux는 시작할 수 있음을 나타낸다.
- Future에는 없는 Flux의 특징
- 하나 이상의 값(Dish) 포함 가능
- 각 값(Dish)이 제공될 때 어떤 일이 발생하는지 지정 가능
- 성공과 실패의 두 가지 경로 모두에 대한 처리 방향 정의 가능
- 결과 폴링 불필요
- 함수형 프로그래밍 지원
- Future는 정확하게 하나의 값을 제공하는 것이 목적이었고, Flux는 다수의 값을 지원하는 것이 목적으로 만들어 졌다.
평범한 서빙점원
class SimpleServer {
private final KitchenService kitchen;
SimpleServer(KitchenService kitchen) {
this.kitchen = kitchen;
}
Flux<Dish> doingMyJob() {
return this.kitchen.getDishes() //
.map(dish -> Dish.deliver(dish));
}
}
- doingMyJob() 함수는 레스토랑 매니저가 서빙 점원에게 kitchen에 가서 요리를 받아오는 일을 수행한다고 볼 수 있다.
- 주방에 요리를 요청한 후에는 deliver(dish)를 호출해서 요리를 손님에게 가져다주는 일을 지정했다.
- 위의 예제 코드는 단순한 형태의 리액티브 컨슈머다. 리액티브 컨슈머는 다른 리액티브 서비스를 호출하고 결과를 변환(transform)한다.
- map() 함수는 인자로 받은 함수를 Flux에 담겨 있는 각 요리에 적용해서 변환하고 Flux에 담아 반환하므로, 매핑 함수는 무언가를 반드시 반환해야 한다.
- 프로젝트 리액터는 함수형 프로그래밍에서 수행하는 변환 뿐만 아니라, onNext(), onError(), onComplete 시그널 처럼 리액티브 스트림 수명주기에 연결 지을 수도 있다.
친절한 서빙점원
class PoliteServer {
private final KitchenService kitchen;
PoliteServer(KitchenService kitchen) {
this.kitchen = kitchen;
}
Flux<Dish> doingMyJob() {
return this.kitchen.getDishes() //
.doOnNext(dish -> System.out.println("Thank you for " + dish + "!")) //
.doOnError(error -> System.out.println("So sorry about " //
+ error.getMessage())) //
.doOnComplete(() -> System.out.println("Thanks for all your hard work!")) //
.map(Dish::deliver);
}
}
- onNext, onError, onComplete 메소드는 2번 이상 사용될 수도 있으므로 필요한 만큼 핸들러를 추가해주면 된다.
- onNext(), onError(), onComplete()는 리액티브 스트림의 시그널이다.
구독 : 흐름의 시작
- 프로젝트 리액터에서는 필요한 모든 흐름과 모든 핸들러를 정의할 수 있지만, 구독(subscription)하기 전까지는 실제로 아무런 연산도 일어나지 않는다.
class PoliteRestaurant {
public static void main(String... args) {
PoliteServer server = //
new PoliteServer(new KitchenService());
server.doingMyJob().subscribe( //
dish -> System.out.println("Consuming " + dish), //
throwable -> System.err.println(throwable));
}
}
- 위의 코드에서 server.doingMyJob()을 호출한 후에 subscribe()를 호출한다.
- doingMyJob()은 Flux<Dish>를 반환하지만 subscribe()를 호출하지 않으면 아무런 일도 일어나지 않는다.
- subscribe의 첫 번째 인자는 Consumer를 받고있다. 이 콜백은 onNext() 시그널과 함께 완성된 모든 요리 각각에 대해 호출한다.
- 두 번째 인자는 throwable → System.err.println(throwable) 이라는 람다식을 받고 있다. 이 콜백은 onError(throwable) 시그널을 받았을 때 어떻게 처리할지 표현한다.
지금까지의 예제를 정리해보자
- 레스토랑 손님들 ⇒ 웹 사이트를 방문하는 사람들
- 주방 ⇒ 다양한 데이터 저장소와 서버 쪽 서비스의 혼합물
- 서빙 점원 ⇒ 웹 컨트롤러
- 주문을 비동기적으로, 논블로킹 방식으로 처리하는 서빙 점원이 하는 일은 리액티브 웹 컨틀롤러가 하는 일과 동일하다.
스프링 웹플럭스의 등장
- 잠재적으로 사용자의 트래픽이 많아질 것이라 예상되는 서비스라면 웹 계층을 확장하는 것이 필수다.
- 확장 요구가 커질수록 스프링 웹플럭스를 활용해서 웹 요청을 리액티브하게 처리하는 것이 올바른 선택이다.
- 스프링 MVC는 자바 서블릿 API를 기반으로 만들어졌다.
- 서블릿 API는 기본적으로 블로킹 방식으로 동작한다.
- 서블릿 3.1에 도입된 비동기 방식은 리액티브 이벤트 루프(event loop)와 배압 시그널을 지원하지 않는다.
- 100% 논블로킹, 비동기 웹 컨테이너 Netty의 등장과 웹플럭스의 사용으로 MVC 모델 그대로 작성한 코드를 네티 위에서 실행할 수 있다.
스프링 부트로 이커머스 플랫폼 만들기
프로젝트 페어런트
- spring-boot-starter-parent 로 프로젝트의 기준을 지정한다.
- 스프링 스타터 페어런트를 적용하면 미리 정의된 여러 가지 속성 정보, 의존관계, 플러그인을 상속받게 된다.
- 여기에는 전체 스프링 포트폴리오와 잭슨, 네티, 프로젝트 리액터 등 다양한 서드파티 라이브러리도 포함된다.
- 다른 라이브러리가 필요하다면 빌드 파일에 추가하기만 하면 스프링 부트가 페어런트 정보를 바탕으로 적합한 버전을 찾아 사용할 수 있게 해준다.
- 스프링 부트 새 버전이 출시되는 경우, 페어런트 버전만 갱신하면 그에 포함된 모든 라이브러리도 적합한 버전으로 자동으로 업그레이드 된다.
스프링 부트 스타터
- 스타터는 모듈화돼 있고 애플리케이션이 필요로 하는 것을 정확히 집어올 수 있도록 설계되었다.
@SpringBootApplication
- 이 애너테이션은 자동설정과 컴포넌트 탐색 기능을 포함하는 복합 애너테이션이다
자동설정
- 자동설정이란 스프링 부트 애플리케이션의 설정 내용을 분석해서 발견되는 정보에 맞게 다양한 빈을 자동으로 활성화하는 조건 분기 로직이다.
- 자동설정 정책
- 클래스패스
- 다양한 설정 파일
- 특정 빈의 존재 여부
- 기타…
- 애플리케이션의 여러 측면을 살펴보고 유추한 다음, 다양한 컴포넌트를 자동으로 활성화한다.
- 직접 지정한 설정이 없으면 스프링 부트가 알아서 필요한 빈을 생성하고 지정한 설정이 있으면 지정한 대로 동작한다.
컴포넌트 탐색
- 스프링이 빈의 존재를 자동으로 찾아내는 기능
- 스프링 애플리케이션이 실행되면 모든 빈은 애플리케이션 컨텍스트에 등록된다.
스프링 웹플럭스 컨트롤러 생성
@RestController
class ServerController(
val kitchenService: KitchenService
) {
@GetMapping("/server",
produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun serveDishes() : Flux<Dish>{
return kitchenService.getDishes()
}
@GetMapping("/served-dishes",
produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun deliverDishes() : Flux<Dish>{
return kitchenService.getDishes()
.map { Dish.deliver(it) }
}
}
- 반환하는 데이터는 text/event-stream 이고, 클라이언트는 서버가 반환하는 스트림을 쉽게 소비(consume)할 수 있다.
@Service
class KitchenService {
fun getDishes(): Flux<Dish> {
return generate { sink: SynchronousSink<Dish> ->
sink.next(randomDish())
}.delayElements(Duration.ofMillis(250))
}
fun randomDish(): Dish {
return menu.get(picker.nextInt(menu.size))
}
private val menu: List<Dish> = Arrays.asList(
Dish("Sesame chicken"),
Dish("Lo mein noodles, plain"),
Dish("Sweet & sour beef")
)
private val picker: Random = Random()
}
- Flux.generate() 메서드의 파라미터 타입은 Consumer<SynchronousSink<T>> 이다.
- sink는 Flux의 핸들로서, Flux에 포함될 원소를 동적으로 발행할 수 있게 해준다.
- curl -N -v localhost:8080/served-dishes
- 위으 명령어로 데이터 스트림을 확인해볼 수 있다.
Reference
- 스프링 부트 실전 활용 마스터, 그렉 턴키스트
'Spring' 카테고리의 다른 글
스프링 영속성 관리 (영속성 컨텍스트) (0) | 2022.07.08 |
---|---|
JDBC와 트랜잭션 문제, 스프링의 해결책 (0) | 2022.06.17 |
스프링 AOP 내부호출 문제와 한계 (0) | 2022.06.16 |
스프링 AOP (0) | 2022.06.12 |
빈 후처리기, BeanPostProcessor (0) | 2022.06.11 |