DIP와 OCP, DI와 IoC, 전략 패턴

헙크·2023년 4월 9일
3
post-thumbnail

0. 개요

이 글에서는 DIP와 DI, IoC에 대해서 설명하고, OCP와 전략패턴까지 나의 생각을 적어보려고 한다. 이 글을 쓰게 된 계기는 DIP와 OCP에 대해서 알아보다가 여러 부차적인 개념들과 확장된 개념들에 대해서 조금이나마 나의 주관을 가질 수 있도록 작성하게 되었다.

1. DIP

1.1. DIP란?

DIP는 SOLID 원칙 중 D에 해당하는 원칙으로, Dependency Inversion Principle의 약자입니다. 이는 의존성 혹은 의존 관계 역전 원칙으로 해석할 수 있습니다.

DIP를 보통 설명할 때 다음과 같이 설명하곤 합니다.

구체적인 것에 의존하지 말고, 추상적인 것에 의존하라.

이를 풀어서 설명하자면, 어떤 구체적인 구현체의 기능을 사용하기 위해 구현체를 필드로 가지기 보다는 추상적인 것을 필드로 가지라는 뜻입니다. 조금 더 쉽게 설명하자면 클래스의 멤버 변수를 정의하는 데에, 구체적인 변수에 구체적인 구현체를 담는 것이 아니라 추상적인 변수에 구체적인 구현체를 담으라는 뜻입니다.

구체적인 것에서 추상적인 것으로 의존 관계를 맺으라는 부분에서 '역전'이 일어났다고 볼 수 있겠네요!ㅎㅎㅎ '역전'이라는 뜻은 '형세가 뒤집혔다'는 뜻입니다.

1.2. 의존관계란?

DIP를 설명하기 전에 의존성 혹은 의존 관계라는 말을 짚고 넘어가겠습니다. 일단 의존성과 의존 관계는 같은 말로 앞으로의 내용은 의존 관계로 통일해 설명하겠습니다.

의존 관계가 있다는 말은, a 클래스에서 b 클래스를 알고 있다는 뜻입니다. 이를 코드로 나타내자면 다음과 같습니다.

public class Person {
	private final Car car = new Car();
    ...
}

public class Car {
	void move() {
    	System.out.println("차를 타고 갑니다");
    }
}

Person이라는 클래스는 Car 클래스와 의존 관계를 가지고 있다고 볼 수 있습니다. 즉, Person 클래스에서 Car 클래스를 알고 있다는 것이죠. 보통 private final Car car = new Car() 부분에서 의존 관계가 있다는 것을 파악할 수 있죠.

1.3. DIP 적용

위의 내용을 코드에 적용시켜 보겠습니다.

// before
public class Person {
	private final Car car = new Car(); // 구체적인 것에 의존하지 말고
    ...
}

---
//after
public class Person {
	private final Vehicle car = new Car(); // 추상적인 것(인터페이스)에 의존하라
    ...
}

public interface Vehicle {
 	void move();
}
    
public class Car implements Vehicle {
  	@Override
    void move() {
      	System.out.println("차를 타고 갑니다");
    }
}

1.4. DIP의 이점

DIP를 만족시키는 것은 추상화된 것과 의존 관계를 맺는 것이고, 이는 다음과 같은 이점을 가져옵니다.

1. 코드의 유연성이 '그나마' 올라가고, 유지보수에 용이하다.

public class Person {
	private final Vehicle vehicle = new Bike();
}

public class Bike implements Vehicle {
	@Override
    void move() {
    	System.out.println("자전거를 타고 갑니다");
    }
}

위의 코드에서 볼 수 있듯이, 추상화된 것과 의존 관계를 맺기 때문에 다른 구현체로 바꾸기 쉽습니다. 예를 들어 Car가 아닌 Bike를 타고 가고 싶다면 Person 코드에서 new Bike()로 바꿔주기만 하면 됩니다. 이로써 코드의 유연성이 '그나마' 올라가고, 유지보수가 '그나마' 용이하다고 할 수 있습니다. 더불어 결합도도 '그나마' 낮아집니다.

2. 테스트 하기 '그나마' 용이하다.

public class Person {
	private final Vehicle vehicle = new TestVehicle();
}

public class TestVehicle implements Vehicle {
	@Override
    void move() {
 		// 아무것도 안합니다.
    }
}

또한 테스트가 용이한 임시의 구현체를 만들어 의존 관계를 맺으면 '그나마' 테스트하기 쉽습니다. 예를 들어, 테스트를 할 때에는 콘솔에 무언가를 찍으면 안되는 상황이라면 다음과 같이 임시의 구현체를 만들면 해결할 수 있습니다. 즉, 원하는 테스트 상황에 맞게 구현체를 만들어 '그나마' 적용할 수 있습니다.

3. 코드를 재사용하기 '그나마' 좋다.

Car를 가지고 있는 PersonBike를 가지고 있는 Person 등 여러 종류의 Person을 만드는 것이 아니라 때에 따라 구현체를 달리하여 Person을 생성하면 되기 때문에 코드를 '그나마' 재사용하기 좋다고 볼 수 있습니다.

1.5. 한계

위에서 설명한 장점은 모두 DIP 원칙을 따랐지만 이전의 단점을 그저 조금 개선한 상황입니다. 모두 완벽하게 개선한 것이 아니라 기존의 코드보다는 '그나마' 낫다는 의미였습니다.

위 코드의 문제점은 클라이언트 코드에서 변경해야 할 포인트가 불가피하게 생긴다는 것입니다. Car에서 구현체가 Bike로 바뀐다면 Person 클래스에서 new 연산자를 통해 다른 구현체로 컴파일 타임에 직접 바꿔주어야 합니다. 구체적인 변수에 구현체를 담는 것보다, 추상적인 변수에 구현체를 담는 것이 '그나마' 코드를 재사용하기 용이하지만, 직접 컴파일 타임에 수정을 해주어야 한다는 점은 여전히 해결되지 않았죠.

또한 런타임에 Person 객체에 원하는 구현체를 바꿔 낄 수 없습니다. 이러한 한계 때문에 속시원하게 장점을 장점이라고 말하지 못했습니다.

1.6. Trouble Shooting

런타임에 내가 타고 가는 탈 것의 종류가 Car인지 Bike인지를 넣어줄 수 있다면 Person을 조금 더 다양하게 활용할 수 있을 것입니다. 그렇다면 이제 DI를 사용하여 해결하면 좋을 때인 것 같습니다.

2. DI

2.1. DI란?

DI(Dependency Injection)는 의존성 주입 기술입니다. 런타임 시 어떤 객체가 필요한 의존성을 외부에서 주입해주는 실질적인 기술입니다.

2.2. DI 적용

public class Person {
   	private final Vehicle vehicle; // 직접 의존 관계를 생성하지 말고
        
    public Person(Vehicle vehicle) {
       	this.vehicle = vehicle;
    }
}

public interface Vehicle {
 	void move();
}
    
public class Car implements Vehicle {
  	@Override
    void move() {
      	System.out.println("차를 타고 갑니다");
    }
}

public class Bike implements Vehicle {
  	@Override
    void move() {
     	System.out.println("자전거를 타고 갑니다");
    }
}

public static void main(String... args) {
	Vehicle car = new Car(); 
    Vehicle bike = new Bike();
    
    // 외부에서 필요에 따라 Vehicle의 구현체를 달리해 주입시킬 수 있다.
	Person carPerson = new Person(car);
    Person bikePerson = new Person(bike);
}

2.3 DI의 이점

DI를 사용하면 객체 내에서 필요한 값을 직접 생성하여 사용하는 대신, 런타임에 다양한 값을 외부에서 주입받아서 다양하게 사용할 수 있습니다.

이로써 Car를 가지는 PersonBike를 가지는 Person 객체를 따로 만들지 않아도 되고, 외부에서 필요한 값을 주입하면 되므로 코드의 재사용성이 올라갑니다.

무엇보다도, Person에서 더이상 new 연산자로 필요한 의존관계를 직접 관리하지 않아도 되기 때문에 주입 받는 구현체가 바뀌더라도 Person 클래스는 수정할 필요가 없게됩니다. 결합도도 낮아졌네요.

따라서 위에서 DIP의 장점을 설명했던 모든 것들이 '확실히' 장점이 될 수 있다고 볼 수 있습니다.

위의 문제 상황에서 DIP를 현실적으로 코드에 적용하려면 DI는 필수적인 것으로 보입니다.

2.4. Trouble Shooting

DI와 IoC의 차이점은 무엇일까요?! 어디에서 사용되는 개념일까요?!

3. IoC

3.1. IoC란?

IoC는 Inversion of Control의 줄인 말로, 제어의 역전이라고 해석할 수 있습니다. DIP는 의존성이 역전 되었다고 설명했었는데 이제는 제어의 역전이라니!

IoC는 어떤 상황을 말하는 것입니다. 말 그대로 이는 원칙이 아니고 상황을 나타내는 말로 사용할 수 있을 것 같습니다.

결론부터 말하자면 위의 코드에서 Person은 외부에서 DI를 받는 부분이 IoC가 일어났다고 할 수 있습니다. Person 자기 자신이 제어의 흐름을 가지고 직접 new 연산으로 의존성을 관리하는 것이 아니라, 외부에서 의존성을 생성해서 Person에 주입해주는 상황이 Person 입장에서는 제어의 흐름권이 빼앗겼다라고 볼 수 있겠네요.

따라서 Person은 자신이 제어 흐름을 가지는 것에서 외부에서 제어 흐름을 가지는 것으로 제어의 역전이 일어났다고 볼 수 있습니다.

DI 기술을 사용했다면 IoC라는 현상이 발생하는 것은 어찌보면 당연한 일입니다.

3.2. Trouble Shooting

지금까지 DIP와 DI, IoC에 대해서 살펴보았습니다.

저는 지금까지 개선된 코드의 구조를 보고 OCP 원칙도 만족한다고 볼 수 있지 않을까 생각되어 OCP에 대해서도 다뤄보려고 합니다.

4. OCP

4.1. OCP란?

OCP 또한 SOLID 원칙 중 O에 해당하는 원칙으로, Open Closed Principle의 약자입니다. 이는 개방 폐쇄 원칙으로 해석할 수 있습니다.

이는 설명하자면 다음과 같습니다.

확장에는 열려있고, 변경에는 닫혀있어야 한다는 원칙

즉, 기존의 코드를 변경하지 않으면서(닫혀 있음) 새로운 기능을 추가할 수 있도록(열려 있음) 설계되어야 한다는 것입니다. OCP는 OOP 패러다임의 추상화와 다형성을 이용하여, 구성 요소 간의 결합도를 낮추고 확장성을 높일 수 있도록 합니다.

4.2. Trouble Shooting

아래의 코드는 DIP를 만족하고, DI 기술을 사용하고 있습니다.

public class Person {
   	private final Vehicle vehicle;
        
    public Person(Vehicle vehicle) {
       	this.vehicle = vehicle;
    }
}

public interface Vehicle {
 	void move();
}
    
public class Car implements Vehicle {
  	@Override
    void move() {
      	System.out.println("차를 타고 갑니다");
    }
}

public static void main(String... args) {
	Vehicle car = new Car(); 
    
	Person carPerson = new Person(car);
}

위의 DIP를 만족하는 코드만 보고 OCP를 만족한다고 말할 수 있을까요?

저는 DIP를 만족하면 OCP를 무조건적으로 만족하지는 않는다고 생각합니다. 그저 OCP를 만족할 여지가 있다고 말할 수 있을 것입니다.

DIP와 OCP 모두 SOLID의 원칙들입니다. 이들은 우리가 추구해야할 가치입니다. 현재의 코드 스냅샷만 보고 DIP와 OCP가 만족하는지 파악할 수는 있지만 미래의 스냅샷들에 대해서는 보장하지 못하죠.

예를 들어, 위의 DIP를 만족하는 코드에서 제가 다음과 같이 기능을 확장한다고 가정하겠습니다.

public class Person {
   	private final Vehicle vehicle;
        
    public Person(Vehicle vehicle) {
    	if (vehicle instanceof Bike) { 
        	this.vehicle = new Bike();
            return;
        }
       	this.vehicle = vehicle;
    }
}

public interface Vehicle {
 	void move();
}
    
public class Car implements Vehicle {
  	@Override
    void move() {
      	System.out.println("차를 타고 갑니다");
    }
}

public class Bike implements Vehicle {
  	@Override
    void move() {
     	System.out.println("자전거를 타고 갑니다");
    }
}

public static void main(String... args) {
	Vehicle car = new Car(); 
    Vehicle bike = new Bike();
    
	Person carPerson = new Person(car);
    Person bikePerson = new Person(bike);
}

이런 상황이라면 이전까지는 DIP를 만족하도록 코드를 짰었는데, 누군가 구조를 확장하는 과정에서 위와 같이 코드를 짠다면 한 순간에 DIP도 만족하지 않고 OCP도 만족하지 않는 코드가 되게 됩니다.

제 생각은, SOLID 원칙은 어찌보면 현재 스냅샷에 대해 판단할 수 있는 가치이고 앞으로 우리가 코드를 작성하는 데에 염두에 두고 추구해야 할 가치라고 생각해야 하지 않나 싶습니다. 혹자는 누가 위에 제가 작성한 코드처럼 코드를 짜겠냐고 하지만.. 사람일은 모르죠.

누군가가 저에게 위의 코드를 보고 DIP와 OCP를 만족하냐고 물어본다면, 저는 “DIP는 만족하지만 OCP는 만족할 여지가 있다. 아직 OCP를 만족하는지는 모른다”라고 대답할 것 같습니다.

따라서, DIP를 만족하면 OCP를 만족할 여지가 있다고 보는 것이 맞다고 생각합니다. DIP를 만족하면서, 추상적인 것에 대한 구현체를 만들고 클라이언트에 이를 DI하도록 한다면 이 스냅샷에서는 OCP를 만족한다고 보는 것이 맞을 것 같습니다.

4.3. OCP 적용

OCP를 만족하도록 구조를 확장한다면 다음과 같은 과정으로 진행될 것입니다.

새로운 Vehicle을 추가할 때에 Vehicle을 implements한 구현체를 만들고 이를 상위에서 DI합니다(확장에 열려있음). 그렇게 하면 기존의 클라이언트 코드인 Person은 손 댈 필요도 없습니다(변경에 닫혀있음).

아래와 같이 Bike를 구현하는 스냅샷에서는 OCP를 만족한다고 볼 수 있겠네요!

public class Person {
   	private final Vehicle vehicle;
        
    public Person(Vehicle vehicle) {
       	this.vehicle = vehicle;
    }
}

public interface Vehicle {
 	void move();
}
    
public class Car implements Vehicle {
  	@Override
    void move() {
      	System.out.println("차를 타고 갑니다");
    }
}

public class Bike implements Vehicle {
  	@Override
    void move() {
     	System.out.println("자전거를 타고 갑니다");
    }
}

public static void main(String... args) {
	Vehicle car = new Car(); 
    Vehicle bike = new Bike();
    
	Person carPerson = new Person(car);
    Person bikePerson = new Person(bike);
}

5. 전략 패턴

지금까지 DIP에서부터 OCP를 만족하는 코드까지 달려왔습니다. 현재 코드 구조를 다른 시각에서 보면, 전략 패턴과 동일한 구조를 가지고 있음을 파악할 수 있습니다.

5.1. 전략 패턴이란?

전략 패턴은 일련의 행동을 정의하고 각 행동을 캡슐화하며 상호 교환 가능하게 만드는 행동 설계 패턴입니다. 이 패턴을 사용하면 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있습니다.

5.2. 전략 패턴의 UML 다이어그램

전략패턴UML

위에서 DIP와 OCP를 만족시키기 위해 작성했던 코드의 구조와 정확히 일치합니다.

이 부분은 구체적인 구현체인 CarBike(ConcreteStrategy1, 2)를 생성하는 부분입니다. Vehicle car = new Car()Vehicle bike = new bike()가 이에 해당하는 것을 확인할 수 있습니다.

이 부분은 Person(Context)이 추상적인 Vehicle(Strategy)에 의존하는 것에 해당합니다.

의존은 추상적인 것과 하되 어찌됐든 구현체를 사용해야 하는데, 이를 DI를 통해 외부에서 주입받도록 할 수 있습니다. 코드로 보면 다음과 같습니다.

public class Person {
   	private final Vehicle vehicle; // 추상적인 곳에 의존하라
        
    public Person(Vehicle vehicle) { // DI - 의존 관계를 주입하라
       	this.vehicle = vehicle;
    }
}

5.3. DIP, OCP와 전략 패턴

전략패턴은 DIP와 OCP를 활용한 객체지향의 꽃이라 불리는 패턴입니다. 이 패턴을 사용함으로써 클라이언트 코드는 변경하지 않아도 되고, 기능을 확장할 수 있어 많이 사용되곤 합니다.

JDBC 인터페이스와 스프링의 DataSource, 메시지 컨버터 등 많은 부분에서 추상화가 이뤄지고 하위 구현체를 외부에서 주입하여 기존 코드의 변경 없이 확장할 수 있도록 합니다. 따라서 프레임워크를 이해할 때에는 이런 DIP와 OCP, 전략 패턴의 개념을 이해하고 여러 기술들의 구조를 이해하는 것이 중요하다고 생각합니다.

한편으로, "DIP와 OCP를 만족하면 전략 패턴이다"라고는 단언하지 못합니다. 추상 팩터리 패턴, 데코레이터 패턴 등 자바에 존재하는 여러 패턴은 기본적으로 추상화와 다형성을 이용한 즉, DIP와 OCP를 활용한 패턴이기 때문입니다. 적어도 전략 패턴을 사용하면 DIP와 OCP를 준수할 수 있다고 볼 수 있겠네요 ㅎㅎㅎㅎ

6. 마무리

이번 글에서는 DIP에서부터 DI, IoC, OCP, 전략 패턴에 대해서 꼬리를 물며 정리해보았습니다. 이 개념들을 정리하면서 좀처럼 그들간의 차이점과 인과 관계를 파악하기 쉽지 않았는데, 이 개념들이 모두 다른 레벨의 개념이라고 이해하니 이해하기 쉬웠습니다.

다시 한 번 간단히 정리하자면, 다음과 같이 정리할 수 있을 것 같네요 ㅎㅎㅎ

  1. DIP와 OCP는 SOLID 원칙이다. OOP를 위해 지속적으로 추구해야 할 가치이다. 현재 코드의 스냅샷으로 이러한 원칙들을 만족한다고 판단할 수 있지만 추후의 상황에 대해서는 아무도 이 원칙들을 만족한다고 보장할 수 없을 것이다.
  2. DI는 기술이다. 이를 코드에 적용함으로써 DIP를 만족할 수 있는 실마리가 된다.
  3. IoC는 현상이다. DI가 적용된 부분에 자연스레 IoC가 일어났다고 볼 수 있다.
  4. 전략 패턴은 DIP와 OCP를 준수하는 전형적인 패턴이다. 하지만 반대로 DIP와 OCP를 만족한다고 전략 패턴이 되는 것은 아니다. 다른 여러 패턴들도 존재한다.

여러 개념들에 대해 알아보고, 각각의 기술과 관점에서 하나하나 살펴보는 것이 꽤 까다로웠습니다. 또한 이를 글로 하나씩 풀어내려다보니 정말 많은 시간이 걸렸네요 ㅠㅠㅠ

어렵지만 중요한 개념인만큼, 이 글을 기반으로 부족한 부분들을 찾아내어 조금씩 더 알아보겠습니다. 제가 잘못 이해하거나 이상하다고 생각되는 부분은 댓글 남겨주세요! 긴 글을 읽어주셔서 감사합니다 :)

2개의 댓글

comment-user-thumbnail
2024년 5월 25일

우와 너무 재밌게 읽었어요! 댓글이 하나도 없는게 신기할 정도네요. 감사합니다 ~~

1개의 답글