소프트웨어 개발

애플리케이션 성능 개선

출제기준 세세항목: 알고리즘 · 소스코드 품질분석 도구 · 코드 최적화

핵심 요약

느린 프로그램은 어디서 느려질까요

애플리케이션이 느리다는 신고를 받으면 많은 사람이 곧바로 서버를 더 좋은 것으로 바꾸고 싶어집니다. 하지만 대부분의 경우 느린 이유는 코드 안에 있습니다. 같은 일을 필요 이상으로 반복하거나, 데이터가 늘어날수록 급격히 느려지는 방식으로 짜여 있거나, 쓸데없이 같은 계산을 여러 번 하고 있는 식입니다.

그래서 성능 개선은 '추측하지 말고 측정하라'에서 시작합니다. 먼저 테스트로 응답 시간이나 처리량을 재고, 어느 부분에서 시간이 가장 많이 드는지 찾아낸 다음 그곳을 고칩니다. 고친 뒤에는 다시 재서 정말 빨라졌는지 확인합니다. 이 항목에서 다루는 알고리즘, 품질분석 도구, 코드 최적화는 이 과정에서 쓰는 세 가지 무기라고 생각하면 됩니다.

알고리즘의 효율을 재는 자: 시간 복잡도

알고리즘은 문제를 푸는 절차입니다. 같은 문제라도 절차에 따라 걸리는 시간이 크게 달라집니다. 이를 비교하려고 쓰는 것이 시간 복잡도이고, 보통 빅오(Big-O) 표기로 나타냅니다. 빅오는 입력 데이터의 개수 n이 커질 때 실행 시간이 얼마나 빨리 늘어나는지를 가장 큰 항만 남겨 표현한 것입니다.

몇 가지를 감각으로 익혀 두면 좋습니다. O(1)은 데이터가 아무리 많아도 시간이 일정합니다. 배열에서 몇 번째 칸을 바로 꺼내는 경우입니다. O(log n)은 매번 절반씩 범위를 줄여 가는 경우로, 데이터가 두 배가 되어도 한 단계만 늘어납니다. O(n)은 데이터를 한 번씩 훑는 경우, O(n log n)은 효율적인 정렬이 보통 이 정도입니다. O(n²)은 데이터마다 다시 전체를 훑는 이중 반복으로, 데이터가 10배 늘면 시간은 약 100배 늘어납니다.

작은 데이터에서는 차이가 잘 보이지 않다가, 실제 서비스에서 데이터가 쌓이는 순간 O(n²) 코드가 갑자기 발목을 잡는 일이 많습니다. 그래서 성능 문제를 볼 때는 '지금 이 코드가 데이터 수에 따라 어떻게 늘어나는가'를 먼저 따져 봅니다.

정렬 알고리즘 한눈에 보기

버블 정렬 1회전 (오름차순)
처음62947
1단계[2][6]947교환
2단계2[6][9]47유지
3단계26[4][9]7교환
4단계264[7][9]교환
[ ]는 그 단계에서 비교한 두 칸. 1회전이 끝나면 가장 큰 값이 맨 뒤에 놓입니다.

정렬은 성능 이야기에서 가장 자주 등장하는 예입니다. 버블 정렬은 바로 옆 두 값을 비교해 순서가 틀리면 바꾸는 일을 끝까지 반복합니다. 한 바퀴(1회전)를 돌면 가장 큰 값이 맨 뒤로 '거품처럼' 떠오르고, 다음 회전에서는 그 자리를 빼고 같은 일을 합니다. 위 그림은 1회전 동안 값들이 어떻게 자리를 바꾸는지 보여 줍니다.

선택 정렬은 남은 값 중 가장 작은 값을 골라 앞자리와 바꾸는 일을 반복하고, 삽입 정렬은 앞쪽을 이미 정렬된 구역으로 보고 새 값을 알맞은 자리에 끼워 넣습니다. 이 세 가지는 이해하기 쉽지만 비교를 이중으로 반복하므로 O(n²)입니다. 다만 삽입 정렬은 데이터가 거의 정렬되어 있으면 매우 빨라져 O(n)에 가깝게 끝납니다.

퀵 정렬은 기준값(피벗)을 하나 정해 그보다 작은 값과 큰 값으로 나눈 뒤, 나뉜 양쪽을 같은 방식으로 다시 정렬합니다. 평균적으로 O(n log n)으로 빠르지만, 피벗이 매번 남은 값 가운데 가장 작거나 가장 큰 값이 되어 한쪽으로만 나뉘면 최악의 경우 O(n²)이 됩니다. 이미 정렬된 데이터에서 맨 앞 원소를 피벗으로 고르는 경우가 그런 예입니다. 기준은 피벗의 자리(위치)가 아니라 값의 크기입니다. 합병 정렬은 반으로 계속 나눴다가 정렬하며 합치는 방식, 힙 정렬은 힙이라는 트리 구조를 이용하는 방식으로, 둘 다 최악의 경우에도 O(n log n)을 유지합니다.

찾는 방법도 알고리즘입니다: 순차 탐색과 이진 탐색

데이터를 찾는 방법도 성능에 큰 영향을 줍니다. 순차 탐색은 처음부터 하나씩 확인하는 방법으로 O(n)입니다. 데이터가 정렬되어 있지 않아도 쓸 수 있다는 장점이 있습니다.

이진 탐색은 정렬된 데이터의 가운데 값을 먼저 보고, 찾는 값이 더 작으면 왼쪽 절반, 크면 오른쪽 절반만 남기는 일을 반복합니다. 한 번 볼 때마다 후보가 절반으로 줄기 때문에 O(log n)입니다. 대신 반드시 미리 정렬되어 있어야 합니다. 자주 찾는 데이터라면 한 번 정렬해 두고 이진 탐색을 쓰는 편이 전체적으로 이득인 경우가 많습니다.

소스코드 품질분석 도구: 사람 대신 코드를 살펴보는 도구

코드가 길어지면 사람이 눈으로 문제를 다 찾기 어렵습니다. 그래서 코드를 자동으로 검사해 주는 품질분석 도구를 씁니다. 크게 두 종류로 나뉩니다.

정적 분석 도구는 프로그램을 실행하지 않고 소스코드 자체를 읽어서 검사합니다. 코딩 규칙을 어긴 곳, 쓰이지 않는 변수, 너무 복잡한 함수, 같은 코드의 중복, 잠재적인 오류 패턴 등을 찾아 줍니다. 실행 전에 미리 걸러낼 수 있어 개발 초기에 특히 유용합니다. PMD, Checkstyle, cppcheck, SonarQube 같은 도구가 여기에 속합니다.

동적 분석 도구는 프로그램을 실제로 실행하면서 동작을 관찰합니다. 메모리를 잡아 놓고 놓아 주지 않는 메모리 누수, 실행 중에만 드러나는 오류, 실제로 시간이 많이 걸리는 구간 등을 찾는 데 강합니다. Valgrind, Avalanche 같은 도구가 대표적입니다.

두 방식은 서로 보완합니다. 정적 분석은 넓게 빨리 훑지만 실행 상황에 따라 달라지는 문제는 놓칠 수 있고, 동적 분석은 실제 상황을 정확히 보지만 실행해 본 경로만 검사할 수 있습니다.

코드 최적화: 읽기 좋은 코드가 고치기도 좋습니다

코드 최적화라고 하면 무조건 빠르게 만드는 기교를 떠올리기 쉽지만, 출발점은 읽기 쉽고 단순한 코드입니다. 구조가 깔끔해야 느린 부분이 눈에 띄고, 고쳐도 다른 곳이 깨지지 않습니다. 이런 코드를 클린 코드라고 부릅니다.

클린 코드가 지향하는 성질을 몇 가지로 정리할 수 있습니다. 누가 읽어도 쉽게 이해되는 가독성, 한 함수가 한 가지 일만 하도록 하는 단순성, 다른 코드에 미치는 영향을 줄이는 의존성 최소화(시험 용어로는 '의존성 배제'), 같은 코드를 여러 번 쓰지 않는 중복 최소화(시험 용어로는 '중복성 최소화'), 세부 내용은 감추고 상위에서는 흐름만 보이게 하는 추상화입니다. 앞에서 배운 결합도·응집도 원칙과도 같은 방향입니다.

반대로 피해야 할 코드도 있습니다. 실행 흐름이 이리저리 얽혀 따라가기 어려운 스파게티 코드, 너무 오래되었거나 설명이 없어 아무도 이해하지 못하는 외계인 코드가 대표적입니다. 이런 코드는 성능 문제를 찾기도, 고치기도 어렵습니다.

겉으로 보이는 동작은 그대로 두고 내부 구조만 더 좋게 바꾸는 작업을 리팩토링이라고 합니다. 성능 개선에서는 측정으로 찾은 느린 부분을 리팩토링으로 정리한 뒤, 필요하면 더 효율적인 알고리즘으로 바꾸고, 반복문 안에서 매번 하던 같은 계산을 밖으로 빼는 식의 조정을 더합니다. 그리고 다시 측정해 효과를 확인합니다.

실무에서는 이렇게 쓰여요

중복 검사를 O(n²)에서 O(n)으로

회원 목록에서 중복 이메일을 찾을 때, 회원을 두 명씩 짝지어 모든 쌍을 한 번씩 비교하면 이중 반복이라 O(n²)입니다. 회원이 1만 명이면 짝을 한 번씩만 비교해도 약 5천만 번에 이릅니다. 이미 본 이메일을 집합(Set)에 담아 두고 '이미 있는가'만 확인하면 한 번 훑는 것으로 끝나 O(n)에 가까워집니다.

배포 전에 정적 분석 돌리기

코드를 저장소에 올릴 때마다 정적 분석 도구가 자동으로 돌도록 설정해 두면, 사용하지 않는 변수나 너무 길어진 함수, 복사해 붙인 중복 코드를 리뷰 전에 미리 걸러낼 수 있습니다. 사람은 남은 시간에 설계와 로직 검토에 집중할 수 있습니다.

반복문 안의 같은 계산 밖으로 빼기

상품 1,000개의 할인가를 계산하는 반복문 안에서 매번 '오늘의 할인율'을 데이터베이스에서 다시 읽고 있었습니다. 할인율은 반복하는 동안 바뀌지 않으므로 반복문 밖에서 한 번만 읽어 두도록 고치자, 데이터베이스 요청이 1,000번에서 1번으로 줄었습니다.

총정리 목차로