-
Bootiful Spring Boot 3 by Josh Long - demo 해보기Spring/Framework 2023. 6. 24. 14:46
목적
Josh Long 한국 방문 세미나 참석위해, 그의 Youtube video 미리 학습합니다
아래 video 를 보며, 8가지를 학습해 볼 수 있습니다.
- Bootiful Spring Boot 3 demo application 을 실행해 봅니다
- postgres:latest @ServiceConnection annotation 이용하여 bean 으로 container를 띄워봅니다
- ProblemDetail 을 이용한 Error Handling 을 해봅니다
- Observation 을 이용하여 custom metric 을 기록해 봅니다
- buildBootImage 를 이용하여, source -> image 로 만들어 봅니다
- declarative Http interface 를 사용해 봅니다
- RouteLocator 와 HttpServiceProxyFactory 를 사용하여 gateway 를 구현해봅니다
- Spring + GraphQL 을 사용해 봅니다
Bootiful Spring Boot 3 by Josh Long @ Spring I/O 2023
https://www.youtube.com/watch?v=FvDSL3pSKNQ
Josh Long 의 영상을 처음 봤는데, 대략 느낌은 이렇습니다.
First impresstion
- 영어로 들어야 하는데, 말의 속도가 빠르고,
- 유머를 많이 구사하며,
- live coding 하셨는데, 코딩 속도도 빨라서, 눈으로 따라가기 어려웠습니다. github demo code 확인 필요!
- 핵심을 강조하기 보단, 특징을 쭉~ 나열하며 소개합니다.
1~2. demo & testContainer
github : https://github.com/joshlong/bootiful-spring-boot-3
git clone 받고 특이한 점은, TestServiceApplication 을 실행해야 합니다
@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 에서 아래 속성을 사용해야 합니다
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
@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
4. Observability
- micrometer.io ( metrics )
- spring cloud sleuth ( tracing )
2개가 구분되어 있었는데, tracing 관련된 것을 micrometer 로 옮겼다고 합니다.
그래서, 아래처럼 "by-name" 이라는 custom metric 을 생성하면, Distributing chasing header 에도 전파되어 사용할 수 있습니다
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 입니다
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 구현이 가능합니다
@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
type Query { customers: [Customer] customersByName(name:String): [Customer] } type Profile { id: ID } type Customer { id: ID name: String profile : Profile }
controller
@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
server.port=9999 spring.graphql.graphiql.enabled=true
graphiql 은 post 로 호출해야해서 chrome extention 을 설치해야 데모 시연이 쉬워집니다
크롬 확장팩을 켜고 주소를 입력합니다.
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
02. graalVm compile 이 너무 오래 걸려서, 그 사이 음악 넣어달라는 issue 를 등록하셨습니다
https://github.com/oracle/graal/issues/5327
개발자 한분이, --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/
04.처음에 Josh Long 의 demo 가 동작하지 않아 찾아본 repo 중 하나입니다
github : https://github.com/roberfg/bootiful
'Spring > Framework' 카테고리의 다른 글
baeldung - spring boot 3 new (0) 2023.10.11 Annotation 이 설정된 Beans 의 Scan 과 추가 Bean Register 위한 배경 지식 (0) 2023.09.16 Next-Generation Cloud Native Apps with Spring Boot 3 • Thomas Vitale • GOTO 2023 - 세미나 리뷰 ( 작성중 ) (0) 2023.09.05 Spring Boot 3.1.0 (0) 2023.06.26 Spring Boot 3.0 upgrade 분류 (0) 2023.06.12