도서/스프링 입문을 위한 자바 객체 지향의 원리와 이해

[도서/스프링 입문] #5 객체 지향 설계 5원칙 - SOLID

yulee_to 2023. 1. 17. 03:20

스프링 입문을 위한 자바 객체 지향의 원리와 이해

✔️ 이 글은 [스프링 입문을 위한 자바 객체 지향의 원리와 이해 - 김종민] 도서를 바탕으로 정리한 글입니다. 


객체 지향 설계 5원칙 SOLID는 응집도는 높이고(High Cohesion), 결합도는 낮추라(Low Coupling)는 고전 원칙을 객체 지향의 관점에서 재정립한 것으로 유지보수성을 극대화해주는 원칙이다.

 

  • 응집도 : 하나의 모듈 내부에 존재하는 구성 요소들의 기능적 관련성으로, 응집도가 높은 모듈은 하나의 책임에 집중하고, 독립성이 높아져 재사용이나 기능의 수정, 유지보수가 용이
    • 응집도가 높으면 변경 대상과 범위가 명확해짐
  • 결합도 : 모듈간의 상호 의존 정도로서 결합도가 낮으면 모듈 간의 상호 의존성이 줄어들어 객체의 재사용성이나 수정, 유지보수가 용이
    • 결합도가 낮으면 검토해야 되는 소스의 수가 적어짐

모듈은 소프트웨어를 각 기능별로 나눈 것으로 여러 코드들이 모여 어떤 기능을 처리하는 덩어리를 의미한다. jar 파일들이 수많은 기능을 가진 클래스들로 묶여 있으니 이 또한 하나의 모듈이다.

 

SRP - 단일 책임 원칙(Single Responsibility Principle)

 

어떤 클래스를 변경해야 하는 이유는 오직 하나 뿐이어야 한다

 

 

하나의 클래스는 하나의 기능을 담당하여 하나의 책임을 수행하는 데 집중되어 있어야 한다는 원칙이다. SRP 원리를 적용하면 책임 영역이 확실해지기 때문에 한 책임의 변경에서 다른 책임의 변경으로의 연쇄 작용과 책임의 순환에서 자유로워질 수 있다. 뿐만 아니라 책임을 적절히 분배함으로써 코드의 가독성 향상, 유지보수 용이라는 이점까지 누릴 수 있고 다른 원칙들을 적용하는 기초가 된다.

 

예시) 여자친구에겐 남자친구, 어머니에겐 아들, 직장 상사에겐 사원인 남자라는 클래스를 하나 만들기보다는 각 역할마다 나눠서 만들어주는 것이 좋다.

 

OCP - 개방 폐쇄 원칙(Open Closed Principle)

 

소프트웨어 구성요소(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하지만
변경에 대해서는 닫혀 있어야 한다.

 

 

확장이란 새로운 기능이 추가되는 것이고, 확장이 일어날 때 다른 클래스 수정은 최소화하도록 프로그램을 작성해야 한다는 원칙이다. 

예를 들어 개발자는 사용하는 장비로 키보드도 있을 수 있고, 마우스, 모니터, 노트북 등 여러가지를 쓸 수 있는데 각 장비를 바꿀 수 있으니 "확장"에는 열려 있어야 한다. 다만 개발자가 키보드가 바뀌어도 사용하는데 문제가 없어야 하기에 "변경"되면 안된다. 즉 키보드라는 역할과 매직키보드라는 구현이 나뉘어 있어야 하고, 클라이언트는 변경되면 안된다. 

 

OCP를 가능케 하는 중요 메커니즘은 추상화와 다형성이다.

다형성과 확장을 가능케 하는 객체지향의 장점을 극대화하여 클래스를 추가해야 한다면 기존 코드를 크게 수정할 필요 없이, 적절하게 상속 관계에 맞춰 추가만 하면 유연하게 확장할 수 있게 해준다. 

 

개방 폐쇄 원칙의 예 - JDBC

JDBC를 사용하는 클라이언트는 데이터베이스가 오라클에서 MySQL로 바뀌더라도 Connection을 설정하는 부분 외에는 따로 수정할 필요가 없다. 심지어 Connection 설정 부분을 별도의 설정 파일로 분리해 두면 클라이언트 코드는 단 한줄도 변경할 필요가 없다.

오라클을 MySQL로 변경해도 자바 애플리케이션은 JDBC 인터페이스라고 하는 완충 장치로 인해 변화에 영향을 받지 않는다. 자바 애플리케이션은 데이터베이스라고 하는 주변의 변화에는 닫혀 있고, 데이터베이스를 교체한다는 것은 데이터베이스가 자신의 확장에는 열려있다는 의미이다.

 

ex) 내가 사용하는 키보드를 나타내는 클래스가 있다고 하자. 나는 현재 매직키보드를 사용 중이기 때문에 MagicKeyboard라는 클래스를 만들었다. 키보드는 언제든지 바꿀 수 있어 클라이언트와 MagicKeyboard 사이에 Keyboard라는 역할 인터페이스를 만들어주었다. 새로운 키보드로 로지텍을 사서 사용하려면 LogitechKeyboard 클래스가 Keyboard를 구현하게 해주면 된다.

클라이언트에서 직접적으로 Keyboard의 구현체를 변경하게 하면 클라이언트 코드가 수정된다. 클라이언트는 어떤 키보드를 사용하든 상관이 없어야 하는데 변경이 된다는 건 OCP를 위반한 것이다. 이를 방지하기 위해서는 AppConfig라는 설정 클래스를 만들어 구현체를 생성하고, Keyboard라는 역할에 지정해줍니다. 그럼 MagicKeyboard 객체를 AppConfig에서 생성하고, Keyboard라는 인터페이스에 MagicKeyboard를 넣어주면 됩니다. 그럼 클라이언트는 Keyboard만 알아도 MagicKeyboard의 객체를 사용할 수 있는 거죠.

LogitechKeyboard로 변경할 때는 생성하려던 객체를 LogitechKeyboard로만 변경해주면 된다. 그럼 Keyboard를 확장하여 새로운 기능을 쉽게 추가할 수 있고, 사용할 구현체를 바꿀 때는 설정을 담당하는 AppConfig 코드만 변경해주면 돼서 클라이언트의 코드는 변경되지 않는다!

 

LSP - 리스코프 치환 원칙(Liskov Substitution Principle)

 

서브 타입은 언제나 자신의 기반 타입(base type)으로 교체할 수 있어야 한다.

 

 

객체 지향의 상속은 다음을 만족해야 한다.

  • 하위 클래스는 상위 클래스의 한 종류이다.
  • 구현 클래스는 인터페이스할 수 있어야 한다.

따라서 객체 지향의 상속은 조직도나 계층도가 아닌, 분류도 형태가 되어야 한다. 즉, 하위 클래스의 인스턴스는 상위 클래스 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는 데 문제가 없어야 한다.

리스코프 치환 원칙 중

  • 하위형에서 선행 조건(함수가 오류 없이 실행되기 위한 모든 조건)은 강화될 수 없다.
  • 하위형에서 후행 조건(함수가 호출된 후에 객체가 유효한 상태로 존재하는 지)은 약화될 수 없다.
  • 하위형에서 상위형의 불변 조건은 반드시 유지돼야 한다.

 

ISP - 인터페이스 분리 원칙(Interface Segregation Principle)

 

클라이언트는 자신이 사용하지 않는 메소드에 의존 관계를 맺으면 안 된다.

 

 

단일 책임 원칙(SRP)과 인터페이스 분리 원칙(ISP)는 같은 문제에 대한 두 가지 다른 해결책이라고 볼 수 있다. 대부분은 SRP 방식이 더 효율적이다.

인터페이스 최소주의 원칙은 인터페이스를 통해 메소드를 외부에 제공할 때는 최소한의 메소드만 제공하라는 원칙이다.

상위 클래스는 풍성할 수록 좋다는 건, 상위 클래스가 풍성할 수록 불필요한 형변환이 줄어들기 때문이다. 인터페이스가 작을 수록 좋다는 인터페이스 최소주의 원칙은 그 역할에 충실한 최소한의 기능만 사용하라는 의미이다.

 

예시 ) 남자라는 클래스를 여자친구를 만날 때는 남자친구 역할만 할 수 있게 인터페이스로 제한하고, 어머니를 만날 때는 아들 인터페이스로 제한해준다.

 

DIP - 의존 역전 원칙(Dependency Inversion Principle)

고차원 모듈은 저차원 모듈에 의존하면 안 된다. 이 두 모듈 모두 다른 추상화된 것에 의존해야 한다.
추상화된 것은 구체적인 것에 의존하면 안 된다. 구체적인 것이 추상화된 것에 의존해야 한다.

 

자주 변경되는 구체(Concrete) 클래스에 의존하지 마라.

 

 

자신보다 변하기 쉬운 것에 의존하던 것을 인터페이스나 추상화된 상위 클래스를 두어 변하기 쉬운 것의 변화에 영향을 받지 않게 하는 것이 의존 역전 원칙이다.

상위 클래스, 인터페이스, 추상 클래스일수록 변하지 않을 가능성이 높기에 하위클래스나 구체 클래스가 아닌 상위 클래스, 인터페이스, 추상클래스를 통해 의존하라는 의미이다. "의존한다"는 것은 해당 클래스에서 사용하는 다른 클래스라고 생각하면 편한데 클라이언트 코드가 구현 클래스에는 의존하지 않고, 인터페이스에만 의존하게 해야 한다. 그럼 역할에만 의존하게 되어 구현이 바뀌어도 클라이언트가 할 수 있는 일에는 변화가 없다. 

 

ex) 자동차->스노우타이어 였던 관계 사이에 자동차->타이어 인터페이스를 두어 스노우타이어, 일반타이어 등이 타이어 인터페이스를 구현하는 구조로 바꿔준다. 스노우타이어는 변하기 쉬운 것으로 스노우타이어가 변하면 자동차도 변하게 되는데, 중간에 타이어 인터페이스를 두어 스노우 타이어가 일반 타이어로 변해도 자동차는 영향을 받지 않는다.

 

정리 - 객체 지향 세계와 SOLID

SoC(Separation Of Concerns)는 관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모으고, 관심이 다른 것은 간으한 한 따로 떨어져 서로 영향을 주지 않도록 분리하라는 관심사의 분리라는 뜻이다. SoC를 적용하면 SRP, ISP, OCP를 만족하게 된다.

 

SOLID를 정리하자면

  • SRP(단일 책임 원칙) : 어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다.
  • OCP(개방 폐쇄 원칙) : 자신의 확장에는 열려있고, 주변의 변화에 대해서는 닫혀 있어야 한다.
  • LSP(리스코프 치환 원칙) : 서브 타입은 언제나 자신의 기반 타입으로 교체할 수 있어야 한다.
  • ISP(인터페이스 분리 원칙) : 클라이언트는 자신이 사용하지 않는 메소드에 의존 관계를 맺으면 안 된다.
  • DIP(의존 역전 원칙) : 자신보다 변하기 쉬운 것에 의존하지 마라. (중간에 추상적인 것을 끼워줌)

 

SOLID 원칙을 적용하면 소스 파일의 개수가 더 많아질 순 있지만, 논리를 잘 분할하고 잘 표현하기 때문에 이해하기 쉽고, 개발하기 쉬우며, 유지와 관리, 보수하기 쉬운 코드가 만들어진다.

728x90