( equals와 ==를 공부하게 된 계기이므로 넘어가도 좋습니다 )
워크북 서비스에서 포스트를 검색할 때 지금은 제목+내용으로 키워드 검색이 가능하도록 개발해 두었지만, 초기에는 searchType을 받아 title이면 제목을, content면 내용을 검색하도록 설계했었습니다.
searchType을 비교하여 분기문을 타도록 구현하려 했었는데, 분명히 title을 파라미터로 받았음에도 title 분기문을 타지 않는 현상이 발생했습니다. 디버깅 결과, java의 equals와 ==를 혼동했기 때문임을 알게 되었고 본 포스트를 작성하게 되었습니다.
실제 service 코드는 아니지만 겪었던 문제 상황을 재현한 자바 코드입니다. service 코드에서는 title 스트링을 request param을 통해 전달받았습니다.
public class EqualsPractice {
public static void searchKeyword(String searchType){
if (searchType == "title") {
System.out.println("제목으로 검색");
}
else {
System.out.println("내용으로 검색");
}
}
public static void main(String[] args) {
String title = new String("title");
searchKeyword(title);
}
}
예를 들어, 이러한 코드가 있다고 생각해봅시다. 분명 "title"이라는 String 객체를 만들고, searchType으로 넘겨주었습니다. 분명 이 코드를 실행하면 "제목으로 검색"이 출력될 것 같습니다.
그러나 실제 출력 결과를 보면
"내용으로 검색"이 출력되는 것을 볼 수 있습니다. 왜 이런 결과가 출력된 것인지 확인해보겠습니다.
== 논리 연산자는 비교하고자 하는 대상의 주소 값을 비교합니다.
그렇다면 String 객체를 생성할 때, 어떻게 주소 값이 부여되는지 살펴볼 필요가 있겠습니다.
public class EqualsPractice2 {
public static void main(String[] args) {
String a = "abc";
String b = "abc";
System.out.println(a == b);
String c = new String("abc");
System.out.println(a == c);
}
}
이 코드를 실행시키면 출력 결과가 어떨까요?
정답은 true, false입니다.
Java에서의 String은 일반적인 Heap 영역에도 저장할 수 있지만, HashTable 구조를 가지는 String constants Pool이라는 공간에 저장이 가능합니다. String constants Pool에 저장된 String 값은 불변성(Immutability)을 가지며, 불변성을 가진다는 의미는 값은 변함이 없고 동일한 String 값을 가지고 있다면 같은 주소를 가리킨다는 의미입니다.
그림을 참고해봅시다.
String literal로 생성한 String 값은 Heap 영역 내 "String Constant Pool"에 저장되어 재사용될 수 있습니다. 그러나 new 연산자로 String 객체를 생성할 경우 같은 내용이라도 여러 개의 객체가 각각 Heap 영역을 차지하게 됩니다.
다시 코드로 돌아가보면, a와 b는 literal로 생성한 String 객체임을 알 수 있습니다. 따라서 String Pool에 저장되어 있을 것이며, 동일한 값을 저장하고 있기 때문에 동일한 주소를 바라보고 있습니다. a == b에서 == 는 주소 값을 비교하는 논리연산자이기에 True라는 값이 나오는 것을 이해할 수 있습니다.
a는 literal로 생성한 String 객체인 반면, c는 new 연산자를 통해 heap 영역에 생성한 String 객체입니다. 코드에서 a == c를 했을 때, 같은 값임에도 다른 주소를 바라보고 있기 때문에 false라는 결과가 나온다는 것을 알 수 있습니다.
그렇다면 equals는 어떨까요?
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) { //String인지 Object의 타입 체크
String aString = (String)anObject; //String으로 형변환
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}
String.java에서 equals를 정의하고 있는 부분입니다.
1) this와 anObject의 주소 값이 같다면 true를 return하고 있습니다.
2) coder는 바이트를 인코딩하는 데 사용되는 인코딩의 식별자입니다. LATIN1, UTF16 인코딩 타입을 지원하고 있습니다.
public static boolean equals(byte[] value, byte[] other) {
if (value.length == other.length) {
for (int i = 0; i < value.length; i++) {
if (value[i] != other[i]) {
return false;
}
}
return true;
}
return false;
}
3) 만약 Latin1 타입일 경우, equals 함수입니다. 주소 값이 아닌, 각각의 value를 비교하고 있는 것을 알 수 있습니다.
이처럼 equals는 1) 주소 값이 같거나(= 결국 값도 같다는 의미) 2) 값이 같을 때 true를 반환하는 함수임을 알 수 있었습니다.
다시 처음의 문제로 돌아가봅시다.
public class EqualsPractice {
public static void searchKeyword(String searchType){
if (searchType == "title") {
System.out.println("제목으로 검색");
}
else {
System.out.println("내용으로 검색");
}
if (searchType.equals("title")) {
System.out.println("제목으로 검색");
}
else {
System.out.println("내용으로 검색");
}
}
public static void main(String[] args) {
String title = new String("title");
searchKeyword(title);
}
}
equals를 추가한 코드를 실행해봅시다.
주소 값으로 비교했을 때는 "내용"이, 값으로 비교했을 때는 "제목"이 출력됨을 확인할 수 있었습니다.