Java

[Java] 객체 지향 설계 5원칙_SOLID

sujin7837 2021. 2. 17. 18:09
반응형

SOLID

-객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙으로 제시한 것으로, 응집도는 높이고(High Cohesion), 결합도는 낮추라(Loose Coupling)는 고전 원칙을 객체 지향의 관점에서 재정립한 것입니다.

 

*결합도와 응집도*

좋은 소프트웨어 설계를 위해서는 결합도(coupling)는 낮추고 응집도(cohesion)는 높이는 것이 바람직합니다.

 

-결합도: 모듈(클래스) 간의 상호 의존 정도로서 결합도가 낮으면 모듈 간의 상호 의존성이 줄어들어 객체의 재사용이나 수정, 유지보수가 용이합니다.

 

결합도 수준: 데이터 결합도 > 스탬프 결합도 > 컨트롤 결합도 > 외부 결합도 > 공유 결합도 > 내용 결합도

 

-응집도: 하나의 모듈 내부에 존재하는 구성 요소들의 기능적 관련성으로, 응집도가 높은 모듈은 하나의 책임에 집중하고 독립성이 높아져 재사용이안 기능의 수정, 유지보수가 용이합니다.

 

응집도 수준: 기능 응집도 > 순차 응집도 > 통신 응집도 > 절차 응집도 > 시간 응집도 > 논리 응집도 > 우연 응집도

 

-SOLID를 소프트웨어에 잘 녹여낸  경우는 그렇지 못한 경우에 비해 상대적으로 이해하기 쉽고, 리팩토링과 유지보수도 수월하며, 논리적으로 정연합니다. 

-SOLID는 객체 지향 4대 특성을 발판으로 하고 있으며, 디자인 패턴의 뼈대이자 스프링 프레임워크의 근간이기도 합니다.

 

  • SRP(Single Responsibility Principle): 단일 책임 원칙
  • OCP(Open Closed Principle): 개방 폐쇄 원칙
  • LSP(Likkov Substitution Principle): 리스코프 치환 원칙
  • ISP(Interface Segregation Principle): 인터페이스 분리 원칙
  • DIP(Dependency Inversion Principle): 의존 역전 원칙

 

SRP(Single Responsibility Principle)-단일 책임 원칙

-어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 합니다. 따라서 클래스를 역할과 책임에 따라 분리해서 각각 하나의 역할과 책임만 갖게 할 수 있습니다.

-단일 책임 원칙은 속성, 메소드, 패키지, 모듈, 컴포넌트, 프레임워크 등에도 적용할 수 있습니다.

 

 

단일 책임 원칙을 지키지 못하는 경우

-가질 수 없는 역할을 클래스에 속성으로 할당한 경우

ex) 사람 클래스에 '군번' 속성을 넣은 경우 -> 사람형 참조 변수 여성이 해당 속성을 가질 수 없습니다.

     해결: 사람 클래스를 남자 클래스와 여자 클래스로 구분한 후, 남자 클래스에만 군번 속성을 포함시킵니다.

 

-하나의 속성이 여러 의미를 갖는 경우

//메소드가 단일 책임 원칙을 지키지 않은 경우

class 강아지 {
	final static Boolean 수컷=true;
    final static Boolean 암컷=false;
    Boolean 성별;
    
    void 소변보다() {
    	if(this.성별==수컷) {
        	//한쪽 다리를 들고 소변을 본다.
        } else {
        	//뒷다리 두 개를 굽혀 앉은 자세로 소변을 본다.
        }
    }
}

'소변보다()' 메소드가 수컷과 암컷의 행위를 모두 구현하려고 하기에 단일 책임 원칙을 위배하고 있습니다.

 

//단일 책임 원칙을 적용해 개선한 코드

abstract class 강아지 {
	abstract void 소변보다()
}

class 수컷강아지 extends 강아지 {
	void 소변보다() {
    	//한쪽 다리를 들고 소변을 본다.
    }
}

class 암컷강아지 extends 강아지 {
	void 소변보다() {
    	//뒷다리 두 개로 앉은 자세로 소변을 본다.
    }
}

위 코드를 단일 책임 원칙을 적용하여 클래스를 분리했습니다.

 

 

애플리케이션의 경계를 정하고, 추상화를 통해 클래스들을 선별하고 속성과 메소드를 설계할 때 반드시 단일 책임 원칙을 고려하는 습관을 들여야 합니다. 또한 리팩토링을 통해 코드를 개선할 때도 단일 책임 원칙을 적용할 곳이 있는지 꼼꼼히 살피는 것이 중요합니다.

 

OCP(Open Closed Principle)-개방 폐쇄 원칙

-소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하지만, 변경에 대해서는 닫혀 있어야 합니다. 즉, 자신의 확장에는 열려있고, 주변의 변화에 대해서는 닫혀 있어야 합니다.

 

ex1)

마티즈는 창문과 기어가 수동이고, 쏘나타는 자동이라고 해보겠습니다. 왼쪽 그림에 의하면 운전자가 자동차를 마티즈에서 쏘나타로 바꿀 경우, 운전자의 운전 습관에도 변화가 생겨야 합니다. 그러나 오른쪽 그림과 같이 상위 클래스 또는 인터페이스를 중간에 두게 되면, 다양한 자동차가 생긴다고 해도 객체 지향 세계의 운전자는 운전 습관에 영향을 받지 않게 됩니다. 운전자 입장에서 주변의 변화에 폐쇄되어 있는 것입니다.

 

 

ex2) JDBC

-JDBC를 사용하는 클라이언트는 데이터베이스가 오라클에서 MySQL로 바뀌더라도 Connection을 설정하는 부분 외에는 따로 수정할 필요가 없습니다. Connection 설정 부분을 별도의 설정 파일로 분리해두면 클라이언트 코드는 단 한 줄도 변경할 필요가 없습니다. 

-오라클을 MySQL이나 MS-SQL로 교체할 때 자바 애플리케이션은 JDBC 인터페이스라고 하는 완충 장치로 인해 변화에 영향을 받지 않습니다. 즉 자바 애플리케이션은 데이터베이스라고 하는 주변의 변화에 닫혀 있는 것입니다. 데이터베이스를 교체한다는 것은 데이터베이스가 자신의 확장에는 열려 있다는 것입니다.

 

JDBC뿐만 아니라 iBatis, MyBatis, 하이버네이트 등등 데이터베이스 프로그래밍을 지원하는 라이브러리와 프레임워크에서도 개방 폐쇄 원칙의 예를 볼 수 있습니다.

 

 

ex3) 현실 세계의 개방 폐쇄 원칙

-편의점에서 일하는 직원이 바뀌더라도 손님이 구매라는 행위를 하는 데는 영향이 없습니다. 즉, 직원 교대라고 하는 주변의 변화에 손님의 구매 행위는 영향을 받지 않는 것이고, 직원은 교대라고 하는 확장 행위에는 열려 있는 것입니다.

-구매 담당자의 행위 추가, 보안 담당자의 행위 추가 등의 확장에 대해 직원은 열려 있습니다.

 

 

ex4) 스프링 프레임워크

 

 

개방 폐쇄 원칙을 무시하고 프로그램을 작성하면 객체 지향 프로그래밍의 가장 큰 장점인 유연성, 재사용성, 유지보수성 등을 얻을 수 없습니다.

 

 

LSP(Likkov Substitution Principle)-리스코프 치환 원칙

-서브 타입은 언제나 자신의 기반 타입(base type)으로 교체할 수 있어야 합니다.

-객체 지향의 상속 조건: 객체 지향에서의 상속은 조직도나 계층도가 아닌 분류도가 되어야 합니다.

  • 하위 클래스 is a kind of 상위 클래스 - 하위 분류는 상위 분류의 한 종류입니다.
  • 구현 클래스 is able to 인터페이스 - 구현 분류는 인터페이스할 수 있어야 합니다.

   위의 두 개의 문장대로 구현된 프로그램이라면 리스코프 치환 원칙을 잘 지키고 있다고 할 수 있습니다. 하위 클래스의 인스턴스는 상위형     객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는 데 문제가 없어야 합니다.

 

   *인터페이스 할 수 있어야 한다*

    AutoCloseable - 자동으로 닫힐 수 있어야 합니다.

    Appendable - 덧붙일 수 있어야 합니다.

    Cloneable - 복제할 수 있어야 합니다.

    Runnable - 실행할 수 있어야 합니다.

 

[그림 5-10]의 경우에는 딸이 아버지, 할아버지의 역할을 하는 것이 논리에 맞지 않습니다. 이는 리스코프 치환 원칙을 위배하는 것입니다. 그러나 [그림 5-11]의 경우에는 고래가 포유류 또는 동물의 역할을 하는 것이 전혀 문제가 되지 않습니다. 따라서 리스코프 치환 원칙이 적용되었다고 할 수 있습니다.

 

  • 하위형에서 선행 조건은 강화될 수 없습니다.
  • 하위형에서 후행 조건은 약화될 수 없습니다.
  • 하위형에서 상위형의 불변 조건은 반드시 유지되어야 합니다. 

 

ISP(Interface Segregation Principle)-인터페이스 분리 원칙

-클라이언트는 자신이 사용하지 않는 메소드에 의존 관계를 맺으면 안 됩니다.

[그림 5-14]는 남자 클래스를 분리하는 것이 아니라, 역할별로 인터페이스를 분할하여 인터페이스 분할 원칙을 적용한 것입니다.

결론적으로 단일 책임 원칙(SRP)과 인터페이스 분할 원칙(ISP)은 같은 문제에 대한 두 가지 다른 해결책이라고 볼 수 있습니다. 프로젝트 요구사항과 설계자의 취향에 따라 두 원칙 중 하나를 선택해서 설계할 수 있으나, 특별한 경우가 아니라면 단일 책임 원칙을 적용하는 것이 더 좋은 해결책이라고 할 수 있습니다.

 

-인터페이스 최소주의 원칙: 인터페이스를 통해 메소드를 외부에 제공할 때는 최소한의 메소드만 제공하라는 원칙입니다.

 

1. 빈약한 상위 클래스를 이용하는 경우

//빈약한 상위 클래스를 이용하는 경우

public class Driver {
	public static void main(String[] args) {
    	사람 김학생=new 학생("김학생", new Date(2000, 01, 01), "20000101-1234567", "20190001");
        사람 이군인=new 군인("이군인", new Date(1998, 12, 31), "19981231-1234567", "19-12345678");
        
        System.out.println(김학생.이름);
        System.out.println(이군인.이름);
        
        //System.out.println(김학생.생일);	//사용불가
        //System.out.println(이군인.생일);	//사용불가
        
        System.out.println(((학생)김학생).생일);	//캐스팅 필요
        System.out.println(((군인)이군인).생일);	//캐스팅 필요
        
        //System.out.println(김학생.주민등록번호);	//사용불가
        //System.out.println(이군인.주민등록번호);	//사용불가
        
        System.out.println(((학생)김학생).주민등록번호);	//캐스팅 필요
        System.out.println(((군인)이군인.주민등록번호);	//캐스팅 필요
        
        김학생.먹다();
        이군인.먹다();
        
        //김학생.자다();	//사용불가
        //이군인.자다();	//사용불가
        
        ((학생)김학생).자다();	//캐스팅 필요
        ((군인)이군인).자다();	//캐스팅 필요
        
        //김학생.소개하다();	//사용불가
        //이군인.소개하다();	//사용불가
        
        ((학생)김학생).소개하다();	//캐스팅 필요
        ((군인)이군인).소개하다();	//캐스팅 필요
        
        ((학생)김학생).공부하다();	//캐스팅 필요
        ((군인)이군인).훈련하다();	//캐스팅 필요
    }
}

빈약한 상위 클래스를 이용한 경우 여기저기 형변환이 발생하면서 상속의 혜택을 제대로 누리지 못하고 있습니다.

 

2. 풍성한 상위 클래스를 이용하는 경우

//풍성한 상위 클래스를 이용하는 경우

public class Driver {
	public static void main(String[] args) {
    	사람 김학생=new 학생("김학생", new Date(2000, 01, 01), "20000101-1234567", "20190001");
        사람 이군인=new 군인("이군인", new Date(1998, 12, 31), "19981231-1234567", "19-12345678");
        
        System.out.println(김학생.이름);
        System.out.println(이군인.이름);
        
        System.out.println(김학생.생일);
        System.out.println(이군인.생일);
        
        System.out.println(김학생.주민등록번호);
        System.out.println(이군인.주민등록번호);
        
        System.out.println(김학생.학번);	//사용불가
        System.out.println(이군인.군번);	//사용불가
        
        System.out.println(((학생)김학생).학번);	//캐스팅 필요
        System.out.println(((군인)이군인.군번);	//캐스팅 필요
        
        김학생.먹다();
        이군인.먹다();
        
        김학생.자다();
        이군인.자다();
        
        김학생.소개하다();
        이군인.소개하다();
        
        김학생.공부하다();	//사용불가
        이군인.훈련하다();	//사용불가
        
        ((학생)김학생).공부하다();	//캐스팅 필요
        ((군인)이군인).훈련하다();	//캐스팅 필요
    }
}

풍성한 상위 클래스를 이용하는 경우에는 빈약한 상위 클래스를 이용한 경우보다 사용 불가능한 경우나 불필요한 형변환이 없음을 볼 수 있습니다.

 

 

DIP(Dependency Inversion Principle)-의존 역전 원칙

-고차원 모듈은 저차원 모듈에 의존하면 안 되며, 이 두 모듈 모두 다른 추상화된 것에 의존해야 합니다.

-추상화된 것은 구체적인 것에 의존하면 안 되며, 구체적인 것이 추상화된 것에 의존해야 합니다.

-자신보다 자주 변경되는 구체 클래스에 의존하면 안 됩니다.

-의존 역전 원칙은 자신보다 변하기 쉬운 것에 의존하던 것을 추상화된 인터페이스나 상위 클래스를 두어 변하기 쉬운 것의 변화에 영향받지 않게 하는 것입니다.

 

스노우타이어는 계절이 바뀌면 교체해야 하는, 자주 변하는 클래스입니다. 이렇게 자주 변경되는 구체 클래스에 자동차가 의존하고 있는 경우를 [그림 5-17]과 같이 개선해야 합니다. [그림 5-17]에서는 추상화된 타이어 인터페이스를 추가하여 의존 관계를 역전시키고 있습니다.

 

상위 클래스일수록, 인터페이스일수록, 추상 클래스일수록 변하지 않을 가능성이 높기에 하위 클래스나 구체 클래스가 아닌 상위 클래스, 인터페이스, 추상 클래스를 통해 의존하라는 것이 의존 역전 원칙입니다.

 

 

SoC(Seperation Of Concerns)-관심사의 분리

관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모으고, 관심이 다른 것은 가능한 한 따로 떨어져 서로 영향을 주지 않도록 분리하라는 것입니다. 즉, 하나의 속성, 하나의 메소드, 하나의 클래스, 하나의 모듈, 또는 하나의 패키지에는 하나의 관심사만 들어 있어야 합니다.

 

 

 

 

반응형