자바(5) - 클래스

객체와 객체 지향 프로그래밍

객체의 사전적 의미로는 ‘의사나 행위가 미치는 대상’이라 한다. 우리 주위에 있는 객체들론 사람, 자동차, 건물 등이 있다. 즉 눈에 보이는 사물은 모두 객체라고 볼 수 있는데, 눈에 안 보이는 것도 객체가 될 수 있다. 가령, 주문, 생산, 관리 등 행동을 나타내는 단어도 객체가 될 수 있다. 이러한 객체들에 대해서 우리는 자바로 표현할 수 있고, 이런 프로그램을 OOP(Object-Oriented Programming), 객체 지향 프로그래밍이라고 한다.

절차 지향 프로그래밍

과거의 프로그래밍은 절차지향적으로 만들었는데, 일련의 순서대로 프로그래밍하는 것을 의미한다. 예를 들어,

기상 -> 씻기 -> 밥 먹기 -> 버스 타기 -> 요금 지불 -> 도착

이런 순이다.

객체 지향 프로그래밍

반면 객체 지향 프로그래밍은 객체를 정의하고 객체 간 협력을 프로그래밍하는 것이다. 위의 절차 지향 과정을 객체로 표현하자면 학생, 밥, 버스 등이 있다. 그리고 ‘밥 먹기’ 같은 행동은 ‘학생’이라는 객체와 ‘밥’이라는 객체가 있어 학생이 밥을 먹는 협력으로 이루어진다. 버스 또한 버스와 학생 간의 상호작용이 일어나는 것이다. 이렇듯 객체 지향 프로그램은 먼저 객체를 만들고 객체 사이에 일어나는 일을 구현하는 것이다.

클래스

객체 지향 프로그램은 클래스를 기반으로 프로그래밍하는 것이다. 클래스는 객체의 속성과 기능을 코드로 구현하는 것이다. 학생 객체를 생각해 보자. 학생은 어떤 정보를 갖고 있을까? 크게 학번, 이름, 학년, 주소 등을 가지고 있을 것이다. 이런 속성들을 특성이라고도 하고 클래스 내부에 변수로 선언한다. 이것들을 ‘멤버 변수’라고 한다. 아무튼 이런 멤버 변수들은 모든 학생들이 갖고 있고, 정보들은 각 객체들마다 다르게 갖고 있을 것이다. 1학년인 철수도 있고, 2학년인 영희도 있을 것이다.

예전부터 클래스를 비유할 때는 클래스는 붕어빵 틀이고, 객체는 붕어빵 틀로 만들어진 붕어빵이라 볼 수 있다. 붕어빵 모양은 같지만 내용물은 하나는 단팥이 될 수도 있고, 다른 하나는 슈크림이 될 수도 있다.

클래스 정의는 다음과 같다.

(접근 제어자) class 클래스이름 {
    멤버 변수;
    메서드;
}

// 실전
public class Student {
    int studentID;
    String studentName;
    int grade;
    String address;
}

클래스 이름 짓는 규칙으로는 단어들의 대문자로 시작한다. 소문자로 시작한다고 오류가 생기는 건 아니지만 관습적으로 사용한다. ex) StudentTest

멤버 변수를 선언할 때 int형, double형 같은 기본 자료형(primitive)으로 선언할 수도 있고, 클래스형으로 선언할 수 있다.

다음은 기능에 해당하는 메서드이다. 멤버 변수들을 활용하여 객체와 관련된 기능을 구현하면 된다. 예를 들어 학생에게 이름을 부여하거나 사는 곳을 출력하는 등이 있다. 메서드 선언은 다음과 같다. 세부 내용은 밑에서 더 다루겠다.

public class Student {
    int studentID;
    String studentName;
    int grade;
    String address;

    // 메소드 부분
    public void showStudentInfo() {
        System.out.println(studentName + "," + address)
    }
}

패키지

패키지는 클래스 파일의 묶음이다. 패키지를 만들면 프로젝트 하위에 물리적으로 디렉터리가 생성된다. 또한 패키지는 계층 구조를 가질 수 있다. 계층 구조를 구성하는 것은 전체 프로젝트의 소스 코드를 어떻게 관리할지와 관련이 있다. 만약에 학교와 관련된 프로젝트가 있다면 프로젝트 안에 학생, 과목, 교실, 교수, 학과 등의 클래스로 만들 수 있고, 이런 클래스를 협력하는 여러 클래스를 넣을 수 있다.

패키지 선언은 다음과 같다.

package domain.student.view;

public class StudentView {}

클래스 이름은 StudentView인데, 전체 이름(class full name)은 domain.studnet.view.StudentView가 된다. 클래스 이름이 같다고 해도 패키지 이름이 다르면 클래스 전체 이름이 다른 것이므로 다른 클래스가 된다. 즉, 다른 패키지에 있으면 클래스명이 같아도 서로 연관이 없다.

메서드

메서드는 함수의 일종인데, 함수는 ‘하나의 기능을 수행하는 일련의 코드’를 말한다. 우리가 특정 입력을 넣으면 그에 맞게 함수 내에서 연산하고 결과값을 돌려준다. 물론 입력값이 없거나 결과값도 없이 만들 수도 있다. 즉 함수는 어떤 기능을 수행하도록 미리 구현해 두고 필요할 때마다 호출하여 사용하는 것이다. 함수를 정의하는 것은 다음과 같다. (메서드도 똑같다.)

(반환형) 함수이름 (매개변수...) {
    내용
}

// 실전
int add (int num1, int num2) {
    int result;
    result = num1 + num2;
    return result;
}

함수 이름은 add이고, 매개 변수로는 num1, num2이고 int 형으로 반환하겠다고 만든 함수이다. return은 반환 값이라 해서 결과 값을 돌려줄 때 사용하는 것이다. 또는 return만 쓸 수 있는데, 이것은 함수 수행을 끝내고 호출한 곳으로 돌아갈 때도 쓸 수 있다. 메서드의 이름은 첫단어는 소문자로 시작하고 이후 단어의 시작은 대문자로 시작하면 된다. 이를 낙타의 등과 같은 형태라 하여 camel notation이라고 한다. ex) setStudentName

실습해볼 코드는 다음과 같다.

public class Student {
    int studentID;
    String studentName;
    int grade;
    String address;

    public String getStudentName() {
        return studentName;
    }

    public void setStudentName(String name) {
        studentName = name;
    }
}

Student클래스로 멤버 변수와 메서드로 구성되어 있다. 이제 이것을 활용해서 드디어 객체를 만들어 볼 것이다. 여기서 잠깐 main()함수에 대해서도 살펴 보겠다. main() 함수는 자바 가상 머신(JVM)이 프로그램을 시작하기 위해 호출하는 함수이다. 클래스 내부에서 만들었지만, 클래스의 메서드는 아니다. main() 함수에서 클래스를 사용하는 방법은 두 가지가 있다. 하나는 우리가 만든 클래스 내부에 main() 함수를 만드는 것이고, 또 하나는 외부에 테스트용 클래스를 만들어 사용하는 것이다. 아래서는 클래스 내부에 만드는 방법이다.

public class Student {
    int studentID;
    String studentName;
    int grade;
    String address;

    public String getStudentName() {
        return studentName;
    }

    public void setStudentName(String name) {
        studentName = name;
    }

    public static void main(String[] args) {
        Student studentHong = new Student();
        studentHong.studentName = "홍석주";

        System.out.println("studentHong.studentName");
        System.out.println(studentHong.getStudentName());
    }
}

다음으론 테스트 클래스를 만들어 사용하는 방법이다. 사용법은 같다.

public class StudetTest {
    public static void main(String[] args) {
        Student studentHong = new Student();
        studentHong.studentName = "홍석주";

        System.out.println("studentHong.studentName");
        System.out.println(studentHong.getStudentName());
    }
}

여기서 Student, StudentTest 클래스는 같은 패키지에 있어서 위처럼 해도 동작하지만 만약, 서로 다른 패키지에 있다면 import문을 사용해서 불러와야 한다.

위에 예제들처럼 클래스를 생성하려면 new 예약어를 사용해야 한다.

클래스형 변수이름 = new 생성자;

이런식의 구조로하여 생성자를 호출하면 새로운 클래스가 생성되고 이것은 메모리 공간(힙 메모리)에 올라간다. 이렇게 사용할 수 있도록 생성된 클래스를 인스턴스라고 하며, 이 인스턴스를 가리키는 클래스형 변수를 참조 변수라고 한다.

힙 메모리는 프로그램에서 사용하는 동적 메모리(dynamic memory) 공간을 말한다. 일반적으로 프로그램은 스택, 힙, 데이터 이렇게 세 영역을 가지고 있는데, 객체가 생성될 때 사용하는 공간이 힙이다. 힙은 동적으로 할당되며 사용이 끝나면 메모리를 해제해 주어야 한다. C나 C++은 프로그래머가 직접 해제해줘야 하는데, 자바에서는 가비지 컬렉터(Garbage Collector)가 자동으로 메모리를 해제해준다.

이 인스턴스를 참조하는 참조 변수를 사용하면 인스턴스의 멤버 변수와 메서드를 참조할 수 있다. 이 때 사용되는 것인 도트(.) 연산자이다. 위 예제 studentHong.studentName = "홍석주";의 부분이 그런 것이다.

인스턴스를 만들게 되면 힙 메모리에 올라가게 되며 이를 참조 변수는 인스턴스를 가리킨다고 했다. 이를 좀 더 명확히 보고자 한다면 참조 변수를 출력해보면 된다.

Student studentHong = new Student();
studentHong.studentName = "홍석주";

Student studentKim = new Student();
studentHong.studentName = "김춘식";

System.out.println(studentHong);
System.out.println(studentKim);

이렇게 하여 결과값을 보면 Student@16f65612, Student@311d617d 등으로 보일텐데 클래스이름@주소값 형태이다. 여기서 나오는 주소값은 해시 코드 값이라고 해서 생성된 객체에 할당하는 가상 주소이다. 이를 확인하여 서로 다른 주소임을 확인하고 다른 객체임을 확인할 수 있다.

생성자

생성자란 클래스를 생성할 때 사용된다. 이전까지는 person.name = “홍석주” 이런식으로 클래스를 만든 후 도트 연산자로 해줬는데, 생성자를 활용하면 우리는 초기값들을 세팅해줄 수 있다.

Person person = new Person();

디폴트 생성자

우리는 생성자라는 것을 만들어 준적이 없는데, 알아서 실행이 되고 있던 것이다. 이것은 자바에서 클래스를 만들면 생성자가 없으면 기본적으로 컴파일할 때 디폴트 생성자를 만들어 주기 때문이다. 컴파일러가 만들어 주는 생성자는 다음과 같다.

public class PErson {
    String name;
    float height;
    float weight;

    // 아무것도 없는 디폴트 생성자이다.
    public Person() {}
}

그렇다면 우리가 임의로 생성자를 만들어 보자.

public class PErson {
    String name;
    float height;
    float weight;

    
    public Person(String name) {
        this.name = name;
    }
}

여기서 this란 객체 자신을 가리키는 것이다. 이 this를 활용하면 각 객체들마다 자신만의 값을 선언하여 활용할 수 있게 되는것이다. 여기서 주의할 점은 위처럼 생성자를 만든다면, 디폴트 생성자는 안 만들어진다. 이점은 유의해서 사용하자. 필요하면 디폴트 생성자도 만들어 주면 된다.

생성자 오버로드

디폴트 생성자라던가 임의의 생성자를 여러개 만들어 두는 경우를 생성자 오버로드라고 한다. 자바에선 이런 기능을 활용하여 매개변수가 다른 생성자를 여러 개 만들 수 있고, 필요에 따라 사용하면 된다.

정보 은닉

우리는 계속해서 public이라는 예약어를 많이 사용했다. public은 접근 제어자 중 하나이다. 만약에 public이었던거를 private로 바꾼다면 다른 클래스에서 이를 사용할 수 없다. 접근 제어자를 활용하면 변수나 메서드가 어디까지 쓰일지를 정할 수 있다. private를 사용하면 다른 클래스에서 못 사용하니 보안적으로는 좋아지나 사용은 필요할 것이다. 그렇다면 어떻게 해야할까. 그렇게 해서 만드는 메서드로 getter, setter 메서드들이다. 예시를 보자

public class MyDate {
    public static void main(String[] args) {
        private int day;
        private int month;
        private int year;

        public setDay(int day) {
            if(month == 2) {
                if(day < 1 || day > 28) {
                    System.out.println("오류입니다.");
                } else {
                    this.day = day;
                }
            }
        }
    }
}

MyDate클래스가 있고, day, month, year에 대해서 private 접근자를 지정했다. 만약에 변수들이 public이었다면, 사용자가 막 지정할 것이다. 그렇지만 private로 되어있기에 setter()함수를 사용해야 하고, 이 함수에서 유효성 같은 것들을 관리하여 값을 올바르게 할당할 수 있게 된다. 아래는 접근제어자 표이다. |접근 제어자|설명| |—–|—-| |public|외부 클래스 어디에서나 접근할 수 있다.| |protected|같은 패키지 내부와 상속 관계의 클래스에서만 접근할 수 있고 그 외 클래스에서는 접근 불가| |아무것도 없는 경우|default이며 같은 패키지 내부에서만 접근할 수 있다.| |private|같은 클래스 내부에서만 접근할 수 있다.|