[SOLID] D - Dependency Inversion Principle

ChoiYongHyeun·2024년 3월 28일
0

프로그래밍 공부

목록 보기
13/15
post-thumbnail

Dependency Inversion Principle(DIP)

DIP 자체는 SOLID 법칙인 OCP , LSP 를 잘 지킨다면 자동으로 지켜지는 원칙이다.

의존성 역전 법칙인 DIP 는 다음과 같다.

  1. 저수준 컴포넌트는 항상 추상화된 컴포넌트를 참조해야 한다.
  2. 만약 저수준 컴포넌트가 구체화된 컴포넌트 혹은 모듈 을 참조한다면 잘못된 아키테쳑이다.
  3. 저수준 컴포넌트가 추상화된 컴포넌트에게 의존함으로서, 사용하는 컴포넌트는 구체화된 컴포넌트 이지만 의존하는 컴포넌트는 추상화된 컴포넌트 여야 한다.

네 ?

DIP 를 이해하기 위해선 우선 의미 용어들을 정확하게 짚고 넘어가자

  • 저수준 컴포넌트 : 가장 기본이 되는 최소 단위의 컴포넌트로 여러 컴포넌트의 기본 요소가 되는 컴포넌트
  • 추상화 된 컴포넌트 : 컴포넌트의 추상화 된 인터페이스르 다음과 같은 양식을 추상화 된 컴포넌트라고 한다.
    export default function Button({ backgroundColor }) {
      return <button style={{ backgroundColor: backgroundColor }}></button>;
    }
  • 구체화 된 컴포넌트 : 추상화 된 컴포넌트로 생성된 컴포넌트로 구현체 라고도 불리며 추상화 된 컴포넌트의 기능이 구현된 컴포넌트
    export default function BlackButton() {
      return <Button backgroundColor='black' />;
    }

클래스 기반의 객체지향 언어가 아닌 리액트를 기준으로 설명하려다 보니 억지스러운 면도 있긴 하지만 내용은 일맥 상통한다.

안정된 추상화

기본적으로 컴포넌트는 계층적 관계를 갖는데 위 예시에서 Button 컴포넌트는 Button 컴포넌트에 의존성을 갖는 BlackButton 의 내용이 수정 되더라도 영향을 받지 않는다.

의존성의 흐름이 Button -> BlackButton 으로 향하기 때문이다.

이처럼 추상화된 컴포넌트는 구체화된 컴포넌트의 수정에는 영향을 받지 않으나

반대로 구체화된 컴포넌트는 추상화된 컴포넌트의 수정에 영향을 받는다.

영향의 흐름은 의존성 흐름과 동일하게 흐르기 때문이다.

구체화된 컴포넌트는 수정이 잦게 일어나며 반대로 추상화 된 컴포넌트는 수정이 자주 일어나지 않는다.

예시를 통해 살펴보기

다음과 같은 예시를 들어 살펴보자 (클래스 기반의 객체 지향에서의 DIP 를 통해 살펴보자 )

class Sqaure {
  constructor() {
    this.width = 5;
    this.heigth = 5;
  }
}

class GeometricCalculator {
  constructor() {
    this.shape = new Sqaure();
  }

  getArea() {
    return this.width * 2;
  }
}

에를 들어 다음처럼 GeometricCalculator 클래스는 내부에서 Square 구현체를 this.shape 에 담아

넓이를 계산하는 getArea 메소드를 구현해두었다.

이 때 GeometricCalculator 에서 Sqaure 가 아닌 Rectangular 를 사용하고 싶다면

GeometricCalculator 내부에서 this.shapenew Rectangular() 로 변경해줘야 할 것이다.

이는 OCP 원칙을 거스른다.

GeometricCalculator 는 여러 기하학적 도형의 너비를 구해야 하는데 현재는 new Sqaure 구현체의 너비를 구하기만 하기 때문에 문제가 생긴 것이다.

사각형의 너비만 계산 가능한 계산기

class Sqaure {
  constructor() {
    this.width = 5;
    this.heigth = 5;
  }
}

class Rectangular {
  constructor() {
    this.width = 5;
    this.heigth = 10;
  }
}

class GeometricCalculator {
  constructor(shape) {
    this.shape = shape;
  }

  getArea() {
    return this.shape.width * this.shape.heigth;
  }
}

const calculator = new GeometricCalculator(new Rectanguler());
console.log(calculator.getArea()); // 50

다음처럼 할 경우엔 GeometricCalculatorSqaure , Rectangular 의 너비는 잘 계산하지만

다른 도형들이 들어올 경우 (원과 같은) 에는 올바른 답을 구하지 못할 것이다.

이는 GeometricCalculator 의 의존성이 Rectangular , sqaure 와 같은 사각형이라는 구현체 (추상화된 객체보다 비교적 구체화 된) 에 의존하고 있기 때문이다.

추상화 된 객체를 참조하는 경우

GeometricCalculator 가 모든 도형들에 대해 너비를 잘 구할 수 있게 하고 싶다고 해보자

class Shape {
  getArea() {
    throw new Error('getArea 메소드는 재정의 되어야 합니다.');
  }
}

다음처럼 모든 도형들의 추상화 된 클래스인 Shape 를 생성해주자

class Sqaure extends Shape {
  constructor() {
    super();
    this.width = 5;
    this.height = 5;
  }

  getArea() {
    return this.width * 2;
  }
}

class Rectangular extends Shape {
  constructor() {
    super();
    this.width = 5;
    this.height = 10;
  }

  getArea() {
    return this.width * this.height;
  }
}

class Circle extends Shape {
  constructor() {
    super();
    this.radius = 10;
  }

  getArea() {
    return Math.PI * this.radius ** 2;
  }
}

이후 Shape 의 구현체인 Square , Rectangular , Circle 들을 만들어주자

width , height , radisuconstructor 에서 받아 줄 수있지만 귀찮으니 그냥 넣어줬다.

class GeometricCalculator {
  constructor(shape) {
    if (!(shape instanceof Shape))
      throw Error('shape 는 항상 Shape 의 자식 객체여야 합니다');
    this.shape = shape;
  }

  getArea() {
    return this.shape.getArea();
  }
}

이후 GeometricCalculatorshape instaceof Shape 결과에만 의존 할 뿐 Shape 의 구현체라면 getArea 는 항상 잘 작동한다.

이렇게 의존성의 관계를 추상화 된 객체인 Shape 에게 갖게 함으로서

클래스를 확장하여 생성하는 것이 가능하다.

이후 추상화 팩토리에 대한 이야기가 나오는데 그 내용은 나중에 다루기로 하자

React 에서 DIP 원칙을 통한 책임 분리

DIP 는 기존적인 의존성의 관계를 변경해줌으로서 컴포넌트간의 관계를 독립적인 구조로 만들어주는 것에 중점을 둔다.

컴포넌트의 책임 분리

이전 SRP 에서 보았듯 컴포넌트의 책임을 분리하는 것 또한 의존성 역전과 관계가 있다.

import { useState, useEffect } from 'react';

export default function UserList() {
  const [userList, setUserList] = useState([]);

  useEffect(() => {
    const fetching = async () => {
      // 이 안에 어떤 복잡한 로직이 존재한다고 해보자
      const res = await fetch('/user');
      const json = await res.json();
      setUserList(json);
    };

    fetching();
  }, []);

  return (
    <ul>
      {userList.map(({ id, userName }) => (
        <li key={id}>{userName}</li>
      ))}
    </ul>
  );
}

이렇게 있을 때 UserList 컴포넌트는 useEffect 내부에 존재하는 콜백 함수의 구현 로직에 따라 렌더링 로직이 결정되는 ,

useEffect 문에 의존하고 있다.

그 뿐만 아니라 useEffect 내부에서 /user 라고 적혀있는 엔드포인트에도 의존하고 있다.

UserList 컴포넌트 내부의 useEffect 는 그저 렌더링에 필요한 데이터를 패칭해오는 모듈일 뿐이다.

DIP 에서는 모듈간 의존성을 명확히 함으로서 컴포넌트의 관심사를 명확하게 한다.

import { useState, useEffect } from 'react';

const useUserList = (endPoint) => {
  const [userList, setUserList] = useState([]);

  useEffect(() => {
    const fetching = async () => {
      const res = await fetch(endPoint);
      const json = await res.json();
      setUserList(json);
    };

    fetching();
  }, [endPoint]);

  return userList;
};

다음처럼 복잡한 useEffect 내부의 글을 useUserList 라는 커스텀훅으로 캡슐화 해줌으로서 추상화 된 객체를 이용했듯

추상화 된 커스텀 훅을 이용해주도록 하자

export default function UserList({ endPoint }) {
  const userList = useUserList(endPoint);
  return (
    <ul>
      {userList.map(({ id, userName }) => (
        <li key={id}>{userName}</li>
      ))}
    </ul>
  );
}

UserList 컴포넌트에서는 어떤 값에 의존하고 있을까 ?

props 로 넘어오는 endPoint props 에 의존하고 있다.

이전과 달라진 점
이전에는 useEffect 자체에 의존하고 있었다면 현재는 props 로 넘어오는 endPoint props 에 의존하고 있다.

이전에는 useEffect 에서 endPoint 가 지정되어 있어 항상 /user 에서 얻어지는 정보만 사용 가능했던 점을 기억하자.

props drilling 을 해제하여 의존성 역전

export function App({ content }) {
  return <FristComponent content={content}></FristComponent>;
}

export function FristComponent({ content }) {
  return (
    <div>
      저는 첫번째 컴포넌트예요
      <SecondComponent content={content}></SecondComponent>
    </div>
  );
}

export function SecondComponent({ content }) {
  return <div>{content}</div>;
}

다음과 같은 코드가 있다고 생각해보자

이 때 의존성 관게를 살펴보면

App , FirstComponent , SecondComponent 모두 content props 에 의존성을 가지고 있다.

SecondComponent -> FirstComponent -> App -> Content 방향으로 말이다.

이 처럼 본인의 컴포넌트에서 사용하지 않는 props 를 하위 컴포넌트에게 건내주기 위해 props 로 받는 경우로 props drilling 이라고 한다.

FirstComponent , App 컴포넌트는 content props 에 의존성을 가지고 있을 필요가 없다.

이 때 contentContext 나 전역 상태관리 라이브러리을 이용하여 SecondeComponent 에게 전달해줘보자

import { useContext } from 'react';

export function App() {
  return <FristComponent></FristComponent>;
}

export function FristComponent() {
  return (
    <div>
      저는 첫번째 컴포넌트예요
      <SecondComponent></SecondComponent>
    </div>
  );
}

export function SecondComponent() {
  const content = useContext('ContentContext');
  return <div>{content}</div>;
}

이렇게 되면 SecondeComponent 의 의존성은 useContext 의 반환값을 향해지고 App , FirstComponent 의 의존성은 해제 된다.

이처럼 리액트에서는 의존성의을 다른 요소로 체계화 함으로서 불필요한 의존성 관계를 해제하여

최적화 하는 것을 중요하게 여긴다.

profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글