객체의 4대 특성과 개념을 인지함으로써, 객체 지향 프로그램을 작성할 수 있는 도구를 얻었다. 하지만, 아무리 좋은 도구를 가지고 있다고 해도 올바르게 사용하지 않으면 없느니만 못하다. 도구가 올바르게 사용하는 법이 있는 것처럼, 객체 지향의 특성을 올바르게 사용하는 방법, 즉 객체 지향 언어를 이용해 객체 지향 프로그램을 올바르게 설계해 나가는 방법이나 원칙이 있는데 바로 객체 지향 설계(OOD; Object Oriented Design)의 SOLID이다. SOLID는 아래 다섯가지 원칙의 앞머리 알파벳을 따 부른다.
- SRP(Single Responsibility Principle) : 단일 책임 원칙
- OCP(Open Closed Principle) : 개방 폐쇄 원칙
- LSP(Liskov Subsisution Principle) : 리스코프 치환 원칙
- ISP(Interface Segregation Principle) : 인터페이스 분리 원칙
- DIP(Dependcy Inversion Principle) : 의존 역전 원칙
이 다섯가지 원칙은 결국 응집도는 높이고(High COhesion), 결합도는 낮추라(Loose Coupling)는 고전 원칙을 객체 지향의 관점에서 재정립 한 것이다.
응집도 : 하나의 모듈 내부에 존재하는 구성 요소들의 기능적 관련성으로, 응집도가 높은 모듈은 하나의 책임에 집중하고 독립성이 높아져 재사용이나 기능의 수정, 유지보수가 용이하다.
결합도 : 모듈(클래스)간 상호 의존 정도로서 결합도가 낮으면 모듈 간의 상호 의존성이 줄어들어 객체의 재사용, 수정, 유지보수가 용이하다.
우선 첫번째로 S를 뜻하는 SRP(Single Responsibility Principle)에 대해 자세하게 정리해보자.
SRP
A class should have only one reason to change. 어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다. 변경의 이유가 단 하나여야 하는 이유는 무엇일까? 크게 두가지 이유로 생각해 볼 수 있다.
- A라는 책임과 B라는 책임을 갖고 있는 클래스가 있을 경우, A만 필요로 하는 애플리케이션을 불필요하게 B를 들고 다녀야 한다.
- 불필요한 변경 임팩트가 발생한다. A와 B 두가지 책임을 가진 클래스에서 A가 변경되었을 경우, B만 필요로 하는 애플리케이션은 불필요하게 다시 컴파일해야하고 리테스트 및 재배포까지 진행해야 한다.
그렇기 때문에, 클래스는 하나의 책임을 가져야 하는 것이다. 여기서 말하는 책임은 단순히 하나의 메소드를 말하는 것이 아니라, 사용자에 대한, 변경의 원천에 대한 것이라고 생각할 수 있다.
따라서 책임은 하나의 특정 액터를 위한 기능 집합이다.
잘못된 예시와 개선 사례 (1)
class dog {
final static Boolean male = true;
final static Boolean female = false;
Boolean sex;
...
void pee(){
if(this.sex == male){
//한쪽 다리를 들고 소변을 본다.
} else {
// 뒷 다리 두 개를 굽혀 앉은 자세로 소변을 본다.
}
}
}
이 예제를 보면, 강아지가 암컷이냐 수컷이냐에 따른 pee()행위가 분기처리 되는 것을 볼 수 있다. 여기서는 수컷 강아지의 행위와 암컷 강아지의 행위를 모두 구현하려고 하기에 단일 책임 원칙을 위배하고 있다.
메서드가 단일 책임 원칙을 지키지 않을 경우 나타나는 대표적인 냄새가 바로 분기 처리를 위한 if문이다. 이런경우 아래와 같이 리팩터링하여 개선할 수 있다.
class dog {
abstract void pee()
}
class maleDog extends dog{
void pee(){
//한쪽 다리를 들고 소변을 본다.
}
}
class femaleDog extends dog{
void pee(){
//뒷다리 두 개로 앉은 자세로 소변을 본다.
}
}
위와 같이 상속을 통해 적용하면, 암컷 강아지의 행위와 수컷 강아지의 행위가 각각 분리된 것을 알 수 있다.
잘못된 예시와 개선 사례 (2)
책과 각종 포스트를 통해 익힌 SRP를 곰곰히 곱씹어보며, 최근 업무를 하며 본 어떤 코드가 SRP를 적용하지 않은 것을 발견하였다(!)
public void getReview(){
if(){
//본인이 셀프리뷰 조회
} else if(){
//협업동료가 리뷰 및 동료리뷰 조회
} else {
//리더가 대상자의 리뷰, 협업동료 리뷰, 리더 리뷰 조회
}
}
구성원이 목표 항목을 수립하고, 해당 목표 완료시 셀프리뷰/동료리뷰/부서장리뷰를 조회하는 함수이다.
본인 / 협업동료 / 리더가 해당 목표 조회시 조회를 할 수 있는 범위도 다르고, 접근 가능/불가여부도 각각 다른데 이 함수에서 if
, else
로 조건만 걸어 계속 처리해준 것을 알 수 있다.
즉, 이 함수에서는 본인, 협업동료, 리더의 목표조회 책임을 한 함수 내에 처리했다. 아래와 같이 리팩토링하면 각각의 역할에 따른 비즈니스 로직에 더 집중할 수 있지 않을까?
public interface ReviewReader{
public void getReview();
}
class User implements ReviewReader{
@Override
public void getReview(){
//본인이 셀프리뷰 조회
}
}
class PeerUser implements ReviewReader{
@Override
public void getReview(){
//협업동료가 리뷰 및 동료리뷰 조회
}
}
class LeaderUser implement ReviewReader{
@Override
public void getReview(){
//리더가 대상자의 리뷰, 협업동료 리뷰, 리더 리뷰 조회
}
}
결론
위 코드를 보고 객체 지향의 4대 특성에 대해 생각해보면, 단일 책임 원칙과 가장 관계가 깊은 것은 바로 모델링 과정을 담당하는 추상화임을 알 수 있다. 단일 책임 원칙은 코드를 작성할 때 항상 고려해야 한다. 단일 책임 원칙은 클래스와 모듈 설계에 지대한 영향을 미치고, 이 원칙이 잘 지켜지면 의존성이 적고 가벼운, 결합도가 낮은 설계로 이어지기 때문이다.
참고 자료