회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
AWS 이용 중이라면 최대 700만 원 지원받으세요
과거 일했던 회사에서 처음 배운 것 중 하나는 어떤 대가를 치르더라도 코드 재작성을 피하는 것이었다. 변경을 거듭할 때마다, 잘 되던 것이 오히려 버그로 회귀할 위험이 있기 때문이다. 버그는 비용이 많이 들고, 그것이 새로운 기능의 일부일 때는 버그를 수정하는 데 시간이 많이 걸린다. 이렇게 회귀하는 것은 버그가 있는 새로운 기능을 릴리스하는 것보다 더 나쁘다. 즉, 한 단계 더 후퇴하는 것이다. 버그가 농구에서 슛을 놓친 것이라면, 회귀는 마치 자기 팀 골대에 골을 넣어 상대팀에게 점수를 내주는 것과 같다. 시간은 소프트웨어 개발에서 가장 중요한 자원이며, 시간을 잃는 것은 가장 심각한 손해이다. 회귀는 가장 많은 시간을 잃게 만든다. 회귀를 피하고 코드를 지키는 것이 합리적이다.
회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!
확인
과거 일했던 회사에서 처음 배운 것 중 하나는 어떤 대가를 치르더라도 코드 재작성을 피하는 것이었다. 변경을 거듭할 때마다, 잘 되던 것이 오히려 버그로 회귀할 위험이 있기 때문이다. 버그는 비용이 많이 들고, 그것이 새로운 기능의 일부일 때는 버그를 수정하는 데 시간이 많이 걸린다. 이렇게 회귀하는 것은 버그가 있는 새로운 기능을 릴리스하는 것보다 더 나쁘다. 즉, 한 단계 더 후퇴하는 것이다. 버그가 농구에서 슛을 놓친 것이라면, 회귀는 마치 자기 팀 골대에 골을 넣어 상대팀에게 점수를 내주는 것과 같다. 시간은 소프트웨어 개발에서 가장 중요한 자원이며, 시간을 잃는 것은 가장 심각한 손해이다. 회귀는 가장 많은 시간을 잃게 만든다. 회귀를 피하고 코드를 지키는 것이 합리적이다.
하지만 코드를 변경하지 않으면 결국 문제가 발생할 수 있다. 새로운 기능을 구현하기 위해서 무언가를 부수고 다시 만들어야 한다면 개발에 방해가 될 수 있기 때문이다. 기존 코드를 그대로 두고 최대한 건드리지 않은 채로 새로운 코드에 모든 것을 추가하는 데 익숙할 수 있다. 하지만 코드를 변경하지 않고 그대로 두면 더 많은 코드가 필요할 수 있으며, 결국 유지보수해야 하는 코드의 양만 증가시킬 뿐이다.
기존 코드를 바꿔야 한다면 이것은 더 큰 문제이다. 이번에는 조용히 넘어갈 수 없다. 기존 코드가 특별한 작업 방식과 밀접하게 연결되어 있다면 수정하는 것 자체가 매우 어려울 수 있다. 이 상황에서 코드를 변경하면 다른 여러 곳을 함께 변경해야 할 수도 있다. 변화에 대한 기존 코드의 이러한 저항을 코드 경직성(code rigidity)이라고 한다. 그것은 코드가 더 경직될수록 이 코드를 조작하기 위해 더 많은 코드를 깨버려야 한다는 것을 의미한다.
코드 경직성에는 여러 요인이 있으며, 그중 하나는 코드에 너무 많은 종속성(또는 의존성)이 있는 경우이다. 종속성은 프레임워크 어셈블리, 외부 라이브러리, 내 코드의 다른 엔터티 등 다양한 것과 연관될 수 있다. 코드가 얽힐 경우 모든 유형의 종속성은 문제를 일으킬 수 있다. 종속은 축복이면서 동시에 저주일 수도 있다. 그림 3-1은 끔찍한 종속성 그래프를 가진 소프트웨어를 보여준다. 구성 요소 중 하나가 손상되면 거의 모든 코드를 변경해야 하는 수준으로, 이는 매우 극단적인 경우이다.
종속성은 왜 문제가 될까? 종속성을 추가하는 것을 고려할 때, 모든 구성 요소를 다른 고객으로 간주하거나 모든 계층을 요구 사항이 서로 다른 시장 집단이라고 생각해 보자. 여러 시장의 고객에게 서비스를 제공하는 것은 단일 시장의 고객에게 서비스를 제공하는 것보다 더 큰 책임이 따른다. 고객의 요구 사항이 각자 다르기 때문에 다양한 요구 사항을 충족해야 한다. 종속성 체인(dependency chains)을 결정할 때 이러한 관계를 생각해 보자. 이상적으로는 가능한 한 적은 유형의 고객에게 서비스를 제공하도록 노력해야 한다. 이것은 구성 요소 또는 전체 계층을 최대한 단순하게 유지하기 위한 중요한 요소다.
우리는 종속성을 피할 수 없으며, 코드를 재사용하려면 종속성은 필수적이다. 코드 재사용은 두 가지 조항으로 이뤄진 계약이다. 구성 요소 A가 구성 요소 B에 의존적이라고 할 때 첫 번째 조항은 ‘B가 A에게 서비스를 제공할 것’이다. 흔히 간과하는 두 번째 조항은 ‘B를 변경할 때마다 A를 유지보수해야 한다’이다. 종속성 체인을 잘 구분하여 관리한다면 코드 재사용으로 인한 종속성은 문제가 없다.
컴파일도 안 되는 데다 테스트에 실패할 수도 있는데 왜 코드를 깨야 할까? 서로 얽혀 있는 의존성이 코드의 경직성을 유발하여 변화에 저항성을 갖게 하기 때문이다. 이것은 시간이 지날수록 여러분을 더 느리게 만들며, 결국 여러분을 멈추게 할 것이다. 초반에 종속성을 끊는 것이 더 쉽기 때문에 당장 코드가 잘 동작하더라도 이러한 문제를 인식하고 코드를 깨야 한다. 그림 3-2에서 종속성이 우리 손을 어떻게 묶어 놓는지를 볼 수 있다.
종속성이 없는 구성 요소를 수정하는 것이 가장 쉽다. 그 외 다른 것을 깨뜨리는 것은 불가능하다. 어떤 구성 요소가 다른 구성 요소 중 하나에 의존하는 경우, 종속성은 일종의 계약을 의미하기 때 문에 이때는 약간의 경직성이 발생한다.
이는 B에서 인터페이스를 변경하면 A도 변경해야 한다는 의미이다. 인터페이스를 변경하지 않고 B의 구현을 변경하더라도 일단 B를 부순 셈이기 때문에 A를 깰 수 있다. 단일 구성 요소에 종속된 구성 요소가 여러 개일 경우 문제는 더 커진다.
A를 변경하려면 종속된 구성 요소도 변경해야 하며, 그중 하나라도 망가질 위험성이 존재하기 때문에 더 어렵다. 프로그래머들은 코드를 더 많이 재사용할수록 시간을 더 많이 절약할 수 있다고 생각하는 경향이 있다. 하지만 이로 인해 어떤 대가를 치러야 하는지를 고민해 봐야 한다.
여러분이 가져야 할 첫 번째 습관은 의존성에 대한 추상화 경계를 넘지 않는 것이다. 추상화 경계는 코드의 계층 주위에 그리는 논리적 경계로, 주어진 계층의 관심사 집합을 의미한다. 예를 들어 코드에 웹, 비즈니스, 데이터베이스 계층을 추상화할 수 있다. 그림 3-3처럼 코드를 계층화할 때 DB 계층은 웹 계층이나 비즈니스 계층을 몰라야 하며, 웹 계층도 DB 계층을 알아서는 안 된다.
경계를 넘는 것은 왜 좋지 않은 걸까? 그 이유는 바로 추상화의 장점을 없애기 때문이다. 하위 계층의 복잡성을 상위 계층으로 끌어올리면 하위 계층에서 일어나는 모든 변경 사항과 그 영향을 관리해야 한다. 각각의 구성원이 각자의 계층을 책임지는 팀이 있다고 생각해 보자. 경계를 넘는다면 웹 계층의 개발자가 갑자기 SQL을 배워야 할 수도 있다. 이뿐만 아니라 DB 계층의 개발자가 필요 이상으로 많은 사람과 소통해야 할 수도 있다. 결국 개발자에게 불필요한 책임과 부담만 지울 뿐이다. 사람들을 납득시킬 합의점을 찾기 위한 시간만 기하급수적으로 늘어날 것이다. 시간도, 추상화의 가치도 잃게 된다.
이러한 경계 문제와 마주친다면 코드를 깨버려 동작이 멈추게 하고, 위반 요소를 제거하고, 코드를 리팩터링 하고, 그 영향을 처리하라. 코드에 의존하는 다른 부분도 수정해야 한다. 이런 경우에는 코드를 깰 위험이 있더라도 방심하지 말고 즉시 차단해야 한다. 코드가 깨지는 것을 두려워한다면 그것은 잘못 설계된 코드이다. 좋은 코드는 깨지지 않는다는 뜻이 아니다. 깨졌을 때 조각을 다시 붙이는 것이 훨씬 더 쉽다는 뜻이다.
테스트의 중요성 코드 변경으로 어떤 시나리오가 실패하는지 확인할 수 있어야 한다. 기존에 가지고 있던 지식에 의존해 코드를 이해할 수 있겠지만, 시간이 지남에 따라 코드가 더 복잡해진다면 코드에 대한 이해는 더 이상 도움되지 않을 것이다. 그런 의미에서 테스트는 더 간단하다. 테스트는 지시 사항을 적어 놓은 목록일 수도 있고, 완전히 자동화된 테스트일 수도 있다. 자동화된 테스트는 보통 한 번 작성해 두면 직접 실행하는 데는 시간을 낭비하지 않기 때문에 일반적으로 바람직한 편이다. 테스트 프레임워크 덕분에 꽤 간단하게 사용할 수 있다. 4장에서 이 주제에 대해 더 자세히 다룰 것이다. |
그림 3-3은 웹 계층이 DB 계층과 공통적인 기능을 가질 수 없다는 것을 의미하는가? 아니, 물론 가질 수 있다. 그러나 그런 경우에는 별도의 구성 요소가 필요하다. 예를 들어 두 계층 모두 공통적인 모델 클래스에 의존적일 수 있다. 이 경우 그림 3 -4와 같은 관계 다이어그램을 사용할 수 있다.
코드를 리팩터링 하면 빌드 프로세스가 중단되거나 테스트가 실패할 수 있다. 또한, 이론적으로 절대 해서는 안 되는 짓이다. 하지만 나는 이러한 위반을 숨겨진 문제라고 생각한다. 즉시 이러한 문제에 주의를 기울여야 하고, 만약 이 과정에서 버그가 더 많이 발생한다면 코드가 작동을 멈춘 것이 아니라 이미 그곳에 있었던 버그가 이제서야 알아차리기 쉽게 드러난 것뿐이다.
예를 들어, 이모티콘으로만 소통할 수 있는 채팅 앱을 위해 API를 작성한다고 생각해 보자. 끔찍하게 들리겠지만, 한때 Yo를 포함한 문자만을 메시지로 보낼 수 있는 채팅 앱이 있었다. (Yo라는 채팅 앱에서는 ‘Yo’가 포함된 문자만 보낼 수 있었다. 한때 이 앱은 1,000만 달러의 가치가 있었다. 이 회사는 2016년에 문을 닫았다. https://en.wikipedia.org/wiki/Yo_(app). ) 아마 이 앱보다는 나을 것이다. 모바일 기기의 요청을 받아서 웹 계층으로 앱을 설계하고, 실제 작업을 수행하는 비즈니스 계층(일명 로직 계층)을 호출한다. 이렇게 분리하면 웹 계층 없이 비즈니스 계층을 테스트할 수 있다. 나중에 모바일 웹 사이트와 같은 다른 플랫폼에서도 동일한 비즈니스 로직을 사용할 수 있다. 따라서 비즈니스 로직을 분리하는 것이 당연하다.
비즈니스 계층은 데이터베이스나 스토리지 기술에 대해 아무것도 모른다. 필요할 때 DB 계층을 호출할 뿐이다. DB 계층은 데이터베이스 기능을 DB와 분리된 형태로 캡슐화한다. 이렇게 분리하면 스토리지 계층의 모의 구현을 비즈니스 계층에 쉽게 연결할 수 있기 때문에 비즈니스 로직을 테스트하기가 더 쉬워진다. 더 중요한 것은 이 아키텍처로 비즈니스 계층이나 웹 계층의 코드를 한 줄도 변경하지 않고 백그라운드에서 DB를 변경할 수 있다는 점이다. 그림 3-5에서 이러한 종 류의 계층화가 어떤 모습인지 볼 수 있다.
여기서 단점은 API에 새로운 기능을 추가할 때마다 새로운 비즈니스 계층 클래스나 메서드, 관련 DB 계층의 클래스와 메서드를 만들어야 한다는 것이다. 특히 마감이 코앞이고 기능이 비교적 단순한 경우에는 작업량이 많아 보일 수 있다. “단순한 SQL 쿼리를 위해 이런 모든 번거로움을 감수해야 하는 이유는 무엇일까?”라고 생각할 수도 있다. 최전방에서 많은 개발자의 환상을 실현시키 고 기존의 추상화를 거슬러 보자.
사용자가 보내고 받은 메시지의 총 수를 보여주는 새로운 통계 탭을 구현하라는 지시를 받았다고 가정해 보자. 백엔드에서의 단순한 SQL 쿼리는 다음과 같다.
SELECT COUNT(*) as Sent FROM Messages WHERE FromId=@userId
SELECT COUNT(*) as Received FROM Messages WHERE ToId=@userId
API 계층에서 이러한 쿼리를 실행할 수 있다. ASP.NET 코어나 웹 개발, SQL에 익숙하지 않더라도 모바일 앱으로 반환할 모델을 정의하는 코드 3-1 정도는 쉽게 이해할 수 있을 것이다. 모델을 자동으로 JSON 형식으로 직렬화한다. 그런 다음 SQL 서버 데이터베이스를 위한 연결 문자열을 가져온다. 이 문자열로 데이터베이스에 연결하고 쿼리를 실행한 다음 결과를 반환한다.
코드 3-1의 StatsController 클래스는 웹 처리에 대한 추상화이다. 여기에는 수신된 쿼리 변수가 함수 인수로 있고 URL은 컨트롤러 이름으로 정의되며 결과는 객체로 반환된다. 따라서 코드 3-1의 https://yourwebdomain/Stats/Get?userId=123과 같은 URL을 사용하면 MVC 인프라가 쿼리 변수를 함수 인수에 매핑하고, 반환된 객체를 JSON 결과에 자동으로 매핑한다. URL, 쿼리 문자열, HTTP 헤더, JSON 직렬화를 실제로 처리하지 않아도 되기 때문에 웹 처리 코드를 작성하는 것이 훨씬 간단하다.
이 코드를 구현하는 데 아마 5분 정도 걸렸던 것 같다. 코드 자체는 매우 간단해 보인다. 우리는 왜 추상화에 신경을 쓰는 걸까? 이렇게 API 계층에 모든 것을 넣으면 되지 않을까?
이러한 솔루션은 완벽한 설계가 필요하지 않은 시제품을 제작할 때 적합할 수 있다. 하지만 생산시스템에서 이런 결정을 내린다면 신중해야 한다. 생산 시스템을 중단할 수 있는가? 몇 분 정도 사이트가 다운되어도 괜찮을까? 괜찮다면 사용해도 상관없다. 다른 팀원의 생각은 어떨까? API 계층의 유지 관리자가 이러한 SQL 쿼리를 여기저기에 넣는 것을 좋아할까? 테스트는 어떤가? 이 코드를 테스트하고 올바르게 실행되는지 확인하려면 어떻게 해야 할까? 여기에 새 필드를 추가하는 것은 어떤가? 다음날 사무실에서 벌어질 일을 상상해 보자. 사람들이 여러분을 안아주며 칭찬할까? 아니면 여러분의 책상과 의자에 압정을 박아둘까?
실제 데이터베이스 구조에 종속성을 추가한 꼴이 된 것이다. 코드에서 사용한 Messages 테이블이나 데이터베이스 기술의 레이아웃을 변경해야 하는 경우, 코드의 모든 부분을 확인하여 새로운 데이터베이스나 새로운 테이블 레이아웃과 함께 모든 것이 잘 동작하는지 확인해야 한다.
프로그래머는 보통 미래의 사건과 비용을 예측하는 데 능숙하지 않다. 마감일을 맞추기 위해 불리한 결정을 내렸던 과거의 순간에 발생한 혼란 때문에 다음 마감일을 맞추기가 더욱 어려워진다. 보통 이것을 기술 부채(technical debt)라고 부른다.
기술 부채는 이미 우리가 의식하고 있는 결정이다. 무의식적인 것은 기술적인 미숙함(technical ineptitude)이라고 부른다. 부채라고 하는 이유는 제때 갚지 않으면 예기치 못한 시점에 그 코드가 여러분을 찾아와서 쇠파이프로 여러분의 다리를 부러트릴 것이기 때문이다.
다양한 이유로 기술 부채가 쌓일 수 있다. 때론 임의의 값을 위해 상수 변수를 만드는 수고를 감당하는 대신 그 값을 그냥 전달하는 것이 더 쉬워 보일 수 있다. “거기에는 문자열이 좋을 것 같다”, “이름을 줄여서 나쁠 것 없다”, “일단 다 복사하고 일부를 바꾸자”, “그냥 정규 표현식을 사용하자.” 이 모든 사소한 잘못된 결정이 여러분과 여러분 팀 성과의 발목을 잡을 것이다. 또한, 시간이 지나면 작업 처리 속도가 현저하게 저하될 것이다. 점점 더 느려지고, 일에 대한 만족감도 줄고, 경영진의 긍정적인 피드백도 줄어들 것이다. 게으름을 잘못 피워서 실패를 자초하는 것이다. 제대로 된 게으름을 피우자. 미래에 올 달콤한 게으름을 위해 일하자.
기술 부채를 다루는 가장 좋은 방법은 당분간 미루는 것이다. 앞으로 더 큰일이 있다면 가볍게 워밍업 할 기회로 사용하자. 코드가 멈출 수도 있다. 코드의 경직된 부분을 확인하고 세분화된 유연성을 확보하는 기회로 삼자. 문제를 해결하고 코드를 수정한 다음에 아직 충분히 제대로 동작하지 않는다고 생각되면 모든 변경 내용을 취소하자.
요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.