좋은 객체 지향 설계의 5가지 원칙(SOLID)
이전 포스팅에 이어서 좋은 객체 지향에 대해 조금 더 자세히 알아보려 한다. 좋은 소프트웨어를 만들기 위해서는 깔끔한 코드
도 중요하겠지만 아키텍처
또한 중요하다. 좋은 아키텍처를 정의하기 위해서는 원칙이 필요한데, 지금부터 설명할 SOLID이다.
SOLID 원칙
SOLID 원칙의 목적은 중간 수준의 소프트웨어 구조가 아래와 같도록 만드는 데 있다.
(중간수준: 모듈 수준에서 작업할 때 적용할 수 있는 수준)
- 변경에 유연하다.
- 이해하기 쉽다.
- 많은 소프트웨어 시스템에 사용될 수 있는 컴포넌트의 기반이 된다.
5가지 원칙
- SRP: 단일 책임 원칙(Single Responsibility Principle)
- OCP: 개방-폐쇄 원칙(Open-Closed Principle)
- LSP: 리스코프 치환 원칙(Liskov Substitution Principle)
- ISP: 인터페이스 분리 원칙(Interface Segregation Principle)
- DIP: 의존관계 역전 원칙(Dependency Inversion Principle)
SRP: 단일 책임 원칙
이름만 듣는다면 모든 모듈이 단 하나의 일만 해야 한다고 오해할 수 있지만 SRP는 “단일 모듈은 변경의 이유가 하나, 오직 하나뿐이어야 한다.”는 의미이다.
예를 들어보자 우리의 서비스는 회원관리 기능을 제공한다. 그리고 이를 위해 MemberService
를 만들었다. 서비스 운영 중 보안이슈로 인해 우리는 DB 암호를 변경하기로 하였다. 그런데 DB 암호를 변경하기 위해서는 MemberService
에 포함된 DB Connection 코드의 일부를 수정해야 하는 일이 벌어졌다.
위의 예시는 SRP를 위반한 사례이다. MemberService
는 변경의 이유가 두 가지나 되기 때문이다.
- 회원관리 업무 로직이 변경될 경우
- DB Connection 정보가 변경될 경우
SRP 원칙을 지킨다는 것은 MemberService
의 역할은 회원관리 업무가 유일하고 변경의 이유 또한 회원관리 업무 로직 변경 단 한가지 뿐이어야한다.
중요한 기준은 변경이다. 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것이라 볼 수 있다.
OCP: 개방-폐쇄 원칙
개방폐쇄 원칙은 “소프트웨어 개체는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.”는 의미이다.
다시 말해 기능을 확장할 수 있어야 하고, 이때 기존 코드를 변경해서는 안된다는 말이다. 이전 게시물의 자동차 예제로 잠깐 돌아가보려 한다.
우리는 이전에 운전자의 역할과 자동차의 역할에 대해 알아보았다. 이번에는 자동차에 집중해보자.
자동차는 역할을 가지고 있고 그에 대한 구현체로 K3, 아반떼, 테슬라 모델3를 가지고 있다. 여기에 새로운 차종인 모닝이 추가된다고 할지라도 운전자
에게 영향을 주지 않는다. 그 이유는 역할과 책임을 명확히 분리했기 때문이다.
이처럼 기능이 확장(새로운 차종) 되더라도 클라이언트(운전자)는 변경되지 않는다. 다시 말해 다형성을 활용하면 된다.
다형성만으로는 해결되지 않는다?
OCP 원칙을 적용하기 위해서 다형성을 활용하면 된다고 하였다. 그런데 다형성만으로는 해결되지 않는다는건 무슨 말일까? 코드로 예시를 들어보겠다.
1
2
3
public interface MemberRepository {
....
}
1
2
3
4
5
6
7
8
9
// Memory Repository
public class MemoryMemberRepository implements MemberRepository {
....
}
// Jdbc Repository
public class JdbcMemberReposititory implements MemberRepository {
....
}
우리는 MemberRepository
인터페이스(역할)를 만들었다. 만약 기존 메모리 저장 방식에서 Jdbc 방식으로 기능을 확장하려 한다면 위 코드처럼 JdbcMemberRepository
를 추가로 만들어주면 된다.
그럼 MemberRepository
를 사용하는(클라이언트) MemberService
를 보자.
1
2
3
4
public class MemberService {
// private MemberRepository memberRepository = new MemoryMemberRepository();
private MemberRepository memberRepository = new JdbcMemberReposititory();
}
우리는 분명 다형성을 활용하여 역할과 구현을 명확히 분리 하였다. 그렇다면 OCP 원칙이 적용되어 클라이언트인 MemberService
는 수정이 없어야 한다. 하지만 구현체를 변경하기 위해서는 MemberService
를 변경해야 하는 일이 발생된다. 이는 OCP 원칙을 위반한 것이다.
이 문제를 어떻게 해결할 수 있을까? 지금은 객체를 생성하고, 연관관계를 맺어주는 별도의 조립, 설정자가 필요하다 정도로 이해하자.
LSP: 리스코프 치환 원칙
리스코프 치환 원칙은 “상호 대체 가능한 구성요소를 이용해 소프트웨어 시스템을 만들 수 있으려면 이들 구성요소는 반드시 서로 치환 가능해야한다.”는 의미이다. 한마디로 부모클래스에 자식 클래스를 대입시켜도 문제 없이 작동해야 한다는 말이다.
자동차 예시를 다시들면 새로운 차종인 모닝을 만들었다. 그런데 운전자가 브레이크를 밟는 순간 차가 앞으로 나아간다 그리고 핸들을 왼쪽으로 돌렸는데 차가 오른쪽으로 움직인다. 이는 LSP를 위반한 경우이다.
분명 자동차는 동작한다.(컴파일러 통과) 하지만 동작의 문제를 넘어 이 경우 아무도 모닝을 구매하지 않을 것이고 기능을 신뢰할 수 없을 것이다. 차는 브레이크를 밟는 순간 멈춰야 하고 핸들을 돌리는 방향으로 움직이여만 한다.
ISP: 인터페이스 분리 원칙
인터페이스 분리 원칙은 “특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다”는 의미다.
예를 들면 자동차 인터페이스 하나 보다는 운전 인터페이스, 정비 인터페이스로 분리하는게 더 좋다는 의미이다.
자동차 인터페이스
-> 운전 인터페이스
와 정비 인터페이스
로 분리
이렇게 분리할 경우 정비 인터페이스
자체가 변해도(정비공정이 달라지는 경우 혹은 다른 변경) 운전자
에게 영향을 주지 않는다. 한마디로 인터페이스가 명확해지고, 대체 가능성이 높아진다.
DIP: 의존관계 역전 원칙
의존관계 역전 원칙은 “구체화에 의존하지 말고 추상화에 의존하라”는 의미이다. 쉽게 이야기하면 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻이다.
앞서 언급한 OCP 적용 문제점에 대해 다시 이야기해보자.
MemberService
는 인터페이스에 의존
하지만, 동시에 구현 클래스도 의존
하고 있다.
1
2
3
4
public class MemberService {
// [인터페이스 의존] [구현클래스 의존]
private MemberRepository memberRepository = new JdbcMemberReposititory();
}
위 코드는 OCP 뿐만 아니라 DIP도 위반하고 있다. 결론적으로 다형성만으로는 OCP, DIP를 지킬 수 없다.
객체 지향 설계와 스프링
다시 스프링으로 돌아와 생각 해보자. 왜 스프링에서 객체 지향에 대한 이야기가 나오는가?
이유는 스프링이 아래의 기술들을 통해 OCP와 DIP를 가능하게 지원하기 때문이다.
- DI(Dependency Injection): 의존관계(의존성) 주입
- DI 컨테이너 제공
스프링을 사용하면 클라이언트 코드의 변경 없이 기능을 확장할 수 있다. (부품을 교체하듯이 개발)
OCP
와 DIP
원칙을 지키기 위해서는 스프링을 사용해야 된다 처럼 보일 수 있지만 사실 순수한 자바코드로도 구현은 가능하다. 다만 이 경우 실제 비즈니스 로직의 구현보다 OCP와 DIP원칙을 지키기 위해 구현할 코드의 양이 더 많아질 수 있다.
정리
모든 설계에 역할과 구현을 분리하자.
애플리케이션 설계는 공연을 설계 하듯 배역만 만들어두고, 배우는 언제든 유연하게 변경할 수 있도록 만드는 것이 좋은 객체 지향 설계이다.
이상적으로는 모든 설계에 인터페이스를 부여하자
(실무에서의 고민)
- 하지만 인터페이스를 도입하면 추상화라는 비용이 발생한다.
- 기능을 확장할 가능성이 없다면, 구체 클래스를 직접 사용하고, 향후 꼭 필요할 때 리팩토링하여 인터페이스를 도입하는 것도 좋은 방법이다.
Comments powered by Disqus.