티스토리 뷰

donaricano-btn
반응형

String 은 웹개발을 할 때 가장 많이 사용하는 자료형입니다. 그러다보니 실무에서는 String 객체를 잘 알고 활용하는 것이 중요합니다. 그런데 생각보다 String 객체를 잘 못 사용해서 생기는 버그가 많습니다. 오늘은 간단하게 String 객체를 비교할 때 주의해야 할 점 세 가지를 알아 보도록 하겠습니다.

 

String class 의 method 를 소개한 포스트가 있으니(링크) 참고하시면 좋을 것 같습니다.

 

1. '==' vs equals()

 

String 자료형을 선언하는 방법은 크게 두 가지가 있습니다. 하나는 리터럴 문자열 값을 대입하는 것이고 하나는 new 키워드를 사용해 String 객체를 직접 생성하는 방법입니다. 그런데 두 값을 '==' 연산을 통해 비교하면 같은 문자열을 저장하고 있어도 false 값이 나오는 것을 알 수 있습니다. 

 

        String a = "태권브이";
        String b = "태권브이";
        String c = new String("태권브이");
        String d = new String("태권브이");
        
        System.out.println(a == b); //true
        System.out.println(a == c); //false
        System.out.println(c == d); //false

 

위 코드를 실행해 보면 첫 줄에는 true 두번째, 세번째 줄에는 false 를 출력하게 됩니다. 분명히 "태권브이" 라는 같은 문자열을 저장하고 있는 객체지만 '==' 연산으로는 제대로 비교가 되지 않습니다. 그 이유는 '==' 연산은 참조 변수가 가르키고 있는 주소값을 비교하기 때문입니다. 각각의 String 객체는 각각 메모리의 다른 영역에 선언되어 있기 때문에 '==' 연산을 하게되면 다른 주소값을 비교 하게 되고 false 값이 나오게 됩니다. 그런데 왜 리터럴 값으로 바로 선언한 a 와 b 는 비교하면 true 값이 나오는 걸까요? 비밀은 String constant pool에 있습니다.

 

 

String은 개발자가 가장 많이 사용하는 객체 중 하나이고 실제로 메모리 영역에는 String 객체가 무수히 많이 생성되게 됩니다. 그런데 같은 문자열 값을 가진 똑같은 객체가 힙영역에 아주 많이 존재하게 된다면 그것은 아주 큰 메모리 낭비일 것입니다. 그래서 JVM은 힙영역중 일부에 String 객체를 관리하는 String Pool을 만들어 두고 사용합니다.

 

개발자가 리터럴 값을 통해 String 변수를 선언하면 호출하는 String 객체의 문자열 값이 String constant pool 있는지 찾아보고 있으면 해당 주소값을 반환합니다. 없다면 String Pool 내부에 새로운 객체를 생성합니다. 이 역할을 하는 것이 String 클래스 내부에 있는 intern() 메소드 입니다. 

 

그래서 사실 리터럴 값으로 선언된 값은 객체 선언 후 intern()메소드를 실행한 것과 같습니다.

 

String a = "태권브이";
String b = new String("태권브이").intern();

System.out.println(a == b); // true

 

위 코드를 실행해 보면 true값이 반환되는 것을 알 수 있습니다. intern() 메소드를 호출해 생성된 객체를 String Pool에서 찾아보고 같은 주소값을 저장했기 때문입니다.

 

이런 문제 때문에 문자열을 비교할 때는 '==' 연산을 사용하는 것에 주의 해야 합니다. String Pool 에 존재하지 않는 문자열 객체라면 다른 주소값을 반환하게 될 것이고 원하는 결과 값을 얻지 못할 확률이 큽니다. 같은 내용의 문자열인지를 비교하려고 할 때는 equals() 메소드를 사용하는 것이 더 좋습니다.

 

    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

 

위 코드는 String 객체 내부에 있는 equals 메소드 입니다. 먼저 주소값을 비교하고 String 객체인지 확인 한 뒤 문자열을 하나하나 비교하며 같은 문자열인지를 확인합니다.

 

2. equals() 메소드를 쓸 때 Null Pointer Exception

 

equals() 메소드를 사용하는 것이 정확한 문자열 비교에 적합한 것은 사실이지만 한가지 문제를 야기시킬 수 있습니다. 바로 Null Pointer Exception 이라는 예외를 발생시킬 수 있다는 것입니다. 

 

    void makeNPEByEquals(String myName) {
        if (myName == "북항") {
            // 1
        }
        if ("북항" == myName) {
            // 2
        }
        if (myName.equals("북항")){
            // 3
        }
        if ("북항".equals(myName)) {
            // 4
        }
    }

 

위 코드 중 실제로 null pointer exception 을 발생시키는 것은 몇 번 조건문일까요? 당연히 '==' 연산이 사용된 위의 두 조건문은 아닐 것이고 equals() 메소드를 사용한 아래 두 조건문 중 하나가 답일 것입니다. 정확한 답을 알기 위해서는 일단 위에 있는 equals() 메소드의 코드를 확인해 보겠습니다.

 

먼저 자신의 value 값과 파라미터 값으로 전달받은 객체의 주소값을 비교하고 그 다음에는 파라미터로 전달받은 객체가 String 객체인지 확인한 후 문자열 비교를 시작합니다. 그렇다면 여기서 일단 파라미터 값으로 전달받은 값에 대해서는 Null Pointer Exception 을 걱정하지 않아도 된다는 것을 알 수 있습니다. String 객체인지 확인하는 과정에서 걸러질 테니까요.

 

결국 정답은 3번 이었습니다. 만약 myName 으로 null 값이 전달된다면 Null pointer Exception 이 발생하게 될 것입니다. 그래서 equals() 메소드를 사용할 때는 4번과 같은 형태로 사용하는 것이 좋습니다.

 

3. substring() 메소드의 비교

 

마지막으로 substring() 메소드를 사용해서 자른 문자열을 비교하는 경우를 생각해 보겠습니다.

 

        String a = "태권브이";
        String b = "태권브이";
        String c = "태권브이 이겨라";
        
        System.out.println(a == b.substring(0,4)); // true
        System.out.println(b == c.substring(0,4)); // false

 

위 코드에서 첫번째 줄은 true, 두번째 줄은 false를 반환하게 됩니다. 역시 substring() 메소드를 살펴보고 왜 이런 결과가 나타났는지 확인해 보겠습니다.

 

    public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }
    
    public String substring(int beginIndex, int endIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        if (endIndex > value.length) {
            throw new StringIndexOutOfBoundsException(endIndex);
        }
        int subLen = endIndex - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
    }

 

위 코드는 String class 안에 있는 substring 메소드 입니다. 중요한 것은 마지막 return 문 입니다. ruturn 의 삼항식에서 만약 자른 문자열의 길이가 원본의 길이와 같으면 자기자신 this 객체를 반환하고 그렇지 않다면 new 키워드를 사용해 새로운 객체를 생성해서 반환하는 것을 알 수 있습니다.

 

결국 substring() 메소드를 통해 자기자신을 반환했기 때문에 위 코드에서 첫번째 줄은 true가 된 것이고 새로운 객체를 생성해서 반환한 두번째 줄은 false가 된 것입니다.

 

 


 

이외에도 String 을 쓰면서 마주할 수 있는 문제들이 다양합니다. 웹 개발 분야에서는 정말 문자열이 많이 사용되기 때문에 String 객체를 잘 사용하는 것이 중요하다고 생각합니다. 추후에 또 String과 관련된 포스팅을 이어가도록 하겠습니다. 감사합니다.

반응형

'language > JAVA' 카테고리의 다른 글

옵저버 패턴(Observer Pattern)  (0) 2020.02.12
전략 패턴(Strategy Pattern)  (0) 2020.02.11
추상 클래스와 인터페이스의 차이  (3) 2019.10.14
final 키워드  (0) 2019.10.12
String class methods  (0) 2019.07.04
donaricano-btn
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
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
글 보관함