2014-10-06

JPA Entity와 Java 8

아직 하이버네이트에 대해서는 테스트해보지 않았지만, 이클립스링크에서 문제가 발생하므로,  비슷한 처리를 하는 하이버네이트에서도 똑같은 문제가 있을 것이다.

엔티티에 @OneToMany@ManyToMany로 컬렉션(List, Set, Map) 필드가 있을 때, 이 필드가 wrapper로 세팅되어있을 경우 (이클립스링크의 경우 IndirectList, IndirectSet, IndirectMap) 일부 Java 8 기능이 먹지를 않는다.

특히 가장 중요한 stream이 동작하지 않는다.
Aㅏ... 망했어요.
이보시오, 이보시오! JPA양반! 그게 무슨 소리요! Stream이 고자라니!

뭐 별 거 있나, wrap 된거를 꺼내오거나 그냥 새로운 컬렉션을 만들어서 값을 복사해서 쓰면 된다.
참 쉽죠? 너무 쉬워서 예제 따위는 없어요.



뱀발:
엔티티 내부에 Java 8 기능을 우겨넣으면 씐나게 로딩하다가 에러가 날 것이다.
아쉽게도 JPA는 Java 8 기능이 들어간 엔티티는 엔티티로 취급하지 않는다 (...)

물론 그런건 별도의 클래스로 분리해 놓고 써먹으면 그만이다.
Share:

2014-07-17

Spring Framework Error Handling: Accept 헤더와 무한루프

이거 때문에 그나마 없던 생활 리듬마저 개박살 나고 말았다아아아아아아~~~~


Spring Framework에는 당연히 에러 처리기가 있고, 적절한 Exception을 적절한 에러 코드로 변환해서 클라이언트에 던진다.
그리고 Spring Boot에는 기본 Error 처리 컨트롤러가 있어서, 적절하게 html 또는 json으로 에러 메세지를 뿌려준다.

그런데, 여기서 모든 문제가 시작된다!

Spring Boot를 사용하는 프로젝트를 하나 만들고, 적절한 Servlet Container Tomcat이라거나 Jetty라거나 를 사용해서 띄워본다.
그리고 (리눅스의 경우) 터미널을 열고 아래 명령을 때려봐라.

HTTP일 경우
curl -H "Accept: application/octet-stream" http://localhost:8080/<context-path>/error

HTTPS일 경우
curl -k -H "Accept: application/octet-stream" https://localhost:8443/<context-path>/error

CPU가 미친듯이 돌아가면서 StackOverflowException을 떨굴 것이다.
지옥에 입장한 것을 축하한다.

원인은 Exception에서 볼 수 있듯이 무한루프인데, Accept에 다른거 text/html이나 application/json 같은거 를 넘기면 정상 동작하는 것을 봐서는 에러 처리 로직에 문제가 있는 것이다.
그래서 디버그를 돌려본 결과...

일단 Spring Boot의 BasicErrorController는 정상적으로 탄다.
그리고 그 결과를 JSON으로 변환하는데, 문제는 Jackson은 application/octet-stream 따원 모른다네.
그래서 Jackson은 난 이런거 모른다네! 하고 에러를 던지고, Spring 에러 핸들러는 이걸 HTTP 응답 코드 406으로 변환한다.

여기까지는 좋았지.
Request에 Accept가 그대로 남아있는고로...

Request의 Accept는 끝이 없고
같은 무한루프를 반복한다.



그래서 이걸 어떻게 해결하느냐고?
안알랴줌.






이면 이 글을 안썼겠지.

Filter를 하나 추가해서 406 응답코드를 인식하면 Request를 Wrapper로 감싼다.
Wrapper는 Accept 헤더의 값을 요청할 경우 null을 떨구면 된다.

참 쉽죠?


p.s
Spring Framework로 생각하고 있었는데 spring-boot의 문제였고, 1.1.5에서 수정되었다.
(https://github.com/spring-projects/spring-boot/issues/1257)
Share:

2014-04-16

Gradle + Eclipse Expert

일단 이 글은 Gradle을 가지고 제대로 삽질 한 번쯤 해봤으면 쉽게 이해할 수 있다.
Gradle 1.11 기준이다. 난 최신을 좋아하지 (물론 삽질도...)

시작하기 전에...

당연히 Eclipse Plugin을 사용해야 된다.
일반 프로젝트라면
apply plugin: 'eclipse'
웹 프로젝트라면
apply plugin: 'eclipse-wtp'

http://kwonnam.pe.kr/wiki/gradle/eclipse도 참고하면 매우 좋다.

Package Explorer와 Project Explorer 정리

Gradle로 그냥 이클립스 프로젝트를 만들면 Dependencies JAR 파일들이 Package Explorer와 Project Explorer를 점령하게 된다. 이제 그것들을 몰아낼 시간이다.

build.gradle에 다음을 추가한다.
eclipse {
    classpath {
        file {
            whenMerged { classpath ->
                def entries = classpath.entries
                entries.findAll { it.kind == 'lib' }*.exported = false
            }
        }
    }
}
그리고 Gradle -> Refresh All을 하면 Maven 쓸 때처럼 깔끔하게 정리된 모습을 볼 수 있다.

WTP Facet 설정

Gradle은 WTP Facet을 알아서 무지 구식으로(!!!) 설정해준다. 진짜로. Servlet 2.3이라니 잘도 이런 구석기 시대 세팅을!
따라서 이런 사태를 방지하기 위해 facet 설정을 따로 해줘야 한다.

아래 설정은 Java 1.7, Servlet 3.0을 사용하는 facet 설정이다.
eclipse {
    wtp {
        facet {
            facet name: 'jst.web', version: '3.0'
            facet name: 'java', version: '1.7'
            facet name: 'wst.jsdt.web', version: '1.0'
        }
    }
}

Project Dependency Correction with Hierarchical Project Name

Spring 프로젝트에서 제공하는 이클립스 Gradle IDE 플러그인이 꽤 괜찮은 기능을 제공하는데, 그 중 하나가 Hierarchical Project Name 지원으로, 이클립스상의 프로젝트명을 root.child 같이 만들어준다. Gradle 프로젝트 임포트시 Use hierarchical project names를 체크하면 이 기능을 사용할 수 있다. 이를 이용해서 여러 프로젝트에 같은 이름의 서로 다른 하위 프로젝트를 가지고 있더라도 아무 문제없이 사용할 수 있다.
다른 프로젝트를 참조하기 전 까지는...

Hierarchical Project Name을 사용한 프로젝트를 dependencies에 추가할 경우 플러그인이 해당 프로젝트를 찾지 못해서 정신줄을 놓게 된다!

뭐 별 거 있나... 직접 수정하면 될 것을...

build.gradle에 다음을 추가한다.

For WTP Project

eclipse {
    wtp {
        component {
            file {
                withXml { provider ->
                    def module = provider.asNode().'wb-module'
                    module.'dependent-module'.findAll { it.@'handle'.startsWith('module:/resource') }.each { entry ->
                        def name = entry.@'handle'.substring('module:/resource/'.length(), entry.@'handle'.lastIndexOf('/'))
                        entry.@'handle' = "module:/resource/${rootProject.name}.${name}/${rootProject.name}.${name}"
                        entry.@'archiveName' = "${name}.jar"
                    }
                }
            }
        }
    }
}

For Normal Project

eclipse {
    classpath {
        file {
            whenMerged { classpath ->
                def entries = classpath.entries
                entries.findAll { it.kind == 'src' && it.path.startsWith('/') }.each {
                    def name = it.path.substring(1)
                    it.path = "/${rootProject.name}.${name}"
                }
            }
        }
    }
}

그리고 Gradle -> Refresh All을 하면 정상적으로 프로젝트 참조를 인식한다.

[2016-08-26] 어... 플러그인이 buildship으로 바뀌면서 이건 더이상 못쓰는데, 사실 더 쉽고 간단한 방법이 있었다. (...)
http://divinespear.blogspot.com/2016/08/gradle-eclipse-expert-2016-edition.html 를 보시라.

Web App Libraries 라이브러리 그룹 비우기

Web App Libraries 라이브러리 그룹은 Maven으로 프로젝트 만들 시절에는 구경도 못해본 거였는데, 이게 가끔 말썽을 부린다.
그래서 이클립스에서 이를 무시하도록 설정을 좀 봐줘야 한다.

build.gradle에 다음을 추가한다.
eclipse {
    classpath {
        containers.removeAll(EclipseWtpPlugin.WEB_LIBS_CONTAINER)
    }
    wtp {
        component {
            file {
                withXml { provider ->
                    def module = provider.asNode().'wb-module'
                    module.'dependent-module'.findAll { it.@'handle'.startsWith('module:/classpath') }*.replaceNode {}
                }
            }
        }
    }
}
그리고 Gradle -> Refresh All을 하면 Web App Libraries에 프로젝트 참조를 제외한 모든 JAR 참조가 날아가 있는 것을 볼 수 있다.

WAR Overlay

Gradle의 WAR Overlay 자체는 참 간단하긴 한데... 이클립스 플러그인이 이를 인식하지 못하니 참으로 통탄할 일이로다.

그래서 역시 설정을 직접 고치면 된다. 안되는게 어디있나... 안되면 되게하라.

build.gradle에 다음을 추가한다.

Eclipse 4.4 이전

eclipse {
    wtp {
        component {
            file {
                withXml { provider ->
                    def module = provider.asNode().'wb-module'
                    module.'dependent-module'.findAll { it.@'handle'.startsWith('module:/overlay') }.each { entry ->
                        entry.'dependency-type'.each {
                            it.value = 'consumes'
                        }
                    }
                }
                whenMerged { component ->
                    // 현재 프로젝트: 무조건 제일 위에 와야함
                    component.wbModuleEntries += new org.gradle.plugins.ide.eclipse.model.WbDependentModule('/', 'module:/overlay/slf/?includes=**/**&excludes=META-INF/MANIFEST.MF')
                    // overlay할 WAR 프로젝트들을 밑에 나열한다.
                    component.wbModuleEntries += new org.gradle.plugins.ide.eclipse.model.WbDependentModule('/', 'module:/overlay/prj/OverlayProject?includes=**/**&excludes=META-INF/MANIFEST.MF')
                    ...
                }
            }
        }
    }
}

Eclipse 4.4 이후

eclipse {
    wtp {
        component {
            file {
                withXml { provider ->
                    def module = provider.asNode().'wb-module'
                    module.'dependent-module'.findAll { it.@'handle'.startsWith('module:/overlay') }.each { entry ->
                        entry.@'handle' = entry.@'handle'.replaceAll('module:/overlay', 'module:/resource')
                        entry.'dependency-type'.each {
                            it.value = 'consumes'
                        }
                    }
                }
                whenMerged { component ->
                    // overlay할 WAR 프로젝트들만 밑에 나열한다. 존나좋군?
                    component.wbModuleEntries += new org.gradle.plugins.ide.eclipse.model.WbDependentModule('/', 'module:/overlay/OverlayProject/OverlayProject?includes=**/**&excludes=META-INF/MANIFEST.MF')
                    ...
                }
            }
        }
    }
}
OverlayProject는 overlay할 WAR 프로젝트명을 써준다. 여러 개의 프로젝트를 overlay할 경우 overlay 순서에 유의해서 추가해주면 된다.

그리고 Gradle -> Refresh All을 하면 정상적으로 WAR Overlay를 인식하고, WTP Server Deploy도 잘 작동한다.

잡담: 이거 삽질이 가장 힘들었다.
Share:

2014-03-10

JPA - Hibernate Specific WTF Rules

JPA 구현 중 가장 널리 쓰이는 것들이 EclipseLinkHibernate이다.
JPA 2.0 이후로는 EclipseLink가 레퍼런스 구현임에도 일단 한국에서는 EclipseLink보다는 Hibernate를 더 많이 쓰는데, Hibernate가 아무래도 역사도 오래됐고 속도는 느리지만 그만큼 편의기능이 많아서 그렇지 않나 싶다.
한국에서 EclipseLink 미는 사람은 저밖에 없는듯?

여기서는 Hibernate 특유의 정신나간 특징들을 내 맘대로 (...) 찝어보겠다.
  1. SEQUENCE를 지원하지 않는 DBMS(예를 들면 MSSQL이나 MySQL)를 타겟으로 @GeneratedValue(strategy = GenerationType.SEQUENCE)를 사용하면 좆된다.
  2. Spring에서 엔티티를 @SessionAttribute로 물릴 경우 @ElementCollectionfetch 속성을 EAGER로 설정해야 한다.
    안그러면 나중에 엔티티 값을 수정할 때 초기화 되지 않았다고 뭐라 그런다.
  3. @ElementCollectionfetch 속성을 EAGER로 설정했으면 Hibernate에서 제공하는 @Fetch로 긁어올 방식을 설정해 주어야 한다.
    안붙여주면 처음 실행할 때부터 테이블 Alias가 없네 하면서 정신줄 놓는다.
일단은 여기까지.
더 밝혀낸 것이 있으면 계속 업데이트될 지도 모른다.
Share:

2013-11-04

java.io.IOException: error=12, Cannot allocate memory? WTF?!

자바의 유서깊은(?) 골칫거리로, 자바에서 fork() + exec() 하는 방식에 문제가 있기 때문이다.
(자세한 내용은 여기를 참고하면 좋다.)

위 링크에서 4가지 해결책이 나온다.
  1. 닥치고 메모리 증설
  2. fork() 호출시 swapfile을 추가하도록 꼼수 사용
  3. 리눅스: sysctlvm.overcommit_memory 속성을 1로 설정
  4. POSIX 호환 시스템: fork() + exec() 대신 posix_spawn() 사용하기

리눅스라면 vm.overcommit_memory=1 이 아무래도 빠르고 편하겠지만, 동작방식이 후덜덜하므로 overcommit이 일어날 때 커널에서 노는 메모리를 수거하는데, 이 때 어떠한 확인도 없이 막무가내로 수거하므로 잠자고 있는 멀쩡한 다른 프로세스의 메모리를 수거할 수 있다! 미션 크리티컬한 시스템에는 그냥 하지마라. 그런 시스템이라면 2웨이 이상일테니 그냥 닥치고 1번이 더 정신건강에 좋을 것이다. 돈은 좀 많이 깨지겠지만 내가 알 게 뭐야...

또한 자바의 fork() + exec()posix_spawn()으로 교체해주는 java_posix_spawn이 개발되어 있으므로, 이를 고려해보는 것도 좋을 것이다. 하지만 실제로는 posix_spawn()을 사용하지 않는다는게 함정vfork() 쓴댄다.

vm.overcommit_memory 관련 문서는 레드햇 문서노벨 문서를 보면 대략 좋다.


2015-02-23:
리눅스에 zram이라는게 있는데, 써본 결과 매우 좋소!
zram 쓰기 전에는 메모리 10그램으로도 헥헥대던게, zram 쓰고 난 다음부터는 메모리에 여유가 생겼다.
물논 CPU가 그만큼 일을 더 하게 되지만 내가 알 게 뭐야...
Share: