본문 바로가기
Backend boot camp/Session4

[Spring WebFlux] Spring WebFlux

by orioncsy 2022. 12. 9.

Spring WebFlux

Spring WebFlux

개념

  • Spring 5 부터 추가된 기술 스택은 Reactive 스택
  • WebFlux는 Reactor의 Flux가 Web에 사용된 기술
  • Reactive Streams를 구현한 구현체라면 Reactor가 아니어도 사용 가능

Spring WebFlux 애플리케이션 VS Spring MVC 애플리케이션

기술 스택 비교

  • WebFlux(Reactive Stack)
    • Non-Blocking 통신 지원
    • Reactive Adapter를 사용해 RxJava 등 다른 리액티브 라이브러리 사용 가능
    • Spring Security를 사용한 보안, WebFilter를 사용해 리액티브 특성에 맞는 인증, 권한 적용
    • 웹 계층에 Spring WebFlux 사용
    • 리액티브 스택을 데이터 액세스 계층까지 확장하여 Non-Blocking 통신 지원
      • R2DBC(Reactive Relation DataBase Connectivity)는 관계형 데이터베이스에 Non-Blocking 통신을 적용하기 위한 표준 사양으로 DB 벤더에서 사양에 맞는 드라이버 구현 및 공급
  • Spring MVC(Servlet Stack)
    • Blocking 통신 지원
    • Servlet API의 스펙에 의존
    • Spring Security를 사용한 보안
    • 웹 계층에 Spring MVC 사용
    • Spring Data Repository로 JDBC, JPA, NoSQL 지원

공통점

  • @Controller 같은 애너테이션을 Spring WebFlux에도 사용 가능
  • RestTemplate을 대체할 수 있는 Webclient를 양 쪽 모두 사용 가능
  • Tomcat, Jetty 등의 서블릿 컨테이너는 서블릿 방식, 리액티브 방식 모두 지원

코드 비교

  • Spring MVC Blocking 처리 방식
    • Spring MVC 기반 메인 애플리케이션 코드
      • Controller 클래스에서 @RestController를 붙여 생성
        • 우선 CoffeeResponseDto라는 클래스를 생성하여 커피ID, 한글 이름, 영어 이름, 가격을 멤버로 선언한다.
        • RestTemplate 객체를 선언해서 생성자로 RestTemplateBuilder를 객체로 받아 build() 메서드를 이용해 할당한다.
        • @GetMapping으로 get 요청만 하나 작성해서 coffee-id를 받아 로그를 현재 시간으로(LocalDateTime.now())를 사용하여 출력한다.
        • 로그를 출력하고 ResponseEntity를 선언하여 restTemplate 객체에 getForEntity() 메서드를 사용하여 uri, CoffeeResponseDto.class를 넘겨주어 객체를 받고 RespopnseEntity.ok(response.getBody())를 리턴한다.
        • uri의 경우 http://localhost:7070/v11/coffes/1로 다른 포트를 사용하는 외부 애플리케이션 포트(7070포트사용)을 할당하여 다른 애플리케이션에 요청을 전송하도록 작성
    • 외부 애플리케이션 controller
      • application.yml 파일에 포트 번호 지정
      server:
      	port: 7070
      
      • 외부 애플리케이션에도 get 핸들러를 작성하고 CoffeeResponseDto 객체를 선언하여 반환한다.
      • 이때 Thread.sleep()을 통해 스레드 동작을 잠시 멈춘다. 외부 애플리케이션 요청 처리 시간을 5초 정도가지는 것을 구현하는 것이다.
    • 메인 애플리케이션 클라이언트 코드
      • @SpringBootApplacation이 붙은 클래스에서
        • @Bean으로 등록하여 CommandLineRunner를 반환하는 run() 메서드를 생성
          • (String… args)→{} 형태로 반환
          • 그 안에 요청 시작 시각을 LocalTime.now()를 통해 로그로 출력
          • for문을 돌아 5번의 요청을 반복하고 로그로 시간을 찍는다.
          • 5번의 요청은 private을 통해 CoffeeResponseDto를 반환하는 메서드를 생성해서 호출
            • RestTemplate 객체를 생성해서 메인 애플리케이션 get 핸들러를 uri로 해서 ResponseEntity 객체에 restTemplate.getForEntity() 메서드를 통해 받아와서 .getBody()로 Dto 객체를 반환한다.
    • 실행할 때는 외부 애플리케이션이 먼저 실행되어야 메인 애플리케이션에서 외부 애플리케이션에 요청이 가능해진다.
    • 요청 결과는 각 요청마다 5초 정도의 시간이 걸려 총 25초가 걸린다. 이것은 Blocking 방식으로 외부 서버와 통신한다는 것을 확인 가능
  • Spring WebFlux의 Non-Blocking 처리 방식
    • 메인 애플리케이션 controller
      • 이전 MVC 코드와 유사하지만 외부 애플리케이션의 통신을 WebClient라는 Rest Client를 사용한다.
      • getCoffee 핸들러 메서드에서 반환 타입이 Mono<CoffeeResponseDto>를 사용한다.
        • 반환으로 Webclient.creat()을통해 생성
        • .uri()로 통신할 uri 작성
        • .retrieve().bodyToMono(CoffeeResponseDto.class)를 통해 요청을 받아서 Mono 타입으로 만든 후 반환
    • 외부 애플리케이션 controller
      • 이전 코드와 유사하지만 @ResponseStatus(HttpStatus.OK)를 통해 핸들러 상태를 전달
      • 반환 타입을 Mono<CoffeeResponseDto>로 변경하고 return Mono.just(responseDto)를 통해 Webflux 활용
    • 메인 애플리케이션 클라이언트 코드
      • 이전 코드와 유사하지만 getCoffee() 메서드를 반환형을 Mono<CoffeeResponseDto>로 변경하고 위에서 작성했던 것과 마찬가지로 WebClient를 생성해서 반환한다.
      • run() 메서드에서는 for문에서 getCoffee() 메서드를 호출해서 .subscribe() 메서드를 사용해 log를 출력하는 람다식을 구현한다.
    • 실행해보면 총 10초 정도의 시간이 소요된다.
  • 즉 Spring webFlux의 경우, Blocking 되지 않고 비동기적으로 요청을 전송하고 처리한다. 이것이 Non-Blocking 방식이다.

Reactive application 구현

프로젝트 설정

build.gradle 설정

  • dependencies에 아래 추가
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
runtimeOnly 'io.r2dbc:r2dbc-h2'
  • 기존의 spring-boot-starter-web에서 webflux로 변경
  • JPA 대신 R2DBC 사용
  • H2 인메모리 대신 io.r2dbc:r2dbc-h2 사용
  • 리액티브 스택은 H2 웹 콘솔을 정상 지원하지 않는다.

application.yml 설정

  • spring.sql.init.shema-locations에서 classpath*:db/h2/schema.sql를 선언한다.
    • Spring Data JPA의 Auto DDL 기능을 제공하지 않아 직접 설정을 추가해야 한다.
  • spring.sql.init.data-locations에서 classpath*:db/h2/data.sql을 실행
  • logging.level.org.springframework.r2dbc 를 DEBUG로 설정
    • DB와 상호작용하는 것을 로그로 출력

DB Schema 설정

  • src/main/resources/db/h2에 scheme.sql 파일의 테이블 생성 스크립트 작성

애플리케이션 공통 설정

  • main() 메서드가 포함된 애플리케이션 클래스에 @EnableR2dbcRepositories를 붙여서 R2DBC repository 사용
  • @EnableR2dbcAuditing을 추가하여 데이터베이스에 엔티티가 저장, 수정될 때 생성, 수정 날짜 자동 저장을 한다.

Controller 구현

  • 기존에 구현된 controller에서 변경
    • postMember()메서드
      • 매개변수를 Mono<MemberDto.Post>로 타입을 변경한다.
      • Mono<MemberDto.Response> 타입의 객체를 선언한다.
        • 입력으로 받은 requestBody를 .flatMap(post -> memberService.createMember(mapper.memberPostToMember(post))) 을 통해 객체를 추가
        • .map(member→mapper.memberToMemberResponse(member))를 통해 ResponseDto 형태로 변환한다.
      • return을 통해 new ResponseEntity<>(위에서 구한 객체, HttpStatus.CREATED)) 반환
    • patchMember() 메서드
      • 위와 같이 같은 방식으로 처리
    • getMember() 메서드
      • 위에서 구한 방식대로 적용한다.
    • getmembers() 메서드
      • Mono<List<MemberDto.Response>> response를 선언한다.
        • memberService.findMembers()하여 PageRequest.of(page-1, size, Sort.by(”memberId”).descending())을 전달한다.
        • .map()을 통해 pageMember→ mapper.membersToMemberResponses(pageMember.getContent())을 전달한다.
      • 결과를 ResponseEntity에 적용하여 반환
    • deleteMember() 메서드
      • Mono<Void>를 선언한 객체에 memberService.deleteMember를 사용하여 적용

Entity 클래스 정의

  • 기존 엔티티 클래스 변형
    • @Entity 애너테이션 제거
    • @Id를 식별자에 추가
    • @CreatedDate, @LastModifiedDate 등을 추가하여 LocalDateTime 객체를 선언한다.

서비스 클래스 구현

  • verifyExistEmail() 메서드
    • 반환형을 Mono<Void>로 변경
    • memberRepository.findByEmail()을 통해 객체를 찾는다.
    • .flatMap()에서 값이 null이 아닌 경우에 return Mono.error()로 에러를 발생시킨다.
    • 값이 null인 경우 return Mono.empty()를 반환
  • findVerifiedMEmber() 메서드
    • memberRepository에서 findById() 로 객체를 찾고 .switchIfEmpty(Mono.err())를 통해 객체가 없다면 에러 발생시킨다.
  • createMember() 메서드
    • 반환 타입을 Mono<Member>로 변경
    • verifyExistEmail()을 통해 회원이 존재하는지 확인
    • .then(memberRepository.save(member))를 통해 없다면 저장
    • .map(resultMember→{});
      • template.insert(new Stamp(resultMember.getMemberId())).subscribe()
        • Stamp 정보를 테이블에 저장하고 subscribe()를 통해 동작을 수행한다.
      • return resultMember를 통해 결과를 반환
  • updateMember() 메서드
    • 위와 마찬가지로 Mono<Member>를 반환 타입으로 설정
    • findVerifiedMember()를 통해 기존 회원 탐색하고
    • .map()에서 beanUtils 객체를 활용하여 member에 null 값을 제외하고 모든 멤버 값을 결과 객체에 넣어준다.
      • BeanUtils.copyProperties(src, to, null, null)을 사용하면 null 값을 제외하고 src의 멤버를 to의 멤버에 복사한다.
    • flatMap()에서 memberRepository.save()를 통해 저장한다.
  • findMember() 메서드
    • 반환 타입을 Mono<Member>로 변경
    • findVerifiedMember()를 사용하여 반환
  • findMembers() 메서드
    • 반환 타입은 Mono<Page<Member>>로 선언
    • 매개 변수로 PageRequest 객체를 받는다.
    • memberRepository.findAllBy(pageRequest)를 통해 회원 목록을 가져온다.
    • .collectList()를 통해 List 객체로 가져오고
    • .zipWith(memberRepository.count())를 통해 전체 데이터의 건 수를 받는다.
    • .map(tuple -> new PageImpl<>(tuple.getT1(), pageRequest, tuple.getT2()));
      • tuple.getT1()은 list 객체이고, tuple.getT2()는 전체 건수이다.
  • deleteMember() 메서드
    • 반환 타입은 Mono<void>로 선언
    • findVerifiedMember를 통해 회원을 찾는다.
    • flatMap()에서 member -> template.delete(query(where("MEMBER_ID").is(memberId)), Stamp.class) 를 통해서 Stamp 테이블에서 해당 memberID를 갖는 값을 삭제한다.
    • then(memberRepository.deleteById(memberId))를 통해서 제거한다.

Repository

  • extends R2dbcRepository<Member,Long>을 통해 R2dbcRepository의 인터페이스를 확장
  • Mono<Member> findByEmail(String email)을 통해 email 값에 해당하는 값을 찾아 Mono 객체로 반환
  • Flux<Member> findAllBy(Pageable pageable)을 통해 Pageable 객체로 받아서 모든 회원 목록을 Flux 객체로 반환

'Backend boot camp > Session4' 카테고리의 다른 글

[Cloud]배포 컨테이너  (1) 2022.12.09
[Cloud] 운영 환경 구성  (1) 2022.12.09
[Spring WebFlux] Reactor  (0) 2022.12.09
[Spring WebFlux] 리액티브 프로그래밍  (0) 2022.12.09
[Spring Security] OAuth 2  (0) 2022.12.09