JHipster의 JDL로 애플리케이션 생성하기

Divide & Conquer·2024년 10월 28일

JHipster JDL (JHipster Domain Language)은 JHipster 애플리케이션의 도메인 모델을 정의하고 관리하기 위한 도메인 특정 언어(DSL)입니다. JDL을 사용하면 엔터티, 관계, 서비스 및 설정을 코드로 간단하고 직관적으로 표현할 수 있습니다. JHipster는 JDL 파일을 기반으로 애플리케이션의 엔터티 및 CRUD 코드를 자동으로 생성할 수 있습니다.

1. JDL

JDL은 JHipster 전용 도메인 언어로 모든 애플리케이션, 배포, 엔터티와 그 관계를 단일 파일(또는 여러 파일)로 설명할 수 있습니다.

jdl은 일단 직관적인 코드 스타일로 이해하기 쉬울 뿐만 아니라, jhipster 웹사이트의 JDL-Studio나 IDE(Intellij, Eclipse, VSCode) Jhipster Plugin을 사용하면 쉽게 작성할 수 있습니다.

아래 예제는 Product라는 entity를 생성하는 예제로 Entity에 name, price, description이라는 속성을 정의하였습니다.

entity Product {
    name String required
    price BigDecimal required
    description String
}

2.JDL 작성

그 외의 Application, Entity, Field, Enum, Relations 등의 작성 방법은 아래 주소에 확인할 수 있습니다.
JHipster JDL
또한, JDL 샘플을 통해 다양한 jdl 작성방법을 확인 할 수 있습니다.(https://github.com/jhipster/jdl-samples)

JDL 샘플을 git으로 내려받아, 내용을 확인해 봤습니다.

모든 샘플을 한번쯤은 다 돌려보고 싶었지만 reactive-mf.jdl과 microservice-ecommerce-store-k8s.jdl 파일 두개만 우선 사용해 보기로 하였습니다.
reactive-mf.jdl은 Frontend가 microfronts 형태로 생성하는 샘플 같아 선택하게 되었고, microservice-ecommerce-store-k8s.jdl은 제가 생각하고 있는 consul 서비스메시에 k8s 오케스트레이션 배포 환경이 생성되는 샘플 같아 선택하게 되었습니다.

reactive-mf.jdl을 이용한 MSA 서비스 구성

reactive-mf.jdl 원본

application {
  config {
    baseName gateway
    reactive true
    packageName com.dnc.msa.gateway
    applicationType gateway
    authenticationType oauth2
    buildTool gradle
    clientFramework react
    prodDatabaseType postgresql
    serviceDiscoveryType consul
    testFrameworks [cypress]
    microfrontends [blog, store]
  }
}

application {
  config {
    baseName blog
    reactive true
    packageName com.dnc.msa.blog
    applicationType microservice
    authenticationType oauth2
    buildTool gradle
    clientFramework react
    databaseType mongodb
    enableHibernateCache false
    serverPort 8081
    serviceDiscoveryType consul
    testFrameworks [cypress]
  }
  entities Blog, Post, Tag
}

application {
  config {
    baseName store
    reactive true
    packageName com.dnc.msa.store
    applicationType microservice
    authenticationType oauth2
    buildTool gradle
    clientFramework react
    databaseType mongodb
    enableHibernateCache false
    serverPort 8082
    serviceDiscoveryType consul
    testFrameworks [cypress]
  }
  entities Product
}

entity Blog {
  name String required minlength(3)
  handle String required minlength(2)
}

entity Post {
  title String required
  content TextBlob required
  date Instant required
}

entity Tag {
  name String required minlength(2)
}

entity Product {
  title String required
  price BigDecimal required min(0)
  image ImageBlob
}

relationship ManyToOne {
  Blog{user(login)} to User with builtInEntity
  Post{blog(name)} to Blog
}

relationship ManyToMany {
  Post{tag(name)} to Tag{post}
}

paginate Post, Tag with infinite-scroll
paginate Product with pagination

deployment {
  deploymentType docker-compose
  serviceDiscoveryType consul
  appsFolders [gateway, blog, store]
  dockerRepositoryName "mraible"
}

deployment {
  deploymentType kubernetes
  appsFolders [gateway, blog, store]
  clusteredDbApps [store]
  kubernetesNamespace demo
  kubernetesUseDynamicStorage true
  kubernetesStorageClassName ""
  serviceDiscoveryType consul
  dockerRepositoryName "mraible"
}

두개(store, blog)의 마이크로 서비스와 하나의 gateway 서비스로 구성된 MSA시스템이며, docker-compose와 kubernetes 환경 배포 스크립트도 생성되도록 구성된 jdl파일입니다.

수정사항

blog application 설정에 보면 databaseType으로 neo4j가 설정되어 있는데 Graph Database의 일종으로 알고만 있지 실제 사용해 본 경험해 본적이 없어, store와 같이 mongodb로 변경하였습니다.

애플리케이션 생성

store-blog라는 폴더를 생성하고, jhipster명령어를 입력하여 애플리케이션 생성을 시작합니다.

중간중간 뻘건 글씨가 콘솔에 찍혀서 신경이 쓰이지만, 쥐뿔로 모르기 때문에 일단 못 본 척하고 넘어갑니다...

몇가지 warning과 eror로 보이는 메세지가 콘솔창에 노출되었지만 설치가 완료됐다는 메세지가 나왔습니다.

애플리케이션 실행하기

IDE에서 프로젝트 열기

프로젝트 생성이 완료되면 IDE로 해당 디록토리에 접근합니다.

blog, docker-compose, gateway, kubernetes, store 폴더와 .yo-rc.json 파일이 생성된걸 확인 할 수 있습니다.

Gateway 애플리케이션 실행

프로젝트에 포함된 애플리케이션들을 하나 하나 실행하여, 문제가 없는지 확인해 봅니다.
우선 gateway 애플리케이션부터 실행합니다.
VSCode의 Spring Boot Dashboard에서 APPS 항목 중 gateway를 debug모드로 실행시킵니다.

역시나.... 뭐... 한번에 통과된 적은 없었닷!

오류를 보면 liquibase 설정 클래스에서 나는 오류로 보이고, String값이 있어야 하는데 null이라서 나는 오류로 보입니다.
해당 클래스에 중단점을 찍고, debug 모드로 실행하여 원인을 확인해 봅니다.

jdbc 설정이 null값으로 들어오네요.

application.yml을 파일을 확인해 보니 jdbc 설정이 없고, application-dev.yml 파일에만 jdbc 설정이 들어있습니다.
spring boot dashbaord에서 애플리케이션 실행시 spring.profiles.active 옵션에 dev를 추가하도록 합니다.

.vscode/launch.json 파일에 "--spring.profiles.active=dev"를 추가합니다.

{
    "configurations": [
        {
            "type": "java",
            ...,
            "args": "--spring.profiles.active=dev",
            ...
        }
    ]
}

설정을 추가하고 애플리케이션을 실행 시키니, GatewayApp이 started 됐다는 성공 메세지가 찍히네요.

하지만 위에 보면 "javax.management.mbeanserver : Exception calling isInstanceOf" 에러가 나면서, 에러로그가 찍힌걸 확인할 수 있습니다.
좀 더 자세한 에러 로그는 아래와 같습니다.

java.lang.ClassNotFoundException: org/springframework/context/support/LiveBeansView
        at java.base/java.lang.Class.forName0(Native Method)
        at java.base/java.lang.Class.forName(Class.java:467)
        at java.management/com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.isInstanceOf(DefaultMBeanServerInterceptor.java:1399)
        at java.management/com.sun.jmx.mbeanserver.JmxMBeanServer.isInstanceOf(JmxMBeanServer.java:1092)
        at java.management/javax.management.InstanceOfQueryExp.apply(InstanceOfQueryExp.java:107)
        at java.management/javax.management.OrQueryExp.apply(OrQueryExp.java:97)
        at java.management/com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.objectNamesFromFilteredNamedObjects(DefaultMBeanServerInterceptor.java:1501)
        at java.management/com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.queryNamesImpl(DefaultMBeanServerInterceptor.java:562)
        at java.management/com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.queryNames(DefaultMBeanServerInterceptor.java:552)
        at java.management/com.sun.jmx.mbeanserver.JmxMBeanServer.queryNames(JmxMBeanServer.java:620)
        at java.management.rmi/javax.management.remote.rmi.RMIConnectionImpl.doOperation(RMIConnectionImpl.java:1491)
        at java.management.rmi/javax.management.remote.rmi.RMIConnectionImpl$PrivilegedOperation.run(RMIConnectionImpl.java:1310)
        at java.management.rmi/javax.management.remote.rmi.RMIConnectionImpl.doPrivilegedOperation(RMIConnectionImpl.java:1405)
        at java.management.rmi/javax.management.remote.rmi.RMIConnectionImpl.queryNames(RMIConnectionImpl.java:572)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:568)
        at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360)
        at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
        at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
        at java.base/java.security.AccessController.doPrivileged(AccessController.java:712)
        at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
        at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:587)
        at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828)
        at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:705)
        at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
        at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:704)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
        at java.base/java.lang.Thread.run(Thread.java:840)
java.lang.ClassNotFoundException: org/springframework/boot/actuate/endpoint/jmx/DataEndpointMBean
        at java.base/java.lang.Class.forName0(Native Method)
        at java.base/java.lang.Class.forName(Class.java:467)
        at java.management/com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.isInstanceOf(DefaultMBeanServerInterceptor.java:1399)
        at java.management/com.sun.jmx.mbeanserver.JmxMBeanServer.isInstanceOf(JmxMBeanServer.java:1092)
        at java.management/javax.management.InstanceOfQueryExp.apply(InstanceOfQueryExp.java:107)
        at java.management/javax.management.OrQueryExp.apply(OrQueryExp.java:97)
        at java.management/javax.management.OrQueryExp.apply(OrQueryExp.java:97)
        at java.management/com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.objectNamesFromFilteredNamedObjects(DefaultMBeanServerInterceptor.java:1501)
        at java.management/com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.queryNamesImpl(DefaultMBeanServerInterceptor.java:562)
        at java.management/com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.queryNames(DefaultMBeanServerInterceptor.java:552)
        at java.management/com.sun.jmx.mbeanserver.JmxMBeanServer.queryNames(JmxMBeanServer.java:620)
        at java.management.rmi/javax.management.remote.rmi.RMIConnectionImpl.doOperation(RMIConnectionImpl.java:1491)
        at java.management.rmi/javax.management.remote.rmi.RMIConnectionImpl$PrivilegedOperation.run(RMIConnectionImpl.java:1310)
        at java.management.rmi/javax.management.remote.rmi.RMIConnectionImpl.doPrivilegedOperation(RMIConnectionImpl.java:1405)
        at java.management.rmi/javax.management.remote.rmi.RMIConnectionImpl.queryNames(RMIConnectionImpl.java:572)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:568)
        at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360)
        at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
        at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
        at java.base/java.security.AccessController.doPrivileged(AccessController.java:712)
        at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
        at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:587)
        at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828)
        at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:705)
        at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
        at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:704)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
        at java.base/java.lang.Thread.run(Thread.java:840)

"java.lang.ClassNotFoundException: org/springframework/boot/actuate/endpoint/jmx/DataEndpointMBean"로 인터넷에서 에러 해결 방법을 찾아봤지만, 딱히 좋은 방법은 없었습니다.
실행되는데는 문제가 없지만, 저 로그가 계속해서 콘솔에 찍히니 아래 링크와 같이 해당 로그가 찍히지 않도록 로그 레벨을 높였습니다.
https://daylersalazar.wordpress.com/2022/07/03/java-lang-classnotfoundexception-endpointmbean-dataendpointmbean-livebeansview-en-jhipster/

로그 레벨을 통해 로그가 출력되지 않도록 수정

logging:
  level:
    ROOT: DEBUG
    tech.jhipster: DEBUG
    org.hibernate.SQL: DEBUG
    com.okta.developer.blog: DEBUG
    javax.management: WARN

로그 설정 추가 후 다시 애플리케이션을 실행하면 아래와 같이 에러 로그 없이 정상적으로 실행되는것을 확인 할 수 있습니다.

store와 blog도 같은 방법으로 설정을 수정하고 실행해 봅니다.

마이크로 서비스 애플리케이션 실행

우선, gateway 애플리케이션처럼 blog, store 애플리케이션의 profile과 log level을 수정하여 애플리케이션을 실행시켰습니다.
blog는 잘 실행되었으나, store는 아래와 같이 에러가 발생했습니다.

오류 로그를 보면
"Error response from daemon: driver failed programming external connectivity on endpoint store-mongodb-1 (bbaa7488db7d07945c84df41e50afa706bba6a3217ba7037ae5bcb55c6d9cb70): Bind for 127.0.0.1:27017 failed: port is already allocated"

127.0.0.1:27017 포트를 이미 사용 중이라는 오류입니다. 27017포트는 mongodb 사용 포트인데요.
blog와 store 애플리케이션 모두 mongodb를 사용하도록 설정하였는데, 이 두 애플리케이션이 사용하는 각각의 mongodb가 도커 컨테이너로 설치되면서 로컬 PC와의 포트 바인딩을 동일한 포트로 설정하고 있어 발생하는 오류입니다.

해결 방법은 store 애플리케이션에서 생성하려는 mongodb의 포트 바인딩을 27018로 변환하여 포트 설정을 변경하면 됩니다.
./src/main/docker/mongodb.yml 수정

./src/main/resources/config/application-dev.yml 수정

jdl 파일에 포함되어 있는 3개의 서비스를 모두 실행하였습니다.
이제 consul ui에서 해당 서비스들의 상태가 모니터링 되는지 확인 보도록 하겠습니다.

애플리케이션 모니터링

http://127.0.0.1:8500 consul 클라이언트에 접속하느 3개의 서비스가 모두 빨간 불이 들어와 있네요.


각각의 서비스를 눌러보면 접속이 되지 않는다는 걸 알 수 있습니다.

진짜 각각의 서비스가 접속이 안되는지 일일이 서비스별 url에 접속해 보면 정상적으로 서비스는 실행되는걸 알 수 있습니다.
그리고, store 애플리케이션 url에 접속하면, jhipster 가이드 페이지가 보이는데 게이트웨이 주소로 접근하라는 메세지 메시지가 보입니다.

그러나, 정작 게이트웨이 url로 접근하면 index.html 파일이 없다는 에러 json 메시지가 브라우저에 노출되네요.

그래서 gateway폴더 하위에서 npm start를 실행시켜 보았습니다.
npm start를 실행하면, 아래와 같이 접속 url이 노출되면서

해당 클라이언트가 웹브라우저로 자동 로딩됩니다.

하지만, 뭐라고 뭐라고 하면서 또 에러 화면이 노출되네요....
징글맞다.....

오류메시지를 구글링해보고 sass 버전을 내려보기로 했습니다.

# sass 삭제
npm uninstall sass
# sass 버전 지정하여 install
npm install -D  sass@1.32.13

그리고 다시 npm start를 재실행했습니다.

어드민 ui는 첫페이지는 정상적으로 열리는걸 확인 했습니다만.....
메뉴를 눌러보니 또 에러가 나네요....

3. 결론

맨 처음부터 너무 설정이 복잡한 애플리케이션 jdl을 선택했나 봅니다. microfronts 설정때문인지 뭔진 모르겠으나, 일단 여기까지만 진행해 보고 다른 jdl을 선택해서 그 녀석도 똑같은 상황이 재현되면 그때 하나하나 따져보며 consul 접속 오류 및 클라이언트의 오류의 원인과 해결 방법을 찾아봐야겠습니다.

profile
IT의 어려운 난제를 작은 단위로 분할하여 정복해 나간다.

0개의 댓글