알고리즘은 코딩 테스트를 통과하기 위한 기술이 아니다

vx_developer·2026년 9월 2일

코테보다가

목록 보기
2/26
post-thumbnail

알고리즘을 처음 공부할 때는 주로 정해진 문제를 해결하는 코드를 작성한다.

문제에는 입력과 출력이 명확하게 주어진다.

쿠폰 목록에서 아직 사용하지 않았고 만료되지 않은 쿠폰만 반환하라.

function getAvailableCoupons(coupons, now) {
  return coupons.filter((coupon) => {
    const isNotUsed = !coupon.isUsed;
    const isNotExpired = new Date(coupon.expiresAt) > now;

    return isNotUsed && isNotExpired;
  });
}

이 코드는 쿠폰 목록을 하나씩 확인하고, 사용되지 않았으며 만료되지 않은 쿠폰만 새로운 배열에 담아 반환한다.

알고리즘의 기본적인 구조를 이해하기에는 좋은 문제다.

  • 입력은 쿠폰 목록과 현재 시간이다.
  • 각 쿠폰의 상태를 확인한다.
  • 조건을 만족하는 쿠폰만 선택한다.
  • 선택한 쿠폰 목록을 출력한다.

하지만 실제 서비스를 개발할 때는 문제가 이렇게 정리된 상태로 전달되지 않는다.

기획서나 작업 티켓에는 보통 다음과 같이 적혀 있다.

사용자가 보유한 쿠폰 중 사용할 수 있는 쿠폰을 보여준다.

이제 개발자가 판단해야 할 내용이 생긴다.

  • ‘보유한 쿠폰’은 어떤 쿠폰을 의미하는가?
  • 다른 사용자에게 선물한 쿠폰도 포함하는가?
  • 만료일 당일까지 사용할 수 있는가?
  • 사용 횟수가 남아 있는 쿠폰은 어떻게 판단하는가?
  • 발행자가 취소한 쿠폰은 어떻게 처리하는가?
  • 서버와 사용자 기기의 시간이 다르면 어느 시간을 기준으로 하는가?
  • 조회하는 동안 쿠폰이 사용되면 화면에는 무엇을 보여주는가?

코딩 테스트에서는 해결해야 할 문제가 먼저 정의된다. 실제 서비스에서는 문제를 정의하는 과정부터 알고리즘의 일부가 된다.

실제 서비스에서 알고리즘은 주어진 문제의 정답을 찾는 기술이 아니라, 현실의 모호한 요구사항을 컴퓨터가 실행할 수 있는 구체적인 절차로 바꾸는 방법이다.

현실의 문제는 입력과 출력이 정리된 상태로 도착하지 않는다

크리스가 쿠폰 서비스에서 받은 쿠폰을 사용하려고 한다고 생각해보자.

현실에서 크리스가 알고 싶은 것은 단순하다.

이 쿠폰을 지금 사용할 수 있는가?

하지만 컴퓨터는 ‘지금 사용할 수 있는 쿠폰’이라는 의미를 스스로 이해하지 못한다.

먼저 서비스가 말하는 ‘사용 가능’의 기준을 구체적으로 정해야 한다.

예를 들어 다음과 같은 정책이 필요할 수 있다.

  1. 쿠폰을 요청한 사용자가 실제 수신자여야 한다.
  2. 쿠폰 상태가 ACTIVE여야 한다.
  3. 남은 사용 횟수가 1회 이상이어야 한다.
  4. 현재 서버 시간이 만료 시각보다 이전이어야 한다.
  5. 발행자가 쿠폰을 취소하지 않았어야 한다.

현실의 문장으로는 “사용할 수 있는 쿠폰”이지만, 프로그램에서는 여러 조건을 순서대로 확인하는 절차가 된다.

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 };
}

이제 처리 절차가 코드에 드러난다.

  1. 쿠폰이 존재하는지 확인한다.
  2. 요청한 사용자가 수신자인지 확인한다.
  3. 쿠폰의 상태를 확인한다.
  4. 남은 사용 횟수를 확인한다.
  5. 서버 시간을 기준으로 만료 여부를 확인한다.
  6. 모든 조건을 통과하면 사용을 허용한다.

순서는 단순한 코드 스타일의 문제가 아니다.

쿠폰이 존재하는지 확인하기 전에 속성을 읽으면 오류가 발생한다. 권한을 확인하기 전에 쿠폰의 상세한 상태를 반환하면 다른 사용자의 쿠폰 정보를 노출할 수 있다. 사용자 기기의 시간을 기준으로 판단하면 기기 설정에 따라 결과가 달라질 수 있다.

알고리즘에서 순서를 정한다는 것은 각 판단의 선행 조건과 실패 결과를 결정하는 일이다.

실제 서비스의 알고리즘은 하나의 함수 안에서 끝나지 않는다

코딩 테스트에서는 하나의 함수가 입력을 받고 정답을 반환하면 문제가 끝나는 경우가 많다.

실제 서비스에서는 사용자의 행동이 여러 계층을 지나간다.

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 또는 다른 언어에서도 같은 해결 원리를 구현할 수 있다.

반대로 코드만 있고 절차를 설명할 수 없다면, 요구사항이 조금만 달라져도 어느 부분을 수정해야 하는지 판단하기 어렵다.

현실의 문제를 알고리즘으로 바꾸기 전에 물어봐야 할 질문

실제 서비스의 요구사항을 받았을 때 바로 코드를 작성하기 전에 다음 질문을 확인할 수 있다.

  1. 사용자가 실제로 하려는 행동은 무엇인가?
  2. 성공한 결과는 어떤 모습이며, 실패한 결과는 어떻게 표현해야 하는가?
  3. 판단에 필요한 입력 데이터는 무엇인가?
  4. 각 데이터는 사용자 입력, 서버, 데이터베이스 중 어디에서 오는가?
  5. 외부에서 들어온 데이터는 어떤 방식으로 검증해야 하는가?
  6. 반드시 먼저 처리해야 하는 선행 조건이 있는가?
  7. 어떤 비즈니스 규칙을 어떤 순서로 확인해야 하는가?
  8. 이 작업이 변경하는 서비스 상태는 무엇인가?
  9. 함께 성공하거나 함께 실패해야 하는 작업은 무엇인가?
  10. 최신 상태에 대한 Source of Truth는 어디에 있는가?
  11. 동시에 여러 요청이 들어오면 같은 결과를 보장할 수 있는가?
  12. 데이터가 증가하면 현재 처리 방식도 계속 사용할 수 있는가?
  13. 실패했을 때 사용자와 시스템에 어떤 상태가 남아야 하는가?
  14. 코드를 보지 않고도 전체 처리 절차를 설명할 수 있는가?

이 질문들은 알고리즘 문제를 풀기 위한 암기 목록이 아니다.

현실의 요구사항을 구현 가능한 문제로 바꾸기 위한 설계 질문이다.

알고리즘은 현실과 코드 사이에 놓인 번역 과정이다

코딩 테스트는 알고리즘의 원리를 연습하기 좋은 환경이다.

문제가 명확하고, 입력과 출력이 정해져 있으며, 제한된 범위 안에서 해결 방법에 집중할 수 있기 때문이다. 탐색, 정렬, 그래프와 동적 계획법 같은 해결 원리를 익히는 데도 도움이 된다.

하지만 알고리즘의 사용처가 코딩 테스트에만 있는 것은 아니다.

실제 서비스에서는 다음과 같은 순간마다 알고리즘이 필요하다.

  • 사용 가능한 쿠폰을 판단할 때
  • 상품 검색 결과의 순서를 정할 때
  • 예약 시간의 충돌을 확인할 때
  • 결제 요청의 중복 처리를 막을 때
  • 작업의 우선순위와 실행 순서를 결정할 때
  • 대량의 데이터를 여러 페이지로 조회할 때
  • 실패한 작업의 재시도 시점을 계산할 때

이 문제들은 처음부터 배열, 그래프나 큐 문제라는 이름으로 주어지지 않는다.

사용자의 행동, 비즈니스 정책과 운영상의 제약으로 나타난다. 개발자는 그 안에서 입력과 출력, 상태, 규칙과 처리 순서를 발견해야 한다.

알고리즘은 코딩 테스트의 정답 코드를 만드는 기술이 아니라, 현실의 모호한 문제를 데이터와 규칙과 순서로 나누어 컴퓨터가 실행할 수 있는 절차로 바꾸는 방법이다.

따라서 알고리즘을 공부한다는 것은 특정 풀이 코드를 많이 외우는 일이 아니다.

현실의 문제에서 중요한 조건을 발견하고, 이를 명확한 입력과 출력으로 정의하며, 올바른 결과를 만드는 처리 순서를 설계하는 연습이다.

profile
Vision eXperience Developer

0개의 댓글