예제: Flashcard
예제 앱은 spring-jdbc-book의 ch07-jdbc-plus와 같은 기능을 Spring Boot 없이 만든다: 덱, 카드, 태그, 간격 반복 학습, 스마트 덱, CSV 임포트와 익스포트, 통계다.
- DB
-
리포지토리는
NamedParameterJdbcTemplate을 직접 사용한다. 실행되는 SQL이 코드에 그대로 보인다. insert는 예외다:SimpleJdbcInsert가 테이블 메타데이터를 읽고 생성된 키를 반환하며,SimplePropertySqlParameterSource가 도메인 레코드에서 값을 바로 가져온다.SmartDeckRepository는 여전히 파라미터를 하나하나 적는데, 이 리포지토리의SmartConditionenum이 enum이 아니라name()으로 드라이버에 도달해야 하기 때문이다. 행 매핑은DataClassRowMapper를 거친다.DataClassRowMapper와SimplePropertySqlParameterSource가 네이티브 이미지에서 필요로 하는 리플렉션 설정은 빌드가 도메인 패키지에서 생성한다.FlashcardDatabase는 H2DataSource를 만들고, 내주기 전에schema.sql을 적용한다.file()은 실행되는 앱에 쓰이고,inMemory()는 테스트마다 따로 쓸 데이터베이스를 준다. - DI
-
컨테이너는 없다.
FlashcardContext가 생성자를 직접 호출하여 모든 것을 연결하며, Spring의ApplicationContext역할을 손으로 대신한다. - 트랜잭션
-
AOP 대신 서비스가
TransactionTemplate을 얇게 감싼Transactions.write()/read()를 호출한다. 감싼 블록이 정확히 트랜잭션 범위다. - JSON API
-
ch07-jdbc-plus의 기능에 더해, 프레임워크의 REST 지원을 보여 주기 위해 JSON API(
/api/decks,/api/decks/{id}/cards)가 같은 서비스 계층 위에 얹혀 있다. 그 와이어 포맷은flashcard.web.Codecs에 손으로 작성한JsonWriter/JsonReader람다와 생성되는 코덱 둘로 들어 있다: 본문 레코드NewDeck은@JsonBound를 지니고, 도메인 레코드DeckSummary는 같은 클래스의 믹스인을 통해 바인딩된다. - 인트로스펙션
-
/_routes는 모든 라우트를 나열하고,/openapi.json은 같은 목록을 OpenAPI 3.1 문서로 낸다. 둘 다 라우팅 테이블을 데이터로 노출하는app.routes()로 만든다. 문서는spider-silk-openapi를 거치고, 무엇을 다룰지는 앱이 정한다: 여기서는/api라우트인데, 나머지는 모두 HTML로 응답하기 때문이다. 그 라우트들은 설명을 붙여 등록한 유일한 라우트이기도 하며,/_routes는 설명을 별도 열에 보여 주고/openapi.json은 각 오퍼레이션의summary에 담는다. - 모든 응답
-
FlashcardContext.createApp은 실제 애플리케이션처럼 응답 전반의 관심사 세 가지를 모두 설정한다.securityHeaders(…)는 기본값에 이 앱이 돌아갈 수 있는 정책을 더해 받는다:default-src 'self'가 앱이 로드하는 모든 것을 포괄하고,style-src는 인라인도 허용하는데stats.jte가 차트 막대의 크기를 style 속성으로 정하기 때문이며, 이는 스타일시트로는 할 수 없는 일이다. HSTS는 의도적으로 뺐는데,main이 앱을http://localhost로 제공하기 때문이다.gzip()은 페이지, 스타일시트, JSON, CSV 익스포트를 똑같이 포괄한다.cors(…)는/openapi.json에만 한정되고, 덱을 생성하는/api/decks에는 적용되지 않는다. 그 문서는 다른 곳에서 제공되는 Swagger UI처럼 이 애플리케이션이 아닌 클라이언트가 읽도록 만든 유일한 리소스다. - 로깅
-
애플리케이션은 slf4j API로 로깅하며, 백엔드는 Logback이고 설정은
src/main/resources/logback.xml이다.FlashcardContext.createApp은 요청마다 한 줄을flashcard.access로거에 기록하는 요청 로거를 등록한다. Jetty의 로그와 프레임워크의 서블릿 로그는 같은 설정을 따르고, spring-jdbc의 로그도 그러한데, spring-jdbc의 commons-logging이 클래스패스에서 slf4j를 찾기 때문이다. 테스트는logback-test.xml로 돌며, 이 파일은 콘솔에 경고와 에러만 남긴다. 로깅이 같은 설정을 Log4j와 JDK 로깅으로 보여 준다. - 라우팅
-
Controller인터페이스는 없다.FlashcardContext.registerRoutes가 테이블 전체이고, 핸들러는Action클래스, public 메서드 참조, 또는 람다로 도착한다.
실행
gradle :example-flashcard:run # precompiled templates
gradle :example-flashcard:run --args=--dev # hot-reloads edited .jte files
# http://localhost:8080
두 명령은 jte의 프로덕션 모드와 개발 모드다: 첫 번째는 빌드가 컴파일한 템플릿 클래스를 렌더링하고, 두 번째는 파일이 바뀐 템플릿을 다음 렌더링 때 다시 컴파일한다.
FlashcardApp은 --dev를 FlashcardContext의 devMode 플래그로 넘기고, FlashcardContext는 그 플래그로 TemplateRenderer를 고른다.
데이터베이스는 FlashcardDatabase.file()이 여는 H2 파일(~/db/spider-silk/flashcard)이므로 데이터가 재시작 후에도 남는다.
gradle :example-flashcard:nativeCompile은 같은 앱을 GraalVM 네이티브 이미지로 빌드한다.