나는 Node.js 진영에서 NestJS와 Next.js를 주로 쓰는 개발자다. 매일 코드를 쓰면서 class를 작성한다. Controller, Service, Repository, Dto — 하루에도 몇 개씩 만든다.
그런데 이상한 점이 있다. 문법은 안다. extends도 알고, implements도 안다. 그런데 어떤 기준으로 class를 나눠야 하는지에서 매번 막힌다.
어느 날 문득 궁금해졌다. class라는 단어는 대체 어디서 온 걸까? 그리고 왜 이렇게 "잘 나누는 것"이 어려울까?
그 답을 찾다 보니, 2,400년 전 아리스토텔레스까지 거슬러 올라가게 되었다.
기원전 4세기, 아리스토텔레스는 세상 만물을 분류하려 했다. 그가 쓴 《카테고리아이(Κατηγορίαι)》는 서양 철학 최초의 분류 체계다. 여기서 "category"라는 단어가 나왔고, 이것이 오늘날 우리가 쓰는 "분류"의 뿌리다.
그의 방법은 단순하면서 강력했다.
- 속(genus): 큰 범주. "동물"
- 종차(differentia): 구분하는 특성. "이성적인"
- 종(species): 속 + 종차. "이성적인 동물" = 인간
이걸 고전적 분류(Classical categorization) 라고 부른다. 핵심 규칙은 이것이다:
모든 것은 명확한 경계를 가지고, 하나의 범주에 속한다.
A이면 B가 아니다. 경계는 칼로 자른 듯 선명하다. 프로그래밍을 하는 사람이라면 이 구조가 익숙할 것이다.
아리스토텔레스의 분류 체계가 얼마나 어려운지 보여주는 유명한 사례가 있다. 고래다.
고래는 바다에 산다. 지느러미가 있다. 헤엄을 친다. 그래서 수천 년 동안 인류는 고래를 물고기로 분류했다. "바다에 사는 것 = 어류"라는 기준이 너무 직관적이었기 때문이다.
하지만 고래는 포유류다. 폐로 호흡하고, 새끼에게 젖을 먹이고, 체온을 스스로 조절한다. 겉모습(서식지, 형태)으로 분류하면 어류지만, 본질(호흡, 번식, 체온)로 분류하면 포유류다.
어떤 기준으로 분류하느냐에 따라 결과가 완전히 달라진다. 이것이 분류의 본질적 어려움이다. 기준은 하나가 아니고, "올바른" 기준이 무엇인지는 맥락에 따라 달라진다. 흥미롭게도, 아리스토텔레스 본인은 고래가 어류가 아님을 관찰을 통해 알고 있었다. 하지만 이후 사람들은 그의 분류 "체계"만 가져가고, 직접 관찰하는 태도는 가져가지 않았다. 체계를 맹신하면 눈앞의 사실도 놓치게 되는 것이다.
이 문제는 프로그래밍에서도 그대로 반복된다.
흥미로운 점이 있다. 프로그래밍의 class라는 용어는 1965년 C.A.R. Hoare가 "Record Handling" 논문에서 "record class"로 처음 사용했고, 이를 Dahl과 Nygaard가 Simula 67에 가져왔다. 아리스토텔레스를 직접 참조한 것은 아니다.
그런데 학자들(Rayside & Campbell, 2000)이 사후에 분석해보니, OOP의 class 구조와 아리스토텔레스의 분류 체계가 놀랍도록 같은 구조를 가지고 있었다. 의도하지 않았지만, 결국 "세상을 나누는 행위"는 같은 패턴으로 수렴한 것이다.
JavaScript로 아리스토텔레스의 분류를 옮기면 이렇게 된다.
class Animal {
constructor(name) {
this.name = name;
}
breathe() {
console.log(`${this.name} is breathing`);
}
}
class Dog extends Animal {
bark() {
console.log(`${this.name}: Woof!`);
}
}
class Cat extends Animal {
meow() {
console.log(`${this.name}: Meow!`);
}
}
깔끔하다. Dog은 Animal이고, Cat은 Animal이다. 경계가 명확하고, 상호 배타적이다. 교과서에 나오는 예제는 항상 이렇게 아름답다.
그런데 여기에 고래를 넣어보자.
class Fish extends Animal {
swim() { }
}
class Mammal extends Animal {
breastfeed() { }
}
class Whale extends ??? { }
바로 이것이다. 교과서 예제에서는 Dog extends Animal이 아름답지만, 현실의 분류는 이렇게 경계가 모호하다. 고래 문제가 코드에서도 그대로 나타난다.
NestJS로 프로젝트를 하나 만든다고 해보자. 사용자(User)와 관련된 기능을 설계한다.
class UserController { }
class UserService { }
class UserRepository { }
여기까진 괜찮다. 그런데 기능이 늘어나면?
class UserService { }
class UserAuthService { }
class UserProfileService { }
class UserNotificationService { }
이쯤 되면 고민이 시작된다.
- 비밀번호 변경은
UserAuthService인가, UserService인가?
- 프로필 사진 업로드는
UserProfileService인가, FileService인가?
- 사용자가 탈퇴할 때 알림을 보내는 건
UserService인가, UserNotificationService인가?
하나의 행위가 여러 class의 경계에 걸친다. 아리스토텔레스가 말한 "명확한 경계, 하나의 범주"가 실무에서는 작동하지 않는 것이다.
여기서 중요한 사실을 하나 발견하게 된다.
같은 요구사항을 보고도 사람마다 class 구조가 완전히 다르다.
3년 차 개발자 A는 이렇게 나눈다:
class UserService {
changePassword() { }
updateProfile() { }
deleteAccount() { }
sendNotification() { }
}
7년 차 개발자 B는 이렇게 나눈다:
class AccountService {
changePassword() { }
deleteAccount() { }
}
class ProfileService {
updateProfile() { }
uploadAvatar() { }
}
class NotificationService {
send() { }
}
둘 다 동작하는 코드를 만들 수 있다. 문법적으로 틀린 것은 없다. 그런데 설계가 다르다. 이 차이는 코딩 실력의 차이가 아니다. JavaScript 문법을 더 많이 아는 사람이 더 좋은 class를 만드는 게 아니다.
이 차이는 경험에서 온다. "이렇게 나눴더니 나중에 고치기 힘들었다"는 경험. "이 구조가 6개월 후에도 버텼다"는 경험. 분류 능력은 코드를 많이 쳤다고 느는 게 아니라, 잘못된 분류의 결과를 많이 겪어봐야 는다.
아리스토텔레스 이후 2,400년 동안, 철학자들은 그의 분류 체계에 의문을 제기해왔다.
비트겐슈타인은 《철학적 탐구》에서 "가족 유사성(family resemblance)"이라는 개념을 제시했다. "게임"이라는 단어를 생각해보자. 보드게임, 카드게임, 올림픽 경기, 아이들의 공놀이 — 이 모든 것이 "게임"이지만, 모든 게임이 공유하는 단 하나의 본질적 속성은 없다. 서로 조금씩 비슷할 뿐이다.
프로그래밍에서도 마찬가지다. UserService와 AuthService와 AccountService의 경계는 칼로 자를 수 없다. 서로 조금씩 겹치고, 조금씩 다르다.
조지 레이코프는 《Women, Fire, and Dangerous Things》에서 더 나아간다. 분류는 객관적인 행위가 아니라, 그 사람의 체화된 경험(embodied experience)에 의존한다고 했다. 같은 대상을 보더라도 어부와 생물학자는 물고기를 다르게 분류한다. 어부는 "먹을 수 있는 것 / 없는 것"으로 나누고, 생물학자는 계통 분류를 따른다.
개발자도 마찬가지다. 프론트엔드 개발자와 백엔드 개발자가 같은 시스템을 보고 다른 class를 그리는 건 당연하다. 분류는 세상에 있는 것이 아니라, 분류하는 사람 안에 있다.
정리하면 이렇다.
class는 아리스토텔레스의 분류 체계와 놀랍도록 같은 구조다
- 고래를 물고기로 분류했듯, "명확한 경계" 전제는 현실에서 작동하지 않는다
- 분류는 개발 능력(문법, 패턴, 알고리즘)과 별개의 능력이다
- 분류 능력은 그 사람의 경험, 도메인 지식, 관점에 따라 달라진다
- 따라서 내가 좋다고 생각한 class 구조가 다른 사람에게는 맞지 않는다
이것이 코드 리뷰에서 "이 class를 왜 이렇게 나눴어?"라는 질문이 끊이지 않는 이유다. 그 질문에는 정답이 없다. 있는 것은 "이 맥락에서 더 나은 선택"뿐이다.
그렇다면 어떻게 해야 하는가?
완벽한 class 구조를 처음부터 설계하려 하지 마라. 아리스토텔레스조차 분류 체계를 여러 번 수정했다. 2,400년이 지나도 철학자들이 그의 체계를 반박하고 있다. 하물며 우리가 첫 설계에서 완벽한 분류를 해낼 수 있을까.
class UserService {
register() { }
login() { }
updateProfile() { }
sendWelcomeEmail() { }
}
class AuthService {
register() { }
login() { }
}
class ProfileService {
updateProfile() { }
}
class EmailService {
sendWelcomeEmail() { }
}
먼저 작성하고, 경험이 경계를 알려줄 때 리팩토링하라.
분류는 계획이 아니라 결과다. 코드도, 블로그 카테고리도, 인생의 대부분도 그렇다. 처음부터 완벽하게 나누려 하면, 정작 중요한 것 — 코드를 쓰고, 글을 쓰고, 무언가를 만드는 것 — 을 시작하지 못한다.
아리스토텔레스도 먼저 관찰하고, 그 다음에 분류했다. 우리도 먼저 만들고, 그 다음에 나누자.