개요
1. SOLID 원칙이란?
2. 단일 책임 원칙 (SRP)
3. 개방 폐쇄 원칙 (OCP)
4. 리스코프 치환 원칙 (LSP)
5. 인터페이스 분리 원칙 (ISP)
6. 의존관계 역전 원칙 (DIP)
✅ SOLID 원칙이란?
소프트웨어 개발의 다섯가지 설계 원칙을 나타내는 약어로 각 원칙을 통해 소프트웨어의 재사용성, 유연성, 확장성을 높일 수 있으며 이는 쉬운 유지 보수를 가능하게 한다.
- 단일 책임 원칙 (SRP, Single Responsibility Principle)
- 개방 폐쇄 원칙 (OCP, Open Close Principle)
- 리스코프 치환 원칙 (LSP, Liskov Substitution Principle)
- 인터페이스 분리 원칙 (ISP, Interface Segregation Principle)
- 의존관계 역전 원칙 (DIP, Dependency Inversion Principle)
📦 콘웨이 법칙
SOLID 원칙의 대전제가 되는 법칙으로 "소프트웨어 구조는 해당 소프트웨어를 개발한 조직의 커뮤니케이션 구조를 닮게 된다." 라는 의미의 법칙으로 소프트웨어를 설계하는 것 또한 사람이기에 사람 간의 커뮤니케이션으로 소프트웨어 구조의 변경 사항이 전달이 된다. 그렇다면 이러한 변경 사항을 다른 컴포넌트에 영향이 가지 않도록 원하는 부분만 변경할 수 있어야 하는데 조직 간의 커뮤니케이션 방식과 소프트웨어 구조를 최대한 통일시켜 미스커뮤니케이션이나 의존성 이슈로 인해 변경이 힘든 구조를 없애고 응집도가 높고 결합도가 낮은 이상적인 컴포넌트의 집합 구조를 만들도록 해야 한다.
- 원칙은 지키면 좋은 것이고, 법칙은 의심할 여지가 없는 진리이기에 콘웨이 법칙을 이해하고 SOLID에 대해서 이해해보자
- 콘웨이 법칙에 입각해 소프트웨어 구조를 조직의 커뮤니케이션 구조와 유사하게 만들 수 있게 도와주는 원칙이 단일 책임 원칙이다.
✅ 단일 책임 원칙 (SRP, Single Responsibility Principle)
책임을 분리하여 어떠한 기능을 수정하였을 때 연관이 없는 기능에 영향이 가지 않도록 하는 것이 목적이다. 이러한 단일 책임 원칙을 지키며 컴포넌트를 설계한다는 것은 요구 사항을 전달하는 책무 단위로 설계한다는 것이다.
- 책임을 동작으로 단순히 바라본다면 컴포넌트를 과하게 쪼개는 행동으로 이어지며 이는 로직을 이해하기 힘들고 공수가 많아진다.
📦 책무 기반 컴포넌트 설계
소프트웨어는 요구 사항에 의해 만들어지며 이러한 요구 사항에 따라 책무(부서)가 나뉘며 컴포넌트 설계를 이러한 요구 사항을 전달하는 책무 단위로 설계하는 방식이다.
- 각 영역의 요구 사항을 명확히 파악하고 영역을 구분해 의존성 없는 독립적인 컴포넌트로 만들어 각 책무의 요구사항 변경에도 사이드이펙트(다른 컴포넌트에 영향을 주는 행위) 없이 유연하게 대처할 수 있도록 설계하고, 구현하는 것이 중요하다.
- 아키텍처 설계 시 명확하게 컴포넌트를 나눌 수 있는 공신력 있는 기준이 된다.
기획(UX) 책무
위와 같은 정보구조도가 있다면 기획 책무의 컴포넌트 설계가 굉장히 쉬워진다. 네이밍도 구조화된 상태 그대로 사용하면 되며, 각 컴포넌트별 단일한 책무를 가지게끔 만들기에 수월해진다. 이렇게 기획 단계에서 단위와 네이밍을 맞추며, 이 구조도를 토대로 상세 기획서를 작성하여 미스커뮤니케이션을 줄이고 각 컴포넌트별 요구 사항과 테스트 코드 작성 문제도 자연스럽게 해결이 된다. 이렇게 기획 책무 하나만 담당하는 컴포넌트를 만들 수 있다.
디자인(UI) 책무
디자인 시스템 구축을 통해 응집도 높은 UI 컴포넌트를 도출할 수 있다. 이러한 디자인 시스템을 구축한다면 개발팀에서 자체적으로 <CheckBox /> 와 같은 공통 컴포넌트를 만들지 않아도 되며, 특정 UI 컴포넌트 개발자가 누구인지 찾을 필요도 없으니 개발에 집중하기 용이해진다. 특히 아토믹 디자인을 통해 단일 책임 원칙을 만족할 수 있는 구조를 만들 수 있으며 Atom, Molecule, Organism, Template, Page 각각이 하나의 책무를 담당하도록 구조화 할 수 있다.
🔥 느낀점 (글쓴이의 생각임 정답이 아님)
단일 책임 원칙에 대해서 관심사 분리와 비슷한 개념으로 접근하는 것도 많이 보았다. 하지만 좀 차이점이 있다고 느껴지는 것이 단일 책임 원칙을 통해서 나눈 컴포넌트에 관심사 분리를 적용해서 좀 더 작은 단위로 나눌 수 있다고 생각이 된다. 단일 책임 원칙에서 말하고자 하는 점은 이러한 전체적인 소프트웨어 설계 단계에서 책무 단위로 나누어 컴포넌트를 구성하는 것에 있다는 생각이 든다. 크게 UI와 UX를 나누었다는 것뿐만아니라 설계 단계에서 구성된 정보구조도를 통해서 구조별 각 기능과 역할을 정의하고 그렇게 생겨난 하나의 구조가 단일 책임을 가지는 구조가 되는 것이다. 디자인 시스템은 UI라는 개념 자체에 대한 책임을 가지고 있다고 생각이 든다.
그렇기에 위와 같은 상황은 단순히 관심사 분리를 실행하였다고 느껴지며, 단일 책임 원칙에 의해 나눈 것은 ActiveUserList라는 컴포넌트 자체가 나뉘어진 것 같다
코드레벨에서의 단일 책임 원칙을 실행한 것이 관심사 분리라고 생각되기도 한다!!
✅ 개방 폐쇄 원칙 (OCP, Open Close Principle)
확장에는 개방적이어야 하고, 변경에는 폐쇄적이어야 한다. 라는 의미를 가진 원칙으로 쉽게 말해 어떠한 변경 사항에 대해서 코드를 추가해서(if문에서 if else를 집어 넣는 것) 문제를 해결하는 것이 아닌 공통적인 부분을 추려서 확장성 있는 코드를 만드는 것이다.
- 원하는 데이터를 추가 / 삭제했을 때 코드가 변할 일이 없게 한다는 느낌으로 데이터의 변화나 서비스 로직의 변경에도 유연하게 대처할 수 있는 구조를 가지는 것이 중요하다.
// data
[{
type: "BANNER",
items: [...]
},
{
type: "RECENTLY_VIEWED",
items: [...]
}]
// 구현 로직
sections.map((section) => {
if(section.type === "BANNER"){
return section.items.map((item) => <Banner item={item} />);
} else if(type === "RECENTLY_VIEWED"){
return section.items.map((item) => <PosterView item={item} />);
}
}
// OCP를 만족하는 코드
sections.map((section) =>
<Section section={section}>
{section.items.map((item) =>
<Item section={section} item={item} />
}
</Section>
주어진 데이터가 변화된다하더라도 OCP를 만족하는 코드는 유연하게 대처가 가능하며 코드를 변경하지 않고도 확장이 가능하다.
✅ 리스코프 치환 원칙 (LSP, Liskov Substitution Principle)
클래스의 상속을 통해서 주로 설명하는 원칙으로 사과는 과일이다와 같으 명확한 관계를 갖는 것이 중요하다는 내용이다. 즉 상속으로 이어진 관계에서는 예상 못할 행동을 하지말라는 내용이다.
- 코드레벨의 예시로는 ApiErrorBoundary라고 네이밍한 컴포넌트는 api 요청 중 발생하는 에러를 처리해야하는데 api와 상관 없는 사소한 에러처리를 이 컴포넌트에서 처리하게 될 경우에 매우 찾기 어려운 상황이 된다. 이러한 상황을 막고자하는 방식으로 리스코프 치한원칙이 사용되기도 한다.
- 위와 같은 예상치 못할 행위는 기술부채로써 남게 되는 경우가 많으며 이러한 문제를 해결 할수록 아키텍처 자체가 명확해진다.
✅ 인터페이스 분리 원칙 (ISP, Interface Segregation Principle)
사용하지 않는 메서드에 대해서는 의존적이지 않아야한다는 것으로 사용하지 않는 인터페이스는 구현하지 말아야 한다는 원칙이다.
- React에서 컴포넌트 간에 Props를 전달하게 되는데 필요로하는 Props만 받아야한다는 것
- 각 컴포넌트 간의 종속성을 줄여 독립적으로 유지 확장할 수 있게 한다.
✅ 의존 역전 원칙 (DIP, Dependency Inversion Principle)
컴포넌트에서 관심사에 맞지 않는 부분에 대한 의존성을 역전시켜 느슨하게 만들어주는 원칙으로 모듈 간의 결합도를 줄이고 유연성을 늘릴 수 있다.
- 인터페이스를 통해 상위 클래스가 하위 클래스에 대해 의존성을 가지는 것을 줄이는 것이 목적
- 서버로부터 API를 가져올 때 데이터를 가져오는 로직을 컴포넌트 안에서 직접 사용하는 것이 아닌 따로 분리하여 사용할 수 있고 한 컴포넌트에서 에러처리 및 너무 많은 정보를 가지고 있을 시 과한 의존성을 가지게 된다. 이러한 의존성을 역전시켜 컴포넌트 하나가 가지는 의존성을 약하게 하는 것이다.
function TicketInfoContainer(){
const { isLoading, data:ticketInfo, error } = useTicketInfoQuery();
const { data:commentInfo, error:commentApiError } = useCommentInfoQuery();
const { data:keywordInfo } = useKeywordInfoQuery();
if(isLoading){
return <Loading />
}
if(error){
return <Error />
}
return (
<TicketInfo
waitfreePeriod={ticketInfo.waitfreePeriod}
waitfreeChargedDate={ticketInfo.waitfreeChargedDate}
rentalTicketCount={ticketInfo.rentalTicketCount}
ownTicketCount={ticketInfo.ownTicketCount}
commentCount={commentInfo.commentCount}
commentError={commentApiError /* 코멘트 에러 발생 시 재시도 버튼 추가해야함 */}
keywordInfo={keywordInfo}
/>
)
}
// DIP 적용
function TicketInfoContainer() {
const [{waitfreePeriod, waitfreeChargedDate, rentalTicketCount, ownTicketCount}, TicketInfoFetcher] = useFetcher(useTicketInfoQuery);
const [{keywordInfo}, KeywordInfoFetcher] = useFetcher(useKeywordInfoQuery);
const [{commentCount}, CommentInfoFetcher] = useFetcher(useCommentInfoQuery);
return (
<TicketInfoFetcher>
<TicketInfo>
<TicketInfo.WaitfreeArea waitfreePeriod={waitfreePeriod} waitfreeChargedDate={waitfreeChargedDate} />
<TicketInfo.TicketArea rentalTicketCount={rentalTicketCount} ownTicketCount={ownTicketCount} />
<KeywordInfoFetcher />
<CommentInfoFetcher>
<TicketInfo.CommentArea commentCount={commentCount} keywordInfo={keywordInfo} />
</CommentInfoFetcher>
</TicketInfo>
</TicketInfoFetcher>
)
}
📌 Reference
'CS > 프로그래밍' 카테고리의 다른 글
MSA(마이크로서비스 아키텍처) (0) | 2024.03.23 |
---|---|
디자인 패턴 - 팩토리 패턴 / 전략 패턴 / 옵저버 패턴 (1) | 2024.03.22 |
디자인 패턴 - 싱글톤 패턴 (3) | 2024.02.28 |
[CS Study] 디자인 패턴 (0) | 2024.02.04 |
솔리드 원칙 (1) | 2024.01.20 |