비개발자가 AI로 개발할때 읽어보면 좋을 팁
이 문제는 “비개발자가 AI로 개발했기 때문에 생긴 단순 실수”로만 볼 일은 아닙니다. 더 정확히 말하면, 소프트웨어를 만들 때 필요한 요구사항 분석, 도메인 설계, 아키텍처 설계, 데이터 모델링, 테스트 설계 같은 중간 과정을 충분히 거치지 않고 구현으로 바로 넘어갔기 때문에 발생한 전형적인 구조적 문제에 가깝습니다.
일반적으로 소프트웨어 개발은 머릿속에 있는 아이디어를 곧바로 코드로 옮기는 작업이 아닙니다. 고객(본인)이 만들고 싶은 서비스가 있다면 먼저 그 서비스가 해결하려는 문제가 무엇인지 구체화해야 합니다. 누가 사용하는지, 어떤 상황에서 사용하는지, 무엇을 입력하는지, 어떤 기준으로 결과를 계산하는지, 어떤 결과를 보여주는지, 예외가 발생하면 어떻게 처리하는지를 하나씩 정의해야 합니다. 이 과정을 요구사항 정의 또는 요구사항 분석이라고 합니다.
저는 이 요구사항 정의에 개발 과정에서 굉장히 많은 공을 들여야 한다고 봅니다. 과장해서 하는 말이 아니라, 요구사항이 충분히 구체적이고 모순 없이 정리되었다면 개발의 30% 정도는 이미 끝났다고 봐도 됩니다. 실제로 구현에 들어가기 전에 무엇을 만들어야 하는지, 어떤 규칙을 따라야 하는지, 어디까지가 정상이고 어디부터가 오류인지가 명확해지기 때문입니다.
반대로 요구사항이 불명확하면 코드를 아무리 빠르게 작성해도 개발이 빨라지는 것이 아닙니다. 구현 도중에 계속 정책을 다시 정해야 하고, 기능을 만든 뒤에 해석이 달랐다는 사실을 알게 되며, 이미 만든 구조를 반복해서 수정하게 됩니다. 요구사항 정의 단계에서 한 시간 고민할 문제를 건너뛰면, 구현과 테스트 단계에서는 며칠 동안 수정해야 할 수도 있습니다.
요구사항은 단순히 “여러 송금 방법을 비교한다” 정도로 끝나서는 안 됩니다. 정상적인 상황뿐 아니라 예외 조건과 경계값까지 함께 정의해야 합니다. 입력 금액이 0인 경우, 음수인 경우, 지나치게 큰 경우, 환율 데이터가 없는 경우, 특정 업체가 일시적으로 중단된 경우, 수수료가 최소값이나 최대값에 걸리는 경우, 사용자가 선택한 자산과 내부 가격 기준이 일치하지 않는 경우에 시스템이 어떻게 동작해야 하는지를 미리 정해야 합니다. AI야 정리해줘! 해서 정리를 하더라도 정리는 해야하는 부분이지 건너뛸 부분은 아닙니다.
그리고 각각의 요구사항에는 가능하면 인수 조건, 즉 Acceptance Criteria가 있어야 합니다. 예를 들어 “웹과 앱의 계산 결과가 같아야 한다”는 요구사항만으로는 부족합니다. 보다 구체적으로는 “동일한 입력 금액, 송금 통화, 수령 통화, 조회 시점 및 업체 조건이 주어지면 웹과 Android 앱은 반올림 이전 계산값까지 동일한 결과를 사용해야 한다”와 같이 정의해야 합니다. 그래야 무엇을 구현해야 하는지 알 수 있고, 테스트에서도 통과와 실패를 구분할 수 있습니다.
코드가 요구사항을 만드는 것이 아니라 요구사항이 코드를 결정해야 합니다. 요구사항이 정리되지 않은 상태에서 바로 구현하면, 기능을 추가할 때마다 그 순간의 판단으로 코드를 만들게 됩니다. 처음에는 앱 화면 안에 계산식을 넣고, 나중에는 웹에서도 비슷한 계산을 만들고, 이후에 공통 계산 로직을 추가하는 식으로 구조가 누적됩니다. 그러면 시스템 전체를 관통하는 하나의 기준이 아니라, 서로 다른 시점에 만들어진 여러 기준이 공존하게 됩니다.
요구사항이 명확해야 그다음 단계인 설계도 가능합니다. 설계의 핵심은 시스템을 역할과 책임에 따라 나누는 것입니다. 특히 화면, 업무 로직, 데이터 처리는 분리되어야 합니다. 보통 이를 관심사의 분리, Separation of Concerns라고 합니다.
첫째는 UI 또는 표현 계층입니다. UI 계층은 사용자가 직접 보는 화면과 입력을 담당합니다. 버튼, 입력창, 아이콘, 문구, 정렬, 화면 이동, 소수점 표시 방식처럼 사용자가 보고 조작하는 부분입니다. 웹과 앱의 UI는 서로 달라도 됩니다. 모바일 앱은 작은 화면에서 빠르게 결과를 확인하는 것이 중요하고, 웹은 더 많은 정보와 분석 내용을 보여줄 수 있기 때문입니다. 님이 만든 서비스에서는 UI가 웹과 앱 두가지의 설계가 필요한거죠. 하지만 본질이 같은 서비스라면 UI외에는 다른부분의 설계는 웹과 앱이 구분될 필요는 없습니다.
둘째는 비즈니스 로직 또는 도메인 계층입니다. 이 영역은 서비스의 핵심 업무 규칙을 담당합니다. 어떤 업체를 비교 대상에 포함할지, 수수료를 어떻게 계산할지, 어떤 환율을 적용할지, 예상 수령액을 어떻게 산출할지, 어떤 크립토 자산의 가격을 사용할지 같은 규칙입니다. 이 부분은 웹과 앱이 달라져서는 안 됩니다. 같은 입력, 같은 기준 시점, 같은 업체 조건을 사용했다면 웹과 앱은 같은 계산 결과를 받아야 합니다. 화면은 달라도 되지만 돈을 계산하는 규칙은 같아야 합니다. 이렇게 관심사가 분리되었다면 님의 서비스도 웹과 앱이 계산결과가 다르게 나오는 일이 없었을 겁니다.
셋째는 데이터 계층입니다. 이 영역은 환율, 크립토 가격, 업체 정보, 수수료 정책, 지원 자산, 네트워크 정보 같은 데이터를 어디에서 가져오고, 어떻게 저장하고, 어떤 형식으로 변환할지를 담당합니다. 외부 API가 반환한 데이터를 화면이나 계산 코드에서 곧바로 사용하는 것이 아니라, 내부 시스템에서 사용하는 표준 데이터 모델로 변환해야 합니다. 외부 업체마다 필드명과 데이터 구조가 달라도 내부에서는 일관된 형식으로 다룰 수 있어야 합니다. 데이터가 분리 설계되지 않으면 AI는 데이터들을 소스에 포함할겁니다. 이건 정말 크리티컬한 문제를 야기할겁니다.
이 세 계층이 분리되지 않으면 화면 코드 안에 데이터와 계산식이 들어가고, 계산 코드가 외부 API의 응답 형식에 직접 의존하며, 앱과 웹이 각각 다른 방식으로 데이터를 해석하게 됩니다. 이 상태에서는 외부 API의 필드 하나가 변경되거나 계산 규칙 하나가 바뀔 때 여러 화면과 여러 서비스가 동시에 영향을 받습니다.
이와 관련된 대표적인 개념이 결합도, 즉 커플링(coupling)입니다. 커플링은 하나의 모듈이나 기능이 다른 모듈의 내부 구조, 데이터 형식, 처리 방식에 얼마나 강하게 의존하는지를 의미합니다. 예를 들어 Android 화면 코드가 업체별 수수료 계산식, 환율 조회 방법, 자산 매핑 방식까지 직접 알고 있다면 UI와 비즈니스 로직 사이의 커플링이 높은 상태입니다.
이 경우 수수료 정책이 바뀌면 공통 계산 코드만 수정해서 끝나는 것이 아닙니다. 앱 화면 코드도 수정해야 하고, 웹 화면 코드도 확인해야 하며, 데이터 변환 코드까지 함께 점검해야 합니다. 한 부분을 수정했는데 예상하지 못한 다른 부분이 반복해서 깨진다면 구성 요소 사이의 커플링이 지나치게 높지 않은지 확인해야 합니다.
좋은 설계는 커플링을 무조건 없애는 것이 아니라 불필요한 커플링을 낮추는 방향을 지향합니다. 소프트웨어의 구성 요소는 서로 협력해야 하므로 어느 정도의 의존 관계는 반드시 존재합니다. 중요한 것은 다른 모듈의 내부 구현을 직접 알고 의존하지 않고, 명확하게 정의된 인터페이스나 API를 통해 필요한 기능만 사용하는 것입니다.
웹과 앱은 계산식 내부가 어떻게 구성되어 있는지 알 필요가 없습니다. 입력값을 전달하면 계산 결과를 반환받는다는 계약만 알면 됩니다. 계산 공식이나 수수료 정책이 바뀌더라도 외부 인터페이스가 유지된다면 웹과 앱은 영향을 받지 않아야 합니다. 이것이 캡슐화와 정보 은닉의 목적입니다.
설계에서는 커플링과 함께 응집도(cohesion)도 중요합니다. 응집도는 하나의 모듈 안에 서로 관련된 책임이 얼마나 잘 모여 있는지를 의미합니다. 화면 모듈은 화면 표시와 사용자 입력에 집중해야 하고, 계산 모듈은 계산 규칙에 집중해야 하며, 데이터 모듈은 데이터 조회와 변환에 집중해야 합니다. 좋은 구조는 일반적으로 낮은 결합도와 높은 응집도, 즉 Low Coupling과 High Cohesion을 목표로 합니다. 서로 다른 책임은 분리하고 같은 책임은 한곳에 모으는 것입니다.
여기서 반드시 구분해야 할 것이 있습니다. 커플링을 낮춘다는 이유로 동일한 로직을 앱과 웹에 각각 따로 구현해서는 안 됩니다. 어떤 기능이 반드시 같은 규칙으로 동작해야 한다면 그 로직은 각각의 서비스에 중복해서 구현되는 것이 아니라 하나의 코드로 모듈화되어야 합니다. 그리고 각 서비스는 그 공통 모듈을 참조해야 합니다.
예를 들어 예상 수령액 계산, 수수료 적용, 환율 적용 순서, 업체 제외 조건, 크립토 자산과 가격 기준의 매핑처럼 웹과 앱에서 동일해야 하는 업무 규칙이 있다고 가정해 보겠습니다. 이 규칙을 Android 앱에 한 번 구현하고, 웹에 다시 구현하고, 서버에도 별도로 구현하면 처음에는 같은 결과가 나올 수 있습니다. 하지만 시간이 지나면 높은 확률로 서로 달라집니다.
수수료 정책이 변경되었을 때 웹은 수정했지만 앱은 빠뜨릴 수 있습니다. 한쪽에서는 반올림 방식을 변경했지만 다른 쪽에서는 과거 방식을 유지할 수 있습니다. 웹에서는 특정 업체를 제외했지만 앱에는 기존 조건이 남을 수 있습니다. 이렇게 같은 업무 규칙의 여러 구현이 시간이 지나면서 서로 달라지는 현상을 구현의 드리프트(drift)라고 할 수 있습니다.
따라서 동일한 기능은 재사용 가능한 하나의 코어 모듈이나 하나의 서버 기능으로 통합되어야 합니다. 모듈화란 단순히 코드를 여러 파일로 나누는 것이 아닙니다. 하나의 명확한 책임과 경계를 가진 기능 단위로 코드를 구성하고, 외부에서는 그 모듈이 공개한 인터페이스만 사용하도록 만드는 것입니다.
예를 들어 계산 기능을 하나의 코어 모듈로 만든다면 업체별 수수료 계산, 환율 선택과 적용, 지원 업체 여부 판단, 업체 제외 조건, 예상 수령액 계산, 크립토 자산과 가격 데이터의 매핑, 데이터 유효성 검증, 원본 계산값과 표시값의 구분, 반올림 규칙 등이 그 안에 함께 존재해야 합니다.
웹과 앱은 이 계산 로직을 각각 다시 구현하는 것이 아니라 해당 모듈이 반환한 결과를 사용해야 합니다. 웹과 앱이 같은 언어와 실행 환경을 사용한다면 공통 라이브러리나 공유 패키지로 만들 수도 있습니다. 하지만 웹, Android, iOS처럼 기술 환경이 서로 다르다면 계산 로직을 백엔드 서버에 두고 모든 클라이언트가 동일한 API를 호출하도록 만드는 방식이 일반적으로 더 안전합니다.
구조는 사용자 입력이 웹 또는 앱을 거쳐 공통 계산 API로 전달되고, 하나의 코어 계산 로직이 결과를 계산한 뒤 다시 각 화면에 반환하는 형태가 되어야 합니다. 웹에서는 계산 근거와 세부 정보를 자세히 보여줄 수 있고, 앱에서는 핵심 금액만 간단하게 표시할 수 있습니다. 하지만 두 화면이 사용하는 계산 결과는 하나의 코어 로직에서 만들어져야 합니다. 각 서비스가 동일한 로직을 복사해 소유하는 것이 아니라 하나의 구현을 참조하는 구조여야 합니다.
이것은 DRY, 즉 Don’t Repeat Yourself 원칙과 관련됩니다. DRY는 단순히 같은 코드 문장을 복사하지 말라는 의미가 아닙니다. 하나의 업무 지식이나 규칙이 시스템 안에서 여러 개의 서로 다른 구현으로 존재하지 않게 하라는 원칙입니다.
로직을 만들고 이 로직을 여기에 적용해줘 또 저기에 적용해줘라고 하는게 아니라 하나의 로직으로 하나의 코드로 모듈을 만들고 각각의 서비스에서 그 모듈을 참조해줘가 되어야합니다.
따라서 공통 로직을 만든다는 것은 공통 함수를 하나 추가하는 것으로 끝나지 않습니다. 기존에 존재하는 모든 계산 경로가 실제로 그 공통 로직만 통과하도록 변경해야 합니다. 공통 계산 모듈을 만들었더라도 Android 앱 내부에 기존 계산식이 남아 있거나, 특정 화면에서 계산 결과를 다시 보정하거나, 업체 제외 조건이 UI 코드에 따로 존재한다면 시스템에는 여전히 여러 개의 계산 기준이 있는 것입니다.
이 경우 공통 로직은 존재하지만 실질적인 단일 기준으로 작동하지 않습니다. 이를 방지하기 위한 개념이 Single Source of Truth, 즉 단일 진실 공급원입니다. 예상 수령액, 수수료, 적용 환율, 자산 가격 기준 같은 핵심 결과를 결정하는 규칙은 시스템 안에서 한곳에만 있어야 합니다. 다른 서비스와 화면은 그 규칙을 복사해서 가지는 것이 아니라 참조해야 합니다.
여기서 참조한다는 것은 코드를 복사해 가져간다는 의미가 아닙니다. 공통 라이브러리를 의존성으로 사용하거나, 공통 모듈의 인터페이스를 호출하거나, 중앙 계산 API에 요청해 결과를 받는 것을 의미합니다.
정리하면 두 가지 원칙을 함께 적용해야 합니다. 책임이 다른 것은 분리해야 하고, 규칙이 같은 것은 통합해야 합니다. UI, 비즈니스 로직, 데이터 처리처럼 책임이 다른 영역은 분리해 커플링을 낮춰야 합니다. 반대로 여러 서비스에서 동일해야 하는 업무 규칙은 각각 구현하지 말고 하나의 코어 로직으로 통합해야 합니다.
이 두 원칙을 혼동하면 한쪽에서는 모든 코드가 강하게 얽히고, 다른 한쪽에서는 동일한 계산식이 앱과 웹에 중복되는 문제가 동시에 발생합니다.
같은 관점에서 객체지향 설계에 대해서 조금 공부해보시거나 AI한테 설계 명령할때 객체지향적으로 설계해달라고 해보는 걸 추천합니다. 이러한 책임 분리와 변경 통제를 위한 방법 중 하나입니다. 객체지향은 단순히 클래스를 많이 만드는 것이 아닙니다. 데이터와 그 데이터를 처리하는 책임을 적절한 객체에 배치하고, 객체들이 명확한 인터페이스를 통해 협력하도록 만드는 방식입니다.
예를 들어 송금 업체마다 수수료 계산 방식이 다르다고 가정할 수 있습니다. 어떤 업체는 고정 수수료를 받고, 어떤 업체는 비율 수수료를 받고, 어떤 업체는 금액 구간별로 다른 수수료를 적용할 수 있습니다. 이때 모든 업체의 조건을 하나의 긴 if문이나 switch문으로 처리하면 업체가 추가될수록 코드가 복잡해집니다.
대신 공통된 계산 인터페이스를 정의하고 업체별 계산 방식을 각각의 구현체로 분리할 수 있습니다. 개념적으로 모든 업체는 현재 비교 가능한 업체인지 판단하고, 적용 환율을 결정하고, 수수료를 계산하고, 예상 수령액을 계산하는 기능을 제공하도록 정의할 수 있습니다. 각 업체는 같은 인터페이스를 따르되 내부 계산 방식은 다르게 구현합니다.
이러한 구조는 전략 패턴(Strategy Pattern)으로 구현할 수 있습니다. 새로운 업체를 추가할 때 기존 계산 코드 전체를 수정하는 대신 새로운 계산 구현체를 추가할 수 있습니다. 이는 객체지향 설계의 개방-폐쇄 원칙, Open-Closed Principle과 관련됩니다. 기능 확장에는 열려 있고 기존 코드 변경에는 닫혀 있도록 설계하라는 원칙입니다.
다만 추상화와 객체지향 구조를 많이 넣는다고 무조건 좋은 설계가 되는 것은 아닙니다. 추상화는 변화하는 부분과 변하지 않는 부분을 구분하기 위해 사용해야 합니다. 아직 변화 가능성도 없고 재사용될 가능성도 낮은 기능까지 인터페이스와 계층으로 감싸면 오히려 복잡성만 높아집니다.
좋은 설계는 모든 것을 추상화하는 것이 아니라, 실제로 변경 가능성이 높고 여러 곳에서 반복되는 규칙을 적절한 수준으로 분리하는 것입니다. 재사용성과 유연성도 마찬가지입니다. 리팩터링의 목적은 단순히 코드 길이를 줄이거나 코드를 보기 좋게 정리하는 것이 아닙니다. 중복을 제거하고, 책임을 명확히 하고, 변경 영향 범위를 줄이고, 테스트하기 쉬운 구조로 만드는 것입니다.
이번 사례처럼 기존 앱과 웹에 이미 계산 로직이 존재한 상태에서 공통 계산 방식을 나중에 적용했다면 단순히 새 모듈을 추가하는 것만으로는 부족합니다. 기존 로직을 모두 찾아 제거하거나 공통 모듈을 호출하도록 변경해야 합니다. 그렇지 않으면 어떤 화면은 신규 로직을 사용하고 어떤 화면은 과거 로직을 사용하는 상태가 됩니다. 이것이 신규 로직과 레거시 로직이 혼재된 상태입니다.
이런 문제를 해결하려면 먼저 현재 시스템 안에서 계산이 수행되는 모든 위치를 식별해야 합니다. 어떤 화면이 어떤 계산 함수를 호출하는지, 어떤 조건에서 별도 보정이 이루어지는지, 업체 제외 조건이 어디에 있는지, 반올림은 어느 단계에서 처리되는지 호출 경로를 추적해야 합니다. 이 작업을 의존성 분석 또는 호출 경로 분석이라고 할 수 있습니다. 그다음 모든 계산 경로를 하나의 코어 로직으로 수렴시켜야 합니다.
자료구조와 데이터 모델 설계도 중요합니다. USDT와 USDC 아이콘이 깨졌고, 사용자가 선택한 크립토 자산과 내부에서 참조하는 가격 기준이 일치하는지 의심되었다는 부분은 단순한 이미지 문제만은 아닐 수 있습니다. 자산을 문자열 이름만으로 구분했거나, 화면 표시용 심볼과 가격 조회용 식별자를 혼용했거나, 네트워크 정보를 데이터 구조에 포함하지 않았을 가능성이 있습니다.
예를 들어 USDT라는 문자열 하나만으로 자산을 표현하는 것은 충분하지 않을 수 있습니다. 실제 시스템에서는 사용자에게 표시할 자산명, 내부 자산 식별자, 가격 조회 API용 식별자, 블록체인 네트워크, 컨트랙트 주소, 소수점 자릿수, 아이콘 리소스 경로, 지원 여부, 외부 업체별 자산 코드, 가격 제공 업체별 매핑 코드 등을 구분해야 합니다.
이런 값들을 하나의 구조화된 자산 객체나 데이터 모델로 관리해야 합니다. 화면마다 문자열을 직접 연결하면 오타, 대소문자 차이, 잘못된 매핑, 기본값 오용 같은 문제가 발생하기 쉽습니다. BTC와 ETH는 정상인데 stablecoin만 이상했다면 stablecoin에만 적용되는 별도의 분기, 매핑 테이블, 기본 이미지 처리 또는 가격 조회 경로가 있었는지 확인해야 합니다.
서로 다른 송금 업체의 예상 수령액 상단값이 소수점 둘째 자리까지 동일했던 현상도 여러 원인을 의심할 수 있습니다. 동일한 계산 결과 객체를 여러 업체가 공유했거나, 반복 처리 과정에서 이전 값을 초기화하지 않았거나, 업체별 값이 아니라 전체 최대값을 잘못 표시했거나, 캐시 키에 업체 식별자가 포함되지 않았거나, 자료구조의 키가 통화쌍만 포함하고 업체를 구분하지 못했거나, 비동기 응답이 잘못된 화면 항목에 연결되었거나, 동일한 객체 참조를 결과 리스트에 반복 저장했거나, 반올림을 계산 중간 단계에서 너무 일찍 수행했거나, 정렬이나 그룹화 과정에서 값이 덮어쓰기 되었을 가능성이 있습니다.
이런 문제는 화면을 눈으로 비교하는 것만으로는 정확히 찾기 어렵습니다. 테스트가 필요합니다.
가장 먼저 필요한 것은 핵심 계산 로직에 대한 단위 테스트(Unit Test)입니다. 단위 테스트는 작은 기능 단위에 정해진 입력을 넣었을 때 예상한 출력이 나오는지를 자동으로 검증하는 방법입니다. 업체 A에 1,000 BRL을 입력했을 때 수수료와 수령액이 예상값과 일치하는지, 업체 B가 제외 조건에 해당하면 결과에서 제거되는지, USDT를 선택했을 때 USDC 가격이 사용되지 않는지, 환율 데이터가 없을 때 잘못된 계산값을 반환하지 않는지, 같은 입력을 반복 실행해도 같은 결과가 나오는지, 업체별 계산 결과가 서로 독립된 객체로 생성되는지, 반올림 이전 값과 화면 표시값이 구분되는지 등을 테스트해야 합니다.
여러 모듈이 연결된 흐름은 통합 테스트(Integration Test)로 확인해야 합니다. 환율 조회, 업체 정보 조회, 수수료 계산, 예상 수령액 반환까지 전체 흐름이 예상대로 동작하는지 검증해야 합니다. 외부 API는 항상 같은 값을 반환하지 않기 때문에 테스트에서는 실제 외부 API 대신 미리 정해진 응답을 반환하는 Mock이나 Stub을 사용할 수 있습니다.
웹과 앱이 같은 API 계약을 따르는지는 계약 테스트(Contract Test)로 확인할 수 있습니다. 요청 필드, 응답 필드, 데이터 타입, 필수값, 오류 형식을 명확히 정의하고 각 클라이언트가 그 계약을 지키는지 검증하는 방식입니다. 또한 동일한 요청을 웹과 앱에서 보냈을 때 같은 서버 응답을 받는지를 자동으로 비교하는 테스트도 필요합니다.
기존에 정상적으로 동작하던 기능이 새로운 수정 이후 깨지는 것을 회귀(regression)라고 합니다. 이번처럼 공통 계산 로직을 적용한 뒤 앱의 일부 과거 기능이 잘못 동작했다면 전형적인 회귀 문제입니다. 이를 방지하기 위해 회귀 테스트(Regression Test)가 필요합니다. 기존에 검증된 입력과 출력 사례를 테스트로 저장해 두고, 코드를 변경할 때마다 과거 결과가 유지되는지 자동으로 확인해야 합니다.
특히 금융 서비스에서는 몇 개의 대표 사례만 테스트해서는 부족합니다. 정상값, 최소값, 최대값, 경계값, 소수점 값, 누락된 데이터, 잘못된 데이터, 외부 API 지연 같은 다양한 조건을 테스트해야 합니다.
금액 계산에서는 부동소수점 처리도 중요합니다. 일반적인 실수형 자료형은 소수 계산에서 미세한 오차를 만들 수 있습니다. 금융 계산에서는 정밀도를 보장하는 Decimal 계열 자료형을 사용하고, 반올림 방식과 소수점 자릿수를 명시적으로 정해야 합니다. 반올림은 계산 중간마다 수행하지 않고 가능한 한 최종 표시 단계에서 수행하는 것이 일반적입니다. 중간 단계에서 반복적으로 반올림하면 업체별 결과가 왜곡될 수 있습니다.
관찰 가능성(Observability)도 필요합니다. 결과 숫자만 사용자에게 보여주는 것으로는 문제가 발생했을 때 원인을 추적하기 어렵습니다. 어떤 환율, 어떤 가격, 어떤 수수료 정책, 어떤 데이터 시점, 어떤 계산 버전을 사용했는지 기록해야 합니다.
하나의 예상 수령액 결과에는 요청 ID, 사용자 입력값, 업체 ID, 입력 통화와 출력 통화, 선택된 크립토 자산, 가격 조회에 사용된 자산 ID, 적용 환율, 환율 데이터의 조회 시각, 적용 수수료, 수수료 정책 버전, 반올림 이전 금액, 최종 표시 금액, 업체 제외 여부와 사유, 계산 로직 버전 등이 함께 남는 것이 좋습니다.
이런 정보를 구조화된 로그로 남기면 결과가 이상할 때 화면을 보며 추측하지 않고 실제 계산 경로를 추적할 수 있습니다.
금융 서비스에서 틀린 숫자가 그럴듯하게 표시되는 것이 위험하다고 인식한 것은 맞습니다. 다만 이를 방지하는 방법은 사람이 매번 웹과 앱을 나란히 놓고 비교하는 것이 아닙니다. 그 방식은 수동 디버깅이지 품질 보증 체계는 아닙니다. 사람이 직접 확인하지 않아도 잘못된 결과를 자동으로 탐지하는 구조가 있어야 합니다.
배포 전에는 자동 테스트를 실행하고 핵심 테스트가 하나라도 실패하면 배포가 중단되도록 해야 합니다. 운영 중에도 비정상적인 결과를 탐지할 수 있어야 합니다. 예를 들어 서로 다른 업체의 결과가 반복적으로 완전히 동일하거나, 수령액이 입력 금액과 비정상적으로 큰 차이를 보이거나, 가격 데이터의 시점이 너무 오래된 경우 경고를 발생시킬 수 있습니다.
이런 검증을 불변조건, 즉 Invariant 검증이라고 볼 수 있습니다. 시스템이 정상이라면 반드시 지켜져야 하는 조건을 정의하고, 조건을 위반하면 오류로 처리하는 방식입니다.
이번 사례에서 핵심 문제는 실수가 발생했다는 사실이 아닙니다. 개발자도 실수합니다. 문제는 그 실수를 자동으로 발견할 수 있는 구조가 없었다는 점입니다. 웹은 정상이고 앱은 비정상인 상태가 배포된 뒤 수동 비교를 통해서만 발견되었다면 설계, 테스트, 배포 절차에 검증 장치가 부족했다는 의미입니다.
이 문제를 해결하려면 현재 발견된 버그만 고쳐서는 안 됩니다. 먼저 시스템 구조를 파악해야 합니다. 앱, 웹, 서버에서 계산 로직이 각각 어디에 존재하는지 목록을 만들고, 동일한 업무 규칙이 중복 구현된 위치를 찾아야 합니다. 기존 방식과 신규 방식이 동시에 존재하는 지점과 업체별 예외 처리, 자산 매핑이 어느 계층에 들어 있는지도 확인해야 합니다.
그다음 핵심 계산 책임을 하나의 코어 모듈이나 서버 API로 집중시켜야 합니다. 앱과 웹은 계산하지 않고 입력을 전달하고 결과를 받아 표현하도록 바꾸는 것이 좋습니다. 기존 계산 코드는 제거하거나 공통 로직 호출로 대체해야 합니다.
그리고 현재 정상이라고 판단한 사례들을 테스트 케이스로 만들어야 합니다. 이후 코드가 변경될 때마다 자동으로 같은 결과가 유지되는지 확인해야 합니다.
개발 방법론 관점에서는 해결하려는 사용자 문제를 정의하고, 요구사항에 충분한 시간과 노력을 투입한 뒤, 기능 요구사항과 비기능 요구사항을 구분하고, 각 요구사항의 인수 조건과 정상·예외·실패·경계 조건을 정리해야 합니다. 그다음 핵심 도메인 개념과 업무 규칙, 데이터 모델과 식별자 체계를 설계하고, UI·비즈니스 로직·데이터 계층의 책임과 모듈 간 인터페이스를 정해야 합니다.
그 이후에 동일해야 하는 로직을 하나의 코어 모듈로 통합하고, 구현 전에 테스트 사례를 정의한 뒤, 핵심 도메인 로직과 API, 외부 데이터 연동, 웹과 앱 UI를 순서대로 구현해야 합니다. 마지막으로 단위 테스트, 통합 테스트, 계약 테스트, 로그와 모니터링을 구성하고, 자동 검증을 통과한 버전만 배포해야 합니다.
질문의 글에서는 먼저 앱을 만들고, 이후 웹을 만들고, 나중에 공통 계산 방식을 적용했다고 설명했습니다. 이 순서 자체가 무조건 잘못된 것은 아닙니다. 프로토타입이나 개념 검증 단계에서는 충분히 가능한 접근입니다.
문제는 요구사항과 구조가 충분히 정리되지 않은 프로토타입에 계속 기능을 덧붙이면서 그것을 운영 서비스로 확장하는 경우입니다. 프로토타입은 아이디어가 가능한지 빠르게 확인하기 위한 코드입니다. 운영 서비스는 정확성, 유지보수성, 테스트 가능성, 장애 대응 가능성, 보안, 데이터 추적성을 갖춰야 합니다.
프로토타입이 화면에서 정상적으로 동작한다고 해서 운영 가능한 소프트웨어가 된 것은 아닙니다. 프로토타입에서 운영 서비스로 넘어갈 때는 요구사항을 다시 확정하고 구조를 정리하는 과정이 필요합니다. 이를 하드닝(hardening) 또는 프로덕션화(productionization)라고 할 수 있습니다. 코드를 정리하고, 중복을 제거하고, 계산 로직을 통합하고, 테스트와 로그를 추가하고, 오류 처리와 보안을 강화해야 합니다.
AI를 사용한 개발에서는 이 과정이 더 중요합니다. AI는 사용자가 요청한 단위 기능을 빠르게 구현하는 데 강합니다. 하지만 요구사항이 명확하지 않으면 AI 역시 그때그때 제시된 지시를 기준으로 코드를 만들 뿐입니다.
이전 요청과 현재 요청의 정책이 충돌하더라도 어느 쪽이 최종 업무 규칙인지 스스로 결정해 주지 못할 수 있습니다. 각 대화에서 주어진 문제를 국소적으로 해결하기 때문에 기존 코드와 유사한 로직을 다시 만들 수 있고, 다른 전제를 가진 코드를 추가할 수 있으며, 어느 코드가 시스템의 최종 기준인지 판단하지 못할 수 있습니다.
사용자가 명확한 요구사항과 아키텍처 원칙을 제시하지 않으면 AI는 현재 오류를 해결하기 위한 코드를 계속 덧붙이는 방식으로 대응할 가능성이 높습니다. 그 결과 개별 기능은 동작하지만 시스템 전체에서는 로직이 분산되고, 중복되고, 서로 충돌할 수 있습니다.
AI를 사용한다고 소프트웨어공학이 불필요해지는 것이 아닙니다. 오히려 구현 속도가 빨라지기 때문에 요구사항 분석, 설계, 테스트, 코드 리뷰, 변경 관리가 더 중요해집니다. 코드를 빠르게 만들 수 있다는 것은 잘못 정의된 요구사항과 잘못된 구조도 더 빠르게 커질 수 있다는 뜻입니다.
이번 문제를 한 문장으로 정리하면 다음과 같습니다. 만들고 싶은 서비스를 머릿속에서 바로 구현으로 옮기면서 가장 많은 공을 들여야 할 요구사항 정의를 비롯해 도메인 설계, 데이터 모델링, 아키텍처 설계, 모듈화, 테스트 설계 같은 중간 과정이 생략되었고, 이후 공통 계산 방식을 뒤늦게 적용하는 과정에서 기존 로직과 신규 로직이 혼재해 앱과 웹의 일관성이 깨진 것입니다.
이것은 비개발자이기 때문에 비웃을 문제는 아닙니다. 하지만 단순한 실수 몇 개로 가볍게 볼 문제도 아닙니다. 전형적인 요구사항 정의, 소프트웨어 설계, 품질 검증 부족의 결과입니다.
하루 동안 웹과 앱을 직접 비교한 것은 문제를 찾기 위한 디버깅으로는 의미가 있습니다. 그러나 그것을 해결 방법으로 삼아서는 안 됩니다. 사람이 매번 두 화면을 열어 비교하지 않아도, 같은 입력은 같은 코어 로직을 거쳐 같은 결과를 반환한다는 사실이 자동으로 검증되어야 합니다.
앞으로 필요한 것은 더 많은 기능을 추가하는 것이 아니라 시스템의 요구사항과 기준을 먼저 세우는 일입니다. 누구를 위한 서비스인지, 어떤 입력을 받아 어떤 규칙으로 계산하는지, 무엇이 정상 결과인지, 무엇이 오류인지, 어떤 데이터가 기준인지부터 문서로 명확히 만들어야 합니다.
그다음 무엇이 코어 로직인지, 계산은 어디에서 이루어지는지, 웹과 앱이 어떤 인터페이스로 연결되는지, 어떤 테스트를 통과해야 배포할 수 있는지를 정의해야 합니다. 요구사항 정의만 제대로 되어도 개발의 상당 부분은 이미 끝난 것입니다. 반대로 이 기반 없이 기능을 계속 추가하면 지금 발견한 버그를 수정하더라도 비슷한 문제가 다른 형태로 반복될 가능성이 높습니다.
개발의 핵심은 코드를 작성하는 행위 자체가 아닙니다. 고객이 원하는 것을 정확한 요구사항으로 바꾸고, 복잡한 요구사항을 구조화하고, 서로 다른 책임을 분리하고, 동일한 규칙은 하나로 통합하고, 변경의 영향을 통제하고, 잘못된 결과를 자동으로 검출할 수 있는 시스템을 만드는 일입니다.
이건 AI로 개발할때도 다르지 않다고 봅니다.
AI를 통해서 전부 하더라도 그냥 아이디어를 죽 늘어놓고 AI야 개발해줘라고 하는 것보다는 AI와 요구사항을 같이 정의하고 정의하는 과정에서 애매한걸 AI에게 시켜서 명확하게 만들고 그 요구사항 정의된 항목을 통해 UI, 프로세스, 데이터구조를 각각 설계하도록 요청하고 그렇게 만들어진 설계를 통해 순차적으로 개발하도록 요청한다면 똑같이 비개발자가 AI통해 만들어도 최종 퀄리티는 완전 다를거라고 봅니다.