[COCOMU] 기술 스택 선정

이프·2025년 4월 13일
0

tech-blog

목록 보기
2/4

기술 선정

코코무 프로젝트는 코딩 스터디 플랫폼으로 스터디 회원 간 코딩 스터디를 진행할 수 있도록 환경을 제공하고 있습니다.

MVP ? (핵심 기능은 Bold)

  • 코코무는 회원가입 및 로그인 기능을 통해 플랫폼에 참여할 수 있습니다.
  • 코코무 회원은 스터디를 생성 및 가입을 할 수 있습니다.
  • 코코무 회원 및 비회원은 생성된 스터디를 필터링 기반 조회할 수 있습니다.
  • 코코무 스터디원은 코딩 스페이스를 생성 및 참여를 할 수 있습니다.
  • 코코무 스터디원은 스터디 내 코딩 스페이스를 필터링 기반 조회할 수 있습니다.
  • 코딩 스터디 방장은 코딩 스터디를 시작/피드백/종료 상태를 관리할 수 있습니다.
  • 코딩 스터디원은 코딩 스페이스에서 코드를 실행 및 제출 할 수 있습니다.
  • 코딩 스터디원은 코딩 스페이스에서 테스트 케이스를 추가할 수 있습니다.
  • 코딩 스터디원은 실행한 코드 및 제출 결과를 확인할 수 있습니다.
  • 코딩 스터디원은 피드백 페이지에서 각 스터디원의 코드에 대해 코드 동시 편집을 통해 피드백을 해줄 수 있습니다.
  • 그 외 부가 기능도 존재합니다.

위 기능을 토대로 코코무 백엔드는 아래와 같은 기술 스택을 선정했습니다.

  • Spring Boot
  • QueryDSL
  • RabbitMQ
  • Docker SandBox
  • STOMP

(더 자세한 기술 스택은 깃허브에서 확인해주세요)


왜 Spring Boot를 선택했는가 – 혼자 백엔드를 맡으며 얻은 결론

💡 선택 배경
혼자서 백엔드 전체를 구현해야 했기 때문에, 다음과 같은 요구사항을 만족할 수 있는 프레임워크가 필요했습니다.

  • API 기반 핵심 기능 구현
  • DB 연동 및 ORM 관리
  • 테스트 및 배포 자동화
  • WebSocket, MQ, S3 등 다양한 라이브러리 연동
  • MVP 단계에서의 빠른 생산성 확보

✅ Spring Boot를 선택한 주요 이유
Web API 구현을 위한 MVC 구조 지원

  1. Jackson, Servlet, Web MVC 등을 자동 구성
    spring-boot-starter-web 사용 시, RESTful API 구현이 표준화되어 있어 유지보수 용이해지는 장점이 있었습니다.

  2. ORM 및 DB 연동의 표준

  • JPA → Spring Data JPA로 추상화 → Repository 인터페이스만으로 CRUD 구현 가능
  • 테스트용 H2, 운영용 MySQL을 profile로 쉽게 전환 가능
  1. 라이브러리 통합 관리
  • STOMP, RabbitMQ, S3 연동 등 다양한 기술을 Spring Boot starter로 통합
  • 개별 설정 부담 없이 의존성만 추가하면 자동 구성됨
  1. Auto Configuration으로 인한 생산성 향상
  • 복잡한 설정 최소화
  • application.yml에 기본 설정만으로 대부분 적용 가능
  1. 테스트 환경 구성의 편의성
  • spring-boot-starter-test로 JUnit, Mockito, Spring Test 등 자동 통합
  • REST Assured, MockWebServer 등을 통한 다양한 테스트를 지원

✨ 결론

  • Spring Boot는 다양한 기술을 빠르게 통합하고, 자동 구성(Auto Configuration)을 통해 MVP 단계에서 높은 생산성을 제공합니다.
  • 혼자서 백엔드 전체를 구현하는 상황에서는 표준화된 설계 방식 덕분에 복잡함을 감소하고 이후 유지보수 및 확장도 수월한 장점이 있으므로 선택했습니다.

왜 QueryDSL을 선택했는가? - 프로젝트에서 느낀 필요성

QueryDSL 선택 이유 요약

  1. 동적 쿼리 구성의 간결성
    복잡한 조건 분기(if, null, List 포함 여부 등)를 깔끔하게 작성 가능
    BooleanBuilder, where() 체이닝 등으로 가독성 향상

  2. 컴파일 시점 오류 검출
    JPQL은 문자열 기반이라 오타, 컬럼명 오류는 런타임에서야 발견됨
    QueryDSL은 도메인 기반 Q클래스를 사용 → 컴파일 단계에서 오류 탐지 가능
    → 리팩토링에 강하고 안정성 향상

  3. IDE 자동완성 지원
    Q클래스를 통한 코드 자동완성 → 쿼리 작성 속도 증가
    JPQL 대비 훨씬 빠른 생산성 확보

  4. 타입 안전(Type-safe)
    필드 이름, 타입, 조인 대상이 정적 타입으로 강제됨
    위험한 런타임 NullPointerException 리스크가 적음

  5. GroupBy, SubQuery, Projection 등 다양한 기능 지원
    GroupBy, Projections.fields, case when 등 복잡한 쿼리 작성 가능
    DTO 매핑도 바로 처리 가능 (→ Projections.fields(UserDto.class, ...))

  6. 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);

❌ 단점:

  • 문자열로 필드명 작성 → 오타 나도 런타임 오류
  • 복잡한 조건 분기(case when, optional null 등)는 작성 어려움
  • 쿼리 길어지면 가독성 최악
  • DTO 생성자 변경 시 쿼리까지 수정 필요

② Spring Data JPA 메서드 이름 기반 👎

List<CodingSpaceTab> findByCodingSpaceIdAndStatus(Long codingSpaceId, TabStatus status);

❌ 단점:

  • 단순 CRUD까지만 유효
  • Join, Projection, GroupBy 등 복잡한 쿼리 불가능
  • DTO 반환 불가 → 엔티티를 그대로 노출하거나 매핑 작업 필요

③ 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();

✅ 장점:

  • 타입 안전 (user.nickname 등은 오타 시 컴파일 오류)
  • IDE 자동완성, 리팩토링 내성 강함
  • case, group by, subquery, list-to-map 반환 등 고급 쿼리 가능
  • DTO 필드와 매핑 명시적으로 설정 가능

🧠 핵심 요약

비교JPQLSpring Data JPAQueryDSL
타입 안정성❌ 문자열 기반◯ 제한적 지원✅ 컴파일 타임 체크
복잡 쿼리 지원❌ 불편함❌ 불가✅ 자유로움
가독성❌ 나쁨◯ 짧은 경우만✅ 매우 좋음
DTO 매핑⚠ new 키워드 필요❌ 불가✅ Projections 지원
유지보수❌ 오타 위험 높음◯ 보통✅ 리팩토링 내성 강함

📌 결론
QueryDSL은 단순히 "동적 쿼리를 편하게 만든다"를 넘어, 타입 안정성, 리팩토링 안전성, 쿼리 가독성, 생산성 향상을 제공합니다. 스스로 백엔드 개발을 책임 졌을 때, 코드 리뷰의 부재나 작업 속도 등의 문제점을 해결해주기 때문에 선택했습니다.


왜 RabbitMQ를 도입했는가? – Private Subnet 기반 아키텍처에서 메시징 방식을 선택한 이유

1. Infra Architecture 구조

  • API서버와 Worker 서버는 VPC 내부의 Private Subnet에 위치
  • API 서버 ↔ Worker 간 직접 통신 불가
  • 대신, 메시지 큐(RabbitMQ)를 통해 비동기 메시지 처리 방식 채택

2. 왜 각 서버를 분리했는가?
API 서버에서 코드 실행까지 맡게 되면, 요청 처리 스레드와 코드 실행 스레드 간 리소스 경쟁 발생의 위험이 있습니다. 이는, 코드 실행 시점까지 응답 대기가 발생하고 사용자 입장에서 지연이 발생한다고 느낄 수 있습니다.

특히, 코드 실행은 I/O 작업이 빈번하므로 API 서버에 스레드 블로킹 위험을 예상하고 API 서버와 Worker 서버를 분리해야한다는 판단을 했습니다.

그 외, 보안 아키텍처 강화의 목적도 있습니다.

  • Worker 서버는 Private Subnet에 배치 → 외부로부터 격리
  • REST API 방식은 인바운드 포트 개방 필요 → 보안 위협 증가
  • 메시지 큐는 단방향(OutBound) 통신만으로도 처리 가능

3. Messaging Queue 선택 이유

  • 비동기 처리 효율성
    코드 실행은 완료 시간이 불확실합니다. 코드에 따라 결과를 출력하기까지 장시간이 소요될 수도 있습니다. 즉, 동기 처리에는 부적합하다고 판단했습니다. 이 때, 메시지 큐 방식을 사용한다면 코드 실행 API 요청 후 즉시 응답을 반환하면서 스레드 점유 시간은 최소화하고 API 서버 리소스를 효율적으로 사용 가능합니다. 그리고 사용자는 코드가 실행이 완료되는 시점에서 비동기로 응답을 받을 수 있습니다.

  • 시스템 안정성과 확장성
    메시지 큐는 버퍼 역할을 해줍니다. 트래픽 폭주 시에도 안정성 확보가 가능하고 Worker 서버에서 장애 발생 시에도 메시지 유실 없이 복구 후 처리가 가능합니다. 또, 분리가 되면서 사용자가 증가하더라도 Worker 서버만 수평 확장(Scale-Out)하기에 용이한 장점이 있습니다. 실제로, 코드 실행에 대한 메트릭을 파싱하는 과정에서 장애 상황이 발생했음에도 RabbitMQ가 메시지를 안전히 보관하는 것을 확인할 수 있었습니다.

4. RabbitMQ를 선택한 이유

당시 메시징 시스템에 대해 깊은 이해가 있었던 것은 아니었습니다.
Kafka, SQS, Redis Pub/Sub 등 다양한 메시징 솔루션이 있다는 건 알았지만,
제가 실질적으로 학습해본 경험이 있는 건 RabbitMQ 뿐이었습니다.

MVP에서는 빠른 생산이 중요하기 때문에 이해도가 가장 높은 기술 스택을 선택하는 것이 최선이라는 판단을 했습니다. 이 부분에 대해서는 리팩토링 과정에서 다양한 메시징 시스템을 학습하고 교체가 필요하면 교체할 예정입니다.

RabbitMQ를 선택하고 나서 실제 서비스에서 다음과 같은 강점을 느낄 수 있었습니다.

  • 설정이 간단하고 빠름, 로컬 테스트부터 운영 환경 배포까지 진입장벽이 낮았음
  • 메시지 영속성(persistence) 보장 → Worker 장애 상황에서도 유실 없음
  • acknowledgement, retry, dead-letter 처리 등 기본으로 제공하는 기능이 강력
  • 관리 UI가 직관적 → 운영/모니터링이 쉬웠음

결론1 - 요구사항 구현에 성공했다.
RabbitMQ를 사용함으로써 우리는 보안, 성능, 확장성, 안정성이라는 4가지 요소를 모두 만족하는 아키텍처를 구현할 수 있었습니다. 특히 Private Subnet 기반의 Worker 서버를 운영하면서도, 완전한 비동기 통신과 메시지 보장성을 확보한 것이 주요한 선택 이유였습니다.

결론2 – 처음 선택이 완벽하지 않아도 괜찮다
RabbitMQ는 저의 경험과 역량 안에서 가장 확실한 선택지였고, 결과적으로도 안정성과 생산성, 운영 편의성 면에서 충분히 좋은 선택이었습니다.
서비스가 성장하거나 요구사항이 바뀌면, Kafka나 SQS로의 전환도 고려할 수 있겠지만, 지금 이 순간 가장 효율적으로 문제를 해결할 수 있는 도구를 선택하는 것이 합리적인 기술 선택이라고 생각합니다.


왜 Docker Sandbox를 사용했는가? – 안전하고 유연한 코드 실행 환경을 만들기 위해

🤔 단순히 ProcessBuilder로 실행해도 되지 않을까?
Java에서 코드를 실행하려면 ProcessBuilder, Runtime.exec()등의 방법으로도 충분히 가능합니다. 그러나 실제 서비스에서 불특정 다수가 업로드하는 코드를 실행해야 하는 상황이라면 어떨까요?

다음과 같은 문제들이 발생합니다.

문제설명
보안 위험사용자 코드가 서버 자원 접근, 파일 시스템 훼손, 무한 루프 등의 행위를 할 수 있음
환경 오염실행 중 생성된 파일, 프로세스, 메모리, 포트 등을 적절히 격리/정리하기 어려움
언어별 실행 환경 관리다양한 언어마다 실행 명령, 런타임, 종속성, PATH 등이 달라 운영이 복잡해짐
자원 회수 불안정코드 실행 중 예외/장시간 실행 시 서버 자원 고갈 위험

코드 레벨에서 Worker 서버에 의도적으로 악 영향을 미칠 수 있습니다.
이것을 해결하기 위한 방법 중 하나가 Sandbox 환경입니다.

🔐 Sandbox란 무엇인가?
Sandbox는 운영체제나 시스템의 나머지 영역과 격리된 실행 환경을 의미합니다.
주로 신뢰할 수 없는 코드나 프로그램을 안전하게 실행하기 위해 사용됩니다.

📦 샌드박스의 주요 특징

  • 격리성(Isolation): 외부 시스템 리소스 접근 불가
  • 제한된 권한: 제한된 파일 접근, 제한된 메모리/CPU 사용
  • 안전한 실험 공간: 테스트 또는 검증 목적의 코드 실행에 이상적
  • 실행 후 정리: 시스템에 흔적 없이 제거 가능

🐳 Docker Sandbox란?
Docker Sandbox는 이 “샌드박스” 개념을 컨테이너 기술로 구현한 형태입니다.
즉, Docker 컨테이너 안에서 코드를 실행함으로써 물리적 서버나 호스트 시스템으로부터 완전히 분리된 공간에서 동작하게 됩니다.

제공설명
완전한 격리컨테이너 내부에서 실행되는 프로세스는 외부 시스템 리소스에 접근할 수 없음
경량 & 빠른 실행VM에 비해 훨씬 가볍고 빠름
멀티언어 대응각 언어별 Docker Image를 사용해 다양한 코드 실행 가능
자원 제어--memory, --cpus, --pids-limit 등으로 실행 환경 자원 제한 가능
보안 강화no network, read-only, seccomp, AppArmor 설정으로 보안성 향상

완전한 격리 환경 (Isolation)

  • 각 코드 실행은 독립된 컨테이너에서 수행
  • 코드가 서버 파일 시스템, 메모리, 네트워크에 직접 접근 불가

언어 실행 환경 통합 관리

  • 각 언어에 맞는 Docker Image만 준비하면 어떤 언어든 실행 가능
  • ex) python, node, gcc, openjdk, rust, go 등

매번 새로운 환경 (Clean Slate)

  • 컨테이너는 실행 후 종료되므로 이전 실행 흔적 없이 깨끗한 상태 유지
  • 상태 의존성 없는 테스트 구현 가능

경량화 및 빠른 스핀업

  • Code Execution 전용 Minimal Image 사용 (alpine, slim 등)
  • 초단위 내 컨테이너 생성/종료 → 빠른 응답 속도 유지

리소스 제어 및 제한

  • Docker의 --memory, --cpus, --pids-limit 옵션으로 자원 남용 방지
  • 무한 루프, 폭주 코드도 시스템 전체에 영향 주지 않음

확장성과 유지보수

  • 코드 실행 로직과 서버 로직을 분리 → 유지보수, 배포 독립성 확보
  • 클러스터링, 모니터링, 오토스케일링 연계도 용이

🧠 왜 Docker Sandbox를 선택했는가?
코딩 스터디 플랫폼은 코드를 실행하는 서비스가 제공됩니다.
즉, 아래와 같은 특징을 가지고 있습니다.

  • 보안이 중요한 코드 실행 환경
  • 실행 환경을 매번 초기화해야 하는 경우
  • 다양한 언어 지원이 필요한 서비스

이 세 가지 조건을 만족하기에 Docker Sandbox는 최적의 선택이었습니다.

💡 결론
Docker Sandbox를 사용함으로써, 코코무는 코드 실행이라는 고위험 작업을
안전하게, 유연하게, 확장 가능하게 처리할 수 있었습니다.
특히 "매번 새로운 환경에서 실행되어야 한다"는 요구사항과
"다양한 언어의 코드 실행을 단일 방식으로 처리하고 싶다"는 문제를
가장 깔끔하게 해결할 수 있는 방법이 바로 Docker였습니다.


왜 STOMP(WebSocket 기반)를 선택했는가? – 알림과 채팅을 동시에 고려한 설계

🧩 1. 기능 요구사항

MVP 단계에서의 필수 요구

  • 방장이 코딩 스페이스를 생성
  • 스터디원이 입장할 때마다 대기방 목록에 실시간 표시
  • 방장이 시작하면, 입장한 모든 유저에게 “시작” 알림 전송 + 페이지 전환
  • 테스트 케이스 추가 시 "테스트 케이스 추가" 알림 전송 + 테스트 케이스 추가
  • 피드백 시 "피드백" 알림 전송 + 페이지 전환
  • 종료 시 "종료" 알림 전송 + 페이지 전환

추후 계획된 기능

실시간 채팅 기능 도입 예정

🧠 2. 처음엔 SSE(Server-Sent Events)를 고려

SSE는 클라이언트에게 서버가 실시간으로 단방향 알림을 전송할 수 있는 기술로, 방장의 시작/피드백 알림을 모든 사용자에게 전달하기에 적절한 방식이라고 생각합니다.

✅ HTTP 기반 → 브라우저 친화적
✅ 구현 간단, 연결당 리소스 소모 적음
❌ 단방향 통신만 가능 → 채팅이나 유저간 상호작용에는 부적합

하지만, 추후 채팅 기능을 도입할 때 WebSocket을 추가한다면 SSE + WebSocket 두 가지의 리소스가 낭비되는 문제가 발생할 것이라고 예상했습니다.

🔄 3. 왜 STOMP를 도입했는가?
“실시간 알림 + 채팅 기능까지 확장 가능한 통신 기술이 필요했다.”

기준SSEWeb SocketSTOMP
실시간 알림
채팅 기능❌ 불가
단방향/양방향단방향양방향양방향
메시지 주제 관리❌ 직접 구현 필요✅ topic 기반 pub/sub
Spring 연동✅ 기본 제공✅ 고급 기능 (Interceptor, @MessageMapping 등)

초기에는 실시간 알림 기능(SSE)만을 고려했지만, 팀원들과의 논의 끝에 향후 채팅 기능이 추가될 예정이라는 방향성이 정해졌습니다. 그에 따라, 양방향 통신이 가능한 WebSocket 기반 구조로 전환하는 것이 자연스러웠습니다.

❓ 그런데 의문이 생겼습니다
"단순 WebSocket으로 충분하지 않을까?"

비교표만 보면 STOMP는 WebSocket과 비교해 메시지 주제(topic) 관리만 가능한 것처럼 보일 수 있습니다. 하지만 실제로는 서비스 구조에 큰 차이를 만드는 포인트가 있습니다.

🎯 왜 코코무는 STOMP를 선택했는가?

  1. 페이지별 단일 이벤트 구독 구조
    • 코딩 스페이스에서는 상태가 바뀌면 즉시 페이지 전환이 일어나야 함
    • 각 페이지는 단 하나의 주제만 구독하면 됨 → STOMP의 pub/sub 구조에 매우 적합
  2. WebSocket은 pub/sub을 직접 구현해야 함
    • 구독 주제 관리, 토픽 라우팅, 클라이언트 관리 등 모두 수동으로 처리해야 함
    • 개발 및 유지보수 비용이 STOMP 대비 높음
  3. 보안/인증 처리의 편리함
    • STOMP는 Spring AOP 기반의 ChannelInterceptor를 통해
    • Bearer Token 인증 처리를 구조적으로 통합할 수 있음

✅ 결론
단순 메시지 전송만이 아닌 명확한 주제(topic) 기반의 구독/전송 구조, 클린한 인증 처리, 추후 확장성까지 고려했을 때 코코무 프로젝트에는 STOMP가 WebSocket보다 훨씬 적합한 선택이었습니다.


느낀점

코코무에서 사용될 기술 스택을 명확한 목적과 합리성을 도출하고 선택한 과정은 짧은 기간 내 퀄리티 높은 MVP를 구현할 수 있는 결과를 가져왔습니다.

profile
if (이런 시나리오는 어떨까?) then(테스트로 검증하고 해결) else(다음 시나리오 고민)

0개의 댓글