종속성이 없는 진정한 모듈식 코드 생성
게시 됨: 2022-03-11소프트웨어 개발은 훌륭하지만… 우리 모두는 그것이 약간의 감정적 롤러코스터가 될 수 있다는 데 동의할 수 있다고 생각합니다. 처음에는 모든 것이 훌륭합니다. 몇 시간은 아니더라도 몇 일 만에 새로운 기능을 차례로 추가합니다. 당신은 롤에있어!
몇 달만 빨리 감으면 개발 속도가 느려집니다. 예전만큼 열심히 하지 않아서일까? 설마. 몇 달만 더 빨리 감으면 개발 속도가 더 느려집니다. 이 프로젝트에 대한 작업은 더 이상 재미가 없고 드래그가 되었습니다.
더 나빠진다. 애플리케이션에서 여러 버그를 발견하기 시작합니다. 종종 하나의 버그를 해결하면 두 개의 새로운 버그가 생성됩니다. 이 시점에서 노래를 시작할 수 있습니다.
코드에 99개의 작은 버그가 있습니다. 99개의 작은 버그. 하나 내려서 패치하고,
...코드에 127개의 작은 버그가 있습니다.
지금 이 프로젝트를 진행하는 것에 대해 어떻게 생각하십니까? 당신이 저와 같다면 아마 당신의 의욕을 잃기 시작할 것입니다. 기존 코드에 대한 모든 변경은 예측할 수 없는 결과를 초래할 수 있기 때문에 이 애플리케이션을 개발하는 것은 고통스러운 일입니다.
이 경험은 소프트웨어 세계에서 일반적이며 많은 프로그래머가 소스 코드를 버리고 모든 것을 다시 작성하려는 이유를 설명할 수 있습니다.
시간이 지남에 따라 소프트웨어 개발이 느려지는 이유
이 문제의 이유는 무엇입니까?
주요 원인은 복잡성 증가입니다. 내 경험에 따르면 전반적인 복잡성에 가장 크게 기여하는 것은 대부분의 소프트웨어 프로젝트에서 모든 것이 연결되어 있다는 사실입니다. 각 클래스의 종속성 때문에 이메일을 보내는 클래스의 일부 코드를 변경하면 사용자가 갑자기 등록할 수 없습니다. 왜 그런 겁니까? 등록 코드는 이메일을 보내는 코드에 따라 달라집니다. 이제 버그를 도입하지 않고는 아무것도 변경할 수 없습니다. 모든 종속성을 추적하는 것은 불가능합니다.
그래서 거기에 있습니다. 문제의 진정한 원인은 코드가 가진 모든 종속성으로 인해 복잡성이 증가하는 것입니다.
큰 진흙 덩어리와 그것을 줄이는 방법
재미있는 점은 이 문제가 이미 몇 년 전부터 알려져 왔다는 것입니다. "큰 진흙 덩어리"라고 불리는 일반적인 안티 패턴입니다. 여러 회사에서 수년 동안 작업한 거의 모든 프로젝트에서 이러한 유형의 아키텍처를 보았습니다.
그렇다면 이 안티 패턴은 정확히 무엇입니까? 간단히 말해서, 각 요소가 다른 요소와 종속되어 있을 때 큰 진흙 덩어리를 얻게 됩니다. 아래에서 잘 알려진 오픈 소스 프로젝트 Apache Hadoop의 종속성 그래프를 볼 수 있습니다. 큰 진흙 공(또는 큰 실)을 시각화하기 위해 원을 그리고 그 위에 프로젝트의 클래스를 고르게 배치합니다. 서로 의존하는 각 클래스 쌍 사이에 선을 긋기만 하면 됩니다. 이제 문제의 원인을 볼 수 있습니다.
모듈식 코드를 사용한 솔루션
그래서 나는 스스로에게 질문을 던졌다. 복잡성을 줄이고 프로젝트의 시작과 같이 여전히 재미있을 수 있을까? 사실 모든 복잡성을 제거할 수는 없습니다. 새로운 기능을 추가하려면 항상 코드 복잡성을 높여야 합니다. 그럼에도 불구하고 복잡성은 이동 및 분리될 수 있습니다.
다른 산업에서 이 문제를 해결하는 방법
기계 산업을 생각해 보십시오. 어떤 작은 기계 공장에서 기계를 만들 때 표준 요소 세트를 구입하고 몇 가지 사용자 정의 요소를 만들어 함께 만듭니다. 그들은 이러한 구성 요소를 완전히 개별적으로 만들고 마지막에 모든 것을 조립하여 몇 가지 조정만 하면 됩니다. 이것이 어떻게 가능한지? 그들은 볼트 크기와 같은 업계 표준을 설정하고 장착 구멍의 크기 및 구멍 사이의 거리와 같은 사전 결정에 따라 각 요소가 어떻게 서로 맞을지 알고 있습니다.
위 어셈블리의 각 요소는 최종 제품이나 기타 부품에 대해 전혀 알지 못하는 별도의 회사에서 제공할 수 있습니다. 각 모듈 요소가 사양에 따라 제조되기만 하면 계획한 대로 최종 장치를 만들 수 있습니다.
소프트웨어 산업에서 이를 복제할 수 있습니까?
물론, 우린 할 수있어! 인터페이스 및 제어 원리의 역전을 사용하여; 가장 좋은 점은 이 접근 방식을 모든 객체 지향 언어(Java, C#, Swift, TypeScript, JavaScript, PHP)에서 사용할 수 있다는 사실입니다. 이 방법을 적용하기 위해 멋진 프레임워크가 필요하지 않습니다. 몇 가지 간단한 규칙을 준수하고 훈련을 받으면 됩니다.
통제의 역전은 당신의 친구입니다
제어 역전(inversion of control)에 대해 처음 들었을 때 나는 즉시 해결책을 찾았다는 것을 깨달았습니다. 기존 종속성을 가져 와서 인터페이스를 사용하여 반전시키는 개념입니다. 인터페이스는 메서드의 간단한 선언입니다. 그들은 구체적인 구현을 제공하지 않습니다. 결과적으로 두 요소를 연결하는 방법에 대한 합의로 사용할 수 있습니다. 원할 경우 모듈식 커넥터로 사용할 수 있습니다. 한 요소가 인터페이스를 제공하고 다른 요소가 이에 대한 구현을 제공하는 한 서로에 대해 전혀 알지 않고도 함께 작동할 수 있습니다. 훌륭합니다.
간단한 예제를 통해 시스템을 분리하여 모듈식 코드를 생성하는 방법을 살펴보겠습니다. 아래 다이어그램은 간단한 Java 애플리케이션으로 구현되었습니다. 이 GitHub 저장소에서 찾을 수 있습니다.
문제
Main
클래스, 3개의 서비스 및 단일 Util
클래스로 구성된 매우 간단한 애플리케이션이 있다고 가정해 보겠습니다. 이러한 요소는 여러 방식으로 서로 의존합니다. 아래에서 "큰 공(big ball of mud)" 접근 방식을 사용한 구현을 볼 수 있습니다. 클래스는 단순히 서로를 호출합니다. 그것들은 단단히 결합되어 있으며 다른 요소를 건드리지 않고 단순히 하나의 요소를 꺼낼 수 없습니다. 이 스타일을 사용하여 만든 응용 프로그램을 사용하면 초기에 빠르게 성장할 수 있습니다. 나는 이 스타일이 쉽게 가지고 놀 수 있기 때문에 개념 증명 프로젝트에 적합하다고 생각합니다. 그럼에도 불구하고 유지 관리조차도 위험할 수 있고 단일 변경으로 인해 예측할 수 없는 버그가 발생할 수 있기 때문에 프로덕션 준비 솔루션에는 적합하지 않습니다. 아래 다이어그램은 진흙 아키텍처의 이 큰 공을 보여줍니다.
의존성 주입이 잘못된 이유
더 나은 접근 방식을 찾기 위해 종속성 주입이라는 기술을 사용할 수 있습니다. 이 방법은 모든 구성 요소가 인터페이스를 통해 사용되어야 한다고 가정합니다. 요소를 분리한다는 주장을 읽었지만 실제로 그러합니까? 아니요. 아래 도표를 보십시오.
현재 상황과 큰 진흙 덩어리의 유일한 차이점은 이제 클래스를 직접 호출하는 대신 인터페이스를 통해 호출한다는 사실입니다. 요소를 서로 분리하는 것을 약간 향상시킵니다. 예를 들어 다른 프로젝트에서 Service A
를 재사용하려는 경우 Interface A
와 함께 Service A
자체, Interface B
및 Interface Util
을 제거하여 이를 수행할 수 있습니다. 보시다시피 Service A
는 여전히 다른 요소에 의존합니다. 결과적으로 한 곳에서는 코드를 변경하고 다른 곳에서는 동작을 엉망으로 만드는 문제가 계속 발생합니다. 여전히 Service B
및 Interface B
를 수정하는 경우 이에 종속되는 모든 요소를 변경해야 한다는 문제가 발생합니다. 이 접근 방식은 아무 것도 해결하지 못합니다. 제 생각에는 요소 위에 인터페이스 레이어를 추가하는 것뿐입니다. 종속성을 주입해서는 안 되며, 대신 한 번에 완전히 제거해야 합니다. 독립만세!
모듈식 코드를 위한 솔루션
내가 믿는 접근 방식은 종속성의 모든 주요 골칫거리를 해결하기 위해 종속성을 전혀 사용하지 않음으로써 해결합니다. 구성 요소와 해당 수신기를 만듭니다. 리스너는 간단한 인터페이스입니다. 현재 요소 외부에서 메서드를 호출해야 할 때마다 리스너에 메서드를 추가하고 대신 호출하면 됩니다. 요소는 파일을 사용하고 패키지 내에서 메서드를 호출하고 기본 프레임워크 또는 기타 사용되는 라이브러리에서 제공하는 클래스만 사용할 수 있습니다. 아래에서 요소 아키텍처를 사용하도록 수정된 애플리케이션의 다이어그램을 볼 수 있습니다.

이 아키텍처에서는 Main
클래스에만 여러 종속성이 있습니다. 모든 요소를 함께 연결하고 애플리케이션의 비즈니스 로직을 캡슐화합니다.
반면 서비스는 완전히 독립적인 요소입니다. 이제 이 애플리케이션에서 각 서비스를 꺼내 다른 곳에서 재사용할 수 있습니다. 그들은 다른 것에 의존하지 않습니다. 하지만 잠시만요, 점점 나아지고 있습니다. 서비스의 동작을 변경하지 않는 한 해당 서비스를 다시 수정할 필요가 없습니다. 이러한 서비스가 해야 할 일을 하는 한 시간이 끝날 때까지 그대로 둘 수 있습니다. 전문 소프트웨어 엔지니어나 goto
문을 혼합하여 만든 최악의 스파게티 코드를 처음 사용하는 코더가 만들 수 있습니다. 논리가 캡슐화되어 있기 때문에 문제가 되지 않습니다. 끔찍하지만 다른 클래스로 유출되지는 않습니다. 또한 여러 개발자 간에 프로젝트 작업을 분할할 수 있는 기능을 제공합니다. 여기에서 각 개발자는 다른 개발자를 방해하거나 다른 개발자의 존재를 알지 않고도 자체 구성 요소에서 독립적으로 작업할 수 있습니다.
마지막으로 마지막 프로젝트를 시작할 때와 마찬가지로 독립 코드 작성을 한 번 더 시작할 수 있습니다.
요소 패턴
반복 가능한 방식으로 생성할 수 있도록 구조적 요소 패턴을 정의합시다.
요소의 가장 간단한 버전은 기본 요소 클래스와 리스너의 두 가지로 구성됩니다. 요소를 사용하려면 리스너를 구현하고 기본 클래스를 호출해야 합니다. 다음은 가장 간단한 구성의 다이어그램입니다.
분명히, 결국에는 요소에 더 많은 복잡성을 추가해야 하지만 쉽게 할 수 있습니다. 논리 클래스가 프로젝트의 다른 파일에 의존하지 않는지 확인하십시오. 이 요소의 기본 프레임워크, 가져온 라이브러리 및 기타 파일만 사용할 수 있습니다. 이미지, 보기, 사운드 등과 같은 자산 파일에 관해서는 요소 내에 캡슐화되어 나중에 쉽게 재사용할 수 있도록 해야 합니다. 전체 폴더를 다른 프로젝트에 복사하기만 하면 됩니다!
아래에서 더 고급 요소를 보여주는 예시 그래프를 볼 수 있습니다. 사용 중인 보기로 구성되며 다른 응용 프로그램 파일에 종속되지 않습니다. 종속성을 확인하는 간단한 방법을 알고 싶다면 가져오기 섹션을 살펴보세요. 현재 요소 외부의 파일이 있습니까? 그렇다면 요소로 이동하거나 수신기에 적절한 호출을 추가하여 이러한 종속성을 제거해야 합니다.
Java로 만든 간단한 "Hello World" 예제도 살펴보겠습니다.
public class Main { interface ElementListener { void printOutput(String message); } static class Element { private ElementListener listener; public Element(ElementListener listener) { this.listener = listener; } public void sayHello() { String message = "Hello World of Elements!"; this.listener.printOutput(message); } } static class App { public App() { } public void start() { // Build listener ElementListener elementListener = message -> System.out.println(message); // Assemble element Element element = new Element(elementListener); element.sayHello(); } } public static void main(String[] args) { App app = new App(); app.start(); } }
처음에는 출력을 인쇄하는 방법을 지정하기 위해 ElementListener
를 정의합니다. 요소 자체는 아래에 정의되어 있습니다. 요소에 대해 sayHello
를 호출하면 단순히 ElementListener
를 사용하여 메시지를 인쇄합니다. 요소는 printOutput
메서드의 구현과 완전히 독립적입니다. 콘솔, 물리적 프린터 또는 멋진 UI에 인쇄할 수 있습니다. 요소는 해당 구현에 의존하지 않습니다. 이 추상화 때문에 이 요소는 다른 애플리케이션에서 쉽게 재사용될 수 있습니다.
이제 기본 App
클래스를 살펴보십시오. 리스너를 구현하고 구체적인 구현과 함께 요소를 어셈블합니다. 이제 사용을 시작할 수 있습니다.
여기에서 JavaScript로 이 예제를 실행할 수도 있습니다.
요소 아키텍처
대규모 응용 프로그램에서 요소 패턴을 사용하는 방법을 살펴보겠습니다. 작은 프로젝트에서 그것을 보여주는 것과 실제 세계에 적용하는 것은 별개입니다.
제가 즐겨 사용하는 풀스택 웹 애플리케이션의 구조는 다음과 같습니다.
src ├── client │ ├── app │ └── elements │ └── server ├── app └── elements
소스 코드 폴더에서 처음에 클라이언트와 서버 파일을 분할했습니다. 브라우저와 백엔드 서버라는 두 가지 다른 환경에서 실행되기 때문에 합리적인 일입니다.
그런 다음 각 레이어의 코드를 앱 및 요소라는 폴더로 나눕니다. 요소는 독립적인 구성 요소가 있는 폴더로 구성되며 앱 폴더는 모든 요소를 함께 연결하고 모든 비즈니스 로직을 저장합니다.
그렇게 하면 다른 프로젝트 간에 요소를 재사용할 수 있으며 모든 응용 프로그램별 복잡성은 단일 폴더에 캡슐화되고 요소에 대한 간단한 호출로 축소되는 경우가 많습니다.
실습 예제
실천이 항상 이론보다 낫다고 믿고 Node.js와 TypeScript로 만든 실제 예제를 살펴보겠습니다.
실생활의 예
고급 솔루션의 시작점으로 사용할 수 있는 매우 간단한 웹 응용 프로그램입니다. 그것은 요소 아키텍처를 따르고 광범위하게 구조적 요소 패턴을 사용합니다.
하이라이트에서 메인 페이지가 요소로 구분되었음을 알 수 있습니다. 이 페이지에는 자체 보기가 포함되어 있습니다. 예를 들어 재사용하려는 경우 전체 폴더를 복사하여 다른 프로젝트에 놓기만 하면 됩니다. 모든 것을 함께 연결하기만 하면 설정이 완료됩니다.
오늘부터 자신의 애플리케이션에 요소를 도입할 수 있음을 보여주는 기본 예입니다. 독립 구성 요소를 구별하고 해당 논리를 분리할 수 있습니다. 현재 작업 중인 코드가 얼마나 지저분한지는 중요하지 않습니다.
더 빠르게 개발하고 더 자주 재사용하십시오!
이 새로운 도구 세트를 사용하여 유지 관리가 더 쉬운 코드를 더 쉽게 개발할 수 있기를 바랍니다. 실제로 요소 패턴을 사용하기 전에 모든 주요 사항을 빠르게 요약해 보겠습니다.
소프트웨어의 많은 문제는 여러 구성 요소 간의 종속성으로 인해 발생합니다.
한 곳을 변경하면 다른 곳에서 예측할 수 없는 동작을 도입할 수 있습니다.
세 가지 일반적인 아키텍처 접근 방식은 다음과 같습니다.
진흙의 큰 공. 빠른 개발에는 훌륭하지만 안정적인 생산 목적에는 그다지 좋지 않습니다.
의존성 주입. 피해야 할 반쯤 구운 솔루션입니다.
요소 아키텍처. 이 솔루션을 사용하면 독립적인 구성 요소를 만들고 다른 프로젝트에서 재사용할 수 있습니다. 안정적인 프로덕션 릴리스를 위해 유지 관리가 가능하고 훌륭합니다.
기본 요소 패턴은 모든 주요 메서드가 있는 메인 클래스와 외부 세계와 통신할 수 있는 간단한 인터페이스인 리스너로 구성됩니다.
전체 스택 요소 아키텍처를 달성하려면 먼저 프론트엔드와 백엔드 코드를 분리합니다. 그런 다음 앱과 요소에 대해 각각에 폴더를 만듭니다. 요소 폴더는 모든 독립 요소로 구성되며 앱 폴더는 모든 것을 함께 연결합니다.
이제 자신만의 요소를 만들고 공유할 수 있습니다. 장기적으로 쉽게 유지 관리할 수 있는 제품을 만드는 데 도움이 됩니다. 행운을 빕니다. 무엇을 만들었는지 알려주세요!
또한 코드를 너무 일찍 최적화하는 경우 동료 Toptaler Kevin Bloch 의 조기 최적화의 저주를 피하는 방법을 읽으십시오.