카테고리 보관물: Java & Spring

java -jar 실행시 외부 jar 파일에 대한 classpath 가 동작하지 않는다.

내가 만든 java 프로그램을 실행 가능한 jar 로 묶는다. 이때 외부 라이브러리를 사용하기 위해서 classpath 에 외부 라이브러리 경로를 적어주고 다음과 같이 실행을 한다.

java -jar myprogram.jar -cp (또는 classpath) "./lib/externaljar.jar" 

위 처럼 실행을 하면 externaljar.jar 에 정의된 클래스를 찾을 수 없다는 NoClassDefFoundError 와 같은 오류를 만나게 될것이다.

java -jar 형태로 실행하게 되면 외부 jar 경로를 명시한 classpath 는 무시된다. 해결 방법으로는 내가 만든 프로그램까지 모두 classpath 에 명시한 뒤 실행할 class 를 직접 명시하는 방법이다.

java -cp "./myprogram.jar:./lib/externaljar.jar" com.wordpress.referto.MainClass 

guava Multimaps.newMultimap() 을 이용하여 원하는 multimap 만들기

guava 는 google 에서 제공하는 다양한 자료구조와 유틸리티들을 모아둔 라이브러리이다. 최근 map 자료구조를 사용하면서 하나의 key 에 value 가 리스트로 붙는 자료구조가 필요하게 되었다. 그냥 자바 코드로 생각하면 다음과 같은 모양이다.

Map<String, List<String>> multimap = new HashMap<String, List<String>>();
multimap.put( "key", new ArrayList<String>() );
multimap.get( "key" ).add("value1");
multimap.get( "key" ).add("value2");

guava 의 Multimap 자료 구조를 이용하면 다음과 같이 단순화 할 수 있다.

Multimap<String, String> multimap = ArrayListMultimap.create();
multimap.put("key", "value1");
multimap.put("key", "value2");

List 와 Map 에도 다양한 implementation 이 있듯이 guava 에서도 multimap 에 대한 다양한 implementation 이 존재한다.

하지만 모든 기대를 충족할 수는 없으니 좀 더 custom 된 multimap 이 필요하게 되었다. 바로 key 는 알파벳순으로 정렬이 되지만 value 는 삽입된 순서대로 정렬이 되는 구조의 multimap 이다. 이러한 multimap 을 위해서 각 특성을 가진 collection 을 인자로 받아서 multimap 을 선언할 수 있도록 Multimaps.newMultimap() 이라는 인터페이스가 존재한다. key 는 정렬을 할 수 있도록 TreeMap 을 사용하였고 value 부분은 삽입 순서만 지키면 되어서 ArrayList 를 사용하였다.

   Multimap<String, String> options = Multimaps.newMultimap(
                new TreeMap<String, Collection<String>>(), new Supplier<List<String>>() {
            @Override
            public List<String> get() {
                return Lists.newArrayList();
            }
        });

        options.put("z", "d");
        options.put("z", "a");
        options.put("b", "c");
        options.put("b", "e");
        options.put("b", "d");
        options.put("a", "c");
        options.put("a", "b");

        final Iterator<String> iter = options.keySet().iterator();

        while ( iter.hasNext() ){
            String s = iter.next();
            System.out.println(s + " / " + options.get(s) );
        }

java string 의 공백 제거, 12288 문자 제거하기

Java 에서 문자열의 앞, 뒤, 그리고 중간 공백을 모두 제거하기 위해서 다음과 같은 코드를 사용한적이 있다.

String s = s.trim().replaceAll(" ", "");

이런식으로 대부분의 공백은 제거 되지만 일부 언어의 공백이 제거되지 않고 남아 있는 경우가 있었는데 그 character 값을 찍어보니 12288값이 나왔다.
정확한 정의는 찾지 못했지만 IDEOGRAPHIC SPACE 라고 불리는 문자이고 한국,중국,일본어 등에서 나오는 공백 문자인것 같다.

이를 제거하기 위해서는 위와 같은 방법이 아닌 아래와 같은 방법을 사용한다.

String s = s.replaceAll("\\p{Z}", "");

좀 더 자세한 답변은 stackoverflow 참고.

Java 에서 public final class 란?

오픈소스 코드를 읽다 보니 다음과 같이 선언된 class 를 종종 보았다.

public final class XXX { }

보통 final 은 변수를 선언할 때 해당 변수의 값이 변경되는것을 방지하기 위해서 사용하는 것인데 class 앞에 사용되는 final 은 어떤 의미인지 의문이 들었다.

답은 간단하다.

final 이 붙은 class 는 더이상 확장할 수 없다. 즉 상속받아서 사용할 수 없다는 것이다.

이것은 원래부터 상속을 위해 디자인된 클래스가 아닌 일반 클래스를 마구잡이로 상속하여 프로그램의 디자인이 깨지고 예상치 못한 일이 발생하는 것을 막아주기 위한 수단이다.

http://stackoverflow.com/questions/218744/good-reasons-to-prohibit-inheritance-in-java

여기서도 볼 수 있듯이 class 는
1. 상속을 위해 디자인되고 그 에 관한 문서를 충분하게 만들거나
2. 상속 자체를 막아버리거나

두가지 경우로 사용하는 것이 안전한 프로그램 방법이라고 설명한다. ( 이펙티브 자바를 보지는 않았지만 그 내용 중 일부에 있다고 함)

maven scope 에서 provided 는 언제 쓸까?

maven 을 쓴지 좀 되었는데 별 생각없이 쓰다가 오늘 이슈를 하나 만났다.
요약하면 같은 라이브러리인데 모듈마다 사용하는 버전이 달라서 하나의 어플리케이션에 버전별로 디펜던시에 추가되는 문제이다.

예를 들면,
mymodule 이라는 모듈이 있고 이것을 Spring application 에서 약간 편리하게 사용하기 위해서 mymodule-spring 이라는 wrapper 같은 것을 만들고 있었는데 스프링 모듈을 가져다 사용해야 했다. 그래서 다음과 같이 pom.xml 에 dependency 를 추가했다.

 <!-- spring -->
 <dependency>
     <groupId>org.springframework</groupId>
     <artifactId>spring-context</artifactId>
     <version>${spring.version}</version>
 </dependency>

그리고는 pom.xml 에 properties 에 spring.version 값을 정의하였다.
그런데 이 모듈을 가져다 사용하는 어플리케이션의 스프링 버전이 mymodule 과 다른 버전이면? 최종적으로 서로 다른 두 버전의 스프링 라이브러리가 필요하게 된다. 처음에 이 mymodule-spring 을 만들게 된 계기가 mybatis-spring 사용하고 난 후여서 mybatis-spring 의 pom.xml 을 확인해봤다. 링크

여기서 확인할 수 있었던 부분이 mybatis 가 spring 모듈을 사용한 부분에서 scope 를 provided로 지정해 준 것이었다.

<dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>${spring.version}</version>
      <scope>provided</scope>
</dependency>

provided scope 에 대해서 maven 의 문서를 보면 다음과 같다.

This is much like compile, but indicates you expect the JDK or a container to provide the dependency at runtime. For example, when building a web application for the Java Enterprise Edition, you would set the dependency on the Servlet API and related Java EE APIs to scope provided because the web container provides those classes. This scope is only available on the compilation and test classpath, and is not transitive.

위 내용은 번역해 보자면

이 스코프는 compile 과 유사하지만 JDK 나 container 가 의존성을 런타임에 제공한다. 예를 들면 JavaEE 웹 어플리케이션을 만들 때, Servlet API 나 JavaEE API 같은 의존성들은 provided 스코프로 지정하는데 이는 웹 컨테이너가 해당 클래스들을 제공하기 때문이다. (즉, 웹 컨테이너가 달라지면 웹 어플리케이션에 제공되는 부분들이 달라질 수 있다는 말) 이 스코프는 컴파일과 테스트 classpath 에서만 유효하고 transitive 하지 않다.

여기서 transitive 하다는 말은 (단어 번역을 잘 못하겠는데) 일반적으로 a 가 b 와 연관이 있고 b 가 c 와 연관이 있을 때, a 는 c와 연관이 있다고 말할 수 있는 상태이다. 즉 a 모듈이 b 를 의존성으로 가지면 c 역시 의존성으로 가지게 된다는 것이고 이는 maven 2.0 부터 지원되는 transitive dependencies 의 개념이다.

위 설명에서 provided 는 not transitive 하기 때문에 어플리케이션에서 mymodule 을 의존성으로 가져도 mymodule 에서 provided 로 선언된 스프링 라이브러리는 의존성을 가지지 않게 되는 결과를 가져온다.

그럼 고민 끝.

Spring MVC + jquery ajax 에서 Json 의 배열을 전송하기

Spring MVC 로 어플리케이션 백엔드를 개발하고 있는데 프론트에서 Json 객체의 배열을 전송하고 Spring 에서는 이 객체를 미리 정의된 Bean 의 List 로 받는 예제이다.

Spring 의 컨트롤러에서 해당 API와 매핑되는 함수의 모양은 다음과 같다.

@RequestMapping(value = "/{somepath}/multi", method = RequestMethod.POST)
@ResponseBody
ResponseEntity insertMulti( @PathVariable(value = "somepath") String somepath,
                            @RequestBody List<SomeBean> dataList ) {

프론트의 javascript 에서는 jquery 를 이용해서 다음과 같이 전송한다.
myJsonDataArray 는 위에서 사용하는 SomeBean 의 필드와 값을 가지고 있는 Json object 의 array 이다.

$.ajax({
       type: "POST",
       url: "somepath/multi",
       data: JSON.stringify( myJsonDataArray ),
       dataType: "json",
       contentType : 'application/json'
});

@PathVariable 에서 . 이후 값이 잘리는 경우

얼마전 Spring 을 이용해서 api 를 만들고 있었는데 Url 패턴에 매칭되어 함수로 진입을 하지만 @PathVariable 을 이용해서 그 값을 가져오면 . 이하 부분이 잘리고 들어오는 경우가 있었다.

@RequestMapping(value = "/{term}", method = RequestMethod.GET)
public ResponseEntity<String> showValuesOfTerm(@PathVariable(value = "term") String  term )

이런 식의 코드를 사용해서 term 에 대한 검색을 진행하는 로직을 담으려고 했다. 그런데 문제가 “dr.김” 이라는 term 이 들어오면 term 변수에는 “dr” 값만 들어오는 것이다.

디버깅을 해보니 spring 에서 url 을 가지고 매핑할 method 를 찾는데 그 때 RequestMapping 의 value 에 있는 url 패턴을 이용하는 방식이  /{term}.* 이런식으로 regex 매칭을 사용하다 보니 실제 term 에 매칭 되는 부분이 “dr.김” 에서 “dr” 부분만 매칭이 되어 . 이하 부분은 잘리게 되었다.

특별한 해결 방법을 찾기 보다는 url  의 path 에서 . 이 들어오는것은 자제하고 파라미터로 해당 값을 받는것으로 수정하였다.

Dependency Injection, Dependency lookup, Provider

일반적으로 스프링을 사용할 때 @Component 또는 @Service 등의 어노테이션이 명시된 클래스의 객체 ( 또는 설정 xml 의 bean 노드에 정의된 ) 는 application context 와 생명주기를 함께하는 싱글톤 객체라고 볼 수 있다. 이런 객체들을 사용하는 곳에서는 @Autowired 어노테이션만 이용하면 해당 객체를 스프링 프레임워크로 부터 주입 받아 사용할 수 있다. 이것이 일반적으로 스프링에서 사용하는 의존성 주입 (DI) 이다. 어떻게 해당 객체를 만드는지에 대해서는 개발자는 신경쓰지 않아도 된다.

DI 을 사용하지 않는 객체들 중에는 메소드 내부 변수처럼 한번만 쓰고 버리는 임시 변수 같은 객체들도 분명히 존재한다. 이러한 객체들은 스프링 프레임워크에서 관리하지 않고 개발자가 직접 new 를 사용하여 객체를 생성하고 관리하게 된다. 하지만 이러한 임시 객체에서 스프링으로부터 의존성을 주입받아야 하는 bean들이 필요하면 어떻게 할까? @Autowired 를 명시해 놓는다고 DI 를 받을 수 있을까?

DL (dependency lookup) 은 일반적으로 프레임워크에서 생성한 객체들을 프레임워크가 직접 주입해주는것이 아니라 개발자가 개발 코드상에서 해당 객체를 요청하여 받아오는 방식을 말한다. application context 나 bean factory 같은것을 사용하여 프레임워크에서 생성하는 객체들을 받아올 수 있다. 이 때는 생성된 임시 객체에서 사용할 각종 서비스빈들을 프레임워크에서 DI 를 받을 수 있다.

이를 위해서 추천할만한 방법으로 JSR-330 에 정의된 Provider 를 이용하는 방법이다. Provider 는 스프링프레임워크에 기본적으로 포함되어 있지 않기 때문에 아래의 dependency 를 추가한다.

<!-- JSR 330 -->

<dependency>

<groupId>javax.inject</groupId>

<artifactId>javax.inject</artifactId>

<version>1</version>

</dependency>

Provider 를 이용한 코드의 예제는 다음과 같다.

@Autowired

private Provider<MyTask> taskProvider;

public void test(){
     MyTask task = taskProvider.get();
}

Provider 는 인테페이스인데 이 인터페이스에 대한 실제 구현은 스프링에서 알아서 바인딩해준다. 또한 위에서 로컬 변수처럼 해당 scope 내에서 생성되었다가 소멸되는 객체의 경우는 scope을 prototype 으로 지정해줘야 한다.

찾아보면서 만들었더니 대충 아래와 같은 모양의 class가 되었다.

@Component
@Scope (“prototype”)

public class MyTask {

@Autowired

MyService myService;
}

reference
http://toby.epril.com/?p=947
http://toby.epril.com/?p=971
http://whiteship.tistory.com/2533

HashMap 내부 구현 및 동작

Java 에서 자료를 저장하고 검색하기 위해 가장 많이 사용하는 자료구조인 HashMap 이 내부적으로 어떻게 구현 되고 동작하는지 알아보자. 모든 부분은 설명하지 않고 중점적으로 사용되는 부분만을 볼 예정.

먼저 HashMap 에 저장되는 가장 기본적인 자료구조는 key, value 값을 저장하고 있는 Entry 가 있다.


Entry<K,V>  {

int hash;   //hashMap 내부에서 계산된 hash 값

final K key; // 입력된 key

V value;  // 입력된 value

Entry<K,V> next;  // linkedList 형태

}

그리고 각 entry 들을 저장하기 위한 array 인 table 이 있다.

Entry<K,V> [] table;

그 외 중요 변수로는

final float loadFactor;

int threshold;

int modCount;

1. put 

put() 메소드를 통해 key, value 값이 저장되면 내부적으로 아래와 같은 모습으로 저장된다.

스크린샷 2013-10-09 오후 9.47.40

이미지 출처 : https://mkbansal.wordpress.com/2010/06/24/hashmap-how-it-works/

저장되는 순서는 아래와 같다.

1) key 객체의 hashCode() 값을 이용하여 내부적으로 새로운 hash 값을 구한다.

2) 새로운 hash 값을 이용하여 table 배열에서 몇번째 index 에 저장할지 index 를 찾는다. ( indexFor 함수 )

3-1) 구해진 index 에 entry 가 이미 존재하면 linked-list 형태로 연결한다.

3-2) 구해진 index 에 entry 가 없으면 그냥 assign 한다.

4) 만약 key 가 기존에 존재하던 값이면 value 만 replace 하고 이전 value 값을 리턴한다.

2. doubling 

위에서도 볼 수 있듯이 hashMap 에서는 entry 를 저장할 수 있는 table 배열의 크기가 정해져 있기 때문에 많은 수의 entry 가 입력되면 collision 이 자주 발생하게 되고 이는 성능저하를 가져오게 된다. 이를 방지하기 위해서 HashMap 은 table 배열의 사이즈를 doubling 하게 된다. ( 배열의 크기는 항상 2의 제곱수를 유지한다 )

1) entry 저장 시 entry 개수가 threshold ( table.size * loadFactor ) 를 넘어서면 table 을 doubling

2) table 사이즈가 커졌기 때문에 기존에 저장된 모든 entry 에 대해서  rehash 를 수행하여 entry 분산

3) table.size 가 maximum_capacity 를 넘어가면 doubling 하지 않음.

doubling 은 기존 배열의 두배 크기의 배열을 새롭게 만들고 기존의 entry 를 rehash 하며 옮겨오는 방식으로 진행된다.

3. indexFor 

앞서 설명했듯이 내부적으로 새롭게 계산된 hash 값을 이용하여 table 에 저장된 index 값을 구하게 되는데 이를 위해 indexFor 함수를 호출한다. 일반적으로 table size 만큼 entry 를 분산해야 하기 때문에 modular 방식을 생각할 수 있겠으나 좀 더 효율적인 분산을 위해 아래와 같은 방법으로 구현되어 있다.

static int indexFor ( int h, int length ){

return h & ( length -1 );

}

h 는 hash 값, length 는 table 배열의 size 이다.

table 의 length 는 2의 제곱수이기 때문에 length – 1 은 모든 비트가 1로 채워진 수가 된다. 그 값과 h 값을 & 연산을 통해서 index 값을 구하게 된다. 이런 방식을 취하면 length 값이 두배로 증가하였을때는 상위 하나의 비트가 더 추가된 값과 h 값이 & 연산을 하게 된다. 그 결과는 반드시 기존 index 값과 같거나 doubling 으로 새롭게 추가된 영역의 index 만을 가지게 되기 때문에 좀 더 효율적인 entry 의 분산을 기대할 수 있다.

스크린샷 2013-10-30 오전 12.29.01
4. ConcurrentModificationException

HashMap 은 thread-safe 한 자료구조가 아니다. 즉, 특정 thread 가 자료를 읽는 중에도 다른 thread 가 map 내부의 자료를 삭제할 수 있다. 이는 multi-thread 환경에서 분명 문제가 된다. 이같은 문제 때문에 HashMap 에서는 entry 들을 iteration 하는 도중 자료구조에 변경이 일어나면 ConcurrentModificationException 을 발생시켜 해당 문제를 알려준다.

이를 위해서 HashMap 내부에 modCount 라는 변수를 두고 HashMap 의 모든 업데이트의 발생시 modCount 를 증가시켜 준다. 그리고 entry 의 iteration 을 시작하기 전에 modCount 값을 다른 임시 변수에 복사 해 두고 매번 next 호출때 마다 두 변수 값을 비교하여 차이가 발생하면 ConcurrentModificationException 을 발생시킨다

5. get

인자로 받은 key 에 매핑되는 value 값을 찾는 방식은 put 의 방식과 유사하다.

1) key 객체의 hashCode() 값을 이용하여 내부적으로 새로운 hash 값을 구한다.

2) 새로운 hash 값을 이용하여 table 배열에서 몇번째 index 에 저장되어 있는지 찾는다.  ( indexFor 함수 )

3-1) 구해진 index 에 entry 의 key 와 동일한 객체인지 equals 를 이용하여 비교하여 동일하면 entry 의 value 를 리턴한다.

3-2) equals 결과가 false 이면 entry 의 next 링크를 계속 따라가며 동일한 key 를 가진 entry 를 찾는다.

일반적인 hash 의 자료구조는 위와 같은 방식으로 구현되어 있다.

주의해서 알아야 할점은 entry 의 개수가 많아지면 doubling 이 자주 일어나고 이 때 새로운 table 을 위한 memory 공간 확보, 기존의 모든 entry 에 대해서 rehash 등으로 인한 오버헤드가 발생한다.  HashMap 생성시 entry 의 개수를 미리 예측할 수 있다면 doubling 으로 인한 오버헤드를 줄일 수 있다. 또한 doubling 으로 인한 memory 낭비가 있을 수 있기 때문에 정적인 데이터의 경우 loadFactor 값을 높게 조정해 볼 수도 있을것이다.

@ModelAttribute 사용시 오류 내용 확인

@ModelAttribute 를 사용하면 request parameter 로 넘어오는 데이터를 이용하여  ModelAttribute 어노테이션으로 선언한 객체를 생성하여 값을 세팅할 수 있다. 이때 객체 바인딩에 오류가 생기면 400 을 리턴하고 함수 진입 조차 하지 못하는데 이 경우 도무지 오류의 내용을 알기 어려운 경우가 있었다.


@RequestMapping( value="edit", method=RequestMethod.POST )

public ModelAndView editMyData ( ModelAndView mav, @ModelAttribute MyData mydata ){

......

}

이 경우 함수의 인자로 BindingResult 를 함께 받아서 출력해보면 오류의 내용을 확인할 수 있다.


@RequestMapping( value="edit", method=RequestMethod.POST )

public ModelAndView editMyData ( ModelAndView mav, @ModelAttribute MyData mydata, BindingResult errors  ){

if ( errors.hasErrors() ){

logger.error( errors.getAllErrors() );

}

}