• [JAVA] :: 상속(inheritance)

    2018. 2. 19. 10:03

    by. 위지원

    뇌를 자극하는 JAVA 프로그래밍을 공부한 내용


    아래 사진과 같이 기존 객체와 아주 유사한 객체를 만들어야하는 경우가 생길 때 상속이 필요하다.

    " 그냥 코드 복붙해서 좀만 더 추가하면 안되느냐? " 라고 생각할텐데 예전에는 그랬었다.

    근데 그렇게되면 중복되는 코드가많아서 관리도 힘들고 ( 예를들어 하나만 변경되도 찾아가서 또 변경하고..또..*a

    아무튼 그래서 상속이란 개념이 생겼다.


    객체지향 프로그래밍에서는 기존의 필요한 클래스를 가져오고 거기다가 필요한것만 추가하게 하여 새로운 클래스를 작성하게 한다.





    예를 들어서 계좌라는 클래스가 아래와 같다고 치자.


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class Account {
        String accountNo;
        String ownerName;
        int balance;
     
        void deposit(int amount) {
            balance += amount;
        }
     
        int withdraw(int amount) throws Exception {
            if (balance < amount)
                throw new Exception("잔액이 부족합니다");
            balance -= amount;
            return amount;
        }
    }
    cs



    그리고 계좌 클래스를 상속받는 직불계좌 클래스를 선언한다. 이때 extends라는 자바 키워드를 사용하여 해당 클래슬 상속할 수 있다.

    계좌클래스를 상속받은 직불계좌 클래스는 계좌클래스에 있는 withdraw나 amount 등 계좌 클래스에 존재하는 변수, 메서드를 자기것인마냥 사용할 수 있다.


    1
    2
    3
    4
    5
    6
    7
    8
    public class CheckingAccount extends Account {
        String cardNo;
        int pay(String cardNo,int amount) throws Exception{
            if(!cardNo.equals(this.cardNo)||(balance<amount))
                throw new Exception("지불이 불가능합니다");
            return withdraw(amount);
        }
    }
    cs



    하나더 .. +)당연히 상속을 받은 친구를 다른이에게 상속시킬 수 있다.


    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class CheckingTrafficCardAccount extends CheckingAccount {
        boolean hasTrafficCard;
     
        int payTrafficCard(String cardNo, int amount) throws Exception {
            if (!hasTrafficCard)
                throw new Exception("교통카드로 등록되지 않았습니다");
            return pay(cardNo, amount);
        }
    }
    cs


    자 이렇게 작성한 클래스를 컴파일해서 아무문제없다면 이 클래스를 사용해보자



    메인함수를 아래와 같이 작성해준다.


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class Main {
        public static void main(String args[]){
            CheckingAccount obj=new CheckingAccount();
            obj.accountNo="111-222-3333";
            obj.ownerName="위지원";
            obj.cardNo="0000-0000-0000-0000";
            obj.deposit(100000);
            try{
                int paidAmount=obj.pay("0000-0000-0000-0000",47000);
                System.out.println("지불액:"+paidAmount);
                System.out.println("잔액:"+obj.balance);
     
            }catch (Exception e){
                System.out.println(e.getMessage());
            }
        }
    }
    cs


    컴파일하고 실행해주면 다음과 같이 결과가 출력된다.




    자 근데 위 메인 코드에서 보면 이부분 좀 ...읏... 생성자를 사용해보자.


    아래 그림과 같이 체킹어카운트 클래스에 생성자를 추가해준다.


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class CheckingAccount extends Account {
        String cardNo;

        CheckingAccount(String accountNo, String ownerName, int balance, String cardNo){
            this.accountNo=accountNo;
            this.ownerName=ownerName;
            this.balance=balance;
            this.cardNo=cardNo;
        }

        int pay(String cardNo,int amount) throws Exception{
            if(!cardNo.equals(this.cardNo)||(balance<amount))
                throw new Exception("지불이 불가능합니다");
            return withdraw(amount);
        }
    }
    cs


    그러면 아래와 같이 새 객체를 생성할때 값을 넣어서 객체를 생성할 수 있다. 코드가 완전 깔끔해졌다 크크 숏코드 만세 결과는 당연히 동일




    내가 상속 받아야 할 친구가 생성자를 가지고 있으면 어떻게 해야할까?

    자 예를 들어서 아래와 같이 계좌 클래스에 생성자를 추가해보자.




    이렇게 생성자를 추가하면 해당 클래스를 상속받는 클래스에 에러가 발생한다. 왜냐면  자바 컴파일러가 컴파일할 때 슈퍼 클래스의 생성자 호출문이 생성자의 첫번째 명령으로 안있으면 자동으로 오엥 없네 하고 ㅇㅋ 슈퍼클래스는 no-arg constructor! 하고 그 위치에 추가해버린다.....




    이것을 해결하는 방법은 2가지이다.

    1) 아규먼트를 안가지는 생성자를 만들던가

    2) 상속받는 클래스에 상속받을 클래스의 생성자 호출문을 적던가

    이중에서 1번째 방법은 슈퍼 클래스를 수정하는 꼴이기때문에 " 슈퍼 클래스의 소스 코드를 건드리지 않고 상속받는 것이 원칙 " 이라는 룰을 깨버리게 되므로 2번을 해야한다.


    위에서도 말했듯이 컴파일러가 자동으로 no-arg를 추가해버려서 오류가 나는것이므로 "ㄴㄴ! 아규먼트 있음!"이라고 선언만 해주면 된다. 아래와 같이


    super(파라미터 형제들);




    이번엔 상속만 고분고분 받지말고 한번 내가 원하는대로 변경해보자. 자 예를 들어 아래와 같이 정의된 클래스가 있다해보자.

    '마이너스 한도'라는 변수가 추가되었다. 마이너스 한도가 넘지 않는 이상 잔액이 빵원이여도 인출이 가능케끔 해야한다.



    원래 인출하는 메서드를 보자 자 보면 잔액이 인출하려는 값보다 크면 무조건 안된다고 거절한다. 마이너스 통장은 잔액이 없어도 돈을 인출하게끔 해야한다. 이 메서드는 그대로 가져다 쓰면 안된다. 수정해서 쓰자.



    그래서 이 account 클래스를 상속받는 마이너스통장 클래스에 withdraw 클래스만 수정을 해준다.  이걸 메서드 오버라이딩이라고한다.





    메인함수에서 실행해보자. 아래와같이 마이너스 한도가 50000인 상태에서  잔액이 150000원으로 빵빵하기 떄문에  50000원을 인출했을때는 별다른 문제 없이 인출된다.




    이상태에서 한도를 다써보자



    그러면 요렇게 된다. 조금만 더빌려볼까..?




    한도가 초과했기때문에 인출이 불가능하다!



    오버라이드하는 메서드 안에서 오버라이드 된 메서드를 호출해보자  ... 이게 무슨말인가 하니.. 이번에는 '누적포인트' 변수가 생겨났다.

    이럴때에는 예금한다를 그대로 쓸 수 없다.



    계좌 클래스의 예금 메서드를 보자. 포인트고 뭐고 그냥 때려박은대로 바로 입력해주신다. 하지만 우리는 0.1%의 보너스 포인트를 적립해주고싶다.



    위에서 바로 배운대로 메서드 오버라이딩을 해보자. 후후 이제 두번째니 쉽다.



    아 근데 두번째 작성하다 보니///... 뭔가 이상한 알수없는 그런 귀찮음이 느껴진다. 바로 겹치는 코드가 있다는 것이다.

    balance+=amount; 가 겹친다. 이건 코드 단! 한줄! 이니까 그냥 모어때라고 느낄 수 있지만.. 이게 수백수십줄이라면.. ...아무튼 그러면

    그냥 메서드를 위에 불러오는게 더 낫지 않을ㄲ ㅏ?! 바로 아래와 같이 말이다. 후 뭔가 더 보람차다 크크



    그리고나서 메인에서 사용을 해주면 깔끔하게 결과를 출력해준다 ^---^





    자 .. 이제 상속을 하는 법을 알아봤으니 상속을 하게 하는 법도 알아보자 . 간단하다 final 키워드를 사용하면 된다.


    아래 사진을 보자. final을 클래스 앞에 위치해주니 계좌 클래스를 상속받고 있던 친구들이 난리가 났다.



    같은 방법으로 메서드도 적용할 수 있다.



    이번에는 인스턴스화를 금지 시켜보자.  책에서 설명하는 예시로는 더이상 계좌를 개설하지 못하도록한다. 라고되어있다. abstract 키워드를 사용하면 된다. 그럼 아래 그림과 같이 에러가 발생한다.




    이처럼 인스턴스화를 막기 위해 추상 클래스를 만드는 경우도 있지만 처음부터 그냥 추상 클래스를 만들어야할때도 있다고 합니다. 예를 들어

    아래와 같이 비슷하지만 다른 두개의 클래스가 있습니다. 이러한 경우 초록색 표와 같이 공통 기능을 뽑아내서 슈퍼 클래스를 작성할 수 있습니다.


    그런데, 생각해보니 이메일과 문자 메세지는 보내는 방법이 다르기때문에 '메시지를 송신하다' 라는 부분을 어떤 로직으로도 구사할 수 없게됩니다. 이럴 때 추상키워드 abstract를 사용하는 겁니다. 본체를 없이 만드는 것이지요.



    아래와 같이 메시지를 발송하는 클래스를 설계합니다. 이때 SendMessage라는 메서드를 추상메서드로 선언합니다.

    여기서 주의할 것은 추상 메서드를 포함하는 클래스는 추상클래스로 선언해야 합니다.



    자 그럼 이 친구를 상속받는 이메일과 핸드폰 메시지 클래스를 작성해봅시다. 아니 이럴수가.. 상속받자마자 에러를 뱉어냅니다.



    네 맞습니다. absract로 선언한 메서드를 구현해줘야지만 이 친구가 조용해집니다. 그렇게 email과 SMS 두개의 클래스를 작성해줍니다.




    다 구현했으면 이를 사용해볼 수 있는 메인 메서드를 작성해봅시다. 그러면 아무런 문제없이 결과를 잘 뱉어내줍니다 ^0^





    근데 우리는 생각하게 됩니다. 왜 빈껍데기 메서드를 선언하는가? 그 답은 이러합니다. 그 메서드를 꼭 구현해줘야해~! 라고 하는것이죠 빼먹으면 안돼! 라고 하게 되는거죠 이렇게 하면 서브클래스들이 꼭 지켜야할 일련의 규칙이 생기는 것입니다.


    그것 외에도 목적은 한가지가 더 있습니다.  이 목적을 설명하기 위해선 우리는 다형성에 대해 먼저 알아야 합니다.


    다형성(polymorphism) : 하나의 변수에 여러 종류의 데이터를 대입할 수 있는 성질

    아래와 같이 super 클래스를 상속받은 친구들은 비슷한 면이 많기에 슈퍼 클래스 객체에 서브 클래스 객체도 대입할 수 있습니다.



    코드로 보자면 아래와 같은 상황이 가능하다는 것ㅇㅂ니다.. 킬킬 역시 코드가 보기 좋다..



    이런 특성을 이용하여 여러객체를 한 개의 로직으로 처리하는 코드를 설계할 수 도 있숩나더,






    메세지 클래스도 다음과 같이 할 수 있습니다. 이때 send에서 SendMessage 메서드는 들어오는 객체가 무엇이느냐에 따라 그 객체의 SendMessage를 호출하게 되겠죠?




    자 이쯤이면 두번째 목적이 뭔지 말할때가 되었습니다.


    메세지 센더의 SendMessage부분을 주석처리해보았습니다.



    에러가 발생합니다.



    그 이유는 자바 가상 기계는 우리가 방금 메일 보내는 클래스에서 본것처럼 객체의 메서드를 호출할 때 뭔타입인지 관심없이 객체가 속하는 클래스의 메서드를 호출합니다.

    반면 그와 달리 자바 컴파일러는 타입을 기준으로 메서드를 찾게됩니다. 그래서 에러가 발생하는 것입니다. 


    추상화의 두번째 목적은 컴파일러를 무사히 통과하여 서브 클래스의 메서드가 실행되게 하는것입니다.



    여기서 잠깐 자바 컴파일러와 자바 가상 기계를 알아보고 가겠습니다.


    자바 컴파일러는 소스코드를 기계에서 실행할 수 있도록 byte code로 변경하는 것입니다.

    자바 가상 기계는 자바 컴파일러가 변환한 코드를 실행시키는 프로그램입니다.


    대화의 장 💬