스프링

관심사의 분리(Seperation Of Concern)

쿠쿠s 2022. 5. 18. 16:49

객체지향의 세계에서는 모든 것이 변한다. 변수나 객체의 필드값이 변하는게 아닌 객체에 대한 설계와 이를 구현한 코드가 변한다는 뜻 입니다. 사용자의 변화하는 요구사항은 소프트웨어 엔지니어링에서 피할 수 없습니다. 그래서 개발자는 객체를 설계할 때 이 '변화하는 미래를 어떻게 대비할 것인가' 를 고려해야 합니다. 가장 좋은 방법은 변화가 이루어질 때 이 변화의 폭을 최소한으로 줄여주는 것이다. 

변화의 폭을 최소한으로 줄이기 위해 관심사의 분리가 필요합니다.

 

사용자는 자동차를 운전한다 라는 프로그램을 만드려고 합니다.  그런데 사용자가 아반떼를 운전하고싶다는 요구사항이 있습니다. 그럼 어떻게 코드를 구현할 수 있을까요?

 

class Avante {

    public void accel(){
        System.out.println("아반떼 가속");
    }

    public stop(){
        System.out.println("아반떼 멈춤");
    }

}

class User{
    private Avante avante = new Avante();

    public void rideCar(){
        avante.accel();
    }

    public void stopCar(){
        avante.stop();
    }
}

간단하게 아반떼 클래스와 사용자 클래스를 만들 수 있습니다. 코드상에서는 문제가 없어 보입니다. 하지만 '사용자는 테슬라를 운전한다'라고 요구사항이 변한다면? 또 '사용자는 제네시스를 운전한다' 등 다양하게 요구사항이 변화할 수 있습니다.

 

이렇게 요구사항이 계속 변하면 User 클래스의 코드 또한 전부 바꿔줘야 합니다. 지금 사용자는 어떤 자동차를 탈지 정하는 것뿐만 아니라 자동차를 운전해야 한다는 다양한 책임을 가지고 있으며 이는 SRP(단일 책임 원칙)를 위반하게 됩니다. 따라서 관심사를 분리해야 합니다.

 

우선 유저클래스와 아반떼 클래스는 너무 긴밀하게 연결(종속)되어 있기 때문에 높은 결합도를 가지고 있습니다. 그렇지 않도록 중간에 추상적인 연결고리를 만들어 줘 봅시다. 바로 인터페이스를 사용하여 아반떼, 제네시스, 테슬라 등등 모두를 추상화할 수 있는 자동차 인터페이스를 만들어 봅시다.

 

인터페이스를 통해 접근하므로 User 클래스는 어떤 자동차를 선택해야 하는 책임이 없어지고, 자신이 사용할 클래스가 어떤 것인지 몰라도 됩니다.

 

interface Car {
    void accel();
    void stop();
}

class Avante implements Car {

    @Override
    public void accel(){
        System.out.println("아반떼 가속");
    }

    @Override
    public void stop(){
        System.out.println("아반떼 멈춤");
    }

}

class Tesla implements Car{

    @Override
    public void accel() {
        System.out.println("테슬라 가속");
    }

    @Override
    public void stop() {
        System.out.println("테슬라 멈춤");
    }
}


class User{

    private Car car = new Avante();

    public void rideCar(){
        car.accel();
    }

    public void stopCar(){
        car.stop();
    }
}

 

이제 사용자가 어떠한 자동차를 운전해도 User 클래스를 전부 뜯어 고칠일은 없을 것 같습니다. 하지만 코드에 new Avante() 라는 구체적인 클래스를 사용할지 선택하는 코드가 남아있습니다. new Tesla()로 바꾸면 되는 아주 짧은 코드이지만 그 자체로도 이미 어떤 구현 클래스의 객체를 이용하는 책임을 가지고 있으므로 이 코드를 User 에서 분리하지 않으면 User는 결국 독립적으로 확장 가능한 클래스가 될 수 없습니다.

 

우리는 위의 uml 같은 구조를 원했지만 사실 아래와 같은 구조를 가지게 된다.

 

 

객체 사이의 관계가 만들어지려면 일단 만들어진 객체가 있어야 하는데 Car car = new Avante( )를 사용하는 방법도 있지만 외부에서 만들어 준 것을 가져오는 방법도 있습니다. 사용하려는 객체를 꼭 User 클래스 내에서 만들 필요가 없습니다.

객체는 메서드 파라미터 등을 이용해 전달할 수 있으니 외부에서 만들 걸 가져오면 됩니다.

 

생성자 파라미터를 이용할 것인데 이때 파라미터의 타입을 인터페이스로 선언하면 전달되는 객체는 이 인터페이스를 구현한 어떤 것이든 될 수 있습니다. 바로 다형성을 이용하는 것 입니다. 이를 통해 파라미터를 통해 전달이 되고, 파라미터로 제공받은 객체는 인터페이스에 정의된 메서드만 사용하면 그 객체가 어떤 클래스로부터 만들어졌는지 신경쓰지 않아도 됩니다.

 

그래서 어떤 자동차를 선택할지 책임을 가지는 제3 클래스를 만들어 관심을 분리해보겠습니다.

 

class User{

    private final Car car;

    public User(Car car) {
        this.car = car;
    }

    public void rideCar(){
        car.accel();
    }

    public void stopCar(){
        car.stop();
    }
}

class Config{
    public User user(){
        return new User(selectCar());
    }

    private Car selectCar() {
        return new Avante();
    }
}

 

이제 User는 자신의 관심사이제 책임인 운전만 하면 됩니다. 어떠한 자동차가 들어와도 상관이 없고, 필요에 따라 자동차도 자유롭게 확장할 수 있는 구조가 되었습니다. 이렇게 해도 User 코드는 아무런 영향을 받지 않습니다.

 

지금까지 해온 리팩토링 작업이 자연스럽게 객체지향 기술을 적용했다고 보면 됩니다.

 

SRP : 한 클래스는 하나의 책임을 가진다.

-> 관심사의 분리, User는 오직 운전만 하는 책임을 가지게 만들었다.

 

OCP : 클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다.

-> 확장이 이루어져도 User 클래스의 코드는 변경할 필요가 없다.

 

DIP :

  • 고차원 모듈은 저차원 모듈에 의존하면 안된다. 
  • 프로그래머는 추상화에 의존해야지 구체화에 의존하면 안 된다.

-> User 클래스는 Car인터페이스에 의존하고 있었다.

 

IoC : 제어의  역전

-> User클래스가 능동적으로 자신이 사용할 객체를 선택하지 않고 제어의 흐름이 넘어가 Config 클래스에서 자신이 사용할 오브젝트를 공급받아 수동적으로 사용해야 하는 입장이 되었다.

 

이 IoC를 통해 DI(Dependency Injection)이 이루어집니다.

 

다시 이 그림을 보면 User는 Car 인터페이스에 직접 의존합니다. User는 Avante 와 Tesla의 존재를 알지도 못합니다. 이 클래스 모델의 관점에서 보면 User는 Avante, Tesla 클래스에는 의존하지 않기 때문입니다. 이렇게 낮은 결합도를 갖게 되는 구조는 직접 실행하기 전까지 객체의 관계를 알 수 없습니다. 런타임시에 User객체가 제3의 존재에게 제어의 흐름이 넘겨 실제 사용할 객체와 의존관계가 결정이 됩니다. 

 

의존관계 주입은 이렇게 런타임시에  구체적인 의존 객체(사용할 클래스)와 그것을 사용할 주체, 보통 클라이언트라고 부르는 객체를 런타임시에 연결해주는 작업을 말합니다.

 

의존관계 주입은 다음과 같은 세 가지 조건을 충족해야 합니다.

  • 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해 인터페이스에 의존하고 있어야 함
  • 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정 (여기서는 Config)
  • 의존관계는 사용할 객체에 대한 래퍼런스를 외부에서 제공(주입)해줌으로써 만들어짐

 

 

 

 

 

 

참고: 토비스프링 3.1