2019-07-19

2019-04-12

흉악한 오라클 PARALLEL

약 350만 건에 이르는 데이터가 들어있는 테이블의 데이터를 삭제할 일이 생겼다.
특정 테이블에 값이 없는 레코드만 삭제를 해야 되는데 오라클 DELETE는 JOIN 그런거 없ㅋ어ㅋ...

그래서 구글신을 영접해보니까 PARALLEL 힌트가 있드라고?
해서 실험해봤습니다.





오늘의 슨슈들입니다.
위에서부터 그냥 쿼리, DELETE에만 PARALLEL을 적용한 쿼리, 서브쿼리에도 PARALLEL을 적용한 쿼리 되시겠습니다.

그냥 쿼리부터 보시겠습니다.
아 시바 할 말을 잊었습니다.
저거 저대로 돌리면 최소 반나절이 증발합니다.

그래서 한번 DELETE에 PARALLEL을 적용해 봤습니다.
오 조금 나아졌네요.
하지만 아직 부족합니다.

여러 테이블에 걸쳐서 PARALLEL을 쓰려면 조금 귀찮은 단계를 거쳐야 합니다.
하지만 성능은 확실하죠.





와아! COST가 쭈욱 내려가네요!
마치 연쇄할인마 스팀을 보는 것 같습니다.

Share:

2018-02-27

2017-09-21

mybatis 유감

mybatis 쓰느니 그냥 SQL 날코딩해서 JDBC로 다이렉트 호출 할래...

스프링이라면 JDBCTemplate이라는 거도 있고
쿼리문 날코딩이 귀찮으면 querydsl로 메타데이터 뽑아서 쿼리를 코드로 짜도 되고...
Share:

2017-02-28

Hudson/Jenkins proxy with Nginx

허드슨이나 젠킨스를 맨날 단독으로 띄워서 쓰다가
Let’s Encrypt를 적용하면서 앞에다가 nginx를 두고 프록시로 요청을 받게 설정했다.

그런데 자꾸 다음 메세지가 나면서 로그인할 때나 일부 링크가 엉뚱한 곳으로 간다.
It appears that your reverse proxy set up is broken

구글신의 신탁을 받아 이곳 저곳을 돌아봤는데 다 소용이 없었다.
그래서 그냥 소스를 까볼까? 해서 봤더니…
소스 링크

X-Forwarded-Host를 받고 계셨어요?

그래서 nginx 설정에서 프록시 헤더를
proxy_set_header   X-Forwarded-Host   $host:$server_port;
proxy_set_header   X-Forwarded-Proto  $scheme;
이렇게 설정해주니까 잘 된다.

역시 답 없을 때는 소스를 직접 들여다 보는게 답이네.
Share:

2017-02-24

enum 네 이놈! - 제네릭과 람다와 enum과 헬게이트

조금 전에 enum들에 널려있는 중복 코드들을 제거하는 와중에 일어난 이야기.

원래대로라면 enum에는 코드값만 넣고 텍스트는 따로 properties에 빼놓는데, 이번 고객은 아아주 특이하게도 그렇게 하면 유지보수하기 귀찮다고 소스에 다 때려박아달랜다.
그래서 다 때려박으면서 예제삼아 코드값 <-> 텍스트 상호전환 코드까지 같이 만들었는데…

누가 (울 회사 직원이라고 말 안할거임) 그걸 그대로 복붙해서 enum 수십 개를 만들었고, CPD는 복붙 고마해라 라고 지랄대고…
해서 코드값 <-> 텍스트 상호전환 하는 부분을 별도의 유틸리티 클래스로 분리했다.

문제는 여기서 발생하는데…

먼저 WithCodeLabel이라는 인터페이스를 만들고 메소드를 정의한 다음, 해당 enum들에 implements를 걸었다.
그리고 맨 처음 유틸리티 클래스를 람다식을 활용하여 만들었다.

public class EnumCodeLabelCache<E extends Enum<E> & WithCodeLabel> {

    private final Map<String, E> codeToLabelMap;
    private final Map<String, E> labelToCodeMap;

    private EnumCodeLabelCache(Class<E> cls) {
        final EnumSet<E> set = EnumSet.allOf(cls);
        codeToLabelMap = set.stream().collect(Collectors.toMap(WithCodeLabel::getCode, Function.identity());
        labelToCodeMap = set.stream().collect(Collectors.toMap(WithCodeLabel::getLabel, Function.identity());
    }
    ...
}

그리고 테스트를 돌렸을 때, 처음 보는 당황스러운 에러가…
java.lang.BootstrapMethodError: call site initialization exception
 at java.lang.invoke.AbstractValidatingLambdaMetafactory.validateMetafactoryArgs(AbstractValidatingLambdaMetafactory.java:233)
 at java.lang.invoke.LambdaMetafactory.metafactory(LambdaMetafactory.java:303)
 at java.lang.invoke.CallSite.makeSite(CallSite.java:302)
 at java.lang.invoke.MethodHandleNatives.linkCallSiteImpl(MethodHandleNatives.java:307)
 at java.lang.invoke.MethodHandleNatives.linkCallSite(MethodHandleNatives.java:297)

…모라고요?

곰곰히 머리를 싸매고 생각하다가 분석을 해보니
enum의 각 값의 RTTI에 WithCodeLabel 인터페이스에 대한 정보가 누락되어 있다는 결과밖에 나오지를 않는다.

그래서 람다 말고 메소드 직접 호출로 변경.

public class EnumCodeLabelCache<E extends Enum<E> & WithCodeLabel> {

    private final Map<String, E> codeToLabelMap = new HashMap<>();
    private final Map<String, E> labelToCodeMap = new HashMap<>();

    private EnumCodeLabelCache(Class<E> cls) {
        final EnumSet<E> set = EnumSet.allOf(cls);
        set.forEach(e -> {
            codeToLabelMap.putIfAbsent(e.getCode(), e);
            labelToCodeMap.putIfAbsent(e.getLabel(), e);
        });
    }
    ...
}

이러니까 잘 된다.

이게 enum만 특히 이런건지, 제네릭 특징인지는 더 살펴봐야 알겠지만, 내가 알 게 뭐야.
Share:

2016-08-26

Gradle + Eclipse Expert - 2016 edition

STS가 3.8.x로 업그레이드 되면서 Gradle STS Plugin이 나가리가 되고 Buildship으로 대체되었다.
적용해보니 좋더라.

... 생각해보니 플러그인이 바뀌어서 기능이 싸그리 증발한게 많아서... 살짝 정오표(?)를 써야겠다는 생각이 들었다.
이전 글은 http://divinespear.blogspot.com/2014/04/gradle-eclipse-expert.html 를 참고할 것.

Project Dependency Correction with Hierarchical Project Name

예전에는 플러그인에서 지원하는걸 썼는데, 꼭 그럴 필요 없더라.

예전에는 플러그인에서 "Use hierarchical project names" 옵션을 선택 한 후 build.gradle에서 이렇게 처리했었다. (일반 프로젝트였을 경우)
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}"
                }
            }
        }
    }
}

근데 이러면 저런 삽질 할 필요가 없더라...
eclipse {
    project {
        name = rootProject.name + '.' + project.name
    }
}

진짜로 이게 끝이다.
난 그동안 뭐한거지...
Share:

2015-11-06

2015-04-15

이클립스 properties 에디터 플러그인

보통 구글신께 신탁을 구하면 제일 처음 나오는 플러그인이 아마도..
http://propedit.sourceforge.jp/index_en.html
이거일거다.

쓰지 마라. 특히 OS가 Windows라면...
사유는 http://kwon37xi.egloos.com/4664893 를 참고하시라.


그리고 그 대안으로
http://sourceforge.net/projects/eclipse-rbe/
이런 애가 등장했는데...

불편하다.
궁금하면 한번 써봐.


그래서 구글신의 신탁의 바다를 떠돌다가 새로운 플러그인을 발견했다.
https://github.com/gildur/SimplePropertiesEditor

소스를 대충 훓어보니 제일 위의 플러그인 같은 문제는 없을 것으로 보인다. 난 리눅스니 윈도우 쓰는 동무들이 테스트좀 해보라우!
그리고 두번째보다는 심플하니까 불편하지도 않고...


2016-09-30 추가:
이 플러그인으로 properties 파일을 열었을 경우 STS의 application.properties 자동완성이 적용이 되지 않는다. 그래서 난 application.yml을 쓰지...

Share:

2015-04-08

에휴...

요새 개발자 뽑는다고 면접보는데 따라들어가서 몇가지 기술적인 질문을 한다.

보통 자바 관련 업무가 많아서 자바 관련 질문을 하는데...
  • 어떻게 된 사람들이 intInteger의 차이를 설명을 못하지?
  • 어떻게 된 사람들이 "두 문자열이 같은지 어떻게 비교하냐"는 질문에 대답을 못하지?
  • 어떻게 된 사람들이 주어진 문자열을 뒤집는걸 만들어보라고 했더니 10분 가까이 해메는거지?
클래스/인터페이스 설계 관련 질문은... 뭐 상급자용 질문이니 넘어간다고 쳐도,

늅늅이나 2~3년차면 모르겠는데 이력서상 경력이 5년이 넘어가는 사람들도 이런다.
어찌하오리까...

p.s.
가장 심한 사람들은 질문자가 무슨 질문을 하는지 조차 알아듣지 못하는 사람.
젠장...

Share:

2015-02-21

spring-boot와 함께하는 spring-batch 삽질

시작하기 전에...

여기 쓰인 예제는 연습삼아 만들어본 도로명주소 데이터 덤프 툴 중 일부입니다.
전체 소스는 공개할 생각이 없으니 알아서 공부해서 만드십쇼.

여기에 XML 설정은 눈 씻고 봐도 찾아볼 수 없습니다. XML 설정은 구글신께 신탁을 부탁합시다.

2015-07-31에 추가:
아주 마아아아아안약에 사이트에 도로명주소+기초구역코드(새 우편번호) 적용을 하고 싶은데 나온 검색 결과가 이거라면, 이걸 시도하기 전에 (즉, 자체 디비구축이라는 삽질을 하기 전에) 다음카카오를 만나세요.
여기로 가시면 자바스크립트로 한방에!

사용법

일단 이거부터...
@EnableBatchProcessing

그리고...
  1. 일(Job)을 만든다.
  2. 단계(Step)를 만든다.
끗!

일단 일(Job)부터 만들자

@Bean
public Job job() {
    return jobBuilderFactory.get("address-import")
                            .start(streetStep())
                            .next(partitionedStreetDetailStep())
                            .next(partitionedStreetExtraStep())
                            .next(partitionedLotStep()).build();
}
  1. Job을 만든다.
  2. 시작할 단계를 설정한다.
  3. 다음 단계... 다음 단계... 다음 단계...
  4. 설정 끗!

Job은 이게 끗이다. 진짜로...

도로명주소는 넣는 순서가 중요해서 설정하지 않았는데, 여러 개의 단계를 동시에 돌릴 수도 있다.
그런데 자바 설정으로는 단계를 멀티쓰레드화 하는게 쉽지 않았다. 나 그냥 GG칠래...

일할 단계(Step)도 만들자

각 단계는 다음 순서대로 이루어진다.
  1. 읽는다.
  2. 가공한다.
  3. 쓴다.
참 쉽죠?

실제로는 쓰기 작업이 있으면 가공 작업은 생략이 가능하고, 가공 작업이 있으면 쓰기 작업을 생략할 수 있다.
@Bean
public Step streetStep() {
    return stepBuilderFactory.get("street-address")
                             .<AddressStreet, AddressStreet> chunk(getChunkSize())
                             .reader(streetReader())
                             .writer(streetWriter())
                             .taskExecutor(taskExecutor)
                             .throttleLimit(THROTTLE_LIMIT)
                             .build();
}
이 예제에는 읽기와 쓰기만 있는데, 도로명 주소는 그냥 읽어서 쓰기만 하면 되기 때문이다.

chunk는 읽기/쓰기 단위를 설정한다. 100을 설정하면 읽고 쓰는걸 100개 단위로 하게 된다.
taskExecutor, throttleLimit는 밑에서 설명을...

읽기

여기서는 주어지는 대상이 파일이니 파일을 읽는 것으로...
@Bean
public FlatFileItemReader<AddressStreet> streetReader() {
    DefaultLineMapper<AddressStreet> lineMapper = new DefaultLineMapper<>();
    lineMapper.setLineTokenizer(lineTokenizer);
    lineMapper.setFieldSetMapper(new FieldSetMapper<AddressStreet>() {
        @Override
        public AddressStreet mapFieldSet(FieldSet fieldSet) throws BindException {
            AddressStreet street = new AddressStreet();
            street.setId(String.format("%12s%2s", fieldSet.readString(0), fieldSet.readString(3)));
            street.setCode(fieldSet.readString(0));
            street.setCodeIndex(fieldSet.readString(3));
            street.setName(fieldSet.readString(1));
            street.setRegion(fieldSet.readString(4));
            street.setCity(fieldSet.readString(6));
            street.setTown(fieldSet.readString(8));
            street.setTownType(fieldSet.readInt(10, 2));
            street.setTownCode(fieldSet.readString(11));
            street.setDisabled(fieldSet.readBoolean(12, "1"));
            return street;
        }
    });

    FlatFileItemReader<AddressStreet> reader = new FlatFileItemReader<>();
    reader.setLineMapper(lineMapper);
    reader.setResource(new PathResource(environment.getRequiredProperty("street")));
    return reader;
}
  1. LineMapper를 만들고
  2. FileItemReader를 맹근 다음에
  3. FileItemReaderLineMapper와 읽을 파일을 설정
해주면 끝난다. 참 쉽죠?

파일을 여러 개 읽으려면 FlatFileItemReader대신 MultiResourceItemReader를 사용하면 된다.

쓰기

파일을 읽었으면 디비에 쏴줘야지...
@Bean
public ItemWriter<AddressStreet> streetWriter() {
    JdbcBatchItemWriter<AddressStreet> writer = new JdbcBatchItemWriter<>();
    writer.setItemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<AddressStreet>());
    writer.setSql("INSERT INTO ADDRESS_STREET (ID, CODE, CODE_INDEX, NAME, REGION, CITY, TOWN, TOWN_CODE, TOWN_TYPE, IS_DISABLED) VALUES (:id, :code, :codeIndex, :name, :region, :city, :town, :townCode, :townType, :disabled)");
    writer.setJdbcTemplate(jdbcTemplate);
    return writer;
}
...설명이 필요 없을정도로 간단하다?

만약에 본인처럼 Bean을 쓰는게 아니라면 BeanPropertyItemSqlParameterSourceProvider는 다른걸로 대체하거나 직접 맹글어야 한다?
그러니까 다같이 JPA를 씁니다?

멀티쓰레딩 - TaskExecutorThrottleLimit

위에서 대충 넘겼던거 여기와서 설명을...

앞뒤 다 자르고 설명하면... 작업 단위를 멀티쓰레드로 실행한다.
TaskExecutor는 다들 알다시피(?) 쓰레드 풀, ThrottleLimit는 동시실행 쓰레드 갯수 제한이다.

TaskExecutor, 뭘 골라야 쓰겄소?

TaskExecutor의 구현이 여럿 있는데, 일반적으로 원샷 실행일 경우 SimpleAsyncTaskExecutor를 많이 쓰는것 같다.

왜인지 모르게 SimpleThreadPoolTaskExecutorThreadPoolTaskExecutor가 의도한 대로 동작하지 않는데 (싱글 쓰레드처럼 동작한다.), 그냥 Executor를 래핑하는 ConcurrentTaskExecutor를 만들어 쓰는게 더 낫다.

ConcurrentTaskExecutorExecutor를 넣지 않으면 Executors.newSingleThreadExecutor()를 설정한다. 뭐요?

파티셔닝 - 여러 개의 I/O를 멀티쓰레드로

위에 설명한게 I/O 단위 하나를 멀티쓰레드로 돌린다면, 이건 작업 하나에 여러 개의 I/O가 있을 때 이걸 멀티쓰레드로 돌리게 해준다.
@Bean
public Step partitionedStreetDetailStep() {
    return stepBuilderFactory.get("partition.street-address-detail")
                             .partitioner(streetDetailStep())
                             .partitioner("partition.street-address-detail.", streetDetailPartitioner())
                             .taskExecutor(taskExecutor)
                             .build();
}
보다시피 step을 하나 감싸는 step이다.
이 때, 내부 step은 Partitioner가 쪼개는 갯수만큼 생성된다.

파일 여러개 읽기

파티셔닝 모드에서 파일 여러개 읽기는 손이 많이 가는 작업이다.
  1. 읽을 파일 목록을 Partitioner로 맹근다.
  2. ItemReader를 맹근다.
  3. 파티션 Step에서 사용하는 내부 Step에 ItemReader를 설정한다.
단계만 보면 참 쉬워보이는데... 전혀 그렇지 않다는게 함정.

일단 파일 목록 파티션부터 맹근다.
@Bean
public Partitioner streetDetailPartitioner() {
    MultiResourcePartitioner partitioner = new MultiResourcePartitioner();
    List<Resource> resources = Arrays.asList(environment.getProperty("street-detail", String[].class))
                                     .stream()
                                     .map(PathResource::new)
                                     .collect(Collectors.toList());
    partitioner.setResources(resources.toArray(new Resource[resources.size()]));
    return partitioner;
}

다음 ItemReader도 맹근 다음...
@Bean
@StepScope
public FlatFileItemReader<AddressStreetDetail> streetDetailReader(@Value("#{stepExecutionContext['fileName']}") String path) {
    DefaultLineMapper<AddressStreetDetail> lineMapper = new DefaultLineMapper<>();
    lineMapper.setLineTokenizer(lineTokenizer);
    lineMapper.setFieldSetMapper(new FieldSetMapper<AddressStreetDetail>() {
        @Override
        public AddressStreetDetail mapFieldSet(FieldSet fieldSet) throws BindException {
            AddressStreetDetail detail = new AddressStreetDetail();
            detail.setId(fieldSet.readString(0));
            detail.setUnderground(fieldSet.readBoolean(3, "1"));
            detail.setBuildingNo(fieldSet.readInt(4));
            detail.setBuildingNoSub(fieldSet.readInt(5));
            detail.setBlockNo(fieldSet.readString(6));
            detail.setExtra(fieldSet.readBoolean(10, "1"));
            AddressStreet street = new AddressStreet();
            street.setId(String.format("%12s%2s", fieldSet.readString(1), fieldSet.readString(2)));
            detail.setStreet(street);
            return detail;
        };
    });

    FlatFileItemReader<AddressStreetDetail> reader = new FlatFileItemReader<>();
    reader.setLineMapper(lineMapper);
    reader.setResource(new PathResource(URI.create(path)));
    return reader;
}

내부 Step도 맹근다.
@Bean
public Step streetDetailStep() {
    return stepBuilderFactory.get("street-address-detail")
                             .<AddressStreetDetail, AddressStreetDetail> chunk(getChunkSize())
                             .reader(streetDetailReader("classpath:/empty.csv"))
                             .writer(streetDetailWriter())
                             .taskExecutor(taskExecutor)
                             .throttleLimit(THROTTLE_LIMIT)
                             .build();
}

@StepScope라는 놈이 지금 막 등장했는데, 매우 중요하다. 이게 없으면 파티션된 작업에서 읽을 파일을 제대로 설정할 수가 없다! 그리고 읽을 파일명은 stepExecutionContext에서 받아오게 된다. (Job에는 같은 역할을 하는 jobExecutionContext가 있다.)

아쉽게도 자바 설정으로 FlatFileItemReader를 생성할 때 읽을 파일을 뭐라도 설정하지 않으면 나중에 실제로 읽을 파일을 설정해 줘도 파일을 읽을 생각을 안한다. 따라서 bean을 만들 때는 아무거나 하나 던져준다. (이 때는 실제 파일이 없어도 신경을 쓰지 않지 시포요. 아니면 아무 내용도 없는 더미 파일을 하나 맹글어서 던져주든가...)

삽질기 - 디비가 지저분해졌어요!

기본적으로 spring-boot는 DataSource bean이 있고 @EnableBatchProcessing이 설정되어있다면 배치 실행 정보를 디비에 우겨넣는다. 원샷 실행이고 실행 정보를 저장해야 할 필요따원 없는데 이러면 참 골치가 아프더라.
원래는 spring.batch.initializer.enabled 속성이 false이면 테이블을 생성하지 않고 잘 돌아야 하는데... 실상은 에러나 막 떨궈댄다.

원인은 DefaultBatchConfigurerDataSource를 주입받기 때문에...
그러니까 DataSource를 무시하도록 직접 BatchConfigurer를 만들면 된다.
@Bean
public BatchConfigurer batchConfigurer() {
    BatchConfigurer configurer = new BatchConfigurer() {
        private PlatformTransactionManager transactionManager;
        private JobRepository jobRepository;
        private JobLauncher jobLauncher;
        private JobExplorer jobExplorer;

        @Override
        public PlatformTransactionManager getTransactionManager() throws Exception {
            return transactionManager;
        }

        @Override
        public JobRepository getJobRepository() throws Exception {
            return jobRepository;
        }

        @Override
        public JobLauncher getJobLauncher() throws Exception {
            return jobLauncher;
        }

        @Override
        public JobExplorer getJobExplorer() throws Exception {
            return jobExplorer;
        }

        @PostConstruct
        public void initialize() {
            if (this.transactionManager == null) {
                this.transactionManager = new ResourcelessTransactionManager();
            }
            try {
                MapJobRepositoryFactoryBean jrf = new MapJobRepositoryFactoryBean(this.transactionManager);
                jrf.afterPropertiesSet();
                this.jobRepository = jrf.getObject();

                MapJobExplorerFactoryBean jef = new MapJobExplorerFactoryBean(jrf);
                jef.afterPropertiesSet();
                this.jobExplorer = jef.getObject();

                SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
                jobLauncher.setJobRepository(jobRepository);
                jobLauncher.afterPropertiesSet();
                this.jobLauncher = jobLauncher;
            } catch (Exception e) {
                throw new BatchConfigurationException(e);
            }
        }
    };
    return configurer;
}
오~예~
Share:

2014-12-16

CentOS에 JDK Alternative 멕이기 [삐-]나게 힘들구먼

RHEL이나 CentOS에서 Oracle JDK 설치 관련 구글신께 신탁을 청하면
그냥 "rpm 올리고 alternative 멕이세요." 라고만 되어있다. 진짜...

그래서 만들었습니다.
Oracle JDK Alternative 자동 등록 스크립트!
기존 구글신 신탁처럼 일부분만 등록해버리는게 아니라 전체를 빠르고 편하게 등록해드립니다.

이거 눌러서 받으시고...

0. 먼저 등록할 JDK부터 설치하고 봅니다. (대략 yum localinstall이 좋소)
1. 일단 적절한 곳에 던저놓으신 다음
2. chmod a+x centos-jdk-alternative-update.sh
3. ./centos-jdk-alternative-update.sh <JDK-DIR>

끗!
참 쉽죠?

물론 약간의 오류가 있을 수도 있지만, 그런거 내가 알 게 뭐야

일단 CentOS 7에 Oracle JDK 8 버전으로 테스트해봤습니다.
아마 이전 버전들 (JDK 6, 7)에서도 큰 문제없이 돌아가지 시포요.

p.s.
alternative 업데이트 후에도 버전이 바뀌지 않으면 다음 명령을 입력해줍니다.

최신버전으로 올릴 때:
update-alternatives --auto java
update-alternatives --auto javac

특정버전으로 설정할 때:
update-alternatives --set java <path>
update-alternatives --set javac <path>

각 버전별 경로는 다음 명령으로 알아낼 수 있습니다.
update-alternatives --display java
update-alternatives --display javac
Share:

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: