(Kotlin) null을 컨트롤 하는 연산자

|

?. - 안전한 호출 (Safe calls)

  • null이 아닌 경우 실행한다.
  • let과 연계 사용시 더 간결하게 처리 가능
    // JAVA
    public String safetyUpperCase(String str) {
        if (str != null) {
            return str.toUpperCase();
        }
    }
    // kotlin
    fun safetyUpperCase(str:String?) = str?.uppercase()
    // str이 null일 경우 let 이하 구문은 실행되지 않는다
    fun combinePrefixIfNotNull(str:String?, prefix:String) = str?.let { prefix + str }

    val s1:String? = null
    val s2:String? = "A"
    combinePrefixIfNotNull(s1, "PREFIX-") // null
    combinePrefixIfNotNull(s2, "PREFIX-") // PREFIX-A

?: - 엘비스연산자 (Elvis operator)

  • null 대신 사용 할 디폴트 값을 지정
    // JAVA
    String value = str == null ? "DEFAULT" : str;
    // kotlin
    val value = str?: "DEFAULT"

as? - 안전한 캐스트 (Safe casts)

  • 지정한 타입으로 캐스트 하고 불가 시 null 반환
    // JAVA
    public Long toLong(Object obj) {
        if (obj instanceof Long) {
            return (Long)obj
        } else {
            return null;
        }
    }
    // kotlin
    fun toLong(obj:Any):Long? = obj as? Long

!! - 널 아님 단언 (not-null assertion)

  • null이 될 수 있는 타입을 null이 될 수 없는 타입으로 강제전환
  • 실제 값이 null일 경우 NPE 발생
  • 안티패턴 : person.company!!.address!!.country ** 어느 값에서 NPE가 발생했는지 알기 어렵다
    fun <T> notNullAssert(nullable:T?):T = nullable!!

    val s1:String? = "NOT-NULL"
    val s2:String? = null
    notNullAssert(s1)
    notNullAssert(s2)  // NullPointerException

(SpringBatch) Job 종료 후 스프링 프로세스가 늦게 끝나는 현상

|

배치 구성을 위해 작업중 발생한 이슈로, Job이 모두 끝났음에도 불구하고 스프링 프로세스가 계속 유지되다 1분후에 종료되는 현상이 있었습니다.

구성

  • Spring boot 2.3.2
  • Spring batch
  • Spring data jpa
  • 기타 등등…

위와 같은 구성으로 배치 프로젝트를 구성하고 간단한 테스트 Job을 만들었습니다.

@EnableBatchProcessing
@SpringBootApplication
public class BatchApplication {
    public static void main(String[] args) {
        SpringApplication.run(BatchApplication.class, args);
    }
}

@Slf4j
@Configuration
@RequiredArgsConstructor
public class TestJobConfig {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;

    @Bean
    @JobScope
    public Step testStep() {
        return stepBuilderFactory.get("testStep")
            .tasklet((contribution, chunkContext) -> {

                ......

                return RepeatStatus.FINISHED;
            })
            .build();
    }

    @Bean
    public Job testJob() {
        return jobBuilderFactory.get("testJob")
            .start(testStep())
            .listener(listener())
            .build();
    }

    @Bean
    public JobExecutionListener listener() {
        return new JobExecutionListener() {
            @Override
            public void beforeJob(JobExecution jobExecution) {
                log.warn("Job Start !!!");
            }

            @Override
            public void afterJob(JobExecution jobExecution) {
                log.warn("Job End !!!");
            }
        };
    }
}
// build.gradle
dependencies {
    implementation "org.springframework.boot:spring-boot-starter-jdbc:${springBootVersion}"
    implementation "org.springframework.boot:spring-boot-starter-data-jpa:${springBootVersion}"
    implementation "org.springframework.boot:spring-boot-starter-batch:${springBootVersion}"

    ......

    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }

    testImplementation "org.springframework.batch:spring-batch-test"
}

테스트를 위해 별다른 로직 없이 간단하게 구성하였으며 시작과 종료시점에 로그를 남기 상태입니다. 현 상태에서 실행하게 되면 아래 로그와 같이 Job이 종료한 시점으로부터 1분이 지나서야 스프링 프로세스가 종료되는 것을 확인 할 수 있습니다.

2020-08-12 20:49:16 [restartedMain] INFO  o.s.b.a.b.JobLauncherApplicationRunner - Running default command line with: [requestDate=32]
2020-08-12 20:49:17 [restartedMain] INFO  o.s.b.c.l.support.SimpleJobLauncher - Job: [SimpleJob: [name=testJob]] launched with the following parameters: [{requestDate=32}]

// Job 시작 시
2020-08-12 20:49:17 [restartedMain] WARN  c.k.m.s.batch.config.TestJobConfig - Job Start !!!
2020-08-12 20:49:17 [restartedMain] INFO  o.s.batch.core.job.SimpleStepHandler - Executing step: [testStep]
2020-08-12 20:49:17 [restartedMain] WARN  c.k.m.s.batch.config.TestJobConfig - requestDate=>32
2020-08-12 20:49:17 [restartedMain] INFO  o.s.batch.core.step.AbstractStep - Step: [testStep] executed in 34ms
2020-08-12 20:49:17 [restartedMain] WARN  c.k.m.s.batch.config.TestJobConfig - Job End !!!
2020-08-12 20:49:17 [restartedMain] INFO  o.s.b.c.l.support.SimpleJobLauncher - Job: [SimpleJob: [name=testJob]] completed with the following parameters: [{requestDate=32}] and the following status: [COMPLETED] in 100ms
// Job 종료

// 스프링 프로세스 종료
2020-08-12 20:50:16 [SpringContextShutdownHook] INFO  o.s.o.j.LocalContainerEntityManagerFactoryBean - Closing JPA EntityManagerFactory for persistence unit 'default'
2020-08-12 20:50:16 [SpringContextShutdownHook] INFO  o.s.s.c.ThreadPoolTaskExecutor - Shutting down ExecutorService 'applicationTaskExecutor'
2020-08-12 20:50:16 [SpringContextShutdownHook] INFO  com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2020-08-12 20:50:16 [SpringContextShutdownHook] INFO  com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
Disconnected from the target VM, address: '127.0.0.1:60141', transport: 'socket'

Process finished with exit code 1

처음에는 Spring batch 쪽 이슈로 생각되었으나 공식 Github Issue에 올라온 내용에 따르면 Spring boot(jpa) 쪽 이슈라고 합니다.

원인은 Spring boot의 빠른 초기 기동을 위해 적용된 jpa의 bootstrap-mode 이슈로 application.yml에 다음과 같이 설정하여 해결 할 수 있습니다.

data.jpa.repositories.bootstrap-mode: default

위 기능은 초기화에 많은 시간을 잡아먹는 repository들의 초기화를 늦춰 boot의 기동시간을 단축 시키는 모드인데, 2.3.2 기준으로 기본값은 deferred로 지연로딩되고 백그라운드 스레드에 의해 특정 시점에 초기화 됩니다. 더 자세한 설명은 Spring Data JPA - Reference Documentation - Bootstrap Mode를 참고하세요.

(직접 디버깅을 하진 못했으나)가설을 세워보면 스프링 프로세스가 기동되면서 백그라운드 스레드는 특정 이벤트가 발생하는 시점까지 대기 하고 있는데, 배치의 Job이 먼저 끝나버리고 초기화가 끝나지 않은 스프링은 초기화를 마친 후 프로세스를 다시 종료한 것으로 유추되어 Job 실행을 1분이상 걸리도록 수정한 후 다시 테스트 하였습니다. 테스트 결과, 아래와 같이 Job이 종료된 이후 스프링 프로세스도 바로 종료 됨을 확인 할 수 있었습니다.

2020-08-12 21:26:37 [restartedMain] INFO  o.s.b.c.l.support.SimpleJobLauncher - Job: [SimpleJob: [name=testJob]] launched with the following parameters: [{requestDate=31}]

// Job 시작
2020-08-12 21:26:37 [restartedMain] WARN  c.k.m.s.batch.config.TestJobConfig - Job Start !!!
2020-08-12 21:26:37 [restartedMain] INFO  o.s.batch.core.job.SimpleStepHandler - Executing step: [testStep]
2020-08-12 21:26:37 [restartedMain] WARN  c.k.m.s.batch.config.TestJobConfig - requestDate=>31

// 2분여간 대기후 종료
2020-08-12 21:28:37 [restartedMain] INFO  o.s.batch.core.step.AbstractStep - Step: [testStep] executed in 2m0s53ms
2020-08-12 21:28:37 [restartedMain] WARN  c.k.m.s.batch.config.TestJobConfig - Job End !!!
2020-08-12 21:28:37 [restartedMain] INFO  o.s.b.c.l.support.SimpleJobLauncher - Job: [SimpleJob: [name=testJob]] completed with the following parameters: [{requestDate=31}] and the following status: [COMPLETED] in 2m0s121ms

// 스프링 프로세스 종료
2020-08-12 21:28:37 [SpringContextShutdownHook] INFO  o.s.o.j.LocalContainerEntityManagerFactoryBean - Closing JPA EntityManagerFactory for persistence unit 'default'
2020-08-12 21:28:37 [SpringContextShutdownHook] INFO  o.s.s.c.ThreadPoolTaskExecutor - Shutting down ExecutorService 'applicationTaskExecutor'
2020-08-12 21:28:37 [SpringContextShutdownHook] INFO  com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2020-08-12 21:28:37 [SpringContextShutdownHook] INFO  com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
Disconnected from the target VM, address: '127.0.0.1:60519', transport: 'socket'

Process finished with exit code 0

참고

Spring Batch - Spring Boot batch job does not shut down automatically after completion when using JPA Spring Boot - Spring Boot batch job does not shut down automatically after completion when using JPA

java.util.ConcurrentModificationException null (with ehcache)

|

Triggering a ConcurrentModificationException

병렬 프로그래밍을 하다 보면 만나게 되는 흔한 오류 중 하나가 java.util.ConcurrentModificationException입니다. thread-safe 하지 않은 ArrayList 같은 객체를 여러 스레드에서 동시에 조작하다 보면 발생하게 되는데 주된 원인은 for-loop 도중 조작할 경우 입니다.

// java
List<Integer> intList = new ArrayList<>();
for (Integer i : intList) {
    ...
}

// class
List<Integer> intList = new ArrayList<>();
Iterator<Integer> iter = intList.iterator();
while(iter.hasNext()) {
    Integer i = iter.next();
    ...
}

for-loop 구문을 class 컴파일 하면 위와 같은 코드로 변환되는데 ArrayList가 Iterator를 생성할 때 내부 클래스인 ArrayList.Itr 클래스로 생성하여 반환되며 Itr 클래스는 생성시 ArrayList의 상위클래스인 AbstractList의 modCount 변수를 자신의 지역변수인 expectedModCount에 할당합니다. modCount는 ArrayList의 변경사항이 생길 때 마다 변경된 카운트를 기록하는 변수로 add / remove / trimToSize 등등 다수의 메서드에서 증가됩니다.

private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;
        ...
}

expectedModCount 변수는 next() 메서드를 실행할 때 기존 자신이 참조하고 있는 ArrayList의 변경사항이 있는지 체크하게 되는데 이 때 참조 리스트의 modCount와 자신의 지역변수인 expectedModCount를 비교하여 다를 경우 java.util.ConcurrentModificationException를 발생시킵니다.

// ArrayList.Itr class
public E next() {
    checkForComodification();
    ...
}

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

static 변수로 선언되어 여러 스레드에 조작되다 발생 할 수도 있으나 아래 예제와 같이 단일 스레드 내에서도 동일하게 발생 할 수 있습니다. 다른 블로그나 StackOverFlow의 질문들을 보면 대부분 remove 구문을 예시로 들지만 remove 뿐만 아닌 modCount가 조작되는 모든 메서드에 해당됩니다.

@Test(expected = ConcurrentModificationException.class)
public void concurrentModificationExceptionTest() {
    List<Integer> list = Lists.newArrayList(1,2,3,4,5,6,7);
    for (Integer i : list) {
        if (i==2) {
            list.add(8);
        }
    }
}

회피 하는 방법은 여러가지가 존재하는데 Avoiding the ConcurrentModificationException in Java문서를 참고하시면 좋습니다.

ConcurrentModificationException with Ehcache

자신이 병렬 스레드를 사용하지 않았고, for-loop문 내에서 조작하지 않았다고 해서 java.util.ConcurrentModificationException이 발생하지 않을꺼라 생각할 수 있으나 in-memory cache를 사용할 때 역시 주의해야 합니다.

// MyCacheService.class
@Cacheable
public List<Integer> getCacheList() { ... }

// OtherService
@Autowired private MyCacheService myCacheService;

public void sortList() {
    List<Integer> cachedList = myCacheService.getCacheList();
    cachedList.sort(Integer::compareTo);
    for(Integer i : cachedList) {
        System.out.println(i);
    }
}

얼핏 보기엔 sortList 메서드는 안전해 보이지만 Ehcache를 사용하고 있다면 부하 상황에서 반드시 java.util.ConcurrentModificationException이 발생하게 됩니다. for-loop 구문 안에서 조작하지 않았는데 발생하는 이유는 Ehcache가 데이터를 heap과 disk에 나눠 관리하기 때문입니다. 해당 데이터가 heap에 존재할 경우 캐시에서 동일한 레퍼런스 객체를 반환하기 때문에 static으로 공유된 객체를 사용하는 것과 동일한 이슈를 발생 시키게 되는데 아래 테스트케이스에서 볼 수 있듯이 캐시에서 받아온 List객체를 조작하게 되면 기존 캐시에 저장된 객체가 같이 바뀌어 있는것을 확인 할 수 있습니다.

@Test
public void cacheValueReferenceTest() {
    final Cache cache = getCache();
    final String key = "TEST_KEY";
    List<Integer> value = Lists.newArrayList(7,6,5,4,3,2,1);
    cache.put(key, value);
    
    value.sort(Integer::compareTo);
    
    List<Integer> cacheValue = cache.get(key, List.class);
    assertTrue(cacheValue.get(0) == 1);
}

동시성 문제가 발생하는 케이스는 아래 테스트케이스에서 확인 할 수 있습니다.

@Test
public void concurrentModifiedExceptionTest() throws InterruptedException {
    final Cache cache = getCache();
    final String key = "TEST_KEY";
    List<Integer> value = Lists.newArrayList(7,6,5,4,3,2,1);
    cache.put(key, value);

    AtomicBoolean isDetectException = new AtomicBoolean(false);
    CountDownLatch latch1 = new CountDownLatch(1);
    CountDownLatch latch2 = new CountDownLatch(1);
    
    Runnable thread1 = () -> {
        try {
            List<Integer> value1 = cache.get(key, List.class);
            for (Integer v : value1) {
                latch2.await();
            }
        } catch (Exception e) {
            log.error("thread1 error : {} {}", e.getClass().getName(), e.getMessage());
            isDetectException.set(e instanceof ConcurrentModificationException);
        } finally {
            latch1.countDown();
        }
    };
    
    Runnable thread2 = () -> {
        List<Integer> value2 = cache.get(key, List.class);
        value2.sort(Integer::compareTo);
        latch2.countDown();
    };

    new Thread(thread1).start();
    Thread.sleep(100);
    new Thread(thread2).start();
    latch1.await();
    assertTrue(isDetectException.get());
}

1번 스레드가 캐시에서 데이터를 가져와 for-loop 지점에 진입한 상태에서 2번 스레드가 캐시값에 접근하여 변경(sort와 같은..)작업을 시도하면 내부적으로 ArrayList의 modCount값이 바뀌면서 1번 스레드의 Iterator.next 구문 실행시 java.util.ConcurrentModificationException를 발생시키게 됩니다.

Conclusion

동시성 문제는 평시엔 잘 발견되지 않기 때문에 일반적인 테스트만 진행하고 실 서비스에 배포되면 운영중 가장 중요한 시기에 장애가 터지게 되어 난감할 때가 많습니다. 매번 성능 테스트를 할 수 없다면 위와 같은 경우에 항상 주의하며 코딩해야하며, 필자는 Ehcache의 프로세스를 확인하지 않아 디버깅에 오래걸렸지만 java.util.ConcurrentModificationException의 경우 반드시 for-loop 안에서 타켓 리스트객체를 수정했거나, 공유되는 리스트객체를 동시에 조작한 두가지 경우에 발생함을 유의하고 빠르게 디버깅 하시기 바랍니다.

Reference

Avoiding the ConcurrentModificationException in Java : https://www.baeldung.com/java-concurrentmodificationexception

Spliterator의 소개 및 활용 (in JDK 1.8)

|

Overview

Spliterator 인터페이스를 접한건 Stream을 복제할 수 있는 방법이 없을까 고민하다 포함된 메서드들 중 spliterator()는 어떤건지 궁금해 찾아보게 됬습니다.
처음에 여기저기 블로그에 정리된 글을 찾아봤을땐 병렬처리를 지원하기 위해 Stream에 종속된 기능 정도로만 생각했는데, Oracle 공식 JDK 문서에는 특정 소스의 객체를 순회하거나 파티셔닝 하기 위한 인터페이스로 Stream의 파이프라이닝 처리를 위해 java.util.stream.Sink 클래스와 함께 핵심적인 역활을 맡고 있습니다. Stream외에도 배열/Collection/IOChannel 등 복수개의 개체를 지닌 모든 소스들에서 구현되거나 사용 할 수 있으며 생성 방법은 아래와 같습니다.

// Stream과 Collection의 경우 기본적으로 spliterator 메서드를 제공합니다.     
Spliterator<Integer> byStream = IntStream.range(0, 100).spliterator();    
Spliterator<Integer> byCollection = (new ArrayList<Integer>()).spliterator();    
// 배열의 경우 Spliterators 클래스를 통해 Spliterator 객체를 생성 할 수 있습니다.
Spliterator<Integer> byArray = Spliterators.spliterator(new int[]{1,2,3,4}, Spliterator.SORTED);

Methods

characteristics

Spliterator 객체의 특성에 대한 int값을 반환하는 메서드로 속성은 ORDERED, DISTINCT, SORTED, SIZED, NONNULL, IMMUTABLE, CONCURRENT, SUBSIZED 가 있으며 각 특성은 어떤 Spliterator 객체인가에 따라 다르며 그에 따른 각 메서드들의 내부적인 동작이 다를 수 있습니다.
예를 IntStream.of의 Spliterator의 경우 “IMMUTABLE, ORDERED, SIZED, SUBSIZED”의 특성을 가지고 있으나 IntStream.generate의 Spliterator의 경우 “IMMUTABLE”의 특성만을 지니고 있습니다. 또한 Set의 Spliterator의 경우 “DISTINCT, SIZED”의 특성을 가지고 있으며 List의 Spliterator의 경우 “ORDERED, SIZED, SUBSIZED”의 특성을 가집니다.

characteristics메서드의 반환값은 int형인데 Spliterator 객체에 포함된 모든 특성값의 합을 반환합니다.

// Set의 경우 DISTINCT=1, SIZED=64 의 합인 65를 반환합니다.
new HashSet().spliterator().characteristics();  // 65
// List의 경우 ORDERED=16, SIZED=64, SUBSIZED=16384 의 합인 16464를 반환합니다.
new ArrayList().spliterator().characteristics(); // 16464

Spliterator의 각 특성은 hasCharacteristics메서드를 통해 확인 할 수 있습니다.

estimateSize

순회할 개체의 사이즈를 알 수 있는 메서드로 SIZED/SUBSIZED 특성을 지녔으며 순회할 남은 개체의 사이즈를 반환해줍니다. 쉽게 말해 개수가 제한되어 있는 Stream의 사이즈를 알 수 있습니다.
Spliterator의 총 개체수가 아닌 순회 할 수 있는 개체의 개수이기 때문에 이미 순회한 개체의 수는 포함되지 않습니다.

@Test
public void estimateSize_test() {
    Spliterator<Integer> spliterator = IntStream.of(1,2,3,4,5).spliterator();
    System.out.println(spliterator.estimateSize()); // print 5
    spliterator.tryAdvance(t -> {});
    spliterator.tryAdvance(t -> {});
    spliterator.tryAdvance(t -> {});
    System.out.println(spliterator.estimateSize()); // print 2
}

tryAdvance

Spliterator의 요소가 남아 있을 경우 인자로 주어진 액션을 실행하고 요소의 존재 유무를 반환하는데, 특성에 ORDERED가 포함되어 있을 경우 순차적으로 요소를 제공하게 됩니다.
이 메서드의 주의할 점은 리턴값이 hasNext의 개념이 아니라 isExecuted의 개념이란 점입니다. 아래 예제를 보면 요소가 3개이나 3번째 호출까지 계속 true를 반환하는 것을 볼 수 있습니다.

@Test
public void tryAdvance_test() {
    Spliterator<Integer> spliterator = IntStream.of(1,2,3).spliterator();
    
    boolean r1 = spliterator.tryAdvance(System.out::println); // print 1
    boolean r2 = spliterator.tryAdvance(System.out::println); // print 2
    boolean r3 = spliterator.tryAdvance(System.out::println); // print 3
    boolean r4 = spliterator.tryAdvance(System.out::println); // not execute

    System.out.println(r1 + ", " + r2 + ", " + r3 + ", " + r4); // true, true, true, false
}

Spliterator 인터페이스에선 본 메서드를 사용하여 순회하는 forEachRemaining 메서드를 기본적으로 제공하며 코드는 아래와 같습니다.

default void forEachRemaining(Consumer<? super T> action) {
    do { } while (tryAdvance(action));
}

구현 메서드가 위와 같이 hasNext / isExecuted의 양쪽 개념을 다 포괄 할 수 있기 때문에 어느쪽이든 크게 상관없다 싶지만 가능하면 isExecuted 개념으로 구현하는 것을 권장합니다.

trySplit

Spliterator를 분할 하는 메서드로 원 객체에서 분할된 Spliterator를 반환하는데 ORDERED 특성일 경우 반환된 값이 앞부분이 되고 원 객체가 뒷부분이 됩니다.

@Test
public void trySplit_test() {
    Spliterator<Integer> origin = IntStream.range(0, 19).spliterator();
    Spliterator<Integer> dest = origin.trySplit();

    System.out.println(dest.estimateSize());  // size is 9
    dest.forEachRemaining(System.out::print); // print 0~8
    System.out.println();
    System.out.println(origin.estimateSize());  // size is 10
    origin.forEachRemaining(System.out::print);  // print 9 ~ 18
}

아쉽게도 분할할 사이즈나 범위는 제어 할 수 없도록 되어 있으며 절반으로 나뉘게 됩니다. 일반적인 Stream의 경우 parallel일 경우에만 분할이 가능하며 아닐 경우 분할한 Spliterator는 null로 반환합니다.

// java.util.stream.StreamSpliterators.AbstractWrappingSpliterator
@Override
public Spliterator<P_OUT> trySplit() {
    if (isParallel && !finished) {
        init();

        Spliterator<P_IN> split = spliterator.trySplit();
        return (split == null) ? null : wrap(split);
    }
    else
        return null;
}

Customize

Stream을 분할 할 수도 있고, size를 미리 알수 있으며, 흐름에 따라 일괄 처리되던 Stream을 단 건으로 실행 할 수 있는 등 이미 그 자체로도 충분히 활용할 곳이 많지만 Spliterator 자체를 커스텀하여 만들 경우 좀 더 다양하게 활용 가능합니다.
다음 Spliterator는 카운트를 지정하여 해당 카운트 만큼 Stream이 실행되면 pinned 메소드에 등록한 리스너를 실행합니다.

public class PinPointSpliterator<T> implements Spliterator<T> {
    private final Spliterator<T> spliterator;
    private final AtomicInteger counter = new AtomicInteger();
    private int pinCount = -1;
    private IntFunction<Boolean> listener = null;
    private boolean isListen = false;
    
    public PinPointSpliterator(Spliterator<T> spliterator) {
        this.spliterator = spliterator;
    }
    
    public PinPointSpliterator(Stream<T> stream) {
        this(stream.spliterator());
    }
    
    public static <T> PinPointSpliterator<T> newInstance(Stream<T> stream) {
        return new PinPointSpliterator<>(stream);
    }
    
    public PinPointSpliterator pinCount(int pinCount) {
        this.pinCount = pinCount;
        return this;
    }

    public PinPointSpliterator pinned(IntFunction<Boolean> listener) {
        this.isListen = listener != null;
        this.listener = listener;
        return this;
    }

    @Override
    public boolean tryAdvance(Consumer<? super T> action) {
        boolean hasNext = this.spliterator.tryAdvance(action);
        if (isListen && pinCount > 0 && counter.incrementAndGet() % pinCount == 0) {
            isListen = listener.apply(counter.get());
        }
        return hasNext;
    }

    @Override
    public Spliterator<T> trySplit() {
        return new PinPointSpliterator(this.spliterator.trySplit());
    }

    @Override
    public long estimateSize() {
        return this.spliterator.estimateSize();
    }

    @Override
    public int characteristics() {
        return this.spliterator.characteristics();
    }
}

실행은 아래와 같이 할 수 있습니다.

@Test
public void pinPointSpliterator_test() {
    List<Integer> buffer = new ArrayList<>();
    PinPointSpliterator<Integer> spliterator = PinPointSpliterator.newInstance(IntStream.range(0, 1000).boxed())
            .pinCount(100)
            .pinned(count -> {
                System.out.println("do listen. buffer size is " + buffer.size());
                buffer.clear();
                return count < 500;
            });
    StreamSupport.stream(spliterator, false)
        .peek(i -> buffer.add(i))
        .collect(Collectors.toList());

    System.out.println("final buffer size is " + buffer.size());
}

위 예제에서 100회 실행시마다 pinned에 설정된 리스너를 실행하는데 buffer의 사이즈를 출력하고 초기화하며, 500회보다 적게 실행된 경우에만 동작하므로 500회 이후엔 모두 buffer에 쌓이므로 500개가 쌓이게 됩니다. 최종 출력은 다음과 같습니다.

do listen. buffer size is 100
do listen. buffer size is 100
do listen. buffer size is 100
do listen. buffer size is 100
do listen. buffer size is 100
final buffer size is 500

Conclusion

워낙 Stream에서 많은 기능을 제공하기에 잘 사용하지 않았었지만 Stream을 쓰다보면 무언가 조금씩 아쉬운 부분들이 존재했습니다. 예를 들어 1000개씩 분할해서 List로 담고 싶다던가, 특정 실행 횟수마다 어떤 동작을 한다던가, Stream의 전체 개수에 따라 다른 액션이 필요할 경우 등등 … 기존 Stream으로는 번거로운 작업들을 좀 더 쉽게 처리 할 수 있어 잘 쓰기만 한다면 많이 활용 할 수 있을것 같습니다.

(SpringData) Spring Data Rest Repository Not Working

|

이슈

오랜만에 SpringBoot로 간단한 REST API 서버를 만들어 볼 일이 생겨서 Spring Initializr를 통해 아래 모듈을 추가하여 작업했습니다.

// build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-data-rest'
	implementation 'org.springframework.data:spring-data-rest-hal-browser'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.h2database:h2'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
spring :
    datasource :
        url : jdbc:h2:mem:data
        driverClassName : org.h2.Driver
        username : sa
        password : 1234
    jpa :
        database-platform : org.hibernate.dialect.H2Dialect
        show-sql: true
        generate-ddl: true
    h2.console :
        enabled : true
        path : /h2-console
        settings :
            trace : false
            web-allow-others : false
    data:
        rest:
            base-path: /api
@Data
@Entity
@Table(name = "article")
public class Article implements Serializable {
    @Id
    @GeneratedValue
    private long articleNo;

    @NotNull
    private String title;

    @Lob
    @NotNull
    private String content;

    ...
}

@RepositoryRestResource
public interface ArticleDao extends CrudRepository<Article, Long> {
    List<Article> findById(@Param("articleNo") long articleNo);
}

@Configuration
@EnableJpaRepositories
public class JpaConfig {

}

@SpringBootApplication
public class DemoApplication {
	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}
}

워낙에 spring-data-rest는 별도 설정할게 없는지라 잘 되겠지 하고 bootRun!! 1.5.X 버전때는 맵핑되는 Request 주소들이 기동시 로그에 다 찍혔는데 2.x 버전에서는 기본적으로 찍히지가 않았습니다. (그냥 그려려니…)

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.1.9.RELEASE)

2019-10-13 23:54:49.092  INFO 9085 --- [  restartedMain] com.example.demo.DemoApplication         : Starting DemoApplication on documents.ct.infn.it with PID 9085 (/Users/jistol/Downloads/demo/out/production/classes started by jistol in /Users/jistol/Downloads/demo)
2019-10-13 23:54:49.096  INFO 9085 --- [  restartedMain] com.example.demo.DemoApplication         : No active profile set, falling back to default profiles: default
2019-10-13 23:54:49.161  INFO 9085 --- [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2019-10-13 23:54:49.161  INFO 9085 --- [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2019-10-13 23:54:49.979  INFO 9085 --- [  restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data repositories in DEFAULT mode.
2019-10-13 23:54:50.006  INFO 9085 --- [  restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 8ms. Found 0 repository interfaces.
2019-10-13 23:54:50.570  INFO 9085 --- [  restartedMain] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$3c00740f] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2019-10-13 23:54:50.592  INFO 9085 --- [  restartedMain] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.hateoas.config.HateoasConfiguration' of type [org.springframework.hateoas.config.HateoasConfiguration$$EnhancerBySpringCGLIB$$bb80c141] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2019-10-13 23:54:51.019  INFO 9085 --- [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8099 (http)
2019-10-13 23:54:51.043  INFO 9085 --- [  restartedMain] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2019-10-13 23:54:51.043  INFO 9085 --- [  restartedMain] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.26]
2019-10-13 23:54:51.135  INFO 9085 --- [  restartedMain] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2019-10-13 23:54:51.136  INFO 9085 --- [  restartedMain] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1975 ms
2019-10-13 23:54:51.436  INFO 9085 --- [  restartedMain] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2019-10-13 23:54:51.702  INFO 9085 --- [  restartedMain] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2019-10-13 23:54:51.779  INFO 9085 --- [  restartedMain] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
	name: default
	...]
2019-10-13 23:54:51.890  INFO 9085 --- [  restartedMain] org.hibernate.Version                    : HHH000412: Hibernate Core {5.3.12.Final}
2019-10-13 23:54:51.892  INFO 9085 --- [  restartedMain] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2019-10-13 23:54:52.125  INFO 9085 --- [  restartedMain] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.0.4.Final}
2019-10-13 23:54:52.323  INFO 9085 --- [  restartedMain] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
Hibernate: drop table article if exists
Hibernate: drop table post if exists
Hibernate: drop sequence if exists hibernate_sequence
Hibernate: create sequence hibernate_sequence start with 1 increment by 1
Hibernate: create table article (article_no bigint not null, content clob not null, created_by varchar(255) not null, created_date timestamp not null, title varchar(255) not null, update_by varchar(255) not null, updated_date timestamp not null, primary key (article_no))
Hibernate: create table post (post_no bigint not null, title varchar(255) not null, primary key (post_no))
2019-10-13 23:54:53.233  INFO 9085 --- [  restartedMain] o.h.t.schema.internal.SchemaCreatorImpl  : HHH000476: Executing import script 'org.hibernate.tool.schema.internal.exec.ScriptSourceInputNonExistentImpl@7f47a56f'
2019-10-13 23:54:53.237  INFO 9085 --- [  restartedMain] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2019-10-13 23:54:53.319  INFO 9085 --- [  restartedMain] o.s.b.d.a.OptionalLiveReloadServer       : LiveReload server is running on port 35729
2019-10-13 23:54:54.058  INFO 9085 --- [  restartedMain] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2019-10-13 23:54:54.120  WARN 9085 --- [  restartedMain] aWebConfiguration$JpaWebMvcConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2019-10-13 23:54:54.584  INFO 9085 --- [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8099 (http) with context path ''
2019-10-13 23:54:54.589  INFO 9085 --- [  restartedMain] com.example.demo.DemoApplication         : Started DemoApplication in 6.029 seconds (JVM running for 7.371)

그래도 H2 DB에 알아서 테이블이 잘 생성됬길래 Entity랑 Repository 설정은 다 잘 붙었나보다 하며 만들어진 API를 보기 위해 Hal-Browser로 접속했는데 profile 외에 아무것도 생성되지 않았습니다.

not working REST Repository

해결

원인은 패키지 구조에서 찾았는데 Configuration을 선언해둔 JpaConfig의 위치가 이슈였습니다.

wrong position

Entity 클래스는 com.example.demo.entity 하위에 위치하는데 @EnableJpaRepositories을 선언한 설정 클래스는 com.example.demo.config 하위에 위치해 있었던 거죠. JPA 설정은 알아서 잘 찾길래 딱히 basePackage 위치도 지정하지 않았는데 Rest Repository 설정엔 영향을 끼치는 것 같습니다. 사실 위 설정 클래스 자체가 없어도 @SpringBootApplication가 설정된 클래스 하위에 위치하면 알아서 스캔하기 때문에 JPA가 잘 동작하는데 말이죠.

/*  그냥 날려 버리거나 basePackages 설정을 추가합니다.
@Configuration
@EnableJpaRepositories(basePackages = "com.example.demo")
public class JpaConfig {

}
*/

resolve issue

위와 같이 Entity들의 CRUD Request Path가 잘 생성 된 것을 확인 할 수 있습니다.