Coderoad

좋은 객체 지향 프로그래밍

2023-01-16 at Spring category

본 문서는 인프런에서 수강할 수 있는 스프링 핵심 원리 - 기본편을 수강한 후, 공부한 내용을 정리한 문서입니다. 본 문서의 모든 저작권은 해당 강의의 저자이신 김영한 우아한형제들 기술이사님께 있습니다.

객체 지향 프로그래밍

객체 지향 프로그래밍의 특징으로는 다양한 것들이 있습니다. 클래스 개념으로 대표되는 캡슐화(Encapsulation), 캡슐화에서 파생된 개념인 접근 제어자를 통한 정보 은닉(Information hiding), 부모 클래스의 요소들을 자식 클래스에서 물려받을 수 있는 상속(Inheritance), 프로그램이 유연하고 변경이 용이하게 해주는 **다형성(Polymorphism)**이 객체 지향의 특징들입니다.

그 중에서도 가장 핵심이라고 할 수 있는 특징은 다형성입니다. Spring은 이 다형성의 장점을 극대화하여 활용할 수 있게 도와주는 프레임워크입니다. Spring이 다형성을 어떻게 다루는지 알아보기 전에 다형성에 대해서 좀 더 알아보겠습니다.

다형성이란?

**다형성(Polymorphism)**을 쉽게 이해하기 위해선 역할구현으로 세상을 구분해보는 것이 가장 좋습니다. 가장 대표적인 예시는 운전자자동차입니다. 운전자와 자동차라는 역할은 그 자리에 어떤 구현체(예를 들어 택시 운전사와 아반떼)가 오던 운전자는 자동차를 운전할 수 있습니다. 즉, 구현체와 관계 없이 동작은 한다는 의미입니다. 물론 운전자에 프로 레이싱 선수, 자동차에 슈퍼카라는 구현체가 운전자와 자동차 역할을 담당한다면 훨씬 좋은 성능을 낼 수 있을 것입니다. 여기서 중요한 점은 운전자, 자동차라는 역할과 그 역할을 수행할 구현체는 유연하고 변경이 용이해야한다는 점입니다.

역할과 구현을 분리하는 것이 백엔드 프로그래밍에서 주는 장점은 다음과 같습니다.

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

우리가 Spring을 공부하는 중요한 이유 중 하나가 백엔드 프로그래밍 분야에서 가장 강력한 기술을 갖기 위해서임은 부정할 수 없는 사실입니다. 위와 같은 장점들을 통해 우린 클라이언트와 상관없이 독립적으로 유연하고 변경이 용이한 환경에서 개발할 수 있습니다.

Java에서의 다형성

Java에서 역할인터페이스(Interface), 구현은 인터페이스를 통해 구현한 구현체입니다. 우리는 항상 객체를 설계할 때 역할과 구현을 명확하게 분리해야 합니다. 역할을 먼저 부여하고, 그 역할을 올바르게 수행하는 구현 객체를 만들어야합니다. 이 과정이 곧 다형성을 극대화하는 과정이기 때문입니다. Java의 강력한 장점이자 Spring이 탄생한 이유인 좋은 객체 지향을 위해서는 필수적입니다.

객체를 역할과 구현으로 구분하는 과정을 Java에서는 **오버라이딩(Overriding)**이라는 문법을 통해 진행할 수 있습니다. 오버라이딩은 Java의 기본 문법으로 프로그램 실행 시에는 오버라이딩된 메소드가 실행된다는 특징이 있습니다. 쉽게 설명하자면 **"덮어쓰기"**입니다. 이 오버라이딩을 통해 확보한 다형성으로 인터페이스를 구현한 구현체를 실행 시점에서 유연하게 변경할 수 있습니다. 물론 인터페이스 뿐만 아니라 클래스 간의 상속 관계에서도 오버라이딩이 가능하고 다형성이 확보됩니다.

코드로 간단하게 Java에서의 다형성을 확인해보면 다음과 같습니다.

//역할
interface Car {
    void accelPedal(int speed);
    void breakPedal(int speed);
}

//구현체 1
class SuperCar implements Car {
    @Override
    public int accelPedal(int speed) {
        speed += 50;
        return speed;
    }

    @Override
    public int breakPedal(int speed) {
        speed -= 50;
        return speed;
    }
}

//구현체 2
class NormalCar implements Car {
    @Override
    public int accelPedal(int speed) {
        speed += 25;
        return speed;
    }

    @Override
    public int breakPedal(int speed) {
        speed -= 25;
        return speed;
    }
}

Car라는 인터페이스를 통해 역할을 설계했고, 해당 역할을 수행할 SuperCar와 NormalCar라는 두 구현체를 만들었습니다. 프로그래머가 나중에 어떤 구현체를 사용하냐에 따라 다른 성능에 차이가 날 것입니다. 오버라이딩을 통해 역할의 메소드를 서로 다른 성능을 가지도록 만들었으며 이를 실행 시점에 유연하게 변경이 가능합니다. 유연하게 변경하는 과정을 코드로 보면 다음과 같습니다.

public class Driver {
    public static void main(String[] args) {
        //NormalCar 자리에 SuperCar 구현체로 바꿔 넣기만 해도
        //훨씬 향상된 성능의 메소드가 수행된다!
        //이것이 유연하고 변경이 용이하다는 의미이다.
        Car myCar = new NormalCar();

        int currentSpeed;
        currentSpeed = myCar.accelPedal(0);
        currentSpeed = myCar.breakPedal(currentSpeed);
    }
}

다형성의 본질

다형성의 본질은 결국 **유연하게 변경 가능하다.**에 있습니다. 다형성의 본질을 더 확실하게 이해하려면 협력이라는 객체 간의 관계를 이해하는데서 시작해야합니다. 현실 세계에서 혼자 있는 객체는 없습니다. 다른 어떤 객체와 협력하지 않는 객체는 존재한다고 할 수 없습니다. 라는 객체는 거주지의 역할을 하는 이라는 객체와 협력 관계에 있습니다. 집은 내가 안전하게 생존할 수 있도록 협력하고 나는 집이 튼튼하게 유지될 수 있도록 돈을 들여 유지보수를 합니다. 만약 이 협력 관계가 없다면 두 객체 모두 빠르게 사라져버릴 것입니다. 사람이 살면서 유지보수하지 않는 집은 집으로서 의미가 없어지고 허물어질 것이며, 집이 없는 사람은 여러 위협에 노출되어 생존에 매우 불리해집니다.

이 협력이라는 관계를 컴퓨터 세계로 가져온다면 클라이언트서버로 볼 수 있습니다. 클라이언트는 서버로 요청을 보내고 서버는 요청에 대한 응답을 클라이언트에게 보냅니다. 클라이언트 객체와 서버 객체는 협력 관계에 있습니다. 만약 두 객체 간의 협력이 없다면 클라이언트가 몇 번이고 요청을 보낸다 한들 서버는 응답을 클라이언트에게 보내지 않을 것입니다. 그렇다면 이 객체들을 클라이언트와 서버로 부르는 것은 옳지 않습니다. 각자 클라이언트와 서버라는 역할을 전혀 수행하지 못하고 있기 때문입니다. 즉, 다형성의 기초였던 역할과 구현으로의 분리는 객체 간 협력 관계가 있어야 성립합니다.

이 협력 관계를 유지하면서 클라이언트를 전혀 변경하지 않고 서버의 구현 기능을 유연하게 변경할 수 있다는 것이 다형성의 본질입니다.

다형성의 장점과 한계

다형성의 장점에는 다음과 같은 것들이 있습니다.

  • 실세계의 역할과 구현이라는 편리한 컨셉을 다형성을 통해 객체 세상으로 가져올 수 있음
  • 유연하고 변경이 용이함
  • 확장 가능한 설계가 가능함
  • 클라이언트에 영향을 주지 않는 변경 가능함

이러한 다형성의 장점을 제대로 활용하기 위해선 역할(인터페이스)를 안정적으로 잘 설계하는 것이 중요합니다. 구현체가 인터페이스에 전혀 영향을 주지 않고 변경이 가능해야하므로 설계 단계부터 완벽히 해야합니다. 만약 그렇지 못할 경우에 한계는 다음과 같습니다.

  • 역할(인터페이스) 자체가 변하면, 클라이언트, 서버 모두에 큰 변경이 발생함
  • 자동차를 비행기로 변경해야할 때..?
  • USB 인터페이스가 변경된다면..?
  • 도량형이 변경되어 버린다면..?

기준이 되는 역할이 바뀌어버리면 구현체도 모두 변경해야하는 치명적인 단점이 존재합니다. 따라서 다형성을 극대화하여 "좋은 객체 지향" 프로그래밍을 하기 위해서는 항상 역할(인터페이스)의 설계에 가장 많은 시간을 들여 변경을 최소화 해야합니다.

Spring과 좋은 객체 지향 프로그래밍

Spring에서는 위에서 말했던 다형성을 극대화하는데 집중합니다. Spring의 주요 개념인 제어의 역전(Inversion of Control), **의존관계 주입(Dependency Injection)**은 다형성을 활용해 역활과 구현을 편리하게 다룰 수 있도록 지원합니다. 다시 말하면 다형성이 Spring의 알파이자 오메가, 전부라고 할 수 있습니다! Spring을 사용한다면 마치 레고 블럭을 조립하듯 구현체를 편리하게 변경할 수 있습니다. "좋은 객체 지향"을 목표로 만들어진만큼 프로그래머로 하여금 더 쉽게 객체 지향적 프로그래밍을 할 수 있도록 돕는 것이 Spring의 역할입니다.

hangillee

Personal blog by hangillee.

Road to good developer.