[Spring] 좋은 객체 지향 설계의 5가지 원칙
스프링 핵심 원리 - 기본편 (김영한) 강의를 바탕으로 정리한 글입니다.
📌 좋은 객체 지향 설계의 5가지 원칙
🫧 SOLID
: 클린코드로 유명한 로버트 마틴이 좋은 객체 지향 설계의 5가지 원칙을 정리
✨ SRP : 단일 책임 원칙
- single responsibility principle
하나의 클래스는 하나의 책임만 존재해야 한다.
즉, 하나의 클래스 하나의 기능만을 수행해야 한다는 뜻이다.
💡 SRP 예시
예를 들어 Member이라는 클래스가 있다고 치자.
이는 멤버 클래스이며, Member 클래스의 로직(생성, 조회. 삭제 등)이 책임이 되는 것이다.
그러나 Member 클래스에서 강의 조회와 같은 부가적인 것들까지 같이 처리하게 되면, Member 클래스는 여러 개의 책임을 맡게 된다.
하나의 클래스에 여러 개의 책임이 부여되면, 추후에 멤버를 삭제하는 로직을 바꿀 때 강의를 추가하는 로직까지 바뀌어버리는 불상사가 생기게 된다.
✨ OCP : 개방-폐쇄 원칙
- Open/closed principle
확장은 열려 있으나 변경에는 닫혀 있어야 한다.
기능을 추가할 때는 열려 있어야 하고, 변경 시에는 최대한 작은 부분만 수정하도록 해야 한다.
우리는 소프트웨어를 개발하고 운영하며 여러 가지 기능들을 추가해 넣을 수 있다.
그렇기 때문에 기능을 추가하는 등의 확장 행위를 할 때 손쉽게 가능하도록 구현해야 한다.
이는 다형성을 활용해, 마치 자동차를 갈아끼우듯 구현이 가능하다.
즉, 자동차가 추가된다고 해서 운전자까지 바뀌어야 하는 것은 아니다.
확장을 해도 클라이언트는 아무런 문제 없이 쓸 수 있어야 한다는 소리이다.
또한, 변경에는 닫혀 있어야 하는데, 만일 변경에 닫혀 있지 않다면 SRP에서 언급했듯, 멤버 로직을 수정할 때 강의 로직까지 수정되는 불상사가 발생할 수 있는 것이다.
💡 OCP 문제점
MemberRepository를 변경하려면, 어쩔 수 없이 MemberService를 건드려야 하는 상황이 온다.
즉, 구현 객체를 변경하려면 클라이언트 코드를 변경해야 한다는 것이다.
분명 다형성을 사용했지만 OCP 원칙을 지킬 수 없다.
이를 해결하기 위해 스프링 컨테이너가 객체를 생성하고, 연관관게를 맺어주는 별도의 조립, 설정자 역할을 해 준다.
✨ LSP : 리스코프 치환 원칙
- Liskov substitution principle
프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
즉, 자동차 역할(인터페이스)의 규약을 다 지키면서 자동차(구현체)를 만들어야 한다는 것이다.
예를 들어 자동차 구현체를 만들었는데 엑셀 밟았더니 뒤로 가는 등, 규약을 어기는 행동을 해서는 안 된다는 것이다.
✨ ISP : 인터페이스 분리 원칙
- Interface segregation principle
클라이언트가 자신이 이용하지 않는 메서드에 의존하면 안된다는 원칙
즉, 특정 객체(클래스)에 대한 책임을 덜어, 기능을 쪼개고 쪼개서 클래스가 단 하나의 책임 (SRP)을 지니게 하는 것을 도와준다.
💡 ISP 예시
갤럭시1이라는 스마트폰을 개발하고자 할 때, 기능은 전화, MP3, 인터넷이 존재한다고 해 보자.
하지만 버전 업그레이드를 계속 진행하면서 갤럭시2를 출시하게 됐다.
이때 갤럭시2에는 MP3 기능이 빠지고 동영상 기능이 들어가게 된다.
이때 갤럭시1과 갤럭시2는 같은 핸드폰이지만, 서로 다른 기능이 존재하게 된다.
그렇다면 갤럭시1과 갤럭시2을 아우르는 핸드폰(인터페이스)에는 갤럭시1에 필요한 내용과 갤럭시2에 필요한 내용이 모두 들어가야 할까?
정답은 아니다. 책임이 과해지기 때문에, 이 인터페이스를 여러 개로 잘게 쪼개어 단일 책임 원칙 (SRP)를 지킬 수 있도록 하는 것이 바로 ISP인 것이다.
✨ DIP : 의존관계 역전 원칙
- Dependency inversion principle
프로그래머는
추상화에 의존해야지,구체화에 의존하면 안 된다.
구현이 역할을 의존해야 하지, 역할이 구현을 의존하면 안 된다.
참고로 의존한다는 것은 해당 내용을 알고 있다는 뜻이다.
💡 DIP 예시
자동차 역할 (인터페이스)가 있고, 실제 자동차 (구현체) 가 있다고 해 보자.
간단히 설명하자면, 사용자 (클라이언트)는 그 어떤 차를 타든 운전을 무리 없이 할 수 있어야 한다.
그것이 자동차 역할이다.
따라서 자동차 역할은 그 어떤 자동차가 와도 운전하는 방식이 똑같아야 한다.
만일 자동차 역할이 자동차에 의존하게 된다면, 자동차에 따라 운전하는 방식이 달라질 위험이 있다.
그러므로 역할은 구현을 의존해서는 안 된다.
💡 DIP 문제
그런데 OCP에서 설명한 MemberSevice는 구현 클래스를 직접 선택하고 있다.
MemberRepository m = new MemoryMemberRepository();
따라서 다형성만으로는 DIP를 지킬 수 없다.
🫧 정리
객체 지향의 핵심은 다형성이다.
다형성은 자바의 오버라이딩으로 구현되며, 쉽게 부품을 갈아끼우듯 설계를 할 수 있게 한다.
그러나 앞서 보았던 OCP 문제와 DIP 문제처럼, 다형성만으로는 OCP, DIP를 지킬 수 없다.
이를 스프링 프레임워크가 도와주어 문제를 해결할 수 있다.
🫧 스프링이 OCP 문제와 DIP 문제를 해결하는 법
- DI 컨테이너 제공
DI 컨테이너는 자바 객체들을 어떤 컨테이너 안에 넣어두고 그 안에서 서로 의존관계를 연결, 주입해주는 기능들을 제공하는 것이다.
이를 통해 클라이언트 코드의 변경 없이 기능 확장이 가능해진다.