
모음(moeum) 프로젝트는
"ERD와 DB를 하나로 MOEUM" 이라는 캐치프라이즈처럼,
ERD를 통해 실제 DB를 직접 조작하고 동기화할 수 있으면 편리하지 않을까라는 고민에서 출발했다.
기존 방식에서는 다음과 같은 불편함이 존재한다.
Spring Data JPA의 운영 환경에서 ddl-auto=update를 사용할 경우 다음과 같은 문제점을 내포하고 있어,
마이그레이션 과정에서 entity와 실제 DB 스키마 사이의 불일치가 발생한다.
이러한 한계를 해결하기 위해 moeum은
ERD 기반 스키마 관리 + Migration 이력 관리 + 승인 기반 배포 구조를 제공한다.
User
└── Workspace (환경 구분 / 승인 정책)
└── Project (DB 연결 정보 + ERD JSON)
├── Schema (ERD 설계 → DDL 생성 → DB 반영)
└── Migration (diff 계산 → 승인 → 적용)
모음의 핵심 기능 중 하나는
외부 Spring Boot 서비스의 DB 구조를 자동으로 수집하는 것이다.
하지만 마이크로서비스 환경에서는 각 서비스의 DB를 직접 등록하기 때문에 매우 번거로울 것 같았다.
이를 해결하기 위해
moeum-spring-boot-starter를 통해서 자동으로 연동할 수 있도록 maven에 라이브러리로써,도입하고자한다.
Spring Boot 실행
→ ApplicationReadyEvent 발생
→ DB metadata 수집
→ moeum 서버 등록
즉, 개발자는 별도의 코드 없이 DB를 자동으로 등록할 수 있다.
Starter를 설계할 때 두 가지 방식이 존재한다.
/moeum UI를 앱 내부에 제공한다외부 서비스
→ starter
→ DB metadata 수집
→ POST /api/integrations/register
→ moeum 서버 저장
Starter에는 DB 메타데이터 수집 및 전송 기능만 포함하여 의존성을 최소화하고, 서비스에 불필요한 UI 및 관리 기능이 포함되지 않도록 한다.
ERD 관리, DDL 생성, Migration 승인 및 이력 관리와 같은 기능은 moeum 서버에서 중앙 집중적으로 처리하여 책임을 분리한다.
각 서비스 내부에 관리용 UI를 노출하지 않아도 되므로 운영 환경 보안과 관리 복잡도를 줄일 수 있다.
기능 개선이나 정책 변경이 발생하더라도 중앙 moeum 서버만 업데이트하면 되므로 각 서비스의 Starter를 반복적으로 수정·배포할 필요가 줄어든다.
여러 서비스의 DB 메타데이터를 한 곳에 모아 워크스페이스 단위의 통합 관리와 협업 흐름을 자연스럽게 지원할 수 있다.
내가 여기에서 2를 선택한 가장 큰 이유는 1을 채택하면 멀티서비스로써 실행이 안되기 때문이다.
따라서 moeum은 중앙 서버 + 경량 Starter 구조로 설계하고, Starter는 JitPack을 통해 배포한다.
JitPack은 GitHub 저장소를 Maven 저장소처럼 사용할 수 있도록 해주는 서비스이다.
즉, GitHub repo를 통해서 Maven dependency로 바로 사용 가능하게 해주는 것이 JitPack이다.
기존 Maven Central 배포는:
반면 JitPack은: GitHub에 push → 즉시 배포할 수 있어서 매우 간단하다.
그냥 로컬에서 사용할 땐 단순하게 아래처럼 세팅후 하면 되지만, 실제로는 4처럼 jitpack을 이용해서 라이브러리르 배포해야함
./gradlew publishToMavenLocal
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
implementation 'io.moeum:moeum-spring-boot-starter:1.0.0'
}
repositories {
maven { url 'https://jitpack.io' }
mavenCentral()
}
dependencies {
implementation 'com.github.{GitHub계정}:moeum-spring-boot-starter:1.0.0'
moeum-spring-boot-starter/
├── jitpack.yml ← JitPack 빌드 환경 정의
├── build.gradle ← maven-publish 설정
├── settings.gradle
└── src/main/
├── java/io/moeum/starter/
│ ├── config/MoeumAutoConfiguration.java ← 자동 설정 진입점
│ ├── properties/MoeumProperties.java ← moeum.* 프로퍼티 바인딩
│ ├── service/MoeumRegistrationService.java ← DB 메타데이터 수집 + 전송
│ └── client/
│ ├── MoeumClient.java ← HTTP 클라이언트
│ └── RegisterPayload.java ← 전송 데이터 구조
└── resources/
└── META-INF/spring/
└── org.springframework.boot.autoconfigure.AutoConfiguration.imports
jdk:
- openjdk17
JitPack 기본 JDK는 8/11 → Java17 코드 에러 발생
plugins {
id 'java-library'
id 'maven-publish'
}
publishing {
publications {
mavenJava(MavenPublication) {
from components.java
}
}
}
이게 있어야 Maven 형태로 배포됨
jitpack.io 접속 → Get it 클릭 → 빌드
아래처럼 git에 push후 jitpack사이트에서 리포지토리를 찾은 후 get it을 누르면, 빌드가 되어서,성공하면 Maven처럼 사용 가능


모음의 고객사나 외부 서비스에서 사용시에는 아래처럼 추가후 사용가능.
repositories {
maven { url 'https://jitpack.io' }
}
dependencies {
implementation 'com.github.{계정}:moeum-spring-boot-starter:v1.0.0'
```
moeum:
server-url: http://...
api-key: ...
}
@EventListener(ApplicationReadyEvent.class)
를 통해서 Spring Boot 완전히 뜬 이후 실행될 때까지 대기함 DataSource 연결이 준비되고,DB 접근 가능 상태여야하기 때문임.
앱 실행
→ ApplicationReadyEvent
→ DB 메타데이터 수집
→ payload 생성
→ moeum 서버 전송
JPA Entity가 아니라 아래와 같이 실제 DB 기준으로 수집한다
핵심은 meta.getTables(catalog, schema, "%", new String[]{"TABLE"})이다
내부적으로 MySQL 기준 information_schema.TABLES를 조회하는 것과 동일한 원리로 내부에 어떤 테이블이 있는지를 수집한다.
private List<RegisterPayload.TableInfo> collectTableMetadata() throws Exception {
List<RegisterPayload.TableInfo> tables = new ArrayList<>();
try (Connection conn = dataSource.getConnection()) {
DatabaseMetaData meta = conn.getMetaData();
String catalog = conn.getCatalog();
String schema = conn.getSchema();
try (ResultSet tableRs = meta.getTables(catalog, schema, "%", new String[]{"TABLE"})) {
while (tableRs.next()) {
String tableName = tableRs.getString("TABLE_NAME");
// 시스템 테이블 제외
if (isSystemTable(tableName)) {
continue;
}
Set<String> primaryKeys = getPrimaryKeys(meta, catalog, schema, tableName);
List<RegisterPayload.ColumnInfo> columns = getColumns(meta, catalog, schema, tableName, primaryKeys);
tables.add(RegisterPayload.TableInfo.builder()
.name(tableName)
.columns(columns)
.build());
}
}
}
return tables;
}
private Set<String> getPrimaryKeys(DatabaseMetaData meta, String catalog, String schema, String tableName) {
Set<String> pks = new HashSet<>();
try (ResultSet pkRs = meta.getPrimaryKeys(catalog, schema, tableName)) {
while (pkRs.next()) {
pks.add(pkRs.getString("COLUMN_NAME"));
}
} catch (Exception e) {
log.debug("[Moeum] PK 조회 실패: table={}, error={}", tableName, e.getMessage());
}
return pks;
}
DB마다 다른 타입을 통일된 ERD로 만들기 위해
DB마다 타입이 다르기 때문에 통일한다.
VARCHAR→VARCHAR
TEXT→TEXT
BIGINT→BIGINT
moeum의 서버에 POST /api/integrations/register(X-Api-Key: xxx
)를 통해서 db정보 및 사용자 api를 전송한다.
이를 통해서 사용자 서비스와 moeum의 서비스를 연동한다.
외부 서비스
→ starter
→ DB metadata 수집
→ HTTP 요청
→ moeum 서버 저장