객체, 클래스, 인터페이스, 패키지, 상속

객체, 클래스, 인터페이스, 패키지, 상속

객체지향 프로그래밍 언어를 처음 써본다면, 코드를 작성하기 전에 기본 개념 몇 가지를 먼저 익혀야 해요. 이 섹션에서는 객체(object), 클래스(class), 상속(inheritance), 인터페이스(interface), **패키지(package)**를 소개해요. 각 개념이 실제 세계와 어떻게 연결되는지 보면서, 동시에 Java 언어의 문법도 자연스럽게 익힐 수 있게 구성했어요.

출처: dev.java - Objects, Classes, Interfaces, Packages, and Inheritance

본문

객체(Object)란 무엇인가?

객체는 서로 연관된 **상태(state)**와 **행동(behavior)**을 묶어 둔 소프트웨어 덩어리예요. 여기서는 상태와 행동이 객체 안에서 어떻게 표현되는지, 데이터 캡슐화가 어떤 개념인지, 그리고 이렇게 설계했을 때 얻는 장점이 무엇인지 설명할게요.

객체는 공통된 두 가지 특성을 가져요. 모든 객체는 상태와 행동을 지닙니다. 개는 상태(이름, 색, 품종, 배고픔)와 행동(짖기, 물건 가져오기, 꼬리 흔들기)을 가져요. 자전거도 상태(현재 기어, 현재 페달 회전수, 현재 속도)와 행동(기어 바꾸기, 페달 회전수 바꾸기, 브레이크 걸기)을 가지죠. 실제 세계의 객체에서 상태와 행동을 찾아내는 연습은 객체지향적으로 사고하기 시작하는 아주 좋은 방법이에요.

지금 바로 주변의 실제 객체들을 잠깐 관찰해볼까요. 보이는 객체마다 두 가지 질문을 던져봐요. "이 객체는 어떤 상태에 있을 수 있을까?" "이 객체는 어떤 행동을 할 수 있을까?" 그리고 관찰한 내용을 꼭 적어보세요. 하다 보면 실제 객체는 복잡도가 제각각이라는 걸 발견하게 돼요. 책상 위 램프는 상태가 두 개(켜짐, 꺼짐), 행동도 두 개(켜기, 끄기)뿐일 수 있지만, 책상 위 라디오는 상태(켜짐, 꺼짐, 현재 볼륨, 현재 주파수)와 행동(켜기, 끄기, 볼륨 올리기, 볼륨 내리기, 탐색, 스캔, 튜닝)이 더 많을 수 있죠. 또 어떤 객체는 그 안에 다른 객체를 담고 있다는 것도 발견할 수 있어요. 이런 실제 관찰이 객체지향 프로그래밍의 세계를 이해하는 출발점이 됩니다.

소프트웨어 객체는 상태와 그에 연관된 행동으로 이뤄져요. 객체는 상태를 필드(field)(어떤 언어에서는 변수)에 저장하고, 행동을 메서드(method)(어떤 언어에서는 함수)로 드러내요. 메서드는 객체의 내부 상태에 작용하면서, 객체와 객체 사이의 소통을 담당하는 기본 수단이 돼요. 내부 상태를 숨기고 모든 상호작용을 객체의 메서드를 통해서만 하도록 요구하는 것을 **데이터 캡슐화(data encapsulation)**라고 해요. 객체지향 프로그래밍의 핵심 원칙이죠.

자전거 하나를 예로 들어볼게요. 상태(현재 속도, 현재 페달 회전수, 현재 기어)를 부여하고, 그 상태를 바꾸는 메서드를 제공함으로써, 객체는 외부 세계가 자신을 어떻게 사용하도록 허용할지를 스스로 통제해요. 예를 들어 자전거에 기어가 6개뿐이라면, 기어를 바꾸는 메서드는 1보다 작거나 6보다 큰 값을 거부할 수 있어요.

코드를 개별 소프트웨어 객체로 묶으면 많은 이점이 생겨요.

  1. 모듈성(Modularity): 객체의 소스 코드는 다른 객체의 소스 코드와 독립적으로 작성하고 유지보수할 수 있어요. 일단 만들어지면 객체는 시스템 안에서 쉽게 주고받을 수 있어요.
  2. 정보 은닉(Information-hiding): 객체의 메서드를 통해서만 상호작용하므로, 내부 구현의 세부사항이 외부 세계에 숨겨져요.
  3. 코드 재사용(Code re-use): 이미 존재하는 객체(아마 다른 개발자가 작성한 것)가 있다면 그 객체를 프로그램에서 그대로 쓸 수 있어요. 이를테면 전문가가 복잡하고 특정한 작업을 위한 객체를 구현·테스트·디버깅해두면, 그걸 신뢰하고 자신의 코드에서 사용할 수 있죠.
  4. 교체 용이성과 디버깅 용이성(Pluggability and debugging ease): 특정 객체에 문제가 생기면 애플리케이션에서 그 객체만 빼내고 다른 객체로 교체하면 돼요. 실제 세계에서 기계를 수리하는 것과 같아요. 볼트가 부러지면 우리는 기계 전체가 아니라 그 볼트만 교체하죠.

클래스(Class)란 무엇인가?

애플리케이션을 만들다 보면 같은 종류의 객체를 아주 많이 만나게 돼요. 똑같은 제조사와 모델의 자전거가 수천 대 존재할 수 있죠. 각 자전거는 같은 설계도로 만들어졌으므로 같은 부품으로 이뤄져 있어요. 객체지향 용어로는, 여러분의 자전거가 '자전거'라는 **클래스(class)의 인스턴스(instance)**라고 말해요. 클래스는 개별 객체를 만드는 설계도(blueprint)인 거죠.

다음 Bicycle 클래스는 자전거를 구현한 한 가지 예입니다.

class Bicycle {

    int cadence = 0;
    int speed = 0;
    int gear = 1;

    void changeCadence(int newValue) {
        cadence = newValue;
    }

    void changeGear(int newValue) {
        gear = newValue;
    }

    void speedUp(int increment) {
        speed = speed + increment;
    }

    void applyBrakes(int decrement) {
        speed = speed - decrement;
    }

    void printStates() {
        IO.println("cadence:" +
                cadence + " speed:" +
                speed + " gear:" + gear);
    }
}

Java 문법이 낯설게 느껴질 수 있지만, 이 클래스의 설계는 앞서 한 자전거 객체에 대한 논의에 기반해 있어요. 필드 cadence, speed, gear가 객체의 상태를 나타내고, 메서드들(changeCadence(), changeGear(), speedUp() 등)이 외부 세계와의 상호작용을 정의하죠.

눈치챘을 수도 있는데, Bicycle 클래스에는 main() 메서드가 없어요. 그 이유는 이 클래스가 완전한 애플리케이션이 아니라, 애플리케이션에서 쓰일 자전거의 설계도일 뿐이기 때문이에요. 새 Bicycle 객체를 만들고 사용하는 책임은 애플리케이션의 다른 클래스에 있어요.

두 개의 Bicycle 객체를 만들어 메서드를 호출하는 BicycleDemo 클래스를 볼게요.

class BicycleDemo {
    public static void main(String[] args) {

        // Create two different
        // Bicycle objects
        Bicycle bike1 = new Bicycle();
        Bicycle bike2 = new Bicycle();

        // Invoke methods on
        // those objects
        bike1.changeCadence(50);
        bike1.speedUp(10);
        bike1.changeGear(2);
        bike1.printStates();

        bike2.changeCadence(50);
        bike2.speedUp(10);
        bike2.changeGear(2);
        bike2.changeCadence(40);
        bike2.speedUp(10);
        bike2.changeGear(3);
        bike2.printStates();
    }
}

이 테스트의 출력은 두 자전거의 최종 페달 회전수, 속도, 기어를 보여줘요.

cadence:50 speed:10 gear:2
cadence:40 speed:20 gear:3

상속(Inheritance)이란 무엇인가?

서로 다른 종류의 객체들은 서로 공통점이 꽤 많아요. 예를 들어 산악자전거, 로드바이크, 탠덤자전거는 모두 자전거의 특성(현재 속도, 현재 페달 회전수, 현재 기어)을 공유해요. 하지만 각각은 서로 구별되는 추가 기능도 정의하죠. 탠덤자전거는 좌석이 둘, 핸들바도 두 벌이에요. 로드바이크는 드롭 핸들바를 갖고요, 일부 산악자전거는 체인링이 하나 더 있어서 낮은 기어비를 제공해요.

객체지향 프로그래밍은 클래스가 다른 클래스로부터 흔히 쓰이는 상태와 행동을 상속받게 해줘요. 이 예에서 BicycleMountainBike, RoadBike, TandemBike의 **슈퍼클래스(superclass)**가 돼요. Java 언어에서 각 클래스는 하나의 직접 슈퍼클래스만 가질 수 있고, 각 슈퍼클래스는 무제한의 서브클래스를 가질 수 있어요.

서브클래스를 만드는 문법은 아주 단순해요. 클래스 선언의 시작 부분에 extends 키워드를 쓰고, 상속받을 클래스 이름을 적으면 돼요.

class MountainBike extends Bicycle {

    // new fields and methods defining
    // a mountain bike would go here

}

이렇게 하면 MountainBikeBicycle과 똑같은 필드와 메서드를 모두 얻으면서도, 자기만의 독특한 기능에만 집중하는 코드를 작성할 수 있어요. 그래서 서브클래스의 코드를 읽기 쉬워지죠. 다만 각 슈퍼클래스가 정의하는 상태와 행동은 꼭 문서화해두어야 해요. 그 코드는 각 서브클래스의 소스 파일에는 나타나지 않으니까요.

인터페이스(Interface)란 무엇인가?

앞서 배웠듯이 객체는 노출하는 메서드를 통해 외부 세계와의 상호작용을 정의해요. 메서드는 객체가 외부 세계에 대해 갖는 인터페이스를 형성하죠. 예를 들어 텔레비전 앞면의 버튼들은, 플라스틱 케이스 안쪽의 전기 배선과 사용자 사이의 인터페이스예요. 출력 버튼을 눌러 텔레비전을 켜고 끄는 거죠.

가장 흔한 형태에서 인터페이스는 빈 몸통(method body)을 가진, 서로 연관된 메서드들의 묶음이에요. 자전거의 행동을 인터페이스로 지정한다면 이렇게 생겼을 거예요.

interface Bicycle {

    //  wheel revolutions per minute
    void changeCadence(int newValue);

    void changeGear(int newValue);

    void speedUp(int increment);

    void applyBrakes(int decrement);
}

이 인터페이스를 구현하려면 클래스의 이름을 바꾸고(예를 들어 특정 자전거 브랜드 ACMEBicycle처럼), 클래스 선언에 implements 키워드를 사용해요.

class ACMEBicycle implements Bicycle {

    int cadence = 0;
    int speed = 0;
    int gear = 1;

    // The compiler will now require that methods
    // changeCadence, changeGear, speedUp, and applyBrakes
    // all be implemented. Compilation will fail if those
    // methods are missing from this class.

    void changeCadence(int newValue) {
        cadence = newValue;
    }

    void changeGear(int newValue) {
        gear = newValue;
    }

    void speedUp(int increment) {
        speed = speed + increment;
    }

    void applyBrakes(int decrement) {
        speed = speed - decrement;
    }

    void printStates() {
        IO.println("cadence:" +
                cadence + " speed:" +
                speed + " gear:" + gear);
    }
}

인터페이스를 구현하면 클래스가 약속하는 행동에 대해 더 공식적으로 변하게 돼요. 인터페이스는 클래스와 외부 세계 사이의 **계약(contract)**을 형성하고, 그 계약은 빌드 시점에 컴파일러가 강제해요. 클래스가 어떤 인터페이스를 구현한다고 선언하면, 해당 인터페이스가 정의한 모든 메서드가 소스 코드에 나타나야 컴파일이 성공해요.

참고로 ACMEBicycle 클래스를 실제로 컴파일하려면, 구현한 인터페이스 메서드의 앞부분에 public 키워드를 붙여야 해요. 그 이유는 나중에 Classes and Objects, Interfaces and Inheritance 섹션에서 배우게 될 거예요.

패키지(Package)란 무엇인가?

패키지는 서로 연관된 클래스와 인터페이스의 묶음을 정리하는 **네임스페이스(namespace)**예요. 개념적으로 패키지는 컴퓨터의 서로 다른 폴더와 비슷하다고 생각할 수 있어요. HTML 페이지는 한 폴더에, 이미지는 다른 폴더에, 스크립트나 애플리케이션은 또 다른 폴더에 보관할 수도 있죠. Java로 작성된 소프트웨어는 수백·수천 개의 클래스로 이루어질 수 있으므로, 연관된 클래스와 인터페이스를 패키지로 묶어 정리해 두는 게 합리적이에요.

Java 플랫폼은 애플리케이션에서 쓸 수 있는 거대한 클래스 라이브러리(패키지들의 집합)를 제공해요. 이 라이브러리를 줄여서 "Application Programming Interface" 즉 API라고 불러요. 그 패키지들은 범용 프로그래밍에서 가장 흔한 작업들을 표현해요. 예를 들어 String 객체는 문자열에 대한 상태와 행동을 담고, File 객체는 파일시스템의 파일을 만들고·삭제하고·조사하고·비교하고·수정하게 해주며, Socket 객체는 네트워크 소켓의 생성과 사용을 가능하게 해요. 다양한 GUI 객체들은 버튼, 체크박스 등 그래픽 사용자 인터페이스와 관련된 모든 것을 다루죠. 선택할 수 있는 클래스가 문자 그대로 수천 개나 돼요. 덕분에 프로그래머는 동작을 위해 필요한 인프라가 아니라, 자신의 애플리케이션 설계에만 집중할 수 있어요.

Java Platform API Specification에는 Java SE 플랫폼이 제공하는 모든 패키지·인터페이스·클래스·필드·메서드의 완전한 목록이 있어요. 그 페이지를 브라우저에 띄워서 북마크해두세요. 프로그래머에게 그 문서는 가장 중요한 단 하나의 참고 자료가 될 거예요.

더 알아보기