티스토리 뷰

자바를 처음 배울 때 어려운 개념 중 하나가 변수의 참조다. 참조는 어떤 객체를 가리키는 값인데 C의 포인터와는 좀 다르지만 비슷하게 이해할 수 있다. 객체를 가지고 프로그램을 만들어야 하는 것이 자바이므로 참조는 자바에서는 가장 근본이 되는 개념이다.

이것은 프로그래밍 언어의 개념에서 값과 참조의 구분에 해당한다. 자바 언어에서 변수는 기본 타입(int, short, long, byte, float, double, char, boolean 8개)은 값 변수, 객체 타입(또는 복합타입이라고도 함)은 참조 변수다. 값 변수는 자체로 메모리와 값을 가진다. 참조 변수는 어딘가에 저장되어 있는 객체를 가리킨다(참조만 가짐). 참조 변수는 일반적으로 선언할 때 null로 초기화하고 필요할 때 new Student(); 처럼 객체를 생성하여 가리키게 된다. 

Student st = null;
...
st = new Student(); 
값변수와 참조 변수의 차이가 가장 잘 나타나는 것은 ==이다. 다른 두 개의 값 변수는 값이 같으면 ==이 참이 된다.

int a, b;
a = scan.nextInt();
b = scan.nextInt();
if (a == b) ...

이 때 a와 b는 각각 다른 메모리를 차지하고 있고 ==은 그 안에 들어있는 값이 같은지를 비교한다.

그러나 참조 변수는 ==이 다른 방식으로 비교된다.

Student st1=new Student();

st1.read(scan);   // 학생 데이터 입력 메소드 Student st2=new Student(st1); // 똑같은 객체를 만드는 복사생성자 if (st1 == st2) ...

위 코드에서 ==은 항상 거짓이 된다. 설사 st1과 st2의 학생 데이터가 완전히 같다(모든 필드가 동일)고 하더라도 거짓이다. 왜냐하면 참조 변수의 ==은 참조값이 같은가를 비교하기 때문이다. 참조는 위에서 얘기했듯이 객체를 가리키는 값이고 C의 포인터와 비슷하여서 C의 포인터를 ==로 비교한 것과 마찬가지로 같은 객체를 가리키고 있는 경우에만 같다.

그럼 두 객체가 값이 같다는 것은 어떻게 나타낼 수 있을까? 쉬운 예로 String 객체의 비교를 생각해 보자. 

String str1 = "ObjectAndReference";
String str2 = scan.next();  // 사용자가 "ObjectAndReference"를 입력함
if (str1 == str2) ...

이 경우 ==은 거짓을 반환한다. 왜? str1이 가리키는 객체와 str2가 가리키는 객체가 다르므로... 그럼 우리는 그 값이 같다는 것을 어떻게 비교해야 할까? equals의 역할이 처음 등장하는 예가 이러한 스트링이 같은가를 비교할 때다.

if (str1.equals(str2)) ...

이 if 문은 참이 되어서 if 부분을 수행하게 된다. 즉 두 스트링이 다른 객체이지만 두 객체가 가지고 있는 값이 같은가를 비교하여 참거짓을 돌려주는 메소드가 equals이다. (C에서의 strcmp와 비슷하다고 볼 수 있다)

그럼 이제 자바의 한겹을 열고 들어가 보자. 과연 이 equals란 어떻게 동작하는 걸까? (C는 strcmp라는 함수가 있음을 우리는 알고 있다. C는 숨겨진 것이 거의 없다. 정보은닉이 별로 없는 언어다. 그래서 쉬울 것 같지만 사실 배우기 더 어렵다^^)

equals 메소드의 기원을 찾아보면 이것은 Object 클래스에 있는 메소드다. 자바의 모든 클래스가 Object를 상속한다는 것은 잘 알고 있을 것이다. 이 Object의 equals는 참조가 같은가를 비교하는 ==과 동일하게 정의된다. 그런데 Object가 가진 equals와 String이 무슨 상관인가? String은 Object를 상속하였는데, 자기 자신의 equals를 오버라이드하여 재정의하였다. 그 재정의한 String 클래스의 equals가 두 스트링이 가진 문자열의 값이 같은가를 비교하게 된다. 그러면 str1.equals(str2)에서 불려지는 메소드는 String 클래스의 equals가 된다.

여기까지는 별로 어렵지 않다. 그러나 사용자 정의 클래스의 경우에는 조금 더 복잡해 진다. Student 클래스의 예를 다시 생각해 보자. Student가 이름, 학과, 학년, 성별, 생년월일을 가진다고 할 때 우리는 동명이인을 생각하면 이름, 학과, 생년월일이 동일하면 같은 인물이라고 정할 수 있다[각주:1]. 같은 사람에게 학번이 두 번 발급되어서는 안 될 것이다. 그럼 우리는 equals로 이러한 동일인 비교를 하려고 한다. 

Student st1=null, st2=null;
st1.read(scan);   // 학생 데이터 입력 메소드
st2.read(scan);
if (st1.equals(st2)) ...

그럼 그런 equals가 언제 true일지를 알려주는 것은 Student 클래스가 해 주어야 한다. Student 클래스의 equals 메소드에서 이러이러한 값이 다 같으면 true를 반환하는 코드가 들어있어야 한다. 그럼 컴파일러는 st1.equals 부분에서 Student의 equals 메소드를 불러주게 된다.

이제 구체적으로 Student의 equals 메소드를 정의하는 방법을 살펴보자. 몇 가지 주의할 사항이 있다.

@Override
public boolean equals(Object other) {
     if (...) return true;
     return false;
}

첫번째 줄의 @Override는 이 메소드가 상속되어 오버라이드한 것임을 나타낸다. 즉 조상 클래스 어딘가에 정의된 equals를 다시 정의해서 이 클래스에 대해 사용함을 나타낸다. 두 번째 줄의 메소드 시그너춰(접근권한, 반환형, 이름, 매개변수)는 똑같이 써주어야 컴파일을 통과할 수 있다. 그 이유는 오버라이드된 경우는 조상 클래스(이 경우는 Object 클래스)에서 정의한 시그너춰와 일치해야 하기 때문이다. 함수 몸체부의 코드는 어떤 경우에 같다고 할 것인지를 if 문으로 표시하여 true를 리턴하게 하고 그렇지 않은 경우에는 false를 리턴한다. 이 경우 다음과 같은 코드가 될 수 있다.

Student otherSt = (Student)other;
if (name.equals(otherSt.name) && dob.equals(other.dob)) ...

여기서 좀더 복잡한 문제가 있는데, other가 Student가 아닌 다른 클래스의 객체가 오는 경우를 어떻게 처리할 것인가의 문제다. 학생은 학생과만 같은지 비교할 수 있다면 다른 클래스 객체가 온 경우는 그냥 false를 리턴하면 된다. 또 null 값이 온 경우도 미리 검사해 주어야 한다. 잘못하면 if 조건문을 검사하는 과정에서 NullPointerException이 발생할 수 있다. 

@Override

public boolean equals(Object other) { if (this == other) return true; if (other == null) return false; if (!(other instanceof Student)) return false; if (...) return true; return false; }

첫 if 문에서는 other가 이 객체와 같은 것을 가리킨다면 비교할 것도 없이 true를 리턴한다. 그리고 null이거나 Student 타입이 아닌 객체라면 false를 리턴한다.

이제 equals 메소드를 다 만들었는데, 이것이 어떻게 사용되는지를 좀더 살펴보자. 위에서 본 것처럼 학생.equals(다른학생) 이렇게 비교하는 목적으로 같은 학생이 두 번 만들어진 경우를 찾을 수 있는데, 이것을 ArrayList와 같은 컬렉션 클래스와 결합하면 진짜 equals의 진가를 발휘하게 된다. ArrayList에는 contains라는 메소드가 주어진 객체가 이미 들어있는가를 검사해주는데, 이때 꼭 같은 객체 참조가 아니고 equals로 값이 같은 것도 검사할 수 있다. 또한 indexOf라는 메소드는 객체와 같은 것이 있으면 그 인덱스를 돌려준다. 이 두 메소드는 ArrayList 안에 있는 객체들을 매개변수로 온 객체와 다 equals로 비교해서 같은 것을 찾아주게 구현되어 있다.

ArrayList<Student> list = new ArrayList<>();
while (scan.hasNext()) {
        st = new Student();
        st.read(scan);
        list.add(st);
}

위 코드는 파일에서 학생 정보를 읽어들여 객체에 저장하고 학생 객체들을 list에 모아두는 것이다. 이 때 같은 학생이 두번 들어온 경우는 제외하고 싶다.

while (scan.hasNext()) {
     st = new Student();
     st.read(scan);
    if (list.contains(st)) {
         System.out.println(st.name + " : 이미 등록된 학생입니다.");
         continue;
     }
     list.add(st);
}

이와 같이 자바 라이브러리 클래스들은 equals가 클래스에서 제공한 "같다"의 로직을 가지고 있다고 보고 그것을 이용한다. 그러므로 우리가 equals 메소드를 오버라이드하면 많은 자바 클래스에서 그 equals가 작동해서 우리가 원하는 "같다"를 가지고 비교 또는 검색을 해준다. 이러한 방식이 Object 클래스의 equals에 의해 보장되는 것으로 이것은 자바의 거대한 프레임워크에 우리가 만든 클래스를 끼워넣기 위해 중요한 연결고리가 된다. 

또한 최근 버전에서 스트링의 equals 대신 contentEquals를 쓰도록 권장하고 있다. equals는 기본적으로 Object에 정의된 메소드를 오버라이드하여 같은 타입의 객체가 아니면 무조건 거짓이 된다. 그러나 스트링의 경우는 문자 배열이나 StringBuilder 같은 문자열을 가지는 다른 타입 객체의 내용과 비교해야 되는 일도 가끔 생긴다. 그래서 equals보다는 contentEquals를 쓰는 것이 더 바람직하다.

  1. 사실 두 객체가 언제 같다고 볼 것이냐는 소프트웨어와 데이터베이스에서는 중요한 문제다. 웹사이트에 동일인이 두번 가입하지 못하게 하려면 이러한 구분이 필요할 것이다. 같은 사람을 여러 번 저장하는 것은 메모리나 효율 면에서도 문제가 되지만 정책적으로 분명하게 정해서 구현되어야 하는 중요한 결정사항이다. [본문으로]
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함