소프트웨어 개발은 오랫동안 사람들이 코드를 읽고 이해할 것이라고 가정해 왔습니다. 우리는 명확한 이름을 사용하고 긴 함수를 분리하여 다음 사람이 우리의 의도를 따라 할 수 있도록 합니다.
하지만 AI가 지속적으로 코드를 생성하게 되면서, 인간의 독해 속도가 병목 현상이 됩니다. 변경 사항을 검토한다는 것은 단순히 diff(차이점)만 보는 것을 넘어섭니다. 주변 로직과 기존 요구사항 역시 중요합니다. 우리가 읽어야 하는 양은 계속해서 늘어나고 있습니다.
우리는 코드를 관찰할 수 있는 방법이 필요합니다. 먼저 변경 사항이 보존해야 할 속성(property)을 결정하고, 그런 다음 그것들을 확인하는 데 필요한 정보를 추출해야 합니다. 목표는 누군가 전체 구현 내용을 읽도록 요구하지 않으면서도, 그 변화가 무엇을 보존하고 무엇을 변경했는지에 따라 변경 사항을 판단하는 것입니다.
관심 있는 속성을 법(Law)으로 표현하기
**법(Law)**은 소프트웨어가 만족해야 하는 속성(property)을 명시합니다. 즉, 어떻게 동작해야 하는지, 어떻게 구조화되어야 하는지, 혹은 변경 후에도 무엇이 진실로 남아 있어야 하는지를 정의합니다.
명세서(Specifications)는 동작(behavior)을 설명합니다. 설계 규칙(Design rules)은 구조와 의존성을 제약합니다. 호환성 요구사항(Compatibility requirements)은 변화를 겪더라도 살아남아야 할 동작을 식별합니다. 이 모든 것은 소프트웨어에 대해 우리가 참이기를 바라는 무언가를 표현합니다. 법(Laws)은 이러한 다양한 요구사항들을 일반화하여 공통된 프레임워크에서 작업할 수 있게 합니다.
주제와 조건이 명확해지면, 익숙한 요구사항들은 법(Law)의 형태로 작성될 수 있습니다.
| 요구사항 출처 | 보존해야 할 속성 | 검토할 내용 |
|---|
| 기능적 명세서 (Functional specification) | 장바구니에 주문 항목을 순서대로 생성하는 것 | 주문 항목을 생성하는 코드 |
| ... | | |
법(Law) 확인을 위한 원자어(Atom) 어휘 정의하기
법(Law)을 확인하려면, 코드에서 무엇을 찾고 어떻게 기록해야 할지 결정해야 합니다. **원자어 정의(Atom definitions)**가 바로 그 어휘를 제공합니다. 우리가 추출하는 모든 사실(fact)은 이러한 정의들을 사용하여 원자어(Atom)로 기록됩니다.
“UI 계층은 영속성 계층(persistence layer)을 직접 호출해서는 안 된다”와 같은 규칙의 경우, 누가 누구를 호출하는지, 그리고 각 계층이 어디에 속하는지를 기록합니다. 주문 라인 순서화(order-line sequencing)의 경우, 어떤 종류의 작업인지, 그리고 그 처리 순서는 어떠한지를 기록합니다. 우리가 확인하고자 하는 속성(property)에 따라 동일한 코드가 서로 다른 관찰 내용(observations)을 요구하게 됩니다.
ArchMap은 이러한 어휘집 내에서 코드로부터 추출된 사실들(facts)을 수집합니다.
| 요소 (Element) | 역할 (Role) |
|---|
| Law | 주체가 만족해야 하는 속성을 명시함 |
| ... | |
| 우리는 AI에게 코드를 관찰하도록 요청한 다음, ArchMap에 있는 값들이 'Law'를 충족하는지 확인합니다. | |
'Law'와 'Atom' 어휘집은 특정 언어나 프레임워크에 독립적입니다. 만약 우리가 “주문 라인 생성(create an order line)” 또는 “재고 할당(allocate stock)”과 같이 의미로 작업을 정의한다면, Python과 Java 구현체 모두에 동일한 Law를 사용할 수 있습니다. AI는 각 구현체를 읽고 공유된 어휘집에서 발견한 내용을 기록합니다.
또한 이 시스템은 코드 조각이 비즈니스 용어상 무엇을 하는지 해석할 수도 있습니다. 여러 함수에 걸쳐 호출 시퀀스를 따라가며, “여기가 재고가 할당되는 곳이다”라고 기록할 수 있습니다. 관찰 내용에는 구문(syntax) 및 타입 정보만으로는 포착하기 어려운 의미가 포함될 수 있습니다.
실제 예시: 재고 할당 수정이 표시 순서를 변경함
Python과 Django로 구축된 전자상거래 프레임워크인 django-oscar는 재고 할당 변경이 구매 항목의 표시 순서에 영향을 미치는 유용한 예시를 가지고 있습니다.
주문이 이루어질 때, 애플리케이션은 바스켓(basket)으로부터 주문 라인을 생성하고 재고를 할당합니다. 원래 이 두 작업 모두 하나의 루프 내에서 실행되었습니다. 의사 코드(pseudocode)로 표현하면 다음과 같습니다:
for line in basket_lines:
create_order_line(line)
allocate_stock(line)
변경 사항이 도입되면서 재고 할당에 대한 일관된 처리 순서가 생겼습니다. 목표는 여러 주문이 동시에 들어올 때 재고 락(stock locks)을 동일한 순서로 확보하는 것이었습니다. 바스켓 라인은 루프에 진입하기 전에 재고 기록별로 정렬되었습니다:
for line in sort_by_stockrecord(basket_lines):
create_order_line(line)
allocate_stock(line)
이 코드는 재고 할당 순서를 맞추었습니다. 또한 두 작업이 여전히 같은 루프 내에 있었기 때문에 주문 라인이 생성되는 순서도 변경되었습니다.
바스켓에 노트가 먼저 오고 펜이 오는 경우라도, 재고 기록은 반대 순서로 정렬될 수 있습니다. 이제 코드는 펜의 주문 라인을 먼저 생성합니다. django-oscar가 주문 라인을 기본 키(primary-key) 순서로 가져왔기 때문에, 이 변경 사항은 주문 상세 정보, 인보이스 및 이메일에 항목이 표시되는 방식에 영향을 미쳤습니다.
두 가지 Law 작성하기
확인해야 할 두 가지 속성이 있습니다:
| Law | Property |
|---|
| L1 | 재고 기록 순서로 재고 할당 (Allocate stock in stock-record order) |
| L2 | 바스켓 순서로 주문 라인 생성 (Create order lines in basket order) |
L1은 변경이 의도한 목표입니다. L2는 반드시 보존해야 하는 것입니다.
AI에게 작업과 그 순서를 추출하도록 요청하기
이 Law들을 확인하려면 각 작업의 종류와 실행 순서가 필요합니다. 다음과 같은 Atom 필드를 정의했습니다:
| Field | 기록할 내용 |
|---|
kind | 주문 라인 생성 또는 재고 할당 |
| ... | |
기록된 loop 또한 두 작업이 함께 재정렬된 이유를 설명하는 데 도움이 됩니다. | |
저는 AI에게 이러한 정의와 Law들을 제공하고, 정렬 변경 전후의 코드를 읽어달라고 요청했습니다. 그 결과로 생성된 ArchMap에는 다음과 같은 관찰 내용이 포함되었습니다:
| Version | Operation | Order | Loop |
|---|
| Before the change | Create order line | Basket order | 1 |
| ... | | | |
| ArchMap은 또한 소스 위치를 기록하므로, 특정 값에 출처가 된 코드를 대조하여 확인할 수 있습니다. | | | |
ArchSig로 두 Law 모두 확인하기
ArchSig는 관찰된 정보를 Law들과 비교하여 확인하는 개발 중인 도구입니다. Atom 정의, Law, 그리고 ArchMap을 v0.6.0에 전달한 결과 다음과 같은 결과가 나왔습니다:
| Version | L1: allocate in stock-record order | L2: create order lines in basket order |
|---|
| Before the change | Not satisfied | Satisfied |
| After sorting | Satisfied | Not satisfied |
재고 할당 순서는 고정되었지만, 주문 라인 시퀀스는 그에 따라 변경되었습니다. 의도된 수정 사항만을 검토할 때는 놓치기 쉬운 이러한 효과는 해당 속성을 법칙(Law)으로 작성하면 명시적으로 확인할 수 있는 무언가가 됩니다.
두 가지 법칙을 모두 만족시키기 위해 루프 분리하기
관찰 결과, 두 작업 모두 동일한 루프 내에 있다는 것을 알 수 있었습니다. 이것이 재고 할당 순서 변경이 주문 라인 생성까지 변경시킨 이유입니다. 루프를 분리하면 각 작업이 필요한 순서를 사용할 수 있게 됩니다:
for line in basket_lines:
create_order_line(line)
...
django-oscar가 수정된 방식도 이와 같습니다. PR #4594에서 재고 주문 변경이 도입되었고, PR #4598에서 루프를 분리했습니다.
동일한 필드를 가진 수정된 코드를 관찰했을 때 다음과 같은 결과가 나왔습니다:
| 작업 | 주문 (Order) | 루프 (Loop) |
|---|
| 주문 라인 생성 | 바스켓 주문 (Basket order) | 1 |
| 재고 할당 | 재고 기록 주문 (Stock-record order) | 2 |
이 ArchMap을 확인한 결과, L1과 L2 모두 만족하는 것을 보여주었습니다. 이제 재고 할당은 요구되는 순서를 따르게 되었으며, 주문 라인은 여전히 바스켓을 따릅니다.
명세(specifications)와 관례(conventions)를 보존해야 할 속성으로 취급하기
이 예시는 재고 처리에 대한 내부 요구사항과 표시 순서에 대한 사용자 가시적 요구사항을 결합합니다. 소프트웨어는 또한 설계 규칙, 호환성 약속, 비즈니스 규칙을 가지고 있습니다. 일부는 명세(specifications)에 존재하고, 다른 일부는 팀이 공유하는 관례(conventions)입니다.
이 모든 것을 보존해야 할 속성으로 생각하면, 변경 후에 확인해야 할 항목 목록에 모두 넣을 수 있게 됩니다. 이것이 그것들을 법칙(Laws)으로 일반화하는 가치입니다. 우리는 고치고 싶은 것과 유지해야 하는 것을 함께 고려할 수 있습니다.
각 요구사항에 대해 무엇이 참이어야 하고, 무엇으로부터 참이어야 하는지 질문해 보세요. 이를 확인하는 데 필요한 정보가 더 명확해집니다. 법칙은 명세와 관례를 코드를 통해 조사할 수 있는 질문으로 바꿉니다.
법칙을 방정식으로 취급하고 해 공간 탐색하기
법(Law)이 충족되지 않는다면 무엇을 변경해야 할까요? 법들을 방정식으로 표현하면 그 해들 중에서 복구 방안을 찾을 수 있습니다. 여러 개의 법(Law)을 동시에 만족하는 소프트웨어는 거대한 연립 방정식 시스템의 해로 간주될 수 있습니다.
(\text{x})를 변수들의 튜플이라고 하고, 법들을 다음과 같이 표현합니다:
$$
f_1(x)=0,\quad f_2(x)=0,\quad\ldots,\quad f_m(x)=0. $$
모든 방정식을 만족하는 튜플들은 해 집합(solution set)을 형성합니다:
$$
\mathcal S={x\mid f_1(x)=\cdots=f_m(x)=0}. $$
진단(Diagnosis)은 현재 값이 (\mathcal S)에 속하는지 여부를 묻습니다. 만약 그렇지 않다면, 우리는 (\mathcal S) 내에서 이동할 상태를 검색할 수 있습니다. 동일한 법들은 문제점을 찾는 것과 그것을 복구하는 것 모두를 지원합니다.
각 방정식은 선택지를 좁힌다
두 개의 유리수 값 변수 (u)와 (v) 그리고 하나의 방정식을 예로 들어보겠습니다:
$$
u+v=2. $$
이 방정식에는 ((u,v)=(0,2),(1,1),(2,0))을 포함하여 무한히 많은 해가 존재합니다. 일반적으로 해는 (t)가 자유롭게 변할 수 있는 형태인 ((t,2-t))의 형태를 가집니다.
여기에 (u-v=0)을 추가해 보겠습니다:
$$
\begin{cases}
u+v=2,\
u-v=0. \end{cases} $$
두 방정식을 더하면 (2u=2)가 되어, 해는 유일하게 ((u,v)=(1,1))이 됩니다. 만약 또한 (u=0)을 요구한다면, 세 방정식 모두를 만족하는 해는 존재하지 않습니다.
해를 기하학적으로 보기
변수들을 좌표로 취급하면 해들은 기하학적 모양을 갖게 됩니다. 방정식 (u+v=2)와 (u-v=0)은 각각 직선을 나타내며, ((1,1))은 이들의 교점입니다. 추가적인 방정식이 하나씩 더해질 때마다 후보군들은 모든 조건을 만족하는 점들로 좁혀집니다.
대수 기하학(Algebraic geometry)은 다항식과 환(rings)을 사용하여 이러한 아이디어를 발전시킵니다. 값, 연산, 관계를 명시함으로써 우리는 해 공간(spaces of solutions)과 그 사이의 사상(maps between them)들을 연구할 수 있습니다.
**대수적 아키텍처 이론(Algebraic Architecture Theory, AAT)**은 이러한 접근 방식을 소프트웨어 구조에 가져옵니다. 이는 상태와 연산에 대한 데이터를 기반으로 법칙 방정식(Law equations)과 해 공간(solution spaces)을 구성합니다. 이 공간들을 연구함으로써 우리는 진단, 변경, 그리고 복구에 대해 추론할 수 있는 방법을 얻게 됩니다. ArchSig는 이 이론을 소프트웨어 분석에 적용합니다.
허용된 변경 사항이 검색 범위를 결정한다
복구(repair)가 반드시 모든 변수의 변경을 허용하는 것은 아닙니다. 현재 상태를 $\mathbf{x}_0$이라 하고, 허용되는 변경을 통해 도달 가능한 상태들을 $\mathcal A(\mathbf{x}_0)$라고 합시다. 복구 후보는 또한 법칙(Laws)을 만족해야 하므로, 다음 교집합에 속합니다:
$$
\mathcal A(\mathbf{x}_0)\cap\mathcal S. $$
만약 이 교집합이 비어 있다면, 허용된 변경 사항으로는 복구를 생성할 수 없습니다. 그렇지 않다면, 그 원소들은 후보 복구 상태들입니다.
위 시스템은 해 $\mathbf{(1,1)}$을 가졌습니다. $\mathbf{(0,2)}$에서 시작하여 $u$가 0으로 유지되어야 하고 오직 $v$만 변경될 수 있다면, 우리는 그 상태에 도달할 수 없습니다. 만약 둘 다 변경 가능하다면, 우리는 $\mathbf{(1,1)}$을 선택할 수 있습니다. 복구가 존재하는지 여부는 법칙이 동일하게 유지되더라도 무엇을 변경하도록 허용하는지에 달려있습니다.
원자(Atoms)와 법칙(Laws)으로부터 하나의 구조를 구축하다
AAT는 사용 가능한 연산과 부분들 간의 관계와 함께 원자(Atoms)와 법칙(Laws)을 결합합니다. 이 이론의 독특한 아이디어는 진단, 복구, 그리고 변경 사항 비교에 이 공통된 구조를 사용하는 것입니다. 그 기초는 Foundations of Algebraic Architecture Theory: A Rising Sea of Geometry, Transport, Comparison, and Reconstruction에서 개발되었습니다.
사실(facts), 연산(operations), 그리고 조건(conditions)을 하나로 모으다
AAT는 원자(Atoms)로부터 수학적으로 시작합니다. 구성 요소와 연산을 나타내는 관계를 원자에 부여함으로써, 우리는 소프트웨어 구조를 수학적 객체로 다룰 수 있게 됩니다. 법칙은 그 객체가 만족해야 하는 방정식을 명시합니다.
또한 허용되는 연산과 그것들이 생성할 수 있는 상태들을 포함합니다. 이를 통해 우리는 특정 연산 후에 여전히 법칙이 성립하는지, 그리고 변경 사항에 의해 무엇이 보존되는지를 질문할 수 있습니다.
큰 객체를 부분별로 연구하기 위해, 우리는 그 부분들과 그들이 공유하는 것을 명시합니다. 우리는 각 부분의 데이터를 포함하고, 부분들 간의 불일치(discrepancy)가 어떻게 측정되는지를 포함합니다. 논문에서는 결과적인 구조를 '전체 기하학 (full geometry)'이라고 부릅니다.
동일한 구조를 사용하여 다른 질문을 던지기
이러한 구조 내에서 여러 친숙한 소프트웨어 질문들을 연구할 수 있습니다:
| 소프트웨어 질문 | 수학적 질문 |
|---|
| 개별적으로 수정된 부분들이 결합될 때 충돌할 수 있는가? | 국소 상태(local states)를 전역 상태(global state)로 붙일 수 있는가? |
| ... | |
예를 들어, 동일한 연산(operations)을 유지하면서 데이터 형식을 변경하고 싶다고 가정해 봅시다. $\text{X}$와 $\text{Y}$를 변환 전후의 객체로, $h$를 변환 맵(conversion map)으로, 그리고 $T_X, T_Y$를 각각에 해당하는 연산으로 놓습니다. 우리는 필요한 관계를 다음과 같이 표현할 수 있습니다:
$$
h(T_X(x))=T_Y(h(x)). $$
연산한 다음 변환하는 것이 변환한 다음 연산하는 것과 동일한 결과를 제공해야 합니다. 우리는 단지 값들이 대응하는지 여부뿐만 아니라, 그 변환이 해당 값들 위의 연산을 존중하는지 확인할 수 있습니다.
진단(diagnosis)을 변경하지 않으면서 세부 사항을 생략할 수 있는 경우는 언제인가?