I can do it(Feat. DEV)

[운동 데이터 최적화 삽질기 구조적 문제를 모르고 자질구레한 해결책만 찾았던 이야기 본문

개발자 모드/경험

[운동 데이터 최적화 삽질기 구조적 문제를 모르고 자질구레한 해결책만 찾았던 이야기

까짓거 해보자 개발자 2025. 9. 19. 12:15
728x90

개발자로 일하다 보면 종종 "아, 내가 이걸 완전히 잘못 이해하고 있었구나"라는 순간을 마주합니다. 저는 최근 운동 데이터 시각화 작업을 하면서 바로 그런 깨달음을 얻었습니다.

몇 달간 Django 쿼리셋 최적화, 인덱스 튜닝, 캐싱 등으로 성능을 개선하려고 씨름했지만 근본적인 한계에 부딪혔고, 뒤늦게 문제의 본질을 깨달았습니다. 애초에 데이터의 성격을 잘못 이해하고 있었던 것이었죠.

오늘은 이 실패담을 통해 OLAP와 OLTP의 차이, 그리고 올바른 문제 진단의 중요성에 대해 이야기해보겠습니다.

시작: 누군가 설계한 시스템을 이어받다

제가 맡은 프로젝트는 사용자들의 운동 데이터를 수집하고 시각화하는 플랫폼이었습니다. 이전 개발자가 구축해놓은 시스템은 전형적인 Django + PostgreSQL 구조였습니다.

# 기존에 설계된 모델들 - 예시

class WorkoutData(models.Model):
    timestamp = models.DateTimeField()
    heart_rate = models.IntegerField(null=True)
    speed = models.FloatField(null=True)  # km/h
    distance = models.FloatField(null=True)  # km
    elevation = models.FloatField(null=True)  # m
    ...

class WeightTraining(models.Model):
    exercise_name = models.CharField(max_length=100)
    sets = models.IntegerField()
    reps = models.IntegerField()
    weight = models.FloatField()  # kg
    ...

 

언뜻 보면 일반적인 Django 모델 같았고, 저는 "운동 데이터가 RDB에 저장되고 있으니까 이걸 사용하면 되겠지"라고 생각했습니다. 그저 기존 코드를 이어받아 시각화(그래프) 기능을 추가하는 작업이라고 생각했죠.

문제 발견: 시각화 쿼리가 너무 느리다

문제는 사용자 대시보드를 만들면서 본격적으로 드러났습니다. 기획에서 생각한 운동 통계에 대해서 보고 싶어하는 요구사항들:

  • 일별/월별 운동량 변화 추이(최대힘, 평균힘 등) - 그래프
  • 운동 모드별 운동 데이터 통계

이런 시각화를 위해 쿼리를 작성하는데... 생각보다 너무 복잡하고 느렸습니다.(운동 데이터 테이블에서 필요한 필드가 json 필드안에 있어 아래 코드와 유사하게 annotate로 커스텀 필드를 만들어 쿼리셋을 조회했어요)

# 예시 코드
class WorkoutAnalyticsService:
    def get_monthly_workout_stats(self, user_id: int, year: int) -> Dict:
        """
        월별 운동 통계 - 이 쿼리가 5초 넘게 걸렸다
        """
        sessions = WorkoutSession.objects.filter(
            user_id=user_id,
            start_time__year=year
        ).annotate(
            month=Extract('start_time', 'month'),
            duration=F('end_time') - F('start_time'),
            avg_heart_rate=Avg('workoutdata__heart_rate'),
            max_speed=Max('workoutdata__speed'),
            total_distance=Sum('workoutdata__distance')
        ).values('month').annotate(
            session_count=Count('id'),
            total_calories=Sum('calories_burned'),
            avg_duration=Avg('duration'),
            workout_types=ArrayAgg('workout_type', distinct=True)
        ).order_by('month')
        
        return list(sessions)

 

한달 치 운동 데이터만 조회하는데 10초가 넘게 걸리는 상황. 이건 분명히 문제가 있다고 생각했습니다.

첫 번째 시도: Django 최적화의 모든 것을 동원하다

당연히 제 첫 반응은 "쿼리 최적화 문제겠지"였습니다. Django 성능 최적화에 대한 모든 기법을 총동원했습니다.

 

1. 디버깅을 통해 근본적인 병목 지점을 찾아 다른 방법으로 리팩토링(이라 쓰고 노가다라고 읽는다..)

뜯어서 말하자면 각 API마다 요청이 느렸던 이유가 상이했습니다. json 필드에서 필요한 필드를 annotate로 새로운 필드로 만들어서 쿼리 시간이 오래걸린다던지, 특정 로직에서 대량의 데이터를 리스트로 변환하는 과정에서 메모리에 적재하는 시간이 오래걸린다던지.. 등 gpt와 끊임 없이 이야기하며 왠만한 최적화를 다 해보았습니다. 결과적으로는 1~2초대로 빨라진 API들도 있었지만 쿼리 요청이 복잡한 API는 여전히 5~10초는 걸렸습니다.

1. select_related와 prefetch_related 활용

# 예시
# N+1 쿼리 문제 해결 시도
def get_optimized_workout_sessions(self, user_id: int):
    return WorkoutSession.objects.filter(
        user_id=user_id
    ).select_related('user').prefetch_related(
        'workoutdata_set',
        'weighttraining_set'
    )

2. 데이터베이스 인덱스 추가

-- 추가한 인덱스들
CREATE INDEX idx_workout_session_user_time ON workout_session(user_id, start_time);
CREATE INDEX idx_workout_data_session_timestamp ON workout_data(session_id, timestamp);
CREATE INDEX idx_workout_data_heart_rate ON workout_data(heart_rate) WHERE heart_rate IS NOT NULL;
CREATE INDEX idx_workout_session_type_time ON workout_session(workout_type, start_time);

 

sql 쿼리문으로 인덱스를 추가하지 않고 장고 모델에 설정을 해서 json필드에 인덱스를 설정을 해서 테스트를 했지만...

결국 풀 스캔이랑 크게 다르지 않은 성능으로 테스트는 실패했죠 ㅠ

3. 집계 테이블까지 도입

을 하려고 했으나 일단 시간에 떠밀려 보류했습니다.
# 예시
# 사전 집계된 데이터를 저장하는 테이블
class DailyWorkoutSummary(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    date = models.DateField()
    total_sessions = models.IntegerField(default=0)
    total_calories = models.IntegerField(default=0)
    total_duration = models.DurationField(default=timedelta(0))
    avg_heart_rate = models.FloatField(null=True)
    workout_types = models.JSONField(default=list)
    
    class Meta:
        unique_together = ['user', 'date']

결과: 개선은 되었지만 근본적 한계는 여전했다

이런 최적화들로 성능은 어느 정도 개선되었습니다:

  • 5~10초 → 2~3초 또는 10초 이상 → 5 ~ 10초 정도로 단축

하지만 여전히 한계가 명확했습니다:

 

1. 여전히 느린 실시간 조회

# 예시
# 여전히 느린 쿼리들
def get_workout_intensity_analysis(self, user_id: int):
    """
    운동 강도별 분석
    """
    return WorkoutData.objects.filter(
        session__user_id=user_id
    ).annotate(
        intensity=Case(
            When(heart_rate__lt=120, then=Value('Low')),
            When(heart_rate__lt=150, then=Value('Moderate')),
            When(heart_rate__lt=180, then=Value('High')),
            default=Value('Very High')
        )
    ).values('intensity', 'session__workout_type').annotate(
        total_time=Sum('session__end_time') - Sum('session__start_time'),
        avg_heart_rate=Avg('heart_rate')
    )

2. 복잡한 json 구조에서 조회 속도 느림

  • json 필드 안에 필드를 불러오는 속도가 느림
  • 비정규화된 구조로 인한 복잡성

3. 확장성 문제

  • 사용자 수가 늘어날수록 선형적으로 증가하는 쿼리 시간
  • 데이터가 쌓일수록 더욱 느려지는 구조

깨달음의 순간: "운동 데이터는 애초에 OLAP 데이터였다"

몇 주간의 최적화 삽질을 하고 있는 와중에 새로운 백엔드 리더가 알려준 정보..OLAP

"잠깐.. 운동 데이터의 특성을 다시 한번 생각해보자."

운동 데이터의 실제 사용 패턴 분석

어떻게 데이터가 생성되는가?

  • 사용자가 운동을 완료하면 세트 당 운동 데이터가 생성됨
  • 운동 중에는 측정된 시계열로 속도, 위치, 힘 등의 데이터가 쌓임
  • 한 번 저장된 운동 데이터는 거의 수정되지 않음 (READ-heavy)

어떻게 데이터가 조회되는가?

  • 개별 운동 기록보다는 통계와 추세에 관심
  • "지난 달 총 몇 번 운동했지?", "이번 주 평균 심박수는?"
  • 복잡한 집계와 그룹핑이 주요 사용 패턴
  • 실시간성보다는 분석 결과의 정확성이 중요

데이터의 성격 자체가...

  • 시계열 데이터 (Time-series data)
  • 분석 중심 (Analytics-focused)
  • 읽기 중심 (Read-heavy)
  • 복잡한 집계 쿼리가 주요 워크로드

이때 깨달았습니다. "이거 완전히 OLAP 성격의 데이터잖아?"

OLAP vs OLTP: 이론적 배경을 뒤늦게 이해하다

OLTP (Online Transaction Processing)

  • 목적: 실시간 거래 처리 (주문, 결제, 예약 등)
  • 특징: 짧고 빠른 트랜잭션, 높은 동시성, ACID 보장
  • 쿼리: 단순한 CRUD 작업
  • 데이터 구조: 정규화된 관계형 구조
 
# OLTP 예시: 전형적인 거래 처리
@transaction.atomic
def create_order(user_id: int, items: List[Dict]) -> Order:
    """빠르고 정확한 주문 처리가 핵심"""
    order = Order.objects.create(user_id=user_id)
    for item in items:
        OrderItem.objects.create(
            order=order,
            product_id=item['product_id'],
            quantity=item['quantity']
        )
    return order

OLAP (Online Analytical Processing)

  • 목적: 데이터 분석과 의사결정 지원
  • 특징: 복잡한 분석 쿼리, 대용량 데이터 처리, 읽기 중심
  • 쿼리: 복잡한 집계, 조인, 그룹핑
  • 데이터 구조: 비정규화된 차원 모델링 (스타 스키마 등)
 
# OLAP 예시: 복잡한 분석 쿼리
def get_sales_analysis(year: int):
    """복잡한 집계와 분석이 핵심"""
    return SalesFact.objects.filter(
        date__year=year
    ).values('product_category', 'region').annotate(
        total_revenue=Sum('sales_amount'),
        avg_order_value=Avg('sales_amount'),
        customer_count=Count('customer_id', distinct=True),
        growth_rate=Window(
            expression=Lag('total_revenue', offset=1),
            partition_by=['product_category'],
            order_by=['date']
        )
    )

문제 진단: 왜 이걸 몰랐을까?

뒤돌아보니 명확한 신호들이 있었습니다:

1. 쿼리 패턴이 완전히 OLAP적이었음

# 내가 작성한 쿼리들을 다시 보니...
# - 복잡한 집계 함수 (SUM, AVG, COUNT)
# - 다중 테이블 조인
# - GROUP BY와 복잡한 조건문
# - 시계열 분석

# 이건 완전히 OLAP 쿼리 패턴이었다!
WorkoutData.objects.filter(
    session__user_id=user_id
).aggregate(
    zone_1=Count('id', filter=Q(heart_rate__lt=120)),
    zone_2=Count('id', filter=Q(heart_rate__range=(120, 150))),
    avg_heart_rate=Avg('heart_rate')
)

2. 데이터 사용 패턴이 완전히 분석 중심이었음

  • 사용자들은 "오늘 운동" 보다는 "이번 달 운동 통계"에 관심
  • 실시간 업데이트보다는 정확한 분석이 중요
  • 개별 레코드보다는 집계된 인사이트가 가치

3. 성능 병목이 전형적인 OLAP 문제였음

  • 복잡한 조인으로 인한 성능 저하
  • 대용량 데이터 스캔
  • 집계 연산의 오버헤드

깨달음: 근본적인 아키텍처 미스매치

결국 제가 몇 달간 씨름한 문제는 **"OLAP 성격의 데이터를 OLTP 구조에 우겨넣고 있었던 것"**이었습니다.

운동 데이터의 본질: OLAP (분석 중심)
기존 시스템 구조: OLTP (거래 처리 중심)
                 ↓
         구조적 미스매치!

아무리 Django 쿼리를 최적화하고, 인덱스를 추가하고, 캐싱을 도입해도 근본적인 한계가 있을 수밖에 없었던 것입니다.

마치 망치로 나사를 조이려고 하면서 "왜 나사가 안 조여지지?"라고 고민했던 격이었죠.

삽질에서 얻은 교훈들

1. 문제의 본질부터 파악하자

성능 문제가 생겼을 때 바로 기술적 해결책에 뛰어들기보다는, 먼저 데이터와 사용 패턴의 본질을 이해해야 합니다.

잘못된 접근: "쿼리가 느리네? 인덱스 추가하고 캐싱하자!"

올바른 접근: "이 데이터는 어떤 성격이고, 어떻게 사용되며, 어떤 아키텍처가 적합할까?"

2. 기존 코드를 맹신하지 말자

이전 개발자가 관계형 DB를 선택했다고 해서 그것이 반드시 최선의 선택은 아닙니다. 항상 의문을 가지고 검토해야 합니다.

3. 최적화 vs 리아키텍처의 구분

단순한 성능 최적화로 해결할 수 있는 문제와 아키텍처 자체를 바꿔야 하는 문제를 구분할 수 있어야 합니다.

4. 이론과 실무의 균형

OLAP/OLTP 같은 이론적 개념들이 실제로 왜 중요한지 체감할 수 있었습니다. 이론을 무시하고 경험만으로는 한계가 있습니다.

마무리: 올바른 진단의 중요성

몇 달간의 삽질은 결코 무의미하지 않았습니다. Django 성능 최적화 기법들을 깊이 학습할 수 있었고, 무엇보다 문제 진단의 중요성을 뼈저리게 깨달았습니다.

개발자로서 기술적 해결책에만 매몰되지 않고, 항상 한 발짝 뒤로 물러나서 문제의 본질을 파악하는 습관을 길러야겠다고 다짐했습니다.

728x90