ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Bootiful Spring Boot 3 by Josh Long - demo 해보기
    Spring/Framework 2023. 6. 24. 14:46

    목적

    Josh Long 한국 방문 세미나 참석위해,  그의 Youtube video  미리 학습합니다

    아래 video 를 보며, 8가지를 학습해 볼 수 있습니다.

     

     

    1. Bootiful Spring Boot 3 demo application 을 실행해 봅니다
    2. postgres:latest @ServiceConnection annotation 이용하여 bean 으로 container를 띄워봅니다
    3. ProblemDetail 을 이용한 Error Handling 을 해봅니다
    4. Observation 을 이용하여 custom metric 을 기록해 봅니다
    5. buildBootImage 를 이용하여, source -> image 로 만들어 봅니다
    6. declarative Http interface 를 사용해 봅니다
    7. RouteLocator 와 HttpServiceProxyFactory 를 사용하여 gateway 를 구현해봅니다
    8. Spring + GraphQL 을 사용해 봅니다

     

    Bootiful Spring Boot 3 by Josh Long @ Spring I/O 2023

    https://www.youtube.com/watch?v=FvDSL3pSKNQ 

     

    Josh Long 의 영상을 처음 봤는데, 대략 느낌은 이렇습니다.

    First impresstion

    1. 영어로 들어야 하는데, 말의 속도가 빠르고,
    2. 유머를 많이 구사하며, 
    3. live coding 하셨는데, 코딩 속도도 빨라서, 눈으로 따라가기 어려웠습니다. github demo code 확인 필요!
    4.  핵심을 강조하기 보단, 특징을 쭉~ 나열하며 소개합니다.

     

    1~2. demo & testContainer

    github : https://github.com/joshlong/bootiful-spring-boot-3

     

    GitHub - joshlong/bootiful-spring-boot-3: Bootiful Spring Boot 3

    Bootiful Spring Boot 3. Contribute to joshlong/bootiful-spring-boot-3 development by creating an account on GitHub.

    github.com

     

    git clone 받고 특이한 점은, TestServiceApplication 을 실행해야 합니다

    https://github.com/joshlong/bootiful-spring-boot-3/blob/main/service/src/test/java/bootiful/service/TestServiceApplication.java

    @TestConfiguration(proxyBeanMethods = false)
    public class TestServiceApplication {
    
    	@Bean
    	@ServiceConnection
    	@RestartScope
    	PostgreSQLContainer<?> postgresContainer() {
    		return new PostgreSQLContainer<>("postgres:latest");
    	}
    
    	public static void main(String[] args) {
    		SpringApplication
    				.from(ServiceApplication::main)
    				.with(TestServiceApplication.class)
    				.run(args);
    	}
    
    }

    01. @ServiceConnection annotation 을 사용하여, PostgreSQLContainer 을 testContainer 로 올립니다

     

    02. main 에서 

    .from(ServiceApplication::main)
    .with(TestServiceApplication.class)

    를 통해  ServiceApplication 을 TestServiceApplication 과 함께 올립니다

     

     

    실행 후 아래 처럼 데모가 실행됩니다.

     

    http://localhost:8080/customers

    http://localhost:8080/customers/Josh

     

    03.  @RestartScope 은 application 재시작 때마다 재생성 된다는 의미이고, 테스트가 진행되는 동안에는 1번 올려서 재사용 합니다

     

    04. 실행중에 container 가 굉장히 빨리뜬다는 점을 거듭 강조합니다

    2023-06-24T16:11:07.212+09:00  INFO 12496 --- [  restartedMain] tc.testcontainers/ryuk:0.5.1             : Creating container for image: testcontainers/ryuk:0.5.1
    2023-06-24T16:11:07.384+09:00  INFO 12496 --- [  restartedMain] o.t.utility.RegistryAuthLocator          : Credential helper/store (docker-credential-desktop) does not have credentials for https://index.docker.io/v1/
    2023-06-24T16:11:07.497+09:00  INFO 12496 --- [  restartedMain] tc.testcontainers/ryuk:0.5.1             : Container testcontainers/ryuk:0.5.1 is starting: 56a68e287de3b51c6e5088be9da7d94cb1593a023fe132ccae8d88d9d676aaba
    2023-06-24T16:11:07.957+09:00  INFO 12496 --- [  restartedMain] tc.testcontainers/ryuk:0.5.1             : Container testcontainers/ryuk:0.5.1 started in PT0.769181S
    2023-06-24T16:11:07.967+09:00  INFO 12496 --- [  restartedMain] o.t.utility.RyukResourceReaper           : Ryuk started - will monitor and terminate Testcontainers containers on JVM exit
    2023-06-24T16:11:07.967+09:00  INFO 12496 --- [  restartedMain] org.testcontainers.DockerClientFactory   : Checking the system...
    2023-06-24T16:11:07.967+09:00  INFO 12496 --- [  restartedMain] org.testcontainers.DockerClientFactory   : ✔︎ Docker server version should be at least 1.6.0
    2023-06-24T16:11:07.968+09:00  INFO 12496 --- [  restartedMain] tc.postgres:latest                       : Creating container for image: postgres:latest
    2023-06-24T16:11:08.011+09:00  INFO 12496 --- [  restartedMain] tc.postgres:latest                       : Container postgres:latest is starting: 0b11e84e6d2f29017dd86f2168f19d8868dd55f03d4e60f9279b829d7121400b
    2023-06-24T16:11:09.536+09:00  INFO 12496 --- [  restartedMain] tc.postgres:latest                       : Container postgres:latest started in PT1.568668S
    2023-06-24T16:11:09.537+09:00  INFO 12496 --- [  restartedMain] tc.postgres:latest                       : Container is started (JDBC URL: jdbc:postgresql://localhost:52919/test?loggerLevel=OFF)
    2023-06-24T16:11:09.570+09:00  INFO 12496 --- [  restartedMain] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
    2023-06-24T16:11:09.727+09:00  INFO 12496 --- [  restartedMain] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@314648e9
    2023-06-24T16:11:09.728+09:00  INFO 12496 --- [  restartedMain] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
    2023-06-24T16:11:09.909+09:00  INFO 12496 --- [  restartedMain] o.s.b.d.a.OptionalLiveReloadServer       : LiveReload server is running on port 35729
    2023-06-24T16:11:10.433+09:00  INFO 12496 --- [  restartedMain] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 13 endpoint(s) beneath base path '/actuator'
    2023-06-24T16:11:10.484+09:00  WARN 12496 --- [  restartedMain] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Failed to start bean 'webServerStartStop'
    2023-06-24T16:11:10.486+09:00  WARN 12496 --- [  restartedMain] o.s.b.f.support.DisposableBeanAdapter    : Invocation of destroy method failed on bean with name 'inMemoryDatabaseShutdownExecutor': org.springframework.boot.autoconfigure.jdbc.DataSourceProperties$DataSourceBeanCreationException: Failed to determine a suitable driver class
    2023-06-24T16:11:10.488+09:00  INFO 12496 --- [  restartedMain] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
    2023-06-24T16:11:10.490+09:00  INFO 12496 --- [  restartedMain] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
    2023-06-24T16:11:10.494+09:00  INFO 12496 --- [  restartedMain] o.apache.catalina.core.StandardService   : Stopping service [Tomcat]
    2023-06-24T16:11:10.511+09:00  INFO 12496 --- [  restartedMain] .s.b.a.l.ConditionEvaluationReportLogger :
    
    -> 3초

     

    05.  스키마와 초기 데이터는 아래 처럼 정의 되었습니다

     

    schema.sql

    https://github.com/joshlong/bootiful-spring-boot-3/blob/main/service/src/main/resources/schema.sql

    create table if not exists customer
    (
        id   serial primary key,
        name varchar(255) not null
    );

     

    data.sql

    https://github.com/joshlong/bootiful-spring-boot-3/blob/main/service/src/main/resources/data.sql

    delete from customer;
    insert into customer (name ) values ('Josh');
    insert into customer (name ) values ('Jürgen');
    insert into customer (name ) values ('Madhura');
    insert into customer (name ) values ('Olga');
    insert into customer (name ) values ('Dr. Syer');
    insert into customer (name ) values ('Yuxin');
    insert into customer (name ) values ('Spencer');
    insert into customer (name ) values ('Stéphane');

     

    또, application.perperties 에서 아래 속성을 사용해야 합니다

    https://github.com/joshlong/bootiful-spring-boot-3/blob/main/service/src/main/resources/application.properties#L1

    spring.sql.init.mode=always

     

     

    3. ProblemDetail

    RFC 7807에서 정의 된 Problem Details for HTTP APIs를 지원합니다.
    발생한 에러를 JSON 형태로 상세하게 응답하기위한 표준입니다.
    MIME type으로 application/problem+json를 사용합니다.

    https://www.rfc-editor.org/rfc/rfc7807

     

     

    https://github.com/joshlong/bootiful-spring-boot-3/blob/main/service/src/main/java/bootiful/service/ServiceApplication.java#L42-L43

    @GetMapping("/customers/{name}")
    Collection<Customer> customersByName(@PathVariable String name) {
        Assert.state(Character.isUpperCase(name.charAt(0)),"the name must start with a capital letter!");
        
        return Observation
                .createNotStarted("by-name", this.registry)
                .observe(() -> this.repository.findByName(name));
    }

    첫 글자가 UpperCase 인지 체크하는  Assert 를 추가하였습니다.

     

     

    Error 처리하기 위해 , 

    @ControllerAdvice 를 사용하고, Return 값으로 ProblemDetail 을 사용할 수 있습니다

    @ControllerAdvice
    class ErrorHandlingControllerAdvice {
    
        @ExceptionHandler
        ProblemDetail handle(IllegalStateException ise, HttpServletRequest request) {
            request.getHeaderNames().asIterator().forEachRemaining(System.out::println);
            var pd = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST.value());
            pd.setDetail(ise.getMessage());
            return pd;
        }
    }

    ProblemDetail class 의 속성들입니다.

    public class ProblemDetail {
    
    	private static final URI BLANK_TYPE = URI.create("about:blank");
    
    	private URI type = BLANK_TYPE;
    
    	@Nullable
    	private String title;
    
    	private int status;
    
    	@Nullable
    	private String detail;
    
    	@Nullable
    	private URI instance;
    
    	@Nullable
    	private Map<String, Object> properties;
     
     ...중략...
     }

     

     

    그리고, properties 로 노출 내용을 지정할 수 있습니다

    server.error.include-exception=false
    server.error.include-stacktrace=never
    server.error.include-binding-errors=never

     

     

    소문자 입력하여 에러 발생 시키면, 아래처럼 표시됩니다.

    {
      type: "about:blank",
      title: "Bad Request",
      status: 400,
      detail: "the name must start with a capital letter!",
      instance: "/customers/josh"
    }

    만약 에러처리하지 않는 다면, 이렇습니다

     

    또, Error Handling  시 HttpServletRequest 로 부터 정보를 같이 기록할 수 있습니다

    ProblemDetail handle(IllegalStateException ise, HttpServletRequest request) {
      request.getHeaderNames().asIterator().forEachRemaining(System.out::println);
    
      // Error Handling 시 HttpServletRequest 로 정보를 얻어낼 수 있습니다
      String method = request.getMethod();
      String requestURI = request.getRequestURI();
      String queryString = request.getQueryString();
      String protocol = request.getProtocol();
      String remoteAddr = request.getRemoteAddr();
    
    
    }

    덧붙여, HttpServletRequest 는  jakarta package 에 속해 있습니다 jakarta.servlet.http.HttpServletRequest 

    oracle 이 email 에 응답을 안해서 생긴 결과라고 하네요.

    oracle 에 대해 불평을 꽤하고, 힘든 작업이였다고 한참 얘기하는데, 다 알아듣진 못했습니다 😅

     

    참고1 : Spring Framework 6.0.x 이후 Web 에러 처리 - ProblemDetail 사용하기

    https://luvstudy.tistory.com/220

    참고 2 : [Spring 6] Problem Details for HTTP APIs

    https://yahoconut.com/7

     

    4. Observability

    • micrometer.io ( metrics )
    • spring cloud sleuth ( tracing )

    2개가 구분되어 있었는데, tracing 관련된 것을 micrometer 로 옮겼다고 합니다.

    그래서, 아래처럼 "by-name" 이라는 custom metric 을 생성하면, Distributing chasing header 에도 전파되어 사용할 수 있습니다

    https://github.com/joshlong/bootiful-spring-boot-3/blob/main/service/src/main/java/bootiful/service/ServiceApplication.java#L44-L46

    private final ObservationRegistry registry;
    ...생략...
    
    @GetMapping("/customers/{name}")
    Collection<Customer> customersByName(@PathVariable String name) {
        ...생략...
    
        return Observation
                .createNotStarted("by-name", this.registry)
                .observe(() -> this.repository.findByName(name));
    }

    HTTP API 호출, Kafka 또는 Local Data Base 를 사용하여도 해당 header 가 전파됩니다

    application.properties 의 아래 속성이 Observation 관련 속성 입니다

    management.endpoints.web.exposure.include=*
    management.endpoint.health.show-details=always
    management.endpoint.health.probes.enabled=true

    default endpoints 는 아래 입니다

    http://localhost:8080/actuator/metrics

    참고1 official : https://spring.io/blog/2022/10/12/observability-with-spring-boot-3

    참고2 : spring boot 3.x + actuator 파헤치기. 7. about metrics

    https://semtul79.tistory.com/17

     

    5. buildBootImage

    아래 1줄로 image 생성이 가능합니다

    ./gradlew bootBuildImage

        [creator]     Saving docker.io/library/service:0.0.1-SNAPSHOT...
        [creator]     *** Images (689de326ae46):
        [creator]           docker.io/library/service:0.0.1-SNAPSHOT
        [creator]     Adding cache layer 'paketo-buildpacks/bellsoft-liberica:native-image-svm'
        [creator]     Adding cache layer 'paketo-buildpacks/syft:syft'
        [creator]     Adding cache layer 'paketo-buildpacks/native-image:native-image'
        [creator]     Adding cache layer 'buildpacksio/lifecycle:cache.sbom'
    
    Successfully built image 'docker.io/library/service:0.0.1-SNAPSHOT'
    
    
    BUILD SUCCESSFUL in 4m 48s
    9 actionable tasks: 7 executed, 2 up-to-date

    시간은 꽤 오래 걸립니다 4m48s  😅

     

    6. HTTP interface

    openfeign 을 대체하는 declarative HTTP interface 입니다

    https://github.com/joshlong/bootiful-spring-boot-3/blob/main/client/src/main/java/com/example/client/ClientApplication.java#L107-L115

    interface CustomerHttpClient {
    
        @GetExchange("/customers/{name}")
        Flux<Customer> customersByName(@PathVariable String name);
    
        @GetExchange("/customers")
        Flux<Customer> customers();
    
    }

     

    @GetExchange annotation 은  org.springframework.web.service.annotation package 에 있으며

    value, url, accet[] 를 속성으로 가지고 있습니다

     

    7. Proxy (Gateway)

    RouteLocator 와 HttpServiceProxyFactory 를 사용하여 gateway 구현이 가능합니다

    https://github.com/joshlong/bootiful-spring-boot-3/blob/main/client/src/main/java/com/example/client/ClientApplication.java#L35-L68

    @Bean
    RouteLocator gateway(RouteLocatorBuilder b) {
      return b
          .routes()
          .route(rs -> rs
              .path("/proxy")
              .filters(f -> f
                  .setPath("/customers")
                  .retry(10)
                  .addResponseHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*")
    //								.requestRateLimiter(null)
    //								.circuitBreaker(null)
    //								.jsonToGRPC(null)
    //								.tokenRelay()
    
              )
              .uri("http://localhost:8080/")
          )
          .build();
    }
    
    @Bean
    ApplicationRunner applicationRunner(CustomerHttpClient http) {
      return a -> http.customers().subscribe(System.out::println);
    }
    
    @Bean
    CustomerHttpClient customerHttpClient(WebClient.Builder builder) {
      var wc = builder.baseUrl("http://localhost:8080/").build();
      return HttpServiceProxyFactory
          .builder(WebClientAdapter.forClient(wc))
          .build()
          .createClient(CustomerHttpClient.class);
    }

     

    아래 url 을 실행하여 client -> service 로 넘겨서 처리하는 것을 확인할 수 있습니다

    http://localhost:9999/proxy

     

     

    8. GrarphQL

    Spring 에서 GraphQL 을 쉽게 사용 가능합니다

     

    schema.graphqls

    https://github.com/joshlong/bootiful-spring-boot-3/blob/main/client/src/main/resources/graphql/schema.graphqls

    type Query {
    
     customers: [Customer]
     customersByName(name:String): [Customer]
    
    }
    
    type Profile {
     id: ID
    }
    
    type Customer {
     id: ID
     name: String
     profile : Profile
    }

     

    controller

    https://github.com/joshlong/bootiful-spring-boot-3/blob/main/client/src/main/java/com/example/client/ClientApplication.java#L73-L102

    @Controller
    class CustomerGraphqlController {
    
    	private final CustomerHttpClient http;
    
    	CustomerGraphqlController(CustomerHttpClient http) {
    		this.http = http;
    	}
    
    	@QueryMapping
    	Flux<Customer> customers() {
    		return this.http.customers();
    	}
    
    	@QueryMapping
    	Flux<Customer> customersByName(@Argument String name) {
    		return this.http.customersByName(name);
    	}
    
    	@BatchMapping(typeName = "Customer")
    	Map<Customer, Profile> profile(List<Customer> customer) throws Exception {
    		// calls http profile service
    		var map = new HashMap<Customer, Profile>();
          for (var c : customer) {
              map.put(c, new Profile(c.id()));
          }
    		System.out.println("getting ALL profiles for [" + customer +
    				"]");
    		return map;
    	}
    }

     

    application.properties

    https://github.com/joshlong/bootiful-spring-boot-3/blob/main/client/src/main/resources/application.properties

    server.port=9999
    spring.graphql.graphiql.enabled=true

     

    graphiql 은 post 로 호출해야해서 chrome extention 을 설치해야 데모 시연이 쉬워집니다

    https://chrome.google.com/webstore/detail/graphiql-extension/jhbedfdjpmemmbghfecnaeeiokonjclb/related

     

    크롬 확장팩을 켜고 주소를 입력합니다.

    http://localhost:9999/graphql

     

    Josh 처럼 아래 쿼리로 조회하면 결과를 확인할  수 있습니다

    {
      customers{
        id , name , profile{
          id
        }
      }
    }

     

    참고1 official home : https://spring.io/projects/spring-graphql

    참고2, Spring Boot GraphQL 사용해보기: https://luvstudy.tistory.com/195

     

     

     

     

     

    ETC

    Josh Long 이 Humor 용도로 사용한 사이트 입니다

     

    01. 인도네시아 자바섬에서 오신분, Gabage Collecting 하면서 찍은 사진은 좋아요를 19,000 표 받았습니다

    https://twitter.com/jtannady/status/981547257479778307

     

    트위터에서 즐기는 Jesslyn 🇮🇩

    “I'm from the island of Java, Indonesia. I am the Java Garbage Collector.”

    twitter.com

     

     

    02. graalVm compile 이 너무 오래 걸려서, 그 사이 음악 넣어달라는 issue 를 등록하셨습니다

    https://github.com/oracle/graal/issues/5327

     

    Please play elevator music during the native-image compilation process · Issue #5327 · oracle/graal

    I already hear elevator music in my head while I do these sometimes long-running compilations. I’d just like everybody else to hear it, too. Thank you in advance, and I appreciate your amazing work

    github.com

     

    개발자 한분이, --josh-long-mode 를 만들어 주셨습니다만, PR 은 merge 되지 않았습니다

    https://twitter.com/fniephaus/status/1587098613519458305

     

    03. 가장 전력을 적게 사용하는 언어 1위는 C이고, 5위가 java 입니다 그리고 python 은 OMZ 입니다

    https://thenewstack.io/which-programming-languages-use-the-least-electricity/

     

    Which Programming Languages Use the Least Electricity?

    Can energy usage data tell us anything about the quality of our programming languages? Last year a team of six

    thenewstack.io

     

    04.처음에 Josh Long 의 demo 가 동작하지 않아 찾아본 repo 중 하나입니다

    github : https://github.com/roberfg/bootiful

     

    GitHub - roberfg/bootiful: Spring Boot 3 examples made by Josh Long

    Spring Boot 3 examples made by Josh Long. Contribute to roberfg/bootiful development by creating an account on GitHub.

    github.com

     

     

     

     

    댓글

Designed by Tistory.