top bar

글 목록

2020년 2월 6일 목요일

[JAVA] SynchronizedList vs CopyOnWriteArrayList

1. 배경


 Spring Bean의 기본 컨셉은 'Singleton' 이고, 이는 많은 사용자가 트래픽을 유발시키는 엔터프라이즈 환경에서 무분별한 객체 생성을 막아 메모리 관련 성능 이슈에 효율적이기 위함이다. 이러한 특징으로 인해 Spring Bean을 설계할 때는 반드시 '무상태'를 고려해야 한다.

하지만 최근 업무에서 특이사항이 생겼다. 아래와 같은 코드를 작성하게 된 것이다.

@Component
public class MetricSubmiter {

 private GaugeService gaugeService;

 public MetricSubmiter(GaugeService gaugeService) {
  this.gaugeService = gaugeService;
 }

 private List<SomeMetric> accumulatedMetricList = ??;

 public void accumulateMetric(SomeMetric someMetric) {
  accumulatedMetricList.add(someMetric);
 }

 @Scheduled(fixedDelay = 5_000)
 public void submitMetrics() {

  SomeMetric someMetric = getWorstMetric(); // from accumulatedMetricList

  if (Objects.isNull(someMetric)) {
   return;
  }

  gaugeService.submit("metric1", someMetric.getMetric1());
  gaugeService.submit("metric2", someMetric.getMetric2());
  gaugeService.submit("metric3", someMetric.getMetric3());
 }

 .
 .
 .
 .
}

 간단히 설명하면 이렇다. 특정 API에서 발생하는 metric 정보(예를 들어 응답시간과 같은)를 누적하고, 5초마다 스케줄러가 동작하여 누적된 리스트로부터 99 percentile의 metric객체를 가져와 GaugeService를 이용해 spring actuator의 'metrics'로 노출시킨다.

 'MetricSubmiter'는 Singleton이지만 'accumulatedMetricList'라는 상태를 가지고 있다. 따라서 API 요청을 처리하는 스레드들, 그리고 5초마다 동작하는 스케줄러가 모두 이 'accumulatedMetricList'에 접근하기 때문에, 해당 리스트를 동기화된 리스트로 생성해야 한다. 이런 상황에서는 어떤 Collection을 선택해야 할까?

2. 두가지 방법


동기화된 ArrayList를 생성하는 방법은 아래 두가지가 있다.

  • Collections.synchronizedList() // since 1.2
  • new CopyOnWriteArrayList (이하 COWAL) // since 1.5

 주석으로 언급했지만, 'synchronizedList'는 자바 1.2 버전 부터 존재한 것으로 보인다. 하지만 또다시 비슷한 컨셉의 COWAL이 등장한 것은 무엇 때문일까? 초창기 멀티 스레드 환경에서, SynchronizedList는 몇가지 한계를 가지고 있었다. 그 중 하나를 예를 들면, ’SyncronizedList’의 모든 읽기와 쓰기 동작은 이름에 걸맞게 ’synchronized’ 키워드로 동기화 되어 있는데, 이는 하나의 스레드만이 해당 리스트에 대해 읽기나 쓰기를 할 수 있다는 것이다. 어찌보면 매우 융통성이 없는 설계 라고 볼 수 있다.


그러므로, Java5 출시와 더불어 좀 더 유연하게 동기화된 리스트인 COWL가 등장하게 되었다.

3. 차이점


SynchronizedList와 뒤늦게 등장한 CopyOnWriteList의 주요 차이점을 정리해보자.

1) Locking of threads

 SynchronizedList는 읽기와 쓰기 동작시 'synchronized' 키워드를 사용하기 때문에 리스트 자체에 lock이 걸린다. 반면 COWL는 그 이름에서 알 수 있듯이 모든 쓰기 동작(add, set, remove, etc)시, 원본배열에 있는 요소를 복사(Copy)하여 새로운 임시배열을 만들고 이 임시배열에 쓰기 동작을 수행 후 원본배열을 갱신한다. 이 덕분에 읽기(get, etc) 동작은 lock에서 자유로울 수 있게 되고, 이는 읽기 Performance에서 COWL가 SynchronizedList보다 우월한 강력한 이유다.

2) Wirte Operations

 COWL은 쓰기 동작 수행시 명시적 락(ReentrantLock)을 사용한다. 결국 두 종류의 Collection 모두 이 동작에서 lock이 걸리는 것이다. 하지만 COWL는 SynchronizedList보다 쓰기 동작이 느리다. 이미 이야기 했지만 COWL은 비용이 상대적으로 높은 배열 복사 작업을 하기 때문이다. 아래를 보자.



더 얘기 할 것도 없다. 단순 동기화만 적용된 SynchronizedList보다 훨씬 더 많은 Overhead가 발생하는 건 당연한 이야기...

3) Behavior during Modification

일반적인 ArrayList는 'fail-fast iterator'이다. 아래의 코드를 보자.



위 코드와 같이, 반복자 동작(next메서드 호출) 도중 상태가 변경 되는 경우 'ConcurrentModificationException'을 발생 시킨다.

하지만 COWL은 아래 처럼 'fail-safe iterator'이다.



COWL가 가지고 있는 'COWIterator'의 next메서드는 상대적으로 심플한 모습이다. 딱 보면 알겠지만, 이것이 상태 변경 상황에서 Exception을 발생 시키지 않고 다시 말해 'fail-safe'한 이유는 복사한 임시배열의 참조를 가지고 있는 'snapshot'을 대상으로 반복을 수행하기 때문이다.

4) Iterating within block

3번에서 이야기한 맥락을 고려해 보면, SynchronizedList는 동기화 블록 안에서 반복이 이루어진다. 하지만 COWL는 snapshot을 대상으로 명시적 락 '밖'에서 안전하게 반복을 수행한다. 이것은 당연히 반복 성능에도 영향을 미칠 것이다.

4. Wrap up


 정리해 보자. SynchronizedList는 단순히 'synchronized' 키워드를 사용한 동기화된 List이다. 따라서 모든 읽기, 쓰기 동작에 lock이 작용된다. 반복 작업 또한 동기화 블록 안에서 수행 된다. 때문에 읽기보다 쓰기 동작이 많고, 크기가 큰 리스트의 경우에 적합할 것이다.

 반면에 COWL는 특유의 동작 방식으로 인해 반복작업(iterating)과 같은 읽기 동작의 Performance가 좋다. 다만 또 그 특유의 동작 방식으로 인해 쓰기 동작에서 상당한 Overhead가 발생한다. 따라서 쓰기보다 읽기 동작이 많고, 크기가 작은 리스트에 적용하는 것이 바람직하다.


.   .   .


 이제 처음의 상황으로 돌아가 보자. 'accumulatedMetricList'의 쓰기 동작은 해당 API로 몰리는 '모든' Request에서 수행된다. 하지만 읽기 동작은 스케줄러에 의해 5초마다 수행된다. 읽기보다 쓰기가 훨씬 많으며, 모든 요청 처리시 SomeMetric 객체가 리스트에 추가 되므로 크기 또한 급속도로 커진다. 이렇게 명백한 이유로 인해 결국 'SynchronizedList'를 선택하게 되었다.

 'Tomcat'과 같은 서블릿 컨테이너 및 각종 프레임워크 덕분에, 직접 작성하지 않고도 알게 모르게 멀티 스레드의 혜택을 보게 된다. 때문에 아무 생각 없이 멍때리고 있다가는 내가 작성하고 있는 코드가 이러한 멀티 스레드 환경에서 동작한다는 사실을 망각할 수 있다. 이것은 정합성이 어긋난 어플리케이션 상태를 유발시키는 실수로 이어질 수 있기에, 그 위험성을 내포하고 있다.

이번 글을 포스팅 하면서, '멀티 스레드 환경'에서의 동기화된 Collection 사용 뿐 아니라, Spring Singleton Bean 설계 또한 고찰 할 수 있었다. 우리는 여러 스레드들과 동거동락한다는 사실을 잊지 말아야 한다.




2020년 1월 29일 수요일

오랫만에 하는 생각 정리

 잘 나가던(내가 아니고) E-커머스 회사의 개발자 생활을 마치고, 현 직장으로 이직한지도 1년하고도 몇개월이 지났다. 이직한 곳은 대기업 통신사. 이직을 결심했을 당시 주변 사람들에게 가장 많이 들었던 말은 ‘그 회사는 내가 행복한 곳이 아니라 내 가족이 행복한 회사’라는 것이었다. 뉘앙스를 조금만 생각해봐도 알겠지만 다소 부정적인 이야기였다.

직장인으로서 행복이 뭘까. 아니, 개발자로서 행복은 뭘까. 이 부분에 대해서는 나 자신만의 확고한 신념이 있다. 물론 정답은 아니겠지만.

 나는 소프트웨어 개발을 좋아한다. 0과 1로 이루어진 컴퓨터 세상이 흥미롭고, 내가 노력과 공을 들여 작성한 나름 깔끔하고 우아한(?) 코드가 ‘제대로’ 동작할 때 희열을 느낀다. 클라우드 기반의 MSA가 대세인 요즘 (한편에는 모놀리식으로 돌아가자는 이야기도 있긴 하지만...), 관련하여 다양한 기술스택을 배우고 적용하는 것 또한 재미있는 도전 거리이다. 아마 죽을때까지(?) 공부해야 할 것 같은 컴퓨터 공/과학의 이론들도 빼놓을 수 없겠다. 마지막으로 방금 열거한 모든 지식을 동료들과 공유하고 함께 성장 한다면, 개발자로서 이보다 더 기쁘고 성취감이 있는것은 세상에 없지 않을까.

이러한 생각들에 비추어 볼때, 과연 내가 현재 근무하고 있는 곳은 개발자로서 이러한 성취감과 행복을 느끼게 해주는 곳일까? 그동안 느꼈던 몇가지를 짧게 정리하고자 한다.


“여기 직접 개발 하나요?”



 면접 말미에 ‘궁금한 점 없어요?’에 대한 내 질문이었다. 아마 많은 사람들이 이 회사의 ‘개발 조직’에 대하여 같은 의문을 품을 것이라 생각한다. 왜냐하면 이 회사의 ‘소프트웨어 개발’ 이라고 하는 것은 거의 대부분 ‘외주 개발’이라고 소문이 나 있기 때문이다. 하지만 다행히도 돌아온 답변은 '네 개발 많이 합니다’ 였다.

 이 회사에서 말하는 것은 바로 ‘내재화’. 뭐 쉽게 말하면 서비스 개발을 이 회사에 소속되어있는 개발자들이 ‘직접’ 한다는 거다. 입사를 하고 몇달을 지내다보니, 내가 들었던 답변은 확실히 맞는 말이었다. 물론 한창 개발자들을 여기저기서 뽑고 있는 상황이었기 때문에 인력이 부족할 수 밖에 없었고, 이를 (같은 계열사인) ‘Business Patner'의 인력들로 대체하기는 했다. 하지만 (내가 우려했던) 그들에 대한 일방적인 ‘관리’를 한 것이 아니라, 동료로서 함께 개발 업무를 진행했다는 것은 참 다행스러운 일이었다.

 그럼에도 불구하고, 내가 속한 조직과 또 몇몇개의 조직을 제외하면 서비스 개발의 거의 대부분이 아직까지도 ‘외주 업체’의 의존도가 높은 것이 사실이다. 태생이 IT 서비스 회사가 아닌 소위 말하는 ‘텔코(Telco)’이기 때문에, 회사의 이러한 행보가 하루아침에 변화 할 수 없다는 것을 감안하더라도 아쉽기는 하다.


열려있는 다양한 기술스택



 대기업에서는, 기존 서비스를 보다 ‘더’ 안정적으로 운영하기 위해 요즘 유행하는 도구나 기술에 대해 보수적인 태도를 취할 것이라 생각했다. 하지만 막상 내가 있는 조직의 기술 스택을  살펴보니 내 생각이 조금은 틀렸다는 것을 알게 되었다. 대부분의 주요 컴포넌트들이 'Spring Boot’로 구동되고 있었고, RabbitMQ와 Kafka, Redis 등 각종 미들웨어 뿐만 아니라 'Spring Cloud'에서 제공하는 다양한 도구들을 이용한 소규모 MSA 아키텍쳐까지 존재했다. 조금 투박하긴 하지만 Jenkins + Ansible 을 이용한 배포는 나름 편리했으며, 로깅과 모니터링을 위해 구축되어 있었던 ELK, TICK 스택의 도구들 또한 견고했다. 또 조만간 AWS나 GCP 등 Public Cloud를 도입할 것이라는 이야기가 들려오고 있다.

 뭐 여기까지는 새롭다고 하기엔 요즘 여러 회사들이 으레 사용하는 기술 스택(또는 플랫폼)이지 않느냐 라고 할수 있을지 모른다. 하지만 내가 이 회사에 입사해서 생각보다 새로운 기술적용에 열려있다고 강하게 느낀 것은 바로 ‘스칼라’ 였다. 내가 속한 팀이 담당하고 있는 대중교통 시스템에는 다양한 컴포넌트가 있는데, BIS (Bus Infomation System)도 그 중 하나이다. 이름에서 알 수 있듯이 버스의 실시간 위치정보와 도착정보를 제공해주는 녀석이다. 당연히 대중교통 서비스에서 굉장히 중요한 축을 담당하고 있는 이 컴포넌트가 전부 다 ‘스칼라’로 되어 있었다.

 스칼라가 이제 막 출시한 따끈따끈한 언어는 아니다. 하지만 여전히 자바와 스프링이 주류인 한국 IT 업계로 비추어 볼때, ‘대기업’인 이 회사에서 ‘비주류’인 언어로 중요한 컴포넌트를 구축했다는 것은 놀라운 사실이었다. 물론 이것은 어디까지나 내 기준. (스칼라 도입의 히스토리와 그 효과는 논외로 하겠다)


개발문화는?




출처:  Wikipedia

 ‘문화’라는 단어는 위에서 언급되는 것처럼 수많은 컨텍스트에서 달리 해석되는 묘한 단어이다. 한편, 개발자들 사이에서도 ‘개발 문화’라는 말은 오래 전 부터 그들만의 의미체계를 가지고 있어왔다. 그렇다면 이 ‘개발 문화’는 무엇일까?

 지극히 사견일 수 있지만, ‘개발 문화’는 ‘좋은 품질의 소프트웨어를 ‘함께' 만들고자 하는 일련의 행동’이다. 모두가 잘 아는 ‘코드리뷰’나 ‘Pair Programming’에서 부터 여러가지 효과적인 설계 방법론의 도입, DevOps와 SRE에서 말하는 수많은 원칙과 도구들, 그리고 ‘애자일’까지.. (언급하지 않은 모든 것들 포함) 이 모든 것들이 결국은 좋은 소프트웨어를 만들고 유지보수 하기 위한 좋은 개발문화 속에 있는 것들이 아니던가.

 사실 위에서 언급한 요소들을 완벽하게 실천 및 적용하는 조직은 많지 않을 것이라 생각한다. 그만큼 좋은 ‘개발 문화’는 쉽게 바꿀 수도, 만들어 낼 수도 없는 것이다. 다만 이러한 것들을 ‘추구’하는 분위기라면 이야기가 다르다. 처음엔 뜬구름 잡는 소리처럼 들려도, “좋은 품질의 소프트웨어를 만들고자 하는 일련의 행동”들을 추구하고 권장하는 문화에 속해 있는 개발자의 사고는 거기에 맞추어 지기 마련이며, 그것은 개발자 개인의 성장과 그것을 통한 조직의 성장, 서비스의 성장으로 귀결된다. 요즘 많은 메이저 IT 회사들이 ‘기술 블로그’나 여러 기술 세미나를 통해 자신들만의 개발문화, 즉 ‘분위기’를 홍보하는 것은 바로 이런 것들을 어필해서 좋은 개발자들의 이목을 끌려는 목적이 크다고 본다.

그래서 이 회사의 개발 문화는 어떠할까? 사실 ‘전사' 차원에서 ‘개발자’와 ‘기술’을 중시하는 문화를 장려하고 있진 않은것 같다. 그래서 이 회사의 개발문화에 대해서는 딱히 할말이 없다. 하지만.. (아래 계속...)


좋아질 것 같다.



 위에서도 언급 했지만 내가 속한 개발 조직은, (내가 알기로) 대략 17년도 말 즈음에 조직 재정비를 시작하여 18년도, 그리고 작년 19년도에 굉장히 많은 개발자를 채용했다. 특히나 N사, K사, C사, W사 등 이름만 대면 알만한 IT 회사로부터 좋은 개발자 분들이 많이 오셨다. 물론 유명 IT 회사 출신이라고 무조건 좋은 개발자라고는 할 수는 없겠으나, 그래도 기술을 중시하는 기업문화 또는 개발문화를 경험한 분들이 많다는 것은 긍정적인 변화의 가능성을 높일 수 있는 것이라 생각한다.

 작년 말 조직 개편으로 인한 어수선한 분위기 속에 반가운 메일을 받았다. 조직 내 어느 한 팀에서만 비정기적으로 시행 하던 ‘Tech Meetup’을 이번에 조직 전체로 확장하게 되어서 세션 발표자와 참석자를 모집한다는 내용이었다. ‘기술 공유’ 또한 아주 좋은 개발 문화의 한 요소라고 생각하기에, 매우 환영 할 만한 일이었다.  게다가 더 큰 범위의 기술조직 차원에서 ‘Tech Culture’를 개선하려는 움직임도 보인다. 이러한 일련의 상황들로 비추어 볼때, '개발 문화'는 계속 좋아 질 수 있지 않을까?


정리


그래서, 글머리에 썼던 '이 회사는 개발자가 다니기 행복한 회사인가?'라는 자문에 대한 자답은, '잘 모르겠다'이다(...). 조금은 허탈한 결론일 수 있겠지만, 그만큼 '행복'이란 것은 어떤 특정한 환경이나 상황으로 쉽게 정의할 수 없다는 것이 내 생각이다.

 결론적으로, 주어진 환경에서 내가 할 수 있는 최선을 다해 일하고, 동시에 나만의 행복을 찾아가는 것이 필요하다는 생각이 든다. 물론 필요하면 어떤 방식으로든 환경을 바꿀 수 도 있다.

 몇년전(...)까지만 해도 나름 열심히 블로그 포스팅을 했었다. 뭐 그때도 내 블로그를 찾아오는 사람들은 많지 않았지만, 누가 보지 않더라도 글이 하나 둘씩 차곡차곡 쌓여가고 내가 공부한 것이 내 것이 되는 것을 느낄때 참 즐겁고 뿌듯했던 기억이 있다. 배우고, 공유하고, 적용하는 것. 그것이 내가 추구했던, 지금도 추구하는 행복임을 다시금 떠올리며 글을 마친다.





2018년 7월 22일 일요일

[JAVA] Java9 모듈 프로그래밍 - 책 리뷰

이 글은 'Java9 모듈 프로그래밍(코시크 코타갈 지음, 한빛 미디어)'에 대한 리뷰 입니다. 책의 내용을 그대로 적은 것 보다는, 읽고 난 뒤 해당 내용에 대한 이해를 바탕으로 사견을 덧붙여 작성했습니다.






 자바 세상의 본격적으로 '함수형 프로그래밍'의 시작을 알린 '자바8'이 출시 된지도 꽤 많은 시간이 지났다. 급기야 '자바10'까지 발표되었으니, Oracle이 Sun을 인수하면서 진화론적으로 정체되어 있던 자바에 꽤나 공을 들이고 있는 모양세다.

 자바10의 Spec을 대충 훑어보니, 지역변수의 타입추론 기능과 G1 GC가 최적화 되는 등 소문대로 Minor한 변화가 이루어 진 것 같다. (여기서 Minor 라는 것은 '패러다임의 변화' 수준이 아니라는 말이다. 오해 ㄴㄴ) 이 시점에서 나의 눈에 들어온 것이 지금 리뷰하려고 하는 'Java9 모듈 프로그래밍' 이라는 책이었다.

 나는 기술서적을 고를때 대략 3가지 정도를 고려하는데, (물론 Yes24의 별점과 리뷰를 보는건 기본) 첫번째로 책의 디자인(...), 두번째로는 저자가 누구인지, 세번째로는 역자가 '테크 트랜스 그룹 T4'가 아닌지 이다. 세번째 이유는 해외에서 극찬을 받고 있는 'Spring In Action' 이라는 책을 해당 번역팀(?)이 번역한 역서를 읽다가, 메서드를 '방법'으로, 오버라이딩을 '과적(...)'으로 번역한 부분을 읽고 책을 던져 버릴 뻔 했어서다. 그래도 내용 자체는 좋으니 완독은 했다...

 아무튼간, 조금 더 최신의 자바에 대해 알고 싶었고, 위의 3가지 조건을 만족 했으며, 자바9의 가장 큰 변화라고 할 수 있는 '모듈'을 다뤘기에 해당 책을 구입하여 2회독을 한 뒤에야 이렇게 리뷰를 남기게 되었다. (특히나 번역이 너무나 매끄럽고 좋았다. 일면식도 없지만, 이 책을 번역해주신 역자께 감사드린다)

이 책은 타이틀이 모든것을 말해준다.


"모듈 프로그래밍"



 소프트웨어 개발에서 '모듈'이라는 용어는 매우 광범위하게 사용된다. 이는 크게 볼때 '시스템 아키텍쳐 관점에서의 컴포넌트', 작게는 '캡슐화된 클래스'가 될 수 있을 것이다. 하지만 이 책에서 말하는 '모듈'이라는 것은 캡슐화된 클래스의 모음, 즉 '라이브러리'에 가깝지만 '더욱 더 강력한 캡슐화가 이루어진 라이브러리'라고 할 수있다. 여기서 말하는 '더욱더 강력한 캡슐화'라는 것은 글을 써 내려가면서 언급하려고 한다.

저자는 책의 도입부에서 기존 자바를 사용할 때 만날 수 있는 2가지 문제점을 실용적이고 실감나는 사례를 들며 풀어 나간다. 여기서 2가지 문제점이란 바로 '완전하지 않은 캡슐화''빌드된 바이너리의 (혹시 모를) 불안정성'이다.

 '완전하지 않은 캡슐화'라는 것은 무엇인가? 더 정확히 말하면 '라이브러리 외부'로 부터의 완전하지 않은 캡슐화이다. 기존의 자바에서는 '접근 지정자(또는 제어자)' 키워드를 이용해 '캡슐화'를 실천한다. 하지만 '접근 지정자'로 할 수 있는 캡슐화의 범위는 멤버 필드, 그리고 메서드뿐이다. 저자는 바로 이 점을 문제 삼는다. 여기서 문제란 라이브러리 외부로부터 오는 '타입' 그 자체에 대한 접근을 막을 수 없다는 것이다. 타입 자체를 캡슐화 하려면 패키지 단위로 해당 타입을 묶어야 하는데, 이것은 해당 타입을 필요로 하는 라이브러리 내부의 클래스 조차 접근하지 못하게 만드는 설계의 한계를 가져오고 만다.

 '빌드된 바이너리의 불안정성'은 바로 'NoClassDefFoundError' 오류로 한번에 설명이 된다. 물론 이 오류를 실무에서 보는 것은 아주 드물다. 하지만 그렇다고 해서 빌드 도구인 Gradle이나 Maven이 필요한 모든 Compiled Class들이 포함된 바이너리를 빌드 해 준다는 '보장'은 없다. 개발자의 실수로 Runtime에 필요한 라이브러리에 대해 빌드 스크립트에서 의존성을 'Test' 범위로 지정해 버린다거나, 빌드 도구의 실수(버그나 오류)로 몇몇 필수적인 Compiled Class가 누락될 수도 있다.


더 나은 캡슐화


 기존의 자바에서도 온갖 디자인 패턴을 통해 캡슐화 시도들이 있어 왔지만, 이것이 사용하지 말아야 할 클래스(예를 들어 'sun.misc.Unsafe' 같은)의 사용까지 막을 수는 없었다는게 저자의 설명이다. 이제 자바9는 비로소 '모듈' 시스템'을 도입 했다. 이것은, 클래스들이 강하게 결합된 기존의 모놀리식(Monolithic) 설계를 버리고 진정한 의미의 독립적인 '모듈' 작성을 가능케 한다.

 자바9는 뿌리까지 바뀌었다. 프로젝트 'Jigsaw'를 통해 자바 플랫폼 API 자체가 모듈 프로그래밍으로 재작성 되었고, 이제 자바 프로그램은 60메가 바이트에 달하는 'rt.jar' 를 통째로 포함하여 빌드할 필요없이 꼭 필요한 플랫폼 모듈만 포함하여 빌드 될 수 있다.

 또한 자바 응용 프로그램 개발자는  자바 프로젝트의 전통적인 소스 디렉토리 구조(src/main/java 와 같은)가 아닌,  'src' 디렉토리 하위에 모듈 별로 'root 디렉토리'를 생성하여 모듈 코드를 작성하게 된다. 그리고 각 root 디렉토리에는 'module-info.java'라는 특수한 이름을 가진 모듈 기술자(module descriptor)파일을 작성하게 되는데, 이 파일에 'requires' 키워드를 이용하여 타 모듈에 대한 의존성 설정을 하고,  'exports' 키워드를 이용하여 현재 모듈이 어떤 패키지를 외부에 노출(export)할 것인지를 정의하게 된다.

 따라서 라이브러리 외부의 프로그램은, 자신이 사용하는 라이브러리의 모듈에서 'export' 하는 패키지의 'public' 클래스만을 접근할 수 있게 된다. 결론적으로 기존 접근 지정자 사용에 '모듈 시스템'이 더해져 더욱 더 꽁꽁 감싸여진, 앞서말한 더욱 더 강력한 캡슐화가 가능해진 것이다.


신뢰성 있는 빌드  


 우리가 사용하는 빌드 도구들은 매우 유용하다. Maven이나 Gradle은 의존성 라이브러리를 자동으로 내려받아주고, '컴파일 - 테스트 - 아카이빙 - 리포팅' 에 이르는 전체적인 빌드의 자동화를 제공한다. 하지만 앞서 언급 했듯이, 이것이 빌드된 바이너리의 안정성을 보장하지는 못한다.

 어찌 되었든 JRE가 누락된 Compiled Class에 대해서 어느 시점에 로딩을 시도하여 'NoClassDefFoundError' 오류가 발생되는 것을 목도하지 않는 이상, 그 전에 모든 필요한 Compiled Class들이 바이너리에 안정적으로 포함되어 있는가를 확인할 수 있는 방법은 사실상 없다. (빌드된 바이너리를 decompile하여 import문에 정의된 클래스들이 잘 포함되어 있는지 일일히 확인하는 정신나간 사람은 없을 것이다) 이러한 문제에 대해 '모듈 시스템'은 어떤 솔루션을 제시 할까?

 자바9 '모듈 시스템'을 이용하여 작성된 바이너리를 구동할 때, 바로 '모듈 기술자'를 통해 해당 기술자에 명시된 모든 의존성을 확인한다. 여기서 중요한점은, 의존성을 확인하는 시점이 프로그램을 구동하여 자바 어플리케이션이 '초기화' 될 때 라는 것이다.

 이것은 우리에게 빌드된 바이너리에 대한 'Fail-Fast'를 제공한다. 특정 기능을 수행하지 않고는 확인할 길이 막막한 'NoClassDefFoundError'와 같은 Runtime 오류를 프로그램 구동시에 바로 잡아주어, 해당 오류에 대한 대처를 빠르게 할 수 있게하고, 그것을 통해 빌드된 바이너리의 신뢰성을 더할 수 있다는 것이다. 이는 '모듈 시스템'의 매우 중요하고 강력한 장점이라고 할 수 있다.

. . .


 지금 까지 이야기한 2가지 담론이 이 책을 이루는 뼈대라고 할 수 있다. 이 책에서는 이러한 뼈대를 바탕으로 독자로 하여금 간단한 '주소록' 프로그램을 모듈을 사용하여 예제로 작성하게 하고, 책을 읽어 가는 동안 점점 발전 시키며 '모듈 시스템'의 여러 기능들을 소개 하는데,

  • 자신이 의존하는 모듈의 의존성을 암시적으로 포함하는 '전이적 의존성'
  • 자신의 특정 패키지를 특정 모듈에만 export하는 '제한적 export'
  • 여러 모듈의 기능을 모아주는 '집합자 모듈'
  • 모듈간 loose coupling을 위한 간접 계층인 '서비스'

등의 기능들이 그것이다

 또한 다양한 모듈 설계 패턴, 링킹을 통해 어떻게 모듈이 해석되고 jar가 아카이빙 되는지, 또 자바9 이전의 레거시 버전에서 어떻게 마이그레이션 하는지에 대한 내용이 이어지는데 설명하는 수준이 깊지만 이해하기가 어렵지 않았다. (언제가 될진 모르겠지만) 자바9로 마이그레이션 하게 된다면 유용한 참고 자료가 될 것 같다.

. . .


리뷰를 마치며


 내가 다니는 회사는 매달 5만원의 도서 구입 쿠폰을 제공하기 때문에 기술 서적을 많이 사고, 많이 읽는 편이다. 하지만 이렇게 책 내용 및 번역의 완성도가 높아 술술 읽혔던 책은 'Java8 In Action' 이후 꽤 오랫만인것 같다.

 자바9의 '모듈 시스템'은 플랫폼 자체를 재작성할 정도로 너무나 큰 변화다. 따라서 실제 운영되고 있는 서비스에 적용하기에는 매우 높은 비용과 시간이 걸릴 것 같다. 사실, 서비스 레벨의 자바 프로그램에서 '모듈'이라는 것은 그 필요성이 간절하지도 않아 보인다. 어찌 되었든, 자바의 신선한 변화를 심도있게 엿볼 수 있었기에 매우 좋은 책을 읽은 것은 확실하다. (자바가 기존의 동적 언어들과 뭔가 비슷해져 가고 있는건 기분탓일까..)




2018년 7월 3일 화요일

[Javascript] Code Snippet - 객체로 Query String 만들기


Javascript 객체를 Query String으로 변환해보자.
Arrow Function을 인자로 받는 배열 고계함수를 사용한다.
buildQueryString(parameters) {
  return encodeURIComponent(
    Object.keys(parameters)
    .map(key => key + '=' + parameters[key])
    .join('&')
  );
}
결과 Query String을 'encodeURIComponent' 로 감싸는 것을 잊지 말자.
개발하다가 남겨두면 좋을 것 같아 메모.

2017년 12월 14일 목요일

[Spring] Request Bean Scope

스프링에서의 Bean이 기본적으로 'Singleton'이라는 것은 누구나 안다. 하지만 Singleton으로 관리되는 Bean의 Lifecycle을 'Scope'어노테이션을 이용해 제어 할 수 있다. 스프링의 Bean Scope는 총 5가지 인데, singleton, prototype, request, session, global session이 그것이다.

이 중에서, 조금은 특이한 방법으로 관리되는 request scope의 bean에 대해서 살펴 보고자 한다.

가령, 아래와 같은 HelloMessageGenerator 라는 클래스가 있다고 하자.

class HelloMessageGenerator {
    public String getMessage() {
        return "Hello " + this;
    }
}

HelloMessageGenerator의 getMessage메소드를 통해서 문자열을 가져오는데, 자바에서 유일하게 오버로딩 된 '+' 연산자를 통해서 'Hello [참조값]' 이라는 문자열을 반환한다. 이를 통해 getMessage를 호출하는데 사용되는 인스턴스의 참조값을 알 수 있다.

그리고 아래처럼 Spring Configuration에, 해당 클래스의 Bean 설정 코드를 작성한다.

@Bean
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public HelloMessageGenerator requestMessage() {
    return new HelloMessageGenerator();
}

HelloMessageGenerator의 빈 설정을 보면 Scope어노테이션에 'proxyMode' 라는 것이 눈에 띈다. Aop를 적용하려는 것도 아닌데 왜 proxyMode를 설정해야 할까?

'request scope' 라는 것은 말그대로 Bean의 lifecycle이 request와 같다는 것이다. 때문에 해당 Bean은 '요청 -> 응답'이 끝나면 제거 되며, 요청이 없으면 생성되지 않는다. reuqest scope의 이러한 속성 때문에 proxy를 설정해야 하는데, 이유는 이렇다. 

일단 아래처럼 Controller를 작성해 보자.

@Controller
public class ScopesController {
    @Resource(name = "requestMessage")
    HelloMessageGenerator requestMessage;
 
    @RequestMapping("/scopes")
    public String getScopes(Model model) {
        requestMessage.setMessage("Good morning!");
        model.addAttribute("requestMessage", requestMessage.getMessage());
        return "scopesExample";
    }
}

서블릿 컨테이너에 스프링 기반의 웹 어플리케이션이 로딩될때 (쉽게 말해 톰캣을 구동할때) 각 빈이 생성되고 빈 컨테이너에 등록되며 'Dependency Injection'이 일어난다. 한마디로 이 시점에 각 객체의 의존관계를 바탕으로 빈이 주입 된다는 것이다. 위 경우는 ScopesController에 HelloMessageGenerator의 인스턴스가 주입되어야 한다. 하지만 해당 빈은 request scope이기때문에 생성할 수 없다. 때문에 'proxy'를 임시적으로 생성하여 의존성 주입을 수행하는 것이다. 그 후에 진짜 요청이 들어오면 다시 초기화를 하여 빈을 사용하게 된다.

proxy는 일단 그렇고, 결과적으로 위 코드를 수행하면 view단에 찍히는 'requestMessage' 문자열이 요청 때마다 다른 값으로 노출 될 것이다. 참고로 session, global session scope 설정 시에도 proxyMode를 지정해 줘야 하는데, 원리는 request scope의 경우와 같다.

정리 끗.

참고 - http://www.baeldung.com/spring-bean-scopes

2017년 11월 11일 토요일

[JAVA] 로 풀어본 '식사하는 철학자들' 문제 (2) - 세마포어

세마포어 (Semaphore)



 '세마포어' 란, 1965년 E.W 다익스트라가 고안한 개념으로서, down과, up 이라는 두개의 함수로 조작하는 정수변수이다. 다시 말하면 Mutual Exclusion(Mutex, 뮤택스)를 조금 더 고도화 하여 구현한 개념이라고 할 수 있는데, 임계영역에 대한 lock을 수행한다. 참고로 세마포어는 뮤택스이지만, 뮤택스라고 해서 모두 다 세마포어는 아니다. (필자가 이해 하기론..)

 어떤이는 세마포어를 열쇠고리에 걸려있는 열쇠의 갯수라고 표현 하더라. 흠.. 그것도 적절한 표현 인 것 같다. 세마포어에 대한 down 연산을 할때는 세마포어 값이 0보다 큰지 검사한다. 만약 그렇다면 이 값을 감소시키고, 스레드는 임계영역에 대한 수행을 계속한다. 만약 이 값이 0이면 스레드는 down의 수행을 완료하지 않고 즉시 잠들게 된다. 값을 검사하고, 변경하고, 경우에 따라 잠드는 이러한 모든 동작은 분할할 수 없는 하나의 원자적 행위(atomic action) 이다. 만약 이 연산에 원자성이 보장 되지 않는다면, 세마포어를 적용한다 하더라도 동시성의 경쟁조건(race condition)에 의한 교착상태가 해결된다는 보장을 할 수 없을 것이다.

 JAVA에도 세마포어 구현체가 있다. 동시성의 영웅, 더그 리(Doug Lee) 형님께서 구현하신 'java.util.concurrent.Semaphore' 클래스가 그것이다. 이 클래스는 down 연산에 'acquire' 메소드를, up 연산에는 'release' 메소드를 사용한다. 의미적으로는 이러한 네이밍이 좀 더 직관적인 것 같다.

 위 클래스의 인스턴스를 생성할때, 세마포어의 'permits', 그러니까 열쇠의 갯수를 생성자의 파라메터로 받는다. 그리고 앞서 말한 acquire 와 release 메소드를 통해서 해당 값을 변경한다. 필자가 예상하기로는 값 변경 작업에 'AtomicInteger' 와 같은 연산의 원자성을 보장하는 방식을 사용 할 것 같았는데, 소스를 살펴보니 아래와 같은 'compareAndSetState' 라는 메소드를 사용 한 것이 눈에 띄었다.

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

 간단하게 말하면 'expect' 값을 'update' 값으로 swap 하는 작업이다. 주석을 살펴보니, 정확히는 모르겠지만 AtomicInteger와 같은 명확한 방식을 사용하지 못했던 히스토리가 있었던 것 같다. 그래서 'the lesser of evils'(차악.. 이라고 읽으면 되려나?)로 native 라이브러리를 사용해서 구현 했다고 한다. 'Unsafe'라는 객체는 java doc 설명도 제대로 안나와 있어 자세히 살펴보진 않았지만 어쨌든 native 라이브러리 이고, 뭐 값을 조작하는 역할을 하는 것 같다.


세마포어의 적용



 서두가 좀 길었다. 그리고 본 단락은 짧을 것이다 -_-; 앞서 살펴본 '식사하는 철학자들' 문제에 세마포어를 적용해보자. 적용 방식은 아주 간단하다. 생각하고(think), 왼쪽 오른쪽 포크를 잡고(take), 먹고(eating), 양 포크를 다시 테이블위에 놓는(put) 행위중에서 'think' 의 바로 다음 5가지 동작을 이진(binary) 세마포어로 묶는 것이다.

먼저 static 변수로 세마포어를 정의한다.

class Philosopher implements Runnable {
    
    .
    .
    public static final Semaphore semaphore = new Semaphore(1);
    .
    .

'이진' 세마포어 이므로 'permits'의 값을 1로 설정한다. (1과 0의 값만 갖도록)

그리고 아래와 같이 철학자들의 행위를 세마포어로 묶어보자.

@Override
public void run() {
    while (true) {
        think();
        try {
            semaphore.acquire();

            takeFork(this.number);
            takeFork((this.number + 1) % 5);
            eat();
            putFork(this.number);
            putFork((this.number + 1) % 5);

            semaphore.release();
        } catch (InterruptedException e) {
            System.out.println("Exception occured!");
            e.printStackTrace();
        }
    }
}

포크를 잡는 행위와 스파게티를 먹는 행위, 그리고 포크를 다시 놓는 행위는 임계영역(critical region)이라고 할 수 있다. 이러한 임계영역을 이진 세마포어로 보호함 으로서 '상호배제' 매커니즘을 적용하면 교착 상태가 발생하지 않는다. 하지만 프로그램을 실행하고 보니 이상한 점이 보였다.























흠.. 그렇다. 뭔가 5명의 철학자가 골고루 스파게티를 먹는 것 같지 않다. 그리고 이론상, 5명의 철학자와 5개의 포크가 있다면 동시에 최대 2명의 철학자가 스파게티를 먹을 수 있어야 한다. 한마디로 지금의 상황은 CPU가 그만큼 효율적으로 스케줄링을 하고 있지 않다는 것이다. 따라서 세마포어에 열쇠의 갯수(permits)를 1에서 2로 늘려 보았다. 이제 더 이상 이진 세마포어는 아니다.

public static final Semaphore semaphore = new Semaphore(2);

그리고 다시 프로그램을 수행했다.























이제 조금 더 고른 스케줄링이 되는 것으로 보인다. 주의해야 할 점은 세마포어의 'permits' 값을 5 이상으로 한다면, 임계영역이 보호되는 것은 말짱 도루묵이라는 것이다. 이는 세마포어를 적용하지 않은 것과 같은 상황이다.

일단 세마포어는 여기까지..



2017년 10월 29일 일요일

[JAVA] 로 풀어본 '식사하는 철학자들' 문제 (1) - 시작

(명시적 'lock'을 사용하는 것으로 예제 전면 수정...)

 요즘 '동시성'에 대해 관심을 가지고 짬을 내서 공부를 하다보니, 학부시절 운영체제를 수강할때 배웠던 '식사하는 철학자들' 문제가 불현듯 떠올랐다. 그래서 이번에는 이 문제를 JAVA로 어떻게 구현하고 해결 하는지 포스팅해 보려고 한다.

알 만한 사람들은 다 아는 아주 고전적이고 유명한 문제라, 개념 자체에 대해서는 간단하게만 정리하고 넘어 가겠다.

다섯명의 철학자가 원형테이블 주위에 앉아있다. 스파게티 한 접시가 각 철학자에게 주어진다. 이를 먹기 위해서는 두개의 포크가 필요하다. 접시와 접시 사이에 하나의 포크가 있다. 아래는 이러한 배치를 가진 테이블의 모습이다.




이 문제의 룰을 간단히 요약해보자.

- 철학자의 삶은 식사시간과 생각하는 시간의 반복이다. (필자 주 : 이 정의에 의미를 두지 말자..)
- 철학자는 배가 고프면 왼쪽 포크, 오른쪽 포크를 한번에 하나씩 잡으려고 시도한다.
- 두개의 포크를 잡게되면 철학자는 잠시동안 먹고, 포크를 테이블위에 올려놓고, 그리고 다시 생각한다.

여기서 핵심적인 질문은 아래와 같다

"각 철학자가 해야할 일을 그대로 수행하면서
절대로 중단 되지 않는 프로그램으로 작성될 수 있는가?"

만약 5명의 철학자들이 서로 사이좋게 양보하며 자신의 왼쪽, 오른쪽에 있는 두개의 포크를 잡고 골고루 식사를 할 수 있다면 그들에겐 아주 행복한 저녁이 될 것이다. 하지만 이를 소프트웨어, 특별히 멀티 스레드 기반의 어플리케이션이라고 보았을 때 이것을 '영원히' 보장 할 수 있는가? 가령, 5명의 철학자가 동시에 왼쪽 또는 오른쪽 포크를 잡는다면 어떻게 될까? 이들은 식사를 하지 못한채 망부석 마냥 한 없이 다른 한 쪽의 포크가 테이블에 올려지길 기다릴 것이다. 사실 이 '식사하는 철학자들' 문제는 교착상태(deadlock)를 다루는 아주 대표적인 예제이다. (참고 - <'운영체제론' 3rd Edition, Andrew S Tanenbaum 저>)

이 예제를 JAVA 스레드기반의 어플리케이션으로 아래와 같이 구현해 보았다.

먼저 이 예제의 'Shared Resource' 라고 할 수 있는 'Fork' 클래스 이다.

class Fork {

    Lock lock = new ReentrantLock();

    public void useFork() {
        lock.lock();
    }

    public void unUseFork() {
        lock.unlock();
    }
}

class Tableware {

    public static final List<Fork> forks = new ArrayList<>();

    static {
        forks.add(new Fork());
        forks.add(new Fork());
        forks.add(new Fork());
        forks.add(new Fork());
        forks.add(new Fork());
    }
}

각 'Fork' 객체는 소유권을 가지지 않은 스레드의 접근을 막기 위해 명시적 'lock' 필드를 가지고 있다. useFork 메서드 수행시 lock을 획득하고, unUseFork 메서드 수행시 lock을 해제 한다. 인덱스 범위는 0 ~ 4 이다.

class Philosopher implements Runnable {

    private String name;
    private int number;

    public Philosopher(String name, int number) {
        this.name = name;
        this.number = number;
    }

    public void think() {
        print(name + " thinking ...");
    }

    public void eat() {
        print(name + " eating ... yum-yum-yum");
    }

    public void takeFork(int i) {

        print(name + " attemp to take (" + i + ") fork ...");

        Fork fork = Tableware.forks.get(i);
        fork.useFork();

        print(name + " take (" + i + ") fork now!");
    }

    public void putFork(int i) {

        print(name + " put (" + i + ") fork down ...");

        Fork fork = Tableware.forks.get(i);
        fork.unUseFork();
    }

    @Override
    public void run() {
        while (true) {
            think();
            takeFork(this.number);
            takeFork((this.number + 1) % 5);
            eat();
            putFork(this.number);
            putFork((this.number + 1) % 5);
        }
    }
}

'Runnable' 인터페이스를 구현한 'Philosopher'(철학자) 클래스이다. 포크를 잡기위한 'takeFork' 메서드와 포크를 테이블에 올려놓기 위한 'putFork' 메서드가 정의되어 있고, 정적 'forks' 리스트에 접근하기 위해 Tableware(식기) 클래스를 사용하고 있다.

'run' 메서드에서는 해당 행위 메서드들을 순차적으로 나열함으로서 철학자들이 해야 하는 일을 구현하였다. 서두에 설명한대로, 생각하고(think), 왼쪽 그리고 오른쪽 포크를 잡으며(takeFork), 먹고(eat), 다시 포크를 내려 놓는다(putFork). 왼쪽, 오른쪽 포크의 인덱스를 구하기 위해 'modulo' 연산을 사용한다.

아래는 위 구현사항을 실행하기 위한 'main' 메서드이다.

public class DiningPhilosophers {

    public static void main(String[] args) {

        Philosopher a = new Philosopher("A", 0);
        Philosopher b = new Philosopher("B", 1);
        Philosopher c = new Philosopher("C", 2);
        Philosopher d = new Philosopher("D", 3);
        Philosopher e = new Philosopher("E", 4);

        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(a);
        exec.execute(b);
        exec.execute(c);
        exec.execute(d);
        exec.execute(e);
    }
}

이제 이 프로그램을 실행해보면 어떤 결과가 나타날까? 일정 시간은 5명의 철학자들이 돌아가면서 식사를 잘 하는 것처럼 보이다가... 아래와 같은 현상에 봉착한다.





 A, B, C, D, E 모두가 다른 한쪽 포크를 잡기 위해 (lock을 획득하기 위해) 시도 하고 있다. 하지만 안타깝게도 자신의 다른 한쪽 포크는 이미 다른 철학자가 잡고 있는 상태이다. 때문에 모든 철학자들이 영원히 식사를 하지 못하게 되는 교착상태에 빠져 버린다.

 '동시성' 문제는 이처럼 '공유자원' 으로 인해 발생한다. 하지만 요즘 많이들 이야기 하고 있는 '함수형 프로그래밍' 에서는 공유 자원이라는 개념자체가 희박하여 (있어도 함수형 언어의 'Runtime' 레벨에서 알아서 해주는..?) 부수효과를 예방하는 '순수함수' 위주로 프로그램을 작성하기 때문에 이런 문제를 걱정할 필요가 없을 수도 있다.

 하지만 아무리 요즘의 언어들이 저수준의 문제를 Runtime에 추상화하는 방향으로 발전하면서 개발자들에게 '비즈니스 로직'에만 집중하게 한다 하더라도, 이러한 프로그램의 본질적인 문제들을 탐구하려고 노력해야 좀 더 엔지니어로서 깊은 내공을 쌓을 수 있다는 것이 개인적인 생각이다.

 흠.. 동시성 예제를 살펴보다가 갑자기 잡설로 빠진 것 같다. 아무튼 교착상태에 빠진 배고픈 철학자들을 구제해 줄만한 해결책은 당연히 있다. 제목에서 이미 보았듯이 '시작' 이므로, 해결책에 대한 코드는 다음 포스팅에 정리 해야 겠다. 오늘은 이만..