🌏 AIGOYA란?
✅ 프로젝트 개요
AIGOYA는 AI 개발에서 발생하는 비효율성을 해결하기 위한 AI 개발 생산성 플랫폼입니다. AI 개발 과정에서 반복되는 전처리, 모델 정의, 학습, 시각화, 평가 등을 프리셋 모듈로 제공하며, 자체 웹 기반 IDE에서 GUI를 활용한 drag-and-drop 개발 환경을 지원합니다. 또한 사용자가 쿠버네티스 기반 클라우드에서 독립된 OS을 점유 사용할 수 있도록 하여, 개발 환경을 손쉽게 구축할 수 있습니다. 부가적으로는 학습 파라미터, 코드 버전, 결과를 함께 저장 관리할 수 있어 프로젝트 추적이 용이하고, 코드 모듈을 공유하고 자신만의 AI 개발자 포트폴리오를 구성할 수 있는 커뮤니티 기능을 제공합니다.
해당 프로젝트는 2024년 4월 시작되었으며, 인공지능융합사업단이 주관한 ‘AI 창업경진대회’에서 수상하며 사업화 지원금 3,500만 원을 확보하였고, 이를 바탕으로 직원 5명과 함께 MVP 개발을 성공적으로 완료하였습니다.
✅ 기술적 핵심과 차별화
- LLM 기반 개발 보조 기능개발자 워크플로우 추천, 코드 생성, 수정까지 LLM을 통해 백엔드에서 자동으로 추천, 수정할 수 있도록 구성했습니다.
- 다량의 프리셋 모듈을 제공하기 위해서 서버에서 스케줄링을 통해서 LLM의 프롬프트 엔지니어링을 통해 프리셋 모듈을 생성하고, 기존의 코드와 중복되지 않도록 벡터 DB를 통해 경량화된 검색의 태그 기반으로 중복되지 않는 코드 생성을 주기적으로 수행했습니다.
- 사용자 검색어에 대해 벡터 기반 검색 시스템을 도입해 논문 요약 및 코드 구현 검색 기능을 기획했으며, 이에 대해 특허 출원도 진행했습니다. (aigoya에서 제공하는 서비스에 특화된 기술입니다.)
- 시각화 및 자유도 높은 UX
- 코드 작성 화면에서 직접 수정 모드, 시각화 모드 전환이 가능해 노코드 툴과는 차별화된 자유도를 제공하여 현업에서도 사용할 수 있는 수준의 자유도를 제공하였습니다.
- 글로벌 커뮤니티 연동
- 코드 모듈은 자동으로 AIGOYA 커뮤니티에 블로그 포스팅 형식의 3개 국어 설명글이 생성되어 지식이 축적되고 자동으로 공유되는 구조를 가지고 있습니다.
🌏 AIGOYA의 비동기 이벤트 처리에서의 Race Condition 해결
✅ 문제 상황
AIGOYA는 코드 모듈 등록 이후 여러 작업이 자동으로 이어지는 구조였기 때문에, 도메인 사이의 로직의 흐름이 매우 복잡했습니다.
예를 들어, 사용자가 코드를 업로드하면 LLM으로 카테고리, 벡터 태그 등 수많은 메타데이터를 생성하고, 외부 저장소에 코드와 메타데이터를 업로드하며, 동시에 코드에 대한 설명을 블로그 포스팅 형태로 만들어 자동 포스팅하는 등의 작업이 발생했고 이 모든 기능들을 스프링에서 제공하는 비동기 이벤트 구조를 사용해 구현하고, 기능이 추가될 때마다 이벤트 리스너를 추가 등록하도록 했습니다.
그러나 이벤트가 많아지면서 비동기 처리 간 충돌(race condition) 문제가 자주 발생하게 되었습니다. 특히 서로 다른 이벤트 리스너들이 동시에 동일한 데이터를 수정하거나 의존 관계가 있는 작업이 먼저 처리되는 현상이 빈번했습니다. 처음에는 이를 인지하지 못해 디버깅에 어려움을 겪었지만 직접 데이터베이스를 조회해보며 데이터가 덮어씌워지는 문제를 발견했고, 이로 인해 정합성 문제가 발생했습니다.
AIGOYA는 현재 베타테스트 단계에 있기 때문에, 단일 서버만을 사용하고 있는 상황입니다.
✅ Step 1-1. 데이터베이스 락 도입
초기에는 단순하게 DB row-level lock을 도입하여 충돌을 완화했습니다.
예를 들어, NodeCreatedEvent
처리 시작 시점에 SELECT FOR UPDATE
를 통해 해당 Node row를 선점하고, 다른 트랜잭션에서는 접근할 수 없도록 막았습니다. (Pessimistic Lock)
@Lock(LockModeType.PESSIMISTIC_WRITE)
fun findByIdForUpdate(id: Long): Optional<Node>
하지만 기획이 확장되며, 일부 작업은 선행 작업의 결과에 의존해야 하는 구조가 되었고, 단순 락만으로는 이벤트의 처리 순서를 보장할 수 없었습니다.
✅ Step1-2. Application Lock 적용하기
비동기 이벤트 처리에서 가장 먼저 고려할 수 있는 방법은 애플리케이션 레벨에서 직접 락(lock)을 제어하는 방식입니다. 이는 비즈니스 로직이 실행되기 전, 코드 수준에서 명시적으로 동시성을 제어하고자 할 때 사용됩니다.
구분 | 예시 | 적용 범위 | 분산 환경 지원 | 설명 |
---|---|---|---|---|
Local Lock | synchronized , ReentrantLock |
JVM 내부 | X | 단일 서버, 단일 인스턴스 내 스레드 동기화 |
Distributed Lock | Redis (Redisson), ZooKeeper, Etcd | 네트워크 기반 | O | 여러 서버 간 공유 자원에 대한 락 획득/해제 가능 |
Local Lock은 아래와 같이 아주 간단하게 구현할 수 있습니다.
val lock = ReentrantLock()
fun processNode(nodeId: Long) {
lock.lock()
try {
// Node 처리 로직
} finally {
lock.unlock()
}
}
하지만 해당 락은 JVM 내부에서만 락이 동작하므로 멀티 인스턴스 환경에서는 적용할 수 없다는 단점이 있습니다.
Distributed Lock, 그중에서도 Redis를 사용하면 아래와 같이 구현이 가능합니다.
val lock: RLock = redissonClient.getLock("node-lock-${nodeId}")
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
// Node 처리 로직
}
} finally {
lock.unlock()
}
Redis 기반 분산 락(Redisson)은 다중 인스턴스 간 동시 접근 제어를 가능하게 하기 때문에 분산 환경에서도 사용할 수 있습니다. 하지만 Redis 의존도 증가하고, 외부 저장소에 들러 락과 관련된 로직을 처리해야 하기 때문에 네트워크 비용이 있으며 부하가 증가하거나, Redis 자체가 SPOF가 될 수 있습니다.
이 솔루션 역시 동시성 제어는 가능하지만 이벤트의 처리 순서를 보장할 수 없었습니다.
✅ Step2. 이벤트 직렬 처리 구조(Command Queue) 도입
AIGOYA에서는 이 한계를 넘어 순서와 정합성까지 보장하기 위해, 이후에는 Command Queue 기반의 명시적 직렬 처리 구조로 아키텍처를 변경하였습니다. 이 구조는 이벤트가 발생하면 등록된 핸들러들을 순차적으로 호출하고, 전체 로직을 단일 쓰레드로 처리하여 순서를 완벽히 제어할 수 있도록 설계되었습니다.
모든 후속 작업을 하나의 커스텀 이벤트 리스너에서 순차적으로 실행하는 방식으로 구조를 변경했습니다.
각각의 처리 단계를 명시적으로 선언하며, 작업 간 순서 보장, 결합도 최소화, 추후 유지보수 및 기능 확장 시 용이하게 만들었습니다.
결국 AIGOYA의 비동기 구조는 아래와 같이 직렬화되었습니다.
[코드 업로드 이벤트 수신]
↓
[카테고리 분류]
↓
[외부 저장소 업로드]
↓
[다국어 블로그 포스트 생성]
↓
[커뮤니티 등록]
가장 간단하게는 아래와 같이 구현이 가능합니다.
※ 아래 코드는 구조의 개념을 설명하기 위한 예시이며, @Async는 Spring의 기본 설정(SimpleAsyncTaskExecutor)에 따라 이벤트 간 순차 실행이 보장되지 않을 수 있습니다.
@Async
@EventListener
fun handleNodeCreatedEvent(event: NodeCreatedEvent) {
nodeDataTypesToTasksService.defineDataTypeAndTask(event.node) // 동기 메서드
nodeS3StoreService.storeNode(event.node, event.code) // 동기 메서드
nodeGithubStoreService.storeNode(event.node, event.code) // 동기 메서드
nodePostWriteService.createNodePost(event.node) // 동기 메서드
}
이 구조는 핸들러 내부의 처리 순서는 보장하지만, 여러 이벤트가 동시에 발생할 경우 이벤트 간 처리 순서는 보장되지 않을 수 있습니다. 따라서 절대적인 직렬 처리와 이벤트 정합성이 필요한 경우에는 SingleThreadExecutor
기반 구조나 persistent queue 설계가 더 적합합니다. (다음 포스팅에서 기술하겠습니다.)
코드 레벨에서 메세지 브로커 없이 해결할 수 있는 여러 솔루션에 대해서는 다음 포스팅에서 기술하겠습니다.
✅ 외부 메시지 브로커를 도입하지 않고 해결한 이유
Kafka 등의 메시지 브로커 도입도 충분히 고려했지만, 현재 시점에 아직 시기상조라 적절치 않다고 판단했습니다.
- 아키텍처 단계에서 당시 AIGOYA는 단일 서버 기반의 초기 서비스였기 때문
- 복잡도 대비 효과에서 이벤트 복잡도나 트래픽 양이 Kafka 도입 수준은 아니었기 때문
- 유지보수 측면에서 Kafka는 운영 복잡도 및 러닝 커브가 크며, 장애 포인트를 늘릴 가능성이 있기 때문
- 단일 쓰레드 기반 Command Queue 구조로도 충분히 정합성 보장 가능한 스케일과 구조였기 때문
Kafka는 운영 복잡도와 러닝 커브가 있는 반면, 장애 복원력과 확장성 측면에서 매우 강력합니다.
AIGOYA는 당시 서비스의 규모와 요구사항을 고려해 Kafka 없이 충분한 구조를 선택했고, 향후 도입 가능성을 고려해 구조를 유연하게 설계했습니다.
결과적으로 외부 브로커를 도입하지 않고, 하나의 스레드 안에서 순차 실행하는 구조로 안정성과 정합성을 확보할 수 있었습니다.
다음 포스팅에서는 코드 레벨에서 어떻게 더 동시성과 순서 보장을 할 수 있는지 다양한 사례와 함께 설명하겠습니다

도움이 되었다면, 공감/댓글을 달아주면 깃짱에게 큰 힘이 됩니다!🌟
비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!
'PROJECT > AIGOYA LABS' 카테고리의 다른 글
[AIGOYA LABS] 트랜잭션 내에서 외부 API 호출을 하겠다고요?!!! (9) | 2025.04.21 |
---|
🌏 AIGOYA란?
✅ 프로젝트 개요
AIGOYA는 AI 개발에서 발생하는 비효율성을 해결하기 위한 AI 개발 생산성 플랫폼입니다. AI 개발 과정에서 반복되는 전처리, 모델 정의, 학습, 시각화, 평가 등을 프리셋 모듈로 제공하며, 자체 웹 기반 IDE에서 GUI를 활용한 drag-and-drop 개발 환경을 지원합니다. 또한 사용자가 쿠버네티스 기반 클라우드에서 독립된 OS을 점유 사용할 수 있도록 하여, 개발 환경을 손쉽게 구축할 수 있습니다. 부가적으로는 학습 파라미터, 코드 버전, 결과를 함께 저장 관리할 수 있어 프로젝트 추적이 용이하고, 코드 모듈을 공유하고 자신만의 AI 개발자 포트폴리오를 구성할 수 있는 커뮤니티 기능을 제공합니다.
해당 프로젝트는 2024년 4월 시작되었으며, 인공지능융합사업단이 주관한 ‘AI 창업경진대회’에서 수상하며 사업화 지원금 3,500만 원을 확보하였고, 이를 바탕으로 직원 5명과 함께 MVP 개발을 성공적으로 완료하였습니다.
✅ 기술적 핵심과 차별화
- LLM 기반 개발 보조 기능개발자 워크플로우 추천, 코드 생성, 수정까지 LLM을 통해 백엔드에서 자동으로 추천, 수정할 수 있도록 구성했습니다.
- 다량의 프리셋 모듈을 제공하기 위해서 서버에서 스케줄링을 통해서 LLM의 프롬프트 엔지니어링을 통해 프리셋 모듈을 생성하고, 기존의 코드와 중복되지 않도록 벡터 DB를 통해 경량화된 검색의 태그 기반으로 중복되지 않는 코드 생성을 주기적으로 수행했습니다.
- 사용자 검색어에 대해 벡터 기반 검색 시스템을 도입해 논문 요약 및 코드 구현 검색 기능을 기획했으며, 이에 대해 특허 출원도 진행했습니다. (aigoya에서 제공하는 서비스에 특화된 기술입니다.)
- 시각화 및 자유도 높은 UX
- 코드 작성 화면에서 직접 수정 모드, 시각화 모드 전환이 가능해 노코드 툴과는 차별화된 자유도를 제공하여 현업에서도 사용할 수 있는 수준의 자유도를 제공하였습니다.
- 글로벌 커뮤니티 연동
- 코드 모듈은 자동으로 AIGOYA 커뮤니티에 블로그 포스팅 형식의 3개 국어 설명글이 생성되어 지식이 축적되고 자동으로 공유되는 구조를 가지고 있습니다.
🌏 AIGOYA의 비동기 이벤트 처리에서의 Race Condition 해결
✅ 문제 상황
AIGOYA는 코드 모듈 등록 이후 여러 작업이 자동으로 이어지는 구조였기 때문에, 도메인 사이의 로직의 흐름이 매우 복잡했습니다.
예를 들어, 사용자가 코드를 업로드하면 LLM으로 카테고리, 벡터 태그 등 수많은 메타데이터를 생성하고, 외부 저장소에 코드와 메타데이터를 업로드하며, 동시에 코드에 대한 설명을 블로그 포스팅 형태로 만들어 자동 포스팅하는 등의 작업이 발생했고 이 모든 기능들을 스프링에서 제공하는 비동기 이벤트 구조를 사용해 구현하고, 기능이 추가될 때마다 이벤트 리스너를 추가 등록하도록 했습니다.
그러나 이벤트가 많아지면서 비동기 처리 간 충돌(race condition) 문제가 자주 발생하게 되었습니다. 특히 서로 다른 이벤트 리스너들이 동시에 동일한 데이터를 수정하거나 의존 관계가 있는 작업이 먼저 처리되는 현상이 빈번했습니다. 처음에는 이를 인지하지 못해 디버깅에 어려움을 겪었지만 직접 데이터베이스를 조회해보며 데이터가 덮어씌워지는 문제를 발견했고, 이로 인해 정합성 문제가 발생했습니다.
AIGOYA는 현재 베타테스트 단계에 있기 때문에, 단일 서버만을 사용하고 있는 상황입니다.
✅ Step 1-1. 데이터베이스 락 도입
초기에는 단순하게 DB row-level lock을 도입하여 충돌을 완화했습니다.
예를 들어, NodeCreatedEvent
처리 시작 시점에 SELECT FOR UPDATE
를 통해 해당 Node row를 선점하고, 다른 트랜잭션에서는 접근할 수 없도록 막았습니다. (Pessimistic Lock)
@Lock(LockModeType.PESSIMISTIC_WRITE)
fun findByIdForUpdate(id: Long): Optional<Node>
하지만 기획이 확장되며, 일부 작업은 선행 작업의 결과에 의존해야 하는 구조가 되었고, 단순 락만으로는 이벤트의 처리 순서를 보장할 수 없었습니다.
✅ Step1-2. Application Lock 적용하기
비동기 이벤트 처리에서 가장 먼저 고려할 수 있는 방법은 애플리케이션 레벨에서 직접 락(lock)을 제어하는 방식입니다. 이는 비즈니스 로직이 실행되기 전, 코드 수준에서 명시적으로 동시성을 제어하고자 할 때 사용됩니다.
구분 | 예시 | 적용 범위 | 분산 환경 지원 | 설명 |
---|---|---|---|---|
Local Lock | synchronized , ReentrantLock |
JVM 내부 | X | 단일 서버, 단일 인스턴스 내 스레드 동기화 |
Distributed Lock | Redis (Redisson), ZooKeeper, Etcd | 네트워크 기반 | O | 여러 서버 간 공유 자원에 대한 락 획득/해제 가능 |
Local Lock은 아래와 같이 아주 간단하게 구현할 수 있습니다.
val lock = ReentrantLock()
fun processNode(nodeId: Long) {
lock.lock()
try {
// Node 처리 로직
} finally {
lock.unlock()
}
}
하지만 해당 락은 JVM 내부에서만 락이 동작하므로 멀티 인스턴스 환경에서는 적용할 수 없다는 단점이 있습니다.
Distributed Lock, 그중에서도 Redis를 사용하면 아래와 같이 구현이 가능합니다.
val lock: RLock = redissonClient.getLock("node-lock-${nodeId}")
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
// Node 처리 로직
}
} finally {
lock.unlock()
}
Redis 기반 분산 락(Redisson)은 다중 인스턴스 간 동시 접근 제어를 가능하게 하기 때문에 분산 환경에서도 사용할 수 있습니다. 하지만 Redis 의존도 증가하고, 외부 저장소에 들러 락과 관련된 로직을 처리해야 하기 때문에 네트워크 비용이 있으며 부하가 증가하거나, Redis 자체가 SPOF가 될 수 있습니다.
이 솔루션 역시 동시성 제어는 가능하지만 이벤트의 처리 순서를 보장할 수 없었습니다.
✅ Step2. 이벤트 직렬 처리 구조(Command Queue) 도입
AIGOYA에서는 이 한계를 넘어 순서와 정합성까지 보장하기 위해, 이후에는 Command Queue 기반의 명시적 직렬 처리 구조로 아키텍처를 변경하였습니다. 이 구조는 이벤트가 발생하면 등록된 핸들러들을 순차적으로 호출하고, 전체 로직을 단일 쓰레드로 처리하여 순서를 완벽히 제어할 수 있도록 설계되었습니다.
모든 후속 작업을 하나의 커스텀 이벤트 리스너에서 순차적으로 실행하는 방식으로 구조를 변경했습니다.
각각의 처리 단계를 명시적으로 선언하며, 작업 간 순서 보장, 결합도 최소화, 추후 유지보수 및 기능 확장 시 용이하게 만들었습니다.
결국 AIGOYA의 비동기 구조는 아래와 같이 직렬화되었습니다.
[코드 업로드 이벤트 수신]
↓
[카테고리 분류]
↓
[외부 저장소 업로드]
↓
[다국어 블로그 포스트 생성]
↓
[커뮤니티 등록]
가장 간단하게는 아래와 같이 구현이 가능합니다.
※ 아래 코드는 구조의 개념을 설명하기 위한 예시이며, @Async는 Spring의 기본 설정(SimpleAsyncTaskExecutor)에 따라 이벤트 간 순차 실행이 보장되지 않을 수 있습니다.
@Async
@EventListener
fun handleNodeCreatedEvent(event: NodeCreatedEvent) {
nodeDataTypesToTasksService.defineDataTypeAndTask(event.node) // 동기 메서드
nodeS3StoreService.storeNode(event.node, event.code) // 동기 메서드
nodeGithubStoreService.storeNode(event.node, event.code) // 동기 메서드
nodePostWriteService.createNodePost(event.node) // 동기 메서드
}
이 구조는 핸들러 내부의 처리 순서는 보장하지만, 여러 이벤트가 동시에 발생할 경우 이벤트 간 처리 순서는 보장되지 않을 수 있습니다. 따라서 절대적인 직렬 처리와 이벤트 정합성이 필요한 경우에는 SingleThreadExecutor
기반 구조나 persistent queue 설계가 더 적합합니다. (다음 포스팅에서 기술하겠습니다.)
코드 레벨에서 메세지 브로커 없이 해결할 수 있는 여러 솔루션에 대해서는 다음 포스팅에서 기술하겠습니다.
✅ 외부 메시지 브로커를 도입하지 않고 해결한 이유
Kafka 등의 메시지 브로커 도입도 충분히 고려했지만, 현재 시점에 아직 시기상조라 적절치 않다고 판단했습니다.
- 아키텍처 단계에서 당시 AIGOYA는 단일 서버 기반의 초기 서비스였기 때문
- 복잡도 대비 효과에서 이벤트 복잡도나 트래픽 양이 Kafka 도입 수준은 아니었기 때문
- 유지보수 측면에서 Kafka는 운영 복잡도 및 러닝 커브가 크며, 장애 포인트를 늘릴 가능성이 있기 때문
- 단일 쓰레드 기반 Command Queue 구조로도 충분히 정합성 보장 가능한 스케일과 구조였기 때문
Kafka는 운영 복잡도와 러닝 커브가 있는 반면, 장애 복원력과 확장성 측면에서 매우 강력합니다.
AIGOYA는 당시 서비스의 규모와 요구사항을 고려해 Kafka 없이 충분한 구조를 선택했고, 향후 도입 가능성을 고려해 구조를 유연하게 설계했습니다.
결과적으로 외부 브로커를 도입하지 않고, 하나의 스레드 안에서 순차 실행하는 구조로 안정성과 정합성을 확보할 수 있었습니다.
다음 포스팅에서는 코드 레벨에서 어떻게 더 동시성과 순서 보장을 할 수 있는지 다양한 사례와 함께 설명하겠습니다

도움이 되었다면, 공감/댓글을 달아주면 깃짱에게 큰 힘이 됩니다!🌟
비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!
'PROJECT > AIGOYA LABS' 카테고리의 다른 글
[AIGOYA LABS] 트랜잭션 내에서 외부 API 호출을 하겠다고요?!!! (9) | 2025.04.21 |
---|