객체 지향은 현실 세계를 모델링하여 데이터와 그 데이터를 처리하는 동작을 하나의 객체로 묶고,
이런 객체들 간의 상호작용을 통해 시스템을 설계하고 구현하는 프로그래밍 패러다임이다.
이러한 객체 지향의 특징에는 4가지가 있다.
1. 캡슐화
2. 상속
3. 다형성
4. 추상화
그 중 상속과 다형성에 대해 알아보자.
상속
상위 클래스의 기능을 하위클래스로 확장하거나 재사용하는 것이다.
하위 클래스는 상위 클래스의 속성과 메서드를 확장하거나 변경하여 사용할 수 있고, 필요에 따라
상속받은 메서드나 속성을 확장하거나 재정의가 가능하다.
공통 기능은 부모 클래스에, 특수 기능은 자식 클래스에서 구현하여 코드의 중복을 줄일 수 있으며,
공통 기능이 부모클래스에 집중되어 수정이 쉽다.
다형성
하나의 이름으로 다양한 동작을 수행할 수 있는 특징으로,
부모 클래스나 인터페이스 타입의 참조 변수를 통해 여러 자식 클래스의 객체를 다룰 수 있다.
컴파일 시간의 다형성과 런타임 다형성이 있다.
동일한 코드로 다양한 객체를 처리 가능하게 만들어 코드 유연성과 유지보수를 용이하게 한다.
1. 컴파일 다형성
메서드 오버로딩을 통해 구현하며, 같은 이름의 메서드가 매개변수의 타입이나 개수에 따라 구분되어 컴파일 시점에 호출될 메서드가 결정된다.
2. 런타임 다형성
메서드 오버라이딩을 통해 구현.
부모 클래스의 참조 변수가 자식 클래스의 객체를 참조할 때, 실행 시점에 실제 객체의 타입에 따라 메서드가 호출된다.
public class ParentExamplee {
public static void main(String[] args) {
Parent1 p = new Child1();
p.parentMethod(); // Hi I'm Parent!
p.childMethod(); // 컴파일 에러
p.sayHello(); // hello Child!
}
}
class Parent1 {
public void parentMethod() {
System.out.println("Hi I'm Parent!");
}
public void sayHello() {
System.out.println("hello Parent!");
}
}
class Child1 extends Parent1{
public void childMethod(){
System.out.println("Hi I'm Child!");
}
@Override
public void sayHello() {
System.out.println("hello Child!");
}
}
p.childMethod();에서 컴파일 에러가 발생한다.
왜일까?
자바에서는 컴파일 타임에 변수의 타입에 따라 어떤 메서드를 호출할 지 결정한다. 컴파일러는 p에 할당될 인스턴스가 런타임에 어떤 값을 가질 것인지에 대한 고려는 하지 않고 오직 선언한 타입(Parent)에 대한 정보만을 고려한다.
변수p는 Parent1 타입으로 선언되었지만, 실제 객체의 타입은 Child1이다.
하지만 컴파일러는 p의 타입을 기준으로 Parent1 클래스에 정의된 메서드만 호출할 수 있다. 따라서, Parent1 클래스에는 childMethod()라는 메서드가 없기 때문에 p.childMethod();는 에러가 발생하는 것이다.
childMethod()는 Child1 클래스에만 존재하는 메서드이기 때문에 호출하려면 p를 Child1 타입으로 강제 형변환을 해야 한다.
해결 방안
1. p를 Child1 타입으로 형변환하여 호출:
((Child1)p).childMethod(); // 강제 형변환 후 호출
2. p를 처음부터 Child1 타입으로 선언:
Child1 p = new Child1();
p.childMethod(); // 정상 작동
p는 Parent1 타입으로 선언되어 있기 때문에 Parent1 클래스에 정의된 메서드만 호출할 수 있다.
childMethod()는 Child1에 정의된 메서드이기 때문에, p를 Child1 타입으로 형변환해야 호출할 수 있다.
따라서, Parent1 클래스에 정의되지 않은 childMethod()는 호출할 수 없기 때문에 컴파일 에러가 발생한다.
p.sayHello()는 자식 클래스인 Child1의 sayHello() 메서드를 호출한다. 이유는 런타임 다형성 때문이다.
컴파일 시점에는 변수 p가 Parent1 타입으로 선언되어 있음을 알 수 있지만, 실제 객체는 Child1 타입이다.
자바는 메서드 호출 시점에 실제 객체의 타입을 확인하고, 해당 타입에 맞는 sayHello() 메서드를 호출한다.
따라서, 런타임 시점에서는 실제 객체가 Child1이기 때문에 Child1에서 오버라이딩된 sayHello()가 호출된다.
참조변수 타입 ?
class Animal {
void sound() { System.out.println("Some sound"); }
}
class Dog extends Animal {
@Override
void sound() { System.out.println("Barking"); }
}
Animal animal = new Dog();
animal.sound(); // "Barking" 출력
참조 변수의 타입은 Animal 부모 클래스지만, 실제로 가리키는 객체는 자식 클래스인 Dog이다.
이로 인해 코드가 더 유연하고 확장 가능하며, 새로운 객체나 기능을 쉽게 추가할 수 있다.
실제 객체 타입
myDog 와 myCat는 각각 Dog 와 Cat 객체를 참조하며, 런타임시 Dog 와 Cat 클래스의 makeSound 메서드가 호출된다.
참조 변수의 타입이 아닌 실제 객체의 타입에 따라, 메서드가 실행되는 것이 바로 다형성의 본질이다.
다형성을 이용하면 같은 타입의 참조 변수를 사용하여 다양한 객체를 다룰 수 있어, 코드를 더 유연하고 확장이 가능하도록 한다.
예로, 여러 종류의 동물(Dog, Cat, Rabbit...)을 하나의 Animal 타입으로 처리할 수 있다면,
동물 종류가 추가되더라도 기존 코드를 거의 수정하지 않고 새로운 동물 종류를 추가할 수 있다.
class Animal {
void makeSound() {
System.out.println("Some sound");
}
}
class Dog extends Animal {
void makeSound() {
System.out.println("Bark");
}
}
class Cat extends Animal {
void makeSound() {
System.out.println("Meow");
}
}
public class Main {
public static void main(String[] args) {
Animal myDog = new Dog();
Animal myCat = new Cat();
makeAnimalSound(myDog);
makeAnimalSound(myCat);
}
static void makeAnimalSound(Animal animal) {
animal.makeSound();
}
}
makeAnimalSound 메서드는 Animal 타입의 매개변수를 받아서 호출이 되기 때문에, 다양한 동물 객체를 동일한 방식으로 처리할 수 있다.
이렇게 하면 동물의 종류가 늘어나더라도 makeAnimalSound 메서드는 수정할 필요가 없다.
사용 이유
다형성을 통해서 다양한 객체를 동일한 방식으로 처리할 수 있다.
만약 여러 동물을 처리하는 메서드를 각각 작성해야 한다면, 매우 번거롭고 중복이 많아지며, 새로운 동물이 추가될 때마다 기존 코드를 수정해야 할 것이다. 정말 비효율적이다.
하지만 다형성을 이용하면, 동물별로 별도의 메서드를 작성할 필요없이 Animal 타입으로 처리할 수 있다.
이를 통해 유지보수가 쉬워지고 확장성이 증가한다.
객체 지향은 상속을 통해 계층적 설계를 가능하게 하고, 다형성을 통해 유연한 코드 작성을 지원한다.
부모 클래스의 메서드를 하위 클래스에서 오버라이딩할 때, 접근 제한자를 더 좁게 설정할 수 있을까?
부모 클래스의 메서드를 하위 클래스에서 오버라이딩할 때, 접근 제한자를 더 좁게 설정할 수 없다. 왜냐하면 자바에서는 Liskov Substitution Principle (LSP)를 따르는데, 이 원칙은 하위 클래스가 부모 클래스의 자리를 대체할 수 있어야 한다는 것이다.
class Parent {
public void show() {
System.out.println("Parent show");
}
}
class Child extends Parent {
// 부모의 public 메서드를 private로 변경하려고 하면 컴파일 에러 발생
@Override
private void show() {
System.out.println("Child show");
}
}
위와 같이 부모 클래스의 public 메서드를 자식 클래스에서 private로 변경하려고 하면 컴파일 에러가 발생한다.
왜 그럴까?
자바에서는 부모 타입의 참조 변수를 통해 자식 객체를 다룰 수 있어, 코드의 유연성과 확장성이 높아진다.
부모 타입으로 자식 객체를 참조할 때, 부모 클래스에서 정의된 메서드는 항상 호출이 가능해야 한다. 그런데, 자식 클래스가 재정의할 때, 부모 메서드의 접근 제한자를 좁히면 이 전제가 깨지게 된다.
즉, 부모 타입으로는 호출할 수 없는데 자식 타입으로는 호출이 가능해지는 상황이 생겨 코드의 일관성을 깨지게 된다.
부모 타입으로 자식 객체를 다룰 때, 부모 클래스에서 제공하는 메서드(인터페이스)를 변함없이 사용할 수 있어야 하는데, 자식이 부모 메서드의 접근을 제한하면, 부모 타입으로 작성된 코드에서 자식 객체를 사용할 때 런타임 에러나 동작 오류가 발생할 수 있다.
'Java' 카테고리의 다른 글
static (0) | 2025.01.07 |
---|---|
ArrayList와 LinkedList (0) | 2025.01.07 |
Thread 구현 동기화와 Lock (0) | 2025.01.07 |
람다와 함수형 프로그래밍의 원리와 활용 (0) | 2025.01.07 |
람다 표현식과 함수형 인터페이스 (0) | 2025.01.07 |