개발자 끄적끄적

SOLID 본문

소프트웨어공학

SOLID

햏치 2024. 5. 1. 00:37

<SOLID>
- SOLID 원칙에 따라 좋은 소프트웨어를 만들 수 있다
  - 이해하기 쉽다
  - 변경 하기 용이(유지보수)
  - 새로운 기능에 대해 확장이 용이

- 로버트 마틴이 주창한 다섯 가지 설계원칙
  - SRP(단일 책임 원칙, Single Responsibility Principle)
  - OCP(개방 폐쇄 원칙, Open Closed Principle)
  - LSP(리스코프의 대입 원칙, Liskov Substitution Principle) 
  - ISP(인터페이스 분리 원칙, Interface Segregation Principle) 
  - DIP(의존성 역전 원칙, Dependency Inversion Principle)
=> Design Pattern에 기반





<SRP(단일 책임 원칙, Single Responsibility Principle)>
- 단일 책임 원칙 즉, 클래스는 단 하나의 책임만을 가지도록 설계해야 한다는 의미이다. 
- 책임
  - Actor : 동일한 needs나 기대를 가지는 사용자 그룹(개개인x)
  - 예제: 회계팀, 감사팀, 인사부서, DB 설계팀, UI 팀 등
  - 한 actor의 요구사항을 만족시키기 위한 기능들의 모임

-단일 책임 원칙
  - 한 클래스는 한(one and only one) actor만을 책임져야 한다






<OCP(개방 폐쇄 원칙, Open Closed Principle)>
- OCP는 기존의 코드를 변경하지 않으면서 새로운 기능을 추가할 수 있도록 설계하는 원칙이다





<OCP를 만족하는 설계가 되기 위해서는>
1. 변하는 것을 식별하고 => 출력형식
2. 변하는 것을 '클래스'로 분리한다 => PrintInXml, PrintlnJson
3. 변하는 것을 포용하는 개념을 '추상 클래스'나 '인터페이스'로 추상화한다 => Printer
  <<interface>>
     Pirnter
+print(name:String, m:double, f:double, h:double, g:Grade)

4. 2번 단계에서 만든 클래스를 3번 단계에서 추출한 '추상화한 개념의 자식클래스'로 모델링한다
PrintInXml ---▷ <<interface>>Printer, PrintInJson ---▷ <<interface>>Printer

즉, OCP를 만족하는 설계
Stuent -> <<interface>> Printer
PrintInXml ---▷ <<interface>>Printer
PrintInJson ---▷ <<interface>>Printer
- 출력 형식에 따라 클래스로 분리하였다
   PrintInXml 클래스는 성적정보를 xml 형식으로 출력하고
   PrintInJson 클래스는 Json 형식으로 출력하고 한다 
   만약 새로운 출력형식에 대한 요구가 있으면 새로운 출력 형식을 표현하는 클래스를 Print의 파생 클래스로 추가하면 된다





<LSP(리스코프의 대입 원칙, Liskov Substitution Principle)>
- 일반화 관계를 적절하게 사용했는지를 점검하는 원칙
- LSP는 일반화 관계는 슈퍼 클래스가 제공하는 오퍼레이션과 
  파생클래스에서 제공하는 오퍼레이션 간에는 '행위적으로 일관성'이 있도록 설계가 되어야 한다는 원칙
- 프로그램에서 슈퍼 클래스의 인스턴스 대신에 파생 클래스의 인스턴스로 대체하여도 프로그램의 의미는 변화되지 않도록 설계




<행위 일관성>
- pre⇒pre'(만약 선조건 pre가 만족된다면 pre'가 만족되어야 한다.)
- post'⇒post(만약 후조건 post'가 만족된다면 post가 만족되어야 한다.)
- 재정의를 하면 안된다 -> 행위 일관성을 만족하지 않는다




<예시>
Listing 1
public class MinMax {
public ArrayList<Integer>mimax(ArrayList<Integer> a) {
int minValue;
int maxValue;
ArrayList<Integer> b;
b=a;
minValue = Collections.min(a);
maxValue = Collections.max(a);
b.set(0, minValue); //가장 작은값, 첫 번쨰 위치
b.set(a.size()-1, maxValue); //가장 큰 값, 마지막 위치
return b;
}
}


Listing 2 //재정의를 잘못함
public class MinMax1 extends MinMax{
public ArrayList<Integer> minmax(ArrayList<Integer> a)
{
int minValue;
int maxValue;
ArrayList<Integer> b;

minValue = Collections.min(a); //ArrayList의 a값을 받아서 가장 작은 값을 구한다
maxValue = Collections.min(a);  //ArrayList의 a값을 받아서 가장 큰 값을 구한다
a.set(0, minValue); //첫 번째 위치에 가장 작은 값을 넣는다
a.set(a.Size()-1, maxValue);  //가장 나중 위치에 가장 큰 값을 넣는다
b=a; 
return b;
}





<행위적으로 일관성이 없다>
//Array List가 들어오면 
pre: ∀i∈int: a[i]∈int 
post: a[0]=smallest(a) and a[size(a)-1]=largest(a) //첫번째 위치에 가장 작은 값, 마지막 위치에 가장 큰 값

//Listing 2
pre1: ∀i∈int: a[i]∈int 
post1: a[0]=smallest(a) and a[size(a)-1]=smallest(a) //첫번째 위치와 마지막위치에 가장 작은 값이 온다


pre(부모)⇒pre1(자식)가 만족되지만  ~(post1⇒post) //행위적으로 일관성이 없다



Listing 3 //재정의 ok
public class MinMax2 extends MinMax{
public ArrayList<Integer> mimax(ArrayList<Integer> a) {
ArrayList<Integer> b;
b=a;
Collections.sort(b); //sort정렬(기본 오름차순)
return b; //첫번째 위치에는 가장 작은 값, 마지막 위치에는 가장 큰 값
}
}



<명세>
Listing 1
pre: ∀i∈int: a[i]∈int
post: a[0]=smallest(a) and a[size(a)-1]=largest(a)

Listing 3
pre2: ∀i∈int: a[i]∈int
post2: for ∀i1, i2: 0≤i1≤i2<size(a), a[i1]≤a[i2]


pre(부모)⇒pre1(자식)가 만족되고  (post2⇒post) //행위적으로 일관성이 있다




<ISP(인터페이스 분리 원칙, Interface Segregation Principle)>
- Clients should not be forced to depend upon interfaces that they do not use.”
- 인터페이스를 클라이언트에 특화되도록 분리시키라는 설계 원칙
- 클라이언트의 관점에서 클라이언트 자신이 이용하지 않는 기능에는 영향을 받지 않아야 한다는 내용이 담겨 있다.




<예제 - ISP 적용 전>
public interface PrintingService{
public void print();
public void fax();
public void copy();
}

public class MultiPrinter implements PrintingService{
@Override
public void print(){
  System.out.println("복합기 출력");
}

@Override
public void fax(){
  System.out.println("복합기 팩스");
}
@Override
public void copy(){
  System.out.println("복합기 복사");
}
}


<<interface>>
  PrintingService
----------------------
+print() : void
+fax() : void
+copy() : void

MultiPrinter
----------------------
+print() : void
+fax() : void
+copy() : void


MultiPrinter ---▷ <<interface>> PrintingService



<But...>
public class HansungPrinter implements PrintingService{
@Override
public void print(){
  System.out.println("한성프린터 출력");
}

@Override
public void fax(){
  throw new UnsupportedOperationException("Not Supported");
}
@Override
public void copy(){
  throw new UnsupportedOperationException("Not Supported");
}
}

- 만약, HansungPrinter가 제공하지 않는 fax()나 copy() 인터페이스에 변화가 생긴다면?
  예를 들어 copy(int pages) 처럼 copy() 연산에 복사할 페이지 수를 인자로 요구하도록 변경된다면 HansungPrinter도 영향을 받는다.




<인터페이스 분리>
<<interface>>
ProfessionalPrintingService
---------------------------------
+print():void

HansungPrinter
---------------------------------
+print():void

MultiPrinter
---------------------------------
+print():void
+fax():void
+copy():void


<<interface>>
PrintingService
---------------------------------
+print():void
+fax():void
+copy():void



HansungPrinter, MultiPrinter ---▷ <<interface>>ProfessionalPrintingService
MultiPrinter ---▷ <<interface>>PrintingService




<DIP(의존성 역전 원칙, Dependency Inversion Principle)>
- 상위 모듈은 하위 모듈에 의존하면 안된다. 
  이 두 모듈 모두 다른 추상화된 것에 의존해야 한다. 
  High-level modules should not depend on low-level modules. Both should depend on abstractions.
- 추상화된 것은 구체적인 것에 의존하면 안된다. 
  구체적인 것이 추상화된 것에 의존해야 한다. 
  Abstractions should not depend on details (concrete implementation). Details should depend on abstraction

- High-level module(=class) : 도메인 핵심 로직(domain core logic)
- Low-level module : 도메인 핵심 로직을 둘러싼 환경
  - 도메인 핵심 로직에 필요한 정보를 제공하고나 도메인 핵심 로직으로 부터 생성된 정보를 외부에 전달
  - 변경될 가능성이 많다.

- 도메인 핵심 로직이 주변 환경의 변화에 영향 받지 않도록 설계
  - 의존 관계를 맺을 때 도메인 핵심 로직(domain core logic)으로 의존 관계 방향이 설정 되어 있어야 한다. 

- 의존 관계를 설정할 때에는 구체적인 클래스 보다는 이를 추상화한 개념과 '인터페이스 같은 수단을 이용하여 관계를 맺도록 설계'해야 한다.




<패키지 분리>
- 패키지는 관련 있는 것들을 묶는 수단
- UML 패키지

'소프트웨어공학' 카테고리의 다른 글

소프트웨어 아키텍처  (0) 2024.05.07
오픈소스 라이선스  (0) 2024.04.30
결합도  (0) 2024.04.16