이 글의 위치: 기술 블로그 → 설계 판단.
작성·검증 방식은 Editorial Policy 를 참고하세요.
확정된 결론이 아니라 지금 시점에서 제가 세워 둔 작업 기준입니다.
어디까지 맡기고 무엇을 붙들 것인가
요즘 개발하면서 계속 붙들고 있는 질문이 하나 있습니다. AI에게 프롬프트로 구현을 맡기고, 신뢰와 직결되는 핵심 비즈니스 로직만 신중하게 직접 쓰면 되지 않을까.
직관적으로는 맞는 것 같은데, 막상 적용하려니 두 군데가 비어 있었습니다. "핵심"의 경계를 무엇으로 긋는가, 그리고 맡긴 부분을 나는 어떻게 인지하고 있을 것인가. 이 글은 그 두 자리를 채워 보려고 정리한 기록입니다.
1. 예전에 문제를 풀던 방식
전에는 이렇게 일했습니다.
시스템 안에 파악되지 않은 기능이 있으면 그 자체로 뭔가 잘못됐다는 느낌부터 들었습니다. 그 찝찝함을 해소하려고 자리에 앉아 깊이 생각하고, 알아내거나, 알아낼 수 없으면 문제의 범위를 최소화했습니다. 그다음엔 어느 부분이 이상한지 로그를 전부 찍어 놓고 읽고 생각하기를 반복했습니다.
그렇게 하면 대개는 이상한 지점을 찾아냈습니다. 고쳐서 배포하면 잘 굴러갈 때도 있었고, 생각지도 못했던 또 다른 문제가 터질 때도 있었습니다.
이 방식의 문제는 실력이 아니라 구조에 있었습니다. 전수 조사는 시스템이 커지는 속도를 따라가지 못합니다. 그리고 "모르는 부분이 없어야 한다"는 전제는 애초에 달성할 수 없는 목표라, 늘 불안이 남습니다.
2. 그래서 옮겨간 방식 — 성공 기준과 통제
결과적으로 제가 도달한 방식은 이렇습니다.
- 기획팀이나 대표님이 원하는 성공 기준을 명확하게 한다.
- 그 부분만 제대로 굴러가게 만든다.
- 예외적이고 사용자가 불편해할 법한 부분은 통제한다.
여기서 "통제"는 전부 대응한다는 뜻이 아니라, 일어나지 않게 막거나 일어나도 안전하게 끝나게 한다는 뜻입니다. 입력을 좁히고, 상태를 제한하고, 실패했을 때 조용히 망가지는 대신 명확히 실패하게 두는 쪽입니다.
이 방식으로 옮기고 나서 안정성이 눈에 띄게 올라갔는데, 나중에 보니 특별한 발명이 아니었습니다. 원래 개발자들이 그렇게 개발해 왔습니다. 명세를 정하고, 그 명세를 만족시키고, 나머지는 방어적으로 막는 것. 제가 뒤늦게 그 자리로 돌아온 셈입니다.
3. AI에 위임할 때도 같은 패턴이 성립한다
흥미로운 건, 이 패턴이 AI에게 구현을 맡길 때 오히려 더 잘 맞는다는 점이었습니다.
| 예전 방식 | 지금 방식 | |
|---|---|---|
| 착수 전 | 전체를 파악하려 함 | 성공 기준 + 통제 범위를 먼저 확정 |
| 구현 중 | 모르는 부분이 있으면 불안 | 경계가 지켜지면 내부는 위임 |
| 문제 발생 시 | 전수 조사로 원인 추적 | 제기된 그 부분만 집중해서 해소 |
| 기존 동작 보증 | 기억과 주의력에 의존 | Test Layer에 성공 기준을 고정 |
| 실패 시 | 고쳤는데 다른 데서 터짐 | 회귀는 테스트가 잡음 |
핵심은 마지막 줄입니다. 한 번 확정한 성공 기준을 Test Layer에 남겨 두면, 그다음부터는 그 기준이 사람의 기억 대신 시스템의 보증이 됩니다. 예전에 "고쳤더니 다른 데서 터지던" 문제가 여기서 상당 부분 해결됩니다.
그리고 오류를 찾는 일 자체는 여전히 가능합니다. 시간이 걸리고, 디버깅하고, 특정 상황을 재현해서 왜 이상한지 추적해야 하지만, 그 과정을 거치면 잘못된 부분은 결국 찾아냅니다. 그래서 "모르는 부분이 있다"는 상태를 예전만큼 두려워하지 않게 됐습니다.
4. 성공 기준만으로는 부족한 지점 — 불변식
다만 이 방식에는 구멍이 하나 있습니다. 최근에야 말로 정리된 부분입니다.
성공 기준은 "무엇이 일어나야 하는가"를 말합니다. 그런데 위험은 대개 "무엇이 일어나면 안 되는가"에 있습니다. 이 둘은 여집합 관계가 아닙니다. 성공 기준을 100개 적어도, 거기에 적히지 않은 실패 모드는 그대로 남습니다.
그래서 Test Layer에 성공 기준만 넣으면 회귀는 잡아도 새로운 실패 모드는 못 잡습니다.
보완책으로 제가 쓰기 시작한 것이 불변식(invariant) 입니다. 시나리오와 무관하게 언제나 참이어야 하는 문장입니다.
| 성공 기준 (시나리오) | 불변식 (항상 참) | |
|---|---|---|
| 형태 | "A 하면 B가 된다" | "어떤 경우에도 C는 아니다" |
| 예시 | 결제하면 주문이 완료된다 | 잔액은 음수가 될 수 없다 |
| 쿠폰을 쓰면 할인된다 | 같은 요청을 두 번 처리해도 결과는 같다(멱등) | |
| 로그인하면 목록이 보인다 | 다른 사용자의 데이터는 조회되지 않는다 | |
| 잡아내는 것 | 기능 회귀 | 예상하지 못한 입력·순서·중복 |
둘이 테스트에서 어떻게 다르게 생겼는지 보면 성격이 분명해집니다. Go 표준 라이브러리의 testing/quick 으로 쓰면 이렇습니다.
// 성공 기준 — 시나리오 하나를 고정한다
func TestWithdraw_성공기준(t *testing.T) {
acc := NewAccount(10_000)
err := acc.Withdraw(3_000)
require.NoError(t, err)
require.Equal(t, 7_000, acc.Balance()) // "출금하면 그만큼 준다"
}
// 불변식 — 어떤 값이 어떤 순서로 와도 넘으면 안 되는 선
func TestWithdraw_불변식(t *testing.T) {
property := func(start uint16, amounts []uint16) bool {
acc := NewAccount(int(start))
for _, amt := range amounts {
_ = acc.Withdraw(int(amt)) // 개별 실패는 허용한다. 다만
}
return acc.Balance() >= 0 // 잔액은 절대 음수가 아니어야 한다
}
// 무작위 입력 수백 건을 만들어 반증을 시도한다
require.NoError(t, quick.Check(property, nil))
}
아래쪽 테스트는 제가 시나리오를 상상하지 않습니다. 값과 순서는 도구가 무작위로 만들고, 저는 "무엇이 깨지면 안 되는지"만 적습니다. 제가 떠올리지 못한 조합을 도구가 대신 찾아 주는 셈이라, 전수 조사를 사람 대신 기계에 넘긴 것에 가깝습니다. 예전에 로그를 전부 찍어 가며 하던 일의 자동화된 형태이기도 합니다.
불변식은 AI에게 위임할 때 특히 강력합니다. 성공 기준은 "이렇게 만들어 줘"라는 지시라 구현 방법이 열려 있지만, 불변식은 어떻게 만들든 넘으면 안 되는 선이라 가드레일로 작동합니다. 성공 기준이 요구사항이라면 불변식은 안전벨트에 가깝습니다.
5. "결제와 데이터 손실 말고 또 무엇이 중요한가"
제가 오래 답을 못 찾던 질문입니다. 신뢰와 직결되는 로직이라고 하면 결제와 데이터 손실은 바로 떠오르는데, 그 외에 무엇이 있는지 목록을 갖고 있지 않았습니다.
정리하면서 제가 세운 판별 기준은 하나입니다.
틀렸을 때 되돌릴 수 있는가.
되돌릴 수 있으면 위임해도 됩니다. 고치고 다시 배포하면 되니까요. 되돌릴 수 없으면 직접 붙들어야 합니다. 결제와 데이터 손실이 중요했던 이유도 결국 이것이었습니다 — 되돌릴 수 없어서.
이 기준으로 목록을 다시 뽑으니 이렇게 나왔습니다.
| 영역 | 왜 되돌릴 수 없나 |
|---|---|
| 돈 — 결제·정산·환불 | 이미 빠져나간 돈은 코드 수정으로 돌아오지 않음 |
| 데이터 영속성 — 삭제·덮어쓰기 | 백업이 없으면 끝 |
| 권한 경계 — 남의 데이터 조회 | 이미 본 것은 못 본 것으로 되돌릴 수 없음 |
| 개인정보 노출 — 로그에 찍히는 것 포함 | 위와 같음. 로그는 특히 조용히 샘 |
| 멱등성 — 재시도로 두 번 실행 | 두 번 결제·두 번 발송된 뒤엔 늦음 |
| 되돌릴 수 없는 외부 부작용 — 메일·푸시·외부 API | 보낸 메일은 회수 불가 |
| 인증·세션 | 탈취된 세션은 사후 조치만 가능 |
| 감사 추적 — 누가 언제 무엇을 | 남기지 않은 기록은 나중에 만들 수 없음 |
특히 권한 경계는 제가 목록에 없던 항목인데, 넣고 보니 결제만큼 중요합니다. 조용히 터지고, 터진 걸 한동안 모르고, 알았을 때는 되돌릴 수 없습니다.
이건 소프트웨어 밖에서도 쓰이는 프레임입니다. 아마존이 의사결정을 나눌 때 쓰는 "한 방향 문(one-way door)과 양방향 문(two-way door)" 과 같은 구분입니다. 양방향 문은 빠르게 결정하고 되돌리면 되고, 한 방향 문은 신중해야 합니다.
한 방향 문은 직접 쓰고, 양방향 문은 위임한다. 지금 제가 쓰는 경계는 이겁니다.
6. 어디까지 알아야 하는가 — 성질과 구현을 나눈다
또 하나 오래 걸려 있던 고민입니다. 모든 걸 알아야 하는 건 아닐까 하는 막연한 두려움이 있었습니다.
그런데 돌아보면, 모든 걸 안다고 느꼈던 순간에도 실제로 하던 일은 같았습니다. 문제를 해결하려고 관련된 기능이 무엇이 있는지 리딩하고 파악하는 것. 그때도 머릿속에 전부 들어 있어서 푼 게 아니라, 그때그때 필요한 만큼 읽어서 풀었습니다. 그렇다면 어차피 모든 걸 안다는 건 불가능하고, 예전에도 그런 적은 없었던 셈입니다.
다만 아무것도 몰라도 된다는 뜻은 아닙니다. 블록체인 개발을 하는데 블록의 개념도 모르는 상황은 분명히 잘못됐습니다. 그 위에서 판단을 내려야 하니까요. 그런데 합의 과정에서 쓰이는 머클트리가 어떤 공식으로 구현되는지까지 알아야 하는가, 여기서는 답이 잘 안 나왔습니다.
정리하면서 세운 기준은 이렇습니다.
성질(property)은 알고 있어야 하고, 구현(implementation)은 필요할 때 내려간다.
| 알아야 하는 것 (성질·계약) | 필요할 때 내려가는 것 (구현) | |
|---|---|---|
| 머클트리 | 루트 하나로 포함 여부를 증명할 수 있고, 비용이 트리 높이에 비례한다 | 해시를 어떤 순서로 짝지어 올리는지, 홀수 노드 처리 |
| 데이터베이스 인덱스 | 조회는 빨라지고 쓰기는 느려진다. 정렬 순서가 성능을 좌우한다 | B-tree 노드 분할 알고리즘 |
| 커넥션 풀 | 크기는 코어 수와 대기 시간으로 정해진다 | 풀 내부의 큐 구현 |
판별 질문 하나로 줄이면 이렇습니다. "이걸 모르면 잘못된 판단을 내리게 되는가."
머클트리 공식을 몰라도 "포함 증명이 로그 비용"이라는 성질만 알면 설계 판단은 내릴 수 있습니다. 반대로 그 성질을 모르면 "전부 다 검증해야 하니 무겁겠다"는 잘못된 판단을 내립니다. 성질을 모르면 판단이 틀리고, 구현을 모르면 최적화를 못 합니다. 그래서 순서가 있습니다 — 성질이 먼저고, 구현은 병목이 실제로 그 층으로 내려왔을 때입니다.
이걸 상태로 표현하면 제가 목표로 삼는 건 "필요할 때 해상도를 높일 수 있는 상태" 입니다. 항상 높은 해상도로 보고 있는 게 아니라, 필요한 지점에서 내려갈 수 있는 상태. 그리고 이 상태를 유지하려면 어디를 모르는지에 대한 지도는 갖고 있어야 합니다. 모르는 것 자체보다 모른다는 사실조차 모르는 게 위험합니다.
7. 6개월 전에 푼 문제가 기억나지 않는 것에 대하여
문제 해결에 몰입하고 나면 이전에 풀었던 문제는 거의 기억나지 않습니다. 6개월 전 것은 더더욱 기억나지 않습니다. 한동안 이게 제 약점이라고 생각했습니다.
그런데 지금은 다르게 봅니다. 원래 그렇게 살아왔고, 그게 정상입니다. 사람의 기억은 그렇게 설계되어 있지 않습니다.
그리고 이 사실이 앞의 Test Layer 이야기와 정확히 연결됩니다. 기억나지 않는다는 건 그 지식이 머리가 아니라 시스템에 있어야 한다는 신호입니다.
| 기억에 의존하면 | 시스템에 남기면 |
|---|---|
| 6개월 뒤엔 사라짐 | 테스트가 계속 검사 |
| "왜 이렇게 했더라" | 결정 기록(ADR)에 이유가 남음 |
| 사람이 바뀌면 소실 | 런북·문서로 인계됨 |
| 회귀를 사람이 발견 | CI가 발견 |
그래서 지금은 기억하려고 애쓰는 대신 기억을 외부화합니다. 6개월 뒤의 나는 남이라고 가정하고, 남이 읽을 수 있게 남깁니다. 기억력이 좋아서 잘하는 게 아니라, 기억이 필요 없게 만들어서 잘하는 쪽이 재현 가능합니다.
8. AI가 만든 것을 더 잘 인지하려면
제가 가장 답을 원했던 부분입니다. 위임은 하되 인지는 유지하고 싶은데 방법이 마땅치 않았습니다. 지금까지 쓰면서 효과가 있었던 것들을 적습니다.
① 본문이 아니라 경계를 읽는다.
AI가 만든 함수의 내부 구현을 한 줄씩 따라가면 금방 지칩니다. 대신 입력·출력·부작용·실패했을 때의 동작 네 가지만 봅니다. 이 넷이 맞으면 내부가 어떻게 생겼든 교체 가능합니다. 이건 예전에 사람이 짠 코드를 리뷰할 때도 결국 하던 일입니다.
② 예측하고 나서 실행한다.
코드를 보고 "이 입력이면 이 결과가 나올 것"이라고 먼저 예측한 다음 실행합니다. 예측과 결과가 다르면 그 지점이 제 이해의 구멍입니다. 이건 제가 기대와 실제의 간극에 대해 정리했던 프레임을 그대로 AI 코드에 적용한 것인데, 코드를 처음부터 읽는 것보다 훨씬 빠르게 모르는 지점을 찾아냅니다.
③ diff가 읽히는 크기로 자른다.
한 번에 큰 덩어리를 맡기면 결과를 검증할 방법이 사실상 없어집니다. 내가 읽고 판단할 수 있는 크기로 잘라서 맡기면, 위임하면서도 인지가 유지됩니다. 속도를 조금 내주고 통제를 사는 거래입니다.
④ 불변식을 먼저 준다.
앞에서 적은 그대로입니다. 만들기 전에 넘으면 안 되는 선을 주면, 검증할 것이 줄어듭니다.
9. 이해가 막힐 때 — 손코딩
방법론과 별개로, 제가 오래 써 온 개인적인 방법이 하나 있습니다. 코드가 도저히 이해되지 않을 때는 핵심 기능을 파악해서 그 함수들을 전부 손으로 써 봅니다.
효과가 확실합니다. 그리고 조금 우습지만 이유도 짐작이 갑니다. 한 번 손코딩을 끝내고 나면 머리가 "다음에 또 이걸 하기 싫어서" 이해가 빨라지는 느낌이 있습니다. 그래도 이해가 안 되면 한 번 더 씁니다. 지금까지 5번을 넘겨 본 적은 없었던 것 같습니다.
나중에 알고 보니 이건 근거가 있는 방식이었습니다. 읽기는 재인(recognition)이고 쓰기는 재생(recall)입니다. 읽을 때는 "아 알겠다" 싶은데 막상 백지에서 못 쓰는 이유가 이것입니다. 그리고 학습 연구에서 말하는 바람직한 어려움(desirable difficulty) — 적당히 힘든 방식으로 익힌 것이 더 오래 남는다는 것 — 과도 맞아떨어집니다. 손이 아픈 만큼 남습니다.
AI 시대의 변주도 하나 생겼습니다. AI가 짜 준 코드 중 이해가 안 되는 핵심 함수를 내가 먼저 손으로 써 보고 나서 AI 것과 비교합니다. 차이가 나는 자리가 정확히 제가 몰랐던 지점입니다. 전체를 옮겨 적는 것보다 빠르고, 검증과 학습이 한 번에 됩니다.
10. 태도에 관한 두 가지
기술 기준 말고, 일하는 방식에서 지키려는 것 두 개를 같이 적어 둡니다.
① 사용자의 불편을 지레짐작하지 않는다.
요구사항에 있는 성공 기준만 진행합니다. "이런 것도 필요하지 않을까" 하고 상상해서 넣기 시작하면 범위가 무한히 늘어나고, 정작 요청받은 것의 완성도가 떨어집니다.
다만 여기엔 구분이 필요합니다. 사용자가 원할 기능을 상상하는 것은 절제할 대상이고, 시스템이 깨질 방식을 예상하는 것은 직무입니다. 전자는 스코프이고 후자는 품질이라, 같은 "미리 생각하기"처럼 보여도 성격이 다릅니다.
② 새로운 요구사항은 완성된 이후에 별도로 일정을 잡는다.
진행 중에 들어온 요구를 그 자리에서 흡수하면 완성 시점이 계속 밀리고, 무엇이 끝났는지 아무도 모르게 됩니다. 지금 하는 것을 끝내고, 새 요구는 새 일정으로 잡는 것. 이게 전문적인 태도라고 생각합니다.
11. 지금의 기준을 한 장으로
정리하면 제가 지금 쓰는 체크리스트는 이렇습니다.
착수 전
- [ ] 성공 기준이 문장으로 적혀 있는가 (누가 무엇을 하면 무엇이 된다)
- [ ] 불변식이 적혀 있는가 (어떤 경우에도 무엇은 아니다)
- [ ] 이 작업에 되돌릴 수 없는 것이 포함되어 있는가 → 있으면 그 부분은 직접
위임할 때
- [ ] 내가 diff를 읽을 수 있는 크기로 잘랐는가
- [ ] 경계(입력·출력·부작용·실패 동작)를 내가 말할 수 있는가
끝낼 때
- [ ] 성공 기준이 Test Layer에 남았는가
- [ ] 불변식이 테스트로 남았는가
- [ ] 6개월 뒤의 내가 읽을 수 있게 결정의 이유가 남았는가
막혔을 때
- [ ] 예측하고 실행해서 어긋난 지점을 찾았는가
- [ ] 그래도 안 되면 손으로 써 본다 (경험상 5번 안에는 뚫림)
마치며
이 기준들이 완성된 답이라고 생각하지는 않습니다. 지금의 제 수준에서 이 해상도가 최선이고, 더 겪으면 또 달라질 것 같습니다.
다만 하나는 분명해졌습니다. 예전에는 "모르는 부분이 없어야 안전하다" 고 믿었는데, 지금은 "되돌릴 수 없는 것만 확실히 알고, 나머지는 필요할 때 내려갈 수 있으면 된다" 고 생각합니다. 전자는 달성 불가능한 목표라 늘 불안했고, 후자는 매일 조금씩 나아질 수 있는 목표라 덜 불안합니다.
그리고 신뢰로운 비즈니스 로직을 알아보는 눈은 결국 경험에서 나온다는 것도요. 목록은 빌려 올 수 있지만, 어떤 것이 되돌릴 수 없는지를 몸으로 아는 건 한 번 겪어 봐야 생기는 것 같습니다. 그래서 지금 목록이 여덟 줄인 게 부족하다고 보지는 않습니다. 앞으로 늘어날 자리라고 봅니다.
함께 읽으면 좋은 글:
- "뭔가 이상한데"가 신호일 때 — 기대와 실제의 간극 — 이 글 8절의 "예측하고 실행하기"의 원형
- 개인 아키텍처 시스템 — 하나의 템플릿 규칙으로 — 판단을 매번 새로 하지 않도록 시스템에 남기는 쪽의 기록
- [설계 판단] 커넥션 풀 크기는 무엇으로 정하는가 — "성질은 알고 구현은 필요할 때"의 실제 사례
- 아키텍처 최종장 — MVC에서 Clean·Hexagonal, 그리고 MSA까지 — 경계를 어디에 긋는가에 대한 정리
참고
- Jeff Bezos, Amazon Shareholder Letter (2015) — 한 방향 문 / 양방향 문 의사결정 구분
- Robert A. Bjork, Desirable Difficulties — 어렵게 익힌 것이 더 오래 남는다는 학습 연구
- Hypothesis · QuickCheck — 불변식을 테스트로 옮기는 속성 기반 테스트 도구
답글 남기기