자바(10) - 인터페이스

|

인터페이스

인터페이스(interface)는 클래스 혹은 프로그램이 제공하는 기능을 명시적으로 선언하는 역할을 한다. 인터페이스는 추상 메서드와 상수로만 이루어져 있다. 구현된 코드가 없기에 인터페이스로 인스턴스를 생성할 수 없다. 인터페이스 선언은 다음과 같다.

public interface Calc {
    double PI = 3.14;
    int ERROR = -9999999;

    int add(int num1, int num2);
    int substract(int num1, int num2);
} 

이런식으로 만들면 컴파일 과정에서 변수들은 상수로(public static final) 변환되고, 메서드들은 추상 메서드(public abstract)로 변환이 된다.

기능을 명시했으니 실제 클래스로 구현을 해야 한다. 인터페이스를 구현한다는 의미이기에 예약어로 implemnets를 사용한다.

public abstract class Calculator implements Calc {
    @Override
    public int add(int num1, int num2) {
        return num1 + num2;
    }
}

public class CompleteCalc extends Calculator {
    @Override
    public int substract(int num1, int num2) {
        return num1 - num2;
    }

    public void showInfo() {
        System.out.println("인터페이스 구현");
    }
}

인터페이스를 implementation하려면 추상클래스와 마찬가지로 오버라이딩을 해줘서 메서드를 구현해주어야 한다. 위 예제 Calculator 클래스처럼 전부 구현하지 않으면 추상 클래스로 선언해주어야 하고, CompleteCalc는 추상 클래스를 상속받았으니 나머지 남은 메서드를 구현해주면 된다. 인터페이스도 상속처럼 마찬가지로 하위 클래스는 상위 클래스 자료형으로 묵시적 형 변환을 할 수 있다.

Calc calc = new CompleteCalc();

이러면 상속때처럼 동일하게 Calc형의 변수와 메서드들만 사용할 수 있게 된다.

인터페이스와 다형성

근데 추상 클래스랑 비슷해 보이는 이 인터페이스를 왜 사용할까. 인터페이스는 클라이언트 프로그램에 어떤 메서드를 제공하는지 미리 알려주는 명세(specification) 또는 약속의 역할을 한다. 예를 들어 Abc 인터페이스를 구현한 A클래스가 있다. 이 클래스를 사용하는 Z 프로그램이 있다 치자. Abc인터페이스는 구현할 추상 메서드가 모두 선언되어 있고, 어떤 매개 변수가 사용되는지, 어떤 자료형 값이 반환되는지 선언되어있다. 즉 Z 프로그램은 A 클래스의 구현 코드를 보지 않아도 Abc 인터페이스의 선언부를 보고 A클래스의 사용법을 알 수 있다.

우리가 나중에 다룰 Spring에서도 다양한 데이터베이스를 사용할 것이다. mysql, postgresql 등 서로 다른 종류들이다. 하지만 이 DB들에 대한 접근들도 인터페이스로 명세되어있기에 우리는 인터페이스의 내용만 살표보면 이용할 수 있게 된다. 이런것들이 가능한 것들이 결국엔 다형성 덕분이다. 다형성을 활용해보도록 하자.

시나리오

콜센터가 있는데 전화가 오면 대기열에 저장이 되고, 상담원이 지정되기 전까지 대기 상태가 된다. 전화를 상담원에게 배분하는 정책은 여러가지 방식이 있다.

  1. 순서대로 배분하기: 모든 상담원이 동일한 상담 건수를 처리하도록 들어오는 전화 순서대로 상담원에게 하나씩 배분.
  2. 짧은 대기열 찾아 배분하기: 고객 대기 시간을 줄이기 위해 상담을 하지 않는 상담원이나 가장 짧은 대기열을 보유한 상담원에게 배분.
  3. 우선순위에 따라 배분하기: 고객 등급에 따라 등급이 높은 고객의 전화를 우선 가져와서 업무 능력이 좋은 상담원에게 우선 배분.

먼저 Scheduler 인터페이슬 만들어 보자. 해당 인터페이스에는 다음 전화를 가져오는 getNextCall()과 상담원에 전화를 배분하는 sendCallToAgent() 메서드를 선언한다.

public interface Scheduler {
    public void getNextCall();
    public void sendCallToAgent();
}

이후 방식에 맞는 클래스들을 구현한다.

// 1
public class RoundRobin implements Sceduler {
    @Override
    public void getNextCall() {
        System.out.println("상담 전화를 순서대로 대기열에서 가져온다.");
    }

    @Override
    public void sendCallToAgent() {
        System.out.println("다음 순서 상담원에게 배분");
    }
}

// 2
public class LeastJob implements Sceduler {
    @Override
    public void getNextCall() {
        System.out.println("상담 전화를 순서대로 대기열에서 가져온다.");
    }

    @Override
    public void sendCallToAgent() {
        System.out.println("현재 상담 업무가 없거나 대기가 적은 상담원에게 할당.");
    }   
}

// 3
public class PriorityAllocation implements Sceduler {
    @Override
    public void getNextCall() {
        System.out.println("고객 등급이 높은 고객의 전화를 먼저 가져온다.");
    }

    @Override
    public void sendCallToAgent() {
        System.out.println("업무 능력이 좋은 상담원에게 우선적으로 배분");
    }   
}

테스트

public class SchedulerTest {
    public static void main(String[] args) throws IOException {
        System.out.println("R: 한명씩 차례로");
        System.out.println("L: 쉬고 있거나 대기가 가장 적은 상담원에게 할당");
        System.out.println("P: 우선순위가 높은 고객 먼저 할당");

        int ch = System.in.read();
        Scheduler sc = null;

        if(ch == 'R' || ch == 'r') {
            sc = new RoundRobin();
        } else if(ch == 'L' || ch == 'l') {
            sc = new RoundRobin();
        } else if(ch == 'P' || ch == 'p') {
            sc = new RoundRobin();
        } else {
            System.out.println("지원되지 않는 기능");
            return;
        }

        // 어떤 정책인가와 상관없이 인터페이스에 선언한 메서드 호출
        sc.getNextCall();
        sc.sendCallToAgent();
    }
}

인터페이스 요소

인터페이스는 추상 메서드로 이루어지므로 인스턴스를 생성할 수 없고 멤버 변수도 사용할 수 없다. 하지만 아래처럼 구성해도 오류가 나지 않는다. 이것은 컴파일하면서 public static final double PI = 3.14 이런식으로 상수로 바꾸어주기 때문이다.

public interface Calc {
    double PI = 3.14;
    int ERROR = -9999999;
}

디폴트 메서드와 정적 메서드

자바 7까지는 인터페이스에서 추상 메서드와 상수, 이 두가지 요소만 선언해서 사용할 수 있었다. 그런데 어떤 인터페이스를 구현한 여러 클래스에서 사용할 메서드가 클래스마다 같은 기능을 제공하는 경우가 있다. 자바 7은 이런 문제를 해결하기 어려워 클래스마다 일일히 구현을 해주어야 하고, 클래스를 생성하지 않아도 사용할 수 있는 메서드가 필요한 경우도 있는데, 인터페이스만으로는 메서드를 호출할 수 없어 불편했다.

이런 문제점을 해결하기 위해서 자바 8부터 인터페이스에 디폴트 메서드와 정적 메서드 기능을 제공한다. 디폴트 메서드는 인터페이스에서 구현 코드까지 작성한 메서드이다. 즉 인터페이스를 구현한 클래스에서 기본적으로 제공할 메서드인 것이다. 정적 메서드는 인스턴스 생성과 상관없이 사용할 수 있는 메서드이다. 그렇다고 인터페이스로 인스턴스를 만들 수 있는 것은 아니니 주의하자.

public interface Calc {

    // 디폴트 메서드 default 예약어 필요
    default void description() {
        System.out.println("정수 계산기를 구현");
    }

    // 정적 메서드
    static int total(int[] arr) {
        int total = 0;
        
        for(int i: arr) {
            total += i;
        }
        return total;
    }
}

// 디폴트 메서드도 오버라이딩 할 수 있다. 인터페이스 본래 메서드를 부르려면 super를 쓴다.
public class CompleteCalc extends Calculator {
    @Override
    public void description() {
        super.description();
    }
}

public class CalculatorTest {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};
        // 인터페이스 정적 메서드 활용
        System.out.print(Calc.total(arr));
    }
}

private 메서드

자바 9부터 인터페이스에 private 메서드를 구현할 수 있다. private 메서드는 인터페이스를 구현한 클래스에서 사용하거나 재정의할 수 없다. 즉 기존에 구현된 코드를 변경하지 않고 인터페이스를 구현한 클래스에서 공통으로 사용하는 경우에 private 메서드로 구현하면 코드 재사용성을 높일 수 있다. private 메서드는 코드를 모두 구현해야 하므로 추상 메서드에 private예약어를 사용할 수 없고 정적 메서드에는 사용 가능하다.

public interface Calc {
    default void description() {
        myMethod();
    }

    static int total(int[] arr) {
        int total = 0;

        for(int i: arr) {
            total += i;
        }
        myStaticMethod();
        return total;
    }

    private void myMethod() {
        System.out.print("private method");
    }

    private static void myStaticMethod() {
        System.out.println("private static method");
    }
}

인터페이스 활용하기

한 클래스가 여러 클래스를 상속받으면 메서드 호출이 모호해지는 문제가 발생할 수 있다. 하지만 인터페이스는 한 클래스가 여러 인터페이스를 구현할 수 있다. 예를들어 고객 클래스가 있는데 고객은 물건을 살 수도 팔 수도 있다. 클래스를 나누어 구현해보자.

public interface Buy {
    void buy();
}

public interface Sell {
    void sell();
}

public class Customer implements Buy, Sell {
    @Override
    public void sell() {
        System.out.println("sell");
    }

    @Override
    public void buy() {
        System.out.println("buy");
    }
}

public class CustomerTest {
    public static void main(String[] args) {
        Customer customer = new Customer();

        Buy buyer = customer;
        buyer.buy();

        Sell seller = customer;
        seller.sell();

        if(seller isinstanceof Custoer) {
            Customer customer2 = (Customer)seller;
            customer2.buy();
            customer2.sell();
        }
    }
}

두 인터페이스의 디폴트 메서드가 중복되는 경우

정적 메서드는 인스턴스 생성과 상관없이 사용할 수 있다. Customer 클래스가 Buy, Sell 두 인터페이스를 구현하고 Buy 인터페이스와 Sell 인터페이스에 똑같은 pay() 정적 메서드가 있다 보자. 이 경우엔 Buy.pay()와 Sell.pay()로 호출하여 문제가 되지 않는데 디폴트 메서드는 문제가 된다. 두 인터페이스에 order()라는 메서드가 있으면 Customer 클래스에서 중복 에러가 나온다. 이런 경우 구현 클래스에서 오버라이딩 해주어야 한다.

인터페이스 상속하기

인터페이스 간에도 상속이 가능하다. 인터페이스 간 상속은 구현 코드를 통해 기능을 상속하는 것이 아니기에 형 상속(type inheritaance)라고 부른다. 클래스의 경우에는 하나의 클래스만 상속받을 수 있지만, 인터페이스는 여러 개 동시에 상속받을 수 있다. 한 인터페이스가 여러 인터페이스를 상속받으면, 상속받은 인터페이스는 상위 인터페이스에 선언한 추상 메서드를 모두 가지게 된다.

public interface X {
    void x();
}

public interface Y {
    void y();
}

public interface MyInterface extends X, Y {
    void myMethod();
}

public class MyClass implements MyInterface {
    @Overrid
    public void x() {
        System.out.println("X");
    }

    @Overrid
    public void y() {
        System.out.println("Y");
    }

    @Overrid
    public void myMethod() {
        System.out.println("myMethod");
    }
}

public class MyClassTest {
    public static void main(String[] args) {
        MyClass mClass = new MyClass();

        // X에 선언한 메서드만 호출 가능
        X xClass = mClass;
        xClass.x();

        // Y에 선언한 메서드만 호출 가능
        Y yClass = mClass;
        yClass.y();

        // 모든 메서드 호출 가능
        MyInterface iClass = mClass;
        iClass.myMethod();
        iClass.x();
        iClass.y();
    }
}

인터페이스 구현과 클래스 상속 함께 쓰기

한 클래스에서 클래스 상속과 인터페이스 구현을 모두 할 수도 있다.

public class Shelf {
    protected ArrayList<String> shelf;
    
    public Shelf() {
        shelf = new ArrayList<String>)();
    }

    public ArrayList<String> getShelf() {
        return shelf;
    }

    public int getCount() {
        return shelf.size();
    }
}

public interface Queue {
    void enQueue(String title);
    String deQueue();
    int getSize();
}

public class BookShelf extends Shelf implements Queue {
    @Override
    public void enQueue(String title) {
        shelf.add(title);
    }

    @Overrid
    public String deQueue() {
        return shelf.remove(0);
    }

    @Override
    public int getSize() {
        return getCount();
    }
}

public class BookShelfTest {
    public static void main(String[] args) {
        Queue shelfQueue = new BookShlef();
        shelfQueue.enQueue("book1");
        shelfQueue.enQueue("book2");
        shelfQueue.enQueue("book3");

        System.out.println(shelfQueue.deQueue());
        System.out.println(shelfQueue.deQueue());
        System.out.println(shelfQueue.deQueue());
    }
}

자바(9) - 추상클래스

|

추상 클래스

추상적이다의 의미는 구체적이지 않고 막연한 것을 뜻한다. 추상 클래스 또한 구체적이지 않은 클래스라고 보면 된다. 추상 클래스를 영어로는 abstract class라고 하고 추상 클래스가 아닌 클래스는 concrete class라고 한다. 이전까지 썼던 클래스들은 전부 concrete 클래스였던 것이다. 추상 클래스는 항상 추상 메서드를 포함한다. 추상 메서드는 구현 코드가 없다. 즉 몸체가 없다는 뜻이다. {}로 감싼 부분을 함수의 구현부(implementation)라고 하는데 이 부분이 없다는 것이다. 추상 메서드는 아래와 같이 선언한다. 참고로 추상 메서드는 new예약어로 인스턴스를 생성할 수 없다.

abstract int add(int x, int y);

선언은 일반 함수랑 비슷해 보이지만 앞에 abstract이라는 예약어가 붙고 중괄호 대신 끝에 세미 콜론으로 마무리 된다. 추상 클래스를 한 번 구현해보자. 먼저 컴퓨터라는 클래스를 만들어 보겠다. 컴퓨터에서 파생되는 종류로는 데스크탑과 노트북이 있다. 컴퓨터의 기능으로는 display, typing이 있고 전원 켜기, 끄기 같은 기능이 있다. 켜기 끄기는 데탑, 노트북의 공통 기능들이니 추상 클래스에서 구현해보도록 한다.

public abstract class Computer {
    public abstract void display();
    public abstract void typing();
    public void turnOn() {
        System.out.println("전원을 켭니다.");
    }
    public void turnOff() {
        System.out.println("전원을 끕니다.");
    }
}

public class DeskTop extends Computer {
    @Override
    public void display() {

    }

    @Override
    public void typing() {

    }
}

public abstract class NoteBook extends Computer {
    @Override
    public void display() {
        System.out.println("NoteBook display()");
    }
}

public class MyNoteBook extends NoteBook {
    @Override
    public void typing() {
        System.out.println("MyNoteBook typing()");
    }
}

추상 클래스 컴퓨터를 상속받은 데스크탑은 반드시 추상메서드를 오버라이딩해서 구현해주어야 한다. 만약 상속받은 클래스에서 추상 메서드들에 대해 전부 구현하지 않으면 그 클래스 또한 추상 클래스로 선언해주어야 하고, 최종적이 클래스에서 남은 추상메서드들을 다 구현해주어야 한다.

템플릿 메서드

템플릿이란 틀이나 견본을 뜻한다. 템플릿 메서드는 틀이 있는 메서드라는 의미이다. 템플릿 메서드는 디자인 패턴의 한 방법으로 모든 객체 지향 프로그램에서 사용하는 구현 방법이다. 아래는 예제이다.

public abstract class Car {
    public abstract void drive();
    public abstract void stop();

    public void startCar() {
        System.out.println("시동 킴");
    }

    public void turnOff() {
        System.out.println("시동 끔");
    }

    // 이게 템플릿 메서드이다.
    final public void run() {
        startCar();
        drive();
        stop();
        turnOff();
    }
}

2개의 추상 메서드와 3개의 구현 메서드가 있다. 시동 on/off메서드는 자동차의 공통 부분이라 추상메서드에서 구현한 것이다. run()메서드는 자동차가 달리는 방법을 순서대로 구현해 둔 것이다. Car 클래스를 상속받으면 어떤 자동차든 모두 이 순서대로 실행된다. 우리는 아래 상속받은 클래스처럼 추상 메서드 부분만 구현해주면 된다.

public class AICar extends Car {
    @Override
    public void drive() {
        System.out.println("AI 운전");
        System.out.println("AI 핸들 조작");
    }

    @Override
    public void stop() {
        System.out.println("AI 정지");
    }
}

public class ManualCar extends Car {
    @Override
    public void drive() {
        System.out.println("사람이 운전");
        System.out.println("사람이 핸들 조작");
    }

    @Override
    public void stop() {
        System.out.println("브레이크로 정지");
    }
}

public class CarTest {
    public static void main(String[] args) {
        Car myCar = new AICar();
        myCar.run();

        Car hisCar = new ManualCar();
        hisCar.run();
    }
}

테스트 부분을 보면 인스턴스들을 만들고 run을 했다. 자동차의 작동 순서는 AI나 수동이나 동일하다. 시동을 키고, 주행하고, 멈추고 시동을 끈다. 그렇기에 템플릿 메서드를 활용하여 메서드 실행 순서와 시나리오를 정의하는 것이다. 이 내용을 하위 클래스에서 재정의 하면 안되기에 final 예약어를 사용하여 재정의 할 수 없게 만든다.

final 예약어

final을 사용하면 더 이상 수정할 수 없음을 의미하는 것이다. |사용위치|설명| |—–|—| |변수|상수를 의미| |메서드|하위 클래스에서 재정의할 수 없음| |클래스|상속할 수 없음|

자바(8) - 상속과 다형성

|

상속

객체 지향 프로그래밍의 중요한 특징 중 하나가 상속(inheritance)이다. 상속은 우리 일상생활에서 흔히 부모님의 재산을 물려받을 때 많이 쓰이는 용어이다. 자바에서의 상속도 마찬가지로 상위 클래스의 내용을 물려 받는다는 의미로 상속이 사용된다.

문법으로는 아래와 같다. extends는 연장, 확장하다의 의미로 Mammal 클래스가 가진 속성이나 기능을 추가 확장하여 Huamn 클래스를 구현한다는 뜻이다.

class Mammal {
    private int age;
    String name;
    void walk() {}
}

class Dog extend Mammal {
    void bark() {}
}

class Human extends Mammal {
    void programming() {}
}

포유류들의 공통 기능으로 나이와 이름 또 걷는 메서드가 있다. 이런 속성들은 상속받고, Dog 클래스만의 기능인 짖는 메서드를 구현해주고, 사람의 고유 기능인 프로그래밍 메서드를 지어주면 된다. 그런데 위 Mammal에서 int age를 private으로 선언해 주었다. private의 범위는 클래스내에서만 이기 때문에 상속받은 클래스들이 위 멤버 변수를 활용 못한다. 이럴 때는 우리가 이전에 배웠던 protected로 선언해주면 상속받는 클래스들도 문제없이 사용할 수 있다.

상속받은 클래스의 인스턴스를 생성하면 상위 클래스의 생성자가 먼저 호출된다. 그렇게 하여 상위 클래스의 멤버 변수가 메모리에 생성되는 것이다.

super

super예약어는 하위 클래스에서 상위 클래스로 접근할 때 사용한다. 하위 클래스는 상위 클래스의 주소, 즉 참조 값을 알고 있다. 이 참조 값을 가지고 있는 예약어가 바로 super이다. 반대로 this는 자기 자신의 참조 값을 가지고 있다 알면 된다. 기본적으로 하위 클래스의 생성자가 실행될 때 상위 클래스의 생성자가 실행된다 했는데, 이것은 컴파일러에서 자동으로 super()를 호출해 주기 때문이다. 이는 상위 클래스의 디폴트 생성자를 호출하는 것이다.

Implicit super consturctor 클래스() is undefined. Must explicitly invoke another consturcotr. 이 에러는 묵시적으로 호출될 디폴트 생성자가 정의되지 않았기 때문에, 반드시 명시적으로 다른 생성자를 호출해야 한다는 뜻이다.

super를 활용하면 상위 클래스의 멤버 변수나 메서드를 하위 클래스에서 참조할 수 있다.

public String showVIPInfo() {
    return super.showCustomerInfo() + "담당 상담원의 아이디는 " + agentID + "입니다.";
}

이런식으로 하면 상위 클래스의 showCustomerInfo메섣드를 호출해서 사용하는 것이다.

상위 클래스로 묵시적 클래스 형 변환

상속에서 중요하게 다뤄야 할 부분이 클래스 간의 형 변환이다. Customer와 VIPCustomer의 관계를 봐보면, 개념적인 면에서는 Customer가 일반적인 개념이고, 기능적인 면에서는 VIPCustomer가 더 많다. 왜냐하면 상속받은 클래스는 상위 클래스 기능을 모두 사용할 수 있고 추가로 더 많은 기능을 구현했기 때문이다. 따라서 VIPCustomer는 VIPCustomer형이면서 Customer형이기도 한다. 그렇기에 VIPCustomer클래스로 인스턴스를 생성할 때 이 인스턴스의 자료형을 Customer형으로 클래스 형 변환하여 선언할 수 있다. 하지만 반대로 Customer 인스턴스를 생성할 때 VIPCustomer형으로 선언할 수 없다. 즉 반대로는 불가하다. 그런데 VIPCustomer로 new 해서 만들면 인스턴스가 만들어져 메모리에 변수하고 올라갈 것이다. 그런데 클래스의 자료형이 Customer로 한정되어서 Customer의 멤버 변수와 메서드만 접근할 수 있게 된다.

//상위 클래스형      하위 클래스형
Customer vc = new VIPCustomer();

책을 참고한 거라 코드 필요한 분은 Do it 자바 검색히서 봐보길 바란다.

메서드 오버라이딩

오버라이딩이란 상위 클래스에서 이미 정의된 내용을 현재 클래스에 맞게 재정의함을 의미한다. 오버라이딩을 하려면 반환형, 메서드 이름, 매개 변수 개수, 매개변수 자료형이 반드시 같아야 한다. 그렇지 않으면 자바 컴파일러는 재정의한 메서드를 기존 메서드와 다른 메서드로 인식한다.

// 기존 메서드
public int calcPrice(int price) {
    bonusPoint += price * bonusRatio;
    return price;
}

// 오버라이드한 메서드. 반환형, 이름, 매개변수 개수, 자료형이 같다.
@Override
public int calcPrice(int price) {
    bonusPoint += price * bonusRatio;
    return price - (int)(price * saleRatio);
}

@Override는 어노테이션이라고 컴파일러에게 알려주는 주석같은 것이다. 나중에 스프링 부트를 보면 알지만 이 어노테이션가지고 많은 활용을 한다. 얼른 공부해서 이해해서 써보고 싶다..!

묵시적 클래스 형 변환과 메서드 재정의

다음 코드를 봐보자.

Customer vc = new VIPCustomer("10030", "seokju", 2000);
vc.calcPrice(10000);

이 코드는 묵시적 형 변환이 일어나 VIPCustomer가 Customer형으로 변환이 되었다. 현재 calcPrice는 Customer와 VIPCustomer에 둘다 있는데 어느께 실행될까? 정답은 VIPCustomer의 메서드가 실행된다. 상속에서 상위 클래스와 하위 클래스에 같은 이름의 메서드가 존재할 때 호출되는 메서드는 인스턴스에 따라 결정된다. 즉 선언한 클래스형이 아닌 생성되니 인스턴스의 메서드가 호출되는 것이다. 이것이 가능한 이유는 가상 메서드덕분이다.

가상 메서드

자바의 클래스는 멤버 변수와 메서드로 이루어져 있다. 클래스를 생성하여 인슨턴스가 만들어지면 멤버 변수는 힙 메모리에 위치한다. 그런데 메서드는 다르다. 변수는 인스턴스마다 갖는 값이 다르기에 인스턴스가 만들어질 때마다 새로 생성되지만, 메서드는 실행해야 할 명령 집합이기에 인스턴스가 달라도 같은 로직을 수행한다. 다시 말해 메서드는 인스턴스가 생성될 때마다 만들어지는 것이 아니다. 메서드는 메서드 영역에 만들어져 메서드를 호출하면 그 영역의 주소를 참조하여 명령이 실행되는 것이다. 이를 가상 메서드 테이블에서 관리하게 되고 재정의가 된 메서드는 새로운 주소에 위치하고 참조하게 되는 것이다.

다형성

묵시적 클래스 형 변환과 가상 메서드를 바탕으로 객체 지향의 중요한 특성인 다형성(polymorphism)을 알아보자. 다형성이란 하나의 코드가 여러 자료형으로 구현되어 실행되는 것을 말한다. 다형성은 추상 클래스, 인터페이스에서 구현되며 안드로이드, 스프링 등 자바 기반의 프레임워크에서 응용하는 객체 지향 프로그램에서 중요한 개념이다. 동물 클래스가 있고 사람, 호랑이, 독수리 클래스를 만들어 보자.

class Animal {
    public void move() {
        System.out.println("동물이 움직입니다.");
    }
}

class Human extends Animal {
    public void move() {
        System.out.println("사람이 두 발로 걷습니다.");
    }
}

class Tiger extends Animal {
    public void move() {
        System.out.println("호랑이가 네 발로 움직입니다.");
    }
}

class Eagle extends Animal {
    public void move() {
        System.out.println("독수리가 하늘을 납니다.");
    }
}

public class AnimalTest {
    public static void main(String[] args) {
        AnimalTest aTest = new AnimalTest();
        aTest.moveAnimal(new Human());
        aTest.moveAnimal(new Tiger());
        aTest.moveAnimal(new Eagle());
    }

    public void moveAnimal(Animal animal) {
        animal.move();
    }
}

결과

사람이 두 발로 걷습니다. 호랑이가 네 발로 움직입니다. 독수리가 하늘을 납니다.

moveAnimal 메서드의 매개변수를 보면 Animal임을 볼 수 있다. 그런데도 동작이 잘 되는 이유는 사람, 호랑이, 독수리 모두 Animal 클래스를 상속받았기에 묵시적 형 변환이 일어나는 것이다. 그리고 가상 메서드의 원리에 따라 각 인스턴스의 move() 메서드가 호출된 것이다. 이처럼 어떤 매개변수가 넘어왔느냐에 따라 출력문이 달라지는 효과를 다형성이라 볼 수 있다. 이렇듯 다형성을 활용하면 코드 양도 줄어들고 유지보수도 편리해진다. 또한 확장성도 넓어진다. 이렇게 다형성을 쓰면서 상속이라는 것이 좋아 보이지만 무조건적으로만 쓰면 안된다. 클래스간의 관계를 잘 살펴보아 따져야한다. IS-A와 HAS-A 관계가 있는데, IS-A(is a relationship; inheritance)관계는 일반적인 개념과 추제적인 개념의 관계이다. 가령 ‘사람은 표유류이다’와 같은 관계다. 이런 관계일 때는 상속을 사용하는 것이 좋다. HAS-A(has a relationship; association) 관계는 한 클래스가 다른 클래스를 소유한 관계이다. 예로 학생 클래스와 과목 클래스가 있으면 학생이 여러 과목을 가지는 형태 같은게 되는 것이다. 이런 경우는 멤버 변수같은 것으로 선언하는 것이 적절하다.

다운 캐스팅과 instanceof

위에서 상위 클래스로 형 변환이 묵시적으로 이루어지는 과정을 보았는데, 이번에는 명시적으로 하위 클래스로 형 변환을 하는 것을 알아보겠다. Animal ani = new Human();같은 코드가 있다 보자. 이렇게 Animal형으로 형 변환이 이루어진 경우에는 Animal 클래스에서 선언한 메서드와 멤버 변수만 사용할 수 있다. 이러면 기껏 만들어둔 메서드와 변수를 못쓰게 되기에 원래 자료형으로 돌릴 필요가 있다. 이렇게 형 변환하는 것을 다운 캐스팅이라고 한다.

instanceof

상속 관계를 보면 모든 인간은 동물이지만 모든 동물이 인간은 아니다. 따라서 다운 캐스팅을 하기 이전에 형 변환된 인스턴스의 원래 자료형을 확인해야 오류를 막을 수 있다. 이때 사용하는 예약어가 instanceof이다. 아래와 같이 사용하면 된다.

Animal hAnimal = new Human();
if(hAnimal isinstanceof Human) {    // hAnimal 인스턴스 자료형이 Human이면
    Human human = (Human)hAnimal;   // Human형으로 다운 캐스팅
}

상위 클래스는 묵시적으로 형 변환이 되지만, 하위 클래스로 형 변환을 할 때는 명시적으로 해야 한다.

자바(7) - 배열과 ArrayList

|

배열

지금까지 사용한 변수는 자료 한 개를 저장하기 위한 공간이었다. 그런데 프로그래밍하다보면 자료형이 같은 자료를 여러 개 처리해야하는 일이 있다. 이런 경우 변수를 각각 선언하기 보다는 여러 자료를 한 번에 처리할 수 있는 기능이 필요하고, 이때 사용되는 것이 배열이다.

배열 사용하기

배열의 선언과 초기화 방법은 다음과 같은데, 주로 첫 번째 방식으로 많이 선언한다.

자료형[] 배열이름 = new 자료형[개수];
자료형 배열이름[] = new 자료형[개수];

// 실제
int[] studentIDs = new int[10];

위 실제처럼 만들개 되면 int형 요소가 10개인 배열을 선언한 것이다. int는 4바이트로 구성되니 10개면 총 40바이트의 메모리가 할당되는 것이다.

선언과 동시에 값까지 넣으려면 다음처럼 한다.

int[] studentIDs = {101, 102, 103};

이처럼 하면 3개 길이의 배열이 만들어 지는 것이다.

배열값을 사용하려면 배열변수[숫자]이렇게 사용하면 된다. [] 이것은 인덱스 연산자라 하여 배열 요소가 저장된 메모리 위치를 찾아주는 역할을 한다. 숫자는 0부터 시작한다.

배열길이를 알고자 할 때는 배열변수.length라 해서 .length를 붙혀주면 된다.

객체 배열 사용하기

참조 자료형 또한 배열을 활용할 수 있다. 아래는 Book 클래스이다.

public class Book {
    private String bookName;
    private String author;
    
    public Book() {}
    
    public Book(String bookName, String author) {
        this.bookName = bookName;
        this.author = author;
    }
    
    public void showBookInfo() {
        System.out.println(bookName + "," + author);
    }
}

테스트 클래스로 결과를 봐보자.


public class BookArray {
    public static void main(String[] args) {
        Book[] library = new Book[5];
        
        for(int i=0; i<library.length; i++) {
            System.out.println(library[i]);
        }
    }
}

결과값은 전부 null이 나올 것이다. 우리가 Book[] libray = new Book[5]; 이 코드로 인스턴스 5개를 만든거 같지만 아니다. 저 코드는 그저 인스턴스 주소 값을 담을 공간 5개를 생성하는 것이다. 공간만 있기 때문에 ‘비어 있다’라는 의미인 null값으로 초기화가 되는 것이다. 그렇기에 인스턴스를 만들어 주고 배열이 참조할 수 있도록 해주면 된다.

public class BookArray {
    public static void main(String[] args) {
        Book[] library = new Book[5];
        
        library[0] = new Book("Book1", "A1");
        library[1] = new Book("Book2", "A2");
        library[2] = new Book("Book3", "A3");
        library[3] = new Book("Book4", "A4");
        library[4] = new Book("Book5", "A5");
        
        for(int i=0; i<library.length; i++) {
            System.out.println(library[i]);
        }
    }
}

배열 복사하기

기존 배열과 자료형 및 배열 크기가 똑같은 배열을 새로 만들거나 배열의 모든 요소에 자료가 꽉 차서 더 큰 배열을 만들어 기존 배열에 저장된 자료를 가져오려 할 때 배열을 복사한다. 배열을 만드는 방법으론 두 가지가 있는데, 첫 번째는 for문을 활용하여 복사하는 것인다. 이건 그냥 반복하면 되는 것이다. 두 번째는 System.arraycopy()메서드를 사용하는 방법이다. System.arraycopy(src, srcPos, des, destPos, length)형태이고 의미는 아래 표와 같다. |매개변수|설명| |——-|—-| |src|복사할 배열 이름| |srcPos|복사할 배열의 첫 번째 위치| |dest|복사해서 붙여 넣을 대상 배열 이름| |destPos|복사해서 대상 배열에 붙여 넣기를 시작할 첫 번째 위치| |length|src에서 dest로 자료를 복사할 요소 개수|

예제로 살펴보자

public class ArrayCopy {
    public static void main(String[] args){
        int[] array1 = {10, 20, 30, 40, 50};
        int[] array2 = {1, 2, 3, 4, 5};
        
        System.arraycopy(array1, 0, array2, 1, 4};
        for(int i = 0; i < array2.length; i++) {
            System.out.println(array2[i]);
        }
    }
}

결과값

1 10 20 30 40

만약 length부분을 5개 했다면 길이 문제로 오류가 발생할 것이다.

ArrayList

기존 배열을 사용하려면 항상 배열 길이를 정하고 시작해야 한다. 만약 100명 학생용으로 만들었는데, 학생 수가 100명이 넘어가면 길이 문제가 나타날 것이다. 배열을 사용하는 중에는 배열 길이를 변경할 수 없기에 코드를 수정해야 한다. 또는 배열 중간의 값을 지워야 한다. 그런데 배열은 중간에 있는 요소를 비워둘 수 없기에 위치를 수정해줘야한다. 이러면 많은 수고로움이 생길 것이다. 이를 해결하기 위해 나타난 자료구조로 ArrayList 객체 배열 클래스를 제공한다. 클래스이기 때문에 유용한 메서드들이 많이 제공된다. |메서드|설명| |—–|—-| |boolean add(E e)|요소 하나를 배열에 추가한다. E는 요소의 자료형을 의미한다. |int size()|배열에 추가된 요소 전체 개수를 반환| |E get(int index)|배열의 index 위치에 있는 요소 값을 반환| |E remove(int index)|배열의 index 위치에 있는 요소 값을 제거하고 그 값을 반환| |boolean isEmpty()|배열이 비어 있는지 확인|

add()메서드를 이용하면 배열 길이와 상관없이 객체를 추가할 수 있다. 배열 요소 개수가 부족하면 배열 크기를 더 키워주고, 중간에 요소 값이 제거되면 그 다음 요소 값을 하나씩 앞으로 이동해준다. 선언 방법은 다음과 같다.

ArrayList<E> 배열 이름 = new ArrayList();

자바(6) - 클래스

|

클래스와 객체 2

this

this란 자신의 메모리를 가리키는 예약어이다. 즉, 생성된 인스턴스 스스로를 가르킨다. 이를 활용하여 인스턴스의 멤버 변수라던가 메서드를 호출하여 이것저것 해볼 수 있다. 아래 예제는 this로 다른 생성자 메서드를 호출해보는 것이다.

class Person {
    String name;
    int age;
    
    Person() {
        this("이름 없음", 1);
    }
    
    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class CallAnotherConst {
    public static void main(String[] args) {
        Person noName = new Person();
        System.out.println(noName.name);
        System.out.println(noName.age);
    }
}

우리가 main메서드에서 매개변수가 없는 인스턴스를 생성했다. 위의 코드를 보면 Person()이 실행될텐데, this(“이름 없음”, 1)이 보일 것이다. 이것을 통해 새로운 생성자를 호출하여 멤버 변수들에 값을 추가했다. this로 다른 생성자를 호출할 때 주의할 점으로는 this 이전에 다른 코드를 넣으면 안된다. 아래처럼 하면 IDE에서부터 빨간줄로 에러 표시를 해줄 것이다. 왜냐하면 생성자는 클래스가 생성될 때 호출되므로 클래스 생성이 완료되지 않은 시점에 다른 코드가 있으면 오류가 발생할 수 있기 때문이다.

Person() {
    this.name = "noname";
    this("이름 없음", 1);
}

this를 사용하여 생성된 클래스 자신의 주소 값을 반환할 수 있다. 인스턴스 주소 값을 반환할 때는 this를 사용하고 반환형은 클래스 자료형을 사용한다.

class Person {
    String name;
    int age;
    
    Person() {
        this("이름 없음", 1);
    }
    
    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    Person returnItSelf() { // 자기 자신의 자료형 입력
        return this;
    }
}

public class CallAnotherConst {
    public static void main(String[] args) {
        Person noName = new Person();
        System.out.println(noName.name);
        System.out.println(noName.age);
        
        Person p = noName.returnItSelf();
        System.out.println(p);
        System.out.println(noName);
    }
}

returnItSelf 메서드는 this를 반환하기에 인스턴스 변수 p도 같은 인스턴스를 가르키고 같은 결과가 출력될 것이다.

객체 간 협력

객체 지향 프로그램은 객체를 정의하고 객체 간 협력으로 만드는 것이다. 예를 들어 학생 객체가 있고, 지하철 객체, 버스 객체를 만들어서 학생이 버스를 타면 버스에 맞는 협력이 지하철을 타면 지하철에 맞는 결과가 만들어 지는 것이다. 아래의 예제를 만들어 보자.

학생 클래스

public class Student {
    public String studentName;
    public int grade;
    public int money;
    
    public Student(String studentName, int money) {
        this.studentName = studentName;
        this.money = money;
    }
    
    public void takeBus(Bus bus) {
        bus.take(1000);
        this.money -= 1000;
    }
    
    
    public void takeSubway(Subway subway) {
        subway.take(1000);
        this.money -= 1500;
    }
    
    public void showInfo() {
        System.out.println(studentName + "님의 남은 돈은 " + money + "입니다.");
    }
}

takeBus, Subway 메소드를 보면 매개변수로 Bus와 Subway 자료형을 넣어 주었다. 이는 Bus와 Subway 참조변수로 지정하여 메소드들을 호출하기 위해서이다.

Bus 클래스

Subway클래스도 이름만 바꿔서 똑같이 해주면 된다.

public class Bus {
    int busNumber;
    int passengerCount;
    int money;
    
    public Bus(int busNumber) {
        this.busNumber = busNumber;
    }
    
    public void take(int money) {
        this.money += money;
        passengerCount++;
    }
    
    public void showInfo() {
        System.out.println("버스 " + busNumber + "번의 승객은 " + passengerCount + "명이고, 수입은 " + money + "입니다.");
    }
}

이제 클래스를 만들었으니 협력해보는 코드를 봐보자.

Student st1 = new Studnet("Hong", 5000);

Bus bus100 = new Bus(100);
st1.takeBus(bus100);
st1.showInfo();
bus100.showInfo();

static 변수

학생 클래수에서 학번을 우리가 지정하지 않고 자동으로 생성되게끔 하고 싶다. 그럴려면 생성될 때마다 순서대로 번호를 지정해주어야 하고, 인스턴스 멤버 변수가 아닌 클래스 전반적으로 공통으로 사용하는 변수를 만들 수 있어야 한다. 이를 위해 static 변수를 선언하여 사용한다. static 변수란 다른 용어로 ‘정적 변수’라고 하고 다음과 같이 선언한다.

static int serialNum;

기존 변수 선언에다가 앞에 static만 붙혀주면 된다. static 변수는 다른 멤버변수들 처럼 인스턴스가 생성될 때마다 새로 생성되는 변수가 아닌 프로그램이 실행되어 메모리에 올라갔을 때 딱 한 번 메모리 공간에 할당된다. 그리고 이 값을 모든 인스턴스가 공유하여 사용한다. static으로 선언한 변수는 인스턴스 생성과 상관없이 먼저 생성되고 그 값을 공유한다고 하여 클래스 변수라고도 부른다.

public class Student {
    public static int serialNum = 1000;
    public int studentID;
    
    public Student() {
        serailNum++;
        studentID = serialNum;
    }
}

static 메서드

static 변수가 있다면 static 메서드라는 것도 있다. static 메서드는 인스턴스 생성이 없이 사용할 수 있다. 선언은 다음과 같다. 리턴타입 앞에 static을 붙혀주면 된다.

public static int getSerialNum() {
}

클래스 메서드 내부에서는 인스턴스 변수를 사용할 수 없다. 아래처럼 사용하면 getSerialNum메서드의 두번째 라인에서 에러가 뜰 것이다. 왜냐하면 멤버변수는 인스턴스가 생성될때 할당되는 것인기에 static 메서드에서는 접근할 수 없기 때문이다.

public class Student2 {
    private static int serialNum = 1000;
    String studentName;
    
    public static int getSerialNum() {
        int i = 10;
        studentName = "홍석주";
        return serialNum;
    }
}

변수 유효 범위

세 가지의 변수 타입을 알아봤다.

  1. 함수나 메서드 안에서만 사용할 수 있는 지역 변수(로컬 변수)
  2. 클래스 안에서 사용하는 멤버 변수(인스턴스 변수)
  3. 여러 인스턴스에서 공통으로 사용할 수 있는 static 변수(클래스 변수)

위 변수들은 어디에 어떻게 선언되었는지 에 따라 유효 범위가 달라진다.

지역 변수의 유효 범위

지역 변수는 함수나 메서드 내부에 선언하기에 함수 밖에서 사용할 수 없다. 즉 어떤 함수에 선언된 변수들은 다른 함수들에서 접근이 불가하다. 지역 변수가 생성되는 메모리를 스택이라고 하는데 함수가 호출되면 스택에 쌓였다가 반환되면 메모리 공간이 해제되어 같이 없어진다.

멤버 변수의 유효 범위

멤버 변수는 인스턴스 변수라고도 한다. 클래스가 생성될 때 힙(heap)메모리에 생성된다. 멤버 변수는 클래스의 어느 메서드에서나 사용할 수 있고, 나중에 사용안하면 가비지 컬렉터에 의해 수거되고 사라진다. 따라서 클래스 내부의 여러 메서드에서 사용할 변수는 멤버 변수로 선언하는 것이 좋다.

static 변수의 유효 범위

사용자가 프로그램을 실행하면 메모리에 프로그램이 상주하는데, 이때 프로그램 영역 중에 데이터 영역이 있다. 이 영역에 상수와 문자열과 함께 static 변수가 생성된다. 인스턴스 변수는 객체가 생성되는 문자 new가 되어야 생성되지만, static 변수는 클래스 생성과 상관없이 데이터 영역에 생성된다. 이렇게 생성된 static 변수는 private이 아니라면 클래스 외부에서도 객체 생성과 무관하게 사용할 수 있다. 이후 프로그램 실행이 끝난 뒤 메모리에 내려가면(엑셀 닫기 같은거) static 변수도 소멸한다. static 변수는 프로그램이 시작할 때부터 끝날 때까지 메모리에 상주하므로 크기가 너무 큰 변수를 static으로 선언하는 것은 좋지 않다.

static 응용 - 싱글톤 패턴

프로그램을 구현하다 보면 여러 개의 인스턴스가 필요할 때가 있고, 하나의 인스턴스로만 관리할 때가 있다. 후자처럼 하나의 인스턴스만 생성하는 디자인 패턴을 싱글톤 패턴(singleton pattern)이라고 한다. 이 패턴은 프레임워크에서 많이 사용하는 패턴이라고 한다. 예를들면 회사와 직원들을 객체지향적으로 구현한다면, 직원들은 여러 명이니 여러 인스턴스로 나뉘겠지만 회사는 하나이니 회사를 싱글톤 패턴으로 만드는 것이다. 회사(Compnay)클래스를 기준으로 한 번 테스트 해보자.

프레임워크란 프로그램을 쉽게 개발하기 위해 구체적인 기능 설계와 구현을 미리 만들어 놓은 도구를 말한다.

디자인패턴은 객체 지향 프로그램을 어떻게 구현해야 좀 더 유연하고 재활용성이 높은 프로그램을 만들 수 잇는지를 정리한 내용이다.

단계 1: 생성자를 private으로 만들기

기본적으로 생성자를 안 만들면 컴파일러가 디폴트 생성자를 만들어 주는데, 이 생성자는 항상 public이다. public이면 외부 클래스에서 인스턴스를 여러 개 만들 수 있기에 이것을 방지하고자 명시적으로 private 생성자를 만들어 준다. 이렇게 하여 Company 클래스 내부에서만 클래스의 생성을 제어할 수 있다.

public class Company {
    pirvate Company() {}
}

단계 2: 클래스 내부에 static으로 유일한 인스턴스 생성하기

단계 1에서 외부 인스턴스를 만들 수 없게 하였다. 하지만 우리가 쓸 인스턴스 하나의 생성이 필요하다. 이 또한 private로 선언하여 외부에서 이 인스턴스에 접근하지 못하도록 제한한다.

public class Company {
    private static Company instance = new Company();
    private Company() {}
}

단계 3: 외부에서 참조할 수 있는 public 메서드 만들기

private의 접근을 위해 public 메서드 하나를 만들어 주고, 리턴으로 위에서 만들어진 인스턴스를 리턴해준다. 이 메서드는 static으로 만들어 주어야 한다. 그래야 인스턴스 생성과 상관없이 호출할 수 있기 때문이다.

public class Company {
    public static Company getInstance() {
        if(instance == null) {
            instance = new Company();
        }
        return instance;
    }
}

단계 4: 실제 사용

아래와 같이 하면 결과값으로 true가 뜰 것이다.

public class CompanyTest {
    public static void main(String[] args) {
        Company myCom1 = Company.getInstance();
        Company myCom2 = Company.getInstance();
        System.out.println(myCom1 == myCom2);
    }
}