아이엠 !나이롱맨😎
article thumbnail
반응형

바늘과 실처럼 자바하면 바로 떠오르는 것이 있습니다. 바로 "객체 지향 언어" 라는 것입니다.

 

"객체 지향"이란 레고라고 생각할 수 있습니다. 레고 블록들을 조립하여 우리는 무엇이든 만들 수 있습니다. 레고를 이용해서 동물원을 만들었다고 생각해보겠습니다. 동물원에는 레고로 만들어진 사자도 있고, 호랑이도 있고, 코끼리도 있고, 제가 좋아하는 판다도 있습니다. 하지만 슬프게도 더 이상 호랑이가 인기가 없어서 고릴라 레고로 바꾸고 싶습니다. 

 

레고 동물원

이럴 경우 우리는 쉽게 동물원에서 호랑이 레고를 빼고 고릴라 레고를 넣어주면 됩니다. 여기서 호랑이, 고릴라가 객체가 되는 것입니다. 호랑이를 뺀 다고 해서 기존에 있던 사자나 판다 레고는 전혀 영향이 없습니다.

 

만약 라이거 레고를 만들고 싶다고, 호랑이와 사자를 마구잡이 섞어서 레고를 조립했다면 어땠을까요?

*참고로 라이거는 호랑이와 사자의 교배로 탄생한 동물입니다

 

호랑이와 사자가 섞여있기 때문에 호랑이만 떼어내기가 상당히 힘들어집니다. 이를 객체 지향적이지 않다고 합니다. 예시가 좀 이상하긴 하네요.

니켈로디언에서 자주 해주던 캣독

"CatDog"라는 어릴 적 티비에서 해준 만화였는데 아주 객체 지향적이지 못하네요.

 

이렇듯 "객체 지향" 이라 하면 확장이 용이해야 하고, 변경이 쉽도록 유연해야 합니다.

 

하지만 "객체 지향"적으로 프로그래밍을 하기가 만만치가 않습니다. 그래서 객체 지향적 프로그래밍을 좀 더 쉽게? 할수 있도록 만든 가이드가 "객체 지향 설계 5대 원칙(SOLID)"입니다.

객체 지향 언어를 사용하는 이유는 확장이 용이하고, 유연하다는 다형성 때문일 것입니다. 다형성은 특히 SOLID 중 OCP, DIP와 관련이 많습니다. 

 

OCP는 Open/Closed Principle의 약어로 소프트웨어 확장에는 열려야 있어야 하고, 변경에는 닫혀 있어야 한다는 원칙입니다.

DIP는 Dependency Inversion Principle의 약어로 구현체에 의존하지 말고, 역할(인터페이스)에 의존해야 한다는 원칙입니다.

 

우리가 처음 자바로 개발을 할 때 나름 다형성 있게 DIP와 OCP를 지키면서 개발을 하려고 합니다. 그리고 개발을 끝내고 딱 코드를 들여다 보면 나름 DIP와 OCP를 잘 지켜 개발했다고 뿌듯해합니다. 하지만 분명 DIP와 OCP를 위배하고 있습니다. 저 또한 맨 처음 자바로 개발을 했을 때 "와 이거 쩐다! 진짜 내가 짰지만 객체 지향적으로 잘 짰다. SOLID 쉽네~" 했습니다. 정말 바보 같은 생각이였죠.

 

그러면 이제 DIP와 OCP를 지킨거 같지만 사실 위배되는 코드를 보여드리겠습니다.

 

클래스 다이어그램은 아래와 같습니다.

  • Car라는 역할(인터페이스)를 만들고 구현으로는 K3와 K5가 있습니다.
  • Engine이라는 역할(인터페이스)를 만들고 구현으로는 Fuel, Electronic이 있습니다.
  • Driver라는 클라이언트가 있고 이 클라이언트는 K3를 현재 사용하고 있습니다.

 

interface Car

public interface Car {
    void findCarName();
    void findCarBrand();
}

class K3

public class K3 implements Car{

    @Override
    public void findCarName() {
        System.out.println("현재 차의 이름은 [K3] 입니다.");
    }

    @Override
    public void findCarBrand() {
        System.out.println("[K3]는 [KIA]에서 만들어졌습니다.");
    }
}

interface Engine

public interface Engine {
    void WayToMove();
}

class Fuel

public class Fuel implements Engine{
    @Override
    public void WayToMove() {
        System.out.println("[연료]를 사용해서 움직입니다.");
    }
}

interface Driver

public interface Driver {
    void findOwnCarDetail();
}

 

class DriverImpl

public class DriverImpl implements Driver{

    Car car = new K3();
    Engine engine = new Fuel();

    @Override
    public void findOwnCarDetail() {
        car.findCarName();
        car.findCarBrand();
        engine.WayToMove();
    }
}

 

단순히 차의 세부정보를 보여주는 코드입니다. 차 이름, 차 브랜드, 기동 방식을 알 수 있습니다. 

 

승진을 해 월급이 올라서 K3에서 K5로 차를 변경하기로 했습니다. 그러면 아래처럼 코드를 바꿔주면 됩니다.

Car car = new K3(); ---> Car car = new K5();
public class DriverImpl implements Driver{

    Car car = new K5();
    Engine engine = new Fuel();

    @Override
    public void findOwnCarDetail() {
        car.findCarName();
        car.findCarBrand();
        engine.WayToMove();
    }
}

혹은 요즘 친환경이 대세니깐 전기차 버전의 K3로 바꾸고 싶습니다.

public class DriverImpl implements Driver{

    Car car = new K3();
    Engine engine = new Electronic();

    @Override
    public void findOwnCarDetail() {
        car.findCarName();
        car.findCarBrand();
        engine.WayToMove();
    }
}

 

K3를 K5로 바꾸거나 Fuel에서 Electronic으로 변경을 할 때 코드만 살짝 바꾸면 되서 얼핏보면 객체 지향적으로 설계한 것처럼 보일 수 있습니다. 하지만 이는 명백히 DIP와 OCP를 위배하고 있습니다.

 

OCP 위배

OCP는 위에서 말씀드린 것처럼 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 하는데 K3를 K5로 바꾸기 위해서 코드를 수정해줘야 합니다. 

 

DIP 위배

DIP는 역할(인터페이스)에 의존하고 구현에는 의존하지 않아야 하는데 위 위코드는 DriverImpl이라는 클래스가 Car(역할), K3(구현)에 모두 의존하고 있습니다. 

 

갓영한님의 말을 빌리자면

"로미오와 줄리엣 공연을 하면 로미오 역할을 누가 할지 줄리엣 역할을 누가 할지는 배우들이 정하는게 아니다. 이전 코드는 마치 로미오 역할(인터페이스)을 하는 레오나르도 디카프리오(구현체, 배우)가 줄리엣 역할(인터페이스)을 하는 여자 주인공(구현체, 배우)을 직접 초빙하는 것과 같다. 디카프리오는 공연도 해야하고 동시에 여자 주인공도 공연에 직접 초빙해야 하는 다양한 책임을 가지고 있다."

클래스 다이어그램

즉 이전 코드는 위와 같이 구현체에 의존하고 있다.

 

이번 글에서 DIP와 OCP가 위배되는 경우에 대해 알아보았습니다. 스프링을 쓰는 가장 중요한 이유가 결국 DIP와 OCP를 해결하고, DI를 통해 객체를 주입시켜주는 거라 생각합니다.

 

다음 포스팅에서는 DIP와 OCP를 해결하는 방법에 대해 알아보겠습니다.

반응형

article prev thumbnail
article next thumbnail
profile on loading

Loading...