Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions TIL/sample/2026-06-16-상속.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
# 2026-06-16 자바 학습 정리 : 상속

## 오늘 배운 내용

- **상속(inheritance)**
- **오버라이드(override)**

## 기억할 것

- java의 상속은 `extends` 키워드를 사용한다.
- JUnit의 `@DisplayName`은 테스트 결과 창에서 사람이 읽기 쉬운 이름을 표시하기 위한 어노테이션이다.
- 메서드 오버라이딩에서 java는 기본이 가상 메서드 이고 c++ `Virtual` 키워드를 붙여야한다.

### 코드 예시 ( java )

```java
// 부모 클래스
class Wizard {
public void heal() {
System.out.println("일반 힐!");
}
}

// 자식 클래스
class GreatWizard extends Wizard {
@Override // 오버라이드 명시 (생략 가능하지만 가독성을 위해 필수)
public void heal() {
System.out.println("대마법사의 슈퍼 힐!");
}
}

public class Main {
public static void main(String[] args) {
// [핵심 상황] 부모 타입 변수에 자식 객체를 대입!
Wizard wizard = new GreatWizard();

wizard.heal();
// 출력 결과: "대마법사의 슈퍼 힐!" (알아서 알맹이를 찾아감)
}
}
```

## C++

### 가상메서드 X

```cpp
#include <iostream>

class Wizard {
public:
// ★ virtual이 없습니다!
void heal() {
std::cout << "일반 힐!\n";
}
};

class GreatWizard : public Wizard {
public:
void heal() {
std::cout << "대마법사의 슈퍼 힐!\n";
}
};

int main() {
// 부모 그릇에 자식 알맹이를 넣음
Wizard* wizard = new GreatWizard();

// [결과] C++은 포인터가 Wizard니까 Wizard의 함수를 실행해버림
wizard->heal(); // 출력: 일반 힐! (자식의 슈퍼 힐이 묻힘)

return 0;
}
```

### 가상메서드 O

```cpp
#include <iostream>

class Wizard {
public:
// ★ virtual을 붙여서 자식이 오버라이딩할 길을 열어줍니다!
virtual void heal() {
std::cout << "일반 힐!\n";
}
};

class GreatWizard : public Wizard {
public:
// 부모가 virtual이면 자식은 자동으로 virtual이 됩니다.
void heal() override {
std::cout << "대마법사의 슈퍼 힐!\n";
}
};

int main() {
// 부모 그릇에 자식 알맹이를 넣음
Wizard* wizard = new GreatWizard();

// [결과] 부모에 virtual이 있으므로 진짜 알맹이인 GreatWizard의 함수를 실행함
wizard->heal(); // 출력: 대마법사의 슈퍼 힐! (정상 작동)

return 0;
}
```

## 실습 코드

```java
package com.survivalcoding;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

class GameTest {

private Hero targetHero;
private Wizard wizard;
private GreatWizard greatWizard;

@BeforeEach
void setUp() {
targetHero = new Hero("전사", 50, 100);
wizard = new Wizard("일반법사", 50, 50, 30, 50);
greatWizard = new GreatWizard("대법사", 60, 60, 60, 100);
}

@Test
@DisplayName("일반 마법사가 힐을 하면 대상의 HP가 20 오르고 마나가 10 깎여야 한다")
void wizardHealTest() {
wizard.heal(targetHero);

assertEquals(70, targetHero.getHp(), "전사의 HP는 50에서 20이 더해져 70이 되어야 합니다.");
}

@Test
@DisplayName("대마법사가 슈퍼힐을 쓰면 대상의 HP가 최대치로 차고 마나가 50 깎여야 한다")
void greatWizardSuperHealTest() {
greatWizard.SuperHeal(targetHero);

assertEquals(100, targetHero.getHp(), "슈퍼 힐을 받으면 최대 체력인 100이 되어야 합니다.");
}

@Test
@DisplayName("마나가 부족하면 힐이 나가지 않고 체력도 그대로여야 한다")
void outOfManaTest() {
Wizard oomWizard = new Wizard("마나없는법사", 50, 50, 5, 50);

oomWizard.heal(targetHero);

assertEquals(50, targetHero.getHp(), "마나가 부족하므로 타겟의 HP는 변하면 안 됩니다.");
}

@Test
@DisplayName("체력을 치유했을 때 최대 체력(MaxHP)을 초과할 수 없다")
void hpNotOverMaxHpTest() {
Hero highHpHero = new Hero("피통전사", 90, 100);

wizard.heal(highHpHero);

assertEquals(100, highHpHero.getHp(), "치유량의 총합이 MaxHP를 넘어가면 MaxHP로 고정되어야 합니다.");
}
}
```

## 어려웠던 점

- 오버라이딩 인식 차이: Java에서는 부모 타입 변수에 자식 객체를 담아도 알아서 자식 메서드가 호출되지만, C++에서는 부모 포인터 타입을 기준으로 함수를 실행해 버려 자식 메서드가 묻히는 정적 바인딩 현상을
직접 코드로 비교하며 개념을 정립하는 과정이 까다로웠다.
- JUnit 테스트 작성 중 `@DisplayName`이 적용되지 않는 것처럼 보여 원인을 찾는 데 시간이 걸렸다.

## 해결 방법

- C++의 가상 함수 테이블(vtable) 구조 이해: C++ 부모 클래스에 virtual 키워드를 붙이면 컴퓨터가 내부적으로 '진짜 알맹이 객체'를 찾아갈 수 있도록 이정표(가상 함수 테이블)를 세워 동적
바인딩을 유도한다는 원리를 깨달으며 두 언어의 차이를 완벽히 이해했다.
- IntelliJ가 테스트를 Gradle Runner로 실행하고 있어 @DisplayName이 보이지 않았음을 확인했다.
- IntelliJ 설정에서

```
설정
└─ 빌드, 실행, 배포
└─ 빌드 도구
└─ Gradle
└─ 다음을 사용하여 테스트 실행
Gradle → IntelliJ IDEA
```
Comment on lines +183 to +190

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

코드 블록 언어를 지정해 주세요.

183행의 fenced code block에 언어 태그가 없어 markdownlint 경고가 납니다. text 같은 언어를 붙이거나, 코드 펜스를 제거하면 됩니다.

🛠️ 제안 수정
-```
+```text
 설정
 └─ 빌드, 실행, 배포
 └─ 빌드 도구
 └─ Gradle
 └─ 다음을 사용하여 테스트 실행
 Gradle → IntelliJ IDEA
-```
+```
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 183-183: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@TIL/sample/2026-06-16-상속.md` around lines 183 - 190, The fenced code block in
the document lacks a language identifier after the opening triple backticks,
which triggers a markdownlint warning. Add a language tag such as `text`
immediately after the opening code fence (the ```) on line 183 to specify the
code block language and resolve the linting error.

Source: Linters/SAST tools


로 변경하여 테스트 결과 창에서 `@DisplayName`을 확인할 수 있었다.

## 내일 더 공부할 것

- 객체지향 프로그래밍(OOP)의 핵심을 이루는 4대 원칙 언어별 차이점
29 changes: 29 additions & 0 deletions game/src/main/java/com/survivalcoding/GreatWizard.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.survivalcoding;

public class GreatWizard extends Wizard {

public GreatWizard(String name, int hp, int maxhp, int mp, int maxmp) {
super(name, hp, maxhp, mp, maxmp);
}

@Override
public void heal(Hero hero) {
if (mp >= 5) {
mp -= 5;
hero.incrementHP(25);
System.out.printf("대마법사의 강력한 힐! %s HP : %d\n", hero.getName(), hero.getHp());
} else {
System.out.println("마나가 부족합니다.");
}
}

public void SuperHeal(Hero hero) {
if (mp >= 50) {
mp -= 50;
hero.incrementHP(hero.getMaxhp()); // 최대 체력만큼 힐
System.out.printf("슈퍼 힐을 시전했습니다. %s HP : %d\n", hero.getName(), hero.getHp());
} else {
System.out.println("마나가 부족합니다.");
}
}
}
38 changes: 32 additions & 6 deletions game/src/main/java/com/survivalcoding/Hero.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,37 @@
package com.survivalcoding;

public class Hero {
String name;
int hp;
private String name;
private int hp;
private int maxhp;

void attack() {
// sout
System.out.println("공격했다");
public Hero(String name, int hp, int maxhp) {
this.name = name;
this.hp = hp;
this.maxhp = maxhp;
}
Comment on lines +8 to 12

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

생성자에서 HP 불변식(0 <= hp <= maxhp)이 보장되지 않습니다.

현재는 생성 시 hp > maxhp 또는 음수 값이 그대로 들어갈 수 있어 객체가 즉시 비정상 상태가 됩니다. incrementHP의 상한 보정 계약과도 어긋납니다.

제안 수정안
 public Hero(String name, int hp, int maxhp) {
-    this.name = name;
-    this.hp = hp;
-    this.maxhp = maxhp;
+    if (hp < 0 || maxhp < 0) {
+        throw new IllegalArgumentException("hp와 maxhp는 0 이상이어야 합니다.");
+    }
+    this.name = name;
+    this.maxhp = maxhp;
+    this.hp = Math.min(hp, maxhp);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public Hero(String name, int hp, int maxhp) {
this.name = name;
this.hp = hp;
this.maxhp = maxhp;
}
public Hero(String name, int hp, int maxhp) {
if (hp < 0 || maxhp < 0) {
throw new IllegalArgumentException("hp와 maxhp는 0 이상이어야 합니다.");
}
this.name = name;
this.maxhp = maxhp;
this.hp = Math.min(hp, maxhp);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@game/src/main/java/com/survivalcoding/Hero.java` around lines 8 - 12, The
Hero constructor does not validate the HP invariant that requires 0 <= hp <=
maxhp. Add validation logic in the Hero constructor to ensure the hp parameter
is within the valid range before assignment. If hp is negative, clamp it to 0;
if hp exceeds maxhp, clamp it to maxhp. This ensures the object maintains its
invariant from creation and aligns with the upper bound correction logic used
elsewhere like incrementHP.

}

public void attack() {
System.out.println(name + "이(가) 공격했다!");
}

public void incrementHP(int amount) {
if (amount + hp > maxhp) {
hp = maxhp;
} else {
hp += amount;
}
}

public String getName() {
return name;
}

public int getHp() {
return hp;
}

public int getMaxhp() {
return maxhp;
}
}
22 changes: 22 additions & 0 deletions game/src/main/java/com/survivalcoding/Wizard.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.survivalcoding;

public class Wizard extends Hero {
protected int mp;
private int maxmp;

public Wizard(String name, int hp, int maxhp, int mp, int maxmp) {
super(name, hp, maxhp);
this.mp = mp;
this.maxmp = maxmp;
}

public void heal(Hero hero) {
if (mp >= 10) {
mp -= 10;
hero.incrementHP(20);
System.out.printf("힐을 시전했습니다. %s HP : %d\n", hero.getName(), hero.getHp());
} else {
System.out.println("마나가 부족합니다.");
}
}
}
57 changes: 57 additions & 0 deletions game/src/test/java/com/survivalcoding/gametest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.survivalcoding;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

class GameTest {

private Hero targetHero;
private Wizard wizard;
private GreatWizard greatWizard;

@BeforeEach
void setUp() {
targetHero = new Hero("전사", 50, 100);
wizard = new Wizard("일반법사", 50, 50, 30, 50);
greatWizard = new GreatWizard("대법사", 60, 60, 60, 100);
}

@Test
@DisplayName("일반 마법사가 힐을 하면 대상의 HP가 20 오르고 마나가 10 깎여야 한다")
void wizardHealTest() {
wizard.heal(targetHero);

assertEquals(70, targetHero.getHp(), "전사의 HP는 50에서 20이 더해져 70이 되어야 합니다.");
}

@Test
@DisplayName("대마법사가 슈퍼힐을 쓰면 대상의 HP가 최대치로 차고 마나가 50 깎여야 한다")
void greatWizardSuperHealTest() {
greatWizard.SuperHeal(targetHero);

assertEquals(100, targetHero.getHp(), "슈퍼 힐을 받으면 최대 체력인 100이 되어야 합니다.");
}

@Test
@DisplayName("마나가 부족하면 힐이 나가지 않고 체력도 그대로여야 한다")
void outOfManaTest() {
Wizard oomWizard = new Wizard("마나없는법사", 50, 50, 5, 50);

oomWizard.heal(targetHero);

assertEquals(50, targetHero.getHp(), "마나가 부족하므로 타겟의 HP는 변하면 안 됩니다.");
}

@Test
@DisplayName("체력을 치유했을 때 최대 체력(MaxHP)을 초과할 수 없다")
void hpNotOverMaxHpTest() {
Hero highHpHero = new Hero("피통전사", 90, 100);

wizard.heal(highHpHero);

assertEquals(100, highHpHero.getHp(), "치유량의 총합이 MaxHP를 넘어가면 MaxHP로 고정되어야 합니다.");
}
}