본문 바로가기
📖 책책책 책을 읽읍시다. 📖/자바 객체 지향의 원리와 이해

Chapter 5. 객체 지향 설계 5원칙 - SOLID

by 컴쏘 2024. 11. 26.

 

객체 지향 언어를 통해 객체 지향 프로그램을 올바르게 설계해 나가는 방법이나 원칙에 대해 알아보자. 

 

SOLID에 대해 간략하게 보면 다음과 같다. 

  • SRP(Single Responsibility Principle) : 단일 책임 원칙 
  • OCP(Open Closed Principle) : 개방 폐쇄 원칙 
  • LSP(Liskov Substitution Principle) : 리스코프 치환 원칙 
  • ISP(Interface Segregation Principle) : 인터페이스 분리 원칙 
  • DIP(Dependency Inversion Principle) : 의존 역전 원칙 

위의 원칙들은 응집도는 높이고, 결합도는 낮추라(개인적으로 개발을 하면서 머리에 늘 새기는 것이다..🥸)는 고전 원칙을 객체 지향의 관점에서 재정립한 것이다. 

 

+) 결합도와 응집도

더보기
더보기

좋은 소프트웨어 설계를 위해서는 결합도는 낮추고 응집도는 높이는 것이 바람직하다. 

  • 결합도 : 모듈(클래스) 간의 상호 의존 정도로서 결합도가 낮으면 모듈 간의 상호 의존성이 줄어들어 객체의 재사용이나 수정, 유지보수가 용이하다. 
  • 응집도 : 하나의 모듈 내부에 존재하는 구성 요소들이 기능적 관련성으로, 응집도가 높은 모듈은 하나의 책임에 집중하고 독립성이 높아져 재사용이나 기능의 수정, 유지보수가 용이하다. 

 

SRP - 단일 책임 원칙

어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다. 

 

SRP는 클래스는 단 하나의 책임만 가져야 한다는 것을 의미이다. 

 

 

다음과 같은 경우는 단일 책임 원칙을 지키지 않은 경우이다. 

  • 하나의 클래스가 여러 책임을 갖는 경우
  • 하나의 속성이 여러 의미를 갖는 경우 
    • 이때는 if 문을 사용하게 된다. (단일 책임 원칙을 준수하지 않은 경우)
    • 하나의 속성에 여러 의미가 포함되면 해당 속성이 담당하는 책임이 명확하지 않기 때문에 여러 경우에 따라 다른 동작을 해야하기 때문이다. 

 

단일 책임 원칙과 관계가 깊은 4대 원칙은 추상화이다. 

  • 애플리케이션의 경계를 설정하고, 추상화를 통해 클래스들을 선별하고 속성과 메서드를 설계할 때 반드시 단일 책임 원칙을 고려하는 습관을 들이자. 
  • 리팩토링할 때도 단일 책임 원칙을 적용할 곳이 있는지 살펴보자. 

 

OCP - 개방 폐쇄 원칙

소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하고, 변경에 있어서는 닫혀있어야 한다. 

 

개방 폐쇄 원칙이 잘 적용된 곳은 JDBC이다. 

  • JDBC를 사용하는 클라이언트데이터베이스가 오라클에서 MySQL로 바뀌더라도 Connection을 설정하는 부분 외에는 따로 수정할 필요가 없다. 
  • Connection 설정 부분을 별도의 설정 파일로 분리해두면 클라이언트 코드는 단 한 줄로 변경할 필요가 없다. 
  • JDBC뿐만 아니라 MyBatis, 하이버네이트 등등 데이터베이스 프로그래밍을 지원하는 라이브러리와 프레임워크에서도 개방 폐쇄 원칙을 볼 수 있다. 

 

Java개방 폐쇄 원칙이 잘 적용된 예시 중 하나이다. 

  • Java 개발자는 작성하고 있는 소스코드가 윈도우에서 구동될 지, 리눅스에서 구동될 지 또는 또 다른 운영체제에서 구동될 지에 대해서는 걱정하지 않는다. 
  • 각 운영체제 별 JVM과 목적파일(.class)가 있기에 개발자는 다양한 구동 환경에 대해서는 걱정하지 않고 본인이 작업하고 있는 개발 PC에 설치된 JVM에서 구동되는 코드만 작성하면 된다. 
  • 개발자가 작성한 소스 코드에는 운영체제의 변화에 닫혀있고, 각 운영체제별 JVM은 확장에 열려있는 구조가 되는 것이다. 
    • 목적파일이라는 완충 장치가 있다.

 

개방 폐쇄 원칙을 지키지 않으면, 객체 지향의 가장 큰 장점인 유연성, 재사용성, 유지보수성 등을 얻을 수 없다. 

 

추가적으로 스프링 프레임워크도 개방 폐쇄 원칙의 좋은 예이다. (스프링의 DI, AOP,...)

 

 

LSP - 리스코프 치환 원칙

서브 타입은 언제나 자신의 기반 타입으로 교체할 수 있어야 한다. 

 

상속에서 객체 지향의 상속은 조직도나 계층도가 아닌 분류도라고 했다. (참고)

따라서, 객체 지향의 상속은 다음 조건을 만족해야 한다. 

  • 하위 클래스 is a kind of 상위 클래스 - 하위 분류는 상위 분류의 한 종류이다. 
  • 구현 클래스는 is able to 인터페이스 - 구현 분류는 인터페이스할 수 있어야 한다. 

 

LSP를 좀 더 풀어서 설명해보면 다음과 같다. 

  • 하위 클래스의 인스턴스상위형 객체 참조 변수에 대입상위 클래스의 인스턴스 역할을 하는 데 문제가 없어야 한다. 
Animal pingu = new Penguin();

 

 

리스코프 치환 원칙과 관련된 규칙은 다음과 같다. 

  • 하위형에서 선행 조건은 강화될 수 없다. 
    • 하위 클래스에서 메서드를 오버라이드할 때, 메서드가 호출되기 위한 입력 조건(선행 조건, precondition)을 상위 클래스보다 더 엄격하게 만들 수 없다.
class BankAccount {
    protected double balance;

    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("입금액은 양수이어야 합니다.");
        }
        balance += amount;
    }
}

class SavingsAccount extends BankAccount {
    @Override
    public void deposit(double amount) {
        if (amount < 1000) { // 상위 클래스에서는 허용하던 작은 금액을 제한 (선행 조건 강화)
            throw new IllegalArgumentException("최소 1000원 이상 입금해주세요.");
        }
        balance += amount;
    }
}

 

  • 하위형에서 후행 조건은 약화될 수 없다. 
    • 하위 클래스에서 메서드를 오버라이드할 때, 메서드가 호출된 후에 보장해야 할 출력 조건(후행 조건, postcondition)을 상위 클래스보다 더 약하게 만들 수 없다.
class Rectangle {
    public int getArea() {
        return width * height;
    }
}

class Square extends Rectangle {
    @Override
    public int getArea() {
        return 0; // 후행 조건 약화: 면적 계산이 올바르지 않음
    }
}

 

  • 하위형에서 상위형의 불변 조건은 반드시 유지돼야 한다. 
    • 상위 클래스의 불변 조건(invariant), 즉 항상 유지되어야 할 조건은 하위 클래스에서도 반드시 유지되어야 한다.
class BankAccount {
    protected double balance;

    public void deposit(double amount) {
        if (amount < 0) {
            throw new IllegalArgumentException("음수는 입금할 수 없습니다.");
        }
        balance += amount;
    }
}

class OverdraftBankAccount extends BankAccount {
    @Override
    public void deposit(double amount) {
        // 불변 조건 위반: 음수 금액 허용
        balance += amount;
    }
}

 

 

ISP - 인터페이스 분리 원칙

클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안된다. 

 

ISP는 SRP와 같은 문제(큰 규모의 클래스를 잘게 나누어 각각 하나의 역할을 수행)에 대한 해결 방안이다.

  • 개발자의 취향에 따를 수 있지만, SRP가 더 좋은 해결책이라고 한다. 

 

인터페이스 분할 원칙을 이야기 할 때, 같이 등장하는 것은 인터페이스 최소 주의 원칙이다. 

 

인터페이스를 통해 메서드를 외부에 제공할 때는 최소한의 메서드만 제공하라는 것이다. (역할에 충실한 최소한의 기능만 공개)

  • 인터페이스에 너무 많은 메서드가 포함되면, 클라이언트는 사용하지 않는 메서드에도 의존하게 되어 코드의 결합도가 높아지고 유지보수가 어려워진다. 

 

DIP - 의존 역전 원칙 

고차원 모듈은 저차원 모듈에 의존해서는 안된다. 이 두 모듈 모두 다른 추상화된 것에 의존해야 한다. 
추상화된 것은 구체적인 것에 의존하면 안된다. 구체적인 것이 추상화 된 것에 의존해야 한다. 
자주 변경되는 구체 클래스에 의존하지 마라. 

 

DIP는 자신보다 변하기 쉬운 것에 의존하던 것을 추상화된 인터페이스나 상위 클래스를 두어 변하기 쉬운 것의 변화에 영향 받지 않게 하는 것이다. 

  • 상위 클래스일수록, 인터페이스일수록, 추상 클래스일수록 변하지 않을 가능성이 높다. 
  • 만약 DIP를 위반했다면 다음과 같은 상황이 발생한다. 
    • 결제 방식의 상위 모듈이 있고, 신용 카드 결제의 하위 모듈이 있다. 
    • 결제 방식이 신용 카드 결제의 하위 모듈의 구현에 직접 의존한다면, 
    • 결제 방식에 간편 결제를 추가하려고 할 때, 상위 모듈인 결제 방식을 수정해야 한다.  
    • 하위 모듈의 변경이 상위 모듈에 영향을 미치게 되고, 결합도가 높아지게 된다. 

 

 

SOLID를 하면 빼놓을 수 없는 것이 SoC(Separation of Concerns: 관심사 분리)이다. 

  • 관심사(Concern)를 분리해 코드의 응집도를 높이고 결합도를 낮추는 설계 원칙이다.
  • 관심사(Concern): 소프트웨어에서 특정한 역할, 책임, 기능을 의미한다. 예를 들어, 사용자 인터페이스, 데이터베이스 처리, 비즈니스 로직 등이 각각의 관심사이다.
  • 관심사의 분리(Separation): 서로 다른 관심사를 별도의 모듈, 클래스, 계층으로 분리하여 설계한다. 이를 통해 코드의 가독성, 유지보수성, 재사용성을 높일 수 있다.

SoC를 지키려고 하면, SOLID는 따라온다. 

 

 

 

SOLID 원칙을 적용하면 소스 파일의 개수는 더 많아지는 경향이 있다. 많아진 파일들이 논리를 더 잘 분할하고, 잘 표현하기에 이해가 쉽고 개발과 유지보수가 쉬워진다.