← back to archive
dev
함수형 프로그래밍, 실무에서 가능할까요? · 01

AI 코딩 시대, 왜 다시 함수형인가

'잘 짜는 것'보다 중요한 '검증 가능성'

  • #함수형 프로그래밍
  • #AI 코딩
  • #소프트웨어 설계
  • #FCIS
  • #DOP

이 글은 "함수형 프로그래밍, 실무에서 가능할까요?" 시리즈의 첫 번째 글이에요. AI가 코드를 대량으로 생성하는 시대에 함수형 사고가 왜 다시 중요해지는지, 그리고 FCIS·DOP가 어떤 답이 될 수 있는지를 다뤄요.

들어가며

AI 코딩 도구가 보편화되면서 흥미로운 역전이 벌어지고 있어요. 코드를 작성하는 비용은 빠르게 줄어드는 반면, 코드를 검증하는 비용이 새로운 병목이 되고 있죠.

이 변화는 "어떤 패러다임이 좋은가"라는 오래된 질문의 평가 기준 자체를 바꿉니다. 과거에는 "사람이 읽고 쓰기 편한 코드"가 중요했다면, 이제는 "AI가 안전하게 다루고, 사람이 빠르게 검증할 수 있는 코드"가 중요해진 거예요.

이 관점에서 함수형 프로그래밍(FP)의 가치가 다시 보이기 시작합니다.

설계 관점에서 FP의 장점

1. 검증 가능성(Verifiability)이 높아요

순수 함수는 같은 입력에 같은 출력을 보장해요. 그리고 이 성질에서 중요한 결과 하나가 따라옵니다 — 입출력 타입이 곧 함수의 전체 동작 명세가 된다는 점이에요. 부수효과가 없다는 건 "함수 바깥에서 일어나는 일이 없다"는 뜻이고 그러면 함수가 무엇을 하는지는 "어떤 값이 들어와서 어떤 값이 나오는가"로 전부 설명되니까요.

"이 입력을 줄 테니 이런 결과를 출력해줘"라는 계약이 타입에 명시적으로 드러나면 사람뿐만 아니라 AI에게도 강력한 힌트가 됩니다. AI가 코드를 작성할 때 타입 에러를 보고 스스로 잘못된 시그니처를 교정할 수 있고 사람이 리뷰할 때도 본문을 다 읽기 전에 시그니처만으로 의도를 빠르게 파악할 수 있어요.

검증 가능성의 또 다른 축은 자동화된 테스트입니다. 순수 함수는 외부 상태나 목(mock)을 준비할 필요 없이 입력과 기대 출력만 적으면 단위 테스트가 끝나요. AI가 생성한 로직이 잘못됐을 때 즉시 잡아내는 비용이 그만큼 낮은 거죠.

반면 값을 mutable하게 다루거나 I/O 로직을 함수 안에 섞어 쓰는 스타일에서는 시그니처가 함수의 동작을 다 말해주지 않습니다. 예를 들어 void saveUser(User u)라는 시그니처를 봐도, 그 함수가 DB에 쓰는지·이메일을 보내는지·캐시를 비우는지 알 수 없죠. 사람도 AI도 본문 전체와 호출 그래프를 따라가야 동작을 파악할 수 있고 그만큼 검증 비용이 커집니다.

2. 동시성·병렬성 처리가 안전해요

동시성 버그가 발생하는 메커니즘은 단순해요. 여러 실행 흐름이 같은 가변 메모리를 동시에 읽고 쓰기 때문이죠. 그래서 불변(immutable) 데이터와 순수 함수를 쓰면 공유 가변 메모리에서 비롯되는 데이터 레이스·경쟁 조건은 원천적으로 사라집니다. (물론 외부 자원 — DB, 파일, 네트워크 — 에 대한 경쟁은 여전히 남지만 그 부분은 FCIS의 Shell 영역으로 격리되니 위치가 분명해지죠.)

이게 AI 코딩 맥락에서 특히 중요한 이유는 AI가 동시성 버그를 만들기 쉬운 구조이기 때문이에요. LLM은 지역적인 패턴을 보고 코드를 생성하는데, 동시성 안전성은 락 획득 순서·임계 구역의 원자성·메모리 가시성 같은 전역 불변식으로 결정돼요. 한 함수만 봐서는 안전 여부를 판단할 수 없는 영역이라 AI가 자신 있게 잘못 짜기 가장 쉬운 영역이에요. 그리고 동시성 버그의 진짜 비용은 작성 시점이 아니라 사후에 있어요 — 재현이 어렵고 운영 환경에서 산발적으로 터지고 디버깅에 며칠이 걸리기도 하죠.

요즘 실무에서 직접 스레드를 다루는 일은 줄었지만, 비동기(async/await) 코드의 race condition, Promise.all 같은 병렬 데이터 처리, 서버 컴포넌트의 요청 병렬화처럼 동시성 모델은 오히려 더 흔해졌어요. 함수형 스타일에서는 이런 코드를 짤 때 AI가 건드릴 영역이 "값 계산" 뿐이라 위험한 영역(공유 상태 조작) 자체를 들어가지 않게 됩니다.

3. 리팩토링이 기계적이에요

1번 섹션에서 얘기한 "같은 입력에 같은 출력"이라는 성질을 조금 다른 각도에서 부르면 참조 투명성(referential transparency) 이에요. 표현식을 그 결괏값으로 바꿔도 프로그램의 의미가 변하지 않는다는 뜻이죠. 예를 들면 이런 거예요.

sum(3, 5); // 이거랑
8; // 이거가 어디서든 바꿔 써도 동일

이 동치성이 프로그램 어디서든 성립하면, "함수 쪼개기", "로직 추출", "인자 순서 바꾸기", "부분 적용", "인라이닝" 같은 변환들이 형식적으로 의미 보존이 보장돼요. 즉 리팩토링 전후의 함수가 모든 입력에 대해 같은 결과를 낸다는 것이 규칙 수준에서 증명되죠.

부수효과가 있는 함수에서는 이 치환이 깨져요.

let counter = 0;
function next() {
  return ++counter;
}
 
next() === 1; // 처음엔 true
next() === 1; // 두 번째엔 false

next()1로 치환할 수 없으니, 이 함수를 "추출"하거나 "인라이닝"하는 순간 곧바로 버그가 됩니다. OOP에서 같은 위험이 일상적으로 일어나요. 메서드를 추출하면 추출된 코드가 this나 인스턴스 변수에 의존할 수 있고 호출 위치에 따라 동작이 달라지죠. 숨겨진 상태 변경이 있을 경우 추출 전후로 순서가 바뀌면서 미묘하게 결과가 달라지기도 해요. "안전한 리팩토링"이라는 게 사실은 사람이 전체 맥락을 보고 매번 판단해야 하는 일이에요.

AI 코딩 맥락에서 이 차이는 꽤 커요. LLM은 리팩토링을 요청하면 그럴듯한 결과를 잘 내놓지만 의미를 미묘하게 바꿔놓는 실수도 같은 빈도로 합니다. 함수형 구조에서는 변환 규칙이 형식적이라 AI(혹은 IDE, hlint 같은 도구)가 결정론적으로 적용할 수 있고 사람도 변환의 정당성을 시그니처만 비교해서 검증할 수 있어요. "시도해보고 테스트 통과 안 하면 되돌린다"는 루프가 그만큼 짧고 신뢰할 만해집니다.

이 성질을 한 단어로 부르면 합성성(composability) 이에요. 작은 순수 함수들을 자유롭게 조립·분해할 수 있고 한 부분을 바꿔도 영향이 그 자리에 머물러요. AI에게 "이 함수만 다른 구현으로 교체해줘"라고 요청해도 다른 곳이 함께 흔들리지 않으니, 부분 수정·교체의 신뢰도가 높아집니다.

그렇다면 단점은 없을까요?

물론 있어요. 전통적으로 지적되어 온 단점은 이런 것들이에요.

  1. 학습 곡선과 인지 부담 — 모나드, 펑터, 커링 같은 개념의 진입 장벽
  2. 성능 오버헤드 — 불변 자료구조의 메모리·CPU 비용
  3. I/O 중심 비즈니스 로직과의 충돌 — 부수효과 격리에 추가 추상화 필요
  4. 생태계와 도구 지원의 비대칭 — 주류 프레임워크는 OOP 전제
  5. 도메인 모델링 표현력 — 같은 ID를 가진 엔티티(사용자, 주문 등)가 시간에 따라 상태가 변해가는 도메인을 표현하기 까다로움

그런데 이 단점들은 순수 함수형 언어(Haskell, PureScript 등)로 풀-함수형 코드를 짤 때 가장 도드라지는 이야기예요. 현실적으로 우리가 마주하는 환경은 TypeScript, Kotlin 같은 멀티패러다임 언어들이고, 여기서는 게임의 룰이 좀 달라집니다.

FCIS와 DOP라는 현실적 합의점

이 시리즈에서 다룰 FCIS(Functional Core, Imperative Shell)DOP(Data-Oriented Programming) 는 순수 함수형의 이상과 현실 사이에서 "함수형의 장점은 취하고, 단점은 회피하자"는 실무적 합의점이에요.

  • FCIS: 양파처럼 안쪽은 순수한 알맹이(Core), 바깥쪽은 명령형 셸(Shell)이 감싸는 구조예요. 비즈니스 로직은 안쪽에서 순수 함수로 짜고 I/O와 부수효과는 바깥쪽 셸이 담당합니다.
  • DOP: 두 가지 원칙으로 요약돼요.
    • 데이터와 동작의 분리: 데이터는 평범한 맵/레코드로, 동작은 별도 함수로 둡니다.
    • 정체성과 상태의 분리: 엔티티의 정체성은 ID로, 상태는 시간에 따라 바뀌는 불변 데이터의 스냅샷으로 표현합니다.

두 개념은 관점이 달라서 충돌하지 않아요. FCIS는 코드의 구조 관점이고 DOP는 데이터의 모델링 관점이라 함께 쓰면 시너지가 납니다.

그런데 왜 하필 FCIS/DOP일까요?

"관심사를 분리하자"는 주장 자체는 새롭지 않아요. DDD, 레이어드 아키텍처, 헥사고날, 클린 아키텍처 모두 같은 이야기를 해왔죠. 차이는 경계를 긋는 기준이 얼마나 모호한가에 있어요.

기존 아키텍처들의 경계 질문은 대체로 사람의 해석이 들어가는 토론거리예요.

  • "이게 도메인 서비스인가, 유스케이스인가?"
  • "이 책임은 애플리케이션 레이어인가, 인프라 레이어인가?"
  • "이건 어그리거트 루트인가, 엔티티인가?"

팀마다, 사람마다 답이 갈리고 AI는 더더욱 일관된 판단을 내리기 어려워요. 반면 FCIS/DOP의 경계 기준은 술어(predicate) 수준으로 떨어집니다.

  • "이 함수가 외부 상태를 읽거나 쓰는가?" → Core냐 Shell이냐가 결정됨
  • "이건 데이터인가, 동작인가?" → 어디에 둘지가 결정됨

클린 아키텍처의 "이건 유스케이스인가 도메인 서비스인가"는 토론거리지만 "이 함수가 DB를 건드리는가"는 토론거리가 아니에요.

이 차이가 AI 시대에 특별히 중요한 이유는, 술어 수준의 기준은 사람의 개입 없이 기계가 적용·검증할 수 있기 때문이에요. AI 인스트럭션, 린터, 아키텍처 테스트로 옮기는 순간 규율이 코드베이스 전체에 균일하게 강제됩니다.

이 구조 위에서 앞서 말한 단점들이 어떻게 상쇄되는지, 그리고 실제 코드로 어떻게 풀어내는지는 시리즈의 다음 글들에서 하나씩 다룰 예정이에요.

AI 시대에 FCIS/DOP가 특별히 잘 맞는 이유

정리하자면 AI 코딩 시대에 FCIS/DOP가 가진 진짜 가치는 이 세 가지예요.

  1. 검증 가능성 — 순수 함수는 AI가 만든 코드의 신뢰성을 사람이 빠르게 판정하게 해줘요
  2. 규율의 기계 강제 가능성 — "부수효과 없음"은 도구로 검증 가능한 명제예요. "올바른 추상화"는 그렇지 않죠
  3. 합성성 — AI가 부분 수정·교체를 할 때 영향 범위가 국소적

AI 코딩 시대에는 검증 가능성생성 친화성을 동시에 만족시키는 현실적 해법으로 보여요.

다음 글에서는 Typescript 코드로 함수형 프로그래밍과 객체지향 프로그래밍을 비교해볼게요.