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 도 이제 기본 중에 기본이 될 것입니다.
그 이외에도 아주 많은 유용한 기능이 있으니 공부해 보시기 바랍니다.^^



2012년 11월 18일 일요일

Java 개발자에게 Spring Framework 의 의미...

Java 진영에서 Spring Framework 의 존재는 거의 모든 기반 기술은 다 해줄테니, 비즈니스 로직에만 집중하라... 입니다만... 

문제는 Spring Framework을 실제 이용하는 사람은 여러가지 기능에 대한 원리에 대한 이해나 필요성에 대해서는 그리 신경 쓰지 않는다는 것입니다.

이런 현실때문에 만약 Spring Framework을 쓰지 않는 곳에서는 프로젝트가 제대로 진행될 수 없거나, 품질이 제대로 나올 수 없을 것 같더군요...
 
이렇게 된게 국가가 공공 표준 프레임웍으로 Spring을 지정해서 그렇게 된게 가장 큰 거 같습니다.


표준화 자체의 효율과 효과에 대해서는 이론의 여지가 없지만, 새로운 발전을 위한 혁신에는 걸림돌이 될 것 같습니다. - Spring 으로 표준화도 혁신이었지만 - 앞으로 또 이런 노력이 계속 필요하지만, 이제는 거의 하지 않는 것 같습니다. ㅎㅎ

이런 면에서 .NET은 .NET Framework 과 NHibernate (myBatis) + DI (Castle.Windsor, NInject 등) + Entrprise Library + 몇가지 Utils 를 가지고 작업하니, 표준으로 내세울 것이 없어, 선택의 폭이 넓고, 공부해야 할 것도 더 많아서, 초기 개발자에게는 더 도움이 되지 않을까 하는 역설적인 상황이 온 거 같습니다.

물론, 개개인의 개발자의 학습에 대한 의욕 및 역량에 따라 다르지만, 객관적인 상황은 그렇다는 얘기입니다...

Java 진영에서는 Ruby, Groovy, Closure, Scala 등 다른 언어들이 있어, 자신의 역량을 높힐 신세계가 훨씬 많습니다만, Java에만 머물겠다면 Spring 외에는 공부할 게 없다는 유혹 또한 있습니다...





Spring Framework만으로 충분하다고 생각하시는 개발자분들은 Spring Framework을 사용하는 것을 넘어 Framework 내부 구조나 원리를 공부하시기를 조심스레 추천합니다.





Spring Framework이 표준화까지 되었다는 것은, 거의 모든 Best Practices 가 녹아 있으므로, 원리를 이해하는 것이 향후 다른 언어를 사용하거나, 제품을 고품질화할 때 도움이 될 것입니다. 


(단순히 Spring 책 한 두권 읽고, 적용하는 수준이 아니라, 기능을 가장 잘 사용할 방안뿐 아니라 왜 그런지 등을 이해하시는 게 필요합니다.)

2012년 11월 12일 월요일

Java에서도 logging 은 slf4j 로 가는군요.

닷넷에서도 log4net 이 처음에는 많이 사용되다가, 속도, appender 문제로 NLog 가 더 많이 쓰이게 되었는데,  Java에서도  log4j 를 직접 사용하던가 common-logging 을 사용하던 방식에서 slf4j 로 facade 해서 사용하는 방식으로 바뀌었군요.

물론 로그 쓰는 함수의 편리성 때문이겠죠?
근데 실제 로깅하는 라이브러리는 설정에 따라 log4j 를 쓸 수도 있고, logback 을 쓸 수도 있겠죠...

기존 log4j 라이브러리를 logback으로 변경했습니다. 뭐 기본적으로 slf4j  를 완벽지원하고, 속도가 빠르다고 하네요.

대부분의 의견이 logback 사용하는 것이 정신 건강에 좋다고 추천을 하는군요.

http://stackoverflow.com/questions/178215/log4j-vs-logback

아직 log4j 를 사용하시는 분들은 logback 으로 변경해 보시기 바랍니다.


2012년 11월 10일 토요일

요즘 공부하고 있는 Scala 라는 언어 소개 자료

요즘 JVM 에서 도는 java, groovy, closure, scala 등을 공부하고 있습니다.

java 야 기본 중에 기본이라, 말하면 잔소리고,

groovy는 동적 언어로서 간단한 프로토 타입이나 테스트 코드 작성에 유용하고, Solution에서 동적 script 가 사용될 때, groovy 를 표준으로 삼게 되면, 많은 잇점이 있습니다. 같은 JVM에서 돌고, 동적인   Script를 class 로 빌드해서 재 사용이 가능하여, 속도면에서도 상당한 장점을 가지게 됩니다. 또한 기존 java 라이브러리를 활용하게 되면, 더욱 강력한 기능을 제공할 수 있습니다.

groovy의 단점이라면, 동적언어로서 숙명적으로 실행 속도가 문제가 됩니다...
java 와 비교해서 같거나 더 뛰어난 성능을 가지면서, 여러가지 발전적인 기능을 가진 언어는 없을까 고민 중에 회사 동료들이 Scala에 대한 정보를 알려줘서 알게 되었습니다.

Scala는 OOP + 함수형 언어 특성 모두를 가지는 다방면의 기능을 가진 언어입니다.
만약 새로운 서비스를 만들게 된다면, Java 보다는 Scala 를 주요 언어로 선정하고 싶을 정도입니다.

다음은 제가 본 Scala 관련 소개 자료 중 유용한 자료라 생각됩니다.
한번 보시고, 괜찮다고 생각되신다면 공부해 보시길...



2012년 11월 3일 토요일

Hibernate Performance Tuning

Hibernate 의 성능을 높히기 위한 방안을 소개한 PPT 자료입니다.




ORM의 가장 큰 특징인 Association의 lazy 특성과 대량의 컬렉션에 대한 처리가 상당히 중요합니다.
상황에 따라서, 알맞는 전략이 필요합니다.

NHibernate 도 거의 같으므로, 참고하시기 바랍니다.

2012년 11월 1일 목요일

PostgreSQL 9 의 Primary Key에 Cluster 특성 지정하기

SQL Server 를 많이 써서 그런지, 당연히 Primary Key로 지정된 놈이 기본적으로 Clustered Index 로 설정되는 줄 알았는데 PostgreSQL은 아니네요^^

그냥 Non-Clustered Index 형태이면서 제약 조건에 나타나내요

다음과 같이 특정 테이블의 Primary 키에 CLUSTER 속성을 주면 간단히 해결이 됩니다.

ALTER TABLE customers CLUSTER ON customers_pkey; 

오늘 하루 종일 PostgreSQL 기본 문법 사항과 성능 관련 사항을 훑어보고 있는데, 상당히 잘 만들어졌다고 봐 집니다. 
다양한 기능을 가지고, 상황에 맞는 기능을 제대로 제공하는군요...

인덱스만 하더라도 BTREE, HASH, GIN (Generalized Inverted Index), GiST (Generalized Search Tree) 를 제공하는군요.

특이한 것은 부분 인덱스라고, 특정 조건에 해당하는 레코드들만 인덱스를 정의하는 기능도 있네요.

CREATE INDEX idx_accounts_interesting ON accounts WHERE interesting is true;

또한  SQL Server 의 경우 WHERE 절에 Upper(), 나 Lower() 등의 함수를 쓰게 되면, 인덱스를 타지 않는데, 이런 상황을 극복할 수 있도록, 이런 함수를 이용한 인덱스도 있습니다.

CREATE INDEX idx_lower_name ON users (lower(name));
SELECT * FROM users WHERE lower(name) = 'x';


좀 더 공부해서, 아예 PostgreSQL 의 Replication과 HA 구성도 함 해봐야 겠습니다.


2012년 10월 27일 토요일

Open Source RDBMS 인 PostgreSQL 9 소개자료

오픈 소스 진영에서 가장 유명한 DB는 MySQL 일 것입니다. PHP + MySQL 조합은 엄청난 유행과 파급력을 발휘했습니다.

다만 Business Application 을 제작하는 분야에서는 Oracle 과 SQL Server 와 같은 상용 RDBMS 를 사용하는 것이 가장 안전하다고 생각했기 때문입니다.

이제 Business Application 분야뿐 아니라 MySQL의 아성이 깨지지 않을 것 같은 서비스 분야에서도 서서히 새로운 RDBMS 가 떠오르고 있습니다.
뭐 예전부터 명성은 높았지만, 국내는 워낙 유행을 타고, 기능보다는 남들이 많이 쓰고, 한글 자료가 많은 그런 제품을 선호하다보니, 그렇게 많은 보급은 되지 않았습니다.

그게 바로 PostgreSQL 입니다. 현재 9.2.1 버전까지 나왔습니다만, 국내에서 채택한 서비스나 제품은 제가 보지를 못했네요^^ 외국에서는 여러 서비스에서 MySQL 을 제치고 PostgreSQL 로 넘어가고 있습니다.

특히나 외국의 대규모 서비스에서는 MySQL 의 MyISAM 은 절대 고려 대상이 아니고, InnoDB 는 초보 수준의 Join 만을 제공하여, 고려 대상에서 제외하려는 경향이 높아지고 있습니다. (물론 아직도 MySQL 의 점유율이 절대적입니다^^)

한국에서도 무리한 비용이 드는 상용 RDBMS 가  아닌, 유사한 성능을 내는 PostgreSQL 같은 오픈소스 RDBMS 의 도입이 많아졌으면 합니다^^



위는 PostgreSQL 에 대한 설명 자료입니다. 한번 정도 읽어보시면 도움이 되실겁니다.

2012년 10월 26일 금요일

새로운 JVM 용 통신 서버 : vert.x

node.js 라는 비동기 통신 서버가 나온게 2009년 인가요?
어쨌든 javascript의 재발견이니, 새로운 시도니 엄청 각광을 받으며, 지금도 잘나가고 있지요...

엄청난 동접 수를 지탱해야 하는 웹 서버의 앞단을 node.js 로 하는 게 거의 진리가 되고, 그 이외에 내부 통신 부분에서도 쓰이도록 했습니다. ㅎㅎ

근데 말이죠 node.js 의 한계가 하나의 프로세스에 하나의 스레드만 있는 구조라 여러 프로세스를 구동 시켜야 하고, 이런 다중 프로세스들의 관리는 또 다른 프로세스 ( 캐시 / MQ / DB ) 에서 관리하도록 해야 합니다.
즉 부가 작업이 많이 들어가게 되지요.

뭐 어차피 대부분의 어플리케이션이 DB, Cache 는 기본으로 사용하니 별 문제가 안되겠지만, 직접 만들어야 한다면 좀 그렇죠?^^

더군다나 client 와 server 모두 javascript 로 되어서 좋다고 하는 분들도 많지만, 여전히 서버 개발자들은 java 를 더 선호하고, JVM 의 성능과 안정성에 많은 신뢰를 보내고 있지요...

여기에 node.js 와 같이 비동기 통신 모듈이면서, JVM 위에서 구동되어, 여러 언어로 구현할 수 있는 vert.x 에 대해 소개합니다.




소개 자료를 보면 기존 node.js 의 장점을 모두 수용하고, 신뢰성 있고 다양한 언어로 구현할 수 있어 많은 개발자가 쉽게 접근할 수 있음을 알 수 있을 것입니다.

물론 nginx 나 play! framework 처럼 이미 잘 만들어진 web server나 web framework을 사용해도 좋지만, 성능이 아주 민감하다면, 고려해볼만 합니다.

닷넷 진영에서는 이러한 정보에 대해 접할 시도나 기회도 아주 적었는데, Java 진영으로 오니, 이러한 새로운 제품이나 시도가 넘쳐나서, 기쁘기도 하고, 버겁기도 하네요^^

어쨌든 Java 진영의 거의 모든 고성능 통신 모듈은 netty.io 를 기반으로 한다는 거...
그러니 netty.io 를 사용하면 기본은 먹고 들어간다는 거^^

2012년 10월 21일 일요일

비동기 프로그래밍 by Netty and Scala

요즘 자바 진영의 개발을 하고 있습니다만...
공부를 계속하다보니, Scala 가 눈에 들어오네요^^

같은 JVM에서 수행되고, Java, C# 보다 더 유연하고, 편하고, 빠르고...
물론 Ruby, Groovy, Python 같은 Dynamic 은 아니니 유연성은 좀 떨어지고, 컴파일 속도가 좀 느린 단점이 있긴 합니다.




요런 발표 자료를 보시면 이해가 좀 더 잘 될 듯...

혹시 Java 진영에서 개발하시는 분 중에 Scala를 모르시는 분들은 관심 가져 보시기 바랍니다.

2012년 6월 17일 일요일

요즘 근황...

새로운 회사로 이직 후, 전혀 새로운 업종과 개발환경 탓에 엄청나게 새로 공부해야 해서...
그동안 블로그를 관리하지 못했습니다.

새로 옮긴 회사는 서비스를 하는 회사이고, .NET 은 쳐다도 안보는 Java + Spring 이 대세인 회사입니다.
거기다가 요즘 서비스업계의 화두인 Node.js 와 같은 새로운 기술과, 저는 처음 접하는 nginx 같은  웹서버 같은 것을 공부했습니다.

그러나... 다른 것 보다도 시급한 것은 Oracle 과 MS SQL Server 를 주로 다루던 제가 MySQL 을 전문적으로 다루는 조직에 오니, 참 난감하더군요....
어떤 기능은 정말 상식 밖으로 지원 안되는 것처럼 느껴지고, 어떤 것은 의외로 잘 지원하고^^

뭐 어쨌든... 3개월이 다 되지 않았지만, 이제는 어느 정도 익숙해져서, 지금까지 배우던 것들을 종합해서 뭔가 써보려고 합니다.

참 OS 도 Ubuntu 12.04 로 바꿨습니다.

그럼 3개월 간 공부한 내용을 좀 적고, 앞으로 이를 .NET 과 다른 점이 무엇이고, 앞으로 중립적으로 선택할 수 있는 기술이 무엇인가? 고민할 수 있는 장을 마련해 보고 싶네요..

1. Java & Spring Framework ( 이건 뭐 그 전에도 좀 알고 있던거라 ...)
2. Python & Django (Python Web Framework)
3. Ruby on Rails
4. Node.js 와 RailwayJS

등 뭐 Language를 가리지 않고, 공부했습니다.

뭐 여러가지 언어를 공부해보면, 다 그게 그거 거든요^^

하지만^^ 요즘은 Ruby on Rails 1.9.3 을 주로 사용합니다. 앞으로 2.0 이상의 버전이 나오면, 성능 상의 문제도 많이 해소 될 듯 합니다.

하지만 대용량 서비스를 하게 된다면 결국은 node.js 로 넘어갈 듯 하기도 하지만, 아직 그런 서비스를 만드는 기획과 시장 여건이 더 힘들 것 같아, 굳이 미리 준비하지 않아도 될 듯 하네요^^

2012년 5월 2일 수요일

Chocolatey 를 이용하여 쉽게 프로그램을 설치하세요.

이제 Windows 에서도 Package Manager가 지원되네요^^
NuGet 은 .NET 라이브러리에 대해서만 자동 설치를 제공하는데 반해, 이를 이용해서,
리눅스나 맥에서 제공하는 것과 같은 기능을 제공하는 chocolatey  를 사용하세요.

다양한 OS 에서 사용하는 Package Manager 에 대한 예는 https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager 에서 보실 수 있습니다.

chocolatey 는 NuGet 을 내부적으로 사용하여, DOS PROMPT 창에서 간단하게 타이핑을 하면, 원하는 제품과 관련 제품을 다운로드 받아 설치까지 해줍니다^^



그 동안 다른 OS 보다 설치가 좀 그랬는데, 참 편해졌습니다.

2012년 3월 15일 목요일

ASP.NET MVC Output Caching with Memcached – Part 1

.NET 4.0 이상 즉 ASP.NET 4.0 이상부터는 Output Cache 에 대한 Provider를 사용자가 설정할 수 있습니다. 제가 회사에서 만든 것은 MongoDB, SharedCache, Memcached 등에 대해 Output Cache 를 저장할 수 있도록 구현했습니다.

개인적으로 Microsoft Server AppFabric 은 별로 좋아하지 않고, 구현하지 않았지만, 그리 어려운 부분은 아닙니다.

이번 글은 일반적으로 가장 많이 사용하고, 속도가 높은 Memcached 를 Cache Repository 로 사용하는 경우에 대해 알아보겠습니다.
 
memcached - a distributed memory object caching system
너무 유명한 제품이라 Memcached 에 대한 소개는 생략하기로 하고, Windows에서 Memcahced를 Windows Service로 사용할 수 있도록 해 주는 방법에 대해 알아봅시다.

우선 Memcached 64-bit for WindowsHow to install Memcached on Windows machine (32-bit) 에서 다운 받아 원하는 위치에 압축을 푸시기만 하면 됩니다.

다음으로 설치 폴더에서 Command Prompt 를 열어

설치폴더/memcached.exe -d install

를 수행하시면, Windows Service로 설치 됩니다.
다음으로는 Memcached 서비스를 실행시킵니다.


설치폴더/memcached.exe -d  start

이제 작업관리자에서 실제 수행 되고 있는지 확인해보시기 바랍니다.

image
이제 실제 Memcached 캐시 서버에 접속하여 캐시를 저장/로드를 수행할 Client를 제작해야 합니다. 이를 위해 Memcached Client Driver 인 Enym.Caching 을 사용합니다.

Enym.Caching 을 사용하여,  제작한 클래스는 다음과 같습니다.
-------------------------------------------------------------------
using System;using Enyim.Caching;using Enyim.Caching.Memcached;using NSoft.NFramework.Json;
namespace NSoft.NFramework.Caching.Memcached{
    /// <summary>
    /// Memcached 캐시 서버를 저장소로 사용하는 Cache Repository 입니다.
    /// 참고: https://github.com/enyim/EnyimMemcached/wiki/MemcachedClient-Usage
    /// </summary>
    public class MemcachedRepository : AbstractCacheRepository
    {
        #region << logger >>

        private static readonly NLog.Logger log = NLog.LogManager.GetCurrentClassLogger();
        private static readonly bool IsDebugEnabled = log.IsDebugEnabled;

        #endregion

        public MemcachedRepository() : base() {}

        public MemcachedRepository(ISerializer serializer) : base(serializer) {}

        private MemcachedClient _client;

        public MemcachedClient Client
        {
            get { return _client ?? (_client = new MemcachedClient()); }
            set { _client = value; }
        }

        /// <summary>
        /// 캐시에 저장된 항목을 반환합니다.
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public override object Get(string key)
        {
            key.ShouldNotBeWhiteSpace("key");

            if(IsDebugEnabled)
                log.Debug("캐시에서 키[{0}]에 해당하는 값을 조회합니다.", key);

            var item = Client.Get(key);
            var isSerialized = (Serializer != null) && (item is CacheItem);

            if(isSerialized == false)
                return item;

            var cacheItem = (CacheItem) item;
            if(Serializer is IJsonSerializer)
                return ((IJsonSerializer) Serializer).Deserialize(cacheItem.ItemData, cacheItem.ItemType);

            return Serializer.Deserialize(cacheItem.ItemData);
        }

        /// <summary>
        /// 캐시에 항목을 저장합니다.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="item"></param>
        /// <param name="validFor"></param>
        public override void Set(string key, object item, TimeSpan validFor = default(TimeSpan))
        {
            key.ShouldNotBeWhiteSpace("key");

            if(IsDebugEnabled)
                log.Debug("캐시에 값을 저장합니다. key=[{0}], item=[{1}], Expiry=[{2}]", key, item, validFor);

            // BUG: MemcachedClient는 유효기간을 지정하면 저장이 되지 않습니다.
            //
            if(Serializer != null)
                Client.Store(StoreMode.Set, key, CreateCacheItem(this, item));
            else
                Client.Store(StoreMode.Set, key, item);
        }

        /// <summary>
        /// 캐시에서 항목을 제거합니다.
        /// </summary>
        /// <param name="key"></param>
        public override void Remove(string key)
        {
            key.ShouldNotBeWhiteSpace("key");
            if(IsDebugEnabled)
                log.Debug("캐시에 저장된 항목을 삭제합니다... key=[{0}]", key);

            Client.Remove(key);
        }

        /// <summary>
        /// 캐시의 모든 항목을 삭제합니다.
        /// </summary>
        public override void Clear()
        {
            if(IsDebugEnabled)
                log.Debug("캐시에 저장된 모든 항목을 삭제합니다...");

            Client.FlushAll();
        }

        private static CacheItem CreateCacheItem(MemcachedRepository repository, object item)
        {
            return
                new CacheItem
                {
                    ItemType = item.GetType(),
                    ItemData = repository.Serializer.Serialize(item)
                };
        }

        /// <summary>
        /// 캐시로 저장되는 정보
        /// </summary>
        [Serializable]
        private class CacheItem
        {
            public Type ItemType { get; set; }
            public byte[] ItemData { get; set; }
        }
    }
}
-------------------------------------------------------------------

CacheItem class는 캐시에 저장된 내용이 Serializable 이 아닌 경우, 예외를 일으키기 때문에 래핑하는 역할을 수해합니다.

이제 실제로 Memcached 를 사용하는 OutputCacheProvider를 제작해봅시다.

-------------------------------------------------------------------
/// <summary>
/// ASP.NET 웹 Page의 Output Cache를 Memcached 서버에 저장해주는 OutputCacheProvider입니다./// </summary>
/// <summary>
/// .NET 4.0 이상에서 ASP.NET 웹 Page의 Output Cache를 Memcached 서버에 저장해주는 OutputCacheProvider입니다./// 참고:///      http://www.4guysfromrolla.com/articles/061610-1.aspx///      http://weblogs.asp.net/gunnarpeipman/archive/2009/11/19/asp-net-4-0-writing-custom-output-cache-providers.aspx///      http://weblogs.asp.net/scottgu/archive/2010/01/27/extensible-output-caching-with-asp-net-4-vs-2010-and-net-4-0-series.aspx/// </summary>
/// <example>
/// <code>
/// <system.web>
///        <compilation debug="true" targetFramework="4.0"/>
///        <caching>
///            <outputCache defaultProvider="MemcachedOutputCacheProvider">
///                <providers>
///                    <add name="MemcachedOutputCacheProvider" 
///                      type="NSoft.NFramework.Caching.Memcached.Web.MemcachedOutputCacheProvider, NSoft.NFramework.Memcached"/>
///                </providers>
///            </outputCache>
///        </caching>
/// </system.web>
/// </code>
/// </example>public class MemcachedOutputCacheProvider : AbstractOutputCacheProvider{
    #region << logger >>

        private static readonly NLog.Logger log = NLog.LogManager.GetCurrentClassLogger();
        private static readonly bool IsDebugEnabled = log.IsDebugEnabled;

        #endregion

    public MemcachedOutputCacheProvider() : base(() => new MemcachedRepository()) {}
}
-------------------------------------------------------------------


아니 뭐야?  뭐가 이리 간단해? 네… 아시다시피 Output Cache 를 Cache에 저장/로드 하는 것은 어떤 캐시 시스템을 사용해도 똑같습니다. 그래서 AbstractOutputCacheProvider 에게 ICacheRepository 를 제공하여, 확장을 손쉽게 할 수 있는 구조를 가지게 되었습니다.

그럼 핵심적인 AbstractOutputCacheProvider 소스를 보시면

-------------------------------------------------------------------
/// <summary>
/// .NET 4.0 이상에서 ASP.NET Page의 OutputCache를 <see cref="CacheRepository"/>를 통해 저장/로드됩니다./// 참고:///      http://www.4guysfromrolla.com/articles/061610-1.aspx///      http://weblogs.asp.net/gunnarpeipman/archive/2009/11/19/asp-net-4-0-writing-custom-output-cache-providers.aspx///      http://weblogs.asp.net/scottgu/archive/2010/01/27/extensible-output-caching-with-asp-net-4-vs-2010-and-net-4-0-series.aspx/// </summary>public abstract class AbstractOutputCacheProvider : OutputCacheProvider, IOutputCacheProvider{
    #region << logger >>

        private static readonly NLog.Logger log = NLog.LogManager.GetCurrentClassLogger();
        private static readonly bool IsDebugEnabled = log.IsDebugEnabled;

        #endregion

    private static readonly Func<ICacheRepository> _defaultCacheRepositoryFactory =
        () =>
        {
            ICacheRepository repository = null;
            try
            {
                if(IoC.IsInitialized)
                    repository = IoC.Resolve<ICacheRepository>();
            }
            catch(Exception ex)
            {
                if(log.IsWarnEnabled)
                    log.WarnException("IoC로부터 ICacheRepository를 Resolve하는데 실패했습니다.", ex);
            }

            if(repository == null)
                repository = new SysCacheRepository();

            return repository;
        };

    protected AbstractOutputCacheProvider() : this(_defaultCacheRepositoryFactory) {}

    protected AbstractOutputCacheProvider(Func<ICacheRepository> cacheRepositoryFactory)
    {
        cacheRepositoryFactory.ShouldNotBeNull("cacheRepositoryFactory");

        CacheRepository = cacheRepositoryFactory();
    }

    /// <summary>
    /// 실제 캐시 저장소에 데이타를 저장/조회하는 API를 제공하는 Repository입니다.
    /// </summary>
    public ICacheRepository CacheRepository { get; protected set; }

    /// <summary>
    /// 출력 캐시에서 지정된 항목에 대한 참조를 반환합니다.
    /// </summary>
    /// <returns>
    /// 캐시에서 지정된 항목을 식별하는 <paramref name="key"/> 값이거나 캐시에 지정된 항목이 없는 경우 null입니다.
    /// </returns>
    /// <param name="key">출력 캐시에서 캐시된 항목에 대한 고유 식별자입니다. </param>
    public override object Get(string key)
    {
        if(IsDebugEnabled)
            log.Debug("ASP.NET Page OutputCache를 로드합니다... key=[{0}]", key);

        return CacheRepository.Get(key);
    }

    /// <summary>
    /// 지정된 항목을 출력 캐시에 삽입합니다. 
    /// </summary>
    /// <returns>
    /// 지정된 공급자에 대한 참조입니다. 
    /// </returns>
    /// <param name="key"><paramref name="entry"/>에 대한 고유 식별자입니다.</param><param name="entry">출력 캐시에 추가할 내용입니다.</param>
    /// <param name="utcExpiry">캐시된 항목이 만료되는 날짜와 시간입니다.</param>
    public override object Add(string key, object entry, DateTime utcExpiry)
    {
        if(IsDebugEnabled)
            log.Debug("ASP.NET Page OutputCache를 캐시에 추가합니다. key=[{0}], utcExpiry=[{1}]", key, utcExpiry);

        CacheRepository.Set(key, entry, utcExpiry.Subtract(DateTime.UtcNow));

        return entry;
    }

    /// <summary>
    /// 지정된 항목을 출력 캐시에 삽입하고 이미 캐시되어 있는 경우 해당 항목을 덮어씁니다.
    /// </summary>
    /// <param name="key"><paramref name="entry"/>에 대한 고유 식별자입니다.</param><param name="entry">출력 캐시에 추가할 내용입니다.</param>
    /// <param name="utcExpiry">캐시된 <paramref name="entry"/>가 만료되는 날짜와 시간입니다.</param>
    public override void Set(string key, object entry, DateTime utcExpiry)
    {
        if(IsDebugEnabled)
            log.Debug("ASP.NET Page OutputCache를 캐시에 설정합니다. key=[{0}], utcExpiry=[{1}]", key, utcExpiry);

        CacheRepository.Set(key, entry, utcExpiry.Subtract(DateTime.UtcNow));
    }

    /// <summary>
    /// 출력 캐시에서 지정된 항목을 제거합니다.
    /// </summary>
    /// <param name="key">출력 캐시에서 제거할 항목에 대한 고유 식별자입니다. </param>
    public override void Remove(string key)
    {
        if(IsDebugEnabled)
            log.Debug("ASP.NET Page OutputCache를 삭제합니다. key=[{0}]", key);

        CacheRepository.Remove(key);
    }
}
-------------------------------------------------------------------

AbstractOutputCacheProvider 를 상속받아, SharedCacheRepository, MongoCacheRepository, RedisCacheRepository 등을 제공하게 되면 각각의 Cache Server 별로 OutputCacheProvider를 손쉽게 만들 수 있습니다. 이렇게 ICacheRepository로 나눈 것은 OutputCache 뿐 아니라 ViewState 를 저장하는 PageStatePersister, 세션 상태를 저장하는 SessionStateStoreProvider 를 손쉽게 구현할 수 있기 때문입니다.

마지막으로, OutputCacheProvider 를 적용하기 위해서는 web.config 에 다음과 같이 지정해 주시면 됩니다.

우선 Memcached 서버에 대한 설정을 지정해 줍니다.

-------------------------------------------------------------------

<configSections>
    <!-- Memcached -->
    <sectionGroup name="enyim.com">
        <section name="memcached" type="Enyim.Caching.Configuration.MemcachedClientSection, Enyim.Caching"/>
    </sectionGroup>
</configSections>

<!-- Memcached -->
<!-- 참고: https://github.com/enyim/EnyimMemcached/wiki/MemcachedClient-Configuration -->
<enyim.com>
    <memcached protocol="Binary">
        <servers>
            <add address="127.0.0.1" port="11211"/>
        </servers>
        <socketPool minPoolSize="10" maxPoolSize="100" connectionTimeout="00:00:10" deadTimeout="00:02:00"/>
    </memcached>
</enyim.com>
-------------------------------------------------------------------

마지막으로 OutputCache 에 대한 Provider를 지정합니다.

-------------------------------------------------------------------
<system.web>
    
    <caching>
        <outputCache defaultProvider="MemcachedOutputCacheProvider">
            <providers>
                <add name="MemcachedOutputCacheProvider"                   type="NSoft.NFramework.Caching.Memcached.Web.MemcachedOutputCacheProvider, NSoft.NFramework.Caching.Memcached"/>
            </providers>
        </outputCache>
    </caching>
-------------------------------------------------------------------

자 좀 복잡해 보이지만,  .NET 4.0 에서 제공하는 이러한 확장성을 제대로 활용하면, 좀 더 유연하고, 견고한 제품을 만들 수 있을거라 생각됩니다.

특히 CMS 등 컨텐츠의 Read 가 월등히 많은 경우에 더욱 효과가 있고, 메모리의 제한이 있다면, MongoDB 등을 활용하는 것도 좋은 방안이 될 수 있습니다.

아니면 gzip으로 압축하여 캐시에 저장하고, Browser가 gzip을 지원하는 경우 gzip 된 컨텐츠를 아무런 처리없이 그대로 응답할 수 있으므로, 서버 리소스 절약 및 성능 향상에 기여할 수 있습니다.

다음으로는 실제 ASP.NET MVC 에서 활용하는 방안에 대해 알아보겠습니다.