단일 책임 원칙
벽에 전동 드릴로 나사를 박는 경우를 생각해보자. 드릴을 나사에서 떼는 순간 벽체의 성분인 컴파운드 가루가 쭉 뿜어 나오면서 나사를 덮어버리는 신기한 현상을 볼 수 있다. 나사를 페인트로 칠할 거라면 몰라도 매번 이런 현상이 나타나는 것이 달갑지 않을 수도 있다. 그렇다고 해서 구멍 하나 편하게 뚫자고 다른 드릴로 바꿀 수도 없다. 이 드릴이 가루를 뿜지 않고 나사 박는 일 하나만 깔끔하게 처리했다면 오랫동안 유용하게 사용할 뿐만 아니라, 다양한 용도에 활용할 정도로 높은 유연성도 갖출 수 있었을 것이다.
단일 책임 원칙single responsibility principle에 따르면 하나의 코드 블록은 한 가지 작업만 제대로 수행해야 한다. 앞에서 예로 든 전동 드릴처럼 기능이 적을수록 오히려 코드 블록의 활용도가 높아진다. 이 원칙에 따라 코드를 작성하면 본인 뿐만 아니라 향후에 참여할 다른 개발자들을 수많은 고통으로부터 해방시켜줄 수 있다.
함수와 메서드를 책임의 관점에서 생각해보자. 책임이 늘어날수록 코드 블록의 유연성과 안정성이 낮아지고, 변경 사항이 늘어나며, 에러가 발생할 가능성도 높아진다. 무엇보다도 코드를 명확하게 작성하고 싶다면 함수나 메서드마다 한 가지 책임만 지도록 작성해야 한다.
함수의 기능을 표현할 때 ‘그리고’란 표현을 피할 수 없다면 함수가 너무 복잡한 것이다. 함수와 인수의 이름만 보고도 기능을 명확히 알 수 있을 정도로 간결하게 작성해야 한다.
최근 필자는 MBTI 성격 유형 검사를 온라인 버전으로 개발하는 프로젝트를 수행한 적이 있었다. 몇 년 전에도 비슷한 작업을 했었는데, 당시 코드를 처음 작성할 때 점수 수집, 차트 생성, 사용자에게 표시할 모든 DOM 관리 기능을 모두 processForm
이라는 함수 하나에 다 집어넣는 방식으로 처리했다.
그런데 기능을 변경할 때마다 구체적으로 수정할 부분을 찾기 위해 산더미 같은 코드 속을 힘들게 뒤져야 하는 문제가 발생했다. 게다가 실행 중 문제가 생기더라도 어디서 에러가 발생했는지를 찾는 것은 더욱 어려웠다.
그래서 이번에는 이러한 실수를 하지 않도록 모든 것을 하나의 모듈 오브젝트로 만들어 처리하지 않고, 단일 책임 원칙에 따라 여러 개의 함수로 분리했다. 이렇게 개선한 코드 중에서 폼을 전송할 때 호출되는 함수를 예로 들면 다음과 같다.
[전체 코드 링크]
읽고, 이해하고, 수정하기에 엄청나게 쉬워졌다. 심지어 개발자가 아니어도 무슨 일을 하는지 알 수 있을 정도다. 코드를 보고 눈치 챘겠지만 여기 나온 함수들은 모두 한 가지 일만 한다. 이것이 바로 단일 책임 원칙을 적용한 예다.
코드를 이렇게 작성해두면 나중에 새로운 기능, 예를 들어 폼 검증 기능을 추가할 때 간단히 메서드 하나만 더 추가하면 된다. 기존에 하나의 거대한 함수로 만들었을 때처럼 복잡한 코드를 뒤져가며 수정하고 또 이로 인해 제대로 작동하지 않을까 걱정할 필요가 없다. 또한 코드 로직과 변수도 영역별로 분리되어 있기 때문에 충돌이 발생할 가능성이 줄어들어 안정성이 크게 향상되고, 같은 함수를 다른 곳에서 재활용하기도 쉽다.
따라서 함수 하나에 책임도 하나라는 것을 명심하자. 긴 함수는 클래스로 만든다. 함수 하나에서 처리하는 작업이 너무 많고, 그 작업들이 서로 밀접하게 엮여 있는 데다, 각각이 모두 동일한 데이터를 처리하고 있다면, 앞에서 소개한 필자의 경험에서 거대한 폼 함수에 했던 것처럼 각각의 작업을 메서드 단위로 분리하여 오브젝트로 만드는 것이 좋다.
명령-질의 분리 원칙
예전에 인터넷에서 재미있는 글을 본 적이 있다. 자신이 키우던 고양이를 읽어버린 섀넌이 회사 동료인 데이비드에게 고양이 찾는 포스터 제작을 부탁하면서 주고 받은 메일인데, 필자가 지금껏 본 글 중에서 가장 웃겼다. 섀넌이 포스터 제작을 요청하는 메일을 보낼 때마다 데이빗은 결과물을 보냈지만, 매번 섀넌이 기대했던 것과는 약간 달랐다. 메일을 주고 받는 과정이 너무 웃겨서 재미로 읽기에는 좋은 글이지만, 코드를 이렇게 작성했다면 결코 웃을 수 없을 것이다.
명령-질의 분리 원칙command-query separation principle은 함수를 호출할 때 본의 아니게 발생한 외부 효과로 예상치 못한 결과가 나오는 일을 방지하는 데 기초가 되는 원칙이다. 함수는 그 성격에 따라 크게 두 가지로 분류할 수 있다. 하나는 어떤 동작을 수행하는 명령command이고, 다른 하나는 답을 구하는 질의query다. 이러한 두 역할은 한데 섞으면 안 된다. 예를 들어 다음 코드를 살펴보자.
실전에서 사용하는 코드는 대체로 이보다 훨씬 복잡해서 외부 효과가 발생하는 부분을 찾기가 상당히 힘든데 반해 이 코드는 금방 원인을 찾을 정도로 단순하다. 그래도 예상치 못한 외부 효과가 어떤 결과를 초래하는지에 대해 개략적으로 이해하기 위한 예제로는 충분하다.
함수 이름(getFirstName
)을 보면 사람 이름을 리턴한다는 것을 알 수 있다. 그런데 정작 이 함수에서 가장 먼저 하는 일은 이름을 소문자로 변환하는 것이다. 함수 이름은 질문에 답하는 질의형 함수인 것처럼 지었지만, 실제로 하는 일는 명령형 함수처럼 데이터의 상태를 변환하고 있다. 이는 함수 이름에 명확히 드러나지 않은 외부 효과에 해당한다.
더 심각한 부분은 소문자로 변환한 이름을 사용자에게 물어보지도 않고 쿠키로 설정하는 것인데, 자칫하면 기존에 사용하던 다른 값을 덮어 쓸 위험이 있다. 질의형 함수는 절대로 데이터를 덮어 쓰는 작업을 수행해선 안 된다.
질의형 함수를 작성할 때는 요청한 값을 리턴하기만 하고, 데이터에 상태를 바꾸는 일은 하지 않는다. 반대로 데이터의 상태를 변경하는 함수는 값을 리턴하는 일도 하지 말아야 한다. 코드를 최대한 명확하게 작성하려면 함수에서 값을 리턴하는 작업과 데이터의 상태를 변경하는 작업을 한 함수에서 동시에 처리해서는 안 된다.
이러한 원칙에 따라 코드를 다음과 같이 개선할 수 있다.
예제 코드가 굉장히 간단하지만 명령-질의 분리 원칙을 따라 코드를 작성하면 수행할 작업을 명확히 드러내고 에러 발생 가능성도 줄일 수 있다는 것을 이해하는 데는 충분하다. 함수와 코드 베이스가 커질수록 이러한 분리 원칙은 더욱 중요하다. 사용할 함수가 무슨 일을 하는지 알아내기 위해 매번 함수의 정의 부분을 뒤져야 한다면 그리 효율적인 방식은 아닐 것이다.
낮은 결합도
직소jigsaw(그림 맞추기) 퍼즐과 레고LEGO의 차이점에 대해 한번 생각해보자. 직소 퍼즐은 모든 조각을 맞추기 위한 방법이 단 한 가지뿐이고 최종 결과물의 형태도 하나다. 반면 레고는 조각을 원하는 방식대로 마음껏 조합하여 다양한 결과를 만들어 낼 수 있다. 만들 대상이 정해지지 않은 상태에서 둘 중 하나를 고르라면 어느 방식이 좋을까?
결합도coupling는 프로그램의 구성 단위끼리 의존하는 정도를 나타내는 척도다. 결합도가 너무 높으면(강한 결합도tight coupling) 융통성이 없어서 바람직하지 않다. 직소 퍼즐인 셈이다. 코드는 레고처럼 유연한 것이 좋다. 코드의 결합도가 낮고(느슨한 결합loose coupling) 명확성이 뛰어나야 좋은 코드다.
명심할 것은 다양한 활용 사례를 포용할 정도로 유연한 형태로 코드를 작성해야 한다. 코드 작성 과정에서 복사해서 붙여 넣는 일이 많거나, 자잘하게 변경할 일이 많거나, 부분 변경 때문에 코드 전체를 다시 작성하는 일이 많다면, 근본적으로 코드의 결합도가 높다는 것을 의미한다(예를 들어, 앞에서 소개한 getFirstName
함수를 재사용 가능한 형태로 변경하려면, 하드 코딩된 형태로 작성된 firstName
함수를 범용 ID를 인수로 전달받는 방식으로 수정해야 한다). 코드의 결합도가 높을 때 나타나는 또 다른 현상으로 함수 안에 ID가 하드 코딩되어 있거나, 매개변수가 너무 많거나, 비슷한 기능을 수행하는 함수가 여러 개 있거나, 함수가 너무 커서 단일 책임 원칙을 위배하는 경우 등이 있다.
클래스로 작성해야 할 것을 일련의 함수나 변수로 작성할 때 코드의 결합도가 높아지는 경우가 대부분이다. 또는 다른 클래스의 메서드나 속성에 의존하도록 클래스를 작성할 때도 결합도가 높아지기도 한다. 함수들 간의 상호 의존성 문제로 시달리고 있다면 함수를 클래스 단위로 나누는 것을 고려해보는 것이 좋다.
나는 인터랙티브 다이얼 코드를 작성할 때 이러한 상황에 맞닥뜨린 적이 있다. 다이얼을 구현하는 과정에서 범위, 핸들 크기, 받침점의 크기 등을 변수로 표현하다 보니 그 수가 많아졌다. 이 때문에 함수에서 받아야 할 매개변수가 엄청나게 많거나, 각 함수마다 이러한 변수들이 하드 코딩된 형태로 작성할 수밖에 없었다. 게다가 다이얼마다 상호작용하는 방식도 달랐다. 이 때문에 이렇게 작성한 함수를 다이얼마다 하나씩 만들다 보니 내용이 거의 같은 함수가 3개나 생겼다. 한마디로 하드 코딩 방식으로 변수와 동작을 작성함으로써 결합도가 높아졌고, 이로 인해 직소 퍼즐처럼 각각의 함수를 단 한 가지 방식으로만 조합할 수밖에 없게 돼버렸다. 코드베이스가 쓸데없이 복잡해진 것이다.
이러한 문제를 해결하기 위해 이러한 함수와 변수들을 나눈 다음, 재사용 가능한 클래스를 정의하여 다이얼마다 인스턴스를 생성해서 할당하도록 수정했다. 출력에 대한 인수로 함수를 받도록 클래스를 작성함으로써, 각 다이얼 오브젝트에 대한 인스턴스를 생성할 때마다 각기 형태로 설정할 수 있게 만들었다. 결과적으로 함수의 수도 줄고 변수도 단 한 군데만 저장하고, 업데이트도 훨씬 쉽게 처리할 수 있게 됐다.
여러 클래스가 상호작용하도록 작성하는 것도 결합도를 높이는 원인이 될 수 있다. 예를 들어 다른 클래스의 오브젝트를 생성하는 클래스를 정의하는 방식으로, 학생들을 생성하는 학부 과정 클래스를 구현하는 경우는 생각해보자. 작성된 CollegeCourse
클래스로 원하는 기능을 수행할 수는 있게 만들었지만, 나중에 Student
클래스의 생성자에 매개변수를 추가해야 할 일이 생겼다. 그런데 Student
클래스를 수정하려면 CollegeCourse
클래스 코드도 바꿔야 되는 상황이 발생한 것이다.
어떤 클래스를 수정할 때마다 그 클래스를 사용하는 코드까지 수정하는 것은 바람직하지 않다. 결합도가 높은 코드로 인해 발생하는 문제의 대표적인 예다. 생성자의 매개변수로 오브젝트를 받고, 이 오브젝트에 비상용 디폴트 값을 넣으면, 결합도를 낮출 수 있어서 매개변수를 새로 추가해도 코드를 망치는 일이 줄어든다.
이 원칙의 핵심은 코드를 직소 퍼즐이 아닌 레고 블록처럼 작성해야 한다는 것이다. 앞에서 설명한 사례와 유사한 문제가 발생한다면 코드의 결합도가 높을 가능성이 크다.
높은 응집도
어린 아이에게 방을 정리하라고 시키면 흩어져 있던 물건을 죄다 옷장에 몰아 넣는 것을 본 적이 있을 것이다. 물론 그렇게 하는 것도 정리의 한 가지 방법이긴 하지만, 원하는 물건을 찾기 힘들거나 한데 붙어 있어야 할 물건이 각기 다른 곳에 흩어져버리는 문제가 발생할 수 있다. 코드를 작성할 때 응집도를 높이는 데 주의를 기울이지 않으면 이런 일이 발생한다.
응집도cohesion란 여러 가지 프로그램 구성 단위가 서로 뭉쳐진 정도를 나타내는 척도다. 응집도는 높을수록 좋고, 이를 통해 코드 블록의 명확성도 높아진다. 반면 응집도가 낮으면 좋지 않고, 복잡도도 높아진다. 코드 블록에서 서로 관련된 함수와 메서드는 가까이 붙어 있어야 한다. 그래야 응집도를 높일 수 있다.
응집도가 높다는 말은 서로 관련 있는 것끼리 붙어 있다는 것을 의미한다. 예를 들어 데이터베이스 관련 함수들이나 특정한 엘리먼트에 대한 함수는 하나의 블록이나 모듈에 있어야 한다. 그러면 코드의 구조를 파악하기 쉽고 각 요소의 위치도 찾기 쉬울 뿐만 아니라 이름이 충돌하는 현상도 줄일 수 있다. 30개의 메서드를 4개의 클래스로 나눠둘 때보다 30개의 함수를 그냥 나열할 때 이름이 충돌할 확률이 훨씬 높다.
여러 함수에서 동일한 변수를 사용하고 있다면, 이들을 한 곳에 모아야 한다. 오브젝트를 사용하는 이유가 바로 여기에 있다. 슬라이더와 같은 페이지의 엘리먼트에 관련된 함수나 변수가 여러 개 있을 때, 이들을 하나의 오브젝트로 묶으면 응집도를 높일 수 있다.
앞에서는 다이얼 프로그램에 대한 클래스를 작성할 때 결합도를 낮추는 방법을 살펴봤다. 응집도를 높이는 것도 결합도를 낮추기 위한 한 가지 방법이 되기도 한다. 높은 응집도와 높은 결합도는 서로 대척점에 있으며, 한쪽이 다른 쪽에 영향을 미친다.
같은 코드가 여러 번 반복되는 것도 응집도가 낮다는 신호다. 비슷한 문장은 함수로 묶고, 비슷한 함수들은 클래스로 묶어야 한다. 경험칙에 따르면 같은 코드를 두 번 이상 반복하지 않아야 한다. 실전에서 이 원칙을 완벽히 지키기는 힘들지만 코드의 명확성을 높이려면 반복을 최소화하도록 최대한 노력해야 한다.
마찬가지로 비슷한 데이터가 여러 변수에 퍼져 있지 않아야 한다. 데이터를 부분마다 여러 곳에서 정의하고 있다면 이를 클래스로 묶어야 한다. 또한 동일한 HTML 엘리먼트에 대한 레퍼런스를 여러 함수에 전달하고 있다면 이 레퍼런스를 클래스의 인스턴스 속성으로 정의한다.
오브젝트를 다른 오브젝트 안에 넣으면 응집도를 더욱 높일 수 있다. 예를 들어 AJAX 함수를 모두 하나의 모듈에 넣고, 그 안에 폼 제출, 콘텐츠 긁어오기 로그인 구문 등에 대한 오브젝트도 담아둔다.
반대로 서로 관련 없는 것들을 한 클래스 안에 넣지 않아야 한다. 한때 일했던 회사에 내부 API로 만든 게 있었는데, Common
이라는 이름의 오브젝트에 자주 사용하는 메서드와 변수를 정의하는 과정에서 서로 관련 없는 함수나 변수들까지 한곳에 뒤죽박죽 섞어 뒀다. 결과적으로 클래스는 엄청나게 커졌고 이해하기도 어려웠다. 응집도에 대한 고려를 전혀 하지 않았기 때문이다.
클래스에 정의된 속성 중에서 다른 메서드에서 활용하는 횟수가 적다면 응집도가 낮다는 것을 의미한다. 마찬가지로 메서드를 재활용하는 사례가 적거나 심지어 어떤 메서드는 전혀 사용하지 않는다면 이 역시 응집도가 낮다는 신호다.
응집도를 높이면 결합도를 낮추는 데 도움된다. 결합도가 높다는 것은 응집도를 더 높여야 한다는 것을 의미한다. 응집도와 결합도가 서로 충돌할 때 굳이 하나를 선택한다면 응집도를 선택하는 것이 좋다. 개발자의 입장에서 낮은 결합도보다 높은 응집도가 도움되는 경우가 더 많기 때문이다. 물론 둘 다 좋으면 더할 나위 없다.
결론
코드가 명확하지 않으면 문제가 발생하기 쉽다. 단순히 들여쓰기만 잘한다고 코드가 명확해지지 않는다. 프로젝트의 시작 단계부터 세심하게 계획해야 한다. 여기서 소개한 단일 책임, 명령-질의 분리, 낮은 결합도, 높은 응집도 원칙은 완벽히 숙달해서 적용하기란 쉽지 않다. 하지만 이러한 원칙을 염두에 두고 작성하면 코드의 명확성을 크게 향상시킬 수 있다. 중요한 프로젝트라면 반드시 고려할 사항이다.
번역 감사. 도움 받았습니다.
코드를 읽는 시간이 코딩 짜는 시간보다 더 많으니 안습 아니겠습니까...
다른 비슷한 글이 있다면 함께 공유하면 좋겠네요.
도움이 되셨다니 기쁩니다. 연관된 글도 하나씩 올리겠습니다. 계속 관심 부탁드려요!