들어가며

  • Spring 프레임워크는 매우 거대한 생태계이지만 핵심적인 원리 원칙 개념은 달라지지 않았다.
  • Spring의 핵심적인 개념은 크게 3가지로 아래와 같다.
    • DI(Dependency Injection) 의존관계 주입
    • IoC(Inversion of Controll) 제어의 역전
    • AOP(Aspect Oriented Programming) 관점 지향 프로그래밍
  • 이번 파트에서는 의존관계 주입으로 일컬어지는 DI에 대해 이 개념이 무었이고, 왜 이 개념이 사용되어야 하는가에 대한 포스트들 중 첫 번째이다.

 

 

다형성

  • 다형성은 부모 타입을 사용하여 자식 타입의 객체를 다룰 수 있게 하는 것으로 이를 적절히 사용하면 프로그래밍을 매우 유연하게 할 수 있다.
  • 하지만 개념이 조금 난해한데. 이를 역할이란 개념과, 구현이라는 개념을 이용하여 알아보도록 하자.
  • 어떤 차량과, 사람이 존재한다고 가정한다. 이때 사람은 운전자 역할이고, 차량은 자동차 역할 이다. 이 역할은 내부의 사람이 바뀌어도, 차량이 바뀌어도, 역할 자체는 바뀌지 않는다. 즉 운전자는 차량을 운전하고, 차량은 엑셀을 밟으면 앞으로, 브레이크를 밟으면 멈추는 동작 자체는 바뀌지 않는다.
  • 그럼 이 역할을 이제 구체화한 사물들이 구현 이라는 개념으로 나타나게 된다.
    • 예를 들어 K3, 아반떼, 카니발은 "자동차" 역할을 구체화한 객체들이다. 즉 자동차의 역할은 모두 충실히 수행하면서 세부적으로 나누어지는 것이다.
    • 사람 역시 김태희, 원빈 등 각 사람은 바뀌어도, "운전자"의 역할은 그대로 수행이 가능하다(운전이 가능하다면)
  • 이 "역할"에 필요한 기능을 반드시 가지고 있어야 하기 때문에 Java에 빗대어 표현하자면 역할은 "인터페이스"로 생각할 수 있다.
  • 또한 "구현"의 개념은 이 인터페이스들을 implement한 구현 클래스들로 생각할 수 있다. 그렇다면 다음과 같이 나타낼 수 있을 것이다.
//interface

public interface Car{
    public void moveFront();
    public void break();
    ...
}


public interface Driver{
    public void drive();
}
  • 차량과 운전자 역할을 이렇게 나타낼 수 있다.
public class K3 implements Car{

    private int velocity;
    private int gas;
    
    K3(){}

    @Override
    public void moveFront(){
        velocity++;
        ...
    }
    
    @Override
    public void break(){
        velocity--;
        ...
    }
    
    ...
}


public class KimJinYong implements Driver{

    ...

    @Override
    public void drive(){
        ...
    }
    
    ...
}
  • 이 클래스들은 위의 역할 인터페이스를 상속받아 구현한 구현체 클래스들이다.
  • 이제 운전자와 차량을 각각 지정해야 한다고 생각해 보자. 다형성을 사용한다면 아래와 같이 할 수 있을 것이다.
public class Main{
    public static void main(String[] args){
        Driver currentDriver = new KimJinYong();
        Car currentDriveCar = new K3();
        
        currentDriver.drive();
        ...
    }
}
  • 잘 보면 인터페이스 타입으로 구현체 클래스를 가져와 사용하고 있는 것을 확인할 수 있다.
  • 이렇게 사용하는 상황을 다형성이라고 한다. 이 다형성은 만약 사용하려고 하는 구현체가 달라질 때 그 강력함을 보인다.

  • 지금 사용하는 자동차 역할의 구현체는 K3이다. 만약 이 구현체를 아반떼나, 카니발로 바꾸어야 한다면 어떻게 해야할까?
  • 다형성을 이용하면 매우 쉽다. 위의 코드에서 new Carnibal()이나, new Abantte() 등으로 지정된 클래스를 바꾸어 주면 된다. 여기서 우리가 흥미롭게 생각해야 하는 것은, Driver 즉 운전자 역할을 맡은 사람은 차량의 구현체가 무엇인지에 대해 전혀 모르고 있다는 것이다.
  • Driver 역할의 어떤 객체는 그저 운전한다는 drive()을 수행하여 Car인터페이스의 moveFront 메서드나, break  메서드를 사용할 수 있으면 된다. 자신이 운전하는게 K3인지, 아반떼인지, 카니발인지는 자신의 관심사가 아니다.
  • 따라서, K3, 아반떼 등의 구현체에 어떠한 수정이 발생하더라도, Driver역할의 구현체는 전혀 변경이 일어나지 않고, 그대로 drive 메서드를 수행시킬 수 있다.
  • 이때 Driver는 Car의 기능을 호출하여 사용하는 사용자(Client)로 생각할 수 있고, Car는 Driver의 요청에 따라 특정 동작을 수행하고, 그 결과나 반응을 되돌려 주는 서버(Server)로 생각할 수 있다. 그런데 Driver 역할을 맡는 구현체는 Car 구현체에 변경이 일어나더라도, 호출하는데 아무런 영향이 없다. 즉 변경이 일어나지 않는다.
  • 이것이 다형성의 본질이며 클라이언트측의 코드(위 상황에서는 Driver)를 변경하지 않아도, 서버의 구현 기능을 유연하게 변경할 수 있다.

 

 

DI의 필요성과 SOLID

  • DI가 매개변수로 사용할 객체를 받아 넣어 주는 개념이란 것은 대충 알았다. 그렇다면 왜 이렇게 하는가에 대해서 우리는 생각해야 한다.
  • 이에 대해 논의해보기 이전에, 먼저 객체 지향 설계에서 지켜야 할 5가지 원칙인 SOLID에 대해서 먼저 알아본다.
  • SOLID는 각각 아래와 같은 개념의 두문자이다.
    • SRP - 단일 책임 원칙
    • OCP - 개방-폐쇄 원칙
    • LSP - 리스코프 치환 원칙
    • ISP - 인터페이스 분리 원칙
    • DIP - 의존관계 역전 원칙
  • 각각의 개념에 대한 세부적인 내용을 알아보자.

SRP

  • 단일 책임 원칙은 하나의 객체는 하나의 책임(responsibility)을 가져야 한다는 것으로 여기서 책임이라는 단어의 뜻이 조금 추상적일 수 있다.
  • 여기서 책임은 어떠한 기능(function)으로 하나의 객체가 여러 기능을 수행해야 할 시, 이 객체가 여러 개의 책임을 가졌다고 말할 수 있다.
  • 즉 객체는 하나의 기능만을 가지고 있어야 하며 이 기능을 수행하는데 최선을 다해야 한다는 것을 의미한다.

 

OCP

  • 개방-폐쇄 원칙은 확장에는 개방되어 있어야 하고, 변경에는 폐쇄되어 있어야 한다 라는 의미이다.
  • 역시 개방과 폐왜의 의미가 조금 명확하지 않은데 각 단어의 뜻은 아래와 같다.
    • 확장에 대한 개방은 말 그대로 확장함에 있어서 어떠한 제약이 발생하지 않아야 한다. 즉 다른 기능을 추가함으로서 확장되더라도 어떠한 문제가 발생하면 안된다.
    • 변경에 대한 폐쇄는 이렇게 모듈에 대한 확장이 발생할 때, 이로 인해서 이 모듈을 사용하는 다른 모듈에 변경이 발생하면 안된다 라는 것을 의미한다.
  • 즉 OCP는 어떤 모듈을의 기능을 추가하는 등의 확장에 있어서 제한이 존재하면 안되고, 동시에 이 모듈을 사용하는 다른 모듈들의 코드가 변경되면 안된다는 의미가 된다.

 

LSP

  • 리스코프 치환 원칙은 하위 타입 객체는 상위 타입 객체에서 수행한 동작를 수행할 수 있어야 한다는 것이다. 동시에 프로그램의 정확성을 깨뜨리지 않아야 한다.
    • 차량 인터페이스에서 엑셀을 밟으면 앞으로 가게 하였는데, 자식 타입에서 뒤로 가게 한다면 LSP 위반이다.
  • 즉 상속 관계에서 부모 객체가 행할 수 있는 동작들을 자식 객체도 역시 정상적으로 수행이 가능해야 한다는 것이다.

 

ISP

  • 인터페이스 분리 원칙은 하나의 범용 인터페이스보다 해당 객체에 대한 특화 인터페이스 몇 개가 붙는 것이 더 효율적이란 개념이다.
  • 말 그대로 자신이 사용하지 않는 기능들이 붙어 있는 범용 인터페이스 보다는 자신이 사용하는 것만 존재하는 몇 개의 인터페이스를 사용하는 것이 더 좋다는 것이다.
  • 즉 클라이언트(사용자)는 자신이 필요한 기능들만 사용할 수 있도록 인터페이스를 구성해주어야 한다는 것이다.

 

DIP

  • 프로그래머는 추상화에 의존해야 하며, 구체화에 의존하면 안된다.
  • 이를 풀어 쓰면 프로그래머는 인터페이스에 의존해야 하며 이를 구현한 구체 클래스에 의존하게 코드를 만들면 안된다. 이는 우리가 위에서 간략히 설명한 역할에 의존해야 한다는 것과 동일한 말이다.
  • 만약 구체 클래스에 의존하게 된다면 추후 변경이 매우 어려워진다는 점을 생각하자.

 

 

 

마무리

  • 이번에는 다형성이 어떠한 의미인지와, 객체 지향적 설계에 필요한 SOLID에 대해 알아보았다.
  • 다음 포스트에서는 이 SOLID원칙을 자바 코드에 대입하여 분석해보면서 일어나는 문제점에 대해 알아보고 어떻게 해결할 수 있는지에 대해 생각해보도록 한다.
복사했습니다!