CodeGym /행동 /JAVA 25 SELF /클래스 계층 구조 만들기, 생활 속 예시

클래스 계층 구조 만들기, 생활 속 예시

JAVA 25 SELF
레벨 17, 레슨 3
사용 가능

1. 클래스 계층 구조란?

프로그래밍(그리고 그 외의 분야)에서 계층 구조는 트리 형태의 구조로, 트리의 꼭대기에는 “공통” 엔티티(기본 클래스)가 있고, 아래로 내려갈수록 더 구체적인 엔티티(하위 클래스)가 위치합니다. Java에서는 상속을 통해 클래스 계층을 만듭니다. 각 하위 클래스는 또 다른 하위 클래스들의 부모가 될 수 있으며 이런 식으로 계속 확장됩니다.

비유:

  • 동물(Animal) — 일반적인 클래스입니다.
  • 포유류(Mammal) — 동물의 특수한 경우입니다.
  • 개(Dog) — 포유류의 특수한 경우입니다.
  • 그리고 구체적인 “샤릭”은 클래스 Dog의 객체입니다.

코드에서는 다음과 같습니다:

class Animal { }
class Mammal extends Animal { }
class Dog extends Mammal { }

클래스 계층 구조를 사용하면 “위쪽”에서 공통 속성과 동작을 정의하고, “아래쪽”에서 세부 사항을 정의할 수 있습니다. 이렇게 하면 코드가 더 논리적이고 중복을 줄일 수 있습니다.

계층 구조 도식

Animal
├── Mammal
│   ├── Dog
│   └── Cat
└── Bird
    └── Sparrow

2. 계층을 설계하는 법: 논리와 실전

공통과 개별을 구분하기

핵심 원칙: 기본 클래스에는 모든 하위 클래스에 공통적인 것이 들어가야 합니다. 반면, 고유한 것은 하위 클래스로 내립니다.

예:

  • 모든 동물은 숨쉬고 먹을 수 있습니다 — 따라서 breathe()와 eat() 메서드는 Animal에 있어야 합니다.
  • 새만 날 수 있습니다 — 따라서 fly() 메서드는 Animal이 아니라 Bird에 있어야 합니다.
  • 개만 짖습니다 — bark() 메서드는 Dog에 있어야 합니다.

예: 동물

// 기본 클래스
class Animal {
    String name;

    Animal(String name) {
        this.name = name;
    }

    void eat() {
        System.out.println(name + "가 먹습니다.");
    }

    void makeSound() {
        System.out.println(name + "가 소리를 냅니다.");
    }
}

// 하위 클래스: 포유류
class Mammal extends Animal {
    Mammal(String name) {
        super(name);
    }

    void feedMilk() {
        System.out.println(name + "가 새끼에게 젖을 먹입니다.");
    }
}

// 하위 클래스: 개
class Dog extends Mammal {
    Dog(String name) {
        super(name);
    }

    @Override
    void makeSound() {
        System.out.println(name + "가 짖습니다: 멍멍!");
    }

    void wagTail() {
        System.out.println(name + "가 꼬리를 흔듭니다.");
    }
}

// 하위 클래스: 고양이
class Cat extends Mammal {
    Cat(String name) {
        super(name);
    }

    @Override
    void makeSound() {
        System.out.println(name + "가 웁니다: 야옹!");
    }

    void purr() {
        System.out.println(name + "가 가르랑거립니다.");
    }
}

// 하위 클래스: 새
class Bird extends Animal {
    Bird(String name) {
        super(name);
    }

    void fly() {
        System.out.println(name + "가 납니다.");
    }

    @Override
    void makeSound() {
        System.out.println(name + "가 지저귑니다: 짹짹!");
    }
}

main에서의 호출:

public class ZooDemo {
    public static void main(String[] args) {
        Dog sharik = new Dog("샤릭");
        Cat murka = new Cat("무르카");
        Bird sparrow = new Bird("보로베이");

        sharik.eat();        // 샤릭이 먹습니다.
        sharik.makeSound();  // 샤릭이 짖습니다: 멍멍!
        sharik.feedMilk();   // 샤릭이 새끼에게 젖을 먹입니다.
        sharik.wagTail();    // 샤릭이 꼬리를 흔듭니다.

        murka.eat();         // 무르카가 먹습니다.
        murka.makeSound();   // 무르카가 웁니다: 야옹!
        murka.feedMilk();    // 무르카가 새끼에게 젖을 먹입니다.
        murka.purr();        // 무르카가 가르랑거립니다.

        sparrow.eat();       // 보로베이가 먹습니다.
        sparrow.makeSound(); // 보로베이가 지저귑니다: 짹짹!
        sparrow.fly();       // 보로베이가 납니다.
    }
}

시각화: 클래스 트리

클래스 부모 특징
Animal
Object
name, eat(), makeSound()
Mammal
Animal
feedMilk()
Dog
Mammal
makeSound(), wagTail()
Cat
Mammal
makeSound(), purr()
Bird
Animal
fly(), makeSound()

3. 생활 속 다른 예시

기하학 도형

클래스 계층 구조는 기하 모델링에 매우 적합합니다.

// 기본 클래스
class Shape {
    void draw() {
        System.out.println("도형을 그립니다.");
    }
}

// 원
class Circle extends Shape {
    double radius;

    Circle(double radius) {
        this.radius = radius;
    }

    @Override
    void draw() {
        System.out.println("반지름 " + radius + "인 원을 그립니다");
    }
}

// 직사각형
class Rectangle extends Shape {
    double width, height;

    Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    void draw() {
        System.out.println("직사각형 " + width + "x" + height + "을 그립니다");
    }
}

사용:

public class ShapeDemo {
    public static void main(String[] args) {
        Shape s1 = new Circle(5);
        Shape s2 = new Rectangle(3, 4);

        s1.draw(); // 반지름 5.0인 원을 그립니다
        s2.draw(); // 직사각형 3.0x4.0을 그립니다
    }
}

교통수단

class Vehicle {
    void move() {
        System.out.println("탈것이 이동합니다.");
    }
}

class Car extends Vehicle {
    @Override
    void move() {
        System.out.println("자동차가 도로를 달립니다.");
    }
}

class Bicycle extends Vehicle {
    @Override
    void move() {
        System.out.println("자전거를 탄 사람이 페달을 밟습니다.");
    }
}

main:

Vehicle v1 = new Car();
Vehicle v2 = new Bicycle();

v1.move(); // 자동차가 도로를 달립니다.
v2.move(); // 자전거를 탄 사람이 페달을 밟습니다.

사용자

class User {
    String username;
    User(String username) { this.username = username; }
    void login() { System.out.println(username + "가 시스템에 로그인했습니다."); }
}

class Admin extends User {
    Admin(String username) { super(username); }
    void deleteUser(String user) {
        System.out.println(username + "가 삭제한 사용자: " + user);
    }
}

class Customer extends User {
    Customer(String username) { super(username); }
    void buy() { System.out.println(username + "가 구매를 완료했습니다."); }
}

4. 유용한 포인트

상속을 남용하지 마세요. 상속은 “is-a” 관계를 위한 도구입니다. “고양이는 동물이다”라고 말하고 싶다면 상속을 사용하세요. 반면 “고양이는 꼬리를 가진다”라면 컴포지션(has-a)을 사용하세요.

나쁨:

class Engine { /* ... */ }
class Car extends Engine { /* 자동차가 엔진인가요? 아니요, 이는 컴포지션입니다! */ }

좋음:

class Car {
    Engine engine; // 자동차는 엔진을 포함한다
}

너무 깊은 계층을 만들지 마세요. 레벨이 많을수록 코드를 이해하고 유지보수하기가 어려워집니다. 보통 대부분의 작업에서는 2–3단계가 최대입니다.

너무 “평면적인” 계층을 만들지 마세요. 20개의 클래스가 모두 하나의 기본 클래스를 직접 상속한다면 아키텍처를 재검토할 필요가 있을 수 있습니다.

“편의상” 하는 상속. 때로는 논리를 고려하지 않고 필드나 메서드를 “얻어 쓰고” 싶을 수 있습니다. 이는 혼란으로 가는 지름길입니다! class A와 class B가 논리적으로 연결되어 있지 않다면, 단지 몇 개의 메서드 때문에 class B를 extends A로 만들지 마세요.

리스코프 치환 원칙 위반. 하위 클래스가 아무런 놀랄 일 없이 부모 대신 사용될 수 없다면 — 계층 구조가 잘못 설계된 것입니다.

코드 중복. “이 메서드가 여기저기서 복사되고 있네”라는 생각이 든다면, 기본 클래스로 빼낼 때가 된 것입니다.

5. 클래스 계층 설계 시 흔한 오류

오류 1: “is-a” 논리 없는 상속.
오직 메서드나 필드에 접근하기 위해 상속을 사용하고, 하위 클래스가 정말로 “부모의 일종”이 아니면, 코드는 금방 엉망이 됩니다. 예를 들어 “자동차가 엔진을 상속한다”는 것은 is-a가 아니라 has-a입니다.

오류 2: 하위 클래스의 고유한 특징을 무시.
모든 하위 클래스가 똑같아 보이고 새로운 것을 전혀 추가하지 않는다면, 계층 구조가 필요 없을 수도 있습니다 — 하나의 클래스만 사용하세요.

오류 3: 하위 클래스 간 코드 중복.
같은 구현을 여러 하위 클래스에 복사한다면, 부모 클래스로 올려야 합니다.

오류 4: 지나치게 복잡하거나 깊은 계층.
다층 계층은 유지보수와 테스트가 어렵습니다. 간단할수록 좋습니다!

오류 5: @Override 애너테이션 없이 메서드 재정의.
애너테이션이 없으면 메서드 시그니처를 실수하기 쉽고, 그 경우 메서드는 재정의되지 않고 새 메서드로 간주됩니다. 항상 @Override를 사용하세요!

1
과제
JAVA 25 SELF, 레벨 17, 레슨 3
잠금
가상 애완동물: 보빅이 먹습니다 🐕
가상 애완동물: 보빅이 먹습니다 🐕
1
과제
JAVA 25 SELF, 레벨 17, 레슨 3
잠금
운전 학원: 이동 및 신호 🚦
운전 학원: 이동 및 신호 🚦
1
과제
JAVA 25 SELF, 레벨 17, 레슨 3
잠금
학교 계층 구조: 나는 누구인가? 🎓
학교 계층 구조: 나는 누구인가? 🎓
1
과제
JAVA 25 SELF, 레벨 17, 레슨 3
잠금
동물 백과사전: 고양이의 특징 🐈
동물 백과사전: 고양이의 특징 🐈
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION