👀 [OOP] 객체지향 5원칙(SOLID) - 의존성 역전 원칙 (Dependency Inversion Principle)
- [OOP] 객체지향 5원칙(SOLID) - 인터페이스 분리 원칙 (Interface Segregation Principle)
- [OOP] 객체지향 5원칙(SOLID) - 리스코프 치환 원칙 (Liskov Subsitution Principle)
- [OOP] 객체지향 5원칙(SOLID) - 개방-폐쇄 원칙 (Open-Closed Principle)
- [OOP] 객체지향 5원칙(SOLID) - 단일 책임 원칙 (Single Responsibility Principle)
- [OOP] 객체지향의 특징 - 다형성(Polymorphism)
- [OOP] 객체지향의 특징 - 상속(Inheritance)
- [OOP] 객체지향의 특징 - 캡슐화(Encapsulation)와 정보 은닉
- [OOP] 객체지향 프로그래밍(Object Oriented Programming)이란?
Table of Contents
의존성 역전 원칙 (Dependency Inversion Principle) 🔗
의존성 역전 원칙이란 객체는 저수준 모듈보다 고수준 모듈에 의존해야한다는 원칙이다.
말이 좀 어렵다. 고수준 모듈은 뭐고, 저수준 모듈은 또 뭐란 말인가?
- 고수준 모듈: 인터페이스와 같은 객체의 형태나 추상적 개념
- 저수준 모듈: 구현된 객체
고/저수준 모델의 정의는 위와 같다. 위 정의를 의존성 역전 원칙에 대입하면, 객체는 객체보다 인터페이스에 의존해야한다로 치환할 수 있다. 즉, 가급적 객체의 상속은 인터페이스를 통해 이루어져야 한다는 의미로 해석할 수 있다.
코드로 보는 의존성 역전 원칙 🔗
예시를 통해 의존성 역전 원칙을 준수하지 않은 경우와 준수한 경우 어떠한 차이가 있는지 코드를 통해 알아보자.
의존성 역전 원칙을 준수하지 않은 코드 🔗
어렸을 적 누구나 한 번씩 단풍에 대한 이야기를 다룬 게임을 해본적이 있을 것이다. 비록 이젠 죽어버렸지만 필자도 매우 좋아했었고, 성인이 된 후에도 방학시즌 이벤트만 되면 으레 복귀해서 게임을 즐기곤 했었다.
그 게임은 RPG 장르로, RPG가 그렇듯 다양한 직업군과 그에 맞는 스킬/무기 시스템이 갖춰져있다.
JAVA
0 | import java.util.Random; |
1 | |
2 | /** |
3 | * 한손검 객체 |
4 | * |
5 | * @author RWB |
6 | * @since 2021.08.17 Tue 01:36:44 |
7 | */ |
8 | public class OneHandSword |
9 | { |
10 | private final String NAME; |
11 | private final int DAMAGE; |
12 | |
13 | /** |
14 | * OneHandSword 생성자 함수 |
15 | * |
16 | * @param name: [String] 무기 이름 |
17 | * @param damage: [int] 데미지 |
18 | */ |
19 | public OneHandSword(String name, int damage) |
20 | { |
21 | NAME = name; |
22 | DAMAGE = damage; |
23 | } |
24 | |
25 | /** |
26 | * 공격 데미지 반환 함수 |
27 | * |
28 | * @return [int] 공격 데미지 (데미지 +-5) |
29 | */ |
30 | public int attack() |
31 | { |
32 | return DAMAGE + new Random().nextInt(10) - 5; |
33 | } |
34 | |
35 | /** |
36 | * 객체 문자열 반환 함수 |
37 | * |
38 | * @return [String] 이름 |
39 | */ |
40 | @Override |
41 | public String toString() |
42 | { |
43 | return NAME; |
44 | } |
45 | } |
수 많은 무기 가운데 하나인 한손검을 구현한 OneHandSword
객체가 있다. 캐릭터는 위와 같은 무기를 장비할 수 있을 것이다. 인스턴스 생성 시 무기의 이름과 데미지를 입력하여 생성한다.
JAVA
0 | /** |
1 | * 캐릭터 객체 |
2 | * |
3 | * @author RWB |
4 | * @since 2021.08.17 Tue 00:46:15 |
5 | */ |
6 | public class Character |
7 | { |
8 | private final String NAME; |
9 | private int health; |
10 | private OneHandSword weapon; |
11 | |
12 | /** |
13 | * Character 생성자 함수 |
14 | * |
15 | * @param name: [String] 이름 |
16 | * @param health: [int] 체력 |
17 | * @param weapon: [OneHandSword] 무기 |
18 | */ |
19 | public Character(String name, int health, OneHandSword weapon) |
20 | { |
21 | NAME = name; |
22 | this.health = health; |
23 | this.weapon = weapon; |
24 | } |
25 | |
26 | /** |
27 | * 공격 데미지 반환 함수 |
28 | * |
29 | * @return [int] 공격 데미지 |
30 | */ |
31 | public int attack() |
32 | { |
33 | return weapon.attack(); |
34 | } |
35 | |
36 | /** |
37 | * 피격 함수 |
38 | * |
39 | * @param amount: [int] 피격 데미지 |
40 | */ |
41 | public void damaged(int amount) |
42 | { |
43 | health -= amount; |
44 | } |
45 | |
46 | /** |
47 | * 무기 교체 함수 |
48 | * |
49 | * @param weapon: [OneHandSword] 무기 |
50 | */ |
51 | public void chageWeapon(OneHandSword weapon) |
52 | { |
53 | this.weapon = weapon; |
54 | } |
55 | |
56 | /** |
57 | * 캐릭터 정보 출력 함수 |
58 | */ |
59 | public void getInfo() |
60 | { |
61 | System.out.println("이름: " + NAME); |
62 | System.out.println("체력: " + health); |
63 | System.out.println("무기: " + weapon); |
64 | } |
65 | } |
게임 캐릭터를 구현한 Character
객체다. 게임 캐릭터가 취할 수 있는 기본적인 행동 일부가 구현되어있으며, 인스턴스 생성 시 캐릭터 이름, 체력, 무기를 입력하여 생성한다.
하지만 다들 알다시피, 무기엔 한손검만 있는 게 아니다. 근접계열 무기만 하더라도 두손검, 단검, 창, 도끼, 둔기 등 다양한 종류가 존재할 수 있다. 그러나 이 Character
객체. 애초에 한손검 외엔 쓸 수가 없는 구조다. Character
의 인스턴스 생성 시 OneHandSword
에 의존성을 가지기 때문. 공격 동작을 담당하는 attack()
메소드 역시 OneHandSword
에 의존성을 가진다.
이 상황에서 한손검을 제외한 다른 무기를 사용하려면 Character
의 코드를 바꿔야한다. 즉, 이전에 다뤘던 개방-폐쇄 원칙을 위배한다. 더 큰 문제는 무기가 바뀔 때마다 이 짓을 해줘야한다.
의존성 역전 원칙을 준수한 코드 🔗
만약 위 코드가 의존성 역전 원칙을 잘 지켰다면 고민할 필요가 없는 문제다. 위 코드의 가장 큰 문제는 이미 완전하게 구현된 저수준 모듈을 의존하고 있다는 점이다. 즉, 추상적인 고수준 모듈을 의존하도록 리팩토링해야한다.
JAVA
0 | /** |
1 | * 공격 인터페이스 |
2 | * |
3 | * @author RWB |
4 | * @since 2021.08.17 Tue 02:07:19 |
5 | */ |
6 | public interface Attackable |
7 | { |
8 | /** |
9 | * 공격 추상 함수 |
10 | * |
11 | * @return [int] 공격 데미지 |
12 | */ |
13 | int attack(); |
14 | |
15 | /** |
16 | * 객체 문자열 반환 추상 함수 |
17 | * |
18 | * @return [String] 이름 |
19 | */ |
20 | @Override |
21 | String toString(); |
22 | } |
우선 고수준 모듈인 Weapon
인터페이스를 생성한다. 공격 데미지를 반환하는 추상 함수 attack()
과 무기 이름을 반환하는 추상 함수 toString()
가 선언되어있다. 앞으로 모든 공격 가능한 무기 객체는 이 인터페이스를 상속받게 될 것이다.
JAVA
0 | import java.util.Random; |
1 | |
2 | /** |
3 | * 한손검 객체 |
4 | * |
5 | * @author RWB |
6 | * @since 2021.08.17 Tue 01:36:44 |
7 | */ |
8 | public class OneHandSword implements Attackable |
9 | { |
10 | private final String NAME; |
11 | private final int DAMAGE; |
12 | |
13 | /** |
14 | * OneHandSword 생성자 함수 |
15 | * |
16 | * @param name: [String] 무기 이름 |
17 | * @param damage: [int] 데미지 |
18 | */ |
19 | public OneHandSword(String name, int damage) |
20 | { |
21 | NAME = name; |
22 | DAMAGE = damage; |
23 | } |
24 | |
25 | /** |
26 | * 공격 데미지 반환 함수 |
27 | * |
28 | * @return [int] 공격 데미지 (데미지 +-5) |
29 | */ |
30 | @Override |
31 | public int attack() |
32 | { |
33 | return DAMAGE + new Random().nextInt(10) - 5; |
34 | } |
35 | |
36 | /** |
37 | * 객체 문자열 반환 함수 |
38 | * |
39 | * @return [String] 이름 |
40 | */ |
41 | @Override |
42 | public String toString() |
43 | { |
44 | return NAME; |
45 | } |
46 | } |
Attackable
를 상속받은 한손검 객체 OneHandSword
객체다. 상속받은 점 외에 크게 달라진 점은 없다.
JAVA
0 | /** |
1 | * 캐릭터 객체 |
2 | * |
3 | * @author RWB |
4 | * @since 2021.08.17 Tue 00:46:15 |
5 | */ |
6 | public class Character |
7 | { |
8 | private final String NAME; |
9 | private int health; |
10 | private Attackable weapon; |
11 | |
12 | /** |
13 | * Character 생성자 함수 |
14 | * |
15 | * @param name: [String] 이름 |
16 | * @param health: [int] 체력 |
17 | * @param weapon: [Attackable] 무기 |
18 | */ |
19 | public Character(String name, int health, Attackable weapon) |
20 | { |
21 | NAME = name; |
22 | this.health = health; |
23 | this.weapon = weapon; |
24 | } |
25 | |
26 | /** |
27 | * 공격 데미지 반환 함수 |
28 | * |
29 | * @return [int] 공격 데미지 |
30 | */ |
31 | public int attack() |
32 | { |
33 | return weapon.attack(); |
34 | } |
35 | |
36 | /** |
37 | * 피격 함수 |
38 | * |
39 | * @param amount: [int] 피격 데미지 |
40 | */ |
41 | public void damaged(int amount) |
42 | { |
43 | health -= amount; |
44 | } |
45 | |
46 | /** |
47 | * 무기 교체 함수 |
48 | * |
49 | * @param weapon: [Attackable] 무기 |
50 | */ |
51 | public void chageWeapon(Attackable weapon) |
52 | { |
53 | this.weapon = weapon; |
54 | } |
55 | |
56 | /** |
57 | * 캐릭터 정보 출력 함수 |
58 | */ |
59 | public void getInfo() |
60 | { |
61 | System.out.println("이름: " + NAME); |
62 | System.out.println("체력: " + health); |
63 | System.out.println("무기: " + weapon); |
64 | } |
65 | } |
게임 캐릭터 Character
객체다. 기존의 OneHandSword
를 파라미터에서 좀 더 고수준 모듈인 Attackable
을 파라미터로 받는 걸 확인할 수 있다. 그 밖의 무기와 관련된 메소드 전부가 그렇다.
하나의 객체였던 저수준 모듈에서 고수준 모듈로 의존성이 바뀌게 되니, Attackable
을 상속하는 모든 객체를 다룰 수 있다. 게임 시스템 내부적으로 모든 공격 가능한 무기는 Attackable
을 상속받기로 가정했으므로, 공격 가능한 모든 무기를 사용할 수 있는 셈이다.
이러한 변경으로 무기의 변경에 따라 Character
의 코드를 변경할 필요가 없으므로, 개방-폐쇄 원칙 또한 준수할 수 있다.
정리 🔗
의존성 역전 원칙은 코드의 확장성 및 재사용성을 추구하기 위한 원칙이다. 경직된 객체보다 구현되지 않아 유연한 인터페이스가 더욱 확장 가능성이 높을 것이다.
다른 원칙에 비해 의존성 역전 원칙은 중요도가 좀 떨어지는데, 그 이유는 타 원칙의 하위호환 격이기 때문이다. 당장 위에서도 언급했듯이, 의존성 역전 원칙은 개방-폐쇄 원칙을 준수할 경우 자연스레 준수하기이다. 뿐만 아니라 1객체 = 1책임인 단일 책임 원칙, 기능별 인터페이스화를 추구하는 인터페이스 분리 원칙을 준수할 경우 역시 마찬가지다.
객체 생성 시 객체로 구현해야할 것과 인터페이스로 구현해야할 것을 적절히 구분하여 올바른 의존 관계를 가지도록 구현하자. 이왕이면 방금 언급한 원칙들을 준수하여 두 마리 토끼를 잡는 것도 매우 좋은 방법일 것이다.
마치며 🔗
이 장을 끝으로 객체지향에 대한 글을 마무리한다. 지금껏 JAVA라는 객체지향 언어를 사용하고 있었음에도, 정작 객체지향에 대해 너무 몰랐다는 것을 새삼 느낀다. 내가 얼마나 비객체지향적으로 코딩했는지도....
객체지향에서의 가장 큰 특징이라면 바로 상속일 것이다. 객체지향의 5원칙 중 대부분이 상속과 직/간접적으로 연관된 것만 봐도, 객체지향의 아이덴티티는 상속이라고 할 수 있을 것이다. 그 만큼 객체지향에서 상속은 중요하면서 동시에 이해하기 어려운 개념이다. 잘 쓰기는 더더욱 어렵다.
물론 객체지향의 의의를 온전히 구현하고, 수립된 원칙들을 전부 지키는 것은 시니어급 개발자라도 매우 어려운 일일 것이다. 당장 나 같아도 "이 많은 걸 다 지켜가며 설계하라고?"라는 생각이 들고, 대부분의 개발은 구현 과정보단 동작 결과에 치중하기 때문이다.
탄탄한 기획과 이에 동반되는 고민은 견고한 설계가 가능하지만, 언제까지나 시간을 낭비할 수 없는 노릇이다. 아무리 탄탄하게 설계 중인 프로젝트라도 개발 기간을 준수하지 못 하면 결과물은 물론, 이 결과물을 내기까지 했던 모든 고민들이 시간낭비로 치부되는 것을 뻔하다.
이러한 원칙을 정해진 시간 내에 구현해야 하므로, 객체지향을 잘 다루기 위해선 많은 노력이 필요할 것이다.
📆 작성일
2021-08-17 Tue 03:06:35
📚 카테고리
🏷️ 태그