2012년 12월 26일 수요일

Java 와 Scala 코드를 동시에 사용하기

한 프로젝트 또는 모듈에서 다른 언어로 된 코드를 동시에 사용하여 개발하는 작업을 별로 효과적이라고 생각지도 않았고, 그런 경험이 없었다.

생각해보면 C# 과 VB.NET 또는 J# 을 한 프로젝트에서 사용한 적이 있긴한데, 거의 실험용이였고, J# 이 도태되고, C# 이 발전하는 바람에 C# 으로만 개발을 했습니다.

Java 개발을 시작하고, Scala 도 알게되고, Groovy 도 알게 되었지만, 순수 Java 코드로만 개발을 했다.
물론 Scala 는 따로 프로젝트를 만들어서 공부하거나 실험하면서 기존 Java 로 된 라이브러리를 잘 사용했습니다만...
Java 개발 시에 Scala 코드나 라이브러리를 사용할 생각은 잘 못했습니다.

이번에 C# 코드를 Java 로 포팅하면서, Default Parameter 지원같이 코딩량을 줄일 수 있는 Scala 가 필요했는데, Java 와 Scala 코드를 동시에 사용하는 코드 개발 방법을 몰라 포기 했었죠.
몇번 시도 했는데, 상호 교차 참조 시에는 문제가 생기더군요... 쩝...

오늘 제대로 된 정보를 얻어 따라해보니 상호 교차 되어도 문제없이 컴파일과 실행이 되는 군요. ㅎㅎ

우선 Java 와 Scala 를 동시에 사용하여 개발하기는
http://www.codecommit.com/blog/scala/joint-compilation-of-scala-and-java-sources 를 참고하시면 됩니다.

전 maven 으로 개발하므로, maven 코드를 그대로 복사해서 사용했습니다.
그 전에도 비슷하게 만들어져 있었는다. compiler plugin 순서를 scala , java 순으로 해야 하더군요 ㅠ.ㅠ

요렇게 한 다음

ValueObjectBase.java
public abstract class ValueObjectBase implements IValueObject {

 //private static final long serialVersionUID = 5546630455380910528L;

 @Override
 public boolean equals(Object obj) {
  return obj == this ||
          (obj != null &&
            getClass() == obj.getClass() &&
            hashCode() == obj.hashCode());
 }

 @Override
 public int hashCode() {
  return System.identityHashCode(this);
 }

 @Override
 public String toString() {
  return this.buildStringHelper().toString();
 }

 /**
  * {@link ValueObjectBase#toString()}을 재정의하지 말고, buildStringHelper를 재정의 하세요.
  */
 protected Objects.ToStringHelper buildStringHelper() {
  return Objects.toStringHelper(this);
 }
}


TimeVal.scala
class TimeVal extends ValueObjectBase with Comparable[TimeVal] {

 var time: DateTime = _

 def this(duration: Duration) {
  this()
  this.time = new DateTime().withMillisOfDay(duration.getMillis.toInt)
 }

 def this(moment: DateTime) {
  this()
  this.time = new DateTime().withMillis(moment.getMillisOfDay.toLong)
 }

 def this(hourOfDay: Int, minuteOfHour: Int = 0, secondOfMinute: Int = 0, millisOfSecond: Int = 0) {
  this()
  time = new DateTime().withTime(hourOfDay, minuteOfHour, secondOfMinute, millisOfSecond)
 }

 def datetime: DateTime = this.time

 def hourOfDay: Int = time.getHourOfDay

 def minuteOfHour: Int = time.getMinuteOfHour

 def secondOfMinute: Int = time.getSecondOfMinute

 def millisOfSecond: Int = time.getMillisOfSecond

 def millis: Long = time.getMillis

 def getDateTime(moment: DateTime): DateTime = moment.withTimeAtStartOfDay().plus(millis)

 def getDateTime(date: DateVal): DateTime = date.getDateTime(this)

 override def hashCode(): Int = HashTool.compute(time)

 protected override def buildStringHelper() =
  super.buildStringHelper()
   .add("time", time)

 def compareTo(other: TimeVal) = {
  Guard.shouldNotBeNull(other, "other")
  time.compareTo(other.time)
 }
}

object TimeVal {

 def now: TimeVal = new TimeVal(DateTime.now())
}


TimeVal 사용 예

public static DateTime TimePart(DateTime dateTime) {
    return new TimeVal(dateTime).datetime();
}


Scala 언어로 된 TimeVal가 Java 클래스를 상속 받고, Class 를 사용하는 것은 당연하지만, 동시에 Java 코드에서 Scala 클래스와 Object를 사용하게 되었습니다.

이로서 코드량을 확연히 줄이거나, Scala 코드로 구현하는 것이 더 효과적일 때에는 자유스럽게 사용하게 되었습니다.

한손잡이보다 양손잡이가 더 좋겠죠? (물론 코드의 품질의 충분조건은 아닙니다^^)

2012년 12월 25일 화요일

LambdaJ 소개

.NET LINQ 처럼 Lambda 표현식으로 데이터리 처리하는 방법을 제공해는 라이브러리입니다.
Closure 라는 함수형 언어도 있지만, Java 만을 고집하시는 분들이라면, 아주 괜찮은 라이브러리라 할 수 있습니다.

특히 컬렉션에 대한 Filtering, Sorting, Aggregation 등을 loop 로 표현하게 되면 상당히 코딩량이 많아지는데, 이를 LambdaJ 를 이용하여 엄청나게 줄일 수 있다는 것이 장점이라고 볼 수 있습니다.

아쉽게도 LINQ 도 마찮가지지만, 성능은 당연히 기존 컴파일 방식보다는 평균 3배가량 느립니다.
요렇게 성능이 느린 것을 극복하기 위해 LINQ는 병렬방식으로 처리할 수 있는 길을 열어 놨는데, LambdaJ 는 아쉽게도 이게 없네요... 병렬 처리가 가능하다면 3배 느린 건 멀티코어로 극복이 가능하거든요^^

Java 8 에서 지원할지, LambdaJ 도 지원할지, 아니면 또 다른 Library 가 있는지는 모르지만...
제가 지금까지 조사한 바로는 없네요. 혹시 아시는 분은 코멘트 부탁 드립니다.

다음은 LambdaJ 에 대한 소개 슬라이드이니 이 것을 보시고 공부해 보시기 바랍니다.




제가 .NET 에서도 LINQ 를 헤비하게 사용하였고, Java 개발에도 LINQ 와 유사한 것을 찾고, 헤비하게 사용하려고 하느냐 하면, Data 관련 여러 작업들을 RDBMS의 SQL문장이 아닌 Object Graph 상에서 수행하기 위해서는 단순한 코드로 표현되는 방법이 필요했구요. 그래야만 ORM인 Hibernate 를 사용하는 효과를 극대화 할 수 있기 때문입니다.

결론적으로 RDBMS에서 데이터 연산과 관련된 부분 중에 대용량이 아닌 경우는 Hibernate 에 의한 Domain Model  로부터 쉽게 도출할 수 있는 방법이 지원된다면,  RDBMS 의존도를 더욱 줄일 수 있기 때문입니다.

Java 8 이 나온다 하더라도, 실제 적용되기까지는 시일이 걸리므로, 우선은 LambdaJ 같은 LINQ   와 유사한 기능을 지원하는 라이브러리를 써야 할 것 같네요.

그럼 Java 8 이나 그 후로는?


2012년 12월 23일 일요일

Friends of Guava


원본 : http://code.google.com/p/guava-libraries/wiki/FriendsOfGuava

Friends of Guava

  • Caliper - we use it to benchmark our code
  • Glazed Lists - observable collections
  • GSON - read and write JSON
  • Guice - a mature dependency injector
  • Dagger - a high-performance dependency injector.
  • Google Web Toolkit - we support it
  • ICU4J - unless all your users live in the same place
  • Joda-Time - do not use JDK date/time libraries!
  • JUNG - graphs done right
  • MOE - this is how we keep our internal and external codebases in sync
  • ProGuard - shrinker/bundler/obfuscator/optimizer
  • Mockito - Googlers' favorite mocking framework

Guava 14.0 rc-1 이 나와서 살펴보다가 이 페이지를 발견했네요^^
흠 Glazed List 는 써야할 것 같고, Spring Core 를 써서 굳이 Guice나 Dagger는 쓸 필요가 없을 듯하고 (개인적으로는)
Caliper는 관심가져봐야 겠네요^^

Joda : Java용 Date 관련 라이브러리

C# 에서는 DateTime 관련 작업을 처리하는데는 상당히 편리합니다.
1. 연산자 overriding 이 됩니다 ( 사칙연산이 되니 코드가 간단해 집니다 )
2. 열거, 필터링 관련 작업은 LINQ가 있어서 엄청 편합니다.

Java 에서는 두 가지 모두 안돼죠 ㅠ.ㅠ
어쩔 수 없이 기본 java.util.Date 와 subclass로 C#으로 된 소스를 포팅하려고 했는데,  기초적인 코드부터 다 만들어야 해서, 어떻하나 하다가 다음과 같은 라이브러리 도움으로 쉽게 해결 할 수 있었습니다.

1. Date 관련 연산은 Joda  Time 라이브러리를 이용
2. LINQ 까지는 안되도, 열거, 필터링, 변환 작업을 수행하는데에는 Google Guava 의 Iterables, Lists 등 Helper class 를 활용했습니다.



C# DateTime 연산 예
public static DateTime EndTimeOfHalfyear(this DateTime dateTime, int yearStartMonth = TimeSpec.CalendarYearStartMonth) {
            return
                dateTime
                    .StartTimeOfHalfyear(yearStartMonth)
                    .AddMonths(TimeSpec.MonthsPerHalfyear)
                    .Add(TimeSpec.MinNegativeDuration);
        }


Java 로 Porting 한 예 (Joda Time Library 사용)
public static DateTime endTimeOfHalfyear(DateTime moment, int startMonthOfYear) {
    return
        startTimeOfHalfyear(moment)
            .plusMonths(TimeSpec.MonthsPerHalfyear)
            .minus(TimeSpec.MinPositiveDuration);
 }


JDK만으로는 할 수 없는 Date관련 API를 Joda 가 상당히 많이 제공하네요^^
포팅이 다 되고 나면,  리팩토링을 하면서 좀 더 효과적인 코드로 변환이 가능할 것 같습니다.

열거, 필터링 관련된 LINQ에는 비할바는 못되지만, Guava 에서 어느정도 지원해주고, 더 필요한 것은 되는 대로 만들어서 추가하니, 포팅 속도가 예상외로 빨라졌습니다.
올해 안에 Time, Period,  Calendar, Working Day 등을 지원하는 라이브러리를 선보일 수 있을 것 같습니다.

앞으로는 가능하면 github  를 공개헤서 거기서 소스를 참고 할 수 있도록 하겠습니다.

2012년 12월 14일 금요일

Windows 8 에서 전원관리 옵션의 중요성

아놔~ Windows 8 깔고, 잘 쓰다가 어떻하다보니 엄청 느려져서, 별 짓을 다 해 봤습니다.

특히 java 병렬 프로그래밍 코딩 후 테스트 시에 요상하게 CPU 가 약 30% 이상이 가지 않더라구요.
작업관리자 창에서 본 CPU 속도도 이상하게 0.76 GHz 로 나타나더니, CPU 이용률도 최대 30%를 넘지 못하더군요...

전원관리 : 균형조정

왜 그런지 구글링해도 명확한 답이 없고... 제가 컴 설정을 뭔가 잘못한거 같은데... 감이 안잡히더군요.
근데, 누군가 전원관리 관리 부분을 보라는 의견이 있어서, 항상 제가 "고성능" 으로 설정을 해 놓는데, 발표 때문에 AC 전원을 안 쓸때, 자동으로 균형조정으로 리셋되었나 봅니다.

그래도 그렇지 Windows 7 에서는 균형조정에서도 CPU 가 100%까지 가던데 왜 그러지??? 
이걸 어디서 설정할 수 있나? 
다행히 전원 옵션의 설정-> 고급 옵션을 찾아 가보니, 다음과 같은 창이 떡하니 뜨더군요....



균형 조정 옵션에서는 프로세스 전원 관리 -> 최대 프로세서 상태 값을 보시면 33% 이죠? 커헉... 
이게 Windows 7 균형 조정 옵션에서는 100% 로더군요... 
제가 그 동안 궁시렁 거린게 다 이것 때문이였어요... 

그래서 우선 Windows 8 전원 옵션 중 "고성능" 을 선택하고,  위의 고급 옵션을 봤더니...



최대 프로세스 상태가 100% 로 떡하니 되어 있더군요...
당장 이 놈으로 설정하고, 단위 테스트를 돌렸습니다....

전원 옵션 : 고성능



보시다 시피 CPU 속도가 3.16GHz 이고 이용률도 100% 가까이 가기도 하지요? 에효....
이 문제로 괜히 컴터 탓만 했네요...

Windows 8 사용하시는 분들 중 AC 전원을 주로 이용하시거나, 성능에 민감하신 분들은 전원 옵션을 꼭 "고성능" 으로 하시고, 혹시 "균형 조정" 으로 돌아갈 수도 있으니, 균형 조정 시에도 최대 프로세스 상태 값을 Windows 7 과 같이 100% 로 변경하시면, 컴퓨터의 성능을 제대로 사용하실 수 있습니다.

2012년 12월 8일 토요일

Hibernate Mapping 방식 비교


Hibernate Mapping 방식은
  1. 전통적인 HBM (Hibernate Mapping) XML 파일 방식
  2. JPA 표준으로 사용되는 Annotation 방식
이 있습니다.

처음 Hibernate를 접하시는 분들은 어떤 것을 배워야 쉽고 빠르게 배울수 있을까 고민이 될 것입니다.
이 글은 그런 분들에게 도움이 되고자 쓰는 글이므로, 이미 익숙한 방법이 있는 경험자는 자신에게 익숙한 방식을 사용하는 것을 첫번째로 고려하시기 바랍니다.

자 그럼 두 가지 방식의 얘를 먼저 보고 얘기해 보도록 하지요.


우선 기존 HBM 방식인 XML로 Customer 정보와 Address 정보를 join 을 이용하여 1:1 테이블을 만드는 방식에 대한 매핑을 구현한 예를 보시죠.
보시다시피 초보자가 해석하거나 작성하는 것은 좀 버거워 보이죠? 다만 좋은 IDE를 사용하신다면, DTD나 XSD를 기준으로 HBM의 골격을 해석하여, 입력 정보를 추천해 줄 것이므로, 그리 걱정하지 않으셔도 됩니다. 하지만 각 Xml Element의 의미를 잘 이해해하는 것은 너무나 당연한 것이겠지요^^

HBM 내부 코드를 간략히 설명하자면, Join_Customer라는 Entity 에 id, name, email 등의 단순 속성을 가지고, Address 라는 Value Object 를 가지는데, Address 정보는 다른 테이블에 저장하여, Customer - Address 가 1:1 관계를 가지게 되도록 합니다.


 

  
   
  

  
  
  

  

  
   
   
    
    
    
           
  
 



다음은 Annotation으로 Entity를 매핑하는 방식으로 살펴보겠습니다.
@Entity 가 엔티티를 표현하는 것이고, @Table은 저장 테이블의 명칭 등을 지정할 수 있습니다.
@SecondaryTable 이 Address가 저장될 join table을 지정하구요. 당연히 두 테이블이 1:1 매핑이 되려면 FK가 있어야 하니 CUSTOMER_ID가 FK가 되도록 지정합니다.

각 속성들이 매핑 정보는 각 속성의 annotation 으로 지정하면 됩니다.
HBM 방식과 크게 차이는 나지 않지요? JPA 표준을 만들때 Hibernate 주요 개발자들이 많이 참여하여 주도 했다고 하네요. 그래서 이질감이 덜한 거 같더군요

package org.jpa.example.domain.model.join;

import com.google.common.base.Objects;
import kr.kth.commons.tools.HashTool;
import kr.kth.data.jpa.domain.JpaEntityBase;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.Generated;
import org.hibernate.annotations.GenerationTime;

import javax.persistence.*;
import java.util.Date;

/**
 * org.jpa.example.domain.model.join.JpaJoinCustomer
 * User: sunghyouk.bae@gmail.com
 * Date: 12. 12. 4.
 */
@Entity
@Table(name = "JPA_JOIN_CUSTOMER")
@SecondaryTable(name = "JPA_JOIN_CUSTOMER_ADDR", pkJoinColumns = @PrimaryKeyJoinColumn(name = "CUSTOMER_ID"))
@Getter
@Setter
public class JpaJoinCustomer extends JpaEntityBase {

 private static final long serialVersionUID = 6609847114968580068L;

 @Id
 @GeneratedValue(strategy = GenerationType.AUTO)
 @Column(name = "CUSTOMER_ID")
 private String id;

 @Column(name = "CUSTOMER_NAME")
 private String name;

 @Column(name = "CUSTOMER_EMAIL")
 private String email;


 @Embedded
 @AttributeOverrides(
  {
   @AttributeOverride(name = "street",
                      column = @Column(name = "STREET", table = "JPA_JOIN_CUSTOMER_ADDR")),
   @AttributeOverride(name = "zipcode",
                      column = @Column(name = "ZIPCODE", table = "JPA_JOIN_CUSTOMER_ADDR")),
   @AttributeOverride(name = "city",
                      column = @Column(name = "CITY", table = "JPA_JOIN_CUSTOMER_ADDR")),
  }
 )
 private JpaJoinAddress joinAddress = new JpaJoinAddress();

 @Temporal(TemporalType.TIMESTAMP)
 @Generated(GenerationTime.INSERT)
 @Column(name = "CREATED_TIME", insertable = false, updatable = false)
 //@Setter(AccessLevel.PROTECTED)
 private Date created;

 @Temporal(TemporalType.TIMESTAMP)
 @Generated(GenerationTime.ALWAYS)
 @Column(name = "UPDATED_TIME", insertable = false, updatable = false)
 //@Setter(AccessLevel.PROTECTED)
 private Date lastUpdated;

 @Override
 public int hashCode() {
  if (isPersisted())
   return HashTool.compute(id);

  return HashTool.compute(name);
 }

 @Override
 protected Objects.ToStringHelper buildStringHelper() {
  return super.buildStringHelper()
              .add("id", id)
              .add("name", name)
              .add("email", email);
 }
}

흠 그럼 본격적으로 두 가지 방식에 대한 차이점을 살펴볼까요?

HBM 방식을 보면서, 무었을 느꼈습니까? HBM 방식은 기존 엔티티 클래스에 어떠한 매핑 정보도 없겠지요? HBM 파일에 모두 정의했으니까요? 즉 엔티티와 매핑 정보를 분리한 것이죠? annotation은 두 정보가 결합된 것이구요. 사실 이 차이밖에 더 없습니다^^

그럼 장단점은 무엇일까요?

분리 시 장점은 당연히 독립적이므로 코드를 따로 쓸 수 있고, 매핑에 대한 변화 시에 기존 코드는 고치지 않고, 매핑 정보만 변경하여 적용 시킬 수 있습니다. 즉 유연성 측면에서 좀 더 좋은 방법이다!!! 라고 할 수 있습니다.

또 한가지는 코드 공개를 단계적으로 하고 싶을 때가 있습니다. HBM만 공개하던가, Java Class 만 공개하던가 하게 되면, 전체 그림을 파악 못하게 할 수 있습니다.

이 것은 장점도 될 수 있고, 단점도 될 수 있겠죠? 유지보수 담당자 입장에서는 한번에 파악하는 것이 좋을 수 있으니, SI 나 서비스 개발 분야라면 한 번에 파악하는 게 좋을 수 있고, 솔루션을 개발하거나  보안이 중요한 분야에서는 분리하는 것이 좋을 수 있습니다.

학습측면을 고려한다면... Hibernate 의 기본 매핑 방식은 HBM 이므로, 많은 자료가 있을 것입니다. 쉽게 참조할 자료가 많습니다. (버전별로 매핑 기능이 추가가 된 것이 많기는 하지만 ) 그래서 좀 더 유리하다고 할 수 있겠지요.

다만 요즘은 annotation 방식으로 개발하는 개발자도 꽤 돼므로, StackOverflow 같은 데 보면 annotation 방식으로 질/답하는 예가 더 많은 것 같더군요.

개인적으로는 HBM 방식은 이미 잘 알고 있습니다. 그래서 요즘은 annotation 방식으로 개발 하려고 합니다. 이유는 첫째, 안써본 방식을 더 알고 싶다는 욕심이 있고, 둘째, 저의 직장이 서비스 개발이 주요 업무이다 보니, 보안성이나 유연성보다 유지보수성이 우선 시 된다는 점입니다.

저와 다른 선호도를 가지셨거나, 솔루션 업체에 계시다면 HBM 방식을 추천합니다.

다음에는 Hibernate 공부에 가장 걸림돌이 되는 테스트 환경 구성에 대해 글을 써 볼까 합니다.
그 다음부터는 Hibernate 의 여러가지 매핑 방식에 대해 몇 단계로 나누어 글을 써 보겠습니다.

2012년 12월 7일 금요일

Spring DI를 내 맘대로 사용하기

Spring Framework는 워낙 잘 만들어져, 흠잡을데가 없다고 봅니다. ㅋ
그만큼 탄탄하게 만들어졌고, 최대한 슬림하게 만드려고 한 노력이 돋보이기도 합니다.

그럼 우선 IoC/DI 분야에서는 Spring이 거의 산업표준이고, Java 개발자라면 필수적으로 사용하는 것이지만, 요즘은 더 가볍고, 코드로 DI를 정의하는 google guice 의 도전을 받고 있습니다.
이에 Spring 또한 Java 표준을 준수하고, 코드로 DI를 정의할 수 있는 기능을 3.0 이후 버전에서 지원하므로, 굳이 guice 를 쓸 필요가 있을까 싶습니다.
guice 와의 비교는 googling 해 보시면 많이 나와 있으니 생략하기로 하고...

이번 글의 주제인 Spring ApplicationContext 를 수동적으로 사용하지 않고, 좀 더 적극적으로 사용하는 방식에 대해 설명하겠습니다.

우선 전체 소스를 보면 (제가 소스 별로 나눠서 쓰게 되면, 편집하는 게 어려워서 포기했습니다. 양해를 구합니다.)


package kr.kth.commons.spring3;

import kr.kth.commons.base.Action0;
import kr.kth.commons.base.AutoCloseableAction;
import kr.kth.commons.base.Guard;
import kr.kth.commons.tools.StringTool;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionValidationException;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

import javax.annotation.concurrent.ThreadSafe;
import java.util.Map;
import java.util.Stack;

import static kr.kth.commons.base.Guard.shouldNotBeNull;

/**
 * Spring Framework 의 Dependency Injection을 담당하는 클래스입니다.
 * User: sunghyouk.bae@gmail.com
 * Date: 12. 11. 23.
 */
@Slf4j
@ThreadSafe
public final class Spring {

 public static final String DEFAULT_APPLICATION_CONTEXT_XML = "applicationContext.xml";

 private static final String LOCAL_SPRING_CONTEXT = "kr.kth.commons.spring3.Spring.globalContext";
 private static final String NOT_INITIALIZED_MSG =
  "Spring ApplicationContext가 초기화되지 않았습니다. 사용하기 전에  Spring.init() 을 호출해주기시 바랍니다.";

 private static final Object syncLock = new Object();
 private static volatile GenericApplicationContext globalContext;
 static ThreadLocal> localContextStack = new ThreadLocal<>();


 public static synchronized boolean isInitialized() {
  return (globalContext != null);
 }

 public static synchronized boolean isNotInitialized() {
  return (globalContext == null);
 }

 private static synchronized void assertInitialized() {
  Guard.assertTrue(isInitialized(), NOT_INITIALIZED_MSG);
 }

 public static synchronized GenericApplicationContext getContext() {
  GenericApplicationContext context = getLocalContext();
  if (context == null)
   context = globalContext;
  Guard.assertTrue(context != null, NOT_INITIALIZED_MSG);
  return context;
 }

 private static synchronized GenericApplicationContext getLocalContext() {
  if (getLocalContextStack().size() == 0)
   return null;
  return getLocalContextStack().peek();
 }

 private static synchronized Stack getLocalContextStack() {
  if (localContextStack.get() == null) {
   localContextStack.set(new Stack());
  }
  return localContextStack.get();
 }

 public static synchronized void init() {
  init(DEFAULT_APPLICATION_CONTEXT_XML);
 }

 public static synchronized void init(String... resourceLocations) {
  if (log.isDebugEnabled())
   log.debug("Spring Context 를 초기화합니다. resourceLocations=[{}]",
             StringTool.listToString(resourceLocations));
  init(new GenericXmlApplicationContext(resourceLocations));
 }

 public static synchronized void init(GenericApplicationContext applicationContext) {
  shouldNotBeNull(applicationContext, "applicationContext");

  if (globalContext == null) {
   if (log.isInfoEnabled())
    log.info("Spring ApplicationContext 를 초기화 작업을 시작합니다...");

   globalContext = applicationContext;

   if (log.isInfoEnabled())
    log.info("Spring ApplicationContext를 초기화 작업을 완료했습니다.");
  } else {
   if (log.isWarnEnabled())
    log.warn("이미 Spring ApplicationContext를 초기화 했으므로, 무시합니다.");
  }
 }

 public static synchronized void initByAnnotatedClasses(Class... annotatedClasses) {
  init(new AnnotationConfigApplicationContext(annotatedClasses));
 }

 public static synchronized void initByPackages(String... basePackages) {
  init(new AnnotationConfigApplicationContext(basePackages));
 }


 public static AutoCloseableAction useLocalContext(final GenericApplicationContext localContext) {
  shouldNotBeNull(localContext, "localContext");

  if (log.isDebugEnabled())
   log.debug("로컬 컨텍스트를 사용하려고 합니다... localContext=[{}]", localContext);

  synchronized (syncLock) {
   getLocalContextStack().push(localContext);

   return new AutoCloseableAction(new Action0() {
    @Override
    public void perform() {
     reset(localContext);
    }
   });
  }
 }

 public static synchronized void reset(final GenericApplicationContext contextToReset) {
  if (contextToReset == null) {
   globalContext = null;

   if (log.isInfoEnabled())
    log.info("Global Spring Context 를 Reset 했습니다!!!");

   return;
  }

  if (log.isDebugEnabled())
   log.debug("ApplicationContext=[{}] 을 Reset 합니다...", contextToReset);

  synchronized (syncLock) {
   if (getLocalContext() == contextToReset) {
    getLocalContextStack().pop();

//    if (getLocalContextStack().size() == 0)
//     Local.put(LOCAL_SPRING_CONTEXT, null);

    if (log.isDebugEnabled())
     log.debug("Local Application Context 를 Reset 했습니다.");
    return;
   }

   if (globalContext == contextToReset) {
    globalContext = null;

    if (log.isInfoEnabled())
     log.info("Global Application Context 를 리셋했습니다!!!");
   }
  }
 }

 public static synchronized void reset() {
  if (getLocalContext() != null)
   reset(getLocalContext());
  else
   reset(globalContext);
 }


 public static synchronized Object getBean(String name) {
  if (log.isDebugEnabled())
   log.debug("ApplicationContext로부터 Bean을 가져옵니다. beanName=[{}]", name);

  return getContext().getBean(name);
 }

 public static synchronized Object getBean(String name, Object... args) {
  if (log.isDebugEnabled())
   log.debug("ApplicationContext로부터 Bean을 가져옵니다. beanName=[{}], args=[{}]", name, StringTool.listToString(args));

  return getContext().getBean(name, args);
 }

 public static synchronized  T getBean(Class beanClass) {
  if (log.isDebugEnabled())
   log.debug("ApplicationContext로부터 Bean을 가져옵니다. beanClass=[{}]", beanClass.getName());

  return getContext().getBean(beanClass);
 }

 public static synchronized  T getBean(String name, Class beanClass) {
  if (log.isDebugEnabled())
   log.debug("ApplicationContext로부터 Bean을 가져옵니다. beanName=[{}], beanClass=[{}]", name, beanClass);

  return getContext().getBean(name, beanClass);
 }

 public static synchronized  String[] getBeanNamesForType(Class beanClass) {
  if (log.isDebugEnabled())
   log.debug("해당 수형의 모든 Bean의 이름을 조회합니다. beanClass=[{}]", beanClass.getName());

  return getContext().getBeanNamesForType(beanClass);
 }

 public static synchronized  String[] getBeanNamesForType(Class beanClass,
                                                             boolean includeNonSingletons,
                                                             boolean allowEagerInit) {
  if (log.isDebugEnabled())
   log.debug("해당 수형의 모든 Bean의 이름을 조회합니다. beanClass=[{}], includeNonSingletons=[{}], allowEagerInit=[{}]",
             beanClass.getName(), includeNonSingletons, allowEagerInit);

  return getContext().getBeanNamesForType(beanClass, includeNonSingletons, allowEagerInit);
 }

 public static synchronized  Map getBeansOfType(Class beanClass) {
  if (log.isDebugEnabled())
   log.debug("해당 수형의 모든 Bean을 조회합니다. beanClass=[{}]", beanClass.getName());

  return getContext().getBeansOfType(beanClass);
 }

 public static synchronized  Map getBeansOfType(Class beanClass,
                                                              boolean includeNonSingletons,
                                                              boolean allowEagerInit) {
  if (log.isDebugEnabled())
   log.debug("해당 수형의 모든 Bean을 조회합니다. beanClass=[{}], includeNonSingletons=[{}], allowEagerInit=[{}]",
             beanClass.getName(), includeNonSingletons, allowEagerInit);

  return getContext().getBeansOfType(beanClass,
                                     includeNonSingletons,
                                     allowEagerInit);
 }

 public static synchronized  T getOrRegisterBean(Class beanClass) {
  return getOrRegisterBean(beanClass, ConfigurableBeanFactory.SCOPE_SINGLETON);
 }

 public static synchronized  T getOrRegisterBean(Class beanClass, String scope) {
  Map beans = getBeansOfType(beanClass, true, true);
  if (beans.size() > 0)
   return beans.values().iterator().next();

  registerBean(beanClass.getName(), beanClass, scope);
  return getContext().getBean(beanClass);
 }

 public static synchronized boolean isBeanNameInUse(String beanName) {
  return getContext().isBeanNameInUse(beanName);
 }

 public static synchronized boolean isRegisteredBean(String beanName) {
  return getContext().isBeanNameInUse(beanName);
 }

 public static synchronized  boolean isRegisteredBean(Class beanClazz) {
  try {
   return (getContext().getBean(beanClazz) != null);
  } catch (Exception e) {
   return false;
  }
 }

 public static synchronized  void registerBean(String beanName, Class beanClass) {
  registerBean(beanName, beanClass, ConfigurableBeanFactory.SCOPE_SINGLETON);
 }

 public static synchronized  void registerBean(String beanName, Class beanClass, String scope) {
  if (isBeanNameInUse(beanName))
   throw new BeanDefinitionValidationException("이미 등록된 Bean입니다. beanName=" + beanName);

  if (log.isDebugEnabled())
   log.debug("새로운 Bean을 등록합니다. beanName=[{}], beanClass=[{}], scope=[{}]", beanName, beanClass, scope);

  BeanDefinition definition = new RootBeanDefinition(beanClass);
  definition.setScope(scope);

  getContext().registerBeanDefinition(beanName, definition);
 }

 public static synchronized void registerBean(String beanName, BeanDefinition beanDefinition) {

  if (isBeanNameInUse(beanName))
   throw new BeanDefinitionValidationException("이미 등록된 Bean입니다. beanName=" + beanName);

  if (log.isDebugEnabled())
   log.debug("새로운 Bean을 등록합니다. beanName=[{}], beanDefinition=[{}]", beanName, beanDefinition);

  getContext().registerBeanDefinition(beanName, beanDefinition);
 }

 public static synchronized void removeBean(String beanName) {
  if (isBeanNameInUse(beanName)) {
   if (log.isDebugEnabled())
    log.debug("ApplicationContext에서 Name=[{}] 인 Bean을 제거합니다.", beanName);
   getContext().removeBeanDefinition(beanName);
  }
 }

 public static synchronized  void removeBean(Class beanClass) {
  if (log.isDebugEnabled())
   log.debug("Bean 형식 [{}] 의 모든 Bean을 ApplicationContext에서 제거합니다.", beanClass.getName());

  String[] beanNames = getContext().getBeanNamesForType(beanClass, true, true);
  for (String beanName : beanNames)
   removeBean(beanName);
 }

 public static synchronized Object tryGetBean(String beanName) {
  try {
   return getBean(beanName);
  } catch (Exception e) {
   if (log.isWarnEnabled())
    log.warn("bean을 찾는데 실패했습니다. null을 반환합니다.", e);
   return null;
  }
 }

 public static synchronized Object tryGetBean(String beanName, Object... args) {
  try {
   return getBean(beanName, args);
  } catch (Exception e) {
   if (log.isWarnEnabled())
    log.warn("bean을 찾는데 실패했습니다. null을 반환합니다.", e);
   return null;
  }
 }

 public static synchronized  T tryGetBean(Class beanClass) {
  try {
   return getBean(beanClass);
  } catch (Exception e) {
   if (log.isWarnEnabled())
    log.warn("bean을 찾는데 실패했습니다. null을 반환합니다.", e);
   return (T) null;
  }
 }

 public static synchronized  T tryGetBean(String beanName, Class beanClass) {
  try {
   return getBean(beanName, beanClass);
  } catch (Exception e) {
   if (log.isWarnEnabled())
    log.warn("bean을 찾는데 실패했습니다. null을 반환합니다.", e);
   return (T) null;
  }
 }
}


Spring.init() 함수를 이용해서, ApplicationContext를 초기화합니다. 아시다시피 xml 도 되고, java class로 정의한 Confiration도 됩니다. 또한 여러 개의 Configuration class 에 대한 것도 되고, 아예 사용자가 ApplicationContext 를 생성하고, 지정할 수도 있습니다.

이 후에 getBean() 이나 getBeansOfType() 은 GenericApplicationContext 에서 제공하는 것입니다.

여기서 제가 제안하는 것은 getOrRegisterBean() 처럼 만약 등록되지 않는 Bean이 있다면, 새로 등록해서 사용하게 하고, tryGetBean 처럼 bean 이 없는 경우 예외를 발생시키는 것이 아니라 null 을 반환하도록 할 수 있는 메소드를 제공합니다.

이런 시도는 IoC ( Inversion Of Control ) 의 환경을 또 한번 뒤엎어본다는 것입니다. ㅎㅎ
환경설정에서 설정한대로 움직이지만, 만약 환경설정에서 빼 먹은 것이 있다면, 코드 상에서 기본값으로 처리하도록 하는 것입니다. (논란의 여지가 있죠? ㅎㅎ )

또 하나는 Spring 의 Bean 의 Lifecycle을 Singleton, Prototype 이 주로 사용되지만, 웹 환경에서는   Session 도 가능하게 되는데, 이렇게 다양한 Lifecycle 로직을 잘 활용하게 되면, 개발자가 직접 인스턴스의 Lifecycle을 관리하는 게 아니라 ApplicationContext 의 Bean에 대한 Lifecycle 관리 기능을 사용하자는 의미가 있습니다.

아쉽게도 Spring 이나 guice 모두 Lifecycle 종류 중에 Thread 별로 Bean을 관리해주는 기능은 없네요... 이게 있으면 좋겠지만... 요건 ThreadLocal 과 조합하면 제대로 표현이 가능할 것입니다^^

2012년 12월 6일 목요일

Google Guava 의 LoadingCache 를 이용한 메모리 캐시

Goolge Guava 는 Apache Commons 에서 제공하지 않는 유용한 Utility성 기능들이 상당히 많습니다.
특히 단순한 캐시 시스템의 경우, 캐시 시스템을 데이터 소스와는 별개로 다뤄야 하는 귀찮은 점이 있는데, 아주 간단하지만, 아예 "캐시 시스템 네가 필요할 때 데이터 소스를 직접 조회해서 캐시에 담아 놓으렴", 그럼 난 그냥 그걸 사용할께...  이런 목적에 딱 맞게 제공되는 클래스가 LoadingCache 라는 게 있습니다.

아래 코드는 특정 URL 의 컨텐츠를 메모리 상에서 캐시하고, 제공할 수 있도록 해주는 클래스인데, Google Guava의 LoadingCache를 이용하여 구현하였습니다.

package kr.kth.commons.caching.repository;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import kr.kth.commons.caching.CacheRepositoryBase;
import kr.kth.commons.tools.StringTool;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.nio.client.DefaultHttpAsyncClient;
import org.apache.http.nio.client.HttpAsyncClient;
import org.apache.http.util.EntityUtils;

import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

/**
 * Google Guava 의 {@link LoadingCache} 를 이용하여, 캐시 값을 구하는 방법을 미리 지정하여, 쉽게 캐시를 운영할 수 있도록 캐시입니다.
 * User: sunghyouk.bae@gmail.com
 * Date: 12. 12. 5.
 */
@Slf4j
public class FutureWebCacheRepository extends CacheRepositoryBase {

 private final LoadingCache cache;

 public FutureWebCacheRepository() {
  cache =CacheBuilder.newBuilder().build(getCacheLoader());
 }

 @Override
 public Object get(String key) {
  try {
   return cache.get(key);
  } catch (ExecutionException e) {
   throw new RuntimeException(e);
  }
 }

 @Override
 public void set(String key, Object value, long validFor) {
  String str = (value != null) ? value.toString() : "";
  cache.put(key, str);
 }

 @Override
 public void remove(String key) {
  cache.invalidate(key);
 }

 @Override
 public void removes(String... keys) {
  cache.invalidateAll(Arrays.asList(keys));
 }

 @Override
 public boolean exists(String key) {
  return cache.getIfPresent(key) != null;
 }

 @Override
 public void clear() {
  cache.cleanUp();
 }

 private static CacheLoader getCacheLoader() {
  return
   new CacheLoader() {
    @Override
    public String load(String key) throws Exception {

     if (FutureWebCacheRepository.log.isDebugEnabled())
      FutureWebCacheRepository.log.debug("URI=[{}] 의 웹 컨텐츠를 비동기 방식으로 다운로드 받아 캐시합니다.", key);

     String responseStr = "";
     HttpAsyncClient httpClient = new DefaultHttpAsyncClient();
     try {
      httpClient.start();
      HttpGet request = new HttpGet(key);
      Future future = httpClient.execute(request, null);

      HttpResponse response = future.get();
      responseStr = EntityUtils.toString(response.getEntity(), Charset.forName("UTF-8"));

      if (log.isDebugEnabled())
       log.debug("URI=[{}]로부터 웹 컨텐츠를 다운로드 받았습니다. responseStr=[{}]",
                 key, StringTool.ellipsisChar(responseStr, 255));
     } finally {
      httpClient.shutdown();
     }
     return responseStr;
    }
   };
 }
}


코드를 보시면 아시겠지만, 처음 필요할 때 웹에서 컨텐츠를 다운받고, 그것을 캐시하여 사용하도록 하고 있습니다.

이 코드에서는 Expiry 에 대한 정책은 지정할 수 없게 되어 있지만, 직접 구현하던가, Guava에서 지원하는지 조사해서 지원한다면 그걸 사용하는 게 정신 건강에 좋겠죠^^

이번 글은 java 에서 apache commons 가 기본 중의 기본 라이브러리지만, google guava 도 이제 기본 중에 기본이 될 것입니다.
그 이외에도 아주 많은 유용한 기능이 있으니 공부해 보시기 바랍니다.^^