
기초편에서 배웠던 자바의 타입을 다루는 방식은 다소 불편하게 느껴졌다.
파이썬은 타입에 대한 자유도가 높아 튜플 안에 문자열과 숫자를 함께 넣을 수 있었지만, 자바에서는 동일한 배열에 서로 다른 타입을 넣을 수 없었다.
그러나 이러한 불편함에도 불구하고 다형성의 존재에 따른 활용을 이해하면 그 필요성의 중요도가 느껴진다.
//다형성 활용 이전
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Caw caw = new Caw();
dog.sound();
cat.sound();
caw.sound();
}
위 코드는 .sound() 메서드를 호출해 각각 "멍멍", "야옹", "음메"와 같은 출력을 생성할 수 있다.
하지만 이 코드에는 .sound() 호출 부분이 중복되는 문제가 있다.
dog, cat, caw를 배열에 넣어 반복문으로 더나은 코드를 만드려해도 타입이 같지 않으므로 for문 내부를 작성할 수 없다.
타입을 하나로 통일하기 위해 Animal 클래스를 만들고, 생성자에서 dog, cat, caw를 구분하는 인자를 받아 처리하는 방법도 있지만, 이는 객체지향 원칙을 훼손한다.(다른 동물이 추가되었을 때, 기존 동물이 삭제될 때, 대부분의 변경 시나리오에서 Animal 클래서 전반에 영향을 준다.)
만약 Dog, Cat, Caw가 Animal이라는 부모 클래스를 상속받고, Animal 클래스에 sound() 메서드가 정의되어 있다면, 다형성을 활용하여 배열 문제를 해결할 수 있다.
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Caw caw = new Caw();
Animal[] animalArr = {dog, cat, caw}
for (Animal animal : animalArr) {
soundAnimal(animal);
}
}
private static void soundAnimal(Animal animal) {
animal.sound();
}
}
animalArr은 개, 고양이, 소의 부모 클래스인 Animal 타입의 배열로, 배열에 들어오는 dog, cat, caw 객체는 자동으로 업캐스팅된다.
이 배열은 for문을 통해 순회하며, 각 객체를 soundAnimal 메서드에 전달한다. soundAnimal 메서드는 입력받은 Animal 타입의 객체에서 .sound() 메서드를 호출하며, 여기서 오버라이딩이 작동한다. 만약 오버라이딩이 없었다면 Animal 클래스의 sound() 메서드가 호출되었을 것이다. 그러나 오버라이딩은 자식 클래스의 메서드를 우선시하여 dog, cat, caw 각각의 고유한 sound() 메서드가 실행된다. 이는 마치 다운캐스팅된 것처럼 동작하며, 자식 클래스의 동작을 강제하는 다형성의 강점을 보여준다. 이 과정을 보면, 형 변환의 필요성 때문에 여러 복잡한 작업을 해야 한다는 생각이 든다.
만약 위의 내용에서 Pig 클래스를 추가하고 이를 Animal의 자식으로 두었다고 가정하자. 이때, 실수로 @Override를 작성하지 않고 sound() 메서드를 구현한다면, 프로그램은 정상적으로 실행되지만 Animal 클래스의 sound() 메서드가 호출되는 문제가 발생한다.
이런 오류나 실수를 원천적으로 차단하기 위해 추상 클래스를 도입할 수 있다.
public abstract class Animal {
public abstract void sound();
public void move() {
System.out.println("펄쩍");
};
}
추상 클래스는 부모 클래스 역할을 제공하지만, 직접 생성될 수 없는 클래스로, 선언 시 abstract 키워드를 사용한다. 추상 메서드 역시 abstract 키워드를 사용하며, 구현 없이 선언만 가능하다. 이러한 추상 메서드는 자식 클래스에서 반드시 오버라이딩해야 하며, 이를 통해 특정 메서드의 구현을 강제한다. 따라서, 위의 Pig 클래스에서 @Override와 함께 sound() 메서드를 구현하지 않으면, 컴파일 오류가 발생하여 실수를 미연에 방지할 수 있다.
오, 블로그 업데이트? 캬, 역시 블로거 사용자1819! 그럼 아까 얘기한 내용들을 네 블로그 글에 착 붙을 마크다운으로 깔끔하게 정리해 줄게. 이대로 복붙해도 괜찮을걸? 폼 미쳤다! 폼 미쳤어! 😎
default & static 메서드위 글에서 인터페이스는 '모든 메서드가 구현 없이 선언만 되는 순수 추상 클래스'의 역할을 한다고 강조했습니다. 하지만 자바 8부터 인터페이스는 이 '순수성'에 조금 변화를 주게 됩니다. 바로
default메서드와static메서드의 도입이죠. 이 두 가지 새로운 기능 덕분에 인터페이스는 단순히 뼈대만 제공하는 설계도를 넘어, 훨씬 더 유연하고 강력한 역할을 수행할 수 있게 되었습니다.
default 메서드: 확장성과 하위 호환성을 동시에 잡다!등장 배경: 기존 인터페이스에 새로운 추상 메서드를 추가하면, 해당 인터페이스를 구현하는 모든 클래스에서 해당 메서드를 구현해야 하는 문제가 발생했습니다. 이는 대규모 프로젝트에서 엄청난 리팩토링 부담으로 이어지곤 했습니다. default 메서드는 이러한 하위 호환성 문제 없이 인터페이스를 확장할 수 있도록 해주는 핵심 기능입니다.
특징:
default 키워드를 사용해 인터페이스 내에서 메서드의 구현부를 직접 정의할 수 있습니다.default 메서드를 필수로 오버라이딩할 필요가 없습니다. 인터페이스가 제공하는 기본 구현을 그대로 사용하거나, 필요에 따라 오버라이딩하여 독자적인 동작을 정의할 수 있습니다.public이 됩니다.코드 예시:
public interface Flyable {
void fly(); // 여전히 추상 메서드 정의 가능
// 자바 8부터 추가된 default 메서드
default void takeOff() {
System.out.println("기본적으로 이륙합니다.");
}
default void land() {
System.out.println("기본적으로 착륙합니다.");
}
}
public class Airplane implements Flyable {
@Override
public void fly() {
System.out.println("비행기가 엔진 소리를 내며 하늘을 납니다.");
}
// takeOff(), land()는 오버라이딩하지 않아도 인터페이스의 기본 구현을 사용
}
public class Helicopter implements Flyable {
@Override
public void fly() {
System.out.println("헬리콥터가 프로펠러를 돌려 비행합니다.");
}
@Override
public void takeOff() { // 필요에 따라 오버라이딩하여 독자적인 이륙 방식 정의
System.out.println("헬리콥터가 수직으로 이륙합니다!");
}
}
다중 구현 시의 충돌: 만약 여러 인터페이스가 동일한 이름의 default 메서드를 가지고 있다면? 이때는 구현 클래스에서 반드시 해당 메서드를 직접 오버라이딩하여 어떤 동작을 할지 명시해야 합니다. 컴파일러가 어떤 default 구현을 사용해야 할지 결정할 수 없기 때문이죠.
static 메서드: 인터페이스 관련 유틸리티 기능 제공!등장 배경: 특정 인터페이스와 직접적으로 관련된 유틸리티성 기능을 제공하고 싶을 때가 있습니다. 이전에는 별도의 유틸리티 클래스를 만들어야 했지만, 자바 8부터는 인터페이스 내부에 static 메서드를 정의하여 더 간결하게 코드를 관리할 수 있게 되었습니다.
특징:
static 키워드를 사용해 인터페이스 내에서 메서드의 구현부를 직접 정의할 수 있습니다.static 메서드를 오버라이딩할 수 없으며, 인스턴스 생성 없이 인터페이스 이름으로 직접 호출해야 합니다.public 접근 제한자를 생략하면 자동으로 public이 됩니다.코드 예시:
public interface NumberUtil {
// 추상 메서드는 여전히 가능
int add(int a, int b);
// 자바 8부터 추가된 static 메서드
static int multiply(int a, int b) {
return a * b;
}
static int subtract(int a, int b) {
return a - b;
}
}
public class MyCalculator implements NumberUtil {
@Override
public int add(int a, int b) {
return a + b;
}
}
// static 메서드 사용 예시
public class Main {
public static void main(String[] args) {
int product = NumberUtil.multiply(5, 10); // 인터페이스 이름으로 직접 호출
System.out.println("곱셈 결과: " + product); // 출력: 곱셈 결과: 50
int difference = NumberUtil.subtract(20, 7);
System.out.println("뺄셈 결과: " + difference); // 출력: 뺄셈 결과: 13
}
}
자바 8의 default와 static 메서드 도입으로 인터페이스는 단순히 '순수 추상 클래스'의 역할을 강제하는 것 이상의 의미를 갖게 되었습니다.
static 메서드로 묶어 인터페이스 내에서 직접 제공할 수 있게 되어 관련 코드의 응집도를 높일 수 있게 되었죠.이러한 변화들 덕분에 인터페이스는 단순히 "뼈대만 던져주는 설계도"에서 벗어나, 어느 정도 "기본적인 살을 붙여줄 수도 있는 좀 더 유연하고 강력한 설계도"가 되었다고 볼 수 있습니다.