[튜터님 피드백]
[잘 작성된 부분]
- 필드를 private으로 두고 getter, setter를 통해 접근하게 만든 방향은 캡슐화 관점에서 기본 틀이 잡혀 있습니다.
- Champion을 추상 클래스로 두고 useQ를 추상 메서드로 강제해서 하위 클래스가 최소 기능을 구현하도록 구성했습니다.
- Ash가 랜덤 요소와 추가 패시브를 자기 클래스 내부로 묶어두어, 챔피언별 고유 로직이 하위 클래스에 모이도록 구성했습니다.
[개선이 필요한 부분]
- Champion의 책임이 과도하게 넓습니다. 전투 로그, 전투 횟수, 레벨업 조건, 부활 횟수, 데미지 계산, 출력까지 한 클래스에 묶여 있어 변경이 생길 때 영향 범위가 커집니다. 전투 규칙과 상태 효과, 로그 저장은 별도 책임으로 분리하는 편이 구조가 안정됩니다.
- battleLogs가 public static으로 열려 있고, ArrayList를 그대로 노출하고 있습니다. 외부에서 임의로 수정할 수 있어 캡슐화가 깨집니다. private static으로 감추고 조회는 복사본이나 읽기 전용 컬렉션으로 제공하는 편이 안전합니다.
- setter 네이밍과 동작이 일관되지 않습니다. setLevel은 값을 대입하지 않고 더하고 있고, setWarCount도 누적 방식입니다. 호출부에서 오해하기 쉬워 객체 상태가 예측 불가능해질 수 있습니다. 증가 목적이면 increaseLevel, addWarCount 같이 의도를 드러내는 이름으로 분리하는 편이 좋습니다.
- checkHp 메서드의 책임과 위치가 어색합니다. 피격자가 자신의 hp를 깎는 로직이면서, 공격자의 공격력까지 참조하고 출력까지 담당합니다. 공격 처리와 방어 처리의 경계가 흐려져 결합도가 올라갑니다. 피해 계산은 한쪽 책임으로 모으고, 외부에서는 결과만 사용하게 만드는 편이 좋습니다.
- 메서드 파라미터로 oneSelf를 넘기는 형태가 반복됩니다. this로 충분한 상황에서 자기 자신을 인자로 다시 넘기면 호출부가 복잡해지고 설계 의도가 흐려집니다.
- 출력이 로직과 강하게 결합돼 있습니다. 메서드가 상태를 바꾸면서 동시에 출력까지 수행하면 테스트가 어렵고, UI 변경이 도메인 변경으로 이어질 수 있습니다. 출력은 상위 계층에서 처리하고, 도메인은 상태 변화만 담당하도록 분리하는 편이 좋습니다.
- 필드들이 외부에서 자유롭게 수정 가능합니다. setHp, setName처럼 전체 변경이 열려 있으면 전투 규칙이 클래스 외부에서 깨질 수 있습니다. 필요한 변경만 허용하고, 중요한 상태는 보호하는 방향이 더 안정적입니다.
- Ash.useQ가 한 메서드 안에서 확률 처리, 피해 처리, 로그, 레벨업, 패시브 적용까지 모두 수행하고 있습니다. 스킬 자체 로직과 성장, 패시브 트리거가 한 곳에 섞여 있어 응집도가 떨어집니다. 스킬 처리와 성장 처리, 패시브 처리를 분리하면 유지보수가 쉬워집니다.
- Champion 내부 필드와 메서드 이름 오타와 일관성 문제가 있습니다. attakDamage 같은 오타, IncreaseAttackDamageFix 같은 혼합 규칙은 유지보수 시 실수를 유발하기 쉽습니다. 이름 규칙을 통일하는 편이 좋습니다.
1. 과제 목표
- 객체지향 OOP 설계를 실전처럼 통합하여 구현
- 클래스/상속/추상화/다형성의 실사용 경험
- static, final, 상수 설계 활용
- 컬렉션과 제네릭 기반의 챔피언 관리
- Optional & 예외를 사용한 “안전한 전투 로직” 구현
2. 필수 구현 요구사항
아래 항목을 모두 포함해야 합니다.
✔ ① Champion 추상 클래스 완성
- 공통 필드: name, level, hp, attackDamage, defense
- 공통 메서드: basicAttack(), takeDamage(), levelUp()
- 추상 메서드: useQ()
✔ ② 챔피언 2명 이상 구현
예: Garen, Ashe, Lux …
- 각 클래스는 useQ() 내용을 다르게 정의
- 원하는 만큼 스킬 데미지/특수효과 추가 가능
✔ ③ static / final 활용
- Champion 생성 시 createdCount 증가
- GameConstants 클래스에 상수 포함
- (MAX_LEVEL, BASE_CDR 등 자유롭게)
✔ ④ ChampionPool(Map) 구현
- 이름으로 챔피언 조회 가능
- Optional<Champion> 기반 안전 조회
✔ ⑤ SafeBattle(전투 모듈) 구현
- 공격/스킬 턴 진행
- 사망자 행동 시 커스텀 예외 처리
- (DeadChampionActionException 등)
✔ ⑥ 최종 "1:1 결투 모드" 실행 클래스
필수 조건:
- 챔피언 Pool 생성
- 사용자 입력 또는 랜덤 선택으로 2명 선택
- 안전 전투(SafeBattle) 진행
- 승패 또는 전투 종료 메시지 출력
- 모든 출력은 콘솔 기반이면 충분함
3. 선택 구현
아래 중 1개 이상 구현하면 보너스 점수 부여.
- 마나(Mana) 시스템 + 마나 부족 예외
- Team<T> 기반 5:5 팀 전투 지원
- Q/W/E/R 모든 스킬 구현
- 패시브(Passive) 능력 구현
- 치명타 확률 / 방어무시 / 회복 등 전투 효과 구현[구현완료]
- Stream을 활용한 전투 로그 요약 출력 [구현완료]
- Enum으로 포지션/챔피언 타입 설계
선택구현은 직전 과제의 중복과 스케줄 부족으로 좀더 학습이 필요한 선택2개 구현 완료 하였습니다.
Main
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
public class Main {
public static void main(String[] args) {
//1.Champion 추상 클래스 완성
//2.챔피언 2명 이상 구현
Champion garen1 = new Garen("가렌1",1,500,50,50);
Champion garen2 = new Garen("가렌2",1,500,50,50);
Champion ash1 = new Ash("에쉬1",1,100,50,50);
Champion ash2 = new Ash("에쉬2",1,100,50,50);
Champion ash3 = new Ash("에쉬3",1,100,50,50);
int count=Champion.getCreatedCount();
System.out.println("생성된 챔피언 수 : "+count);
System.out.println("");
//3.static / final 활용 Champion 생성 시 createdCount 증가 GameConstants 클래스에 상수 포함
//5.SafeBattle(전투 모듈) 구현 공격/스킬 턴 진행 사망자 행동 시 커스텀 예외 처리
try{
for(int j=0;j<6;j++){//라운드제 총 6라운드
garen1.useQ(ash1);
ash1.useQ(garen1);
}
}catch (DeadChampionActionException e) {
// 내가 만든 커스텀 예외 메시지를 여기서 캐치
System.out.println("[전투 중단] " + e.getMessage()+"\n");//버퍼가 별도로 존재한다. 그래서 출력되는 순서가 다를수있다.
} catch (Exception e) {
// 혹시 모를 다른 에러 처리
System.err.println("예상치 못한 오류 발생!");//버퍼가 별도로 존재한다. 그래서 출력되는 순서가 다를수있다.
//[문제]err 출력과 out 출력시 오류문구 출력 위치가 다르다
//[해결]버퍼가 별도로 존재한다. 그래서 출력되는 순서가 다를수있다.
}
//4.ChampionPool(Map) 구현 이름으로 챔피언 조회 가능 Optional<Champion> 기반 안전 조회 //널일수 있기 때문에
ChampionPool pool = new ChampionPool();
pool.addChampion(garen1.getName(),garen1);
pool.addChampion(ash1.getName(),ash1);
pool.addChampion(ash2.getName(),ash2);
pool.addChampion(ash3.getName(),ash3);
pool.addChampion(garen2.getName(),garen2);
pool.getChampion("가렌1").ifPresent(c -> {
System.out.println(c.getName() + " 찾았습니다.\n");
});
pool.getChampion("가렌3").ifPresentOrElse(
c -> System.out.println(c.getName() + " 찾았습니다.\n"), // 찾았을 때
() -> System.out.println("해당 챔피언을 찾지 못했습니다.\n") // 못 찾았을 때
);
//랜덤 선택으로 챔피언 2명 선택
Random rand = new Random();
List<Champion> championList = new ArrayList<>(pool.getAllChampions());//pool에 담긴 챔피언을 리턴받아 배열에 저장
Champion champ1;//랜덤 선정 챔피언
Champion champ2;//랜덤 선정 챔피언
if(championList.size()>=2){//2명 이상의 챔피언이 있는지 확인
int index1 = rand.nextInt(championList.size());
int index2 = rand.nextInt(championList.size());
champ1 = championList.get(index1);
champ2 = championList.get(index2);
while(champ1.equals(champ2)){//같은 챔피언이면 다시 뽑기
index1 = rand.nextInt(championList.size());
index2 = rand.nextInt(championList.size());
champ1 = championList.get(index1);
champ2 = championList.get(index2);
if(champ1.equals(champ2)){
continue;
}else{
System.out.println(champ1.getName()+" vs "+champ2.getName());
break;
}
}
}else{
champ1 = null;
champ2 = null;
System.out.println("현재 챔피언이 2명이상 없습니다.");
}
//안전 전투 진행(라운드제, 사망시 전투불능 출력)
String loser="모두 생존 무승부";
try{
for(int j=0;j<3;j++){//라운드제 총 6라운드
champ1.useQ(champ2);
champ2.useQ(champ1);
//[문제]variable champ1 might not have been initialized 초기화 되지 않아 오류가 발생
//[해결]랜덤으로 선정된 챔피언 2명의 객체를 null로 초기화하여 오류를 해결
}
}catch (DeadChampionActionException e) {
// 내가 만든 커스텀 예외 메시지를 여기서 캐치
loser = e.getDeadChampionName();
System.out.println("[전투 중단] " + e.getMessage()+"\n");//버퍼가 별도로 존재한다. 그래서 출력되는 순서가 다를수있다.
} catch (Exception e) {
// 혹시 모를 다른 에러 처리
System.err.println("예상치 못한 오류 발생!");//버퍼가 별도로 존재한다. 그래서 출력되는 순서가 다를수있다.
}
//승패 출력후 전투
System.out.println("패자 : "+loser);
//[문제] loser 값을 초기화 하지 않아 오류가 발생
//[해결] 아무도 죽지않고 라운드가 종료된다면 loser가 발생하지 않기 때문에, 상단에 변수 초기화 진행
//Stream을 활용한 전투 로그 요약 출력
System.out.println("--- 전체 전투 로그 요약 ---");
Champion.battleLogs.stream()
.forEach(log->System.out.println(log));
}
}


GameConstants
public class GameConstants {
//속성
private static final int MAX_LEVEL = 18;
private static final int MIN_HP = 50;
//생성자
//기능
public static int getMaxLevel() {
return MAX_LEVEL;
}
public static int getMinHP() {
return MIN_HP;
}
}
Champion
import java.util.ArrayList;
import java.util.List;
public abstract class Champion {
//속성
private String name = "";
private int level = 0;
private int hp = 0;
private int attakDamage = 0;
private int defense = 0;
private static int createdCount = 0;//챔피언 생성수 저장
private int warCount = 0;
private int previousLevel = 0;
private int resurrectionCount = 2;
public static List<String> battleLogs = new ArrayList<>();//로그 저장
//생성자
public Champion(String name, int level, int hp, int attakDamage, int defense) {
this.name = name;
this.level = level;
this.hp = hp;
this.attakDamage = attakDamage;
this.defense = defense;
this.createdCount++;//챔피언 생성시 마다 ++
}
//기능
//필드 접근 게터 세터
public void addBattleLog(String log) {
battleLogs.add(log);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getLevel() {
return level;
}
public void setLevel(int level) {
this.level += level;
}
public int getHp() {
return hp;
}
public void setHp(int hp) {
this.hp = hp;
}
public void setIncreaseHp(int hp) {
this.hp += hp;
}
public int getAttakDamage() {
return attakDamage;
}
public void setAttakDamage(int attakDamage) {
this.attakDamage = attakDamage;
}
public int IncreaseAttackDamageFix(int IncreaseAttackDamage) {
return attakDamage+=IncreaseAttackDamage;
}
public int getDefense() {
return defense;
}
public void setDefense(int defense) {
this.defense = defense;
}
public static int getCreatedCount() {
return createdCount;
}
public int getWarCount() {
return warCount;
}
public void setWarCount(int warCount) {
this.warCount += warCount;
}
public int getPreviousLevel() {
return previousLevel;
}
public void setPreviousLevel(int previousLevel){
this.previousLevel = previousLevel;
}
public int getResurrectionCount() {
return resurrectionCount;
}
public void setResurrectionCount(int resurrectionCount) {
this.resurrectionCount -= resurrectionCount;
}
public void basicAttack(){
}
public void takeDamage(){
}
public void levelUp(){
}
//기능
public abstract void useQ(Champion target);
public void checkHp(Champion opponent, int attackDamege,int status){
System.out.println(opponent.getName()+"이+(가) "+this.getName()+"을(를) 공격합니다.");
int finalBonusDamage = 0;
if(status == 0){//방어구 무시 모드
finalBonusDamage = attackDamege;
if(finalBonusDamage < 0){
finalBonusDamage = 0;
}
System.out.println(this.getName()+"이(가) "+(opponent.getAttakDamage()+finalBonusDamage)+"피해를 입었습니다.");
}else if(status == 1){//치명타 모드
finalBonusDamage = attackDamege-this.getDefense();
if(finalBonusDamage < 0){
finalBonusDamage = 0;
}
System.out.println(this.getName()+"이(가) "+(opponent.getAttakDamage()+finalBonusDamage)+"피해를 입었습니다.");
}
this.setHp(this.getHp()-(opponent.getAttakDamage()+finalBonusDamage));
if(this.getHp()<=0){
this.setHp(0);
}
System.out.println(this.getName()+"의 체력이"+this.getHp()+"남았습니다.");
System.out.println("");
}
public void checkLevel(Champion oneSelf){
if(this.getWarCount()>2){//2회 이상 스킬 사용시 매스킬시 레벨업
if(this.getLevel()<=GameConstants.getMaxLevel()){//멕스레벨 넘지않게
System.out.println(oneSelf.getName()+"의 레벨업 되었습니다. 레벨 : "+this.getLevel());
this.setLevel(1);
}else{
System.out.println(oneSelf.getName()+"은(는) 만렙입니다.");
}
}else{
System.out.println(oneSelf.getName()+"의 전투횟수 부족으로 레벨업이 불가합니다.");
}
System.out.println("");
}
}
DeadChampionActionException
public class DeadChampionActionException extends RuntimeException {
//속성
private String deadChampionName;
//생성사
public DeadChampionActionException(String message,String deadChampionName) {
super(message);
this.deadChampionName = deadChampionName;
}
public String getDeadChampionName() {
return deadChampionName;
}
}
Ash
import java.util.Random;
public class Ash extends Champion{
//속성
//상속받은 부모클래스 그대로 사용
Random rand = new Random();
//생성자
public Ash(String name, int level,int hp,int attackDamage,int defense) {
super(name, level, hp, attackDamage, defense);//초기 셋팅
}
//기능
@Override
public void useQ(Champion target){
if(this.getHp()<=0){//체력이 없다면
//메인으로 오류를 전달해라
throw new DeadChampionActionException(this.getName()+"이(가) 죽어서 공격할수 없습니다.",this.getName());
}
if(rand.nextBoolean()){//50% 확률 치명타 발생시키기
int ignoringDamage = rand.nextInt(1,100);//1~100사이 랜덤한 숫자 발생
target.checkHp(this, ignoringDamage,0);//추가 공격력 치명타 발생!
System.out.println(this.getName()+"이+(가) "+target.getName()+"에게 방어구 무시를 발동시켜 추가 데미지를 입힙니다.");
}else{
target.checkHp(this, 100,0);
}
System.out.println(this.getName()+"이+(가) "+target.getName()+"에게 하늘의 화살 Q스킬을 시전합니다.");
this.addBattleLog(this.getName()+"이+(가) "+target.getName()+"에게 하늘의 화살 Q스킬을 시전합니다.");
target.checkHp(this,120,0);//적의 체력을 확인해라
this.setWarCount(1);//스킬 사용시 전투 카운트 1증가
this.checkLevel(this);//나의 레벨 체크
this.IncreaseAttackDamage(this);//일정 조건이 맞다면 추가 패시브 적용 메서드
}
public void IncreaseAttackDamage(Champion oneSelf){
if(this.getLevel()>4){//5레벨 이상부터 1레벨씩 체력이 100씩 증가
if(oneSelf.getLevel()>oneSelf.getPreviousLevel()){//이전 레벨보다 증가 했는지?
System.out.println(oneSelf.getName()+"은(는) 레벨업 효과를 받습니다.(공격력 증가)");
this.IncreaseAttackDamageFix(50);//공격력 증가
System.out.println(oneSelf.getName()+"은(는) 공격력이 "+this.getAttakDamage()+" 되었습니다.");
this.setPreviousLevel(oneSelf.getLevel());//적용이 되었기때문에, 현재 레벨을 이전레벨로 다시 셋팅
}else{
System.out.println(oneSelf.getName()+"은(는) 1레벨업 해야 공격력 증가 효과를 받습니다.");
}
}else{
System.out.println(oneSelf.getName()+"은(는) 아직 레벨업 효과를 적용하기엔 레벨이 부족합니다.");
}
System.out.println("");
}
}
Garen
import java.util.Random;
public class Garen extends Champion {
//속성
//상속받은 부모클래스 그대로 사용
Random rand = new Random();
//생성자
public Garen(String name, int level,int hp,int attackDamage,int defense) {
super(name, level, hp, attackDamage, defense);//초기 셋팅
}
//기능
@Override
public void useQ(Champion target){
if(this.getHp()<=0){
if(this.getResurrectionCount()>0){//부활 횟수 체크
this.setHp(100);//100만 회복
this.setResurrectionCount(1);
System.out.println(this.getName()+"이+(가) 부활하였습니다. 남은횟수 : "+this.getResurrectionCount());
}else{
throw new DeadChampionActionException(this.getName()+"이(가) 죽어서 공격할수 없습니다.\n",this.getName());
}
}
//[문제] throw 작성시 오류 발생
//[해결]DeadChampionActionException클래스에 extends Exception을 해서 문제가 발생 extends RuntimeException 오류해결
//[이유]Exception은 Checked Exception이라 대책이 없다면 무조건 오류 RuntimeException은 Unchecked Exception 컴파일러가 미리 체크하지 않기때문에 오류가 발생하지 않음
System.out.println(this.getName()+"이+(가) "+target.getName()+"에게 화려한 칼부림 Q스킬을 시전합니다.");
this.addBattleLog(this.getName()+"이+(가) "+target.getName()+"에게 화려한 칼부림 Q스킬을 시전합니다.");
if(rand.nextBoolean()){//50% 확률 치명타 발생시키기
int ciriticalDamage = rand.nextInt(1,100);//1~100사이 랜덤한 숫자 발생
target.checkHp(this, ciriticalDamage,1);//추가 공격력 치명타 발생!
System.out.println(this.getName()+"이+(가) "+target.getName()+"에게 치명타를 발동시켜 추가 데미지를 입힙니다.");
}else{
target.checkHp(this, 100,1);
}
this.setWarCount(1);
this.checkLevel(this);
this.IncreaseHp(this);
}
public void IncreaseHp(Champion oneSelf){
if(this.getLevel()>3){//5레벨 이상부터 1레벨씩 체력이 100씩 증가
if(oneSelf.getLevel()>oneSelf.getPreviousLevel()){
System.out.println(oneSelf.getName()+"은(는) 레벨업 효과를 받습니다.(체력 증가)");
this.setIncreaseHp(100);
System.out.println(oneSelf.getName()+"은(는) HP가 "+this.getHp()+" 되었습니다.");
this.setPreviousLevel(oneSelf.getLevel());
}else{
System.out.println(oneSelf.getName()+"은(는) 1레벨업 해야 체력 증가 효과를 받습니다.");
}
}else{
System.out.println(oneSelf.getName()+"은(는) 아직 레벨업 효과를 적용하기엔 레벨이 부족합니다.");
}
System.out.println("");
}
}
ChampionPool
import java.util.*;
public class ChampionPool {
//속성
//생성자
public ChampionPool() {
}
//기능
private Map<String, Champion> pool = new HashMap<String, Champion>();//사용 선언
public Optional<Champion> getChampion(String championName) {
return Optional.ofNullable(pool.get(championName));//pool.get(championName) 널일수 있기 때문에 옵셔널에 담아서 리턴
}
public void addChampion(String name,Champion champion) {
// 챔피언의 이름을 키(Key)로 해서 지도(Map)에 저장
pool.put(name, champion);
}
public List<Champion> getAllChampions() {
return new ArrayList<>(pool.values());//pool에 담긴 챔피언들을 리턴
}
}
[느낀점]
기능적 구현은 완료하였으나,
JAVA의 객체지향의 온전한 독립과 유지보수 가독성에서는 많이 부족하다.
앞으로 좀더 기능별로 분리를 통해, 가독성과 유지보수성을 높여야 겠다고 느꼈다.
그래서, 이부분의 부족으로 후반부 기능 구현시 개발하는 나조차도 코드의 가독성이 떨어지며, 오류의 발견도 늦어졌다.
앞으로 이부분을 명심하고 성장하는 발판으로 새겨야겠다.
'spring_2기[본캠프] > 과제' 카테고리의 다른 글
| [과제]일정관리앱 2 트러블 슈팅 (0) | 2026.01.09 |
|---|---|
| [과제]일정 관리 앱 만들기 (0) | 2026.01.02 |
| [라이브 코딩테스트] (0) | 2025.12.23 |
| [과제] 커머스3 (필수기능 완료 + 도전레벨3) (0) | 2025.12.22 |
| [과제] 커머스2-2 (필수기능 완료 + 도전레벨2) (0) | 2025.12.19 |