알고리즘을 처음 공부할 때는 주로 정해진 문제를 해결하는 코드를 작성한다.
문제에는 입력과 출력이 명확하게 주어진다.
쿠폰 목록에서 아직 사용하지 않았고 만료되지 않은 쿠폰만 반환하라.
function getAvailableCoupons(coupons, now) {
return coupons.filter((coupon) => {
const isNotUsed = !coupon.isUsed;
const isNotExpired = new Date(coupon.expiresAt) > now;
return isNotUsed && isNotExpired;
});
}
이 코드는 쿠폰 목록을 하나씩 확인하고, 사용되지 않았으며 만료되지 않은 쿠폰만 새로운 배열에 담아 반환한다.
알고리즘의 기본적인 구조를 이해하기에는 좋은 문제다.
하지만 실제 서비스를 개발할 때는 문제가 이렇게 정리된 상태로 전달되지 않는다.
기획서나 작업 티켓에는 보통 다음과 같이 적혀 있다.
사용자가 보유한 쿠폰 중 사용할 수 있는 쿠폰을 보여준다.
이제 개발자가 판단해야 할 내용이 생긴다.
코딩 테스트에서는 해결해야 할 문제가 먼저 정의된다. 실제 서비스에서는 문제를 정의하는 과정부터 알고리즘의 일부가 된다.
실제 서비스에서 알고리즘은 주어진 문제의 정답을 찾는 기술이 아니라, 현실의 모호한 요구사항을 컴퓨터가 실행할 수 있는 구체적인 절차로 바꾸는 방법이다.
크리스가 쿠폰 서비스에서 받은 쿠폰을 사용하려고 한다고 생각해보자.
현실에서 크리스가 알고 싶은 것은 단순하다.
이 쿠폰을 지금 사용할 수 있는가?
하지만 컴퓨터는 ‘지금 사용할 수 있는 쿠폰’이라는 의미를 스스로 이해하지 못한다.
먼저 서비스가 말하는 ‘사용 가능’의 기준을 구체적으로 정해야 한다.
예를 들어 다음과 같은 정책이 필요할 수 있다.
ACTIVE여야 한다.현실의 문장으로는 “사용할 수 있는 쿠폰”이지만, 프로그램에서는 여러 조건을 순서대로 확인하는 절차가 된다.
function canRedeemCoupon({ coupon, requesterId, now }) {
return (
coupon.recipientId === requesterId &&
coupon.status === "ACTIVE" &&
coupon.remainingUses > 0 &&
now < coupon.expiresAt
);
}
현실에 존재하는 소유 관계, 쿠폰 상태, 남은 횟수와 만료 정책이 프로그램이 처리할 수 있는 비교 연산으로 바뀌었다.
하지만 조건을 코드로 옮겼다고 해서 문제가 모두 해결된 것은 아니다.
coupon이 존재하지 않는다면 어떻게 해야 할까? expiresAt이 잘못된 값이라면 어떻게 해야 할까? 사용할 수 없는 경우 단순히 false만 반환하면 사용자에게 이유를 어떻게 설명할 수 있을까?
현실의 문제를 코드로 바꾸려면 정상적인 경우뿐 아니라 실패의 의미도 함께 정의해야 한다.
“크리스가 받은 쿠폰”이라는 현실의 표현에는 이미 여러 의미가 들어 있다.
컴퓨터가 이를 판단하려면 각각의 의미가 데이터로 표현되어야 한다.
const coupon = {
id: "coupon_123",
title: "Dinner Date",
recipientId: "user_chris",
status: "ACTIVE",
remainingUses: 1,
expiresAt: new Date("2026-12-31T13:00:00Z"),
};
이 객체는 단순히 관련된 값을 묶은 것이 아니다.
현실에 존재하는 쿠폰의 정체성, 소유 관계, 현재 상태와 사용 제약을 프로그램이 판단할 수 있는 형태로 모델링한 것이다.
여기에서 중요한 점은 알고리즘이 데이터와 분리되어 존재하지 않는다는 것이다.
recipientId가 없다면 수신자를 확인하는 절차를 만들 수 없다. remainingUses가 없다면 여러 번 사용할 수 있는 쿠폰을 표현하기 어렵다. expiresAt의 시간 기준이 불명확하면 만료 여부도 일관되게 판단할 수 없다.
결국 어떤 알고리즘을 만들 수 있는지는 서비스가 현실을 어떤 데이터로 표현했는지에 영향을 받는다.
앞에서 작성한 코드는 정상적인 쿠폰이 들어온다고 가정한다.
function canRedeemCoupon({ coupon, requesterId, now }) {
return (
coupon.recipientId === requesterId &&
coupon.status === "ACTIVE" &&
coupon.remainingUses > 0 &&
now < coupon.expiresAt
);
}
코드는 짧지만 실패 원인을 구분하기 어렵다. 쿠폰이 존재하지 않으면 오류가 발생하고, 외부에서 잘못된 시간이 들어와도 그대로 비교한다.
실제 서비스에서는 각 조건을 어떤 순서로 확인할지도 결정해야 한다.
function evaluateCouponRedemption({
coupon,
requesterId,
serverNow,
}) {
if (!coupon) {
return { allowed: false, reason: "COUPON_NOT_FOUND" };
}
if (coupon.recipientId !== requesterId) {
return { allowed: false, reason: "NOT_RECIPIENT" };
}
if (coupon.status !== "ACTIVE") {
return { allowed: false, reason: "COUPON_NOT_ACTIVE" };
}
if (coupon.remainingUses <= 0) {
return { allowed: false, reason: "NO_REMAINING_USES" };
}
if (serverNow >= coupon.expiresAt) {
return { allowed: false, reason: "COUPON_EXPIRED" };
}
return { allowed: true, reason: null };
}
이제 처리 절차가 코드에 드러난다.
순서는 단순한 코드 스타일의 문제가 아니다.
쿠폰이 존재하는지 확인하기 전에 속성을 읽으면 오류가 발생한다. 권한을 확인하기 전에 쿠폰의 상세한 상태를 반환하면 다른 사용자의 쿠폰 정보를 노출할 수 있다. 사용자 기기의 시간을 기준으로 판단하면 기기 설정에 따라 결과가 달라질 수 있다.
알고리즘에서 순서를 정한다는 것은 각 판단의 선행 조건과 실패 결과를 결정하는 일이다.
코딩 테스트에서는 하나의 함수가 입력을 받고 정답을 반환하면 문제가 끝나는 경우가 많다.
실제 서비스에서는 사용자의 행동이 여러 계층을 지나간다.
flowchart TD
A["사용 버튼 클릭"]
--> B["API 요청"]
--> C["입력·사용자 검증"]
--> D["최신 쿠폰 조회"]
--> E["사용 가능 여부 판단"]
--> F["쿠폰 상태 변경"]
--> G["결과 응답과 UI 갱신"]
크리스가 쿠폰의 사용 버튼을 누르면 프런트엔드는 서버에 요청을 보낸다. 서버는 로그인한 사용자를 확인하고 요청값을 검증한다. 데이터베이스에서 최신 쿠폰을 조회한 뒤 비즈니스 규칙을 적용한다.
사용 가능한 쿠폰이라면 상태나 남은 횟수를 변경하고, 결과를 다시 화면에 전달한다.
이 전체 흐름에는 여러 종류의 알고리즘이 함께 존재한다.
따라서 실제 서비스에서 알고리즘을 찾으려면 반복문이나 재귀 함수만 살펴봐서는 안 된다.
사용자의 행동이 데이터의 조회와 판단, 변경을 거쳐 결과로 돌아오는 전체 데이터 흐름을 살펴봐야 한다.
쿠폰을 사용할 수 있는지 확인한 다음 남은 횟수를 줄이는 코드를 생각해보자.
const coupon = await couponRepository.findById(couponId);
if (canRedeemCoupon({ coupon, requesterId, now })) {
await couponRepository.updateRemainingUses(
couponId,
coupon.remainingUses - 1
);
}
하나의 요청만 처리할 때는 자연스럽게 동작한다.
하지만 남은 횟수가 1회인 순간에 두 요청이 동시에 들어오면 두 요청 모두 같은 값을 조회할 수 있다. 두 요청이 모두 쿠폰을 사용할 수 있다고 판단하면 1회용 쿠폰이 두 번 사용될 수 있다.
현실의 규칙은 다음과 같다.
쿠폰의 남은 사용 횟수가 있을 때만 한 번 차감한다.
이 규칙은 확인과 변경이 떨어져 있으면 보장하기 어렵다. 따라서 두 작업을 하나의 상태 변경 절차로 다뤄야 한다.
const redeemedCoupon =
await couponRepository.redeemIfAvailable({
couponId,
requesterId,
redeemedAt: serverNow,
});
if (!redeemedCoupon) {
return {
success: false,
reason: "COUPON_NOT_REDEEMABLE",
};
}
return {
success: true,
coupon: redeemedCoupon,
};
redeemIfAvailable은 데이터베이스의 최신 상태를 기준으로 사용 조건을 확인하고, 조건을 만족할 때만 사용 횟수를 변경해야 한다.
이 코드에서 중요한 것은 함수 이름이나 문법이 아니다.
“확인한다”와 “변경한다”라는 현실의 두 행동이 반드시 하나의 작업처럼 성공하거나 실패해야 한다는 서비스 규칙을 절차에 반영한 것이다.
알고리즘은 계산 결과만 만드는 것이 아니라 서비스의 상태가 올바르게 변하도록 통제하기도 한다.
사용 가능한 쿠폰을 보여준다는 요구사항을 다음과 같이 구현할 수 있다.
const availableCoupons = coupons.filter(
(coupon) =>
coupon.status === "ACTIVE" &&
coupon.remainingUses > 0 &&
serverNow < coupon.expiresAt
);
사용자가 보유한 쿠폰이 10개이고 이미 화면에 데이터를 받아둔 상태라면 충분히 적절한 방법이다.
하지만 전체 사용자에게 발행된 쿠폰 수백만 개를 서버 메모리로 가져온 뒤 같은 코드를 실행해서는 안 된다.
이 경우에는 데이터베이스에서 필요한 데이터만 조회해야 한다.
SELECT id, title, expires_at, remaining_uses
FROM coupons
WHERE recipient_id = $1
AND status = 'ACTIVE'
AND remaining_uses > 0
AND expires_at > $2
ORDER BY expires_at ASC
LIMIT 20;
두 방법은 모두 사용 가능한 쿠폰을 찾는다. 그러나 처리하는 데이터의 위치와 규모가 다르다.
| 상황 | 적절한 처리 방식 |
|---|---|
| 화면에 있는 소수의 쿠폰 필터링 | 배열의 filter 사용 |
| 서버에서 사용자 쿠폰 조회 | 데이터베이스 조건 검색 |
| 쿠폰이 매우 많은 경우 | 인덱스와 페이지네이션 적용 |
| 쿠폰 상태가 자주 변경되는 경우 | 최신 데이터가 있는 서버와 데이터베이스에서 판단 |
알고리즘은 코드만 보고 선택할 수 없다.
데이터가 어디에 있는지, 얼마나 많은지, 얼마나 자주 변경되는지, 결과를 얼마나 빠르게 제공해야 하는지에 따라 적절한 절차가 달라진다.
알고리즘을 제대로 이해했는지는 코드를 외워서 다시 작성할 수 있는지만으로 판단하기 어렵다.
다음과 같이 서비스의 언어로 설명할 수 있어야 한다.
로그인한 사용자가 쿠폰 사용을 요청하면 서버는 요청값과 사용자 신원을 검증한다. 데이터베이스에서 최신 쿠폰을 확인하고, 요청자가 수신자이며 쿠폰이 활성 상태이고 남은 횟수와 유효기간이 충족될 때만 사용 횟수를 한 번 차감한다. 처리 결과는 성공 여부와 실패 이유로 반환한다.
이 설명에는 특정 프로그래밍 언어나 프레임워크가 등장하지 않는다.
그런데도 다음 내용을 알 수 있다.
이렇게 설명할 수 있다면 JavaScript, Java, Swift 또는 다른 언어에서도 같은 해결 원리를 구현할 수 있다.
반대로 코드만 있고 절차를 설명할 수 없다면, 요구사항이 조금만 달라져도 어느 부분을 수정해야 하는지 판단하기 어렵다.
실제 서비스의 요구사항을 받았을 때 바로 코드를 작성하기 전에 다음 질문을 확인할 수 있다.
이 질문들은 알고리즘 문제를 풀기 위한 암기 목록이 아니다.
현실의 요구사항을 구현 가능한 문제로 바꾸기 위한 설계 질문이다.
코딩 테스트는 알고리즘의 원리를 연습하기 좋은 환경이다.
문제가 명확하고, 입력과 출력이 정해져 있으며, 제한된 범위 안에서 해결 방법에 집중할 수 있기 때문이다. 탐색, 정렬, 그래프와 동적 계획법 같은 해결 원리를 익히는 데도 도움이 된다.
하지만 알고리즘의 사용처가 코딩 테스트에만 있는 것은 아니다.
실제 서비스에서는 다음과 같은 순간마다 알고리즘이 필요하다.
이 문제들은 처음부터 배열, 그래프나 큐 문제라는 이름으로 주어지지 않는다.
사용자의 행동, 비즈니스 정책과 운영상의 제약으로 나타난다. 개발자는 그 안에서 입력과 출력, 상태, 규칙과 처리 순서를 발견해야 한다.
알고리즘은 코딩 테스트의 정답 코드를 만드는 기술이 아니라, 현실의 모호한 문제를 데이터와 규칙과 순서로 나누어 컴퓨터가 실행할 수 있는 절차로 바꾸는 방법이다.
따라서 알고리즘을 공부한다는 것은 특정 풀이 코드를 많이 외우는 일이 아니다.
현실의 문제에서 중요한 조건을 발견하고, 이를 명확한 입력과 출력으로 정의하며, 올바른 결과를 만드는 처리 순서를 설계하는 연습이다.