불필요한 코딩을 줄이자!
아파치 Commons Lang 클래스 네 개로 코드 재사용의 이점을 배워보자
난이도 : 중급
Andrew Glover, 필자 겸 개발자
원문 게재일 : 2008 년 12 월 16 일
번역 게재일 : 2009 년 2 월 10 일
아 파치 Commons 프로젝트의 Lang 라이브러리에 포함된, 실전을 통해 다듬어진 오픈 소스 유틸리티를 활용해 코딩을 줄여 봅니다. 다른 사람이 작성한 신뢰성 높은 코드를 재사용하면 여러분의 소프트웨어를 더욱 빨리 출시할 수 있고 오류도 줄일 수 있습니다.
시작하기 전에
이 튜토리얼에 대해
Commons Lang은 자바(Java™) 언어를 이용한 소프트웨어 개발의 다양한 측면에 관련된 많은 부 프로젝트를 가진 대규모 프로젝트인 아파치 Commons의 컴포넌트 중 하나다. Commons Lang은 표준 API인 java.lang
을 확장하여 문자열 처리 메서드, 기본 수학 메서드, 객체 리플렉션(reflection), 객체 생성과 직렬화(serialization), System
프로퍼티(property) 등을 제공한다. 또한 상속 받을 수 있는 열거형(enum
type), 여러 형태의 중첩된 예외(nested Exception
), java.util.Date
에 대한 개선, hashCode
, toString
, equals
같은 메서드를 구현하는 데 도움이 되는 유틸리티도 제공한다. 나는 Commons Lang이 서로 다른 다양한 애플리케이션 분야에
걸쳐 유용함을 알게 되었다. Commons Lang을 사용함으로써 코딩을 줄일 수 있을 것이고 결과적으로 상용 소프트웨어를 더
빨리 출시할 수 있고 오류도 줄일 수 있다. 이 튜토리얼에서는 몇 가지 Commons Lang 클래스를 사용하는 데 있어 기본
개념을 단계별로 살펴보고, 많은 코드를 직접 작성할 필요가 없도록 해당 코드를 활용해 본다.
목표
다음 내용을 배운다.
equals
와hashCode
같이 정해진 규칙에 따라 구현해야 하는 메서드를 구현해 본다.- 구현된 메서드의 동작을 검증해 본다.
Comparable
인터페이스의compareTo
메서드를 구현해 본다.
이 튜토리얼을 마칠 때쯤이면 Commons Lang 라이브러리의 혜택을 이해하고 코드를 적게 쓰는 법을 배울 것이다.
필요한 사전 지식
이 튜토리얼을 충분히 활용하려면 자바 문법과 자바 플랫폼 상의 객체 지향 개발에 대한 기본 개념에 친숙해야 한다. 또한 리펙터링(refactoring)과 통상의 단위 테스트(unit testing)에도 친숙해야 한다.
시스템 요구 사항
이 튜토리얼을 따라가고 예제를 실행해 보려면 다음이 필요하다.
- 다음 둘 중 하나가 설치되어 있어야 한다.
- The Commons Lang 프로젝트의 현재 배포판(이 글을 쓰는 시점에서는 2.4 버전이다). 다운로드하고 압축을 푼 다음 commons-lang-2.4.jar를 클래스 경로(classpath)에 추가한다.
이 튜토리얼을 위해 권장하는 시스템 구성은 다음과 같다.
- 썬 JDK 1.5.0_09(또는 더 최근 버전)나 IBM JDK 1.5.0 SR3을 지원하고 최소 500MB 이상의 주 메모리를 가진 시스템
- 이 튜토리얼에서 다룰 소프트웨어 컴포넌트와 예제를 설치하는 데 디스크 공간에 최소 20MB의 여유가 있어야 한다.
이 튜토리얼의 지시와 예제는 마이크로소프트 윈도우(Microsoft® Windows®) 운영체제를 가정한 것이다. 하지만 이 튜토리얼에 소개된 모든 도구는 리눅스(Linux®)와 유닉스(UNIX®) 시스템에서도 동작한다.
코드 재사용의 이점
소 프트웨어 개발 초창기에는 개발자의 생산성이 개발자가 작성하는 코드 분량에 비례한다고 생각했다. 이는 그 당시에는 그럴 듯해 보이는 기준이었는데, 코드는 궁극적으로 잘 동작할 것으로 생각되는 바이너리 자산을 만들어낼 것이므로 많은 코드를 작성하는 것처럼 보이는 사람은 동작하는 애플리케이션을 향해 열심히 일하고 있는 것이어야 했다. 이 기준은 다른 산업에도 적용되는 것 같이 보인다. 더 많은 세금 환급을 처리하는 회계사나, 더 많은 에스프레소 음료를 만들어내는 바리스타(barista)가 생산성이 높아야 한다. 그렇지 않나? 양쪽 다 만들어 내야 하는 아이템을 많이 만들어 내기 때문에 각자의 사업을 위해 명백히 더 많은 수입을 올려야 한다.
하지만 시간이 흐르면서 작성한 코드 줄 수가 많다고 생산성이 높지는 않음을 알게 되었다. 많은 양의 코드는 분명 활동이 있다는 뜻이다. 하지만 활동이 반드시 일의 진척과 관련 있지는 않다. 매일 부정확한 세금 환급을 만들어 내는 회계사는 무척 활발하지만 고객이나 고용주 입장에서는 별 가치가 없다. 또 커피를 눈깜짝할 사이에 내어 놓지만 주문을 잘못 받는 바리스타는 분명 많은 활동을 하지만 생산적이지는 않다.
더 많은 코드는 더 많은 오류를 의미할지도 모른다
다행히도 소프트웨어 업계는 코드가 너무 많으면 나쁠 수도 있다는 점을 통상 인정한다. 두 개의 연구 결과에 따르면 평균적인 애플리케이션은 통상 코드 1000줄 당 20개에서 250개의 오류를 가지고 있다고 한다(참고자료). 이 척도는 오류 밀도(defect density)로 알려져 있다. 이 데이터로부터 끌어낼 수 있는 주된 결론은 바로 코드 줄 수가 적으면 오류도 적다는 것이다.
물
론 여전히 코드는 작성해야 한다. 현재 기술 수준은 아직 애플리케이션이 스스로를 작성할 수 있는 단계에는 이르지 못했다. 하지만
이제 많은 코드를 빌려올 수는 있다. 아직 업무 컴포넌트(business component)에 있어 재사용을 현실화하지는
못했다. 즉 예를 들어 한 개발자가 다른 개발자의 Account
객체를 재사용할 수 있게 하려는 비전 말이다. 하지만 플랫폼 관점에서 재사용은 이미 우리 곁에 있다. 오픈 소스 프레임워크와 재사용하기 쉬운 지원 코드의 확산은 (예를 들면) Account
객체를 가능한 적은 줄의 코드로 구현하는 데 도움이 된다.
예를 들어, 하이버네이트(Hibernate)와 스프링(Spring)은 자바 공동체 내에서 널리 사용된다. Account
객체를 예로 들면 오늘날 (Account
객체가 필요한) 온라인 주문 애플리케이션을 만드는 신규 개발 프로젝트에 투입되는 팀이라면 객체 관계
매핑(object-relational mapping, 줄여서 ORM) 프레임워크를 완전히 새로 만드는 대신 하이버네이트나 그에
맞먹는 괜찮은 ORM 프레임워크를 사용함으로써 굉장한 이득을 얻을 수 있다. 단위 테스트(JUnit 같은 것을 쓰지 않을까?)나
의존성 주입(dependency injection, 이 경우라면 스프링(Spring)이 가장 가능성 큰 후보일 것이다) 같은
애플리케이션의 다른 측면도 마찬가지다. 이게 바로 재사용이다. 그저 우리가 한 때 재사용이 이러저러할 것이라고 생각했던 것과
다를 뿐이다.
이런 프레임워크를 빌리거나 재사용하면 궁극적으로 코드를 덜 작성할 수 있고 당면한 업무 문제에 더 적절히 집중할 수 있다. 프레임워크 자체는 많은 코드로 이뤄져 있으나 여기서 중요한 것은 여러분이 프레임워크를 작성하거나 유지 보수할 필요가 없다는 것이다. 이것이 바로 성공적인 오픈 소스 프로젝트의 좋은 점이다. 즉, 다른 사람들이 여러분을 위해 작성하고 유지 보수해 준다. 그리고 당연하지만 그 사람들은 그런 일을 하는 데 있어 여러분보다 낫다.
적은 편이 낫다
코 드 줄 수가 적으면 시장에 더 빨리 출시할 수 있고 오류도 줄일 수 있다. 하지만 재사용은 코드 작성을 줄여준다는 면에서뿐 아니라 “군중의 지혜”라는 것을 활용할 수 있다는 점에서도 중요하다. 하이버네이트, 스프링, JUnit, 아파치 웹 서버 같은 인기 있는 오픈 소스 프레임워크나 도구는 지구촌 다수의 사람이 다양한 애플리케이션에 사용한다. 이런 실전에서 다듬어지고 테스트된 소프트웨어는 오류가 없지는 않겠지만 발생하는 어떤 이슈든 발견되고 고쳐질 것이며 비용도 없다는 점을 확신할 수 있다.
아 파치 Commons 프로젝트는 나온 지 수년이 되었고 안정적이다. 최신판에는 대략 90개 클래스와 거의 1800개의 단위 테스트가 포함되어 있다. 테스트 커버리지(coverage)에 대한 정보는 공개되지 않았지만(그리고 분명 사람에 따라서는 이 프로젝트의 테스트 커버리지가 낮다고 할 사람도 있겠지만) 이 수치는 그 자체로 명백하다. 즉, 본질적으로 클래스 당 테스트가 20개 있다는 것이다. 개인적으로 이 프로젝트의 코드가 최소 여러분의 코드만큼은 테스트됐을 거라고 확신한다.
객체 규약(Object contracts)
Commons Lang 라이브러리에는 통칭해 빌더(builder)라고 알려진 유용한 클래스들이 포함되어 있다. 이 절에서는 java.lang.Object equals
메서드를 작성하고, 이 때 작성할 코드 양을 줄이기 위해 이 클래스 중 하나를 사용하는 법을 배워 본다.
메서드 구현에서 과제
모든 자바 클래스는 특별히 지정하지 않아도 java.lang.Object
에서 상속 받는다. 또 알겠지만 Object
클래스에는 통상 재정의(override)되어야 하는 다음 세 개의 메서드가 정의되어 있다.
equals
hashCode
toString
이 중 equals
와 hashCode
메서드는 제대로 작성됐는지 여부가 컬렉션과 심지어 (하이버네이트를 포함한) 영속성 프레임워크(persistence framework) 같은 자바 플랫폼의 다른 측면에 영향을 끼친다는 점에서 특별하다.
equals
와 hashCode
를 구현해 본 적이 없는 사람은 별 거 아니라고 생각하겠지만 사실 그렇지 않을 수도 있다. Joshua Bloch가 지은 Effective Java(참고자료)를 보면 equals
메서드 구현 상세만 해도 10페이지가 넘는다. 그리고 equals
메서드를 구현하고 나면 반드시 hashCode
메서드도 구현해야 한다(equals
에 대한 규약에 따르면 값이 같은 객체 두 개는 해시 코드(hash code)가 같아야 하기 때문이다). Bloch는 hashCode
메서드를 설명하는 데 6페이지를 더 쓴다. 즉, 명백히 단순한 두 메서드를 제대로 구현하는 법에 대한 상세 정보가 최소 16페이지다.
이 equals
메서드를 구현하는 데 있어 과제는 이 메서드가 따라야 하는 규약에 있다.
- 반사적(reflexive)이어야 한다.
- 어떤
null
이 아닌 객체foo
에 대해foo.equals(foo)
가true
이어야 한다.
- 어떤
- 대칭적(symmetric)이어야 한다.
null
이 아닌 두 객체foo
와bar
에 대해foo.equals(bar)
가true
면,bar.equals(foo)
도true
여야 한다.
- 전이적(transitive)이어야 한다.
null
이 아닌 세 객체foo
,bar
,baz
에 대해foo.equals(bar)
가true
고bar.equals(baz)
가true
면foo.equals(baz)
도true
여야 한다.
- 일관성이 있어야 한다.
- 두 객체
foo
와bar
에 대해foo.equals(bar)
가true
면equals
메서드는 (두 객체가 실제 변경되지 않는다는 전제 하에) 몇 번을 호출하든 항상true
여야 한다.
- 두 객체
null
값을 제대로 처리해야 한다.foo.equals(null)
은false
를 돌려줘야 한다.
위 조건을 읽고 Effective Java 책을 공부한 뒤라도 여러분의 Account
객체의 equals
메서드를 제대로 구현하는 것이 만만치 않다고 느낄지도 모른다. 하지만 앞서 생산성과 활동에 대해 언급했던 것을 명심하자.
자신의 사업을 위해 온라인 웹 애플리케이션을 만든다고 가정해 보자. 이 애플리케이션을 빨리 완성할수록 사업에서 더 빨리 돈을 벌 수 있다. 이 단순한 사실을 생각할 때 이제 객체들에 equals
규약을 제대로 구현하고 테스트하는 데 몇 시간(또는 며칠?)을 쓸 것인가? 아니면 다른 사람의 코드를 재사용하는 게 타당할까?
equals
구현하기
equals
메서드를 구현할 때는 Commons Lang EqualsBuilder
가 유용하다. 이 클래스는 파악하기 쉽다. 본질적으로 알아야 하는 것은 이 클래스에 정의된 append
와 isEquals
메서드 둘뿐이다. 이 append
메서드는 프로퍼티 두 개를 받는데, 하나는 equals가 정의된 객체의 프로퍼티고 다른 하나는 비교할 객체 상의 동일한 프로퍼티다. 이 append
메서드는 EqualsBuilder
객체를 돌려주기 때문에 연속된 호출을 사슬 형태로 엮어 객체의 모든 필요한 프로퍼티를 비교할 수 있다. 그리고 이 호출 사슬은 isEquals
메서드를 호출해서 마무리할 수 있다.
예를 들어 Listing 1에서 보는 것처럼 Account
객체를 만들어 보자.
Listing 1. 간단한 Account
객체
import org.apache.commons.lang.builder.CompareToBuilder; |
이 Account
객체는 예제이기 때문에 아주 단순하고 다른 클래스와 연계 없이 독립적이다. 여기서 기본 equals
구현을 그대로 사용할 수 있는지 보기 위해 Listing 2에 보인 것처럼 간단한 테스트를 돌려볼 수 있다.
Listing 2. Account
객체의 기본 equals
메서드 테스트
import org.junit.Test; |
Listing 2에서 보는 것처럼 각자 별도의 참조 값(reference)을 가지는 (즉, 두 객체를 ==
로 비교하면 false
다) 똑 같은 Account
객체를 두 개 생성했다. 이 두 객체의 값이 같은지 비교하면 JUnit이 친절하게 ‘의도했던 것과 달리 false
가 나왔다’고 알려준다.
앞서 equals
메서드가 자바 언어의 컬렉션 클래스를 포함한 자바 플랫폼의 다양한 측면에서 활용된다고 했음을 기억하자. 따라서 이 메서드가 제대로 동작하도록 구현하는 게 당연하다. 그래서 equals
메서드를 재정의해 보겠다.
equals
규약은 null
객체에는 해당하지 않음을 기억하자. 또 객체 두 개가 다른 타입(예를 들어 Account
와 Person
객체)이면 같을 수 없다. 마지막으로 자바 코드에서 equals
메서드는 명백히 ==
연산자와 다르다(기억할지 모르겠는데 두 객체가 같은 참조 값을 가지면 true
를 돌려준다. 이 경우 결과적으로 그 두 객체는 같아야 한다). 두 객체는 같지만(그래서 equals
에서 true
를 돌려주지만) 참조 값은 다를 수도 있다.
따라서 equals
메서드의 첫 번째 측면은 Listing 3처럼 작성할 수 있다.
Listing 3. equals
메서드 내에 포함될 간단한 조건문들
if (this == obj) { |
Listing 3에는 조건문이 두 개 있는데 이는 equals가 정의된 객체와 인자로 넘어온 obj
객체의 프로퍼티들을 비교하기 전에 확인해야 하는 것들이다.
다음으로 equals
메서드는 Object
타입을 인자로 받으므로 Listing 4처럼 obj
를 Account
로 타입 변환해야 한다.
Listing 4. obj
인자를 타입 변환하기
Account account = (Account) obj; |
이 equals
내 논리가 지금까지 제대로 됐다고 가정하고 이제 EqualsBuilder
객체를 이용해 보자. 이 객체는 equals
가 정의된 객체(this
)와 equals로 넘어온 타입의 비슷한 프로퍼티들을 append
메서드로 비교하도록 설계됐다는 점을 상기하자. 이 메서드는 사슬 형태로 연결될 수 있으므로 마지막에 true
나 false
를 돌려주는 isEquals
메서드를 호출할 수 있다. 결과적으로 Listing 5처럼 한 줄짜리 코드를 작성할 수 있다.
Listing 5. EqualsBuilder
재사용하기
return new EqualsBuilder().append(this.id, account.id) |
지금까지 것을 모두 모으면 Listing 6의 equals
메서드와 같다.
Listing 6. 지금까지 작성한 equals
전체 코드
public boolean equals(Object obj) { |
이제 아까 실패했던 테스트를 다시 돌려 보자(Listing 2를 본다). 이번에는 테스트가 성공할 것이다.
여기까지 스스로 equals
메서드를 구현해 보는 데 시간을 소모하지 않았다. 하지만 제대로 된 equals
메서드를 어떻게 작성할지 여전히 궁금하다면, 많은 조건문이 필요하다고 말하는 것만으로도 충분하다. 예를 들어 EqualsBuilder
를 사용하지 않고 작성된 equals
메서드에서는 creationDate
프로퍼티를 Listing 7처럼 비교할 수 있다.
Listing 7. 직접 작성한 equals
메서드의 일부
if (creationDate != null ? !creationDate.equals( |
이
경우 삼항 연산자(ternary)를 사용해 코드가 약간 더 정확해졌다. 하지만 이론의 여지가 있기는 해도 대신 이해하기가
어려워졌다. 그럼에도 불구하고 여기서 중요한 것은 각 객체 프로퍼티의 다양한 측면을 비교하는 일련의 조건문을 작성하거나 (정확히
같은 일을 하도록) EqualsBuilder
를 활용하는 두 가지 길이 있다는 것이다. 여러분이라면 어느 쪽을 택하겠는가?
equals
메서드를 잘 정리하고 가능한 코딩을 적게 하길 (이는 유지 보수할 코드가 더 적다는 의미다) 정말 원한다면 리플렉션(reflection)의 위력을 활용해 Listing 8과 같은 코드를 작성할 수 있다.
Listing 8. EqualsBuilder
의 리플렉션 API 사용하기
public boolean equals(Object obj) { |
코드를 줄이는 데 어때 보이는가?
Listing 8은 단점이 있다. EqualsBuilder
는 (private
필드를 비교하기 위해) 비교할 객체에 대한 접근 제어를 몰래 무력화해야 한다. 만약 VM이 보안을 염두에 두고 구성된 경우 그런 시도가 실패할지도 모른다. 그리고 Listing 8처럼 리플렉션을 과하게 사용하면 equals
메서드의 실행 성능에 영향을 줄 수 있다. 하지만 새로운 프로퍼티가 추가됐을 때 equals
메서드를 갱신할 필요가 없다는 장점은 있다(리플렉션을 안 쓰는 경우는 갱신을 해 줘야 한다).
EqualsBuilder
를 사용하면 재사용의 위력을 느낄 수 있다. 이 클래스는 equals
메서드를 구현하는 데 두 가지 선택을 제시한다. 어느 쪽을 선택할지는 자기 몫이고 여러분이 처한 특정 상황에 영향을 받는다. 한 줄 스타일은 단순하고 매혹적이다. 하지만 이제 알겠지만 안전하기까지 하다.
객체 해시하기
이제 너무 많은 코드를 작성하지 않고도 제대로 동작하는 equals
메서드를 정복했다. 하지만 hashCode
메서드도 함께 재정의해야 함을 잊어서는 안 된다. 이 절에서는 hashCode 메서드를 작성하는 법을 보이겠다.
hashCode
메서드 작성하기
이 hashCode
메서드도 작성 규약이 있다. 하지만 equals
메서드만큼 수학적 형식에 입각하지는 않는다. 그래도 규약을 제대로 따르는 것이 중요하다. 먼저 equals
처럼 결과가 일관성이 있어야 한다. 그리고 두 객체 foo
와 bar
가 있을 때 foo.equals(bar)
가 true
면 foo
와 bar
의 hashCode
는 모두 같은 값을 돌려 줘야 한다. foo
와 bar
가 같지 않으면 다른 해시 코드를 돌려줄 필요는 없다. 하지만 Javadoc에 따르면 해당 객체들이 다른 결과를 돌려주는 편이 일반적으로 더 잘 동작할 것이다.
이미 눈치 챘을지도 모르지만 hashCode
메서드는 재정의하지 않으면 겉보기에는 정수 난수(random integer) 같이 보이는 값을 돌려준다. 이는 통상 플랫폼이
객체의 메모리 주소를 정수로 변경해서 돌려주기 때문이다. 그럼에도 불구하고 문서에 따르면 이는 필수 사항이 아니라서 바뀔 수
있다고 되어 있다. 하지만 그와 무관하게 equals
메서드를 재정의하면 hashCode
메서드도 재정의해야 이치에 맞다(비록 있는 그대로 동작하는 것처럼 보이지만 Joshua Bloch가 쓴 Effective Java는 hashCode
메서드를 제대로 구현하는 데 대해 6페이지를 할애했다는 점을 상기하자).
Commons Lang 라이브러리는 EqualsBuilder
와 거의 비슷한 HashCodeBuilder
클래스를 제공한다. 하지만 두 프로퍼티를 비교하는 대신 위에 언급한 규약을 따르는 정수를 만들어 내려고 한번에 하나의 프로퍼티를 추가한다.
앞선 Account
객체에 Listing 9에 보인 것처럼 hashCode
메서드를 재정의해 보자.
Listing 9. A 기본 hashCode
메서드
public int hashCode() { |
해시 코드를 생성할 때는 비교할 것이 없으므로 HashCodeBuilder
를 사용하면 한 줄짜리 코드가 나온다. 여기서 중요한 것은 HashCodeBuilder
를 제대로 초기화하는 것이다. HashCodeBuilder의 생성자는 두 개의 int
를 받아 해시 코드를 계산하는 데 사용한다. 이 int
두 개는 반드시 홀수여야 한다. HashCodeBuilder의 append
메서드는 하나의 프로퍼티를 받는다. 그리고 이전처럼 호출을 사슬 형태로 연결할 수 있다. 이 호출 사슬의 마지막에는 toHashCode
메서드를 호출한다.
이 정도 설명을 했으니 Listing 10에 보인 것처럼 hashCode
메서드를 구현할 수 있을 것이다.
Listing 10. HashCodeBuilder
로 hashCode
메서드 구현하기
public int hashCode() { |
여기서 생성자에 11과 21을 넘겼다. 이는 이 객체를 위해 완전히 임의로 선택한 홀수들이다. 앞서 작성했던 AccountTest
를 열어(Listing 2를 보라) 두 객체에 대해 equals
가 true
를 돌려주면 hashCode
가 같은 숫자를 돌려줘야 한다는 규약을 확인하기 위한 간단한 테스트를 추가해 보자. Listing 11에 수정한 테스트를 보였다.
Listing 11. 두 개의 값이 같은 객체에 대해 hashCode
규약 점검하기
import org.junit.Test; |
Listing 11에서 값이 같은 객체 두 개의 해시 코드 값이 같음을 확인했다. 다음 Listing 12에서는 값이 다른 객체 두 개는 해시 코드가 다른지를 확인해 보겠다.
Listing 12. 값이 다른 객체 두 개에 대해 hashCode
규약을 확인하기
@Test |
궁금해서 hashCode
메서드를 직접 구현해 보길 원한다면 어떻게 해야 할까? 먼저 hashCode
규약을 명심하고 작성하면 Listing 13과 같을 것이다.
Listing 13. 직접 hashCode
구현하기
public int hashCode() { |
말할 필요도 없이 이 코드는 유효한 hashCode
메서드 구현이다. 하지만 여러분이라면 이 두 hashCode
메서드 중 어느 쪽을 택하겠는가? 어느 쪽을 더 빨리 이해할 수 있는가? 여기서 한번 더 언급하지만, Listing 13에서는 많은 조건 논리를 피하기 위해 삼항 연산자를 사용했다. 짐작이 가겠지만 Commons Lang의 HashCodeBuilder
는 내부적으로 비슷한 일을 한다. 하지만 여기서 중요한 것은 Commons Lang의 개발자들이 유지 보수하고 테스트한다는 점이다.
EqualsBuilder
처럼 HashCodeBuilder
도 리플렉션을 이용하는 다른 API를 제공한다. 이를 사용하면 객체의 각 프로퍼티를 append
메서드로 직접 추가할 필요가 없어 Listing 14처럼 hashCode
메서드를 구현할 수 있다.
Listing 14. HashCodeBuilder
의 리플렉션 API 사용하기
public int hashCode() { |
이전과 마찬가지로 이 메서드는 내부에서 자바 리플렉션을 이용하기 때문에 보안 설정이 바뀌면 제대로 동작하지 않을 수도 있고 성능이 얼마간 느려질 수 있다.
무던하게 Comparable 인터페이스 구현하기
다소 수학적 형식에 따르는 규약을 요구하는 또 하나의 흥미로운 메서드로 Comparable
인터페이스의 compareTo
메서드가 있다. 이 인터페이스는 특정 객체들의 순서를 결정하는 데 세부적인 제어가 필요할 때 꽤 중요하다. 이 절에서는 Commons Lang의 CompareToBuilder
를 사용하는 법을 살펴 보겠다.
출력 순서 지정하기
이전 자바 프로그래밍 경험 덕에 이미 눈치챘을 수도 있지만 특정 경우에 대해서는 객체 순서를 어떻게 결정할지에 대한 기본 방식들이 있다. Collections
클래스의 sort
메서드가 그런 예다.
예를 들어 Listing 15의 Collection
은 순서가 없고 그냥 그대로 두면 그대로 남아 있을 것이다.
Listing 15. String
리스트
ArrayList<String> list = new ArrayList<String>(); |
Listing 16처럼 list
를 Collections
클래스의 sort
메서드에 넘겨주면 기본 순서가 적용될 것이다. 이 경우 기본 순서는 알파벳 순이다. Listing 16은 Listing 15에 보인 이름 리스트를 정렬해 알파벳 순서로 정리된 결과를 출력한다.
Listing 16. String
리스트 정렬
Collections.sort(list); |
Listing 17에 출력 결과를 보였다.
Listing 17. String
들을 정렬한 출력 결과
sorted is Andy |
이게 제대로 동작하는 이유는 당연하지만 자바의 String
클래스가 Comparable
인터페이스를 구현하고 따라서 알파벳 순서로 정렬하도록 compareTo
메서드가 구현되어 있기 때문이다. 사실 자바 언어의 거의 모든 핵심 클래스가 이 인터페이스를 구현한다.
Account
객체들이 다양한 방법으로 정렬되어야 한다면 어떻게 해야 할까? 예를 들어 id
나 성(last name) 기준으로 말이다. 어떻게 할 수 있을까?
당연하지만 먼저 Comparable
인터페이스를 구현하고 compareTo
메서드를 구현해야 한다. 이 메서드는 본질적으로 자연스러운 순서(natural ordering)를 위해서만 사용될 수 있다. 즉, 객체가 자신의 프로퍼티에 따라 정렬될 때에 한한다. 결과적으로 compareTo
메서드는 equals
메서드와 꽤 비슷하지만 Account
컬렉션이 프로퍼티에 따라 정렬되는 걸 허용한다. 프로퍼티는 compareTo
메서드를 통해 처리된다.
이 메서드 구현에 관한 문서를 읽는다면 이 메서드가 equals
와 아주 비슷함을 발견할 것이다. 즉, 제대로 작성하기가 까다롭다(Effective Java는 이 주제에 대해 4페이지를 할애한다). 이제 의심할 여지 없이 이 패턴을 이해했을 것이다. Commons Lang을 활용할 것이다.
compareTo
구현하기
Commons Lang은 CompareToBuilder
라는 적절한 이름이 붙은 클래스를 제공한다. 이 클래스는 EqualsBuilder
와 거의 똑같이 동작한다. 사슬 형태로 연쇄 호출할 수 있는 append
메서드가 있고 최종적으로 toComparision
메서드를 통해 int
값을 돌려준다.
따라서 compareTo를 구현하려면 먼저 Listing 18처럼 Account
클래스가 Comparable
인터페이스를 구현하도록 해야 한다.
Listing 18. Comparable
인터페이스 구현하기
public class Account implements Comparable {} |
다음으로 Listing 19에 보인 것처럼 compareTo
메서드를 구현해야 한다.
Listing 19. compareTo
의 기본 구현
public int compareTo(Object obj) { |
이 메서드 구현은 두 단계 과정이다. 먼저 넘겨 받은 인자 타입을 원하는 타입으로 변환한다(이 경우는 Account
). 그리고 CompareToBuilder
를 이용해 객체의 프로퍼티들을 비교한다. Commons Lang 문서에 따르면 equals
메서드에서 비교한 것과 같은 프로퍼티들을 비교해야 한다. 따라서 Account
객체의 compareTo
메서드는 Listing 20과 같아야 한다.
Listing 20. CompareToBuilder
사용하기
public int compareTo(Object obj) { |
정말 코딩을 줄이고자 한다면 Listing 21처럼 리플렉션 형태의 CompareToBuilder
API를 사용할 수도 있음을 잊지 말자.
Listing 21. CompareToBuilder
의 리플렉션 API 사용하기
public int compareTo(Object obj) { |
이제 예를 들어 Account
객체의 집합이 자연스러운 순서에 따라 정렬되어 있다는 점을 이용하려고 한다면 Listing 22에 보인 것처럼 Collections.sort
를 사용할 수 있다.
Listing 22. Account
객체 정렬하기
Date now = new Date(); |
이 코드는 객체를 먼저 id
, 다음으로 이름, 그 다음으로 성 등의 자연스러운 순서에 따라 객체들을 출력한다. 결과적으로 정렬 결과는 Listing 23과 같을 것이다.
Listing 23. 정렬된 Account
객체들
new Account(0, "Andrew", "Glover", "z@zell.com", new Date()) |
이 결과가 말이 되냐는 다른 문제다. 다음 절에는 Commons Lang이 더 읽을 만한 결과를 만들도록 도와줄 수 있음을 알게 될 것이다.
객체의 문자열 표현
Object
의 기본 toString
구현은 객체의 완전히 명시된 이름(fully qualified name)에 @
문자를 붙이고 그 객체의 해시 코드 값을 추가한 문자열을 돌려준다. 그리고 이미 알고 있겠지만 이는 객체를 서로 구분하는 데 그다지 큰 도움이 되지 않는다. Commons Lang은 편리한 ToStringBuilder
클래스를 제공하는데, 더 읽을 만한 toString
결과를 만드는 데 도움이 된다.
toString
구현하기
아마 다들 toString
메서드를 한번 이상은 구현해 봤을 것이다. 나도 마찬가지다. 이 메서드는 그리 복잡하지 않고 잘못 구현하기도 어렵다. 하지만 toString을 구현하는 것은 성가신 일이 되기도 한다. 그리고 Account
객체가 이미 Commons Lang 라이브러리를 사용하고 있기 때문에 일단 ToStringBuilder
가 어떻게 사용되는지를 보자.
ToStringBuilder
는 앞서 다룬 세 클래스와 같은 방식으로 동작한다. 객체를 생성한 다음 프로퍼티들을 추가하고 toString
을 부른다. 그게 전부다.
이제 toString
메서드를 재정의해 Listing 24에 보인 코드를 추가해 보자.
Listing 24. ToStringBuilder
사용하기
public String toString() { |
늘 그렇지만 Listing 25에 보인 것처럼 리플렉션을 활용할 수도 있다.
Listing 25. ToStringBuilder
의 리플렉션 API 사용하기
public String toString() { |
ToStringBuilder
를 어떻게 사용할지를 선택하는 것과 무관하게 toString
을 호출하면 한층 읽기 편한 String
이 나온다. 예를 들어 Listing 26의 객체를 보자.
Listing 26. 특정 Account
객체
new Account(10, "Andrew", "Glover", "ajg@me.com", now); |
Listing 27에서 보는 것처럼 출력 결과가 꽤 읽을 만하다.
Listing 27. ToStringBuilder
에서 나온 출력
com.acme.app.Account@47858e[ |
자신의 객체에 대해 이런 형태의 String
표현이 맘에 들지 않는 경우를 위해 Commons Lang 라이브러리는 원하는 출력을 만드는 데 도움이 되는 도움 클래스를 몇 개 제공한다. 어쨌든 ToStringBuilder
를 사용하면 예를 들어 로그 파일 내 객체에 대해 일관된 형태의 표현을 기록할 수도 있다.
적은 것이 많은 것이다
다행히 지난 20년 남짓한 기간 동안 소프트웨어 산업은 많은 코드가 나쁠 수도 있음을 알기 시작했다. 앞서 언급했던 연구들로 인해 코드 줄 수가 적을수록 오류도 적다고 가정할 수 있다.
오 픈 소스 소프트웨어의 번창은 코드 재사용이 이미 실현되고 있음을 뜻한다. 비록 여전히 진정한 컴포넌트 재사용의 날을 바라는 우리지만 이제 재사용하기 쉬운 프레임워크와 지원 코드가 있어 가능한 적은 줄 수의 코드로도 애플리케이션을 작성할 수 있게 되었다.
자,
무엇을 기다리는가? 가서 아파치 Commons Lang 프로젝트를 사용하기 시작하자. 사용하면서 이 편리한 라이브러리에 다른
무엇이 있는지도 살펴 보자. 결과적으로 줄어드는 시간과 타이핑이 살펴 보는 노력을 충분히 보상해 줄 것이다.
기사의 원문보기
다운로드 하십시오
설명 | 이름 | 크기 | 다운로드 방식 |
---|---|---|---|
이 글의 예제 프로젝트 코드 | j-lessismore.zip | 6.9MB | HTTP |
참고자료
교육- Commons Lang:
java.lang
API를 위한 많은 도움 유틸리티를 제공한다.
- In pursuit of code quality(Andrew Glover, developerWorks): 이 연재는 소프트웨어 품질을 보장하고 측정하는 기법, 도구, 방법을 살펴 본다.
- Effective Java: Programming Language Guide(Joshua Bloch, Prentice Hall, 2001년): Bloch가 쓴 이 책은
java.lang.Object
메서드 구현의 상세를 다룬다.
- "Hashing it out"(Brian Goetz, developerWorks, 2003년 5월): Java theory and practice 연재 중 하나인 이 글에서는
hashCode
와equals
메서드를 효과적이고 적절하게 정의하는 규칙과 가이드라인을 소개한다.
- "Why Do CMMI Assessments?"(Donna Dunaway와 Marilyn Bush, InformIt, 2005년 6월): CMMI® Assessments: Motivating Positive Change(Addison Wesley, 2005년)의 견본 장으로 몇몇 오류 밀도에 대한 연구들을 간략히 소개한다.
- "Linux: Fewer Bugs Than Rivals"(Michelle Delio, Wired, 2004년 12월): 오류 밀도에 대한 몇몇 척도를 소개하는 또 다른 글이다.
- 기술 서점을 살펴 보면서 이와 다른 기술 주제에 대한 책을 찾아 보자.
- developerWorks 자바 기술 존: 자바 프로그래밍의 모든 측면에 대한 수백 개의 글을 찾아 보자.
제품 및 기술 얻기
- 썬 JDK 1.5나 더 최근 버전: 이 튜토리얼에 소개된 예제를 따라 하려면 최소 1.5.0_09 이상이 필요하다.
- Commons Lang: Commons Lang을 다운로드하자.
'Java' 카테고리의 다른 글
Apache Commons DBCP 환경설정 값 (0) | 2015.06.10 |
---|---|
성능좋은 file 입출력(파일 이동) (0) | 2009.12.02 |
Collections를 이용한 List 객체들의 Sorting(정렬) [펌] (0) | 2009.11.26 |
이미지 썸네일 처리 (0) | 2009.11.25 |