모범 사례 및 튜토리얼

스프링 부트 애플리케이션에 랫팩 사랑을 불어넣으세요

8 분 읽기

저는 최근에 Ratpack을 사용하기 시작했는데 꽤 마음에 듭니다. 주로 처음부터 시작하는 간단한 프로젝트들을 진행했습니다. 하지만 기존 스프링 부트(Spring Boot) 애플리케이션에서 전통적인 MVC 부분을 대체하기 위해 그것을 사용하고 싶습니다. 이것은 모든 것이 이미 잘 마련되어 있기 때문에 실제로 쉽게 할 수 있습니다. 스프링 모듈.

이 블로그를 구독하신다면 에 관한 예전 게시물을 기억하실지도 모릅니다 저장, 색인 및 검색 ~가 있는 파일 카우치베이스 그리고 스프링 부트를 사용할 것입니다. 관련 코드 예를 들어, 핵심 아이디어는 Spring MVC를 Ratpack으로 교체하고 기존의 레거시, 동기식, 블로킹 서비스를 비동기 및 넌블로킹(non-blocking) 방식으로 전환하는 것입니다. 결과 코드는 다음에서 확인할 수 있습니다. 깃허브 또한.

올바른 의존성 추가하기

나는 Gradle을 사용하고 있다. Ratpack은 이것과 매우 잘 통합되어 있다. 모듈을 추가하기 위해 해야 할 일은 올바른 의존성을 호출하여 추가하는 것뿐이다. ratpack.dependency("myFavoriteModule"). 따라서 저희의 경우, Spring Boot에 대한 지원을 추가하려면 추가해야 합니다 ratpack.dependency("spring-boot"). 안타깝게도 Ratpack이 자동으로 관리하는 버전은 자동 Couchbase 구성을 지원하는 버전인 1.4.0.M3보다 낮습니다. 따라서 이번에는 의존성을 수동으로 추가해야 합니다.

    dependencies {
      compile ratpack.dependency("guice"),
              ratpack.dependency("rx"),
              ratpack.dependency("handlebars"),
              "com.couchbase.client:java-client:2.3.1",
              "org.springframework.boot:spring-boot-autoconfigure:1.4.0.M3",
              "io.ratpack:ratpack-spring-boot:1.3.3",
              "org.slf4j:slf4j-simple:1.7.12",
              "org.codehaus.plexus:plexus-utils:3.0.21",
              "commons-codec:commons-codec:1.10" 
    }

여기서 볼 수 있는 것은 ratpack.dependency("spring-boot") 추가하는 바로 가기입니다 org.springframework.boot:spring-boot-autoconfigure:1.4.0.M3 그리고 io.ratpack:ratpack-spring-boot:1.3.3. 이 모듈을 사용하면 Ratpack 서버를 Spring 애플리케이션에 통합할 수 있습니다. Ratpack 컨텍스트에서 Spring @Bean을 가져오고, 핸들러를 Spring 구성으로 선언할 수 있습니다.

Ratpack 구성 선언

Spring Boot에서 정말 좋아해야 할 한 가지는 자동 설정입니다. Couchbase SDK가 클래스패스에 있고 속성이 있는지 확인하기만 하면 됩니다. spring.couchbase.bootstrap-hosts 가 선언됩니다. 그 순간 Spring 빈이 기본 버킷을 위해 인스턴스화됩니다. 그리고 이 버킷 인스턴스는 빈으로 또는 Ratpack의 컨텍스트에서 사용할 수 있게 됩니다. 따라서 Ratpack 레이어에 Couchbase용 바인딩을 선언할 필요가 없습니다.

Ratpack으로 전통적으로 가장 먼저 하는 일은 서버를 시작하고 설정과 핸들러를 정의하는 것입니다. 여기서는 이미 실행 중인 Spring Boot 애플리케이션이 있습니다. @Configuration이 주석으로 지정된 모든 클래스는 자동으로 감지되어 애플리케이션 설정에 추가됩니다. 해당 설정을 선언하는 첫 번째 단계는 구현하는 클래스를 만드는 것입니다 RatpackServerCustomizer 그리고 @Configuration으로 어노테이트하세요. 이를 통해 핸들러, 바인딩, 그리고 서버 설정 목록을 정의할 수 있습니다. 다음 예제에서는 몇 가지 서버 속성을 등록하고 여러 클래스를 Ratpack의 컨텍스트에 바인딩하고 있습니다. ‘server.maxContentLength’ 속성은 업로드할 수 있는 파일의 최대 크기입니다.

    package org.couchbase.devex;

    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    import java.util.Map;

    import org.couchbase.devex.domain.StoredFileRenderer;
    import org.couchbase.devex.service.SearchService;
    import org.springframework.context.annotation.Configuration;

    import com.google.common.collect.ImmutableMap;

    import ratpack.form.Form;
    import ratpack.func.Action;
    import ratpack.guice.BindingsSpec;
    import ratpack.handlebars.HandlebarsModule;
    import ratpack.handlebars.Template;
    import ratpack.handling.Chain;
    import ratpack.rx.RxRatpack;
    import ratpack.server.BaseDir;
    import ratpack.server.ServerConfigBuilder;
    import ratpack.spring.config.RatpackServerCustomizer;
    import rx.Observable;

    @Configuration
    public class RatpackConfiguration implements RatpackServerCustomizer {

        @Override
        public List getHandlers() {
            List handlers = new ArrayList();
            handlers.add(fileApi());
            return handlers;
        }

        @Override
        public Action getServerConfig() {
            return config -> config.baseDir(BaseDir.find())
                    .props(ImmutableMap.of("server.maxContentLength", "100000000", "app.name", "Search Store File"));

        }

        @Override
        public Action getBindings() {
            return bindingConfig -> bindingConfig.module(HandlebarsModule.class).bind(FileHandler.class)
                    .bind(StoredFileRenderer.class).bind(ErrorHandlerImpl.class).bind(ClientHandlerImpl.class);
        }

        private Action fileApi() {
            return chain -> chain.prefix("file", FileHandler.class).post("fulltext", ctx -> {
                ctx.parse(Form.class).then(form -> {
                    String queryString = form.get("queryString");
                    SearchService searchService = ctx.get(SearchService.class);
                    Observable<map> files = searchService.searchFulltextFiles(queryString);
                    RxRatpack.promise(files).then(response -> ctx
                            .render(Template.handlebarsTemplate("uploadForm", "text/html", m -> m.put("files", response))));
                });
            }).post("n1ql", ctx -> {
                ctx.parse(Form.class).then(form -> {
                    String queryString = form.get("queryString");
                    SearchService searchService = ctx.get(SearchService.class);
                    Observable<map> files = searchService.searchN1QLFiles(queryString);
                    RxRatpack.promise(files).then(response -> ctx
                            .render(Template.handlebarsTemplate("uploadForm", "text/html", m -> m.put("files", response))));
                });
            });
        }
    }
</map</map

애플리케이션 템플릿 시스템은 Handlebars를 기반으로 하므로 HandlebarsModule이 필요합니다. FileHandler는 ‘/file’ API에 대한 모든 호출을 처리하며, StoredFileRenderer는 StoredFile이 올바르게 렌더링되도록 보장합니다. 마지막 두 바인딩은 오류 처리를 위한 것입니다.

여기서 가장 중요한 일은 파일API 내 핸들러를 선언하는 메서드입니다. 핸들러는 사용자가 특정 URL을 호출했을 때 어떤 동작이 수행될지를 정의합니다. 여기서는 ‘/file/*’에 대한 모든 호출을 FileHandler 클래스와 연결합니다. 또한 ‘/fulltext’ 및 ‘/n1ql’에 대한 POST 요청의 동작도 정의합니다.

Ratpack은 Promise를 사용합니다. 따라서 POST 요청에서 Form을 파싱할 때 Promise를 얻게 됩니다. 각 POST에서 볼 수 있듯이 SearchService는 Ratpack의 컨텍스트에서 가져옵니다. 설정에서 바인딩된 적이 없더라도 말입니다. 이는 통합의 일환으로 Spring 빈을 컨텍스트에서 사용할 수 있기 때문입니다.

다음 단계는 Observable을 반환하는 해당 검색 서비스를 호출하는 것입니다. Observable용 래퍼를 제공하는 Ratpack의 rx-java 모듈을 사용할 수 있습니다. 그러면 이것이 promise로 래핑됩니다. 그런 다음 응답을 간단히 렌더링하면 됩니다.

이 시점에서 우리는 모든 Spring MVC를 제거했습니다 컨트롤러. 보시다시피 제 서비스는 Observable을 반환합니다. 이전 애플리케이션에서는 그렇지 않았습니다.

랫팩용 서비스 마이그레이션

대부분의 서비스가 카우치베이스(Couchbase)에 의존하고 있습니다. SDK가 RxJava 기반이어서 대부분 비동기 및 넌블로킹 방식으로 쉽게 전환하고 Observable을 반환하도록 만들 수 있습니다.

RxJava 사용하기

이것은 아주 간단한 예제입니다. 결과를 Map의 List로 매핑하는 N1QL 쿼리입니다. 처음 두 줄은 주로 쿼리를 정의하는 것이므로 전혀 변경되지 않습니다. 두 번째 버전에서는 동기식 버킷을 사용할 때 매핑이 더 자연스럽게 느껴지는 것을 볼 수 있습니다.

    public List<map<string, object="">> searchN1QLFiles(String whereClause) {
        N1qlQuery query = N1qlQuery.simple(
                "SELECT binaryStoreLocation, binaryStoreDigest FROM `default` WHERE type= 'file' " + whereClause);
        query.params().consistency(ScanConsistency.STATEMENT_PLUS);
        N1qlQueryResult res = bucket.query(query);
        List<map<string, object="">> filenames = res.allRows().stream().map(row -> row.value().toMap())
                .collect(Collectors.toList());
        return filenames;
    }
</map<string,></map<string,>

된다

    public Observable<map<string, object="">> searchN1QLFiles(String whereClause) {
        N1qlQuery query = N1qlQuery.simple(
                "SELECT binaryStoreLocation, binaryStoreDigest FROM `default` WHERE type= 'file' " + whereClause);
        query.params().consistency(ScanConsistency.STATEMENT_PLUS);
        return bucket.async().query(query).flatMap(AsyncN1qlQueryResult::rows).map(r -> r.value().toMap());
    }
</map<string,>

레거시 코드의 차단을 어떻게 해야 할까요?

내 서비스 중 일부는 오래된 블로킹 코드에 의존하고 있습니다. 이를 넌블로킹으로 만드는 마법 같은 방법은 없지만, 간단히 Promise로 감싸는 것은 가능합니다. 이를 통해 핸들러에서 쉽게 사용할 수 있습니다. 블로킹 호출을 감싸는 것은 매우 쉬우며, 함수를 ‘Blocking.get()’으로 감싸기만 하면 됩니다. 여기 아주 간단한 예시가 있습니다:

    public String getSha1Digest(InputStream is) {
        return DigestUtils.sha1Hex(is);
    }

된다

    public Promise getSha1Digest(InputStream is) {
        return Blocking.get(() -> DigestUtils.sha1Hex(is));
    }

결론

이제 Spring Boot 애플리케이션에 Ratpack을 적용하는 데 필요한 거의 모든 것을 알게 되었습니다. 빠진 부분이 있다고 느껴지시면 저에게 연락해 주세요 트위터 또는 아래 댓글에 남겨주세요.

이 기사 공유하기

작가

로랑은 파리에 사는 찌질한 매니아 기질이 있는 헤비메탈 마니아입니다. 그는 주로 자바(Java)로 코드를 작성하고 AsciiDoc으로 구조화된 텍스트를 작성하며, 데이터, 리액티브 프로그래밍 및 기타 유행어 같은 주제에 대해 자주 이야기합니다. 그는 또한 Clever Cloud와 Nuxeo의 전직 개발자 애드보케이트로 활동하며 해당 커뮤니티가 더 크고 강하게 성장하도록 시간과 전문 지식을 쏟아부었습니다. 현재는 카우치베이스(Couchbase)에서 개발자 관계(Developer Relations)를 총괄하고 있습니다.

댓글 남기기

데이터베이스의 확장성을 과거의 일로 남겨두세요

각 스택 추가마다 추가 비용이 발생하고 관리해야 할 항목도 더 많아집니다. 2분 안에 절약할 수 있는 금액을 추산해 보세요.