함수, 클래스, 파일의 적절한 크기를 논하면서, 모듈 경계는 줄 수가 아니라 인터페이스 대 구현 비율과 물리적 제약을 기준으로 잡아야 한다는 원칙을 다룬 글.
2025년 11월 28일
TigerStyle은 몇 가지 다소 자의적인 한계에 대해 꽤 엄격하다.
…우리는 함수당 70줄이라는 하드 제한을 강제한다…
…모든 줄 길이에 대해, 예외 없이 최대 100 컬럼이라는 하드 제한을 둔다…
동시에, 꽤나 큰 파일들도 몇 개 있어서, [git 히스토리에 큰 바이너리 블롭을 두지 않는다]는 정책에서 예외로 명시해야 할 정도다: tidy.zig#L746.
도대체 함수/클래스/파일의 크기를 어느 정도로 만들어야 할까? 여기에 대해 나는 두 가지 답을 가지고 있다.
첫 번째 원칙은, 크기 자체는 중요하지 않다는 것이다. 대신, 서로 관련된 것들은 함께 두고, 독립적인 것들은 떨어뜨려 두어야 한다. 개별 컴포넌트의 크기만 최소화하려고 해서도 안 되고, 컴포넌트 사이의 의존성 개수만 줄이려고 해서도 안 된다. 그렇게 하면, 결국 컴포넌트가 딱 하나뿐인 퇴화한 해답이나, 아니면 코드의 각 줄이 하나의 파일이 되어 버리는 퇴화한 해답으로 가게 된다.
그 대신, 모듈 크기와 그 인터페이스의 _비율_을 최적화해야 한다. 부피를 표면적으로 나누어야 한다. 중요한 것은 크기가 아니라 형태다!
데이터 구조를 별도 파일로 옮겨야 하는 시점은, 그 데이터 구조가 스스로 완결적일 때다. 그게 열 줄이든 만 줄이든 상관없다. 우리는 replica.zig 같은 파일도 있고, timestamp_range.zig처럼 작은 파일도 있다.
이 규칙을 함수에 적용할 때 좋은 시각적 비유가 있다. 함수에는 입력, 즉 인자의 개수가 있다. 그리고 출력도 있다(보통 하나지만, 서로 무관한 것들의 묶음일 수도 있다). 입력과 출력의 개수를 합친 것이 인터페이스의 크기다. 그리고 본문의 길이가 구현의 크기를 나타낸다. 원하는 것은, 인터페이스에 비해 상대적으로 본문이 큰 함수다. 뒤집힌 모래시계 모양이 필요하다. 그 역이 더 직관적일 수 있는데, 모래시계 모양(위와 아래가 넓고 가운데가 가는) 함수나 모듈은 코드 냄새다.
이는 의존성을 선택할 때에도 유용한 원칙이다. 의존성은 유용하다. 거기서 실제 일을 해 준다! 하지만 종종, 어떤 의존성을 자세히 들여다보면, 그 자체로는 의미 있는 일을 거의 하지 않고, 실제 로직(더 아래 단계의 의존성에 구현되어 있는)을 다른 인터페이스로 재포장하기만 하는 경우가 있다. 이런 경우에는 그 **접착제(glue)**를 잘라내고, 바로 알고리즘의 핵심으로 들어가야 한다.
이성적 논리와는 별도로, 물리적 한계가 존재한다. 모니터의 세로 픽셀은 한정되어 있고, 그 안에 코드를 넣어야 한다. 그래서 100 컬럼 제한이 나온다. 이 제한 덕분에, 현대의 16:9 디스플레이에서 코드를 나란히 두 개까지는 편안하게 띄워 볼 수 있다. 이 "둘"이 중요하다 — 코드를 비교할 수 있어야 하고, 불변식을 서로 맞추기 위해 호출자와 피호출자 둘 다를 동시에 볼 수 있어야 한다.
세로 공간도 가로 공간만큼이나 제한되어 있다. 함수가 화면 안에 다 들어올 때와, 딱 한두 줄 더 길어져서 끝부분이 보이지 않을 때 사이에는 날카로운 불연속점이 있다. 그래서 함수 길이 상한을 정하는 셸링 포인트(Schelling point)가 생긴다. 한 화면에 들어가는 편이 더 낫다. 대략 60~70줄 정도다.
하지만 파일 크기나 파일 개수에는 본질적인 상한이 없다. 그러니 그것들은 커져도 된다. 다만, "직선 탐색(linear search)"만으로 스스로를 제한하지 않도록 해야 한다. 프로젝트 안의 어떤 파일이든, 이름의 몇 글자만 쳐서 빠르게 열 수 있어야 한다. 퍼지(fuzzy) 검색은 선택이 아니라 필수다. 비슷하게, 큰 파일을 효율적으로 탐색하는 법도 배워야 한다. 모든 함수 목록을 빠르게 가져올 수 있는가? 퍼지 이름으로 함수로 점프할 수 있는가?
물리적 제약은 제한이지만, 더 나은 설계를 이끄는 유용한 길잡이가 되기도 한다. "컷(cut)"의 크기가 모듈의 줄 수에 직접적으로 의존하는 것은 아니지만, 대개는 상관관계가 존재한다. 만 줄짜리 파일이 진짜로는 서로 싸우고 있는 세 개의 서브시스템이 아닌지 확실한가? 오늘 내가 쓴 다른 글에서도 언급했듯, 좋은 인터페이스 설계는 자연스럽지 않다. 한 번 보고 나면 그 결과물의 형태는 자명해 보인다. 어려운 부분은, 애초에 _인터페이스라는 게 존재한다(혹은 존재할 수 있다)_는 사실을 깨닫는 것이다. 그리고 코드 전체가 한눈에 들어오지 않는다면, 한 발짝 물러나 화면에서 눈을 떼고 생각해 볼 때가 된 것일지도 모른다.
추신: "Matters"는 동사가 아니라 복수형 명사다.