[Spring 공부] Spring 핵심 원리 - 1
2022. 02. 23.
Spring Study
Spring에 대해 처음 공부하는 사람이 정리를 목적으로 작성한 글입니다. 오개념 등 잘못된 부분이 있을 경우 댓글로 가감없이 지적해주세요! 확인하는 대로 정확한 정보를 기반해 빠르게 수정할 수 있도록 하겠습니다.
개요
이번 Spring Boot 스터디는 배달의 민족의 김영한님께서 인프런에 올려주시는 자바 스프링 완전 정복 시리즈를 기반으로 진행한다. 그 시리즈의 가장 첫 강의인 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술을 끝내고, 다음 강의인 스프링 핵심 원리 - 기본편을 정리해보려 한다.
이번 포스트에서는 강의 초반부의 내용이자, 개인적으로 큰 감명을 받은, 그리고 학교에서 배우지 못한 객체 지향 프로그래밍의 장점 중 하나인 다형성에 대한 이야기를 해보려 한다.
다형성
여러분들은 다형성이 뭐라고 알고 계신가요?
이 다형성이라는 개념은 어디선가 자바를 배워본 경험이 있는 사람이라면 꼭 한번씩 자바의 특징, 객체 지향 프로그래밍의 특징으로 배워본 경험이 있을 것이다. 나의 경우 지금까지 이해하기를 대충 부모 타입에 자식 타입을 끼워넣을 수 있다 정도로 알고 있었다.
이를 처음 배울 때 여느 사람들과 같이 직사각형 - 정사각형이라는 예시로 배웠었다. 이를테면 코드로 나타내면 다음과 같을 것이다.
public class Rectangle {
private int width;
private int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
}
public class Square extends Rectangle {
@Override
public void setWidth(final int width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(final int height) {
super.setWidth(height);
super.setHeight(height);
}
}
정사각형은 직사각형이니 이를 코드로 표현하면 위와 같이 되고, Rectangle
이란 타입에 Square
객체를 끼워넣을 수 있다는 정도로 나는 다형성을 이해하고 있었다. 그리고 조금 더 시간이 지나서 어디선가 SOLID 라는 것을 줏어 들었을 때, 위의 예시가 객체 지향 설계의 원칙 그 중에서도 리스코프 치환 원칙(LSP)를 지키지 않고 있다는 사실을 알게 되었다. (그 이유에 대해선 이 글이 도움이 많이 되었다.)
알음알음 자투리 지식을 습득할수록, 나에겐 확신보단 혼란만이 가중되었다. “그래서 이 SOLID 라는 원칙을 다 지키는게 현실적으로 가능한건가? 이러면 상속이고 컴포지션이고 뭔가 제대로 쓰기 너무 힘들 것 같은데?” 라는 생각만 가지게 되었다. 그 이후로는 정제된 객체 지향 설계에 의한 코드보다는 흔히들 말하는 Best Practice의 모양을 따라하기에 급급했던 것 같다. 적당히 따라하니까 적당히 이쁘고 적당히 클린하고 유지보수도 적당히 되고(물론 아무도 앞의 내용을 인정한 적은 없음ㅋㅋ) 또 적당히 돌아가니까 점점 코드레벨의 정밀한 설계와 패턴에는 관심이 없어지고, 아키텍처나 인프라적인 부분에서의 설계에만 관심을 가졌던 것 같다.
조금의 시간이 흘러 이 강의를 만나고서야 SOLID에 대한 의문, 다형성에 대한 의문을 거의 완벽하게 풀 수 있었던 것 같다. 또, 이 강의 덕분에 다시 객체 지향 프로그래밍에 관심도 생기고 학습에 대한 의지도 생겼다.
사설이 길었다. 이 강의에서는 직사각형 - 정사각형 예제처럼 실생활의 상황을 빗대어서 객체 지향 프로그래밍, 특히 다형성에 대해 설명해준다. 천천히 살펴보자.
역할과 구현
다형성을 설명하기 위해서 가장 중요한 개념은 역할과 구현이다. 예를 들어 운전자와 자동차로 생각해보자.
어떤 운전자 역할을 맡은 사람이 자동차 역할을 사용한다고 생각할 수 있다. 그리고 저 운전자 역할에는 구현으로써 내가 들어갈 수도, 아니면 다른 누군가가 들어갈 수도 있다. 동일하게 자동차 역할에는 구현으로써 소나타가 들어갈 수도, 아니면 운만 좋으면 테슬라 모델 3 혹은 벤츠 C 클래스가 들어갈 수도 있다.
역할과 구현을 분리하는 것은 그냥 운전자 구현과 자동차 구현만을 갖는 것에 비해서 많은 장점을 가진다.
어떤 장점을 가지는데?
보통 이러한 패턴 혹은 모델에서 무언가를 사용하는 측을 클라이언트(Client)라 한다. 위의 예시에서는 운전자가 클라이언트가 된다.
역할과 구현을 분리하면 다음의 장점을 가지게 된다.
- 클라이언트는 대상(자동차)의 역할(인터페이스)만 알면 된다.
- 클라이언트는 대상(자동차)의 내부 구조나 구체적인 내부 구현을 몰라도 된다.
- 클라이언트는 대상(자동차)의 내부 구조나 구체적 내부 구현이 변경되어도 영향을 받지 않는다.
- 클라이언트는 대상(자동차)의 구현 자체를 변경해도(자동차를 바꿔도) 영향을 받지 않는다.
우리가 자동차를 운전한다고 하자.
- 면허만 소지하고 있다면 대충 가속 페달을 밟으면 앞으로 가고, 브레이크를 밟으면 멈추는 자동차의 역할(인터페이스)를 숙지하고 있기 때문에 자동차의 종류(구체적 구현)에 대해서는 모르더라도 운전을 할 수 있다.
- 소나타의 엔진 종류가 무엇이고 타이어는 무엇을 쓰며 배기량이 몇인지에 대해 전혀 알 필요가 없다.
- 만약 정비를 받으러 가서 브레이크를 신형으로 바꾸거나, 배터리를 바꾸더라도 운전하는 기본 기능 자체에는 영향을 받지 않는다.
- 조금 극단적으로 소나타를 테슬라 모델 3로 변경을 하더라도, 우리는 운전을 할 수 있다.
이와 같이 역할과 구현을 분리하면 굉장히 유연해지고 변경도 편리해진다.
자바에서는 어떻게 해?
자바에서 역할과 구현을 분리하기 위해, 자바 언어가 지원하는 다형성을 활용한다.
역할은 인터페이스로, 구현은 인터페이스를 구현한 클래스로 작성할 수 있다.
따라서 객체를 설계할 때 역할과 구현을 설계 단계에서부터 명확히 분리해야 한다. 그리고 당연히 역할(인터페이스)를 먼저 부여하고, 그 역할을 수행할 수 있는 구체적인 구현을 작성하는 편이 나을 것이다.
그리고 당연하게, 인터페이스에 변경이 일어나는 순간 해당 인터페이스를 구현하는 클래스들의 구현을 모두 수정해야할 것이니 설계 단계에서부터 인터페이스를 안정적으로 잘 설계해야 할 것이다.
다형성의 본질은?
그렇다면 이 다형성의 본질은 무엇이라고 할 수 있을까? 아무래도,
- 인터페이스를 구현한 객체 인스턴스를 실행 시점에서 유연하게 변경할 수 있다. 예를 들어, 자동차 인터페이스를 구현한 객체 인스턴스가 소나타, 그랜저, 마티즈가 있다고 할 때, 실행 시점에 원하는 대로 차종을 고를 수 있다.
- 서로 클라이언트 - 서버가 되는 객체간의 협력 관계에서, 클라이언트는 서버의 구체적인 구현이 아닌 인터페이스에만 의존하게 되기 때문에 서버의 내부 구현을 유연하게 변경할 수 있다.
정도로 정리할 수 있을 것 같다.
그럼 다형성만 있으면 SOLID 지키기 ㄱㄴ?
한마디로 이야기하면 ㅂㄱㄴ ㅋㅋ
다형성을 활용하더라도 개방-폐쇄 원칙(OCP)와 의존 관계 역전 원칙(DIP)를 지킬 수 없다.
예를 들어 운전자 역할의 구현으로 DriverA
가 있고, 자동차 역할의 구현으로는 위의 3가지를 그대로 활용한다고 하자. 그러면 DriverA
내부에서는 자동차를 사용하기 위해 해당 인스턴스를 생성해서 사용해야 할 것이다. 자, 원래 자동차로 소나타를 사용하고 있었다고 하자.
public class DriverA implements Driver {
private Car driverACar = new Sonata();
}
그리고 이 차를 테슬라 모델 3로 변경하고 싶다고 하자. 어떻게 해야 할까?
어쩔 수 없다. 클라이언트인 DriverA
내부에서 탈 자동차를 직접 바꿔주어야 한다.
public class DriverA implements Driver {
// private Car driverACar = new Sonata();
private Car driverACar = new TeslaModel3();
}
사용하는 구현 객체를 변경하기 위해 클라이언트인 DriverA
의 코드도 변경해야 하기 때문에 확장에는 열려있고 변경에는 닫혀있어야 한다는 OCP를 지키지 못한다. 또한, DriverA
는 Car
라는 인터페이스에도 의존하지만 동시에 Sonata
와 TeslaModel3
에도 동시에 의존하기 때문에 DIP마저 위반한다.
아니 그럼 어케함;;
다형성만으론 OCP와 DIP를 지킬 수 없다는 슬픈 사실을 확인했다. SOLID를 지키기 위해선 확실히 뭔가 더 필요하다.
스프링과 객체 지향
스프링과 객체 지향의 관계에서도 다형성이 가장 중요한 키워드이다. 스프링은 다형성을 극대화해서 이용할 수 있게 프레임워크단에서 개발자를 도와준다.
스프링이라 하면 굉장히 유명한 키워드인 제어의 역전(IoC)나 의존 관계 주입(DI)는 다형성을 활용해서 역할과 구현을 편리하게 분리하고, 다룰 수 있도록 지원해준다. 그리고 의존 관계 주입(DI)과 DI 컨테이너 제공을 통해 다형성 뿐 아니라 OCP, 그리고 DIP도 지킬 수 있게 도와준다. 이 말인 즉슨 클라이언트의 코드 변경 없이 기능을 확장할 수 있게 된다는 뜻이다. 이렇게 스프링을 쓰면 마치 부품을 교체하듯이, 조립하듯이 개발을 할 수 있다.
다음은
이 강의에서도 실습을 통해서 개념을 이해하는 내용이 바로 따라온다. 코딩과 관련된 부분을 너무 참조하기에는 포스트가 길어지기도 하고, 너무 강사분의 설명 노하우를 유출하는 죄책감이 있어서 코딩을 하는 파트는 제외하려 한다. (진짜 명강의이니 궁금하신 분들은 꼭 들어보길 바란다.)
다음 포스트에서는 IoC, DI, 그리고 컨테이너에 대한 내용을 정리해 보도록 하겠다.
끝.