스프링에서 왜 객체지향 이야기가 나오는가?

김태현·2023년 2월 26일
1

스프링부트

목록 보기
2/2

처음 자바언어에 대해 공부할때 객체지향을 이해하는데 많은 어려움이 있었습니다. 현재까지도 객체지향 프로그래밍에 대해 정확히 이해하지는 못한것 같습니다 ㅎㅎ

어찌저찌 자바문법을 떼고 스프링 프레임워크 공부를 시작했을때 DI, Ioc 등의 낯선 개념에 막막함과 좌절감을 맛봤던 기억이 납니다.

이번 포스팅은 당시에 모르고 넘어갔던 내용들에 대해 다시 공부하면서 알게된 점을 위주로 작성해보려고 합니다.


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

먼저 객체지향 프로그래밍이 왜 중요한지, 기업과 선배 개발자 분들이 왜 객체지향 프로그래밍을 강조하는지 알아야합니다.

객체지향 프로그래밍

객체지향 프로그래밍은 프로그램의 기능이 아닌 객체가 중심이 되어 "누가 어떤 일을 할 것인가?, 즉 어떤 클래스가 어떤 역할을 할 것인가?" 가 핵심이 됩니다. 객체 각각의 역할을 정의해 나가는것에 집중하는 것입니다.

절차지향 프로그래밍

반대로 절차지향 프로그래밍은 프로그램의 기능이 핵심이 됩니다. "무엇을 어떤 절차로 할 것인가?" 즉 어떤 기능을 어떤 순서로 처리하는가에 집중하게 됩니다.

객체지향 프로그래밍은 프로그램은 유연하고 변경이 용이하게 만들기 때문에 주로 대규모 소프트웨어 개발에 사용된다는 특징을 갖고있습니다.

유연하고 변경이 용이하다?

  • 레고 블럭을 조립하듯이
  • 컴퓨터 부품을 갈아끼우듯이

객체지향 프로그래밍의 특징을 잘 이해하고 실제 개발에 적용하게 되면 코드의 재사용성, 유지보수성, 확장성 등을 높일 수 있다는 장점이 있습니다.


스프링 프레임워크와 객체지향의 관계

우리는 객체지향 핵심 개념이 "캡슐화, 상속, 추상화, 다형성" 이라는 것을 이미 알고있습니다.
스프링 프레임워크는 이 네가지 개념중 다형성을 극대화해서 개발할 수 있게 도와줍니다. 제어의 역전(IoC), 의존관게 주입(DI)은 다형성을 활용해서 역할과 구현을 편리하게 분리할 수 있도록 지원합니다.

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

  • 클린코드 저자 “로버트 마틴”이 좋은 객체 지향 설계의 5가지 원칙을 정리했습니다.
  1. SRP: 단일 책임 원칙(Single Responsibility Principle)
  2. OCP: 개방-폐쇄 원칙(Open/Closed Principle)
  3. LSP: 리스코프 치환 원칙(Liskov Substitution Principle)
  4. ISP: 인터페이스 분리 원칙(Interface Segregation Princlple)
  5. DIP: 의존관계 역전 원칙(Dependency Inversion Principle)

SRP: 단일 책임 원칙

한 클래스는 하나의 역할만 가져야합니다.
하나의 역할이라는 것은 모호합니다. (역할이 클 수도 있고, 작을 수도 있다)
역할을 정할 때 중요한 기준은 변경입니다.
어떠한 변경이 있을 때, 파급효과(사이트 이팩트)가 적으면 단일 책임 원칙을 잘 지켰다고 할 수 있습니다.

OCP: 개방 - 폐쇄 원칙

소프트웨어 요소는 확장에는 열려있고 변경에는 닫혀있어야 합니다.
어려운 말이지만 다형성을 활용하면 쉽게 적용할 수 있습니다.
인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현합니다.
이는 기존 클래스를 변경하는 것이 아니고 새로운 클래스를 하나 만드는 것이기 때문에 확장에는 열려있고 변경에는 닫혀있어야 한다는 OCP 원칙을 잘 지켰다고 할 수 있습니다.

아래 코드는 구현체를 만들고 적용을 한다고 가정해보겠습니다.
실제 개발할 땐 new 연산자의 사용을 최대한 줄이고 DI 받아 객체를 사용하는 것이 좋습니다.

public class MemberService{
		MemberRepository m = new MemberRepository(); // 기존코드
		MemberRepository m = new JpaMemberRepository(); // 변경코드
}

개발 시점에 H2 데이터베이스에 데이터를 저장하는 기존 로직에서 실제 데이터베이스(MySql, Oracle) 등으로 코드를 변경하는 상황이라고 가정하겠습니다. 구현 객체를 변경하려면 MemberService클래스의 기존 코드를 변경해야 합니다.
분명 인터페이스(MemberRepository)를 구현한 새로운 클래스(JpaMemberRepository)을 만들어 다형성을 사용했지만 OCP 원칙을 지킬수 없습니다.
즉 클라이언트가 코드를 변경해야 하는 상황입니다.

이러한 문제를 해결하기 위해서 객체를 생성하고 연관관계를 맺어주는 별도의 설정자가 필요합니다.

이 별도의 설정자는 바로 스프링컨테이너입니다. 결과적으로 OCP 원칙을 지키기 위해 DI와 IoC가 필요한 것이죠

❎ 위 코드는 MeberService 클래스가 구현클래스(JpaMemberRepository())를 알고 있기 때문에 DIP 원칙을 위배하고 있습니다. 아래 DIP 원칙에서 추가적으로 설명하겠습니다.

LSP: 리스코프 치환 원칙

  • 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 합니다. (인터페이스의 구현체가 제 동작을 해야한다!)
  • 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다는 것, 다형성을 지원하기 위한 원칙, 인터페이스를 구현한 구현체를 믿고 사용하려면 이 원칙이 필요합니다.
  • 단순히 컴파일에 성공하는 것(컴파일 에러가 나지 않는것)을 넘어서는 개념입니다.
  • 기능적으로 보장이 되어야합니다.
    • 온라인 쇼핑몰에서 특정 상품을 10% 할인하는 이벤트를 할때, 10%의 금액을 더 받는다면? 이는 리스코프 치환 원칙을 지키지 못한 것입니다.

ISP: 인터페이스 분리 원칙

  • 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 좋습니다.
  • 자동차 인터페이스 → 운전 인터페이스, 기능 인터페이스로 분리 할 수 있습니다.
    • 엑셀이 고장났다면 운전 인터페이스만 고치면 되고,
    • 자동차에서 음악이 나오지 않는다면 기능 인터페이스만 고치면 됩니다.
  • 인터페이스가 명확해지고, 대체 가능성이 높아지게 됩니다.
  • 가능한 작은 단위로 인터페이스를 쪼개는 것이 좋은 설계라고 할 수 있습니다.

DIP: 의존관계 역전 원칙

  • 프로그래머는 추상화에 의존해야하며, 구체화에 의존하면 안됩니다. 의존성 주입(DI)은 이 원칙을 따르는 방법 중 하나입니다.
    • 쉽게 이야기하면 클라이언트가 구현 클래스를 바라보지 않고 인터페이스만 바라보게 하라는 원칙입니다.

      public class MemberService{
      		MemberRepository m = new JpaMemberRepository(); // 변경코드
      }
    • 위 코드에서 MemberSeviceMemberRepository() 또는 JpaMemberRepository() 등의 구현 클래스를 바라보는 것이 아닌 인터페이스를 바라보게 해야한다는 것입니다.

  • 클라이언트가 인터페이스에 의존하면 구현체를 유연하게 변경할 수 있습니다. 반대로 구현체에 의존하게 되면 변경이 아주 어려워집니다.
  • 위 MemberService는 인터페이스(MemberRepository)에 의존하지만 동시에 구현클래스(JpaMemberRepository)에도 의존하고 있습니다.
    MemberService 클라이언트가 구현 클래스를 직접 선택하고 있기 때문입니다.
    (MemberRepository m = new JpaMemberRepository();)
    추상화에 의존해야하는데 구체화에 의존하고 있는 상황이기 때문에 DIP 원칙을 지키지 못한 것입니다.

💡 그럼 도대체 어떻게 하라는거야?

클라이언트(MemberService)는 인터페이스(MemberRepository)에만 의존하도록 설계를 해야합니다.

IoC, DI 등을 이용하면 추상 클래스만 의존하도록 설계할 수 있습니다.
다음 포스팅에 자세한 설명을 이어가겠습니다.


정리

객체지향의 핵심은 다형성

다형성 만으로는 클라이언트 코드 변경을 막을 수 없다.

다형성 만으로는 구현 객체를 변경할 때 클라이언트 코드도 함게 변경된다.

다형성 만으로는 OCP와 DIP를 지킬 수 없다.

  • DIP → 인터페이스만 의존하면 구현된 기능이 없는데 어떻게 프로그래밍이 동작해?
  • 무언가가 더 필요하다
  • 그 무언가가 바로 스프링프레임워크 (Ioc, DI 등의 개념)
profile
안녕하세요. Java&Spring 기반 백엔드 개발자 김태현입니다.

0개의 댓글