객체지향 설계는 긴 세월과 수많은 시행착오를 거치며 5가지 원칙이 정리되었다. 이것은 객체지향 설계의 5원칙이라고 하며, 앞글자를 따서 SOLID라고 한다.
SPR(Single Responsibility Principle) : 단일 책임 원칙
OCP(Open Closed Principle) : 개방 폐쇄 원칙
LSP(Liskov Substitution Principle) : 리스코프 치환 원칙
ISP(Interface Segregation Principle) : 인터페이스 분리 원칙
DIP(Dependency Inversion Principle) : 의존 역전 원칙
이 원칙들은 응집도는 높이고 결합도는 낮추자는 고전 원칙을 객체 지향의 관점에서 재정립한 것으로 볼 수 있다.
한 클래스는 하나의 책임만 가져야 한다.
위 그림은 아래 처럼 분리해야한다.
사진 출처 - SLENDER ANKLES's 개발블로그
중요한 기준은 변경이다. 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것
예) UI 변경, 객체의 생성과 사용을 분리
소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하지만 변경에 대해서는 닫혀 있어야 한다. 즉, 자신의 확장에는 열려있고, 주변의 변화에 대해서는 닫혀 있어야 한다는 것이다. 이것은 interface 를 통해 구현하여 해결한다.
public class MemberServiceImpl implements MemberService{
// 여기서 MemberRepository는 인터페이스
//private MemberRepository memberRepository = new MemoryMemberRepository();
private MemberRepository memberRepository = new JdbcMemberRepository();
}
jdbc로 변경하려면 코드 수정이 불가피하다.
구현 객체를 변경하려면 클라이언트 코드를 변경해야 한다.
분명 다형성을 사용했지만 OCP 원칙을 지킬 수 없다
객체를 생성하고, 연관관계를 맺어주는 별도의 조립, 설정자가 필요하다.
//설정자
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(new JdbcMemberRepository());
}
}
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
위 코드 AppConfig클래스라는 설정자클래스에서만 구현 객체를 갈아끼워주면 기존의 코드변경은 필요없다.
얼마든지 클래스를 만들어서 확장시킬순있지만, 기존의 코드에 변경이없으므로 OCP 원칙에 위배되지않는다.
고로 확장에는 열려있으며, 변경에는 닫혀있다.
서브타입은 언제나 자신의 기반타입으로 교체할 수 있어야 한다. 즉, 하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 수행하는 데 문제가 없어야 한다.
Person 멍멍이 = new Dog();
멍멍이란 강아지가 태어나서 두발로걷기(), 사람들과 대화하기()
이상하다,규약이 맞지않는다.
Animal 멍멍이 = new Dog();
멍멍이란 강아지가 태어나서 네발로걷기(), 멍멍!! 짖기()
이제는 말이된다.
인터페이스 분리 원칙은 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다는 원칙이다.인터페이스 분리 원칙은 큰 덩어리의 인터페이스들을 구체적이고 작은 단위들로 분리시킴으로써 클라이언트들이 꼭 필요한 메서드들만 이용할 수 있게 한다. 이와 같은 작은 단위들을 역할 인터페이스라고도 부른다. 인터페이스 분리 원칙을 통해 시스템의 내부 의존성을 약화시켜 리팩토링, 수정, 재배포를 쉽게 할 수 있다.
운전자가 자동차를 운전한다. 라는 명제를 객체간 관계로 비유하면 자동차에 대한 인터페이스, 운전자에 대한 인터페이스를 각각 분리하는 것이다.
그럼 운전자는 아빠가 될수도 있고, 택시기사,버스기사가 될수 있다. 자동차는 버스가 될 수도 있고, 택시가 될수도 있고, 스포츠카가 될 수도 있다. 확장성이 커지는 셈이다.
프로그래머는 “추상화에 의존해야지, 구체화에 의존하면 안된다.” 의존성 주입은 이 원칙
을 따르는 방법 중 하나다.
• 쉽게 이야기해서 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻
• 역할(Role)에 의존하게 해야 한다는 것과 같다. 객체 세상도 클라이언트
가 인터페이스에 의존해야 유연하게 구현체를 변경할 수 있다! 구현체에 의존하게 되면 변
경이 아주 어려워진다.
위에 OCP 코드를 가져와서 보면
public class MemberServiceImpl implements MemberService{
// 여기서 MemberRepository는 인터페이스
private MemberRepository memberRepository = new JdbcMemberRepository();
}
위 코드에서 MemberServiceImpl는 memberRepository도 의존하고 JdbcMemberRepository클래스도 의존한다.
DIP를 위반하는 코드이다.
//설정자
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(new JdbcMemberRepository());
}
}
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
위 처럼 MemberServiceImpl는 MemberRepository라는 인터페이스만을 의존하게해서
MemberServiceImpl는 AppConfig에서 유현하게 구현체를 바꿔 낄수있다.
인터페이스를 의존해서 구현이 유연해지고 변경에 자유롭다.