2024. 3. 2. 12:41ㆍJava
[ Extends와 Implements ]
디자인 패턴을 공부하다보니 extends 키워드와 Implements 키워드를 쓸 일이 굉장히 많아졌다. 그런데 어떤 상황 별 사용하는 건지 굉장히 혼잡하다는 느낌이 들었고 이 부분에 관해서 한 번 정리를 해보고자 한다.
[ extends 키워드 ]
extends는 상속과 관련이 있다. extends로 상속을 받으면 자식은 부모의 내부 형질을 물려받는다. 이건 장점일 수도 있고 단점일 수도 있다. 우선 extends의 메서드 접근 제어자 별 알아야 할 점들을 알아보고
[ extends와 접근 제어자 ]
더이상 구현할 필요가 없다면 부모의 메서드나 필드를 그대로 물려받아도 되고 부모의 메서드나 필드를 상속 받아서 재정의해도 된다. 해당 방식은 상속 받은 클래스 내에서 super라는 키워드를 통해서 지원이 된다. 일단 메서드만 따지고 봐보자. Fruit은 Fruit 내부에 printName이 public 접근 제어자로 선언이 되어있다. public 접근 제어자로 선언이 되어있기 때문에 Apple에서 상속을 받아도 super 접근자를 통해서 Fruit에 접근을 할 수 있다. 그러니 apple에서 출력을 할 때 super.printName을 쓰는 printName2에서는 출력값이 "this is a fruit method!"가 나오게 되고 @Override로 오버라이드한 printName은 기존값과 다른 값 "this is a apple method!"가 나오게 된다.
확인용 코드:
Fruit.java
public class Fruit {
public void printName(){
System.out.println("this is a fruit method!");
}
}
Apple.java
public class Apple extends Fruit {
public String name;
@Override
public void printName(){
System.out.println("this is a apple method!");
}
public void printName2(){
super.printName();
}
public static void main(String args[]){
Apple apple = new Apple();
apple.printName();
apple.printName2();
}
}
상속은 private 접근자가 아닌 protected이상으로 열린 메서드에만 해당이 된다. 만약에 메서드가 public이 아니고 private 접근 제어자로 되어있다면? super 키워드로 접근이 불가능해서 컴파일러가 오류를 내보낸다. 이는 위의 코드에서 Fruit.java의 printName을 public에서 private으로 바꿔서 확인해보길 바란다. 일반 클래스가 상속이 가능하게 만드려면 protected, default public을 사용해야 한다.
그러면? abstract는 어떨까? abstract를 사용하기 위해서는 클래스 자체가 abstract이어야 한다. 그리고 abstract로 추상 클래스화 시키면 내부에 구체적인 코드와 구체적이지 않은 코드를 혼합해서 사용할 수 있다. 템플릿 메서드 패턴과 같은 상황에서 일반적으로 사용하는 코드 형식을 한번 가져와봤다. abstract로 된 부분은 강제적으로 반드시 오버라이드 해야 한다.
확인용 코드:
Fruit.java
public abstract class Fruit {
public void execute(){
System.out.println("any logic 1");
printName();
System.out.println("any logic 2");
}
public abstract void printName();
}
Apple.java
public class Apple extends Fruit {
public String name;
@Override
public void printName(){
System.out.println("this is a apple method!");
}
public static void main(String args[]){
Apple apple = new Apple();
apple.execute();
}
}
정리를 하자면 extends로 상속을 받을 때 메서드에서는 private 접근 제어자를 쓰면 상속도 못하고 사용도 불가능하게 만들 수 있다. 그리고 protected, default, public을 사용하면 부모걸 사용해도 되고 상속해도 되며 super를 사용하면 부모 메서드를 this를 사용하면 본인의 메서드를 사용할 수 있다. 반대로 강제로 상속을 시키고 싶다면 abstract 클래스로 만들어서 자유롭게 놔두고 싶은 부분은 public, default, protected로 메서드를 생성하고 아닌 부분은 abstract 메서드로 만들어 강제로 작성하게 만들어줄 수도 있다. 이 정보에 따라 결정은 본인이 알아서 하는 것이라 생각을 한다. 예시는 이후에 아래에 두겠다.
이제는 필드에 관해서 알아보자. 필드 역시 private은 상속을 받았을 때 getter나 setter를 열지 않으면 접근이 불가능하고 protected 이상이면 본인이 설정한 파일 위치에 따라 접근 가능할 수도 있고 안될 수도 있다. private으로 getter, setter를 열면 사용은 가능한데, 이게 무슨 의미가 있는지 잘 모르겠다. 아무튼 이걸 확인해볼 수 있는 코드를 아래에서 살펴보자.
확인용 코드:
Fruit.java
public class Fruit {
private String name;
private int price;
private int quantity;
void execute() {
System.out.println("execute()");
}
void printName(){
System.out.println("name = " + name);
}
public String getName() {
return name;
}
public int getPrice() {
return price;
}
public int getQuantity() {
return quantity;
}
}
Apple.java
public class Apple extends Fruit {
@Override
public void printName(){
System.out.println("this is a apple method!");
}
@Override
public void execute(){
// 모두 상속 받아서 사용 불가능
// System.out.println(this.name);
// System.out.println(this.price);
// System.out.println(this.quantity);
System.out.println(this.getName() + super.getName());
}
public static void main(String args[]){
Apple apple = new Apple();
apple.execute();
}
}
해당 코드를 돌려보고 컴파일러 에러도 주석을 해제해서 사용해보면 무슨 느낌인지 감이 올 것이다. extends를 사용할 때에는 필드는 반드시 상속 시킬 것이라면 protected이상으로 열어두고 사용해야 한다.
** 별도 내용 **
참고로 상속을 받은 메서드는 메모리 상에서 힙 영역에 오른다. Apple에서도 String name을 정의하면 super.name과 this.name은 별개로 사용이 가능할텐데, 아래 그림과 같이 힙 영역에 메모리가 올라가기 때문이다. 조금 이미지가 별로지만 이전에 공부했던게 문득 생각났다.
[ 상속 extends 활용 예시 ]
예시 1: 클래스 별 공통 내용이 많은 경우에는 꼭 protected이상으로 공통 부분을 최대한으로 묶어 필드와 메서드를 생성해준다. 그리고 필요한 부분에 있어서 printName을 오버라이드하거나 새로운 메서드를 추가해준다. 이런 경우에는 부모에 최대한 많은 공통 요소가 들어가 있을 수록 좋은 설계이다.
확인용 코드:
Fruit.java
public class Fruit {
String name;
int price;
int quantity;
void printName(){
this.name = "Fruit";
System.out.println("name = " + name);
}
}
Apple.java
public class Apple extends Fruit {
@Override
public void printName(){
name = "apple";
System.out.println("this is a "+ name + "apple method!");
}
public void makeJuice(){
System.out.println("Make Apple Juice");
}
}
예시 2: 템플릿 메서드 패턴과 같이 특정된 공통 로직을 한 곳에서 처리하기 위한 코드를 작성하는 용도로 상속을 쓴다고 생각해보자. 그 땐 abstract 클래스로 생성해서 필드는 비워두고 공통 로직은 default로 개별 로직은 abstract 메서드로 처리를 해줘야 한다. 처리 부분은 logicA부분에 놔두었다.
확인용 코드:
Template.java
public abstract class Template<T> {
public T execute(){
System.out.println("================");
T result = logic();
System.out.println("================");
return result;
}
abstract T logic();
}
LogicA.java
public class LogicA extends Template{
@Override
String logic() {
System.out.println("I'm logicA!");
return null;
}
public static void main(String args[]){
Template template = new Template() {
@Override
String logic() {
System.out.println("I'm inner class");
return null;
}
};
template.execute();
}
}
[ Implements 키워드 ]
extends로 상속을 받는 경우에 대해서 어느정도 위를 보고 정리가 되었으면 좋겠다. extends를 쓰면 단점이 있는데, 부모와 자식 사이의 결합이 지나치게 강하다. 1번 케이스 같은 경우에는 새로운 메서드가 추가되는 건 괜찮다고 치더라도 기존 게 사라지면? 자식의 내용을 모두 고쳐야 한다. 2번 케이스 같은 경우에는 새로운 메서드가 공용 로직으로 추가되는 건 상관이 없는데 abstract처럼 강제성이 있는 로직을 추가하거나 삭제하는 경우 상속 받은 모든 자식의 코드를 수정해야 한다.
이런 상황을 피하고 싶을 때, 최소한의 공통 설계만을 넣어 강제성 있게 사용하고 싶은 경우 implements 키워드로 상속을 해주면 된다. 이 때 부모 클래스는 interface로 작성을 하며 하위 메서드는 default로 작성을 하면 자동으로 public abstract가 생략된 메서드로 처리를 해준다.
확인용 코드:
Engine.java
public interface Engine {
void run();
}
AppleCar.java
public class AppleCar implements Engine {
@Override
public void run() {
System.out.println("Apple카 시동켜유...");
}
}
앞의 예시를 듣다보면 "아니 Engine도 추가적으로 늘어나거나 메서드 없어지면 implements한 부분을 다 없애야 하는데요?"라고 생각할 수 있다. 설명에서 다른 부분은 다 차이없게 적었지만 extends와 implements 부분에서는 진한 글씨로 extends는 최대한 공통 속성을 뽑아낸다라는 부분에 강조를 implements에는 최소한의 공통 속성을 작성한다에 강조를 했다. 이렇게 해야 좋은 설계인 이유는 extends는 interface가 아닌 이상 여러 개를 상속 받지 못한다. 그런데 implements는 여러 개를 상속 받을 수 있다. 만약에 AppleCar에서 시동거는 것말고 바퀴로 이동하는 걸 만들고 싶으면 Engine에서 추가를 할 게 아니라 Tire같은 인터페이스를 만들어서move()를 만들어주는 게 더 나은 설계일 것이라고 생각한다. 그래서 각각의 상속 과정에서 명칭을 extend는 과일과 사과, implements는 Engine과 AppleCar로 이름을 지었다.
이렇게 extends와 implements에 관해서 알아봤는데 어느정도 정리가 되었으면 좋겠다!
'Java' 카테고리의 다른 글
JPA에서 조회 값을 Optional로 바꾸어보자 - 1편 (0) | 2024.03.21 |
---|---|
자바의 예외 계층 - 체크 예외와 언체크 예외 (0) | 2024.02.26 |
Spring JDBC로 알아보는 예외 덩어리 처리 방법 (0) | 2024.02.22 |
자바의 람다와 스트림 - 1(TIL) (0) | 2024.01.20 |
static initializer block와 initializer block (0) | 2024.01.19 |