- 클린아키텍처 - (좋은벽돌까지만이라도 만들어보자) 로버트C.마틴2024년 10월 14일
- 2료일
- 작성자
- 2024.10.14.:51
내 기억상으로는 작년 정확히 이맘때 이 책을 읽었던 경험이 있다. 이번에 객체지향에 대해 생각할 기회가 생겨 어떤 책을 읽을까 고민하다 다시 돌아왔다. 그 이유로는 객체지향 또한 아키텍처이다. 왜 우리는 아키텍처 즉 설계에 신경을 쓰고 개발을 진행해야할까?를 먼저 고민하며 정리하는게 순서가 맞다는 생각이 들었다. 당연히 내가 글을 쓰기에 사견이 들어간다. 양이 많기에 먼저 로버트선생님이 말씀하신 벽돌까지만 만들고 그 후는 다음 글에서 쓸 예정이다.
1. 설계와 아키텍처란?
나: 데드라인이 코앞이다. 그러면 개발자인 나는 일단 먼저 찍어내고 추후에 리팩토링해야겠다를 머리속에 가진 사람이다. 하지만 이 책에서는 나와 같은 사람을 꾸짖고 있다. 이렇게 되면 결국 생산성의 결과로 보았을때는 더 안좋아진다고 한다.
그 이유로는 회사에 가서 그 데드라인에 맞춰 어떠한 개발을 다 끝냈으면 그 다음엔 리팩기간이 없다. 바로 새 기능요구가 들어온다. 그렇게 되면 악코드+ 악코드 + 악악악....😫😫😫
악들이 쌓인다. 결국 처음부터 깔끔한 아키텍쳐를 통해 설계를 진행한다면 효율성을 증가시킬수 있다.
2. 기능 VS 아키텍처
소프트웨어는 이름 그대로 Soft = 부드러워야한다. 즉 하드웨어인 기계를 부드럽게 변경하는 역할이다. 그렇다면 동작하도록 만드는게 중요할까 쉽게 변경가능하게 만드는 게 중요할까? 뭐 물론 둘다겠지만 정답은 쉽게 변경가능한 것이다. 그 이유로는 변경하는 것에도 인력이 들어가니 비용이 들어가는데 동작은 하지만 바꾸는데 cost가 많이 든다면 방치할수밖에 없다. 사실 요즘에는 더더욱 동의가 되는 말이다. 동작은 GPT 같은 AI를 활용하면 어떻게든 굴러가게는 만들 수 있다. 하지만 우리는 한가지 기능을 개발하는 것이 아니고 전체의 의존성관리 등 사이즈가 있는 것을 개발한다. 이를 변경하기 쉽게 만들어주는 것이 곧 그대로 동작이 안될때 되도록 고치거나 하는것에 훨씬 용이하다.
정답은 : 아키텍처
아키텍처를 위해 투쟁해라!!!!!! 개발팀들이여!!!ㅋㅋㅋ
3. 객체지향 프로그래밍
- 제어흐름의 간접적인 전환에 대해 규칙을 부과하는 패러다임
객체? class나 struct로 이루어져 있으며 메소드와 프로퍼티의 결합체의 인스턴스
제공해야할 기능을 찾거나 세분화하여 그 기능을 알맞은 객체에 할당하는 프로그래밍 방법이다.
캡슐화, 상속, 다형성을 기반으로 설명해야한다.
- 캡슐화: 프로퍼티와 함수가 집단내에서 데이터는 구분선 바깥에서 은닉되고, 일부 함수만 외부에 노출됨.
- 하지만 OO가 아닌 다른언어에서도 ㄱㄴ 오히려 OO에서 깨진다. 실제로 많은 OO 언어가 캡슐화 강제 X, 그래서 결론은 단지 프로그래머가 캡슐화된 데이터를 우회해서 사용하지 않을 거라는 믿음을 기반으로 한다고 한다.
- 상속: 뭐 OO가 나오기 전에도 구현으로 가능했지만 OO로 더 나은 상속을 제공함.
- 다형성: 함수를 가리키는 포인터를 응용한 것, 새롭게 나온게 X.
- But) 함수 포인터에 대한 직접적인 사용 없애주고 실수위험도를 낮춰 더 안전하게 사용 ㄱㄴ
4. 함수형 프로그래밍
- 할당문에 대해 규칙 부과하는 패러다임
5. SOLID
그러면 어떻게 좋은 아키텍처를 정의할수 있을까? 바로 SOLID원칙이다. 여기서 설명하기를 SOLID는 객체지향에만 적용된다는 뜻은 아니고 1. 변경에 유연하고, 2. 이해하기 쉽고, 3. 많은 소프트웨어 시스템에 사용될 수 있는 컴포넌트의 기반이 되는 것에 목적이 있다고 한다.
SRP: 단일 책임 원칙
응?? 모듈은 하나의 일만 해야한다? ㄴㄴ 그것은 함수이다. 좋은 함수는 하나의 기능만 하는 것.
여기서 말한 단일 책임은 변경의 이유가 한개뿐이어야 한다는 것이다. 즉 하나의 모듈은 하나의 오직 하나의 액터에 대해서만 책임져야한다.
이렇게 말하면 못알아먹으니 친절하게 위반하는 징후들을 보여준다.우발적 중복:
하나의 객체에서 다양한 액터들의 처리를 함.
ex) 기업이라는 클래스에서 calculatePay()메서드는 회계팀에서 기능을 정의하며 보고를 위해 사용. save()메서드는 디비관리자가 기능을 정의하고 보고를 위해 사용. 이렇게 되면 두 회계팀과 디비관리자는 서로 결합되어 버렸다. 즉 디비관리자가 뭘 바꾸면 회계팀에도 영향을 줄 수있다. 뭐 월급이라는 메서드가 있다고 하자. 그런데 회계팀에서 디비관리자와 독립적으로 월급을 선정하는 알고리즘이 다르다고 하자. 하지만 디비관리자는 변경을 원하지 않을때조차 알지 못한다.
병합:
하나의 기업 클래스를 디비관리자와 회계팀에서 변경사항을 적용한다면 어떻게 될까 당연히 충돌이 일어나겟지.
결국 액터를 뒷받침 하는 코드를 분리해야한다.클래스를 두개로 분리함으로써 서로의 존재를 모르게해 우연한 중복을 피할수 있다.
OCP: 개방-폐쇄 원칙
확장에는 열려있고 변경에는 닫혀있어야한다. 즉 요구사항을 살짝 확장하는데 많이 수정해야한다면 그건 실패한 아키텍처이다.
class Tools { var name: String init(name: String) {} func getThisName() {} } class TapTools { func tap(with tool: Tools) { if tool.name == "hammer" { print("hammer") } } } func main() { let hammer = Tools(name: "hammer") let taptools = TapTools() taptools.tap(with: hammer) }
요 코드에서 만약 해머 말고 못이나 다른 것을 추가해주려면 우리는 그때마다 분기처리를 통해 코드를 수정해야한다.
만약 여기서 프로토콜을통해 추상화에 의존하도록 코드를 수정하면 어떻게 될까?
protocol Tools { func tap() } class Hammer: Tools { func tap() {print("깡깡")} } class Nails: Tools { func tap() {print("아얏")} }
이렇게 되면 확장에는 열려있고 변경에는 닫혀있도록 수정할 수 있다.
추가 궁금증 navigationPath는 enum 타입으로 관리한다. 그런데 path가 추가되면 navigationDestination에 추가를 해주어야 한다.
이는 OCP를 위배한다.
LSP: 리스코프 치환 원칙
자식 타입은 부모타입으로 교체할 수 있어야한다.
위반 케이스: 정사각형/직사각형 문제
- Square은 Rectangle의 하위타입으로 적합하지 않은 이유는 가로세로 독립적으로 변경가능하지만 Sqaure는 반드시 함께이기에
- 이를 막는 방법은 User에서 Rectangle이 실제로 Square타입인지 검사하는 매커니즘 추가 But 의존하게 되며 타입치환 불가.
근데 왜 이게 문제지..? 걍 자식타입의 기능만 잘 알고 사용하면 되는거아닌가? 하지만 추상화라는 개념은 우리가 해당 객체의 정체를 몰라야한다. Rectangle인지 자식인지 모르고 그냥 다 Rectangle로 판단해야한다는 것이다. 그래서 위반이 생긴다.
이것또한 프로토콜을 각각 상속하도록 하면 문제가 해결한다.
ISP: 인터페이스 segregation Principle)
인터페이스를 나눠라. 즉 프로토콜을 각각의 기능이 모두 필요할때 채택해라 그게 아니면 프로토콜을 쪼개라.
DIP: 의존성 역전 법칙
드뎌 마지막이다. "의존성이 극대화된 시스템"? : 추상에 의존하며 구체에는 의존하지 않는 시스템.
하지만 현실적으로 규칙이라하기에는 비현싫적 ex) String Class는 swift의 기본제공 클래스. 안정적이다. DIP를 적용할 필요가 없다. 만약 하시도한다면 불필요한 복잡도를 높이기만 한다.
상위 레벨의 모듈은 하위 레벨의 모듈에 의존해서는 안되고 둘 모두 추상화 수준에 의존해야 한다.
결국 인터페이스로 안정된 추상화가되는데. 이는 인터페이스(프로토콜)에 변경이 생기면 구현체들도 수정해야하지만, 구현체들에 변경이 생기더라도 인터페이스는 영향X => 인터페이스는 구현체보다 변동성이 낮다. 그리고 낮추려고 노력해야한다.
빨리 가는 유일한 방법은 제대로 가는 것이다. - 로버트 C.마틴
자 여기까지 좋은 벽돌을 골랐고 이제는 건물을 세워볼 단계이다.
다음글이전글이전 글이 없습니다.댓글