1. 요약
2. 상속과 구성
2-1. 상속
2-2. 상속의 문제점
2-3. 구성
2-4. 구성의 문제점
2-5. 그렇다면 무엇을 사용해야 할까?
3. 결합을 더 느슨하게 하기
4. Delegation
4-1. 개념
4-2. 예제
4-3. Delegation을 쉽게 사용하는 방법
1. 요약
🧑💻: Delegation이 무엇인지 아시나요?
👨🏻🦱: Delegation은 어떤 기능을 자신이 처리하지 않고 다른 객체에 위임시켜 그 객체가 일을 처리하도록 하는 것입니다.
Java와 달리 Kotlin은 by 예약어를 통해 Delegation을 편하게 사용할 수 있습니다.
+)
다른 클래스를 private 인스턴스 변수로 가지고 있는 Composition과
다른 클래스의 메서드를 호출하여 결과를 반환하는 Forwarding을 이용해 Delegation 패턴을 만들 수 있습니다.
2. 상속과 구성
Delegation에 대해 이해하려면 상속과 구성에 대한 학습이 선행되는 것이 좋다.
이미 두 개념에 대해 알고 있다면 바로 [목차 4]로 넘어가면 된다.
잘 모른다면 [목차 2]부터 정독하는 것을 추천한다.
2-1. 상속
상속과 구성은 객체지향 프로그래밍에서 기능의 재사용을 위해 사용하는 대표적인 디자인 패턴이다.
(참고로 재사용이 목적의 전부는 아니다)
두 패턴은 각각 장단점이 있으며 상황에 따라 맞게 사용해야 한다.
우선 상속에 대해 먼저 알아보자.
class Animal {
public void run() {
System.out.println("달려달려~");
}
}
class Dog extends Animal {
}
자식 클래스가 부모 클래스에게 상속을 받으면
자식 클래스는 부모 클래스의 변수와 메서드 등을 재사용할 수 있다.
위 코드에서는 Dog 클래스가 Animal을 상속받고 있다.
즉, Dog 클래스는 run이라는 메서드를 따로 선언하지 않아도 사용할 수 있다.
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.run(); // Animal 클래스의 run이 실행되어 "달려달려~"가 출력됨
}
}
따라서 위 메인문을 실행시키면 Dog에는 run이 없지만
부모 클래스인 Animal의 run이 실행되며 "달려달려~"가 출력되는 것이다.
이렇게 상속은 기능을 재사용하기 위해 사용하는 기법이다.
2-2. 상속의 문제점
class Animal {
/*
public void run() {
System.out.println("달려달려~");
}
*/
// 메서드 이름을 run에서 dash로 바꿨다.
public void dash() {
System.out.println("달려달려~");
}
}
class Dog extends Animal {
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.run(); // 에러 발생!! 수정이 필요하다.
}
}
예를 들어 Animal의 run 메서드를 dash로 바꿔달라는 요구사항이 들어왔다고 해보자.
이 경우 더 이상 Dog은 run 메서드를 사용할 수 없으며
dog 객체를 사용하는 곳을 모두 수정해야 하는 상황이 되어버린다.
위 예제에서는 메인문에서만 run을 사용하고 있지만
dog 객체를 통해 run을 사용하는 곳이 100군데 있다고 해보자.
100군데 전부 다 dog.run()에서 dog.dash()로 수정해야 하는 상황이 오는 것이다.
이렇게 부모 클래스의 변경사항이 자식 클래스에게 영향을 미치는 것을 보고
둘 사이의 결합도가 강하다고 표현한다.
이것은 상속의 문제점 중 하나이고 이는 구성을 사용함으로써 해결할 수 있다.
다음 목차를 이어서 보자.
2-3. 구성
class Pistol {
public void bang() {
System.out.println("권총 빵야!");
}
}
class Police {
private Pistol pistol = new Pistol(); // Police 클래스가 Pistol 클래스를 인스턴스 변수로 가지고 있다(= Composition)
public void bang() {
pistol.bang();
}
}
이번엔 구성에 대해서 알아보자.
구성도 마찬가지로 코드의 재사용을 위해 사용할 수 있는 패턴이다.
위 코드에서는 Police가 Pistol을 상속받고 있지 않음에도 Pistol의 bang 메서드를 재사용하고 있다.
이것이 가능한 이유는 Police 클래스가 Pistol 객체를 인스턴스 변수로 가지고 있기 때문이다.
한 클래스가 다른 클래스를 인스턴스 변수로 가지고 있는 형태를 우리는 구성(Composition)이라고 한다.
public class Main {
public static void main(String[] args) {
Police police = new Police();
police.bang(); // "권총 빵야!" 출력
}
}
실행 결과는 위와 같이 "권총 빵야!"가 출력될 것이다.
class Pistol {
/*
public void bang() {
System.out.println("권총 빵야!");
}
*/
// 메서드 이름을 bang에서 fire로 바꿨다.
public void fire() {
System.out.println("권총 빵야!");
}
}
class Police {
private Pistol pistol = new Pistol();
public void bang() {
pistol.bang(); // 여기 한 군데에서만 에러가 난다.
}
}
public class Main {
public static void main(String[] args) {
Police police = new Police();
police.bang(); // 여기서는 에러가 나지 않는다. 수정이 필요없다.
}
}
[목차 2-2]와 같은 상황을 똑같이 대입해보자.
bang 메서드를 fire로 바꿔달라는 요구사항이 들어왔을 때
우리는 police 객체를 통해 bang을 사용하는 곳이 100군데가 있다고 해도 수정할 필요가 없다.
단지 Police 클래스 안에 있는 bang 메서드 한 군데만 수정하면 될 뿐이다.
이때 우리는 Police 클래스와 Pistol 클래스의 결합이 느슨하다고 표현할 수 있다.
2-4. 구성의 문제점
class Animal {
public void run() {
System.out.println("달려달려~");
}
}
class Dog extends Animal {
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.run(); // "달려달려~" 출력
}
}
[목차 2-1]에서 사용했던 코드를 다시 가져왔다. (상속을 이용한 코드)
Animal 클래스에 먹기, 숨쉬기, 잠자기를 추가해보자.
class Animal {
public void run() {
System.out.println("달려달려~");
}
public void eat() {
System.out.println("먹기");
}
public void breath() {
System.out.println("숨쉬기");
}
public void sleep() {
System.out.println("잠자기");
}
}
class Dog extends Animal {
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.run();
dog.eat();
dog.breath();
dog.sleep();
}
}
Animal 클래스에 사용할 메서드만 추가해주면
dog 객체에서 달리기, 먹기, 숨쉬기, 잠자기를 사용할 수 있다.
class Pistol {
public void bang() {
System.out.println("권총 빵야!");
}
}
class Police {
private Pistol pistol = new Pistol();
public void bang() {
pistol.bang();
}
}
public class Main {
public static void main(String[] args) {
Police police = new Police();
police.bang();
}
}
이번엔 [목차 2-3]의 코드를 가져왔다.
Pistol 클래스에 재장전, 조준을 추가해보자.
class Pistol {
public void bang() {
System.out.println("권총 빵야!");
}
public void reload() {
System.out.println("재장전");
}
public void aiming() {
System.out.println("조준");
}
}
class Police {
private Pistol pistol = new Pistol();
public void bang() {
pistol.bang();
}
public void reload() {
pistol.reload();
}
public void aiming() {
pistol.aiming();
}
}
public class Main {
public static void main(String[] args) {
Police police = new Police();
police.bang();
police.reload();
police.aiming();
}
}
구성을 사용하니 같은 내용의 코드가 반복되고 상속을 사용한 코드보다 양이 많아지는 것을 볼 수 있다.
이는 소프트웨어 개발의 3대 원칙 중 하나인 DRY(= Do not Repeat Yourself)에 어긋난다.
이것이 바로 구성의 단점이다.
* 정확히는 Delegate 패턴의 단점이다.
[목차 4]에서 다시 설명하겠지만 Delegate와 Composition은 거의 같은 의미로 혼용되어 쓰이고 있다.
2-5. 그렇다면 무엇을 사용해야 할까?
일각에서는 상속보다는 구성을 선호하라는 말이 있다.
틀린 말은 아니다. 하지만 이는 상속을 완전히 배제하라는 뜻으로 받아들이면 안 된다.
현실적으로 불가능하거니와 바람직하지 않고, 상속과 구성을 상황에 맞게 같이 사용하는 것이 맞다.
일반적으로 상속을 사용해야 하는 상황은 (is-a) 포함 관계에 있을 때이다.
개는 동물이다(Dog is a Animal)처럼 말이다.
반면 구성을 사용해야 하는 상황은 (has-a) 포함 관계에 있을 때이다.
컴퓨터는 램을 가지고 있다(Computer has a RAM)처럼 말이다.
물론 이것만으로 상속을 사용해야 할지 구성을 사용해야 할지를 100% 확신할 순 없다.
이에 대해서는 나도 아직 경험이 충분하지 않아 공부를 더 해봐야 할 것 같다.
언제 무엇을 사용해야 하는지에 대해 더 자세히 알고 싶다면 위 두 블로그를 추천한다.
상속 | 구성 | |
캡슐화 | 위반함 | 위반하지 않음 |
결합도 | 강함 | 느슨함 |
다중상속 | 불가능 | 비슷하게 구현 가능 |
DRY 원칙 | 위반하지 않음 | 위반 |
위 표는 상속과 구성의 차이를 정리한 표이다. (참고용)
3. 결합을 더 느슨하게 하기
[목차 2]에서 구성이 상속보다 결합도가 낮다고 했다.
하지만 낮다고 했지 없다고 하진 않았다. (뭐 임마?)
이번 목차에서는 구성을 사용하면서 결합도를 더 낮추는 방법을 알아보자
(Delegation을 배우기 위한 과정이기도 하다)
class Pistol {
public void bang() {
System.out.println("권총 빵야!");
}
}
class Police {
private Pistol pistol = new Pistol();
public void bang() {
pistol.bang();
}
}
public class Main {
public static void main(String[] args) {
Police police = new Police();
police.bang();
}
}
경찰과 권총 예제를 다시 가져왔다.
음, 우리는 여기에 샷건을 하나 더 추가해보자.
class Pistol {
public void bang() {
System.out.println("권총 빵야!");
}
}
class Shotgun {
public void bang() {
System.out.println("샷건 빵야!");
}
}
class Police {
private Pistol pistol = new Pistol();
private Shotgun shotgun = new Shotgun();
public void bang() {
// 어라.. 어떻게 하지...?
}
}
public class Main {
public static void main(String[] args) {
Police police = new Police();
police.bang();
}
}
샷건 클래스를 추가했고 Police 클래스에 pistol 객체와 shotgun 객체를 생성했다.
자, 이제 Police가 총을 쏘려고 한다.
bang을 호출하면 pistol의 bang을 호출해야 할까 shotgun의 bang을 호출해야 할까?
방법은 여러 가지가 있을 것이다.
police 객체가 bang을 호출할 때 어떤 총을 쏠지 값을 넘겨줘서 분기를 태우든지
pistolBang(), shotgunBang()처럼 이름을 달리 하든지...
그렇다. 굉장히 좋지 않은 생각이다.
지금 위 구조는 police가 어떤 총을 사용할지 변화는 상황에 따른 유연성이 부족하다.
긴말하지 않고 여기서 더 느슨하게 결합하려면
interface를 사용하면서 외부에서 객체를 주입해주면 된다. 다음 예제를 보자.
interface Gun {
void bang();
}
class Police implements Gun {
private Gun gun;
public Police(Gun gun) {
this.gun = gun;
}
@Override
public void bang() {
gun.bang();
}
}
class Pistol implements Gun {
@Override
public void bang() {
System.out.println("권총 빵야!");
}
}
class Shotgun implements Gun {
@Override
public void bang() {
System.out.println("샷건 빵야!");
}
}
public class Main {
public static void main(String[] args) {
Pistol pistol = new Pistol();
Police police = new Police(pistol);
police.bang();
}
}
interface를 만들고 외부에서 객체를 주입해 결합도를 느슨하게 만들었다.
Police는 이제 bang을 할 때 이것이 권총인지 샷건인지 신경 쓸 필요가 없다.
단지 외부에서 넘겨받은 gun 객체의 bang을 실행할 뿐이다.
4. Delegation
4-1. 개념
드디어 Delegation에 대해 알아볼 차례이다.
한 클래스가 다른 클래스를 인스턴스 변수로 가지고 있는 형태를 구성(Composition)이라고 한다.
그리고 다른 클래스의 메서드를 호출하여 결과를 반환하는 것을 전달(Forwarding)이라고 한다.
그리고 Composition과 Forwarding을 합쳐서 Delegation이라고 한다.
검색해보면 인터넷에서는 셋을 거의 비슷한 의미로 사용하고 있고
특히 Composition이랑 Delegation을 혼용해서 사용하여 이해하는데 애를 많이 먹었다.
그도 그럴 것이 Composition을 쓰면 어쨌든 Forwarding을 쓰게 되고 어쨌든 Delegation 패턴이 된다.
그래도 엄연히 따지면 그 의미가 각각 다르다는 거...
그렇다면 Delegation의 의미를 예제로 통해 더 자세히 알아보자
4-2. 예제
interface Car {
void drive();
}
class Father implements Car {
private Car car;
public Father(Car car) {
this.car = car;
}
@Override
public void drive() {
car.drive();
}
}
class Me implements Car {
@Override
public void drive() {
System.out.println("운전합니다 빵빵");
}
}
public class Main {
public static void main(String[] args) {
Me me = new Me();
Father father = new Father(me);
father.drive();
}
}
(대리기사를 번역하기 애매해서 아빠로 예제를 만들었는데, 아빠 대신 대리기사를 대입해보면 이해가 좀 더 쉽다)
[목차 4-1]과 똑같은 예제이며 이해를 돕기 위해 클래스와 메서드명을 바꿔봤다.
위 예제에서는 내가 가지고 있는 차를 아빠에게 위임함으로써 아빠가 차를 몰 수 있게 된다.
(= Father에게 Me가 구현한 Car의 메서드를 위임함으로써 Father가 drive를 호출할 수 있다)
그렇다면 위임이란 단어의 뜻은 무엇인가
위임이란 어떤 일을 책임 지워 맡김이라는 뜻을 가지고 있다.
우리가 대리운전을 부르면 대리기사님께 차 운전을 책임 지워 맡기지 않는가? 똑같다.
우리가 부동산 계약을 할 때 집주인 대신 중개인이 책임지고 부동산 계약을 하지 않는가? 똑같다.
즉, 위임은 어떤 기능을 자신이 처리하지 않고 다른 객체에 위임시켜 그 객체가 일을 처리하도록 하는 것이다.
정리하자면 father가 drive 할 수 있는 이유는 위임을 받았기 때문이다.
그리고 다시 반복하자면 Delegation은?
Composition + Forwarding이 합쳐진 것이다.
4-3. Delegation을 쉽게 사용하는 방법
아쉽게도 Java에는 없다.
몇몇 언어에서는 이를 쉽게 사용할 수 있도록 지원한다.
가령 Kotlin에서는 by라는 키워드를 사용하면 위임받을 내용이 얼마나 길든 딱 한 줄로 해결할 수 있다.
(Java는 따로 언어 차원에서 지원하지 않기 때문에 Java를 쓰는 사람들이 상속을 많이 쓴다 카더라를 들었다)
by에 대해서 이야기를 시작하면 또 끝없이 길어지기 때문에
다음 포스팅에서 이어서 알아봐야 할 것 같다.
오늘의 공부 끝!
💡 느낀 점
- 원래 Delegation에 대해서 공부하려고 시작한 포스팅인데 덕분에 상속과 구성에 대해 자세히 알게 됐다. 굳
- by 키워드에 이렇게나 많은 필요 지식이 뒤에 깔려있었다니... by를 아무 생각 없이 사용했던 지난날들을 반성해야겠다.
- Java는 왜 위임을 위한 키워드를 제공하지 않을까 궁금해서 찾아봤는데, 이에 대해 stackoverflow에 올라온 질문 글이 있다. 흥미로우니 읽어보는 것 추천
- 이전에 나는 상속만 쓸 줄 알았지 구성에 대해서 깊게 고민해보지 않은 것 같다. 앞으로는 구성도 염두에 두면서 코드를 짜는 습관을 길러야겠다.
📘참고한 자료
- favor object composition over class inheritance의 두 가지 해석 - 류광의 번역 이야기
- Delegate Pattern이란? - Dev.Cho
- 컴포지션 - Damir
- 상속보다 합성을 사용해야 하는 이유
- Composition vs Extends
- 위임패턴 - KAMIYU
- Kotlin Delegation 이해하기 - Ready Kim
'오늘은 뭘 배울까? > Android' 카테고리의 다른 글
화면을 회전할 때 viewModel의 onCleared가 호출되지 않는 이유 (0) | 2022.08.26 |
---|---|
Kotlin 확장 함수(Extension Function)를 아시나요? (2) | 2022.07.16 |
Observable Field와 LiveData의 차이가 무엇인가요? (0) | 2022.07.09 |
Activity Intent Flag에 대해서 설명해 보세요 (2) | 2022.07.08 |
lateinit과 by lazy의 차이가 무엇인가요? (8) | 2022.06.22 |
댓글