본문 바로가기
공부정리

객체지향의 5원칙, SOLID

by 에드박 2021. 2. 14.

OOD (Object Oriented Design, 객체 지향 설계)

소프트웨어 개발에서 OOD(Object Oriented Design)는 유연하고 확장 가능하며 유지 관리 및 재사용이 가능한 코드를 작성하는데 중요한 역할을 합니다.

 

OOD를 사용하면 많은 이점이 있지만 모든 개발자는 프로그래밍에서 좋은 OOD를 위해 SOLID 원칙에 대한 지식도 있어야 합니다.

 


SOLID 원칙

SOLID 원칙은 Uncle Bob이라고도 알려진 Robert C. Martin(로버트 마틴)에 의해 도입되었으며 프로그래밍 코딩 표준입니다.

 

이 원칙은 다섯 가지 원칙의 약어입니다.

  1. 단일 책임 원칙 (Single Responsibility Principle, SRP)
  2. 개방 / 폐쇄 원칙 (Open/Closed Principle, OCP)
  3. Liskov의 대체 원칙 (Liskov's Substitution Principle, LSP)
  4. 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
  5. 의존성 역전 원칙 (Dependency Inversion Principle, DIP)

SOLID 원칙은 강한 결합을 줄이는데 도움이 됩니다.

 

강한 결합이란? 클래스 그룹이 서로 크게 의존한다는것을 의미합니다.(-> 피해야 하는 코드입니다.)

 

긴밀한 결합의 반대는 느슨한 결합이며 느슨하게 결합된 클래스가 있는 코드는 좋은 코드로 간주됩니다.

 

느슨하게 결합된 클래스의 장점

  • 코드의 변경을 최소화
  • 코드를 보다 재사용 가능하고 유지 관리 가능
  • 유연하고 안정적으로 만드는데 도움

S , 단일 책임의 원칙 (Single Responsibility Principle, SRP)

 

이 원칙은 "클래스는 변경해야 할 이유가 하나만 있어야합니다." 라고 말합니다.

-> 즉, 모든 클래스는 단일 책임 또는 단일 작업 또는 단일 목적을 가져야합니다.

 

소프트웨어 개발을 예로들면

  • 프론트 엔드 디자이너가 디자인을 담당
  • 백엔드 개발자가 백엔드 개발 부분을 처리
  • 테스터가 테스트를 담당

위처럼 다른 일을 하는 다른 구성원으로 나눌 수 있습니다. 이러면 모든 사람이 단일 작업 또는 단일 책임을 가진다고 말할 수 있습니다.

 

대부분의 잘못된 프로그래밍은 기능이나 새로운 동작을 추가해야할 때 완전히 잘못된 기존의 클래스에 모든것을 구현한다는 것입니다. 이렇게 되면 코드를 길고 복잡하게 만들고 나중에 무언가를 수정해야 할 때 시간을 소비해야 합니다.

 

우리는 애플리케이션에서 레이어를 사용해 God 클래스를 쪼개서 더 작은 클래스 또는 모듈로 나눠야 합니다.

God 클래스 : 모든 역할 또는 여러가지 역할을 수행하는 전지전능한 클래스 

 


O , 개방/폐쇄 원칙 (Open/Closed Principle, OCP)

이 원칙은 "소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장을 위해 개방되어야 하지만 수정을 위해 폐쇄되어야 한다" 라고 명시합니다. 즉, 클래스 동작을 수정하지 않고 확장할 수 있어야 합니다.

 

개발자 A가 만든 클래스에 개발자 B는 개발자 A가 만든 기존 클래스를 확장할 수 있지만 개발자 B는 기존 클래스를 직접 수정하지 않아야 합니다.

 

이 원칙을 사용하면 기존 코드와 수정된 코드를 분리하여 더 나은 안정성과 유지 관리성을 제공하고 코드에서 변경사항을 최소화 할 수 있습니다.

 


L , Liskov의 대체 원칙 (Liskov's Substitution Principle, LSP)

이 원칙은 Barbara Liskov가 1987년에 도입했으며 이 원칙은 "파생 또는 자식 클래스는 기본 또는 부모 클래스로 대체 가능해야 합니다." 라고 명시합니다.

즉, 부모 클래스의 자식인 모든 클래스가 예기치 않은 동작없이 부모 대신 사용할 수 있도록 해야합니다.

(부모와 자식을 바꿔도 부모의 동작을 대체할 수 있어야합니다.)

 

농부의 아들이 아버지로부터 농업기술을 물려받았을 때 필요한 경우 아들이 아버지를 대신할 수 있어야한다는 것입니다. 아들이 농부가 되고싶다면 아버지를 대신할 수 있지만 아들이 축구선수가 되고싶다면 아들은 같은 가족 계층에 속하더라도 아버지를 대신할 수 없습니다.

 

LSP는 위의 OCP(개방/폐쇄 원칙)을 위반하지 않도록 인도해주는 원칙입니다.

쉽게 저지를 수 있는 오류

전형적인 예로 네개의 면을 가진 직사각형 예제가 있습니다.

 

직사각형(Rectangle)의 높이는 모든 값이 될 수 있고 너비는 모든 값이 될 수 있습니다.

정사각형(Square)은 너비와 높이가 같은 직사각형 입니다.

그래서 우리는 직사각형 클래스의 속성을 정사각형 클래스로 확장할 수 있다고 말할 수 있습니다.

class Square extends Rectangle {
}

현실의 논리대로면 정사각형이 직사각형을 확장하는데 아무 문제가 없을것입니다. 실제로 Square is a Rectangle(정사각형은 직사각형이다) 도 성립합니다. 하지만 이렇게 하면 실제로 LSP를 위반하게 됩니다. 

 

그럼 코드로 이것을 직접 해보겠습니다.

 

아래는 직사각형 Rectangle 클래스와 정사각형 Square 클래스의 코드입니다.

실행 내용은 너비를 정하는 setHeight를 4 setWidth를 5로 설정하고 넓이를 구한 뒤 출력하는 것입니다.

 

package solid;

class Rectangle {
    public int width;
    public int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getArea() {
        return height * width;
    }
}

class Square extends Rectangle {

    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width;
    }

    @Override
    public void setHeight(int height) {
        this.height = height;
        this.width = height;
    }
}

public class LspExample {
    public static void main(String[] args) {
        Rectangle r = new Rectangle();
        r.setHeight(4);
        r.setWidth(5);
        int areaR = r.getArea();

        r = new Square();
        r.setHeight(4);
        r.setWidth(5);
        int areaS = r.getArea();

        System.out.println("Rectangle's area == 20 : " + (areaR == 20));
        System.out.println("Square's area == 20 : " +(areaS == 20));
    }
}

 

실행결과

위의 실행결과를 보면 실제로 정사각형(Square 클래스)은 직사각형(Rectangle 클래스)을 대체하지 못하고 있습니다.

똑같은 너비와 높이를 설정했음에도 정사각형의 넓이는 기대하는 값인 20이 아니라는 결과를 보여줍니다.

 

다른 결과가 나오는 이유는 정사각형의 setWidth(5)를 호출하는 순간 너비와 높이는 둘다 5로 설정됩니다.

 

이때 LSP에서 말하는 것은 본질적인(현실적인) 행위보다 클라이언트(코드의 사용자)에게 의존하는 행위를 더욱 중요시 한다는 점입니다.

 

즉, 현실에서 정사각형은 직사각형이다! 보다 getArea() 라는 메서드에서 "너비와 높이는 각각 독립적으로 설정될 수 있는 값이다" 라고 작성한 프로그래머의 행위를 더욱 중요시 한다는 것입니다.

 

다시 처음으로 돌아가서 "자식 클래스는 부모 클래스를 대체할 수 있어야한다" 라는 원칙을 생각해보면

정사각형 클래스는 직사각형 클래스를 대체하지 못한다고 말할 수 있습니다.

 

이런 LSP 원칙은 객체지향 디자인에서 상속에 대한 룰을 어느정도로 엄격하게 지키냐는 가치판단의 문제입니다.

상속에 대한 룰을 가장 엄격하게 적용하는 것이라 할 수 있습니다.

 

위의 사진을 보면 이해가 쉬울것이라 생각합니다.

"오리처럼 생겼고, 오리처럼 울음소리를 내지만, 배터리가 필요하다면 -> 당신은 아마 잘못된 추상화를 한것입니다."


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

이 원칙은 SOLID원칙 에서 클래스 대신 인터페이스에 적용되는 첫번째 원칙이며 단일 책임 원칙과 유사합니다.

 

"어떤 클라이언트도 그들과 무관한 인터페이스를 구현하도록 강요하지 마십시오." 라고 말합니다.

 

여기서 당신의 주요 목표는 뚱뚱한 인터페이스를 피하는데 집중하고 작고 세세한 클라이언트별 인터페이스를 선호하는 것입니다.

 

  • 하나의 일반 인터페이스보다 많고 작은 클라이언트 인터페이스를 선호해야합니다.
  • 각 인터페이스에는 특징이 있어야합니다.

만약 채식주의자 손님이라면 식당의 전체 메뉴판을 가질 필요없이 채식주의자용 메뉴판만 가지고 있으면 됩니다.

즉, 메뉴판는 고객 유형에 따라 달라야합니다.

 

모든 사람을 위한 전체 메뉴는 하나가 아닌 여러개의 메뉴로 나눌 수 있습니다.

(ex. 채식주의자 메뉴, 고기류 메뉴, 디저트 메뉴, 등등등)

 

이 원칙을 사용하면 필요한 변경의 부작용과 빈도를 줄이는데 도움이 됩니다.

 


D , 의존성 역전 원칙(Dependency Inversion Principle, DIP)

  • 고수준 모듈(또는 클래스)는 저수준 모듈(또는 클래스)에 의존해서는 안됩니다. 둘 다 추상화에 의존해야 합니다.
  • 추상화는 세부 사항에 의존해서는 안됩니다. 세부 사항은 추상화에 따라 달라집니다.

위의 내용은 단순히 상위 모듈(또는 클래스)이 하위 수준 모듈(또는 클래스)에 더 많이 의존하는 경우 코드가 강한 결합을 가지며 한 클래스에서 변경을 시도하면 다른 클래스를 손상시킬 수 있음을 나타냅니다.

그러니 추상화를 통해 가능한한 많이 느슨하게 결합된 클래스를 만들어야 합니다.

 

이 원칙의 주된 동기는 의존성을 분리하는 것이므로 클래스 A가 변경되면 클래스 B는 변경 사항에 신경을 쓰거나 알 필요가 없습니다.

 

예를들어 TV 리모컨에는 배터리가 필요하지만 배터리 브랜드에는 의존하지 않습니다.

어떤 브랜드의 배터리든 사용할 수 있으며 작동합니다. 그래서 우리는 TV 리모컨이 배터리 브랜드 이름과 느슨하게 결합되어 있다고 말할 수 있습니다.

 

아래는 배터리를 추상화하여 TV 리모컨과 느슨한 결합을 구현한 코드입니다. 해당 코드는 리모컨에 어떤 배터리를 넣든 잘 동작합니다.

package solid;

public class DipExample {
    public static void main(String[] args) {
        Battery battery = new NoBrand();
        new TvRemote(battery).doSomething();
    }
}

class TvRemote {
    Battery battery;

    public TvRemote(Battery battery) {
        this.battery = battery;
    }

    public void doSomething() {
        if (battery != null) {
            System.out.println("무슨 건전지든 잘 작동합니다.");
        }
    }
}

class Battery {

}

class Energizer extends Battery {

}

class Bexel extends Battery {

}

class NoBrand extends Battery {

}

 

 

이 원칙을 사용하면 코드의 재사용성을 증가시킬 수 있습니다.

 


참고문헌

- www.geeksforgeeks.org/solid-principle-in-programming-understand-with-real-life-examples/?ref=lbp

-

-

-

-

-

댓글