9 분 소요

스프링 이란??

스프링은 너무 많은 기능을 제공해 주고 있어 정의하기 쉽지가 않다.
스프링은 프레임워크인데 주요 기능 및 특징을 간단히 정리해보면

기술 :

1. 의존 주입 (DI, Dependency Injection) 지원

2. AOP (Aspect - Oriented Programming) 지원

3. MVC 웹 프레임워크 제공 등등


언어 :

자바, 코틀린, 그루비

이 외에도 수많은 기능 및 특징이 있다.
이렇게 방대하고 많은 스프링 프레임워크를 편리하게 사용할 수 있도록 지원해주는 것 또한 존재하는데

스프링 부트 :

스프링을 편하게 사용할 수 있도록 지원한다. 따라서 최근에는 기본으로 많이 사용 한다고 한다.

기능 :

1. 단독으로 실행할 수 있는 스프링 애플리케이션 쉽게 생성

2. Tomcat 같은 웹 서버 내장해서 별도로 설치 안해도 됨

3. 손쉬운 빌드 구성을 위한 starter 종속성 제공

4. 스프링과 외부 라이브러리 (3rd parth) 자동 구성 등등


이제 스프링에 대해 대충 알았으니 스프링을 왜 생겨났는지 알아보자

스프링이 필요로 하는 가장 큰 이유 :

좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 역할을 하기 때문


스프링이 어떻게 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와줄까???
그 전에 좋은 객체 지향 프로그래밍이 뭐지 ??

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


먼저 객체 지향 프로그래밍이 뭘까?

객체 지향 프로그래밍 (OOP, Object Oriented Programming) :

실세계에 존재하는 정보 혹은 상황을 “객체” 들의 모임과 “객체들 간의 메시지”를 주고받는 관계로 보자는 것이다.

즉 기존에 컴퓨터 프로그램을 단순히 명령어의 목록으로 보지말고 객체들의 모임과 객체들이 메시지를 주고 받으며 데이터를 처리한다고 보는 프로그래밍 이다.

이렇게 하면 뭐가 좋은데 ??

프로그램을 유연하고 변경이 용이하게 만들 수 있다. 따라서 대규모 개발에 많이 쓰인다.

하 이거 또 추상적인 표현이 나왔네… 프로그램을 유연하고 변경이 용이하게???? 이게 무슨 뜻??
이 뜻을 이해하기 위해선 객체 지향 프로그래밍 특징을 알아야 한다.

객체 지향 프로그래밍의 특징을 보면

1. 다형성 :

하나의 클래스나 메서드가 다양한 방식으로 동작 가능
대표적으로 오버라이딩과 오버로딩이 있다.

오버라이딩 : 부모 클래스의 메서드를 자식 클래스에서 재정의 오버로딩 : 이름이 같은 메서드가 매개변수의 자료형 혹은 수에 따라 다르게 정의 가능

단, 하위 클래스는 인터페이스 규약을 꼭 지켜야 한다. 즉 오버로딩, 오버라이딩 하면서도 인터페이스의 규약을 지키는 선에서 다르게 재정의를 해야한다.!!

2. 추상화 :

공통의 속성이나 기능을 가진 것들의 공통점을 일반화하여 묶고 각각의 세부적인 특징을 제거하여 단순하게 묶는 것이다. 객체 지향적 관점에서는 클래스를 정의하여 공통의 속성이나 기능 등을 묶는 것이다.

3. 캡슐화 :

클래스 내에 있는 정보들을 캡슐로 감싸서 숨기는 것을 의미한다. (클래스의 접근 지정자 이용)
이렇게 하여 외부에서 클래스 내의 정보들에 접근을 막아 정보 은닉을 할 수 있다.

4. 상속:

클래스와 클래스 사이의 관계를 의미하는데 클래스 간의 상속으로 엮이면 자식 클래스는 부모 클래스의 정보들을 상속 받을 수 있다.

등등이 있다.



-> 이 특징으로 인하여 프로그램을 역할과 구현으로 구분하기 쉽고 그로 인해 프래그램을 유연하고 용이하게 만들 수 있기 때문이다.

쉽게 생각하면 역할이 인터페이스이고 구현이 인터페이스를 구현(정의)한 객체라고 생각할 수 있다.

역할과 구현으로 분리 ??

현재 스프링 수업을 듣고 있는 김영한 강사님께서는 이 역할과 구현의 분리를 실세계에서 비유해주셨는데 훨씬 이해가 잘되었다. 만약 운전자와 자동차가 있다고 가정하자.
운전자의 역할과 자동차의 역할이 각자 있을 것이다.

그리고 자동차 종류가 매우 다양한데 자동차 역할을 여러 종류의 자동차들 각각을 따로 구현을 해야한다.
만약 자동차가 k3, 아반떼가 있다고 하자 k3를 운전할 줄 아는 운전자가 아반떼를 운전할 수 있을까? -> 당연하다
-> 자동차의 종류가 바뀌어도 운전자에게 영향을 주면 안된다.!!

유연하고 변경 용이하다 -> 운전자가 k3를 운전하다가 아반떼를 타고싶을 때 쉽게 운전할 수 있어야 한다.

어떻게 이게 가능?? 자동차의 역할(인터페이스)를 다 따라서 각 종류의 자동차를 구현하였기 때문에

운전자는 자동차의 역할에 대해서만 알고 있지 자동차 구현에 대한 것은 알지 못한다.

자동차의 구체적인 구현이 바뀌더라도 자동차의 역할은 바뀌지 않고 운전자 입장에서 자동차 구현이 바뀌더라도 운전할 수 있어야 한다.!!

운전자 역할 -> 자동차 역할

자동차 역할 <- 자동차 구현 : k3, 아반떼….

웹 서비스 입장에서 생각해보자 웹 서비스를 사용하는 클라이언트들은 웹은 구체적인 구현에 대해 알 필요가 없다. 만약 구현을 알아야만 서비스를 사용할 수 있다면 너무 끔찍하다… 그저 서비스를 사용하기만 하면 될 뿐이다.
마찬가지이다 운전자는 자동차의 구현에대해 알 필요가 없다. 아니 알고 싶지도 않다.. 자동차의 역할만 안다면 자동차 구현이 다르든 바뀌던 아니면 새로운 종류의 자동차가 나오더라도 운전자는 운전을 할 수 있다.!!
또한 클라이언트(운전자)에 영향을 주지 않으면서 새로운 기능을 제공할 수도 있다. 즉 구현이 바뀌더라도 클라이언트는 그것을 배울 필요가 없다.

이 모든 것을 역할과 구현으로 나누었기 떄문에 가능하다!!

또 다른 예시로는 내가 재밌게 본 드라마 나의 아저씨를 예로 들어보자!!
나의아저씨에서 남자 주인공 역으로 “박동훈”, 여자 주인공 역으로 “이지안”이 있다.
이 “박동훈”, “이지안”역을 역할로 볼 수 있다.

하지만 이 “박동훈” 역할은 이선균 배우가 맡을 수 있지만 다른 배우가 맡을 수도 있다. Ex) 조정석 배우
마찬가지로 “이지안” 역할은 아이유 배우가 맡을 수 있지만 다른 배우가 맡을 수도 있어야 한다. Ex) 전지현 배우
즉 각 역할을 다른 배우로도 대체가 가능하다. (가능 해야한다)
이렇듯 “박동훈”, “이지안” 역할은 역할로 구현은 “박동훈”을 이선균 배우로 할지 조정석 배우로할지 “이지안”을 아이유 배우가 할지 전지현 배우가 할지로 나누어져 있기 때문에 배우들을 대체 가능하다!!!
배우가 바뀌어도 역할은 바뀌지 않는다. “박동훈”역할을 이선균 배우가 하고 있고 “이지안”역할을 아이유 배우가 하고 있다가 아이유 배우 대신 전지현 배우가 대체하엿다해서 이선균 배우에게 영향을 전혀 끼치지 않는다. 그냥 대본에 따른 연기를 하면 된다.
-> “박동훈” 역할이 클라이언트 , “이지안”역할이 서버라고 가정하면 “이지안” 역할의 배우가 바뀐다해서(구현이 바뀐다해서) 클라이언트에게 영향을 끼치지 않는다.

이 모든 예가 객체 지향의 특징으로 유연하고 변경이 용이함을 보여준다.

정리 해보자

역할과 구현을 분리하면

  1. 클라이언트는 대상의 역할(인터페이스)만 알면 됨.
  2. 클라이언트는 구현 대상의 내부 구조를 몰라도 됨.
  3. 클라이언트는 구현 대상의 내부 구조가 바뀌어도 영향 X.
  4. 클라이언트는 심지어 구현 대상 자체를 변경해도 영향 X.

-> 프로그램이 단순해지고 유연해지며 변경 또한 편리해지는

스프링 프레임워크는 자바 언어로 구성 되어있다.
그러면 자바 언어로 역할과 구현을 나누어 보자

역할 : 인터페이스

구현 : 인터페이스를 구현(정의)한 클래스, 구현 객체


=> 따라서 객체를 설계할때 인터페이스(역할)를 먼저 부여를 한 후 그 역할을 수행하는 구현 객체를 만들자.(정의 하자)

객체간의 협력 관계을로 생각하자
클라이언트 : 요청
서버 : 응답

이라고 보면 수 많은 객체 클라이언트와 객체 서버는 서로 협력 관계를 가지게 된다!!!
어떻게 자바에선 역할과 구현을 나누는지 생각해보자

객체 지향의 특성 중 다형성 그 중에서도 오버라이딩을 생각해보면 어떤 인터페이스(역할)가 있고 그 인터페이스를 구현(정의)한 객체를 실행 시점에서 유연하게 변경할 수 있다. -> 유연, 변경 편리

즉 다형성 특징을 통해 (특히 오버라이딩 역할) 클라이언트를 변경하지 않고 서버의 구현 기능을 유연하게 변경할 수 있다.



역할과 구현을 분리 한다는 것을 정리 해보면

  1. 실세계에서 존재하는 정보를 역할과 구현이라는 편리한 컨셉을 객체지향의 특징들을 이용해 프로그램의 객체 세상으로 가져올 수 있다.
  2. 유연하고 변경이 용이하다.
  3. 설계의 확장이 가능하다.
  4. 클라이언트에게 영향을 주지 않으면서 내부 구조를 바꿀 수 있다.

-> 단 인터페이스(역할)을 안정적으로 잘 설계해야 역할을 기준으로 구현을 할 수 있기 때문에 역할을 잘 설계하는 것이 중요하다.!!


하지만 역할과 구현을 분리함에 있어서 문제점, 한계점은 없을까??

역할과 구현의 분리의 문제점(한계점)

역할(인터페이스) 자체가 변해버리면 클라이언ㅌ와 서버 모두에 큰 영향을 미친다. -> 클라이언트, 서버 모두 큰 변경이 발생.

ex) 역할 : 자동차에서 => 비행기로 바꾼다면 ???
구현도 모두 바뀔 뿐더러 (여러 비행기 종류가 있게지 ex) 보잉 등등), 운전자 역할 또한 변경됨. 당연하지 자동차랑 비행기는 완전히 달라지니까 운전 방법 또한 다르겠지



자 지금까지 스프링이 왜 생기게 되었냐?
-> 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 역할

좋은 객체 지향이 뭘 해주길래??
-> 프로그램을 유연하고 변경이 용이하게 만들 수 있다

프로그램을 어떻게 유연하고 변경이 용이하게 하지??
-> 객체 지향 특징을 통해 역할과 구현을 분리

여기서 의문 좋은 객체 지향을 어떻게 하는지? 의문이 든다..

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

1. SRP(Single Responsibility Principle) 단일 책임 원칙 :

한 클래스는 하나의 책임만 가져야 한다는 원칙이다.
여기서 책임이 무슨 책임을 뜻하는 건지 모르겠다… 거기다가 굳이 하나의 책임 ???
이 것은 문맥과 상황에 따라 다르게 해석되기 때문에 모호하다.
하지만 중요한 기준을 세워두면 상황에 따라 해석할 수 있는데 “변경” 이 그 기준이 된다.
프로그램 내에서 “변경”이 일어났을때 파급 효과 (영향)이 적다면 SRP를 잘 따르는 것으로 보면 된다.
ex) 한 클래스에 어떤 데이터를 읽는 기능과 어떤 데이터를 쓰는 기능 둘다 존재한다고 하면 만약 데이터를 읽는 기능에 “변경”이 일어났을때 이 클래스에 영향을 끼칠 것이다. 또한 데이터를 쓰는 기능에 “변경”이 일어 났을때에도 이 클래스에 영향을 끼칠 것이다. 따라서 클래스는 SRP를 지키지 않은 코드이고, 데이터를 읽는 기능, 쓰는 기능 두가지를 나눠서 각각의 클래스로 나눠야 SRP를 지킨 코드가 될 것이다.

2. OCP(Open / Closed Principle) 개방 / 폐쇄 원칙 :

소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀있다는 원칙이다.
먼가 이상하다.. 확장에는 열려있다고 했다. 확장을 하려면 코드를 변경에야 할 것 아닌가?? 근데 변경에는 닫혀있다니.. 통 이해가 되지 않는다.
-> 이 문제는 이전에 설명한 객체 지향의 특징 중 다형성으로 충분히 해결이 가능하다
만약 어떤 확장을 해야하는 상황이라면 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현한다면 기존 코드를 변경하지 않으면서 확장이 가능하기 때문이다. (이것 또한 “역할과 구현의 분리”로 가능하네..)


하지만 OCP에 큰 문제점이 있다. 아래 예를 들어 보자
ex)
public class A {
private B b = new C();
}

-> 확장

public class A {
//private B b = new C();
private B b = new D();
}

A : 클라이언트 코드
B : 서버 코드
라고 하자

만약 A 클라이언트가 구현 클래스를 직접 선택한다고 하자
B b = new C(); => 기존 코드
B b = new D(); => 변경 코드 일때

구현 객체를 변경하려면 클라이언트 코드를 변경해야 하는 상황이다. -> 예시 코드에선 분명히 다형성을 적용 (역할과 구현을 확실히 분리함) 했지만 OCP를 지킬 수 없는 상황이다.
이걸 어떻게 해결하지 ?….
-> 객체 생성하고 연관관계를 맺어주는 별도의 조립, 설정자가 필요한 상황이다.
이걸… 구현하기엔 너무 복잡한데…
그래서 Spring 프레임 워크가 생겨난 것임… -> 위 문제를 Spring Container가 해결 해준다..

3. LSP(Liskov Substitution Principle) 리스코프 치환 원칙:

프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다는 원칙이다.
이 원칙을 보면 확실히 이전에 설명한 객체 지향의 특성을 지키기 위한 원칙임을 알 수 있다.
다형성의 특징을 보면 하위 클래스는 인터페이스 규약을 꼭 지켜야 한다고 했는데 이 것을 의미하는 원칙이 LSP 이다.
ex) 쉽게 자동차로 예를 들면 엑셀을 밟으면 앞으로 가야하는 기능을 다형성 특징으로 구현을 다양하게 하는 경우에 더 빨리 앞으로 가게 한다던지, 조금 천천히 앞으로 가게 한다던지 등등 앞으로 가는 기능은 지켜야 한다. 하지만 옆으로 가거나 뒤로 가게 하는 구현을 하면 안된다는 것이다.
오버라이딩 입장에서 이런 기능을 나타내는 메서드를 재정의 할때 엑셀을 밟았을때 앞으로 가야하는 기본 인터페이스 규약은 꼭 지켜야 한다는 뜻이다.

따라서 LSP를 잘 지키면 이너페이스가 명확해지고, 대체 가능성이 높아지는 프로그램이 된다.!!!

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

특정 클라이언트를 위한 인터페이서 여러 개가 범용 인터페이스 하나보다 낫다는 원칙이다. 어떤 한 인터페이스가 여러개의 역할을 담고 있다면 각자의 역할이 서로 엮이게 되면서 서로 영향을 주게 된다. 따라서 하나의 역할이 변하면 다른 역할에 영향을 끼치기 때문에 문제가 된다.
따라서 인터페이스는 최대한 각각의 역할에 따라 분리가 되어야 한다.
ex)
자동차 인터페이스 -> 운전 인터페이스와 정비 인터페이스로 분리 하고
사용자 클라이언트를 -> 운전자 클라이언트와 정비사 클리이언트로 분리 한다면
만약에 정비 인터페이스가 변한다고 해도 운전자 인터페이스에 영향을 끼치지 않기 때문에 인터페이스 훨씬 명확해지는 장점이 있다.

즉 ISP를 지키면 인터페이스가 명확해지고 대체 가능성이 높아진다.!!!



5. DIP(Dependency Inversion Principle) :

프로그래머는 “추상화에 의존해야하고 구체화에 의존해선 안된다.”라는 원칙이다.
이전에 잠깐 언급된 의존성 주입은 DIP를 따르는 방법중 하나이다.
이 말을 다르게 표현하면 구현 클래스에 의존하지 말고 인터페이스에 의존 해라는 의미이다.!

즉 DIP는 이전에 설명한 “역할과 구현의 분리” 중에서도 “역할”에 의존해야 한다는 것이다.
객체 세상도 클라이언트가 인터페이스에 의존해야 유연하게 여러 구현을 할수 있기 때문이다 (구현체를 변경할 수 있음.) -> 유연하고 변경에 용이.

여기서 큰 문제가 있다.!!
OCP를 설명할때 예에서 A는 인터페이스에 의존하지만 구현 클래스도 동시에 의존하고 있다.
A 클라이언트가 구현 클래스를 직접 선택 하기 때문이다.
그럼 이전에 설명한 예시는 DIP를 위반하는 것으로 볼 수 있다.

정리 하자면..

  1. 객체 지향의 특성으로 개발이 편리하다.
  2. 다형성의 특성으로 구현 객체를 변경할 때 클라이언트 코드도 변경되는 문제가 존재했다.
    -> 따라서 다형성의 특성만으로는 OCP, DIP를 지킬 수 없었다. 이러한 문제를 해결할 방법을 찾아야 한다.

Spring에 대해 배우고자 하였지만 지금까지 Spring에 대한 이야기가 거의 없었다…
하지만 지금 까지 Spring이 왜 생겨 났는지를 알기 위해 이야기를 이어 왔다.
위의 정리 2에서 다형성의 특징만으로는 OCP, DIP를 지킬 수 없었다. 이 문제를 해결하기 위해 Spring이 생겨났다.

즉 OCP, DIP를 지키지 못했다는 것은 좋은 객체 지향 프로그래밍을 하지 못했다는 것이고 OCP, DIP를 지켜서 좋은 객체 지향 프로그래밍을 하게끔 도와주기 위해 Spring이 생겨난 것이다.


그럼 어떻게 Spring으로 OCP, DIP를 가능하게 하지??

스프링에서 OCP, DIP를 가능하게 하는 기술 :

1. DI(Dependency Injection) 의존관계, 의존성 주입

2. DI container 제공


-> 이 기술로 클라이언트 코드의 변경없이 기능이 확장이 가능하게 된다. 따라서 쉽게 부품을 교체하듯이 개발이 가능하게 된다.!!!

위 DI에 대해서는 앞으로 차차 알아가게 될 것이다.

Spring이 존재하기 이전에 개발자들은 좋은 객체 지향 개발을 하기 위해서 특히 OCP, DIP를 지키면서 개발을 하기 위해서 너무 많은 고생을 하게 되었고 자연스럽게 이러한 문제를 해결하기 위해 Spring 프레임워크가 생기게 되었다. (정확히는 DI container가 생기게 되었다.)

지금 까지 Spring이 왜 생겨나게 되었는지 알게 되었다.

이후에는 Spring이 왜 생겨났는지 코드를 통해 느껴보고자 한다.

Reference :

김영한 강사님 스프링 핵심 원리 - 기본편 강의 중