프로그래밍언어 자바 Part-1

4. 객체지향 개념과 자바


이번 강좌에서는 객체지향 프로그래밍의 주요 개념과 자바 객체지향 프로그래밍을 하기 위한 기본적인 내용을 배우게 됩니다.

이 강의를 통해 객체지향이 무엇인지 이해하고 자바 프로그램 설계에 객체지향 개념을 적용할 수 있습니다.




01: 객체, 클래스, 인스턴스

객체지향

객체지향이란 현실 세계의 객체 모델을 바탕으로 프로그램을 구조화하고 개발하는 프로그래밍 기법을 말합니다. 전통적인 프로그래밍 언어는 크게 객체지향 프로그래밍 언어(Object-Oriented Programming Language)와 절차지향 프로그래밍 언어(Procedure-Oriented Programming Language)로 구분 되었습니다.

이는 과거 프로그래밍 언어의 구조적 특징에 따른 분류이며 최근에 널리 쓰이고 있는 프로그램 언어들은 기본적으로 객체지향에 기반을 두고 있습니다. 또한 과거 세대 프로그래밍 언어 중 하나인 LISP에 적용되었던 함수형 언어(Functional Programming Language)의 개념이 보편적으로 확대되고 있으며 자바의 경우에도 JDK8 에서 부터 이러한 최신 경향을 반영하기 시작했습니다.

객체(Object)

객체는 영어로 Object 가 됩니다. 사전적인 의미로는 오감을 통해 알수 있는 물건, 물체가 됩니다. 즉, 우리눈에 보이는 모든 것들이 객체 입니다.

일반적으로 객체는 해당 객체가 가지고 있는 속성과 객체가 할 수 있는 동작으로 설명할 수 있습니다.

# 속성
- 색상: 빨간색
- 제조사: 현대
- 모델명: 소나타
- 출력: 180 마력
- 타이어: 17인치
...

# 동작
- 시동: 엔진을 동작 시킴
- 전진: 차를 앞으로 움직이게 함
- 후진: 차를 뒤로 움직이게 함
- 브레이크: 속도를 감소 시킴
...

이처럼 객체는 실제 우리눈에 보이는 대상이며 구체적인 값을 가지고 있습니다. 그런데 앞에서 정의한 빨간색 소나타 뿐만 아니고 파란색 소나타도 있고 검은색 제네시스도 있고 흰색 BMW 도 있습니다. 즉 공통적인 성질을 가지고 있지만 구체적인 값들이 다른 여러 객체가 있을 수 있기 때문에 공통적인 속성과 동작을 가지는 상위 개념을 정의할 수 있는데 그것이 바로 클래스 입니다.

클래스(Class)

클래스는 객체를 정의하기 위한 틀로써 표현하고자 하는 객체들의 속성과 동작을 정의하고 있습니다. 앞의 소나타 예에서 소나타는 Car 라고 하는 클래스로 정의할 수 있는 것입니다.

실제 프로그램 안에서 속성은 필드(멤버변수)의 형태로 동작은 메서드의 형태로 표현되게 됩니다. 이해를 돕기위해 구조를 조금 단순화해서 자동차 클래스를 구현하면 다음과 같습니다.

class Car {
    String color;
    String model;
    int power;

    public void go() {
    }

    public void break() {
    }
}

이제 클래스를 통해 여러 객체를 생성할 수 있는데 클래스를 통해 생성된 객체를 인스턴스(Instance) 라고 합니다.

인스턴스(Instance)

인스턴스는 클래스를 통해 만들어진 구체적인 객체라고 볼 수 있습니다. 앞에서 만든 Car 클래스를 통해 다음과 같이 여러 자동차 인스턴스를 만들 수 있습니다. 이때 new 연산자가 사용되며 클래스의 생성자 메서드를 통해 객체를 초기화 하게 됩니다.

기본 생성자는 생략이 가능하며 필요에 따라 인자가 있는 생성자를 만들 수 있습니다.

Car sonata = new Car();
Car grandeur = new Car('그랜저','red',180);

sonata.go();
grandeur.break();
...

» 실습: 클래스와 인스턴스 실습 예제

실습개요

자동차 게임에 사용될 Car 클래스를 정의하고 서로 다른 값을 가지는 여러 자동차 인스턴스를 생성

먼저 자동차 클래스를 앞에서의 예를 사용해 다음과 같이 구현 합니다.

소스코드

public class Car {
    private String color;
    private String model;
    private int power;
    private int curSpeed;

    public Car() {
        curSpeed = 0;
    }

    public Car(String color, String model, int power) {
        this.color = color;
        this.model = model;
        this.power = power;
    }

    public void go() {
        if (power < 200) {
            curSpeed += 10;
        } else if (power >= 200) {
            curSpeed += 20;
        }
        System.out.printf("Accelerate %s, Current Speed %d\n", model, curSpeed);
    }

    public void stop() {
        curSpeed = 0;
    }
    // getter/setter 메서드 생략
}

다음은 CarGame 클래스를 만들어 여러 자동차를 생성해 봅니다.

소스코드

public class CarGame {
    public static void main(String[] args){
        Car c1 = new Car();
        c1.setColor("RED");
        c1.setModel("Hyundai Sonata");
        c1.setPower(180);

        Car c2 = new Car("BLUE","BMW 520",210);
        c1.go();
        c2.go();
    }
}

실행결과

Accelerate Hyundai Sonata, Current Speed 10
Accelerate BMW 520, Current Speed 20



02: 상속과 오버라이딩

앞에서 Car 클래스를 통해 서로 다른 두 모델의 자동차 인스턴스를 생성 했습니다. 자동차를 앞으로 움직이는 go() 메서드는 차량의 마력에 따라 증가 속도를 다르게 해서 차이를 두었습니다.

사실 자동차라는 기본적인 개념은 동일하지만 실제 존재 하는 자동차는 성격이 다른 여러 종류가 존재 합니다. 예를들어 버스, 트럭, 세단, SUV, 스포츠카 등으로 구분할 수 있습니다.

버스와 세단은 모두 자동차 이지만 크기나, 탑승정원, 엔진등에서 많은 차이가 있습니다. 스포츠카의 경우 문이 2개인 경우가 많고 차체가 낮으며 속도가 빠릅니다. SUV는 4륜구동이며 오프로드를 달리기 위한 기능드이 추가되어 있습니다.

이처럼 서로 다른 자동차의 성격을 Car 라고 하는 클래스에 모두 담는 것은 거의 불가능 합니다. 엄밀히 말하면 Car 는 매우 추상적인 개념이고 세단이나 스포츠카는 좀 더 구체적인 자동차의 성격을 규정하고 있습니다.

상속(Inheritance)

상속은 바로 클래스간의 상하 관계로 추상적인 슈퍼클래스(Super Class) 혹은 부모 클래스로 부터 서브클래스(Sub Class) 혹은 자식 클래스를 만드는 것으로 상속이라는 관계를 통해 계층구조를 형성하게 합니다.

클래스 상속의 특징은 다음과 같습니다.

이러한 상속을 이용하면 코드의 재사용이 가능해지고 부모클래스 레벨에서 호환되는 서브클래스를 사용해 다형성의 기반을 마련할 수 있습니다.

프로그램 문법상으로는 extends 키워드를 사용하며 자바의 경우 두개 이상의 클래스를 동시에 상속받는 다중 상속은 지원하지 않습니다. 대신 뒤에서 배우게 될 인터페이스를 이용해 여러 클래스의 속성을 가지는 서브 클래스 구현이 가능 합니다.

Class SubClass extends SuperClass {

}

만일 전투게임을 만든다고 할때 게임에는 무기 아이템들이 존재하고 각각의 무기 아이템들은 비슷한 기능(예를 들면 총)을 가지고 있지만 구체적으로는 성격이 다른 종류가 존재하게 됩니다. 예를들어 샷건은 근거리에 효과가 좋고 단발 사격이 되는 반면 기관총은 연발이 가능하고 중거리에서 유용합니다. 저격소총은 대부분 단발이며 장거리에서 효과가 있습니다.

클래스의 상속관계로 구현 한다면

Class Gun {
}

Class ShotGun extends Gun{
}

Class M416 extends Gun {
}

오버라이딩(Overriding)

슈퍼클래스로 부터 상속받은 메서드를 다시 정의하는 것을 말합니다. 당연히 메서드의 이름과 리턴 타입, 인자등이 모두 동일해야 하며 다를 경우 새로운 메서드가 추가되는 형식이 됩니다.

오버라이딩을 통해 객체지향의 특징중 하나인 다형성 구현이 가능해 집니다. 예를들어 애완동물 클래스가 있고 bark() 라는 메서드가 있다고 했을때 애안동물 클래스를 상속받는 강아지와 고양이 클래스를 만들경우 bark() 메서드의 오버라이딩을 통해 서로 다른 동작이 가능하게 할 수 있습니다.

class Pet {
    void bark() {
        System.out.println("pipi");
    }
}

class Dog extends Pet {
    void bark() {
        System.out.println("woof woof");
    }
}

class Cat extends Pet {
        void bark() {
        System.out.println("mew mew");
    }
}

다음과 같이 Pet 클래스 타입으로 Dog 클래스의 인스턴스를 생성해 bark() 메서드를 호출하면 woof woof 가 출력 됩니다.

Pet pet = new Dog();
pet.bark();     // woof woof

Dog 대신 Cat 인스턴스를 생성하면 동일 코드에서 mew mew 가 출력되어 동일한 구조에서 다른 동작 즉 다형성을 구현할 수 있게 됩니다.

여기에서 Pet 클래스의 bark() 메서드는 사실상 구현할 필요가 없는 메서드 입니다. 어차피 구체적인 애완동물 클래스가 구현되어야 세부 내용이 결정될 것이기 때문에 굳이 메서드 구현을 해둘 필요가 없는것이지요.

이러한 경우 일반적인 클래스가 아닌 추상클래스나 인터페이스를 통해 구체적인 내용을 구현하는 것이 아닌 규격을 제시 하는 형태로 구현하는 것이 좋습니다.

» 실습: 상속과 오버라이딩 실습 예제

실습개요

전투 게임에 사용되는 무기 아이템을 상속 관계로 구현하고 오버라이딩을 통해 각각의 무기별 특성을 구현 합니다.

먼저 기본이 되는 Gun 클래스를 만듭니다.

소스코드

public class Gun {
    protected String model;       // model name of gun
    protected int bulletCount;    // total count of bullet

    public void fire() {
        System.out.println(model + "=>");
        bulletCount -= 1;
    }

    public Gun(String model) {
        bulletCount = 10;
        this.model = model;
    }
}

다음은 Gun 을 상속받는 두개의 서로다른 총기를 생성 합니다.

소스코드

public class AssaultRifle extends Gun {
    public void fire() {
        bulletCount -= 5;
        System.out.printf("%s => => => => => , %d\n",model, bulletCount);
    }

    public AssaultRifle(String model) {
    	super(model);
        bulletCount = 40;
    }	
}

소스코드

public class ShotGun extends Gun {
    public void fire() {    	
        bulletCount -= 1;
    	System.out.printf("%s =}}} , %d\n",model, bulletCount);
    }
    
    public ShotGun(String model) {
    	super(model);
    }
}

메인 프로그램은 GunGame 클래스로 다음과 같이 Gun 타입 객체를 생성해 fire() 메서드를 호출

소스코드

public class GunGame {
	public static void main(String[] args) {
		Gun gun = new ShotGun("S12K");
		// Gun gun = new AssaultRifle("M416");
		
		gun.fire();
	}
}

실행결과

ShotGun 객체를 생성하는 코드에 주석을 하고 AssaultRifle 객체 생성 코드 주석을 해제해 번갈아 실행해 각각의 총이 다르게 발사되는 것을 확인하도록 합니다.

S12K =}}} , 9
or
M416 => => => => => , 35



03: 추상클래스와 인터페이스

추상클래스(Abstract Class)

추상클래스는 추상메서드(abstract method)를 포함하고 있는 클래스를 말합니다. 추상메서드란 앞의 Pet 클래스와 같이 구체적이지 않은 내용을 정의한 메서드를 말하는 것으로 추상 메서드로 정의된 메서드는 서브 클래스에서 반드시 오버라이딩을 통해 구현해야 합니다.

따라서 추상클래스 자체는 new 를 통해 인스턴스로 만들 수 없고 반드시 상속을 통해 구체적인 클래스를 만들어 사용해야 합니다. 소프트웨어 디자인 패턴에서는 이렇게 추상클래스를 상속해서 구현하는 클래스를 콘크리트 클래스(Concrete Class) 라고도 부릅니다.

추상클래스는 추상메서드 이외에 다른 메서드를 포함할 수 있으면 멤버 필드역시 가질 수 있습니다. 앞의 Pet 클래스를 추상클래스로 정의 하면 다음과 같이 됩니다.

abstract class Pet {
    abstract void bark(){};
}

참고로 메서드의 abstact바디부 표시인 {}는 생략이 가능합니다.

이클립스와 같은 개발도구를 사용하는 경우 추상클래스 상속시 자동으로 추상메서드 구현을 알려주고 기본 코드를 생성해 구현부를 작성할 수 있도록 도와주게 됩니다.

인터페이스(Interface)

인터페이스는 말 그대로 무언가를 이어주기 위한 연결고리로 추상클래스와 유사하지만 상수와 추상메서드로만 구성된 형태를 말합니다. 추상메서드로만 구성이 되기 때문에 그 자체로는 아무런 기능을 하지 않지만 마치 설계도 처럼 향후 구현될 클래스들을 연결해 사용할 수 있는 기반 구조를 제공하고 있습니다.

Pet 클래스를 인터페이스로 정의 하면 다음과 같습니다.

interface Pet {
    void bark();
}

추상클래스와 달리 인터페이스는 상속(extends)이 아니라 구현(implements)을 통해 클래스를 정의하게 됩니다.

class Dog implements Pet {
    ...
}

또한 일반적인 클래스들은 다중상속이 안되지만 인터페이스의 경우 다중 구현이 가능합니다. 예를들어 로봇 애완견을 만들기 위해 Pet 과 Robot 인터페이스를 동시에 구현할 수 있습니다.

class RobotDog implments Pet, Robot {
    ...
}

이경우 양쪽 인터페이스 모두의 추상메서드가 구현되어야 합니다. 또한 상속받을 클래스가 있다면 상속과 함께 사용하는 것도 가능합니다.

class RobotCat extends Pet implements Robot {
    ...
}

그러면 언제 추상클래스를 사용하고 언제 인터페이스를 사용해야 할까요? 일반적으로 설계의 관점에서는 인터페이스를 사용하고 구현시 유연함을 위해서는 추상클래스를 사용할 수 있습니다.

다만 JDK 8 에서 추가된 default 를 이용하면 인터페이스에 메서드 구현이 가능하고 이를 구현하는 클래스에서 해당 메서드의 오버라이딩도 가능해져 사실상 추상클래스의 역할을 겸하게 되었다고 볼 수 있습니다.

추가적으로 static 메서드의 구현도 가능해져 인터페이스의 활용도가 높아졌다고 볼 수 있습니다. 물론 실제 구현에는 추상클래스, 인터페이스 등이 모두 혼재해서 사용되고 있으니 필요에 따라 적절히 선택해 사용하면 됩니다.

interface Pet {
    void bark();
    default eat(int amount) {
        ...
    };
    static void wake() {
        ....
    }
}

» 실습: 추상클래스와 인터페이스 실습 예제

실습개요

추상클래스와 인터페이스를 이용해 프로그램 구조를 설계하고 구현하는 과정을 실습합니다.

먼저 Pet 인터페이스와 Robot 추상클래스를 구현합니다.

소스코드

public interface Pet {
    void bark();
}

소스코드

public abstract class Robot {
    private String name;

    void move() {
        System.out.println("Robot moved !!");
    }

    abstract void charging();

    public String getName() {
        return name;
    }

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

}

Robot 추상클래스를 상속받고 Pet 인터페이스를 구현하는 RobotDog 클래스를 생성합니다. 이클립스와 같은 개발도구를 사용하면 자동으로 구현해야 하는 메서드들의 기본 코드 블럭이 생성 됩니다.

소스코드

public class RobotDog extends Robot implements Pet {

    @Override
    public void bark() {
        System.out.println("Woof Woof~~");
    }

    @Override
    void charging() {
        System.out.println(getName() + " go to charging station");
    }

    public static void main(String[] args) {
        RobotDog rd = new RobotDog();
        rd.setName("robo dog");
        rd.bark();
        rd.move();
        rd.charging();
    }
}

실행결과

Woof Woof~~
Robot moved !!
robo dog go to charging station



04: 패키지와 제어자

패키지(package)

패키지는 자바 클래스들을 구분하기 위해 사용합니다. 객체지향 프로그래밍에서는 여러 클래스들이 사용되고 내가 직접 만들지 않은 클래스 혹은 라이브러리들을 사용하는 경우도 많이 있습니다. 이때 클래스들의 이름이 모두 동일하다면 문제가 발생할 수 있습니다.

예를들어 회원관리 프로그램에 Login 클래스가 있을수 있으며 카페나 블로그, 쇼핑몰 쪽에서도 Login 클래스가 있을수 있습니다. 이처럼 서로 다른 목적에서 개발된 클래스들을 서로 구분하기 위해 패키지를 사용하게 됩니다.

그리고 이러한 패키지들을 전세계적으로 문제 없이 관리하기 위한 일종의 규약이 존재하는데 패키지 이름의 관리를 역도메인(reverse domain) 방식으로 하는 것입니다.

도메인은 인터넷에서 기관을 이름으로 구분한 것인데 패키지는 이를 뒤집어서 사용하게 됩니다.

예를들어 네이버의 도메인은 naver.com 인데 만일 네이버 개발자들이 자바 클래스를 개발하게 된다면 com.naver 를 루트 패키지명으로 사용하게 되는것입니다. 실제 네이버는 카페, 블로그, 메일 등과 같은 서비스를 운영하고 있기 때문에 이들에 사용되는 클래스들은 각각 com.naver.cafe, com.naver.blog, com.naver.mail 과 같은 패키지명을 사용하게 되는 것이지요.

이와같은 규칙 때문에 체계적으로 클래스들을 관리할 수 있고 또 서로 다른 기관에서 만든 클래스들도 혼선없이 사용할 수 있는 것입니다.

처음 자바를 시작하는 경우 학습의 목적이므로 별도의 패키지를 만들지 않는 경우가 많으나 가급적 패키지명을 사용하는 습관을 들이는것이 좋습니다.

package 선언

클래스에 패키지를 선언하는 것은 package 키워드를 이용해 소스 상단에 패키지 이름을 넣어주는 것입니다.

package com.my.study;

class MyClass {

}

package import

현재 구현중인 클래스에서 동일패키지에 있는 클래스가 아닌 다른 클래스(외부 라이브러리 등)를 사용하는 경우 반드시 import 문을 사용해 해당 클래스의 패키지를 명시해 주어야 합니다. 만일 import 문을 사용하지 않는다면 소스코드에서 해당 클래스 사용시 매번 클래스 이름 앞에 패키지명까지 붙여주어야 합니다.

class MyClass {
 java.util.Scanner scan = new java.util.Scanner(System.in);   
}

대부분의 경우 매번 패키지명을 붙여주는 방식 보다는 해당 클래스의 패키지를 import 해주는 방법을 사용합니다.

import java.util.Scanner;

class MyClass {
    Scanner scan = new Scanner(System.in);
}

원칙적으로는 사용되는 모든 클래스를 import 해주는 것이며 특정 패키지의 모든 클래스를 한번에 import 하기 위해서는 다음과 같이 *을 사용하기도 합니다.

import java.util.*;

그러나 이러한 방식은 많은 라이브러리를 동시에 사용하는 경우 클래스 이름 중복으로 인한 문제가 발생할 수 있으므로 주의해야 합니다. 보통은 개발도구에서 자동으로 패키지 import 를 관리하므로 신경쓰지 말고 개별 클래스들이 import 될 수 있도록 하는 것이 좋습니다.

실제 패키지는 디스크 상에는 디렉토리 개념으로 소스 폴더를 열어보면 com 폴더 아래 my 폴더가 있고 그아래 study 폴더에 자바 소스파일들이 위치한 것을 확인할 수 있습니다.

제어자(Modifier)

제어자는 클래스, 변수, 메서드의 선언부에 사용되어 부가적인 의미를 부여합니다. 이미 여러 코드에서 나온 public, static 같은 키워드들이 여기에 해당 됩니다. 이러한 제어자에는 클래스의 접근 범위와 관련된 접근 제어자(access modifier)와 일반 제어자가 있습니다.

이들 제어자는 상황에 따라 클래스, 메서드, 변수등에 사용하며 하나의 대상에 여러 개의 제어자를 조합해서 사용할 수 있으나, 접근제어자는 단 하나만 사용할 수 있습니다.

static

클래스 혹은 공통적인 이라는 의미를 가지고 있으며 앞에서 배운 클래스 변수나 메서드의 선언에 사용할 수 있습니다. static 의 특징은 다음과 같습니다.

final

변경할 수 없다는 의미를 가지고 있으며 변수나 메서드, 클래스에 사용할 수 있습니다.

abstract

앞에서 살펴본 추상 클래스와 추상 메서드를 선언할때 사용합니다.

접근 제어자(access modifier)

접근 제어자는 멤버 또는 클래스에 사용하며 외부에서의 접근을 제어하기 위해 사용합니다. 예를 들어 내가 만드는 패키지의 클래스들중 일부는 외부에서 사용할 수 있도록 하고 일부 클래스는 내가 만든 클래스에서만 사용할 수 있도록 하는등의 제어가 가능합니다.

보통 자바를 처음 학습하는 동안에는 특별한 사용이 필요 없지만 패키지구조가 복잡한 프로젝트를 진행하거나 라이브러리를 만드는 등의 작업을 하는 경우에는 접근 제어자를 적절하게 사용해 주어야 합니다.

지정되어 있지 않다면 default가 됩니다.

유형별로는 다음과 같은 접근제어자 사용이 가능합니다.

일반적으로 생성자의 접근 제어자는 클래스의 접근 제어자와 일치하며 생성자에 접근 제어자를 사용하는 경우 인스턴스의 생성을 제한할 수 있습니다. 이는 소프트웨어 디자인 패턴에서 단일 인스턴스를 보장하는 싱글턴 패턴(Singleton Pattern)의 구현에 활용되기도 합니다.

캡슐화와 접근 제어자

캡슐화는 객체지향 프로그램의 대표적인 특징중 하나 입니다. 접근 제어자를 사용하면 클래스 외부로 부터의 접근을 제어할 수 있으므로 객체를 캡슐화 할 수 있습니다.

예를들어 private의 경우 동일 클래스 내에서만 접근이 가능하므로 멤버필드에 private 를 선언하면 해당 변수를 클래스 외부에서 접근할 수 없게 됩니다.

이 경우 클래스 외부에 해당 멤버의 접근을 제공하기 위해 getter, setter 메서드를 제공하는 방식을 사용하게 됩니다. getter 는 멤버값을 제공하기 위해 setter 는 멤버값의 변경을 위해 사용합니다.

메서드 생성 규칙은 멤버타입 getXxx(), setXxx(멤버타입 인자) 형식 입니다. 이클립스와 같은 개발도구에서는 private 멤버변수들에 대해 자동으로 getter, setter 메서드 생성을 제공하는 기능이 있습니다.

private int count;

public int getCount() {
    return count;
}

public void setCount(int count) {
    this.count = count;
}

보통 위와 같은 형식을 취하며 단순히 값을 넘겨주거나 설정하기도 하고 원하는대로 데이터를 조작하거나 처리 기능을 구현하면 됩니다.


참고 자료