본문 바로가기

Java

불필요한 코딩을 줄이자!(IBM developerWorks)

불필요한 코딩을 줄이자!

아파치 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 클래스를 사용하는 데 있어 기본 개념을 단계별로 살펴보고, 많은 코드를 직접 작성할 필요가 없도록 해당 코드를 활용해 본다.

목표

다음 내용을 배운다.

  • equalshashCode 같이 정해진 규칙에 따라 구현해야 하는 메서드를 구현해 본다.
  • 구현된 메서드의 동작을 검증해 본다.
  • Comparable 인터페이스의 compareTo 메서드를 구현해 본다.

이 튜토리얼을 마칠 때쯤이면 Commons Lang 라이브러리의 혜택을 이해하고 코드를 적게 쓰는 법을 배울 것이다.

 

필요한 사전 지식

이 튜토리얼을 충분히 활용하려면 자바 문법과 자바 플랫폼 상의 객체 지향 개발에 대한 기본 개념에 친숙해야 한다. 또한 리펙터링(refactoring)과 통상의 단위 테스트(unit testing)에도 친숙해야 한다.

 

시스템 요구 사항

이 튜토리얼을 따라가고 예제를 실행해 보려면 다음이 필요하다.

이 튜토리얼을 위해 권장하는 시스템 구성은 다음과 같다.

  • 썬 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

 

이 중 equalshashCode 메서드는 제대로 작성됐는지 여부가 컬렉션과 심지어 (하이버네이트를 포함한) 영속성 프레임워크(persistence framework) 같은 자바 플랫폼의 다른 측면에 영향을 끼친다는 점에서 특별하다.

 

equalshashCode를 구현해 본 적이 없는 사람은 별 거 아니라고 생각하겠지만 사실 그렇지 않을 수도 있다. 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이 아닌 두 객체 foobar에 대해 foo.equals(bar)true면, bar.equals(foo)true여야 한다.
  • 전이적(transitive)이어야 한다.
    • null이 아닌 세 객체 foo, bar, baz에 대해 foo.equals(bar)truebar.equals(baz)truefoo.equals(baz)true여야 한다.
  • 일관성이 있어야 한다.
    • 두 객체 foobar에 대해 foo.equals(bar)trueequals 메서드는 (두 객체가 실제 변경되지 않는다는 전제 하에) 몇 번을 호출하든 항상 true여야 한다.
  • null 값을 제대로 처리해야 한다.
    • foo.equals(null)false를 돌려줘야 한다.

 

위 조건을 읽고 Effective Java 책을 공부한 뒤라도 여러분의 Account 객체의 equals 메서드를 제대로 구현하는 것이 만만치 않다고 느낄지도 모른다. 하지만 앞서 생산성과 활동에 대해 언급했던 것을 명심하자.

 

자신의 사업을 위해 온라인 웹 애플리케이션을 만든다고 가정해 보자. 이 애플리케이션을 빨리 완성할수록 사업에서 더 빨리 돈을 벌 수 있다. 이 단순한 사실을 생각할 때 이제 객체들에 equals 규약을 제대로 구현하고 테스트하는 데 몇 시간(또는 며칠?)을 쓸 것인가? 아니면 다른 사람의 코드를 재사용하는 게 타당할까?

 

equals 구현하기

 

equals 메서드를 구현할 때는 Commons Lang EqualsBuilder가 유용하다. 이 클래스는 파악하기 쉽다. 본질적으로 알아야 하는 것은 이 클래스에 정의된 appendisEquals 메서드 둘뿐이다. 이 append 메서드는 프로퍼티 두 개를 받는데, 하나는 equals가 정의된 객체의 프로퍼티고 다른 하나는 비교할 객체 상의 동일한 프로퍼티다. 이 append 메서드는 EqualsBuilder 객체를 돌려주기 때문에 연속된 호출을 사슬 형태로 엮어 객체의 모든 필요한 프로퍼티를 비교할 수 있다. 그리고 이 호출 사슬은 isEquals 메서드를 호출해서 마무리할 수 있다.

 

예를 들어 Listing 1에서 보는 것처럼 Account 객체를 만들어 보자.


Listing 1. 간단한 Account 객체

          import org.apache.commons.lang.builder.CompareToBuilder;

import org.apache.commons.lang.builder.EqualsBuilder;

import org.apache.commons.lang.builder.HashCodeBuilder;

import org.apache.commons.lang.builder.ToStringBuilder;



import java.util.Date;



public class Account implements Comparable {

private long id;

private String firstName;

private String lastName;

private String emailAddress;

private Date creationDate;



public Account(long id, String firstName, String lastName,

String emailAddress, Date creationDate) {

this.id = id;

this.firstName = firstName;

this.lastName = lastName;

this.emailAddress = emailAddress;

this.creationDate = creationDate;

}



public long getId() {

return id;

}



public String getFirstName() {

return firstName;

}



public String getLastName() {

return lastName;

}



public String getEmailAddress() {

return emailAddress;

}



public Date getCreationDate() {

return creationDate;

}

}

 

Account 객체는 예제이기 때문에 아주 단순하고 다른 클래스와 연계 없이 독립적이다. 여기서 기본 equals 구현을 그대로 사용할 수 있는지 보기 위해 Listing 2에 보인 것처럼 간단한 테스트를 돌려볼 수 있다.


Listing 2. Account 객체의 기본 equals 메서드 테스트

          import org.junit.Test;

import org.junit.Assert;

import com.acme.app.Person;



import java.util.Date;



public class AccountTest {

@Test

public void verifyEquals(){

Date now = new Date();

Account acct1 = new Account(1, "Andrew", "Glover", "ajg@me.com", now);

Account acct2 = new Account(1, "Andrew", "Glover", "ajg@me.com", now);



Assert.assertTrue(acct1.equals(acct2));

}

}

 

Listing 2에서 보는 것처럼 각자 별도의 참조 값(reference)을 가지는 (즉, 두 객체를 ==로 비교하면 false다) 똑 같은 Account 객체를 두 개 생성했다. 이 두 객체의 값이 같은지 비교하면 JUnit이 친절하게 ‘의도했던 것과 달리 false가 나왔다’고 알려준다.

 

앞서 equals 메서드가 자바 언어의 컬렉션 클래스를 포함한 자바 플랫폼의 다양한 측면에서 활용된다고 했음을 기억하자. 따라서 이 메서드가 제대로 동작하도록 구현하는 게 당연하다. 그래서 equals 메서드를 재정의해 보겠다.

 

equals 규약은 null 객체에는 해당하지 않음을 기억하자. 또 객체 두 개가 다른 타입(예를 들어 AccountPerson 객체)이면 같을 수 없다. 마지막으로 자바 코드에서 equals 메서드는 명백히 == 연산자와 다르다(기억할지 모르겠는데 두 객체가 같은 참조 값을 가지면 true를 돌려준다. 이 경우 결과적으로 그 두 객체는 같아야 한다). 두 객체는 같지만(그래서 equals에서 true를 돌려주지만) 참조 값은 다를 수도 있다.

 

따라서 equals 메서드의 첫 번째 측면은 Listing 3처럼 작성할 수 있다.


Listing 3. equals 메서드 내에 포함될 간단한 조건문들

          if (this == obj) {

return true;

}

if (obj == null || this.getClass() != obj.getClass()) {

return false;

}

 

Listing 3에는 조건문이 두 개 있는데 이는 equals가 정의된 객체와 인자로 넘어온 obj 객체의 프로퍼티들을 비교하기 전에 확인해야 하는 것들이다.

 

다음으로 equals 메서드는 Object 타입을 인자로 받으므로 Listing 4처럼 objAccount로 타입 변환해야 한다.


Listing 4. obj 인자를 타입 변환하기

          Account account = (Account) obj;

 

equals 내 논리가 지금까지 제대로 됐다고 가정하고 이제 EqualsBuilder 객체를 이용해 보자. 이 객체는 equals가 정의된 객체(this)와 equals로 넘어온 타입의 비슷한 프로퍼티들을 append 메서드로 비교하도록 설계됐다는 점을 상기하자. 이 메서드는 사슬 형태로 연결될 수 있으므로 마지막에 truefalse를 돌려주는 isEquals 메서드를 호출할 수 있다. 결과적으로 Listing 5처럼 한 줄짜리 코드를 작성할 수 있다.


Listing 5. EqualsBuilder 재사용하기

          return new EqualsBuilder().append(this.id, account.id)

.append(this.firstName, account.firstName)

.append(this.lastName, account.lastName)

.append(this.emailAddress, account.emailAddress)

.append(this.creationDate, account.creationDate)

.isEquals();

 

지금까지 것을 모두 모으면 Listing 6의 equals 메서드와 같다.


Listing 6. 지금까지 작성한 equals 전체 코드

          public boolean equals(Object obj) {

if (this == obj) {

return true;

}

if (obj == null || this.getClass() != obj.getClass()) {

return false;

}



Account account = (Account) obj;



return new EqualsBuilder().append(this.id, account.id)

.append(this.firstName, account.firstName)

.append(this.lastName, account.lastName)

.append(this.emailAddress, account.emailAddress)

.append(this.creationDate, account.creationDate)

.isEquals();

}

 

이제 아까 실패했던 테스트를 다시 돌려 보자(Listing 2를 본다). 이번에는 테스트가 성공할 것이다.

 

여기까지 스스로 equals 메서드를 구현해 보는 데 시간을 소모하지 않았다. 하지만 제대로 된 equals 메서드를 어떻게 작성할지 여전히 궁금하다면, 많은 조건문이 필요하다고 말하는 것만으로도 충분하다. 예를 들어 EqualsBuilder를 사용하지 않고 작성된 equals 메서드에서는 creationDate 프로퍼티를 Listing 7처럼 비교할 수 있다.


Listing 7. 직접 작성한 equals 메서드의 일부

          if (creationDate != null ? !creationDate.equals(

person.creationDate) : person.creationDate != null){

return false;

}

 

이 경우 삼항 연산자(ternary)를 사용해 코드가 약간 더 정확해졌다. 하지만 이론의 여지가 있기는 해도 대신 이해하기가 어려워졌다. 그럼에도 불구하고 여기서 중요한 것은 각 객체 프로퍼티의 다양한 측면을 비교하는 일련의 조건문을 작성하거나 (정확히 같은 일을 하도록) EqualsBuilder를 활용하는 두 가지 길이 있다는 것이다. 여러분이라면 어느 쪽을 택하겠는가?

 

equals 메서드를 잘 정리하고 가능한 코딩을 적게 하길 (이는 유지 보수할 코드가 더 적다는 의미다) 정말 원한다면 리플렉션(reflection)의 위력을 활용해 Listing 8과 같은 코드를 작성할 수 있다.


Listing 8. EqualsBuilder의 리플렉션 API 사용하기

          public boolean equals(Object obj) {

return EqualsBuilder.reflectionEquals(this, obj);

}

 

코드를 줄이는 데 어때 보이는가?

 

Listing 8은 단점이 있다. EqualsBuilder는 (private 필드를 비교하기 위해) 비교할 객체에 대한 접근 제어를 몰래 무력화해야 한다. 만약 VM이 보안을 염두에 두고 구성된 경우 그런 시도가 실패할지도 모른다. 그리고 Listing 8처럼 리플렉션을 과하게 사용하면 equals 메서드의 실행 성능에 영향을 줄 수 있다. 하지만 새로운 프로퍼티가 추가됐을 때 equals 메서드를 갱신할 필요가 없다는 장점은 있다(리플렉션을 안 쓰는 경우는 갱신을 해 줘야 한다).

 

EqualsBuilder를 사용하면 재사용의 위력을 느낄 수 있다. 이 클래스는 equals 메서드를 구현하는 데 두 가지 선택을 제시한다. 어느 쪽을 선택할지는 자기 몫이고 여러분이 처한 특정 상황에 영향을 받는다. 한 줄 스타일은 단순하고 매혹적이다. 하지만 이제 알겠지만 안전하기까지 하다.

 

객체 해시하기

 

 

이제 너무 많은 코드를 작성하지 않고도 제대로 동작하는 equals 메서드를 정복했다. 하지만 hashCode 메서드도 함께 재정의해야 함을 잊어서는 안 된다. 이 절에서는 hashCode 메서드를 작성하는 법을 보이겠다.

 

hashCode 메서드 작성하기

 

hashCode 메서드도 작성 규약이 있다. 하지만 equals 메서드만큼 수학적 형식에 입각하지는 않는다. 그래도 규약을 제대로 따르는 것이 중요하다. 먼저 equals처럼 결과가 일관성이 있어야 한다. 그리고 두 객체 foobar가 있을 때 foo.equals(bar)truefoobarhashCode는 모두 같은 값을 돌려 줘야 한다. foobar가 같지 않으면 다른 해시 코드를 돌려줄 필요는 없다. 하지만 Javadoc에 따르면 해당 객체들이 다른 결과를 돌려주는 편이 일반적으로 더 잘 동작할 것이다.

 

이미 눈치 챘을지도 모르지만 hashCode 메서드는 재정의하지 않으면 겉보기에는 정수 난수(random integer) 같이 보이는 값을 돌려준다. 이는 통상 플랫폼이 객체의 메모리 주소를 정수로 변경해서 돌려주기 때문이다. 그럼에도 불구하고 문서에 따르면 이는 필수 사항이 아니라서 바뀔 수 있다고 되어 있다. 하지만 그와 무관하게 equals 메서드를 재정의하면 hashCode 메서드도 재정의해야 이치에 맞다(비록 있는 그대로 동작하는 것처럼 보이지만 Joshua Bloch가 쓴 Effective JavahashCode 메서드를 제대로 구현하는 데 대해 6페이지를 할애했다는 점을 상기하자).

 

Commons Lang 라이브러리는 EqualsBuilder와 거의 비슷한 HashCodeBuilder 클래스를 제공한다. 하지만 두 프로퍼티를 비교하는 대신 위에 언급한 규약을 따르는 정수를 만들어 내려고 한번에 하나의 프로퍼티를 추가한다.

 

앞선 Account 객체에 Listing 9에 보인 것처럼 hashCode 메서드를 재정의해 보자.


Listing 9. A 기본 hashCode 메서드

          public int hashCode() {

return 0;

}

 

해시 코드를 생성할 때는 비교할 것이 없으므로 HashCodeBuilder를 사용하면 한 줄짜리 코드가 나온다. 여기서 중요한 것은 HashCodeBuilder를 제대로 초기화하는 것이다. HashCodeBuilder의 생성자는 두 개의 int를 받아 해시 코드를 계산하는 데 사용한다. 이 int 두 개는 반드시 홀수여야 한다. HashCodeBuilder의 append 메서드는 하나의 프로퍼티를 받는다. 그리고 이전처럼 호출을 사슬 형태로 연결할 수 있다. 이 호출 사슬의 마지막에는 toHashCode 메서드를 호출한다.

 

이 정도 설명을 했으니 Listing 10에 보인 것처럼 hashCode 메서드를 구현할 수 있을 것이다.


Listing 10. HashCodeBuilderhashCode 메서드 구현하기

          public int hashCode() {

return new HashCodeBuilder(11, 21).append(this.id)

.append(this.firstName)

.append(this.lastName)

.append(this.emailAddress)

.append(this.creationDate)

.toHashCode();

}

 

여기서 생성자에 11과 21을 넘겼다. 이는 이 객체를 위해 완전히 임의로 선택한 홀수들이다. 앞서 작성했던 AccountTest를 열어(Listing 2를 보라) 두 객체에 대해 equalstrue를 돌려주면 hashCode가 같은 숫자를 돌려줘야 한다는 규약을 확인하기 위한 간단한 테스트를 추가해 보자. Listing 11에 수정한 테스트를 보였다.


Listing 11. 두 개의 값이 같은 객체에 대해 hashCode 규약 점검하기

          import org.junit.Test;

import org.junit.Assert;

import com.acme.app.Account;



import java.util.Date;



public class AccountTest {

@Test

public void verifyAccountEquals(){

Date now = new Date();

Account acct1 = new Account(1, "Andrew", "Glover", "ajg@me.com", now);

Account acct2 = new Account(1, "Andrew", "Glover", "ajg@me.com", now);



Assert.assertTrue(acct1.equals(acct2));

Assert.assertEquals(acct1.hashCode(), acct2.hashCode());

}

}

 

Listing 11에서 값이 같은 객체 두 개의 해시 코드 값이 같음을 확인했다. 다음 Listing 12에서는 값이 다른 객체 두 개는 해시 코드가 다른지를 확인해 보겠다.


Listing 12. 값이 다른 객체 두 개에 대해 hashCode 규약을 확인하기

          @Test

public void verifyAccountDifferentHashCodes(){

Date now = new Date();

Account acct1 = new Account(1, "John", "Smith", "john@smith.com", now);

Account acct2 = new Account(2, "Andrew", "Glover", "ajg@me.com", now);



Assert.assertFalse(acct1.equals(acct2));

Assert.assertTrue(acct1.hashCode() != acct2.hashCode());

}

 

궁금해서 hashCode 메서드를 직접 구현해 보길 원한다면 어떻게 해야 할까? 먼저 hashCode 규약을 명심하고 작성하면 Listing 13과 같을 것이다.


Listing 13. 직접 hashCode 구현하기

          public int hashCode() {

int result;

result = (int) (id ^ (id >>> 32));

result = 31 * result + (firstName != null ? firstName.hashCode() : 0);

result = 31 * result + (lastName != null ? lastName.hashCode() : 0);

result = 31 * result + (emailAddress != null ? emailAddress.hashCode() : 0);

result = 31 * result + (creationDate != null ? creationDate.hashCode() : 0);

return result;

}

 

말할 필요도 없이 이 코드는 유효한 hashCode 메서드 구현이다. 하지만 여러분이라면 이 두 hashCode 메서드 중 어느 쪽을 택하겠는가? 어느 쪽을 더 빨리 이해할 수 있는가? 여기서 한번 더 언급하지만, Listing 13에서는 많은 조건 논리를 피하기 위해 삼항 연산자를 사용했다. 짐작이 가겠지만 Commons Lang의 HashCodeBuilder는 내부적으로 비슷한 일을 한다. 하지만 여기서 중요한 것은 Commons Lang의 개발자들이 유지 보수하고 테스트한다는 점이다.

 

EqualsBuilder처럼 HashCodeBuilder도 리플렉션을 이용하는 다른 API를 제공한다. 이를 사용하면 객체의 각 프로퍼티를 append 메서드로 직접 추가할 필요가 없어 Listing 14처럼 hashCode 메서드를 구현할 수 있다.


Listing 14. HashCodeBuilder의 리플렉션 API 사용하기

          public int hashCode() {

return HashCodeBuilder.reflectionHashCode(this);

}

 

이전과 마찬가지로 이 메서드는 내부에서 자바 리플렉션을 이용하기 때문에 보안 설정이 바뀌면 제대로 동작하지 않을 수도 있고 성능이 얼마간 느려질 수 있다.

 

무던하게 Comparable 인터페이스 구현하기

 

 

다소 수학적 형식에 따르는 규약을 요구하는 또 하나의 흥미로운 메서드로 Comparable 인터페이스의 compareTo 메서드가 있다. 이 인터페이스는 특정 객체들의 순서를 결정하는 데 세부적인 제어가 필요할 때 꽤 중요하다. 이 절에서는 Commons Lang의 CompareToBuilder를 사용하는 법을 살펴 보겠다.

 

출력 순서 지정하기

 

이전 자바 프로그래밍 경험 덕에 이미 눈치챘을 수도 있지만 특정 경우에 대해서는 객체 순서를 어떻게 결정할지에 대한 기본 방식들이 있다. Collections 클래스의 sort 메서드가 그런 예다.

 

예를 들어 Listing 15의 Collection은 순서가 없고 그냥 그대로 두면 그대로 남아 있을 것이다.


Listing 15. String 리스트

          ArrayList<String> list = new ArrayList<String>();

list.add("Megan");

list.add("Zeek");

list.add("Andy");

list.add("Michelle");

 

Listing 16처럼 listCollections 클래스의 sort 메서드에 넘겨주면 기본 순서가 적용될 것이다. 이 경우 기본 순서는 알파벳 순이다. Listing 16은 Listing 15에 보인 이름 리스트를 정렬해 알파벳 순서로 정리된 결과를 출력한다.


Listing 16. String 리스트 정렬

          Collections.sort(list);



for(String value : list){

System.out.println("sorted is " + value);

}

 

Listing 17에 출력 결과를 보였다.


Listing 17. String들을 정렬한 출력 결과

          sorted is Andy

sorted is Megan

sorted is Michelle

sorted is Zeek

 

이게 제대로 동작하는 이유는 당연하지만 자바의 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) {

return 0;

}

 

이 메서드 구현은 두 단계 과정이다. 먼저 넘겨 받은 인자 타입을 원하는 타입으로 변환한다(이 경우는 Account). 그리고 CompareToBuilder를 이용해 객체의 프로퍼티들을 비교한다. Commons Lang 문서에 따르면 equals 메서드에서 비교한 것과 같은 프로퍼티들을 비교해야 한다. 따라서 Account 객체의 compareTo 메서드는 Listing 20과 같아야 한다.


Listing 20. CompareToBuilder 사용하기

          public int compareTo(Object obj) {

Account account = (Account) obj;

return new CompareToBuilder().append(this.id, account.id)

.append(this.firstName, account.firstName)

.append(this.lastName, account.lastName)

.append(this.emailAddress, account.emailAddress)

.append(this.creationDate, account.creationDate)

.toComparison();

}

 

정말 코딩을 줄이고자 한다면 Listing 21처럼 리플렉션 형태의 CompareToBuilder API를 사용할 수도 있음을 잊지 말자.


Listing 21. CompareToBuilder의 리플렉션 API 사용하기

          public int compareTo(Object obj) {

return CompareToBuilder.reflectionCompare(this, obj);

}

 

이제 예를 들어 Account 객체의 집합이 자연스러운 순서에 따라 정렬되어 있다는 점을 이용하려고 한다면 Listing 22에 보인 것처럼 Collections.sort를 사용할 수 있다.


Listing 22. Account 객체 정렬하기

          Date now = new Date();

ArrayList<Account> list = new ArrayList<Account>();

list.add(new Account(41, "Amy", "Glover", "ajg@me.com", now));

list.add(new Account(10, "Andrew", "Glover", "ajg@me.com", now));

list.add(new Account(1, "Andrew", "Blover", "ajg@me.com", new Date()));

list.add(new Account(2, "Andrew", "Smith", "b@bb.com", now));

list.add(new Account(0, "Andrew", "Glover", "z@zell.com", new Date()));



Collections.sort(list);



for(Account acct : list){

System.out.println(acct);

}

 

이 코드는 객체를 먼저 id, 다음으로 이름, 그 다음으로 성 등의 자연스러운 순서에 따라 객체들을 출력한다. 결과적으로 정렬 결과는 Listing 23과 같을 것이다.


Listing 23. 정렬된 Account 객체들

          new Account(0, "Andrew", "Glover", "z@zell.com", new Date())

new Account(1, "Andrew", "Blover", "ajg@me.com", new Date())

new Account(2, "Andrew", "Smith", "b@bb.com", now)

new Account(10, "Andrew", "Glover", "ajg@me.com", now)

new Account(41, "Amy", "Glover", "ajg@me.com", now)

 

이 결과가 말이 되냐는 다른 문제다. 다음 절에는 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() {

return new ToStringBuilder(this).append("id", this.id).

.append("firstName", this.firstName)

.append("lastName", this.lastName)

.append("emailAddress", this.emailAddress)

.append("creationDate", this.creationDate)

.toString();

}

 

늘 그렇지만 Listing 25에 보인 것처럼 리플렉션을 활용할 수도 있다.


Listing 25. ToStringBuilder의 리플렉션 API 사용하기

          public String toString() {

return ToStringBuilder.reflectionToString(this);

}

 

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[

id=10,firstName=Andrew,lastName=Glover,emailAddress=ajg@me.com,

creationDate=Tue Nov 11 17:20:08 EST 2008]

 

자신의 객체에 대해 이런 형태의 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 연재 중 하나인 이 글에서는 hashCodeequals 메서드를 효과적이고 적절하게 정의하는 규칙과 가이드라인을 소개한다.
  • "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 자바 기술 존: 자바 프로그래밍의 모든 측면에 대한 수백 개의 글을 찾아 보자.

제품 및 기술 얻기