[Common] Low Coupling & High Cohesion

seohyun Kang·2023년 2월 20일
0

React

목록 보기
5/9

Introduction

개발자로 처음 이직하던 시기에 뭣도 모르고 만능 컴포넌트 혹은 모듈을 고민하던 시간이 있었습니다. 이후, 다양한 프로젝트를 경험하면서 아키텍처를 공부하고 적용해보면서 이해하는 부분도 이해하지 못한 부분도 생겼는데 그 중 가장 난해한 부분이 결합도와 응집도 부분입니다. 기초적인 것처럼 보이면서도 상황에 맞춰 최적을 찾아갈 뿐 정답은 없어보이는 이 문제를 해결하기 위해 기록겸 블로그를 작성합니다.

결합도 (Coupling)

결합도는 모듈(클래스 파일)간의 상호 의존 정도 또는 연관된 관계의 끈끈함 정도를 의미한다고 보면 된다. (결합도를 의존도라고 부르기도 한다)

어떤 클래스가 다른 클래스에 대해 너무 자세히 알고 있다면 두 모듈은 높은 결합도를 가지게 되고, 반대로 어떤 클래스가 다른 클래스에 대해 꼭 필요한 지식만 가지고고 있다면 두 모듈은 낮은 결합도를 가진다고 말할 수 있는 것이다.

만약 클래스들 간에 연관이 있을때 인터페이스로 제대로 분리되어 있지 않고 불필요하게 많은 정보를 알고 있다면 이는 결합도가 높게 측정되게 된다.

결합도는 시스템 요소의 연결과 같은 의미이다. (여기서, 시스템 요소의 정의는?) 높은 결합도는 혼돈의 연결을 가르킨다. (어느 요소가 누구와 어떻게 연결되었는지 알 수가 없음, A 기능 수정시 그 외 요소에서 사이드 이펙트 발생 가능성이 높아짐) 결합도는 낮을 수록 좋다. 문제는 어떤 요소를 어느 정도 수준으로 연결을 해야하는가?

예를 들어 아래와 같은 두 컴포넌트가 존재할 때,

A Wrapper : React.FC <= Single Module

- Wrapper
	ㄴ Container (View Controller)
	ㄴ View1 (Header)
        ㄴ * Button
        ㄴ ...
    ㄴ View2 (Content)
    	ㄴ View2-1
			ㄴ ...
        ㄴ View2-2
        	ㄴ ...
        

B Wrapper : React.FC <= Single Module

- Wrapper
	ㄴ Container (View Controller)
	ㄴ View1 (Header)
    	ㄴ * Button
        ㄴ ...
    ㄴ View2 (Content)
    	ㄴ View2-1
        	ㄴ ...
        ㄴ View2-2
        	ㄴ ...
            
> * 공통 컴포넌트

A Wrapper - B Wrapper는 공통적으로 Button 컴포넌트를 사용하는데, A Wrapper에서 사용하는 기능의 형태가 B Wrapper에서 사용하는 형태와 다르면? (이 경우 공통 컴포넌트로 만드는게 맞는가?)


Button : React.FC = ({ 
  type,
  onChange,
  value,
  CTA를 위한 변수,
  GHOST를 위한 변수
}) => {

	switch(type) {
    	case "CTA" : 
        	return (
            	...
            )
        case "GHOST" : 
        	return (
            	...
            )
    }

}

실제로 한 코드에서 여러 종류의 버튼 형태를 커버하기 위해서 위와 같이 작업한 적이 있었고 GHOST라는 형태에 다른 기능이 들어가게 되어 GHOST_2, GHOST_3 타입이 추가되었다. (이러한 경우도 사실상 재사용성이 없는 것과 같은 결과를 내는 것 같다. 거기에 더해 같은 형태의 다른 기능을 하는 요소가 추가되어 개발간에 혼란만 가중하는 것 같다 - 예를 들어 GHOST인줄 알고 수정했는데 GHOST_2를 수정해야 했다던지...)

위와 같은 형태는 결국...

src/Components

ㄴ Button1 (CTA)
ㄴ Button2 (GHOST_1)
ㄴ Button3 (GHOST_2)

과 같다. 재사용성도 떨어지고 복잡도가 올라가는 것 같다. 여러 시스템들 중에서 각각의 시스템에 종속된(응집된) 구조화의 필요성이 생겼다.

응집도 (Cohesion)

응집도는 하나의 클래스가 기능에 집중하기 위한 모든 정보와 역할을 갖고 있어야 한다는 의미이다.

정확히 응집도는 한 모듈 내의 구성 요소 간의 밀접한 정도를 의미하는데, 한 모듈이 하나의 기능(책임)을 갖고있는 것은 응집도가 높은 것이고, 한 모듈이 여러 기능을 갖고 있는 것은 응집도가 낮은 것이다.

응집도가 높은 모듈은 하나의 모듈 안에 함수나 데이터와 같은 구성 요소들이 하나의 기능을 구현하기 위해 필요한 것들만 배치되어 있고 긴밀하게 협력한다. 반대로 응집도가 낮은 모듈은 모듈 내부에 서로 관련 없는 함수나 데이터들이 존재하거나 관련성이 적은 여러 기능들이 서로 다른 목적을 추구하며 산재해 있다.

응집도는 여러 요소들이 특정한 기준, 지표를 중심으로 묶인 정도를 지칭합니다. (아직 나는 이 기준, 지표를 알 수가 없다...) 낮은 응집도 = 혼돈을 말한다. (요소의 경계를 알 수가 없고 이 요소가 어디에 어떻게 어떠한 방법으로 영향을 주는지도 파악이 명확하지 않다)

Low Cohesion Exmaple

ㄴ View1
State1 : any;
State2 : any;

	ㄴ View1-1
	* Derived State1
    * Derived State2
    * Combined State1 (Derived State1-1 + Derived State2-1)
    
		ㄴ View1-1-1
        * Derived State1-1ㄴ
        * Derived State2-1
        
		ㄴ View1-1-2
        * Derived State1-1
        * Combined State1-1

좋은 예제인지는 모르겠지만, 이전에 개발을 하면서 위와 같이 개발을 한 경험이 있습니다. Root에서 선언한 State1 & State2라는 상태값을 View1-1에서 가공하여 Derived State1, Derived State2, Combined State1을 만들어 냅니다.

해당 값을 전달받은 View1-1-1 & View1-1-2는 그 값을 또 재가공하여 Derived State1-1 & Derived2-1, Derived State1-1 & Combined State1-1을 생성합니다.

이제 제가 State1, State2 혹은 Derived State 등을 바꾸려고 하면 예상치 못한 사이드 이펙트가 발생하게 됩니다.

예시

1. God Component (High Coupling & High Cohesion)

src/Components/Button.tsx

IProps {
    onClick? : () => void;
    name : string;
    type : "CTA" || "GHOST" || "GHOST_2" || "PLAIN';
    disabled : boolean;
}

const Button : React.FC<IProps> = ({ onClick, name, type, disabled }) => {
	const getButtonType = (type) => {
      switch(type) {
		case "CTA"
        	return (...)
            
        case "GHOST" 
        	return (...)
        ...etc
      }
    }
    
    const isGhost = type === "GHOST" || type === "GHOST_2";
	const isCTA = type === "CTA";
    const isPlain = type === "PLAIN";
    
	return (
    	<StyledButton className={getButtonType(type)} 
        	disabled={isGhost ? !isGhost : disabled }
        	onClick={() => {
           		if(isCTA) {
                	onClick();
                }
            }}
        >
        	{name}
        </StyledButton>
    )
}

God Component는 용어 그래도 모든걸 해결할 수 있는(있다고 착각하는) 컴포넌트입니다. God Component는 onClick이라는 동작을 수행하며 다양한 종류의 버튼 타입에 대응하기 위해 내부적으로 다양한 옵션이 정의되어 있어 Button과 View 사이의 결속도 높습니다.

이 때문에, 완벽하게 유사한 경우가 아니면 사용자는 Button을 재사용하기 위해 내부적으로 기능을 추가해야합니다. 만약 Button의 Function을 수정하는 경우 기존에 해당 기능을 사용하는 View에서 사이드 이펙트가 발생할 가능성이 높습니다.

2. Wrong Boundary (High Coupling & Low Cohesion)

src/Components/Button.tsx

IProps {
	type : "user" || "item";
    requestData : async () => void;
}

const List : React.FC = ({type}) => {
	const [fromData, setFromData] = useState<any[]>([]);
    const [data, setData] = useState<any>({
    	name : ""
    });
    
    const getData = useCallback(async () => {
	    const data = await requestData();
        if(type === "user") {
        	setFromData(data);
        } else {
        	setData(data);
        }
        
    },[type]);
    
    const sendName = async (name : string) => {
    	await fetch(name);
    }
    
    useEffect(() => {
    	getData();
    },[type])
    
   	const isUser = type === "user";
	
	return (
    	<div>
           {isUser && fromData.map(el => {
           	return (
            	{el}
           	)
           })}
           {!isUser && (
           <button onClick={() => {
           		sendName(data.name);
           }}>
           {data.name}
           </button>
           )}
		</div>
    )
}

Wrong Boundary의 경우를 예시 코드로 설명하자면, 사용자 목록을 보여주는 기능과 아이템 이름을 보여주고 서버로 전달하는 기능이 존재하여 해당 컴포넌트는 사용자 목록 호출과 아이템 이름을 노출하는 용도 외에는 사용할 수 없습니다.

해당 이슈는 결국 사용자 목록을 보여주는 기능과 아이템 이름과 관련된 기능을 분리하는 쪽으로 수정이 필요합니다. 다행히도 이러한 이슈는 발견하기 쉽고 분리도 비교적 쉽습니다.

3. Destructive & decoupling (Low Coupling & Low Cohesion)

src/Components/Button.tsx

IProps {
	name : string;
	onClick : () => void;
}

const View : React.FC = ({ name, onClick }) => {
	
    const A_Function  = () => {
    	derived();
        return ...;
    }
    
    const B_Function  = () => {
    	derived();
        return ...;
    }
    
    const Calculator = () => {
    	return ...;
    }
    
    const derived = () => {
	    const cal = Calculator();
        return ...
    }

	return (
    	<>
          <AComponent {...props}>
          <BComponent {...props}>
          <CComponent {...props}>
       </>
    )
}

가지고 있는 가장 큰 이슈 중 하나로 보통 View에서 많이 발생합니다. View가 너무나도 많은 기능과 변수를 한 번에 가지고 있습니다. 하나의 시스템 자체이다 보니 결합도는 작지만 너무많은 기능을 수행하여 응집도가 떨어지게 됩니다. 이러한 이슈의 큰 문제점은 코드를 파악할 수가 없습니다. 결국 이러한 이슈를 수정하기 위해서는 data를 Child 요소에게 전달하고 Hook을 사용하여 기능을 수행하는 Function을 분리하여 데이터, 함수 등을 분리할 필요가 있습니다.

4. Ideal (Low Coupling & High Cohesion)

interface IProps {
	header : any;
    body : any;
}

const useTableDataTransform = () => {
	const formatter = (key : string, el : any) => {
      seitch (key) {
          case "" : 
              return ...;

          default :
              return "";

		}
    }
    return {
    	formatter
    }
}

const Table : React.FC<IProps> = ({ header, body }) => {
	const { formatter } = useTableDataTransform();
	const headers = Object.keys(header);
    const newBody = headers.(el => formatter(el, body[el]));
    
    return (
    	<table>
        	<thead>
            	<tr>
                	{headers.map((el) => {
                    	const { name } = header[el];
                    	return (
                        	<th>{name}</th>
                        )
                    })
                </tr>
            </thead>
            <tbody>
            	<tr>
                	{headers.map((el) => {
                    	const item = newBody[el];
                        return (
                        	<td>
                            	{item}
                            </td>
                        )
                    })
                </tr>
            </tbody>	
        </table>
    )
}

Table Component는 header & body를 전달받습니다. 만약 body의 데이터를 재정의하는 함수가 Table안에 선언되어 있다면 header의 값이 추가될 때, 우리는 Table Component를 수정해야합니다. 하지만 useTableDataTransform에 body의 값을 재정의하는 함수가 있기 때문에 Table과 View사이의 의존성이 사라지게 됩니다. 또한, Body의 Key값이 필요없거나 기능이 필요할 경우 새로운 Hook을 적용하고 Table이라는 기능을 위한 응집도를 증가시킬 수 있습니다.

결과적으로...

사실 결합도와 응집도라는 개념이 아직 완벽하게 정리가 되지 않았고 사실 예시로 든 내용이 적절한 예시인지 모르겠지만, 해당 블로그를 작성하면서 어느정도 개념적인 접근 방법은 정리가 된 것 같습니다. 개발자로서 앞으로 할 일은 해당 내용을 바탕으로 개발에 적용하고 수정 사항이 있으면 기존에 작성된 내용을 정리하면서 스펙업을 해야하지 않을까 생각합니다.


Reference :
How to structure components in react
Coupling - Cohesion
[Coupling, Cohesion[(https://www.leafcats.com/68)

0개의 댓글