on
[OOP] SOLID
SOLID
‘로버트 마틴’이 이야기한 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙이다.
Single Responsibility Principle
단일책임의 원칙
“하나의 클래스는 하나의 책임만 가져야 한다”
클래스 하나에 한가지 기능만 사용 하라는 것이다. 한 클래스에 데이터베이스에 접근하는 책임과, 네트워크를 요청하는 책임, 각종 기능을 한번에 작성했다고 가정을 해보자. 데이터 베이스에 접근하는 부분이 변경 되서 코드를 수정하게 되었다. 한 클래스에서 작성했다보니 데이터 베이스에 접근하는 부분을 변경을 했지만 나도 모르게 클래스 내의 다른 책임에 영향을 줄 수도 있다. 또한 코드를 작성한 개발자라 하더라도 1년 뒤에 그 코드를 봤을 때 자신의 코드를 빨리 이해할 수 있을까? 이 메소드가 네트워크를 위해서 존재하는 메소드인지, 어떤 이유에서 있는 함수인지 모호해 질 수 있다.
1년 후에 코드를 봐도 문제가 없다고 생각을 한다고 한다면, 개발한 개발자가 퇴사를 한 후 다른 개발자가 유지보수를 한다면 그 코드를 쉽게 이해할 수 있을까 ? 가독성에 관하여 생각해 봐야 한다. 책임 또는 기능에 따라서 클래스를 명확하게 분리한다면 어떤 일이 일어날까?
아주 쉬운 이야기로 UserDao.class 라는 클래스를 만들었다고 생각을 해보자. 클래스 내부의 코드를 보지 않아도 Dao(Data Access Object) 개념만 안다면 이 클래스가 어떤 기능을 할지 아는 개발자가 더 많을 것이다. 클래스에 한 가지 기능만을 담기만 해도 얼마나 프로그램의 가독성이 올라갈 지 짐작할 수 있다.
또한 DataBase의 User Table이 변경이 되어서 코드를 수정해야하는 상황이 온다면 이 경우 UserDao.class를 수정하면 된다. 유지보수도 얼마나 간편해 지는가! 하지만 너무 극단적인 사례일 수도 있지만 안드로이드라고 가정을 해본다면 MainActivity 에 모든 기능이 절차지향으로 구현이 되있다면 Ctrl+F로 ‘user’를 검색하는 일까지 벌어질 수 있다.
Open/closed principle
개방 / 폐쇠의 법칙
“확장에는 열려있어야하고, 변경에는 닫혀있어야 한다”
기존의 코드를 수정하지 않고, 기능을 확장 할 수 있어야 한다. 확장이 되는 부분을 추상화 해서 설계를 하라는 의미이다. 무슨 말 인지 이해가 잘 안될 수도 있다. 예를 들어보면 이해가 조금 더 빠를 것이다.
class Piano{
public void play(){
System.out.println("피아노 연주");
}
public void stop(){
System.out.println("피아노 연주 중지");
}
}
public class ClassicMusic {
public void musicStart(Piano piano){
piano.play();
}
public void musicEnd(Piano piano){
piano.stop();
}
}
ClassicMusic(기존의 코드로 이해) 이라는 클래스가 있고 이 클래스는 musicStart, musicEnd 메소드를 통해서 피아노를 연주를 시작하고, 중지할 수 있다. 하지만 클래식 음악을 피아노에서 바이올린으로 바꾸고 싶다.
class Violin{
public void play(){
System.out.println("바이올린 연주");
}
public void stop(){
System.out.println("피아노 연주 중지");
}
}
public class ClassicMusic {
public void musicStart(Violin violin){
violin.play();
}
public void musicEnd(Violin violin){
violin.stop();
}
}
그래서 바이올린 클래스를 만들고, 기존의 코드를 수정하였다. 어떤 생각이 드는가? 아무 생각이 없다면 조금 더 생각해 볼 시간을 가져보는 것도 좋다. 기존의 ClassicMusic의 코드 중 몇 군대를 수정 했는지 벌써부터 count 하기도 싫어진다. 기능을 확장할 때마다 이런 수고를 감수할 것인가? 지금은 코드가 간단해서 그렇지만 클래스가 조금만 복잡하다면 이 코드를 유지보수 할수 있겠는가? 추상화는 어떨때 사용하는 것인가? ClassicMusic의 메소드들이 Piano, Violin 과같은 구체적인 클래스에 의존하지 않고, 추상화 된 클래스에 의존한다면 어떻게 될 것인가?
public class ClassicMusic {
public void musicStart(Instrument instrument){
instrument.play();
}
public void musicEnd(Instrument instrument){
instrument.stop();
}
}
interface Instrument{
void play();
void stop();
}
이렇게 인터페이스를 만들고 ClassicMusic의 코드를 변경한다.
class Piano implements Instrument{
public void play(){
System.out.println("피아노 연주");
}
public void stop(){
System.out.println("피아노 연주 중지");
}
}
class Violin implements Instrument{
public void play(){
System.out.println("바이올린 연주");
}
public void stop(){
System.out.println("피아노 연주 중지");
}
}
Instrument 라는 인터페이스를 통해서 추상화를 하고, ClassicMusic 에서는 추상화 된 Instrument 에 의존하는 메소드를 만든다. 그렇게 되면 Instrument가 피아노, 바이올린, 첼로 등등 무엇으로 확장이 되어도 기존의 코드를 수정할 일이 없기 때문이다.
Liskov substitution principle
리스코프 치환의 원칙
“서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다”
프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다. 서브 타입은 기반 타입이 약속한 규약을 지켜야 합니다.
- 다른 기능을 수행 ex) 추가 메소드 이지만 삭제 수행
- 다른 추가적인 예외를 발생 ex) RuntimeException 으로 명세되어 있는데HttpClientException 이 발생
- 다른 리턴값을 리턴하면 안될 것이다.
Interface segregation principle
인터페이스 분리 원칙
“특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다”
범용 인터페이스를 사용해서 하나의 클래스에 필요없는 메소드 까지 구현해야 한다면 잘못된 것이다. 더 인터페이스를 세분화 할 필요가 있다는 것이다.
휴대용 가습기 클래스를 만들려고 한다. 여기서 전자제품 이라는 범용 인터페이스를 구현하는 것 보다는 가습기, 휴대용 인터페이스를 구현하는 편이 낫다는 이야기이다. 전자제품이라는 인터페이스를 구현한다면 필요없는 기능까지 구현하게 될 것이다. 이 법칙은 따로 예제 코드를 통해서 이야기 하지 않아도 이해를 했을 것이라 생각 한다.
Dependency inversion principle
의존관계 역전의 법칙
프로그래머는 “추상화에 의존해야지, 구체화에 의존하면 안된다.”
고 수준 모듈은 저 수준 모듈에 의존하면 안된다. 자주 바뀌는 구체적인 저 수준 모듈에 의존하면 안된다. 또한 저 수준 모듈은 추상화 된 고 수준 모듈에서 정의한 추상타입에 의존해야 한다.
예를 들어보면 이해가 조금 더 빠를 것이다. Open/closed principle 에서 사용한 코드를 실행한다고 했을 때
public static void main(String[] args) {
Instrument instrument = new Piano(); // DIP 적용
Piano piano = new Piano(); // DIP 위반
}
또는 Open/closed principle에서 사용한 코드를 참고해 보면
public class ClassicMusic {
public void musicStart(Instrument instrument){
instrument.play();
}
public void musicEnd(Instrument instrument){
instrument.stop();
}
}
musicStart 메소드에서 Instrument 추상화에 의존하고 있기 때문에 Instrument 을 구현한 하위 타입이 세부적으로 어떻게 구현이 되었는지 (피아노, 바이올린, 첼로 ….) 신경을 쓰지 않고 개발할 수 있다. 다른말로 하면 Instrument 하위 타입을 세부적으로 구현을 하지 않고도 독립적으로 프로그래밍을 할 수 있다.