코코무 프로젝트는 코딩 스터디 플랫폼으로 스터디 회원 간 코딩 스터디를 진행할 수 있도록 환경을 제공하고 있습니다.
MVP ? (핵심 기능은 Bold)
위 기능을 토대로 코코무 백엔드는 아래와 같은 기술 스택을 선정했습니다.
(더 자세한 기술 스택은 깃허브에서 확인해주세요)
💡 선택 배경
혼자서 백엔드 전체를 구현해야 했기 때문에, 다음과 같은 요구사항을 만족할 수 있는 프레임워크가 필요했습니다.
✅ Spring Boot를 선택한 주요 이유
Web API 구현을 위한 MVC 구조 지원
Jackson, Servlet, Web MVC 등을 자동 구성
spring-boot-starter-web 사용 시, RESTful API 구현이 표준화되어 있어 유지보수 용이해지는 장점이 있었습니다.
ORM 및 DB 연동의 표준
✨ 결론
- Spring Boot는 다양한 기술을 빠르게 통합하고, 자동 구성(Auto Configuration)을 통해 MVP 단계에서 높은 생산성을 제공합니다.
- 혼자서 백엔드 전체를 구현하는 상황에서는 표준화된 설계 방식 덕분에 복잡함을 감소하고 이후 유지보수 및 확장도 수월한 장점이 있으므로 선택했습니다.
✅ QueryDSL 선택 이유 요약
동적 쿼리 구성의 간결성
복잡한 조건 분기(if, null, List 포함 여부 등)를 깔끔하게 작성 가능
BooleanBuilder, where() 체이닝 등으로 가독성 향상
컴파일 시점 오류 검출
JPQL은 문자열 기반이라 오타, 컬럼명 오류는 런타임에서야 발견됨
QueryDSL은 도메인 기반 Q클래스를 사용 → 컴파일 단계에서 오류 탐지 가능
→ 리팩토링에 강하고 안정성 향상
IDE 자동완성 지원
Q클래스를 통한 코드 자동완성 → 쿼리 작성 속도 증가
JPQL 대비 훨씬 빠른 생산성 확보
타입 안전(Type-safe)
필드 이름, 타입, 조인 대상이 정적 타입으로 강제됨
위험한 런타임 NullPointerException 리스크가 적음
GroupBy, SubQuery, Projection 등 다양한 기능 지원
GroupBy, Projections.fields, case when 등 복잡한 쿼리 작성 가능
DTO 매핑도 바로 처리 가능 (→ Projections.fields(UserDto.class, ...))
JPA와의 통합
JPAQueryFactory 기반으로 JPA Entity를 그대로 사용 가능
영속성 컨텍스트, 트랜잭션 등 JPA 장점 그대로 유지 가능
🎯 예제 시나리오
“특정 코딩스페이스에 있는 ACTIVE 상태의 탭과 유저 정보를 조회”
① JPQL 방식 (문자열 기반) 👎
@Query("SELECT new co.kr.dto.UserDto(u.id, u.nickname, u.profileImageUrl, t.role) " +
"FROM CodingSpaceTab t JOIN t.user u " +
"WHERE t.codingSpace.id = :spaceId AND t.status = 'ACTIVE'")
List<UserDto> findActiveTabsBySpaceId(@Param("spaceId") Long spaceId);
❌ 단점:
② Spring Data JPA 메서드 이름 기반 👎
List<CodingSpaceTab> findByCodingSpaceIdAndStatus(Long codingSpaceId, TabStatus status);
❌ 단점:
③ QueryDSL 방식 (타입 안전, 가독성 최고) ✅
return queryFactory
.select(Projections.fields(UserDto.class,
user.id,
user.nickname,
user.profileImageUrl,
codingSpaceTab.role
))
.from(codingSpaceTab)
.join(codingSpaceTab.user, user)
.where(
codingSpaceTab.codingSpace.id.eq(spaceId),
codingSpaceTab.status.eq(TabStatus.ACTIVE)
)
.fetch();
✅ 장점:
🧠 핵심 요약
비교 | JPQL | Spring Data JPA | QueryDSL |
---|---|---|---|
타입 안정성 | ❌ 문자열 기반 | ◯ 제한적 지원 | ✅ 컴파일 타임 체크 |
복잡 쿼리 지원 | ❌ 불편함 | ❌ 불가 | ✅ 자유로움 |
가독성 | ❌ 나쁨 | ◯ 짧은 경우만 | ✅ 매우 좋음 |
DTO 매핑 | ⚠ new 키워드 필요 | ❌ 불가 | ✅ Projections 지원 |
유지보수 | ❌ 오타 위험 높음 | ◯ 보통 | ✅ 리팩토링 내성 강함 |
📌 결론
QueryDSL은 단순히 "동적 쿼리를 편하게 만든다"를 넘어, 타입 안정성, 리팩토링 안전성, 쿼리 가독성, 생산성 향상을 제공합니다. 스스로 백엔드 개발을 책임 졌을 때, 코드 리뷰의 부재나 작업 속도 등의 문제점을 해결해주기 때문에 선택했습니다.
1. Infra Architecture 구조
2. 왜 각 서버를 분리했는가?
API 서버에서 코드 실행까지 맡게 되면, 요청 처리 스레드와 코드 실행 스레드 간 리소스 경쟁 발생의 위험이 있습니다. 이는, 코드 실행 시점까지 응답 대기가 발생하고 사용자 입장에서 지연이 발생한다고 느낄 수 있습니다.
특히, 코드 실행은 I/O 작업이 빈번하므로 API 서버에 스레드 블로킹 위험을 예상하고 API 서버와 Worker 서버를 분리해야한다는 판단을 했습니다.
그 외, 보안 아키텍처 강화의 목적도 있습니다.
3. Messaging Queue 선택 이유
비동기 처리 효율성
코드 실행은 완료 시간이 불확실합니다. 코드에 따라 결과를 출력하기까지 장시간이 소요될 수도 있습니다. 즉, 동기 처리에는 부적합하다고 판단했습니다. 이 때, 메시지 큐 방식을 사용한다면 코드 실행 API 요청 후 즉시 응답을 반환하면서 스레드 점유 시간은 최소화하고 API 서버 리소스를 효율적으로 사용 가능합니다. 그리고 사용자는 코드가 실행이 완료되는 시점에서 비동기로 응답을 받을 수 있습니다.
시스템 안정성과 확장성
메시지 큐는 버퍼 역할을 해줍니다. 트래픽 폭주 시에도 안정성 확보가 가능하고 Worker 서버에서 장애 발생 시에도 메시지 유실 없이 복구 후 처리가 가능합니다. 또, 분리가 되면서 사용자가 증가하더라도 Worker 서버만 수평 확장(Scale-Out)하기에 용이한 장점이 있습니다. 실제로, 코드 실행에 대한 메트릭을 파싱하는 과정에서 장애 상황이 발생했음에도 RabbitMQ가 메시지를 안전히 보관하는 것을 확인할 수 있었습니다.
4. RabbitMQ를 선택한 이유
당시 메시징 시스템에 대해 깊은 이해가 있었던 것은 아니었습니다.
Kafka, SQS, Redis Pub/Sub 등 다양한 메시징 솔루션이 있다는 건 알았지만,
제가 실질적으로 학습해본 경험이 있는 건 RabbitMQ 뿐이었습니다.
MVP에서는 빠른 생산이 중요하기 때문에 이해도가 가장 높은 기술 스택을 선택하는 것이 최선이라는 판단을 했습니다. 이 부분에 대해서는 리팩토링 과정에서 다양한 메시징 시스템을 학습하고 교체가 필요하면 교체할 예정입니다.
RabbitMQ를 선택하고 나서 실제 서비스에서 다음과 같은 강점을 느낄 수 있었습니다.
결론1 - 요구사항 구현에 성공했다.
RabbitMQ를 사용함으로써 우리는 보안, 성능, 확장성, 안정성이라는 4가지 요소를 모두 만족하는 아키텍처를 구현할 수 있었습니다. 특히 Private Subnet 기반의 Worker 서버를 운영하면서도, 완전한 비동기 통신과 메시지 보장성을 확보한 것이 주요한 선택 이유였습니다.
결론2 – 처음 선택이 완벽하지 않아도 괜찮다
RabbitMQ는 저의 경험과 역량 안에서 가장 확실한 선택지였고, 결과적으로도 안정성과 생산성, 운영 편의성 면에서 충분히 좋은 선택이었습니다.
서비스가 성장하거나 요구사항이 바뀌면, Kafka나 SQS로의 전환도 고려할 수 있겠지만, 지금 이 순간 가장 효율적으로 문제를 해결할 수 있는 도구를 선택하는 것이 합리적인 기술 선택이라고 생각합니다.
🤔 단순히 ProcessBuilder로 실행해도 되지 않을까?
Java에서 코드를 실행하려면 ProcessBuilder, Runtime.exec()등의 방법으로도 충분히 가능합니다. 그러나 실제 서비스에서 불특정 다수가 업로드하는 코드를 실행해야 하는 상황이라면 어떨까요?
다음과 같은 문제들이 발생합니다.
문제 | 설명 |
---|---|
보안 위험 | 사용자 코드가 서버 자원 접근, 파일 시스템 훼손, 무한 루프 등의 행위를 할 수 있음 |
환경 오염 | 실행 중 생성된 파일, 프로세스, 메모리, 포트 등을 적절히 격리/정리하기 어려움 |
언어별 실행 환경 관리 | 다양한 언어마다 실행 명령, 런타임, 종속성, PATH 등이 달라 운영이 복잡해짐 |
자원 회수 불안정 | 코드 실행 중 예외/장시간 실행 시 서버 자원 고갈 위험 |
코드 레벨에서 Worker 서버에 의도적으로 악 영향을 미칠 수 있습니다.
이것을 해결하기 위한 방법 중 하나가 Sandbox 환경입니다.
🔐 Sandbox란 무엇인가?
Sandbox는 운영체제나 시스템의 나머지 영역과 격리된 실행 환경을 의미합니다.
주로 신뢰할 수 없는 코드나 프로그램을 안전하게 실행하기 위해 사용됩니다.
📦 샌드박스의 주요 특징
🐳 Docker Sandbox란?
Docker Sandbox는 이 “샌드박스” 개념을 컨테이너 기술로 구현한 형태입니다.
즉, Docker 컨테이너 안에서 코드를 실행함으로써 물리적 서버나 호스트 시스템으로부터 완전히 분리된 공간에서 동작하게 됩니다.
제공 | 설명 |
---|---|
완전한 격리 | 컨테이너 내부에서 실행되는 프로세스는 외부 시스템 리소스에 접근할 수 없음 |
경량 & 빠른 실행 | VM에 비해 훨씬 가볍고 빠름 |
멀티언어 대응 | 각 언어별 Docker Image를 사용해 다양한 코드 실행 가능 |
자원 제어 | --memory, --cpus, --pids-limit 등으로 실행 환경 자원 제한 가능 |
보안 강화 | no network, read-only, seccomp, AppArmor 설정으로 보안성 향상 |
완전한 격리 환경 (Isolation)
언어 실행 환경 통합 관리
매번 새로운 환경 (Clean Slate)
경량화 및 빠른 스핀업
리소스 제어 및 제한
확장성과 유지보수
🧠 왜 Docker Sandbox를 선택했는가?
코딩 스터디 플랫폼은 코드를 실행하는 서비스가 제공됩니다.
즉, 아래와 같은 특징을 가지고 있습니다.
이 세 가지 조건을 만족하기에 Docker Sandbox는 최적의 선택이었습니다.
💡 결론
Docker Sandbox를 사용함으로써, 코코무는 코드 실행이라는 고위험 작업을
안전하게, 유연하게, 확장 가능하게 처리할 수 있었습니다.
특히 "매번 새로운 환경에서 실행되어야 한다"는 요구사항과
"다양한 언어의 코드 실행을 단일 방식으로 처리하고 싶다"는 문제를
가장 깔끔하게 해결할 수 있는 방법이 바로 Docker였습니다.
🧩 1. 기능 요구사항
MVP 단계에서의 필수 요구
추후 계획된 기능
실시간 채팅 기능 도입 예정
🧠 2. 처음엔 SSE(Server-Sent Events)를 고려
SSE는 클라이언트에게 서버가 실시간으로 단방향 알림을 전송할 수 있는 기술로, 방장의 시작/피드백 알림을 모든 사용자에게 전달하기에 적절한 방식이라고 생각합니다.
✅ HTTP 기반 → 브라우저 친화적
✅ 구현 간단, 연결당 리소스 소모 적음
❌ 단방향 통신만 가능 → 채팅이나 유저간 상호작용에는 부적합
하지만, 추후 채팅 기능을 도입할 때 WebSocket을 추가한다면 SSE + WebSocket 두 가지의 리소스가 낭비되는 문제가 발생할 것이라고 예상했습니다.
🔄 3. 왜 STOMP를 도입했는가?
“실시간 알림 + 채팅 기능까지 확장 가능한 통신 기술이 필요했다.”
기준 | SSE | Web Socket | STOMP |
---|---|---|---|
실시간 알림 | ✅ | ✅ | ✅ |
채팅 기능 | ❌ 불가 | ✅ | ✅ |
단방향/양방향 | 단방향 | 양방향 | 양방향 |
메시지 주제 관리 | ❌ 직접 구현 필요 | ❌ | ✅ topic 기반 pub/sub |
Spring 연동 | ✅ 기본 제공 | ✅ | ✅ 고급 기능 (Interceptor, @MessageMapping 등) |
초기에는 실시간 알림 기능(SSE)만을 고려했지만, 팀원들과의 논의 끝에 향후 채팅 기능이 추가될 예정이라는 방향성이 정해졌습니다. 그에 따라, 양방향 통신이 가능한 WebSocket 기반 구조로 전환하는 것이 자연스러웠습니다.
❓ 그런데 의문이 생겼습니다
"단순 WebSocket으로 충분하지 않을까?"
비교표만 보면 STOMP는 WebSocket과 비교해 메시지 주제(topic) 관리만 가능한 것처럼 보일 수 있습니다.
하지만 실제로는 서비스 구조에 큰 차이를 만드는 포인트가 있습니다.
🎯 왜 코코무는 STOMP를 선택했는가?
✅ 결론
단순 메시지 전송만이 아닌 명확한 주제(topic) 기반의 구독/전송 구조, 클린한 인증 처리, 추후 확장성까지 고려했을 때 코코무 프로젝트에는 STOMP가 WebSocket보다 훨씬 적합한 선택이었습니다.
코코무에서 사용될 기술 스택을 명확한 목적과 합리성을 도출하고 선택한 과정은 짧은 기간 내 퀄리티 높은 MVP를 구현할 수 있는 결과를 가져왔습니다.