:: TECHNOLOGY :: 2003 AUTUMN 023

Similar documents
JAVA PROGRAMMING 실습 08.다형성

슬라이드 1

PowerPoint Presentation

gnu-lee-oop-kor-lec06-3-chap7

PowerPoint Presentation

q 이장에서다룰내용 1 객체지향프로그래밍의이해 2 객체지향언어 : 자바 2

PowerPoint Presentation

쉽게 풀어쓴 C 프로그래밍

JVM 메모리구조

쉽게 풀어쓴 C 프로그래밍

PowerPoint Presentation

JAVA PROGRAMMING 실습 05. 객체의 활용

C# Programming Guide - Types

C++ Programming

Microsoft PowerPoint - 2강

No Slide Title

<4D F736F F F696E74202D20C1A63038C0E520C5ACB7A1BDBABFCD20B0B4C3BC4928B0ADC0C729205BC8A3C8AF20B8F0B5E55D>

JUNIT 실습및발표

PowerPoint Presentation

PowerPoint 프레젠테이션

Microsoft PowerPoint - chap02-C프로그램시작하기.pptx

PowerPoint Presentation

1

5장.key

제11장 프로세스와 쓰레드

작성자 : 김성박\(삼성 SDS 멀티캠퍼스 전임강사\)

Microsoft PowerPoint 자바-기본문법(Ch2).pptx

Microsoft PowerPoint - Lect04.pptx

(8) getpi() 함수는정적함수이므로 main() 에서호출할수있다. (9) class Circle private double radius; static final double PI= ; // PI 이름으로 로초기화된정적상수 public

JAVA 프로그래밍실습 실습 1) 실습목표 - 메소드개념이해하기 - 매개변수이해하기 - 새메소드만들기 - Math 클래스의기존메소드이용하기 ( ) 문제 - 직사각형모양의땅이있다. 이땅의둘레, 면적과대각

07 자바의 다양한 클래스.key

슬라이드 1

설계란 무엇인가?

A Hierarchical Approach to Interactive Motion Editing for Human-like Figures

No Slide Title

Microsoft PowerPoint 장강의노트.ppt

JAVA PROGRAMMING 실습 02. 표준 입출력

Microsoft PowerPoint - C++ 5 .pptx

Microsoft PowerPoint - Chap12-OOP.ppt

Design Issues

PowerPoint Presentation

PowerPoint Presentation

17장 클래스와 메소드

KNK_C_05_Pointers_Arrays_structures_summary_v02

Microsoft PowerPoint - Lect07.pptx

PowerPoint Presentation

Microsoft PowerPoint - ch09 - 연결형리스트, Stack, Queue와 응용 pm0100

유니티 변수-함수.key

Microsoft PowerPoint - ch07 - 포인터 pm0415

PowerPoint 프레젠테이션

PowerPoint Presentation

Microsoft PowerPoint - chap06-2pointer.ppt

10.0pt1height.7depth.3width±â10.0pt1height.7depth.3widthÃÊ10.0pt1height.7depth.3widthÅë10.0pt1height.7depth.3width°è10.0pt1height.7depth.3widthÇÁ10.0pt1height.7depth.3width·Î10.0pt1height.7depth.3width±×10.0pt1height.7depth.3width·¡10.0pt1height.7depth.3width¹Ö pt1height.7depth.3widthŬ10.0pt1height.7depth.3width·¡10.0pt1height.7depth.3width½º, 10.0pt1height.7depth.3width°´10.0pt1height.7depth.3widthü, 10.0pt1height.7depth.3widthº¯10.0pt1height.7depth.3width¼ö, 10.0pt1height.7depth.3width¸Þ10.0pt1height.7depth.3width¼Ò10.0pt1height.7depth.3widthµå

제8장 자바 GUI 프로그래밍 II

PowerPoint Presentation

PowerPoint Template

ThisJava ..

C++ Programming

C++ Programming

Microsoft PowerPoint - chap11

JAVA PROGRAMMING 실습 05. 객체의 활용

설계란 무엇인가?

Chapter 4. LISTS

PowerPoint 프레젠테이션

교육자료

Spring Data JPA Many To Many 양방향 관계 예제

PowerPoint 프레젠테이션

슬라이드 1

Frama-C/JESSIS 사용법 소개

Microsoft PowerPoint - java2 [호환 모드]

Microsoft PowerPoint - Java7.pptx

Microsoft PowerPoint - java1-lab5-ImageProcessorTestOOP.pptx

Microsoft PowerPoint - 04-UDP Programming.ppt

(Microsoft PowerPoint - java2-lecture3.ppt [\310\243\310\257 \270\360\265\345])

1. 객체의생성과대입 int 형변수 : 선언과동시에초기화하는방법 (C++) int a = 3; int a(3); // 기본타입역시클래스와같이처리가능 객체의생성 ( 복습 ) class CPoint private : int x, y; public : CPoint(int a

Microsoft PowerPoint - lec2.ppt

PowerPoint Presentation

Microsoft PowerPoint - 26.pptx

[ 마이크로프로세서 1] 2 주차 3 차시. 포인터와구조체 2 주차 3 차시포인터와구조체 학습목표 1. C 언어에서가장어려운포인터와구조체를설명할수있다. 2. Call By Value 와 Call By Reference 를구분할수있다. 학습내용 1 : 함수 (Functi

PowerPoint 프레젠테이션

쉽게 풀어쓴 C 프로그래밍

쉽게 풀어쓴 C 프로그래밍

PowerPoint Presentation

1. auto_ptr 다음프로그램의문제점은무엇인가? void func(void) int *p = new int; cout << " 양수입력 : "; cin >> *p; if (*p <= 0) cout << " 양수를입력해야합니다 " << endl; return; 동적할

쉽게 풀어쓴 C 프로그래밍

오버라이딩 (Overriding)

(Microsoft PowerPoint - 07\300\345.ppt [\310\243\310\257 \270\360\265\345])

11장 포인터

C++ Programming

A Dynamic Grid Services Deployment Mechanism for On-Demand Resource Provisioning

Microsoft PowerPoint - Introduction to Google Guava.pptx

PowerPoint Presentation

슬라이드 1

<322EBCF8C8AF28BFACBDC0B9AEC1A6292E687770>

예제 2) Test.java class A intvar= 10; void method() class B extends A intvar= 20; 1"); void method() 2"); void method1() public class Test 3"); args) A

Microsoft PowerPoint - CSharp-10-예외처리

비트와바이트 비트와바이트 비트 (Bit) : 2진수값하나 (0 또는 1) 를저장할수있는최소메모리공간 1비트 2비트 3비트... n비트 2^1 = 2개 2^2 = 4개 2^3 = 8개... 2^n 개 1 바이트는 8 비트 2 2

Microsoft PowerPoint - VB.NET_06.pptx

gnu-lee-oop-kor-lec11-1-chap15

Transcription:

:: TECHNOLOGY :: Java 용법마스터 객체의개념과기본메소드익히기 글 이해일 < 컴포넌트비전 ( 주 ) 책임컨설턴트 > jongidal@hitel.net 영어단어를아는사람은많지만영어를제대로구사하는사람은드물듯이, 현대프로그래밍의기본언어인 Java 를 유창하게 쓰는사람은의외로많지않은것같다. 이글은 Java 언어의자연스러운용법몇가지를통해 Java 를 유창하게 쓸수있도록하기위해기획된것이다. Java 의실전적용을목표로꼭익혀야할핵심용법을다룰것이다. 이번호에서는객체의개념부터시작해주요기본메소드를살펴본다. 022 ORACLE KOREA MAGAZINE

:: TECHNOLOGY :: 2003 AUTUMN 023

:: TECHNOLOGY :: Java 할줄아세요? 우리가외국어를배울때꼭익혀야하는세가지는문법, 어휘, 용법이다. 언어가어떤구조를가지는지 ( 문법 ), 구성요소는무엇인지 ( 어휘 ), 어떻게쓰는것이자연스러운지 ( 용법 ) 를익혀야유창하게외국어를구사할수있다. 문법과어휘는책만봐도익힐수있지만용법은직접그나라에가서살아보거나그말을모국어로쓰는외국인과부딪혀보기전에는익히기어렵다. 프로그래밍언어도마찬가지다. 우선배우려는프로그래밍언어의핵심문법을이해해야한다. 절차형언어인지, 객체지향언어인지알아야하고어떤특징을가지는지이해해야한다. 또, 자료구조, 연산자, 키워드, 표준라이브러리같은프로그래밍언어어휘들도배워야한다. 여기까지는책을읽거나강의를듣는것만으로쉽게 (?) 익힐수있다. 하지만, 정확한용법을벗어난외국어가어색한것처럼프로그래밍언어의정확한용법을모르고서는자연스럽고우아한코드를만들어낼수없다. 자연스럽고우아한코드를만드는것은미학의문제만은아니다. 중요한외교석상에서어설픈외국어를쓴다면망신을당하는차원을넘어국가대사를그릇칠수도있는것처럼어설프게만든코드는전체시스템을죽일수도있다. 특히, 요즘같이정보시스템없이돌아가는업무를상상하기힘든상황에서제대로프로그래밍언어를구사하는것은정말중요한일이다. Java 할줄아세요? 라는질문은 영어할줄아세요? 라는질문과비슷하다. 우리의 예 라는대답은과연어떤 예 일까? 하지만, 미리기죽을필요는없다. 우리가태어났을때외국어를몰랐지만계속배우고익히면유창하게외국어를구사할수있는것처럼프로그래밍언어도배우고익혀유창하고우아하게구사하면된다. 언어는사고와서로상호작용하면서같이커가는것이다. 언어를통해표현하면생각이자라고생각이자라면표현도더우아해진다. 이기사에서는 Java라는프로그래밍언어의자연스러운용법을몇가지소개하여우리가 Java스럽게 생각하고 Java를모국어처럼유창하게말하는데작은도움을주고자한다. 이번호에서는Java 프로그래밍언어의용법을객체라는일반론관점에서살펴보고, 아주중요한기본메소드인 equals, hashcode, copareto, compare의정확한용법을살펴볼것이다. 그리고, 다음호에서는프로그래밍일반론으로불변규칙을지키는방법, API 만드는방법, Java의중요한특징중하나인예외처리, 유용한구현패턴들을살펴볼계획이다. 객체를이해하면 Java가쉬워진다고대로부터귀납법과연역법 ( 요즘은 Bottom-up( 상향식 ) 과Top-down( 하향식 ) 이라고도한다 ) 이라는사물을이해하는방법이전해내려온다.. 상향식방법은구체적인사실로부터추상적인개념을이해하는방식이고, 하향식방법은추상적인개념으로부터구체적인사실들을이해하는것이다. 이둘은서로양립하는관계가아니라서로보완하는관계이다. Java와객체지향의관계도마찬가지다. Java 언어를쓰면서객체지향사고를단단히할수있고객체지향 사고가단단해질수록Java라는언어를우아하고유창하게말할수있는것이다 < 그림1>. 그러면,Java와객체지향개념이어떻게서로영향을미치는지살펴보자. < 그림1> Java와객체지향의관계모든것은객체다객체지향언어에서모든것은객체다. 객체란프로그램이해결해야하는문제공간의구성요소와이문제를해결하는해결책들을추상화한것이다. 객체지향언어는문제를기계나컴퓨터의관점에서기술하고해결하는것이아니라문제의관점에서기술하고해결하려고노력한다. 컴퓨터가해결해야하는복잡한문제를우리가매일만나는세상사를해결하듯이접근할수있다는것이객체지향언어의가장큰장점일것이다. 그렇다면객체는어떻게동작할까? 객체는클라이언트로부터요청을받았을때만자신의오퍼레이션을수행할수있다. 이렇게오퍼레이션을수행해야만객체상태가바뀌고이런상태변화를통해객체는문제를해결한다 < 그림2>. < 그림2> 객체가동작하는원리객체의목적은자신의클라이언트에게서비스를제공하는것이다. 객체혼자서는아무런의미가없다. 한객체가받아들일수있는요청이무엇인지모아놓은것이바로인터페이스이다. 클라이언트는객체가요청을받아들여서어떻게처리하는지알고싶지도않을것이고알필요도없을것이다. 궁금한것은인터페이스뿐이다. 객체지향의핵심은바로인터페이스이다. 객체지향의핵심은인터페이스이다어떤객체가A 타입이라는것은이객체가A라는인터페이스가받아들일수있는모든요청을받아서서비스를제공한다는뜻이다. 한객체는여러타입일수있고전혀관련이없는객체들도같은타입일수있다. < 그림 3> 처럼어떤인터페이스가다른인터페이스를포함할수도있다. 이경우포함된인터페이스를 슈퍼타입 (supertype) 이라고하고포함하는 024 ORACLE KOREA MAGAZINE

:: TECHNOLOGY :: 인터페이스를 서브타입 (subtype) 이라고한다. 서브타입은슈퍼타입이받아들일수있는모든요청을받아들일수있고자신만의요청도받아들일수있다. 이런포함관계가있을때서브타입인터페이스가슈퍼타입인터페이스를상속받는다고말한다. < 그림3> 인터페이스의상속관계앞에서도이야기했듯이객체는인터페이스를통해서만자신을외부에드러낼수있다. 외부에서객체에대해알수있는것은인터페이스밖에없고사실인터페이스만알면된다. 이말은타입만같으면요청을받아서처리하는객체가어떻게구현되었든아무런상관없다는뜻이다. 이렇게클라이언트의요청을처리할객체를바꿀수있는기법을 동적결합 (dynamic binding) 이라고한다. 동적결합은타입만같으면언제든지다른객체로교체할수있는다형성 (polymorphism) 때문에가능하다. 이개념은상당히중요하다. 인터페이스, 동적결합, 다형성때문에객체지향언어로개발한프로그램이뛰어난유연성과재사용성을제공할수있는것이다. 이사실은요청에도똑같이적용된다. 요청을받아서처리하는객체는요청이어떻게구현되었는지알필요가없다. 요청이제공하는인터페이스만알면된다. 다음코드를살펴보자. // Good - 인터페이스로선언한다. List subscribers = new Vector(); // Bad - 구현체로선언한다. Vector subscribers = new Vector(); 만약 Vector가아닌 ArrayList로구현체를바꿔야한다면, 인터페이스로선언한경우에는 subscribers를초기화하는부분을다음과같이바꿔주기만하면나머지코드는손댈필요가전혀없다. 하지만, 구현체로선언했고나머지코드에서구현체만제공하는기능을쓰고있다면구현체를바꾸려할때많은부분을고쳐야한다. List subscribers = new ArrayList(); 인자를정의할때도인터페이스로정의하는것이좋다. 예를들어, 인터페이스가있는데도 Hashtable과같은구현체를인자로받는메소드를만들면안된다. 인터페이스인 Map을써야한다. Map을인자로받으면 Hashtable, HashMap, TreeMap과같이현재구현된모든 Map 구현체뿐만아니라, 앞으로구현될모든Map 구현체들을인자로받아처리할수있다. Hashtable로인자를정의했다면이오퍼레이션을호출할때 Hashtable이아닌 Map 객체는모두 Hashtable로바꿔야할것이다. 이것은필요하지도않고오류가발생하기도쉽다. 이렇게구현체가아닌인터페이스로프로그램을만드는것은객체지향프로그래밍의중요한원칙이다. 그렇다면클래스는무엇인가? 클래스는객체의구현을정의한것이다. 한객체는여러타입일수있고여러클래스로부터생성된객체라도같은타입일수있지만, 한객체의클래스는단하나이다. 클래스는객체의정적인모습을표현한다. 그런데, 객체지향세계를객체사이의관계가아닌클래스사이의관계로이해하려는오류를범하는경우가많다. 원래시간과상황에따라바뀌는것보다는고정된것을이해하는것이더쉽고, 객체가클래스로부터생성되기때문에이런오류를범하는것같다. 하지만, 세상의모든사물이시간과상황에따라상태가변하는것처럼시스템의상태도계속변한다. 이렇게계속변하는상태와관계를고정된클래스로파악하기는힘들다. 또, 인터페이스, 동적결합, 다형성으로실제로동작하는객체가실행시점에바뀔수있기때문에항상어떤객체가어떤상태에있는지파악해야하고객체들사이의관계도어떻게변하고있는지파악해야시스템을이해할수있다. 클래스상속과인터페이스상속은구분해야한다. 클래스상속은코드재사용이목적이지만인터페이스상속은다른객체로대체하는것이목적이다. 많은언어가문법차원에서클래스와인터페이스를구분하지않아서혼란스러울수있지만, 이개념만은확실히구분해야한다 (Java에는 interface 라는키워드가있어서인터페이스와클래스를구분할수는있지만클래스로도타입을정의할수있기때문에역시혼동이생길수있다. 언어자체가인터페이스와클래스를구분하는것을강제하지는못하지만타입을정의할때는인터페이스를쓰는것이좋다는개념을꼭명심하고가능하면인터페이스로타입을정의해야한다.) 클래스상속은코드를재사용하는것이기때문에부모클래스가어떻게구현되었는지자세하게알아야하고부모클래스의구현이변하면바로이부모클래스를상속받은모든서브클래스들이영향을받는다. 이런구현종속성은인터페이스상속에선나타나지않는다. 인터페이스에는아무런구현이없기때문이다. < 그림 3> 에서보았듯이서브타입은슈퍼타입을단순히포함한다. 그렇다면, 안전하게코드를재사용할방법은없는것일까? 다행히도 컴포지션 이라는방법이있다 ( 많은책이나교육과정에서이방법을가르치지않아안타깝다.) 컴포지션방법은기존클래스를상속받아새로운클래스를만드는것이아니라재사용하려는객체에대한참조를 private 필드로갖는새로운클래스를만드는것이다. 재사용하려는기존클래스가새로운클래스의한구성요소가되므로이런방식을 컴포지션 (composition) 이라고한 2003 AUTUMN 025

:: TECHNOLOGY :: 다. 컴포지션에서는새로운클래스가기존클래스의인터페이스만알면되지자세한구현방식을알필요는없다. 이것은서브타입이단순히슈퍼타입의인터페이스를포함하는인터페이스구현과비슷한개념이다. 새로운클래스의객체는전달받은요청을자신이포함하고있는객체에게위임한다. 이런방식을 포워딩 (forwarding) 이라하고새로운클래스의메소드를 포워딩메소드 (forwarding method) 라고한다. 이렇게만든클래스는기존클래스의세부구현방법에의존하지않기때문에기존클래스의구현이바뀐다해도영향을받지않는다. 구성요소를몇개나저장했는지알수있는Set를만들때HashSet와같은기존Set 구현체를상속받는것이아니라다음과같이컴포지션과포워딩메소드를쓰는것이좋다. 이때, 재사용하려는클래스가인터페이스를구현하고있다면새로만드는클래스도같은인터페이스를구현하는것이좋다. public class InstrumentedSet implements Set { // 선언은항상 interface로. // 재사용하려는 Set 객체 private final Set s; private int addcount = 0; public InstrumentedSet(Set s) { this.s = s; public boolean add(object o) { addcount++; return s.add(o); public boolean addall(collection c) { addcount += c.size(); return s.addall(c); public int getaddcount() { return addcount; // 포워딩메소드들 public void clear() { s.clear(); public boolean contains(object o) { return s.contains(o); public boolean isempty() { return s.isempty(); public int size() { return s.size(); public Iterator iterator() { return s.iterator(); public boolean remove(object o) { return s.remove(o); public boolean containsall(collection c) { return s.containsall(c); public boolean removeall(collection c) { return s.removeall(c); public boolean retainall(collection c) { return s.retainall(c); public Object[] toarray() { return s.toarray(); public Object[] toarray(object[] a) { return s.toarray(a); public boolean equals(object o) { return s.equals(o); public int hashcode() { return s.hashcode(); public String tostring() { return s.tostring(); 어떻게객체를생성할것인가? 객체지향세계에서가장중요한객체는어떻게생성하는것이좋을까? 상속보다는컴포지션이권장되면서객체를생성하는방법은더욱더중요해지고있다. 객체를생성하는가장흔한방법은 public 생성자를쓰는것이다. 이방법을쓰면반드시생성하려는객체의클래스이름이명시적으로나오게되므로객체생성이특정구현에완전히얽매이게된다. 따라서이렇게객체를생성하면나중에다른구현체로바꾸려할때문제가생긴다. 또, 싱글톤 (singleton) 처럼객체수를제한할필요가있다면 public 생성자로객체를생성하지못하게막아야한다. 객체생성은어떻게제어할까? 우선팩토리메소드라는방법을살펴보자. 팩토리메소드는단순하게객체생성을책임지는메소드이다. 이메소드는생성하는객체를정의한클래스의스태틱메소드일수도있고객체생성전담클래스에정의된메소드일수도있다. 팩토리메소드를쓰면 public 생성자보다다음과같은몇가지좋은점이있다. 생성자는클래스와같은이름만가질수있지만팩토리메소드는의미있는이름을가질수있어서코드를이해하기쉬워진다. 예를들어소수 ( ) 일가능성이큰 BigInteger 객체를생성할때, 생성자인 BigInteger(int, int, Random) 보다팩토리메소드인 BigInteger.probablePrime를쓰는쪽이훨씬더이해하기쉽다. 또, 생성자를중복정의할때어쩔수없이같은시그니처를가질수밖에없어곤란한경우가있지만, 팩토리메소드를쓰면이름만바꾸면된다. 팩토리메소드를쓰면특정시점에존재하는객체의수를엄격하게관리할수있다. 싱글톤처럼객체수가제한되거나내용이동등한불변클래스의객체가단하나만존재하게하려면, 팩토리메소드로객체생성을제어해야한다. 예를들어, Boolean에는기본타입 boolean 값을받아이에해당하는 Boolean 객체를만들어내는valueOf라는팩토리메소드가있다. Boolean 은true아니면false에해당하는객체두개만있으면된다. 따라서, public 생성자로새로운객체를매번생성할필요가없이객체두개를미리만들어놓고필요할때마다이객체를제공하면된다. public static Boolean valueof(boolean b) { 026 ORACLE KOREA MAGAZINE

:: TECHNOLOGY :: return (b? Boolean.TRUE : Boolean.FALSE); 생성자는반드시자신을정의한클래스의객체를생성해야하지만, 팩토리메소드는리턴타입과그하위타입에해당하는어떤객체라도생성할수있다. 다시말하면, 타입만맞으면언제라도구현체를마음대로바꿀수있다. 이것은아주중요한특성으로, 팩토리메소드를쓰는가장큰이유라고할수있다. 예를들어, 컬렉션프레임워크에는수정할수없는컬렉션 (unmodifiable collection), 동기화컬렉션 (synchronized collection) 과같은20여개의편리한구현클래스들이있다. 이클래스들의객체는모두java.util.Collections 에있는팩토리메소드로만얻을수있다. 만약이20개클래스가모두외부에드러났다면컬렉션프레임워크는지금보다훨씬복잡했을것이다. 다행히팩토리메소드가이런복잡성을감췄기때문에오로지인터페이스만알면모든구현클래스를마음대로쓸수있다. 또, 팩토리메소드를쓰면클라이언트가리턴받은객체를실제구현클래스타입이아닌인터페이스타입으로만참조하도록강제할수있다는장점도있다. 클래스를만들때습관처럼public 생성자를만들지말고자유롭게객체가생성될필요가있는지다시한번생각해보라. 객체생성을제어하고싶다면팩토리메소드를쓰는것이좋다. 모든생성자를 private로만들어외부에서해당클래스의객체를만들지못하게막아야하는경우가있다. 상태가없는유틸리티클래스 (java.util.collections, java.util.arrays 같은것들 ) 는객체를만들필요가없다. 따라서, 이런클래스의생성자는모두 private이고메소드는모두 static 이다. 싱글톤은정확히하나의객체만존재하는클래스로생성자를private으로정의하고클라이언트가이클래스의유일한인스턴스에접근할수있도록 public static 필드나메소드를제공하는방식으로구현한다. 우선 public static 필드를쓰는방식을살펴보자. public class JNDIService implements IService { public static final JNDIService SINGLETON = new JNDIService(); private JNDIService() { super(); caches = new HashMap(); JNDIService의 private 생성자는이클래스가로딩되는시점에단한번호출된다. 클라이언트는접근할수있는 JNDIService 생성자가없기때문에더이상이클래스의객체를만들수없다. JNDIService 객체는 SINGLETON이참조하는객체하나만존재한다. 다음으로public static 메소드를쓰는방식을살펴보자. public class JNDIService implements IService { private static JNDIService singleton = null; private JNDIService() { super(); caches = new HashMap(); public synchronized static JNDIService getinstance() { if (singleton == null) { singleton = new JNDIService(); return singleton; public static 필드를쓰면자동으로 JNDIService 객체가하나생성된다. 만약싱글톤을생성하는비용이크다면이것은낭비다. 이런경우에는 public static 메소드방식을써서늦은초기화 (lazily initialization) 를해야한다. 하지만, 다중스레드환경에서싱글톤을보장하려면앞의코드처럼반드시동기화해야한다. 늦은초기화도필요하고동기화도피해야한다면다음과같이보유자클래스 (Initiate-on-demand holder class) 구현패턴을써야한다. public class JNDIService implements IService { // 보유자클래스 private static class SingletonHolder { static final JNDIService SINGLETON = new JNDIService(); 2003 AUTUMN 027

:: TECHNOLOGY :: private JNDIService() { super(); caches = new HashMap(); // 동기화도필요없고비교도필요없다. public static JNDIService getinstance() { return SingletonHolder.SINGLETON; 이구현패턴은모든클래스의초기화는이클래스가처음으로쓰이는순간이루어진다는사실에기초한것이다. getinstance 메소드를호출하여 SingletonHolder 클래스의 SINGLETON 필드를처음으로읽는순간 SingletonHolder 클래스가초기화된다. 따라서, 동기화가필요없고비교도필요없다. 아무런추가비용없이늦은초기화의장점까지제공할수있는아주유용한구현패턴이다. 객체를생성할때상황에따라여러가지전략이필요하다. 무심코public 생성자를만들고있는자신을한번돌아보라. 객체생성은많은비용이들어가는작업일수도있고시스템의유연성에큰영향을미치는작업일수도있기때문에신중해야한다. 어떻게객체를파괴할것인가? Java 플랫폼의모든배열과객체들은 힙 (heap) 이라는메모리공간에저장된다. new 키워드를쓸때마다힙의새로운메모리가객체에할당된다. 하지만, Java는C++ 같은언어와달리할당한메모리를명시적으로반환하는방법이없다. 사실, 이작업은가비지컬렉터가담당한다. 가비지컬렉터는아주낮은우선순위를가진백그라운드스레드로동작하면서어떤객체의메모리를반환해야하는지계속검사한다. 만약, 메모리를반환해야하는객체를찾았고시간도충분하다면가비지컬렉터는종료자를수행하는것과같이몇가지필요한작업을처리하고객체를파괴한다음에이객체의메모리를힙으로반환한다. 프로그래머가무엇을하든가비지컬렉터에게이작업을강제로시킬수는없다. 하지만, 객체참조에null을대입하고System.gc() 를호출하면 < 그림 4> 처럼객체그래프에서더이상참조되지않는객체들이생기고 JVM 이한가하다면가비지컬렉션이일어나면서 ( 반드시가비지컬렉션이일어난다는것을보장할수는없다.) 더이상참조되지않는객체들을파괴한다 ( 실제로이것보다훨씬복잡한알고리즘을쓰고있지만, 이정도만알아도충분할것이다.) < 그림4> 가비지컬렉션대상을고르는방법이런방식으로메모리를할당하고반환하면아주안전하기는하지만프로그래머가메모리를직접관리할수없어서불편할수있다. 이문제를해결하기위해 JDK 1.2 배포판부터 java.lang.ref 패키지를제공해서프로그램에서가비지컬렉터에접근할수있는방법을제공하고있다. 자세한것은 Java 명세문서를참조하기바란다. 그런데, Java에서는가비지컬렉터가객체파괴를처리해주므로메모리에대해신경쓰지않아도될것같지만, 몇가지주의할점이있다. 다음과같이구현한간단한스택을한번살펴보자. public class stack { private object [] elements; private int size = 0; public Stack(int initialcapacity) { this.elements = new Object[initialCapacity]; public void push(object e) { ensurecapacity(); elements[size++] = e; 028 ORACLE KOREA MAGAZINE

:: TECHNOLOGY :: public Object pop() { if(size == 0) throw new EmptyStackException(); return elements[--size]; private void ensurecapacity() { if (elements.length == size) { Object[] oldelements = elements; elements = new Object[2*elements.length+1]; System.arraycopy(oldElements, 0, elements, 0, size); 이스택은별문제가없어보이지만스택에서팝 (pop) 된객체들에대한참조를스택이계속쥐고있기때문에이객체들은가비지컬렉션대상이되지않고끝까지남는다. 가비지컬렉터가있는언어에서도메모리누수현상 ( 의도하지않은객체유지 (unintentional object retention) 가더적절한표현이다.) 이일어날수있다! 이런문제는간단하게해결할수있다. 쓸모없어진참조에다음과같이null을대입해버리면된다. 이메소드들은꼭알아두자 Java의모든클래스는java.lang.Object를상속받는다. java.lang.object에는 equals, hashcode, clone, tostring, finalize와같이하위클래스에서재정의할수있는 final이아닌public이나 protected 메소드들이있다. 이메소드들을재정의할때꼭지켜야하는규칙들이정해져있다. 여러분이만든클래스가이메소드들을재정의하면서규칙을지키지않는다면커다란혼란이일어난다. 이런혼란을막기위해서간단하지만아주중요한java.lang.Object의기본메소드인 equals와hashcode를정확히구현하는방법을살펴보자. 또, java.lang.object의메소드는아니지만꼭알아두어야할 Comparable과 Comparator 인터페이스에있는compareTo 메소드와compare 메소드도살펴보겠다. equals 메소드 equals 메소드를살펴보기전에우선 같다 라는것을구분해보자. same, equal, equivalent, identical은모두우리말로 같다 로바꿀수있지만이것들을구분할필요가있다. < 그림 5> 에서참조1과참조2는 같은 객체를참조하고있다. 이경우 같은 객체를참조한다는것은한메모리주소에있는 동일한 (identical) 객체를참조한다는뜻이다. 이경우참조1 == 참조2 의결과는true이다. 당연히두참조가가리키는객체의내용은같다. public Object pop() { if(size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; // 쓸모없는참조를없앤다. return result; Stack처럼자신만의메모리공간을가지는클래스는언제나메모리누수가일어날가능성이있다. 어떤객체를보관할필요가없는지는프로그래머만알수있다. 따라서, 프로그래머가직접필요없는객체참조를null로만들지않으면가비지컬렉터는어떤객체를가비지컬렉션할지알수가없다. 따라서, 자신만의메모리공간을가지는클래스를쓸때보관할필요가없는객체가생기면이객체에대한참조변수에 null을대입하여가비지컬렉션대상으로만들어야한다. 객체파괴에서또하나주의할점은, finalize, System.runFinalization, System.runFinalizersOnExit, Runtime.runFinalizersOnExit와같은종료자는쓰지않는것이좋다는것이다. 종료자들은정확히실행된다는보장도없고종료자메소드에서처리하지않는예외가발생하면예외도무시되고종료자도끝나버린다. 이런여러가지문제때문에종료자에의존하는코드는되도록만들지않도록한다. < 그림5> 동일한객체. 참조1 == 참조2, 참조1.equals( 참조2) 는모두 true 그러나, < 그림 6> 에서참조 1과참조2는동일하지않은객체를참조하고있다. 다시말하면, 참조1 == 참조2의결과는false이다. 하지만, 참조1과참조 2가가리키는두객체의상태는같다 ( 필드에저장된값이객체상태를결정한다.) 이때 같다 는것은두객체의내용이 동등하다 (equivalent) 는뜻이다. equals 메소드는이런내용이동등한지검사하여같다고판단하면true를리턴해야한다. 2003 AUTUMN 029

:: TECHNOLOGY :: return (this == obj); 따라서, 객체내용의동등성이중요하지않은몇가지경우 ( 예를들면, Thread처럼객체내용이라는것이아예없는경우 Random처럼객체내용은있지만의미가없는경우 외부에드러나지않는객체라서equals 메소드가호출되는일이없는경우 불변객체나싱글톤처럼내용이같은객체가중복으로존재할수없는경우 상위클래스에서이미적절한equals 메소드를정의해놓은경우 ) 가아니라면항상 equals 메소드를재정의해야한다. equals 메소드는아주간단해보이지만지켜야하는구현계약은생각보다복잡하고조심하지않으면문제가생기기쉽다. equals 메소드의구현계약은다음과같다 (java.lang.object의equals 메소드명세 ) < 그림 6> 동등한객체. 참조 1 == 참조 2는 false, 참조1.equals( 참조2) 는 true 하지만, java.lang.object의 equals 메소드는다음과같다. 따라서, equals 메소드를재정의하지않으면객체내용이동등한지검사할수없다. public boolean equals(object obj) { equals 메소드는동등관계 (equivalence relation) 를구현한다. 1. 반사적 (reflexive) 이다 : 모든참조값 x에대해x.equals(x) 는true를리턴해야한다. 2. 대칭적 (symmetric) 이다 : 모든참조값 x와 y에대해 y.equals(x) 가 true를리턴할때만x.equals(y) 는true를리턴해야한다. 3. 추이적 (transitive) 이다 : 모든참조값 x, y, z에대해만약x.equals(y) 와 030 ORACLE KOREA MAGAZINE

:: TECHNOLOGY :: y.equals(z) 가 true를리턴한다면 x.equals(z) 도 true를리턴해야한다. 4. 일관적 (consistent) 이다 : 모든참조값 x,y에대해, 만약, equals 메소드가비교할때쓰는정보가변하지않는다면 x.equals(y) 의결과는항상일관성이있어야한다. 즉, 한번true면계속 true를, 한번false면계속 false를리턴해야한다. 5. null이아닌모든참조x에대해, x.equls(null) 는반드시 false를리턴한다. 다. 심지어예외가발생할수도있다. 이렇게어떤클래스가구현계약을어기면다른클래스가예측할수없는행동을한다. CaseInsensitiveString의 equals 메소드가String까지비교하려고한것이문제이다. 다른타입의객체와동등성을비교하려하지말아야한다. 그렇다면, 추이성은언제깨질까? 2차원평면위의한점을표현하는간단한클래스를만들어보자. 이구현계약들중에서2번, 3번은특히어기기쉽다. 대 / 소문자를구분하지않는문자열을표현하는클래스인 CaseInsensitiveString를예로들어대칭성을어기는경우를살펴보자 ( 참고로, 여기서다루는예제는 Joshua Bloch의 [Effective Java Programming Language Guide](Addison- Wesley) 에나온것을인용했음을밝혀둔다.) public final class CaseInsensitiveString { public boolean equals(object o) { if (o instanceof CaseInsensitiveString) return s.equalsignorecase(((caseinsensitivestring)o).s); // 대칭성에문제가생긴다. if (o instanceof String) return s.equalsignorecase((string)o); return false; // 이하생략 public class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; public boolean equals(object o) { if (!(o instanceof Point)) return false; Point p = (Point)o; return p.x == x && p.y == y; // 이하생략 이클래스를상속받아 색깔 정보를추가해보자. 위코드에서는대 / 소문자를구분하지않는문자열과보통문자열 (String 객체 ) 의동등성을비교하고있다. 하지만 String의equals 메소드는대 / 소문자를구분하지않는문자열을처리할수없기때문에대칭성이깨진다. 다음과같이대 / 소문자를구분하는문자열객체와일반문자열객체가있다고하자. CaseInsensitiveString cis = new CaseInsensitiveString("KoReA"); String s = "Korea"; 예상한대로 cis.equals 메소드는 true를리턴하지만 String.equals 메소드는 false를리턴한다. 이것이왜문제일까? CaseInsensitiveString 객체를컬렉션에넣어보자. List list = new ArrayList(); list.add(new CaseInsensitiveString("KoReA")); public class ColorPoint extends Point { private Color color; public ColorPoint(int x, int y, Color color) { super(x, y); this.color = color; // 이하생략 이클래스는 equals 메소드를재정의하지않았기때문에 Point 클래스의equals 메소드를그대로쓴다. ColorPoint가색깔을비교하지않는것은분명히이상하기때문에다음과같이위치도같고색깔도같을때만 true를리턴하도록equals 메소드를재정의해보자. list.contains( Korea ) 의결과는무엇일까? 이결과는정해져있지않 public boolean equals(object o) { 2003 AUTUMN 031

:: TECHNOLOGY :: if (!(o instanceof ColorPoint)) return false; ColorPoint cp = (ColorPoint)o; return super.equals(o) && cp.color == color; 이번에는 Point의 equals 메소드가 ColorPoint 객체를받아들여비교할수있기때문에대칭성이깨진다. ColorPoint의 equals 메소드는항상 false를리턴하고point의equals 메소드는위치만같다면true를리턴한다. 그렇다면, 다음과같이 ColorPoint.equals가 Point 객체도비교할수있게고치면되지않을까? public boolean equals(object o) { if(!(o instanceof Point)) return false; // o가그냥point일때는색깔은비교하지않고위치만비교한다. // 추이성이깨지는부분이다. if (!(o instanceof ColorPoint)) return o.equals(this); // o가 ColorPoint인경우색깔과위치를모두비교한다. ColorPoint cp = (ColorPoint)o; return super.equals(o) && cp.color == color; 안타깝게도이번에는추이성이깨진다. 다음과같이 Point의인스턴스와ColorPoint의인스턴스를만들어이사실을확인해보자. ColorPoint p1 = new ColorPoint(1, 2, Color.RED); Point p2 = new Point(1, 2); ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE); p1.equals(p2) 와 p2.equals(p3) 는 true를리턴하지만 p1.equals (p3) 는false를리턴한다. p1.equals(p2) 와p2.equals(p3) 는색깔을뺀위치만비교하고 p1.equals(p3) 는색깔과위치를모두비교하기때문에이런문제가생긴것이다. 그렇다면도대체이문제는어떻게해결해야할까? 사실이런문제는객체지향언어에서나타나는객체동등성에대한근본문제다. 객체를만들수있는클래스를상속받아새로운필드를추가하면서표준구현계약을준수하는equals 메소드를만들수없다. 이문제는정면돌파가거의불가능하기때문에피해가야만한다. 상속대신컴포지션을써야한다. // equals 구현계약을지키면서새로운부분을추가하기. public class ColorPoint { // Point 객체와컴포지션관계를가진다. private Point point; private Color color; public ColorPoint(int x, int y, Color color) { point = new Point(x, y); this.color = color; /** * ColorPoint 인스턴스의 Point로서의모습을리턴한다. */ public Point aspoint() { return point; // ColorPoint 객체만비교한다. public boolean equals(object o) { if(!(o instanceof ColorPoint)) return false; ColorPoint cp = (ColorPoint)o; return cp.point.equals(point) && cp.color.equals(color); // 이하생략 ColorPoint는 Point 타입이아니기때문에 Point의 equals 메소드에 ColorPoint 객체가전달되면항상 false를리턴한다. ColorPoint와 Point 의상속관계를끊음으로써 ColorPoint와Point의equals 메소드는정확한타입의객체를비교할수있다. 정확한타입의객체를비교하게하는것이핵심이다. 또, ColorPoint가 Point로어떻게표현되는지알아야한다면 aspoint와같은뷰메소드를제공하면된다. 이모든구현계약을지키는 equals 메소드를만드는비법은다음과같다 ( 이비법은Joshua Bloch의 [Effective Java Programming Language Guide](Addison-Wesley) 에나온것을인용했다.) 1. 연산자를써서인자가 this 객체를참조하는지검사한다. 만약그렇다면, true 를리턴한다. 이것은성능을최적화하기위해수행하는작업이다. 비교작업이복잡하다면이검사를하는것이좋다. 2. instanceof 연산자를써서인자의타입이올바른지검사한다. 만약, 타입이틀리다면 false를리턴한다. 이때, null이인자로넘어오면 instanceof 연산자는항상 false를리턴하므로따로 null 검사를하지않아도된다. 보통, 올바른타입은호출하는 equals 메소드를정의한클래스타입이다. 하지만, 이클래스가인터페이스를구현한다면이인터페이스도올바른타입이될수있 032 ORACLE KOREA MAGAZINE

:: TECHNOLOGY :: 다. 즉, 같은인터페이스를구현한클래스들은서로비교할수있다. 컬렉션 프레임워크의 Set, List, Map, Map.Entry와같은인터페이스를구현한클래스들은이인터페이스의타입인지비교한다. 3. 인자를정확한타입으로변환한다. 이타입변환은이미 instanceof로타입을검사했기때문에항상성공한다. 4. 주요필드 (significant field) 에대해인자의필드와 this 객체의해당필드의값이동등한지검사한다. 모든필드가동등하다면 true를리턴하고하나라도동등하지않다면 false를리턴한다. 해당필드가 float나 double이아닌기본타입이라면 == 연산자로비교한다. Float와 double 타입은각각 Float.floatToIntBits 메소드와 Double.doubleToLongBits 메소드로 int 와 long 값으로변환한다음 == 연산자로비교한다. 객체참조필드의경우에는그객체의 equals 메소드로비교한다. 배열필드의경우모든각구성요소에대해지금까지설명한작업을수행한다. null에대한참조가허용된객체참조필드에대한비교는 NullPointerException을막기위해다음과같은구현패턴을써서비교한다. 표준형식에대하여 java.math.bigdecimal의경우를예로들어보자. 다음과같이 BigDecimal 인스턴스를생성해보자. BigDecimal n = new BigDecimal ( "-12345.6789" ) 이때, BigDecimal 내부에서는 -12345.6789라는수를어떻게저장하고있을까? BigDecimal의소스를보면다음과같은필드를정의하고있다. private BigInteger intval; private int scale = 0; (field == null? o.field == null : field.equals(o.field)) 만약, this 객체의필드와인자가가리키는객체의필드가동일한 (identical) 객체를참조하는경우라면다음과같이비교하는것이더빠르다. (field == o.field (field!= null && field.equals(o.field))) BigDecimal은 -12345.6789라는숫자를그대로보관하는것이아니라, 이수의숫자부분 (-123456789) 은 BigInteger intvalue 필드에, 10의제곱수부분 (-4) 은int scale 필드에보관한다.-12345.6789 를-123456789와 -4로나누어 -123456789x10-4 로표현하는것이 10 진수를표현하는표준형식 (canonical form) 이다. 이렇게표준형식으로저장하면어떤숫자라도정확하게표현할수있다. 앞에서살펴본 CaseInsensitiveString 과같은클래스는단순히필드의동등 성을검사하는것만으로 equals 메소드를구현할수없다. 이런경우, 무엇을어떻게비교하고검사하는지가클래스명세에서부터확실하게드러나야한다. 이경우표준형식 (canonical form) 을정해놓고여기에각객체의정보를저장한다음, equals 메소드에서이표준형식을써서정확하고빠른비교작업을수행하게만들수도있다. 그러나, 객체의내용이변할때마다표준형식에저장한내용도계속바꿔주어야하기때문에표준형식은불변클래스에쓰는것이가장좋다. 이비법에따라전화번호를표현하는 PhoneNumber 클래스의 equals 메소드를다음과같이구현할수있다. rangecheck(extension, 9999, "extension"); this.areacode = (short) areacode; this.exchange = (short) exchange; this.extension = (short) extension; private static void rangecheck(int arg, int max, String name) { if (arg < 0 arg > max) throw new IllegalArgumentException(name + ":" + arg); public final class PhoneNumber { private final short areacode; private final short exchange; private final short extension; public PhoneNumber(int areacode, int exchange, int extension) { rangecheck(areacode, 999, "area code"); rangecheck(exchange, 999, "exchange"); public boolean equals(object o) { if (o == this) return true; if (!(o instanceof PhoneNumber)) return false; PhoneNumber pn = (PhoneNumber)o; // extention, exchange, areacode 필드가equals의비교대상이다. 2003 AUTUMN 033

:: TECHNOLOGY :: return pn.extension == extension && pn.exchange == exchange && 로추가한다.) pn.areacode == areacode; equals 메소드를제대로만드는것은생각보다어렵고중요하다. equals 메소드의구현계약을지키지않으면이클래스를쓰는다른클래스들이문제를일으킨다는점과 equals 메소드구현계약을지키려면자기와같은타입만비교해야한다는것은꼭기억해야한다. hashcode 메소드 hashcode 메소드의구현계약은다음과같다.(java.lang.Object의 hashcode 메소드명세 ) 1. 애플리케이션이일단실행된다음동일한객체의 hashcode를여러번호출하더라도 equals 메소드에서비교하는필드의값을바꾸지않는다면, 항상같은정수값을리턴해야한다. 하지만, 이정수값은애플리케이션이다시실행되면바뀔수도있다. 2. equals(object) 메소드의리턴값이 true인두객체의 hashcode 메소드는같은정수값을리턴해야한다. 3. equals(object) 메소드의리턴값이 flase인두객체의 hashcode 메소드가반드시다른정수값을리턴할필요는없다. 하지만, 해시 (hash) 알고리즘을쓰는컬렉션의성능을향상시키려면동등하지않은객체는다른정수값을리턴하는것이좋다. < 그림7> 해시에데이타를저장하는과정이렇게기록한주소록에서원하는연락처를찾는순서는다음과같을것이다 < 그림8>. 1. 찾으려는연락처이름의첫글자를뽑는다 ( 첫글자는 이 다.) 2. 첫글자에해당하는주소록페이지를펼친다 ( 이 로시작하는이름을모아놓은페이지를펼친다.) 3. 이페이지에서찾으려는이름을가진연락처를찾는다 ( 이해일 이라는이름을찾는다.) 이조항들중에서특히두번째조항을어기기쉽다. equals 메소드를재정의했으면꼭hashCode 메소드를재정의해야한다. 그렇지않으면해시알고리즘을기반으로동작하는HashMap, HashSet, Hashtable과같은컬렉션에서문제가생긴다. 문제가생기는이유를알려면해시가동작하는원리를이해해야한다. 해시가동작하는원리는아주간단하고일상생활에서많이볼수있다. 우리가많이쓰는주소록이바로해시를이용한것이다. 이주소록은이름의첫글자에따라페이지를구분하고설명을쉽게하기위해이주소록에기록한이름은유일하다고가정하자. 이주소록에이름이 이해일 이고연락처가 1230000 인정보를다음과같은순서로기록한다 < 그림7>. 1. 이름의첫글자가무엇인지알아낸다 ( 첫글자는 이 다.) 2. 이첫글자에해당하는주소록페이지를펼친다 ( 이 로시작하는이름을모아놓은페이지를펼친다.) 3. 주소록에이미같은이름이있는지확인한다. 같은이름이있다면고쳐쓰고같은이름이없다면연락처를추가한다 ( 이해일 이라는이름이없으니까새 < 그림8> 해시에서데이타를꺼내는과정주소록이바로해시테이블이고이름이연락처의키이고, 이름의첫글자가해시코드이다. 이름의첫글자를알아내는것이해시함수 (hash function) 가하는일이다. 034 ORACLE KOREA MAGAZINE

:: TECHNOLOGY :: Java에서 hashcode 메소드가바로해시함수역할을한다. 따라서, 컬렉션프레임워크의해시는키객체의hashCode 메소드로버킷을결정한다. 키객체가동등한데해시코드값이다르면어떻게될까? 동등한키객체를가진데이타가다른버킷에들어가기때문에동일한 (identical) 키객체를쓰지않는다면해시테이블에저장한데이타를찾아낼방법이없다. 따라서, equals 메소드를재정의했으면꼭hashCode 메소드를재정의해야한다. 해시의키로쓰일때문제가발생할수있기때문이다. 그렇다면, hashcode 메소드는어떻게만드는것이좋을까? < 그림 8> 을보면, 해시는버킷을찾을때는다루는데이타의개수와상관없이일정한성능을내지만, 한버킷안에서슬롯을찾을때는리스트의검색과같은성능을낸다. 따라서, 최대한균등하게해시코드를만들어내는것이좋다. 다음과같은hashCode 메소드는최악이다. public int hashcode() { return 31; 이hashCode 메소드는구현계약의두번째조항은만족하지만모든객체의해시코드값이같기때문에모두한버킷에들어간다. 이해시테이블은이제해시테이블이아닌 O(N) 의성능을보이는연결리스트일뿐이다! 모든객체가서로다른해시코드를가진다면성능은가장좋겠지만, 현실적으로이런해시코드를만들어내는것은거의불가능하고메모리소모도많으므로적절한지점에서타협해야한다. 모든알고리즘이그렇듯이해시도성능과메모리사이에서적절한균형을이뤄야한다. 간단하면서도거의균등한해시코드값을만드는 hashcode 메소드구현법 ( 이방법은 Joshua Bloch의 [Effective Java Programming Language Guide](Addison-Wesley) 에나온것을인용했다.) 은다음과같다. 1. result라는 int 타입변수에 0이아닌상수값 ( 예를들면, 17과같은값 ) 을저장한다. 2. 객체의주요필드 (equals 메소드에서비교하는필드 ) 인모든 f들에대해각각다음작업을수행한다. a. 각필드의해시코드c를다음과같이계산한다. i. f가boolean인경우,(f? 0 : 1) 를계산한다. ii. f가byte, char, short, int인경우,(int)f를계산한다. iii. f가long인경우,(int)(f ^ (f>>>32)) 를계산한다. iv. f가float인경우, Float.floatToIntBits(f) 를계산한다. v. f가 double인경우, Double.doubleToLongBits(f) 를계산한후, 2.a.iii와같이계산한다. vi. f가객체참조이고, 이클래스의 equals 메소드에서비교하는대상이라면, f의 hashcode 메소드의리턴값을계산한다. 만약, 더복잡한비교가필요다면 f를표준형식 (canonical form) 으로바꿔서해시코드를계산한다. 만약, f가null이면 0으로계산한다 ( 아무상수나괜찮지만 0으로계산하는것이관례이다.) vii. f가배열인경우, 각구성요소하나하나를하나의필드처럼처리한다. 위의규칙을적용하여배열의주요구성요소의해시코드를모두계산한후2.b 단계에따라한번더계산한다. b. 2.a에따라계산한해시코드인c와 1에서정의한result를다음과같이더한다. result = 37*result + c; c. result를리턴한다. equals 메소드에서비교하지않는필드는해시코드를계산할때빼야한다. 만약이필드들을빼지않으면 hachcode 메소드구현계약의두번째조항을어길가능성이커진다. 위비법에서쓴17과37은임의의소수이다. 정확한이유는밝혀지지않았지만, 홀수인소수를쓰면해시코드값이균등해진다고한다. 이방법에따라전화번호를표현하는 PhoneNumber 클래스의 hashcode 메소드를다음과같이구현할수있다 ( 이예제는 Joshua Bloch 의 [Effective Java Programming Language Guide](Addison-Wesley) 에서나온것을인용했다.) public final class PhoneNumber { private final short areacode; private final short exchange; private final short extension; public PhoneNumber(int areacode, int exchange, int extension) { rangecheck(areacode, 999, "area code"); rangecheck(exchange, 999, "exchange"); rangecheck(extension, 9999, "extension"); this.areacode = (short) areacode; this.exchange = (short) exchange; this.extension = (short) extension; private static void rangecheck(int arg, int max, String name) { if (arg < 0 arg > max) throw new IllegalArgumentException(name + ":" + arg); public boolean equals(object o) { if (o == this) return true; if (!(o instanceof PhoneNumber)) 2003 AUTUMN 035

:: TECHNOLOGY :: int len = count; for (int i = 0; i < len; i++) { h = 31*h + val[off++]; hash = h; return h; return false; PhoneNumber pn = (PhoneNumber)o; // extention, exchange, areacode 필드가 equals의비교대상이다. // 따라서, 해시코드값도이필드들을써서계산해야한다. return pn.extension == extension && pn.exchange == exchange && pn.areacode == areacode; public int hashcode() { int result = 17; result = 37*result + areacode; result = 37*result + exchange; result = 37*result + extension; return result; 이방식은 JDK 1.4 배포판에서실제로쓰고있는방식으로다음은 java.lang.string의hashcode 메소드이다. public int hashcode() { int h = hash; if (h == 0) { int off = offset; char val[] = value; 이메소드를보면String 객체를구성하는모든문자가해시코드값계산에쓰인다는것을알수있다. JDK 1.2에서는성능을높이려고처음16개문자로만해시코드값을계산했는데, URL과같이계층구조를가지는문자열들을다룰때심각한문제가있었다. 예를들어, http://java.sun.com/j2se, http://java.sun.com/j2ee과같이 http://java.sun.com으로시작하는모든url은처음16자가같기때문에같은해시코드값을가진다. String 객체는불변객체이고해시코드계산비용이많기때문에 hashcode 메소드가처음호출될때해시코드값을계산하는늦은초기화기법을쓴것도눈여겨보기바란다. 앞에제시한해시코드구현방식은간단하긴하지만성가신작업이고개선의여지도있다. 앞으로나올 JDK 배포판에서는모든객체의해시코드값을구해주는유틸리티메소드가제공되길기대해보자. 객체의순서를어떻게정할까? 해야할일은많은데자원은제한되어있기때문에해야할일의우선순위를결정해야할경우가많다. 이처럼순서를결정하는일은실제세상에서자주수행하는작업이다. 따라서, 순서를다루는코드를만드는일도굉장히많다. 순서란무엇일까? 순서란어떤기준에따라정한선후관계이다. 따라서, 순서에는반드시기준이라는것이따라다닌다. 순서를정할때쓰는기준에는두종류가있다. 자연수는 1, 2, 3처럼크기를기준으로, 날짜는 2003년8월3일, 2003년8월4일, 2003년8월5일처럼시간을기준으로, 문자열은 Java, 이해일, 홍길동처럼사전식배열을기준으로순서를결정하는것이자연스럽다. 이렇게결정한순서를 자연스러운순서 (natural ordering) 라고한다. 하지만, 상식을벗어나는순서가필요하거나순서를생각하기힘든추상개념의순서를정해야한다면사용자맞춤순서를써야한다. 예를들어, 문자열을사전순서가아닌문자열길이를기준으로순서를정할수도있다. 이런순서가바로 맞춤순서 (custom ordering) 이다. Java에서는 Comparable과 Comparator라는인터페이스로객체순서결정을지원한다. 이두인터페이스를쓰면 Arrays.binarySearch, Arrays.sort, Collections.binarySearch, Collections.max, Collections.min, Collections.sort와같은순서를다루는유틸리티메소드와TreeMap, TreeSet과같이자동정렬맵이나집합을다루는컬렉션을마음대로쓸수있다. 036 ORACLE KOREA MAGAZINE

:: TECHNOLOGY :: Comparable 인터페이스 Comparable 인터페이스는객체들사이의자연스러운순서를결정할때쓴다. 이인터페이스에는compareTo라는메소드하나밖에없다. 이메소드의시그니처는다음과같이아주단순하다. public int compareto(object o) 이메소드는this 객체와인자로받은o를비교하여this가o보다순서가앞서면양수를, 순서가같으면 0을, 순서가뒤지면음수를리턴한다. 이렇게간단한작업만해주면 Arrays, Collections, TreeMap, TreeSet이제공하는유용한기능을모두쓸수있다니정말즐거운일이아닌가? 하지만, 이때도몇가지구현계약을지켜야한다. compareto의구현계약은다음과같다 (java.lang.comparable의 compareto 메소드명세 ). this 객체와인자로받은객체사이의순서를비교한다. this 객체가인자로받은객체보다크면양의정수를, 같으면 0을, 작으면음의정수를리턴한다. 만약, 인자로받는객체의타입이 this 객체와비교할수없는것이라면, ClassCastException을던진다. 다음설명에서 sgn(expression) 이라는표현은수학의 signum 함수와같은것이다. 이함수는표현식 (expression) 의값이음수면 -1, 영이면 0, 양수면 1 을리턴한다. 1. 모든 x, y에대해sgn(x.compareto(y)) == -sgn(y.compareto(x)) 임을보장해야한다 ( 이식은 y.compareto(x) 가예외를던질때만 x.compareto(y) 도같은예외를던져야한다는뜻이다.) 2. 추이성관계를보장해야한다. (x.compareto(y) > 0 && y.compareto(z) > 0) 라는것은x.compareTo(z) > 0라는뜻이다. 3. x.compareto(y) == 0이면, 모든 z에대해sgn(x.compareto(z)) == sgn(y.compareto(z)) 라는뜻이다. 4. 엄격한요구사항은아니지만,(x.compareTo(y) == 0) == (x.equals(y)) 인것이좋다. 만약, 어떤클래스가 Comparable 인터페이스를구현하면서이조항을지키지않는다면반드시다음과같이이사실을알려주는것이좋다. 주의 : 이클래스의자연스러운순서는equals 메소드와맞지않는다. compareto 메소드의구현계약은 equals 메소드와비슷하지만 compareto 메소드는다른클래스의객체를비교할수없다. 다른클래스의객체를비교하려고하면 ClassCastException이발생한다. 언어차원에서강제로서로다른클래스의객체를비교하지못하게할수는없지만, JDK 1.4 배포판부터는Java 플랫폼라이브러리의모든클래스는다른클래스의객체를비교하는compareTo 메소드를제공하지않는다. 어떤클래스가hashCode 메소드의구현계약을어겼을때해시알고리즘을쓰는컬렉션이제대로동작하지않을수있는것처럼, compareto 메소드의구현계약을어긴다면객체의순서를비교하는 TreeSet, TreeMap과 같은자동정렬컬렉션이나Collections, Arrays와같이정렬과검색을처리하는유틸리티메소드를제공하는클래스들이제대로동작하지않을것이다. 1번, 2번, 3번조항은당연한것이기때문에자세히살펴보지는않겠지만 compareto 메소드는동등성검사를내포하고있으므로 equals 메소드의구현계약이따라야하는계약조항 ( 반사성, 대칭성, 추이성, null과다름 ) 과제약을그대로따라야한다는것을기억해야한다. equals 메소드와같이구체클래스를상속받아새로운필드를추가하면서구현계약을지키는 compareto 메소드를만드는것은불가능하다. 이문제는equals 메소드와똑같은방식으로피해야한다. 상속대신에이클래스의인스턴스에대한참조를멤버필드로가지는클래스를만들고이필드를리턴하는뷰메소드를제공하는것이좋다. 4번조항은필수조항이라기보다는강력한권고사항이다. compareto 메소드와 equals 메소드의동등성검사결과는같은것이좋은데, 이것을어기면컬렉션을쓸때문제가생길수있다. 보통컬렉션은equals 메소드를써서구성요소의동등성을비교하지만, 자동정렬컬렉션은 compareto 메소드를써서구성요소의동등성을비교하기때문에일관성없는결과가나올수있다. BigDecimal 클래스의 compareto 메소드와 equals 메소드의결과는일치하지않는다. HashSet 인스턴스를하나생성하여 new BigDecimal( 1.0 ) 과 new BigDecimal( 1.00 ) 을넣으면이집합은두개의원소를가진다. BigDecimal 클래스의 equals 메소드는 new BigDecimal( 1.0 ) 과 new BigDecimal( 1.00 ) 을 동등하다 고판단하지않기때문이다. 하지만, HashSet 대신에자동정렬컬렉션인 TreeSet에 new BigDecimal( 1.0 ) 과 new BigDecimal( 1.00 ) 을넣으면단하나의원소만가진다. TreeSet은 compareto 메소드를쓰고 BigDecimal의 compareto 메소드는두 BigDecimal 인스턴스를 동등하다 고판단하기때문이다. 전화번호를표현하는PhoneNumber클래스가자연스러운순서를지원하게만들어보자 ( 이. 예제는 Joshua Bloch의 [Effective Java Programming Language Guide](Addison-Wesley) 에나온것을인용했다.) public final class PhoneNumber implements Comparable { private final short areacode; private final short exchange; private final short extension; public PhoneNumber(int areacode, int exchange, int extension) { rangecheck(areacode, 999, "area code"); rangecheck(exchange, 999, "exchange"); rangecheck(extension, 9999, "extension"); this.areacode = (short) areacode; this.exchange = (short) exchange; this.extension = (short) extension; 2003 AUTUMN 037

:: TECHNOLOGY :: return 0; private static void rangecheck(int arg, int max, String name) { if (arg < 0 arg > max) throw new IllegalArgumentException(name + ":" + arg); public boolean equals(object o) { if (o == this) return true; if (!(o instanceof PhoneNumber)) return false; PhoneNumber pn = (PhoneNumber)o; return pn.extension == extension && pn.exchange == exchange && pn.areacode == areacode; public int hashcode() { int result = 17; result = 37*result + areacode; result = 37*result + exchange; result = 37*result + extension; return result; public int compareto(object o) { PhoneNumber pn = (PhoneNumber)o; 여기서 compareto 메소드가리턴값으로양수, 0, 음수만정의했지수의크기를정의하지않았다는점과 rangecheck 메소드로전화번호를구성하는번호가절대음수가될수없다는불변조건이보장되므로, 다음과같이좀더빠르고간단하게compareTo 메소드를구현할수있다. public int compareto(object o) { PhoneNumber pn = (PhoneNumber)o; // 지역번호를비교한다. int areacodediff = areacode - pn.areacode; if (areacodediff!= 0) return areacodediff; // 지역번호가동일하면국번을비교한다. int exchangediff = exchange - pn.exchange; if (exchangediff!= 0) return exchangediff; // 마지막으로내선번호를비교한다. return extension - pn.extension; 이방법은비교하는필드값이음수가아니라는불변조건이보장될때만쓸수있다는것을다시한번강조한다. 만약, 필드값이음수가된다면찾아내기아주어려운버그로남을것이다. // 전화번호는지역번호- 국번- 내선번호순서로구성된다. 따라서이순서대로비교해야한다. // 지역번호를비교한다. if (areacode < pn.areacode) return -1; if (areacode > pn.areacode) return 1; Comparator 인터페이스 Comparator 인터페이스는맞춤순서를정하고싶을때쓴다. 이인터페이스에는compare라는메소드하나밖에없다. public int compare(object o1, Object o2) // 지역번호가동일하면교환번호를비교한다. if (exchange < pn.exchange) return -1; if (exchange > pn.exchange) return 1; // 지역번호와교환번호가동일한경우에는내선번호를비교한다. if (extension < pn.extension) return -1; if (extension > pn.extension) return 1; 이메소드는서로비교해야할객체두개 (o1, o2) 를인자로받아서o1이 o2보다순서가앞서면양수를, 같으면0을, 뒤지면음수를리턴한다. Comparator는GoF의전략패턴에나오는전략클래스로정렬순서를결정하는알고리즘을제공하는역할을한다. 예를들어, Collections.sort 메소드중에서Comparator를쓰는것의시그니처를살펴보자. public static void sort(list l, Comparator c); // 모든필드는동일하다. 이메소드는정렬에필요한비교알고리즘을 Comparator 타입의인자 038 ORACLE KOREA MAGAZINE

:: TECHNOLOGY :: 로받는다. 예를들어, 문자열길이순서대로정렬할필요가있다고하면다음과같이Comparator 객체를생성해서제공하면된다 ( 이예제는 Joshua Bloch의 [Effective Java Programming Language Guide](Addison- Wesley) 에나온것을인용했다.) public class StringLengthComparator implements Comparator { private StringLengthComparator() { public static final StringLengthComparator INSTANCE = new StringLengthComparator(); public int compare(object o1, Object o2) { String s1 = (String)o1; String s2 = (String)o2; return s1.length() - s2.length(); // 사용법. 문자열리스트l을문자열길이순서대로정렬한다. Collections.sort(l, StringLengthComparator.INSTANCE); 하지만, 보통 Comparator는익명클래스이거나중첩클래스로구현한다. // 익명클래스로구현하기 ( 길이를기준으로순서를정한다.) Arrays.sort(stringArray, new Comparator() { public int compare(object o1, Object o2) { String s1 = (String)o1; String s2 = (String)o2; return s1.length() - s2.length(); ); String s1 = (String) o1; String s2 = (String) o2; int n1=s1.length(), n2=s2.length(); for (int i1=0, i2=0; i1<n1 && i2<n2; i1++, i2++) { char c1 = s1.charat(i1); char c2 = s2.charat(i2); if (c1!= c2) { c1 = Character.toUpperCase(c1); c2 = Character.toUpperCase(c2); if (c1!= c2) { c1 = Character.toLowerCase(c1); c2 = Character.toLowerCase(c2); if (c1!= c2) return c1 - c2; return n1 - n2; Comparator는인자로받은두객체를비교하고 Comparable이 this 객체와인자로받은객체를비교한다는점만빼면 Comparator는 Comparable과같다. 따라서, Comparator의구현계약은 Comparable의구현계약과같다. Comparable이나 Comparator를쓰면객체에원하는순서를줄수있다. 자연스러운순서가필요하면Comparable을쓰고맞춤순서가필요하면 Comparator를쓰면된다. // 중첩클래스로구현하기 ( 대소문자를구분하지않고순서를정한다.) // java.lang.string public class String implements Comparable, { public static final Comparator CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator(); private static class CaseInsensitiveComparator implements Comparator, java.io.serializable { public int compare(object o1, Object o2) { 참고문헌 Object-Oriented Analysis and Design, Grady Booch, Addison-Wesley Design Patterns, Erich Gamma et al., Addison-Wesley Effective Java Programming Language Guide, Joshua Bloch, Addison-Wesley Practical Java Programming Language Guide, Peter Haggar, Addison-Wesley Java Pitfalls, Michael C. Daconta et al., Wiley Java Rules, Duglas Dunn, Addison-Wesley Java Tutorial, http://java.sun.com/docs/books/tutorial/ JDK 1.4.2 SDK Document, http://java.sun.com/j2se/1.4.2/docs/api/ 2003 AUTUMN 039