객체지향 프로그래밍의 5대 원칙은 로버트 마틴(Robert C. Martin)이 2000년대 초반에 정리한 원칙으로, 앞글자를 따서 'SOLID'라고 합니다. 이 원칙들은 유지보수가 쉽고, 확장 가능하며, 유연한 소프트웨어를 만들기 위한 지침입니다.
클래스는 오직 하나의 책임만 가져야 하며, 클래스를 변경하는 이유도 오직 하나여야 한다.
// 잘못된 예: UserService 클래스가 너무 많은 책임을 가짐
class UserService {
void createUser(User user) { /* ... */ }
void sendEmail(String to, String subject) { /* ... */ }
void generateReport() { /* ... */ }
}
// 개선된 예: 각 클래스가 하나의 책임만 가짐
class UserService {
void createUser(User user) { /* ... */ }
}
class EmailService {
void sendEmail(String to, String subject) { /* ... */ }
}
class ReportService {
void generateReport() { /* ... */ }
}
소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다.
// 잘못된 예: 새로운 도형을 추가할 때마다 클래스 수정 필요
class AreaCalculator {
double calculateArea(Object shape) {
if (shape instanceof Rectangle) {
Rectangle rectangle = (Rectangle) shape;
return rectangle.width * rectangle.height;
} else if (shape instanceof Circle) {
Circle circle = (Circle) shape;
return Math.PI * circle.radius * circle.radius;
}
return 0;
}
}
// 개선된 예: 인터페이스와 다형성 사용
interface Shape {
double calculateArea();
}
class Rectangle implements Shape {
double width;
double height;
@Override
public double calculateArea() {
return width * height;
}
}
class Circle implements Shape {
double radius;
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
// 새로운 도형을 추가해도 AreaCalculator 수정 불필요
class AreaCalculator {
double calculateArea(Shape shape) {
return shape.calculateArea();
}
}
상위 타입의 객체를 하위 타입의 객체로 치환해도 프로그램의 정확성은 변하지 않아야 한다.
// 잘못된 예: Rectangle을 상속한 Square가 LSP 위반
class Rectangle {
private double width;
private double height;
public void setWidth(double width) {
this.width = width;
}
public void setHeight(double height) {
this.height = height;
}
public double getArea() {
return width * height;
}
}
class Square extends Rectangle {
@Override
public void setWidth(double width) {
super.setWidth(width);
super.setHeight(width); // 정사각형이므로 가로=세로
}
@Override
public void setHeight(double height) {
super.setHeight(height);
super.setWidth(height); // 정사각형이므로 가로=세로
}
}
// 문제 발생 코드
void resizeRectangle(Rectangle rectangle) {
rectangle.setWidth(10);
rectangle.setHeight(20);
// Rectangle이면 area는 200이 되어야 하는데
// Square가 전달되면 area는 400이 됨 (마지막 setHeight가 width도 변경)
assert rectangle.getArea() == 200; // Square 객체가 전달되면 실패
}
클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.
// 잘못된 예: 거대한 인터페이스
interface Worker {
void work();
void eat();
void sleep();
}
// 일부 메서드만 필요한 클래스들이 불필요한 메서드에 의존하게 됨
class Robot implements Worker {
public void work() { /* ... */ }
public void eat() { /* 로봇은 먹지 않음 */ }
public void sleep() { /* 로봇은 자지 않음 */ }
}
// 개선된 예: 인터페이스 분리
interface Workable {
void work();
}
interface Eatable {
void eat();
}
interface Sleepable {
void sleep();
}
// 로봇은 필요한 인터페이스만 구현
class Robot implements Workable {
public void work() { /* ... */ }
}
// 사람은 모든 기능 필요
class Human implements Workable, Eatable, Sleepable {
public void work() { /* ... */ }
public void eat() { /* ... */ }
public void sleep() { /* ... */ }
}
고수준 모듈은 저수준 모듈에 의존해서는 안 됨. 둘 다 추상화에 의존해야 함. 또한 추상화는 세부 사항에 의존해서는 안 되며, 세부 사항이 추상화에 의존해야 함.
// 잘못된 예: 고수준 모듈이 저수준 모듈에 직접 의존
class LightBulb {
void turnOn() {
// 전구를 켜는 코드
}
void turnOff() {
// 전구를 끄는 코드
}
}
class Switch {
private LightBulb bulb; // 직접적인 의존
public Switch() {
this.bulb = new LightBulb(); // 강한 결합
}
void operate() {
// 전구 제어 로직
}
}
// 개선된 예: 의존성 역전 및 의존성 주입
interface Switchable {
void turnOn();
void turnOff();
}
class LightBulb implements Switchable {
@Override
public void turnOn() {
// 전구를 켜는 코드
}
@Override
public void turnOff() {
// 전구를 끄는 코드
}
}
class Fan implements Switchable {
@Override
public void turnOn() {
// 선풍기를 켜는 코드
}
@Override
public void turnOff() {
// 선풍기를 끄는 코드
}
}
class Switch {
private Switchable device; // 추상화에 의존
// 의존성 주입
public Switch(Switchable device) {
this.device = device;
}
void operate() {
// device 제어 로직
}
}
// 사용 예
Switch lightSwitch = new Switch(new LightBulb());
Switch fanSwitch = new Switch(new Fan());
SOLID 원칙은 완벽하게 지켜야 하는 규칙이라기보다 좋은 설계를 위한 지침으로 이해하는 것이 중요합니다. 상황과 맥락에 따라 적절하게, 또는 부분적으로 적용하는 것이 바람직합니다.