SOLID(객체지향 5대 원칙)란.
정리를 하게 된 이유.
- 이 원칙은 수도 없이 외우고 또 외웠었는데, 기억이 잘 나지 않아서 이번 기회에 새롭게 재정리를 해보려고 한다. 이 원칙을 지켜서 개발을 해보고 싶고, 리펙토링도 해보고싶은데 아쉬운 부분이 있어서 정리와 함께 앞으로 노력해보려고 한다. 학부시절 소프트웨어 공학때도 배웠고, 항상 감사하게 생각하고 있는 분에게 항상 질문을 받았었다.
SOLID
SOLID란 로버트 마틴이 2000년대 초에 명명한 객체 지향 프로그래밍의 다섯 가지 기본원칙을 마이클 페더스가 원칙의 앞글자를 따서 SOLID라는 이름으로 소개를 했다고 한다.
- 단일 책임 원칙(Single Responsibility Principle) SRP
- 개방 폐쇄 원칙(Open/Closed principle) OCP
- 리스코프 치환 원칙(Liskov Substitution Principle) LSP
- 인터페이스 분리 원칙(Interface Segregation Principle) ISP
- 의존관꼐 역전 원칙(Dependency Inversion Principle) DIP
위 5가지 원칙에 대해 정리를 해보려고 한다.
SRP : 단일 책임 원칙
- 말 그대로 단 하나의 책임만을 가져야 한다는 것을 의미한다. 책임의 기본 단위는 객체를 의미하며 하나의 객체가 하나의 책임을 가져야 한다는 의미다.
- 책임은 객체지향에 있어서 객체가 할 수 있는 것과 해야하는 것으로 나뉘어져 있다.
- 요약을 하자면 객체는 자신이 할 수 있는 것과 해야하는 것만 수행 할 수 있도록 설계되어야 한다는 법칙이다.
public class Student{
public void getCourse(){}
public void addCourse(){}
public void save(){}
public Student load(){}
public void printOnReportCard(){}
public void printOnAttendanceBook(){}
}
위 코드는 공부를 하면서 참고한 블로그에서 정리한 코드와 내용을 다시 한번 정리를 해보겠다. 학생이라는 클래스는 수강과목을 조회하고 추가하고 데이터베이스에 저장하고 저장된 학생을 불러오고 기록을 출력하는 책임을 갖고있다. 이렇게 하나의 클래스가 다양한 책임을 갖는 경우가 문제가 되는 이유는 변경이라는 관점에서 문제를 일으킨다.
수강과목을 조회하는 부분과 데이터베이스에서 학생 정보를 가져오는 코드는 어딘가에서 연결될 확률이 높다. 이러한 코드끼리의 결합은 하나의 변화에 대해 많은 병경사항을 발생기키고 관련된 모든 기능을 테스트해봐야 하는 단점이 있다. 이렇게 되며 유지보수 하기 어려운 대상이 된다. 따라서. 각 객체는 하나의 책임만을 수행 할 수 있도록 변경하는 것이 좋다.
위 클래스는 학생–학생DAO–성적표–출석부 등의 클래스를 통해 쪼개어 관리하는 것이 변경에 유연하게 대처할 수 있는 설계라고 할 수 있다. 단일책임에 적절하게 분리하면, 변경된 부분만을 수정할 수 있고 각각 의존하고 있는 영역이 줄어들어 변경에 유연한 대처가 가능해진다.
위 단일 책임 원칙을 회사 Controller에 적용을 해봤다.
OCP : 개방 폐쇄 원칙
- 개방 폐쇄 원칙이란 기존의 코드를 변경하지 않으면서 기능을 추가할 수 있도록 설계되어야 한다는 뜻이다. 요구사항이 변경되었을 때 코드에서 변경되어야 하는 부분과 변경되지 않아야 하는 부분을 명확하게 구분하여, 변경되어야 하는 부분을 유연하게 작성하는 것을 의미한다.
아래 참고한 블로그의 코드가 이해하기 쉬워 참고하여 재정리하며 알아보기로 한다.
public class Computer{
private KakaoMessenger kakaomessenger;
public static void main(String args){
Computer computer = new Computer();
computer.boot();
}
pricate void boot(){
kakaoMessenger = new KakaoMessenger();
kakaomessenger.boot();
}
}
public class KakaoMessenger{
public void boot(){
System.out.println("Kakao BOOTING...");
}
}
위 코드에선 카카오톡이 함께 실행되는 코드이다. 하지만 카카오를 쓰지 않고 라인을 사용한다는 변경이 생기면? 라인을 생성하고 라인에게 boot을 실행하라는 메시지를 보내야 할 것 이다. 외부의 변경사항에 의해 Production Code에 변경사항이 발생한다. 이런 문제를 해결하기 위해 아래와 같이 인터페이스를 통해 메신저를 분리해보자.
public class Computer{
private Messenger messenger;
public static void main(String[] args){
Computer computer = new Computer();
computer.setMessenger(new LineMessenger());
computer.boot();
}
pricate void setMessenger(Messenger messenger){
this.messenger = messenger;
}
private void boot(){
System.out.println("BOOTING....");
messenger.boot();
}
}
public class LineMessenger implements Messenger{
@Override
public void boot(){
System.out.println("Line BOOTING....");
}
}
public interface Messenger{
void boot();
}
어떤 메신저로 변경되어도 하나의 클래스만 추가하면서 외부의 변경에는 유연하게 대응 할 수 있고, OCP원칙을 지키는 코드를 완성 할 수 있다. 근데 아직 이 원칙에 대해 완벽히 이해하지 못했는데 코드가 변경됐는데…? 라는 생각이 들었다. 라인이라는 객체를 또 메인 안에서 바꿔서 사용해야하는거 아닌가?
- Production Code가 처음 이해 했을 땐, 메인에서 모든 로직과 메서드를 동작시키는 메인 클래스 코드라고 생각을 했다. Production Code란 기존에 짜여져 있는 코드를 의미하며 위의 코드에서 메신저가 어떤 메신저인지 판별하는 if 문을 통해 해결할 수도 있지만 이는 기존 코드의 전체 작동방식을 이해해야 할 수 있다.
- 하지만 위와 같이 인터페이스를 통해 분리하면 기존의 코드에는 변경이 없으며 단순히 하나의 클래스를 추가하고 오버라이딩만 해준다면 내부 동작원리를 알지 못해도 사용 할 수 있다.
LSP : 리스코프 치환 원칙
- 리스코프 치환 원칙은 자식 클래스는 최소한 자신의 부모 클래스에서 가능한 행위를 수행할 수 있어야 한다는 의미이다.
부모 클래스의 인스턴스 대신에 자식 클래스의 인스턴스로 대체해도 프로그램의 의미는 변화되지 않는다. 이를 위해 부모 클래스와 자식 클래스의 행위는 일관되어야 한다.
public class Bag{ private double price; public double getPrice(){ return price; } public void setPrice(double price){ this.price = price; } } public class DiscountedBag extends Bag{ private double discountRate; public void setDiscountRate(double discountRate){ this.discountRate = discountRate; } public void applyDiscount(int price){ super.setPrice(price - (int)(discountRate * price)); } }
Bag을 사용하고 있는 부분을 DiscountedBag으로 대체하여도 LSP에 위반되지 않는다. 부모의 기능을 오버라이딩 하지 않고 그대로 사용하고 있기 때문에 일반화 관계가 성립한다. 하지만 DiscountedBag 클래스에는 applyDiscount의 기능을 가지고 있다. 이 기능을 사용하게 되면 부모와 자식은 대체관계가 되지 않는다. 자식클래스가 부모클래스를 오버라이딩하거나 추가적인 기능을 통해 부모의 상태를 변경시키는 것은 LSP원칙을 위반하는 것.
LSP는 서브 클래스가 슈퍼 클래스의 책임을 무시하거나 재정의 하지 않고 확장만 수행한다는 것을 의미한다. 위의 코드에서는 부모의 변수를 재정의 하는 applyDiscount가 있기 때문에 부모가 수행하고 있는 책임을 그대로 수행하면서, 추가적인 필드나 기능을 제공하려는 경우에만 상속을 하는 것이 바람직하며 부모 클래스의 책임을 변화시키는 기능은 LSP법칙에 위배 된다고 볼 수 있다.
ISP : 인터페이스 분리 원칙
-
인터페이스 분리 원칙은 클라이언트에서는 클라이언트 자신이 이용하지 않는 기능에는 영향을 받지 않아야 한다는 내용이다. 에를 들어 복합기를 이용하는 다양한 사람들을 생각을 해보면, 복사를 하고싶은 사람과 프린트를 하고 싶은 사람, 팩스를 보내고 싶은 사람은 복합기가 다양한 기능을 ㅔㅈ공하고 있지만 본인이 원하는 기능만 작동되면 자신이 이요하지 않는 기능에 대해서는 영향을 받지 않는다. 이러한 기능을 제공하고 싶을 때 사용되는 것이 ISP이며 사용 방법은 범용의 인터페이스를 만드는 것이 아니라, 클라이언트에 특화된 인터페이스를 사용해야한다. 인터페이스를 클라이언트에 특화되도록 분리시키라는 설계원칙이다. ISP와 SRP는 동일한 문제에 대해 다른 해결책을 제시하고 있는 것과 비슷하다. 하나의 클래스가 기능이 비대하다면 책임을 분할하라는 것이 SRP 비대한 기능을 인터페이스로 분할하여 사용하라는 것이 ISP이다.
-
예를 들어 게시판의 여러 기능을 구현한 메서드를 제공하는 클래스에는 CRUD가 제공된다. 그러나 클라이언트에 따라서 게시판의 이러한 기능 중 일부분만 사용할 수 있고 관리자는 모든 기능을 사용할 수 있다고 가정하자. 게시판은 관련된 책임을 수행하므로 SRP를 만족하지만, 이 클래스의 모든 메서드가 들어있는 인터페이스가 클라이언트와 관리자 모두가 사용한다면 ISP에는 위배된다. 이 경우 관리조용 인터페이스와 일반 인터페이스를 분리함으로써 ISP의 위반 또한 함께 해결 할 수 있다.
DIP : 의존 역전 원칙
-
객체지향 프로그래밍에서 객체는 서로 도움을 주고 받으며 의존 관계를 발생시킨다. 이런 의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것 보다는 변화하기 어려운 것, 거의 변화가 없는 것에 의존하라는 가이드라인을 제공하는 원칙이다.
-
DIP를 만족하려면 어떤 클래스가 도움을 받을 때 혹은 의존할 때 구체적인 클래스는 변화할 확률이 높기 때문에 이를 추상화한 인터페이스나 추상 클래스와 의존 관계를 맺도록 설계해야 한다. 컴퓨터와 키보드의 에제에서도 단순히 한 제조사의 키보드가 아닌 전체 키보드와 의존관계를 맺음으로써 DIP원칙을 준수한 것이다.
public class kid{
private Toy toy;
public void setToy(Toy toy){
this.toy = toy;
}
public void play(){
System.out.println(toy.toStirng());
}
}
public class Lego extend Toy{
@Override
public String toString(){
return "Lego";
}
}
public abstract Toy{
public abstract String toString();
}
public class Main{
public static void main(String[] args){
Kid kid = new Kid();
kid.setToy(new Lego());
kid.play();
}
}
위와 같이 변경될 수 있는 장난감은 abstract class 혹은 interface를 통해 관리함으로써 변경사항에 대해서 유연하게 대처할 수 있고 변화하는 부분을 추상화하여 변화되지 않는 형태로 만든 추상클래스를 의존하기 때문에 DIP 원칙과 OCP 둘다 만족하는 형태를 갖는다.