Development

1. 우리의 코드가 스파게티인 이유 - [이벤트 기반 프로그래밍 시리즈]

성난소 2025. 3. 21. 20:21

 

 

닿을 수 없는 클린코드

저는 14년 동안 프로그래밍을 해 왔습니다.
저는 깔끔한 코드를 좋아합니다.
그런데 지금까지 직접 작성했던 그 어떤 소스 코드에서도
깔끔하다는 느낌을 받아본 적이 없습니다.
깔끔한 코드를 위해 많은 고민을 하고 공부를 하며, 책도 많이 읽고 적용해보았지만,
그러한 문제의식이 말끔히 해결된 적은 없는 것 같습니다.

프로젝트 초기에, 나중을 생각해서 열심히 인터페이스화와 관심사 분리를 해봐도
코드를 추가하고 수정을 반복하다 보면
어느 순간 눈앞에는 스파게티 코드가 펼쳐져 있곤 합니다.
왜 이렇게 되는 걸까요?
곰곰이 생각해본 끝에 한 가지 의문에 이르게 되었습니다.


왜 우리는 함수를 중심으로 코딩을 하는가

우리는 함수를 중심으로 코드를 작성합니다.
새로운 기능을 만들 때를 생각해봅시다.
일반적으로 우선 인터페이스와 함수를 선언합니다.

interface IGrabbable {
    void grab(Object other);
}


그리고 나서 인터페이스를 구현하며 함수를 작성합니다.

class Hand implements IGrabbable {
    @Override
    public void grab(Object other) {
        //...
    }
}


마지막으로 다른 객체에서 그 함수를 호출합니다.

class Robot
{
private IGrabbable rightHand = new Hand();
    
public void pickup(Object other) {
    
     // move to other;
        
        rightHand.grab(other);
    }
}


이렇듯 함수는 간편하며 이해하기 쉽습니다.
프로그래밍이 탄생했을 때 가장 먼저 생겨난 개념 중 하나가 함수일 것입니다.
그저 간편하고 오랜 시간 사용되어 왔기에,
우리가 함수 중심의 코딩 방식을 채택한 것은 아닐까요?
정말 이것이 최선일까요?


함수의 단점

함수가 탄생한 목적을 다시 생각해보면,
객체지향이라는 개념조차 없었던 시절에
함수는 단순히 코드의 재활용을 위해 만들어졌습니다.
프로그래머가 타이핑하는 수고를 덜고, 소스 코드의 중복을 줄이기 위한 의도가 컸습니다.
즉, SOLID라는 이름으로 널리 알려진 객체지향 원칙을 지키도록 돕는 도구로 탄생한 것은 아니라는 뜻입니다.
물론 원래 심장병 치료제로 개발되었다가 다른 용도로 더 널리 쓰이는 비아그라 같은 예도 있지만,
아쉽게도 함수는 그런 운 좋은 사례가 아닌 듯합니다.
오히려 함수는 객체지향에 있어서 커다란 단점을 내재하고 있습니다.

 

1. 함수는 그 자체로 SOLID 법칙 중 일부를 위반합니다

SOLID의 두 번째 알파벳, O는 개방-폐쇄 원칙입니다.
객체의 코드는 확장에는 열려 있고, 변경에는 닫혀 있어야 합니다.
함수의 파라미터 목록을 떠올려봅시다.

public void doSomething(String name, String action);


성능상 문제로 두 번째 action을 정수형 actionCode로 바꾸려고 한다면,
이 함수를 호출하는 모든 부분의 코드를 고쳐야 하므로 변경에는 닫혀 있다고 볼 수 있습니다.

그렇다면 함수의 코드는 확장에 열려 있을까요?
예시로 들었던 Java는 기본 파라미터를 지원하지 않습니다.
파라미터를 추가한다면 호출부 코드를 함께 수정해야 하니, 확장에도 닫혀 있다고 할 수 있습니다.

반면 C++처럼 기본 파라미터를 지원하는 언어에서는 호출부를 고치지 않고도 파라미터를 추가할 수 있습니다.

//C++
void doSomething(String name, String action, bool special = false);


그러나 여기서 파라미터를 더 추가하려고 한다면 마찬가지로 난감해집니다.
기본값을 지원하지 않는 파라미터는 추가할 수 없고,
기본값을 지원한다고 해도 제한이 있기 마련입니다.
결국 함수는 확장에 닫혀 있습니다.


2. 함수는 수동적입니다

함수는 다른 곳에서 호출해주지 않으면 절대 동작하지 않습니다.
객체 지향에서 public 함수로 인터페이스를 노출하는 객체들도 동일합니다.
해당 객체의 public 함수를 다른 객체가 호출해주지 않으면, 그 객체는 절대 움직이지 않습니다.
함수를 통해 동작하는 객체는 게으릅니다.

한편,
어디서든 함수를 호출만 해주면 무조건 응답하는 예스맨이기도 합니다.
정말 필요한 상황에서 호출되고 있는지 자체를 함수는 알 길이 없습니다.
함수를 통해 동작하는 객체는 무책임합니다.


3. 함수는 관계를 강제합니다

함수로만 동작하는 수동적인 객체의 특성 때문에,
결국 객체들은 함수를 호출하는 객체와 호출당하는 객체의 상하관계를 가지게 됩니다.
하위 객체는 상위 객체의 호출을 거부할 수 없으므로,
상위 객체는 하위 객체를 올바르게 호출해야 할 책임을 가지게 됩니다.

백엔드에서 널리 사용되는 Spring 프레임워크에서는 이러한 상하관계가
Controller - Service - Repository로 이루어진 Layered architecture로 표현됩니다.
책임을 적절히 나눈 구조라고 하지만
사실 이런 구조에서 대부분의 책임은 상위 객체에 집중됩니다.
하위 객체를 올바르게 호출해야 하고,
함수의 반환값을 보고 후속처리를 해야 하며,
예외가 발생하면 그것 역시 상위 객체가 처리해야 합니다.
책임이 분산된 것이 맞는지 의문이 들 수 있습니다.

보통 최상위 객체로는 외부 세계와 상호 작용하는 객체가 선택될 수밖에 없습니다.
Layered architecture에서 통신을 담당하는 Controller가 최상위 자리에 있는 것은 결코 우연이 아닙니다.
외부 세계는 통제 불가능하기 때문에, 외부 세계의 변화를 감지하고
알맞은 객체를 선택하여 호출하며, 모든 것을 책임질 객체가 필요하기 때문입니다.

요즘 회사에서도 수평적인 관계가 대세입니다.
그러나 소스 코드 세계에서 객체 사이는 철저한 상명하복 관계인 것입니다.


4. 함수는 소스코드를 고치기 힘들게 만듭니다

함수는 보통 한 번만 호출되는 경우가 드뭅니다.
애초에 여러 번 활용하려고 정의하는 것이 함수이므로,
최소 두세 군데, 많으면 수십 군데에서 함수를 호출하게 됩니다.

그런데 앞서 말했듯,
함수 파라미터를 바꾸려고 하면 호출부 곳곳을 전부 살펴봐야 합니다.
함수 내부의 내용을 변경하고 싶을 때도 마찬가지입니다.
내용이 달라지면 호출부 전체의 동작까지 달라질 수 있으므로,
마찬가지로 호출부를 모두 살펴봐야 합니다.
수십 군데에서 호출하는 함수를 수정해야 한다면?
아마 대부분은 쉽게 시도하지 못할 것입니다.


5. 함수는 소스코드를 읽기도 힘들게 만듭니다

함수는 고치는 것뿐만 아니라 읽기도 어렵게 만듭니다.

호출부가 여러 군데 있는 함수를 예로 들어봅시다.
호출부마다 다른 파라미터 조합으로 호출할 가능성이 큽니다.
때문에 함수 안에서는 다양한 맥락에서의 동작이 모두 고려되어야 합니다.
따라서 함수의 내용을 읽을 때, 내가 보고 있는 맥락과 관련 없는 맥락까지 구분하면서 이해해야 합니다.

물론 정말 깔끔하게 한 가지 동작만 담당하는 함수가 있을 수도 있습니다.
하지만 실제 비즈니스 요구 사항을 만족시키려다 보면
기능이 점차 추가되고, if문이 생겨나게 됩니다.
함수를 새로 만들어 호출부에서 두 함수를 각각 호출하도록 할 수도 있지만,
그러려면 모든 호출부 코드를 수정하거나 추가해야 하므로
대부분은 복잡해지는 것을 감수하고 if문만 덧붙이게 됩니다.
게다가 함수 자체가 코드 재활용을 목적으로 탄생한 것이기 때문에,
한 번의 호출로 여러 가지 일을 시키고 싶은 마음이 드는 것도 어쩔 수 없습니다.

결국 함수의 파라미터가 늘어나고,
기능이 계속 추가되고,
함수 내부에 if문이 붙고,
다른 클래스들 여기저기에서 호출하다 보면,
짜잔~

읽기 어려운 스파게티 코드가 탄생합니다.

 



그렇다면 함수를 쓰지 말라는 이야기인가요?

이렇게 함수에 대하여 비판적으로 길게 썼지만, 함수는 절대적으로 필요한 존재입니다.
제가 말씀드리고 싶은 요점은 함수를 아예 쓰지 말자는 것이 아니라,
“함수를 써야 할 곳”과 “쓰지 않아야 할 곳”을 잘 구분하여 사용하자는 것입니다.

함수가 수동적이라서 객체까지 수동적으로 만든다고 이야기했지만,
수동적일 수밖에 없는 객체들이 분명히 존재합니다.
예를 들어 Json 파싱, 파일 로딩을 담당하는 객체들은 수동적일 수밖에 없습니다.
이런 객체들은 주로 라이브러리에서 많이 볼 수 있는데,
라이브러리 역시 본질적으로 코드 재활용을 목적으로 탄생한 것이므로,
함수와 거의 같은 개념이라고 볼 수 있습니다.
이처럼 수동적인 객체들은 함수와 인터페이스를 활용하여 동작하는 것이 자연스럽습니다.

그러나
문제는 모든 객체를 수동적인 객체처럼 작성하는 데에 있습니다.

우리는 자동화를 위해, 나아가 우리의 편의를 위해 프로그래밍을 합니다.
그럼에도 왜 지시하지 않으면 절대 움직이지 않고,
호출만 하면 무조건 따라 하는 ‘멍청한’ 객체만 만들어내고 있을까요?
저는 자율적으로 결정하고, 스스로 움직이는 ‘자율적인 객체’를 원합니다.


대안은 이벤트 기반 프로그래밍

최근 이벤트라는 개념을 적절히 활용해보니,
이 문제들을 해결하면서 깔끔한 코드를 작성할 수 있다는 사실을 깨달았습니다.
최근 진행한 프로젝트에 이를 적용해보았는데,
처음으로 어느 정도 스스로 만족스러운 코드를 작성했다고 느꼈습니다.
그래서 제가 발견한 이벤트 프로그래밍의 특징과 함께,
관련 원칙과 팁을 이벤트 기반 프로그래밍 시리즈로 공유해보려고 합니다.

관심 있으시다면 구독하여 지켜봐 주시기 바랍니다.
또한 다른 의견이나 궁금한 점이 있으시다면 댓글로 말씀해주세요.
토의는 언제나 환영합니다.

'Development'의 다른글

  • 현재글 1. 우리의 코드가 스파게티인 이유 - [이벤트 기반 프로그래밍 시리즈]

관련글