명시적이든 암묵적이든
소프트웨어 개발에서 "Stateless"라는 개념은 마치 이상적인 목표처럼 여겨진다. 특히 함수형 프로그래밍과 서버리스 아키텍처에서는 상태를 없애고 순수한 로직만으로 시스템을 구성할 수 있다는 주장이 따라붙는다. 그러나 현실은 다르다. 결국 상태(State)와 인스턴스(Instance)에서 벗어날 수 없으며, 이를 관리하는 것이 개발자의 숙명이다.
RESTful API는 Stateless하다. 각 요청은 독립적이며, 클라이언트의 상태를 서버가 직접 유지하지 않는다. 하지만 RESTful API를 실행하는 서버 인스턴스는 Stateless하지 않다. 요청을 처리하는 과정에서 데이터베이스, 캐시, 인증 세션 등과 상호작용하며, 반드시 상태를 저장하는 무언가와 연결될 수밖에 없다. 즉, API 자체는 Stateless하지만, 그것을 실행하는 물리적 환경은 Stateful하다.
함수도 마찬가지다. 함수는 입력을 받아 출력을 반환하는 순수한 연산 단위이며, 함수 자체는 Stateless하다. 그러나 함수형 프로그래밍(FP)이 제공하는 고차 함수, 클로저, 커링 등의 기법을 사용하면 함수 내부에 상태를 저장하게 된다. FP가 Stateless한 패러다임이라고 이해했지만, 사용하면서 혼동을 느끼는 순간이 여기에서 비롯된다. FP는 함수 자체의 상태를 관리하지 않는 것이 아니라, 상태를 다루는 방식을 다르게 설정할 뿐이다.
이러한 차이는 함수형 프로그래밍과 객체지향 프로그래밍(OOP)의 상태 관리 방식에서 더욱 뚜렷하게 나타난다. 결국 상태를 다룰 수밖에 없다는 점에서, 두 패러다임의 차이는 상태를 어디에서, 어떻게 다루느냐로 귀결된다.
함수형 프로그래밍은 명시적(Explicit) 상태 관리 방식을 취한다. 모든 데이터는 함수의 인자로 전달되고, 함수 내부에서 상태를 저장하지 않는다. 상태의 흐름이 분명하게 드러나며, 함수가 예측 가능하다.
const updateUserLastLogin = (db: DatabaseConnection, user: User) => ({ ...user, lastLogin: new Date(), });
이 함수는 db와 user를 명확하게 전달받는다. 내부적으로 user의 상태를 변경하는 것이 아니라, 새로운 객체를 반환한다. 따라서 상태의 변화를 추적하기 쉽고, 상태를 다루는 방식이 코드에서 명확하게 드러난다.
반면, 객체지향 프로그래밍은 암묵적(Implicit) 상태 관리 방식을 따른다. 상태는 객체 내부에 숨겨지고, 메서드를 호출함으로써 상태가 변경된다.
class UserRepository {
constructor(private db: DatabaseConnection) {}
async updateUserLastLogin(user: User) {
user.lastLogin = new Date();
return this.db.save(user);
}
}
이 코드에서는 db가 객체 내부에 캡슐화되어 있으며, 메서드를 호출하면 객체의 상태가 바뀔 수 있다. 하지만 updateUserLastLogin을 호출하는 입장에서는 어떤 상태가 변경되었는지 직접 알기 어렵다.
즉, FP는 "어떤 상태가 바뀌었는지"를 코드에서 드러내지만, OOP는 상태 변경을 내부적으로 처리하여 숨긴다. FP에서는 상태를 직접 전달하고 다루며, OOP에서는 상태를 캡슐화하고 메서드를 통해 조작한다.
이 차이는 테스트와 유지보수 방식에서도 영향을 미친다. FP는 상태를 명시적으로 다루기 때문에, 개별 함수의 동작을 독립적으로 테스트하기가 쉽다. 반면 OOP에서는 객체의 내부 상태를 추적해야 하기 때문에, 특정한 상태에서만 재현되는 버그가 발생하기 쉽다. 그러나 OOP는 상태를 캡슐화하기 때문에, 전체적인 구조를 관리하기가 FP보다 수월하다.
그렇다고 해서 FP가 반드시 Stateless한 패러다임은 아니다. FP의 핵심 기법인 고차 함수, 클로저, 커링 등을 활용하면, 결국 상태를 암묵적으로 유지하는 구조가 발생할 수밖에 없다.
예를 들어, 클로저를 활용하여 의존성을 유지하는 방식은 OOP의 인스턴스 필드와 본질적으로 다르지 않다.
const createUserRepository = (db: DatabaseConnection) => ({ getUser: async (id: number) => await db.query(`SELECT * FROM users WHERE id = ${id}`), });
이 코드에서 db는 함수 인자로 전달되는 것처럼 보이지만, 실제로는 createUserRepository 호출 시 클로저 내부에 저장된다. 즉, FP의 문법을 따르지만, 결국 특정한 상태를 암묵적으로 기억하는 방식이 된다.
이와 같은 구조적 한계는 고차 함수가 많아질수록 더욱 심화된다. 다차원의 고차함수를 사용하면, 더 이상 모든 상태를 코드에서 직접 추적하기 어려워진다. 결국 함수형 프로그래밍이라고 해도, 일정 수준 이상 복잡해지면 OOP와 유사한 상태 유지 방식이 필요해진다.
이것은 어떻게 보면 "Stateless는 없다"는 주제를 더욱 명확하게 뒷받침하는 요소다. FP는 상태를 코드에서 명확히 드러내는 것을 목표로 하지만, 이를 유지할 수 있는 한계는 인간의 인지 능력에도 의존한다. 일정 수준 이상의 추상화를 거치면, 명시적으로 상태를 관리하는 FP조차도 내부적으로 암묵적인 상태를 유지할 수밖에 없다.
즉, 함수형 프로그래밍은 상태를 보다 명확하게 다루는 방식일 뿐이지, 그것이 곧 Stateless를 의미하는 것은 아니다. 특정한 패턴을 통해 OOP보다 더 투명한 상태 관리를 할 수 있지만, 차상위 함수 추상화가 증가할수록 상태의 흐름을 완벽하게 추적하기 어려워진다. 이는 결국 모든 프로그램이 본질적으로 Stateful할 수밖에 없으며, 상태를 관리하는 것이 개발자의 역할이라는 점을 다시금 상기시킨다.
"Stateless"라는 개념은 언어적 함정이다. RESTful API가 Stateless하다고 해서, 그것을 실행하는 서버가 Stateless한 것은 아니다. 함수가 Stateless하다고 해서, FP가 Stateless한 것은 아니다. FP가 상태를 숨기지 않고 명시적으로 드러내는 방식일 뿐, 결국 상태를 다룰 수밖에 없다는 점은 OOP와 동일하다.
개발자는 언제나 상태를 저장하고 관리하는 역할을 수행해야 한다. OOP에서는 객체 내부에서 상태를 관리하고 캡슐화하며, FP에서는 상태를 숨기지 않고 명시적으로 다룬다. 그러나 어느 방식이든 상태 자체를 없앨 수는 없다.
"Stateless"라는 개념은 상태를 제거하는 것이 아니라, 상태를 어디에 둘 것인가의 문제를 의미할 뿐이다. FP와 OOP는 그 해법이 다를 뿐, 둘 다 상태를 다룰 수밖에 없는 현실을 피할 수 없다. 결국, Stateless는 없다. 다만, 상태를 숨길 뿐이다.