본문 바로가기

코딩 이야기/안드로이드&코틀린 공부

[안드로이드&코틀린 공부] 상속, 추상클래스, 인터페이스

반응형

 

상속

기본 개념

상속은 이름 그대로 자식 클래스가 부모 클래스의 필드와 함수를 그대로 가져오는 개념입니다.

필드와 함수를 물려주는 쪽이 부모 클래스가 되고, 받는 쪽이 자식 클래스가 됩니다.

상속받는 필드와 함수는 접근 제한자의 영향을 받습니다. 즉 public, protected, private를 통해 접근을 제한하여 사용하는 것이 가능합니다.

상속받는 필드와 함수는 자식 클래스 내에서 재정의할 수 있습니다. 이를 오버라이딩이라고 부릅니다.

이를 이용하여 같은 부모 클래스의 함수를 물려받더라도 자식 클래스 내에서 오버라이딩한다면 같은 함수도 자식 클래스마다 다르게 사용할 수 있습니다.

장점

코드 재사용유지보수가 용이합니다.

class BreadMaker{
	private String Bread = "플랫 브레드";
	public String makername = "조우식표";
	public String getBread(){
		return Bread;
	}
}

class MakeSandwich extends BreadMaker{
	String topBread = getBread();
	String meat = "풀드포크";
	String bottomBread = getBread();
	public void outSandwich(){
		System.out.println(makername + topBread + meat + bottomBread + "샌드위치");
	}
}

class MakeCreamBread extends BreadMaker{
	String Bread = getBread();
	String Cream = "우유크림"
	String makername = "특 조우식표";
	public void outCreamBread(){
		System.out.println(makername + Cream + Bread);
	}
}

뜬금없지만 상황을 가정해서, 한 사장님이 동시에 샌드위치 가게와 크림빵가게를 운영한다고 합시다. 이때 각 매장에 빵 굽는 기계를 둘 필요는 크게 없습니다. 빵만 공급하는 시설을 따로 만들게 되면 매장 공간도 확보하고, 이후에 매장을 확장하고 체인점을 내도 빵을 일정하고 품질과 제품 균일하게 공급할 수 있으니까요.

코드또한 마찬가지로 상속에서 이와 같이 적용됩니다.

이렇게 자주 사용하는 필드와 함수가 있을때 상속을 통해 여러 클래스나 이후에 만들 클래스에서도 재사용할 수 있습니다. 이러면서 접근 제어자를 통해 지켜야할 필드 또한 지킬 수 있습니다.

하나 상황을 더 적용해보겠습니다.

만약 플랫 브레드가 전국적으로 품귀현상으로 구할 수 없어서 허니오트빵으로 바꾸게 되었습니다. 이때 만약 빵 굽는 기계를 매장마다 두었다면, 사장은 모든 매장에 각각 연락을 취해서 빵 만드는 법, 재료를 다 변경해야합니다. 하지만 우린 빵만 공급하는 시설을 따로 두었기에 상관이 없죠. 시설만 바꿔도 모든 매장이 전부 바뀌게 됩니다.

class BreadMaker{
	private String Bread = "허니오트빵";
	public String makername = "조우식표";
	public String getBread(){
		return Bread;
	}
}

class MakeSandwich extends BreadMaker{
...
}
class MakeCreamBread extends BreadMaker{
...
}

이 코드처럼 BreadMaker 클래스의 Bread만 바꾸어도 다른 클래스 또한 다 변경되기 때문에 유지보수 면에서도 매우 유용합니다.

단점

class BreadMaker{
	public String Bread = "수플레"; //이전엔 허니오트빵
	public String makername = "조우식표";
	public String getBread(){
		return Bread;
	}
}

class MakeSandwich extends BreadMaker{
...
}
class MakeCreamBread extends BreadMaker{
...
}
class MakeKnotted extends BreadMaker{
...
}
class MakeMilkyShop extends BreadMaker{
...
}

사장님의 사업이 초 대박이 났다고 합시다. 사장님은 유행을 따라잡기 위해 도넛가게들을 만들었지만 빵은 그대로 허니오트빵이여서 문제였고, 빵을 수플레로 바꿔야했습니다. 그래서 공급시설의 빵을 수플레로 바꿨더니 다른 매장에서 멘붕이 와버렸습니다.

이처럼 부모클래스의 필드나 함수의 변경은 자식클래스에 영향을 주기 때문에 유지보수에 득이 되기도 하지만 실이 되기도 합니다.

상속의 깊이가 깊어지면 문제가 생겼을 때 찾기가 어렵다.

다중 상속

(출처: https://www.geeksforgeeks.org/diamond-problem-solution/)

상속은 매우 유용한 기능이지만 이러한 상황이 발생할 수 있습니다.

위의 사진에서 Class D의 Func1라는 함수를 Class B와 Class C가 상속받았다고 합시다.

그런데 이때 Class A가 Class B와 Class C의 상속을 동시에 받는 상태에서 Func1를 호출하면 어떻게 될까요?

이런 모호한 상황에서는 에러가 발생합니다.

컴파일러는 Class A의 Func1과 Class B의 Func1 중에 무엇을 사용해야할 지 알 수 없기 때문입니다.

(물론 자바에서는 다중 상속을 지원하지 않아서 고려할 필요는 없지만요…)

해결법

  1. Class A::Func1() 이런식으로 범위 지정 연산자를 사용하면 됩니다.(다른 언어)
  2. 간접 상속을 사용하면 됩니다.

비슷하지만 다른 상황(super)

만약 부모 클래스가 가진 변수와 자식 클래스의 변수의 이름이 같다면 이 또한 문제가 될까요? 이러한 상황은 문제가 되진 않습니다. super라는 키워드를 통해 부모클래스의 변수와 자식클래스의 변수를 다르게 취급할 수 있습니다.

class BreadMaker{
	private String Bread = "플랫 브레드";
	public String makername = "조우식표";
	public String getBread(){
		return Bread;
	}
}

class MakeSandwich extends BreadMaker{
...
}

class MakeCreamBread extends BreadMaker{
	String Bread = getBread();
	String Cream = "우유크림";
	String makername = "특 조우식표";
	public void outCreamBread(){
		System.out.println(makername + Cream + Bread);
		System.out.println(super.makername + Cream + Bread);
	}
}

(예시 한번 더 쓰겠습니다..)

MakeCreamBread 클래스의 makername과 부모클래스인 BreadMaker의 makername은 다릅니다. 하지만 super라는 키워드를 통해 자식 클래스에서 오버라이딩 되어도 부모클래스의 변수를 참조할 수 있습니다.

추상 클래스

추상 클래스는 다른 클래스는 만들기 위한 설계도의 느낌입니다. adstract 라는 키워드를 사용합니다.

일반 클래스에는 추상 메소드를 만들 수 없고, 추상 클래스에서만 추상 메소드를 만들 수 있습니다. 그러면 추상 클래스에서는 일반 메소드를 만들 수 없는건가 하실 수 있지만, 그렇진 않습니다. 추상 클래스에서는 추상 메소드와 일반 메소드 둘 다 가능합니다.

추상 메소드는 정말 모양 틀의 역할이기때문에 반드시 자식 클래스에서 정의를 해줘야하며, 추상 부모 클래스에서의 추상 메소드는 바디를 아예 가질 수 없습니다.

abstract class a{
    public abstract void abs();
    public void notabs(){
        System.out.println("notabs");
    }
};

class b extends a{
    public void abs(){
        System.out.println("abs");
    }
}

public class Main
{
	public static void main(String[] args) {
	    b ins = new b();
	    ins.abs();
	    ins.notabs();
	}
}

이런 식으로 사용하게 됩니다.

abstract class a{
    public abstract void abs(){};// <==== 이부분
    public void notabs(){
        System.out.println("notabs");
    }
};

class b extends a{
...
}

public class Main
{
...
}

이런 식으로 추상 메소드인데 바디를 가지면 바로 에러입니다.

error: abstract methods cannot have a body

자식 클래스에서 정의를 안하는 경우

error: b is not abstract and does not override abstract method abs() in a

일반 클래스에 추상 메소드를 넣는 경우

error: a is not abstract and does not override abstract method abs() in a

인터페이스

인터페이스는 추상 클래스와도 매우 비슷합니다. 하지만 추상 클래스는 일반 메소드을 포함할 수 있지만, 인터페이스는 추상 메소드와 상수만 포함합니다.

implements 라는 키워드를 통해 구현하게 되며, 클래스들과는 다르게 다중상속이 가능합니다.

interface a{
    public abstract void func();
}

interface b{
    public abstract void func();
}

class c implements a,b{
    public void func(){
        System.out.println("a&b");
    }
}

물론 이와 동시에 평범한 클래스를 상속받는 것도 가능합니다.

interface a{
    public abstract void func();
}

interface b{
    public abstract void func();
}

class justclass{
    public void func(){
        System.out.println("justclass");
    }
}

class c extends justclass implements a,b{
    public void func(){
        System.out.println("a&b");
    }
}

평범한 클래스에서 동일한 이름의 함수를 갖고 있다고 해도, 인터페이스에서 구현해야할 추상 메소드로 인해 오버라이딩도 같이 되어버리기 때문에 위의 예시에서 출력은 a&b가 출력됩니다.

장점

  • 상속의 단점에서 부모클래스와 자식클래스의 연결성 때문에 문제가 될수도 있다고 했지만, 인터페이스는 이를 방지하며 유지보수성을 높입니다
  • 다른 개발자가 개발하더라도 정형화된 코드를 짜는 것이 가능해집니다.
반응형