[JAVA_개념 복습] 상속과 다형성, 캡슐화의 중요성과 getter/setter 사용, 추상 클래스

dejeong·2024년 10월 26일
0

JAVA

목록 보기
24/24
post-thumbnail

상속(Inheritance)

상속을 사용하면 자식 클래스가 부모 클래스의 필드와 메서드를 물려받는다. 자식 클래스는 부모 클래스의 기능을 재사용할 수 있고, 필요에 따라 추가하거나 재정의(오버라이딩)할 수 있다.

// 부모 클래스 (Super Class)
class Animal {
    String name;

    void eat() {
        System.out.println(name + " is eating.");
    }
}

// 자식 클래스 (Sub Class)
class Dog extends Animal {
    // 추가적인 필드
    String breed;

    // 메서드 오버라이딩 (부모 클래스의 메서드를 재정의)
    @Override
    void eat() {
        System.out.println(name + " (Dog) is eating.");
    }

    // 자식 클래스의 새로운 메서드
    void bark() {
        System.out.println(name + " is barking.");
    }
}

// 자식 클래스 (Sub Class)
class Cat extends Animal {
    // 메서드 오버라이딩 (부모 클래스의 메서드를 재정의)
    @Override
    void eat() {
        System.out.println(name + " (Cat) is eating.");
    }

    // 자식 클래스의 새로운 메서드
    void meow() {
        System.out.println(name + " is meowing.");
    }
}

// 메인 클래스
public class Main {
    public static void main(String[] args) {
        // Dog 객체 생성
        Dog dog = new Dog();
        dog.name = "Buddy";
        dog.breed = "Golden Retriever";
        dog.eat();  // 오버라이딩된 메서드 호출
        dog.bark();

        // Cat 객체 생성
        Cat cat = new Cat();
        cat.name = "Whiskers";
        cat.eat();  // 오버라이딩된 메서드 호출
        cat.meow();
    }
}
Buddy (Dog) is eating.
Buddy is barking.
Whiskers (Cat) is eating.
Whiskers is meowing.
  • dogCat 클래스는 Animal 클래스를 상속받음
  • Animal 클래스에 정의된 name 필드와 eat() 메서드는 DogCat이 상속받아 사용 가능
  • DogCat 클래스는 eat() 메서드를 오버라이딩하여 자신만의 방식으로 정의
  • Dogbark()라는 메서드를 추가했고, Catmeow() 메서드를 추가

다형성(Polymorphism)

다형성은 하나의 객체가 여러 타입을 가질 수 있는 성질이다, 상속을 통해 부모 클래스 타입의 참조 변수로 자식 클래스의 객체를 참조할 수 있으며, 동일한 메서드 호출이 참조하는 객체에 따라 다르게 동작할 수 있다.

// 부모 클래스 (Super Class)
class Animal {
    String name;

    void eat() {
        System.out.println(name + " is eating.");
    }
}

// 자식 클래스 (Sub Class)
class Dog extends Animal {
    @Override
    void eat() {
        System.out.println(name + " (Dog) is eating.");
    }

    void bark() {
        System.out.println(name + " is barking.");
    }
}

// 자식 클래스 (Sub Class)
class Cat extends Animal {
    @Override
    void eat() {
        System.out.println(name + " (Cat) is eating.");
    }

    void meow() {
        System.out.println(name + " is meowing.");
    }
}

// 메인 클래스
public class Main {
    public static void main(String[] args) {
        // 다형성 적용: 부모 클래스 타입으로 자식 객체 참조
        Animal myDog = new Dog();
        myDog.name = "Buddy";
        myDog.eat();  // Dog 클래스의 eat() 메서드가 호출됨

        Animal myCat = new Cat();
        myCat.name = "Whiskers";
        myCat.eat();  // Cat 클래스의 eat() 메서드가 호출됨

        // 배열을 사용한 다형성 적용
        Animal[] animals = new Animal[2];
        animals[0] = new Dog();  // Dog 객체
        animals[1] = new Cat();  // Cat 객체

        animals[0].name = "Rex";
        animals[1].name = "Mittens";

        for (Animal animal : animals) {
            animal.eat();  // 각 객체의 eat() 메서드가 호출됨
        }
    }
}
Buddy (Dog) is eating.
Whiskers (Cat) is eating.
Rex (Dog) is eating.
Mittens (Cat) is eating.
  • myDogmyCatAnimal 타입의 참조 변수지만, 각각 DogCat 객체를 참조한다. 참조된 객체에 따라 오버라이딩된 메서드가 호출된다.
  • animals 배열에 DogCat 객체를 추가하고, 루프를 통해 각각의 eat() 메서드를 호출하면 객체의 실제 타입에 맞는 메서드가 실행된다.

다형성의 주요 개념

  1. 부모 클래스 타입의 참조 변수로 자식 클래스 객체를 참조할 수 있다.
  2. 오버라이딩된 메서드는 참조하는 객체의 타입에 맞게 실행된다.
  3. 동적 바인딩: 컴파일 시점이 아닌 런타임 시점에 호출할 메서드가 결정된다.

✅ 상속: 부모 클래스의 속성과 메서드를 자식 클래스가 물려받아 재사용하거나 오버라이딩할 수 있는 기능

✅ 다형성: 하나의 객체가 여러 타입을 가질 수 있으며, 동일한 메서드 호출이 참조하는 객체에 따라 다르게 동작하는 성질


클래스가 부모 클래스를 상속받을 때는 오버라이딩?

클래스가 부모 클래스를 상속받을 때는 오버라이딩을 꼭 해줄 필요는 없다. 즉, 부모 클래스에서 상속받은 메서드를 그대로 사용할 수도 있고, 필요하다면 자식 클래스에서 해당 메서드를 오버라디잉(재정의)할 수 있다. 오버라이딩은 선택 사항이지 필수는 아니다.

부모 클래스의 메서드를 그대로 사용할 때 - 오버라이딩 X

자식 클래스는 부모 클래스에서 제공하는 기본 기능을 그대로 사용할 수 있다. 이 경우, 오버라이딩을 하지 않고 부모 클래스의 메서드를 사용하면 된다.

class Parent {
    void greet() {
        System.out.println("Hello from Parent!");
    }
}

class Child extends Parent {
    // greet() 메서드를 오버라이딩하지 않음
}

public class Main {
    public static void main(String[] args) {
        Child child = new Child();
        child.greet();  // 부모 클래스의 greet() 메서드가 호출됨
    }
}
Hello from Parent!

부모 클래스의 메서드를 재정의(오버라이딩)할 때 - 오버라이딩 O

자식 클래스가 부모 클래스의 메서드를 자신만의 방식으로 동작하도록 변경하려면 오버라이딩을 할 수 있다. 이 경우, 자식 클래스에서 메서드의 구현을 다시 작성하게 된다.

class Parent {
    void greet() {
        System.out.println("Hello from Parent!");
    }
}

class Child extends Parent {
    @Override
    void greet() {
        System.out.println("Hello from Child!");
    }
}

public class Main {
    public static void main(String[] args) {
        Child child = new Child();
        child.greet();  // 자식 클래스의 greet() 메서드가 호출됨
    }
}
Hello from Child!

부모 클래스의 메서드를 강제로 오버라이딩해야 하는 경우 - 오버라이딩 O

부모 클래스에서 메서드를 abstract로 정의하면, 해당 메서드는 자식 클래스에서 반드시 오버라이딩해야 한다. 추상 클래스를 상속받은 경우에 오버라이딩이 필수가 된다.

abstract class Parent {
    // 추상 메서드, 구현부가 없음
    abstract void greet();
}

class Child extends Parent {
    @Override
    void greet() {
        System.out.println("Hello from Child!");
    }
}

public class Main {
    public static void main(String[] args) {
        Child child = new Child();
        child.greet();  // 자식 클래스에서 구현한 greet() 메서드가 호출됨
    }
}

Parent 클래스의 greet() 메서드는 추상 메서드이므로 자식 클래스에서 반드시 구현해주어야 한다. 추상 메서드를 오버라이딩하지 않으면 컴파일 오류 발생


캡슐화(Encapsulation)

캡슐화는 객체 지향 프로그래밍의 중요한 개념 중 하나로, 객체의 내부 상태(필드)를 외부로부터 보호하고 객체의 속성에 대한 접근을 제한하여 데이터의 무결성을 보장하는 방법이다. 캡슐화를 통해 객체의내부 구현을 감추고, 외부와의 인터페이스를 통해서만 데이터를 처리하게 한다.

캡슐화의 중요성

  • 데이터 보호:
    캡슐화를 통해 객체의 내부 데이터를 외부로부터 보호할 수 있다. 외부에서 직접 필드에 접근하는 것을 막고, 적절한 메서드를 통해서만 데이터가 수정되도록 제한할 수 있다.
    예를 들어, 사용자가 직접 필드를 수정하지 못하도록 막고, 검증 로직을 통해 유효한 값만 저장하도록 할 수 있다.
  • 코드 유지보수성 향상:
    객체의 내부 구현이 변경되더라도, 외부에서 접근하는 인터페이스(getter, setter 등)는 동일하게 유지된다. 따라서 내부 구현을 수정해도 외부에 영향을 주지 않으며, 코드 유지보수가 용이해진다.
  • 캡슐화된 객체의 일관성 유지:
    특정 조건을 만족해야만 필드 값을 변경할 수 있게 할 수 있다. 예를 들어, 나이(age)는 음수가 될 수 없기 때문에, 필드를 보호하고 유효성 검사를 통해 잘못된 값이 입력되지 않도록 할 수 있다.
  • 정보 은닉:
    캡슐화는 클래스의 중요한 정보를 숨기고, 외부에서 접근할 수 없도록 제한한다. 이를 통해 불필요하거나 위험한 접근을 막고, 객체의 상태를 안전하게 유지할 수 있다.

캡슐화가 적용되지 않은 경우의 문제점

필드를 public으로 선언하여 외부에서 직접 접근하게 한다면, 데이터의 유효성을 보장할 수 없게 되고, 객체의 내부 상태가 쉽게 손상될 수 있다. 예를 들어, 나이를 음수로 설정하는 등의 문제가 발생할 수 있다.

public class Person {
    public int age;  // 공개 필드, 외부에서 직접 접근 가능
}

public class Main {
    public static void main(String[] args) {
        Person person = new Person();
        person.age = -5;  // 음수 나이 설정 가능, 데이터 무결성 파괴
        System.out.println("Age: " + person.age);
    }
}

캡슐화의 요약

  • 캡슐화는 객체의 속성을 외부에서 직접 접근하지 못하게 하여 데이터의 무결성을 유지하고, 유효성 검사를 통해 안전하게 데이터에 접근하도록 한다.
  • GetterSetter 메서드는 필드에 대한 접근을 간접적으로 허용하고, 내부 데이터의 유효성을 검증하는 중요한 역할을 한다.
  • 캡슐화를 통해 객체의 정보 은닉이 가능하고, 코드의 유지보수성이 높아진다.

캡슐화 예시

캡슐화는 클래스의 필드를 private으로 선언하고, 외부에서 접근할 수 있도록 public 메서드를 제공하여 필드에 간접적으로 접근하게 한다. 이를 통해 객체의 상태를 보호하고, 유효성 검사를 적용할 수 있다.

예시 1: 간단한 Account 클래스

public class Account {
    // private 필드, 외부에서 직접 접근 불가
    private String accountNumber;
    private double balance;

    // Constructor to initialize the account
    public Account(String accountNumber, double initialBalance) {
        this.accountNumber = accountNumber;
        if (initialBalance >= 0) {
            this.balance = initialBalance;
        } else {
            System.out.println("Initial balance cannot be negative.");
        }
    }

    // Getter for accountNumber
    public String getAccountNumber() {
        return accountNumber;
    }

    // Getter for balance
    public double getBalance() {
        return balance;
    }

    // Setter for balance with validation
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        } else {
            System.out.println("Deposit amount must be positive.");
        }
    }

    // Withdraw method with validation
    public void withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
        } else {
            System.out.println("Invalid withdraw amount.");
        }
    }
}

메인 클래스

public class Main {
    public static void main(String[] args) {
        // Account 객체 생성, 초기 잔액은 1000
        Account account = new Account("123-456", 1000);

        // 현재 계좌 번호와 잔액 확인
        System.out.println("Account Number: " + account.getAccountNumber());
        System.out.println("Balance: " + account.getBalance());

        // 입금 시도
        account.deposit(500);
        System.out.println("After deposit, Balance: " + account.getBalance());

        // 유효하지 않은 입금 시도
        account.deposit(-200);  // 오류 메시지 출력

        // 출금 시도
        account.withdraw(300);
        System.out.println("After withdrawal, Balance: " + account.getBalance());

        // 유효하지 않은 출금 시도
        account.withdraw(1500);  // 오류 메시지 출력
    }
}

출력 결과:

Account Number: 123-456
Balance: 1000.0
After deposit, Balance: 1500.0
Deposit amount must be positive.
After withdrawal, Balance: 1200.0
Invalid withdraw amount.
  • 필드 보호:
    • accountNumberbalanceprivate으로 선언되어 외부에서 직접 접근할 수 없다.
    • getAccountNumber()getBalance() 메서드를 통해 필드 값을 읽을 수 있다.
  • 유효성 검증:
    • 입금과 출금 시, deposit()withdraw() 메서드에서 유효성 검사를 수행한다. 금액이 양수인지, 출금 시 잔액이 충분한지 등을 확인하여 잘못된 데이터가 입력되지 않도록 방지한다.
    • 잘못된 입금이나 출금을 시도할 경우, 오류 메시지를 출력하고 잔액을 변경하지 않는다.
  • 외부와의 간접 통신:
    • 외부에서는 deposit(), withdraw() 메서드를 통해서만 계좌 잔액을 변경할 수 있다. 잔액을 직접 수정하지 못하도록 막고, 안전하게 데이터를 처리한다.

Getter와 Setter의 사용

Getter와 Setter는 객체의 캡슐화된 필드에 접근하고 수정하는 메서드이다. 보통 클래스의 필드를 private으로 선언하여 외부에서 직접 접근하지 못하도록 하고, public 접근 제한자를 가진 getter와 setter 메서드를 제공하여 필드에 대한 간접 접근을 허용합니다.

public class Person {
    // private 필드 - 외부에서 직접 접근 불가
    private String name;
    private int age;

    // Getter for name
    public String getName() {
        return name;
    }

    // Setter for name
    public void setName(String name) {
        this.name = name;
    }

    // Getter for age
    public int getAge() {
        return age;
    }

    // Setter for age with validation
    public void setAge(int age) {
        if (age >= 0) {  // 나이는 0 이상이어야 한다는 조건을 추가
            this.age = age;
        } else {
            System.out.println("Invalid age!");
        }
    }
}
public class Main {
    public static void main(String[] args) {
        Person person = new Person();
        
        // Setter를 사용해 값 설정
        person.setName("John");
        person.setAge(25);

        // Getter를 사용해 값 가져오기
        System.out.println("Name: " + person.getName());
        System.out.println("Age: " + person.getAge());

        // 유효하지 않은 값 설정
        person.setAge(-5);  // Invalid age! 출력
    }
}
Name: John
Age: 25
Invalid age!
  • Person 클래스의 nameage 필드는 private으로 선언되어 외부에서 직접 접근할 수 없다.
  • setName()setAge() 메서드를 통해 nameage 필드에 값을 설정할 수 있다.
  • setAge() 메서드에서는 나이가 0보다 작지 않도록 유효성 검사를 추가하여, 잘못된 값을 입력하려고 할 때는 경고 메시지를 출력하고 필드를 수정하지 않는다.
  • getName()getAge() 메서드를 통해서만 필드 값을 읽을 수 있다.

Getter와 Setter 사용의 장점

  • 필드 값에 대한 제어:
    • Setter 메서드에서 유효성 검사를 통해 유효하지 않은 데이터를 필드에 할당하는 것을 방지할 수 있다.
    • Getter 메서드에서 필드 값을 가공한 후 반환할 수도 있다.
  • 내부 구현의 변경 가능성:
    • 필드의 이름이나 구조가 바뀌어도 외부에서는 Getter/Setter 메서드만을 통해 접근하므로, 외부 코드의 수정이 최소화된다.
  • 데이터의 캡슐화 및 정보 은닉:
    • 필드를 private으로 선언하고, Getter/Setter를 사용함으로써 필드에 대한 직접적인 접근을 차단할 수 있다. 이를 통해 객체의 무결성을 보장할 수 있다.

Getter와 Setter의 주요 차이점

특징GetterSetter
역할필드 값을 읽고 반환필드 값을 설정하거나 변경
메서드 명명 규칙get 접두사를 사용해 getFieldName 형식으로 작성set 접두사를 사용해 setFieldName 형식으로 작성
리턴 타입필드의 데이터 타입과 동일한 값을 반환반환 값이 없으며, 일반적으로 void 타입
인수인수를 받지 않음필드의 값을 설정하기 위한 인수를 받음
읽기 전용 vs 쓰기 전용필드에 읽기 접근 권한만 제공필드에 쓰기 접근 권한을 제공
사용 목적외부에서 필드의 값을 확인할 때 사용외부에서 필드의 값을 변경하고자 할 때 사용
유효성 검사 여부일반적으로 유효성 검사가 필요하지 않음유효성 검사를 포함할 수 있어 값이 잘못 설정되는 것을 방지

BankAccount 클래스 예제

public class BankAccount {
    // Private 필드: 외부에서 직접 접근 불가
    private String accountHolder;  // 계좌 소유자
    private double balance;        // 잔액

    // 생성자: 초기 잔액을 설정
    public BankAccount(String accountHolder, double initialBalance) {
        this.accountHolder = accountHolder;
        if (initialBalance >= 0) { // 초기 잔액이 0 이상일 때만 설정
            this.balance = initialBalance;
        } else {
            System.out.println("Initial balance cannot be negative.");
            this.balance = 0;
        }
    }

    // Getter for accountHolder: 소유자 이름을 읽기 전용으로 제공
    public String getAccountHolder() {
        return accountHolder;
    }

    // Getter for balance: 잔액을 읽기 전용으로 제공
    public double getBalance() {
        return balance;
    }

    // Setter for balance: 유효성 검사 포함하여 잔액 추가 (입금 기능)
    public void deposit(double amount) {
        if (amount > 0) { // 입금액이 양수일 때만 허용
            balance += amount;
        } else {
            System.out.println("Deposit amount must be positive.");
        }
    }

    // Withdraw 메서드: 유효성 검사 포함하여 잔액 감소 (출금 기능)
    public void withdraw(double amount) {
        if (amount > 0 && amount <= balance) { // 잔액 범위 내에서만 출금 허용
            balance -= amount;
        } else {
            System.out.println("Invalid withdraw amount.");
        }
    }
}

메인 클래스

public class Main {
    public static void main(String[] args) {
        // 계좌 생성
        BankAccount account = new BankAccount("Alice", 1000);

        // 계좌 소유자 확인: Getter 사용
        System.out.println("Account Holder: " + account.getAccountHolder());

        // 초기 잔액 확인: Getter 사용
        System.out.println("Initial Balance: " + account.getBalance());

        // 입금: Setter의 역할 (간접적 데이터 수정)
        account.deposit(500);
        System.out.println("Balance after deposit: " + account.getBalance());

        // 잘못된 입금 시도: 음수 입금 (Setter 유효성 검사)
        account.deposit(-100);  // 유효성 검사 실패

        // 출금: Setter의 역할 (간접적 데이터 수정)
        account.withdraw(300);
        System.out.pintln("Balance after withdrawal: " + account.getBalance());

        // 잘못된 출금 시도: 잔액 초과 출금 (Setter 유효성 검사)
        account.withdraw(2000); // 유효성 검사 실패
    }
}

출력 결과

Account Holder: Alice
Initial Balance: 1000.0
Balance after deposit: 1500.0
Deposit amount must be positive.
Balance after withdrawal: 1200.0
Invalid withdraw amount.
  • Getter (getAccountHolder, getBalance):

    • getAccountHolder()getBalance()읽기 전용 메서드로, 계좌 소유자와 잔액을 확인하는 데 사용된다.
    • 외부에서는 balanceaccountHolder 필드에 직접 접근할 수 없기 때문에, Getter 메서드를 통해 필드 값을 안전하게 가져올 수 있다.
  • Setter (deposit, withdraw):

    • deposit()withdraw()는 간접적으로 잔액을 수정하는 Setter 메서드로, 쓰기 전용 메서드이다.
    • deposit()에서는 양수 금액만 입금 가능하고, withdraw()에서는 출금액이 잔액보다 클 수 없도록 유효성 검사가 포함되어 있다.
    • 잘못된 입금이나 출금 시도를 할 경우, 오류 메시지를 출력하고 잔액을 변경하지 않도록 제어한다.
  • Getter읽기 전용 접근을 제공하며, 필드 값을 변경하지 않고 가져오기만 한다.

  • Setter쓰기 전용 접근을 제공하며, 필드 값에 대한 유효성 검사를 통해 데이터 무결성을 유지하면서 안전하게 값을 변경할 수 있게 한다.

✅ 캡슐화와 getter, setter 둘은 서로 연관되어 있으며, getter와 setter가 캡슐화를 구현하는 도구로 자주 사용된다.


캡슐화와 Getter/Setter의 연관성

  • 캡슐화의 구현 수단:
    • 캡슐화는 클래스 내부의 필드를 외부로부터 보호하여 정보 은닉을 실현하는 기법이다.
    • 이를 구현하기 위해 필드를 private으로 선언하고, 필요한 경우 getter와 setter 메서드를 사용해 외부에서 필드에 접근할 수 있도록 한다.
    • Getter와 Setter 메서드는 캡슐화된 필드에 대해 간접 접근 경로를 제공한다.
  • 유효성 검사와 데이터 보호:
    • Setter 메서드를 통해 필드 값을 설정할 때 유효성 검사를 추가할 수 있다. 이를 통해 클래스의 데이터 무결성을 유지하고 오류를 방지한다.
    • 예를 들어, age 필드는 0 이상이어야 한다는 조건을 setter 메서드에 설정하여 잘못된 데이터가 저장되지 않도록 한다.

캡슐화와 Getter/Setter의 차이점

캡슐화Getter/Setter
클래스의 필드를 외부로부터 보호하여 정보 은닉을 실현하는 개념적 기법입니다.캡슐화된 필드에 간접적으로 접근하기 위해 사용하는 메서드입니다.
데이터의 무결성을 보장하고, 필드에 대해 불필요한 접근을 제한합니다.특정 조건에 따라 필드 값을 읽거나 수정하는 데 사용됩니다.
주로 필드를 private으로 선언하고, 필요 시 메서드로 접근을 제한합니다.get 메서드는 필드 값을 반환하고, set 메서드는 필드 값을 수정합니다.
필드에 직접 접근하지 못하게 하고, 필요한 검증을 통해 안전하게 데이터를 관리합니다.Getter와 Setter로 필드 값에 대한 제어가 가능하며, 유효성 검사 등을 추가할 수 있습니다.
캡슐화를 통해 객체의 내부 구현을 변경해도 외부에 영향이 적게 만듭니다.외부에서는 getter와 setter만 호출하므로, 필드의 변경 사항이 덜 영향을 미칩니다.

추상 클래스

하나 이상의 추상 메서드를 포함하고 있으며, 객체를 직접 생성할 수 없는 클래스이다. 추상 클래스는 구현해야 할 메서드를 자식 클래스에서 반드시 재정의하도록 강제하여, 코드의 일관성을 유지하고 유연성을 제공하는 역할을 한다.

추상 클래스란?

  • 정의: 추상 클래스는 abstract 키워드를 사용해 선언하며, 객체를 직접 생성할 수 없다. 주로 상속을 통해 하위 클래스에서 구현을 공유하고, 일부 메서드는 추상적으로 남겨둬 하위 클래스가 고유의 기능을 정의하도록 한다.
  • 목적: 공통된 속성기본 동작을 제공하면서도 일부 기능은 각 하위 클래스에 맞게 구현하게 하는 구조를 제공한다.

추상 클래스의 특징

  • 추상 메서드 포함 가능: 메서드 선언부만 있고 구현이 없는 추상 메서드를 가질 수 있다. 이 경우, 추상 메서드는 abstract 키워드를 사용한다.
  • 일반 메서드와 필드 포함 가능: 추상 클래스는 추상 메서드뿐만 아니라 구현된 메서드필드도 포함할 수 있다.
  • 하위 클래스에서 상속받아 구현: 추상 클래스를 상속받는 모든 하위 클래스는 추상 메서드를 반드시 구현해야 한다.
  • 객체 생성 불가: 추상 클래스는 직접 인스턴스화할 수 없으며, 상속받은 하위 클래스의 인스턴스를 통해 접근할 수 있다.

추상 클래스를 사용하는 이유와 시기

  • 공통된 기능을 재사용: 여러 클래스에서 공통적으로 사용할 기본 구현을 제공하여 코드의 중복을 줄인다.
  • 일관성 있는 구현 강제: 각 하위 클래스가 특정 메서드를 반드시 구현하도록 일관성을 강제한다.
  • 확장성과 유연성 제공: 기본적인 기능은 추상 클래스에서 제공하고, 구체적인 구현은 하위 클래스에서 작성해 유연성을 제공한다.

추상 클래스는 다음과 같은 경우에 유용하다:

  • 클래스들이 유사한 기능이나 속성을 가지며, 이를 통해 코드의 재사용을 기대할 수 있을 때.
  • 각 클래스가 서로 다른 구현 세부 사항을 포함할 때는 하위 클래스에서 추상 메서드를 구현하게 한다.

사용 방법

추상 클래스는 abstract 키워드로 정의되며, 추상 메서드는 구현 없이 선언만 되어 있다. 추상 클래스를 상속하는 모든 하위 클래스는 추상 메서드를 반드시 오버라이드해야 한다.

예시 코드

Animal 추상 클래스 예제

아래 예제는 Animal이라는 추상 클래스를 만들고, 이를 상속받는 DogCat 클래스를 통해 추상 클래스의 활용을 보여준다.

// 추상 클래스 Animal
abstract class Animal {
    private String name;

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

    // 추상 메서드: 하위 클래스에서 구현해야 함
    public abstract void makeSound();

    // 구현된 메서드: 모든 하위 클래스에서 공통으로 사용할 수 있음
    public void sleep() {
        System.out.println(name + " is sleeping.");
    }
}

// Dog 클래스는 Animal을 상속받아 추상 메서드를 구현
class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    // 추상 메서드 구현
    @Override
    public void makeSound() {
        System.out.println("Woof!");
    }
}

// Cat 클래스는 Animal을 상속받아 추상 메서드를 구현
class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }

    // 추상 메서드 구현
    @Override
    public void makeSound() {
        System.out.println("Meow!");
    }
}

메인 클래스

public class Main {
    public static void main(String[] args) {
        // Animal animal = new Animal("Unknown"); // 에러! 추상 클래스는 인스턴스화 불가

        Animal dog = new Dog("Buddy");
        Animal cat = new Cat("Kitty");

        dog.makeSound();  // "Woof!"
        dog.sleep();      // "Buddy is sleeping."

        cat.makeSound();  // "Meow!"
        cat.sleep();      // "Kitty is sleeping."
    }
}
  1. 추상 클래스 Animal:
    • makeSound()라는 추상 메서드를 선언하여 하위 클래스에서 해당 메서드를 구현하도록 강제한다.
    • sleep() 메서드는 구현된 메서드로, 모든 하위 클래스에서 공통적인 동작을 제공한다.
  2. 하위 클래스 Dog와 Cat:
    • DogCat 클래스는 Animal을 상속받아 각각 makeSound() 메서드를 구현한다.
    • makeSound() 메서드는 Dog에서는 "Woof!"를, Cat에서는 "Meow!"를 출력하도록 한다.
  3. Main 클래스:
    • DogCat 객체는 각각 Animal 타입으로 참조된다. 이는 다형성을 사용해 다양한 Animal 객체를 처리할 수 있게 한다.
    • Animal 객체를 직접 생성하려고 하면 에러가 발생한다.

추상 클래스 사용의 장점

  • 코드 재사용성: sleep() 같은 메서드를 여러 하위 클래스에서 중복 구현할 필요 없이 부모 클래스에서 공통으로 제공할 수 있다.
  • 일관성 강제: 모든 Animal 클래스는 반드시 makeSound() 메서드를 구현해야 하므로, 하위 클래스 간의 일관성이 유지된다.
  • 다형성: 다양한 Animal 객체를 Animal 타입으로 처리할 수 있어 유연한 코드 설계가 가능하다.

추상 메서드

메서드의 선언만 있고, 구현은 없는 메서드이다. 즉, 메서드 이름, 반환형, 매개변수만 정의하고, 실제 메서드가 해야 할 작업은 하위 클래스가 구체적으로 구현하도록 강제한다. 추상 메서드는 추상 클래스 또는 인터페이스 안에서만 선언할 수있으며, abstract 키워드를 사용하여 정의된다.

  • 메서드 선언만 있음 : 메서드의 본체가 없으며, 중괄호 {} 대신 세미콜론 ; 으로 끝난다.
  • 구현 강제 : 추상 메서드를 가진 추상 클래스를 상속받는 하위 클래스는 추상 메서드를 반드시 구현(오버라이딩)해야 합니다.
  • 추상 클래스나 인터페이스에서만 선언 가능 : 일반 클래스에서 추상 메서드를 선언할 수 없다.

추상 메서드의 목적

추상 메서드는 구체적인 구현을 자식 클래스가 수행하도록 설계한다. 이를 통해 상위 클래스는 메서드의 형태(이름, 반환형 등)만 정의하고, 구체적인 내용은 필요에 따라 다양하게 구현할 수 있어 유연성일관성을 모두 제공한다.

추상 메서드의 예제

Animal이라는 추상 클래스와, 그 안에 선언된 추상 메서드 makeSound()를 보여준다. DogCat 클래스는 Animal을 상속받아 각각 makeSound()를 자신에 맞게 구현한다.

// 추상 클래스 Animal
abstract class Animal {
    // 추상 메서드: 구현이 없음, 하위 클래스에서 오버라이딩해야 함
    public abstract void makeSound();

    // 일반 메서드: 모든 하위 클래스에서 공통적으로 사용할 수 있음
    public void sleep() {
        System.out.println("Animal is sleeping.");
    }
}

// Dog 클래스는 Animal을 상속받아 추상 메서드를 구현
class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Woof!");
    }
}

// Cat 클래스는 Animal을 상속받아 추상 메서드를 구현
class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Meow!");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal cat = new Cat();

        // 추상 메서드 makeSound()의 구현 호출
        dog.makeSound(); // "Woof!"
        cat.makeSound(); // "Meow!"

        // 일반 메서드 sleep() 호출 (공통 동작)
        dog.sleep(); // "Animal is sleeping."
        cat.sleep(); // "Animal is sleeping."
    }
}
  • 추상 메서드 makeSound():
    • DogCat 클래스는 각각 makeSound()를 고유하게 오버라이드하여 동물마다 다른 소리를 낸다.
  • 일반 메서드 sleep():
    • sleep() 메서드는 Animal 클래스에 구현되어, 모든 하위 클래스에서 공통적으로 사용할 수 있다.
    • dog.sleep()cat.sleep()은 동일한 출력을 가지며, 각 하위 클래스에서 다시 구현할 필요 없이 공통 동작을 재사용한다.

추상 메서드의 사용 이유

  • 상속 구조에서 일관성 유지 : 모든 하위 클래스가 특정 기능을 반드시 구현하도록 강제할 수 있다.
  • 유연성 제공 : 각 하위 클래스가 고유한 방식으로 메서드를 구현할 수 있어, 다양한 형태의 객체를 동일한 방식으로 처리할 수 있다.

추상 메서드는 공통 인터페이스를 제공하면서도 구체적인 동작을 하위 클래스에서 구현하도록 하여 유연하고 일관성 있는 객체 지향 구조를 설계하는 데 매우 유용하다.


추상 클래스와 인터페이스의 차이

추상 클래스와 인터페이스는 모두 자바에서 추상화를 지원하는 기법이다. 하지만 두 개념은 상속 구조와 기능 구현 방식, 사용할 수 있는 메서드와 필드 등에 있어 차이점이 있다.

기본 개념 및 목적

  • 추상 클래스: 클래스 상속을 위해 설계된 클래스이다. 객체를 생성할 수 없고, 다른 클래스에서 상속받아야 한다. 공통된 속성이나 기능을 여러 클래스에서 재사용하고자 할 때 사용된다.
  • 인터페이스: 클래스가 따라야 하는 일종의 계약으로, 메서드의 형태만 정의하며 구현 내용은 없다. 다중 구현을 허용해 여러 클래스 간의 공통 기능을 정의하고자 할 때 사용된다.

2. 상속과 구현

  • 추상 클래스: 단일 상속만 가능하므로, 한 클래스가 하나의 추상 클래스만 상속받을 수 있다.
  • 인터페이스: 다중 구현이 가능하므로, 한 클래스가 여러 인터페이스를 동시에 구현할 수 있다.

3. 구현 내용

  • 추상 클래스: 일반 메서드(구현된 메서드)와 추상 메서드(구현되지 않은 메서드) 모두 포함할 수 있다.
  • 인터페이스: 자바 8 이상에서는 defaultstatic 메서드를 통해 일부 구현이 가능하지만, 기본적으로는 모든 메서드가 추상 메서드이다.

4. 필드 선언

  • 추상 클래스: 인스턴스 변수와 상수 모두 선언할 수 있다. 접근 제어자도 자유롭게 설정할 수 있다.
  • 인터페이스: public static final 상수만 선언할 수 있으며, 모든 필드는 자동으로 상수로 취급된다.

5. 접근 제어자

  • 추상 클래스: 메서드와 필드에 대해 다양한 접근 제어자를 사용할 수 있다 (private, protected, public 등).
  • 인터페이스: 모든 메서드가 기본적으로 public이며, public abstract로 간주된다.

비교 예제 코드

추상 클래스 예제

abstract class Animal {
    // 필드와 구현된 메서드를 가질 수 있음
    private String name;

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

    // 추상 메서드: 하위 클래스에서 구현
    public abstract void makeSound();

    // 구현된 메서드
    public void sleep() {
        System.out.println(name + " is sleeping.");
    }
}

class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    @Override
    public void makeSound() {
        System.out.println("Woof!");
    }
}

인터페이스 예제

interface Flyable {
    // 모든 필드는 public static final
    int MAX_ALTITUDE = 10000;

    // 추상 메서드
    void fly();
}

interface Swimmable {
    void swim();
}

class Bird implements Flyable, Swimmable {
    @Override
    public void fly() {
        System.out.println("Bird is flying.");
    }

    @Override
    public void swim() {
        System.out.println("Bird is swimming.");
    }
}

주요 차이점 요약표

특성추상 클래스인터페이스
상속 개수 제한단일 상속다중 구현 가능
메서드추상 메서드와 구현된 메서드 모두 포함 가능자바 8부터 default, static 메서드 포함 가능
필드인스턴스 변수와 상수 모두 가능public static final 상수만 가능
생성자생성자 선언 가능생성자 선언 불가
목적공통된 속성 및 기능 상속클래스 간 공통 행위 정의

언제 사용해야 할까?

  • 추상 클래스공통된 속성이나 기능을 가진 클래스들을 만들고자 할 때 유용하다. 이를 통해 기본 동작을 제공하고, 일부 메서드를 추상 메서드로 남겨두어 하위 클래스가 자신만의 기능을 정의하도록 할 수 있다.
  • 인터페이스클래스들이 따라야 할 규격을 정의하고, 다중 구현이 필요할 때 사용한다. 특히 클래스들이 서로 관련이 없는 경우에도 특정 기능을 공유하게 할 수 있다.

따라서, 클래스들 간에 공통된 속성일부 구현이 필요하면 추상 클래스를, 클래스 간의 행위의 표준화가 필요하다면 인터페이스를 선택하는 것이 좋다.

profile
룰루

0개의 댓글