Enterprise Features
1. 개요
ElastiCORE는 단순한 CRUD 자동화를 넘어 대규모 엔터프라이즈 환경에서 요구되는 다양한 기능을 지원합니다. 다중 데이터소스 분리, 도메인별 블루프린트 파일 관리, 커스텀 인터페이스 구현, 감사 로깅, 보안 통합, 캐싱 전략까지 — 복잡한 시스템을 체계적으로 구성할 수 있는 도구를 제공 합니다.
이 문서는 여러 팀이 협업하거나 복수의 데이터베이스/도메인을 다루는 중대형 프로젝트에 특히 유용합니다.
2. 다중 DataSource 설정
env.yml에서 데이터소스 정의
대규모 서비스에서는 업무 도메인별로 데이터베이스를 분리하는 경우가 많습니다. ElastiCORE는 env.yml에서 복수의 데이터소스를 선언하고, 각 포트(Port)에서 사용할 데이터소스를 명시적으로 지정할 수 있습니다.
# src/main/resources/elasticore/env.yml
datasource:
main:
type: jpa
url: jdbc:postgresql://db-main:5432/maindb
username: app_user
password: ${DB_MAIN_PASSWORD}
hikari:
maximum-pool-size: 20
analytics:
type: jpa
url: jdbc:postgresql://db-analytics:5432/analyticsdb
username: analytics_user
password: ${DB_ANALYTICS_PASSWORD}
hikari:
maximum-pool-size: 10
log:
type: jpa
url: jdbc:mysql://db-log:3306/logdb
username: log_user
password: ${DB_LOG_PASSWORD}
운영 환경에서는 비밀번호를 ${ENV_VAR} 형태로 주입하는 것을 강력히 권장합니다. Kubernetes Secret이나 AWS Secrets Manager와 연동하면 보안성을 높일 수 있습니다.
Port DSL에서 데이터소스 지정
포트 정의 시 @datasource("...") 어노테이션으로 어떤 데이터소스를 사용할지 명시합니다.
transaction:
port:
# 메인 DB를 사용하는 사용자 서비스
UserService:
meta: dbms @datasource("main")
methods:
findByEmail:
params:
email: string
return: User
# 분석 DB를 사용하는 통계 서비스
AnalyticsService:
meta: dbms @datasource("analytics")
methods:
getDailyStats:
params:
date: localdate
return: DailyStatsDTO
# 로그 DB에 접근하는 감사 서비스
AuditLogService:
meta: dbms @datasource("log")
methods:
saveAuditLog:
params:
log: AuditLogDTO
return: void
트랜잭션 포트별 데이터소스 분리 예시
ElastiCORE가 생성하는 코드는 데이터소스별로 독립적인 EntityManagerFactory와 트랜잭션 매니저를 구성합니다. 따라서 도메인 경계를 넘는 분산 트랜잭션은 명시적으로 처리해야 합니다.
// 생성된 서비스 코드 예시 (참고용)
@Service
@Transactional("mainTransactionManager")
public class UserServiceImpl implements UserService {
// main 데이터소스 트랜잭션 범위 내에서 동작
}
@Service
@Transactional("analyticsTransactionManager")
public class AnalyticsServiceImpl implements AnalyticsService {
// analytics 데이터소스 트랜잭션 범위 내에서 동작
}
서로 다른 데이터소스에 걸친 단일 트랜잭션이 필요한 경우, JTA(Java Transaction API) 또는 Saga 패턴 적용을 검토하세요. ElastiCORE는 단일 데이터소스 트랜잭션을 자동 관리하지만, 분산 트랜잭션은 애플리케이션 레벨에서 직접 처리해야 합니다.
3. 대규모 모델 관리
도메인별 블루프린트 파일 분리 전략
수십 개 이상의 엔티티를 단일 파일로 관리하면 충돌이 잦고 가독성이 떨어집니다. ElastiCORE는 블루프린트 파일을 여러 개로 분리하는 것을 지원합니다. elasticore/ 디렉토리 안의 모든 .yml 파일을 자동으로 인식합니다.
멀티모듈 프로젝트 구조 예시
project/
├── module-user/
│ └── src/main/resources/elasticore/
│ ├── env.yml
│ └── user-domain.yml # User, Role, Permission 엔티티
├── module-order/
│ └── src/main/resources/elasticore/
│ ├── env.yml
│ └── order-domain.yml # Order, OrderItem, Payment 엔티티
├── module-inventory/
│ └── src/main/resources/elasticore/
│ ├── env.yml
│ └── inventory-domain.yml # Product, Stock, Warehouse 엔티티
└── module-common/
└── src/main/resources/elasticore/
└── common-domain.yml # 공통 Enumeration, 공유 DTO
각 모듈의 build.gradle에 ElastiCORE 플러그인을 독립적으로 적용합니다.
// module-order/build.gradle
plugins {
id 'io.elasticore' version '1.x.x'
}
elasticore {
basePackage = 'com.example.order'
blueprintPath = 'src/main/resources/elasticore'
}
도메인 간 참조 처리
한 모듈의 엔티티가 다른 모듈의 엔티티를 참조해야 할 때는 DTO를 통한 간 접 참조를 권장합니다. 직접적인 JPA 연관관계 대신 ID 기반 참조를 사용하면 모듈 간 결합도를 낮출 수 있습니다.
# module-order/order-domain.yml
dto:
OrderSummaryDTO:
meta: dto
fields:
orderId: long
userId: long # User 엔티티의 ID만 참조 (직접 연관관계 없음)
userEmail: string # 필요한 데이터는 API 호출로 조합
totalAmount: decimal
네임스페이스 분리
모듈별로 기본 패키지를 다르게 설정하여 클래스 충돌을 방지합니다.
# module-user/env.yml
project:
name: user-module
basePackage: com.example.user
generatePath: src/main/java
# module-order/env.yml
project:
name: order-module
basePackage: com.example.order
generatePath: src/main/java
여러 도메인에서 공통으로 사용하는 Enumeration이나 Value Object는 module-common에 정의하고, 다른 모듈이 이를 의존성으로 포함하도록 구성하세요.
4. 커스텀 인터페이스
엔티티에 커스텀 인터페이스 추가
ElastiCORE는 @implements(...) 어노테이션을 통해 생성된 엔티티 클래스가 특정 인터페이스를 구현하도록 지시할 수 있습니다.
entity:
User:
meta: entity @expose(50) @implements(Auditable, Serializable)
fields:
uid: long @id @sequence
email: string(100)! @unique
name: string(50)!
createdAt: localdatetime
updatedAt: localdatetime
Order:
meta: entity @expose(50) @implements(Serializable, Versionable)
fields:
oid: long @id @sequence
userId: long!
status: OrderStatus
version: int @version
생성된 엔티티의 인터페이스 구현
ElastiCORE는 선언된 인터페이스를 implements 절에 추가한 엔티티 클래스를 생성합니다. 인터페이스 자체는 개발자가 직접 정의합니다.
// 개발자가 정의하는 인터페이스
public interface Auditable {
LocalDateTime getCreatedAt();
LocalDateTime getUpdatedAt();
void setCreatedAt(LocalDateTime createdAt);
void setUpdatedAt(LocalDateTime updatedAt);
}
// ElastiCORE가 생성하는 엔티티 (개념적 예시)
@Entity
@Table(name = "ec_user")
public class User implements Auditable, Serializable {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long uid;
@Column(name = "email", length = 100, nullable = false, unique = true)
private String email;
// ... 나머지 필드 및 메서드
}
Auditable 인터페이스를 구현하면 Spring Data의 @EntityListeners(AuditingEntityListener.class)와 함께 자동 감사 필드 채움을 활용할 수 있습니다. Serializable은 Redis 캐시나 세션 직렬화에 필요합니다.
5. 감사 로깅 (Audit)
@audited 어노테이션 활용
엔티티 변경 이력을 자동으로 추적하려면 @audited 어노테이션을 메타에 추가합니다.
entity:
Product:
meta: entity @expose(50) @audited
fields:
pid: long @id @sequence
name: string(200)!
price: decimal!
stock: int
description: string(1000) @notaudited # 이력 추적 제외
JPA Envers 연동 가이드
ElastiCORE의 @audited는 내부적으로 Hibernate Envers와 연동됩니다. build.gradle에 의존성을 추가합니다.
dependencies {
implementation 'org.springframework.data:spring-data-envers'
implementation 'org.hibernate.orm:hibernate-envers'
}
생성된 감사 테이블은 원본 테이블명에 _aud 접미사가 붙습니다.
-- ElastiCORE + Envers가 자동 생성하는 테이블 구조
CREATE TABLE ec_product_aud (
pid BIGINT NOT NULL,
rev INTEGER NOT NULL,
revtype SMALLINT,
name VARCHAR(200),
price DECIMAL,
stock INTEGER,
PRIMARY KEY (pid, rev)
);
CREATE TABLE revinfo (
rev INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
revtstmp BIGINT
);
변경 이력 조회
// 특정 엔티티의 전체 변경 이력 조회
@Service
@RequiredArgsConstructor
public class ProductAuditService {
private final AuditReader auditReader;
public List<ProductRevision> getProductHistory(Long productId) {
List<Number> revisions = auditReader.getRevisions(Product.class, productId);
return revisions.stream()
.map(rev -> {
Product snapshot = auditReader.find(Product.class, productId, rev);
RevisionInfo revInfo = auditReader.findRevision(RevisionInfo.class, rev);
return new ProductRevision(snapshot, rev, revInfo.getRevisionDate());
})
.collect(Collectors.toList());
}
public Product getProductAtRevision(Long productId, int revision) {
return auditReader.find(Product.class, productId, revision);
}
}
RevisionListener를 구현하면 변경자 ID, IP 주소 등 추가 정보를 감사 테이블에 함께 저장할 수 있습니다.
6. 보안 통합
Spring Security와의 연동 패턴
ElastiCORE가 생성하는 REST API 엔드포인트는 Spring Security 설정과 자연스럽게 통합됩니다. @expose 어노테이션으로 노출된 엔티티의 API에 보안 규칙을 적용합니다.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// ElastiCORE 생성 API 경로 패턴에 보안 규칙 적용
.requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/products/**").hasRole("MANAGER")
.requestMatchers(HttpMethod.PUT, "/api/products/**").hasRole("MANAGER")
.requestMatchers(HttpMethod.DELETE, "/api/products/**").hasRole("ADMIN")
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
}
API 접근 제어
메서드 수준 보안을 활용하면 서비스 레이어에서 세밀한 접근 제어가 가능합니다. ElastiCORE가 생성하는 서비스 인터페이스에 @PreAuthorize를 커스텀 확장으로 추가할 수 있습니다.
// 생성된 서비스를 확장하는 커스텀 구현
@Service
public class SecureProductService extends GeneratedProductServiceImpl {
@Override
@PreAuthorize("hasRole('MANAGER') or #productId == authentication.principal.ownedProductId")
public ProductDTO updateProduct(Long productId, ProductUpdateDTO dto) {
return super.updateProduct(productId, dto);
}
@Override
@PreAuthorize("hasRole('ADMIN')")
public void deleteProduct(Long productId) {
super.deleteProduct(productId);
}
}
민감 데이터 처리
민감한 필드는 DSL에서 명시적으로 표시하여 직렬화에서 제외하거나 마스킹 처리합니다.
entity:
User:
meta: entity @expose(50)
fields:
uid: long @id @sequence
email: string(100)! @unique
password: string(255)! @sensitive # JSON 응답에서 자동 제외
name: string(50)!
phoneNumber: string(20) @sensitive # 마스킹 처리 대상
ssn: string(14) @sensitive @encrypted # 저장 시 암호화
// @sensitive 필드는 생성된 DTO에서 자동으로 제외됨
public class UserDTO {
private Long uid;
private String email;
private String name;
// password, phoneNumber, ssn 필드는 포함되지 않음
}
@encrypted 어노테이션을 사용하는 경우, 암호화 키를 코드나 설정 파일에 하드코딩하지 마세요. AWS KMS, HashiCorp Vault 등의 키 관리 서비스를 활용하세요.
7. 캐싱 전략
@cache 어노테이션 활용
조회가 빈번하고 변경이 적은 엔티티나 메서드에 @cache 어노테이션을 적용하면 Spring Cache 추상화와 자동으로 연동됩니다.
transaction:
port:
ProductService:
meta: dbms @datasource("main")
methods:
getProductById:
meta: @cache("products")
params:
productId: long
return: ProductDTO
findActiveCategories:
meta: @cache("categories") @cacheTtl(3600) # 1시간 TTL
return: list(CategoryDTO)
updateProduct:
meta: @cacheEvict("products") # 변경 시 캐시 무효화
params:
productId: long
dto: ProductUpdateDTO
return: ProductDTO
Spring Cache 연동
Redis를 캐시 저장소로 활용하는 일반적인 설정입니다.
// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()
)
);
Map<String, RedisCacheConfiguration> cacheConfigurations = Map.of(
"products", defaultConfig.entryTtl(Duration.ofMinutes(60)),
"categories", defaultConfig.entryTtl(Duration.ofHours(3)),
"userProfiles", defaultConfig.entryTtl(Duration.ofMinutes(15))
);
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigurations)
.build();
}
}
캐시 무효화 전략
// 캐시 무효화가 필요한 시나리오를 명시적으로 관리
@Service
@RequiredArgsConstructor
public class ProductCacheManager {
private final CacheManager cacheManager;
// 특정 상품 캐시만 제거
public void evictProduct(Long productId) {
Cache productCache = cacheManager.getCache("products");
if (productCache != null) {
productCache.evict(productId);
}
}
// 카테고리 전체 캐시 초기화 (관리자 작업 후)
public void clearAllCategories() {
Cache categoryCache = cacheManager.getCache("categories");
if (categoryCache != null) {
categoryCache.clear();
}
}
}
Redis 캐시를 사용할 경우, ElastiCORE가 생성하는 DTO 클래스는 Serializable을 구현하거나 Jackson 직렬화가 가능해야 합니다. @implements(Serializable) 어노테이션을 엔티티에 추가하면 생성된 클래스에 자동으로 적용됩니다.
정리
| 기능 | DSL 어노테이션 | 주요 연동 기술 |
|---|---|---|
| 다중 데이터소스 | @datasource("name") | Spring Data JPA 멀티 DataSource |
| 커스텀 인터페이스 | @implements(Interface) | 개발자 정의 인터페이스 |
| 감사 로깅 | @audited, @notaudited | Hibernate Envers |
| 민감 데이터 제외 | @sensitive | Jackson 직렬화 제어 |
| 데이터 암호화 | @encrypted | JPA Converter |
| 캐싱 | @cache, @cacheEvict | Spring Cache + Redis |
엔터프라이즈 기능은 프로젝트 요구사항에 따라 선택적으로 적용하세요. 처음부터 모든 기능을 활성화하는 것보다, 실제로 필요한 시점에 점진적으로 도입하는 것이 유지보수 측면에서 유리합니다.