SOLID ?Single Responsibility Principle(단일 책임 원칙)여러가지 책임을 하고 있는 경우한가지 책임만 하는 경우Open-Closed Principle(개방-폐쇄 원칙)예시Liskov Substitution Principle(리스코프 치환 원칙)위반한 경우올바른 경우Interface Segregation Principle(인터페이스 분리 원칙)Dependency Inversion(의존성 역전 원칙)강하게 종속되어 있는 경우의존이 역전된 경우REF
SOLID ?
Single Responsibility Principle(단일 책임 원칙)
- 클래스는, 오직 하나의 대해서만 책임져야 한다.
- 만약 클래스가 여러가지 작업을 책임져야 한다면, 이는 버그 발생 가능성을 높이게 됩니다. 또한 많은 기능 중 한가지를 변경할 때 모르는 사이에 다른 기능에 영향을 줄 수 있기 때문이다.
- SRP의 목적은 행동을 분리하는 것이고, 이로 인해 어떤 기능을 수정하더라도, 연관없는 기능에는 영향이 가지 않게 될 것이다.
여러가지 책임을 하고 있는 경우
- 아래의 예시는 캐릭터가 검사일때와 궁수일때의 책임을 수행하고 있으며 두 가지의 책임을 가지고 있으며 응집도가 낮다고 할 수 있다.
- 다른 책임과 position이라는 필드를 공유하고 있는 것처럼 의존도가 높아 결합도가 높다고 할 수 있다.
public class Character { String position; public Character(String position) { this.position = position; } public String attack() { if (position == "검사") { return "칼로 휘두른다."; } else { return "활을 쏜다."; } } }
한가지 책임만 하는 경우
- 아래의 클래스는 추상클래스를 통해 검사와 궁수의 책임을 분리하여 각 하나의 책임만 맡고 있기 때문에 응집도가 높으며 의존도가 존재하지 않기 때문에 결합도도 낮다고 할 수 있다.
public abstract class Character { public abstract String attack(); } public class Warrior extends Character { public String attck() { return "칼을 휘두른다."; } } public class Archer extends Character { public String attck() { return "활을 쏜다."; } }
Open-Closed Principle(개방-폐쇄 원칙)
- 클래스는 확장에는 개방적이어야 하고, 변경이는 폐쇄적이어야 한다.
- 클래스의 현재 코드를 변경하는것은 해당 클래스를 사용하고 있는 모든 시스템에 영향을 주게 된다.
- 만약 클래스에 더 많은 기능을 부여하고 싶다면, 가장 이상적인 접근방법은 기존 기능을 변경하는것이 아닌 새로운 함수를 추가하는 것이다.
- OCP의 목적은 클래스의 존재하는 기능의 변경 없이 해당 클래스의 기능을 확장시키는 것이다. 이로 인해 사용중인 클래스의 변경으로 인한 버그 발생을 피할 수 있다.
예시
- 아래는 단일책임 원칙에 사용했던 예시로 새로운 직업이 만약에 새로 생긴다 해도 다음과 같이 다른 클래스의 변경 없이 단순히 새로운 직업에 대한 클래스를 추가하기만 하면 된다.
public abstract class Character { public abstract String attack(); } public class Thief extends Character { public String attck() { return "표창을 날린다."; } }
Liskov Substitution Principle(리스코프 치환 원칙)
- 만약 S가 T의 서브타입이라면, T는 어떠한 경고도 내지 않으면서, S로 대체가 가능하다.
- 자식 클래스가 부모클래스의 기능을 똑같이 수행할 수 없을때, 이는 버그를 발생시키는 요인이 된다.
- 만약 어떤 클래스가 자신으로부터 다른 클래스를 생성했다면, 이제 그 클래스는 부모 클래스가 되고, 생성된 클래스는 자식 클래스가 된다. 자식 클래스는 부모 클래스가 할 수 있는 모든 것을 할 수 있어야 한다. 이를 상속이라고 표현한다.
- 자식 클래스는 부모 클래스처럼 똑같은 요청에 대해 똑같은 응답을 할 수 있어야 하고, 응답의 타입 또한 같아야 한다.
위반한 경우
public class Rectangle { int width; int height; public int getWidth() { return width; } public int getHeight() { return height; } public void setWidth(int width) { this.width = width; } public void setHeight(int height) { this.height = height; } public int getArea() { return width * height; } } public class Square extends Rectangle { @Override public void setWidth(int width) { super.setWidth(width); super.setHeight(getWidth()); } @Override public void setHeight(int height) { super.setHeight(height); super.setWidth(getHeight()); } } // 50 Rectangle rectangle = new Rectangle(); rectangle.setWidth(10); rectangle.setHeight(5); // 25 Rectangle rectangle = new Square(); rectangle.setWidth(10); rectangle.setHeight(5);
동일한 값을 넣었는데 다른 값을 반환하고 있으며 직사각형과 정사각형은 상속관계가 될 수 없는 구조이다.
사각형의 특징을 갖고 있지만 두 사각형 모두 사각형의 한 종류일 뿐이지 하나가 다른 하나를 완전히 포함하진 못한다.
올바른 경우
public class Shape { int width; int height; public int getWidth() { return width; } public int getHeight() { return height; } public void setWidth(int width) { this.width = width; } public void setHeight(int height) { this.height = height; } public int getArea() { return width * height; } } public class Rectangle extends Shape { public Rectangle(int width, int height) { setWidth(width); setHeight(height); } } public class Square extends Shape { public Square(int length) { setWidth(length); setHeight(length); } } // 50 Shape rectangle = new Rectangle(10, 5); // 25 Shape rectangle = new Square(5);
Interface Segregation Principle(인터페이스 분리 원칙)
- 클라이언트는 사용하지 않는 메서드에 대해 의존적이지 않아야 한다.
- 클래스가 서로 관계없는 기능들을 가지고 있다면 낭비가 되고, 예상치 못한 버그를 발생시킬 수 있다.
- 클래스는 해당 역할에 대해 액션만 수행해야 하고, 이를 제외한 다른 액션은 완전히 삭제하거나 다른 곳(다른 클래스 등)으로 이동시켜야 한다.
- ISP의 목적은 액션 집합을 더 작은 액션 집합으로 쪼개서, 클래스가 필요한 액션들만 실행할 수 있도록 하는 것이다.
Dependency Inversion(의존성 역전 원칙)
- 추상은 구체에 의존하지 않아야 하며, 구체는 추상에 의존해야 한다.
- 고수준의 모듈은 저수준의 모듈에 의존적이면 안되고, 둘다 추상에 의존적이어야 한다.
- 고수준 모듈(또는 클래스) : 도구와 함께 동작하는 클래스
- ex) 바이트 데이터를 읽어와 암호화 하고 결과 바이트 데이터를 쓴다.
- 저수준 모듈(또는 클래스) : 수행하기 위한 도구
- ex)
- 파일에서 바이트 데이터를 읽어온다.
- AES 알고리즘으로 암호화한다.
- 파일에 바이트 데이터를 쓴다.
- 추상 : 두 클래스를 연결하는 인터페이스
- 구체 : 도구가 동작하는 방법
- DIP는 액션을 수행할 때 클래스가 도구와 융합하면 안된다고 말한다. 보다 좋은 방법은 인터페이스와 융합하여 클래스와 도구를 연결하는 것이다.
- 두 클래스와 인터페이스는 어떻게 도구가 동작하는지 알 수 없어야 한다. 하지만, 도구는 인터페이스 사양을 충족해야 한다.
- DIP의 목적은 인터페이스를 통해 고수준 클래스이 저수준 클래스에 대해 의존성을 가지는 것을 줄이는 것이다.
강하게 종속되어 있는 경우
- 코드를 아래처럼 작성한다면 키보드와 마우스에 강하게 종속된다. 즉 맥미니는 특정 키크론 키보드와 로직텍 마우스밖에 사용할 수 없게 된다 만약 이 맥미니에 키크론키보드가 아니라 다른 키보드 브랜드를 사용한다고 하면 무조건 맥미니 클래스를 수정해야 한다.
- 즉 확장에 닫혀있고 수정에 열리게 된다.
public class MacMini { private final 키크론키보드_K3 키보드; private final 로지텍마우스_XXX 마우스; public MacMini() { 키보드 = new 키크론키보드_K3(); 마우스 = new 로지텍마우스_XXX(); } }
의존이 역전된 경우
- 맥미니가 인터페이스에 의존하게 되면서 인터페이스를 확장한 모든것을 사용할 수 있게되며 확장에는 열리고 수정에는 닫히게 된다.
public interface Keyboard { } public interface Mouse { } public class Keychron implements Keyboard { } public class Hhkb implements Keyboard { } public class Logitech implemets Mouse { } public class Trakpad implements Mouse { } public class MacMini { private final Keyboard keyboard; private final Mouse mouse; public MacMini(Keyboard keyboard, Mouse mouse) { this.keyboard = keyboard; this.mouse = mouse; } } MacMini macmini = new MacMini(new Keychron(), new Logitech()); MacMini macmini = new MacMini(new Hhkb(), new Trakpad());