빠른 코드만으로는 부족한 순간
앞 장에서는 알고리즘 복잡도를 다루었다. 복잡도는 입력 크기가 커질 때 코드가 얼마나 느려질지 예측하는 도구였다. 그런데 개발자가 마주치는 문제 중에는 입력 크기만으로 설명되지 않는 문제가 많다.
사용자가 이 버튼을 누를까? 검색 결과의 첫 번째 항목을 클릭할까? 이 메일은 스팸일까? 이 서버는 오늘 밤 장애를 낼까? 추천한 상품을 사용자가 좋아할까?
이 질문들에는 확실한 정답이 없다. 같은 사용자라도 아침과 밤의 행동이 다르고, 같은 검색어라도 사람마다 원하는 것이 다르며, 같은 에러 로그라도 실제 장애로 이어질 때도 있고 조용히 사라질 때도 있다. 여기서 필요한 것이 확률이다.
확률은 미래를 맞히는 마법이 아니다. 확률은 불확실한 상황을 숫자로 표현하는 언어다. 개발자에게 확률이 중요한 이유는 단순하다. 실무의 많은 판단이 0 또는 1이 아니라, 0.73이나 0.04 같은 값 위에서 이루어지기 때문이다.
스팸 필터는 메일이 스팸일 확률을 계산한다. 추천 시스템은 사용자가 어떤 콘텐츠를 좋아할 가능성을 추정한다. 검색 시스템은 이 문서가 사용자의 의도에 맞을 가능성을 점수화한다. 장애 예측 시스템은 특정 지표 패턴이 실제 장애로 이어질 위험을 계산한다.
확률을 배우면 개발자는 이런 질문을 조금 더 명확하게 바꿀 수 있다.
'이 사용자가 구매할까?'가 아니라 '이 사용자가 장바구니에 상품을 담은 뒤 24시간 안에 구매할 확률은 얼마인가?'라고 묻게 된다. '이 서버 위험한가?'가 아니라 '최근 10분 동안 오류율과 응답 시간이 이렇게 변했을 때 30분 안에 장애가 발생할 확률은 얼마인가?'라고 묻게 된다.
질문이 정확해지면 데이터도 정확히 모을 수 있고, 의사결정도 더 일관되게 만들 수 있다.

표본공간: 가능한 결과 전체를 먼저 정한다
확률을 이해하려면 먼저 표본공간이라는 말을 알아야 한다. 이름은 딱딱하지만 뜻은 간단하다. 표본공간은 어떤 실험이나 상황에서 나올 수 있는 모든 결과의 집합이다.
동전을 던진다면 표본공간은 {앞면, 뒷면}이다. 주사위를 던진다면 {1, 2, 3, 4, 5, 6}이다. 여기까지는 익숙하다. 그런데 개발자에게 더 중요한 표본공간은 로그와 사용자 행동 속에 있다.
예를 들어 쇼핑몰 서비스에서 한 사용자의 세션이 다음 중 하나의 결과로 끝난다고 해 보자.
| 결과 | 설명 |
|---|---|
bounce | 첫 화면만 보고 이탈 |
view_product | 상품 상세 페이지까지 방문 |
add_to_cart | 장바구니에 담음 |
purchase | 구매 완료 |
error | 오류 때문에 정상 종료 실패 |
이때 표본공간은 사용자의 세션이 끝날 수 있는 모든 결과다. 여기서 사건은 표본공간 안에서 우리가 관심을 갖는 결과의 묶음이다.
예를 들어 purchase는 구매 사건이다. add_to_cart 또는 purchase를 묶으면 '구매 의도가 강한 행동'이라는 사건으로 볼 수 있다. error는 장애 경험 사건이다. bounce는 이탈 사건이다.
확률은 결국 이렇게 계산한다.
확률 = 관심 있는 사건이 발생한 횟수 / 가능한 전체 경우의 횟수
100,000개의 세션 중 3,000개가 구매로 끝났다면 구매 확률은 3,000 / 100,000 = 0.03, 즉 3%다. 100,000개 중 500개가 오류로 끝났다면 오류 확률은 0.5%다.
여기서 중요한 것은 분모다. 확률을 계산할 때 가장 흔한 실수는 '전체'를 잘못 잡는 것이다. 전체 방문자를 기준으로 할 것인가, 로그인 사용자를 기준으로 할 것인가, 검색을 한 사용자만 기준으로 할 것인가, 장바구니에 상품을 담은 사용자만 기준으로 할 것인가에 따라 확률은 완전히 달라진다.
예를 들어 전체 세션 기준 구매율이 3%라고 하자. 그런데 장바구니에 상품을 담은 세션만 보면 구매율이 25%일 수도 있다. 두 숫자는 모두 맞을 수 있다. 다만 서로 다른 표본공간을 보고 있을 뿐이다.
확률을 다룰 때는 항상 먼저 물어야 한다.
내가 지금 보고 있는 전체 경우는 무엇인가?
이 질문을 빼먹으면 확률은 정확한 숫자처럼 보이는 착각이 된다.
로그 이벤트를 사건으로 바라보기
개발자는 이미 확률의 재료를 매일 다루고 있다. 바로 로그다.
로그에는 사용자가 무엇을 했는지, 시스템이 어떤 응답을 했는지, 어떤 에러가 발생했는지가 남는다. 확률의 관점에서 보면 로그 이벤트 하나하나는 사건의 후보가 된다.
예를 들어 다음과 같은 이벤트가 있다고 해 보자.
page_view
search
search_result_click
add_to_cart
purchase
login_fail
api_timeout
payment_error이 이벤트들을 단순히 기록으로만 보면 '무슨 일이 있었는지'만 알 수 있다. 하지만 확률로 보면 '앞으로 무슨 일이 일어날 가능성이 높은지'를 추정할 수 있다.
예를 들어 하루 로그가 다음과 같다고 하자.
| 사건 | 발생 횟수 |
|---|---|
| 전체 세션 | 100,000 |
| 검색을 한 세션 | 40,000 |
| 검색 결과를 클릭한 세션 | 18,000 |
| 장바구니에 담은 세션 | 6,000 |
| 구매한 세션 | 2,000 |
| 결제 오류가 발생한 세션 | 300 |
이 표에서 단순 확률을 계산할 수 있다.
전체 세션 중 검색을 한 확률은 40,000 / 100,000 = 40%다. 전체 세션 중 구매한 확률은 2,000 / 100,000 = 2%다. 전체 세션 중 결제 오류를 경험한 확률은 300 / 100,000 = 0.3%다.
하지만 실무에서 더 자주 필요한 질문은 단순 확률이 아니다. 우리는 보통 이런 질문을 한다.
검색을 한 사용자가 구매할 확률은 얼마인가? 검색 결과를 클릭한 사용자가 장바구니에 담을 확률은 얼마인가? 결제 오류를 경험한 사용자가 이탈할 확률은 얼마인가?
이런 질문에는 조건부 확률이 필요하다.
조건부 확률: 이미 알게 된 사실이 답을 바꾼다
조건부 확률은 어떤 정보가 이미 주어졌을 때 사건이 일어날 확률이다.
표기는 P(A | B)처럼 쓴다. 읽을 때는 'B가 일어났을 때 A가 일어날 확률'이라고 읽는다. 여기서 세로줄 |는 '조건으로' 정도로 이해하면 된다.
예를 들어 P(구매 | 검색)은 검색을 한 사용자가 구매할 확률이다. 전체 사용자 중 구매할 확률과 다르다. 왜냐하면 이미 '검색을 했다'는 정보가 주어졌기 때문이다.
조건부 확률의 핵심은 분모가 바뀐다는 데 있다.
P(구매 | 검색) = 검색도 하고 구매도 한 세션 수 / 검색을 한 세션 수
앞의 예에서 검색을 한 세션이 40,000개이고, 그중 구매까지 한 세션이 1,600개라고 하자. 그러면 검색 후 구매 확률은 1,600 / 40,000 = 4%다.
전체 구매율이 2%였는데 검색한 사용자만 보면 4%가 되었다. 검색 행동은 구매 가능성을 높이는 신호일 수 있다.
이번에는 검색 결과 클릭을 조건으로 보자. 검색 결과를 클릭한 세션이 18,000개이고, 그중 구매한 세션이 1,300개라면 다음과 같다.
P(구매 | 검색 결과 클릭) = 1,300 / 18,000 ≈ 7.2%
조건이 더 구체적이 되자 구매 확률은 더 올라갔다. 이것이 조건부 확률의 힘이다. 사용자가 이미 어떤 행동을 했는지 알면, 다음 행동의 가능성을 더 잘 추정할 수 있다.
추천 시스템, 검색 랭킹, 광고 타기팅, 이탈 예측, 장애 예측은 모두 조건부 확률의 세계에 있다. 핵심 질문은 대개 다음과 같은 형태다.
| 문제 | 조건부 확률로 바꾼 질문 |
|---|---|
| 추천 | 이 사용자가 이 상품을 봤을 때 클릭할 확률은? |
| 검색 | 이 검색어가 입력됐을 때 이 문서가 클릭될 확률은? |
| 광고 | 이 사용자 특성이 주어졌을 때 광고를 전환할 확률은? |
| 장애 예측 | 이 지표 패턴이 나타났을 때 장애가 발생할 확률은? |
| 보안 | 이 로그인 시도가 특정 패턴을 가질 때 공격일 확률은? |
개발자는 조건부 확률을 배울 때 공식을 외우기보다 '분모를 바꾸는 기술'로 이해하면 좋다. 전체 사용자를 볼 것인가, 검색한 사용자만 볼 것인가, 모바일에서 접속한 사용자만 볼 것인가, 최근 7일 안에 재방문한 사용자만 볼 것인가. 이 분모 선택이 분석의 품질을 결정한다.
같은 확률이라도 조건이 바뀌면 의미가 바뀐다
조건부 확률은 숫자를 해석하는 방식도 바꾼다.
전체 구매율이 2%인 쇼핑몰에서 어떤 캠페인의 구매율이 5%라면 좋아 보인다. 하지만 그 캠페인이 원래 구매 가능성이 높은 VIP 사용자에게만 노출되었다면 이야기가 달라진다. VIP 사용자의 평소 구매율이 8%였다면, 캠페인 구매율 5%는 오히려 나쁜 결과일 수 있다.
숫자는 혼자 말하지 않는다. 숫자가 나온 조건을 함께 봐야 한다.
검색에서도 마찬가지다. '노트북'이라는 검색어의 클릭률이 20%이고 '13인치 가벼운 개발자용 노트북'이라는 검색어의 클릭률이 50%라고 하자. 후자가 더 좋은 검색 결과라고 단정할 수 있을까? 꼭 그렇지는 않다. 두 검색어는 의도가 다르다. 짧고 넓은 검색어는 사용자의 의도가 불명확하고, 긴 검색어는 이미 원하는 것이 뚜렷하다. 조건이 다르기 때문에 확률도 다르게 해석해야 한다.
장애 예측도 비슷하다. 전체 요청 중 타임아웃 비율이 0.1%라면 작아 보인다. 하지만 특정 API, 특정 리전, 특정 배포 버전에서만 보면 타임아웃 비율이 8%일 수 있다. 전체 평균은 문제를 숨길 수 있다. 조건부 확률은 숨어 있는 위험을 꺼내는 도구다.
독립과 종속: 서로 영향을 주는가
확률에서 독립이라는 말은 두 사건이 서로 영향을 주지 않는다는 뜻이다.
사건 A와 B가 독립이라면 B가 일어났다는 사실을 알아도 A의 확률이 변하지 않는다.
P(A | B) = P(A)
예를 들어 공정한 동전을 두 번 던질 때 첫 번째 결과가 앞면이었다고 해서 두 번째 결과가 앞면일 확률이 바뀌지는 않는다. 두 번째도 여전히 1/2이다. 이때 두 사건은 독립이다.
하지만 개발 실무의 데이터에서는 독립이 아닌 경우가 훨씬 많다.
사용자가 상품 상세 페이지를 봤다는 사실은 구매 확률에 영향을 준다. 푸시 알림을 받은 사실은 앱 실행 확률에 영향을 줄 수 있다. 서버 CPU 사용률이 급격히 올라갔다는 사실은 응답 지연 확률에 영향을 줄 수 있다. 같은 데이터센터에 있는 서버들은 전원 장애나 네트워크 장애를 함께 겪을 수 있다.
두 사건이 서로 영향을 주거나, 같은 원인에 의해 함께 움직인다면 종속이라고 한다.
독립과 종속을 구분하는 일은 단순한 이론 문제가 아니다. 잘못 가정하면 실제 시스템 판단이 틀어진다.
예를 들어 서버 A와 서버 B가 각각 하루에 장애 날 확률이 1%라고 해 보자. 두 서버가 완전히 독립이라면 둘 다 동시에 장애 날 확률은 다음과 같이 계산할 수 있다.
0.01 × 0.01 = 0.0001, 즉 0.01%다.
아주 낮아 보인다. 그래서 '서버 두 대면 충분히 안전하겠네'라고 판단할 수 있다.
그런데 두 서버가 같은 랙, 같은 전원, 같은 네트워크 스위치, 같은 배포 파이프라인에 의존한다면 어떨까? 이 경우 장애는 독립이 아니다. 전원 문제가 생기면 둘 다 동시에 죽을 수 있고, 잘못된 배포가 나가면 둘 다 같은 버그를 가질 수 있다. 실제 동시 장애 확률은 독립 가정으로 계산한 0.01%보다 훨씬 높을 수 있다.
이런 이유로 안정적인 시스템은 단순히 서버 개수를 늘리는 데서 끝나지 않는다. 리전 분산, 가용 영역 분리, 배포 단계화, 장애 격리 같은 설계가 필요하다. 수학적으로 말하면 종속성을 줄여야 한다.
독립을 잘못 믿을 때 생기는 오류
데이터 분석과 모델링에서 독립을 잘못 가정하면 꽤 위험한 결과가 나온다. 대표적인 실수는 다음과 같다.
같은 사용자가 여러 번 남긴 이벤트를 서로 독립인 데이터처럼 취급한다.
같은 캠페인에 노출된 사용자 집단을 일반 사용자와 동일한 집단처럼 비교한다.
같은 장애 원인을 공유하는 서버들을 독립적인 서버처럼 계산한다.
추천 시스템에서 사용자의 최근 클릭들이 서로 영향을 주지 않는다고 가정한다.
A/B 테스트에서 한 사용자가 여러 기기나 계정으로 중복 참여한 사실을 무시한다.
예를 들어 어떤 사용자가 상품 A를 보고, 상품 B를 보고, 상품 C를 봤다고 하자. 이 세 행동은 독립일 가능성이 낮다. 사용자는 이미 특정 카테고리에 관심이 있고, 추천 시스템도 그 관심을 반영해 비슷한 상품을 계속 보여줬을 수 있다. 그런데 이 세 클릭을 완전히 별개의 사용자 세 명이 한 클릭처럼 처리하면 관심도를 과대평가할 수 있다.
스팸 탐지도 마찬가지다. 메일 제목에 '무료'가 들어갔고, 본문에 '당첨'이 들어갔고, 링크가 5개 있다고 하자. 이 특징들은 서로 독립이 아닐 수 있다. 스팸 메일 작성자는 이런 표현을 함께 쓰는 경향이 있기 때문이다. 그런데 독립이라고 가정하면 계산은 쉬워지지만, 결과는 실제보다 과하거나 부족할 수 있다.
물론 실무에서는 계산을 단순화하기 위해 일부러 독립을 가정하기도 한다. 중요한 것은 그 가정이 완벽하다고 믿지 않는 것이다. 가정은 도구이지 현실 자체가 아니다.
베이즈 정리: 결과를 보고 원인을 거꾸로 추론하기
베이즈 정리는 확률을 배우는 많은 사람이 어려워하는 개념이다. 하지만 개발 사례로 보면 생각보다 직관적이다.
우리는 보통 원인에서 결과를 생각한다.
스팸 메일이면 '무료'라는 단어가 들어갈 확률이 높다. 사용자가 특정 상품에 관심이 있으면 그 상품을 클릭할 확률이 높다. 서버에 메모리 누수가 있으면 시간이 지날수록 메모리 사용량이 증가할 확률이 높다.
그런데 실무에서 우리가 궁금한 것은 반대 방향인 경우가 많다.
메일에 '무료'라는 단어가 들어있을 때 이 메일이 스팸일 확률은 얼마인가? 사용자가 이 상품을 클릭했을 때 이 카테고리를 좋아할 확률은 얼마인가? 메모리 사용량이 증가하고 있을 때 실제로 메모리 누수가 있을 확률은 얼마인가?
베이즈 정리는 이런 '거꾸로 추론'을 도와준다.
공식은 다음과 같다.
P(A | B) = P(B | A) × P(A) / P(B)
처음 보면 낯설지만, 뜻은 어렵지 않다.
P(A | B)는 우리가 알고 싶은 확률이다. B라는 증거가 있을 때 A라는 원인이 맞을 확률이다. P(B | A)는 A가 맞을 때 B라는 증거가 나타날 확률이다. P(A)는 증거를 보기 전 A가 원래 얼마나 흔한지에 대한 확률이다. 이것을 사전 확률이라고 부른다. P(B)는 B라는 증거가 전체적으로 얼마나 흔한지다.
스팸 필터로 생각해 보자.
우리가 알고 싶은 것은 이것이다.
P(스팸 | 메일에 무료라는 단어가 있음)
그런데 우리가 데이터에서 더 쉽게 셀 수 있는 것은 보통 반대 방향이다.
P(메일에 무료라는 단어가 있음 | 스팸)
즉, 스팸 메일 중 '무료'라는 단어가 들어간 비율이다. 베이즈 정리는 이 반대 방향의 정보를 이용해 우리가 원하는 확률을 계산하게 해 준다.
스팸 필터를 베이즈식으로 생각하기
숫자로 살펴보자. 메일 10,000개를 분석했더니 다음과 같았다고 하자.
| 구분 | 메일 수 | '무료' 포함 메일 수 |
|---|---|---|
| 스팸 | 1,000 | 600 |
| 정상 | 9,000 | 450 |
| 전체 | 10,000 | 1,050 |
스팸 메일 중 '무료'가 들어간 확률은 600 / 1,000 = 60%다. 정상 메일 중 '무료'가 들어간 확률은 450 / 9,000 = 5%다.
이제 메일 하나가 도착했는데 제목에 '무료'가 들어 있다. 이 메일이 스팸일 확률은 얼마일까?
'무료'가 들어간 전체 메일은 1,050개다. 그중 스팸은 600개다. 따라서 다음과 같다.
P(스팸 | 무료) = 600 / 1,050 ≈ 57.1%
여기서 중요한 점은 '스팸이면 무료가 들어갈 확률'과 '무료가 들어가면 스팸일 확률'이 같지 않다는 것이다.
P(무료 | 스팸) = 60%
P(스팸 | 무료) ≈ 57.1%
두 값은 비슷해 보이지만 개념적으로 완전히 다르다. 데이터가 바뀌면 차이가 훨씬 커질 수 있다.
예를 들어 회사 전체에서 스팸 비율이 매우 낮다면, '무료'라는 단어가 들어 있어도 스팸일 확률은 생각보다 낮을 수 있다. 반대로 스팸이 많이 들어오는 환경이라면 같은 단어도 더 강한 신호가 된다. 이것이 사전 확률의 힘이다.
개발자가 흔히 하는 실수 중 하나가 이 사전 확률을 무시하는 것이다. 어떤 증거가 강해 보여도, 원래 그 사건이 얼마나 흔한지 함께 봐야 한다.
보안에서도 마찬가지다. 어떤 로그인 패턴이 공격에서 자주 나타난다고 해서, 그 패턴을 보인 모든 사용자를 공격자로 봐서는 안 된다. 정상 사용자 수가 압도적으로 많다면 오탐이 크게 늘어날 수 있다.
검색 랭킹과 추천에서의 베이즈적 감각
검색 시스템은 사용자의 의도를 직접 볼 수 없다. 사용자가 검색창에 입력한 단어, 클릭한 문서, 머문 시간, 다시 검색했는지 여부 같은 신호만 볼 수 있다. 결국 검색 랭킹은 이런 질문에 답하려고 한다.
이 검색어와 이 사용자 상황이 주어졌을 때, 이 문서가 만족스러운 결과일 확률은 얼마인가?
단순히 클릭률만 보면 위험하다. 어떤 문서는 제목이 자극적이어서 클릭은 많이 받지만 사용자가 바로 뒤로 가기를 누를 수 있다. 또 어떤 문서는 노출 수가 적어서 클릭률이 우연히 높게 보일 수 있다.
예를 들어 문서 A는 10번 노출되어 5번 클릭되었다. 클릭률은 50%다. 문서 B는 10,000번 노출되어 3,000번 클릭되었다. 클릭률은 30%다. 클릭률만 보면 A가 더 좋아 보인다. 하지만 A의 데이터는 너무 적다. 10번 중 5번이라는 결과는 우연일 수 있다. 반면 B는 많은 사용자에게 반복적으로 선택되었다.
이때 필요한 감각은 '관측된 확률'과 '그 확률을 얼마나 믿을 수 있는가'를 구분하는 것이다. 베이즈적 사고는 새 증거가 들어올 때 기존 믿음을 조금씩 업데이트하는 방식에 가깝다. 처음에는 전체 평균에 가깝게 조심스럽게 보고, 데이터가 충분히 쌓이면 해당 문서나 상품의 실제 성능을 더 강하게 반영한다.
추천 시스템도 비슷하다.
사용자가 개발 서적을 여러 번 클릭했다면 '이 사용자는 개발 서적을 좋아할 확률이 높다'고 볼 수 있다. 그런데 방금 한 번 클릭한 것만으로 단정하면 안 된다. 선물용으로 본 것일 수도 있고, 실수로 누른 것일 수도 있다. 추천은 사용자의 행동을 증거로 삼아 관심 확률을 계속 업데이트하는 과정이다.
좋은 추천 시스템은 한 번의 행동에 과하게 반응하지 않는다. 동시에 반복되는 신호는 놓치지 않는다. 이 균형 감각이 확률적 사고다.
장애 예측도 확률 문제다
장애는 개발자가 가장 싫어하는 불확실성 중 하나다. 장애가 날지 안 날지 미리 정확히 알 수 있다면 좋겠지만, 현실은 그렇지 않다. 대신 우리는 위험 신호를 보고 확률을 추정한다.
예를 들어 다음 조건이 있다고 하자.
최근 5분 동안 오류율이 평소보다 3배 높다.
응답 시간 p95가 2초를 넘었다.
큐에 쌓인 작업 수가 계속 증가하고 있다.
직전에 새 버전이 배포되었다.
각 신호 하나만 보면 큰 문제가 아닐 수도 있다. 하지만 이 신호들이 함께 나타나면 장애 확률은 올라간다. 조건부 확률로 표현하면 다음과 같다.
P(30분 안에 장애 발생 | 오류율 증가, 응답 지연, 큐 증가, 직전 배포)
운영 시스템은 이 확률이 일정 수준을 넘으면 알림을 보내거나 자동으로 스케일 아웃하거나 배포를 중단할 수 있다.
여기서도 중요한 것은 임계값이다. 장애 확률이 1%일 때도 알림을 보낼 것인가? 10%일 때 보낼 것인가? 50%일 때 보낼 것인가? 정답은 상황에 따라 다르다.
결제 시스템처럼 장애 비용이 매우 큰 곳에서는 낮은 확률에도 빠르게 대응해야 한다. 반면 내부 배치 작업처럼 조금 늦어도 되는 시스템에서는 너무 민감한 알림이 오히려 피로를 만든다.
확률은 판단을 대신해 주지 않는다. 대신 판단의 재료를 명확히 만들어 준다.
확률값은 의사결정 임계값으로 이어진다
모델이 '이 메일은 스팸일 확률이 0.82입니다'라고 말한다고 해 보자. 이제 개발자는 무엇을 해야 할까?
0.82는 단지 확률이다. 실제 제품에서는 이 값을 행동으로 바꿔야 한다. 바로 여기서 임계값이 등장한다.
스팸 확률이 0.95 이상이면 자동으로 스팸함으로 보낼 수 있다. 0.70 이상이면 사용자에게 경고 표시만 할 수 있다. 0.50 이상이면 추가 검사를 수행할 수도 있다. 임계값을 어떻게 잡느냐에 따라 사용자 경험과 위험이 달라진다.
임계값은 단순히 높은 값을 고르면 되는 문제가 아니다. 잘못 막았을 때의 비용과 놓쳤을 때의 비용을 함께 봐야 한다.
스팸 필터에서 정상 메일을 스팸으로 보내면 중요한 메일을 놓칠 수 있다. 이것은 오탐의 비용이다. 반대로 스팸 메일을 정상 메일함에 두면 사용자가 피싱 링크를 클릭할 수 있다. 이것은 미탐의 비용이다.
보안 시스템에서는 미탐 비용이 매우 클 수 있다. 결제 사기 탐지에서는 정상 결제를 막는 것도 비용이지만, 사기를 놓치는 비용도 크다. 의료나 안전 관련 시스템에서는 작은 확률도 무시하면 안 된다.
이때 쓸 수 있는 간단한 생각법이 기대 손실이다.
기대 손실 = 사건이 일어날 확률 × 사건이 일어났을 때의 손실
예를 들어 어떤 장애가 발생할 확률이 5%이고, 실제로 발생하면 1,000만 원의 손실이 난다고 하자. 기대 손실은 다음과 같다.
0.05 × 10,000,000 = 500,000
즉, 지금 아무것도 하지 않았을 때 평균적으로 50만 원 정도의 위험을 떠안는 셈이다. 만약 미리 서버를 증설하거나 배포를 중단하는 비용이 10만 원이라면 대응하는 편이 합리적일 수 있다. 반대로 예방 조치 비용이 300만 원이라면 다른 선택을 고민해야 한다.
확률은 '무서우니 막자'와 '아직 괜찮겠지' 사이에 숫자를 놓는다. 감정적인 판단을 줄이고, 비용과 위험을 함께 보게 만든다.
확률을 코드로 옮기는 법
확률 계산은 거창한 모델에서만 시작하지 않는다. 처음에는 단순한 카운팅으로 충분하다.
예를 들어 이벤트 로그에서 검색 후 구매 확률을 구한다고 해 보자. 핵심은 두 값을 세는 것이다. 검색을 한 세션 수와, 검색도 하고 구매도 한 세션 수다.
sessions_with_search = 검색 이벤트가 있는 세션 수
sessions_with_search_and_purchase = 검색 이벤트와 구매 이벤트가 모두 있는 세션 수
P(구매 | 검색) = sessions_with_search_and_purchase / sessions_with_search코드로 옮기면 대략 이런 모양이 된다.
function probabilityOfPurchaseGivenSearch(sessions) {
let searched = 0;
let searchedAndPurchased = 0;
for (const session of sessions) {
const hasSearch = session.events.includes('search');
const hasPurchase = session.events.includes('purchase');
if (hasSearch) searched += 1;
if (hasSearch && hasPurchase) searchedAndPurchased += 1;
}
if (searched === 0) return 0;
return searchedAndPurchased / searched;
}이 코드는 수학적으로 특별한 일을 하지 않는다. 분모와 분자를 정확히 세고 있을 뿐이다. 하지만 바로 이 단순함이 중요하다. 확률 계산의 많은 오류는 복잡한 공식이 아니라 잘못된 카운팅에서 시작된다.
사용자 단위로 셀 것인지, 세션 단위로 셀 것인지, 이벤트 단위로 셀 것인지도 정해야 한다. 한 사용자가 하루에 검색을 100번 했을 때 이를 100개의 독립 사건으로 볼 것인지, 한 명의 사용자 행동으로 볼 것인지에 따라 결과가 달라진다.
확률을 코드로 옮길 때는 다음 질문을 습관처럼 확인해야 한다.
표본공간은 무엇인가?
사건은 어떤 조건으로 정의되는가?
분모와 분자는 같은 기준으로 세고 있는가?
시간 범위는 적절한가?
같은 사용자의 반복 행동을 어떻게 처리할 것인가?
이 질문들은 간단하지만 강력하다. 분석 쿼리, 대시보드, 추천 모델, 알림 정책을 만들 때 모두 필요하다.
확률을 읽을 때 조심해야 할 함정
확률은 강력하지만 오해하기도 쉽다. 특히 개발 실무에서는 숫자가 대시보드에 예쁘게 표시되기 때문에 더 위험하다. 그래프가 깔끔하다고 해서 정의가 올바른 것은 아니다.
첫 번째 함정은 표본이 너무 작은데 확률을 믿는 것이다. 2명 중 1명이 구매하면 구매율은 50%다. 하지만 이 숫자를 근거로 제품 전략을 바꾸기는 어렵다. 10,000명 중 5,000명이 구매한 50%와 2명 중 1명이 구매한 50%는 같은 숫자지만 같은 신뢰도를 갖지 않는다.
두 번째 함정은 전체 평균만 보는 것이다. 전체 오류율이 낮아도 특정 사용자 그룹, 특정 지역, 특정 기기에서 오류율이 높을 수 있다. 평균은 전체 그림을 보여 주지만, 문제를 숨기기도 한다.
세 번째 함정은 상관관계를 원인으로 착각하는 것이다. 비 오는 날 음식 배달 주문이 늘었다고 해서 비가 모든 주문 증가의 직접 원인이라고 단정할 수는 없다. 주말, 프로모션, 지역 이벤트, 계절 같은 다른 요인이 함께 작용했을 수 있다. 이 문제는 다음 장의 통계에서 더 자세히 다룰 것이다.
네 번째 함정은 모델의 확률을 현실의 보장으로 받아들이는 것이다. 모델이 장애 확률을 80%라고 말해도 반드시 장애가 난다는 뜻은 아니다. 80%는 같은 조건이 여러 번 반복될 때 그중 상당수가 장애로 이어질 수 있다는 뜻이다.
다섯 번째 함정은 피드백 루프다. 추천 시스템이 특정 상품을 많이 노출하면 그 상품은 더 많은 클릭을 받는다. 그러면 시스템은 그 상품이 인기가 많다고 판단해 더 많이 노출한다. 이 과정이 반복되면 실제 선호보다 시스템의 노출 정책이 확률을 만들어낼 수 있다.
확률을 다룰 때 개발자는 숫자만 보지 말고 숫자가 만들어진 경로를 봐야 한다.
확률은 예측보다 의사결정에 가깝다
확률을 배우면 무언가를 정확히 맞힐 수 있을 것 같은 기대가 생긴다. 하지만 실무에서 확률의 진짜 가치는 예측 그 자체보다 의사결정에 있다.
사용자가 구매할 확률이 12%라는 예측이 있다고 하자. 이 숫자만으로는 충분하지 않다. 개발자는 다음 행동을 정해야 한다. 쿠폰을 줄 것인가? 추천 상품을 바꿀 것인가? 알림을 보낼 것인가? 아무것도 하지 않을 것인가?
각 행동에는 비용이 있다. 쿠폰을 주면 마진이 줄어든다. 알림을 보내면 사용자가 피로를 느낄 수 있다. 추천을 바꾸면 더 좋은 결과가 나올 수도 있지만, 기존 관심사를 방해할 수도 있다.
따라서 좋은 시스템은 확률과 비용을 함께 본다.
예를 들어 사용자의 구매 확률이 낮다고 해서 무조건 쿠폰을 주면 안 된다. 어차피 구매할 사용자에게 쿠폰을 주면 손해다. 구매 확률이 너무 낮은 사용자에게 쿠폰을 줘도 효과가 없을 수 있다. 오히려 '쿠폰을 주었을 때 구매 확률이 얼마나 증가하는가'를 봐야 한다.
검색도 마찬가지다. 클릭 확률이 높은 문서가 항상 좋은 문서는 아니다. 사용자가 클릭한 뒤 만족할 확률, 다시 검색하지 않을 확률, 오래 머물 확률을 함께 봐야 한다.
스팸 필터도 마찬가지다. 스팸 확률만 보지 않고, 정상 메일을 잘못 막았을 때의 비용과 스팸을 놓쳤을 때의 비용을 함께 봐야 한다.
확률은 '무엇이 일어날 것인가'라는 질문에서 출발하지만, 결국 '그래서 우리는 무엇을 할 것인가'라는 질문으로 이어진다.
개발자를 위한 확률 감각 정리
확률을 처음 배울 때 모든 공식을 한꺼번에 외울 필요는 없다. 개발자에게 먼저 필요한 것은 확률적으로 질문하는 습관이다.
사용자 행동을 볼 때는 '전체 사용자 중 몇 명인가'와 '특정 행동을 한 사용자 중 몇 명인가'를 구분해야 한다. 시스템 장애를 볼 때는 '평균적으로 얼마나 자주 발생하는가'와 '이런 신호가 있을 때 얼마나 위험한가'를 구분해야 한다. 추천이나 검색을 만들 때는 '클릭했는가'뿐 아니라 '어떤 조건에서 클릭했는가'를 봐야 한다.
이번 장의 핵심을 정리하면 다음과 같다.
표본공간은 가능한 전체 결과이며, 로그 이벤트와 사용자 행동도 표본공간으로 볼 수 있다.
사건은 표본공간 안에서 우리가 관심을 갖는 결과의 묶음이다.
조건부 확률은 이미 알고 있는 정보가 있을 때 확률이 어떻게 바뀌는지 다룬다.
독립은 한 사건이 다른 사건의 확률을 바꾸지 않는다는 뜻이고, 실무 데이터에서는 종속인 경우가 많다.
베이즈 정리는 증거를 보고 원인을 거꾸로 추론하는 도구다.
확률값은 임계값, 비용, 리스크와 결합될 때 실제 의사결정이 된다.
복잡도는 코드가 얼마나 커지는 비용을 예측하게 해 주었다. 확률은 세상이 얼마나 불확실한지, 그리고 그 불확실성 속에서 어떤 선택을 해야 하는지 생각하게 해 준다.
다음 장에서 다룰 통계는 이 확률의 짝꿍이다. 확률이 '가능성의 언어'라면, 통계는 실제 데이터에서 그 가능성을 추정하고 검증하는 방법이다. 로그는 이미 쌓이고 있다. 이제 그 로그에서 의미를 뽑아낼 차례다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.
