개발지식/Java

좋은 객체 지향 설계의 5가지 원칙(SOLID)

감발자 2023. 12. 30. 16:38

SOLID 설계원칙은 객체 지향 프로그래밍의 단골 면접 질문 중 하나라고 합니다. 

 

좋은 설계란 시스템에 새로운 요구사항이나 변경사항이 있을 때, 영향을 받는 범위가 적은 구조를 말한다. 그래서 시스템에 예상하지 못한 변경사항이 발생하더라도, 유연하게 대처하고 이후에 확장성이 있는 시스템 구조를 만들 수 있다.

즉, SOLID 객체 지향 원칙을 적용하면 코드를 확장하고 유지 보수 관리하기가 더 쉬워지며, 불필요한 복잡성을 제거해 리팩토링에 소요되는 시간을 줄임으로써 프로젝트 개발의 생산성을 높일 수 있다.

 

이 5가지 원칙들은 서로 독립된 개념이 아니라 개념적으로 연관되어 있다. 

 

 

(1)SRP : 단일 책임 원칙(Single Responsibility Principle)

// Before SRP
class MobilePhone {
    void makeCall() { /* ... */ }
    void takePhoto() { /* ... */ }
    void playMusic() { /* ... */ }
}

// After SRP
class Call {
    void makeCall() { /* ... */ }
}
class Camera {
    void takePhoto() { /* ... */ }
}

class MusicPlayer {
    void playMusic() { /* ... */ }
}
  • 한 클래스(객체)는 단 하나의 책임을 가져야 한다는 원칙 
  • 하나의 책임 ==  하나의 기능 담당
  •  변경이 있을 때 파급효과가 적으면 SRP를 따른 것으로 생각하자
  • 프로그램의 유지보수를 높이기 위한 설계 기법 

(2)OCP : 개방 폐쇄 원칙(Open Closed Principle)

// Before OCP
class Circle {
    double radius;

    double area() {
        return Math.PI * radius * radius;
    }
}

// After OCP
interface Shape {
    double area();
}

class Circle implements Shape {
    double radius;

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
}

class Rectangle implements Shape {
    double width;
    double height;

    @Override
    public double area() {
        return width * height;
    }
}
  • 확장에는 열려 있으나 변경에는 닫혀 있어야 한다는 원칙
  • 기능 추가 요청이 오면 클래스를 확장을 통해 손쉽게 구현하면서, 확장에 따른 클래스 수정은 최소화하도록 프로그램을 작성해야 하는 설계 기법 
    • [확장에 열려 있다] 새로운 변경 사항이 발생했을 때 유연하게 코드를 추가함으로써 큰 힘을 들이지 않고 애플리케이션 기능을 확장할 수 있음 
    • [변경에 닫혀 있다] 새로운 변경사항이 발생했을 때 객체를 직접적으로 수정을 제한함. 
  • 추상화 사용을 통한 관계 구축을 권장 
  • 다형성 확장 
  • 위와 같은 문제를 해결하기 위해 객체를 생성하고 연관관계를 맺어주는 외부 존재, 설정자가 필요 -> 스프링 컨테이너

(3)LSP : 리스코프 치환 원칙(Liskov substitution Principle)

class Bird {
    void fly() { /* ... */ }
}

class Sparrow extends Bird {
    @Override
    void fly() { /* ... */ }
}

class Ostrich extends Bird {
    // This class doesn't override fly()
}
  • 서브 타입은 언제나 기반(부모) 타입으로 교체할 수 있어야 한다는 원칙
  • 인터페이스 규약을 지켜 다형성을 활용해야 한다는 의미
  • 상위 클래스 타입으로 객체를 선언하여 하위 클래스의 인스턴스를 받으면, 업캐스팅 된 상태에서 부모의 메서드를 사용해도 동작이 의도대로 흘러가야 함
  • 예를 들어, 자동차 인터페이스의 엑셀 기능은 앞으로 가야 한다는 기능. 이를 구현한 구현체는 느리든 빠르든 앞으로 가야하는 기능의 원칙을 기준으로 가져야 함(뒤로 가면 LSP를 지키지 못했다는 것)

(4)ISP : 인터페이스 분리 원칙(Interface segregation Principle)

// Before ISP
interface Messaging {
    void send();
    void receive();
    void display();
}

// After ISP
interface Sender {
    void send();
}

interface Receiver {
    void receive();
}

interface Displayable {
    void display();
}
  • 인터페이스를 각각 사용에 맞게 잘 분리 해야 한다는 설계원칙 
  •  
  • 자동차 인터페이스 -> 운전 인터페이스, 정비 인터페이스로 분리
  • 사용자 클라이언트 -> 운전자 클라이언트, 정비사 클라이언트로 분리
  • 분리하면 서로 영향을 안 줌

(5)DIP : 의존관계 역전 원칙(Dependency Inversion Principle)

// Before DIP
class MusicPlayer {
    void playMP3() { /* ... */ }
}

class AudioApp {
    private MusicPlayer musicPlayer = new MusicPlayer();

    void playAudio() {
        musicPlayer.playMP3();
    }
}

// After DIP
interface AudioPlayer {
    void play();
}

class MP3Player implements AudioPlayer {
    @Override
    public void play() { /* ... */ }
}

class AudioApp {
    private AudioPlayer audioPlayer;

    AudioApp(AudioPlayer audioPlayer) {
        this.audioPlayer = audioPlayer;
    }

    void playAudio() {
        audioPlayer.play();
    }
}
  • 추상화에 의존해야지 구체화에 의존하면 안 됨
  • 클라이언트가 클래스가 아닌 인터페이스에 의존하라는 의미
  • 앞에서 이야기한 '역할'에 의존하게 해야한다는 것

다형성만으로는 OCP, DIP를 지킬 수 없다. 스프링 컨테이너라는 빈팩토리가 필요하다.

 

 

출처: https://inpa.tistory.com/entry/OOP-💠-객체-지향-설계의-5가지-원칙-SOLID [Inpa Dev 👨‍💻:티스토리]

출처: https://blog.stackademic.com/solid-principles-explained-with-real-time-examples-e39d1c167ba5