Spring

[스프링] 스프링 핵심 원리 기본편 정리: 1. 객체 지향 설계와 스프링

아윤_ 2023. 8. 4. 12:31

 
해당 강의는 인프런에서 '김영한' 님의 "스프링 핵심 원리 기본편" 강의를 보고 정리한 내용이다.
 
무료 강의인 "스프링 입문" 강의를 듣고 스프링에 대해 자세히 공부하고 싶다는 마음이 생겨 로드맵을 따라 강의를 듣고 공부 내용을 정리하고자 한다.
 
 
[참고 링크]
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보세요! 📢

www.inflearn.com

 
 


 

객체 지향 설계와 스프링

 

 

 

자바 진영의 추운 겨울과 스프링의 탄생

 

과거 스프링이 탄생하기 이전에, EJB(Enterprise Java Beans)라는 자바 진영의 표준 기술이 존재했다. 
하지만, EJB는 너무 복잡하고, 속도가 느리다는 여러 가지 문제들이 존재했다.
 
이러한 EJB의 문제를 해결하기 위해 당시 개발자였던 '로드 존슨'이 EJB의 문제점을 지적하며, EJB 없이도 고품질의 확장 가능한 애플리케이션을 개발할 수 있는 예제 코드를 선보이는 책을 출간하게 된다.
 
출간 직후, '유겐 휠러'와 '얀 카로프'가 로드 존슨에게 오픈소스 프로젝트를 제안하게 되며, '스프링'이라는 전설이 시작된다.
 
스프링EJB라는 겨울을 넘어 새로운 시작, 즉 봄이라는 의미로 지은 이름이라고 한다. 개인적으로 이름의 뜻이 인상 깊었다.  

 

 

 

스프링이란 무엇인가?

 

스프링 생태계

 

스프링은 하나가 아닌, 여러가지 기술들의 모음이다. 스프링에는 다음과 같이 다양한 기술들이 존재한다.

 

  • 필수 기술: 스프링 프레임워크, 스프링 부트
  • 선택 기술: 스프링 데이터, 스프링 세션, 스프링 시큐리티, 스프링 Rest Docs, 스프링 배치, 스프링 클라우드

 
필수 기술로는 스프링 프레임워크와, 스프링 부트가 있으며, 선택 기술로는 다음과 같은 기술들이 존재한다.
이외에도 더 많은 기술들이 존재하는데, 이러한 내용은 해당 사이트를 참고하면 확인할 수 있다.
https://spring.io/projects

 

Spring | Projects

Projects From configuration to security, web apps to big data—whatever the infrastructure needs of your application may be, there is a Spring Project to help you build it. Start small and use just what you need—Spring is modular by design.

spring.io

 

우리는 필수 기술인 스프링 프레임 워크와 스프링 부트에 대해 좀 더 자세히 살펴볼 것이다.

 

 

스프링 프레임워크


스프링 프레임워크는 스프링에서 가장 중요한 기술이다. 스프링 프레임워크에도 다양한 기술들이 존재하며, 이는 다음과 같다.
 

  • 핵심 기술 : 스프링 DI 컨테이너, AOP, 이벤트, 기타
  • 웹 기술 : 스프링 MVC, 스프링 WebFlux
  • 데이터 접근 기술 : 트랜잭션, JDBC, ORM 지원, XML 지원
  • 기술 통합 : 캐시, 이메일, 원격 접근, 스케줄링
  • 테스트 : 스프링 기반 테스트 지원
  • 언어 : 코틀린, 그루비

 
최근에는 스프링 부트를 통해서 스프링 프레임워크의 기술들을 편리하게 사용하고 있다.
 
 

스프링 부트

 

스프링 부트스프링을 편리하게 사용할 수 있도록 지원하는 기술로, 최근에는 기본으로 사용하고 있다.
 

스프링 부트의 장점

  • 단독으로 실행할 수 있는 스프링 애플리케이션을 쉽게 생성할 수 있다.
  • Tomcat 같은 웹 서버를 내장해서 별도의 웹 서버를 설치하지 않아도 된다.
  • 손쉬운 빌드 구성을 위한 starter 종속성을 제공한다.
  • 스프링과 3rd path(외부) 라이브러리를 자동으로 구성한다.
  • 메트릭, 상태 확인, 외부 구성 같은 프로덕션 준비 기능을 제공한다.
  • 관례에 의한 간결한 설정이 가능하다.

 
주의할
스프링 부트는 스프링 프레임워크와 별도로 사용할 수 있는 게 아니다. 즉, 스프링 부트는 단독적으로 사용할 수 없다. 스프링 부트는 단지 스프링의 여러 가지 기술들을 중간에서 편리하게 사용할 수 있는 기능을 제공하는 역할만 한다. 
 

 

스프링 단어의 사용


스프링이라는 단어는 다음과 같이 문맥에 따라 다르게 사용된다.

  • 스프링 DI 컨테이너 기술
  • 스프링 프레임워크
  • 스프링 부트, 스프링 프레임워크 등을 모두 포함한 스프링 생태계

 
예를 들어, 어떤 사람이 스프링이 좋다고 말했을 경우에는 세 번째 의미로 사용되고, 소스 코드 상에서 스프링 컨테이너에 대해 얘기할 경우에는 첫 번째 의미로 사용된다.

 

 

스프링은 왜 만들었을까?

 

로드 존슨이 스프링을 왜 만들었는지에 대해 알아보기 전에 기억해야 될 점이 있는데, 그것은 바로 기술의 핵심 개념이다.
 

이 기술을 왜 만들었는가?
이 기술의 핵심 컨셉은 무엇인가?

스프링과 같이 아무리 복잡하고 큰 기술도 이 핵심 개념은 정말 단순한 개념에서 시작되며, 꼭 알아야 되는 중요한 사항이다. 지금부터 그 본질적인 개념에 대해 알아보자.

 

 

스프링의 진짜 핵심

 
스프링의 진짜 핵심은 다음과 같다.

  • 스프링은 자바 언어 기반의 프레임워크이다.
  • 자바 언어는 객체 지향 언어라는 가장 큰 특징을 가진다.
  • 스프링은 객체 지향 언어가 가진 가장 강력한 특징을 살려내는 프레임워크이다.

 
즉, 스프링은 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크이다.

 

 

 

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

 

객체 지향 프로그래밍


객체 지향 프로그래밍이란, 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위, 즉 "객체"들의 모임으로 파악하고자 하는 것이다.
각각의 객체협력을 통해 메시지를 주고받고, 데이터를 처리할 수 있다.
 
객체 지향 프로그래밍은 프로그래밍을 유연하고, 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용된다.
 
객체 지향에는 추상화, 캡슐화, 상속, 다형성과 같은 여러 특징들이 존재하는데, 그중 객체지향의 핵심은 다형성이다.
 

 

다형성


레고 블록을 조립하거나 키보드와 마우스를 갈아 끼우는 것과 같이 컴포넌트를 쉽고 유연하게 변경하면서 개발할 수 있는 방법다형성이라고 한다.
 
다형성에 대해 쉽게 이해하기 위해 다형성을 실세계에 비유해서 역할구현으로 이 세상을 구분한다고 가정해 보자.
 
 

운전자 - 자동차 
 

먼저, 아래의 그림은 운전자라는 역할자동차라는 역할이 존재할 때, 자동차라는 역할을 K3, 아반떼, 테슬라 모델 3으로 구현한 모습이다.
 
운전자의 입장에서 봤을 때, 운전자는 K3를 타다가 아반떼로 차를 바꿔도 운전을 할 수 있다. 즉, 자동차가 바뀌어도 운전자에게 영향을 주지 않는다.
 
운전자를 클라이언트라고 했을 때, 클라이언트가 자동차의 내부 구조를 알지 못하거나, 내부 구조가 바뀐다고 하더라도 자동차가 자동차의 역할만 수행하고 있다면 클라이언트에게 영향을 주지 않는다.
 
또한, 자동차의 역할만 잘 구현하고 있다면, 언제든지 새로운 자동차가 나올 수 있다. 즉, 자동차 세상을 무한히 확장 가능하다.
 
 

 

 
공연 무대 - 로미오와 줄리엣 공연

 
또 다른 예시로, 로미오와 줄리엣의 공연 무대에 대해 살펴보자.
 
그림과 같이 로미오줄리엣이라는 역할과 이 역할을 수행하는 배우가 여러 명 있다.
로미오와 줄리엣 공연을 할 때 배우는 대체가 가능해야 한다.
 
이와 같이 로미오와 줄리엣 공연에서 배우는 변경 가능한 대체 가능성이 있다. 객체 지향 프로그래밍에서 유연하고 변경이 용이하다는 뜻은 이러한 것을 의미한다. 
 
로미오 역할의 입장에서 봤을 때 줄리엣 역할을 김태희, 송혜교 둘 중 누가 할지는 상관이 없고, 대본에만 충실하면 된다.
 
로미오 역할을 클라이언트라고 했을 때, 줄리엣 역할이라는 구현이 바뀐다고 해서 로미오 역할에 영향을 주지 않는다.
 
 

 

 

역할과 구현을 분리

 
이처럼 세상을 역할구현으로 분리할 경우, 세상이 단순해지고, 유연해지며 변경이 편리해진다.
 

장점

  • 클라이언트는 대상의 역할(인터페이스)만 알면 된다.
  • 클라이언트는 구현 대상의 내부 구조를 몰라도 된다.
  • 클라이언트는 구현 대상의 내부 구조가 변경되어도 영향을 받지 않는다.
  • 클라이언트는 구현 대상 자체를 변경해도 영향을 받지 않는다.

 

자바 언어에서의 역할과 구현의 분리

  • 자바 언어의 다형성을 활용한 것이다.
  • 역할인터페이스를 의미한다.
  • 구현은 인터페이스를 구현한 클래스 또는 구현 객체를 의미한다.
  • 객체를 설계할 때에는 역할구현을 명확히 분리해야 한다.
  • 객체 설계 시에는 역할(인터페이스)을 먼저 부여하고, 그 역할을 수행하는 구현 객체를 만들어야 한다.

 
자바 언어의 다형성에 대해 좀 더 자세히 알아보기 전에 우리는 객체의 협력이라는 관계부터 생각해야 한다.
이 세상에 혼자 있는 객체는 없으며, 객체는 클라이언트와 서버로 이루어진다.
 
여기서 클라이언트요청하는 사람을 의미하며, 서버는 요청을 받아 응답하는 사람을 말한다.
 
이처럼 수많은 객체 클라이언트와 객체 서버는 서로 협력 관계를 가진다.
 

객체의 협력 관계

 
 

자바 언어의 다형성


자바 언어에서는 다형성을 어떻게 구현했을까?
이는 오버라이딩을 떠올리면 된다. 오버라이딩은 자바의 기본 문법이며, 기본적으로 오버라이딩 된 메서드가 실행된다.
 
그림은 MemberService가 인터페이스인 MemberRepository의 save() 메서드를 호출한 경우를 나타낸다. 이때 MemberRepository의 save()가 호출되는 것이 아니라, MemoryMemberRepository 또는 JdbcMemberRepository의 save() 메서드가 호출된다. 이러한 것을 오버라이딩이라고 한다.
 

자바 언어의 다형성 - 오버라이딩

 
 
이처럼 다형성은 인터페이스를 구현한 객체를 실행 시점에 유연하게 변경할 수 있다는 장점이 있다. 물론 클래스 상속 관계도 다형성과 오버라이딩이 적용된다.
 
 

 
 
클라이언트를 MemberService라고 했을 때,  MemberService는 MemberRepository를 의존하는 것을 알 수 있다.
의존한다는 것은 의존하는 대상인 MemberRepository에 대해 알고 있다는 것을 의미한다.
 
 

 

 
 
두 그림을 살펴보면, MemberRepository에 이를 구현한 MemoryMemberRepository, JdbcMemberRepository를 할당할 수 있지만, MemberRepository 인터페이스와 전혀 관련이 없는 객체는 할당할 수 없다.
 
 

 
 
MemberRepository에 MemoryMemberRepository를 넣으면, 클라이언트는 빨간색 서버를 바라보게 되고, JdbcMemberRepository를 넣으면, 클라이언트는 초록색 서버를 바라보게 된다. 
 

 

다형성의 본질


지금까지의 내용을 통해 우리는 다형성의 본질에 대해 알 수 있다.
 

  • 인터페이스를 구현한 객체 인스턴스를 실행 시점유연하게 변경할 수 있다.
  • 다형성의 본질을 이해하려면 협력이라는 객체사이의 관계에서 시작해야 한다.
  • 클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있다.

 
 

정리

  • 실세계의 역할과 구현이라는 편리한 컨셉을 다형성을 통해 객체 세상으로 가져올 수 있다.
  • 다형성 덕분에 유연하고 변경이 용이해지며, 이를 무한히 확장 가능하다.
  • 클라이언트에 영향을 주지 않는 변경이 가능하다.
  • 인터페이스를 안정적으로 잘 설계하는 것이 중요하다.

 

 

스프링과 객체 지향


여태까지 우리는 객체 지향에 대해 살펴보았다. 이제 스프링과 객체 지향이 어떠한 연관이 있는지 알아보자.
 

  • 객체 지향에서는 다형성이 가장 중요하다.
  • 스프링은 다형성을 극대화해서 이용할 수 있도록 도와준다.
  • 스프링에서 이야기하는 제어의 역전(IoC), 의존관계 주입(DI)은 다형성을 활용해서 역할과 구현을 편리하게 다룰 수 있도록 지원한다.
  • 스프링을 사용하면 마치 레고 블록을 조립하고, 공연 무대의 배우를 선택하듯이 구현을 편리하게 변경할 수 있다.

 

 
 

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


스프링과 객체 지향 설계에 대해 제대로 이해하려면 다형성 외에도 좋은 객체 지향 설계의 5가지 원칙 (SOLID)에 대해 더 알아야 한다. 면접에도 자주 나오는 질문 유형이라고 하니 집중해서 살펴보자.

 

 

SOLID 

  
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)


SRP한 클래스는 하나의 책임만 가져야 한다는 것을 의미한다.
 
하나의 책임이라는 것은 클 수 있고, 작을 수도 있으며, 문맥과 상황에 따라 다르기 때문에 모호하다.
 
중요한 판단 기준은 변경이며, 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것이라고 볼 수 있다.
 

 

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


OCP는 가장 중요한 원칙 중 하나로
소프트웨어 요소는 확장에는 열려 있으나, 변경에는 닫혀 있어야 하는 것을 의미한다.
 
이게 무슨 말인지 이해가 되지 않을 것이다. 확장을 하려면 당연히 기존 코드를 변경해야 되는데 말이다..!
 
여기서 다형성을 활용해 보자. 운전자와 자동차의 예시에서 운전자가 자동차를 K3에서 아반떼로 바꾸어도 운전자는 그냥 운전을 할 수 있다. 새로운 자동차가 나온다고 해서 운전자는 바뀌지 않는다.
 
즉, 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현하는 경우를 말한다.
 
지금까지 배운 역할과 구현의 분리를 생각하면 이해하는데 도움이 될 것이다.
 
 
해당 코드의 경우, MemberService는 MemberRepository 인터페이스에 대해 알고 있고,  MemoryMemberRepository를 할당한 경우이다.

public class MemberService{
	private MemberRepository memberRepository = new MemoryMemberRepository();
}

 
여기서는 MemberServicer가 MemberRepository 인터페이스에 대해 알고 있는 건 동일하지만, 할당된 객체가 JdbcMemberRepository로 변경된 것을 알 수 있다.

public class MemberService{
	//private MemberRepository memberRepository = new MemoryMemberRepository();
    private MemberRepository memberRepository = new JdbcMemberRepository();
}

 
여기서 뭔가 의문점이 생긴다. 만약 OCP 원칙을 잘 지키고 있다면, 클라이언트인 MemberService의 변경 없이 JdbcMemberRepository를 적용할 수 있어야 되는데, MemberService의 코드가 바뀌는 것을 알 수 있다. 즉, 해당 코드는 변경에 닫혀있지 않다.
 
 

OCP 개방-폐쇄 원칙의 문제점

  • MemberService 클라이언트가 구현 클래스를 직접 선택하고 있다.
    • MemberRepository m = new MemoryMemberRepository(); //기존 코드
    • MemberRepository m = new JdbcMemberRepository(); //변경 코드
  • 구현 객체를 변경하려면 클라이언트 코드를 변경해야 한다.
  • 분명 다형성을 사용했지만 OCP 원칙을 지킬 수 없다.

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

 

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

 

LSP는 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다는 것을 말한다.
 
다시 말해, 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다. 즉, LSP는 다형성을 지원하기 위한 원칙이다. 인터페이스를 구현한 구현체를 믿고 사용하기 위해서는 이 원칙이 필요하다.
 
예를 들어, 자동차라는 인터페이스가 있을 때, 구현체에서 액셀이라는 기능을 구현할 때 액셀을 밟으면 앞으로 가도록 해야 하는데 뒤로 가는 차를 구현했을 경우 LSP를 위반하게 된다.
 
하지만, 뒤로 가는 차를 구현하더라도 오류가 나지 않고 컴파일에 성공하기 때문에 단순히 컴파일에 성공하는 것을 넘어선다.

 

 

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

 
ISP는 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다는 것을 말한다.
 
예를 들어, 자동차라는 인터페이스가 있을 때, 이를 운전 인터페이스와 정비 인터페이스로 분리하는 경우에 사용자 클라이언트를 운전자 클라이언트와 정비사 클라이언트로 분리할 수 있다.
 
이렇게 인터페이스를 분리할 경우 정비 인터페이스 자체가 변해도 운전자 클라이언트에 영향을 주지 않는다.
즉, 인터페이스가 명확해지고 대체 가능성이 높아진다.
 
 

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


OCP와 같이 가장 중요한 원칙 중 하나로, 두 원칙은 서로 연관이 있다.
 
DIP란 프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안 된다."라는 것을 의미한다. 의존성 주입은 이 원칙을 따르는 방법 중 하나이다.
 
쉽게 이야기해서 클라이언트가 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻이다.
 
앞에서 이야기한 역할(Role)에 의존하게 해야 한다는 것과 같다. 객체 세상도 클라이언트가 인터페이스에 의존해야 유연하게 구현체를 변경할 수 있다. 구현체에 의존하게 되면 변경이 아주 어려워진다. 
 
 

DIP 의존관계 역전 원칙의 문제점

  • OCP에서 설명한 MemberService는 인터페이스에 의존하지만, 구현 클래스도 동시에 의존한다.
  • MemberService 클라이언트가 구현 클래스를 직접 선택한다.
    • MemberRepository m = new MemoryMemberRepository();

 

  • 즉, DIP를 위반한다.

 

 

정리

  • 다시 정리하면, 객체 지향의 핵심은 다형성이다.
  • 하지만 다형성 만으로는 쉽게 부품을 갈아 끼우듯이 개발할 수 없다.
  • 구현 객체를 변경할 때 클라이언트 코드도 함께 변경된다.
  • 다형성 만으로는 OCP, DIP를 지킬 수 없다. 
  • 무언가 더 필요하다.

 
 
 

객체 지향 설계와 스프링 

 

다시 스프링으로


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

  • 스프링은 다음 기술로 다형성 + OCP, DIP를 가능하게 지원한다.
    • DI(Dependency Injection) : 의존관계, 의존성 주입
    • DI 컨테이너 제공

 

  • 클라이언트 코드의 변경 없이 기능을 확장할 수 있다.
  • 쉽게 부품을 교체하듯이 개발이 가능하다.

 

 

스프링이 없던 시절

 
옛날에 어떤 개발자가 좋은 객체 지향 개발을 하려고 OCP, DIP 원칙을 지키면서 개발을 해보니 너무 할 일이 많아 스프링 프레임워크를 만들어버렸다.
 
순수하게 자바로 OCP, DIP 원칙들을 지키면서 개발을 해보면, 결국 스프링 프레임워크를 만들게 된다. (더 정확히는 DI 컨테이너)
 
DI 개념은 말로 설명해도 이해가 잘 안 되며, 코드로 짜봐야 비로소 그 필요성을 알게 된다. 앞으로는 스프링이 왜 만들어졌는지 코드로 이해해 보는 시간을 갖게 될 것이다.
 

 

정리

  • 모든 설계에 역할구현을 분리하자.
  • 자동차, 공연의 예를 떠올려보자.
  • 애플리케이션 설계도 공연을 설계하듯이 배역만 만들어두고, 배우는 언제든지 유연하게 변경할 수 있도록 만드는 것이 좋은 객체 지향 설계다.
  • 이상적으로는 모든 설계에 인터페이스를 부여하자.