개발지식/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