당니의 개발자 스토리
좋은 객체 지향 프로그래밍이란? 본문
이번 시간에는 좋은 객체 지향 프로그램이 뭔지 알아보겠습니다.

객체 지향의 특징은 추상화, 캡슐화, 상속, 다형성 등등이 있습니다.
다형성이란?



객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위, 즉 "객체"들의 모임으로 파악하고자 하는 것이다. 각각의 객체는 메시지를 주고받고, 데이터를 처리할 수 있다.
또 객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용된다.
여기서 핵심 키워드는 객체들의 모임, 그리고 또 객체끼리는 메시지를 주고받고 데이터를 처리할 수 있다는 것입니다. 뭔가 객체들이 서로 메시지를 주고받으면서 협력을 한다는 거죠.
그리고 객체 지향의 장점으로 또 한가지, 유연하고 변경이 용이하대요. 그래서 대규모 소프트웨어 개발에 많이 사용이 된대요.
유연하고 변경이 용이하다는 뜻이 뭘까요?

유연하고 변경이 용이하다는 건 레고 블럭을 조립하듯이, 마치 기존의 키보드와 마우스를 새로운 키보드, 마우스로 갈아끼우듯이, 컴퓨터 메모리를 더 업그레이드해서 컴퓨터 부품을 꼽고 갈아끼우듯이,
컴포넌트를 쉽고 유연하게 변경하면서 개발할 수 있는 방법이 객체 지향인 겁니다.
이건 객체 지향의 가장 큰 장점입니다.
궁극의 유연함과 궁극의 변경이 용이한 방법, 이게 바로 객체 지향의 핵심인 다형성입니다. Polymorphism 이라고도 하죠.

지금부터 여러분들이 책으로만 배운 다형성 말고, 진짜 다형성에 대해서 알려드릴게요.
왜 이 다형성이 중요한지 알려드리겠습니다.
먼저 다형성을 실세계로 좀 비유를 해볼게요.
이 세상을 역할과 그 역할을 직접 행하는 구현으로 구분해 볼게요.
쉽게 얘기하면, 역할이 인터페이스고, 구현이 실제 그 인터페이스를 구현한 객체라고 생각하시면 돼요.
운전자와 자동차 예제를 들어볼게요.

운전자라는 역할이 있고 자동차라는 역할이 있어요. 예를 들어, 이 자동차라는 역할을 K3라는 자동차가 구현을 하고, 아반떼라는 자동차가 구현하고, 테슬라 모델3가 구현을 한단 말이에요.
자 이 자동차 역할을 3개의 다른 자동차로 구현을 했죠. 이때 운전자가 K3를 타다가 아반떼로 차를 바꾼다고 할 때, 여전히 운전할 수 있나요, 없나요? 당연히 있죠.
왜냐면 자동차란 역할에 대한 구현만 바뀌었을 뿐이란 말이에요. 그래서 자동차가 바뀌어도 운전자한테 영향을 안줍니다. 이게 중요한 거거든요.
유연하고 변경이 용이하다는 말은 무슨 뜻이냐면, 내가 자동차 역할을 K3에서 테슬라로 바꿨어. 그래도 운전자는 운전면허증 가지고 있으면, 여전히 운전할 수 있잖아요.
내가 테슬라를 산다고 해서 내가 다른 운전면허를 따야 되는 건 아니잖아요.
왜 그러냐면, 자동차 역할의 인터페이스를 따라서, 자동차들을 구현했기 때문이에요.
그럼 운전자는 뭐만 알고 있으면 되나요? 자동차 인터페이스에 대해서만, 즉 자동차 역할에 대해서만 의존하면 되는 거예요.
그런데 여기서 진짜 중요한 건, 이렇게 자동차라는 역할을 만들고, 역할과 구현을 분리한 건 누구를 위해서 그렇게 했을까요?
물론 자동차 업계를 위해서 그런 것도 있지만, 사실은 운전자를 위해서 이렇게 한 거예요. 이 운전자를 클라이언트라고 할게요.
이렇게 하면 클라이언트가 자동차의 내부 구조를 몰라도 되죠. 그리고 구현한 애들이 내부적으로 바뀌더라도, 어쨌든 자동차 역할만 그대로 맞춰서 하고 있으면, 내부적으로 바뀌어도 클라이언트에게 영향을 안주죠. 자동차 종류가 바뀌어도 운전자를 바꿀 필요가 없습니다.
즉, 다른 대상으로 쉽게 변환이 가능하고 심지어 완전히 새로운 차가 나와도 기존 자동차 역할을 그대로 따라할 수 있으면 새로운 자동차도 문제없는 거죠.
자동차 세상을 무한히 확장 가능한 거예요. 그 자동차 역할만 구현하면 되잖아요. 대상(클라이언트)을 바꾸지 않고 새로운 자동차를 출시할 수 있는 거예요. 이게 다형성이고, 유연하고 변경이 용이하죠.
이거를 조금 더 디테일하게 얘기하면, 클라이언트에 영향을 주지 않고 새로운 기능을 제공할 수 있다는 거예요. 왜 이게 가능하냐면, 역할과 구현으로 세상을 구분했기 때문에 가능한 거예요.
여기서 진짜 중요한 거는 새로운 자동차가 나와도 클라이언트는 새로운 걸 안 배워도 된다는 게 중요한 거예요. 코드로 치면, 클라이언트 코드를 변경할 필요가 없는거죠. 클라이언트를 바꿀 필요가 없는 거예요.
자 한 가지 예제를 더 들어볼게요.

이제 여러분 공연 무대에요. 여러분들이 공연을 기획하고 운영하는 사람으로 생각을 해볼게요. 그러면 로미오라는 역할이 있고 줄리엣이라는 역할이 있어요.
로미오라는 역할을 장동건이 할 수도 있고 원빈이 할 수도 있고, 줄리엣이라는 역할은 김태희가 할 수도 있고 송혜교가 할 수도 있죠. 그러니까 로미오와 줄리엣 공연을 할 때는 배우는 대체가 가능해야 돼요.
이 로미오와 줄리엣 공연에서 역할과 구현을 나누었잖아요. 그러면 변경 가능한 대체 가능성이 생겨요. 이게 바로 유연하고 변경에 용이하다는 뜻이에요. 내부 구조를 몰라도 돼요.
로미오 역할을 하는 사람은 줄리엣 역할을 김태희가 하든, 송혜교가 하든 몰라도 되잖아요. 대본만 보고 충실히 대본에 따라서만 하면 되는 거지, 줄리엣 역할을 누가 하는지는 상관이 없는 거예요.
로미오가 클라이언트고, 줄리엣이 서버가 된다고 가정을 해볼게요.
줄리엣 이라는 구현이 바뀐다고 해서, 로미오 역할에 영향을 주지 않아요. 따라서, 다른 대상으로 대체 가능합니다. 이게 바로 유연하고 변경이 용이하다는 거에요. 그리고 이게 다형성이죠.
자 이 다형성을 실세계에 비유한 것들의 예시로.

방금의 예시 두개와 키보드, 마우스, 세상의 인터페이스들을 다른 걸로 꼽는 거, 만약에 시끄러운 기계식 키보드 쓰다가 리얼포스 무접점으로 바꿔도 그냥 할 수 있잖아요.
그리고 정렬 알고리즘 같은 경우에도 실제 코드 상에서는 정렬 알고리즘의 기능만 똑같으면, 성능이 더 나은 알고리즘으로 교체할 수도 있겠죠.
그리고 할인 정책 같은 것도 기존 로직에서 할인 정책의 로직이 바뀌어도 기존 코드를 바꾸지 않고 한다거나 이런 것들을 구현할 수가 있습니다.
그래서 좀 정리를 해보면, 역할과 구현으로 세상을 구분을 하면, 세상이 단순해지고 유연해지고 변경도 편리해져요.

그래서 어떤 장점이 있냐면, 핵심은 클라이언트예요. 클라이언트는 대상 역할의 인터페이스만 알면 돼요.
아까 공연 예시에서 로미오 역할을 하는 사람은 대본에서의 줄리엣 역할만 알면 되지, 김태희가 할지 송혜교가 할지, 아니면 어떤 무명 배우가 할지에 대해서는 몰라도 되는 거예요.
그리고 클라이언트는 구현 대상의 내부 구조를 몰라도 돼요. 자동차가 막 디테일하게 어떻게 돌아가는지 몰라도 되잖아요. 그냥 액셀 밟으면 가고, 왼쪽으로 돌리면 왼쪽으로 가고 이것만 알면 되잖아요.
클라이언트는 구현대상의 내부구조가 변경되어도 영향을 받지 않아요. 차 기름 종류를 바꾼다고 해서 클라이언트가 영향을 받지 않죠.
그리고 클라이언트는 구현대상 자체를 변경해도 영향을 받지 않아요. 예를 들어 K3에서 테슬라로 바꿔도, 반대로 테슬라에서 아반떼로 바꿔도 영향을 받지 않는 거예요. 전기차가 나와도 세상을 바꿀 필요가 없는 거예요. 이게 바로 역할과 구현을 분리한 것에서 오는 장점입니다.
프로그램 언어에서도 결과적으로는 이런 개념을 차용한 거죠.

결과적으로는 Java 언어에서는 다형성이라는 것을 차용해서 해결하는데요.
이걸 쉽게 얘기하면, 역할을 인터페이스라고 보시면 되고요. 구현은 인터페이스를 구현한 클래스나 구현 객체로 보시면 됩니다. 그래서 객체를 설계할 때 역할과 구현을 명확히 분리해서 설계를 하는 거예요.
그래서 객체를 설계할 때 일단 역할, 즉 인터페이스를 먼저 설계해서 부여하고, 그 역할을 수행하는 구현 객체를 그 다음에 만드는 거죠. 물론 꼭 인터페이스가 아니고, 일반 상속관계도 다형성이 가능합니다.
근데 가급적이면 인터페이스 하는게 낫겠죠.
인터페이스는 다중으로 구현할 수도 있지만, 그냥 일반 상속관계는 단일 상속밖에 안되는 등 여러가지 문제들이 있기 때문입니다.
핵심은 "구현보다는 인터페이스가 먼저"라는 거에요.
그래서 이 다형성을 지금부터 코드로 쭉 풀어갈 텐데, 설명하기 전에 객체의 협력이라는 관계부터 생각해야 돼요.

혼자 있는 객체는 없어요. 우리가 다형성을 공부할 때 오해하는 것 중에 다형성이라는 게 부모가 있고, 그걸 구현하면 끝인 줄 알아요. 지금 여기에는 클라이언트가 없단 말이에요.
중요한 건 사실 클라이언트가 중요하거든요.
클라이언트는 요청하는 사람, 서버는 그 요청을 받아서 응답하는 사람 이라고 보시면 됩니다.
그래서 수많은 객체 클라이언트와 객체 서버가 서로 요청하고 응답하면서, 서로 협력관계를 가집니다.
객체끼리 요청과 응답을 주고 받으면서, 클라이언트와 서버 역할을 주고, 받기도 하는거죠.
그리고 참고로 이거를 이제 단순한 객체끼리 요청을 하고 응답을 줄 수도 있고요. 이게 더 개념이 커지면, 여러 서버끼리, 시스템끼리 요청을 주고 받을 수도 있겠죠.

클라이언트가 서버에 요청하는 그림을 그렸고요.
클라이언트는 동시에 서버가 될 수 있어요. 그림 보시면 클라이언트가 서버한테 요청을 하고, 또 서버가 클라이언트가 돼서, 다른 서버들한테 요청을 할 수가 있겠죠.
물론 여기서 응답이라는 개념이 리턴값이 꼭 없어도 돼요. 뭐 내부적으로 print 할 수도 있고, 리턴값이 void가 될 수도 있는 거예요. 어쨌든 클라이언트가 요청한 행위를 하는 게 응답이라고 보시면 됩니다.
그냥 그 행위를 하면, 요청에 대한 응답을 한 거예요.
자바 언어는 다형성을 어떻게 구현했을까요?

자바 언어의 다형성은 오버라이딩으로 동작을 하고, 오버라이딩은 자바의 기본 문법입니다.
결과적으로 오버라이딩이 된 메서드가 실행이 되죠. 우측 상단에 있는 그림으로 이해할 수 있습니다.
인터페이스를 구현한 객체를 실행시점에 유연하게 변경할 수 있는게 Java 언어가 가진 다형성의 장점이구요. 물론 클래스 상속 관계에서도 다형성에는 오버라이딩이 적용이 됩니다.
참고로 오버로딩(Overloading)은 메서드를 많이 초과해서 로딩했다고 해서, 메서드를 여러 개 정의한다고 기억하고, 오버라이딩(Overriding)은 원래 기능을 넘어서 올라타버린다고 해서 재정의한다고 기억하면 편하다.
자 그래서 클라이언트가 멤버 서비스라고 보고요.

이 클라이언트는 뭐에 의존하나요? 멤버 서비스 클라이언트는 멤버 리퍼지토리를 의존합니다.
의존한다는 건 뭐냐면, 내가 쟤(의존하는 대상)를 알고 있다는 거예요.
자 그런데 이 멤버 리포지토리에다가

멤버 리포지토리 인터페이스를 구현한, 메모리 멤버 리포지토리랑 jdbc 멤버 리포지토리를 이렇게 할당할 수 있나요? 없나요?
예를 들어서 이런 코드들이죠.

위에 보시면, 멤버 서비스가 멤버 리포지토리 인터페이스를 선언을 해놓고 거기에다가 값을 메모리 멤버 리포지토리로 대입을 할 수 있죠. 그리고 그거를 jdbc 멤버 리포지토리로도 넣을 수도 있죠. 왜냐하면 다형성이기 때문에 가능하죠.
멤버 리포지토리가 부모 타입이라서, 저렇게 할당해도 다 받아들일 수 있는 거에요. 옛날에 공부할 때 이렇게 배웠었는데, 부모는 마음이 넓기 때문에 자식들을 다 품을 수 있다, 반대로 자식들은 부모 마음을 잘 모르기 때문에 부모를 자식이 대입하는 건 안됩니다. 전문용어로 업캐스팅, 다운캐스팅입니다.
업캐스팅, 다운캐스팅



한편 이 멤버 리포지토리에는 인터페이스나 부모와 전혀 관계없는 애는 할당할 수가 없죠.

그래서 이 멤버 서비스라는 이 파란색 클라이언트에 멤버 리포지토리 인터페이스가 있는데, 여기에 빨간색인 메모리 멤버 리포지토리를 넣으면 두번째 그림인 파란색 클라이언트가 빨간색을 바라보는 그림이 되는 거고, 그 다음에 이 초록 색깔 jdbc 멤버 리포지토리를 넣으면 이 파란 색깔 클라이언트는 초록 색깔 서버를 보게 되고, save()를 호출하면, 초록 색깔에 있는 save()가 호출이 되겠죠.
자 그래서 다형성의 본질은 뭐냐면,

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

이 클라이언트는 계속 파란 색깔인데, 멤버 서비스가 바라보는 멤버 리포지토리는 빨간 색깔이든지 초록 색깔이든지 바꿀 수 있죠.
이게 바로 다형성의 본질, 즉 클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있는 겁니다.
그래서 이제 정리를 해보자면,

실세계의 역할과 구현이라는 편리한 컨셉을 다형성을 통해 객체 세상으로 가지고 올 수가 있습니다.
이것 덕분에 유연하고 변경이 용이해지고 확장 가능한 설계가 됩니다.
확장 가능한 설계는 우리가 메모리 멤버 리포지토리로 저장을 하다가 db에 저장하는 게 필요해져서 jdbc 멤버 리포지토리를 구현해서 꽂아도, 멤버 서비스 같은 클라이언트와 다른 코드들한테 영향을 주지 않는다는 겁니다. 그게 확장 가능한 설계죠. 멤버 리포지토리의 구현체를 무한하게 확장할 수 있어요.
그래서 클라이언트에 영향을 주지 않는 변경이 가능하고요.
근데 제일 중요한 거는 사실 인터페이스가 깨지면 다 끝나는 거죠.
그래서 이 역할, 그러니까 인터페이스를 안정적으로 잘 설계하는 게 진짜 중요합니다.
자 한계점은 이런거에요.

역할, 인터페이스 자체가 변하면, 클라이언트와 서버 모두 큰 변경이 발생합니다.
자동차가 비행기로 바뀌면 기능이 엄청 추가가 되겠죠.
연극을 예로 들면 대본 자체가 변해요. 그러면 이제 그 대본을 쓰는 배우는 다 영향을 받는 거죠.
usb 인터페이스가 변경되면, 기존 usb 인터페이스 쓰는 애들은 더 이상 그거를 못 쓰겠죠.
이런 경우는 클라이언트에도 큰 영향을 미치게 됩니다.
그래서 설계할 때 인터페이스를 안정적으로, 또 가장 변화가 없는 방식으로 설계하는 게 제일 중요해요. 비단 이거는 이런 Java의 인터페이스를 얘기하는 것 뿐만 아니에요. API 설계할 때도 이 API를 안정적으로 잘 설계하는 게 진짜 중요해요.
자 근데 지금 스프링 얘기를 하는데 자꾸 객체 지향 얘기를 하잖아요. 이제 스프링 이야기를 좀 해볼게요.

제가 지금까지 말씀드렸던 것 중에 다형성이 제일 중요해요. 어쩌면 객체 지향의 개념 중에서 객체 지향의 꽃은 다형성이에요. 이전에 스프링이 좋은 객체지향 프로그래밍을 할 수 있도록 도와주는 도구라고 했잖아요.
스프링은 이 다형성을 극대화해서 이용할 수 있도록 도와줘요.
스프링에서 이야기하는 제어의 역전, 의존관계의 주입, 이런거는 다 다형성을 활용해서 역할과 구현을 편리하게 다룰 수 있도록 지원하는 기능이에요.
사실 스프링 컨테이너가 제공하는 건, 이 다형성을 되게 편리하게 사용할 수 있도록 지원하는 기능이에요. 어쩌면 그게 전부예요.
그렇기 때문에 스프링을 사용하면, 마치 레고 블럭을 조립하듯이, 공연 무대의 배우를 선택하듯이, 구현을 편리하게 변경할 수가 있는 거예요. 메모리 멤버 리포지토리에서 jdbc 멤버 리포지토리로, 또는 jdbc 멤버 리포지토리에서 jpa로 막 바꾸는데도 기존 코드에 영향이 전혀 없었잖아요.
어떻게 가능하냐? 이거는 스프링의 DI 컨테이너, 의존관계 주입, 이런 것들과 다형성이 합쳐져서 가능했던 거에요.
혹시 이 개념에 대해서 잘 모르더라도, 편하게 듣고 뒤에 코드에서 자연스럽게 이해를 시켜 드릴게요.
자, 그런데 스프링과 객체 지향 설계에 대해서 제대로 이해를 하려면, 사실은 다형성 외에 한 가지를 더 알아야 돼요.
뭐냐면, 소위 SOLID 라고 하는 좋은 객체 지향 설계 5가지 원칙입니다. 면접에도 엄청나게 잘 나오는 내용입니다.
다형성과 이 좋은 객체 지향 설계 5가지 원칙을 합쳐야 스프링에 대해서 제대로 설명할 수가 있어요.
그럼 다음 시간에는 SOLID에 대해서 설명을 해드리겠습니다.
'스프링 > 스프링 핵심 원리 - 기본편' 카테고리의 다른 글
| 프로젝트 생성 (0) | 2024.01.16 |
|---|---|
| 객체 지향 설계와 스프링 (0) | 2024.01.16 |
| 좋은 객체 지향 설계의 5가지 원칙(SOLID) (0) | 2024.01.16 |
| 스프링이란? (0) | 2024.01.15 |
| 이야기 - 자바 진영의 추운 겨울과 스프링의 탄생 (0) | 2024.01.15 |