본문 바로가기

CS ﹒ Algorithm/DesignPatterns

디자인 패턴 (3) Abstract-Factory Pattern : 추상 팩토리 패턴 & 팩토리 메서드와 비교하기

 

 

 

Abstract-Factory Patrern?

 

구체적인 클래스에 의존하지 않고 서로 연관되거나 의존적인 객체들의 조합을 만드는 인터페이스를 제공하는 패턴.

이렇게 말하면 어렵지만 그냥 클래스 집합 만들어주는 패턴이라고 생각하면 쉽다.

 

팩토리 메서드 패턴과 무엇이 다를까?

팩토리 메서드 패턴은 구현하는 개발자 입장에서 구체적인 객체 생성 과정을 하위 클래스로 옮겨 유연한 개발을 하는 것이 목적이고, 추상 팩토리 패턴은 클라이언트 입장에서 구체적인 클래스의 목록을 모르고도 인스턴스를 생성해서 사용할 수 있게 해주는 것이 목적이다.

 

즉, 일반적인 개발 환경보다는 프레임워크 같이 다른 개발자가 사용할 코드를 작성할 때 주로 사용되는 패턴이다.

 

추상 팩토리 패턴은 추상 메서드 패턴과 헷갈릴 수 있기 때문에 둘을 동시에 적용할 예제를 만들어 왔다.

(물론 현실과는 완전히 동떨어진 예제이다.)

 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
 
@Data
public class Character {
    private String name;
    private Job job;
    private int HP;
    private int MP;
    private int exp;
    private int level;
    private BasicWeapon basicWeapon;
    private BasicShield basicShield;
    private HighEndWeapon highEndWeapon;
    private HighEndShield highEndShield;
 
 
    public Character(String name, Job job) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("유효하지 않은 이름");
        }
        if (job == Job.WARRIOR) {
            this.HP = 150;
            this.MP = 100;
        }
        if (job == Job.MAGICIAN) {
            this.HP = 100;
            this.MP = 150;
        }
 
        this.name = name;
        this.job = job;
        this.basicShield = basicShield;
        this.basicWeapon = basicWeapon;
    }
 
    public void checkExp() {
        if ( exp > (level * 10000)) {
            System.out.println("레벨 업까지 충분한 경험치가 모였습니다.");
        } else {
            System.out.println("현재 경험치는 " + exp + "이며 레벨업까지 필요한 경험치는 " + (level*10000-exp) + "입니다.");
        }
    }
 
    public void levelUp() {
        if (exp >= (level * 10000)) {
            if (this.job == Job.WARRIOR) {
                this.HP += 50;
                this.MP += 10;
            }
 
            if (this.job == Job.MAGICIAN) {
                this.HP += 10;
                this.MP += 50;
            }
 
            this.exp -= this.exp - (level*1000);
            this.level += 1;
        }
 
        if (exp <= (level * 10000)) {
            System.out.println("경험치가 충분하지 않습니다.");
        }
    }
}
 
cs

 

 

 

세상에서 제일 지저분한 클래스가 있다.

이 클래스는 내부에 여러 객체와 검증, 조건문이 죄다 뒤섞여있으며 언제 조건들이 더 추가될지 모른다.

우선 지난시간에 배운 팩토리 메서드 패턴을 먼저 적용해보겠다.

 

 

 

 

1. 공통되지 않는 로직, 검증 로직 내보내기

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Data
public abstract class CharacterV2 {
    private String name;
    private Job job;
    private int HP;
    private int MP;
    private int exp;
    private int level;
    private BasicWeapon basicWeapon;
private BasicShield basicShield;
private HighEndWeapon highEndWeapon;
private HighEndShield highEndShield;
 
    private Equipment_Factory equipmentFactory;
 
    public void checkExp() {
        if ( exp > (level * 10000)) {
            System.out.println("레벨 업까지 충분한 경험치가 모였습니다.");
        } else {
            System.out.println("현재 경험치는 " + exp + "이며 레벨업까지 필요한 경험치는 " + (level*10000-exp) + "입니다.");
        }
    }
 
    public void levelUp() {
        if (exp >= (level * 10000)) {
            updateStats();
        }
 
        if (exp <= (level * 10000)) {
            System.out.println("경험치가 충분하지 않습니다.");
        }
    }
 
    public abstract void updateStats();
}
cs

 

 

 

2. 별도 로직을 가지고 있는 클래스는 따로 분리 (Warrior 클래스는 생략)

 

1
2
3
4
5
6
7
public class MagicianMaker implements Factory_Method {
    @Override
    public CharacterV2 getCharacter() {
        return new Magician();
    }
}
 
cs

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Magician extends CharacterV2 {
    private int HP;
    private int MP;
 
    public Magician() {
        setHP(100);
        setMP(150);
        setJob(Job.MAGICIAN);
    }
 
    @Override
    public void updateStats() {
        this.HP += 10;
        this.MP += 50;
    }
}
cs

 

 

 

 

3.  공통 검증 로직은 모아서 팩토리 메서드 내부에서 실행한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface Factory_Method {
    default CharacterV2 createOrder(String name) {
        validate(name);
        CharacterV2 character = getCharacter();
        character.setName(name);
        return character;
    }
 
    default void validate(String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("유효하지 않은 이름");
        }
    }
 
    CharacterV2 getCharacter();
}
cs

 

 

 

 

여기까지 진행했으면 이제 Character 클래스가 많이 깨끗해진 것을 확인할 수 있다.

별도로 구현해야할 로직은 각 클래스에서 구현하고 있으며 검증 로직 또한 팩토리 메서드 내부에서 실행하고 있기 때문에 Character클래스는 언제든 변경하기 쉽고 확장 가능한 클래스가 되었다.

 

 

하지만 이것들이 우리의 심기를 거스른다.

우리의 손은 두 개이기 때문에 필요한 건 평범한 장비 세트, 혹은 좋은 장비 세트 이렇게 둘 중 하나인데

( 이 바보같은 게임은 무조건 장비를 세트로만 착용할 수 있다고 가정한다. )

매번 쥐어줄 장비의 이름은 일일히 입력해줘야 한다.

 

게다가 여기서는 고작 무기와 방패만 있지만, 만약 투구 갑옷 건틀렛 열 손가락 반지 목걸이 코걸이?.. 그리고 빤스까지 직접 입력해줘야 한다고 생각해보자. 정말 끔찍한 일이 아닐 수 없다.

 

어차피 세트로만 장비를 착용할 수 있는 이 바보 같은 게임에서 "유저인" 우리는 저 장비 하나하나의 효과가 궁금하지도 않고, 하나의 뭉탱이가 필요할 뿐인데 세트를 맞추기 위해 필요한 장비 이름을 하나하나 외워야하는 어이없는 일이 벌어지고 말았다.

 

추상 팩토리 패턴을 사용해서 이것을 해결해보자.

 

 

 

 

1. 우선 모든 Shield와 Weapon을 인터페이스로 변경하였다.

내부는 비워놨기 때문에 따로 올리지 않겠다.

1
2
3
4
5
6
7
8
9
10
11
@Data
public abstract class CharacterV2 {
    private String name;
    private Job job;
    private int HP;
    private int MP;
    private int exp;
    private int level;
    private Weapon weapon;
    private Shield shield;
}
cs

 

 

 

2. 추상 팩토리 패턴도 팩토리 메서드와 마찬가지로 대신 생성해줄 추상 클래스를 만들어준다.

1
2
3
4
5
public interface Equipment_Factory {
    Weapon getWeapon();
    Shield getShield();
}
 
cs

 

 

 

 

3. 그리고 같은 종류의 장비들을 모아서 반환해주는 팩토리 클래스를 구현한다.

각각의 아이템 클래스는 미리 만들어둔 Shild와 Weapon을 implemnts하고 있을 뿐 별다른 내용이 없으므로 생략하겠다.

1
2
3
4
5
6
7
8
9
10
11
12
public class BaseEquipmentFactory implements Equipment_Factory{
    @Override
    public Weapon getWeapon() {
        return new BaseWeapon();
    }
 
    @Override
    public Shield getShield() {
        return new BaseShield();
    }
}
 
cs
1
2
3
4
5
6
7
8
9
10
11
12
public class HighendEquipmentFactory implements Equipment_Factory {
    @Override
    public Weapon getWeapon() {
        return new HighEndWeapon();
    }
 
    @Override
    public Shield getShield() {
        return new HighEndShield();
    }
}
 
cs

 

 

 

 

4. 결과물

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class FactoryAbstract_Main {
    public static void main(String[] args) {
        // 기존
        Character Kim = new Character("김씨", Job.MAGICIAN);
        Kim.setBasicShield(new BasicShield());
        Kim.setBasicWeapon(new BasicWeapon());
        //추상팩토리
        Equipment_Factory baseEquipmentSet = new BaseEquipmentFactory();
        //추상메서드
        Factory_Method characterFactory = new WarriorMaker();
        CharacterV2 hehe = characterFactory.createOrder("힝힝이");
        hehe.setWeapon(baseEquipmentSet.getWeapon());
        hehe.setShield(baseEquipmentSet.getShield());
    }
}
cs

 

 

 

1. 추상 메서드 패턴을 적용하기 전후 차이

=> 공통되지 않은 로직과 검증 로직들을 모두 분리하여 추후 요구사항이 추가되어도 유연하게 수정할 수 있는 구조로 변경.

 

2. 추상 팩토리 패턴을 적용하기 전후 차이

=> 내부에 필요한 클래스를 공통 역할을 가진 것들끼리 묶어놔서 클라이언트는 내부의 객체 하나하나를 알지 못해도 기능을 모두 이용할 수 있음.(인터페이스 역할)