Spring Framwork5에서 새롭게 추가된 모듈이다. webflux는 reactive 스타일의 어플리케이션 개발을 도와주는 모듈이라한다. 즉 Reactive Programming방식의 스프링이다.
Reactive Programming(반응 형 프로그래밍)은비동기(Asynchronous)식 및 이벤트 기반(event-driven)의Non Blocking이다.
웹 플럭스는 기존 MVC처럼 이용할 수 있고, 기본으로 Netty를 제공한다고 함.
WebFlux는 아래와 같은 용도로 사용하는 것을 추천 한다고 합니다.
비동기 - 논블록킹 리액티브 개발에 사용
효율적으로 동작하는 고성능 웹어플리케이션 개발
서비스간 호출이 많은 마이크로서비스 아키텍처에 적합
기존 MVC VS 웹 플럭스
기존 MVC 방법 Spring MVC의 경우 어플리케이션이 실행되면서 Thread Pool을 만들어 놓는다. 요청이 들어오면 그 요청을 Queue에 쌓고 이것을 Thread Pool의 Thread들이 요청을 가져가 처리한다. 하지만 많은 요청이 들어와 Pool Size를 초과하여 Queue에 계속 요청이 쌓이는 경우가 발생할 수 있다.
웹 플럭스
웹 플럭스의 2가지 방식
1. 기존 에노테이션 방식
@Controller
...{
// 애너테이션 기반 라우팅
@GetMapping("/hello")
@ResponseBody
public Mono<String> getHello() {
return demoService.getHello();
}
}
2. 함수형 프로그래밍 방식
@Component
// 함수 기반 라우팅
@Bean
public RouterFunction<ServerResponse> routes(DemoHandler demoHandler) {
return RouterFunctions
.route(RequestPredicates.GET("/hello"), demoHandler::getHello);
}
두가지 중에 편한 방법을 통해서 사용해도 무방하다.
웹 플럭스의 반환형
아래와 같이MVC에서 일반적으로 사용하던 Plain Object를 사용할 수 없고 반드시 Publisher Object로 감싸서 반환해야 한다. 이 때 Publisher는 Reactive Stream Interface중 하나인데 (Processor, Publisher, Subscriber, Subscription) Reactive Stream Interface에 관한 설명은brunch.co.kr/@springboot/153참고하기 바란다.
2. Mono & Flux란?
Publisher의 실제 구현체 즉 웹 플럭스에서 반환하는 형식이라고 생각해도 무방.
Flux (0-N개의 데이터)
Flux는 Publisher의 구현체로서, 0-N개의 데이터를 발행(전달, 방출)할 수 있다. 하나의 데이터를 전달할때마다 onNex 이벤트를 발생시키고, 모든 데이터 전달이 완료되면 onComplete 이벤트가 발생하며 오류가 발생할 경우 onError이벤트가 발생한다.
class CustomRunnable implements Runnable {
public void run() {
try {
for (int i=0; i<100; ++i) {
System.out.println("[" + Thread.currentThread().getName() + "] CustomRunnable " + i);
Thread.sleep(10);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class ThreadPractice {
public static void main(String[] args) {
Thread t1 = new CustomThread();
Thread t2 = new Thread(new CustomRunnable());
t1.start();
t2.start();
}
}
@Async 이용하기
@Service
public class TestService {
@Async
public void customThread(int i) {
for (int i=0; i<100; ++i) {
System.out.println("[" + Thread.currentThread().getName() + "] CustomRunnable " + i);
Thread.sleep(10);
}
}
}
@Congiguration 사용하기
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public Executor asyncThreadTaskExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(50);
threadPoolTaskExecutor.setMaxPoolSize(100);
threadPoolTaskExecutor.setQueueCapacity(200);
threadPoolTaskExecutor.setThreadNamePrefix("thread-pool");
return threadPoolTaskExecutor;
}
}
스프링을 이용하면 스레드 구성을 다음과같이 간편하게 할 수 있다
@Asnyc 메소드를 이용할 때 주의사항
public 메소드를 이용해야한다
자가호출(self)을 하면 안된다
이러한 주의사항이 있는 이유는 spring AOP가 프록시 패턴의 개념을 이용하기 때문이다. 즉 프록시 클래스에서 @Async 메소드를 호출해야 하는데 private하거나, self로 호출하면 프록시클래스에 의해 호출되지 않기 때문이다.
※프록시 패턴글에 자세한 설명 있음
2. completablefuture란?
@Async같은 비동기 메소드를 사용할 때, Void형태를 사용한다면 문제가 되지 않는다. 하지만 return이 존재한다면 기존에는 Future나 ListenableFuture를 이용하여 해결했지만 JAVA 8버전 부터는 CompletableFuture를 제공하므로 이를 사용하는 방법을 정리한다.
다음은 Completeablefuture를 사용한 예제이다.
다음과 같이 @Async 메소드에서는 결과 값을 CompletableFuture객체에 담아서 반환해야하고 호출한 곳에서는 이 CompletableFuture 객체를 이용해야한다.
@Service
public class GitHubLookupService {
@Async
public CompletableFuture findUser(String user) throws InterruptedException {
//로직
return CompletableFuture.completedFuture(results);
}
}
return을 CompletableFuture를 통해서 하는것을 알았으니 return 받은 쪽 에서는 어떻게 사용하는지 확인해보자.
get 함수란?
다음과 같이 .get()메소드를 통해 결과를 확인할 수 있다.
get을 하게 되면 page1의 수행이 끝날때까지 잠시 기다렸다가 결과값을 반환하게 된다.
public void run(String... args) throws Exception {
//호출 해서 저장
CompletableFuture page1 = gitHubLookupService.findUser("PivotalSoftware");
//다른 로직
System.out.println(page1.get());
}
그림으로 표현해보면 다음과 같다.
thenCompose 란?
하나의 CompletableFuture가 수행하고 나온 결과를 넘겨주어 다음 CompletableFuture객체가 이를 가지고 다른 메소드를 수행하는 방법.
public void run(String... args) throws Exception {
//호출 해서 저장
CompletableFuture<Integer> price1 = testService.getPrice(100);
//다른 로직
price1.thenCompose(result -> testService.getPrice(result)).join();
}
그림으로 표현하면 다음과 같다.
thenCombine 이란?
CompletableFuture를 2개 병렬 실행해서 결과를 조합할 때 사용한다. thenCompose와 달리 두개를 동시에 실행한다는 차이가 있다.
public void run(String... args) throws Exception {
//호출 해서 저장
CompletableFuture<Integer> price1 = testService.getPrice();
CompletableFuture<Integer> price2 = testService.getPrice();
//다른 로직
price1.thenCombine(price2, (a, b) -> a+b);
}
allof 란?
thenCombine처럼 병렬 실행해서 결과를 조합할 때 사용한다. CompletableFuture를 3개이상 조합할 때 사용한다.
public void run(String... args) throws Exception {
//호출 해서 저장
CompletableFuture<Integer> price1 = testService.getPrice();
CompletableFuture<Integer> price2 = testService.getPrice();
CompletableFuture<Integer> price3 = testService.getPrice();
//다른 로직
CompletableFuture.allOf(price1,price2,price3).join();
}
allof를 지정하여 사용하고싶지않을 때
리스트에 CompletableFuture객체를 add하고 add한 리스트를 allof에 넣어서 사용할 수 있다.
public class Coffee extends CaffeineBeverage {
public void brew() {
System.out.println("필터로 커피 우려냄");
}
public void addCondiments() {
System.out.println("설탕과 커피를 추가함");
}
}
템플릿 메소드에서의 후크
정해진 알고리즘에서의 if문을 통해 과정을 실행시킬지 말지 선택하도록 하는부분을 후크라고 한다.
선택적인 파라미터가 많을수록 파라미터의 제공 상태를 일관성있게 해주고(어떤 파라미터를 제공하는지, null인지) 최종적으로 생성된 객체를 return하는 패턴
2. 예시
만약 다음과 같은 UserInfo 클래스가 있다고 할때 생성자를 통해 객체를 생성하게 될때 파라미터의 개수가 많거나 Null이거나 하는경우 어떠한 파라미터를 제공하는지 알아보기도 힘들고 객체 생성이 용이하지않음.
public class UserInfo {
private String name;
private int age;
public UserInfo(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void getUserInfo() {
System.out.println(name, age);
}
}
따라서 빌더패턴을 적용해보면 다음과 같다. 아래와 같이 set을 통해 변수값을 지정하고 build를 통해 객체를 생성하는 방식이다. 사용자는 set을 통해 파라미터값을 정하기 때문에 생성자 방식처럼 순서에 맞게 작성하지 않아도 된다는 장점이 있고 예시는 클래스를 따로 나눴지만 내부 클래스를 이용해 하나의 클래스처럼 이용할 수 있다.
public class UserInfoBuilder {
private String name;
private int age;
public UserInfoBuilder setName(String name) {
this.name = name;
return this;
}
public UserInfoBuilder setAge(int age) {
this.age = age;
return this;
}
public UserInfo build() {
return new UserInfo(name, age);
}
}
UserInfoBuilder userInfoBuilder = new UserInfoBuilder();
UserInfo userInfo3 = userInfoBuilder.setName("테스터3").setAge(26).build();
롬복을 이용하게 되면 @Builder 어노테이션으로 같은 기능을 수행하여 코드량을 줄일 수 있다.
3. 추상 팩토리 패턴과 빌더패턴의 차이
앞서 나온 추상팩토리 패턴과 빌더패턴 모두 객체를 생성하는 관점에서 비슷하게 느껴질 수 있으나, 다음과 같은 차이가 있다.
추상팩토리는 제품의 최종단계가 아닌 원재료 즉 전체를 위한 하위객체를 바로 리턴하고,
public class NYPizzaingredientFactory implements PizzaIngredientFactory{
@Override
public Dough createDough() {
return new ThinCrustdough();
}
@Override
public Sauce createSauce() {
return new MarinaraSauce();
}
}
빌더 패턴은 정보가 합쳐진 최종 객체를 반환한다는 특징이 있다.
쉽게 생각해서 빌더패턴은 최종 결과물을 사용자가 받아보게 되고, 팩토리 패턴은 최종 결과물에 필요한 객체를 받아보게 된다.
퍼사드 패턴을 이용하지 않았다면 Movie를 볼때의 과정을 전부 직접 호출했어야하고, 다른 과정 음악듣기 같은경우 Movie와 다른 과정으로 직접 실행해야한다 하지만 퍼사드 패턴을 이용하면 다음과 같이 실행할 수 있다.
HomeTheaterFacade homeTheater =
new HomeTheaterFacade(amp, tuner, dvd, cd,
projector, screen, lights, popper);
homeTheater.watchMovie("Raiders of the Lost Ark");
public class MallardDuck implements Duck {
public void quack() {
System.out.println("오리!");
}
}
Turkey 인터페이스
public interface Turkey {
public void gobble();
}
WildTurkey 클래스
public class WildTurkey implements Turkey {
public void gobble {
System.out.println("칠면조!");
}
}
어댑터 클래스
public class TurkeyAdapter implements Duck {
Turkey turkey;
public TurkeyAdapter(Turkey turkey) {
this.turkey = turkey;
}
public void quack() {
turkey.gobble();
}
}
Test클래스
public class Test {
public static void main(String[] args) {
MallardDuck duck = new MallardDuck();
WildTurkey turkey = new WildTurkey();
Duck turkeyAdapter = new TurkeyAdapter(turkey);
System.out.println(duck.quack());
System.out.println(turkeyAdapter.quack());
즉 칠면조 클래스를 덕인터페이스에 맞게 변환하여 오리처럼 사용할 수 있게 해주는 패턴이 어댑터 패턴이다.
즉, 어떤 객체(A)에서 다른 객체(B)의 메서드를 실행하려면 그 객체(B)를 참조하고 있어야 하는 의존성이 발생합니다.
그러나 커맨드 패턴을 적용하면 의존성을 제거할 수 있습니다.
즉 내가 B라는 객체의 메서드를 실행 시킬때 B를 실행시키는걸 모른채로 메서드를 실행하게 만든다.
2. 예시
다음과 같이 Lamp를 on 하는 클래스가 주어지고, 이를 사용할 Remote 클래스를 다음과 같이 만든다.
Lamp 클래스
public class Lamp {
public void On(){
System.out.println("Lamp on");
}
}
Remote 클래스
public class Remote {
private Lamp lamp;
public Remote(Lamp lamp){
this.lamp = lamp;
}
public void execute(){
lamp.on();
}
}
Client 클래스
public class Client {
public static void main(String args[]){
Lamp lamp = new Lamp();
Remote remote = new Remote(lamp);
remote.execute();
}
}
만약 다음과 같이 Remote코드를 작성했을 경우 Lamp ON 이외에 다른 클래스(Streo On)가 주어질 경우 Remote가 수정되어야 하는 문제가 생긴다. 따라서 커맨드 패턴을 적용하면 다음과 같다.
Command 클래스
public interface Command {
public void execute();
}
LightOnCommand 클래스
public class LightOnCommand implements Command {
Light light; //이 Light 객체는 실제 불키는 방법을 알고있는 리시버 객체
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.on();
}
}
SimpleRemoteControl 클래스 (인보커)
public class SimpleRemoteControl {
Command slot;
public SimpleRemotecontrol() { }
public void setCommand(Command command) {
slot = command;
}
public void buttonWasPressed() {
slot.execute();
}
}
RemoteControlTest 클래스 (클라이언트)
public class RemoteControlTest {
public static void main(String[] args) {
SimpleRemoteControl remote = new SimpleRemoteControl();
Light light = new Light();
LightOnCommand lightOn = new LightOnCommand(light);
remote.setCommand(lightOn);
remote.buttonWasPressed();
}
}
즉 커맨드 패턴은 다음과 같은 구조를 띄게 된다. 따라서 Lamp 이외의 클래스를 사용하려면 LightOnCommand와 같은 XXXCommand 클래스를 만들고 적용할 수 있기 때문에 확장에 용이하다.
3. 커맨드 패턴의 적용 예시
java의 Thread가 커맨드 패턴이 적용된 예시이다.
메인 클래스의 Thread 객체에서 start를 통해 Command의 run 메소드를 실행시키지만 Thread 입장에서는 어떤것을 실행시키는지 알 수 없다.
따라서 run = execute를 의미, start = buttonWasPressed를 의미한다.
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new Command());
t.start(); /* run을 실행하지만 Command가 무엇을 하는지는 모른다 */
}
}
class Command implements Runnable {
@Override
public void run() {
System.out.println("RUN!");
}
}