Framework/Spring

좋은 객체지향 프로그래밍이란?

인프런에서 김영한 님의 스프링 완전 정복 로드맵에 나오는 좋은 객체지향 프로그래밍이란 무엇인가에 대해 학습한 내용을 정리하고자 한다.

우아한 형제들 기술이사 김영한의 스프링 완전 정복 로드맵 중 스프링 핵심 원리 - 기본편

 

객체지향 프로그래밍이란?

객체지향 프로그래밍컴퓨터 프로그래밍의 패러다임 중 하나이다. 객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위, 즉 "객체"들의 모임으로 파악하고자 하는 것이다. 각각의 객체는 메시지를 주고받고, 데이터를 처리할 수 있다. - 위키 백과

 

기존의 절차 지향 프로그래밍에서는 상태(변수)와 행동(함수)으로 프로그램을 설계했다면, 객체지향 프로그래밍에서는 프로그램을 구성하는 단위를 현실세계에서의 단위와 일치시키는 방법으로 설계한다. "객체"라는 독립된 단위가 서로 티키타카하는 개념이다. 현실세계에서는 사람들이 각자 다른 직업을 가지고 있고 성격도 다르다. 할 수 있는 행동도 다르고 능력치도 다르다. 또한, 현실세계에 존재하는 물건들 또한 각자의 특성을 가지고 있고 움직임과 크기도 다르다. 이러한 것들이 서로 상호작용하는 모습을 프로그램 설계에 녹아들도록 하는 것이 객체지향 프로그래밍 방식이다.

 

객체지향의 특징

객체지향의 특징은 크게 4가지로 나눌 수 있다.

  • 캡슐화 (Encapsulation)
    변수와 함수를 하나로 묶는 것을 의미하며, 현실세계에서 독립된 단위의 개념을 적용한 것이다.
    Java에서는 클래스를 통해 구현된다.
  • 추상화 (Abstraction)
    객체의 공통적인 속성과 기능을 추출해 모아서 정의하는 것을 의미한다. 여러 자동차 종류와 모델들이 있지만, 자동차에는 핸들, 기어, 엑셀, 브레이크, 의자, 계기판 등 공통적으로 가지고 있는 성질들이 있다. 이를 프로그래밍에서 묶어 정의할 수 있는 것을 말한다.
  • 상속성 (Inheritance)
    자식 클래스가 부모 클래스의 특성과 기능을 물려받는 것을 의미한다. 현실세계에서 컴퓨터라는 것이 부모 클래스라면, 자식 클래스는 삼성의 PC, 애플의 맥, 게이밍 PC가 될 수 있다. 이는 컴퓨터라는 공통된 특징을 그대로 물려받아서 각기 다르게 표현됨을 프로그래밍에 적용한 것이다.
  • 다형성 (Polymorphism)
    말 그대로 다양한 형태를 가지고 있을 수 있다는 특성이다. 가장 이해하기 쉬운 예제는 연극 공연 무대이다. 보통 연극 공연에서 역할 A와 역할 B는 공연 날짜에 따라 배우가 바뀐다. 하지만 역할 A, B가 하는 대사와 행동은 변하지 않는다. 이때, 역할 A, B는 형태가 다양할 수 있다고 말할 수 있다. 객체지향 프로그래밍에서는 다형성의 특징을 활용해 역할(연극 공연의 역할 A, B)과 실제 구현(역할을 맡아서 연기할 배우)을 분리하여 설계하면 유지보수가 쉽다는 장점이 있다.

필자는 객체지향의 특징 중에서 가장 중요한 것이 "상속"이라고 생각했다. 단지 구현에 초점을 두었을 때, 여러 객체를 생성하고 객체 간의 티키타카를 구현하는 과정에서 공통적으로 상속을 받아 작성하면 번거로움을 줄일 수 있고 깔끔한 코드가 작성된다고 생각했다. 하지만, 객체지향 프로그래밍에서 가장 중요하고 유용한 특징은 "다형성"이다. 앞서 4가지 특징에서 말했듯이, 다형성은 역할과 구현을 분리하여 설계하므로 구조가 유연해진다. 구조가 유연해진다는 것은 변경이 편리해진다는 것이다. 그럼 변경이 편리해진다는 것이 무슨 뜻일까?

 

객체지향의 다형성을 적극 활용해 설계하자.

예를 들어, 어떤 쇼핑몰 사이트를 만든다고 가정하자. 쇼핑몰 사이트를 만들 때 다른 부분들은 모두 기획되어 있으나, DB를 어떤 것으로 사용할지 정하지 못했다는 설정을 부여해보자. DB를 로컬에 직접 구현할지, AWS와 같은 서비스를 이용할지, 또 관계형 DB를 사용할지, NoSQL DB를 사용할지 모른다. 하지만 개발자는 이 사항이 정해질 때까지 기다릴 수 없다. 그럼 먼저 개발을 시작할 때, 상품의 등록/조회/수정/삭제, 상품의 구입/취소/장바구니/찜 등의 여러 기능들을 정의해둔다.(역할) 그리고 정의한 기능들을 가지고 나머지 부분에 대한 개발을 진행한다. 개발이 다 되어갈 때쯤, DB가 정해졌다. 이때, 먼저 정의해둔 기능들을 실제 DB에 연결하는 작업을 쉽게 진행하면 된다.(구현) 이렇게 역할과 구현을 구분하면 추후 DB를 다른 것으로 업데이트할 때에도 앞서 개발을 진행한 기능적인 부분들에 대한 코드를 변경할 필요가 없다. 이는 구조가 유연해지고 변경이 편리하다는 것을 뜻한다.

 

필자는 단순히 프로그램의 작성 측면에서만 보고 "상속"이 가장 중요하다고 생각했던 것이다. 하지만, 실제로 현업에서 운영되는 소프트웨어들은 유지보수가 가장 중요하다. 이 유지보수에는 "다형성"이 큰 역할을 해낸다.

 

그럼 이렇게 역할과 구현을 분리하는 것은 Java에서 어떻게 활용할 수 있을까?

Java에서는 인터페이스와 클래스 개념이 존재한다. 역할과 구현을 구분했을 때, 역할은 인터페이스로, 구현은 클래스를 활용해 만들어낼 수 있다. 결론적으로 객체 설계 시에 역할(인터페이스)을 먼저 부여하고, 그 역할을 수행하는 구현 객체를 만드는 것이 중요하다고 말할 수 있겠다. 또한, 인터페이스를 사용해서 다형성을 잘 활용하는 만큼, 인터페이스를 안정적으로 잘 설계하는 것 또한 중요하다고 말할 수 있다.

 

좋은 객체지향 설계의 5가지 원칙(SOLID)

클린코드로 유명한 로버트 마틴이 좋은 객체지향 설계의 5가지 원칙을 정리했다.

  • SRP: 단일 책임 원칙 (Single Responsibility Principle)
    한 클래스는 하나의 책임만 가져야 한다. 이때, 하나의 책임이라는 것은 클 수도 있고, 작을 수도 있어 모호하다. 그래서 중요한 기준은 "변경"이라고 생각해야 한다. 변경이 있을 때 파급 효과가 적으면 SRP를 잘 따랐다고 볼 수 있다.
  • OCP: 개방-폐쇄 원칙 (Open / Closed Principle)
    소프트웨어 요소는 확장에는 열려있으나, 변경에는 닫혀있어야 한다. 말로만 들어서는 앞뒤가 안 맞는 말이다. 하지만 앞서 언급한 객체지향의 가장 크고 중요한 특징인 "다형성"을 생각해보면 가능하다. 확장은 구현의 형태를 다양하게 함으로써 열려있게 할 수 있고, 역할을 정의한 부분을 변경하지 않게 설계함으로써 OCP를 따르게 할 수 있다.
    (하지만 역할의 코드에서 구현할 클래스를 직접 선택해야 한다는 부분에서 다형성을 사용했음에도 불구하고 사실상 OCP를 따를 수 없다. 이런 경우에 DI(Dependency Injection: 의존성 주입)라는 개념을 활용해 해결 가능하다.)
  • LSP: 리스코프 치환 원칙 (Liskov Substitution Principle)
    프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다. 상위 타입의 객체를 하위 타입의 객체로 치환해도 동작에 문제가 없어야 한다는 것을 의미한다. 예를 들어, 자동차 엑셀은 앞으로 가라는 기능을 가지고 있다. 이를 구현할 때, 뒤로 가게 구현한다면 LSP를 위반한 것이다. 느리더라도 앞으로 가게 만들어야 한다. 
  • ISP: 인터페이스 분리 원칙 (Interface Segregation Principle)
    특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다. 인터페이스를 세세하게 분리해야 한다는 뜻이다. 인터페이스를 세세하게 분리한다면 명확해지고, 대체 가능성이 높아진다.
  • DIP: 의존관계 역전 원칙 (Dependency Inversion Principle)
    프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안 된다. 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻이다. 앞서 언급한 역할과 구현 중에 "역할"에 집중하라는 의미이다. DB가 바뀌든지 간에 역할에 맞춰 개발한 쇼핑몰의 기능들은 정상적으로 작동할 수 있도록 추상화된 역할에 의존해야 한다. 이 원칙도 "다형성"이 객체지향의 핵심임을 의미한다.
    (이 역시 OCP에서 언급한 문제점이 동일하게 발생한다. 역할의 코드에서 구현할 클래스를 직접 선택해야 한다는 부분에서 다형성을 사용했음에도 불구하고 사실상 DIP를 따를 수 없다. 이러한 경우에도 OCP와 동일하게 DI라는 개념을 활용해 해결 가능하다.)

결론적으로, 좋은 객체지향 프로그래밍을 위해서는 객체지향의 특징을 적극 활용할 뿐만 아니라, SOLID 원칙을 잘 따르도록 설계해야 한다. 하지만, 객체지향의 가장 큰 특징인 "다형성"만으로는 OCP, DIP를 지킬 수 없다. 이를 위해 스프링에서는 DI라는 개념을 통해 OCP, DIP를 지키도록 만들 수 있다.

 

DI는 스프링에서 굉장히 중요한 부분이므로 이에 대해서는 따로 포스팅을 분리해서 작성해보도록 하겠다.