Java 하면 떠오르는 단어는 객체지향입니다.
객체지향은 시간이 지나도 유지 보수와 확장이 용이하다는 장점(캡.상.추.다 특성)이 있어서 큰 규모의 비즈니스 로직을 설계 할 때 많이 사용됩니다.
그렇다면 어떻게 하면 객체지향적으로 프로그래밍 할 수 있을까요?
2000년대 초반 '로버트 마틴' 이 SOLID라는 약어로 명명한 객체지향 설계 5원칙을 살펴보겠습니다.
(객체 지향의 장점을 사용하여 설계하는 방법 느낌쓰~입니다)
하나의 모듈(클래스, 속성, 메서드 등)은 하나의 책임과 역할만 갖도록 해야한다.
[예시]
남자라는 클래스로 만들고 아빠, 축구 선수, 직장 상사의 책임과 역할을 모두 갖도록 설계했습니다.
class Man {
String housePosition = "father"; // 아빠
int backNumber = 7; // 축구 선수 등번호
String rank = "boss"; // 직장 상사
void babySit() {
System.out.print("아기 돌보기");
}
void soccer() {
System.out.print("축구 하기");
}
void work() {
System.out.print("업무 하기");
}
}
만약, 축구 선수를 은퇴한다면?? 남자라는 클래스에서 모든 부분을 들여다보며 축구 선수와 관련된 코드들을 지우고 수정해야 하겠죠?? 유지보수가 힘드네요😭
여기에 SRP를 적용해보겠습니다.
// 아빠
class Father {
String housePosition = "father"; // 집 구성원: 아빠
void babySit() {
System.out.print("아기 돌보기");
}
}
// 축구 선수
class SoccerPlayer {
int backNumber = 7; // 등번호: 7번
void soccer() {
System.out.print("축구 하기");
}
}
// 직장 상사
class Boss {
String rank = "boss"; // 직급: 사장
void work() {
System.out.print("업무 하기");
}
}
남자를 아빠, 축구 선수, 직장 상사에 맞춰 하나의 역할만 수행하도록 분리하는 것이 SRP 입니다.
이렇게 되면 축구 선수를 은퇴할 때, 축구 선수 클래스만 수정하고 다른 코드들은 쳐다볼 필요도 없겠죠? 😁
[장점]
이렇듯 SRP를 지켜 설계하면, 변경이 필요할 때 수정할 대상이 명확해집니다.
그리고 시스템이 커질수록 장점은 더욱 극대화될 것입니다.
자신의 확장에는 열려 있고, 주변의 변화에는 닫혀 있어야 한다.
(새로 추가되는 코드 혹은 수정되는 코드는 기존 코드의 변경을 초래하지 않아야 한다는 의미)
[예시]
이렇게 OCP가 적용된다면, 다양한 DB에 대한 변경에는 확장적이지만 코드 수정에는 폐쇄적(상위 모듈을 수정할 필요가 없음)인 것을 알 수 있습니다.
(상속 / 인터페이스의 사용과 관련, OCP를 지키려면 추상화, 다형성 사용)
반대로 OCP를 어기면, if-else문 switch문이 남발되며 스파게티 코드가 될 것 입니다.🥲
[장점]
이렇듯 OCP를 지켜 설계하면, 유연성, 사용성, 유지보수성을 얻을 수 있습니다.
상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.
[예시]
유명한 예제로 직사각형 - 정사각형 예제가 있습니다. (정사각형은 높이/길이가 모두 같으므로 setH(), setW() 모두 동일하게 세팅됩니다.)
정사각형이 직사각형을 상속받은 경우 리스코프 치환 원칙에 따라 정사각형은 직사각형을 대표해야 하므로, 정사각형을 통해 길이 = 5, 높이 = 10 으로 설정한다면 직사각형의 면적과 동일하게 50이 나와야 합니다.
하지만 마지막에 설정된 길이 10에 의해서, 10 * 10 = 100이 결과로 나옵니다.
이 예제는 사실 직사각형과 정사각형은 서로 상속관계가 전혀 될 수 없습니다.
사각형의 특징을 서로 갖고있긴 하지만, 두 사각형 모두 사각형의 한 종류일 뿐으로, 하나가 다른 하나를 완전히 포함하지 못 하는 구조입니다.
여기에 잘못 적용된 LSP를 풀어보겠습니다.
사각형 인터페이스를 만들고, 직사각형과 정사각형은 사각형 인터페이스를 구현하여 사용하면 됩니다.
이로써, 직사각형과 정사각형은 서로 상속관계가 아니므로, 리스코프 치환 원칙의 영향에서 벗어났습니다.
결국 LSP는 객체 지향의 상속이라는 특성을 올바르게 활용하면 자연스럽게 얻게 되는 것입니다.
[장점]
에러가 없는데 예측가 다르게 동작하는 코드는 디버깅하는 입장에서 정말 난감합니다..
LSP를 잘 지켜서 설계한다면 논리에 맞고, 예측가능한 코드를 작성할 수 있습니다.
자신이 사용하지 않는 메서드와 의존 관계를 맺지 않는다.
(클라이언트 입장에서 사용하는 기능만 제공하도록 인터페이스를 분리해야 한다.)
[예시]
복합기 객체는 충분히 높은 응집도의 작은 단위로 설계되었지만, 목적과 관심이 각기 다른 클라이언트가 있다면 인터페이스를 통해 적절하게 분리해줄 필요가 있습니다.
여기서 ISP를 적용해보겠습니다.
복사, 프린터, 팩스 각각의 인터페이스를 만들고 원하는 기능만을 골라서 복합기를 만들면 기능 유지보수가 용이해집니다.
[장점]
이렇게 분리해주면, 인터페이스가 명확해지고, 변경에 대한 영향을 더욱 세밀하게 제어할 수 있습니다.
고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 된다.
(저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.)
(자신보다 변하기 쉬운 것에 의존하지 마라)
[예시]
이렇게 클래스를 설계하면, Zoo에 동물들이 추가될 때 마다 Zoo에서 뻗어나가는 화살표가 많아지고 이는 곧 Zoo에서 수정해야 할 코드들이 늘어남을 의미합니다.
여기서 DIP를 적용해보겠습니다.
이렇게 설계되면, High level 클래스인 Zoo에서 Low level 클래스인 동물들에 대한 직접적인 의존을 하지 않기 때문에 Zoo에 대한 코드 수정이 필요 없습니다.
결국, Low level이 계속 확장 되더라도, High level은 독립적이며 추가적인 수정이 필요하지 않습니다.
(객체간의 결합도을 느슨하게 해줍니다.)
(이를 통해 다양한 설계, 복잡한 시스템 설계가 가능하게 됩니다.)
[장점]
코드 변경을 최소화할 수 있습니다.
코드의 유연성과 확장성을 향상시킬 수 있고 이는 생산성 향상으로도 이어집니다.
그 밖에도 재사용성 증가, 테스트 용이 등등 여러 가지 장점들이 존재합니다.
위의 5가지 원칙을 지키면서 프로그램을 작성하기는 생각보다 어렵습니다..
(특히, 리스코프 치환 원칙은 어렵게 느껴집니다..)
하지만 최대한 지키며 작성을 하게 된다면, 그 프로그램은 이해하기 쉽고 유지 보수가 용이할 가능성이 높아집니다.
"공든 탑이 무너지랴" 라는 명언이 떠오르는 주제였습니다.