brunch

You can make anything
by writing

C.S.Lewis

by 에디의 기술블로그 Sep 12. 2018

스프링5 웹플럭스 정리 및 샘플 코드

-스프링5 웹플럭스 기본 정리 및 샘플 코드


이 글은 2년전에 작성된 글로, 당시 필자가 웹플럭스에 대해서 전혀 모르는 상황에서, 빠르게 검토 후 작성한 글입니다. 해당 글은 내용이 많이 허접하니 읽지 않는 것을 추천드립니다. 


나중에 시간 여유가 된다면 각잡고 글을 작성하겠습니다.







지난주 카카오에서 "if 카카오" 라는 기술세미나가 진행되었다. 필자는 접수 시작하자마자 신청을 하였지만, 높은 경쟁률을 뚫지 못하고 결국 참석하지 못했다. 아쉽다 가고 싶었는데... (참고로 2012년 쯤, 다음에서 주최한 DevOn 이라는 세미나를 참석한 기억이 있다. 당시 어쿠스틱 밴드가 공연을 했었는데 아직도 기억에 남는 인상 깊은 세미나였다. 기술 세미나에 어쿠스틱 밴드가 등장하다니!!!) 

사실 이번 카카오 세미나를 참석하지 못해서 제일 아쉬운 점은 토비님의 발표를 듣지 못했다는 사실이다. 필자는 스프링5 웹플럭스에 대해서 그동안 거의 관심이 없었지만, 최근에 조금씩 관심을 갖기 시작하였다. 토비님께서 이번 카카오 세미나에서 웹플럭스 테스트 관련한 주제로 발표를 진행하셨는데, 참석하지 못해서 너무 아쉬운 마음이다. 동영상이라도 올려주시면 좋을 텐데, PPT 자료만 공개가 되어서 더더욱 아쉽게 생각한다. 아쉬운 마음을 뒤로한 채, 스프링5 웹플럭스에 대해서 아주 기초적인 내용 중심으로 독학으로 주말 스터디를 진행하였고, 그 내용을 이번 글에서 공유한다. 


참고로 이 글은 공식 레퍼런스를 참고하여 작성하는 글이다. 완벽한 정보를 정리하고 싶지만 짧은 시간으로 학습해서 글로 써내기가 쉽지가 않다. 기초적인 내용이니, 웹플럭스를 잘 아는 개발자는 굳이 안 읽어도 된다. 시간이 남아서 읽어보는 개발자가 있다면 이 글에서 잘못된 내용에 대해서 피드백을 해주길 바란다. 사실 댓글 달아주시는 분들은 거의 없겠지만, 댓글이 달리면 매우 기쁘고 행복할 것이다. 


https://docs.spring.io/spring/docs/5.0.0.BUILD-SNAPSHOT/spring-framework-reference/html/web-reactive.html




스프링 웹플럭스

스프링 웹플럭스에 대해서 간단하게 소개한다. 


리액티브 프로그래밍이란?

필자가 간단하게 정의한 리액티브 프로그래밍 이란, 비동기 프로세스로 동작하는 이벤트 기반의 non-blocking 애플리케이션을 구현하는 프로그래밍이다. 자세한 내용은 아래 링크를 참고하자. 영어다...

https://spring.io/blog/2016/06/07/notes-on-reactive-programming-part-i-the-reactive-landscape


블록킹 IO

Socket(이하 소켓)은 서버-클라이언트 아키텍처에서 두 시스템 통신의 끝 지점이다. 참고로 java.net 패키지는 연결의 클라이언트 측과 서버 측을 각각 구현하는 클래스를 제공한다. 클라이언트가 서버에 요청을 하면 서버는 요청을 처리한 후 응답을 보내야 하는데, 이때 연결을 설정하기 위해 소켓이 사용된다. 애플리케이션은 소켓에 바인딩해야 하며, 서버는 클라이언트의 요청을 받기 위해서, 소켓을 수신 대기해야 한다. 블록킹 IO 환경에서는 요청에 대한 응답을 처리할 때까지 Thread 가 차단된다. 



만약 동시 요청을 수행하려면 여러 개의 Thread 가 필요하다. 즉, 클라이언트의 요청이 추가되면 새로운 Thread를 아래 그림처럼 신규로 할당해야 한다. 

사실 우리가 운영하는 웹서비스는 대부분 이렇게 구축이 되어있을 것이다. 각 Thread는 메모리 할당이 필요하며, 클라이언트 요청이 증가할수록 Thread를 관리하는 것이 부담스러워진다. 또한, 클라이언트 요청을 수신하는 여러 개의 Thread는 리소스 낭비가 될 수도 있다. 


Non-블록킹 IO

스프링5에 웹플럭스가 도입되면서 동시에 Netty 임베디드 서버가 기본 스펙으로 추가되었다. Netty는 비동기 이벤트 기반의 고성능 네트워크 프레임워크 서버이다. Non-블록킹 IO는 클라이언트의 요청 각각에 서버의 Thread를 바인딩하지 않는다. 대신, 개별 버퍼를 사용해서 요청에 대한 알림을 주고받는다. Non-블록킹 IO는 여러 연결을 하나의 Thread로 처리할 수 있다. 

애플리케이션 서버 비교

Tomcat : 1Request = 1 Thread

Node.js : All Request = 1 Thread

Netty : Many Reqeust = 1 Thread

Netty 서버는 매우 유연한 모델을 제공한다. 이 글에서는 Netty 에 대해서는 자세하게 설명하지는 않겠다. 아래 링크를 참고하길 바란다. 


https://netty.io/


스프링 웹플럭스

스프링 웹플럭스는 스프링5에서 새로 등장한, 웹 애플리케이션에서 리액티브 프로그래밍을 제공하는 프레임워크이다. 스프링웹플럭스는 기존 스프링 MVC 를 전부 대체하는 개념은 아니다. 웹애플리케이션의 용도에 맞게 MVC 와 웹플럭스는 공존해서 사용이 가능하다. 웹플럭스의 용도는 아래와 같다. (용도는 토비님의 2017년 스프링캠프를 참고하였다.)

비동기-논블록킹 리액티브 개발에 사용

효율적으로 동작하는 고성능 웹 애플리케이션 개발

서비스간 호출이 많은 마이크로서비스 아키텍처에 적합

https://www.youtube.com/watch?v=2E_1yb8iLKk


스프링 웹플럭스는 기존 MVC 와도 함께 사용할 수 있다는데, 아래와 같이 프레임워크가 구성되어 있다. 


참고 - https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html

스프링 웹플럭스 프레임워크는 두가지 방식으로 개발이 지원된다.

기존의 @어노테이션 방식

새로운 함수형 모델 방식


필자는 웹플럭스를 제대로 사용 경험이 없어서 판단하기는 어렵지만, 토비님께서 MVC와 웹플럭스를 한프로젝트에서 같이 사용하는 것이 좋지 않다는 의견을 주셨다고 한다.(세미나를 다녀온 분에 전해듣기로...)


스프링 웹플럭스 샘플 코드

간단한 샘플 코드를 작성하겠다. 허접한(?) 개발자인 필자가 작성한 코드의 구조는 아래와 같다.


디펜던시 추가


dependencies {  

   compile('org.springframework.boot:spring-boot-starter-webflux')

   //생략...


컨트롤러(기존 방식 사용)

컨트롤러는 기존의 MVC 방식과 유사하다.(아니 같다..) 물론 위에 설명했듯이 두 가지 방식중 하나인 펑셔널(?)하게 작성할 수도 있는데 잠시 후에 설명하겠다. 스프링 웹플럭스는 Mono 와 Flux 로 데이터를 응답하는데 Mono 는 단일 데이터이고, Flux 는 리스트 데이터이다. 


/////////////////////////////Controller Layer

@RestController  

@RequiredArgsConstructor  

public class ProgrammerController {

    private final ProgrammerUseCase programmerUseCase;

    

    @GetMapping("/programmers/{name}")  

    public Mono<Person> hello(@PathVariable("name") String name) {  

        return Mono.just(programmerUseCase.findByName(name));  

    }    

    

    @GetMapping("/programmers")  

    public Flux<Person> all() {  

        return... 생략 

    }

}


/////////////////////////////Service Layer

public interface Personal {

    void init();

    Person findByName(String name);

    List<Person> findAll();

}


@Service

@RequiredArgsConstructor

public class ProgrammerUseCase {

    

    private final Personal personal;

    public void init(){

        personal.init();

    }

    public Person findByName(String name){

        return personal.findByName(name);

    }

    public List<Person> findAll(){

        return personal.findAll();

    }

}


@Component

@RequiredArgsConstructor

public class ProgrammerProvider implements Personal {

    private final ProgrammerRepository programmerRepository;

    @Override

    public void init() {

        programmerRepository.put(new Person("eddy", 1981));

        programmerRepository.put(new Person("hye", 1982));

    }

    @Override

    public Person findByName(String name) {

        return programmerRepository.findByName(name);

    }

    @Override

    public List<Person> findAll() {

        return programmerRepository.findAll();

    }

}



/////////////////////////////Domain Layer

@Data

@AllArgsConstructor

@NoArgsConstructor

public class Person {

    private String name;

    private Integer born;

}


/////////////////////////////Repository Layer

@Repository

public class ProgrammerRepository {

    

    private static HashMap<String, Person> programmerHashMap = new HashMap<>();

    

    public Person findByName(String name){

        return programmerHashMap.get(name);

    }

    public List<Person> findAll(){

        //return programmerHashMap.values().stream().collect(Collectors.toList());

        return new ArrayList<>(programmerHashMap.values());

    }

    public void put(Person person){

        programmerHashMap.put(person.getName(), person);

    }

}



새로운 함수형 모델 사용

@EnableWebFlux 를 사용하면 새로운 함수형 모델(펑셔널?하게) 엔드포인트를 정의할 수 있다. 


//ProgrammerFunctionalConfiguration.java

@Configuration  

@EnableWebFlux  

public class ProgrammerFunctionalConfiguration {           

    @Bean  

    public RouterFunction<ServerResponse> routes(ProgrammerFunctionalHandler handler) {  

         return RouterFunctions.route(GET("/personfunctional"), handler::findByName);  

    }  

}


//ProgrammerFunctionalHandler.java

@Component  

@RequiredArgsConstructor  

public class ProgrammerFunctionalHandler {    


    private final ProgrammerUseCase programmerUseCase;    

 

    public Mono<ServerResponse> findByName(ServerRequest request) {  

        Mono<Person> helloworldMono = Mono.just(programmerUseCase.findByName("eddy"));  

         return ServerResponse.ok().body(helloworldMono, Person.class);  

      }  

}


테스트 코드 작성

테스트 코드 작성 방법은 아직 자세히 모르겠다. 필자가 TDD 에 매우 약하기는 하다. 일단, @WebFluxTest 의 용도는 잘 몰라서 일단 주석처리했다. 추가하면 오류가 난다. 아마 @SpringBootTest 랑 꼬이거나 뭔가 필자가 잘못한게 있을 것이다. 참고로 JUnit5 를 적용하였다. 


@ExtendWith(SpringExtension.class)

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

//@WebFluxTest //해당 어노테이션의 용도를 아직은 잘 모르겠다. 

public class ProgrammerControllerTests {

    

    @Autowired

    private ApplicationContext applicationContext;

    

    @Autowired

    private WebTestClient webTestClient;

   

    @Autowired

    private ProgrammerRepository programmerRepository;

    

    @BeforeEach

    @DisplayName("테스트 데이터 초기화")

    public void init(){

        //Autowired 를 안한다면 아래와 같이 작성해야 한다. 

        //webTestClient = WebTestClient.bindToApplicationContext(applicationContext).configureClient().build();

        programmerRepository.put(new Person("eddy", 1981));

    }

    

    @Test

    @DisplayName("이름 검색 응답 테스트")

    public void programmerResonseTest(){

        Person person = webTestClient.get().uri("/programmers/eddy").exchange()

                .expectStatus().isOk()

                .expectBody(Person.class)

                .returnResult().getResponseBody();                

        assertEquals(person.getName(), "eddy");

        assertEquals(person.getBorn().intValue(), 1981);

    }

}



허접한 필자의 샘플 코드는 아래 github 을 참고하면 된다. 

https://github.com/sieunkr/spring-reactive/tree/master/sample01




스프링 데이터 리액티브 

위에 설명한대로, 스프링 웹플럭스는 Mono, Flux 타입으로 데이터를 응답해야 한다. 필자는 아직 Flux 등의 타입으로 변환하는 노하우가 없는 상태이다. 친절하게도 스프링 에서는, 아니 스프링부트 2.0 이상에서는 스프링 데이터 프로젝트에서 리액티브 프로그래밍을 위한 라이브러리를 다양하게 제공한다. 특히, 일부 스타터도 제공하는데, 스프링 부트 2.0.4 기준으로 스프링 Data 프로젝트에서 리액티브 프로그래밍을 지원해주는 스타터는 아래와 같다. 


Spring Data Reactive MongoDB

Spring Data Reactive Cassandra

Spring Data Reactive Couchbase

Spring Data Reactive Redis

물론, 해당 라이브러리 외에 다른 RDBMS, NoSQL 등에 대해서 스프링 부트 기반으로 리액티브 프로그래밍이 가능하겠지만, 스타터를 제공해주면 더 편리하게 비즈니스 로직에 집중할 수 있다. MongoDB 관련해서만 간단하게 샘플코드를 작성해보았다. 


디펜던시


dependencies {

    compile('org.springframework.boot:spring-boot-starter-data-mongodb-reactive')

    compile('org.springframework.boot:spring-boot-starter-webflux')

    compileOnly('org.projectlombok:lombok')

    testCompile('org.springframework.boot:spring-boot-starter-test')

    testCompile('io.projectreactor:reactor-test')

    생략...

}


Repository 정의

먼저 @EnableReactiveMongoRepositories 를 선언해야 한다. 필자는 Main 애플리케이션 에 선언하였다.


@SpringBootApplication

@EnableReactiveMongoRepositories

public class DemoApplication {

생략...


그리고, ReactiveMongoRepository 인터페이스를 상속받는 Repository 를 작성하자. 이때 리턴 타입을 Flux , Mono 를 사용할 수 있다. 라이브러리에서 웹플럭스를 위한 Flux,Mono 타입으로 변환해준다. 


public interface  VideoRepository extends ReactiveMongoRepository<Video, Integer> {

    Flux<Video> findAll();

    Mono<Video> findByNid(String nid);

}



Repository 사용

만약 컨트롤러에서 Repository 를 바로 사용한다면 아래와 같이 작성하면 된다. (솔직히 컨트롤러에서 Repository 를 바로 사용하는 레이어가 익숙하지는 않다. 실무에서는 웹플럭스 Repository 인터페이스를 어떻게 사용하는지 궁금하다.)


@Autowired

    private VideoRepository videoRepository;

    @GetMapping

    public Flux<Video> findAll() {

        return videoRepository.findAll();

    }



마무리

간단하게 정리해봤다. 많이 부족한 글이지만, 이번 글을 통해서 스프링 웹플럭스에 대해서 조금은 이해를 하게 되었다. 하지만 아직 의문으로 남아있는 점은 아래와 같다. 


기존 톰캣 환경에서도 멀티 Thread 환경으로 구축이 가능한데, 웹플럭스의 핵심(?) 장점은 무엇인가?

실무에서 웹플럭스는 어떻게 사용하는가?

토비님 말씀대로 정말 MVC 와 같이 사용하면 별로인가?

테스트 코드는 어떻게 작성하면 되는가?


글을 작성하고 보니, 세미나를 참석하지 못한것이 더욱 아쉽게 느껴진다. 혹시 노하우가 있는 개발자는 피드백을 주길 바란다. 또는 필자가 나중에 웹플럭스를 실무에서 도입해서 노하우가 생기는 날이 온다면!! 그때 열심히 스터디를 하고 글을 멋지게 다시 작성해보겠다. (웹플럭스를 당장 쓸일이 없기 때문에, 언제가 될지 모른다. ) 


그럼 이상 허접한 글을 마친다. 끝!



[개인적인 추가의견]

마이크로서비스 환경에서, 필자는 메시지패턴을 선호하는데, 메시지패턴은 메시지브로커를 중심으로 시스템을 통합하여 운영하여 기존 동기 프로토콜을 사용했던 시스템 통합의 단점을 보완하고, 유연하고 확장성 높은 시스템 아키텍처를 구축할 수 있었다. 이런 예전 경험했던 사례들은, 리액티브 프로그래밍으로 구현해도 유사한 장점을 만들어 낼 수 있을 것 같다는 생각이다. 물론, 웹플럭스에 대해서 아직 필자가 이해도가 낮고, 시스템 아키텍처에는 정답이 없다고 생각한다. MVC , 웹플럭스, 메시지패턴 등 현재 팀 환경, 개발자의 역량, 기획자의 요구사항 등 전반적인 상황을 판단해서 가장 적합한 아키텍처를 구축하고, 추후에 환경 변화에 빠르게 대응할 수 있는, 빠르게 적응하여 변화 가능한 아키텍처를 구축하는 것이 더 중요한 과제일 것이다. 토비님의 강의에서도 설명해주셨지만, 단지 웹플럭스를 쓰고 싶다는 이유 하나만으로는 웹플럭스를 도입해야 하는 명분은 되지 않는다는 점을 이해하고 있어야 한다. 


레퍼런스

https://docs.spring.io/spring/docs/5.0.0.BUILD-SNAPSHOT/spring-framework-reference/html/web-reactive.html

https://docs.oracle.com/javase/tutorial/networking/sockets/index.html

https://www.baeldung.com/spring-data-mongodb-reactive

https://github.com/spring-projects/spring-data-examples/tree/master/mongodb/reactive

매거진의 이전글 JUnit5 in Spring
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari