오늘은 객체 설계의 원칙에 대해 말해보겠습니다.
이번 글에서는 용어 중 클래스와 객체를 그냥 객체로 통일하겠습니다.
엄밀하게 말하면 클래스는 소스 코드를 나타내고 객체는 실제 메모리에 올라가 있는 실체를 말하지만, 객체로 통일하여 표기하고자 합니다.
객체 설계의 원칙은 디자인 패턴을 기반으로 나온 원칙입니다. 디자인 패턴을 이해하는 것은 고단한 일이지만, 그래도 개발자라면 패턴을 숙지하는 것이 좋은 코드를 작성하는데 도움이 됩니다.
물론 디자인 패턴도 무분별하게 적용하다 보면 소스 코드가 복잡해지는 양면성을 가지고 있습니다.
한 가지 원칙을 따르다 보면 다른 원칙이 훼손이 되는 그런 양면성을 가진 원칙이기 때문입니다.
그렇기 때문에 디자인 패턴을 이해하기 위해서는 전체적인 관점을 유지하는 것이 매우 중요합니다.
제가 경험한 개발자 중 한 분은 자신의 소스 코드를 디자인 패턴을 사용해서 작성했는데, 이해하기 더 어려워지는 경우를 본 적이 있습니다.
그 개발자 분은 단순한 연산도 메서드로 분리하여 메서드가 하나의 역할을 하는 것은 맞는데 세부적인 구현도 전부 분리시켜 더 읽기 어려운 코드를 만들어 버렸죠.
개발자의 핵심 역량은 가독성이 좋고, 소스 코드의 양이 적으며, 중복이 거의 없고, 수정이 편리한 소스 코드를 작성하는 것이라 할 수 있습니다.
1. 단일 책임 원칙 (SRP, Single Responsibility Principle)
“하나의 객체는 하나의 책임만 가진다”
수정할 곳이 하나여야 한다는 말입니다. 책임을 나눈다는 말은 하나의 기능을 수행하기 위해 여러 객체를 거쳐야 한다는 말입니다.
즉, 수정이 한 곳이 이루어지지 않고 다수의 객체에서 이루어져야 한다면 잘못된 코드라는 말입니다.
그리고 이 원칙은 복잡성의 기준으로 봐야 합니다.
복잡성의 기준은 사람이 이해하기 쉬운 수준을 기준으로 합니다. 너무 잘게 쪼개는 것도 너무 크게 작성하는 것도 좋지 않습니다. 책임을 하나만 지운다고 해서 하나의 메서드에 모든 것을 다 작성하는 것도 옳지 않고, 정말 세부적인 내용까지 분리하는 것도 옳지 않습니다.
객체의 숫자가 증가한다고 복잡성이 비례해서 증가하는 것이 아닙니다. 어느 수준까지는 복잡성을 유지하다가 갑자기 증가하겠죠. 그렇기 때문에 적당한 수준으로 분리해야 합니다.
단일 책임 원칙이 잘 적용된 소스 코드는 변경사항이 발생했을 때, 가장 적은 수정으로 애플리케이션을 수정할 수 있고 전체 애플리케이션의 수정 영향도가 줄어들게 되어 있습니다.
2. 개방 폐쇄 원리(OCP, Open-Closed Principle)
"확장에는 개방되고 수정에는 닫힌다"
이 말은 기존 코드를 수정하지 않고 기능을 추가할 수 있어야 한다는 말입니다. 인터페이스를 다루는 설계 원칙인데, 어댑터 패턴을 설명하기 좋은 원리이다.
이 원리는 인터페이스는 처음 설계할 당시 이후에는 변경을 허용해서는 안 된다는 원칙입니다.
인터페이스를 식별할 때 수정 여부를 꼭 고려해야 한다는 말입니다.
OPC를 구현하기 위해서는 DI(Dependency Injection), IoC(Inversion Of Container)가 필요합니다. Java의 Spring 프레임워크의 사상이죠.
2. 리스코프 치환 원칙 (LSP, Liskov Subsitution Principle)
"상속을 받아 구현된 객체는 언제든지 같은 부모를 상속받는 다른 객체로 교체할 수 있어야 한다"
다형성과 확장성을 극대화하기 위한 원칙입니다. 여기서는 상속보다는 인터페이스를 사용하는 것을 추천합니다.
이것이 불가능하다면 상속 설계를 잘못했다는 뜻입니다. 부모와 자식 간의 관계가 명확하지 않고 설계 상 오류가 있다는 뜻으로 객체 간의 관계를 재 정의해보세요.
변경 사항이 발생했을 때 기존의 객체를 그대로 두고, 하위 객체를 통해 수정이 가능해야 한다는 말입니다.
4. 인터페이스 분리 원칙 (ISP, Interface Segragation Principle)
"객체는 자신이 호출하지 않은 메서드에 의존하지 않아야 한다"
인터페이스를 설계할 때 구현하는 객체에서 사용하지 않는 메서드가 있다면 인터페이스를 다시 설계하라는 말입니다.
인터페이스를 나눔으로써 결합도를 낮춥니다.
구현할 객체에게 무의미한 메서드의 구현을 방지하기 위해 반드시 필요한 메서드만을 상속/구현하도록 합니다. 만약 상속할 객체의 규모가 너무 크다면, 해당 객체의 메서드를 작은 인터페이스로 나누는 것이 좋습니다.
다시 말하면 상속을 구현하는 객체에서 사용하지 않는 메서드가 있다면 설계를 잘못했다는 말이고, 그런 경우 인터페이스를 세부적으로 나누어 구현하는 것이 좋다는 말입니다.
5. 의존관계 역전 원칙 (DIP, Dependency Inversion Principle)
“상위 계층이 하위 계층에 의존해서는 안 된다.”
무분별한 의존 관계를 맺는 것을 방지하기 위해 의존 관계를 맺을 때 구현 독립성을 가져야 한다는 말입니다. 즉, 직접적인 상속보다는 추상 클래스나 인터페이스와 의존 관계를 맺는 것이 좋다입니다.
의존성 역전 원칙은 코드의 확장성 및 재사용성을 추구하기 위한 원칙입니다. 경직된 객체보다 구현되지 않아 유연한 인터페이스가 더욱 확장 가능성이 높은 것입니다.
마무리하며
오늘은 객체 설계의 원칙을 알아봤습니다.
하지만 이러한 원칙을 개념적으로 이해한다고 해서 좋은 소스 코드가 바로 나오지 않습니다. 좋은 설계는 많은 경험에서 나옵니다.
경험이 부족한 상태에서 무리하게 원칙을 적용하다 보면 더 많은 소스 코드를 작성해야 하는 생산성이 떨어지는 코드를 설계하게 되겠지요.
원칙은 원칙일 뿐 개발자는 좋은 코드를 작성하는 것도 중요하지만 생산성을 어느 정도 가지고 갈 수 있는 코드를 작성하는 것도 중요합니다.
'IT > 아키텍처' 카테고리의 다른 글
결합도(Coupling)과 응집도(Cohesion) (0) | 2022.07.13 |
---|---|
Bad smells in code(마틴 파울러의 코드의 악취) (0) | 2022.06.24 |
리팩터링(Refactoring) (0) | 2022.06.24 |
네이밍 룰 - 메소드 [2편, 단어] (0) | 2022.05.26 |
네이밍 룰 - 메서드 [1편, 기본편] (0) | 2022.05.26 |
댓글