AI 코딩 시대에, 그대들은 어떻게 코딩할 것인가?
AI 시대에 좋은 설계란 코드를 빨리 생성하는 구조가 아니라, 생성된 변경을 작고 확실하게 검증할 수 있는 구조이다.
이 글은 Unite Seoul 2026 강연을 바탕으로 AI 시대에 Clean Architecture가 더 중요해진 이유를 설명한다. 또한 순수 함수와 데이터 중심 설계로 로직을 분리해 AI가 이해하고 사람이 검증하기 쉬운 코드를 만드는 방법을 이야기한다.
코드 작성 비용보다 검증 비용이 중요해졌다
과거에는 개발자가 코드를 직접 작성하고 실행한 뒤 결과를 확인했다. 현재는 AI가 초안을 작성하고 개발자가 검토·수정·승인하는 비중이 커졌다. 작성 주체는 달라졌지만 최종 책임은 여전히 사람에게 남아 있다.
AI는 보일러플레이트, DTO(Data Transfer Object, 계층 사이에서 데이터를 전달하는 객체), 인터페이스 연결과 기계적인 변환을 빠르게 만든다. 따라서 코드 한 줄을 생산하는 비용은 계속 낮아진다. 역설적이게도 AI가 더 많은 코드를 작성할수록 사람이 검토해야 할 코드와 변경 범위도 커진다. 코드 생성 비용은 줄지만 사양 준수와 회귀 여부를 확인하는 비용은 그대로 남는다.
AI 이전 Unity 설계의 두 선택지
LLM 이전의 Unity 개발에서 개발자들이 선택했던 대표적인 설계 구조는 MVP와 Clean Architecture였다. 두 방식을 개념부터 짧게 정리한다.
MVP란 무엇인가
MVP(Model-View-Presenter)는 화면 코드를 세 역할로 나누는 패턴이다.
- Model은 프로젝트의 데이터와 상태를 담당한다.
- View는 버튼, 텍스트와 애니메이션처럼 화면에 보이는 요소를 담당한다.
- Presenter는 Model과 View 사이에서 입력을 처리하고 화면에 표시할 값을 전달한다.
MVP의 장점은 단순함이다. 역할이 세 개뿐이므로 별도의 긴 설명 없이도 구조를 이해하기 쉽다. 작은 기능을 빠르게 만들 때는 이 단순함이 큰 이점이다.
문제는 프로젝트의 기능과 상태가 복잡해질 때 발생한다. 처음에는 중개자였던 Presenter가 화면 전환, 데이터 가공, 상태 보관과 도메인 판단까지 맡기 쉽다. 결국 하나의 Presenter를 이해하려면 여러 Manager, Repository와 Unity 객체를 함께 읽어야 한다. 테스트에서도 주변 객체를 모두 준비해야 하므로 작은 규칙 하나를 검증하기 어려워진다.
Clean Architecture란 무엇인가
경계란 핵심 로직과 UI, 네트워크, 저장소 같은 외부 요소를 나누는 기준이다. Clean Architecture는 핵심 비즈니스 규칙을 UI, 네트워크와 저장소 같은 외부 기술에서 분리한다. 안쪽의 정책은 바깥쪽 구현을 직접 알지 않으며, 의존성은 핵심 규칙을 향하도록 제한한다.

Unity에 적용하면 강화 가능 여부나 보상 계산 같은 도메인 규칙이 안쪽에 놓인다.
MonoBehaviour, API 통신, 로컬 저장과 UI 갱신은 바깥 경계에 놓인다.
핵심 로직은 Unity Editor를 실행하지 않고도 일반 C# 테스트로 검증할 수 있다.
장점은 변경 영향과 테스트 범위를 제한할 수 있다는 점이다. 네트워크 구현이나 화면 Prefab이 바뀌어도 핵심 규칙은 그대로 유지할 수 있다. 규모가 커지고 인력이 교체되어도 각 코드가 속할 위치를 비교적 명확하게 정할 수 있다.
단점은 구조를 정의하고 유지하는 비용이다. 계층, DTO와 인터페이스가 늘면 단순한 기능도 여러 파일을 오가며 추적해야 한다. 팀마다 경계를 다르게 해석하면 같은 책임이 여러 계층에 중복되고 코드 위치를 결정하는 시간도 길어진다. 불필요한 인터페이스와 매핑 코드가 쌓이면 분리의 이점보다 탐색과 변경 비용이 더 커질 수 있다.
AI 시대에는 두 설계의 손익이 달라진다
MVP의 약점은 더 커진다
AI는 기존 코드의 구조를 따라 새 코드를 추가한다. Presenter에 화면 제어, 상태 관리와 도메인 로직이 섞여 있으면 AI도 같은 위치에 기능을 추가한다. 그 결과 Presenter의 의존성이 빠르게 늘고 수정에 필요한 컨텍스트도 커진다.
사람은 변경된 Presenter뿐 아니라 연결된 Manager와 View까지 검토해야 한다. 독립적인 테스트가 없다면 Play Mode에서 관련 화면을 직접 실행해 회귀 여부를 확인해야 한다. 따라서 MVP는 작은 UI에는 적합하지만 기능이 계속 추가되는 대규모 프로젝트의 기본 구조로는 불리하다.
Clean Architecture의 장점은 커지고 비용은 줄어든다
Clean Architecture의 명시적인 경계는 AI가 읽어야 할 문맥을 줄인다. 입력과 출력이 고정된 UseCase 하나를 수정한다면 AI는 프로젝트 전체보다 그 경계 안에 집중할 수 있다. 사람도 같은 범위의 로직과 테스트를 검토하면 된다.
반면 과거의 주요 단점이었던 작성 비용은 AI가 상당 부분 흡수한다. DTO 정의, 인터페이스 연결, 계층 사이의 변환과 테스트 뼈대는 규칙만 분명하면 AI가 잘 작성한다. 반복성이 높은 코드는 Source Generator나 T4(C# 템플릿을 이용해 반복 코드를 생성하는 도구)로 더 강하게 고정할 수도 있다.
손익 변화를 정리하면 다음과 같다.
AI가 반복 코드의 작성 비용을 낮춘다.
→ 계층, DTO와 접착 코드의 상대적 비용이 감소한다.
→ Clean Architecture의 단점이 줄어든다.
AI가 변경량과 생성 속도를 높인다.
→ 숨은 의존성과 비대한 책임의 검증 비용이 증가한다.
→ MVP의 약점이 커진다.
최종 책임은 사람에게 남는다.
→ 경계를 나누면 사람이 읽어야 할 코드와 테스트 범위가 줄어든다.
→ 따라서 명시적인 경계, 독립 테스트와 제한된 영향 범위가 더 중요해진다.
따라서 AI 시대에는 Clean Architecture 쪽으로 선택의 균형이 이동된다. 과거에는 “이 정도 기능에 계층을 나누는 비용을 감당할 수 있는가”를 먼저 물었다. 이제는 “AI가 만든 변경을 어디까지 읽고 어떻게 검증할 것인가”를 먼저 물어야 한다.
계층이 많다고 좋은 설계는 아니다
이 결론을 계층과 인터페이스를 많이 만들라는 주장으로 받아들이면 안 된다. Clean Architecture의 목적은 폴더 수가 아니라 변경과 검증의 경계를 만드는 것이다.
구현체가 하나이고 대체 가능성이 없는 단순 계산에 인터페이스를 추가할 이유는 없다. 데이터를 전달하기 위해 이름만 다른 DTO를 계층마다 만들 필요도 없다. 반대로 네트워크, 시간, 난수, 저장소처럼 테스트에서 교체해야 하는 경계는 명시하는 편이 좋다.
AI는 보일러플레이트를 싸게 만들지만, 불필요한 추상화를 이해하는 비용까지 없애 주지는 않는다. 좋은 구조는 핵심 규칙의 위치와 데이터 흐름이 명확한 구조이다.
AI와 인간이 함께 다루기 쉬운 코드의 세 조건
순수 함수, 데이터 중심 설계, 구조적 안전망이 핵심 조건이다.

함수형 패러다임: 판단을 순수 함수로 만든다
순수 함수는 명시적인 입력만 사용하고 같은 입력에 같은 결과를 반환한다. 전역 상태와 Unity 객체를 참조하지 않으므로 AI가 읽을 범위가 줄고 단위 테스트도 단순해진다. FP의 목적은 클래스를 없애는 것이 아니라 도메인 판단을 부수효과에서 분리하는 것이다.
데이터 중심 설계: 사양을 값으로 드러낸다
데이터 중심 설계는 상태와 사양을 명시적인 값으로 표현하고 변환 과정을 분리한다.
계층 사이에는 변경 가능한 객체 대신 UserData, DataTable, API Schema와 ViewEntity를 전달한다.
이 글에서는 DOD의 성능 최적화보다 데이터 계약과 변환 흐름을 명확히 만드는 데 집중한다.
구조적 안전망: 규칙을 기계가 검사하게 한다
구조적 안전망은 설계 규칙을 사람의 기억이 아니라 도구로 검증하는 방식이다. Assembly Definition, Analyzer, 테스트와 CI로 잘못된 의존과 회귀를 자동으로 차단한다. 반복 코드는 Source Generator로 통일해 사람과 AI가 같은 규칙을 따르게 한다.
장비 강화 기능(예시)로 보는 데이터 흐름
장비 강화는 사용자마다 달라지는 장비와 골드를 읽고, 기획이 정한 비용과 성공 확률에 따라 상태를 변경한 뒤 결과 화면을 갱신한다.
먼저 알아둘 용어
데이터 흐름에 등장하는 이름부터 한 번에 정리한다.
- View: 버튼, 텍스트와 애니메이션을 가진
MonoBehaviour이다. 계산이 끝난 값을 화면에 반영한다. - Controller: 버튼 클릭 같은 사용자 입력을 받아 CommandUseCase로 전달하는 진입점이다.
- CommandUseCase: 강화 실행처럼 상태를 바꾸는 유스케이스이다. 외부 요청을 조정하지만 화면 데이터는 만들지 않는다.
- API/Gateway: 서버 요청처럼 애플리케이션 바깥의 작업을 감싼 경계이다.
- Repository: 서버에서 받은 최신 UserData를 저장하고 조회하는 인터페이스이다.
- UserData: 장비 레벨과 보유 골드처럼 사용자마다 달라지고 지속적으로 바뀌는 상태이다.
- DataTable: 강화 비용, 성공 확률과 최대 레벨처럼 기획이 정의한 읽기 전용 사양이다.
- QueryUseCase: UserData와 DataTable을 읽어 화면에 필요한 값으로 변환하는 유스케이스이다.
- ViewEntity: 라벨, 활성화 여부처럼 View가 계산 없이 사용할 수 있도록 가공이 끝난 값이다.
- Presenter: QueryUseCase가 만든 ViewEntity를 구독해 View에 전달하는 얇은 연결 계층이다.
이 용어들은 구현을 복잡하게 보이게 만드는 이름이 아니다. 각 코드가 상태를 바꾸는지, 값을 읽는지, 화면에 반영하는지를 구분하기 위한 경계다.
쓰기 흐름과 읽기 흐름을 분리한다
전체 흐름은 쓰기와 읽기의 한 바퀴로 구성된다.

쓰기: View → Controller → CommandUseCase → API/Gateway → Repository
읽기: Repository → QueryUseCase → ViewEntity → Presenter → View
Command는 상태를 바꾼다. 강화 버튼을 누르면 Controller가 CommandUseCase를 호출하고, CommandUseCase는 Gateway를 통해 서버에 강화를 요청한다. 서버 응답은 Repository의 UserData를 갱신한다.
Query는 상태를 바꾸지 않는다. 갱신된 UserData와 DataTable을 읽어 다음 강화 레벨, 비용 라벨, 성공 확률과 버튼 활성화 여부가 담긴 ViewEntity를 만든다. Presenter는 그 값을 View에 전달할 뿐 별도의 판단을 하지 않는다.
Command와 Query를 나누면 질문도 단순해진다. 쓰기 흐름에서는 “이 입력이 어떤 상태 변경을 만드는가”를 검증한다. 읽기 흐름에서는 “이 상태와 사양이 어떤 화면 값으로 변환되는가”를 검증한다.
Unity 객체는 경계에 남긴다
MonoBehaviour, UI 컴포넌트, 애니메이션과 네트워크 호출은 부수효과를 가진다.
이를 억지로 순수 함수로 만들 필요는 없다.
대신 이 객체가 도메인 규칙을 직접 판단하지 않도록 한다.
View는 CanEnhance를 계산하지 않는다.
Gateway는 화면 문자열을 만들지 않는다.
Repository는 강화 성공 확률을 판정하지 않는다.
각 경계가 받은 값에 따라 한 가지 역할만 수행하면 핵심 로직을 별도로 검증할 수 있다.
C#으로 구현하는 장비 강화 예제
핵심인 불변 데이터, 순수 Query, 부수효과 경계만 코드로 확인한다.
using System;
public sealed record Equipment(int Id, int Level);
public sealed record UserData(Equipment Equipment, int Gold);
public sealed record EnhanceRule(int CostGold, float SuccessRate);
public sealed record EquipmentViewEntity(
string LevelLabel,
string CostLabel,
string SuccessRateLabel,
bool CanEnhance);
public static class EquipmentEnhanceQuery
{
public static EquipmentViewEntity CreateViewEntity(
UserData userData,
EnhanceRule rule)
{
var nextLevel = userData.Equipment.Level + 1;
var successRatePercent = (int)Math.Round(
rule.SuccessRate * 100f,
MidpointRounding.AwayFromZero);
return new EquipmentViewEntity(
LevelLabel: $"+{userData.Equipment.Level} → +{nextLevel}",
CostLabel: $"{rule.CostGold} 골드",
SuccessRateLabel: $"{successRatePercent}%",
CanEnhance: userData.Gold >= rule.CostGold);
}
}
EnhanceRule은 DataTable에서 읽은 한 행이다.
EquipmentEnhanceQuery는 UserData + DataTable → ViewEntity 변환만 담당한다.
Unity API와 Repository를 참조하지 않으므로 입력과 출력만으로 테스트할 수 있다.
상태 변경과 화면 반영은 바깥 경계에 둔다.
using System.Threading;
using System.Threading.Tasks;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public interface IEnhanceGateway
{
Task<UserData> EnhanceAsync(
int equipmentId,
CancellationToken cancellationToken);
}
public sealed class EnhanceCommandUseCase
{
private readonly IEnhanceGateway _gateway;
public EnhanceCommandUseCase(IEnhanceGateway gateway) =>
_gateway = gateway;
public Task<UserData> ExecuteAsync(
int equipmentId,
CancellationToken cancellationToken)
=> _gateway.EnhanceAsync(equipmentId, cancellationToken);
}
public sealed class EquipmentEnhanceView : MonoBehaviour
{
[SerializeField] private TMP_Text levelLabel = null!;
[SerializeField] private TMP_Text costLabel = null!;
[SerializeField] private TMP_Text successRateLabel = null!;
[SerializeField] private Button enhanceButton = null!;
public void Render(EquipmentViewEntity entity)
{
levelLabel.text = entity.LevelLabel;
costLabel.text = entity.CostLabel;
successRateLabel.text = entity.SuccessRateLabel;
enhanceButton.interactable = entity.CanEnhance;
}
}
CommandUseCase는 서버 요청만 조정하고, View는 완성된 ViewEntity를 UI에 반영한다.
강화 판정은 서버에 두고 화면 계산은 Query에 모아 같은 규칙을 여러 계층에 중복하지 않는다.
무엇을 어떻게 테스트할 것인가
전체 구조에서 확인할 대상은 Command, Query, View의 세 부분이다.

QueryUseCase: 값의 변환을 테스트한다
순수 Query는 입력과 기대 출력만 준비하면 된다.
using NUnit.Framework;
public sealed class EquipmentEnhanceQueryTests
{
[Test]
public void CreateViewEntity_EnoughGold_FormatsValuesAndEnablesButton()
{
var user = new UserData(
Equipment: new Equipment(Id: 10, Level: 5),
Gold: 500);
var rule = new EnhanceRule(
CostGold: 300,
SuccessRate: 0.7f);
var actual = EquipmentEnhanceQuery.CreateViewEntity(user, rule);
Assert.That(actual.LevelLabel, Is.EqualTo("+5 → +6"));
Assert.That(actual.CostLabel, Is.EqualTo("300 골드"));
Assert.That(actual.SuccessRateLabel, Is.EqualTo("70%"));
Assert.That(actual.CanEnhance, Is.True);
}
}
이 테스트는 사양을 문장보다 구체적으로 보여 준다. 레벨 5, 골드 500, 비용 300과 성공 확률 70%가 어떤 화면을 만드는지 한눈에 확인할 수 있다.
CommandUseCase: 상태 전이를 테스트한다
다음 상태 전이를 E2E(End-to-End, 서버와 클라이언트를 연결한 전체 흐름 테스트)로 검증한다.
- 성공하면 장비 레벨이
+5에서+6으로 증가하고 골드가 300 감소한다. - 실패하면 장비 레벨은
+5로 유지되지만 골드는 300 감소한다. - 응답으로 받은 UserData가 Repository에 반영된다.
- 강화와 무관한 UserData는 변경되지 않는다.
클라이언트 단위 테스트에서는 Gateway를 대체해 요청 인자와 취소 토큰 전달을 확인할 수 있다. 다만 서버의 확률 판정 로직을 클라이언트에 복제해서 테스트하지 않는다.
View: 렌더링과 연출을 테스트한다
View 테스트는 EquipmentViewEntity를 주입했을 때 텍스트와 버튼 상태가 맞는지 확인한다.
애니메이션이 포함된다면 Play 호출 과정에서 오류가 없는지도 검증한다.
중요 화면에는 Screenshot 기반 Visual Regression 테스트를 추가할 수 있다.
이렇게 나누면 테스트 실패의 의미도 분명해진다. Query 실패는 화면 데이터 변환 사양의 문제이고, Command 실패는 상태 변경이나 외부 연동의 문제이며, View 실패는 Unity 객체 연결이나 렌더링의 문제다.
마치며
AI가 작성 비용을 낮출수록 설계는 생성 속도보다 검증 범위를 줄이는 데 초점을 맞춰야 한다. MVP는 작은 기능에 적합하지만, 장기 운영 프로젝트에서는 Clean Architecture의 명시적인 경계와 독립 테스트가 더 중요하다. 핵심 로직을 순수 함수에 모으고 데이터 흐름을 제한하면 AI가 수정하고 사람이 검증하기 쉬운 코드를 만들 수 있다.