Skip to content

Releases: xsko/ElastiCORE

ElastiCORE 3.0.261022-rc1

Pre-release

Choose a tag to compare

@github-actions github-actions released this 22 Oct 07:38
bef6a5a

같은 요구사항(도서·회원·대출 API)을 에이전트가 ElastiCORE 로 만들게 하고 어디서 Java 를 손으로 쓰는지 보았다. 네 군데였다:
서버가 정하는 값(반납일)을 클라이언트가 보내도 막을 수 없었고, "아직 반납하지 않은" 검색을 DSL 로 쓸 수 없었고,
ISBN 13자리 같은 정규식 검사는 @pattern 이 컴파일되지 않는 코드를 냈고, member: Member! 는 키(memberId)가 아니라
중첩 객체를 요구했다. 아래 네 항목이 그 자리를 DSL 로 메운다. 네 항목 모두 그 문법을 쓰지 않는 프로젝트의 생성 결과는
바뀌지 않고, 2.x 생성기와 2.x 와 같은 출력을 내는 jpa-springboot 번들은 그대로다(elcoreRulesCheck -Pstrict 150 MATCH /
0 DIFF / 2 ACCEPTED, elcoreTreeCheck 0 differ / 0 missing / 0 extra).

New Features (DSL - 요구사항을 손코딩 없이)

필드 @readonly 는 서버가 정하는 값 - v3 매퍼가 DTO 에서 엔티티로 쓰지 않는다

  • 필드 @readonly(별칭 @noinput)는 지금까지 DTO 에 @JsonProperty(access=READ_ONLY) 만 붙였다. JSON 요청의 값은 Jackson
    이 버렸지만, v3 매퍼는 서버 코드가 DTO 에 넣은 값(dto.setX(..) 후 service.save(dto))을 그대로 엔티티에 썼고, ! 가
    붙은 @readonly 필드는 DTO 의 @NotNull 이 남아 그 값을 보낼 수 없는 생성 요청을 모두 400 으로 거절했다. DDD 의 나뉜
    루트에서는 매퍼가 요청 값을 to.creation()::setX 로 새 루트에 넣었다
  • 이제 v3 번들(jpa-springboot-v3·DDD·헥사고날·Kotlin)에서 @readonly/@noinput 필드는
    • 매퍼의 DTO -> 엔티티 방향(toEntity, mapping(dto, entity, ...), EntityManager 판 포함)이 쓰지 않는다. 엔티티 -> DTO
      방향은 그대로라 응답에 나간다. 엔티티 필드에 붙이면 그 엔티티의 모든 DTO 에, dto.yml 의 DTO 필드에 붙이면 그 DTO 의
      매핑에만 적용된다. @id 에 붙은 @readonly 는 식별자 쓰기를 그대로 둔다
    • DTO 의 @NotNull·@NotBlank 를 뺀다. 엔티티 컬럼(nullable = false)은 그대로라 서버가 채운다
    • DTO 의 @Schema 에 accessMode = Schema.AccessMode.READ_ONLY 를 더한다. springdoc 은 @JsonProperty 만으로도
      readOnly: true 를 냈다 - @Schema 만 읽는 도구를 위해 적는다
    • 나뉜 루트에서는 여전히 잠긴 필드다: protected setter 와 creation() setter 가 남아 서버 코드가 새 루트에 넣는다
  • v3 동작 변경: 필드에 @readonly 를 붙이고 서버 코드에서 DTO 에 값을 넣어 저장하던 코드는 그 값이 더는 저장되지 않는다.
    엔티티에 넣는다(확장 서비스의 beforeSave, 루트의 메서드). HTTP 동작은 같다 - JSON 의 값은 이전에도 버려졌다. 이유:
    @readonly 는 "요청으로 받지 않는다" 는 선언인데 매퍼가 그 선언을 무시해, 서비스가 creation().setX(null) 같은 우회를
    쓰고 있었다(새 루트에서는 아무 일도 하지 않는다). @readonly 가 없는 프로젝트의 v3 출력은 바뀌지 않는다
  • 가이드: annotation guide 에 @readonly / @noinput 절(생성물 표, @behavior 와의 비교, creation().setX(null) 은 새 루트에서
    아무 일도 하지 않는다는 것), §2.8 감사 필드, jsonview 표, @behavior 는 나뉜 루트에서만 효과가 있다는 것. dsl guide 필드 표와
    @behavior 행, rule guide 의 잠긴 루트 표와 '새 루트의 null'
  • 확인: 규칙 단위 테스트(Java·Kotlin 매퍼, DTO, 2.x 호환 출력, 나뉜 루트, @editable 루트), hexagonal Java·Kotlin 도서관 모델을
    RulesGenRunner 로 생성해 매퍼·DTO·LoanBase 를 읽는 테스트. elasticore-template-ddd 의 catalog Book 에 shelfCode: string(10)! @readonly 와 그 값을 채우는 beforeSave 를 임시로 넣고 Spring Boot 3.3.2 에서 실행해 확인했다: 클라이언트가
    값을 보낸 POST·PUT 은 200 이고 beforeSave 의 값이 남는다. service.save(dto) 도 DTO 에 넣은 값 대신 beforeSave 의 값을
    저장한다. /v3/api-docs 는 readOnly: true 이다. 변경 전 출력에서는 POST 가 400(shelfCode: must not be null)이었고,
    DTO 에 넣은 값이 저장되었다
  • 남은 것: DTO 의 @ref(...) 필드에 붙인 @readonly 는 아직 관계 쓰기(reverseRefPass)를 막지 않는다. 나뉜 루트의 필드
    @behavior 는 @JsonProperty READ_ONLY 만 붙고 @Schema accessMode 는 붙지 않는다
  • 관련 파일: WriteGuardRules.java, DtoBaseRules.java, MapperBaseRules.java, ServerSetFieldTest.java, SealedRootTest.java,
    ServerSetFieldGenerationTest.java

@search(isnull) - 값이 비어 있는지로 검색한다

  • "아직 반납하지 않은 대출", "아직 입고된 적 없는 상품"처럼 값이 비어 있는지로 찾으려면 Service 에
    Q.Loan.returnedDate(Op.IS_NULL, null) 를 손으로 쓰거나 Repository 에 JPQL 을 따로 두어야 했다. @search(isnull) 은
    2.x 와 규칙 엔진 모두에서 경고 없이 같은 값 비교(c.equal)로 만들어졌고, 가이드는 ISNULL/ISNOTNULL 을 미구현으로
    적고 있었다
  • 이제 returnedDate: date @search(isnull) 은 검색 DTO 에 Boolean returnedDateEmpty 하나를 만든다. 보내지 않거나
    null 이면 조건 없음, true 면 c.isNull(r.get("returnedDate")), false 면 c.isNotNull(...). @Schema 설명은
    ... Field is empty (true) or set (false). field:returnedDate 이고, 보내는 값이 날짜가 아니므로 @DateTimeFormat·
    @JsonFormat·example 은 붙지 않는다. @ref(order.shippedDate) 와 쓰면 r.get("order").get("shippedDate") 를
    검사하고, 쉼표 뒤 두 번째 값은 다른 조건처럼 엔티티 필드 이름이다(@search(isnull,open) -> openEmpty, r.get("open"))
  • Kotlin(val xEmpty = searchDTO.xEmpty, if(xEmpty!!), Specification<E> { r,q,c -> c.isNull(...) })과
    TypeScript(xEmpty?: boolean)도 같은 규칙에서 나온다
  • 필드 하나에 조건 하나: isnull 을 적은 필드는 between 의 From/To 를 만들지 않는다. @expose 의 타입별 자동
    할당(날짜 -> between)은 바뀌지 않는다
  • v3 번들(jpa-springboot-v3·DDD·헥사고날·Kotlin·ts-model) 전용이다. 2.x 생성기와 2.x 호환 번들(jpa-springboot)은
    이전처럼 같은 값 비교를 만들고 @Schema 문구도 그대로다 - StringUtils.getOperatorDescription(op) 는 바뀌지 않고 새
    문구는 getOperatorDescription(op, true) 에만 있다
  • v3 번들에서 이미 @search(isnull) 을 적어 둔 필드는 검색 속성이 <필드>(필드 타입, 같은 값 비교)에서
    <필드>Empty(Boolean)로 바뀐다. 미구현으로 문서화된 키워드가 조용히 같은 값 비교가 되던 것을 바로잡는 것이며,
    저장소의 블루프린트 중 쓰는 곳은 없다
  • 확인: 단위 테스트로 Java·Kotlin·TypeScript 출력과 2.x 호환 출력이 그대로인 것을 확인했다. elasticore-template-ddd
    의 Product.lastRestocked 에 잠시 @search(isnull) 을 붙여 생성·컴파일하고, H2 위에서
    POST /api/v1/shop/product/search 가 true 는 입고일이 없는 상품만, false 는 있는 상품만, 생략과 null 은 둘 다
    돌려주는 것을 확인했다. elasticore-template-kt 의 Project.endDate 로 Kotlin 을 생성해 compileKotlin 이 통과하고
    TypeScript 에 endDateEmpty?: boolean; 이 나오는 것을 확인했다(샘플 변경은 되돌렸다)
  • 남은 것: List 필드나 ! 필드에 isnull 을 적어도 경고하지 않는다. v3 의 별칭 구분자는 쉼표인데 가이드 §3.1 은 $ 로
    적고 있다(별개 버그)
  • 관련 파일: SearchDtoBaseRules.java, MapperSpecRules.java, StringUtils.java, SearchDtoTest.java,
    MapperSpecTest.java, TypeScriptModelTest.java, OperatorDescriptionTest.java,
    elasticore-annotation-guide.md(3.2, 3.4.1, 3.8, 8.5), elasticore-dsl-guide.md, elasticore-dev-guide.md

Bug Fixes (생성기)

@pattern 이 컴파일되는 @Pattern 을 쓰고, @min 처럼 값 다음에 메시지를 받는다

  • 규칙 엔진의 v3·Kotlin 번들에서 @pattern(regexp="...", message="...") 이 컴파일되지 않았다. 네 줄짜리
    @Pattern( regexp="...", msg="..." ) 를 썼는데 Jakarta @Pattern 에는 msg 속성이 없다. 이제 한 줄
    @Pattern(regexp = "...", message = "...") 을 쓴다
  • 위치 형식 @pattern('^[0-9]{13}$', 'ISBN must be 13 digits'), @pattern('^[A-Z]+$') 도 읽는다. 이전에는 아무것도
    생성하지 않았다. 메시지는 따옴표와 괄호 밖의 첫 쉼표 뒤라서 {10,13} 은 정규식에 남는다. 메시지를 생략하면 TypeScript
    클라이언트와 같은 "<라벨> is not in the expected form" 을 쓴다. TS 의 validate...() 도 같은 값을 읽는다.
    이전에 아무 검사도 없던 필드(위치 형식, 메시지 없는 이름 형식)에 요청 검사(400)가 새로 생긴다
  • Kotlin 은 문자열 템플릿이 되는 $(영문자·_·{ 앞)를 \$ 로 쓴다. @Pattern 은 문자열 필드에만 붙는다. 날짜의
    @pattern(yyyyMMdd) 는 이전처럼 날짜 형식이다. clean 출력은 문자열 필드의 @pattern 을 Swagger example 로 쓰지 않는다
    (위치 형식에서는 ^[0-9]{13}$', 'ISBN must be 13 digits 같은 깨진 값이 나왔다)
  • 정규식은 Java 문자열 리터럴의 내용으로 그대로 들어가므로 백슬래시는 \\d 처럼 두 번 쓴다. \d 처럼 하나만 쓰면
    규칙 엔진이 DSL 경고를 낸다(2.x 추출기는 경고에 실패하므로 규칙 엔진에서만)
  • 2.x 생성기(elcore)와 2.x 호환 번들(jpa-springboot)의 출력은 그대로다(msg=, 이름 형식에 메시지가 있을 때만) -
    2.x 의 @pattern 은 여전히 컴파일되지 않는 코드를 내며, 2.x 호환을 위해 손대지 않았다
  • 확인: 도서관 모델(isbn: string(13)! @id @pattern('^[0-9]{13}$', 'ISBN must be 13 digits'))을 layered·Kotlin·TS 로
    생성했다. Java DTO 는 javac, Kotlin DTO 는 kotlinc 1.9.24 로 컴파일했고, Hibernate Validator 8 이 12345 를
    "ISBN must be 13 digits" 로 거부하는 것을 확인했다. TS 의 validateBook() 이 node 에서 같은 메시지를 내는 것도 확인했다
  • 남은 것: = 가 든 정규식은 이름 형식으로만 쓸 수 있다. 따옴표 하나로 감싼 이름 형식의 값은 공백에서 잘린다(기존 파서).
    DDD 루트의 저장 시점 검사(@min 처럼)는 하지 않는다
  • 관련 파일: FieldSpec.java, DtoBaseRules.java, FieldDoc.java, TypeScriptDtoFileRules.java, AbstractModelLoader.java,
    PatternAnnotationTest.java, DtoGapCoverageTest.java, elasticore-annotation-guide.md

필수 관계(post: Post!)는 저장 요청에서 FK id 필드로 요구한다

  • @expose/@service 엔티티가 자동 생성하는 DTO 에서 ! 가 붙은 to-one 관계는 중첩 DTO(PostDTO post)에 @NotNull·
    requiredMode=REQUIRED 가 붙고 FK id 필드(postPostId)에는 붙지 않았다. 매퍼는 저장/수정할 때 postPostId 만 읽으므로
    (em.getReference), 키만 보낸 요청은 @Valid 검증에서 거절되고 중첩 객체만 보낸 요청은 검증을 통과해도 관계를 연결하지
    못했다. 요청은 둘 다 보내야 했다
  • 이제 v3·Kotlin·TypeScript 생성(jpa-springboot-v3·DDD·헥사고날·Kotlin·TypeScript 번들)에서는 FK id 필드가 @NotNull·
    requiredMode=REQUIRED 이고, 응답용 중첩 DTO(accessMode = READ_ONLY)는 선택이다. TypeScript 는 post?: PostDTO;·
    postPostId: number; 로 선언하고 validateXxx 가 post 를 요구하지 않는다. ddd-reference 를 쓰는 번들은 ID 필드(memberId)가
    이미 필수였고, 중첩 DTO 만 선택이 된다. 키 이름 규칙(필드 + 키, 키가 id 면 topicId)은 그대로다
  • 동작 변경 (v3·Kotlin·TypeScript 사용자): 중첩 post 만 보내던 클라이언트는 이제 @Valid 검증에서 거절된다 - FK id 필드를
    보내야 한다(그 요청은 이전에도 관계를 연결하지 못했다). postPostId 를 함께 보내던 클라이언트는 그대로 동작한다. OpenAPI 의
    required 가 post 에서 postPostId 로 옮겨 간다
  • 바뀌지 않는 것: 2.x 생성기(elcore)와 2.x 와 같은 출력을 내는 jpa-springboot 번들(중첩 DTO 에 @NotNull 그대로), 검색 DTO
    (XxxSrchDTO 의 postPostId 는 검색 조건이라 필수가 아니다), Entity(@JoinColumn(nullable = false)), 매퍼, dto.yml 에 직접
    선언한 DTO. ! 가 붙은 엔티티 참조가 없는 프로젝트의 생성 결과는 바뀌지 않는다
  • 확인: Post·Comment(post: Post!) 게시판을 v3 layered·Kotlin·TypeScript·호환 모드·2.x 로, Member·Book·Loan
    (member: Member!, book: Book!, Book 의 키 isbn)을 헥사고날로 생성해, v3 CommentDTO 의 postPostId 에 @NotNull·REQUIRED
    가 붙고 post 에는 없는 것, 키가 id 인 topic: Topic! 의 topicId 가 필수인 것, 선택 관계의 postPostId 와 CommentSrchDTO 의
    postPostId 가 그대로인 것, 헥사고날 LoanDTO 의 memberId·bookIsbn 이 필수이고 member·book 이 선택인 것, 호환 모드 출력이
    2.x 와 같은 것을 테스트로 확인했다. DTO 와 검색 DTO 가 함께 쓰는 키 필드는 복사본에만 @notnull 을 붙여, 어느 DTO 를 먼저
    만들어도 검색 DTO 가 바뀌지 않는 것도 확인했다
  • 남은 것: TypeScript 의 XxxFields 는 FK id 필드를 여전히 readOnly: true 로 표시하고 validateXxx 가 검사하지 않는다
  • 관련 파일: SpecFactory.java, RulesGenRunner.java, RulesVerifyRunner.java, RequiredReferenceDtoTest.java,
    elasticore-annotation-guide.md, elasticore-dsl-guide.md, elasticore-rule-guide.md

Improvements (런타임 - 손으로 쓴 규칙의 409)

ConflictException - 손으로 쓴 규칙이 409 를 자기 메시지로 낸다

  • "빌려줄 권수가 없다", "이미 반납한 대출이다" 같은 규칙을 beforeSave 나 루트 메서드에 쓸 때 409 로 답할 예외가 없었다.
    DataIntegrityViolationException 을 던지면 409 는 나가지만 detail 이 데이터베이스 제약 문구("A unique value is already
    taken, or the row is still referenced by another.")로 바뀌어 이유를 알 수 없었다
  • io.elasticore.springboot3.validation.ConflictException (BadRequestException 과 같은 자리)을 던지면 생성된
    XxxApiErrors 가 409, code: conflict, 던진 메시지를 detail 로 답한다. Java 와 Kotlin 의 apiErrors 템플릿에 처리기를
    더했다. 2.x 와 jpa-springboot 번들에는 API 오류 처리기가 없으므로 바뀌지 않는다
  • XxxApiErrors.java 는 한 번만 생성되는 파일이라 이미 생성한 프로젝트는 그 처리기 다섯 줄을 손으로 더하거나 파일을 지우고
    다시 생성한다. 저장소의 샘플에는 더해 두었다
  • 확인: 도서 대출 프로젝트의 LoanService.beforeSave 와 반납 메...
Read more