좋은 소프트웨어를 설계하기 위해서는?
코드의 재사용 및 유지보수가 용이한 것이 좋은 소프트웨어다.
이를 위해 결합도(Coupling)는 낮추고 응집도(Cohesion)는 높여야한다.
예를들어 A클래스와 B클래스가 서로 의존하고 있을 경우, A코드를 수정하면 B코드에도 영향을 끼칠 수 있다. 이런 경우를 결합도가 높다고 일컫는다.
응집도는 한 모듈 내부의 처리 요소들이 서로 관련되어있는 정도를 말한다. 응집도가 높으면 해당 모듈은 하나의 책임에 집중하고 독립성이 높아져, 재사용 및 유지보수가 용이하다.
SOLID (객체지향 설계)
1. SRP (Single Responsibility Principle) 단일 책임 원칙
요약 : 한 클래스는 하나의 책임만 가진다.
예시로 아래 코드는 객체지향의 '다형성'을 무시하고 절차지향으로 선언되어있다.
class Unit(){
private String name;
private int speed;
public void move(){
if(name.equals("슬라임")){
speed+=3;
}else if(name.equals("주황버섯"){
if(angry) speed = 10;
else speed = 5;
}else{
// ...
}
}
}
만약 몬스터가 추가된다면 if-else구문은 끝없이 늘어날 수 있다.
또한 클래스 내부의 함수가 바뀔경우, 또다른 클래스에도 영향을 끼칠 수 있고, 나아가서 move라는 함수 자체에도 영향을 끼칠 수 있다.
위 코드를 단일 책임 원칙을 지켜 코드를 작성하면 다음과 같다.
class 슬라임 extends Unit{
public void move(){
this.speed += 3;
}
}
class 주황버섯 extends Unit{
public void move(){
if(angry) speed = 10;
else speed = 5;
}
}
// ...
2. OCP (Open Closed Principle) 개방 폐쇄 원칙
요약 : 자신의 확장에는 열려있으나, 주변의 변화에는 닫혀있어야 한다.
자주 사용된 모듈에 하나의 수정을 가할 때, 그 모듈을 이용하는 다른 모듈을 줄줄이 고쳐야 한다면 프로그램 수정이 고될것이다.
개방 폐쇄 원칙은 상위 클래스나 인터페이스를 중간에 둠으로써, 자신은 변화에 대해서 폐쇄적이지만 상위클래스나 인터페이스는 변화에 대해 확장을 개방해줄 수 있다.
아래 예시를 봐보자.

JDBC의 경우 Application에는 폐쇄적이지만, 자식클래스에게는 개방적으로 확장될 수 있도록 설계되어있다.
3. LSP (Liskov Substitution Principle) 리스코프 치환 원칙
요약 : 서브 타입은 언제나 자신의 기반(상위) 타입으로 교체 할 수 있어야 한다.
즉, 자식타입은 부모타입으로 교체가 가능해야한다.
예를들어 아래 도표를 봐보자.

직사각형의 자식클래스에 정사각형이 있다 가정하자. 정사각형은 직사각형이 될 수 있다. 고로, 자식클래스가 부모클래스로 교체될 수 있다.
하지만 반대의 케이스를 봐보자. 정사각형의 자식클래스에 직사각형이 있다 가정하자. 하지만 모든 직사각형이 정사각형이 될 수 있는것은 아니다. 고로, 자식클래스가 부모클래스로 교체될 수 없으므로, 상속관계를 유지할 수 없다.
4. ISP (Interface Segregation Principle) 인터페이스 분리 원칙
요약 : 클라이언트는 자신이 사용하지 않는 메서드에 의존관계를 맺으면 안된다.
예를들어 다음과 같은 상속관계가 있다고 가정하자.
interface Animal {
eat(): void;
fly(): void;
}
class Bird implements Animal {
eat(): void {
console.log('새가 음식을 먹었어요!');
}
fly(): void {
console.log('새가 하늘을 날았어요!');
}
}
class Human implements Animal {
eat(): void {
console.log('아 배부르다');
}
fly(): void {
// ????
}
}
사람은 하늘을 날 수 없다. 고로 fly()함수는 사람 입장에서는 불필요한 메서드를 상속받는 것이 되어버린다.
이를 위해 사람이 불필요한 메서드를 상속받을 수 없게 인퍼페이스를 상속받는 인터페이스를 만들면 된다.
interface Animal {
eat(): void;
}
interface FlyableAnimal extends Animal {
fly(): void;
}
class Bird implements FlyableAnimal {
eat(): void {
console.log('새가 음식을 먹었어요!');
}
fly(): void {
console.log('새가 하늘을 날았어요!');
}
}
class Human implements Animal {
eat(): void {
console.log('아 배부르다');
}
// fly() 메서드를 구현하지 않는다. ISP 준수!
}
단, 상속에 상속을 받는 클래스가 증가한다면 SRP(단일책임원칙)에 위배될 수 있는데, 이는 프로젝트 요구 사항에 따라 SRP와 ISP중 적절히 선택하면 된다.
5. DIP (Dependency Inversion Principle) 의존 역전 원칙
요약 : 자신보다 변하기 쉬운 것에 의존하지 말아야 한다.
예를 들어 아래 도표를 봐보자.

사람이 '옷'이라는 클래스에 의존을 하는데, 문제는 옷은 여러가지가 있다. 계절에 따라 봄,여름,가을 옷 등이 있기 때문이다.
이렇게 자신보다 변하기 쉬운 것에 의존을 하면, 코드 유지보수에 어려움이 생길 수 있다.

가운데 '옷'이라는 인터페이스를 둠으로써 사람은 상대적으로 변하기 어려운 것에 의존하게 됐다.
이를 코드로 구현하면 다음과 같다.
public class Person {
private Clothe clothe;
public void wearClothe(Clothe clothe) {
this.clothe = clothe;
}
public void picnic() {
clothe.picnic();
}
}
wearClothe를 통해 Clothe 인터페이스를 구현한 클래스를 외부에서 주입해 옷을 입는다.
public interface Clothe {
public void picnic();
}
public class Summer implements Clothe {
@Override
public void picnic() {
System.out.println("여름옷을 입고 피크닉을 간다.");
}
}
public class Winter implements Clothe {
@Override
public void picnic() {
System.out.println("겨울옷을 입고 피크닉을 간다");
}
}
public class Main {
public static void main(String[] args) {
Person person = new Person();
person.wearClothe(new Summer());
person.picnic();
person.wearClothe(new Winter());
person.picnic();
}
}
참고자료
https://ko.wikipedia.org/wiki/SOLID_(객체_지향_설계)
SOLID (객체 지향 설계) - 위키백과, 우리 모두의 백과사전
ko.wikipedia.org
'👩🏻💻 Programming > OOP' 카테고리의 다른 글
객체와 객체지향, 그리고 객체지향 4대 특성 (0) | 2021.07.27 |
---|---|
[Spring] MVC 패턴 (0) | 2021.06.08 |