유희왕이란?

만화 원작 유희왕이 배경
1996년 주간 소년 만화 원작이 되어 흥했지만, 햔제는 ocg (오프라인 카드 게임)이 원작이 되어 현재는 ocg에서 만화,애니 등을 제공하고있습니다.
왜 이런 프로젝트를 잡게 되었는가

평소에도 유희왕 카드게임에 관심이 많았기에 가장 친숙하고 게임 시스템을 이미 이해하고있는 것 위주로 프로젝트를 잡게 되었습니다.
Yu-Gi-Oh! 카드 게임
개발 환경

게임 다운로드 및 실행
최신 버전: v2.0 (2025-11-27 배포)
시스템 요구사항
- OS: Windows 10/11
- 설치 크기: 약 62MB (완전 독립형)
- 추가 설치 요구사항: 없음 (JRE 포함)
특징: Java 설치 불필요 - EXE 파일만 실행하면 설치 마법사가 자동 시작됩니다.
목차
프로젝트 개요
프로젝트 소개
이 프로젝트는 유명한 트레이딩 카드 게임(유희왕)을 Java로 구현한 PC 버전 게임입니다. 단순한 학습 프로젝트를 넘어 실제 배포 가능한 완성 소프트웨어로 제작되었습니다.
개발 목표
- 복잡한 게임 규칙을 정확하게 코드로 구현
- MVC 패턴을 활용한 확장성 높은 아키텍처 설계
- 직관적인 GUI 제공
- AI 플레이어 구현으로 혼자도 게임 가능
- 최종 사용자도 사용 가능한 배포판 생성
프로젝트 규모
- 총 코드 라인: 약 5,000+ 줄
- 클래스 개수: 40+개
- 패키지 구조: 3개 주요 패키지 (game, gui, listener)
- 개발 기간: 약 2개월
- 개발 언어: Java (JDK 25)
- GUI 프레임워크: Java Swing
개발 중 직면한 주요 문제와 해결 전략
Problem #1: 플랫폼 간 경로 호환성 문제
문제 상황
1
2
3
4
에러 메시지:
java.nio.file.NoSuchFileException: resources/Blue-Eyes-White-Dragon.jpg
at java.base/sun.nio.fs.WindowsFileSystemProvider.newFileChannel(...)
at GetCardImage.java:25
증상:
-
Mac/Linux에서 개발한 코드를 Windows에서 실행하면 이미지 로드 실패
-
카드가 회색 박스로만 표시되거나 아예 보이지 않음
해결
- os 경로 문제를 적당히 포기하여 해결.
해결 방법
Step 1: 클래스패스 리소스 사용
Problem #1: String 비교 로직 버그
문제 상황
1
2
3
4
5
6
7
8
증상: 게임이 실행되지만 게임 로직이 작동하지 않음
- 몬스터를 소환하려고 해도 "소환할 수 없습니다" 에러
- 배틀포지션을 변경해도 변경되지 않음
- 페이즈가 정상적으로 진행되지 않음
콘솔 출력:
현재 페이즈: MAIN PHASE 1
페이즈 조건 체크: false (계속 거짓!)
근본 원인 분석
문제가 있던 코드:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Player.java - 초기 버전
public void summonMonster(MonsterCard monsterCard) throws WrongPhaseException {
// 이 조건이 MAIN PHASE 1이 맞는데도 항상 거짓으로 판정됨!
if (field.getPhase() == "MAIN PHASE 1") {
System.out.println("소환 가능");
} else {
throw new WrongPhaseException("현재 페이즈에서는 소환 불가");
}
}
public boolean switchMonsterMode(MonsterCard monsterCard) {
// 여기서도 같은 문제
if (field.getPhase() == "MAIN PHASE 2") {
// 배틀포지션 변경 로직
}
}
왜 이런 일이?
Java의 == 연산자는 메모리 주소를 비교합니다:
1
2
3
4
5
6
7
8
9
10
11
12
13
String phase1 = "MAIN PHASE 1";
String phase2 = "MAIN PHASE 1";
//같은 문자열이지만 다른 메모리 위치
System.out.println(phase1 == phase2);// false
System.out.println(phase1.equals(phase2));// true
//메모리 주소 확인
System.out.println("phase1: " + System.identityHashCode(phase1));
System.out.println("phase2: " + System.identityHashCode(phase2));
해결 방법
모든 String 비교를 .equals()로 변경:
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// 수정된 Player.java
public void summonMonster(MonsterCard monsterCard)
throws WrongPhaseException, MaxFieldSizeException, SummonLimitException {
//올바른 비교 방식
if (!field.getPhase().equals("MAIN PHASE 1") &&
!field.getPhase().equals("MAIN PHASE 2")) {
throw new WrongPhaseException(
"Cannot summon in " + field.getPhase() + " phase");
}
if (field.getMonsters().size() >= 5) {
throw new MaxFieldSizeException("Field is full (max 5 monsters)");
}
if (monsterSummoned) {
throw new SummonLimitException("Already summoned this turn");
}
//안전한 소환
field.setMonster(monsterCard);
hand.removeCardFromHand(monsterCard);
monsterSummoned = true;
System.out.println("Successfully summoned: " + monsterCard.getName());
}
//배틀포지션 변경
public boolean switchMonsterMode(MonsterCard monsterCard)
throws WrongPhaseException, DefenseModeException {
if (!field.getPhase().equals("MAIN PHASE 1") &&
!field.getPhase().equals("MAIN PHASE 2")) {
throw new WrongPhaseException(
"Cannot switch mode in " + field.getPhase());
}
if (monsterCard.getHaveAttacked()) {
throw new DefenseModeException("Cannot switch after attacking");
}
String newMode = monsterCard.getMode().equals("ATTACK")
? "DEFENSE"
: "ATTACK";
monsterCard.setMode(newMode);
System.out.println("Switched to: " + newMode);
return true;
}
//배틀 로직
public void attack(MonsterCard attacker, MonsterCard defender, Player opponent)
throws AlreadyAttackedException, WrongPhaseException, DefenseModeException {
// 다양한 조건 체크 - 모두 .equals() 사용
if (attacker.getHaveAttacked()) {
throw new AlreadyAttackedException(
"Already attacked with this monster this turn");
}
if (!field.getPhase().equals("BATTLE PHASE")) {
throw new WrongPhaseException(
"Can only attack during Battle Phase");
}
if (!attacker.getMode().equals("ATTACK")) {
throw new DefenseModeException(
"Cannot attack with Defense position monster");
}
// 데미지 계산
int damage = calculateDamage(attacker, defender);
opponent.lifepoints -= damage;
attacker.setHaveAttacked(true);
System.out.println("battle" + attacker.getName() +
" attacked for " + damage + " damage!");
}
디버깅 과정 (어떻게 버그를 찾았나?)
처음에는 왜 소환이 안 되는지 몰라서 단계적으로 디버그:
1
2
3
4
5
6
7
8
9
10
11
12
13
// 디버깅을 위해 추가한 코드
System.out.println("--- Debug Info ---");
System.out.println("Phase value: '" + field.getPhase() + "'");
System.out.println("Phase type: " + field.getPhase().getClass());
System.out.println("Is 'MAIN PHASE 1'? " + field.getPhase() == "MAIN PHASE 1");
System.out.println("Using equals(): " + field.getPhase().equals("MAIN PHASE 1"));
// 출력 결과:
// --- Debug Info ---
// Phase value: 'MAIN PHASE 1'
// Phase type: class java.lang.String
// Is 'MAIN PHASE 1'? false ← 이상!
// Using equals(): true ← 이것이 정답!
이 결과를 보고 == vs .equals()의 차이
핵심 교훈
Java String 비교 규칙:
1
2
3
4
5
6
7
8
9
10
11
12
// 1
if (string1 == string2) { }
// 2
if (string1.equals(string2)) { }
// 대소문자 무시 비교
if (string1.equalsIgnoreCase(string2)) { }
// null 안전 비교 (Java 11+)
if (Objects.equals(string1, string2)) { }
1
2
3
4
## Problem #3: 이미지 로드 후 턴 전환 시 카드 미표시
###문제 상황
증상: 턴이 전환될 때 상대 플레이어의 행동이 이미지 없이 표시됨
- 상대가 카드를 소환하지만 회색 박스로만 보임
- “END TURN”을 누르면 이미지가 나타남 (너무 늦음!)
- GUI가 끊김
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
### 원인 분석
**문제가 있던 코드:**
```java
// OpponentPlayerStrategy.java - 초기 버전
public void drawCard() {
MonsterCard newCard = opponent.drawCard();
// GetCardImage 호출이 없음!
// 게임 로직만 처리하고 이미지는 로드하지 않음
gui.addToHand(newCard, gui.getOpponentPlayer());
}
public void summonMonster() {
for (MonsterCard card : opponent.getHand().getCardsInHand()) {
try {
opponent.summonMonster(card);
// 여기도 이미지 로드 없음!
gui.summonMonster(findHandButton(card), gui.getOpponentPlayer());
break;
} catch (Exception e) {
// 에러 처리
}
}
}
public void endTurn() {
opponent.endTurn();
game.switchPlayer();
/ 다음 카드 준비 없음!
}
실행 흐름 분석:
1
2
3
4
5
6
7
8
9
10
11
12
플레이어: "END TURN" 클릭시 상대방
1drawCard() 실행
게임 로직: 덱에서 카드 제거
GUI 업데이트: 손에 카드 추가
이미지 로드: 없음
summonMonster() 실행
게임 로직: 필드에 카드 추가
GUI 업데이트: 필드에 버튼 생성
이미지 로드: 없음
ttack() 실행
게임 로직: 데미지 계산
GUI 업데이트: 라이프 감소
해결 방법
상대 행동 전후로 이미지 로드 추가:
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
public class OpponentPlayerStrategy {
private Game game;
private GUI gui;
public OpponentPlayerStrategy(Game game, GUI gui) {
this.game = game;
this.gui = gui;
}
// 드로우 카드 처리
public void drawCard() {
MonsterCard newCard = opponent.drawCard();
new GetCardImage(newCard, "s");
System.out.println("?? Drew: " + newCard.getName());
// Step 2: GUI에 카드 추가
gui.addToHand(newCard, gui.getOpponentPlayer());
}
// 몬스터 소환 처리
public void summonMonster() {
for (MonsterCard card : opponent.getHand().getCardsInHand()) {
try {
if (card.getImageSmall() == null) {
new GetCardImage(card, "s");
System.out.println("??? Loaded small image: " + card.getName());
}
if (card.getImageLarge() == null) {
new GetCardImage(card, "l");
System.out.println("??? Loaded large image: " + card.getName());
}
opponent.summonMonster(card)
HandButton handBtn = gui.findHandButton(card);
MonsterButton monsterBtn = gui.summonMonster(
handBtn,
gui.getOpponentPlayer()
);
System.out.println("12Summoned: " + card.getName() +
" (ATK: " + card.getAttack() + ")");
break;
} catch (WrongPhaseException e) {
System.out.println(" Wrong phase: " + e.getMessage());
} catch (Exception e) {
System.out.println("Cannot summon: " + e.getMessage());
}
}
}
//공격처리
public void attack() {
ArrayList<MonsterCard> fieldMonsters =
opponent.getField().getMonsters();
if (fieldMonsters.isEmpty()) {
System.out.println("?? No monsters to attack with");
return;
}
for (MonsterCard attacker : fieldMonsters) {
// 이미 공격했는지 확인
if (attacker.getHaveAttacked()) {
continue;
}
try {
if (game.getPlayer().getField().getMonsters().isEmpty()) {
opponent.attackDirectly(attacker, game.getPlayer());
System.out.println("Direct attack! " +
game.getPlayer().lifepoints + " LP remaining");
} else {
MonsterCard defender =
game.getPlayer().getField().getMonsters().get(0);
opponent.attack(attacker, defender, game.getPlayer());
System.out.println("배틀" + attacker.getName() +
" vs " + defender.getName());
}
} catch (AlreadyAttackedException e) {
System.out.println(" Already attacked");
} catch (Exception e) {
System.out.println(" Attack failed: " + e.getMessage());
}
}
}
public void endTurn() { // 턴 종료 처리
System.out.println("Opponent ending turn...");
// Step 1: 다음 턴을 위해 준비
Deck opponentDeck = opponent.getDeck();
if (!opponentDeck.getDeck().isEmpty()) {
MonsterCard nextCard = opponentDeck.getDeck().get(0);
// 미리 다음 드로우 카드의 이미지 로드
new GetCardImage(nextCard, "s");
System.out.println(" Pre-loaded next card: " + nextCard.getName());
}
opponent.endTurn();// 턴 종료 처리
game.switchPlayer();// 턴 넘기니 상대방 측 시작
System.out.println(" Back to player's turn");
}
}
성능 최적화 전략
이미지 로드 타이밍:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//타이밍 분석
long startTime = System.currentTimeMillis();
// 이미지 로드
new GetCardImage(card, "s");
long loadTime = System.currentTimeMillis() - startTime;
System.out.println("이미지 로드 시간: " + loadTime + "ms");
// 타이밍 최적화
public void optimizedCardDisplay(MonsterCard card) {// 메인 스레드에서 빠르게 실행
SwingUtilities.invokeLater(() -> {
// 이미지 로드를 백그라운드에서
new Thread(() -> {
new GetCardImage(card, "s");
// 로드 완료 후 UI 업데이트
SwingUtilities.invokeLater(() -> {
gui.refreshCardDisplay(card);
});
}).start();
});
}
핵심
비동기 처리의 중요성:
- UI 응답성 유지 - 긴 작업을 미리 처리
- 순서 보장 - 이미지 로드 게임 로직00 GUI 업데이트
- 미리 로드 - 다음 단계에서 필요한 리소스 미리 준비
Problem #: GUI 컴포넌트 Z-Order 겹침
문제 상황
1
2
3
4
증상: 게임 실행 후 버튼을 클릭해도 작동하지 않음
- 페이즈 컨트롤 버튼이 보이지 않음
- 정보 패널이 카드 뒤에 숨음
- 마우스 커서가 손 위에서 시계 모양으로 변함 (클릭 불가)

0근본 원인 분석
문제가 있던 코드:
1
2
3
4
5
6
7
8
// GUI.java - 초기 버전
priva0te void setPanels() {
// 순서가 완전히 잘못됨
this.add(activePlayer); // 맨 먼저 추가 = 맨 뒤
this.add(opponentPlayer); // 그 다음 (일부 가려짐)
this.add(infoPanel); // 그 다음 (완전히 가려짐)
this.add(phaseControlPanel); // 마지막 (맨 앞이지만 작아서 숨김)
}
Swing의 z-order 규칙:
1
add() 순서 역순으로 화면 렌더링먼저,add() = 맨 뒤 (배경), 나중 add() = 맨 앞 (클릭 가능)
해결 방법
명시적 z-order 설정:
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// 수정 GUI
private void setPanels() {
getContentPane().removeAll();//기존 컴포넌트 제거
try {//배경 이미지 추가 (맨 뒤)
ImageIcon backgroundIcon = new ImageIcon(
GetCardImage.class.getResource("/com/gui/resources/newBackground.png"));
JLabel background = new JLabel(backgroundIcon);
background.setBounds(0, 0, 1366, 768);
// z-order = 0 (가장 뒤)
getContentPane().add(background, Integer.valueOf(0));
System.out.println(" Background added (z-order: 0)");
} catch (Exception e) {
System.err.println(" Background image not found");
}
// Step 3: 플레이어 필드 추가 (배경 위)
activePlayer.setBounds(0, 450, 1366, 318);
getContentPane().add(activePlayer, Integer.valueOf(1));
System.out.println(" Active player field added (z-order: 1)");
opponentPlayer.setBounds(0, 0, 1366, 320);
getContentPane().add(opponentPlayer, Integer.valueOf(1));
System.out.println(" Opponent player field added (z-order: 1)");
// Step 4: 정보 패널 추가 (플레이어 필드 위)
infoPanel.setBounds(1050, 300, 300, 400);
getContentPane().add(infoPanel, Integer.valueOf(2));
System.out.println(" Info panel added (z-order: 2)");
// Step 5: 페이즈 컨트롤 추가 (정보 패널 위)
phaseControlPanel.setBounds(10, 420, 1346, 30);
getContentPane().add(phaseControlPanel, Integer.valueOf(3));
System.out.println(" Phase control added (z-order: 3)");
try {
ImageIcon logoIcon = new ImageIcon(
GetCardImage.class.getResource("/com/gui/resources/logo.png"));
JLabel logo = new JLabel(logoIcon);
logo.setBounds(1280, 10, 70, 70);
getContentPane().add(logo, Integer.valueOf(4));
System.out.println(" Logo added (z-order: 4)");
} catch (Exception e) {
System.err.println(" Logo image not found");
}
getContentPane().revalidate();
getContentPane().repaint();
System.out.println(" Panel layout refreshed");
}
// z-order 시각화
public void debugZOrder() {
System.out.println("/n Component Z-Order Debug:");
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Component[] components = getContentPane().getComponents();
for (int i = 0; i < components.length; i++) {
System.out.println(String.format(
"%d. %s (클릭: %s)",
i,
components[i].getClass().getSimpleName(),
components[i].isVisible() ? "O" : "X"
));
}
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━/n");
}
Z-Order 다이어그램:
1
2
3
4
5
6
7
8
화면
z-order 3: 페이즈 컨트롤 (버튼, 클릭 가능)
z-order 2: 정보 패널 (카드 정보, 클릭 가능)
라이프포인트, 페이즈 정보
z-order 1: 플레이어 필드 (게임 필드, 클릭 가능)
상대 필드 (상단)
내 필드 (하단)
z-order 0: 배경 (무시, 클릭 불가)이미지: Blue-Eyes Dragon
핵심
Swing 컴포넌트 배치 규칙:
- 명시적 z-order 사용 -
add(component, Integer.valueOf(n)) - 절대 위치 설정 -
setBounds(x, y, width, height) - 항상 revalidate() + repaint() - 변경사항 반영
- 고려해야 할 사항:
- 클릭 가능 영역 (버튼)
- 시각적 계층 (배경, 카드, UI)
- 마우스 이벤트 전파
게임 시스템 설명
턴 구조 및 페이즈
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
플레이어 1 턴 시작
│
├─ Draw Phase (자동)
│ └─ 덱에서 카드 1장 드로우
│
├─ Main Phase 1 (플레이어 조작)
│ ├─ ? 몬스터 소환 (최대 1마리)
│ ├─ ? 배틀포지션 변경
│ └─ ? 마법/함정 활성화 (미구현)
│
├─ Battle Phase (플레이어 조작)
│ ├─ ? 몬스터 공격 (카드당 1회)
│ ├─ ? 직접 공격
│ └─ ? 공격 선택 취소
│
├─ Main Phase 2 (플레이어 조작)
│ ├─ ? 추가 몬스터 소환 불가
│ ├─ ? 배틀포지션 변경 불가
│ └─ ? 마법/함정 활성화 (미구현)
│
└─ End Phase
├─ 턴 종료 로직 실행
│ ├─ monsterSummoned = false (초기화)
│ └─ 모든 몬스터 haveAttacked = false (초기화)
├─ 플레이어 전환
└─ 상대방 턴 시작
플레이어 2 턴 시작 (AI 자동 실행)
│
├─ Draw Phase
├─ Main Phase 1
├─ Battle Phase
├─ Main Phase 2
└─ End Phase (자동 턴 종료)
플레이어 1 턴 재개...
배틀 시스템
데미지 계산 규칙
Case 1: ATTACK vs ATTACK (공격 vs 공격)
1
2
3
4
5
6
7
8
9
10
11
12
13
// 밑 케이스 3까지 예시입니다.
int attackerPower = 2500;
int defenderPower = 1500;
if (attackerPower > defenderPower) {
// 방어 몬스터 파괴
defender 제거
// 데미지 = 공격력 차이
opponentLifePoint -= (2500 - 1500) = 1000
결과: 상대 라이프 1000 감소
}
Case 2: ATTACK vs DEFENSE (공격 vs 방어)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int attackerPower = 2500;
int defenderDefense = 3000;
if (attackerPower > defenderDefense) {
// 방어 몬스터 파괴 (데미지 없음)
defender 제거
opponentLifePoint -= 0
결과: 방어 카드만 파괴
} else {
// 방어 성공 (아무 일 없음)
defender 유지
opponentLifePoint -= 0
결과: 변화 없음
}
Case 3: Direct Attack (직접 공격)
1
2
3
4
5
// 상대 필드에 몬스터가 없을 때
int damage = attacker.getAttack();
opponentLifePoint -= damage;
결과: 직접 데미지 (모두 적용)
기술 아키텍처
MVC 패턴 구현
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
┌─────────────────────────────────────────────────────────┐
│ MVC Architecture │
├─────────────────────────────────────────────────────────┤
│ │
│ Model Layer (게임 로직) │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Game.java (게임 관리) │ │
│ │ ├─ Player (플레이어 1, 2) │ │
│ │ │ ├─ Hand (손) │ │
│ │ │ ├─ Deck (덱) │ │
│ │ │ ├─ Field (필드) │ │
│ │ │ │ ├─ MonsterZone │ │
│ │ │ │ ├─ SpellZone │ │
│ │ │ │ └─ Graveyard │ │
│ │ │ └─ lifepoints (라이프포인트) │ │
│ │ │ │ │
│ │ └─ Card (카드 기본 클래스) │ │
│ │ ├─ MonsterCard (몬스터) │ │
│ │ └─ SpellCard (마법/함정) │ │
│ └───────────────────────────────────────────────────┘ │
│ ↑ ↓ │
│ setters/getters 읽기 위주 접근 │
│ │
│ View Layer (사용자 인터페이스) │
│ ┌───────────────────────────────────────────────────┐ │
│ │ GUI.java (메인 프레임) │ │
│ │ ├─ PlayerPanel (플레이어 화면) │ │
│ │ │ ├─ FieldPanel (필드 표시) │ │
│ │ │ └─ HandPanel (손 표시) │ │
│ │ ├─ InfoPanel (정보 표시) │ │
│ │ └─ PhaseControlPanel (페이즈 버튼) │ │
│ └───────────────────────────────────────────────────┘ │
│ ↑ ↓ │
│ 클릭 이벤트 화면 갱신 │
│ │
│ Controller Layer (이벤트 처리) │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Listeners (마우스 이벤트 처리) │ │
│ │ ├─ SelectHandCardListener │ │
│ │ ├─ SummonMonsterListener │ │
│ │ ├─ AttackListener │ │
│ │ ├─ StartAttackListener │ │
│ │ ├─ DefenseModeListener │ │
│ │ ├─ PhaseListener │ │
│ │ ├─ EndTurnListener │ │
│ │ └─ OpponentStrategyListener (AI) │ │
│ └───────────────────────────────────────────────────┘ │
│ ↓ ↑ │
│ 사용자 행동 게임 상태 변경 │
│ │
└─────────────────────────────────────────────────────────┘
패턴 이점
| 이점 | 설명 |
|---|---|
| 독립성 | 게임 로직 변경 시 GUI 수정 불필요 |
| 테스트 용이성 | 각 계층을 독립적으로 테스트 가능 |
| 재사용성 | 다른 UI (CLI, 웹)로도 확장 가능 |
| 유지보수성 | 각 계층의 책임이 명확함 |
핵심 기능 구현
카드 소환 시스템
1
2
3
4
5
6
7
8
9
10
11
12
13
// 완전한 소환 프로세스
public void summonMonster(MonsterCard card) {
// 1단계: 조건 검증
validateSummonConditions(card);
// 2단계: 게임 상태 변경
field.setMonster(card);
hand.removeCardFromHand(card);
monsterSummoned = true;
// 3단계: UI 업데이트
gui.visualizeSummon(card);
}
배틀 시스템
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 복잡한 데미지 계산
public void attack(MonsterCard attacker, MonsterCard defender, Player opponent) {
// 1단계: 유효성 검사
validateAttack(attacker);
// 2단계: 데미지 계산
int damage = calculateDamage(attacker, defender);
// 3단계: 상태 변경
applyDamage(opponent, damage);
handleCardDestruction(attacker, defender);
// 4단계: UI 업데이트
gui.visualizeBattle(attacker, defender, damage);
}
성능 최적화
이미지 캐싱
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 한 번 로드한 이미지는 재사용
private static Map<String, ImageIcon> imageCache = new HashMap<>();
public GetCardImage(Card card, String size) {
String key = card.getName() + "_" + size;
if (imageCache.containsKey(key)) {
this.image = imageCache.get(key);
return; // 빠른 반환
}
// 첫 로드 시만 시간 소요
BufferedImage loaded = loadImage(card, size);
this.image = new ImageIcon(loaded);
imageCache.put(key, this.image);
}
메모리 최적화
- 불필요한 객체 즉시 GC 대상 처리
- ArrayList -> LinkedList 선택 (성능 고려)
배포 방식
EXE 설치 프로그램 생성

장점:
- Java 설치 불필요
- 설치 마법사 제공
- 바탕화면 바로가기 생성
- 시작메뉴 등록
- 단일 파일 배포
결론 및 학습 성과
이 프로젝트를 통해 얻은 실무 경험:
개발자로서의 성장
- 문제 해결 능력: 버그를 체계적으로 분석하고 해결
- 아키텍처 설계: MVC 패턴의 실제 적용
- 사용자 경험: GUI 반응성과 예외 처리의 중요성
- 배포: 실제 사용자가 사용 가능한 수준의 완성도
포트폴리오 가치
실제 배포를 해본 프로젝트로 어느정도는 가치가 있는 프로젝트라고 생각합니다. —
마무리
감사합니다.