섹션 1. 객체 지향 설계와 스프링

Zion Yu·2021년 3월 2일
0
post-thumbnail

본 시리즈는 우아한형제들 개발 팀장이신 김영한님의 스프링 핵심 원리 - 기본편 강의를 들으며 개인적으로 정리한 내용을 담고 있습니다. 제가 들은 강의는 인프런에 등록되어 있습니다. 모든 다이어그램을 포함한 사진의 출처는 위 강의의 강의록임을 밝힙니다. 개인적으로 정리한 내용이기 때문에 글 내용에 오류가 있을 수 있으며 이에 대한 피드백은 댓글로 부탁드립니다.

이야기 - 자바 진영의 추운 겨울과 스프링의 탄생

  • 2000년대 초반에는 자바 진영에서 EJB(Enterprise JavaBeans)를 표준으로 강력히 밀었고 실제로 많이 사용되었다.
    • EJB는 (이론적으로는) 좋은 기능들이 많았으나 여러 문제를 갖고 있었다.
    • 복잡하고, 느리고, 어렵고, 코드도 지저분하고...
  • POJO라는 개념이 튀어나왔다.
    • Plain Old Java Object
    • '오래된 방식의 간단한 Object로 돌아가자'는 뜻을 갖고 있다.
    • EJB가 너무 복잡하고 무겁다보니 이에 반발로 나온 용어라고 한다.
    • 이 용어만 봐도 당시 개발자들의 EJB에 대한 원성이 느껴진다.
  • 스프링은 이러한 배경 속에서 탄생했다.
    • EJB를 대체할 목적으로, 단순하게! 만들었다.
  • 이 무렵 Hibernate도 등장한다.
    • EJB 엔티티빈(ORM 기술)을 대체할 목적으로 등장했다.
    • 시작은 미약했으나, 결국 자바 진영에서 JPA를 새로운 표준으로 정착시키는 결과를 낳는다.
      • JPA는 인터페이스이고, JPA를 구현하는 구현체는 다양하다.

스프링의 역사

  • 2002년 로드 존슨이 책을 출간하는데, 로드 존슨은 이 책에서
    • EJB의 문제점을 지적하고
    • 'EJB 없이도 app을 잘 만들 수 있다!'며 30,000라인의 예제 코드를 실었다.
    • 이 책에는 BeanFactory, ApplicationContext, POJO, 제어 역전, 의존관계 주입 등의 내용이 고스란히 녹아있다.
  • 결국 로드 존슨이 출간한 책은 대박을 치고 다른 개발자들의 제안으로 오픈 소스 프로젝트를 시작하게 된다.
  • 이들은 왜 스프링에 열광했는가? 다른 개발자들은 왜 오픈 소스 프로젝트를 하자고 제안했을까? 본 강의에서는 이에 대해 알아보고자 한다.

스프링이란?

  • 스프링의 생태계
    • 스프링 프레임워크
    • 스프링 부트
    • 스프링 데이터: DB의 CRUD 관련 기능
    • 스프링 세션: 세션 관련 기능
    • 스프링 시큐리티: 보안 관련 기능
    • 스프링 Rest Docs: API 문서화 관련 기능
    • 스프링 배치: 배처 처리 관련 기능
      • 큰 작업을 배치 단위로 쪼개서 처리하기 위한 기능들
    • 스프링 클라우드: 클라우드 관련 기능

스프링 프레임워크

스프링 프레임워크는 스프링의 핵심!

  • 구성요소
    • 핵심 기술: 스프링 DI 컨테이너, AOP, 이벤트, 기타
    • 웹 기술: 스프링 MVC, 스프링 WebFlux
    • 데이터 접근 기술: 트랜잭션, JDBC, ORM 지원, XML 지원
    • 기술 통합: 캐시, 이메일, 원격접근, 스케줄링
    • 테스트: 스프링 기반 테스트 지원
    • 언어: Kotlin, Groovy

스프링 부트

스프링 부트는 스프링을 편하게 사용할 수 있도록 지원한다. 최근에는 거의 기본적으로 사용하는 추세.

  • 별도의 웹 서버를 세팅하지 않고 단독으로 실행 가능 (자체 서버를 내장하고 있다.)
  • 초기 설정을 편하게 할 수 있도록 도와준다! (starter 종속성 제공)
  • 스프링과 외부 라이브러리 자동 구성: 스프링 버전과 외부 라이브러리 버전을 서로 호환되게끔 지정해준다. 개발자가 버전에 대해 고민하고 찾아볼 필요가 없다.
  • 모니터링 관련 기능 제공

스프링 부트는 스프링과 떨어질 수 없는 관계이다. 스프링이 없다면 스프링 부트는 아무런 의미가 없다.

참고: '스프링'이라는 건 뭘 뜻하나?

  • 문맥에 따라 다음 중 하나이다.
    • 스프링 DI 컨테이너 기술
    • 스프링 프레임워크
    • 스프링 부트, 스프링 프레임워크 등을 모두 포함한 스프링 생태계
  • 아래로 갈수록 더 큰 개념

스프링은 왜 등장했는가?

⭐어떤 기술이든지 왜 만들었는가?핵심 컨셉은 무엇인가?가 중요하다.

모든 기술은 핵심 컨셉을 알아야 '잘' 사용할 수 있다.

  • 스프링은 객체 지향 언어가 가진 강력한 특징을 살려낼 수 있는 프레임워크이다.
  • 즉, 스프링은 좋은 객체 지향 app을 개발할 수 있게 도와주는 프레임워크이다.
  • 기존의 EJB로 개발을 하면 EJB에 의존적인 개발을 해야만 했고, 이는 결국 객체 지향의 '좋은' 성질을 모두 잃는 결과로 이어졌다.
  • 많은 개발자들이 Spring의 등장에 열광한 이유는 바로 이것이다. 객체 지향의 성질을 살릴 수 있어서!

따라서 우리가 스프링을 제대로 이해하려면 좋은 객체 지향 프로그래밍이 무엇인가?에 대해 먼저 알아야 한다.


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

  • 객체 지향 프로그래밍이 뭘까?
    • 객체 지향 프로그래밍은 프로그램을 객체의 모임으로 파악하고자 하는 것이다.
    • 각 객체는 메시지를 주고 받는다.
    • 객체 지향 프로그래밍은 프로그램을 유연하고, 변경이 용이하게 만든다.
  • 유연하고, 변경이 용이?
    • 컴포넌트를 쉽고 유연하게 변경하면서 개발
    • 이는 객체지향의 특성 중 다형성과 관련이 있다!

다형성

  • 실제 세계를 역할-구현의 모델로 나눠보자.
    • 역할의 예시: 자동차 역할, 운전자 역할
    • 구현의 예시: K3, 아반떼, BMW 320d 등등.. (모두 자동차의 구현 예시)
    • 구현이 바뀌어도(차가 바뀌어도) 다른 역할에게는(운전자에게는) 아무런 영향을 끼치지 않는다. 이를테면, 운전 면허를 다시 따지 않아도 된다. 역할(자동차)이 똑같기 때문(=인터페이스가 똑같기 때문)
    • 운전자(클라이언트)는 자동차 내부의 구현 상태를 몰라도 된다! 이를테면, 가솔린차든, 전기차든, 디젤이든 전부 무관하다.
    • 해로운 구현(뭔가 잘못된 구현)이 발생하더라도 운전자(클라이언트)는 바뀔 필요가 없다!
  • 역할과 구현을 분리
    • 세상을 역할구현으로 구분하면 세상이 단순해지고 유연해지며, 변경이 용이해잔다.
    • 클라이언트는 대상의 역할(인터페이스)만 알면 된다.
    • 클라이언트는 구현 대상의 내부 구조를 몰라도 된다.
    • 클라이언트는 구현 대상의 내부 구조가 변경되어도 영향을 받지 않는다.
    • 클라이언트는 구현 대상 자체를 변경해도 영향을 받지 않는다.
  • Java에서는?
    • 역할 == 인터페이스
    • 구현 == 인터페이스를 구현한 클래스
  • 객체를 설계할 때 역할구현을 명확히 분리하여 설계해야 한다!
  • 객체를 설계할 때 역할을 먼저 부여하고, 그 역할을 수행하는 구현 객체 만들기

참고: 객체는 서로 협력한다.

  • 혼자 있는 객체는 없다!
  • 객체들 간에는 클라이언트, 서버의 관계가 존재한다. 즉, 요청하고 응답하는 관계가 존재한다.
  • 수많은 객체 클라이언트와 객체 서버는 서로 협력 관계를 가진다.
    • 혹은 시스템 간에 요청을 주고 받을 수도 있다.

참고: Java의 다형성

  • Java는 Overriding으로 다형성을 구현할 수 있다.

다형성의 본질

  • '인터페이스를 구현한 객체 인스턴스'를 실행 시점유연하게 변경할 수 있다.
    • 즉, 클라이언트를 변경하지 않고 서버의 구현 기능을 유연하게 변경할 수 있다는 게 다형성의 메리트이다!

역할과 구현을 분리 - 최종 정리

  • 실세계의 역할과 구현이라는 컨셉을 컴퓨터 안으로 끌고오면 다음과 같은 장점이 있다.
    • 유연하고, 변경이 용이하다!
    • 구현체를 무한히 확장(추가)할 수 있다.
    • 구현체 변경 시에 클라이언트에 영향을 주지 않는다.
  • 따라서 인터페이스를 잘 설계하는 것이 중요하다.
    • 이것은 자바뿐만 아니라 모든 API 설계에도 중요하다. (프론트-백 간의 API)
  • 한계점
    • 역할 자체가 변하면(인터페이스 자체가 변하면), 클라이언트와 서버 모두에 변경에 발생한다.

스프링과 객체 지향

객체 지향의 꽃은 다형성이다.

  • 스프링은 다형성을 극대화해서 이용할 수 있게 도와준다.
  • 스프링의 IoC, DI는 다형성을 활용해서 역할과 구현을 편하게 다룰 수 있도록 도와준다.
  • 스프링을 사용하면 구현을 편리하게 변경할 수 있다!

그런데 스프링과 객체 지향 설계에 대해 잘 이해하려면 다형성 뿐만 아니라 하나 더 알아야 할 게 있다.

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

SOLID

SOLID는 유명 도서 클린코드의 저자 로버트 마틴이 정리한 좋은 객체 지향 설계의 5가지 원칙이다. 참고로 SOLID는 면접 질문으로도 자주 나온다고 한다.

SRP: 단일 책임 원칙

  • 한 클래스는 하나의 책임만 가져야 한다.
  • 근데 하나의 책임이라는 게 모호하다.
    • 문맥과 상황에 따라 다름.
  • 중요한 기준은 변경이다. 변경이 있을 때 파급 효과가 적으면 SRP를 잘 따른 것

OCP: 개방-폐쇄 원칙

  • 소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야 한다.
  • 확장을 할 때 기존 코드를 변경하면 안된다는 말이다. 어떻게???
  • 다형성을 활용하면 가능하다.
    • 기존의 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현!
  • 그러나 문제점이 있다.
    • 클라이언트 측에서 구현 클래스를 직접 선택해야한다!
//  MemberRepository m = new MemoryMemberRepository();
    MemberRepository m = new JdbcMemberRepository();
  • 구현 객체를 변경하려면 클라이언트의 코드를 변경해야 하는 상황이다. 다형성을 이용했으나 OCP 원칙을 지킬 수 없다!
  • 이걸 어떻게 해결하나?
    • 객체를 생성하고, 연관 관계를 맺어주는 별도의 조립자, 설정자가 필요하다!
      → 이게 스프링?

LSP: 리스코프 치환 원칙

  • 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
  • 다형성에서 하위 클래스(구현 클래스)는 인터페이스의 규약을 지켜야 한다는 것이다. 즉, 비즈니스 룰을 지켜라.
    • 단순히 컴파일 에러가 없으면 된다는 뜻이 아니다. 그 이상의 의미.
  • 예시) 자동차 인터페이스의 엑셀은 앞으로 가는 기능, 뒤로 가게 구현하면 LSP 위반. 방향을 반드시 지켜줘야 함.

ISP: 인터페이스 분리 원칙

  • 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
    • 기능을 적당한 크기로 쪼개는 게 중요하다. 역할을 크게 하나 만들지 말고 여러 개의 역할로 쪼개라는 뜻.
    • 인터페이스가 명확해지고, 대체 가능성이 높아진다.

DIP: 의존관계 역전 원칙

  • 프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다. 의존성 주입은 이 원칙을 따르는 방법 중 하나다.
  • 즉, 구현 클래스에 의존하지 말고 인터페이스에 의존하라는 뜻.
    • 구현에 의존하지 말고, 역할에 의존하라는 것
    • 구현에 의존하게 되면 변경이 굉장히 어려워진다.
  • 문제점
    • 그러나 클라이언트는 구현 클래스도 동시에 의존하게 된다.
//  MemberRepository m = new MemoryMemberRepository();
    MemberRepository m = new JdbcMemberRepository();
  • 클라이언트에서 구현 클래스를 직접 선택하기 때문에.. DIP 위반!

    • 즉, 위 코드에서 클라이언트는 구현에 의존하고 있는 것이다. (직접 호출하기 때문에)

여기까지 정리

  • 객체 지향의 핵심은 다형성이다.
  • 그러나 다형성을 만족하는 것만으로는 OCP, DIP를 지킬 수 없다.
  • 뭔가가 더 필요하다?

다시 스프링으로!

스프링이 필요하다!

  • 스프링은 다음과 같은 기술로 다형성 + OCP, DIP를 만족시킨다.
    • DI(Dependency Injection): 의존관계, 의존성 주입
    • DI 컨테이너 제공
  • 클라이언트 측 코드를 변경하지 않고! 확장을 할 수 있다. (OCP, DIP 만족)
  • 순수 자바로 OCP, DIP을 지키려면 결국 스프링 DI 컨테이너를 만들게 된다.
  • 스프링은 결국 누군가 이것을 프레임워크로 모아둔 것이다. 계속 같은 코드 쓰기 힘드니까..

다음 섹션부터 이를 코드로 이해해보자.


요약

  • 모든 설계에서 역할과 구현을 분리하자.
  • app을 설계할 때, 역할만 만들어두고 구현은 언제든지 유연하게 변경할 수 있도록 만드는 게 좋은 객체 지향 설계이다.
  • 이상적으로는 모든 설계에 인터페이스를 부여하는 게 좋다.
    • 인터페이스를 먼저 설계하면 다른 부분부터 먼저 개발할 수 있다.
    • 구현 기술이 바뀌더라도 나중에 변경이 용이하다.
  • 그러나 실무에서는 추상화라는 비용이 발생한다.
    • 코드가 추상화되기 때문에 구현 클래스가 뭔지 알기 어려운 문제가 있다.
    • 즉 trade-off 관계이기 때문에, 기능을 확장할 가능성이 없다면 구현 클래스를 직접 사용하고 향후 필요할 때 리팩토링을 통해 인터페이스를 도입하는 것도 방법이다.

0개의 댓글