티스토리 뷰
25-1 학기에 짰던 애니멀 코드를 SOLID 원칙에 맞게 리팩토링해보자.
기존 클래스 다이어그램은 아래와 같다.

★<1>★ 현재 코드 SOLID 원칙에 위배되는 문제점 파악 -> 해결
1. Single responsibility 단일 책임 원칙 위반
동물 추상클래스도, 사자, 상어 등 각각의 클래스도, 심지어는 메인에서도 과도하게 여러 책임들이 부과돼있다.
대표적으로, 동물 추상 클래스를 봐보자.
BEFORE
package 애니멀;
public abstract class 동물 implements Huntable, Playable{
public String name;
public int hp;
public int power;
static int numOfAnimals;
//play() 특성 구현
@Override
public void play() {
// TODO Auto-generated method stub
System.out.println(this.name+"이 신나게 뛰어놉니다~~");
}
//hunt() 구현
@Override
public void hunt(동물 target) {
// TODO Auto-generated method stub
System.out.printf("%s(%d|%d)이(가) %s(%d|%d)을(를) 사냥합니다!\n", this.name, this.hp, this.power, target.name, target.hp, target.power);
if(this.getClass() == target.getClass()) {
System.out.println("자신과 동족은 사냥불가");
return;
}
if(this.hp <= 0) {
System.out.println("hp가 0 이하면 사냥불가");
return;//이 메소드 종료
}
this.hp += target.power;
target.hp -= this.power;
System.out.println(target.name+"사냥 성공!");
}
//생성자 만들기
public 동물() {
System.out.println("동물생성");
numOfAnimals++;
}
public 동물(String name) {
this();
this.name = name+numOfAnimals;
}
public 동물(String name, int hp, int power) {
this(name);//윗 생성자 호출
this.hp = hp;
this.power = power;
System.out.println("이름 : "+this.name);
}
//보여주기
public void show() {
System.out.printf("== %s ==\n", this.name);
System.out.printf("hp : %d power : %d \n", this.hp, this.power);
//System.out.println("현재 동물 총 "+동물.numOfAnimals+"마리");
}
}
현재 동물 클래스에는
(1) play()
(2) hunt()
(3) 생성자로 생성
(4) show()
메소드들이 있다. 이는 Single Responsibility 단일 책임 원칙에 부합하지 않는다.
또한 (2)hunt() 메소드에 주목해보면 Liscov Substitution 리스코프 치환 원칙을 치명적으로 위반하는 걸 알 수 있다.
왜냐하면 동물 추상 클래스는 모든 동물들의 상위 클래스로서 공통의 특성만이 있어야 하는데 정작 토끼와 기린은 초식동물이기에 huntable 기능이 없어야 한다!
따라서 책임을 줄이고, hunt() 메소드는 없애는 방향으로 고쳐야 한다.
다음은 고친 동물 추상 클래스의 모습이다.
AFTER
package Animal_Refactoring;
public abstract class 동물{
public String name;
public int hp;
public int power;
public void play() {
}
public 동물(String name, int hp, int power){
this.name = name;
this.hp = hp;
this.power = power;
}
}
2. Liscov Substitution 리스코프 치환 원칙 위반
모든 동물은 Huntable(사냥할 수 있는) 인터페이스를 implements하는데 정작 초식동물은 Huntable하지 않다. 그런데도 초식동물은 동물이 물려받은 Huntable을 간접적으로 implements 하고 있다. 이는 리스코프 치환 원칙에 위배된다.
BEFORE

차라리 사자, 상어만 Huntable 인터페이스를 implements해야 하는 걸로 고쳐야 하고, 초식동물 클래스는 없어도 된다.
다음은 고친 클래스 다이어그램의 모습이다.
AFTER

3. Dependency Inversion 의존성 역전 원칙 위반
BEFORE
package 애니멀;
import java.util.Scanner;
import java.util.*;
public class Main {
//0. showAnimals()
public static void showAnimals(동물 [] animal) {
for(동물 mal : animal) {
if(mal == null) System.out.print(" X |");
else System.out.printf("%s(%d) | ", mal.name, mal.hp);
}
System.out.println();
}
public static void main(String[] args) {
//1. 동물 생성 -> AnimalFactory에서
사자 a = new 사자("심바", 100, 50);
상어 b = new 상어("죠스바", 100, 30);
토끼 c = new 토끼("토롱이", 10, 3);
기린 d = new 기린("기인기린", 15, 5);
//2. 생성된 동물 배열에 넣는다 -> BattleManager에서
동물 [] animal = {a, b, c, d, new 사자("사자사자", 100, 45)};
//3. 노는 거 보여주기 -> BattleManager에서
System.out.println("숲속에 동물이 총 " + 동물.numOfAnimals +"마리가 있습니다.");
System.out.println("==========================================");
for(동물 ani : animal) ani.play();
System.out.println("==========================================");
int r=1;
System.out.println("사냥을 시작합니다.!");
showAnimals(animal);
//4. 규칙에 맞게 헌팅
while(true) {
//동물 한마리만 남으면 break
if(동물.numOfAnimals ==1) break;
//어태커, 타겟 랜덤 선택
int attacker = (int)(Math.random()*1000)%동물.numOfAnimals;
int target = (int)(Math.random()*1000)%동물.numOfAnimals;
//조건 안맞으면 다시
if (attacker == target || animal[attacker]==null || animal[target]==null) continue;
//hunt() 실행
System.out.println();
System.out.println(" >>> Round " + r + "<<<");
System.out.println();
animal[attacker].hunt(animal[target]);
//System.out.println("****");
animal[attacker].show();
//사망처리
if (animal[target].hp <=0) {
동물.numOfAnimals--;
System.out.println(animal[target].name + "은(는) 죽었다....");
animal[target]=animal[동물.numOfAnimals];
animal[동물.numOfAnimals]=null;
}
//hp가 마이너스인 게 사라지지 않아서 hunt행위한 이후 null은 아니지만 hp가 -인 동물 조사.
for(int i=0; i<동물.numOfAnimals; i++) {
if(animal[i]!=null && animal[i].hp<=0) {
System.out.println(animal[i].name+"은(는) 죽었다..");
동물.numOfAnimals--;
animal[i] = animal[동물.numOfAnimals];
animal[동물.numOfAnimals] = null;
i--;
}
}
showAnimals(animal);
System.out.println("현재 동물 총 "+동물.numOfAnimals+"마리");
r++;
}
System.out.println("사냥하기를 종료합니다!");
}
}
현재 Main에서 모든 걸(구체적으로 설명) 직접 생성해서 호출하고, 조작하고 있으므로 "외부에서 생성해 와서 주입 받는" Di/IoC 의존성 역전 원칙에 어긋난다.
메인 클래스 현재 상태 :
-동물 생성
-동물 모아둔 배열 animal 생성
-동물 별로 .play() 메소드 실행
-hunt()로 사냥 실행
-showAnimal()
따라서 GameManager 클래스를 만들어 책임을 분리시키고(단일책임의 원칙 준수), 여러 객체들을 주입 받는 식으로(의존성의 역전 원칙 실현) 만들어 Main을 변경해보자.
AFTER
package Animal_Refactoring;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
public class Main {
public static void main(String[] args) {
//게임매니저 객체 생성
//헌트룰 객체 생성
//동물Info 객체 생성
Map<String,Integer> hp = Map.of("사자",150,"상어",100,"기린",30,"토끼",15);
Map<String,Integer> power = Map.of("사자",30,"상어",30,"기린",15,"토끼",5);
동물Info info = new 동물Info(hp, power);
AnimalFactory factory = new AnimalFactory(info);
HuntRule rule = new HuntRule();
GameManager gm = new GameManager(rule);
ZooKeeper keeper = new ZooKeeper(factory);
gm.loadAnimals(keeper.provide());
gm.playAnimals();
gm.doGame();
}
}
간소화하였고, 각 메소드를 실행시킬 때, 인자로 주입 받는 방식(Di/IoC 원칙)을 사용했다.
★<2>★ 리팩토링 중 다이어그램 변화 과정
-초식동물 추상클래스를 없애고, Huntable 인터페이스를 육식동물에게만 구현시켰다.

-책임을 분산시키기 위해, AnimalFactory, ZooKeeper, HuntRule, 동물_Info 클래스를 만들었다.
또한 Playable 인터페이스를 없앴는데, 이는 어차피 동물들의 공통 추상 클래스에 명시돼 있기 때문이다.

-메인의 책임을 분산시키기 위해 GameManager 클래스를 만들었다.

-커피메이커에서의 매니저 격인 ZooKeeper 클래스가 동물, AnimalFactory(동물 생성) 클래스를 사용하도록 만들었다.

최종 클래스들은 위와 같고, 각각을 설명해보면 다음과 같다.
커피메이커 코드와 비교해보자.
| 커피메이커 코드 | 헌터앤애니멀 리팩토링 코드 |
| Menu (커피 메뉴, 커피의 가격 담고 있다) | 동물Info (동물의 hp, power 담고 있다) |
| CoffeeFactory(음료 생성) | AnimalFactory (동물 생성) |
| CoffeeMachine(음료 만드는 데 필요한 머신들) | HuntRule(전투 규칙) |
| CoffeeMaker(음료 제조 담당) | GameManager(라운드, 동물 헌팅 매칭, 사망 정리 등, 게임 담당) |
| Manager(전체 조절) | ZooKeeper(전체 조절) |
| Main(조립) | Main(조립) |
★<3>★ 전체 코드
1. 동물 추상 클래스 및 동물 하위 클래스들, huntable 인터페이스
package Animal_Refactoring;
public abstract class 동물{
public String name;
public int hp;
public int power;
public void play() {
System.out.println(name+"이(가) 신나게 놉니다!");
}
public 동물(String name, int hp, int power){
this.name = name;
this.hp = hp;
this.power = power;
}
}
-------------------------------------
package Animal_Refactoring;
public class 기린 extends 동물{
public 기린(String name, int hp, int power) {
super(name, hp, power);
}
public void play() {
System.out.println("기린이 높은 나무의 잎을 뜯으며 놉니다~");
}
}
--------------------------------------
package Animal_Refactoring;
public class 토끼 extends 동물{
public 토끼(String name, int hp, int power) {
super(name, hp, power);
}
public void play() {
System.out.println("깡총깡총 뛰어놉니다~");
}
}
------------------------------------
package Animal_Refactoring;
public class 상어 extends 동물 implements Huntable{
public 상어(String name, int hp, int power) {
super(name, hp, power);
}
public void play() {
System.out.println("지느러미로 파도를 가르며 놉니다~");
}
@Override
public void hunt(동물 target, HuntRule rule) {
rule.doHunt(this, target);
}
}
-------------------------------------
package Animal_Refactoring;
public class 사자 extends 동물 implements Huntable{
public 사자(String name, int hp, int power) {
super(name, hp, power);
}
public void play() {
System.out.println("사자가 초원을 달리며 어흥거리며 놉니다~");
}
@Override
public void hunt(동물 target, HuntRule rule) {
rule.doHunt(this, target);
}
}
------------------------------------
package Animal_Refactoring;
public interface Huntable {
public void hunt(동물 target, HuntRule rule);
}
2. 동물Info
-동물들의 hp/power 담기 위해 만들었다.
package Animal_Refactoring;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class 동물Info {
private final Map<String, Integer> hp;
private final Map<String, Integer> power;
public 동물Info(Map<String, Integer> hp, Map<String, Integer> power) {
this.hp = Map.copyOf(hp);
this.power = Map.copyOf(power);
}
public int getHp(String name) {
return hp.getOrDefault(name, 0);
}
public int getPower(String name) {
return power.getOrDefault(name, 0);
}
public boolean hasAnimal(String name) {
return power.containsKey(name);
}
}
3. AnimalFactory
-info를 인자로 받아서 동물 객체 생성
package Animal_Refactoring;
import java.util.HashMap;
import java.util.Map;
public class AnimalFactory {
private final 동물Info info;
public AnimalFactory(동물Info info) {
this.info = info;
}
public 동물 createAnimal(String name) {
int hp = info.getHp(name);
int power = info.getPower(name);
switch(name) {
case "사자": return new 사자(name, hp, power);
case "상어": return new 상어(name, hp, power);
case "토끼": return new 토끼(name, hp, power);
case "기린": return new 기린(name, hp, power);
default: return null;
}
}
}
4. ZooKeeper
-factory를 인자로 받아서 실제 동물들 한번에 생성
package Animal_Refactoring;
import java.util.ArrayList;
import java.util.List;
public class ZooKeeper {
private AnimalFactory factory;
public ZooKeeper(AnimalFactory factory) {
this.factory = factory;
}
public List<동물> provide(){
List <동물> list = new ArrayList<>();
list.add(factory.createAnimal("사자"));
list.add(factory.createAnimal("상어"));
list.add(factory.createAnimal("기린"));
list.add(factory.createAnimal("토끼"));
return list;
}
}
5. HuntRule
- 규칙에 맞게 사냥할 수 있도록 규칙 모음
package Animal_Refactoring;
public class HuntRule {
public void doHunt(동물 attacker, 동물 target) {
//조건 확인
//동족 사냥 불가
if(attacker.getClass()==target.getClass()) {
System.out.println("자신과 동족은 사냥불가");
return;
}
if(attacker.hp<=0) {
System.out.println("hp가 0 이하면 사냥불가");
return;//hp 0 이하면 불가
}
//헌팅, hp, power 관련 연산
System.out.printf(
"%s(%d|%d)이(가) %s(%d|%d)을(를) 사냥합니다!\n",
attacker.name, attacker.hp, attacker.power,
target.name, target.hp, target.power
);
attacker.hp += target.power;
target.hp -= attacker.power;
}
}
6. GameManager
-동물 리스트를 받아서 게임 진행
package Animal_Refactoring;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class GameManager {
private HuntRule rule;
private final List<동물> 동물원;
public void showAnimals(List<동물> 동물원) {
for(동물 a : 동물원) {
if(a==null) System.out.print(" X |");
else System.out.printf("%s(hp: %d, power: %d) | ", a.name, a.hp, a.power);
}
}
//rule 생성자로 주입받고
public GameManager(HuntRule rule) {
this.rule = rule;
this.동물원 = new ArrayList<>();
}
public void loadAnimals(List<동물> other) {
for(동물 a : other) {
if(a!=null) 동물원.add(a);
}
}
//놀고 있는 동물들
public void playAnimals() {
System.out.println("현재 동물원에는......");
for(동물 a:동물원) {
a.play();
}
System.out.println("==============================");
}
//라운드 하나에 대한 메소드
public void oneRound() {
if(동물원.size()==1) return;
int attacker = (int)(Math.random()*1000%동물원.size());
int target = (int)(Math.random()*1000%동물원.size());
while(attacker==target) {
target = (int)(Math.random()*1000%동물원.size());
}
동물 A = 동물원.get(attacker);
동물 B = 동물원.get(target);
if(A instanceof Huntable) {
((Huntable) A).hunt(B, rule);
}
else {
System.out.println(A.name+"은 사냥 불가");
}
for(int i=0; i<동물원.size(); i++) {
동물 x = 동물원.get(i);
if(x != null && x.hp<=0) {
System.out.println("X "+x.name+"은(는) 죽었다...");
동물원.remove(i);
i--;
}
}
}
//실제 게임 진행 메소드
public void doGame() {
int r = 1;
System.out.println("사냥을 시작합니다.!");
showAnimals(동물원);
while(true) {
if(동물원.size()==1) break;
System.out.println();
System.out.println("----------------------------");
System.out.println(" >>> Round " + r + " <<<");
System.out.println();
oneRound();
showAnimals(동물원);
System.out.println();
System.out.println("현재 동물 총"+동물원.size()+"마리가 있습니다.");
r++;
System.out.println("----------------------------");
}
System.out.println("사냥하기를 종료합니다!");
}
}
6. Main
- 각각의 클래스들 조립하기
package Animal_Refactoring;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
public class Main {
public static void main(String[] args) {
//게임매니저 객체 생성
//헌트룰 객체 생성
//동물Info 객체 생성
Map<String,Integer> hp = Map.of("사자",150,"상어",100,"기린",30,"토끼",15);
Map<String,Integer> power = Map.of("사자",30,"상어",30,"기린",15,"토끼",5);
동물Info info = new 동물Info(hp, power);
AnimalFactory factory = new AnimalFactory(info);
HuntRule rule = new HuntRule();
GameManager gm = new GameManager(rule);
ZooKeeper keeper = new ZooKeeper(factory);
gm.loadAnimals(keeper.provide());
gm.playAnimals();
gm.doGame();
}
}
★<4>★ 알게 된 점, 느낀 점
일단 미흡한 점들이 너무나 많다. 현재 코드의 문제점은 다음과 같다.
1. 동물을 한 마리 추가할 때 총 3개의 클래스(Main, Zookeeper, AnimalFactory)를 수정해야 한다. 이는 Open/Closed 원칙에 위배된다. 어떻게든 한 곳에서만 손쉽게 추가하기 위해 하고 싶었지만 배움이 짧아 구현하지 못했다. 너무 아쉽다.
2. 현재 GameManager에 굉장히 많은 책임이 부과됐다. Main은 가볍게 했지만 GameManager가 무거워진 것이다. 이또한 책임을 분산시키고 싶었으나 그러지 못했다. 이는 단일책임원칙(Single Responsibility)에 위배된다.
3. 마지막으로 커피메이커 코드에서의 매니저 역할을 ZooKeeper에게 맡기고 싶었는데 GameManager와 결국 그 역할을 나누게 되었다. 하지만 원래는 ZooKeeper가 전체를 총괄하는 식으로 만들고 싶었었다.
교수님의 커피메이커 코드를 숙지한 후 진행했음에도 SOLID 원칙을 지키는 것이 매우 어려웠다. 특히나 동물 생성 시 한 곳에서만 추가할 수 있도록 구현하고 싶었는데 이게 왜 이렇게 어렵던지 역시 이론과 실제는 다르다는 것을 느꼈다.
또한 리스코프 치환 원칙을 배울 때에는 이걸 어긴 사례가 별로 없을 것 같다고 생각했었는데, 내 과거 코드를 열어보니 바로 있어서 놀라웠다. 이걸 지금은 손쉽게 고쳤던 것처럼, 나중에 지금의 최종 버전을 손쉽게 고칠 수 있는 날이 왔으면 좋겠다. 공부를 더 해야겠다.
'학교 강의 > Java프로그래밍및실습2' 카테고리의 다른 글
| [디자인 패턴] Observer 패턴 조사 (2) | 2025.10.17 |
|---|---|
| [디자인 패턴] 전략(Strategy) 패턴 실습하기 (0) | 2025.10.15 |
| [SOLID원칙] 문제 만들기 (0) | 2025.10.04 |
| [SOLID 원칙] 커피메이커.ver1 코드 이해하기 - SOLID 원칙 적용 (2) | 2025.10.01 |
| [Thread] 문제 만들기 (0) | 2025.09.30 |
