자바9 자바 플랫폼 모듈 시스템

https://docs.oracle.com/javase/9/whatsnew/toc.htm#JSNEW-GUID-C23AFD78-C777-460B-8ACE-58BE5EA681F6

여기서 소개하고 있는 기능을 하나씩 살펴보는 중이다. 먼저 젤 위에 있는 모듈 시스템에 대해 살짝 봤다. 아주 오래전 OSGi라는 기술이 반짝하고 뜨다가 급속도로 식어가는 모습을 본적이 있다. 왠지 이 기능은 그때 그 기술을 연상시킨다. 딱히 좋은 기억은 아니다. 어렵게 공부했는데 제대로 쓰지도 못해보고 잊혀졌다. 아무튼..

  • 컴파일 타임과 런 타임 사이에 링크 타임이라는 페이즈를 추가했고 그 시점에 모듈을 조립하고 최적화 하여 커스텀 런타임 이미지로 만들 수 있다.
  • javac, jlink, java에 모듈 관련 옵션이 추가됐다.
  • JAR 파일 안에 module-info.java 파일이 있는 모듈러 JAR라는 생겼다. (OSGi랑 아주 비슷하다. 그럴 수 밖에 하려는게 비슷하니까.)
  • JMOD라는 포맷을 추가했다고 하는데 잘 모르겠다. 네이티브 코드랑 설정 파일만 들어있는 JAR랑 비슷하다는 모르겠다. 자세한건 jmod 참조.
  • JDK 자체가 모듈로 바꼈다. 그래서
    • JDK 모듈을 맘대로 조립해 쓸 수 있다.
    • JDK랑 JRE 런타임 이미지를 재건축하여 성능, 보안, 유지용이성을 개선했다.
    • 런타임 이미지에 들어있는 리소스, 클래스, 모듈 이름에 쓰이는 새로운 URI 스키마를 만들었다. 내부 구조나 이미지 형식을 노출시키지 않고 명명할 수 있는 스키마인가 보다.
    • Removes the endorsed-standards override mechanism and the extension mechanism. 이건 뭔지 모르겠다.
    • rt.jar랑 tool.jar를 자바 런타임 이미지에서 제거했다.
    • 대부분의 내부 API는 접근을 막았지만 널리 자주 쓰이는 내부 API는 대체제가 생길때까지 열어둘 것이다. (참 애매하구만.) jdeps -jdkinternals로 내부 API 뭘 쓰고 있는지 찾아낼 수 있다.

이걸 만든 사람의 강연인데..  볼만하다.

  • public이어도 이제 다 공개 되어 쓸 수 있는건 아니다. 그 모듈에서 공개한 패키지에 들어있는 public 클래스만 쓸 수 있게 된다.
  • 그렇게 귀찮고 보수적으로 굴어서 얻는게 뭐냐면 런타임에 클래스 찾을라고 수 많은 JAR 파일 다 뒤지고 다니지 않고, 아마도 링크 타임에 모듈 인포 파일에 있는 패키지 정보 가지고 금방 찾을 수 있을테니 성능이 좋다고 한다. 당연히 보수적이니까 보안상으로도 좋다고 하겠지.
  • 대신 그렇게 하려면 한 패지지는 무조건 한 JAR에서만 공개해야 한다.
  • OSGi랑 다른점은 똑같은 JAR 다른 버전 여러개 지원하지 않는다. 툴에다 맡긴다는 말이 있는데 Maven이랑 다른게 뭐지? 내가 뭘 잘못알아 들었을지도..
  • 패지키 export는 컴파일 타임과 런타임 둘 다 해당된다.
  • 그런데 패키지 opens는 런타임에 리플렉션으로 그 패키지 안에 있는거 다 까볼 수 있게 해준다. 하지만 컴파일시엔 못쓴다.
  • jdeps로 해당 JAR 파일이 사용하는 패키지 목록을 뽑을 수 있다. (근데 서드 파티 패키지는 잘 안뽑히는듯?)

시간이 지나면 다들 모듈로 바뀌어 가겠지만 주로 라이브러리를 사용하는 입장에서는 크게 달라지는건 없을것 같다. 어차피 똑같은 JAR 파일일 뿐이고 public은 설마 다 export 해주겠지..

2017년 9월 말 근황

2015년 10월 아마존으로 이직하면서 시애틀로 넘어왔다. 그리고 아마존에서 1년간 시달리다 스트레스를 견디지 못하고 다시 이직하기로 결심했고 다행히 마이크로소프트에서 일자리를 구할 수 있었다. 아마존으로 이직할 때까지만 해도 그동안 쌓아온 기술 스택을 유지하고 싶었다. 하지만 내 코가 석자인 상황에서 차마 욕심을 부릴 수 없었고 그동안 쌓아온 모든 기술을 포기하더라도 마이크로소프트에서 새로 시작하기로 마음 먹었다.

막상 C#과 비주얼스튜디오를 써보니 자바나 인텔리J와 크게 다르진 않았다. 오히려 IDE는 더 견고해 보였고 C#은 자바보다 좀 더 문법적으로 자유롭고 기능이 많아 보여 재밌어 보인다. 하지만 자바나 스프링을 공부할 때처럼 공들여 공부하진 않았다. 왠지 모르겠지만 별로 그러고 싶은 맘이 들지 않았다. 원랜 그래야 하는데 말이지..

굳이 변명을 하자면 업무를 하는데 있어서 툴이나 언어도 중요하지만 그보다는 현재 팀이 담당하고 있는 플랫폼을 이해하는게 더 중요했다. 우리팀은 익스체인지 즉 마이크로소프트 오피스 군에 속해 있는 이메일 서버를 배포하는 일을 한다. 팀은 크게 운영 팀과 개발 팀으로 나뉘어 있는데 난 개발 팀에 속해있고, 익스체인지를 설치하는 Setup.exe 를 비롯해 익스체인지를 설치하는 서버에 설치해야 하는 다른 컴포넌트를 배포하기 위한 컴포넌트 프레임워크와 워크프로우를 개발 및 관리하고 있다. 쉽게 말해… 깡통 서버에 윈도 설치하고 그 위에 익스체인지를 비롯한 갖가지 소프트웨어를 설치하는 작업을 자동화 시키는 소프트웨어를 개발 및 관리화는 중이다.

이 팀이 해결해야 하는 주요 문제는 Speed와 Reliability. 즉 배포 시간를 줄이는 것이고 배포 실패율을 줄이는 것이다. 그래야만 더 자주 최소한의 다운타임으로 새로운 버전을 데이터센터에 배포할 수 있다. 익스체이지만을 설치하는데만 평균 A분이 걸린다. 거기에 각종 다른 컴포넌트까지 같이 배포하면 현재 기준 B시간 정도 걸린다. 그나마도 많이 줄어든거다. 내가 합류 하기 전엔 평균 C시간이었다고 한다. 거기에 여러가지 기발한 아이디어와 개선을 거쳐 현재까지도 계속해서 개선하고 있다. (혹시나 싶어서 일부터 삐- 처리함.)

같이 일하는 사람들은 아마존에서 같이 일하던 사람들에 비하면 나잇대가 많은 편이다. 마이크로소프트에서 일한지 20년이 넘은 분과 거의 다되가는 프린시펄 엔지니어가 두명이나 있고 시니어 엔지니어 한명, 그리고 나 포함 SDE2 두명과 SDE1 한명 총 엔지니어 6명에 매니저 1명이 우리 개발팀이다. 사우스 아프리카 태생인 영국인이자 캐나다인이자 미국인인 백인 아저씨와 태국 태생인 캐나다인이자 미국인인 동양인 아저씨 그리고 인도에서 넘어온지 5년쯤 된 인도인과 10년 넘어 미국인이 된 인도인 그리고 맥시코에서 넘어 온지 10년 넘어 미국인이 된 친구와  역시 멕시코에서 유학와서 대학에서 박사 따고 일하고 있는 20대 친구가 있다. 순수 미국인은 없지만 인종은 뭐 적당히 잘 섞여 있는것 같다. 맥시코에서 넘어온 젊은 친구들 빼고는 다들 가족이 있고 자녀가 있다. 사적으로 친해지긴 어렵지만 업무 상으론 다들 겸손하고 점잖은 편이고 무엇보다 내 영어가 엉망이어도 잘 이해해준다.

업무외 시간에는 주로 게임을 했다. 원랜 공부를 하거나 집안일을 해도 됐을법한 시간인데 그냥 그러고 싶었다. 한동안 와우를 했고 요즘엔 롤을 하고 있다. 배치고사를 2승 8패 하는 바람에 브론즈5에서 시작해서 이제 실버4인데 골드까지 갈 수 있을지 모르겠다. 북미섭에서 하고 솔랭은 서포터만 한다. 혹시 버스 태워 주실 분 있으시면 닉넴 keesun 친추 해주시길…

한동안 친구의 사이드 프로젝트에 참여했다. 부끄럽게도 도무지 프론트 기술을 감당할 수가 없었고 최근엔 다른 사이드 프로젝트를 시작했다. 이젠 스프링 마저 많이 변하고 잊어버려서 서버 기술만 감당하기도 힘들 지경이다. 그래도 하다보면 다시 익숙해 지겠지…

이제 여름이 끝났고 가을이 왔다. 반팔을 입기엔 꽤나 서늘한 날씨지만 아직까지 우기가 시작되진 않았다. 바람 불고 구름이 좀 있지만 화창한 편이고 공기는 쾌청하다. 한국에서 사나 여기서 사나 나는 어차피 별반 다를거 없는 삶이지만 공기가 좋은건 맘에 든다. 별거 안하고 그냥 숨만 쉬어도 뭔가 득보는 기분? 하지만 뭐 그것도 밖에 나갔을 때 얘기지 집에 처박혀 있음 거기나 여기나…

다시 블로깅을 시작해볼까 한다. 주로 사이드 플젝을 하면서 공부한 기술을 블로깅하지 않을까 싶다. 매날 나가는 서버 비용 5달라와 매년 나가는 도메인 비용이 아까워서라도…

Spring @PathVariable에 대한 에러 처리는 어떻게 할까

@RequestMapping(method = RequestMethod.GET, value = "/shuttle/{number}")
public ResponseEntity shuttle(@PathVariable int number) {
    //
}

뭐 이런 API를 만든다고 했을 때 스프링으로 number에 해당하는 부분을 손쉽게 타입 변환까지 거쳐서 int로 받아 사용할 수 있다. 그런데, 만약 입력한 값이 int 타입이 아니라면 무슨 일이 생길까. 스프링은 무슨 값이 오던 int 타입으로 타입 변환을 시도할 것이고 에러가 발생한다. 그리고 응답은 400으로 나가며 본문의 메시지는 비어있다. 깔끔한 API라고 보긴 어렵다. 뭔가 에러 상황에서는 왜 그런 에러가 발생했는지 클라이언트가 알기 쉽도록 안내해 주는게 좋은 API일 것이다. 그렇다면 무슨 에러가 발생하며 어떻게 처리할 수 있나 알아보자.

@Test
public void testMethodArgumentTypeMismatchException() throws Exception {
    mockMvc.perform(get("/shuttle/aaa"))
            .andDo(print())
            .andExpect(status().isBadRequest());
}

테스트 코드다. 실행하면 콘솔에서 이런 내용을 확인할 수 있다.

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /shuttle/aaa
       Parameters = {}
          Headers = {}

Handler:
             Type = me.whiteship.web.ShuttleController
           Method = public org.springframework.http.ResponseEntity me.whiteship.web.ShuttleController.shuttle(int)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = org.springframework.web.method.annotation.MethodArgumentTypeMismatchException

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 400
    Error message = null
          Headers = {}
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

뭐 이미 답이 다 이 안에 있다. 우선 응답이 썩 사용자에 친화적이지 않다는 것을 알 수 있고, 어떤 에러가 발생했는지도 알 수 있다. 그걸 잡아서 처리하면 되겠다.

@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
    return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
}

이런식으로 MATME를 잡아서 처리하는 핸들러 코드를 추가하면 원하는 메시지를 응답으로 담을 수 있다. 그냥 getMessage()를 꺼내서 보내는 것보다는 좀 더 손을 대야겠지만 그건 뭐 취향으로.. 그리고 이걸 해보며 드는 생각은 이렇다.

  • 이 예외 처리기는 공용으로 사용하기엔 뭔가 너무 일반화되어 있다는 느낌이 든다. 아무래도 컨트롤러 안에 두는게 좋겠다.
  • 저 예외 클래스가 제공하는 메서드중 getName()과 getParameter()를 잘 사용한다면 어쩌면 @ControllerAdvice에 둘 수도 있을지 모르겠다.
  • 타입이 String인 변수에 @PathVariable을 사용했다면 이 에러를 잡을 일이 없을 것이다.

DateTimeFormatter 사용할 때 파싱하려는 문자열의 Locale 정보도 꼭 주는게 좋다.

다음과 같이 DateTimeFormatter를 사용해서 “6:55 AM”이라는 문자열을 LocalTime으로 파싱하려고 한다.

@Test
public void testParsingTime() {
    String time = "6:55 AM";
    LocalTime localTime = LocalTime.parse(time, DateTimeFormatter.ofPattern("h:mm a"));
    assertThat(localTime.getHour(), is(6));
    assertThat(localTime.getMinute(), is(55));
}

코드에는 별로 크게 이상한 점이 보이지 않는다. 파싱하는 패턴이 올바른지 궁금하다면 샘플 데이터를 몇가지 더 살펴볼 수 있는데, 지금 내가 파싱하려는 데이터는 이런식이다.

7:15 AM, 8:00 AM, 11:05 AM, 1:00 PM, 1:35 PM

시간은 두자리를 꼭 채우지는 않기 때문에 h 하나만 사용하면 되고 시간과 분은 콜론(:)으로 구분하고 다음에 분은 두자리를 꼭 지키고 있기 때문에 mm을 사용했다.
마지막에 스페이스를 하나 띄고나서 a로 AM, PM을 파싱하고자 한다. 딱히 틀린건 없다고 생각했는데 막상 실행하면 에러가 난다.

java.time.format.DateTimeParseException: Text '6:55 AM' could not be parsed at index 5
    at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1947)
    at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1849)
    at java.time.LocalTime.parse(LocalTime.java:441)
    at me.whiteship.domain.ShuttleTest.testParsingTime(ShuttleTest.java:20)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:78)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:212)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:68)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)

이 에러를 해결하려면 Locale 정보를 추가해야 한다.

@Test
public void testParsingTime() {
    String time = "6:55 AM";
    LocalTime localTime = LocalTime.parse(time, DateTimeFormatter.ofPattern("h:mm a")
            .withLocale(Locale.ENGLISH)); // without Locale an exception may occur.
    assertThat(localTime.getHour(), is(6));
    assertThat(localTime.getMinute(), is(55));
}

현재 내가 작업중인 개발 환경의 JVM 환결 설정에 Locale 정보다 아마도 KOREA로 되어있는것 같다. 그래서 그 기본값에 따라 AM, PM 정보를 파싱하려는데, 한국어로는 그게 “오전”, “오후”로 표기하니까 AM, PM 정보를 파싱할 수 없어서 에러가 발생한 것이고, 데이터가 영어니까 영문 Locale로 파싱하도록 수정한 코드는 잘 실행된다.