본문 바로가기

방법/객체지향프로그래밍

객체지향프로그래밍 (OOP) - 구성

구성(Composition) - "바구니 안에 담기"

정의

  구성이란 객체들의 관계를 구축하는 방법으로, 한 객체가 다른 객체들을 멤버변수로 갖고 있는 것(HAS-A 관계)이다.

마치 메인보드, 식판, 배틀그라운드의 장비창과 같이 하나의 이 된 객체들의 바구니의 이름을 짓은 것이 구성이라고 할 수 있다. (구성, 합성, 조합 등 다양한 말로 불린다.)

 

  특히 메인보드의 경우는 구성을 잘 설명할 수 있을 것 같다. 자세한 (콤퓨타는 하나도 몰라서 틀려도 무시해주길..) 

  • 일단, 각자의 자리가 정해져있다. CPU 자리면 CPU가, SSD 자리면 SSD가, 파워 자리면 파워가 꽂힌다.
  • 또한, 무조건 한 모델의 SSD만 들어갈 수 있는 것이 아니라 메인보드가 지원하는 소켓에 맞는 SSD라면 만든 회사에 상관없이 들어갈 수 있다.

자세한 사항은 왜? 챕터에서 설명하도록 한다.

 

메인보드는 잘 모르겠고, 식판은 밥과 스팸, 양상추를 품고 있다.


뭐가 다른가?

  이전 포스팅에서 상속보다 구성이 더 좋다고 언급했었다. 왜 더 좋은지 알아보려면 일단 상속과의 차이점을 알아야 하니, 바로 알아보자.

 

 IS-A 관계와 HAS-A 관계: 상속과 구성 모두 "객체들의 관계를 정의함으로 코드 재사용과 구현을 용이하게 하는 것"이다. 여기서 다른 점! 상속은 수직적 관계이다. 부모가 달라지면 자식도 달라져야 하고, 부모가 추가하면 자식에서도 추가되야 한다. 반면에 구성은 수평적 관계로, 구성 클래스는 필드 클래스를 사용할 뿐, 기능을 강제하진 않는다.

 

중복 관리: 상속은 부모에서 중복 소스를 정리해 놓는다. 구성은 구성 클래스에서 필드 클래스를 호출하는 형식으로 행위를 정리하여 놓는다.


왜?

구성은 객체지향의 4대 특징도 아닌데 왜 써야하는가? 상속에게 미안하지만, 또 상속과 비교하며 설명하도록 하겠다.

 

 상속은 필드와 메서드를 전달해주는 아주 좋은 개념이다. 하지만 상속은 결함과 불필요한 메서드들(형은 필요한데 나는 필요없는 것이라던지)도 같이 상속해준다..! 

 구성은 아니다! 구성 클래스(바구니)에서 써야할 메서드만 있으면 필드 클래스를 신경쓰지 않는다. 아주 이상적인 관계라고 볼 수 있다.

 

 또한, 상속이 상속이 아닐 때가 있다.

예를 들어 심장 클래스를 구현해두었다. 그리고 각가가 사람 클래스와 동물 클래스가 심장 클래스를 상속받아 사용한다. 이것이 일반적으로 생각하는 상속의 개념이라고 볼 수 있을까? 비록 구현은 될 지 몰라도 맞는 관계라고는 할 수 없을 것이다.(코드는 결국 사람도 보는 것이다. 사람이 보기에 이해하기 어렵다면 좋은 코드가 아니다. 이해하기 어려우면 고치기 어렵기 때문이다.)

 구성은 아니다! 바구니라고 말했다시피, 애초에 구성은 "담는다"라는 개념으로 차용하여 사용한다. 코드로 구현된 것을 볼 때에도 "아! 이 인터페이스형 변수를 필드로 갖고 있구나!"라고 단번에 이해할 수 있을 것이다. 이해할 수 있다면? 고치기도 쉬울 것이다!


어떻게?

어떻게?는 "필드로 담는다"라고 다 설명하긴 했지만, 상속과 비교하며 예시를 봐보자.

 

일단 상속의 경우다.

public class Inheritance {
    public static void main(String[] args) {
        Player p1 = new Player();
        Player p2 = new AxPlayer();
        Player p3 = new SwordPlayer();
    }
}


class Player {
    void eat() {
        System.out.println("밥 먹음");
    }

    void attack() {
        System.out.println("맨손으로 공격");
    }

    void defend() {
        System.out.println("맨손으로 방어");
    }

    void sleep() {
        System.out.println("잠자기");
    }

    void die() {
        System.out.println("죽음");
    }
}

class AxPlayer extends Player {
    @Override
    void attack() {
        System.out.println("도끼로 공격");
    }
}

class SwordPlayer extends Player {
    @Override
    void attack() {
        System.out.println("검으로 공격");
    }
}

음~ 잘 구현헀는걸~ 공통 구현 부분을 부모 클래스에 구현하여 나머지 부분을 잘 구현해서 사용했군~

 

정말 그럴까? 이게 끝일까? 갑옷, 펫, 장신구, 직업 등은 업데이트할 때마다 생겨날 것이다. 그렇다면..? 다 따로따로 구현해주어야 하므로 클래스 폭발이 발생할 것이다..!

  • SwordPlayerHasAxAndDogAndRing
  • SwordPlayerHasBowAndDogAndRing
  • SwordPlayerHasBowAndCatAndRing

그렇다면 구성은?

public class Composition {
    public static void main(String[] args) {
        Player p1 = new Player(new Ax(), new NoArmor());
        Player p2 = new Player(new Bow(), new IronArmor());

        p1.attack();
        p1.defend();

        p1.setArmor(new WoodArmor());
        p1.defend();

        p2.attack();
        p2.defend();
    }
}

class Player {
    private Weapon weapon;
    private Armor armor;

    public Player(Weapon weapon, Armor armor) {
        this.weapon = weapon;
        this.armor = armor;
    }

    public void setWeapon(Weapon weapon) {
        this.weapon = weapon;
    }

    public void setArmor(Armor armor) {
        this.armor = armor;
    }

    public void attack() {
        weapon.attack();
    }

    public void defend() {
        armor.defend();
    }
}


interface Weapon {
    void attack();
}

class Ax implements Weapon {
    @Override
    public void attack() {
        System.out.println("도끼로 공격");
    }
}

class Sword implements Weapon {
    @Override
    public void attack() {
        System.out.println("검으로 공격");
    }
}

class Bow implements Weapon {
    @Override
    public void attack() {
        System.out.println("활로 공격");
    }
}

interface Armor {
    void defend();
}

class NoArmor implements Armor {
    @Override
    public void defend() {
        System.out.println("맞으니까 그대로 아픔");
    }
}

class WoodArmor implements Armor {
    @Override
    public void defend() {
        System.out.println("맞으니까 조금 아픔");
    }
}

class IronArmor implements Armor {

    @Override
    public void defend() {
        System.out.println("맞았는데 하나도 안아픔");
    }
}

 

자 보자! 위의 경우는 Player라는 바구니에 Weapon, Armor 필드들을 담은 것이다.

 일단, 무기를 바꾸더라도 클래스를 변경할 필요가 없다. 같은 무기 다른 갑옷을 사용하더라도 구현해놓은 인터페이스를 가져다 쓰면 되므로 코드 재사용이 가능하다. 

 또한 펫, 장신구, 직업 등이 추가되더라도 필드만 추가하면 될 뿐이다. 클래스 폭발이 일어날 일은 없으니 안심해도 된다.

 

구현 방식을 더 자세히 설명하자면, Player를 보면 attack에서 단순히 Weapon의 attack을 호출한다. 이를 포워딩(forwarding)이라고 한다고 한다. 어쨋든 구성은 보통 포워딩 방식으로 필드 클래스를 사용하는 방식으로 구현한다. (별도의 로직을 삽입하여도 무관할 것이다.)


오늘은 구성에 대해 알아보았다.

구성을 사용하여 좀 더 예쁘게 관계를 정의할 수 있길 바란다. (상속도 좋은 점 많다.. 상속을 미워하지 말길..)

 

오늘 내용의 이해가 어려운 부분이 있었다면, 멤버 변수, 캡슐화, HAS-A 관계 등을 찾아보면 좋을 것 같다.

좀 더 심화해서 공부하고 싶다면? OCP, 스트래티지 패턴, 스테이트 패턴 키워드가 도움이 될지도 모르겠다.

 

 

 

참고

https://medium.com/humans-create-software/composition-over-inheritance-cb6f88070205

https://devjino.tistory.com/19

https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5%EC%9D%98-%EC%83%81%EC%86%8D-%EB%AC%B8%EC%A0%9C%EC%A0%90%EA%B3%BC-%ED%95%A9%EC%84%B1Composition-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0#%ED%95%A9%EC%84%B1_(Composition)_%EC%9D%B4%EB%9E%80