💋 인트로
요즘 스프링 프레임워크의 사용 위주로 공부중이다.
스프링이 낯설다 보니깐 사용법 위주로 공부하게 되는데, 이전에 자바만을 공부하면서 지키려고 노력했던 다형성을 많이 신경쓰지 않으면서 개발하게 된 것 같다는 기분이 들었다. 물론 이건 내가 초짜라 그렇다.
스프링은 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크이다!
잘 사용한다면 객체 지향 애플리케이션을 만들기 위해서 완전 빠방하게 잘 도와줄텐데, 일단 그전에 이번 포스팅을 통해서 좋은 객체지향이 뭔지에 대해서 공부해보자!
암튼 오늘은 로버트 마틴이 정리했던 객체지향의 5가지 원칙에 대해서 정리해보려고 한다!
💋 SOLID
로버트 마틴이 정리한 5가지 원칙이다. 그냥 앞글자를 따서 SOLID 원칙이라고도 하고, 암튼 내용을 잘 살펴보면서 이해하면 된다.
근데 진짜 줄이기 엄청 좋아하시는 것 같다.
- SRP(Single Responsibility Principle): 단일 책임 원칙
- OCP(Open Closed Principle): 개방 폐쇄 원칙
- LSP(Liskov Substitution Principle): 리스코프 치환 원칙
- ISP(Interface Segregation Principle): 인터페이스 분리 원칙
- DIP(Dependency Inversion Principle): 의존 관계 역전 원칙
원칙이 겁나게 많다. 하나씩 살펴보면서, 자바라는 언어가 가진 객체지향 정신이 무엇인지 이해해보자!
✔ SRP(Single Responsibility Principle): 단일 책임 원칙
하나의 클래스는 하나의 책임만을 가져야 한다는 원칙이다.
이 원칙을 잘 지켰냐 보려면, 변경을 해보면 된다. 변경을 했을 때, 파급 효과가 적으면 잘 설계한 것이다.
UI를 변경했을 때 다른 코드를 모두 바꿔야 한다면 단일 책임 원칙을 잘 지켰나 의심해 봐야 한다....
한 클래스 안에 view, db, buisness 로직 코드가 다 들어있다면..? SRP 만든 사람이 깜짝 놀랄 코드가 될 것이다.
근데 이 원칙에 대해서 조금 모호한 부분이 있는데, 하나의 책임이라는 것은 주관적이다.
암튼 변경이 있을 때 하나의 클래스에서 하나의 부분만 고치면 된다면 SRP를 잘 지켰다고 생각해도 좋다!
✔ OCP(Open Closed Principle): 개방 폐쇄 원칙
가장 중요한 원칙이다!
소프트웨어 요소는 기능의 확장에는 열려 있으나, 코드의 변경에는 닫혀 있어야 한다.
그런데 어떻게 코드의 변경 없이 기능을 확장할 수 있는거지...?
다형성을 잘 활용하라는 말이다!
인터페이스를 안정적으로 설계해서 사용하고 있다고 생각해보자!
새로운 기능이 추가되어 소프트웨어가 확장되었을 때, 인터페이스를 구현한 새로운 클래스를 만들어서 확장된 기능을 추가한 후에 기존에 사용하던 인터페이스만 갈아끼우면 되는 것이다!
예시로, InMemoryProductRepository를 사용하고 있다가, 데이터베이스와 관련해서 더 좋은 DB를 사용하게 되어서, 기능에 변화가 생겼다고 생각해보자! 만약에 ProductRepository 인터페이스를 잘 구현하고 있었다면, 이 인터페이스를 구현한 또 다른 DbProductRepository 클래스를 만들어서 갈아끼우는 것은 매우 쉬울 것이다!
public final class ProductService {
private ProductRepository productRepository = new InMemoryProductRepository();
}
다른 구현체로 바꿔 끼워보자!
public final class ProductService {
// private ProductRepository productRepository = new InMemoryProductRepository();
private ProductRepository productRepository = new JdbcProductRepository();
}
근데 이것도 사실 기존 코드에 변경이 있는건 맞다!
생성자 주입 방식으로 변경하더라도, ProductService 클래스 내의 코드 변경은 없겠지만, 이 클래스를 생성하는 곳에서의 코드를 변경해야 하므로 어찌 되었든 기존 코드에 변경이 생기게 된다....
그러면 이건 OCP 원칙을 지킨 것이 아니게 되는데...?
해결하기 위해서는 객체를 생성하고, 조립해주는 별도의 외부 객체가 필요하다.
이 역할은 스프링의 DI 프레임워크의 IoC container가 해줄 것이다!
✔ LSP(Liskov Substitution Principle): 리스코프 치환 원칙
프로그램 객체는 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
예를 들어서, 자동차 인터페이스가 있다고 생각해보자.
이 인터페이스를 구현해 본다고 생각해보자. 악셀을 눌렀을 때 앞으로 가야 한다. 뒤로 가면 안된다!!!
물론 뒤로 가도록 만들어도 컴파일은 잘 될 것이다. 자바 문법상 틀린 내용은 아닐 테니깐!
LSP 원칙은 단순히 컴파일 성공을 넘어서는 말이다!
다형성에서 하위 클래스는 인터페이스의 규약을 지켜야 한다는 원칙이다.
인터페이스에서 원하는 기능에 대해서 세부 구현 내용은 변할 수 있지만, 우리가 기대하는 결과는 그대로 나타나야만 한다.
다형성의 장점으로, 상위 객체는 하위 객체가 어떤 역할을 하는 지에 대해서 인터페이스에 정의된 내용만 알고, 그 세부 내용을 모르더라도 사용할 수 있다는 것이 있다. 암묵적으로, 인터페이스에 정의된 내용에 대해서 세부적으로는 관심이 없지만 구현체는 잘 했을 거라고 믿는 것이다.
이 원칙을 지켜야지만 우리는 다형성을 지원하기 위해 만들어진 인터페이스의 구현체를 믿고 사용할 수 있다.
✔ ISP(Interface Segregation Principle): 인터페이스 분리 원칙
특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다는 원칙이다.
예를 들어서 설명해 보자면, 자동차 인터페이스가 있다고 생각해보자.
현재 자동차 인터페이스 내에는 운전과 정비에 관한 역할(메서드)들이 정의되어 있다.
이 인터페이스를 운전 인터페이스, 정비 인터페이스로 분리해 보자.
이렇게 되면 정비에 관한 내용이 변하더라도 운전에 대한 내용은 변함없이 그대로 사용할 수 있게 된다.
이 원칙은 인터페이스의 역할을 너무 크게 잡지 말고, 각 기능에 맞게 적절하게 쪼개서 사용해야 한다는 것이다.
이 원칙을 지키면, 인터페이스가 명확해지면서 대체 가능성이 높아진다.
✔ DIP(Dependency Inversion Principle): 의존 관계 역전 원칙
드디어 마지막 원칙이다!
구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 원칙이다.
프로그래머는 추상화에 의존하고, 구체화에 의존하면 안된다는 원칙이다.
의존성 주입은 이 원칙을 따르는 방법 중 하나다!
역할과 구현을 철저히 분리해야 한다. 클라이언트는 인터페이스에 의존해야 하고, 구현체에 의존하면 안된다.
이렇게 해야 변경에 아주 유연하게 될 수 있다.
위에서 봤던 OCP에서 등장했던 코드를 다시 한 번 살펴보자.
public final class ProductService {
// private ProductRepository productRepository = new InMemoryProductRepository();
private ProductRepository productRepository = new JdbcProductRepository();
}
이 코드에서 ProductService는 JdbcProductRepository라고 하는 '구현체'에 의존하고 있다.
ProductService를 클라이언트라고 생각해보면, 현재 코드에서 클라이언트는 구체적인 구현 클래스를 직접적으로 선택해서 생성하고 있다.
따라서 ProductService는 인터페이스에도 의존하지만, 구체 구현 클래스에도 직접 동시에 의존하고 있다!
DIP를 위반한다!!!!!
따라서 코드를 변경해야 하는 문제가 생기는 것이다.
객체 지향의 핵심은 다형성이고, 다형성을 잘 지키면서 코드를 작성한다면 쉽게 부품을 갈아 끼우듯 기능을 변경할 수 있다.
하지만 다형성만으로는 구체적인 구현 클래스를 변경할 때, 클라이언트의 코드 중 클래스를 생성하는 부분에서 변경이 필요하다.
따라서 다형성만으로는 OCP, DIP를 지킬 수가 없다. 무언가 더 필요한데, 이건 스프링이 해줄거다!
'JAVA' 카테고리의 다른 글
[JAVA] 쓰레드 풀(Thread Pool): 개념, 장점, 사용 방법, 코드 예시 (feat. Baeldung) (0) | 2023.09.09 |
---|---|
[JAVA] 프로세스와 스레드: 개념, Java의 쓰레드 구현, I/O Blocking (0) | 2023.09.08 |
[Spring] Spring Core(3): IoC Container의 개념, 생명 주기 (2) | 2023.04.23 |
[JAVA] 좋은 코드가 되려면 꼭 지켜야 할 기본적인 컨벤션 (0) | 2023.04.13 |
[JAVA] 제네릭(Generic)이란? (0) | 2023.04.12 |