SOLID 원칙
December 14, 2015
프로그래밍 설계를 하다보면 객체지향 5대원칙 또는 SOLID 원칙이란 단어를 들어본 적이 있을 것이다. 당시에 구글링을 하여 찾아보았지만 프로그래밍 내공이 부족하여 잘 이해가 되지 않았다. 그때보다 조금(?) 나아진 현재 다시 공부하면서 나름대로 이해한 내용을 여기에 정리해 보고자 한다.
객체지향 5대 원칙이란,
SRP(단일 책임 원칙), OCP(개방-폐쇄 원칙), LSP(리스코프 치환 원칙), DIP(의존 역전 원칙), ISP(인터페이스 분리 원칙)을 말하며, 앞자를 따서 SOILD 원칙이라고 부른다. 프로그래머가 시간이 지나도 유지 보수와 확장이 쉬운 소프트웨어를 만드는데 이 원칙들을 적용할 수 있다.
1. Single Responsiblity Principle (단일 책임 원칙)
소프트웨어의 설계 부품(클래스, 함수 등)은 단 하나의 책임만을 가져야 한다.
여기서 책임이란, ‘기능’ 정도의 의미로 해석하면 된다.
설계를 잘한 프로그램은 기본적으로 새로운 요구사항과 프로그램 변경에 영향을 받는 부분이 적다. 다시말해, 응집도는 높고 결합도는 낮은 프로그램을 뜻한다. 만약 한 클래스가 수행할 수 있는 기능, 즉 책임이 많아진다. 책임이 많아지면 클래스 내부의 함수끼리 강한 결합을 발생할 가능성이 높아진다. 이는 유지보수에 비용이 증가하게 되므로 따라서 책임을 분리시킬 필요가 있다.
2. Open-Closed Principle (개방-패쇄 원칙)
기존의 코드를 변경하지 않고(Closed) 기능을 수정하거나 추가할 수 있도록(Open) 설계해야 한다.
OCP에 만족하는 설계를 할 때 변경되는 것이 무엇인지에 초점을 맞춘다. 자주 변경되는 내용은 수정하기 쉽게 설계 하고, 변경되지 않아야 하는 것은 수정되는 내용에 영향을 받지 않게 하는 것이 포인트다. 이를 위해 자주 사용되는 문법이 인터페이스(Interface)이다. 다음 예제를 함께 보자.
class SoundPlayer {
void play() {
System.out.print("play wav"); // wav 재생
}
}
pulbic class Client {
public static void main(String[] args){
SoundPlayer sp = new SoundPlayer();
sp.play();
}
}
SoundPlayer 클래스는 음악을 재생해주는 클래스이다. 이 클래스는 기본적으로 wav파일을 재생할 수 있다. 그러나 SoundPlayer가 다른 포맷의 파일, 예를 들어 Mp3 파일을 재생하도록 요구사항이 변경 되었다고 하자. 요구사항을 만족 시키기 위해서는 SoundPlayer의 play() 메소드를 수정하여야 한다. 그러나 이러한 소스코드 변경은 OCP 원칙에 위배된다.
그렇다면 어떻게 해야 OCP 원칙을 만족시킬 수 있을까? 다양한 방법이 있지만 여기선 앞에서 언급한 인터페이스를 이용하여 OCP를 만족시켜 보자. 먼저 변해야 하는것은 무엇인지 정의한다. 위 클래스에서는 play() 메소드가 변해야 하는 것이다. 따라서 play() 메소드를 인터페이스로 분리한다.
interface playAlgorithm {
public void play();
}
class Wav implements playAlgorithm {
@Override
public void play() {
System.out.println("Play Wav");
}
}
class Mp3 implements playAlgorithm {
@Override
public void play() {
System.out.println("Play Mp3");
}
}
일단 재생하고자 하는 파일 클래스(Wav, Mp3)를 만들어 PlayAlgorithm 인터페이스의 play() 메소드를 재정의하도록 설계한다.
class SoundPlayer {
private playAlgorithm file;
public void setFile(playAlgorithm file) {
this.file = file;
}
public void play() {
file.play();
}
}
pulbic class Client {
public static void main(String[] args){
SoundPlayer sp = new SoundPlayer();
sp.setFile(new Wav());
sp.setFile(new Mp3());
sp.play();
}
}
SoundPlayer 클래스에서는 playAlgorithm 인터페이스를 멤버 변수로 만든다. 그 후 SoundPlyaer의 play() 함수는 인터페이스를 상속받아 구현된 클래스의 play()함수를 실행시키게 한다. 마지막으로 메인함수에서 setter를 이용하여 우리가 플레이하고자 하는 파일의 객체를 지정해주면 된다. 앞에서 언급하진 않았지만 이와 같은 설계를 디자인 패턴에서는 Strategy Pattern(전략 패턴)이라고 한다. 결과적으로 우리는 SoundPlayer 클래스의 변경 없이 재생되는 파일을 바꿀 수 있으므로 위 코드는 OCP를 만족한다. 앞서 말했듯이 OCP를 만족한 설계는 변경에 유연하므로 유지보수 비용을 줄여주고 코드의 가독성 또한 높아지는 효과를 얻을 수 있다.
3. Liskov Substitution Principle (리스코프 치환 원칙)
자식 클래스는 부모클래스에서 가능한 행위를 수행할 수 있어야 한다.
리스코프 치환 원칙은 MIT 컴퓨터 사이언스 교수인 리스코프가 제안한 설계 원칙이다. 부모 클래스와 자식 클래스 사이의 행위에는 일관성이 있어야 한다는 원칙이며, 이는 객체 지향 프로그래밍에서 부모 클래스의 인스턴스 대신 자식 클래스의 인스터스를 사용해도 문제가 없어야 한다는 것을 의미한다.
상속 관계에서는 일반화 관계(IS-A)가 성립해야 한다. 일반화 관계에 있다는 것은 일관성이 있다는 것이다. 따라서 리스코프 치환 원칙은 일반화 관계에 대해 묻는 것이라 할 수 있다.
이해를 돕기위해 도형을 예시를 들어보자. 도형 클래스와 사각형 클래스가 있고, 사각형 클래스는 도형 클래스의 상속을 받는다고 가정하자.
(1) 도형은 둘레를 가지고 있다.
(2) 도형은 넓이를 가지고 있다.
(3) 도형은 각을 가지고 있다.
일반화 관계(일관성인지 확인하는 방법은 단어를 교체해 보면 알 수 있다. (1) ~ (3)의 도형이란 단어 대신 사각형을 넣어보자.
(1) 사각형은 둘레를 가지고 있다.
(2) 사각형은 넓이를 가지고 있다.
(3) 사각형은 각을 가지고 있다.
(1) ~ (3) 모두 딱히 이상한 부분이 보이지 않는다. 따라서 도형과 사각형 사이에는 일관성이 있다고 할 수 있다.
여기서 원(Circle) 이라는 도형에 대해 생각해보자. 원 클래스 역시 도형 클래스의 상속을 받는다고 가정하자. 앞에서 언급한 (1) ~ (3)의 도형 단어 대신 원을 대입해보자.
(1) 원은 둘레를 가지고 있다.
(2) 원은 넓이를 가지고 있다.
(3) 원은 각을 가지고 있다.
문장을 읽어보면 (3)번 문장이 어색하다는 것을 알 수 있다. 따라서 도형 클래스는 LSP을 만족하지 않은 설계라 할 수 있다. 따라서 (3)문장에 대해서는 일반화 관계가 성립하도록 수정되어야 한다.
4. Dependency Inversion Principle (의존 역전 원칙)
의존 관계를 맺을 때, 변화하기 쉬운것 보단 변화하기 어려운 것에 의존해야 한다.
여기서 말하는 변화하기 쉬운것이란 구체적인 것을 말하고, 변화하기 어려운 것이란 추상적인 것을 말한다. 객체지향적인 관점에서 보자면 변화하기 쉬운것이란 구체화 된 클래스를 의미하고, 변화하기 어려운 것은 추상클래스나 인터페이스를 의미한다. 따라서 DIP를 만족한다는 것은 의존관계를 맺을 때, 구체적인 클래스보다 인터페이스나 추상 클래스와 관계를 맺는다는 것을 의미한다.
DIP를 만족하면 의존성을 외부에서 전달해주는 방법으로 유연한 설계를 할 수 있다. 앞에 언급한 SoundPlayer 클래스 다시 보자.
class SoundPlayer {
private playAlgorithm file;
public void setFile(playAlgorithm file) {
this.file = file;
}
public void play() {
file.play();
}
}
pulbic class Client {
public static void main(String[] args){
SoundPlayer sp = new SoundPlayer();
sp.setFile(new Wav());
sp.setFile(new Mp3());
sp.play();
}
}
우리는 setFile 클래스를 이용하여 실행하고자 하는 파일을 쉽게 바꿀 수 있다. 마찬가지로 새로운 오디오 파일 포맷(예를들면 FLAC)을 실행시키고자 한다면, 새로운 클래스(FLAC)를 만든 후 play 인터페이스를 상속받아 구현한 후 setFile 메소드를 이용하여 file 멤버 변수에 주입시키면 된다. 이와같은 기술을 ‘의존성 주입’ 이라 한다.
5. Interface Segregation Principle (인터페이스 분리 원칙)
한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다. 하나의 일반적인 인터페이스보다는, 여러 개의 구체적인 인터페이스가 낫다.
다시 말해서, 자신이 사용하지 않는 기능(인터페이스)에는 영향을 받지 말아야 한다는 의미이다.
한가지 예를 들어보자. 우리는 스마트폰으로 전화, 웹서핑, 사진 촬영 등 다양한 기능을 사용할 수 있다. 그런데 전화를 할 때에는 웹서핑, 사진촬영 등 다른 기능은 사용하지 않는다. 따라서 전화기능과 웹서핑 기능 사진 촬영 기능은 각각 독립된 인터페이스로 구현하여, 서로에게 영향을 받지 않도록 설계해야 한다. 이렇게 설계된 소프트웨어는 인터페이스 분리 원칙을 통해 시스템의 내부 의존성을 약화시켜 리팩토링, 수정, 재배포를 쉽게 할 수 있다.