#SOLID 원칙
java 가 객체지향언어로서 객체지향의 solid 5대 원칙을 알아보자.
#Single Responsibility principle (SRP)
작성된 클래스는 하나의 기능만 가져야 한다.
하나의 책임에 집중 -> 책임 영역 확실해짐
다른 책임의 변경으로 연쇄작용에서 자유로워짐
유지보수가 용이해짐
도메인이해가 필요함
적용방법
- 두클래스가 중복되고비슷한 책임을 갖고 잇다면 부모클래스로 정의하여 부모클래스에 위임하고
각각 클래스에서는 유사한 책임은 부모클래스에 명백히 위임하고 각각 다른 기능을 정의한다.
적용사례
/**
*
* single responsibility principle 적용되기 전 클래스
*
*
*/
public class Guitar {
//생성자
public Guitar(String serialNumber,Double price, Maker maker, String model, int stringNum) {
}
private String seriaNumber;
private Double price;
private Maker maker;
private String model;
private int stringNum;
}
우선 변화 요소 유무 부터 나누면
serialNumber 은 고유정보이다. 다른 클래스와 구분되는 정보이다.
price,Meker,model,stringNum 등은 특성 정보로 변경이 발생 할 수 있는 부분이다.
따라서 특정 정보군에 변화가 발생하면 항상해당 클래스를 수정해야 하는 부담이 발생하게 됨으로 이부분을 SRP 이 적용대상이 된다.
변경이 예상되는 객체들을 따로 분리한다.
model,price,Maker,StringNum 을 Spec 클래스에 모아둔다
class Gutar{
public Gutar(String serialNumber,GuitarSpec spec ) {
}
}
class GuitarSpec{
double price;
String model;
Maker maker;
int StringNumber;
}
만약 GuitarSpec 에 name 이라는 필드가 추가되도 guitar 클래스에서 책임지지 않아도 된다. 스펙 클래스에서만 추가해주면 된다.
이렇게 각 객체간 응집력을 높이고 결합력이 있다고 분리시켜 객체간의 순 작용 할 수 있도록 한다.
#Open Close Principle(OCP)
확장에는 열려있고 변경에는 닫혀있어야 한다.
즉 추가 요구 사항이나 변경 사항이 있더라도 기존 구성요소에 수정이 일어나지 않고 확장하여 재사용할 수 있어야함.
OCP는 관리가능하고 재사용 가능한 코드를 만드는 기반임
추상화와 다형성을 통해 OCP 를 가능케함
적용방법
- 변경(확장)될 것과 변하지 않을 것을 구분
- 이 두 모듈이 만나는 지점에 인터페이스 정의(?) -> 공통되는 부분을 부모클래스로 만들기
- 구현에 의존하기보다 정의한 인터페이스에 의존하도록 코드 작성
예를 들어보자 SRP 예에 들었던 Guitar 뿐만 아니라 바이올린 ,첼로 등등등 악기들을 추가 해야된다고 생각해보자.
Guitar, GuitarSpect 는 오로지 기타를 위한 클래스로 만들어졌다. 결합도 높게 설계햇다면 확장적이지 못할 뿐더러 많은 수정이 발생되어 유지보수가 어렵다.
기타와 바이올린에 getPlay()라는 메소드를 추가한다고 가정하면 해당 클래스 모두 수정이 필요하다.
OCP 원칙대로 수정해보자
우선 기타와 추가될 악기들의 공통 속성들을 모두 담을수 있는 악기 인터페이스와 추가될 알기 스텍들과 공통이 될수 있는 속성을 스펙 인터페이스로 만든다.
악기, 악기 스펙은 수정이 없으면서 악기는 계속 확장적이게 된다. 상위클래스나 인터페이스는 일종의 완충 장치인것이다.
악기스펙 인터페이스에서는 가격정보,모델정보,연주하기() 메서드를 정의하고 각각 기타,바이올린 등 추가되는 악기 클래스에서 입맛에 맞게 메서드를 재저의하면 되다.
더 깔금한 예시로 postgreSQL, Oracle, sybase 데이터베이스에 모두 확장적이면서 자바어플리케이션 입장에서 수정은 폐쇄적인것 임을 알아야한다.
이것이 바로 OCP 이다.
#Liskov Substitution Principle(LSP)
하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는데 문제가 없어야 한다.
즉 하위클래스가 상위클래스 역할을 대신할 때 논리적으로 맞아야 한다.
' A is a kind of B 즉 a kind of B is A '
잘못된 상속 관계 : 엄마와 딸 // 딸은 아버지의 한 종류다 ? NO !!
올바른 상속 관계 : 고양이와 동물 // 고양이는 동물의 한 종류다 ? YES !!
객체지향에서의 상속은 상위,하위 클래스를 설계하는 것이 계층이 아니라 분류이다. 상속은 확장이다 라고 생각해야한다.
아래 간단한 예제로 이해해보자.
void fuction(){
LinkedList list = new LinkedList();
modify(list);
}
void modify(LinkedList list){
list.add("test");
doSomethingWith(list);
}
List만 사용할 것이라면 이 코드도 문제는 없다. 만약 속도 개선을 위해 HashSet을 사용해야 하는 경우가 발생한다면 LinkdList 를 다시 HashSet으로 어떻게 변경할수 있을까 ? LinkdList 와 HashSet 은 Collection 인터페이스를 상속하고 있으므로 아래와 같이 구현할수 있다.
void fuction(){
Collection collection = new HashSet();
modify(collection);
}
void modify(Collection collection){
collection.add("test");
doSomethingWith(collection);
}
Collection 프레임 워크가 LSP를 준수하지 않았다면 Collection 인터페이스를 통해 수행하는 작업이 제대로 동작하지 않았을것이다.
LSP 준수하여 HachSet는 modify() 메소드 동작이 가능하다. 또한 OCP 구조도 된다. modify()는 컬렉션에는 확장이 열러있고 modify 에 변경은 닫혀있다.
#InterFace Segregation Principle(ISP)
인터페이스 분리 원칙은 SRP와 같은 맥락의 대한 다른 해결책을 제시하는 것이다.
한 클래스에 너무 많은 책임을 주어 상황에 관련 되지 않은 메소드 까지 구현되어 있다면,
SRP 원칙은 기존 클래스를 여러 책임 단위로 쪼갠다.
하지만 ISP는 기존 클래스를 그대로 두고 인터페이스 최소주의 원칙에 따라 각 상황에 맞는(책임) 기능만 제공하도록 인터페이스로 분리한다고 생각하면 된다.
job클래스에 요리하기 (), 기도하기(), 사격하기(), 개발하기() 메소드가 있다. 하지만 모든 사람 하는 기능은 아니다.
public class Job {
void shoot(){};
void pray(){};
void cook(){};
void develop(){};
}
각각 직업 특성에 맞게 책임을 인터페이스로 분리해보자
우선 책임별 인터페이스 생성한다.
interface Shootable {
void shoot();
}
interface Cookable {
void cook();
}
interface Prayable {
void pray();
}
interface Developable {
void develop();
}
그다음 다중구현을 통해 각각 메서드를 오버라이딩해서 재정의한다.
public class job implements Shootable,Developable,Cookable,Prayable{
@Override
public void shoot() {
System.out.println("사격한다.");
}
@Override
public void cook() {
System.out.println("요리한다");
}
@Override
public void pray() {
System.out.println("기도한다.");
}
@Override
public void develop() {
System.out.println("개발한다.");
}
}
public class ISPmain {
public static void main(String[] args){
Developable developer = new job();
developer.develop();
Cookable cooker = new Person();
cooker.cook();
}
}
직업중에 개발자라면 위에 예제와 같이 참조형을 Developable 타입으로 정의하고 job 인스턴스를 받으면 개발자는 develop() 기능만 할당 받게 된다.
상황에 맞게 기존 클라이언트는 변경없이 관련 메소드만 제한을 강제하여 사용할 수 있다.
#Dependency Inversion Principle (DIP)
'추상화 된 것은 구체적인 것에 의존하면 안된다. 구체적인 것이 추상화 된 것에 의존해야 한다.'
즉 자신보다 변하기 쉬운 것에 의존하지 마라.
구체적으로 추상클래스 또는 상위클래스는 구체적인 구현클래스 또는 하위클래스에게 의존적으면 안된다.
왜냐면 구체적인 클래스는 코딩에 있어서 가장 전면적으로 노출되고 사용되기 때문에 시시때때로 변화할 수 있으므로 변화에 민감하다.
예를 들면 BMW 자동차는 스노우타이어를 장착하고 있다. 하지만 스노우 타이어는 계절의 영향을 받아 겨울 지나고 다른 타이어로 교체할것이다.
BMW가 자신보다 더 변화에 민감한 스노우 타이어를 의존하고 있다. 이 의존을 일방적인 방향을 역전 시켜보자
자신 보다 변하기 쉬운 것에 의존하던것을 추상화 된 인터페이스나 상위클래스를 두어 변하기 쉬운것의 변화에 영향 받지 않게 의존 방향을 역전시켰다.
타이터 인터페이스에 의존하면서 직접적으로 스노우,일반타이어와 의존하는것을 피했다.
또 스노우,일반 타이어는 기존에 어떤 것도 의존하지 않았지만 인터페이스를 의존해야한다. 이것이 Dependency Inversion Principle 이다.
#마무리
객체지향 OOP 의 4대 요소인 캡슐화,상속,추상화,다형성이라는 재료와 객체지향 성계 SOLID 원칙을 통해 객체지향을 어떻게 설계하는지 배웠다.
결국 실무에서 설계를 잘해야 유지보수 측면에서 굉장히 용이하고 개발 시간과 비용을 절감할수 있다.