Soyeon Park 님의 블로그
객체지향 설계 원리 본문
객체지향 언어가 도입되면서 전통적 설계 원리인 추상화라는 개념은 인터페이스와 구현의 분리로 확장되었다.
인터페이스는 공개된 메서드의 프로토타입(추상)만을 정의해 놓은 것이다. 대부분의 클래스는 메서드의 구현을 포함하는데 여기에서 공개된 메서드를 인터페이스로 따로 정의하고 이를 구현 상속한 것으로 관계를 맺는다. 객체지향 다형성을 이용하다면, 인터페이스로 상속된 여러 구현을 가질 수 있다. 하나의 추상 개념을 필요에 따라 여러 버전으로 구체화하는 것이다.
컴포넌트의 공개 인터페이스(사용자가 알아야 하는 부분)는 컴포넌트가 어떻게 구현되는지와 분리하자는 것이다. 인터페스와 분리된 구현은 쉽게 변경할 수 있다.
캡슐화 원리는 구현 세부 사항을 숨기도록(정보은닉) 한다. 인터페이스 분리 원리는 추상화를 지시한다.
1. 단일 책임 원리 Single Responsibility Principle
클래스의 역할과 책임을 단순화해야 한다. 클래스를 변경해야하는 이유가 오직 하나 뿐이어야 한다. 관련 없는 책임은 다른 클래스로 분리하자.
2. 개방 폐쇄의 원리 Open Close Principle
OCP는 소프트웨어 개체가 확장을 위해서는 열려있어야 하지만, 수정을 위해서는 닫혀야 한다는 것이다. 다형성에 대해서 생각해보자. 상속을 이용하여 클래스가 정의되어 있을 때, 다형성이 적용되어 서로 대체할 수 있는 인터페이스 "구현"이 될 수 있다. 클래스 자체를 수정하지 않고 확장할 수 있다. Client에서 접근하는 쪽은 수정이 닫혀있고, 내부 구현 모듈이 확장된다.
3. 리스코프 교체의 원리 Liskov Substitution Principle
상위 클래스A를 상속 받은 하위 클래스B는 프로그램 동작을 방해하지 않고 A를 B로 대체할 수 있어야 한다. 상위 클래스에 하위 클래스를 넣어도 문제가 없어야 한다는 뜻이다.
fly() 메서드를 포함한 Bird() 상위 클래스를 가정하자. 이를 상속 받아서 구현하는 참새() 클래스와 Ostrich() 클래스가 있다. 참새 클래스는 fly()를 구현할 수 있다. 그러나 타조는 날지 못하므로 fly()를 구현하지 않고, Exception을 날린다. Ostrich 클래스는 자신의 부모 클래스를 대체하지 못하므로, LSP에 위배된다. 구현하지 못하는 메서드에 대해서 다 예외를 줄 것인가? 인터페이스 구현으로 해결할 수 있다. flyable을 <<interface>>로 두자. fly() 를 그냥 상속받는게 아니라 interface로 두고, Bird()에서는 모든 새가 가지고 있는 공통 속성만을 상속받자는 것이다. 할 필요 없는 Exception을 방지하자.
class Bird {
void layEggs() { ... } // 모든 새의 진짜 공통점
}
interface Flyable {
void fly(); // '나는 능력'은 인터페이스로 분리
}
class Sparrow extends Bird implements Flyable {
public void fly() { System.out.println("참새는 난다."); }
}
class Ostrich extends Bird {
// 억지로 fly()를 구현할 필요도 없고, 에러를 던질 필요도 없다.
}
Rectangle 클래스는 width와 height를 가지고 있다. getArea()를 w*h로 구한다. Square 클래스는 Rectangle 클래스를 상속받는다. 그러나 getArea가 다르게 동작한다. 어떻게 해결할 수 있을까? 아래처럼 할 수 있을 것 같다.
interface Shape {
int getArea();
}
class Rectangle implements Shape {
private int width, height;
public void setWidth(int w) { this.width = w; }
public void setHeight(int h) { this.height = h; }
public int getArea() { return width * height; }
}
class Square implements Shape {
private int side;
public void setSide(int s) { this.side = s; }
public int getArea() { return side * side; }
}
4. 인터페이스 분리의 원리 Interface Segregation Principle
ISP는 클라이언트가 사용하지 않는 인터페이스를 강제로 구현해서는 안된다는 것이다. 사용하지 않는 메서드가 있다면 fat interface다. 다수의 작은 인터페이스를 만들어 필요한 것만을 사용하도록 해야한다.
Shape 인터페이스에는 면적 계산하는 area()와 부피 계산하는 volume() 메서드가 있다. Cylinder, Square, Circle 클래스는 Shape 인터페이스를 구현한다. 그런데, Square와 Circle은 2D라서 volume()이 필요 없다. 그러면 fat interface가 되는 것이다.
Shape 인터페이스에는 area()만 두고, 3D Shape 인터페이스를 따로 둬서 Shape를 확장하자. 위에서, flyable 인터페이스를 따로 뺀 것과 동일하다.
5. 의존 관계 역전의 원리 Dependency Inversion Principle
높은 수준의 모듈은 변화가 많은 하위 모듈에 쉽게 영향을 받으면 안된다. 하위 모듈에 의존되지 않고 추상적인 부분에 의존되어야 한다. 상위클래스는 하위클래스에 끌려다니면 안된다는 것이다.
날씨 데이터를 가져오는 WeatherService 상위 클래스가 있다. 낮은 수준의 OpenWeatherMapAPI를 직접 의존한다면 이 모듈이 바뀔 때마다 문제가 생긴다. 따라서 중간에 interface를 하나 둬서, 영향을 받지 않도록 끊어준다. 즉, 높은 수준의 모듈이 영향을 받지 않으려면 높은 수준의 모듈과 낮은 수준의 모듈을 서로 분리하는 추상화를 도입하여야 한다. 높은 수준의 모듈이 낮은 수준의 추상화된 인터페이스에 의존하게 해야한다.
'소프트웨어공학' 카테고리의 다른 글
| GoF Design Pattern (0) | 2026.04.18 |
|---|---|
| SOLID - 코드로 이해하기 (0) | 2026.04.14 |
| 결합도와 응집도 (Coupling and Cohension) (0) | 2026.04.14 |
| 전통적인 설계 원리 (0) | 2026.04.13 |