본문 바로가기

JAVA

클래스 상속의 함정

다음과 같은 두 클래스가 있다.

 

public class Bus {

    // 버스의 좌석은 20석
    int seats = 20;
    int occupied;

    // 승객 1명 추가
    public void add1Passenger() {
        if (seatsAvailable())
            occupied += 1;
            System.out.println("잔여 좌석 : " + (seats - occupied));
        else
            System.out.println("빈 좌석이 없습니다.");
    }

    // 빈 자리가 있는지 검사하는 메서드
    private boolean seatsAvailable() {
        return occupied < seats;
    }
}
public class LargeBus extends Bus{

    // 대형 버스의 좌석은 40석
    int seats = 40;

    // 대형 버스에는 짐칸이 있음
    float cargoSpace = 1000f;
    float usedSpace;

    // 짐 넣기
    public void loadCargo(float vol) { // vol은 짐의 부피
        if(spaceAvailable(vol))
            usedSpace += vol;
            System.out.println("남은 적재공잔 : " + (cargoSpace - usedSpace));
        else
            System.out.println("짐칸에 빈 공간이 없습니다.");
    }

    // 짐을 넣을 빈 공간이 있는지 확인하는 메서드
    private boolean spaceAvailable(float vol) {
        return usedSpace + vol <= cargoSpace;
    }
}

 

코드 설명 :

  • Bus 클래스
    • 일반 버스로, 좌석이 20석이다.
    • 승객을 1명 추가하는 메서드가 존재한다.
  • LargeBus
    • 대형 버스로, Bus 클래스의 자원을 상속받는다.
    • 부모 클래스인 Bus 클래스의 '승객 1명 추가' 메서드를 사용할 수 있다.
    • 좌석수는 40석이다.
    • 부모 클래스인 Bus와는 달리 화물칸이 존재하고, 화물을 적재하는 메서드가 추가되었다.

위 코드를 기반으로 다음과 같이 코드를 작성하면 주석 안의 결과가 출력된다

 

Bus bus = new Bus();
LargeBus largeBus = new LargeBus();
Bus bus2 = new LargeBus();

System.out.println(bus.seats);  // 20
System.out.println(largeBus.seats);  // 40
System.out.println(bus2.seats);  // 20

 

주목해야 할 부분은 bus2의 좌석수이다. 인스턴스는 대형 버스인데, 왜 40석이 아닌 20석인 것인가?


특정 부모 클래스를 상속받은 클래스는, 그 부모 클래스 타입으로도 참조될 수 있다.

 

하지만 이와 같이 참조하면 부모 클래스의 형태로만 참조하고, 자식 클래스의 자원은 참조하지 않게 된다.

 

그래서 LargeBus에서 seats 속성을 40으로 설정한 값이 무시되는 것이다.

 

그런 이유로, bus2에서는 LargeBus 클래스의 화물 관련 속성 및 메서드 또한 호출이 불가능해진다.

 

bus2.cargoSpace;  // 호출 불가
bus2.loadCargo(123);  // 호출 불가

 

이런 상호작용으로 인해, bus2는 대형버스인데도 좌석이 20석밖에 되지 않는 불일치가 생긴다.

 

그렇게 되면 승객을 40명 태울 수 있음에도 20명밖에 태울 수 없게 된다.

 


그렇다면 LargeBus 타입으로 LargeBus 인스턴스를 참조하여 승객을 추가하면 제대로 작동할까?

 

public static void main(String[] args) {

    LargeBus largeBus = new LargeBus();
    
    // 승객 추가
    largeBus.add1Passenger();
}

-------------- 출력결과 --------------

잔여 좌석 : 19

예상대로라면 잔여 좌석은 39석이 나와야 하는데 19석이 나오고 있다.

 

분명 버스의 총 좌석을 출력하면 40이 나오는데 말이다. 왜 그럴까?

 

그 원인은 다음과 같다.

  1. 승객을 추가하는 메서드(add1Passenger, seatsAvailable)가 부모 클래스(Bus)에 구현되어 있음
  2. LargeBus에서 정의된 seats 필드는 Bus의 seats를 덮어씌운 것이 아님

 

largeBus는 add1Passenger() 메서드를 사용하기 위해 Bus 클래스를 참조해야 하고, 그 메서드 안에서 호출되는 seatsAvailable() 메서드를 호출할 때 seats 속성을 LargeBus 클래스의 것이 아닌 Bus 클래스의 것을 참조하게 된다.

 

이를 통해, 자식 클래스에서 어떤 필드를 부모 클래스의 것과 동일한 이름(위의 경우는 seats)으로 하면 부모 클래스의 필드를 덮어씌우는(오버라이딩하는) 것이 아니라는 것을 알게 되었다.

 

필드값들은 이름을 동일하게 지어도 부모 및 자식 클래스에 독립적으로 존재하고,

 

메서드에서 필드값을 사용하는 경우, 그 메서드가 구현된 클래스의 필드값을 사용하게 된다.

 


이런 문제는 어떻게 해결해야 하는가?

 

답은 메서드 오버라이딩에 있다.

 

앞서 말했듯이, 필드값은 오버라이딩이 되지 않는다.

 

반면에 메서드는 오버라이딩이 되므로, 이를 활용하여 다음과 같이 꼼수 메서드를 작성하면 된다.

 

public class Bus {

int occupied;

    // 꼼수 메서드
    int getSeats() {
        return 20;
    }

    // 승객 1명 추가
    public void add1Passenger() {
        if (seatsAvailable()) {
            occupied += 1;
            System.out.println("잔여 좌석 : " + (getSeats() - occupied));
        } else
            System.out.println("빈 좌석이 없습니다.");
    }

    // 빈 자리가 있는지 검사하는 메서드
    private boolean seatsAvailable() {
        return occupied < getSeats();
    }
}
public class LargeBus extends Bus {

    // 대형 버스에는 짐칸이 있다고 가정
    float cargoSpace = 1000f;
    float usedSpace;

    // 꼼수 메서드
    @Override
    int getSeats() {
        return 40;
    }

    // 짐 넣기
    public void loadCargo(float vol) { // vol은 짐의 부피
        if (spaceAvailable(vol)) {
            usedSpace += vol;
            System.out.println("남은 적재공잔 : " + (cargoSpace - usedSpace));
        } else
            System.out.println("짐칸에 빈 공간이 없습니다.");
    }

    // 짐을 넣을 빈 공간이 있는지 확인하는 메서드
    private boolean spaceAvailable(float vol) {
        return usedSpace + vol <= cargoSpace;
    }
    
    public static void main(String[] args){
    	Bus lb = new LargeBus();
        lb.add1Passenger();
}

두 클래스에서 seats 필드를 없애고, 총 좌석 수를 얻기 위해선 이를 반환하는 getSeats() 메서드를 호출하게 했다.

 

참조 타입이 부모 클래스인 Bus여도 자식 클래스인 LargeBus로 인스턴스를 생성하면 생성 과정에 getSeats() 메서드가 오버라이딩 되면서 불일치가 해소된다.