하나의 클라우드 서버에서 도커를 사용해 운영하는 환경이라면, **'자원의 효율성'**과 '관리의 복잡도' 사이에서 선택해야 합니다. 결론부터 말씀드리면, 서버의 사양(특히 RAM)이 넉넉하지 않다면 '하나의 프로그램'으로 합치는 것이 유리하고, 시스템의 독립성과 유지보수가 최우선이라면 '별도의 프로그램'으로 나누는 것이 좋습니다.
상세한 비교를 통해 본인의 상황에 맞는 선택을 도와드리겠습니다.
여러 개의 DB를 하나의 스프링 부트 앱에서 관리하는 방식입니다.
각 프로그램이 도커 컨테이너 하나씩을 차지하고 독립적으로 실행되는 방식입니다.
만약 자원이 한정적이라면 일단 하나의 스프링 부트 프로젝트 안에서 패키지를 명확히 나누어(예: com.app.db1, com.app.db2) 개발하는 것을 추천합니다.
현재 1대의 클라우드 서버에 도커로 올리시는 상황이라면, 메모리 효율을 위해 "하나의 스프링 부트 프로그램"으로 시작하시는 것을 권장합니다.
ARM 4코어에 24GB RAM이라면 아주 훌륭한 사양입니다(오라클 클라우드 ARM 인스턴스로 추정됩니다). 이 정도 사양이면 자바 스프링 부트를 여러 개 띄워도 충분히 감당 가능하지만, **"메모리 낭비가 싫고 가벼운 독립 프로그램"**을 원하신다면 자바 외에 다음의 대안들이 가장 적합합니다.
클라우드 네이티브 환경과 마이크로서비스(MSA)에서 스프링 부트의 가장 강력한 대안입니다.
GORM이나 sqlx 같은 라이브러리를 통해 질문자님이 원하시는 대량 Insert 및 프로시저 호출을 매우 빠르고 가볍게 처리할 수 있습니다.자바/스프링의 구조(DI, 레이어드 아키텍처)를 좋아하신다면 가장 배우기 쉬운 대안입니다.
TypeORM이나 Prisma를 사용하여 DB 작업을 쉽게 할 수 있으며, 비동기 처리에 강점이 있어 IO 작업(DB Insert)이 많을 때 유리합니다."메모리 낭비를 절대 용납할 수 없고 최고의 성능을 원한다"면 선택합니다.
언어를 바꾸는 것이 부담스럽다면, 스프링 부트의 최신 기능을 활용하는 방법도 있습니다.
Spring Native를 공식 지원합니다. Docker 빌드 시 Native Image 옵션을 주면 됩니다.| 비교 항목 | Go | Node.js (NestJS) | Rust | Spring Native |
|---|---|---|---|---|
| 메모리 점유 | 매우 낮음 (20-50MB) | 낮음 (100-150MB) | 극도로 낮음 (5-10MB) | 낮음 (50-100MB) |
| 개발 생산성 | 높음 | 매우 높음 | 낮음 (어려움) | 높음 (기존 지식 활용) |
| ARM 최적화 | 최상 | 상 | 최상 | 상 |
| 추천 상황 | 작고 빠른 서비스 여러 개 | JS/TS가 익숙할 때 | 극한의 성능 최적화 | 자바를 유지하고 싶을 때 |
결론적으로 질문자님께는 Go(Golang)를 강력히 추천드립니다.
24GB 램이라면 Go로 만든 서비스 수십 개를 띄워도 메모리가 남아돌 것입니다. 또한 Go의 goroutine은 대량의 Insert 작업을 비동기로 처리할 때 CPU 4코어를 가장 효율적으로 사용할 수 있게 해줍니다.
구현 팁:
도커 환경에서 각 프로그램을 Alpine Linux 기반의 Go 바이너리 이미지로 만들면, 컨테이너 이미지 크기도 20MB 내외로 줄일 수 있어 배포와 운영이 매우 쾌적해집니다.
파이썬의 FastAPI를 앞서 논의한 자바(Spring Native), Go와 비교하면, **"개발 속도와 편의성"**에서는 압승이지만, **"자원 효율성과 대량 데이터 처리 성능"**에서는 중간 정도의 위치를 차지합니다.
질문하신 요구사항(대량 Insert, 프로시저 호출, 메모리 효율)을 중심으로 FastAPI를 비교 분석해 드리겠습니다.
SQLAlchemy나 Tortoise-ORM 같은 라이브러리를 사용해 DB에 데이터를 붓는 작업 자체는 비동기(Async)로 효율적으로 처리할 수 있습니다.SQLAlchemy나 원시 드라이버(psycopg2, aiomysql 등)를 사용하여 프로시저를 호출하는 코드가 자바보다 훨씬 짧고 간결합니다.| 특성 | Spring Native (Java) | Go (Golang) | FastAPI (Python) |
|---|---|---|---|
| 개발 속도 | 보통 (설정 복잡) | 빠름 (문법 단순) | 가장 빠름 (매우 생산적) |
| 실행 속도 | 매우 빠름 | 가장 빠름 | 보통 (인터프리터 한계) |
| 메모리 효율 | 우수 (80MB 내외) | 최상 (30MB 내외) | 좋음 (100MB 내외) |
| 비동기 처리 | CompletableFuture 등 | Goroutine (최강) | Async/Await (쉬움) |
| 자동 문서화 | Swagger 설정 필요 | 직접 작성/라이브러리 | 기본 내장 (매우 강력) |
이런 경우라면 FastAPI를 강력 추천합니다:
http://localhost/docs에서 테스트 가능한 API 문서(Swagger)가 생성됩니다.반면, 이런 경우라면 다시 고민해 보세요:
질문자님의 서버 사양(ARM 4코어, 24GB RAM)은 파이썬 FastAPI를 독립적인 여러 개로 운영하기에 매우 넉넉하고 훌륭한 환경입니다.
개인적으로는 대량의 Insert 작업을 동적으로 처리하는 로직이 코드 가독성이 좋아야 한다면 FastAPI로 먼저 프로토타입을 만들어 보시는 것을 추천합니다. 24GB RAM이라면 FastAPI 컨테이너 50개를 띄워도 메모리가 남습니다.
스프링 부트에서 여러 개의 데이터베이스(Multi-DataSource)를 연결하려면, 기본적으로 제공되는 자동 설정(Auto-configuration)을 비활성화하고 개발자가 직접 DataSource를 빈(Bean)으로 등록해야 합니다.
주로 JPA를 사용하는 경우와 JdbcTemplate/MyBatis를 사용하는 경우로 나뉘는데, 핵심 절차는 다음과 같습니다.
데이터베이스별로 설정 정보를 구분하여 작성합니다. (예: primary와 secondary)
spring:
datasource:
primary:
jdbc-url: jdbc:mysql://localhost:3306/db1
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
secondary:
jdbc-url: jdbc:postgresql://localhost:5432/db2
username: user
password: password
driver-class-name: org.postgresql.Driver
주의: url이 아니라 jdbc-url로 작성해야 히카리(HikariCP)가 정확히 인식합니다.
각 DB에 대해 DataSource, TransactionManager, 그리고 연결 라이브러리(JPA/MyBatis) 설정을 각각 만듭니다.
@Configuration
@EnableJpaRepositories(
basePackages = "com.example.repository.primary", // Primary용 Repository 위치
entityManagerFactoryRef = "primaryEntityManagerFactory",
transactionManagerRef = "primaryTransactionManager"
)
public class PrimaryDbConfig {
@Primary
@Bean
@ConfigurationProperties("spring.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Primary
@Bean
public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(
EntityManagerFactoryBuilder builder) {
return builder
.dataSource(primaryDataSource())
.packages("com.example.entity.primary") // Primary용 Entity 위치
.persistenceUnit("primary")
.build();
}
@Primary
@Bean
public PlatformTransactionManager primaryTransactionManager(
@Qualifier("primaryEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}
@Configuration
@EnableJpaRepositories(
basePackages = "com.example.repository.secondary", // Secondary용 Repository 위치
entityManagerFactoryRef = "secondaryEntityManagerFactory",
transactionManagerRef = "secondaryTransactionManager"
)
public class SecondaryDbConfig {
@Bean
@ConfigurationProperties("spring.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public LocalContainerEntityManagerFactoryBean secondaryEntityManagerFactory(
EntityManagerFactoryBuilder builder) {
return builder
.dataSource(secondaryDataSource())
.packages("com.example.entity.secondary") // Secondary용 Entity 위치
.persistenceUnit("secondary")
.build();
}
@Bean
public PlatformTransactionManager secondaryTransactionManager(
@Qualifier("secondaryEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}
앞서 질문하신 대량 Insert 및 프로시저 호출을 위해 JdbcTemplate을 여러 개 만들고 싶다면 위 DataSource 빈을 주입받아 각각 생성하면 됩니다.
@Configuration
public class JdbcTemplateConfig {
@Bean
public JdbcTemplate primaryJdbcTemplate(@Qualifier("primaryDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
public JdbcTemplate secondaryJdbcTemplate(@Qualifier("secondaryDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
사용할 때는 @Qualifier를 통해 어떤 DB를 사용할지 명시하거나, 패키지 경로를 분리하여 사용합니다.
@Service
public class MyService {
private final JdbcTemplate primaryJdbcTemplate;
private final JdbcTemplate secondaryJdbcTemplate;
public MyService(@Qualifier("primaryJdbcTemplate") JdbcTemplate primaryJdbcTemplate,
@Qualifier("secondaryJdbcTemplate") JdbcTemplate secondaryJdbcTemplate) {
this.primaryJdbcTemplate = primaryJdbcTemplate;
this.secondaryJdbcTemplate = secondaryJdbcTemplate;
}
public void doWork() {
// DB 1에 대량 Insert
primaryJdbcTemplate.batchUpdate(...);
// DB 2의 프로시저 실행
SimpleJdbcCall jdbcCall = new SimpleJdbcCall(secondaryJdbcTemplate).withProcedureName("...");
jdbcCall.execute(...);
}
}
@Primary를 가져야 합니다. (스프링 부트가 어떤 것을 기본으로 쓸지 알아야 하기 때문)ChainedTransactionManager가 필요하지만 설정이 매우 복잡해지므로, 가급적 서비스 로직에서 순차적으로 처리하는 것을 권장합니다.spring.datasource.primary.hikari.*와 같이 상세 커넥션 풀 설정을 추가하여 각 DB 환경에 맞게 튜닝할 수 있습니다.질문하신 대량 Insert는 1번 DB에서, 데이터 정리는 2번 DB에서 해야 하는 상황이라면 위와 같은 Multi-DataSource 설정이 반드시 필요합니다.
요청하신 구성은 **"기본 DB(A)는 설정을 최소화하고, 나머지(B, C)는 필요할 때만 명시적으로 사용"**하는 구조입니다.
이를 위해서는 DB A를 @Primary로 설정하는 것이 핵심입니다. 이렇게 하면 @Autowired 시 별도의 이름(Qualifier)을 지정하지 않아도 자동으로 DB A를 사용하게 됩니다.
구체적인 설정 방법을 단계별로 안내해 드립니다.
데이터베이스 3개의 정보를 각각 정의합니다.
spring:
datasource:
# DB A (메인, JPA)
primary:
jdbc-url: jdbc:mysql://localhost:3306/db_a
username: user
password: pass
driver-class-name: com.mysql.cj.jdbc.Driver
# DB B (가끔, JPA)
secondary:
jdbc-url: jdbc:mysql://localhost:3306/db_b
username: user
password: pass
driver-class-name: com.mysql.cj.jdbc.Driver
# DB C (가끔, JdbcTemplate)
third:
jdbc-url: jdbc:mysql://localhost:3306/db_c
username: user
password: pass
driver-class-name: com.mysql.cj.jdbc.Driver
DB A에 해당하는 Entity와 Repository는 별도의 패키지(com.example.repository.primary)에 모아야 합니다.
@Configuration
@EnableJpaRepositories(
basePackages = "com.example.repository.primary", // A의 리포지토리 경로
entityManagerFactoryRef = "primaryEntityManagerFactory",
transactionManagerRef = "primaryTransactionManager"
)
public class PrimaryDbConfig {
@Primary
@Bean
@ConfigurationProperties("spring.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Primary
@Bean
public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(
EntityManagerFactoryBuilder builder) {
return builder
.dataSource(primaryDataSource())
.packages("com.example.entity.primary") // A의 엔티티 경로
.build();
}
@Primary
@Bean
public PlatformTransactionManager primaryTransactionManager(
@Qualifier("primaryEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}
DB B의 Entity와 Repository도 전용 패키지(com.example.repository.secondary)에 관리합니다.
@Configuration
@EnableJpaRepositories(
basePackages = "com.example.repository.secondary", // B의 리포지토리 경로
entityManagerFactoryRef = "secondaryEntityManagerFactory",
transactionManagerRef = "secondaryTransactionManager"
)
public class SecondaryDbConfig {
@Bean
@ConfigurationProperties("spring.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public LocalContainerEntityManagerFactoryBean secondaryEntityManagerFactory(
EntityManagerFactoryBuilder builder) {
return builder
.dataSource(secondaryDataSource())
.packages("com.example.entity.secondary") // B의 엔티티 경로
.build();
}
@Bean
public PlatformTransactionManager secondaryTransactionManager(
@Qualifier("secondaryEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}
JPA가 필요 없으므로 DataSource와 JdbcTemplate만 빈으로 등록합니다.
@Configuration
public class ThirdDbConfig {
@Bean
@ConfigurationProperties("spring.datasource.third")
public DataSource thirdDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public JdbcTemplate thirdJdbcTemplate(@Qualifier("thirdDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
이제 원하시는 대로 DB A는 아무 명시 없이, B와 C는 명시적으로 사용할 수 있습니다.
@Service
@RequiredArgsConstructor
public class MyService {
// 1. DB A: @Primary 덕분에 그냥 주입받으면 됨
private final PrimaryRepository primaryRepo;
// 2. DB B: B 전용 패키지의 리포지토리를 주입받으면 됨
private final SecondaryRepository secondaryRepo;
// 3. DB C: 여러 JdbcTemplate 중 이름을 지정해서 주입받음
@Qualifier("thirdJdbcTemplate")
private final JdbcTemplate thirdJdbcTemplate;
@Transactional // DB A(기본) 트랜잭션 적용
public void doWork() {
// DB A 사용
primaryRepo.save(new PrimaryEntity());
// DB B 사용 (B의 리포지토리를 호출하면 자동으로 B의 DB와 연결됨)
secondaryRepo.save(new SecondaryEntity());
// DB C 사용 (JdbcTemplate으로 쿼리 실행)
thirdJdbcTemplate.execute("INSERT INTO some_table ...");
}
@Transactional(transactionManager = "secondaryTransactionManager") // DB B 트랜잭션이 필요할 때
public void doSecondaryWork() {
secondaryRepo.save(new SecondaryEntity());
}
}
@Primary의 마법: PrimaryDbConfig에서 모든 빈에 @Primary를 붙였기 때문에, 스프링은 "이게 기본이다"라고 인식합니다. 따라서 DB A를 쓸 때는 특별한 설정 없이 평소처럼 코딩하면 됩니다.JdbcTemplate은 빈 이름(thirdJdbcTemplate)으로 구분하여 주입받으면 혼선 없이 사용 가능합니다.@Transactional만 쓰면 DB A의 트랜잭션이 걸립니다. DB B에 트랜잭션을 걸고 싶을 때만 @Transactional(transactionManager = "secondaryTransactionManager") 처럼 이름을 명시해 주시면 됩니다.이 구성은 성능 저하가 거의 없으며, 코드 가독성 면에서도 질문하신 의도에 가장 잘 부합하는 방식입니다.
결론부터 말씀드리면, 아니요. 모두 롤백되지 않습니다. 오직 DB A만 롤백됩니다.
이것이 멀티 데이터소스 환경에서 가장 주의해야 할 '트랜잭션 격리' 문제입니다. 왜 이런 결과가 나오는지, 그리고 어떻게 해결할 수 있는지 상세히 설명해 드리겠습니다.
앞서 작성한 doWork() 메서드에는 @Transactional이 붙어 있습니다.
@Transactional // <- 아무 설정이 없으면 @Primary인 DB A의 트랜잭션 매니저를 사용함
public void doWork() {
primaryRepo.save(new PrimaryEntity()); // (1) DB A 작업
secondaryRepo.save(new SecondaryEntity()); // (2) DB B 작업
thirdJdbcTemplate.execute("..."); // (3) DB C 작업 -> 여기서 에러 발생!
}
primaryTransactionManager(DB A용)의 관리를 받습니다. (3)번에서 예외가 발생하면, 스프링은 DB A의 트랜잭션을 롤백합니다.secondaryRepo는 별도의 secondaryTransactionManager를 사용합니다. 현재 트랜잭션은 DB A의 것이므로, DB B의 작업은 **트랜잭션 없이 실행되거나 실행 즉시 커밋(Auto-commit)**되어 버립니다. 따라서 에러가 나도 이미 저장된 데이터는 취소되지 않습니다.thirdJdbcTemplate 역시 DB A의 트랜잭션 매니저와는 아무 상관이 없습니다. 실행하는 순간 즉시 DB에 반영됩니다.서로 다른 디비를 하나의 트랜잭션으로 묶는 것은 상당히 어려운 기술적 과제입니다. 크게 3가지 방법이 있습니다.
여러 트랜잭션 매니저를 하나로 묶어 순차적으로 롤백을 시도하는 방식입니다.
deprecated(권장되지 않음) 되었으나, 가벼운 용도로는 여전히 구현해서 쓰기도 합니다.Atomikos나 Bitronix 같은 외부 트랜잭션 관리자를 사용하는 방식입니다.
현대적인 마이크로서비스 아키텍처에서 가장 권장되는 방식입니다.
public void doWork() {
try {
primaryRepo.save(...);
secondaryRepo.save(...);
thirdJdbcTemplate.execute(...);
} catch (Exception e) {
// 수동으로 DB B와 DB C의 데이터를 삭제하거나 취소하는 로직 수행
secondaryRepo.delete(...);
throw e; // DB A는 @Transactional에 의해 자동 롤백됨
}
}
만약 질문하신 작업이 **"반드시 세 디비가 동시에 성공하거나 실패해야 하는 매우 중요한 작업"**이라면, 하나의 스프링 부트 앱에서 처리하기보다는 다음과 같이 구조를 잡는 것이 좋습니다.
요약하자면: 현재 설정으로는 DB A만 안전하게 보호됩니다. B와 C의 롤백이 반드시 필요하다면 추가적인 트랜잭션 설계가 필요합니다.