이 글은 프로그래머스 - 실무 자바 개발을 위한 OOP와 핵심 디자인 패턴 강의를 정리한 내용입니다.
클래스와 상속에 관한 기본 문법 중 다음 세 가지를 살펴보겠습니다.
자바 기본서에서 많이 들어본 내용들이지만 각각의 내용을 설명하려고 하면 쉽지 않습니다.
그저 문법 중 하나로 느껴질 수 있지만, 객체지향 개념에서 아주 핵심적인 내용들이고 면접에서도 자주 물어보는 내용들이기 때문에 반드시 제대로 숙지하는게 중요합니다.
상속은 부모 클래스의 필드와 메서드를 자식 타입의 클래스로 확장하는 것을 의미합니다. 그리고 다형성은 부모 타입의 레퍼런스 변수에 자식 타입의 인스턴스를 넣어서 사용할 수 있는 특성을 의미합니다.
Parent
public class Parent {
public int parentPublicInt;
protected int parentProtectedInt;
private int parentPrivateInt;
public void someMethod() {
System.out.println("Parent someMethod");
}
}
Child
public class Child extends Parent {
public void anotherMethod() {
System.out.println("Child anotherMethod");
this.parentProtectedInt = 0;
this.parentPublicInt = 0;
// this.parentPrivateInt = 0; // 상속 되지 않았기 때문에 불가능!
}
}
상속에 대한 문법은 부모 클래스와 자식 클래스 사이에 extends
를 통해서 이루어집니다. 이렇게 상속하면 자식 클래스는 부모 클래스의 필드와 메서드를 사용할 수 있게됩니다.
여기서 '사용한다는 것'의 의미는 자식 클래스에서 부모 클래스의 필드와 메서드를 가지고 있다는 것과 같은 의미입니다.
물론 모든 필드와 메서드가 상속되는게 아닌, 상속이 가능한 요소에 한해서만 이루어집니다. 여기서 상속이 가능한 경우는 부모 클래스 내부 필드와 메서드의 접근 제어자를 통해 결정됩니다. 위 코드에서는 public & protected 접근 제어자를 가진 필드와 메서드만 상속되고, private 접근 제어자를 가진 요소는 상속되지 않습니다.
Main
public class ExtendsExampleMain {
public static void main(String[] args) {
Parent parent = new Parent();
Parent parentTypeChild = new Child(); // 다형성
Child child = new Child();
parent.someMethod();
parentTypeChild.someMethod();
// parentTypeChild.anotherMethod(); // Child 인스턴스지만, 타입이 Parent이기 때문에 호출이 불가능함.
child.someMethod(); // Parent에게 상속 받은 메서드
child.anotherMethod();
}
}
이렇게 상속 관계에 있으면 다형성도 함께 발생합니다. 다형성이 생기면 위 코드와 같이 부모 타입인 parentTypeChild
레퍼런스 변수에 Child
타입의 인스턴스를 넣을 수 있습니다.
그리고 parentTypeChild
를 통해 부모 타입의 메서드인 someMethod
도 호출할 수 있습니다.
여기서 주의할 점은, 분명 자식 타입인 Child
클래스는 anotherMethod
라는 메서드를 가지고 있지만, 이 메서드는 parentTypeChild
에서 호출할 수 없다는 점 입니다. parentTypeChild
는 부모 타입인 Parent
클래스의 레퍼런스 변수이기 때문입니다.
반대로 child
변수는 자식 타입인 Child
타입의 레퍼런스 변수이기 때문에, 부모 타입 메서드인 someMethod
는 물론 자식 타입 메서드인 anotherMethod
도 사용할 수 있습니다.
이번에는 오버라이딩과 오버로딩에 대해서 알아보겠습니다.
오버라이딩은 부모 클래스에서 상속받은 메서드를 자식 클래스에서 다시 정의하는 행위입니다.
이때, 오버라이딩의 조건으로 메서드의 시그니쳐가 모두 같아야합니다.
코드로 살펴보겠습니다.
Parent
public class Parent {
public void someMethod() {
System.out.println("Parent someMethod");
}
}
Child
public class Child extends Parent {
@Override
public void someMethod() {
System.out.println("Child someMethod");
}
}
위 코드를 살펴보면 부모 타입의 메서드인 someMethod
를 자식 타입인 Child
에서 오버라이딩하고 있습니다.
여기서 @Override
애노테이션은 필수 값은 아닙니다. 하지만 만약 Parent
클래스의 메서드가 사라지거나, 이름이 변경될 경우 @Override
애노테이션이 붙은 메서드는 에디터에서 오류를 출력하여주기 때문에 두 메서드 간 잘못된 오버라이딩을 빠르게 눈치챌 수 있습니다.
Main
public class OverridingExampleMain {
public static void main(String[] args) {
Parent parent = new Parent();
Parent child = new Child(); // 다형성
parent.someMethod();
child.someMethod();
}
}
parent
변수에는 Parent
타입 인스턴스를, child
변수에는 Child
타입 인스턴스를 넣어주었습니다. 이는 다형성의 특성이 적용되기에 가능한 문법입니다.
그리고 각각에 대해서 someMethod
를 호출해주었습니다.
실행결과는 다음과 같습니다.
Parent someMethod
Child someMethod
첫 번째 결과는 원래 부모클래스에서 정의된 결과이고, 두 번째 결과는 Child
에 의해서 오버라이딩된 결과입니다.
여기서 우리가 확인할 수 있는 사실은, 아무리 Parent
타입의 레퍼런스 변수라도 실제 호출된건 child
에 들어있는 Child
타입의 메서드라는 점입니다.
오버로딩이란 동일한 메서드 이름으로 파라미터, 리턴 타입이 다른 메서드를 여러개 정의하는 것입니다.
역시 코드로 살펴보겠습니다.
AddCalculator
public class AddCalculator {
public int add(int num1, int num2) {
return num1 + num2;
}
// return 타입만 다르게 오버로딩은 불가능
// public long add(int num1, int num2) {
// return num1 + num2;
// }
public long add(long num1, long num2) {
return num1 + num2;
}
public double add(double num1, double num2) {
return num1 + num2;
}
}
위 코드를 살펴보면, 세가지 add
메서드가 오버로딩 된 것을 확인할 수 있습니다.
이렇게 오버로딩된 메서드는 각각의 파라미터에 대해 올바른 메서드가 호출됩니다.
만약 파라미터가 같다면 리턴 타입이 다르더라도 오류가 발생합니다. 같은 파라미터에 대해 어떤 메서드를 실행해야할지 알 수 없기 때문입니다.
Main
public class OverloadingExampleMain {
public static void main(String[] args) {
AddCalculator addCalculator = new AddCalculator();
int intResult = addCalculator.add(10, 20);
long longResult = addCalculator.add(10L, 20L);
double doubleResult = addCalculator.add(10.0, 20.0);
System.out.println(intResult);
System.out.println(longResult);
System.out.println(doubleResult);
}
}
실행하면 다음과 같은 결과가 출력됩니다.
30
30
30.0
처음 개발을 하다보면 어떤 경우에 상속을 해야할지 판단하기 어렵습니다.
상속을 하게 되면 메서드를 물려받기 때문에 메서드를 재사용하기 위해 상속을 하는 경우가 있습니다. 그러나 이렇게 메서드를 재사용하기 위해 상속하는건 문법적으로는 가능하나, 객체지향적으로는 좋은 코드가 아닙니다.
❗️ 메서드를 재사용하기 위해서는 상속이 아니라 전략 패턴(Strategy Pattern)의 구성(Composition, has-a)을 활용하는게 좋습니다.
결과적으로 상속은 메서드가 아니라 필드를 재사용하려고 할 때 해야합니다.
상속을 하기 적절한 상황이라 상속을 진행하더라도 역시 주의해야할 사항이 있습니다. 부모 타입이 할 수 있는 일은 자식 타입도 할 수 있어야 한다는 것입니다.
이게 바로 SOLID원칙 중 하나인 리스코프 치환 원칙(LSP)입니다.
리스코프 치환 원칙을 지키는 상속이야말로 진정한 다형성이 있는 상속이됩니다.