상세 컨텐츠

본문 제목

개발자라면 알아야 할 솔리드 (SOLID) 원칙. 제대로 알고 코딩을 하는가? 8분에 배워보자

개발 (Development & Coding)/한글본 (Korean)

by 랑크 2023. 10. 15. 06:00

본문

개발자라면 꼭 알고 코딩을 해야 하는 원칙 중 하나가 이 SOLID (솔리드) 원칙입니다.  객체 지향 설계 5원칙이라고 불리고 있는 이 원칙은 디버깅이나 코드관리의 효율성, 그리고 나아가서는 버그를 줄일 수 있게 도와줍니다.  혹시 이 원칙을 모르고 코딩을 하신다면 쓰레기 코드를 만들고 계실 확률이 아주 높으므로 개발을 그만두는 것이 좋습니다.  개발도 학문입니다.

 

원칙은 어렵지만 좀 더 쉽게 배울 수 있도록 리액트 (React, JavaScript)를 이용한 예제로 설명을 해 봤습니다.

 

728x90

SOLID 원칙이란?

 

SOLID 원칙은 애플리케이션을 쉽게 재사용하고 (reusable), 관리를 하고 (maintainable), 확장을 하고 (scalable), 간단히 분해 할 수 있는 (loosely coupled) 다섯 가지 기본원칙으로 구성되어 있습니다.  이 다섯 가지 원칙은 다음과 같습니다:

 

  • Single-Responsibility Principle (단일 책임 원칙)
  • Open-Closed Principle (개방-폐쇄 원칙)
  • Liskov Substitution Principle (리스코프 치환 원칙)
  • Interface Segregation Principle (인터페이스 분리 원칙)
  • Dependency Inversion Principle (의존 역전 원칙)

 

각각의 원칙을 React의 예제를 이용하여 알아보겠습니다.  프레임워크나 개발 언어는 달라도 콘셉트는 같습니다.


Single-Responsibility Principle (단일 책임 원칙)

 

“하나의 모듈은 한 가지만 해야한다”
- Wikipedia


단일 책임 원칙은 하나의 컴포넌트는 하나의 목표와 책임만 있어야 한다는 뜻입니다.  특정하나의 목표가 있고 책임만 있게 되면 다른 것과 연결되어 꼬이는 문제가 없어지죠.  SRP 원칙을 따른다면 이해하기 쉽고, 정확하게 정리된 코드가 나오게 됩니다.  

 

아래의 예제를 보겠습니다.

// ❌ 나쁜 예제: 여러 책임이 정해진 컴포넌트

const Products = () => {
    return (
        <div className="products">
            {products.map((product) => (
                <div key={product?.id} className="product">
                    <h3>{product?.name}</h3>
                    <p>${product?.price}</p>
                </div>
            ))}
        </div>
    );
};

 

위의 예제에는 Products라는 컴포넌트가 여러가지 책임을 지면서 SRP 원칙을 무시하고 있습니다.  이 컴포넌트는 데이터를 가져와 map이라는 loop을 돌면서 UI를 렌더링 하고 있죠.  이렇게 만들어진 코드는 이해하고, 관리하고, 테스트하기가 어렵습니다.

 

대신에 아래와 같은 코드를 사용하여 SRP 원칙을 준수합니다:

// ✅ 좋은 예제: 작은 모듈을 이용하여 책임을 분산 시킴

import Product from './Product';
import products from '../../data/products.json';

const Products = () => {
    return (
        <div className="products">
            {products.map((product) => (
                <Product key={product?.id} product={product} />
            ))}
        </div>
    );
};

// Product.js
// Product를 렌더링만 하는 컴포넌트
const Product = ({ product }) => {
    return (
        <div className="product">
            <h3>{product?.name}</h3>
            <p>${product?.price}</p>
        </div>
    );
};

 

Product라는 컴포넌트는 Product의 UI 렌더링이라는 하나의 역할을 맡게 되고 Products는 데이터를 불러와 Product애 전달하는 하나의 역할로 나뉘어 지게 됩니다.  이렇게 나누게 되면 각 컴포넌트는 하나만 하게 되죠.  각 컴포넌트마다 코드가 더 간단하여 쉽게 이해하고, 테스트와 관리가 쉬워집니다.


Open-Closed Principle (개방-폐쇄 원칙)

 

 

“소프트웨어의 개체들은 (클래스, 모듈, 함수 등등) 확장에는 개방이 되어야 하고 수정에는 폐쇠되어야 한다”
- Wikipedia


개방-폐쇄 원칙에는 컴포넌트들이 (새 기능등의) 확장성에는 개방이 되어야하고 (기존 코드를 수정하는) 수정에는 폐쇄가 되어야 합니다.  이 원칙은 변화에 적응을 하고 모듈러화 되어 관리에 편하게 만들게 도와줍니다.

 

다음의 예제를 통하여 알아보겠습니다:

// ❌ 나쁜 예제: 개방-폐쇄 원칙을 무시

// Button.js
// Existing Button component (기존 컴포넌트)
const Button = ({ text, onClick }) => {
  return (
    <button onClick={onClick}>
      {text}
    </button>
  );
}


// Button.js
// 현존하는 버튼 컴포넌트를 아이콘 prop을 첨가하여 수정함 (수정된 컴포넌트)
const Button = ({ text, onClick, icon }) => {
  return (
    <button onClick={onClick}>
      <i className={icon} />
      <span>{text}</span>
    </button>
  );
}


// Home.js
// 👇 피해야 할 부분: 기존 컴포넌트 prop이 수정 됨
const Home = () => {
  const handleClick= () => {};

  return (
    <div>
      {/* ❌ 피해야 할 부분 */}
      <Button text="Submit" onClick={handleClick} icon="fas fa-arrow-right" /> 
    </div>
  );
}

 

위의 예제는 Button이라는 기존 컴포넌트에 icon이라는 prop을 넣어 수정을 하였습니다.  기존의 컴포넌트를 수정하여 새로운 기능을 첨가하는 것은 개방-폐쇄 원칙을 따르는 방법이 아닙니다.  이렇게 수정하는 방식은 컴포넌트가 차후에 어떤 방식으로 변할지 모르게 되고 예상하지 못하는 결과를 초래할 수도 있습니다.

 

대신에 아래의 방법을 사용합니다:

// ✅ 좋은 예제: 개방-폐쇄 원칙을 준수

// Button.js
// Existing Button functional component (기존 컴포넌트)
const Button = ({ text, onClick }) => {

  return (
    <button onClick={onClick}>
      {text}
    </button>
  );
}

// IconButton.js
// IconButton component
// ✅ 좋은 방법: 기존의 Button컴포넌트를 수정하지 않았습니다

const IconButton = ({ text, icon, onClick }) => {

  return (
    <button onClick={onClick}>
      <i className={icon} />
      <span>{text}</span>
    </button>
  );
}

const Home = () => {
  const handleClick = () => {
    // Handle button click event
  }

  return (
    <div>
      <Button text="Submit" onClick={handleClick} />
      <IconButton text="Submit" icon="fas fa-heart" onClick={handleClick} />
    </div>
  );
}

 

위의 예제는 IconButton이라는 다른 컴포넌트를 만들게 되었습니다.  이 컴포넌트는 icon이라는 prop을 이용하고 button이라는 HTML button element를 확장하였죠.  이 방법은 개방-폐쇄 원칙을 준수합니다.

 

 

반응형

Liskov Substitution Principle (리스코프 치환 원칙)

 

“서브타입 (자식)의 객체는 슈퍼타입(부모)의 객체의 역할을 대체할 수 있어야 한다”
- Wikipedia

 

Liskov Substitution Principle (리스코프 치환 원칙, LSP)는 객체지향 개발에서 객체 계층 간의 대체역할을 강조하는 원칙입니다.  리액트에서의 LSP 원칙은 파생된 컴포넌트가 베이스 컴포넌트의 역할을 할 수 있어야 한다는 의미입니다.  

 

다음의 예제를 통해 이해하여 보겠습니다:

// ⚠️ 나쁜예제
// 이 방법은 파생된 컴포넌트의 행동을 수정하고
// 베이스 컴포넌트로 대체되었을때 문제가 생길수 있으므로  
// 리스코프 치환 원칙을 무시합니다

const BadCustomSelect = ({ value, iconClassName, handleChange }) => {
  return (
    <div>
      <i className={iconClassName}></i>
      <select value={value} onChange={handleChange}>
        <options value={1}>One</options>
        <options value={2}>Two</options>
        <options value={3}>Three</options>
      </select>
    </div>
  );
};


const LiskovSubstitutionPrinciple = () => {
  const [value, setValue] = useState(1);

  const handleChange = (event) => {
    setValue(event.target.value);
  };


  return (
    <div>
      {/** ❌ 피해야 할 부분 */}
      {/** 아래의 커스텀 Select는 베이스 컴포넌트 select의 특성이 없음 */}
      <BadCustomSelect value={value} handleChange={handleChange} />
    </div>
  );

 

위의 예제에는 BadCustomSelect라는 컴포넌트가 커스텀 select input으로 사용되고 있습니다.  하지만 베이스 컴포넌트인 select input element의 특성이 없어서 리스코프 치환 원칙을 무시하고 있죠.

 

대신에 아래의 방법을 사용합니다:

// ✅ 좋은 예제
// 이 방법은 베이스 컴포넌트 select input과 대체될 수 있는 리스코프 치환 원칙을 따르고 있습니다.

const CustomSelect = ({ value, iconClassName, handleChange, ...props }) => {
  return (
    <div>
      <i className={iconClassName}></i>
      <select value={value} onChange={handleChange} {...props}>
        <options value={1}>One</options>
        <options value={2}>Two</options>
        <options value={3}>Three</options>
      </select>
    </div>
  );
};


const LiskovSubstitutionPrinciple = () => {
  const [value, setValue] = useState(1);

  const handleChange = (event) => {
    setValue(event.target.value);
  };

  return (
    <div>
      {/* ✅ 이 CustomSelect 컴포넌트는 리스코프 치환 원칙을 준수 합니다 */}
      <CustomSelect
        value={value}
        handleChange={handleChange}
        defaultValue={1}
      />
    </div>
  );
};

 

위의 수정된 코드는 CustomSelect라는 컴포넌트가 베이스 컴포넌트인 select input를 확장한 컴포넌트이죠.  value라는 props가 존재하고, iconClassName, handleChange, 그리고 마지막으로 남은 모든 props를 …props로 보내고 있습니다.  이 CstomSelect 컴포넌트는 모든 select의 특성이 계승이 되므로 리스코프 치환 원칙 (LSP)을 따르게 됩니다.


Interface Segregation Principle (인터페이스 분리 원칙)

 

“어떤 코드도 사용하지 않는 다른 메서드에 의존하면 안 된다”
- Wikipedia


Interface Segregation Principle (인터페이스 분리 원칙, ISP)은 인터페이스들은 클라이언트에 맞추어지게 개발이 되어야 하고 너무 방대해지거나 필요 없는 기능이 있으면 안 된다는 원칙입니다.

 

다음의 예제를 보겠습니다:

// ❌ 피해야 할 부분: 불필요한 정보를 노출하는 컴포넌트
// 이 코드는 불필요한 정보에 의존하여 코드가 더 복잡해 졌습니다

const ProductThumbnailURL = ({ product }) => {
  return (
    <div>
      <img src={product.imageURL} alt={product.name} />
    </div>
  );
};


// ❌ 나쁜 예제
const Product = ({ product }) => {
  return (
    <div>
      <ProductThumbnailURL product={product} />
      <h4>{product?.name}</h4>
      <p>{product?.description}</p>
      <p>{product?.price}</p>
    </div>
  );
};


const Products = () => {
  return (
    <div>
      {products.map((product) => (
        <Product key={product.id} product={product} />
      ))}
    </div>
  );
}

 

위의 예제에는 product라는 불필요한 정보를 ProductThumbnailURL이라는 컴포넌트에 전달하고 있습니다.  차후에 ProductThumbnailURL라는 컴포넌트를 분리하고 product가 아닌 다른 정보를 넣는다면 문제가 생기겠죠.  이 방법은 인터페이스 분리 원칙 (ISP)을 준수하고 있지 않습니다.

 

아래의 예제는 다시 ISP에 맞추어 리팩터링 된 코드입니다:

// ✅ 좋은 예제: 불필요한 dependency를 줄이면서
// 코드를 더욱더 관리과 확장하기 편하게 만들었음.

const ProductThumbnailURL = ({ imageURL, alt }) => {
  return (
    <div>
      <img src={imageURL} alt={alt} />
    </div>
  );
};


// ✅ 좋은 예제
const Product = ({ product }) => {
  return (
    <div>
      <ProductThumbnailURL imageURL={product.imageURL} alt={product.name} />
      <h4>{product?.name}</h4>
      <p>{product?.description}</p>
      <p>{product?.price}</p>
    </div>
  );
};


const Products = () => {
  return (
    <div>
      {products.map((product) => (
        <Product key={product.id} product={product} />
      ))}
    </div>
  );
};

 

위의 코드는 ProductThumbnailURL 컴포넌트가 product라는 모든 정보보다는 src와 alt에 필요한 정보만 받고 있습니다. 이렇게 되면 차후에 분리가 되어도 문제가 없어지죠.  이 방법은 인터페이스 분리 원칙 (ISP)을 준수하고 있습니다.


Dependency Inversion Principle (의존 역전 원칙)

 

“하나의 객체는 추상성이 낮은 클래스보다 추상성이 높은 클래스와 의존 관계를 맺어야 한다.”
- Wikipedia


의존 역전 원칙 (DIP)는 높은 위치의 컴포넌트는 낮은 위치의 컴포넌트에 의존하면 안 된다는 뜻이 담겨있습니다.  모듈화에 중점적인 의미가 있는 이 원칙은 관리의 효율성을 중요시합니다.

 

다음의 예제를 보겠습니다:

// ❌ 나쁜 예제
// 이 컴포넌트는 추상성을 배제하므로 
// 의존 역전 원칙 (DIP)에 어긋나는 코드입니다.

const BadCustomForm = ({ children }) => {
  const handleSubmit = () => {
    // submit operations
  };

  return <form onSubmit={handleSubmit}>{children}</form>;
};


const DependencyInversionPrinciple = () => {
  const [email, setEmail] = useState();

  const handleChange = (event) => {
    setEmail(event.target.value);
  };

  return (
    <div>
      {/** ❌ 피해야 할 부분: 타이트하게 연관성이 지어진 컴포넌트는 수정하기 힘들다 */}
      <BadCustomForm>
        <input
          type="email"
          value={email}
          onChange={handleChange}
          name="email"
        />
      </BadCustomForm>
    </div>
  );
};

 

BadCustomForm이라는 컴포넌트는 유연성이 없고 수정하기 힘들게 자식과 아주 타이트하게 연관이 되어 있습니다.  form submit의 이벤트가 정해져 있는 컴포넌트가 만들어지므로 추상성이 배제된 코드이죠.  분리가 되면 사용하지 못하는 컴포넌트가 되어 의존 역전 원칙 (DIP)을 준수하지 않는 코드가 됩니다.

 

이 상황에는 대신에 다음의 코드를 사용합니다:

// ✅ 좋은 예제
// 이 컴포넌트는 의존 역전 원칙 (DIP)을 준수합니다

const AbstractForm = ({ children, onSubmit }) => {
  const handleSubmit = (event) => {
    event.preventDefault();
    onSubmit();
  };

  return <form onSubmit={handleSubmit}>{children}</form>;
};


const DependencyInversionPrinciple = () => {
  const [email, setEmail] = useState();

  const handleChange = (event) => {
    setEmail(event.target.value);
  };

  const handleFormSubmit = () => {
    // submit business logic here
  };

  return (
    <div>
      {/** ✅ 추상성을 이용한다 */}
      <AbstractForm onSubmit={handleFormSubmit}>
        <input
          type="email"
          value={email}
          onChange={handleChange}
          name="email"
        />
        <button type="submit">Submit</button>
      </AbstractForm>
    </div>
  );
};

 

위의 코드는 AbstractForm 컴포넌트에 추상성을 이용하고 있습니다.  onSubmit이라는 함수를 prop으로 받고 form submission에 이용을 하죠.  이렇게 되면 onSubmit이라는 함수에 유연성이 생기고 더 높은의 레벨 컴포넌트를 수정하지 않고도 분리가 되어도 어떤 함수를 사용해도 되는 관리가 쉬워지는 컴포넌트가 만들어집니다.  


Conclusion

 

이 SOLID 원칙들은 개발자들에게 디자인이 잘되고 관리와 확장하기가 편한 코드를 만들어 줍니다.  이런 원칙들을 준수함으로써 모듈화, 코드 재사용, 유연성을 높이고 복잡성을 줄여줍니다.

 


유익하거나 즐거우셨다면 아래의 ❤️공감 버튼이나 구독 (SUBSCRIBE) 버튼을 눌러 주세요~ 

감사합니다

 

 

 

더보기

참고:

hackernoon.com

 

728x90
반응형

관련글 더보기