네이버를 만든 기술, 읽으면서 배운다 - 자바편

60

Upload: gahee-yoon

Post on 22-Jul-2016

252 views

Category:

Documents


12 download

DESCRIPTION

 

TRANSCRIPT

Page 1: 네이버를 만든 기술, 읽으면서 배운다 - 자바편
Page 2: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

지은이 강경태, 강운덕, 구태진, 김민수, 김택수, 박세훈, 송기선, 이상민, 정상혁, 최동순

테크니컬 에디터 박춘권, 김붕미, 김지현, 원혜정, 유영경, 장주혜

펴낸이 박찬규 엮은이 이대엽 디자인 북누리 표지디자인 아로와 & 아로와나

펴낸곳 위키북스 전화 031-955-3658, 3659 팩스 031-955-3660

주소 경기도 파주시 교하읍 문발리 파주출판도시 535-7 세종출판벤처타운 #311

가격 20,000 페이지 304 책규격 172 x 235 x 15mm

초판 발행 2015년 3월 5일

ISBN 978-89-98139-78-0 (93000)

등록번호 제406-2006-000036호 등록일자 2006년 05월 19일

홈페이지 wikibook.co.kr 전자우편 [email protected]

이 도서의 국립중앙도서관 출판시도서목록 CIP는

서지정보유통지원시스템 홈페이지(http://seoji.nl.go.kr)와

국가자료공동목록시스템(http://www.nl.go.kr/kolisnet)에서 이용하실 수 있습니다.

CIP제어번호 CIP2015003859

Page 3: 네이버를 만든 기술, 읽으면서 배운다 - 자바편
Page 4: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

추천사

자바가 나온 지 벌써 20년이 지났다. 지금 보면 별것 아니지만 웹 브라우저의 자바 애플릿에서 이미지가

움직이는 것을 보고 정말 대단한 기술이라 생각했는데, 이제는 자바가 탑재된 휴대폰에서 3D 게임까지

즐기는 시대에 접어들었다. 자바는 20년 동안 버전업을 거치면서 성능도 향상됐고 언어의 성숙도도 비약

적으로 발전했다.

자바는 JVM에서 작동하는 객체지향 프로그래밍 언어로 복잡한 포인터 연산을 없애고 이를 JVM에 일임

했다. 이는 자바의 강점이자 약점인 부분으로 개발자가 메모리를 신경 쓰지 않고 프로그래밍할 수 있게 했

지만 오히려 메모리 누수 현상을 일으켰다. 아울러 JVM 성능 문제를 해결하려 인터프리터와 컴파일 방식

의 장점을 가져오기도 했다. 몇 가지 약점에도 불구하고 자바는 자신만의 고유한 강점으로 웹 애플리케이

션 서버와 함께 생태계를 꾸준히 확장해 왔다.

자바는 프로그래밍 언어와 API 측면에서 꾸준히 진화해왔다. 최근 자바 8로의 변화는 혁명적이라고 할 수

있을 정도로 문법과 API에 큰 변화가 있었다. 늦은 감은 있지만 람다 표현식을 도입했고 그 외에 컬렉션

API, 날짜 API, 동시성 개선 등을 통해 자바는 더욱 성숙된 프로그래밍 언어로 나아가고 있다.

시중에 자바 입문서는 많이 나와 있지만 자바의 API와 내부 작동 구조를 설명하는 책은 그리 많지 않다. 이

책은 최신 자바 8의 특징부터 이와 관련된 자바 주요 API의 작동 원리, JVM의 구조와 명세, 가비지 컬렉션

작동 원리 등 자바 개발자가 중고급 개발자로 성장하는 데 필수적인 내용으로 구성돼 있다. 무엇보다도 네

이버의 현업 개발자들이 실제로 네이버 서비스를 개발하면서 겪은 자바 관련 기술 경험을 모은 책이어서

알기 쉬우면서도 전문성이 깊이 느껴지는 책이다.

옥상훈(okgosu)

네이버 D2 에반젤리스트

제4대 한국자바개발자협의회 회장

4

Page 5: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

머리말

이 책은 네이버 기술 블로그인 헬로월드(http://helloworld.naver.com)에 올라왔던 자바와 관련된 글을

엄선해서 엮은 책이다. 헬로월드는 네이버 사내 기술 지식을 외부 개발자에게 공유할 목적으로 2011년

12월에 개설된 블로그로, 프런트엔드, 모바일, 데이터베이스, 성능, 보안 등 다양한 영역에 걸쳐 네이버 개

발자들이 축적한 개발 지식과 문제 해결 경험, 실험 결과가 꾸준히 공개되고 있다.

헬로월드에 올라온 글을 모아 책으로 내자는 의견이 여러 번 나왔으나 쉽지 않은 일이었다. 얼핏 생각하면

이미 공개된 글을 단순히 모으기만 하면 책이 될 것 같았다. 하지만 워낙 다양한 기술 영역을 다루고 있어

책으로 엮을 주제를 선정하는 것부터 고민이었다. 여러 글을 모아 검토하고 다양한 의견을 거쳐 선정한 주

제가 ‘자바’다. 국내 소프트웨어 개발 환경에서 자바 개발의 비중이 높고 다양한 언어와 플랫폼을 사용하

는 네이버에서도 자바 개발은 개발의 큰 축을 담당한다. 그래서 우리가 전달하는 지식이 독자에게 도움이

될 여지도 크다고 생각해 자바 개발자가 관심이 있을 주제의 글을 모아서 정리했다. 몇 년 동안 내부에서

만 공개됐던 글도 선별하고 다듬어 실었다.

다양한 저자가 각기 다른 시기에 쓴 글이라 주제를 선정한 뒤에도 이를 정리하는 과정이 쉽지 않았다. 작

성한 지 오래돼 최신 정보를 반영하지 못하는 글은 저자에게 부탁해 내용을 보강해야 했다. 일부 글에 자

바 8 관련 내용이 들어간 것도 이 덕분이다. 또한 차례대로 글을 읽을 독자를 위해 글마다 제각각이던 용

어도 일관되게 통일했다.

이 책에 실린 글은 서로 독립적이지만 전체적인 흐름을 따라 읽을 수 있게 글을 분류하고 배치했다. 자바

의 표면인 API부터 시작해 JVM의 더 깊은 곳을 다루고, 그 후에는 JVM과 다른 영역과의 연결을 다루도록

구성했다.

5

Page 6: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

1장에서는 자바의 문법이나 라이브러리의 불편한 점, API와 라이브러리를 사용할 때 조심할 점, JDK에서

개선된 내용 등을 다룬다.

2장에서는 애플리케이션에 문제가 생겼을 때 오류 스택 분석에 필요한 지식과 JVM이 남기는 정보를 분

석하는 방법, JVM 자체의 오류에 대처한 경험 등을 정리했다.

3장에서는 평소에는 문제가 잘 드러나지는 않지만 시스템의 성능과 안정성을 개선하기 위해 깊이 알아야

하는 영역인 가비지 컬렉션에 관한 글을 모았다.

4장에서는 서버 애플리케이션의 외부 요소 중 핵심이라 할 수 있는 데이터베이스 연결 설정을 분석한 글

을 모았다.

이 책은 네이버의 주요 서비스 개발 조직과 웹플랫폼개발랩, 생산성혁신랩 등 기술 전문 조직의 개발자들

이 문제를 해결하고 고민하며 얻은 지식, 서비스를 운영하면서 겪은 경험이 바탕이 됐다. 자바 라이브러리

를 설계하거나 사용자가 많은 시스템에서 발생하는 문제를 예방하고 해결하는 등 자바의 더 깊은 곳을 들

여다보는 독자에게 네이버 개발자의 지식과 경험이 더욱 와 닿기를 기대한다.

앞으로 자바 10, 자바 11이 나오고 시간이 흐르면 더 최신 정보를 담은 책이 나올 것이다. 그래도 이 시대

의 자바 기술과 네이버에서 일한 개발자의 노력을 담은 이 책이 타임캡슐이 돼 오랜 시간이 지난 후에도

누군가의 책장에 꽂혀 있으면 좋겠다. 그것이 인터넷에 있던 글을 종이로 옮긴 가장 큰 의미가 아닐까 생

각한다.

머리말

6

Page 7: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

회사의 내외부에 기술을 공유하는 일을 이끌어 주신 네이버의 전현직 조직장님들 덕분에 이 책에 실린 글

이 나올 수 있었습니다. 송창현 님, 김태웅 님, 김성관 님, 진은숙 이사님, 김정민 이사님, 이승배 이사님,

박종빈 랩장님, 유석문 이사님께 감사드립니다.

실무에서는 개발자 지원 프로그램인 ‘D2’의 에반젤리스트분들과 사내 기술 잡지 The Platform 편집진의

공이 가장 큽니다. 출판에 부정적인 의견이 대부분일 때 이를 적극적으로 추진해 주신 옥상훈 님이 아니었

다면 이 책은 나오지 못했습니다. 헬로월드 블로그와 사내 세미나를 통해 기술 공유 문화를 촉진하고 계신

조한용 님 덕분에 이 책에 들어간 콘텐츠가 외부에도 공개될 수 있었습니다. 기사 섭외와 직접 기고로 몇

년 동안 사내 기술 잡지 The Platform을 이끌어 온 송기선 님 등 The Platform 편집진의 힘으로 이 책의 씨

앗이 뿌려졌습니다.

그리고 책을 제안하고 출판까지 이어 주신 위키북스의 박찬규 대표님, 책의 방향을 함께 고민하고 다양한

저자의 글을 다듬는 어려운 작업을 하신 기술문서팀의 박춘권 차장님께도 감사드립니다.

7

감사의 말

Page 8: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

목차

8

추천사 ............................................................................................................................... 4

머리말 ............................................................................................................................... 5

감사의 말 .......................................................................................................................... 7

1부 자바의 API 이해하기

01 / 자바의 날짜와 시간 API ................................................................................19

자바 클래스에 담긴 제도의 역사 ...................................................................................19

1582년 10월 4일의 다음 날은? ...............................................................................20

서울 1988년 5월 7일 23시의 1시간 후는? ..............................................................21

서울 1961년 8월 9일 23시 59분의 1분 후는? ........................................................23

협정세계시 2012년 6월 30일 23시 59분 59초의 2초 후는? .................................25

JDK 기본 날짜 클래스의 문제점 ..................................................................................27

불변 객체가 아니다 ...................................................................................................28

int 상수 필드의 남용 ................................................................................................30

헷갈리는 월 지정 ......................................................................................................30

일관성 없는 요일 상수 ...............................................................................................32

Date 클래스와 Calendar 클래스의 불편한 역할 분담 ..............................................32

오류에 둔감한 시간대 아이디 지정 ...........................................................................33

java.util.Date 하위 클래스의 문제 ...........................................................................33

자바의 개선된 날짜, 시간 API, Joda-Time ..................................................................34

JSR-310, 새로운 자바의 날짜 API................................................................................38

마치며 .............................................................................................................................42

Page 9: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

02 / 자바의 HashMap은 어떻게 작동하는가?........................................44

HashMap과 HashTable ...............................................................................................45

해시 분포와 해시 충돌 ...................................................................................................46

자바 8 HashMap에서의 separate chaining .............................................................50

해시 버킷의 동적 확장 ...................................................................................................53

보조 해시 함수 ................................................................................................................55

String 객체에 대한 해시 함수 .......................................................................................57

자바 7에서 String 객체에 대한 별도의 해시 함수 .......................................................58

마치며 .............................................................................................................................60

03 / 자바에서 외부 프로세스를 실행할 때 ..................................................61

java.lang.Process 클래스를 생성하는 2가지 방법 .....................................................61

외부 프로세스가 전달하는 스트림 처리 문제 ...............................................................63

특정 환경에서 발생하는 메모리 할당 실패 문제 ..........................................................66

fork-exec 호출이 문제가 되는 경우 ..........................................................................66

대처 방안 ...................................................................................................................68

Security Manager 클래스를 이용한 실행 제한 ..........................................................70

정책 설정 파일 위치와 실행 옵션 ..............................................................................71

정책 설정 방법 ..........................................................................................................72

마치며 .............................................................................................................................74

9

Page 10: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

10

04 / 람다가 이끌어 갈 모던 자바 .......................................................................75

컬렉션 처리.....................................................................................................................76

클래식 자바.....................................................................................................................77

기본 컬렉션 처리 API ................................................................................................77

자바의 기본 컬렉션을 개선한 라이브러리 .................................................................79

JVM에서 실행되는 다른 언어의 익명 함수 ..................................................................90

모던 자바 ........................................................................................................................97

클로저와 람다 표현식을 둘러싼 논란 ........................................................................97

람다 표현식과 Stream 인터페이스의 도입................................................................99

람다 표현식의 내부 구현 ........................................................................................ 107

애플리케이션 코드의 개선 ...................................................................................... 112

람다 표현식을 활용한 프레임워크 .......................................................................... 116

마치며 .......................................................................................................................... 120

2부 문제 분석과 사례

05 / JVM 이해하기 ................................................................................................... 123

가상 머신 ..................................................................................................................... 123

자바 바이트코드 .......................................................................................................... 125

현상 ........................................................................................................................ 125

원인 ....................................................................................................................... 126

목차

Page 11: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

클래스 파일 포맷 ......................................................................................................... 130

JVM 구조 ..................................................................................................................... 135

클래스 로더 ............................................................................................................ 136

런타임 데이터 영역 ................................................................................................ 138

실행 엔진 .............................................................................................................. 142

자바 SE 7의 JVM 명세 ............................................................................................... 146

String in switch Statements ............................................................................... 147

자바 SE 8의 JVM 명세 ............................................................................................... 149

마치며 .......................................................................................................................... 150

06 / 스레드 덤프 분석하기 .................................................................................. 151

자바 그리고 스레드 ..................................................................................................... 151

자바 스레드 배경 지식 ................................................................................................ 152

스레드 동기화 ........................................................................................................ 152

스레드 상태 ............................................................................................................ 152

스레드의 종류 ........................................................................................................ 153

스레드 덤프 생성 ......................................................................................................... 153

jstack을 이용하는 방법 .......................................................................................... 154

자바 VisualVM을 이용하는 방법 ........................................................................... 154

kill을 이용하는 방법 ............................................................................................... 155

스레드 덤프의 정보 ..................................................................................................... 156

스레드 덤프 유형별 패턴 ............................................................................................ 157

잠금을 소유하지 못하는 경우(BLOCKED) ............................................................. 157

교착 상태인 경우 ................................................................................................... 159

원격 서버로부터 메시지를 수신받기 위해 계속 대기하는 경우 ............................... 161

WAIT 상태에 있는 경우 ......................................................................................... 161

스레드 리소스를 정상적으로 정리하지 못하는 경우 ............................................... 162

11

Page 12: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

스레드 덤프를 이용한 문제 해결 사례 ....................................................................... 163

상황1: CPU 사용률이 비정상적으로 높을 때 ......................................................... 163

상황2: 실행 성능이 비정상적으로 느릴 때 ............................................................. 164

스레드 덤프 분석을 쉽게 만드는 코딩 ....................................................................... 165

스레드에 이름 부여하기 ......................................................................................... 165

MBean으로 더 자세한 정보 얻기 ........................................................................... 167

마치며 .......................................................................................................................... 168

07 / 자바 애플리케이션 분석을 위한 BTrace ........................................ 169

BTrace란? ................................................................................................................... 169

특정 메서드 호출 시의 스택 트레이스 확인 ............................................................... 171

실행 시간과 횟수 측정 ................................................................................................ 174

객체의 필드값 확인 ..................................................................................................... 178

BTrace의 제약사항 ..................................................................................................... 179

마치며 .......................................................................................................................... 180

08 / 하나의 메모리 누수를 잡기까지 ........................................................... 181

장애 징조 - 상반기의 장애 처리 일지 ........................................................................ 181

모니터링 개선 .............................................................................................................. 182

나 혼자 죽을 수 없다 .............................................................................................. 182

문자 메시지 스트레스 ............................................................................................. 183

문제 원인 찾기 ............................................................................................................. 184

문제 해결 ..................................................................................................................... 186

히스토그램 비교 ..................................................................................................... 186

12

목차

Page 13: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

힙 덤프 분석 ........................................................................................................... 187

마치며 .......................................................................................................................... 191

09 / 고맙다 JVM, 사과해라 JVM 크래시 ................................................. 193

JVM 오류 현상 ............................................................................................................ 194

JVM의 명백한 버그 ..................................................................................................... 194

버그 리포트와 릴리스 노트 ......................................................................................... 197

마치며 .......................................................................................................................... 199

3부 가비지 컬렉션

10 / 자바 가비지 컬렉션의 작동 과정 .......................................................... 201

가비지 컬렉션 과정 - Generational Garbage Collection .................................... 201

Young 영역의 구성 .................................................................................................... 204

Old 영역에 대한 가비지 컬렉션 ................................................................................. 206

Serial GC(-XX:+UseSerialGC) ............................................................................. 207

Parallel GC(-XX:+UseParallelGC) ....................................................................... 207

Parallel Old GC(-XX:+UseParallelOldGC) ......................................................... 208

CMS GC(-XX:+UseConcMarkSweepGC) .......................................................... 208

G1 GC ................................................................................................................... 209

마치며 .......................................................................................................................... 211

13

Page 14: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

11 / 가비지 컬렉션 모니터링 방법 ................................................................. 212

가비지 컬렉션 모니터링이란? .................................................................................... 212

가비지 컬렉션을 모니터링하는 방법은? .................................................................... 213

jstat .............................................................................................................................. 213

-verbosegc 옵션 ......................................................................................................... 219

VisualVM과 Visual GC .............................................................................................. 222

HPjmeter .................................................................................................................... 224

마치며 .......................................................................................................................... 225

12 / 가비지 컬렉션 튜닝 ........................................................................................ 226

가비지 컬렉션 튜닝을 꼭 해야 할까? ......................................................................... 226

Old 영역으로 넘어가는 객체의 수 최소화 .............................................................. 228

Major GC 시간 줄이기 .......................................................................................... 228

가비지 컬렉션의 성능을 결정하는 옵션 ..................................................................... 228

가비지 컬렉션 튜닝의 절차 ......................................................................................... 230

1. 가비지 컬렉션 상황 모니터링............................................................................. 230

2. 모니터링 결과 분석 후 가비지 컬렉션 튜닝 여부 결정 ....................................... 231

3. 가비지 컬렉션 방식과 메모리 크기 지정 ............................................................ 231

4. 결과 분석 ........................................................................................................... 231

5. 결과가 만족스러울 경우 전체 서버에 반영 및 종료 ............................................ 232

가비지 컬렉션 상황 모니터링 및 결과 분석하기 ....................................................... 232

가비지 컬렉션 방식 지정과 메모리 크기 지정 ........................................................... 233

가비지 컬렉션 방식 지정 ........................................................................................ 233

메모리 크기 지정 .................................................................................................... 235

14

목차

Page 15: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

가비지 컬렉션 튜닝 결과 분석 .................................................................................... 236

튜닝 사례 ..................................................................................................................... 237

튜닝 사례 1 ............................................................................................................ 237

튜닝 사례 2 ............................................................................................................ 240

마치며 .......................................................................................................................... 243

13 / 자바의 Reference 클래스와 가비지 컬렉션 ................................ 244

가비지 컬렉션과 접근 가능성 ..................................................................................... 245

soft reference, weak reference, phantom reference ........................................ 247

Reference 클래스와 접근 가능성 .............................................................................. 248

접근 가능성의 세기 ..................................................................................................... 249

softly reachable과 SoftReference .......................................................................... 251

weakly reachable과 WeakReference .................................................................... 252

ReferenceQueue ....................................................................................................... 252

phantomly reachable과 PhantomReference ...................................................... 253

마치며 .......................................................................................................................... 255

14 / 가비지 컬렉션과 Statement Pool ..................................................... 256

HTTP 요청에서 응답까지 가비지 컬렉션 발생 과정 ................................................. 256

Statement 객체의 크기 ............................................................................................. 257

Statement Pool의 캐시 적중률이 Major GC에 미치는 영향 ................................ 258

마치며 .......................................................................................................................... 259

15

Page 16: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

15 / 아파치 MaxClients와 톰캣의 Major GC ..................................... 260

서비스 운영 환경 ......................................................................................................... 260

stop the world와 503 오류 ...................................................................................... 261

MaxClients와 backlog .............................................................................................. 262

MaxClients 설정 계산 방법 ....................................................................................... 264

마치며 .......................................................................................................................... 265

4부 데이터베이스 연결 설정

16 / JDBC의 타임아웃 이해하기 ..................................................................... 267

어느 날 발생한 DDoS 공격, 그 뒤로 먹통이 된 WAS .............................................. 267

왜 JDBC 드라이버에 대해 알아야 하는가? ............................................................... 268

WAS와 DBMS의 통신 시 타임아웃 계층 .................................................................. 269

트랜잭션 타임아웃이란? ........................................................................................ 271

Statement 타임아웃이란? .................................................................................... 272

JDBC 드라이버의 Statement 타임아웃 작동 방식 .................................................. 272

JDBC 드라이버의 소켓 타임아웃이란? ..................................................................... 277

운영체제 수준의 소켓 타임아웃 설정 ........................................................................ 279

마치며 .......................................................................................................................... 280

16

목차

Page 17: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

17 / Commons DBCP 이해하기 ................................................................... 281

버전 선택과 속성 설정 방법........................................................................................ 282

JDK 버전과 Commons DBCP 버전 ....................................................................... 282

Commons DBCP 속성 설정 .................................................................................. 283

커넥션의 개수 .............................................................................................................. 284

커넥션 풀의 저장 구조 ............................................................................................ 284

커넥션 개수 관련 속성 ............................................................................................286

커넥션을 얻기 전 대기 시간........................................................................................ 288

TPS(transaction per seconds)............................................................................. 288

TPS와 커넥션 개수와의 관계 .................................................................................. 289

적절한 maxWait 값은? ......................................................................................... 292

커넥션의 검사와 정리 ................................................................................................. 293

유효성 검사 쿼리 설정 ............................................................................................ 293

Evictor 스레드와 관련된 속성 ................................................................................ 295

statement pooling 관련 옵션 .................................................................................. 297

기본값을 그대로 쓰기를 권장하는 옵션 ..................................................................... 297

removeAbandoned 옵션 ..................................................................................... 297

defaultAutoCommit 속성 .................................................................................... 298

마치며 .......................................................................................................................... 298

17

Page 18: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

자바의 API 이해하기

1부

01 자바의 날짜와 시간 API

02 자바의 HashMap은 어떻게 작동하는가?

03 자바에서 외부 프로세스를 실행할 때

04 람다가 이끌어 갈 모던 자바

1부에서는 개발자가 접하는 자바의 API와 그 내부 구현을 살펴본다. 이를 통해 JDK(Java development kit)의

발전 과정과 거기에 기여한 오픈소스 라이브러리의 작동 원리, API를 사용할 때의 주의점 등을 파악할 수 있다.

Page 19: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

01. 자바의 날짜와 시간 API 19

자바의 기본 SDK(software development kit)인 JDK(Java development kit)에서 날짜와

시간을 다루는 java.util.Date 클래스와 java.util.Calendar 클래스는 사용하기 불편한 것으

로 악명이 높다. 이를 답답하게 여긴 사람들이 이 클래스를 대체하려고 Joda-Time 같은 오픈

소스 라이브러리를 만들기도 했다. 다행히 JDK 8은 개선된 날짜와 시간 API를 제공한다.

이 글에서는 자바의 날짜와 시간 API의 문제점을 짚어보고, 여러 오픈소스 라이브러리와

JDK 8에서 문제점이 어떻게 개선됐는지 살펴보겠다.

자바 클래스에 담긴 제도의 역사

우선 java.util.Calendar 클래스와 java.util.Date 클래스로 간단한 예제를 작성해 보겠다. 평

범한 예제보다는 날짜와 시간 계산이 사회 제도나 과학과 복잡하게 얽혀 있음을 보여 주는 예

제를 만들었다.

정상혁

01자바의 날짜와

시간 API

Page 20: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

20 1부 _ 자바의 API 이해하기

1582년 10월 4일의 다음 날은?

예제 1-1에서는 UTC(universal time coordinated, 협정세계시) 시간대를 기준으로 1582년

10월 4일에 하루를 더한 날짜가 10월 5일인 것을 테스트한다. JUnit 프레임워크1와 Fest

Assertions 라이브러리2를 활용했다.

예제 1-1 1582년 10월 4일의 다음 날짜 구하기

public class OldJdkDateTest {

@Test

public void shouldGetAfterOneDay() {

TimeZone utc = TimeZone.getTimeZone("UTC");

Calendar calendar = Calendar.getInstance(utc);

calendar.set(1582, Calendar.OCTOBER , 4);

String pattern = "yyyy.MM.dd";

String theDay = toString(calendar, pattern, utc);

assertThat(theDay).isEqualTo("1582.10.04");

calendar.add(Calendar.DATE, 1);

String nextDay = toString(calendar, pattern, utc);

assertThat(nextDay).isEqualTo("1582.10.05");

}

private String toString(Calendar calendar, String pattern, TimeZone zone) {

SimpleDateFormat format = new SimpleDateFormat(pattern);

format.setTimeZone(zone);

return format.format(calendar.getTime());

}

...

}

이 테스트는 실패한다. 예제 1-1에서 계산한 1582년 10월 4일의 다음 날은 1582년 10월 15

일이다. 따라서 마지막 줄을 다음과 같이 고쳐야 테스트를 통과한다.

assertThat(nextDay).isEqualTo("1582.10.15");

1 http://junit.org

2 https://github.com/alexruiz/fest-assert-2.x/wiki

Page 21: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

01. 자바의 날짜와 시간 API 21

1582년에서 실종된 10일은 그레고리력을 처음 적용하면서 율리우스력에 의해 그동안 누적

된 오차를 교정하기 위해 건너뛴 기간이다. 태양의 황경이 0도가 되는 춘분이 1582년에는 10

일 정도 어긋났다. 교황 그레고리우스 13세는 더 정교한 그레고리력을 1582년 10월 15일에

처음 적용했고, 10월 5일에서 14일의 날짜는 그해 달력에서 제외했다. 율리우스력은 4년마

다 윤년을 두지만, 그레고리력에서는 4년마다 윤년을 두되 매 100번째 해는 윤년이 아니고,

매 400번째 해는 윤년이라는 차이가 있다.

예제 1-1에서 Calendar.getInstance() 메서드는 java.util.GregorianCalendar 클래스의

인스턴스를 반환한다. GregorianCalendar 클래스는 그레고리력과 율리우스력을 같이 구현

하고 있고, setGregorianChange() 메서드로 두 역법의 전환 시점을 지정할 수 있다. 그런데

AD 4년의 3월 1일 이전에는 윤년을 불규칙하게 뒀기 때문에 GregorianCalendar 클래스로

구한 날짜는 정확하지는 않다. 이런 설명은 GregorianCalendar 클래스의 API 문서3에 나와

있다.

Calendar.getInstance( ) 메서드는 GregorianCalendar 외에도 Locale 정보에 따라

JapaneseImperialCalendar, BuddhistCalendar 등도 반환한다. 역사와 천문학이 복합적으

로 담긴 클래스라 할 만하다.

서울 1988년 5월 7일 23시의 1시간 후는?

예제 1-2에서는 ‘Asia/Seoul’ 시간대에서 ‘1988.05.07 23:00’의 1시간 후가 ‘1988.05.08

00:00’임을 테스트한다.

예제 1-2 서울 1988년 5월 7일 23시의 1시간 후 구하기

@Test

public void shouldGetAfterOneHour() {

TimeZone seoul = TimeZone.getTimeZone("Asia/Seoul");

Calendar calendar = Calendar.getInstance(seoul);

calendar.set(1988, Calendar.MAY , 7, 23, 0);

String pattern = "yyyy.MM.dd HH:mm";

String theTime = toString(calendar, pattern, seoul);

3 http://docs.oracle.com/javase/7/docs/api/java/util/GregorianCalendar.html

Page 22: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

22 1부 _ 자바의 API 이해하기

assertThat(theTime).isEqualTo("1988.05.07 23:00");

calendar.add(Calendar.HOUR_OF_DAY, 1);

String after1Hour = toString(calendar, pattern, seoul);

assertThat(after1Hour).isEqualTo("1988.05.08 00:00");

}

이 테스트도 실패한다. 이상하게도 1시간 후는 5월 8일 새벽 1시다. 이는 그 시기에 서울에

적용된 일광절약시간제(daylight saving time), 즉 서머타임 때문이다. 서머타임이 시작되

는 시점에서는 1시간을 건너뛴다. 해당 시간대가 서머타임 적용 시간대인지는 TimeZone.

inDaylightTime() 메서드로 확인할 수 있다. 예제 1-2의 마지막 두 줄을 다음과 같이 바꾸

면 테스트를 통과하고, 이 시간대에 일어난 일을 좀 더 잘 설명할 수 있다.

assertThat(seoul.inDaylightTime(calendar.getTime())).isTrue();

assertThat(after1Hours).isEqualTo("1988.05.08 01:00");

우리나라는 1988년 이후로는 서머타임을 실시하지 않지만 2009년에 정부가 적극적으로 도

입을 검토하기도 했다.4 미국은 2007년부터 서머타임을 한 달 더 늘려 시행하고 있다. 이렇게

서머타임은 국가마다 계속 변화하는 제도인데 자바는 그런 데이터를 어디에서 참조하고 있

을까?

앞에서 말한 시간대 데이터베이스라는 곳에 데이터가 정리된다. tzdata, IANA Time Zone

Database, Olson database 등 다양한 이름으로 불리는 이 정보는 자바 외에도 오라클

(Oracle), PHP 등 다양한 플랫폼에서 활용되는 국제 표준 데이터다. 자바는 운영체제에 의

존하지 않고 독립적으로 시간대 데이터베이스를 업데이트한다. 즉, 운영체제의 시간대 패치

가 업데이트되지 않아도 자바는 최신 정보를 유지할 수 있고, 운영체제의 시간대 패치는 자바

의 시간대 정보에 영향을 미칠 수 없다. 그리고 JRE(Java runtime environment) 전체를 업

그레이드하지 않고 TZUpdater라는 도구로 시간대 데이터만 최신으로 업데이트하는 방법도

지원한다.5

4 “ ‘서머타임’ 내년 4월 부활?”, 머니투데이, 2009. 7. 28., http://www.mt.co.kr/view/mtview.php?type=1&no=20090728165017605

57&outlink=1.

5 http://www.oracle.com/technetwork/java/javase/tzupdater-readme-136440.html

Page 23: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

01. 자바의 날짜와 시간 API 23

JRE의 최신 시간대 데이터의 변경 이력을 보면 요르단, 리비아 등에서 일어난 변경 사항이

2013년에도 반영된 것을 확인할 수 있다.6 아마 우리나라 제도에 변화가 생긴다면 이 데이터

가 잘 갱신됐는지 유심히 살펴봐야 할 것이다.

그런데 예제 1-2의 실행 결과와 수정한 예제의 실행 결과는 이 글을 작성한 시점에서는 실제

역사적 사실과 부합하지 않는다. 위키백과에 정리된 한국 표준시 자료7나 과거의 보도 기사8

를 보면 그해 서머타임이 시작된 시간은 5월 8일 새벽 2시였다. 그렇다면 예제 1-3과 예제 1-4

는 시간대 데이터베이스의 오류를 보여 준다. 이 글을 작성하면서 IANA 쪽에 오류를 수정

한 패치를 보냈고9, 2014년 11월에 배포된 tzdata2014j에 수정 사항이 반영됐다.10 JDK에서

도 TZUpdater 1.4.10이 오류가 수정된 이 데이터를 참조하고, JDK 8u40, JDK 7u80, JDK

7u75 등 여러 버전에 백포트 패치됐다. 컴퓨터에서 새벽 0시가 아닌 새벽 2시로 서머타임 변

경 시점이 나온다면 오류가 수정된 버전을 쓰고 있는 것이다.

서울 1961년 8월 9일 23시 59분의 1분 후는?

예제 1-3은 ‘1961.08.09 23:59’의 1분 후가 ‘1961.08.10 00:00’임을 테스트한다.

예제 1-3 서울 1961년 8월 9일 23시 59분의 1분 후 구하기

@Test

public void shouldGetAfterOneMinute() {

TimeZone seoul = TimeZone.getTimeZone("Asia/Seoul");

Calendar calendar = Calendar.getInstance(seoul);

calendar.set(1961, Calendar.AUGUST, 9, 23, 59);

String pattern = "yyyy.MM.dd HH:mm";

String theTime = toString(calendar, pattern, seoul);

assertThat(theTime).isEqualTo("1961.08.09 23:59");

calendar.add(Calendar.MINUTE, 1);

6 http://www.oracle.com/technetwork/java/javase/tzdata-versions-138805.html

7 위키백과, “한국 표준시”, http://ko.wikipedia.org/wiki/한국_표준시.

8 “내일부터 서머타임... 새벽2시를 3시로”, 매일경제, 1988. 5. 7.

9 1988년 서머타임뿐만 아니라 우리나라의 시간대 정보에 관한 거의 모든 부분에 오류가 있었다. 이에 대한 더 자세한 내용과 패치를 보낸 과

정은 “시간대 DB에서 우리나라 시간의 오류”(http://blog.benelog.net/3120317)를 참고한다.

10 tzdata2014j의 배포 내용은 https://bugs.openjdk.java.net/browse/JDK-8064560을 참고한다.

Page 24: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

24 1부 _ 자바의 API 이해하기

String after1Minute = toString(calendar, pattern, seoul);

assertThat(after1Minute).isEqualTo("1961.08.10 00:00");

}

이 테스트도 실패한다. 신기하게도 23시 59분의 1분 후는 0시 30분이다. 다음과 같이 마지막

줄을 수정하면 테스트를 통과할 수 있다.

assertThat(after1Minute).isEqualTo("1961.08.10 00:30");

1961년 8월 10일은 대한민국의 표준시가 UTC+8:30에서 현재와 같은 UTC+9:00로 변경

된 시점이다. 일제 강점기 동안 UTC+9:00이던 표준시가 해방 이후 1954년에 UTC+8:30으

로 바뀌었다가 1961년에 다시 UTC+9:00으로 바뀐다.11 이 표준시 변경 때문에 30분을 건너

뛴 것이다.

1961년 당시 최고 권력 기관이었던 국가재건최고회의는 표준시를 일본과 동일하게 바꾸기

로 결정했다. 최근에는 일제의 잔재에서 벗어난다는 의미에서 다시 한 번 표준시를 바꾸자는

의견도 나오고 있다.12 우리나라 근현대사와 관련이 있는 예제라고 할 만하다.

그러나 여기서 또 한 번의 반전이 있다. “yyyy.MM.dd HH:mm (Z)” 형식으로 1954년과

1961년, 1968년의 특정 시간에서 UTC와의 차이를 출력하면 다음과 같다.13

≑≑ 1954.03.20 22:59 (+0900)

≑≑ 1954.03.20 23:00 (+0800)

≑≑ 1961.08.09 23:59 (+0800)

≑≑ 1961.08.10 00:30 (+0830)

≑≑ 1968.09.30 23:59 (+0830)

≑≑ 1968.10.01 00:30 (+0900)

11 위키백과, “한국 표준시”, http://ko.wikipedia.org/wiki/한국_표준시.

12 “조명철, 한국표준시 우리 고유 시간으로 변경 추진”, 연합뉴스, 2013. 11. 21., http://www.yonhapnews.co.kr/bulletin/2013/11/21/0

200000000AKR20131121169700001.HTML.

13 테스트에 사용한 소스 코드는 https://github.com/benelog/java-date-time/blob/master/jdk7-date-time/src/main/java/com/nbp/

weblab/date/TimeZoneChangePoint.java를 참고한다.

Page 25: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

01. 자바의 날짜와 시간 API 25

이 테스트 결과에 따르면 우리나라의 시간대 변경 시점은 다음과 같다.

≑≑ 1954년: UTC+0900 -> UTC+0800

≑≑ 1961년: UTC+0800 -> UTC+0830

≑≑ 1968년: UTC+0830 -> UTC+0900

그러나 위키백과와 과거 신문에서 확인한 역사적 사실로는 1950년 이후로는 1954년에

UTC+0900에서 UTC+0830로, 1961년에 UTC+0830에서 UTC+0900로 두 번의 변경

이 있을 뿐이다. 즉 예제 1-3에서 보이는 30분의 변화는 역사적으로는 UTC+0830에서

UTC+0900로의 변화였지만, 프로그램이 실행된 결과로는 시간대 데이터베이스의 자료에

따라서 UTC+0800에서 UTC+0830로 바뀐 차이를 보여 준다.

이 역시 서머타임 자료와 마찬가지로 시간대 데이터베이스의 오류였고 IANA에서 서머타임

과 함께 수정했다. 모든 플랫폼에서 참조하는 국제 표준 데이터에서 수십 년 동안 우리나라

에 관한 정보가 오류투성이인 채로 유지됐다는 점은 상당히 놀라웠다. 국제화 관련 개발을

하는 개발자라면 특히 오래된 과거 데이터에 관해서는 시간대 데이터베이스의 신빙성을 계

속 의심해 봐야 할 것이다.

협정세계시 2012년 6월 30일 23시 59분 59초의 2초 후는?

예제 1-4는 UTC ‘2012.06.30 23:59:59’의 2초 후가 ‘2012.07.01 00:00:01’인 것을 테스

트한다.

예제 1-4 UTC 2012년 6월 30일 23시 59분 59초의 2초 후 구하기

@Test

public void shouldGetAfterTwoSecond() {

TimeZone utc = TimeZone.getTimeZone("UTC");

Calendar calendar = Calendar.getInstance(utc);

calendar.set(2012, Calendar.JUNE, 30, 23, 59, 59);

String pattern = "yyyy.MM.dd HH:mm:ss";

String theTime = toString(calendar, pattern, utc);

assertThat(theTime).isEqualTo("2012.06.30 23:59:59");

Page 26: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

26 1부 _ 자바의 API 이해하기

calendar.add(Calendar.SECOND, 2);

String afterTwoSeconds = toString(calendar, pattern, utc);

assertThat(afterTwoSeconds).isEqualTo("2012.07.01 00:00:01");

}

지금까지의 예제와는 다르게 위의 테스트는 성공한다. 별로 특별할 것이 없다면 이번 예제는

왜 넣었을까? 2012년 6월 30일은 가장 최근에 ‘윤초’가 적용된 때다. 즉 예제 1-4의 결과는

윤년이나 서머타임과는 달리 자바에서 윤초가 Calendar 클래스의 연산에 적용되지 않는다

는 것을 보여 준다.

윤년보다는 다소 낯선 윤초가 필요한 이유는 다음과 같다. UTC는 세슘 원자의 진동수에 바

탕을 둔 원자시계가 기준이고, UT1(세계시)은 지구의 움직임을 관찰한 결과가 기준이다. 원

자시계는 일정한 반면 지구의 움직임은 미세하게나마 불규칙적이므로 이 둘 사이에는 오차가

발생한다. 윤초는 그 둘의 오차를 보정하기 위해 추가하는 1초다. IERS(International Earth

Rotation Service, 국제지구자전사업)라는 기관에서 윤초 수정을 결정한다.

최근에는 윤초를 폐지하자는 주장도 있다.14 윤초가 컴퓨터 시스템에서 복잡한 문제를 일으킬

수 있기 때문이다. 대부분의 시스템에서 시간은 데이터 정렬, 복제에 핵심적인 키 역할을 한

다. 윤초의 적용으로 만약 같은 초가 반복된다면 그 사이의 데이터가 엉킬 가능성이 크다.

윤초가 마지막으로 적용된 2012년 6월 30일에는 레딧(Reddit)15, 포스퀘어(Foursquare)16,

옐프(Yelp)17, 링크드인(LinkedIn)18 등 많은 기업이 장애를 겪었다. 리눅스와 자바

를 사용하는 시스템이 많았고 카산드라(Cassandra)19, 하둡(Hadoop)20, 엘라스틱서치

(Elasticsearch)21 등 데이터 저장, 검색 플랫폼에서 CPU를 100% 사용하는 문제가 발생했다

14 “전문가들 시간 미세조정 ‘윤초’ 폐지 논쟁 지속”, 연합뉴스, 2013. 09. 21., http://www.yonhapnews.co.kr/bulletin/2013/09/21/0200

000000AKR20130921027300009.HTML.

15 http://www.reddit.com

16 https://www.foursquare.com

17 http://www.yelp.com

18 https://www.linkedin.com

19 http://cassandra.apache.org

20 http://hadoop.apache.org

21 http://www.elasticsearch.org

Page 27: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

01. 자바의 날짜와 시간 API 27

고 한다.22 국내에서도 하둡을 사용하는 시스템에서 유사한 문제가 많이 발생했다.23 리눅스 커

널과 자바의 복합적인 문제로 추정된다. 대부분 시간 재설정, 서버 재시작 등으로 이 문제를

해결한 듯하다. 구글은 윤초 적용 이전에 점진적으로 시간을 더해가는 ‘leap smear’라는 기법

으로 이런 장애를 예방했다.24

API 문서에 따르면 Date 클래스25가 UTC를 정확히 반영하는지는 JVM(Java virtual

machine)의 실행 환경에 따라 다르다고 한다. 대부분의 운영체제에서 모든 경우에 하루는

86,400초(24 × 60 × 60)이고, 컴퓨터 시계의 대부분은 윤초를 반영할 정도로 정교하지 못

하다고 언급하고 있다. 참고로 System.currentTimeMillis() 메서드는 1970년 1월 1일 이후

로 지나간 밀리초를 반환하는데, 윈도우나 안드로이드 등의 운영체제에서 테스트한 결과로는

그 기간 중의 윤초가 특별히 더해지진 않는다.26

뒤에서 소개할 Joda-Time과 JSR-310을 포함해 모든 환경에서 윤초를 명시적으로 지원하

는 자바 라이브러리는 아직 없다. 어쨌든 윤초 동기화 시에는 운영체제, 자바, 미들웨어, 애플

리케이션의 상호작용이 불안정해질 가능성이 높다는 점은 분명하다.

JDK 기본 날짜 클래스의 문제점

앞의 예제로 날짜와 시간 계산이 생각보다 어렵고 고려해야 할 것도 많으며, 깊이 이해하기

위해서는 배경 지식도 많이 필요한 영역임을 확인했다. 그런데 이러한 어려움과는 별도로

Calendar 클래스와 Date 클래스에 문제가 많다.

22 “윤초 때문에… 포스퀘어-링크드인 장애”, 지디넷, 2012. 7. 2., http://www.zdnet.co.kr/news/news_view.asp?artice_

id=20120702094444.

23 윤초 때문에 하둡을 사용하는 시스템에 문제가 발생한 사례는 “1초가 하둡을 죽였다.”(http://agbird.egloos.com/5654329)를 참고한다.

24 Christopher Pascoe, “Time, technology and leaping seconds”, http://googleblog.blogspot.kr/2011/09/time-technology-and-

leaping-seconds.html.

25 http://docs.oracle.com/javase/7/docs/api/java/util/Date.html

26 System.currentTimeMillis() 메서드와 윤초에 관한 자세한 내용은 “Do Java system milliseconds take account of leap seconds?”(http://

stackoverflow.com/questions/16022346/do-java-systemmilliseconds-take-account-of-leap-seconds)와 댓글을 참고한다.

Page 28: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

28 1부 _ 자바의 API 이해하기

불변 객체가 아니다

VO(value object)는 값에 의해 동등성이 판단되는 객체다.27 VO는 완전한 불변 객체일 때 별

칭 문제, 스레드 불안정성 등의 부작용에서 자유롭고 여러 객체에서 공유해도 안전하다.28 날

짜, 돈 등의 객체는 VO의 대표적인 예로 자주 제시된다. C#, 파이썬 같은 언어에서는 날짜 클

래스가 한 번 생성된 이후에는 내부 속성을 바꿀 수 없다.

불행히도 자바의 기본 날짜, 시간 클래스는 불변 객체가 아니다(not immutable). 앞의 코드

에서 Calendar 클래스에 set 메서드를 호출해 날짜를 지정하고, 다시 같은 객체에 set(int,

int) 메서드를 호출해 수행한 날짜 연산 결과는 같은 인스턴스에 저장됐다. Date 클래스에

도 값을 바꿀 수 있는 set 메서드가 존재한다. 이 때문에 Calendar 객체나 Date 객체가 여

러 객체에서 공유되면 한 곳에서 바꾼 값이 다른 곳에 영향을 미치는 부작용이 생길 수 있다.

“Effective Java, Second Edition”의 저자 조슈아 블로크(Joshua Bloch)도 Date 클래스는

불변 객체여야 했다고 지적했다.29

이를 안전하게 구현하려면 이들 객체를 복사해서 반환하는 기법을 권장한다. 그림 1-1에서

코드의 startTime 필드는 내부의 Date 객체를 외부에서 조작할 수 있기 때문에 악의적인 클

라이언트 코드가 값을 수정할 수 있다. endTime 필드처럼 방어 복사 기법을 써서 새로운 객

체를 생성해서 반환하도록 구현하는 것이 바람직하다.30

27 디팍 알루어(Deepak Alur)의 “Core J2EE Patterns: Best Practices and Design Strategies” 초판에서는 티어(tier) 사이의 데이터를 전송

하는 객체를 ‘value object’라고 명명했기 때문에 용어의 혼동을 유발했다. 하지만 개정판에서는 이를 TO(transfer object)라는 이름으로 수

정했다. J2EE 패턴의 TO와 같은 패턴을 마틴 파울러(Martin Fowler)는 DTO(data transfer object)라고 이름 붙였다. 이 패턴들의 개념을

이해하는 데 참고할 수 있는 자료는 다음과 같다.

� VO

� Martin Fowler, 2002, “Patterns of Enterprise Application Architecture”, Addison Wesley, p. 486.

� Martin Folwer, “ValueObject”, http://martinfowler.com/bliki/ValueObject.html.

� TO, DTO

� Alur, Curpi, and Malks, 2001, “Core J2EE Patterns: Best Practices and Design Strategies”, Prentice Hall, “7.7 Service to Worker.”

� Sun Microsystems, “Core J2EE Patterns - Transfer Object”, http://www.oracle.com/technetwork/java/transferobject-139757.html.

� Martin Fowler, 2002, “Patterns of Enterprise Application Architecture”, Addison Wesley, p. 401.

� Martin Folwer, “Data Transfer Object”, http://martinfowler.com/eaaCatalog/dataTransferObject.html.

28 VO의 불변성이 왜 좋은 설계인지는 다음 자료에 자세히 설명돼 있다.

� Evans, 2003, “Domain-Driven Design”, Addison Wesley, p. 99 ~ 103.

� Martin Fowler, 2002, “Patterns of Enterprise Application Architecture”, Addison Wesley, p. 486.

� Joshua Bloch, 2008, “Effective Java, Second Edition”, Addison Wesley, p. 73 ~ 80.

29 Joshua Bloch, 2008, “Effective Java, Second Edition”, Addison Wesley, p. 80.

30 Joshua Bloch, 2008, “Effective Java, Second Edition”, Addison Wesley, p. 184 ~ 188.

Page 29: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

01. 자바의 날짜와 시간 API 29

그림 1-1 Date의 방어 복사 기법과 FindBugs의 취약점 지적

이런 취약점은 정적 분석 도구로 발견할 수도 있다. FindBugs의 다음 규칙은 그림 1-1의

startTime 필드와 같이 취약한 코드를 경고해 준다.

≑≑ EI_EXPOSE_REP31

≑≑ EI_EXPOSE_REP232

그림 1-1에서 취약성이 있는 코드의 아래에 추가한 주석은 FindBugs에서 보여 주는 경고문

을 그대로 옮긴 문장이다.

31 http://findbugs.sourceforge.net/bugDescriptions.html#EI_EXPOSE_REP

32 http://findbugs.sourceforge.net/bugDescriptions.html#EI_EXPOSE_REP2

Page 30: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

30 1부 _ 자바의 API 이해하기

int 상수 필드의 남용

Calendar 클래스를 사용한 날짜 연산은 예제 1-5와 같이 int 상수 필드를 사용한다.

예제 1-5 초 더하기 코드

calendar.add(Calendar.SECOND, 2);

첫 번째 파라미터에 Calendar.JUNE과 같이 전혀 엉뚱한 상수가 들어가도 이를 컴파일 시점

에서 확인할 방법이 없다. 이뿐만 아니라 Calendar 클래스에는 많은 int 상수가 쓰였는데 이

어서 설명할 월, 요일 지정 등에서도 많은 혼란을 유발한다.

헷갈리는 월 지정

앞에서 1582년 10월 4일을 지정하는 코드는 다음과 같았다.

예제 1-6 10월 지정 코드

calendar.set(1582, Calendar.OCTOBER , 4);

그런데 월에 해당하는 Calendar.OCTOBER 값은 실제로는 ‘9’다. JDK 1.0에서 Date 클래

스는 1월을 0으로 표현했고, JDK 1.1부터 포함된 Calendar 클래스도 이러한 관례를 답습했

다. 그래서 1582년 10월 4일을 표현하는 코드를 다음과 같이 쓰는 실수를 많은 개발자들이

반복하고 있다.

예제 1-7 실수로 쓰기 쉬운 10월 지정 코드

calendar.set(1582, 10 , 4);

또는 일부러 가독성을 높이기 위해서 10월을 10-1로 표현한 기법을 쓰는 사람도 있다.

예제 1-8 10월 지정 코드의 한 방식

calendar.set(1582, 10 - 1 , 4);

Page 31: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

01. 자바의 날짜와 시간 API 31

예제 1-9는 이로 인해서 일어날 수 있는 실수를 보여 주는 코드다.33 예제 1-9의 테스트는

성공한다.

예제 1-9 1999년 12월 31일을 지정하려다 2000년으로 넘어간 코드

@Test

public void shouldGetDate() {

Calendar calendar = Calendar.getInstance();

calendar.set(1999, 12, 31);

assertThat(calendar.get(Calendar.YEAR)).isEqualTo(2000);

assertThat(calendar.get(Calendar.MONTH)).isEqualTo(Calendar.JANUARY);

assertThat(calendar.get(Calendar.DAY_OF_MONTH)).isEqualTo(31);

}

1999년 12월 31일을 지정하려 했으나, 12월의 상수 값은 11이므로 직접 숫자 12를 대입하

면 2000년 1월 31일로 넘어간다. 숫자 12 대신 11 혹은 Calendar.DECEMBER 상수로 지

정해야 1999년 12월 31일이 된다.

13월을 의미하는 12를 넣어도 Calendar.set() 메서드가 오류를 반환하지 않기 때문에 이런

실수를 인지하기 더욱 어렵다. calendar.setLenient(false) 메서드를 호출하면 잘못된 월이 지

정된 객체에서 IllegalArgumentException을 반환한다. 그렇게 지정해도 Calendar.set() 메

서드가 호출되는 시점이 아니라 Calendar.get() 메서드가 호출될 때 예외(exception)가 발

생한다는 점도 주의해야 한다.

참고로 FindBugs에서는 0 ~ 11을 벗어난 월을 지정할 때 경고를 보여 주기도 한다.

그림 1-2 Calendar 클래스에 대한 잘못된 월 지정과 FindBugs의 경고

33 조슈아 블로크의 “Java Puzzlers”에 있는 “Puzzle 61: The Dating Game”의 코드와 유사하게 테스트를 만들었다.

Page 32: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

32 1부 _ 자바의 API 이해하기

일관성 없는 요일 상수

예제 1-10은 2014년 1월 1일이 수요일임을 확인하는 코드다.

예제 1-10 요일 확인하기

@Test

@SuppressWarnings("deprecation")

public void shouldGetDayOfWeek() {

Calendar calendar = Calendar.getInstance();

calendar.set(2014, Calendar.JANUARY, 1);

int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);

assertThat(dayOfWeek).isEqualTo(Calendar.WEDNESDAY);

assertThat(dayOfWeek).isEqualTo(4);

Date theDate = calendar.getTime();

assertThat(theDate.getDay()).isEqualTo(3);

}

Calendar.get(Calendar.DAY_OF_WEEK) 함수에서 반환한 요일은 int 값으로, 일요일이 1

로 표현된다. 따라서 수요일은 4고, 보통 Calendar.WEDNESDAY 상수와 비교해서 확인한

다. 그런데 calendar.getTime() 메서드로 Date 객체를 반환받아 Date.getDay() 메서드로

요일을 구하면 일요일은 0, 수요일은 3이 된다. 두 클래스 사이에서 요일 지정 값에 일관성이

없는 것이다.

Date.getDay() 메서드는 요일을 구하는 메서드로는 이름이 모호하기도 하다. 현재는 사용하

지 않는(deprecated) 메서드라서 그나마 다행이다.

Date 클래스와 Calendar 클래스의 불편한 역할 분담

JDK 1.0에서는 Date 클래스가 날짜 연산을 지원하는 유일한 클래스였다. JDK 1.1 이후부터

Calendar 클래스가 포함되면서 날짜 간의 연산, 국제화 지원 등은 Calendar 클래스에서 주

로 담당하고 Date 클래스의 많은 기능이 사용되지 않게 됐다.

특정 시간대의 날짜를 생성하거나 연, 월, 일 같은 날짜 단위의 계산은 Date 클래스만으로는

수행하기 어렵기 때문에 날짜 연산을 위해 Calendar 객체를 생성하고, 다시 Calendar 객체

Page 33: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

01. 자바의 날짜와 시간 API 33

에서 Date 객체를 생성한다. 최종 결과에는 불필요한 중간 객체를 생성해야 하는 셈인데, 쓰

기에도 번거롭고 Calendar 클래스는 생성 비용이 비싼 편이기 때문에 비효율적이기도 하

다.34 불편함을 덜기 위해 실무에서는 Date의 연산에 아파치의 Commons Lang 라이브러리35

에 있는 DateUtils 클래스36의 plusDays() 메서드나 plusMonth() 메서드 같은 메서드를 주

로 활용한다. 그러나 DateUtils 클래스를 쓰더라도 중간 객체로 Calendar 객체를 생성하는

것은 마찬가지다.

날짜와 시간을 모두 저장하는 클래스의 이름이 ‘Date’라는 점도 다소 아쉽다. Calendar.

getTime() 메서드도 Date 타입을 반환하는데 메서드 이름만 봐서는 반환 타입을 예측하기

가 힘들다.

오류에 둔감한 시간대 아이디 지정

예제 1-11은 시간대의 아이디를 ‘Asia/Seoul’ 대신 ‘Seoul/Asia’로 잘못 지정한 코드다.

예제 1-11 잘못 지정한 시간대 아이디

@Test

public void shouldSetGmtWhenWrongTimeZoneId(){

TimeZone zone = TimeZone.getTimeZone("Seoul/Asia");

assertThat(zone.getID()).isEqualTo("GMT");

}

그러나 이 코드는 오류가 발생하지 않고, 아이디가 ‘GMT’인 시간대가 지정된 것처럼 테스트

를 통과한다. 이런 특성 때문에 찾기 어려운 버그가 생길 수도 있다.

java.util.Date 하위 클래스의 문제

java.util.Date 클래스를 상속한 하위 클래스에도 문제가 많다.

34 Joshua Bloch, 2008, “Effective Java, Second Edition”, Addison Wesley, p. 22.

35 http://commons.apache.org/proper/commons-lang

36 http://commons.apache.org/proper/commons-lang/javadocs/api-2.6/org/apache/commons/lang/time/DateUtils.html

Page 34: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

34 1부 _ 자바의 API 이해하기

java.sql.Date 클래스는 상위 클래스인 java.util.Date 클래스와 이름이 같다. 이 클래스를 두

고 자바 플랫폼 설계자가 클래스 이름을 지으면서 깜빡 존 듯하다는 조롱까지 나왔다.37 그리

고 이 클래스는 Comparable 인터페이스에 대한 정의를 클래스 선언에서 하지 않았기 때문

에 Comparable과 관련된 제네릭 선언을 복잡하게 만들었다.38

java.sql.Timestamp 클래스는 java.util.Date 클래스에 나노초(nanosecond) 필드를 더한

클래스다. 이 클래스는 equals() 선언의 대칭성을 어겼다. Date 타입과 Timestamp 타입을

섞어 쓰면 a.equals(b)가 true라도 b.equals(a)는 false인 경우가 생길 수 있다.39

자바의 개선된 날짜, 시간 API, Joda-Time

좋은 API는 잘못 사용하거나 혼동의 여지가 없어야 하고 문서가 없어도 쉽게 사용할 수 있어

야 한다.40 그러나 자바의 기본 API는 문서를 열심히 보기 전까지는 제대로 사용하기 어렵다.

이런 문제점 때문에 JDK의 날짜, 시간 API를 대체하는 라이브러리가 많이 나와 있다. 대표적

으로 다음과 같은 것들이 있다.

≑≑ Joda-Time: http://www.joda.org/joda-time

≑≑ Time and Money Code Library: http://timeandmoney.sourceforge.net41

≑≑ CalendarDate: http://calendardate.sourceforge.net

≑≑ date4j: http://www.date4j.net

이 가운데 Joda-Time은 기본 JDK를 대체하는 날짜와 시간 API 중 가장 널리 쓰인다. 앞에

서 나왔던 예제 1-1, 예제 1-2, 예제 1-3, 예제 1-4, 예제 1-9, 예제 1-10, 예제 1-11 등을

테스트를 통과하는 상태로 Joda-Time으로 옮기면 다음과 같다.

37 Joshua Bloch, Neal Gafter, 2005, “Java Puzzlers”, Addison Wesley, p. 163.

38 Comparable 클래스와 Generics 선언에 관한 문제는 “Effective & Agile Java Generics”(http://blog.benelog.net/viewer/2173103)를

참고한다.

39 Joshua Bloch, 2008, “Effective Java, Second Edition”, Addison Wesley, p. 41.

40 조슈아 블로크의 “How to Design a Good API & Why it Matters” 강연(http://www.infoq.com/presentations/effective-api-design)

에서도 강조되는 원칙이다.

41 “Domain-Driven Design: Tackling Complexity in the Heart of Software”의 저자 에릭 에반스(Eric Evans)가 참여한 것으로 유명하다.

Page 35: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

01. 자바의 날짜와 시간 API 35

예제 1-12 Joda-Time을 이용한 날짜 연산

public class JodaTimeTest {

@Test // 예제 1-1: 1일 후 구하기

public void shouldGetAfterOneDay() {

Chronology chrono = GregorianChronology.getInstance();

LocalDate theDay = new LocalDate(1582, 10, 4, chrono);

String pattern = "yyyy.MM.dd";

assertThat(theDay.toString(pattern)).isEqualTo("1582.10.04");

LocalDate nextDay = theDay.plusDays(1);

assertThat(nextDay.toString(pattern)).isEqualTo("1582.10.05");

}

@Test // 예제 1-1: 1일 후 구하기

public void shouldGetAfterOneDayWithGJChronology() {

Chronology chrono = GJChronology.getInstance();

LocalDate theDay = new LocalDate(1582, 10, 4, chrono);

String pattern = "yyyy.MM.dd";

assertThat(theDay.toString(pattern)).isEqualTo("1582.10.04");

LocalDate nextDay = theDay.plusDays(1);

assertThat(nextDay.toString(pattern)).isEqualTo("1582.10.15");

}

@Test // 예제 1-2: 1시간 후 구하기

public void shouldGetAfterOneHour() {

DateTimeZone seoul = DateTimeZone.forID("Asia/Seoul");

DateTime theTime = new DateTime(1988,5,7,23,0, seoul);

String pattern = "yyyy.MM.dd HH:mm";

assertThat(theTime.toString(pattern)).isEqualTo("1988.05.07 23:00");

assertThat(seoul.isStandardOffset(theTime.getMillis())).isTrue();

DateTime after1Hour = theTime.plusHours(1);

assertThat(after1Hour.toString(pattern)).isEqualTo("1988.05.08 01:00");

assertThat(seoul.isStandardOffset(after1Hour.getMillis())).isFalse();

}

@Test // 예제 1-3: 1분 후 구하기

public void shouldGetAfterOneMinute() {

Page 36: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

36 1부 _ 자바의 API 이해하기

DateTimeZone seoul = DateTimeZone.forID("Asia/Seoul");

DateTime theTime = new DateTime(1961, 8, 9, 23, 59, seoul);

String pattern = "yyyy.MM.dd HH:mm";

assertThat(theTime.toString(pattern)).isEqualTo("1961.08.09 23:59");

DateTime after1Minute = theTime.plusMinutes(1);

assertThat(after1Minute.toString(pattern)).isEqualTo("1961.08.10 00:30");

}

@Test // 예제 1-4: 2초 후 구하기

public void shouldGetAfterTwoSecond() {

DateTimeZone utc = DateTimeZone.forID("UTC");

DateTime theTime = new DateTime(2012, 6, 30, 23, 59, 59, utc);

String pattern = "yyyy.MM.dd HH:mm:ss";

assertThat(theTime.toString(pattern)).isEqualTo("2012.06.30 23:59:59");

DateTime after2Seconds = theTime.plusSeconds(2);

assertThat(after2Seconds.toString(pattern)).isEqualTo("2012.07.01

00:00:01");

}

@Test // 예제 1-9: 1999년 12월 31일을 지정하는 코드

public void shouldGetDate() {

LocalDate theDay = new LocalDate(1999, 12, 31);

assertThat(theDay.getYear()).isEqualTo(1999);

assertThat(theDay.getMonthOfYear()).isEqualTo(12);

assertThat(theDay.getDayOfMonth()).isEqualTo(31);

}

@Test (expected=IllegalFieldValueException.class) // 예제 1-9: 1999년 12월 31

일을 지정하는 코드의 실수

public void shouldNotAcceptWrongMonth() {

new LocalDate(1999, 13, 31);

}

@Test // 예제 1-10: 요일 확인하기

public void shouldGetDayOfWeek() {

LocalDate theDay = new LocalDate(2014, 1, 1);

Page 37: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

01. 자바의 날짜와 시간 API 37

int dayOfWeek = theDay.getDayOfWeek();

assertThat(dayOfWeek).isEqualTo(DateTimeConstants.WEDNESDAY);

assertThat(dayOfWeek).isEqualTo(3);

}

@Test(expected=IllegalArgumentException.class) // 예제 1-11: 잘못 지정한 시간대 아이디

public void shouldThrowExceptionWhenWrongTimeZoneId(){

DateTimeZone.forID("Seoul/Asia");

}

}

예제 1-12에서 볼 수 있는 특징은 다음과 같다.

≑≑ LocalDate, DateTime 등으로 지역 시간과 시간대가 지정된 시간을 구분했다. LocalDate와

LocalTime으로 날짜와 시간을 별도의 클래스로 구분할 수도 있다.

≑≑ plusDays, plusMinutes, plusSeconds 등 단위별 날짜 연산 메서드를 LocalDate 클래스와

DateTime 클래스에서 지원한다. 메서드가 호출된 객체의 상태를 바꾸지 않고 새로운 객체를 반환

한다. 불변 객체다.

≑≑ 월의 int 값과 명칭이 일치한다. 1월은 int 값이 1이다.

≑≑ GregorianChronology를 썼을 때는 1582년 10월을 특별하게 취급하지는 않는다. GJChronology

를 사용하면 JDK의 GregorianCalendar와 같이 10월 4일 다음 날이 10월 15일로 나온다.

≑≑ 서머타임 기간이면 DateTimeZone.isStandardOffset() 메서드의 반환 값이 false다.

≑≑ 13월 같이 잘못된 월이 전달되면 객체 생성 시점에서 IllegalFieldValueException을 반환한다.

≑≑ 요일 상수는 DateTimeConstants 클래스에 정의된 WEDNESDAY와 동일한 int 값을 사용하고 있

다. 참고로 MONDAY가 1, SUNDAY가 7이며 전체 라이브러리에서 이 값을 일관성 있게 사용한다.

≑≑ 잘못된 시간대 아이디를 지정하면 IllegalArguementException을 반환한다.

그 밖에 Joda-Time에서는 시간 간격에 대한 개념을 섬세하게 정의하고 Duration, Period,

Interval 등으로 역할을 분담한 클래스로 구현했다.

그레고리력과 율리우스력뿐만 아니라 불교, 이슬람교, 콥트교회, 에티오피아의 달력까지도

지원한다. 다양한 달력은 org.joda.time.chrono.BaseChronology 클래스의 하위 클래스로

구현돼 있다.

Page 38: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

38 1부 _ 자바의 API 이해하기

Joda-Time은 JAR 파일에 별도로 시간대 데이터베이스를 포함하고 있다. 그래서 JDK에서

참조하는 시간대 데이터베이스와는 별도로 정보를 갱신할 수도 있다. Jar의 압축을 해제하면

{root}/src/java/org/joda/time/tz/src 폴더에 시간대 정보 파일이 있다. 이 폴더를 덮어쓰

면 특정 애플리케이션만의 시간대 정보를 정의할 수도 있다.

Joda-Time은 다음과 같이 다른 언어로 래핑되거나 포팅돼 있기도 하다.

≑≑ Scala Wrapper: https://github.com/nscala-time/nscala-time

≑≑ Clojure Wrapper: https://github.com/clj-time/clj-time

≑≑ .NET 포팅: https://code.google.com/p/noda-time

스프링(Spring) 프레임워크에서도 Joda-Time을 기본으로 지원한다. 스프링의 Web MVC

프레임워크는 사용자가 입력한 문자열을 원하는 객체로 변환할 때 Converter라는 인터페이

스를 활용하는데, 클래스패스에 Joda-Time이 포함돼 있으면 이 라이브러리의 객체를 변환

하는 Converter 구현체를 자동으로 등록한다.42

하이버네이트(Hibernate) 프레임워크43에서도 Joda-Time을 쓸 수 있다. Joda-time-

hibernate 모듈44을 이용하면 데이터베이스에 저장된 TIMESTAMP 같은 타입을 Date 클래

스와 같은 JDK의 기본 클래스 대신 Joda-Time의 클래스로 매핑할 수 있다.

JSR-310, 새로운 자바의 날짜 API

2014년에 최종 배포된 JDK 8에는 JSR-310이라는 표준 명세로 날짜와 시간에 대한 새로운

API가 추가됐다.45 앞에서 설명한 Joda-Time에 가장 많은 영향을 받았고, 그 밖에 Time and

Money 라이브러리나 ICU 등 여러 오픈소스 라이브러리를 참고했다고 한다.

예제 1-12를 JDK 8의 ZonedDateTime 클래스를 이용해 작성하면 다음과 같다.

42 스프링 프레임워크와 Joda-Time에 관한 자세한 내용은 “Spring 3 Type Conversion and Validation”(http://spring.io/blog/2009/11/17/

spring-3-type-conversion-and-validation)을 참고한다.

43 http://hibernate.org

44 http://www.joda.org/joda-time-hibernate

45 https://java.net/projects/jsr-310

Page 39: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

01. 자바의 날짜와 시간 API 39

예제 1-13 JSR-310을 이용한 날짜 연산

public class Jsr310Test {

@Test // 예제 1-1: 1일 후 구하기

public void shouldGetAfterOneDay() {

LocalDate theDay = IsoChronology.INSTANCE.date(1582, 10, 4);

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd");

assertThat(theDay.format(formatter)).isEqualTo("1582.10.04");

LocalDate nextDay = theDay.plusDays(1);

assertThat(nextDay.format(formatter)).isEqualTo("1582.10.05");

}

@Test // 예제 1-2: 1시간 후 구하기

public void shouldGetAfterOneHour() {

ZoneId seoul = ZoneId.of("Asia/Seoul");

ZonedDateTime theTime = ZonedDateTime.of(1988, 5, 7, 23, 0, 0, 0, seoul);

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd

HH:mm");

assertThat(theTime.format(formatter)).isEqualTo("1988.05.07 23:00");

ZoneRules seoulRules = seoul.getRules();

assertThat(seoulRules.isDaylightSavings(Instant.from(theTime))).

isFalse();

ZonedDateTime after1Hour = theTime.plusHours(1);

assertThat(after1Hour.format(formatter)).isEqualTo("1988.05.08 01:00");

assertThat(seoulRules.isDaylightSavings(Instant.from(after1Hour))).

isTrue();

}

@Test // 예제 1-3: 1분 후 구하기

public void shouldGetAfterOneMinute() {

ZoneId seoul = ZoneId.of("Asia/Seoul");

ZonedDateTime theTime = ZonedDateTime.of(1961, 8, 9, 23, 59, 59, 0, seoul);

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm");

assertThat(theTime.format(formatter)).isEqualTo("1961.08.09 23:59");

ZonedDateTime after1Minute = theTime.plusMinutes(1);

assertThat(after1Minute.format(formatter)).isEqualTo("1961.08.10

00:30");

}

Page 40: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

40 1부 _ 자바의 API 이해하기

@Test // 예제 1-4: 2초 후 구하기

public void shouldGetAfterTwoSecond() {

ZoneId utc = ZoneId.of("UTC");

ZonedDateTime theTime = ZonedDateTime.of(2012, 6, 30, 23, 59, 59, 0, utc);

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd

HH:mm:ss");

assertThat(theTime.format(formatter)).isEqualTo("2012.06.30 23:59:59");

ZonedDateTime after2Seconds = theTime.plusSeconds(2);

assertThat(after2Seconds.format(formatter)).isEqualTo("2012.07.01 00:00:01");

}

@Test // 예제 1-9: 1999년 12월 31일을 지정하는 코드

public void shouldGetDate() {

LocalDate theDay = LocalDate.of(1999, 12, 31);

assertThat(theDay.getYear()).isEqualTo(1999);

assertThat(theDay.getMonthValue()).isEqualTo(12);

assertThat(theDay.getDayOfMonth()).isEqualTo(31);

}

@Test(expected=DateTimeException.class)

// 예제 1-9: 1999년 12월 31일을 지정하는 코드의 실수

public void shouldNotAcceptWrongDate() {

LocalDate.of(1999, 13, 31);

}

@Test // 예제 1-10: 요일 확인하기

public void shouldGetDayOfWeek() {

LocalDate theDay = LocalDate.of(2014, 1, 1);

DayOfWeek dayOfWeek = theDay.getDayOfWeek();

assertThat(dayOfWeek).isEqualTo(DayOfWeek.WEDNESDAY);

}

@Test(expected=ZoneRulesException.class) // 예제 1-11: 잘못 지정한 시간대 아이디

public void shouldThrowExceptionWhenWrongTimeZoneId(){

ZoneId.of("Seoul/Asia");

}

}

Page 41: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

01. 자바의 날짜와 시간 API 41

‘java.time.*’ 패키지로 시작하지만 Joda-Time과 거의 유사한 모습이다. 다음과 같은 특징이

있고 Joda-Time에서 개선된 점도 많다.

≑≑ DateTime 클래스 대신 ZoneDateTime 클래스가 사용된다. 시간대 정보를 가지고 있는 클래스임

을 더욱 명확히 표현하려 한 듯하다.

≑≑ 요일 클래스는 enum 상수로 제공한다. 잘못 지정하거나 혼동할 여지가 없다.

≑≑ 생성자 대신 of() 메서드 같은 정적 팩터리 메서드(static factory method)를 많이 사용한다.

DateTimeFormatter.ofPattern() 메서드와 Instant.from() 메서드 등이 그 예다. 정적 팩터리 메

서드는 가독성 있는 이름을 따로 붙일 수 있고, 생성자와는 달리 한번 생성된 객체를 재활용할 수도

있다.46

≑≑ Joda-Time보다 클래스별 역할이 더 세분화됐다. ZoneRules 클래스가 그 예다.

≑≑ 서머타임 기간이면 TimeZoneRules.isDaylightSavings() 메서드의 반환 값이 true다.

≑≑ 잘못된 시간대 아이디를 지정하면 ZoneRulesException을 반환한다.

≑≑ 잘못된 월을 지정하면 객체 생성 시점에 DateTimeException을 반환한다.

그 밖에도 여러 장점이 있다. Calendar 클래스와 Date 클래스, Joda-Time의 시간 클래스가

밀리초(millisecond) 단위의 정밀성을 가졌던 반면, JSR-310의 클래스는 나노초까지 다룰

수 있다. 시계의 개념도 도입돼서 현재 시간과 관련된 기능을 테스트할 때도 유용하다. java.

time.Clock 클래스의 하위 클래스로 SystemClock, FixedClock 등이 제공된다.

이미 스프링 프레임워크 4.0에서는 JSR-310을 기본으로 지원한다. ZoneDateTime 등의 타

입이 Controller의 메서드 파라미터로 선언되면 사용자가 입력한 문자열을 날짜 객체로 변환

한다. JDK 8과 JSR-310 명세가 논의된 지 오래돼서인지 이 기능은 2012년부터 계획돼 있

었다.47

46 Joshua Bloch, 2008, “Effective Java, Second Edition”, Addison Wesley, p. 5 ~ 10.

47 스프링 프레임워크에서 JSR-310을 지원하는 프로젝트의 이력은 https://jira.springsource.org/browse/SPR-9641을 참고한다.

Page 42: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

42 1부 _ 자바의 API 이해하기

JSR-310을 JDK 7에서 쓸 수 있는 백포트 모듈도 존재한다. pom.xml 파일에 다음과 같이

의존성을 추가하면 사용할 수 있다.

예제 1-14 JSR-310의 백포트 모듈의 의존성 선언

<dependency>

<groupId>org.threeten</groupId>

<artifactId>threetenbp</artifactId>

<version>0.8.1</version>

</dependency>

패키지가 ‘java.time.*’ 대신 ‘org.threeten.bp.*’라는 점을 제외하면 대부분의 클래스가 동일하

다. 다만 백포트 모듈의 ZonedDateTime.toString(DateTimeFormatter) 메서드가 java.time

패키지에서는 ZonedDateTime.format(DateTimeFormatter)으로 바뀌는 등 미묘한 차이

가 존재하기는 한다.

마치며

우리 생활과 밀접하게 연관돼 있으면서도 많은 역사가 반영돼 있기에 날짜 클래스는 도메인

자체가 쉬운 편은 아니다. 시차나 각국의 제도 변경을 의식해야 하는 국제화 시대에서는 더욱

그렇다. 그런 어려움을 자바의 불편한 API가 더 어렵게 만들었지만 너무나도 오랫동안 개선

되지 않았다.

JSR-310은 2007년에 처음 제안된 명세였다. 7년이나 지난 2014년에서야 JDK 8에 포함돼

최종 배포된 것이 아쉽지만 자바의 다른 명세를 보더라도 이 느린 속도가 아주 놀랍지는 않

다. 한번 공개된 API는 영원하고, 특히 날짜에 관해서는 초창기의 시행착오가 너무도 뼈아팠

기에 그만큼 신중할 수밖에 없지 않았을까? API 설계를 가끔 고민하는 입장에서 그런 신중함

을 누릴 수 있는 상황이 부럽기도 하다.

Page 43: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

01. 자바의 날짜와 시간 API 43

그리고 본문에 나온 예제는 모두 다음 주소에서 전체 소스를 확인할 수 있다.

표 1-1 예제를 확인할 수 있는 주소

클래스 이름 소스 코드 URL 설명

OldJdkDateTest.java

https://github.com/benelog/java-date-time/

blob/master/jdk7-date-time/src/test/java/

com/nbp/weblab/date/OldJdkDateTest.java

기본 JDK 클래스 예제

JodaTimeTest.java

https://github.com/benelog/java-date-time/

blob/master/jdk7-date-time/src/test/java/

com/nbp/weblab/date/JodaTimeTest.java

Joda-Time 예제

Jsr310BackportTest.java

https://github.com/benelog/java-date-time/

blob/master/jdk7-date-time/src/test/java/

com/nbp/weblab/date/Jsr310BackportTest.

java

JSR-310의 JDK 7용 백

포트 모듈 예제.

JDK 7 이상이 설치돼 있

어야 한다.

Jsr310Test.java

https://github.com/benelog/java-date-time/

blob/master/jdk8-date-time/src/test/java/

com/nbp/weblab/date/Jsr310Test.java

JSR-310 + JDK 8 예제.

JDK 8이 설치돼 있어야

한다.

Page 44: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

44 1부 _ 자바의 API 이해하기

이 글은 자바 7과 자바 8을 기준으로 HashMap이 어떻게 구현돼 있는지 설명한다.

HashMap 자체의 소스 코드는 오라클 JDK(Java development kit)나 OpenJDK나 모두 같

기 때문에 이 글이 설명하는 HashMap 구현 방식은 오라클 JDK와 OpenJDK 모두에 해당

한다고 할 수 있다. 자바가 아닌 다른 언어를 주로 사용하는 개발자도 자바의 HashMap이 현

재 어떻게 구현돼 있고 어떻게 발전됐는지 알면 라이브러리나 프레임워크 구현에 대한 혜안

을 얻을 수 있을 것이라고 기대한다.

HashMap은 자바 컬렉션 프레임워크(Java collections framework)에 속한 구현체 클래스

다. 자바 컬렉션 프레임워크는 1998년 12월에 발표한 자바 2에서 정식으로 선보였다. Map

인터페이스 자체는 자바 5에서 제네릭이 적용된 것 외에 처음 선보인 이후 변화가 없지만

HashMap 구현체는 성능을 향상시키기 위해 지속적으로 변화해 왔다.

이 글에서는 어떤 방식으로 HashMap 구현체의 성능을 향상시켰는지 소개한다. 구체적으로

다루는 내용은 amortized constant time을 위해 어떻게 해시 충돌(hash collision) 가능성을

줄이고 있는가에 관한 것이다.

송기선

02자바의 HashMap은 어떻게 작동하는가?

Page 45: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

02. 자바의 HashMap은 어떻게 작동하는가? 45

HashMap과 HashTable

이 글에서 말하는 HashMap과 HashTable은 자바의 API 이름이다. HashTable이란

JDK 1.0부터 있던 자바의 API고, HashMap은 자바 2에서 처음 선보인 자바 컬렉션 프레

임워크에 속한 API다. HashTable 또한 Map 인터페이스를 구현하고 있어 HashMap과

HashTable이 제공하는 기능은 같다. 다만 HashMap은 보조 해시 함수(additional hash

function)를 사용하기 때문에 보조 해시 함수를 사용하지 않는 HashTable에 비해 해시 충돌

발생이 덜해 상대적으로 성능상 이점이 있다. 보조 해시 함수가 아니더라도 HashTable은 거

의 변화가 없는 반면, HashMap은 지속적으로 개선되고 있다. 그러나 HashTable의 현재 가

치는 JRE(Java runtime environment) 1.0과 JRE 1.1 환경을 대상으로 구현한 자바 애플리

케이션이 잘 작동할 수 있도록 하위 호환성을 제공하는 데 있기 때문에 이 둘 사이에 성능과

기능을 비교하는 것은 큰 의미가 없다.

HashMap과 HashTable을 정의하면 ‘키에 대한 해시 값을 사용해 값을 저장하고 조회하

며, 키-값 쌍의 개수에 따라 동적으로 크기가 증가하는 연관 배열(associative array)’이라

고 할 수 있다. 이 연관 배열을 지칭하는 다른 용어가 있는데 대표적으로 Map, Dictionary,

Symbol Table 등이 있다.

예제 2-1 HashTable과 HashMap의 선언부

public class Hashtable<K,V> extends Dictionary<K,V>

implements Map<K,V>, Cloneable, java.io.Serializable {

public class HashMap<K,V> extends AbstractMap<K,V>

implements Map<K,V>, Cloneable, Serializable {

연관 배열을 지칭하기 위해 HashTable에서는 Dictionary라는 이름을 사용하고, HashMap

에서는 이름 그대로 맵(map)이라는 용어를 사용한다.

맵은 원래 수학 함수에서의 대응 관계를 지칭하는 용어로, 경우에 따라서는 함수 자체를 의미

하기도 한다. 즉 HashMap이란 이름에서 알 수 있듯이 HashMap은 키 집합인 정의역과 값

집합인 공역의 대응에 해시 함수를 이용한다.

Page 46: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

46 1부 _ 자바의 API 이해하기

키 값

그림 2-1 함수로서의 맵

해시 분포와 해시 충돌

동일하지 않은 어떤 객체 X와 Y가 있을 때, 즉 X.equals(Y)가 ‘거짓’일 때 ‘X.hashCode() !=

Y.hashCode()’라면 이때 사용하는 해시 함수는 완전한 해시 함수(perfect hash functions)

라고 한다( ).

Boolean같이 서로 구별되는 객체의 종류가 적거나 Integer, Long, Double 같은 Number 객

체는 객체가 나타내려는 값 자체를 해시 값으로 사용할 수 있기 때문에 완전한 해시 함수 대

상으로 삼을 수 있다. 하지만 String이나 POJO(plain old java object)에 대해 완전한 해시

함수를 제작하는 것은 사실상 불가능하다.

적은 연산만으로 빠르게 작동할 수 있는 완전한 해시 함수가 있다 하더라도, 그것

을 HashMap에서 사용할 수 있는 것은 아니다. HashMap은 기본적으로 각 객체의

hashCode() 메서드가 반환하는 값을 사용하는데, 결과 자료형은 int다. 32비트 정수 자료형

으로는 완전한 자료 해시 함수를 만들 수 없다. 논리적으로 생성 가능한 객체의 수가 232보다

많을 수 있고, 모든 HashMap 객체에서 O(1)을 보장하기 위해 랜덤 접근이 가능하게 하려면

원소가 232인 배열을 모든 HashMap이 가지고 있어야 하기 때문이다.

따라서 HashMap을 비롯한 많은 해시 함수를 이용하는 연관 배열 구현체에서는 메모리를

절약하기 위해 실제 해시 함수의 표현 정수 범위 보다 작은 M개의 원소가 있는 배열만을

Page 47: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

02. 자바의 HashMap은 어떻게 작동하는가? 47

키 값

그림 2-1 함수로서의 맵

해시 분포와 해시 충돌

동일하지 않은 어떤 객체 X와 Y가 있을 때, 즉 X.equals(Y)가 ‘거짓’일 때 ‘X.hashCode() !=

Y.hashCode()’라면 이때 사용하는 해시 함수는 완전한 해시 함수(perfect hash functions)

라고 한다( ).

Boolean같이 서로 구별되는 객체의 종류가 적거나 Integer, Long, Double 같은 Number 객

체는 객체가 나타내려는 값 자체를 해시 값으로 사용할 수 있기 때문에 완전한 해시 함수 대

상으로 삼을 수 있다. 하지만 String이나 POJO(plain old java object)에 대해 완전한 해시

함수를 제작하는 것은 사실상 불가능하다.

적은 연산만으로 빠르게 작동할 수 있는 완전한 해시 함수가 있다 하더라도, 그것

을 HashMap에서 사용할 수 있는 것은 아니다. HashMap은 기본적으로 각 객체의

hashCode() 메서드가 반환하는 값을 사용하는데, 결과 자료형은 int다. 32비트 정수 자료형

으로는 완전한 자료 해시 함수를 만들 수 없다. 논리적으로 생성 가능한 객체의 수가 232보다

많을 수 있고, 모든 HashMap 객체에서 O(1)을 보장하기 위해 랜덤 접근이 가능하게 하려면

원소가 232인 배열을 모든 HashMap이 가지고 있어야 하기 때문이다.

따라서 HashMap을 비롯한 많은 해시 함수를 이용하는 연관 배열 구현체에서는 메모리를

절약하기 위해 실제 해시 함수의 표현 정수 범위 보다 작은 M개의 원소가 있는 배열만을

사용한다. 따라서 다음과 같이 객체에 대한 해시 코드의 나머지 값을 해시 버킷 인덱스 값으

로 사용한다.

예제 2-2 해시를 사용하는 연관 배열 구현체에서 저장/조회할 해시 버킷을 계산하는 방법

int index = X.hashCode() % M;

이 코드와 같은 방식을 사용하면 서로 다른 해시 코드를 가지는 서로 다른 객체가 1/M의 확

률로 같은 해시 버킷을 사용하게 된다. 이는 해시 충돌을 회피하도록 잘 구현됐느냐에 상관

없이 발생할 수 있는 또 다른 종류의 해시 충돌이다. 이렇게 해시 충돌이 발생하더라도 키-값

쌍 데이터를 잘 저장하고 조회할 수 있게 하는 방식에는 대표적으로 두 가지가 있는데, 하나

는 open addressing이고, 다른 하나는 separate chaining이다. 이 둘 외에도 해시 충돌을 해

결하기 위한 다양한 자료구조가 있지만 거의 모두 이 둘을 응용한 것이라고 할 수 있다.

int index = X.hashCode() % M;

A, B, C, D 순서로 HashMap에 삽입될 때

A, B, C, D에 대한 index 값이 차례로 0, 1, 2M + 1, M + 1이라고 할 때

A

A

0

0

B

1

1

C

......

D

M - 1

M - 1

B C D

open addressing(선형 검색법)

separate chaining

그림 2-2 open addressing과 separate chaining 구조

open addressing은 데이터를 삽입하려는 해시 버킷이 이미 사용 중이면 다른 해시 버킷에 해

당 데이터를 삽입하는 방식이다. 데이터를 저장, 조회할 해시 버킷을 찾을 때는 선형 검색법

(linear probing), 2차 검색법(quadratic probing) 등의 방법을 사용한다. separate chaining

Page 48: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

48 1부 _ 자바의 API 이해하기

에서 각 배열의 인자는 인덱스가 같은 해시 버킷을 연결한 연결 리스트(linked list)의 첫 부분

(head)이다.

둘 모두 최대 연산 횟수는 O(M)이다. 하지만 open addressing은 연속된 공간에 데이터를 저

장하기 때문에 separate chaining보다 캐시 효율이 높다. 따라서 데이터 개수가 충분히 적다

면 open addressing이 separate chaining보다 더 성능이 좋다. 하지만 배열의 크기가 커질수

록(M 값이 커질수록) 캐시 효율이 높다는 open addressing의 장점은 사라진다. 배열의 크기

가 커지면 L1, L2 캐시 적중률(cash hit ratio)이 낮아지기 때문이다.

자바의 HashMap에서 사용하는 방식은 separate chaining이다. open addressing은 데이

터를 삭제할 때 효율적으로 처리하기 어려운데, HashMap에서는 remove() 메서드가 매우

빈번하게 호출될 수 있기 때문이다. 게다가 HashMap에 저장된 키-값 쌍 개수가 일정 개수

이상으로 많아지면 일반적으로 open addressing은 separate chaining보다 느리다. open

addressing의 경우 해시 버킷을 채운 밀도가 높아질수록 worst case 발생 빈도가 더 높아지

기 때문이다. 반면 separate chaining 방식의 경우 해시 충돌이 잘 발생하지 않도록 ‘조정’할

수 있다면 worst case 또는 worst case에 가까운 일이 발생하는 것을 줄일 수 있다(여기에 관

해서는 “보조 해시 함수”(55쪽)에서 설명하겠다).

예제 2-3 자바 7에서의 해시 버킷 관련 구현

transient Entry<K, V>[] table = (Entry<K, V>[]) EMPTY_TABLE;

// transient로 선언된 이유는 직렬화(serialize)할 때

// table 배열 자체를 직렬화하는 것보다

// 키-값 쌍을 차례로 기록하는 것이 더 효율적이기 때문이다.

static class Entry<K, V> implements Map.Entry<K, V> {

final K key;

V value;

Entry<K, V> next;

int hash;

Entry(int h, K k, V v, Entry<K, V> n) {

value = v;

next = n;

key = k;

Page 49: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

02. 자바의 HashMap은 어떻게 작동하는가? 49

hash = h;

}

public final K getKey() {...}

public final V getValue() {...}

public final V setValue(V newValue) {...}

public final boolean equals(Object o) {...}

public final int hashCode() {...}

public final String toString() {...}

void recordAccess(HashMap<K,V> m) {...}

void recordRemoval(HashMap<K,V> m) {...}

}

separate chaining 방식을 사용하기 때문에 자바 7에서의 put() 메서드 구현은 예제 2-4와

같다.

예제 2-4 put() 메서드 구현

public V put(K key, V value) {

if (table == EMPTY_TABLE) {

inflateTable(threshold); // table 배열 생성

}

// HashMap에서는 null을 키로 사용할 수 있다.

if (key == null)

return putForNullKey(value);

// value.hashCode() 메서드를 사용하는 것이 아니라 보조 해시 함수를 이용해

// 변형된 해시 함수를 사용한다. "보조 해시 함수"(55쪽)에서 설명한다.

int hash = hash(key);

// i 값이 해시 버킷의 인덱스다.

// indexFor() 메서드는 hash % table.length와 의도가 같은 메서드다.

int i = indexFor(hash, table.length);

// 해시 버킷에 있는 연결 리스트를 순회한다.

// 만약 같은 키가 이미 저장돼 있다면 교체한다.

for (Entry<K, V> e = table[i]; e != null; e = e.next) {

Object k;

if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

Page 50: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

50 1부 _ 자바의 API 이해하기

V oldValue = e.value;

e.value = value;

e.recordAccess(this);

return oldValue;

}

}

// 삽입, 삭제 등으로 이 HashMap 객체가 몇 번이나 변경(modification)됐는지

// 관리하기 위한 코드다.

// ConcurrentModificationException을 발생시켜야 할지 판단할 때 사용한다.

modCount++;

// 아직 해당 키-값 쌍 데이터가 삽입된 적이 없다면 새로 Entry를 생성한다.

addEntry(hash, key, value, i);

return null;

}

그러나 자바 8에서는 예제 2-4에서 볼 수 있는 것보다 더 발전된 방식을 사용한다.

자바 8 HashMap에서의 separate chaining

자바 2부터 자바 7까지의 HashMap에서 separate chaining 구현 코드는 조금씩 다르지만

구현 알고리즘 자체는 같았다. 만약 객체의 해시 함수 값이 균등 분포(uniform distribution)

상태라고 할 때 get() 메서드 호출에 대한 기댓값은 이다. 그러나 자바 8에서는 이보다 더

나은 을 보장한다. 데이터의 개수가 많아지면 separate chaining에서 연결 리스트 대신

트리를 사용하기 때문이다.

데이터의 개수가 많아지면 과 의 차이는 무시할 수 없다. 게다가 실제 해시 값은 균등 분

포가 아닐뿐더러 설사 균등 분포를 따른다고 하더라도 생일 문제(birthday problem)1가 설

명하듯 일부 해시 버킷 몇 개에 데이터가 집중될 수 있다. 그래서 데이터의 개수가 일정 수 이

상일 때는 연결 리스트 대신 트리를 사용하는 것이 성능상 이점이 있다.

1 생일 문제는 여러 사람이 모여 있을 때 생일이 같은 사람이 있을 확률을 구하는 문제다. 더 자세한 내용은 http://en.wikipedia.org/wiki/

Birthday_problem을 참고한다.

Page 51: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

02. 자바의 HashMap은 어떻게 작동하는가? 51

연결 리스트를 사용할 것인가 트리를 사용할 것인가에 대한 기준은 하나의 해시 버킷에 할당

된 키-값 쌍의 개수다. 예제 2-5에서 보듯 자바 8의 HashMap에서는 상수 형태로 기준을

정하고 있다. 즉 하나의 해시 버킷에 8개의 키-값 쌍이 모이면 연결 리스트를 트리로 변경한

다. 만약 해당 버킷에 있는 데이터를 삭제해 개수가 6개에 이르면 다시 연결 리스트로 변경한

다. 트리는 연결 리스트보다 메모리 사용량이 많고, 데이터의 개수가 적을 때 트리와 연결 리스

트의 worst case 수행 시간 차이를 비교하는 것은 의미가 없기 때문이다. 8과 6으로 2 이상의

차이를 둔 것은, 만약 차이가 1이라면 어떤 한 키-값 쌍이 반복되어 삽입/삭제되는 경우 불필

요하게 트리와 연결 리스트를 변경하는 일이 반복되어 성능 저하가 발생할 수 있기 때문이다.

예제 2-5 자바 8 HashMap의 TREEIFY_THRESHOLD와 UNTREEIFY_THRESHOLD

static final int TREEIFY_THRESHOLD = 8;

static final int UNTREEIFY_THRESHOLD = 6;

자바 8 HashMap에서는 Entry 클래스 대신 Node 클래스를 사용한다. Node 클래스 자체는

자바 7의 Entry 클래스와 내용이 거의 같지만, 연결 리스트 대신 트리를 사용할 수 있도록 하

위 클래스인 TreeNode가 있다는 것이 자바 7 HashMap과 다르다.

이때 사용하는 트리는 red-black tree인데 자바 컬렉션 프레임워크의 TreeMap과 구현

이 거의 같다. 트리 순회 시 사용하는 대소 판단 기준은 해시 함수 값이다. 해시 값을 대소 판

단 기준으로 사용하면 total ordering에 문제가 생기는데, 자바 8의 HashMap에서는 이를

tieBreakOrder() 메서드로 해결한다.

예제 2-6 자바 8 HashMap의 Node 클래스

transient Node<K,V>[] table;

static class Node<K,V> implements Map.Entry<K,V> {

// 클래스 이름은 다르지만 자바 7의 Entry 클래스와 구현 내용은 같다.

}

// LinkedHashMap.Entry 클래스는 HashMap.Node를 상속한 클래스다.

// 따라서 TreeNode 객체를 table 배열에 저장할 수 있다.

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {

Page 52: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

52 1부 _ 자바의 API 이해하기

TreeNode<K,V> parent;

TreeNode<K,V> left;

TreeNode<K,V> right;

TreeNode<K,V> prev;

// red-black tree에서 노드는 red이거나 black이다.

boolean red;

TreeNode(int hash, K key, V val, Node<K,V> next) {

super(hash, key, val, next);

}

final TreeNode<K,V> root() {

// 트리 노드의 root를 반환한다.

}

static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {

// 해시 버킷에 트리를 저장할 때는 root 노드에 가장 먼저 접근해야 한다.

}

// 순회하며 트리 노드 조회

final TreeNode<K,V> find(int h, Object k, Class<?> kc) {}

final TreeNode<K,V> getTreeNode(int h, Object k) {}

static int tieBreakOrder(Object a, Object b) {

// 트리 노드에서에서 어떤 두 키의 comparator 값이 같다면 서로 동등하게 취급된다.

// 그런데 어떤 두 키의 해시 값이 서로 같아도 이 둘은 서로 동등하지

// 않을 수 있다. 따라서 어떤 두 키의 해시 함수 값이 같을 경우

// 임의로 대소 관계를 지정해야 하는 경우가 있다.

}

final void treeify(Node<K,V>[] tab) {

// 연결 리스트를 트리로 변환한다.

}

final Node<K,V> untreeify(HashMap<K,V> map) {

// 트리를 연결 리스트로 변환한다.

}

Page 53: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

02. 자바의 HashMap은 어떻게 작동하는가? 53

// 다음 두 메서드의 역할은 메서드 이름만 봐도 알 수 있다.

final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,

int h, K k, V v) {}

final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,

boolean movable) {}

// red-black 구성 규칙에 따라 균형을 유지하기 위한 것이다.

final void split (…)

static <K,V> TreeNode<K,V> rotateLeft(…)

static <K,V> TreeNode<K,V> rotateRight(…)

static <K,V> TreeNode<K,V> balanceInsertion(…)

static <K,V> TreeNode<K,V> balanceDeletion(…)

static <K,V> boolean checkInvariants(TreeNode<K,V> t) {

// 트리가 규칙에 맞게 잘 생성된 것인지 판단하는 메서드다.

}

}

해시 버킷의 동적 확장

해시 버킷의 개수가 적다면 메모리 사용을 아낄 수 있지만 해시 충돌로 성능 손실이 발생한

다. 그래서 HashMap은 키-값 쌍 데이터 개수가 일정 개수 이상이면 해시 버킷의 개수를 두

배로 늘린다. 이렇게 해시 버킷 개수를 늘리면 값도 작아져 해시 충돌로 인한 성능 손실 문

제를 어느 정도 해결할 수 있다.

해시 버킷 개수의 기본값은 16이고, 데이터의 개수가 임계점에 이를 때마다 해시 버킷 개수

의 크기를 두 배씩 증가시킨다. 버킷의 최대 개수는 230개다. 그런데 이렇게 버킷 개수가 두 배

로 증가할 때마다 모든 키-값 데이터를 읽어 새로운 separate chaining을 구성해야 하는 문

제가 있다. HashMap 생성자의 파라미터로 초기 해시 버킷 개수를 지정할 수 있으므로 해당

HashMap 객체에 저장될 데이터의 개수가 어느 정도인지 예측 가능한 경우에는 이를 생성

자의 파라미터로 지정하면 불필요하게 separate chaining을 재구성하지 않게 할 수 있다.

Page 54: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

54 1부 _ 자바의 API 이해하기

예제 2-7 자바 7 HashMap에서의 해시 버킷 확장

// 파라미터로 사용하는 newCapacity는 언제나 2a이다.

void resize(int newCapacity) {

Entry[] oldTable = table;

int oldCapacity = oldTable.length;

// MAXIMIM_CAPACITY는 230이다.

if (oldCapacity == MAXIMUM_CAPACITY) {

threshold = Integer.MAX_VALUE;

return;

}

Entry[] newTable = new Entry[newCapacity];

// 새 해시 버킷을 생성한 다음 기존의 모든 키-값 데이터를

// 새 해시 버킷에 저장한다.

transfer(newTable, initHashSeedAsNeeded(newCapacity));

table = newTable;

threshold = (int) Math.min(newCapacity * loadFactor,

MAXIMUM_CAPACITY + 1);

}

void transfer(Entry[] newTable, boolean rehash) {

int newCapacity = newTable.length;

// 모든 해시 버킷을 순회하면서

for (Entry<K, V> e : table) {

// 각 해시 버킷에 있는 연결 리스트를 순회하면서

while (null != e) {

Entry<K, V> next = e.next;

if (rehash) {

e.hash = null == e.key ? 0 : hash(e.key);

}

// 해시 버킷 개수가 변경됐기 때문에

// index 값(hashCode % M)을 다시 계산해야 한다.

int i = indexFor(e.hash, newCapacity);

e.next = newTable[i];

newTable[i] = e;

e = next;

}

}

}

Page 55: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

02. 자바의 HashMap은 어떻게 작동하는가? 55

해시 버킷 크기를 두 배로 확장하는 임계점은 현재의 데이터 개수가 ‘부하율(load factor)

× 현재의 해시 버킷 개수’에 이를 때다. 이 부하율은 0.75, 즉 3/4이다. 이 부하율 또한

HashMap의 생성자에서 지정할 수 있다.

임계점에 이르면 항상 해시 버킷 크기를 두 배로 확장하기 때문에 N개의 데이터를 삽입했을

때의 키-값 쌍 접근 횟수는 다음과 같이 분석할 수 있다.

즉, 기본 생성자로 생성한 HashMap을 이용해 많은 양의 데이터를 삽입할 때는 최적의 해시

버킷 개수를 지정한 것보다 약 2.5배 많이 키-값 쌍 데이터에 접근해야 한다. 이는 곧 수행 시

간이 2.5배 길어진다고 할 수 있다. 따라서 성능을 높이려면 HashMap 객체를 생성할 때 적

정한 해시 버킷 개수를 지정해야 한다.

그런데 이렇게 해시 버킷 크기를 두 배로 확장하는 것에는 결정적인 문제가 있다. 해시

버킷의 개수 M이 2a 형태가 되기 때문에 ‘index = X.hashCode() % M’을 계산할 때

X.hashCode()의 하위 a개의 비트만 사용하게 된다는 것이다. 즉 해시 함수가 32비트 영역

을 고르게 사용하도록 만들었다 하더라도 해시 값을 2의 승수로 나누면 해시 충돌이 쉽게 발

생할 수 있다. 이 때문에 보조 해시 함수(supplement hash function)가 필요하다.

보조 해시 함수

‘index = X.hashCode() % M’을 계산할 때 사용하는 M 값은 소수일 때 index 값의 분포가

가장 균등할 수 있다. 그러나 M 값이 소수가 아니기 때문에 별도의 보조 해시 함수를 이용해

index 값 분포가 가급적 균등할 수 있게 해야 한다.

보조 해시 함수의 목적은 ‘키’의 해시 값을 변형해 해시 충돌 가능성을 줄이는 것이다. 이 보조

해시 함수는 JDK 1.4에 처음 등장했다. 자바 5 ~ 자바 7은 같은 방식의 보조 해시 함수를 사

용하고, 자바 8부터는 다시 새로운 방식의 보조 해시 함수를 사용하고 있다.

Page 56: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

56 1부 _ 자바의 API 이해하기

예제 2-8 자바 7 HashMap에서의 보조 해시 함수

final int hash(Object k) {

// 자바 7부터는 JRE를 실행할 때 데이터 개수가 일정 수 이상이면

// String 객체에 대해서 JVM에서 제공하는 별도의 옵션으로

// 해시 함수를 사용하게 할 수 있다.

// 만약 이 옵션을 사용하지 않으면 hashSeed의 값은 0이다.

int h = hashSeed;

if (0 != h && k instanceof String) {

return sun.misc.Hashing.stringHash32((String) k);

}

h ^= k.hashCode();

// 해시 버킷의 개수가 2a이기 때문에 해시 값의 a비트 값만을

// 해시 버킷의 인덱스로 사용한다. 따라서 상위 비트의 값이

// 해시 버킷의 인덱스 값을 결정할 때 반영될 수 있도록

// shift 연산과 XOR 연산을 사용해 원래의 해시 값이 a비트 내에서

// 최대한 값이 겹치지 않고 구별되게 한다.

h ^= (h >>> 20) ^ (h >>> 12);

return h ^ (h >>> 7) ^ (h >>> 4);

}

그런데 자바 8에서는 자바 7보다 더 단순한 형태의 보조 해시 함수를 사용한다.

예제 2-9 자바 8 HashMap에서의 보조 해시 함수

static final int hash(Object key) {

int h;

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

예제 2-9에서 볼 수 있는 것처럼 자바 8의 HashMap 보조 해시 함수는 상위 16비트 값을

XOR로 연산하는 매우 단순한 형태의 보조 해시 함수를 사용한다. 이유는 두 가지다. 첫 번째

는 자바 8에서는 해시 충돌이 많이 발생하면 연결 리스트 대신 트리를 사용하므로 해시 충돌

시 발생할 수 있는 성능 문제가 완화됐기 때문이다. 두 번째는 최근의 해시 함수는 균등 분포

가 잘 되게 만들어지는 경향이 있어 자바 7까지 사용했던 보조 해시 함수의 효과가 크지 않기

때문이다. 두 번째 이유가 좀 더 결정적인 원인이 되어 자바 8에서는 보조 해시 함수의 구현을

바꿨다.

Page 57: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

02. 자바의 HashMap은 어떻게 작동하는가? 57

개념상 해시 버킷 인덱스를 계산할 때는 ‘index = X.hashCode() % M’처럼 나머지 연산을

사용하는 것이 맞지만 M 값이 2a일 때는 해시 함수의 하위 a비트만을 취한 것과 값이 같다.

따라서 나머지 연산 대신 ‘1 << a – 1’과 비트 논리곱(AND, &) 연산을 사용하면 수행이 훨

씬 더 빠르다.

String 객체에 대한 해시 함수

String 객체에 대한 해시 함수 수행 시간은 문자열 길이에 비례한다. 그래서 JDK 1.1에서는

String 객체에 빠르게 해시 함수를 수행하기 위해 일정 간격의 문자에 대한 해시를 누적한 값

을 문자열에 대한 해시 함수로 사용했다.

예제 2-10 JDK 1.1에서의 String 클래스의 해시 함수

public int hashCode() {

int hash = 0;

int skip = Math.max(1, length() / 8);

for (int i = 0; i < length(): i+= skip)

hash = s[i] + (37 * hash);

return hash;

}

예제 2-10에서 볼 수 있듯이 모든 문자에 대한 해시 함수를 계산하는 게 아니라 문자열의 길

이가 16을 넘으면 최소 하나의 문자를 건너가며 해시 함수를 계산했다.

그러나 이런 방식은 심각한 문제를 일으켰다. 웹의 URL은 길이가 수십 글자에 이르면서 앞

부분은 동일하게 구성되는 경우가 많다. 이 경우 서로 다른 URL의 해시 값이 같아지는 빈도

가 매우 높아질 수 있다는 문제가 있다. 따라서 이런 방식은 곧 폐기됐고, 예제 2-11에서 보

여 주는 방식을 현재의 자바 8까지도 계속 사용하고 있다.

예제 2-11 String 클래스의 해시 함수

public int hashCode() {

int h = hash;

if (h == 0 && value.length > 0) {

char val[] = value;

Page 58: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

58 1부 _ 자바의 API 이해하기

for (int i = 0; i < value.length; i++) {

h = 31 * h + val[i];

}

hash = h;

}

return h;

}

예제 2-11은 호너의 방법(Horner’s method)을 구현한 것이다. 호너의 방법은 다항식을 계

산하기 쉽도록 단항식으로 이뤄진 식으로 표현하는 것이다. 즉 예제 2-11에서 계산하고자

하는 해시 값 h는 다음과 같다.

이렇게 단항식을 재귀적으로 사용해 다항식 연산을 표현할 수 있다.

String 객체의 해시 함수에서 31을 사용하는 이유는 31이 소수이고 어떤 수에 31을 곱하는

것은 빠르게 계산할 수 있기 때문이다. ‘31N=32N-N’인데, 32는 25이니 어떤 수에 대한 32

를 곱한 값은 시프트(shift) 연산으로 쉽게 구현할 수 있다. 따라서 N에 31을 곱한 값은 ‘(N

<< 5) – N’과 같다. 31을 곱하는 연산은 이렇게 최적화된 머신 코드로 생성할 수 있기 때문

에 String 클래스에서 해시 값을 계산할 때는 31을 승수로 사용한다.

자바 7에서 String 객체에 대한 별도의 해시 함수

JDK 7u6부터 JDK 7u25까지는 HashMap에 저장된 키-값 쌍이 일정 개수 이상이면 String

객체에 한해 별도의 해시 함수를 사용할 수 있게 하는 기능이 있다. 이 기능은 JDK 7u40부터

는 삭제됐고 당연히 자바 8에도 해당 기능은 없다. 여기서 말하는 ‘일정 개수 이상’이나 ‘별도

의 해시 함수 사용 여부 지정’은 JVM(Java virtual machine)을 가동할 때 옵션으로 지정할

수 있다.

Page 59: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

02. 자바의 HashMap은 어떻게 작동하는가? 59

예제 2-12 자바 7의 String에 대한 hash32() 메서드

hashSeed = useAltHashing

? sun.misc.Hashing.randomHashSeed(this)

: 0;

...

int h = hashSeed;

if (0 != h && k instanceof String) {

return sun.misc.Hashing.stringHash32((String) k);

}

...

String 클래스에 있는 hash32() 메서드

int hash32() {

int h = hash32;

if (0 == h) {

h = sun.misc.Hashing.murmur3_32(HASHING_SEED, value, 0, value.length);

h = (0 != h) ? h : 1;

hash32 = h;

}

return h;

}

JDK 7u6부터 JDK 7u25까지는 jdk.map.althashing.threshold 옵션을 지정하면

HashMap에 저장된 키-값 쌍이 일정 개수 이상일 때 String 객체에 String 클래스의

hashCode() 메서드 대신 sun.misc.Hashing.stringHash32() 메서드를 사용할 수 있게 했

다. sun.misc.Hashing.stringHash32() 메서드는 String 클래스의 hash32() 메서드를 호출

하게 한 것이고, hash32() 메서드는 MurmurHash를 구현한 것이다. 이 MurmurHash를

이용해 String 객체에 대한 해시 충돌을 매우 낮출 수 있었다고 한다.

그러나 부작용도 있다. MurmurHash는 해시 시드 값(hash seed)이 필요한데, 이를 위한 것

이 sun.misc.Hashing.randomHashSeed() 메서드다. 이 메서드에서는 Random.nextInt()

메서드를 사용한다. Random.nextInt() 메서드는 CAS(compare and swap) 연산을 사용

하는 AtomicLong 객체를 사용하는데, CAS 연산은 코어가 많을수록 성능이 떨어진다. 즉,

Page 60: 네이버를 만든 기술, 읽으면서 배운다 - 자바편

60 1부 _ 자바의 API 이해하기

JDK 7u6부터 등장한 String 객체에 대한 별도의 해시 함수는 멀티 코어 환경에서는 성능이

떨어졌고, 이런 문제로 인해 JDK 7u40부터는 해당 기능을 사용하지 않는다. 당연히 자바 8

도 사용하지 않는다.

마치며

지금까지 설명한 내용은 자바 HashMap에서는 해시 충돌을 방지하기 위해 separate

chaining과 보조 해시 함수를 사용한다는 것, 자바 8에서는 separate chaining에서 연결 리

스트 대신 트리를 사용하기도 한다는 것, String 클래스의 hashCode() 메서드에서 31을 승

수로 사용하는 이유는 성능 향상을 위한 것으로 정리할 수 있다.

HashMap은 처음으로 등장한 이후 성능 향상을 위해 꾸준히 개선돼 왔다. JDK 1.4에서 처

음 등장한 보조 해시 함수와 자바 8의 트리 노드가 대표적인 예다. 그러나 자바 7의 일부 버전

에서 사용했던 MurmurHash처럼 성능 향상을 위해 시도한 방법이 결과적으로 좋지 않아 결

국에는 삭제되기도 하고, 많은 해시 함수가 균등 분포 결과 값을 내도록 잘 작성됨에 따라 기

존보다 더 단순한 형태의 보조 해시 함수를 사용하도록 변화하기도 했다.

웹 애플리케이션 서버의 경우에는 HTTPRequest가 생성될 때마다 여러 개의 HashMap이

생성된다. 수많은 HashMap 객체가 1초도 안 되는 시간에 생성되고 또 가비지 컬렉션 대상

이 된다. 컴퓨터 메모리 크기의 증가가 보편화되면서 메모리 중심적인 애플리케이션 제작도

늘었다. 따라서 갈수록 HashMap에 더 많은 데이터를 저장하고 있다고 할 수 있다. 자바 9,

자바 10의 HashMap이 어떤 모습일지 지금은 알 수 없지만, 컴퓨팅 환경은 계속 변하고 그

에 맞춰 HashMap 구현도 계속 변할 수밖에 없다는 것은 자명하다.