TL;DR
- Lean에서 도메인의 언어로 프로그램을 정의하면 컴파일러가 실행 세부 사항을 도출하며, 도메인 경계 사이의 규칙 불일치로 생기는 오류를 줄이는 방식임.
- 틱택토의 플레이어, 9개 칸, 수순, 게임 상태를 명시적으로 모델링하고, 차례와 승자는 저장하지 않고 보드에서 계산함.
- 유효한 수순임을 증명하는 값이 있어야 보드를 갱신할 수 있으며, 잘못된 수순은 적절한 오류로 거부됨.
- LeanAPI와 LeanDB를 이용해 REST API와 데이터베이스를 연결하고, 도메인 타입에서 JSON과 테이블 구조를 도출함.
- 규칙 변경은 API에 자동 적용되지만 기존 데이터의 호환성은 별도의 제품 결정이며, 도메인 모델을 여러 시스템 경계에 공유하면 규칙 불일치 오류를 줄일 수 있음.
도메인은 어휘와 규칙임
- 도메인 주도 개발(DDD)은 프로그램을 도메인의 언어로만 정의하고, 컴파일러가 실행 세부 사항을 도출하게 하는 방식임.
- 틱택토는 작고 따라가기 쉬운 사례로 선택됐으며, 예제는 앞서 공개한
LeanAPI와LeanDB라이브러리를 기반으로 함. - Lean 같은 정리 증명 언어(theorem-proving language)는 도메인을 추상적이면서도 정확하게 정의할 수 있음. 대부분의 다른 언어에서는 무엇을 할지와 어떻게 실행할지를 섞게 됨.
- 도메인은 단어와 그 의미로 이루어진 어휘, 그리고 단어를 결합하는 규칙으로 정의됨.
- 틱택토 도메인의 구성 요소는 다음과 같음.
- 플레이어는 X와 O 두 종류임.
- 보드는 9개 칸으로 이루어진 격자이며, 각 칸은 비어 있거나 X 또는 O가 표시된 상태임.
- 수순은 플레이어와 칸의 조합임.
- 게임 상태는 진행 중이며 플레이어의 차례인 상태, 또는 승자나 무승부가 결정된 종료 상태임.
- 게임 상태는 X 차례, O 차례, X 승리, O 승리, 무승부의 다섯 가지임.
- 보드는 지금까지 진행된 유효한 수순의 순서 있는 목록으로 정의됨. 각 칸의 표시는 해당 칸에 표시한 플레이어가 있으면 그 플레이어, 없으면 빈 값으로 나타냄.
- 차례와 승자는 별도로 저장하지 않고 보드에서 계산함. 승리 조건은 세 행, 세 열, 두 대각선으로 이루어진 8개 줄이며, 9개 수순이 모두 진행됐고 승자가 없으면 무승부임. 그 외에는 수순 수의 홀짝으로 차례를 판별함.
규칙
- 수순은 게임이 진행 중이고, 선택한 칸이 비어 있으며, 해당 플레이어의 차례일 때 유효함.
Board.play는 유효한 수순이라는 증명을 인자로 요구함. 수순이 유효하지 않으면 컴파일러가 함수를 실행하지 않으며, 게임을 불법 상태로 만들 수 없음.
네트워크를 통한 게임 진행
- 게임 정의만으로는 실제 플레이가 불가능하므로, 다른 참여자가 접근할 수 있도록
LeanAPI로 REST API를 제공하고LeanDB로 게임을 데이터베이스에 저장함. - REST 엔드포인트는 다음과 같음.
POST /games: 게임을 시작하고 ID를 반환함.POST /games/:game/moves: 플레이어와 칸을 본문에 담아 수순을 제출함.GET /games/:game: 보드와 상태를 조회함.PUT과DELETE는 제공하지 않음. 도메인에는 게임을 수정하거나 수순을 되돌리는 개념이 없으므로 해당 메서드가 수행할 작업도 없음.
저장되는 데이터
- 게임은 보드이므로 데이터베이스에는 보드가 저장됨.
LeanDb.Model은 엔티티, 표현 방식, 생성된 저장 단계를 정의하고,LeanApi.Core는 연산과 엔드포인트를 정의함.- 규칙 파일은 JSON이나 데이터베이스 테이블을 알지 못함. 서버에서 기본 도메인 타입에
Domain인스턴스를 파생하고, 보드는 수순 목록으로 표현한 뒤 이를 재생해 검증하도록 선언함. - 테이블과 열, JSON 형식은 도메인 정의에서 도출됨. SQLite의
game테이블은 자동 증가 ID와 텍스트 형식의board열을 가짐. - 예시 데이터베이스의 보드 값은 플레이어와 칸을 짝지은 수순 목록으로 저장됨. 차례와 승자는 저장하지 않고 계속 보드에서 계산함.
수순 실행
- 새 게임은 빈 보드로 데이터베이스에 삽입됨.
- 클라이언트는 불법 수순을 요청할 수 있으므로, 서버는 다음 오류를 반환할 수 있음: 게임 없음, 게임 종료, 차례가 아님, 칸이 이미 차지됨.
- 수순 처리 과정은 게임을 조회하고, 존재하지 않으면
noSuchGame을 반환한 뒤 플레이어와 칸으로 수순을 구성하는 순서임. require는 실행 중에 수순이 유효하다는 증명을 만들며, 유효하지 않으면 해당MoveError를 반환하고 HTTP 422 상태 코드를 사용함.- 증명을 받은 뒤 새 보드를 만들고 데이터베이스의 게임을 갱신하며, 갱신된 보드의 상태를 반환함.
API
- 게임 조회 API는 보드와 상태를 함께 반환함. 게임이 없으면
noSuchGame오류를 반환함. - API 엔드포인트는
POST /games,POST /games/:game/moves,GET /games/:game임.
서버 실행
- 서버 진입점은 API를 지정하고 데이터베이스 파일을
tictactoe.sqlite로 설정함. lake exe tictactoe를 실행하면 포트 8080에서 API가 제공됨.- 새 데이터베이스에서 게임을 만들고 X가 중앙에 둔 뒤, O가 같은 칸에 두면
squareTaken오류가 반환됨. X가 차례가 아닌데 수순을 두면notYourTurn오류가 반환되고, 올바른 수순은 다음 차례 상태를 반환함. - 예시 게임에서는 X가 중앙, 위쪽 왼쪽, 위쪽, 아래쪽 칸에 두고 O가 위쪽 왼쪽과 왼쪽에 둔 결과 X가 승리함. 게임 종료 후 O가 오른쪽에 두려는 요청은
gameOver오류로 거부됨. - 거부된 수순은 HTTP 422 상태 코드를 반환하며 데이터는 변경하지 않음. 도메인에 없는 칸 이름이나 플레이어 값을 보내면
badRequest가 반환되고, 지원하지 않는PUT요청에는 405 Method Not Allowed가 반환됨.
규칙 변경
- 하우스 룰로 X가 첫 수를 중앙에 둘 수 없게 하려면 유효 수순 규칙에 조건 하나를 추가하면 됨.
- 수순 처리 코드가
ValidMove전체를 검사하므로 서버 코드를 변경하지 않고도 새 규칙이 컴파일되며 즉시 적용됨. - 기존 데이터베이스의 게임은 이전 규칙에 따라 진행됐을 수 있음. 예를 들어 중앙에서 시작한 게임 1은 새 규칙으로 재생할 수 없어 서버가 유효한 게임으로 제공하지 못하고 내부 오류를 반환함.
- 이는 진행 중인 게임에 바람직하지 않을 수 있으며, 게임마다 규칙 버전을 저장할지, 기존 게임을 이전 규칙으로 마무리하게 할지, 종료할지는 제품 결정임. 재생 검증은 규칙상 존재할 수 없는 게임을 조용히 제공하지 않고 문제를 드러냄.
- 검증 규칙은 API에 자동 적용됨. API가 규칙을 별도로 복제하지 않고 규칙의 정의 자체를 검사하며, 서버가 추가하는 것은 각 수순 실패 방식의 오류 이름뿐임.
결론
- 도메인 모델을 애플리케이션의 중심에 두면 도메인에 규칙이나 요소를 추가할 때 데이터베이스와 API 같은 경계 전반에 로직이 자동으로 이어지는 방식임.
- 예제는 API 서버와 데이터베이스라는 두 경계만 사용해 흐름을 따라가기 쉽게 구성됨.
- 실제 애플리케이션에는 더 많은 경계가 있으며 데이터 모델은 이를 넘나들어야 함. 도메인 주도 개발은 React 컴포넌트, 프런트엔드
localStorage, 백엔드 데이터베이스, Kafka, 같은 데이터 모델을 공유하는 마이크로서비스 등으로 확장할 수 있음. - 애플리케이션과 서비스의 각 부분이 도메인을 서로 다르게 이해하면서 생기는 규칙 불일치 오류를 줄이고 코드베이스를 크게 단순화할 수 있다는 관점임.
- GitHub 저장소
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요