ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 자바 - 객체지향 프로그래밍
    자바/Java 공부 2018. 1. 19. 23:22

    자바는 객체지향(Object Oriented) 프로그래밍 언어이다.

    객체지향에는 많은 개념들이 존재한다.

    • 클래스, 객체, 인스턴스
    • 상속
    • 인터페이스
    • 다형성
    • 추상화



    클래스


    "동물"이라는 클래스는 다음과 같이 만들 수 있다.

    Animal.java

    public class Animal {
    
    }

    이 껍데기뿐인 클래스도 아주 중요한 기능을 가지고 있다. 그 기능은 바로 객체(object)를 만드는 기능이다.

    객체는 다음과 같이 만들 수 있다.

    Animal cat = new Animal();

    new 는 객체를 생성할 때 사용하는 키워드이다. 이렇게 하면 Animal 클래스의 인스턴스(instance)인 cat, 즉 Animal의 객체가 만들어진다.

    ※ 객체와 인스턴스

    클래스에 의해서 만들어진 객체를 인스턴스라고도 한다. 그렇다면 객체와 인스턴스의 차이는 무엇일까? 이렇게 생각 해 보자. Animal cat = new Animal() 이렇게 만들어진 cat은 객체이다. 그리고 cat이라는 객체는 Animal의 인스턴스(instance)이다. 즉 인스턴스라는 말은 특정 객체(cat)가 어떤 클래스(Animal)의 객체인지를 관계위주로 설명할 때 사용된다. 즉, "cat은 인스턴스" 보다는 "cat은 객체"라는 표현이 "cat은 Animal의 객체" 보다는 "cat은 Animal의 인스턴스" 라는 표현이 훨씬 잘 어울린다.


    -객체변수(Instance variable)

    public class Animal { String name; }

    Animal 클래스에 name 이라는 String 변수를 추가했다. 이렇게 클래스에 선언된 변수를 객체 변수 라고 부른다. 또는 인스턴스 변수, 멤버 변수, 속성이라고도 말한다.

    객체 변수를 출력하려면 객체 변수에 어떻게 접근해야 하는지를 먼저 알아야 한다.

    객체 변수는 다음과 같이 도트연산자(.)를 이용하여 접근할 수 있다.

    객체.객체변수
    

    즉, Animal cat = new Animal() 처럼 cat 이라는 객체를 생성했다면 이 cat 객체의 객체 변수 name에는 다음과 같이 접근할 수 있다.

    cat.name   // 객체: cat, 객체변수: name
    

    이제 객체 변수에 어떤 값이 대입되어 있는지 다음과 같이 출력해 보자.

    public class Animal {
        String name;
    
        public static void main(String[] args) {
            Animal cat = new Animal();
            System.out.println(cat.name);
        }
    }
    

    실행해 보면 다음과 같은 결과가 출력된다.

    null
    

    cat.name을 출력한 결과값으로 null이 나왔다. null이라는 것은 값이 할당되어 있지 않은 상태를 말한다. 객체 변수로 name 을 선언했지만 아무런 값도 대입을 하지 않았기 때문에 null 이라는 값이 출력된 것이다.


    -메소드

    클래스에는 객체 변수와 더불어 메소드(Method)라는 것이 있다. 메소드는 클래스 내에 구현된 함수를 의미하는데 보통 함수라고 말하지 않고 메소드라고 말한다.

    이제 메소드를 이용하여 Animal 클래스의 객체 변수인 name 에 값을 대입해 보도록 하자.

    아래와 같이 setName 메소드를 추가 해 보자.

    public class Animal {
        String name;
    
        public void setName(String name) {
            this.name = name;
        }
    
        public static void main(String[] args) {
            Animal cat = new Animal();
            System.out.println(cat.name);
        }
    }
    

    Animal클래스에 추가된 setName메소드는 다음과 같은 형태의 메소드이다.

    • 입력: String name
    • 출력: void (리턴값 없음)

    즉, 입력으로 name이라는 문자즉, 입력으로 name이라는 문자열을 받고 출력은 없는 형태의 메소드이다. 메소드의 입출력에 대한 자세한 내용은 다음 장에 준비되어 있다. 지금 진행하고 있는 사항이 도무지 이해가 되지 않는다면 다음 장을 먼저 보고 다시 돌아와도 좋다.

    이번에는 setName 메소드의 내부를 살펴보자. setName 메소드는 다음의 문장을 가지고 있다.

    this.name = name;
    

    여기서 this에 대해서 이해하는 것은 꽤 중요하다. 이 문장에 대한 설명은 잠시 보류하고 일단은 우선 이 메소드를 호출 하는 방법에 대해서 먼저 알아보자.

    객체 변수에 접근하기 위해서 객체.변수 와 같이 도트연산자(.)로 접근할 수 있었던 것과 마찬가지로 객체가 메소드를 호출하기 위해서는 객체.메소드 로 호출할 수 있다.

    즉, 우리가 만든 setName메소드를 호출하려면 다음과 같이 호출해야 한다.

    cat.setName("boby");

    setName 메소드 내부에 사용된 this는 Animal 클래스에 의해서 생성된 객체를 지칭한다. 만약 Animal cat = new Animal() 과 같이 cat이라는 객체를 만들고cat.setName("boby") 와 같이 cat객체에 의해 setName 메소드를 호출하면 setName 메소드 내부에 선언된 this는 바로 cat 객체를 지칭하게 된다.

    만약 Animal dog = new Animal()로 dog 객체를 만든 후 dog.setName("happy")와 같이 호출한다면 setName 메소드 내부에 선언된 this는 바로 dog 객체를 가르키게 된다.

    따라서 this.name = "boby"; 문장은 다시 다음과 같이 해석되어 진다.

    cat.name = "boby";

    Animal 클래스의 객체변수 name이 cat객체와 dog객체간 서로 공유되는 변수라면 아마도 그럴것이다.

    다음과 같이 확인해 보자.

        public static void main(String[] args) {
            Animal cat = new Animal();
            cat.setName("boby");
    
            Animal dog = new Animal();
            dog.setName("happy");
    
            System.out.println(cat.name);
            System.out.println(dog.name);
        }
    

    결과는 다음과 같이 출력되었다.

    boby
    happy
    

    결과를 보면 name 객체 변수는 공유되지 않는다는 것을 확인할 수 있다.

    이 부분은 정말 너무너무 중요해서 강조하고 또 강조하고 백만번 강조해도 지나치지 않다. 클래스에서 가장 중요한 부분은 그 뭐라해도 이 객체 변수의 값이 독립적으로 유지된다는 점이다. 사실 이 점이 바로 클래스 존재의 이유이기도 하다. 객체 지향적(Object Oriented)이라는 말의 의미도 곱씹어 보면 결국 이 객체 변수의 값이 독립적으로 유지되기 때문에 가능한 것이다.

    (참고. 객체 변수의 값은 공유되지 않지만 나중에 알게될 static을 이용하게 되면 객체 변수를 공유하도록 만들 수도 있다.)


    -메소드(method)

    보통 다른언어에는 함수라는 것이 별도로 존재한다. 하지만 자바는 클래스를 떠나 존재하는 것은 있을 수 없기 때문에 자바의 함수는 따로 존재하지 않고 클래스 내에 존재한다. 자바는 이 클래스 내의 함수를 메소드라고 부른다.

    입력을 가지고 어떤 일을 수행한 다음에 결과물을 내어놓는 것, 이것이 메소드가 하는 일이다.


    메소드를 사용하는 이유?

    가끔 프로그래밍을 하다 보면 똑같은 내용을 자신이 반복해서 적고 있는 것을 발견할 때가 있다. 이 때가 바로 메소드가 필요한 때이다. 여러 번 반복해서 사용된다는 것은 언제고 또다시 사용할 만한 가치가 있는 부분이라는 뜻이다. 


    public class Test {
        public int sum(int a, int b) {
            return a+b;
        }
    
        public static void main(String[] args) {
            int a = 3;
            int b = 4;
    
            Test myTest = new Test();
            int c = myTest.sum(a, b);
    
            System.out.println(c);
        }
    }
    

    위 코드는 sum메소드에 3, 4 라는 입력값을 전달하여 7이라는 값을 돌려받는 예제이다.

    실행해보면 7이라는 값이 출력되는 것을 확인할 수 있다.


    메소드의 구조

    자바의 메소드 구조는 아래와 같다.

    public 리턴자료형 메소드명(입력자료형1 입력변수1, 입력자료형2 입력변수2, ...) {
        ...    
        return 리턴값;  // 리턴자료형이 void 인 경우에는 return 문이 필요없다.
    }

    평범한 메소드

    입력 값이 있고 리턴값이 있는 메소드가 평범한 메소드이다.

    위처럼 입력값과 리턴값이 있는 메소드는 다음처럼 사용된다.

    리턴값받을변수 = 객체.메소드명(입력인수1, 입력인수2, ...)
    

    실제코드의 예는 다음과 같다.

    Test myTest = new Test();
    int c = myTest.sum(a, b);

    출력 값을 받는 C의 자료형은 메소드와 같이 int여야 한다.


    입력값이 없는 메소드

    입력값이 없는 메소드가 존재할까? 당연히 그렇다. 다음을 보자.

    public String say() {
        return "Hi";
    }
    

    say 메소드의 입출력 자료형은 다음과 같다.

    • 입력 값 - 없음
    • 리턴 값 - String 자료형

    say라는 이름의 메소드를 만들었다. 하지만 입력 인수부분을 나타내는 괄호 안이 비어있다.

    이 메소드는 어떻게 쓸 수 있을까? 다음과 같이 따라해 보자.

    Test myTest = new Test();
    String a = myTest.say();
    System.out.println(a);

    위 예제를 실행하면 다음과 같이 "Hi"라는 문자열이 출력된다.

    Hi

    즉, 입력값이 없고 리턴값만 있는 메소드는 다음과 같이 사용된다.

    리턴값받을변수 = 객체.메소드명()

    리턴값이 없는 메소드

    리턴값이 없는 메소드 역시 존재한다. 다음의 예를 보자.

    public void sum(int a, int b) {
        System.out.println(a+"과 "+b+"의 합은 "+(a+b)+"입니다.");
    }
    

    위 sum 메소드의 입출력 자료형은 다음과 같다.

    • 입력 값 - int 자료형 a, int 자료형 b
    • 리턴 값 - void (없음)

    리턴값이 없는 메소드는 명시적으로 리턴타입 부분에 void라고 표기한다. 리턴값이 없는 메소드는 돌려주는 값이 없기 때문에 다음과 같이 사용한다.

    Test myTest = new Test();
    myTest.sum(3, 4);
    

    즉, 리턴값이 없는 메소드는 다음과 같이 사용된다.

    객체.메소드명(입력인수1, 입력인수2, ...)
    

    실제로 위 메소드를 호출해 보면 다음과 같은 문자열이 출력된다.

    3과 4의 합은 7입니다.

    입력값도 리턴값도 없는 메소드

    이것 역시 존재한다. 다음의 예를 보자.

    public void say() {
        System.out.println("Hi");
    }
    

    위 say 메소드의 입출력 자료형은 다음과 같다.

    • 입력 값 - 없음
    • 리턴 값 - void (없음)

    입력 값을 받는 곳도 없고 return문도 없으니 입력값도 리턴값도 없는 메소드이다.

    이것을 사용하는 방법은 단 한가지이다.

    Test myTest = new Test();
    myTest.say();
    

    즉, 입력값도 리턴값도 없는 메소드는 다음과 같이 사용된다.

    객체.메소드명()

    return의 또 다른 쓰임새

    특별한 경우에 메소드를 빠져나가기를 원할 때 return만 단독으로 써서 메소드를 즉시 빠져나갈 수 있다. 다음 예를 보자.

    public void say_nick(String nick) {
        if ("fool".equals(nick)) {
            return;
        }
        System.out.println("나의 별명은 "+nick+" 입니다.");
    }

    이 메소드 역시 리턴값은 없다. 문자열을 출력한다는 것과 리턴값이 있다는 것은 전혀 다른 말이다. 혼동하지 말도록 하자,

    이 메소드는 입력값으로 'fool'이라는 값이 들어오면 문자열을 출력하지 않고 메소드를 즉시 빠져나간다.

    참고. return 문만을 써서 메소드를 빠져나가는 이 방법은 리턴자료형이 void형인 메소드에만 해당된다. 


    메소드 내에서 선언된 변수의 효력 범위

    그 이유는 메소드 내에서 사용되는 변수는 메소드 안에서만 쓰여지는 변수이기 때문이다. 즉 public void vartest(int a) {라는 문장에서 입력 인수를 뜻하는 변수 a는 메소드 안에서만 쓰이는 변수이지 메소드 밖의 변수 a가 아니라는 말이다.

    그래서 이렇게 메소드 내에서만 쓰이는 변수를 로컬 변수(local variable)라고도 말한다.


    ※ 만약 vartest의 입력값이 int 자료형이 아닌 객체였다면 얘기가 다르다. 객체를 메소드의 입력으로 넘기고 메소드가 객체의 속성값(객체변수 값)을 변경한다면 메소드 수행 이후에도 객체는 변경된 속성값을 유지한다. 이러한 차이가 나는 이유는 메소드에 전달하는 입력 자료형의 형태 때문인데 메소드에 값을 전달하느냐 아니면 객체를 전달하느냐에 따라 차이가 난다.


    그렇다면 vartest라는 메소드를 이용해서 메소드 외부의 a를 1만큼 증가시킬 수 있는 방법은 없을까?

    다음과 같이 vartest메소드와 main메소드를 변경해 보자.

    public int vartest(int a) {
        a++;
        return a;
    }
    
    public static void main(String[] args) {
        int a = 1;
        Test myTest = new Test();
        a = myTest.vartest(a);
        System.out.println(a);
    }
    

    해법은 위 예처럼 vartest메소드에 return문을 이용하는 방법이다. vartest 메소드는 입력으로 들어온 값을 1만큼 증가시켜 리턴한다. 따라서 a = myTest.vartest(a)처럼 하면 a의 값은 다시 vartest메소드의 리턴값으로 대입된다. (1만큼 증가된 값으로 a의 값이 변경된다.)


    이번에는 아까 잠깐 언급한 객체를 넘기는 방법에 대해서 알아보자.

    다음의 예를 보자.

    public class Test {
    
        int a;  // 객체변수 a
    
        public void vartest(Test test) {
            test.a++;
        }
    
        public static void main(String[] args) {
            Test myTest = new Test();
            myTest.a = 1;
            myTest.vartest(myTest);
            System.out.println(myTest.a);
        }
    }
    

    이전 예제에서는 a 라는 int 자료형 변수를 main메서드에 선언했는데 위 예제에는 다음과 같이 Test 클래스의 객체변수로 선언했다.

    int a; // 객체변수 a
    

    그리고 vartest 메소드는 다음과 같이 Test클래스의 객체를 입력받아 해당 객체의 객체변수 a의 값을 1만큼 증가시키는 역할을 하도록 수정했다.

        public void vartest(Test test) {
            test.a++;
        }
    

    그리고 main메소드에서는 vartest메소드에 1이라는 값을 전달하던것을 Test클래스의 객체인 myTest를 넘기도록 다음과 같이 수정했다.

    myTest.vartest(myTest);
    

    이렇게 수정하고 프로그램을 실행시켜보면 myTest객체의 객체변수 a의 값이 원래는 1이었는데 vartest 메소드 실행 후 1만큼 증가되어 2라는 값이 출력되는 것을 확인할 수 있다.

    여기서 주목해야 하는 부분은 vartest메소드의 입력 파라미터가 값이 아닌 Test클래스의 객체라는데 있다. 이렇게 메소드가 객체를 전달 받으면 메소드 내의 객체는 전달받은 객체 그 자체로 수행된다. 따라서 입력으로 전달받은 myTest 객체의 객체변수 a의 값이 증가하게 되는 것이다.

    메소드의 입력항목이 값인지 객체인지를 구별하는 기준은 입력항목의 자료형이 primitive 자료형인지 아닌지에 따라 나뉜다. int 자료형과 같은 primitive 자료형인 경우 값이 전달되는 것이고 그 이외의 경우(reference 자료형)는 객체가 전달된다.

    위 예제에는 다음과 같은 문장이 있다.

    myTest.vartest(myTest);
    

    myTest라는 객체를 이용하여 vartest라는 메소드를 호출할 경우 굳이 myTest라는 객체를 전달할 필요가 없다. 왜냐하면 전달하지 않더라도 vartest 메소드는 this라는 키워드를 이용하여 객체에 접근할 수 있기 때문이다. this를 이용하여 vartest메소드를 수정한 버전은 다음과 같다.

    public class Test {
    
        int a;  // 객체변수 a
    
        public void vartest() {
            this.a++;
        }
    
        public static void main(String[] args) {
            Test myTest = new Test();
            myTest.a = 1;
            myTest.vartest();
            System.out.println(myTest.a);
        }
    }



    Call by value


    메소드에 값(primitive type)을 전달하는 것과 객체(reference type)를 전달하는 것에는 큰 차이가 있다. 이것은 매우 중요하기 때문에 이전에 잠깐 언급했지만 다시한번 자세히 알아보도록 하자.


    이제 예제를 다음과 같이 변경 해 보자.

    class Updater {
        public void update(Counter counter) {
            counter.count++;
        }
    }
    
    public class Counter {
        int count = 0;
        public static void main(String[] args) {
            Counter myCounter = new Counter();
            System.out.println("before update:"+myCounter.count);
            Updater myUpdater = new Updater();
            myUpdater.update(myCounter);
            System.out.println("after update:"+myCounter.count);
        }
    }
    

    이전 예제와의 차이점은 update 메소드의 입력항목이다. 이전에는 int count와 같이 값을 전달받았다면 지금은 Counter counter와 같이 객체를 전달받도록 변경한 것이다.


    상속

    Animal.java

    public class Animal {
        String name;
    
        public void setName(String name) {
            this.name = name;
        }
    }
    

    Dog.java

    public class Dog extends Animal {
    
    }
    

    클래스 상속을 위해서는 extends 라는 키워드를 사용한다.

    자식클래스 extends 부모클래스

    Dog 클래스에 name 이라는 객체변수와 setName 이라는 메소드를 만들지 않았지만 Animal클래스를 상속을 받았기 때문에 그대로 사용이 가능하다. Dog 클래스에 다음과 같은 main 메소드를 구현하고 실행시켜 보자.

    public class Dog extends Animal {
        public static void main(String[] args) {
            Dog dog = new Dog();
            dog.setName("poppy");
            System.out.println(dog.name);
        }
    }
    

    실행해보면 "poppy"라는 문자열이 출력되는것을 확인할 수 있다.

    보통 부모 클래스를 상속받은 자식 클래스는 부모 클래스의 기능에 더하여 좀 더 많은 기능을 갖도록 설계한다.


    IS - A 관계

    Dog클래스는 Animal클래스를 상속받았다. 자바는 이러한 관계를 IS-A 관계라고 표현한다. 즉 "Dog is a Animal" 과 같이 말할 수 있는 관계를 IS-A 관계라고 한다.

    이렇게 IS-A 관계(상속관계)에 있을 때 자식 객체는 부모 클래스의 자료형인 것처럼 사용할 수 있다.

    즉, 다음과 같은 코딩이 가능하다.

    Animal dog = new Dog();
    

    하지만 이 반대의 경우, 즉 부모 클래스로 만들어진 객체를 자식 클래스의 자료형으로는 사용할 수 없다.


    자바에서 만드는 모든 클래스는 Object라는 클래스를 상속받게 되어 있다. 사실 우리가 만든 Animal 클래스는 다음과 기능적으로 완전히 동일하다. 하지만 굳이 아래 코드처럼 Object 클래스를 상속하도록 코딩하지 않아도 자바에서 만들어지는 모든 클래스는 Object 클래스를 자동으로 상속받게끔 되어 있다.

    public class Animal extends Object {
        String name;
    
        public void setName(String name) {
            this.name = name;
        }
    }
    

    따라서 자바에서 만드는 모든 객체는 Object 자료형으로 사용할 수 있다. 즉, 다음과 같이 코딩하는 것이 가능하다.

    Object animal = new Animal();
    Object dog = new Dog();

    Object 클래스가 뭔데?



    메소드 오버라이딩


    이번에는 Dog 클래스를 좀 더 구체화 시키는 HouseDog 클래스를 만들어 보자. HouseDog 클래스는 Dog 클래스를 상속하여 다음과 같이 만들 수 있다.

    HouseDog.java

    public class HouseDog extends Dog {
        public static void main(String[] args) {
            HouseDog houseDog = new HouseDog();
            houseDog.setName("happy");
            houseDog.sleep();
        }
    }
    

    HouseDog 클래스를 실행 해 보면 sleep 메소드가 호출되어 다음과 같은 결과가 출력될 것이다.

    happy zzz


    그런데 HouseDog, 즉 집에서 키우는 개들은 잠을 집에서만 잔다고 한다. HouseDog 클래스로 만들어진 객체들은 sleep 메소드 호출 시 "happy zzz" 가 아닌 "happy zzz in house" 를 출력해야 한다고 가정 해 보자.

    이렇게 하려면 어떻게 해야 할까?

    다음과 같이 HouseDog 클래스를 수정해 보자.

    public class HouseDog extends Dog {
        public void sleep() {
            System.out.println(this.name+" zzz in house");
        } 
    
        public static void main(String[] args) {
            HouseDog houseDog = new HouseDog();
            houseDog.setName("happy");
            houseDog.sleep();
        }
    }
    

    Dog 클래스에 있는 sleep 메소드를 HouseDog에 내용만 조금 변경하여 구현하고 실행 해 보았더니 다음처럼 원하던 결과값을 얻을 수 있었다.

    HouseDog 클래스에 Dog 클래스와 동일한 형태(입출력이 동일)의 sleep 메소드를 구현하면 HouseDog 클래스의 sleep 메소드가 Dog 클래스의 sleep 메소드보다 더 높은 우선순위를 갖게 되어 HouseDog 클래스의 sleep 메소드가 호출되게 된다.

    이렇게 부모클래스의 메소드를 자식클래스가 동일한 형태로 또다시 구현하는 행위를 메소드 오버라이딩(Method Overriding)이라고 한다. (※ 메소드 덮어쓰기)


    메소드 오버로딩

    이미 sleep이라는 메소드가 있지만 동일한 이름의 sleep메소드를 또 생성할 수 있다. 단, 메소드의 입력항목이 다를 경우만 가능하다. 새로 만든 sleep메소드는 입력항목으로 hour라는 int 자료형이 추가되었다.

    이렇듯 입력항목이 다른 경우 동일한 이름의 메소드를 만들 수 있는데 이것을 어려운 말로 메소드 오버로딩(method overloading)이라고 부른다.


    다중상속

    다중 상속은 클래스가 동시에 하나 이상의 클래스를 상속받는 것을 뜻한다. C++, 파이썬 등 많은 언어들이 다중 상속을 지원하지만 자바는 다중 상속을 지원하지 않는다.

    위 main 메소드에서 test.msg(); 실행 시 A 클래스의 msg 메소드를 실행해야 할까? 아니면 B 클래스의 msg 메소드를 실행해야 할까?

    다중 상속을 지원하게 되면 이렇듯 애매모호한 부분이 생기게 된다. 자바는 이러한 불명확한 부분을 애초에 잘라 낸 언어이다.

    ※ 다중상속을 지원하는 다른 언어들은 이렇게 동일한 메소드를 상속받는 경우 우선순위등을 적용하여 해결한다.


    생성자

    객체 변수에 값을 무조건 설정해야만 객체가 생성될 수 있도록 강제할 수 있는 방법은 없을까?

    생성자(Constructor)를 이용하면 된다.

    클래스 가장 상단에 다음과 같은 메소드를 추가해 보자.

    생성자의 규칙

    1. 클래스명과 메소드명이 동일하다.
    2. 리턴타입을 정의하지 않는다.

    생성자는 객체가 생성될 때 호출된다. 객체가 생성될 때는 new라는 키워드로 객체가 만들어질 때이다.

    즉, 생성자는 다음과 같이 new라는 키워드가 사용될 때 호출된다.

    new 클래스명(입력항목, ...)

    우리가 만든 생성자는 다음과 같이 입력값으로 문자열을 필요로 하는 생성자이다.

    public HouseDog(String name) {
        this.setName(name);
    } 
    

    따라서 다음과 같이 new 키워드로 객체를 만들때 문자열을 전달해야만 한다.

    HouseDog dog = new HouseDog("happy");   // 생성자 호출 시 문자열을 전달해야 한다.
    

    만약 다음처럼 코딩하면 컴파일 오류가 발생할 것이다.

    HouseDog dog = new HouseDog();
    

    오류가 발생하는 이유는 객체 생성 방법이 생성자의 규칙과 맞지 않기 때문이다. 생성자가 선언된 경우 생성자의 규칙대로만 객체를 생성할 수 있다.


    default 생성자

    이번에는 default 생성자에 대해서 알아보자.

    다음의 코드를 보자.

    public class Dog extends Animal {
        public void sleep() {
            System.out.println(this.name + " zzz");
        }
    }
    

    그리고 다음 코드를 보자.

    public class Dog extends Animal {
        public Dog() {
        }
    
        public void sleep() {
            System.out.println(this.name + " zzz");
        }
    }
    

    첫번 째 코드와 두번 째 코드의 차이점은 무엇일까? 두번 째 코드에는 생성자가 구현되어 있다. 생성자의 입력 항목이 없고 생성자 내부에 아무 내용이 없는 위와 같은 생성자를 default 생성자라고 부른다.

    위와 같이 디폴트 생성자를 구현하면 new Dog() 로 Dog 객체가 만들어 질 때 위 디폴트 생성자가 실행된다.

    만약 클래스에 생성자가 하나도 없다면 컴파일러는 자동으로 위와같은 디폴트 생성자를 추가한다. 하지만 사용자가 작성한 생성자가 하나라도 구현되어 있다면 컴파일러는 디폴트 생성자를 추가하지 않는다.

    ※ 이러한 이유로 위에서 살펴본 HouseDog 클래스에 name을 입력으로 받는 생성자를 만든 후에 new HouseDog() 는 사용할 수 없게 되는 것이다. (HouseDog클래스에 이미 생성자를 만들었기 때문에 컴파일러는 디폴트 생성자를 자동으로 추가하지 않는다.)


    생성자 오버로딩

    하나의 클래스에 여러개의 입력항목이 다른 생성자를 만들 수 있다.

    위 HouseDog 클래스는 두 개의 생성자가 구현되어 있다. 하나는 String 자료형을 입력으로 받는 생성자이고 다른 하나는 int 자료형을 입력으로 받는 생성자이다. 두 생성자의 차이는 입력 항목이다. 이렇게 입력 항목이 다른 생성자를 여러 개 만들 수 있는데 이런 것을 생성자 오버로딩(Overloading) 이라고 말한다. (※ 메소드 오버로딩과 마찬가지 개념이다.)

    이제 HouseDog 객체는 다음과 같이 두 가지 방법으로 생성이 가능하다.

    HouseDog happy = new HouseDog("happy");
    HouseDog yorkshire = new HouseDog(1);
    

    main메소드를 실행하면 다음과 같은 결과가 출력될 것이다.

    happy
    yorkshire



    인터페이스

    이런 케이스를 코드로 담아보자.

    Animal.java

    public class Animal {
        String name;
    
        public void setName(String name) {
            this.name = name;
        }
    }
    

    Tiger.java

    public class Tiger extends Animal {
    
    }
    

    Lion.java

    public class Lion extends Animal {
    
    }
    

    ZooKeeper.java

    public class ZooKeeper {
        public void feed(Tiger tiger) {
            System.out.println("feed apple");
        }
    
        public void feed(Lion lion) {
            System.out.println("feed banana");
        }
    
        public static void main(String[] args) {
            ZooKeeper zooKeeper = new ZooKeeper();
            Tiger tiger = new Tiger();
            Lion lion = new Lion();
            zooKeeper.feed(tiger);
            zooKeeper.feed(lion);
        }
    }
    

    이전 챕터에서 보았던 Dog 클래스와 마찬가지로 Animal을 상속한 Tiger와 Lion이 등장했다. 그리고 사육사 클래스인 ZooKeeper 클래스가 위와 같이 정의되었다. ZooKeeper 클래스는 호랑이가 왔을 때, 사자가 왔을 때 각각 다른 feed 메소드가 호출된다. *feed는 메소드 오버로딩

    동물원에 호랑이와 사자뿐이라면 ZooKeeper 클래스는 완벽하겠지만 악어, 표범등이 계속 추가된다면 ZooKeeper는 육식동물이 추가될 때마다 매번 다음과 같은 feed 메소드를 추가해야 한다.

    ...
    
    public void feed(Crocodile crocodile) {
        System.out.println("feed strawberry");
    }
    
    public void feed(Leopard leopard) {
        System.out.println("feed orange");
    }
    
    ...
    

    이렇게 육식동물이 추가 될 때마다 feed 메소드를 추가해야 한다면 사육사(ZooKeeper)가 얼마나 귀찮겠는가?

    이런 어려움을 극복하기 위해서 이제 인터페이스의 마법을 부려보자.

    다음과 같이 육식동물(Predator) 인터페이스를 작성 해 보자.

    Predator.java

    public interface Predator {
    
    }
    

    위 코드와 같이 인터페이스는 class가 아닌 interface 라는 키워드를 이용하여 작성한다.

    public class Lion extends Animal implements Predator {
    
    }
    

    인터페이스 구현은 위와같이 implements 라는 키워드를 사용한다.

    Tiger, Lion이 Predator 인터페이스를 구현하면 ZooKeeper 클래스의 feed 메소드를 다음과 같이 변경 할 수 있다.

    변경전

    public void feed(Tiger tiger) {
        System.out.println("feed apple");
    }
    
    public void feed(Lion lion) {
        System.out.println("feed banana");
    }
    

    변경후

    public void feed(Predator predator) {
        System.out.println("feed apple");
    }

    feed 메소드의 입력으로 Tiger, Lion을 각각 필요로 했지만 이제 이것을 Predator라는 인터페이스로 대체할 수 있게 되었다. tiger, lion은 각각 Tiger, Lion의 객체이기도 하지만 Predator 인터페이스의 객체이기도 하기 때문에 위와같이 Predator를 자료형의 타입으로 사용할 수 있는 것이다.

    • tiger - Tiger 클래스의 객체, Predator 인터페이스의 객체
    • lion - Lion 클래스의 객체, Predator 인터페이스의 객체

    ※ 이와같이 객체가 한 개 이상의 자료형 타입을 갖게되는 특성을 다형성(폴리모피즘)이라고 하는데 이것에 대해서는 "다형성" 챕터에서 자세히 다루도록 한다.


    자, 그런데 위 ZooKeeper 클래스에 약간의 문제가 발생했다. 아래의 ZooKeeper클래스의 feed 메소드를 보면 호랑이가 오던지, 사자가 오던지 무조건 "feed apple" 이라는 문자열을 출력한다. 사자가 오면 "feed banana" 를 출력해야 하지 않겠는가!


    역시 인터페이스의 마법을 부려보자.

    Predator 인터페이스에 다음과 같은 메소드를 추가 해 보자.

    Predator.java

    public interface Predator {
        public String getFood();
    }
    

    getFood 라는 메소드를 추가했다. 그런데 좀 이상하다. 메소드에 몸통이 없다?

    인터페이스의 메소드는 메소드의 이름과 입출력에 대한 정의만 있고 그 내용은 없다. 그 이유는 인터페이스는 규칙이기 때문이다. 위에서 설정한 getFood라는 메소드는 인터페이스를 implements한 클래스들이 구현해야만 하는 것이다.


    육식 동물들의 종류만큼의 feed 메소드가 필요했던 ZooKeeper 클래스를 Predator 인터페이스를 이용하여 구현했더니 단 한개의 feed 메소드로 구현이 가능해졌다. 여기서 중요한 점은 메소드의 갯수가 줄어들었다는 점이 아니라 ZooKeeper클래스가 동물들의 종류에 의존적인 클래스에서 동물들의 종류와 상관없는 독립적인 클래스가 되었다는 점이다. 바로 이 점이 인터페이스의 핵심이다.


    아마도 여러분은 컴퓨터의 USB 포트에 대해서 알고 있을 것이다. USB 포트에 연결할 수 있는 기기는 하드디스크, 메모리스틱, 디지털카메라 등등 무척 많다.

    바로 이 USB포트가 물리적 세계의 인터페이스라고 할 수 있다.

    USB포트의 규격만 알면 어떤 기기도 만들 수 있다. 또 컴퓨터는 USB 포트만 제공하고 어떤 기기가 만들어지는 지 신경쓸 필요가 없다. 바로 이 점이 인터페이스의 핵심이다.



    다형성

    객체지향 프로그래밍의 특징 중에 다형성(폴리모피즘, Polymorphism)이라는 것이 있다.

    도대체 폴리모피즘은 무엇이고 이게 왜 필요한 걸까?

    Bouncer.java

    public class Bouncer {
        public void barkAnimal(Animal animal) {
            if (animal instanceof Tiger) {
                System.out.println("어흥");
            } else if (animal instanceof Lion) {
                System.out.println("으르렁");
            }
        }
    
        public static void main(String[] args) {
            Tiger tiger = new Tiger();
            Lion lion = new Lion();
    
            Bouncer bouncer= new Bouncer();
            bouncer.barkAnimal(tiger);
            bouncer.barkAnimal(lion);
        }
    }
    

    barkAnimal 메소드는 입력으로 받은 animal 객체가 Tiger의 객체인 경우에는 "어흥"을 출력하고 Lion 객체인 경우에는 "으르렁"을 출력하게 한다.

    ※ instanceof 는 특정 객체가 특정 클래스의 객체인지를 조사할 때 사용되는 자바의 내장 키워드이다. animal instanceof Tiger 는 "animal 객체가 new Tiger로 만들어진 객체인가?" 를 묻는 조건식이다.

    *barkAnimal 메소드의 입력자료형은 Tiger나 Lion이 아닌 Animal이다. IS-A 관계


    Crocodile, Leopard 등이 추가되면 barkAnimal 메소드는 다음처럼 수정되어야 할 것이다.

    public void barkAnimal(Animal animal) {
        if (animal instanceof Tiger) {
            System.out.println("어흥");
        } else if (animal instanceof Lion) {
            System.out.println("으르렁");
        } else if (animal instanceof Crocodile) {
            System.out.println("쩝쩝");
        } else if (animal instanceof Leopard) {
            System.out.println("캬옹");
        }
    }
    

    흠, 별로 좋지 않다. 마음이 무거워 진다.

    우리는 인터페이스를 배웠으므로 좀 더 나은 해법을 찾을 수 있을 것 같다.

    자, 다음처럼 Barkable 이란 인터페이스를 작성 해 보자.

    Barkable.java

    public interface Barkable {
        public void bark();
    }

    Tiger.java

    public class Tiger extends Animal implements Predator, Barkable {
        public String getFood() {
            return "apple";
        }
    
        public void bark() {
            System.out.println("어흥");
        }
    }

    이렇게 Tiger, Lion 클래스에 bark 메소드를 구현하면 Bouncer 클래스의 barkAnimal 메소드를 다음처럼 수정할 수 있다.

    바뀌기 전

    public void barkAnimal(Animal animal) {
        if (animal instanceof Tiger) {
            System.out.println("어흥");
        } else if (animal instanceof Lion) {
            System.out.println("으르렁");
        }
    }
    

    바뀐 후

    public void barkAnimal(Barkable animal) {
        animal.bark();
    }

    복잡하던 조건분기문도 사라지고 누가봐도 명확한 코드가 되어 버렸다. Amazing!!


    ※ 폴리모피즘을 이용하면 위의 예에서 보듯이 복잡한 if else 의 조건문을 간단하게 처리할 수 있는 경우가 많다.

    위 예제에서 사용한 tiger, lion 객체는 각각 Tiger, Lion 클래스의 객체이면서 Animal 클래스의 객체이기도 하고 Barkable, Predator 인터페이스의 객체이기도 하다. 이러한 이유로 barkAnimal 메소드의 입력 자료형을 Animal에서 Barkable 로 바꾸어 사용할 수 있는 것이다.

    이렇게 하나의 객체가 여러개의 자료형 타입을 가질 수 있는 것을 객체지향 세계에서는 다형성, 폴리모피즘(Polymorphism)이라고 부른다.

    즉 Tiger 클래스의 객체는 다음과 같이 여러가지 자료형으로 표현할 수 있다.

    Tiger tiger = new Tiger();
    Animal animal = new Tiger();
    Predator predator = new Tiger();
    Barkable barkable = new Tiger();
    

    여기서 알아두어야 할 사항은 Predator 로 선언된 predator 객체와 Barkable 로 선언된 barkable 객체는 사용할 수 있는 메소드가 서로 다르다는 점이다. predator 객체는 getFood() 메소드가 선언된 Predator 인터페이스의 객체이므로 getFood 메소드만 호출이 가능하다. 이와 마찬가지로 Barkable 로 선언된 barkable 객체는 bark 메소드만 호출이 가능하다.

    만약 getFood 메소드와 bark 메소드를 모두 사용하고 싶다면 어떻게 해야 할까?

    Predator, Barkable 인터페이스를 구현한 Tiger 로 선언된 tiger 객체를 사용하거나 다음과 같이 getFood, bark 메소드를 모두 포함하는 새로운 인터페이스를 새로 만들어 사용하면 된다.

    BarkablePredator.java

    public interface BarkablePredator  {
        public void bark();
        public String getFood();
    }
    

    또는

    public interface BarkablePredator extends Predator, Barkable {
    }
    

    두번 째 방법은 기존의 인터페이스를 활용하는 방법이다. 두 번째 방법대로 하면 Predator의 getFood 메소드, Barkable의 bark 메소드를 그대로 상속받을 수 있다.

    인터페이스는 일반 클래스와는 달리 extends 를 이용하여 여러개의 인터페이스(Predator, Barkable)를 동시에 상속할 수 있다. 즉, 다중 상속이 지원된다. (※ 일반 클래스는 단일상속만 가능하다.)

    Bouncer 클래스의 barkAnimal 메소드의 입력 자료형이 Barkable이더라도 BarkablePredator를 구현한 lion객체를 전달 할 수 있다. 그 이유는 BarkablePredator는 Barkable 인터페이스를 상속받은 자식 인터페이스이기 때문이다. 자식 인터페이스로 생성한 객체의 자료형은 부모 인터페이스로 사용하는 것이 가능하다. (자식 클래스의 객체 자료형을 부모 클래스의 자료형으로 사용가능하다는 점과 동일하다.)


    추상클래스

    추상클래스(Abstract Class)는 인터페이스의 역할도 하면서 뭔가 구현체도 가지고 있는 자바의 돌연변이 같은 클래스이다. 혹자는 추상클래스는 인터페이스로 대체하는것이 좋은 디자인이라고도 얘기한다.

    추상 클래스에는 abstract 메소드 뿐만 아니라 실제 메소드도 추가가 가능하다. 추상클래스에 실제 메소드를 추가하면 Tiger, Lion 등으로 만들어진 객체에서 그 메소드들을 모두 사용할 수 있게 된다.

    예를 들어 아래와 같이 isPredator 라는 메소드를 Predator 추상클래스에 추가하면 이 클래스를 상속받은 Tiger, Lion 등에서 사용가능하게 된다.

    public abstract class Predator extends Animal {
        public abstract String getFood();
    
        public boolean isPredator() {
            return true;
        }
    }