데이터 지향 프로그래밍 1주차 후기 - 상태와 동작을 분리해 바라보기

데이터 지향 프로그래밍에서 마감에 쫓긴 프로그래머 테오가 도서관 시스템에 VIPMember 역할 하나를 추가했을 뿐인데, 화면의 클래스 관계선이 빠르게 늘어났습니다. 발표 자료에는 Member와 책을 추가할 권한을 표현한 UserWithBookItemRight를 함께 상속받는 구조가 나타났고, 스터디원들 사이에서는 상속 대신 컴포지션이나 별도의 역할 객체로 풀 수 있지 않겠느냐는 의견이 곧바로 이어졌습니다.

이날 대화는 어느 한 패러다임의 승리를 선언하는 방향으로 흐르지 않았습니다. 오히려 우리가 익숙하게 사용해 온 설계가 어떤 조건에서 복잡해지는지, 데이터와 동작을 분리하면 무엇이 단순해지고 또 어떤 문제가 남는지를 하나씩 확인하는 시간이었습니다.

데이터 지향 프로그래밍 첫 모임은 두 차례 발표와 자유 토론으로 진행됐습니다. 발표자는 책의 앞부분을 도서관 관리 시스템 예제로 풀어냈고, 발표 중에도 질문과 실무 경험담이 자연스럽게 오갔습니다.

데이터 지향 프로그래밍 1주차 후기 - 상태와 동작을 분리해 바라보기
첫 발표를 시작하며 공유한 ‘데이터 지향 프로그래밍’ 표지 화면. 발표자는 책의 등장인물과 도서관 예제를 별도의 자료로 정리해 왔습니다.

이번 스터디에서 다룬 내용


익숙한 도서관 시스템에서 시작했습니다

책의 예제는 도서관 관리 시스템입니다. 사서와 일반 회원이 이메일과 비밀번호로 로그인하고, 도서를 검색하거나 빌리고 반납합니다. 사서는 대출 금지를 해제하고 대출 현황을 조회할 수 있습니다. 처음에는 익숙하고 단순해 보이는 요구사항입니다.

발표에서는 먼저 UML(Unified Modeling Language)을 이용해 라이브러리, 회원, 사서, 도서, 카탈로그 사이의 관계를 살펴봤습니다. UML은 시스템을 구성하는 요소와 관계를 공통된 기호로 표현하는 도식입니다. 검은 마름모로 표현한 포함 관계, 일반적인 연관 관계, 점선으로 그린 의존 관계, 빈 삼각형으로 표시한 상속 관계를 하나씩 확인했습니다.

데이터 지향 프로그래밍 1주차 후기 - 상태와 동작을 분리해 바라보기 - 이미지 2
라이브러리와 카탈로그, 회원 사이의 포함 관계를 나누어 살펴본 장면. 복잡한 전체 그림을 작은 관계부터 읽어 갔습니다.

여기서 실무 경험도 자연스럽게 나왔습니다. 모든 클래스와 메서드를 빠짐없이 UML로 그리면 그림 자체가 또 하나의 복잡한 산출물이 될 수 있습니다. 그래서 실제 업무에서는 핵심 구조를 공유하거나 논쟁이 필요한 부분만 간략히 그리는 경우가 많다는 의견이 나왔습니다. 플로 차트나 손으로 그린 구조도처럼 더 가벼운 표현이 소통에 충분할 때도 있습니다.

UML을 얼마나 자세히 그릴지가 중요한 것이 아니라, 설계에 참여한 사람들이 같은 구조를 떠올릴 수 있느냐가 더 중요하다는 이야기였습니다.

또한, 스터디원들은 저자가 데이터 지향 프로그래밍의 유용성을 강조하기 위해 의도적으로 복잡한 객체 지향 설계를 제시했다는 점에도 공감했습니다.

상태와 메서드를 한곳에 모으면 언제 복잡해질까

책은 상태와 그 상태를 다루는 메서드를 한 단위로 캡슐화하는 전형적인 객체 지향 설계를 출발점으로 삼습니다. 이 결합은 관련된 데이터와 행동을 한곳에서 찾게 해 준다는 분명한 장점이 있습니다.

문제는 여러 객체가 공유된 가변 상태를 참조하거나, 하나의 상태를 바꾸는 경로가 많아질 때 나타납니다. 이때 특정 시점의 값을 추론하려면 객체 하나만이 아니라 그 객체에 접근할 수 있는 여러 실행 흐름과 호출 순서를 함께 이해해야 합니다. 이는 객체 지향 자체의 필연적인 문제라기보다, 가변 객체와 복잡한 관계를 많이 둔 설계에서 커지는 비용에 가깝습니다.

발표 자료에는 같은 객체의 isBlocked 값을 연속해서 두 번 출력하는 예제가 등장했습니다. 두 번 읽는 사이 다른 실행 흐름이 상태를 바꾸면, 같은 메서드 안에서도 서로 다른 값이 출력될 수 있습니다. 코드 몇 줄만 보면 단순하지만 실행 시점과 상태 변경의 주체까지 함께 생각해야 결과를 예상할 수 있습니다.

데이터 지향 프로그래밍 1주차 후기 - 상태와 동작을 분리해 바라보기 - 이미지 3
다른 실행 흐름이 두 번의 읽기 사이에 상태를 변경할 수 있어 결과가 달라지는 예제. 발표에서는 이를 ‘예상치 못한 코드 동작’으로 설명했습니다.

이런 경쟁 상태를 막기 위해 뮤텍스나 락을 사용할 수 있습니다. 락은 한 번에 하나의 작업만 공유 상태를 변경하도록 보호하는 유효한 동기화 수단입니다. 다만 여러 락의 획득 순서와 범위를 개발자가 직접 관리하면 서로가 가진 락을 기다리며 멈추는 교착 상태나, 대기 시간이 길어지는 성능 문제까지 함께 다뤄야 합니다.

불변 데이터, 즉 생성한 뒤 내부 값을 직접 바꾸지 않는 데이터는 공유 가변 상태로 인한 동기화 필요를 줄여 줍니다. 그렇다고 데이터베이스 트랜잭션이나 여러 작업 사이의 조정까지 사라지는 것은 아닙니다. 어떤 종류의 상태를 어디에서 변경할지 경계를 명확히 하는 일이 여전히 필요합니다.

직렬화와 DTO는 비용인 동시에 계약입니다

다음으로는 API가 데이터를 주고받을 때 생기는 문제를 살펴봤습니다.

직렬화는 메모리 속의 객체나 데이터를 JSON처럼 저장하거나 전송할 수 있는 형태로 바꾸는 과정입니다. 반대로 JSON을 애플리케이션에서 사용할 수 있는 값으로 되돌리는 과정은 역직렬화라고 합니다.

복잡한 객체 관계를 외부 API 형식에 그대로 노출하기는 어렵습니다. 그래서 요청과 응답을 위한 DTO(Data Transfer Object), 즉 계층 사이에서 데이터를 전달하기 위한 별도의 객체와 매핑 코드가 필요해질 수 있습니다.

데이터 지향 프로그래밍 1주차 후기 - 상태와 동작을 분리해 바라보기 - 이미지 4
객체 그래프를 외부 전송 형식으로 바꾸는 과정에서 클래스와 변환 코드가 늘어날 수 있다는 발표 내용.

DTO는 외부 계약과 내부 도메인 모델을 분리해 줍니다. 내부 구현을 바꾸더라도 API 계약을 안정적으로 유지할 수 있다는 장점이 있습니다. 반면 엔드포인트마다 비슷한 DTO와 변환 계층이 과도하게 늘어나거나, 쿼리와 응답 구조가 강하게 결합되면 작은 요구사항도 여러 파일의 변경으로 번질 수 있습니다.

따라서 DTO 자체가 문제라기보다, 계약을 분리해서 얻는 안정성과 반복되는 매핑 비용 사이에서 적절한 경계를 선택하는 문제에 가깝습니다.

VIP 회원 하나가 보여 준 확장 비용

도서관 시스템에 새로운 권한을 추가하는 장면에서는 클래스 관계가 더 눈에 띄게 복잡해졌습니다. 요구사항에는 다른 회원의 대출 목록을 볼 수 있는 슈퍼 회원과, 일반 회원의 대출·반납 기능에 더해 책을 추가할 수 있는 VIP 회원이 구분되어 있었습니다. 발표 자료의 VIP 예제에서는 책 추가 권한을 기존 상속 계층에 끼워 넣으면서 여러 부모 역할을 동시에 참조하는 구조가 만들어졌습니다.

데이터 지향 프로그래밍 1주차 후기 - 상태와 동작을 분리해 바라보기 - 이미지 5
VIP 역할을 기존 클래스 계층에 추가하며 상속 관계선이 늘어난 장면. 이 화면을 계기로 컴포지션과 역할 분리에 관한 대화가 이어졌습니다.

스터디원들은 상속을 그대로 늘리기보다 권한이나 기능을 별도의 객체로 분리하고 컴포지션으로 조합할 수 있다는 의견을 나눴습니다. 객체 지향 안에서도 더 나은 해법을 만들 수 있다는 뜻입니다.

책의 예제가 던진 질문은 객체 지향으로 이 요구사항을 구현할 수 있느냐가 아니었습니다. 기존 구조가 새로운 역할을 받아들일 때 몇 개의 클래스를 이해하고 수정해야 하는지, 하나의 작은 기능이 전체 계층에 어느 정도 영향을 주는지를 살펴보자는 쪽에 가까웠습니다.

화면의 관계선이 늘어날수록 논의도 자연스럽게 확장됐습니다. 책임을 어디에 둘지, 상속과 컴포지션을 언제 선택할지, 기능이 더 추가된 뒤에도 지금의 구조를 쉽게 이해할 수 있을지가 중요한 판단 기준으로 남았습니다.

데이터와 동작을 분리하면 무엇이 달라질까

2장에서는 같은 도서관 시스템을 DOP 방식으로 다시 구성했습니다.

데이터는 회원, 카탈로그, 라이브러리처럼 값의 구조로 두고, 동작은 필요한 데이터를 매개변수로 받는 코드 모듈로 옮겼습니다. 발표의 자바식 예제에서는 이 동작들을 정적 메서드로 표현했습니다. 핵심은 static이라는 문법 자체보다 함수가 숨은 인스턴스 상태에 의존하지 않고, 어떤 데이터를 사용하는지 매개변수로 드러내는 데 있습니다.

데이터 지향 프로그래밍 1주차 후기 - 상태와 동작을 분리해 바라보기 - 이미지 6
라이브러리와 카탈로그 데이터를 함수 밖에 두고, 코드 모듈은 필요한 데이터를 매개변수로 받도록 구성한 예제.

여기서 무상태 함수와 순수 함수를 구분할 필요가 있습니다. 무상태 함수는 호출 사이에 자체적인 상태를 보존하지 않는 함수입니다. 순수 함수는 같은 입력에 항상 같은 결과를 내고, 외부 상태를 읽거나 변경하는 관찰 가능한 사이드 이펙트가 없어야 합니다. 정적 메서드라고 해서 자동으로 순수해지는 것은 아니며, 전역 값이나 데이터베이스에 의존하거나 전달받은 값을 직접 변경한다면 여전히 사이드 이펙트를 가질 수 있습니다.

발표에서 다룬 도서관 예제에서는 모듈 사이의 포함과 상속 관계가 줄고, 필요한 다른 코드를 사용하는 관계가 중심이 됐습니다. 슈퍼 회원과 VIP 회원의 권한도 기존 클래스 계층을 다시 구성하지 않고 권한 확인 함수와 조건을 몇 줄 추가하는 방식으로 비교적 작게 반영할 수 있었습니다.

다만 참가자들은 이 예제가 DOP의 장점을 설명하기 위해 단순화됐을 가능성도 짚었습니다. 잘 설계한 객체 지향 구조라면 같은 요구사항을 충분히 유연하게 수용할 수 있다는 반론도 나왔습니다. 따라서 이 결과를 DOP의 보편적인 확장성에 대한 입증으로 보기는 어렵습니다. 데이터와 동작 사이의 결합은 줄어들 수 있지만, 공유하는 데이터 구조와 스키마가 새로운 계약이 됩니다. 결합이 완전히 사라진다기보다 결합의 모양이 바뀐다고 보는 편이 정확합니다.

작은 예제에서 줄어든 변경 범위가 더 큰 시스템에서도 유지되는지는 이후 기능을 계속 추가하며 검증하기로 했습니다.

그렇다면 실제 상태는 어디에서 바뀔까

DOP 다이어그램이 단순해지자 오히려 새로운 질문이 나왔습니다.

함수를 모두 무상태로 만들더라도 회원의 대출 상태는 결국 어딘가에 저장되어야 합니다. 데이터베이스를 조회하고 수정하거나, 파일을 기록하고, 네트워크를 통해 다른 서비스에 요청을 보내는 순간 외부 세계와의 상호작용이 생깁니다.

이처럼 함수가 전달받은 값만 계산하는 것을 넘어 DB, 파일, 네트워크, 로그 같은 외부 상태를 읽거나 바꾸는 일을 사이드 이펙트라고 합니다. 실제 애플리케이션에서 사이드 이펙트를 완전히 없앨 수는 없습니다. 중요한 것은 여러 곳에 흩어 놓지 않고 경계로 모아 예측하고 테스트할 수 있게 만드는 것입니다.

데이터 지향 프로그래밍 1주차 후기 - 상태와 동작을 분리해 바라보기 - 이미지 7
두 번째 발표에서 DOP에도 학습과 검증이 필요하다고 정리한 화면.

데이터의 표현을 더 직접적으로 다루는 것과 원본 값을 누구나 임의로 변경하도록 두는 것은 별개의 문제입니다. 책의 DOP는 데이터를 불변 값으로 취급하고 데이터의 스키마를 표현과 분리해 검증하는 원칙을 함께 제안합니다. 실제 시스템에서는 입력 검증, 권한 관리, 트랜잭션과 스키마 관리가 여전히 필요합니다.

또한 코드와 데이터를 분리하면 어떤 동작이 어떤 데이터와 관련되는지 한곳에서 발견하기 어려워질 수도 있습니다. 객체의 캡슐화가 주던 탐색성과 데이터 보호 장치를 무엇으로 대신할지도 고민해야 합니다.

첫 모임에서는 이 질문에 답을 내리기보다, 책의 후반부에서 사이드 이펙트와 데이터 무결성을 어떻게 다루는지 더 확인하기로 했습니다. DOP를 새로운 정답으로 받아들이기보다 실제 코드에서 검증하려는 태도가 이날 토론의 중요한 특징이었습니다.

AI가 코드를 작성해도 우리는 코드를 읽어야 할까

발표가 끝난 뒤 대화는 자연스럽게 AI가 작성한 코드를 어떻게 이해하고 검토할 것인가로 이어졌습니다.

한 발표자는 규칙과 완료 조건이 비교적 명확한 바둑 프로그램 구현 작업에서 AI가 누락된 규칙을 찾아내고 테스트와 구현을 보완하는 데 특히 유용했다고 경험을 공유했습니다. 요구사항의 경계가 분명한 문제에서는 AI가 빠르게 여러 가능성을 탐색할 수 있다는 이야기였습니다.

반대로 초기 전제나 설계 방향이 잘못됐는데도, 주어진 컨텍스트 안에서는 그 방향과 일관된 코드를 계속 생성할 수 있다는 경험도 나왔습니다. 사람이 전체 맥락을 놓치면 결과물이 동작한다는 이유로 잘못된 구조를 뒤늦게야 발견하게 될 수 있습니다.

이때 필요한 것이 휴먼 인 더 루프(Human-in-the-loop)입니다. AI의 모든 출력물을 사람이 한 줄씩 확인한다는 뜻이라기보다, 요구사항과 핵심 설계, 보안, 데이터 변경, 배포처럼 위험도가 높은 지점에 사람의 판단과 승인 절차를 두는 방식입니다.

AI가 만든 코드의 이해를 돕기 위해 퀴즈나 요약 보고서를 다시 생성하는 방법도 논의됐습니다. 하지만 보조 설명을 한 번 더 만드는 것만으로 시스템의 구조를 이해했다고 보기는 어렵습니다. 중요한 코드는 결국 직접 읽고, 실제 동작과 테스트를 통해 확인해야 한다는 의견이 모였습니다.

물론 생성되는 모든 코드를 같은 깊이로 읽는 것은 현실적으로 어렵습니다. 그래서 지금 더 깊이 들어가야 하는 문제인지, 현재 수준에서 멈춰도 되는지 판단하는 일종의 ‘토끼굴 센서’가 필요하다는 이야기도 나왔습니다. 개발자의 역할은 모든 것을 암기하는 데 있지 않고, 무엇을 반드시 이해해야 하는지 선택하는 데 있다는 뜻으로 들렸습니다.

대화에서는 자신이 무엇을 모르는지도 모르는 지식의 사각지대와 특정 AI 도구에 개발 생산성을 지나치게 의존할 때 생기는 외부 의존성에 대한 이야기도 오갔습니다. AI를 적극적으로 사용하되, 도구가 바뀌어도 문제를 정의하고 설계를 판단할 수 있는 역량은 유지해야 한다는 결론으로 이어졌습니다.

다음 스터디에서 직접 확인할 질문들

첫 모임에서는 하나의 답을 정하기보다, 다음 학습과 실험으로 이어질 질문을 남겼습니다. 스터디를 마치며 남은 챕터에서 다음과 같은 내용에 대해 고민해보면 좋겠다고 생각했습니다.

특히 작은 예제에서 단순해진 DOP 구조가 요구사항과 데이터가 늘어났을 때도 같은 장점을 유지하는지가 다음 학습의 중요한 기준이 될 것 같습니다.

마무리

이번 스터디를 통해 객체 지향과 데이터 지향 중 하나를 선택해야 한다는 결론을 내리지는 않았습니다.

대신 질문의 모양이 조금 달라졌습니다. 클래스가 무엇을 상속해야 하는지만 보기보다 데이터가 어디에서 와서 어디로 이동하는지, 상태가 어느 경계에서 바뀌는지, 새로운 요구사항이 들어왔을 때 몇 개의 관계를 다시 이해해야 하는지를 함께 보기 시작했습니다.

첫 발표에서 복잡한 UML을 작은 관계로 나누어 읽었던 것처럼, 복잡한 시스템도 데이터와 동작, 순수한 계산과 사이드 이펙트, AI의 생성과 사람의 판단으로 나누어 보면 이전에는 보이지 않던 경계가 드러날 수 있습니다.

다음 후기에서는 이 원칙들을 실제 코드와 더 큰 예제에 적용했을 때도 같은 단순성을 유지하는지 살펴보겠습니다.

첫 모임부터 발표 자료를 정성스럽게 준비하고, 각자의 경험과 질문을 아낌없이 나눠 주신 모든 분께 감사드립니다!

데이터 지향 프로그래밍 1주차 후기 - 상태와 동작을 분리해 바라보기 - 이미지 8




작성 참고