
프로그래밍을 처음 배울 때는 주어진 문제를 코드로 구현하는 연습을 많이 한다.
예를 들어 다음과 같은 문제가 주어진다.
쿠폰 목록을 만료일이 가까운 순서로 정렬하라.
function sortCouponsByExpiry(coupons) {
return [...coupons].sort(
(first, second) =>
new Date(first.expiresAt) -
new Date(second.expiresAt)
);
}
이 코드는 원본 배열을 복사한 뒤 각 쿠폰의 만료 시각을 비교하여 가까운 순서로 정렬한다.
문제가 이미 명확하게 정의된 상황이라면 적절한 해결책이다.
지금은 이 정도의 코드도 AI에게 요청하면 바로 생성할 수 있다. 다른 언어로 변환하거나, 테스트를 추가하거나, 함수 이름을 개선하는 일도 어렵지 않다.
그런데 실제 서비스에서 개발자에게 전달되는 요구사항은 보통 알고리즘 문제처럼 정리되어 있지 않다.
사용자가 중요한 쿠폰을 놓치지 않게 해주세요.
이 요구사항을 받았을 때 바로 만료일 정렬 코드를 만드는 것이 문제 해결일까?
아직은 아니다.
코드를 생성하는 능력은 주어진 작업을 구현하는 능력이다. 문제를 해결하는 능력은 그보다 앞에서 무엇을 구현해야 하는지 정의하고, 가능한 방법 중 적절한 것을 선택하는 능력이다.
실제 서비스에서 문제 해결은 코드를 작성하는 일이 아니라, 원하는 결과를 명확히 정의하고 그 결과에 가장 적합한 처리 방법을 선택하는 과정이다.
쿠폰 서비스 사용자 크리스가 다음과 같은 의견을 남겼다고 생각해보자.
만료일이 가까운 쿠폰을 위에 보여주세요.
이 문장은 해결해야 할 문제처럼 보이지만 실제로는 사용자가 생각한 해결 방법에 가깝다.
크리스가 겪은 상황을 조금 더 살펴보면 다음과 같을 수 있다.
크리스가 원하는 진짜 결과는 목록의 정렬 자체가 아니다.
사용할 수 있는 쿠폰이 있다는 사실을 만료 전에 발견하고 싶다.
이제 여러 해결 방법을 생각할 수 있다.
모두 같은 문제를 해결하려는 방법이지만 구현해야 하는 코드와 필요한 데이터는 서로 다르다.
사용자가 제안한 기능을 그대로 구현하는 것도 필요할 수 있다. 그러나 개발자는 그 기능이 실제 문제를 해결하는지 먼저 확인해야 한다.
“쿠폰을 만료일순으로 정렬한다”는 문장만 보고 코드를 생성하면 다음과 같은 결과를 얻을 수 있다.
function sortCouponsByExpiry(coupons) {
return [...coupons].sort(
(first, second) =>
new Date(first.expiresAt) -
new Date(second.expiresAt)
);
}
이 코드는 정상적으로 실행될 수 있다. 하지만 실제 데이터에서는 예상하지 못한 문제가 생긴다.
const coupons = [
{
title: "Dinner Date",
status: "USED",
expiresAt: "2026-09-05T13:00:00Z",
},
{
title: "Movie Night",
status: "ACTIVE",
expiresAt: null,
},
{
title: "Coffee Together",
status: "ACTIVE",
expiresAt: "invalid-date",
},
];
만료일이 없는 쿠폰과 잘못된 날짜가 포함되어 있다. 이미 사용한 쿠폰도 가장 위에 나타날 수 있다.
코드는 요청받은 정렬을 수행했지만 사용자가 중요한 쿠폰을 발견하도록 돕지는 못한다.
문제는 정렬 문법에 있지 않다.
다음 내용이 결정되지 않은 상태에서 구현부터 시작했기 때문이다.
코드를 정확하게 생성했어도 입력과 규칙이 잘못 정의되어 있다면 서비스의 결과는 올바르지 않다.
“중요한 쿠폰을 놓치지 않게 한다”는 문장을 컴퓨터가 처리할 수 있는 문제로 바꿔보자.
먼저 입력을 정의한다.
그다음 출력과 규칙을 정의한다.
isExpiringSoon을 표시한다.이제 요구사항을 하나의 처리 과정으로 표현할 수 있다.
function buildAvailableCouponList(coupons, serverNow) {
return coupons
.filter((coupon) =>
isCouponAvailable(coupon, serverNow)
)
.map((coupon) => ({
...coupon,
isExpiringSoon: isWithinDays(
coupon.expiresAt,
serverNow,
7
),
}))
.sort(compareCouponExpiry);
}
이 함수는 단순히 배열을 정렬하지 않는다.
현실의 “중요한 쿠폰”이라는 표현이 필터링 조건, 계산된 상태와 정렬 기준으로 바뀌었다.
이때 status, remainingUses, expiresAt은 데이터베이스에 저장된 원본 데이터다. isExpiringSoon은 현재 시간과 만료 시각을 이용해 계산한 데이터다.
function isWithinDays(expiresAt, now, days) {
if (!expiresAt) {
return false;
}
const expiryTime = new Date(expiresAt).getTime();
if (!Number.isFinite(expiryTime)) {
return false;
}
const difference = expiryTime - now.getTime();
const limit = days * 24 * 60 * 60 * 1000;
return difference > 0 && difference <= limit;
}
isExpiringSoon을 데이터베이스에 영구 저장하면 시간이 지나면서 실제 상태와 달라질 수 있다. 오늘은 7일 이상 남았더라도 내일은 만료 임박 쿠폰이 될 수 있기 때문이다.
따라서 만료 시각을 Source of Truth로 두고, 만료 임박 여부는 필요한 시점에 계산하는 편이 자연스럽다.
문제를 제대로 정의하면 어떤 데이터를 저장하고 어떤 값을 계산해야 하는지도 구분할 수 있다.
여기서 기획 방향이 바뀌었다고 생각해보자.
전체 목록을 만료일순으로 보여주는 대신, 홈 화면에 가장 먼저 만료되는 쿠폰 하나만 표시하기로 했다.
필요한 출력은 다음과 같다.
현재 사용할 수 있는 쿠폰 중 만료 시각이 가장 가까운 쿠폰 하나
이 경우 모든 쿠폰을 정렬한 뒤 첫 번째 쿠폰을 가져올 수 있다.
function findNextExpiringCoupon(coupons, now) {
const sortedCoupons = buildAvailableCouponList(
coupons,
now
);
return sortedCoupons[0] ?? null;
}
결과는 올바를 수 있다. 하지만 쿠폰 하나를 찾기 위해 전체 목록의 순서를 모두 결정한다.
가장 가까운 쿠폰 하나만 필요하다면 목록을 한 번 확인하면서 현재까지 가장 가까운 쿠폰만 기억할 수 있다.
function findNextExpiringCoupon(coupons, now) {
let nextCoupon = null;
for (const coupon of coupons) {
if (!isCouponAvailable(coupon, now)) {
continue;
}
if (!coupon.expiresAt) {
continue;
}
if (
nextCoupon === null ||
coupon.expiresAt < nextCoupon.expiresAt
) {
nextCoupon = coupon;
}
}
return nextCoupon;
}
이 코드는 전체 쿠폰의 순서를 만들지 않는다.
각 쿠폰을 확인하면서 지금까지 발견한 가장 가까운 만료일만 유지한다. 모든 쿠폰을 확인한 뒤에는 가장 먼저 만료되는 쿠폰 하나가 남는다.
두 방법 중 하나가 언제나 더 좋은 것은 아니다.
| 필요한 결과 | 적절한 방법 |
|---|---|
| 전체 쿠폰을 만료일순으로 표시 | 전체 목록 정렬 |
| 가장 먼저 만료되는 쿠폰 하나 표시 | 한 번 탐색하며 최솟값 유지 |
| 만료 임박 쿠폰 3개 표시 | 정렬 후 일부 선택 또는 상위 후보만 유지 |
| 데이터베이스의 대량 쿠폰 조회 | 조건 검색과 ORDER BY, LIMIT |
| 만료 하루 전 사용자에게 알림 | 예약 작업 또는 주기적인 대상 조회 |
문제의 출력이 달라지면 적절한 알고리즘도 달라진다.
코드를 잘 작성하는 것만으로는 이 선택을 할 수 없다. 무엇이 필요한 결과인지 먼저 알아야 한다.
만료일순으로 정렬한 뒤 쿠폰 화면이 느려졌다고 생각해보자.
AI에게 다음과 같이 요청할 수 있다.
이 정렬 알고리즘을 더 빠르게 바꿔줘.
하지만 화면이 느린 원인이 정렬이라는 근거는 아직 없다.
실제 지연은 다음과 같은 곳에서 발생할 수 있다.
쿠폰 20개를 정렬하는 시간은 매우 짧은데 데이터베이스 조회에 2초가 걸릴 수도 있다. 이 상황에서 정렬 함수를 개선해도 사용자가 느끼는 속도는 거의 달라지지 않는다.
문제 해결은 “느리다”라는 증상을 곧바로 특정 코드의 문제로 바꾸는 일이 아니다.
다음 흐름으로 확인해야 한다.
flowchart TD
A["사용자가 느끼는 문제"]
--> B["관찰 가능한 기준 정의"]
--> C["데이터로 원인 측정"]
--> D["해결 방법 후보 비교"]
--> E["구현 후 결과 검증"]
예를 들어 목표를 다음과 같이 정의할 수 있다.
사용자가 쿠폰 화면을 열면 사용 가능한 쿠폰 20개가 1초 안에 표시되어야 한다.
그다음 구간별 시간을 측정한다.
| 구간 | 측정할 내용 |
|---|---|
| 데이터베이스 | 쿠폰 조회 시간과 조회한 행의 수 |
| 서버 | 검증, 변환 및 정렬 시간 |
| 네트워크 | 응답 크기와 전송 시간 |
| 프런트엔드 | 렌더링과 이미지 로딩 시간 |
| 사용자 경험 | 화면을 사용할 수 있게 되는 전체 시간 |
측정 결과 데이터베이스 조회가 병목이라면 인덱스나 쿼리를 개선해야 한다. 응답 데이터가 너무 많다면 페이지네이션이 필요하다. 이미지가 원인이라면 정렬 알고리즘을 수정할 이유가 없다.
적절한 해결 방법을 선택하려면 코드보다 먼저 문제의 위치를 찾아야 한다.
쿠폰 목록을 만료일순으로 정렬하는 기능을 배포했다고 생각해보자.
테스트도 통과했고 화면에도 오류가 없다.
그렇다면 문제는 해결된 것일까?
기술적으로는 기능이 동작한다. 하지만 처음 해결하려던 문제는 “사용자가 중요한 쿠폰을 놓친다”는 것이었다.
이를 확인하려면 코드의 동작뿐 아니라 사용자 결과도 살펴봐야 한다.
코드가 요구사항대로 실행되는지는 테스트로 확인할 수 있다. 그러나 기능이 사용자의 문제를 해결했는지는 운영 지표와 사용자 행동을 통해 확인해야 한다.
문제 해결은 배포에서 끝나지 않는다.
정의한 문제가 실제로 줄어들었는지 확인할 때 비로소 해결 방법을 평가할 수 있다.
AI는 쿠폰 목록을 정렬하는 함수를 빠르게 만들 수 있다.
다음과 같은 작업도 맡길 수 있다.
하지만 어떤 기능이 실제로 필요한지는 서비스의 목적과 제약 조건에 따라 달라진다.
AI가 적절한 해결책을 제안하려면 개발자가 다음 정보를 제공해야 한다.
이 정보가 없다면 AI는 일반적인 가정을 바탕으로 코드를 생성한다. 코드는 자연스러워 보여도 실제 서비스와 맞지 않을 수 있다.
AI에게 일을 잘 맡기는 능력도 결국 문제를 명확히 정의하는 능력에서 시작한다.
서비스 개발에서는 문제, 목표, 방법과 코드가 쉽게 섞인다.
이를 다음과 같이 구분할 수 있다.
| 구분 | 쿠폰 서비스의 예 |
|---|---|
| 문제 | 사용자가 만료 예정 쿠폰을 발견하지 못한다 |
| 목표 | 사용하지 못하고 만료되는 쿠폰을 줄인다 |
| 해결 방법 | 만료 임박 표시, 정렬, 배너 또는 알림 |
| 구현 | 필터링 함수, 정렬 알고리즘, 조회 쿼리, 알림 작업 |
코드는 마지막 단계에 있다.
문제를 해결 방법처럼 표현하면 선택지가 줄어든다.
문제: 쿠폰이 만료일순으로 정렬되지 않는다.
이렇게 정의하면 답은 정렬 기능으로 제한된다.
반면 사용자 결과를 중심으로 정의하면 여러 방법을 비교할 수 있다.
문제: 사용자가 곧 만료되는 쿠폰을 발견하지 못한다.
이제 정렬 외에도 배너, 표시, 알림과 목록 필터링을 고려할 수 있다.
좋은 문제 정의는 코드를 더 복잡하게 만드는 것이 아니라 불필요한 코드를 만들지 않게 한다.
새로운 기능이나 버그를 맡았을 때 바로 코드를 생성하기 전에 다음 질문을 확인할 수 있다.
이 질문에 답한 뒤에는 AI가 만든 코드도 훨씬 구체적으로 평가할 수 있다.
코드를 생성하는 능력은 중요하다.
명확하게 정의된 작업을 정확한 문법으로 구현하고, 읽기 쉬운 구조로 정리하며, 테스트 가능한 형태로 만드는 능력은 여전히 필요하다.
그러나 실제 서비스의 문제는 처음부터 함수 이름과 입력값을 갖춘 상태로 주어지지 않는다.
개발자는 사용자의 불편과 비즈니스 요구사항을 살펴보고 다음 내용을 결정해야 한다.
이 과정이 빠지면 올바른 코드로 불필요한 기능을 만들 수 있다. 성능이 좋은 알고리즘으로 잘못 정의된 문제를 더 빠르게 처리할 수도 있다.
코드를 생성하는 능력은 정해진 해결 방법을 구현하는 능력이고, 문제를 해결하는 능력은 무엇을 해결할지 정의하고 여러 방법의 비용을 비교하여 적절한 선택을 하는 능력이다.
AI가 코드를 더 빠르게 생성할수록 이 차이는 더욱 분명해진다.
앞으로 개발자의 가치는 얼마나 많은 코드를 직접 입력했는지만으로 결정되지 않는다. 무엇을 만들지, 왜 이 방법을 선택했는지, 실제 문제가 해결되었는지를 설명할 수 있어야 한다.