Java Time

JAVA|2020. 7. 6. 20:13

제품이 JDK6 그리고 JDK7을 주로 사용하다보니 Calendar와 Date 사용에 익숙했었지만,

요즘 JDK8을 사용하며 다시 Time관련쪽을 살펴볼일이 있었습니다.

(이전에는 JodaTime도 써보고 했지만.. 하위버전에 Date와 Calendar가 익숙하다보니..)

 

때문에 JDK8에 신규로 등장한 LocalDateTime과 LocalDate, LocalTime에 대해 살펴보고

왜 변경되었는지 살펴볼 예정입니다.

 

Calendar, Date 문제점 및 불편하던점

1. 불변 객체가 아님

 - 객체를 외부에서 조작해서 문제가 생길수도 있음

 - 외부로 노출될때는 카피를 해서 노출하도록 해야함

 

2. int 상수 필드 남용

  - Calendar에서 set 하는 경우 Calendar의 상수를 사용하는데, 예를 들면 'Calendar.DAY_OF_MONTH', 상수로는 파악하기 어려움

 

3. 월 지정

  - 월지정에 1월을 0으로 표현, 1일은 1로 표현, 12를 넣어도(13월이 되는) 에러발생하지 않음

 

4. 일관성 없는 요일 상수

  - Calendar에서 일요일은 1, Date에서 일요일은 0

 

5. 불필요한 클래스 분리

  - 기존은 Date만 존재, 국제화 사용, 날짜 단위 계산 등으로 인해 Calendar 추가

  - 최종 결과물이 Date인 경우, Calendar를 통해 Date를 재생성,

  - Calendar의 생성비용은 비싼편

  - 날짜 시간을 모두 저장하는 클래스이름이 Date라는 부분

6. 오류 발생시키지 않는 시간대 지정 

  - TimeZone 지정에 오류가 있더라도 오류를 발생시키지 않는

 

7. java.util.Date 하위 클래스 문제

  - jdbc 활용시 사용되기도 하는 java.sql.Date는 상위 클래스와 이름이 같다

  - TimeStamp 클래스는 Date에 나노초를 더한 클래스, equals의 대칭성 문제가 발생할 수 있음

 

 

참고 및 출처 : https://d2.naver.com/helloworld/645609

 

8. SimpleDateFormat의 Thread-Safe 하지 않음

  - Date를 포맷팅 하기 위해 주로 사용되는 SimpleDateFormat는 ThreadSafe하지 않아 매번 생성하곤 합니다(혹은 sync를 잡아서..)

  - 신규로 추가된 DateTimeFormatter는 Thread-safe 합니다, 즉 한번 생성하고 재활용해서 여러 스레드에서 쓸수 있습니다.

 

JDK8 에서 추가된 LocalDateTime, LocalDate, LocalTime

  - LocalDateTime : 시간과 날짜 표현

  - LocalDate : 날짜만 표현

  - LocalTime : 시간만 표현

 

추가된 Time 클래스 특성

위에 특성들이 개선된 내용들이겠지만 몇가지만 보면

  - 불변클래스로 디자인

    - 생성자가 아닌 static 메소드를 통해 객체를 생성하도록 하며, 다양한 메소드로 다양한 요건들 충족할 수 있도록 지원

    - 3개 클래스 모두 상속을 막은 final class

    - 3개 클래스 모두 Temporal, TemporalAdjuster 인터페이스 구현

      - Temporal(시간) : TemporalAccessor를 상속 받은 인터페이스, plus 및 minus 메소드 지원

        - TemporalAccessor : get 및 query , getLong

      - TemporalAdjuster(시간조정자) : adjusInto 메소드 지원, DateTime에 adjusInto(LocalDate)를 하면 Time은 그대로 Date만 변경

 

 

Date & Calendar 사용법과 많이 달라 메소드를 다양하게 사용해봐야 할것

'JAVA' 카테고리의 다른 글

DCL(Double Checked Locking)  (0) 2020.09.08
Java Stream  (0) 2020.07.14
SortedQueue...  (0) 2020.07.02
java.util.function 인터페이스  (0) 2019.12.30
JEP 218: Generics over Primitive Types  (0) 2019.12.10

댓글()

SortedQueue...

JAVA|2020. 7. 2. 14:05

자바에.. SortedQueue가 있는줄 알았는데...

없다..?;;;

 

"왜 있는줄 알았지?" 하고 찾아보니 SortedQueue는 없지만 SortedSet이 있었다..

역시 툴 자동완성을 자주 쓰다보니..

헤깔렸다보다..(뭐.. 그냥 인터페이지 차이 뿐이니..)

 

다만 한가지 더는 TreeSet은 Queue쪽 인터페이스는 안되고

(하긴 지금 생각해보니 당연한건가.. set이랑 queue랑 조금 의미가 안맞긴하겠다.. 그냥 쌓이는 queue랑 중복 제거하는 set이랑..)

 

의도대로 하려면 Queue queue = new PriorityQueue<>(Comparator)...

Queue queue = new PriorityQueue<>((o1,o2)->o2.cnt-o1.cnt)...

 

그리고 하나더

 

TreeSet은 add한 후 add한 객체를 변경하면 순서가 변경되지 않는다.

PriorityQueue도 그런가? 했는데 테스트 해보니 마찬가지로 동작한다(add할때 순서가 정해지는 가보다..).

 

마지막으로

TreeSet에 add한 객체를 수정해도 순서가 안바뀌긴 하는데

remove를 하고 add하면 당연히 바뀌겠지만..

remove 안하고 add하면 어떻게 될까..?

 

테스트 해보니 결과는 "순서가 바뀌지 않았다"

 

필요한대로 동작하는지

항상 조금씩 유닛테스트를 돌리면서 진행하다보니..

이런 폐해가...

 

 

이런 실수도 실력이니..

다시 실수 안하기 위해 까먹지 않기위해 끄적끄적..

'JAVA' 카테고리의 다른 글

Java Stream  (0) 2020.07.14
Java Time  (0) 2020.07.06
java.util.function 인터페이스  (0) 2019.12.30
JEP 218: Generics over Primitive Types  (0) 2019.12.10
[CompletableFuture][#3] 메소드 정리-1  (0) 2019.09.01

댓글()

[Kafka] 토픽 삭제 후, 브로커 기동안됨

Server|2020. 6. 24. 18:51

카프카는 파티션 갯수를 늘리고 다시 줄일수 없다고 해서

토픽을 삭제 후 다시 생성하기 위해

 

kafka-topics.sh 를 통해(window에서 테스트를 해서 실제로는 .bat)

topic을 삭제 했습니다.

 

그런데. . kafka 브로커가 다운되더니.. 부팅되지 않는 문제가 발생하기 시작했습니다.

(부팅시 에러는 삭제한 토픽 파일을 읽는데 파일 권한에러..인데.. 전혀 접근 권한 문제 일으킬만한건 없습니다)

 

그래서 이런저런 글들을 살펴보며 겪은 부분들을 남깁니다...

 

먼저 delete.topic.enable=true

를 설정 하지 않아서 kafka-topics로 삭제했을때 마크만 된 상태였습니다.

그래서 위 설정을 해줬지만 마찬가지로 정상 부팅은 되지 않았습니다

(지금 생각해보니 설정을 하고 kafka-topics로 다시 삭제를 해볼껄 그랬나봅니다..)

(그런데.. 생각해보니 kafka-topics로 삭제할때 IP:PORT는 zookeeper쪽을 입력해줬는데.. 의미가 있나.. 싶네요)

 

 

두번째로 시도한건 recovery-point-offset-checkpoint, replication-offset-checkpoint 수정 입니다.

글을 보니 다른 브로커로 파티션 옮길때 수정하는 것으로 보이던데

이 방법을 시도해본 이유는 

"나는 토픽을 삭제했는데 왜 해당 토픽 관련 에러가 나는것인가?, 해당 토픽 내용들을 다 삭제해야 겠다"

였습니다.

파일 구조는 recovery~~~나 replication~~ 이나 거의 유사 했고

 

0
59
__consumer_offsets 22 0
__consumer_offsets 30 0
__consumer_offsets 8 0
__consumer_offsets 21 0
__consumer_offsets 4 0
__consumer_offsets 27 0
__consumer_offsets 7 0
__consumer_offsets 9 0
__consumer_offsets 46 0
topic3 0 0
__consumer_offsets 25 0
__consumer_offsets 35 0
__consumer_offsets 41 0
__consumer_offsets 33 0
__consumer_offsets 23 0
__consumer_offsets 49 0
topic2 0 0
__consumer_offsets 47 0
__consumer_offsets 16 0
__consumer_offsets 28 0
__consumer_offsets 31 0
__consumer_offsets 36 0
__consumer_offsets 42 28
__consumer_offsets 3 0
__consumer_offsets 18 0
__consumer_offsets 37 0
bars 0 0
__consumer_offsets 15 0

이런식이였습니다, 자세한 정보를 추가로 찾아봐야겠지만

두번째 줄에 59는

두줄을 빼고 데이터부가 59라인이 있다는것을 말합니다.

(결국 마지막 데이터 줄은 61라인)

그래서 삭제할 토픽의 데이터의 offsets 디렉토리 정보와 토픽 정보를 라인을 삭제하고 두번째 라인에서 값을 빼줬습니다.

 

그후, 부팅해보니 수정했던 replication-offset-checkpoint 가 다시 삭제했던 내용들을 복원해놓고 있었습니다

(대체 토픽정보를 어디서 복원을 하는거지.. 레플리카 구성한것도 아니고..)

 

마지막 세번째, 파일들은 대략 다 열어 봤고 마지막 의심가는건 zookeeper밖에 없었습니다

zookeeper 데이터를 찾아보니 /brokers/topics 에 삭제했던 topic 정보가 남아있었습니다

 

rmr /brokers/topics/대상토픽

으로 삭제하고 

replication-offset-checkpoint를 수정 후

broker를 띄우니 잘 뜨는것을 확인했습니다.

 

지금은 토픽을 그냥 삭제하는데 발생한 이슈였지만.. 운영중에 갑작이 이렇게 부팅이 안되는 문제가 발생하면.....;;;;

kafka를 사용하는 방법도 더 해봐야겠지만

내부 파일 구조 체계들을 알아둬야 이런 난감한 상황에 잘 대처 할 수 있을것 같습니다

'Server' 카테고리의 다른 글

[redis] O(n) 명령어들 및 개선 방법  (0) 2020.08.11
[Oracle] auto commit error  (0) 2020.07.29
Zookeeper  (0) 2019.12.09
Nginx Service 로 등록  (0) 2014.08.13
Jconsole (톰캣 모니터링,자바 모니터링)  (0) 2014.08.12

댓글()

[Spring] LifeCycle, SmartLifeCycle

JAVA/Spring|2020. 6. 23. 19:02

kafkaMessageListener를 살펴보다가 

SmartLifecycle을 통해 org.springframework.kafka.listener.ConcurrentMessageListenerContainer 를 start 시키는것을 보고

 

SmartLifecycle과 LifeCycle에 대해 조금 살펴보았습니다.

추후 이와같이 서버 기동시에 세팅하고 종료 시키는 Bean이 필요하면 써먹으면 어떨까 합니다.

기존에는 초기화 Bean 을 만들고 @PostConstruct를 달아 초기화 로직을 돌리고 종료는 알아서 하곤 했습니다.

 

org.springframework.context.SmartLifeCycle은 Lifecycle, Phased 인터페이스를 구현한 인터페이스 입니다.

SmarLifeCycle을 살펴보기 이전에 LifeCycle을 살펴보고 SmarLifeCycle 을 살펴보겠습니다.

 

LifeCycle

LifeCycle은 라이프 사이클을 컨트롤 하기위해 start(), stop(), isRunning() 메소드로 구성되어 있으며

주석을 살펴보니 LifeCycle 인터페이스는 자동실행은 담당하지 않고 자동실행을 하고 싶으면"SmartLifecycle"을 활용하라고 되어있습니다.

 

Bean이 LifeCycle 인터페이스를 구현한다면 ApplicationContext에서 Start와 Stop 시그널을 준다고 되어있고

JMX를 통해서도 제어 가능하다고 되어있습니다

 

그리고 주요한 부분중 하나는 Scope가 "싱글톤" 일때만 동작한다는 것 입니다.

(테스트 해보니 "프로토타입"의 경우 동작하지 않는것을 확인 했습니다.)

(서버 다운시 ShutDown Hook에서 동작하다 보니 프로토타입은 적합하지 않나봅니다)

(동작하려면 서버 종료때까지 Bean이 살아있어야 한다는 말이기 때문에..)

 

SmarLifeCycle

SmarLifeCycle은 위에 적은대로 Lifecycle, Phased 를 구현한 인터페이스이지만 LifeCycle과 동작이 조금 다릅니다.

종료시점에 isRunning을 체크하여 stop을 호출해준다는 부분은 같지만

앞에 설명대로 SmartLifeCycle은 자동 실행으로 기동시점에 start() 메소드를 호출해줍니다.

DefaultLifecycleProcessor:140에 startBeans를 보면, 두 가지 Bean 모두 lifecycleBean 맵에 존재 하지만 체크로직으로 SmarLifecycle Bean만 phases에 들어가는 것을 볼수 있고 하단에 start()를 호출해주는 부분을 볼수 있습니다.

 

그리고 종료(stop)하는 부분도 LifeCycle살짝 다르긴한데

DefaultLifecycleProcessor:221에 doStop을 보면, SmarLifecycle은 Callback을 넘겨받는 stop이 불리며

Lifecycle은 단순 stop만 호출하고 끝나는 것을 볼수 있습니다.

 

 

서버 다운시 돌고있는것을 정리만 해주기 원한다면 Lifecycle을 

서버 부팅시 start, 종료시 stop 모두 서버와 Lifecycle을 함께 간다면 SmartLifecycle Bean을 고려해보는것도 좋을것 같습니다.

 

'JAVA > Spring' 카테고리의 다른 글

[Spring] SessionStatus는 어떻게 동작할까?  (5) 2020.09.08
[JPA] LockType  (0) 2020.07.23
[MSA] sidecar 패턴  (0) 2020.01.13
[SpringConfig] Properties 암호화  (0) 2019.12.24
spring boot log level 변경(actuator)  (0) 2019.12.19

댓글()

[SpringBatch] Step 커스텀 트랜잭션 처리

JAVA/SpringBatch|2020. 6. 18. 20:43

자사 제품을 SpringBatch을 커스텀 하여 제공하는 방향으로 가고 있는데

기존 자사 모듈 코드들을 SpringBatch에서 사용 할 수 있도록 몇가지 작업들(Mock 처리 및 ThreadLocal로 값 세팅)이 필요했습니다.

 

그래서 처음에는 간단한 Tasklet을 가지고 테스트 하며 어디에 적용할지 생각했을때

AOP를 적용하는 방법을 생각하다 Tasklet이 Bean 대상이 아닐경우 동작 안하는 케이스가 생길수 있기 때문에

다른 방향을 생각하다 TransactionChunkListener의 존재를 알고 해당 부분으로 선후 작업 처리를 할 수 있도록 했습니다.

(그냥 트랜잭션 앞뒤에 해주면 되겠다고 생각했었습니다.)

 

최근 ETL에서 다른것들을 테스트 할 일이 있어 테스트하다보니 ItemReader의 open()시에 자사 모듈이 정상적으로 동작하지 않는것을 확인하고 SpringBatch 코드를 열어보게 되었습니다.

 

결국 open을 호출하는 코드는 org.springframework.batch.core.step.AbstractStep에 :205에서 호출하고 있었고

초기화하는 ChunkListener의 경우 :208에 doExecute 안쪽에서 수행되고있었습니다.

 

어떻게 처리를 할까 고민을 하다 

open을 수행하는 :205 를 보니 :204 beforeStep에서 stepExecutionListener를 호출해주고 있었습니다.

 

결국에는 stepExecutionListener에서 자사 모듈을 위한 초기화 작업과 폐기 처리들을 하고

TransactionChunkListener는 실제 트랜잭션 처리만 할 수 있도록 변경했습니다.

(open을 트랜잭션 밖에서 하고 있다는게 의외긴 했습니다..)

 

역시 틈틈히 관련 코드들을 봐야 제대로된 코드를 작성할 수 있을것 같습니다.

 

 

댓글()

properties 에 따른 @Qualifier(${variable}) 처리

JAVA/SpringBatch|2020. 5. 27. 20:14

properties에 따라서 Injection해야하는 Bean을 변경해야 했습니다

물론 Profile을 통해서 Bean 등록을 바꾼다던지 여러 방법이 있겠지만

 

프레임워크 엔진에는 구현체가 여러개가 있고

사용자가 선택해서 쓸수 있도록 제공하기 위해 Profile을 통해서는 애매하여

@Qualifier에 변수를 적용하여 처리를 시도해봤습니다.

 

@Autowired

@Qualifier("${user.handler:simpleHandler}")

user.handler에 bean명이 설정되어있으면 설정된 bean값을 쓰고 아니면 기본값인 simpleHandler Bean을 주입하길 원했지만 동작하지 않았습니다

(@ComponentScan이나 @ImportResource 도 다 변수가 적용됬는데 @Qualifier는 동작을 안했습니다..;;)

 

그래서 어떻게 처리를 할까 고민 하다

기본으로 돌아가서 ApplicationContext.getBean()을 활용하는 방법으로 일단 적용해두었습니다.

 

@Value("${user.handler:simpleHandler}")
String handlerBeanName;

@PostConstruct
private void postConstruct() {
	this.handler = applicationContext.getBean(handlerBeanName);
}

 

추후 @Qualifier는 어떤 단계에서 어떤방식으로 처리하는지 봐야할것 같습니다.

댓글()

[SpringBatch] @ConfigurationProperties 이용한 데이터소스 주입 관련

JAVA/SpringBatch|2020. 4. 28. 21:20

설정이 다 jar 내에 있으면 좋겠지만

외부에서 설정 받는 경우를 고려해야하고 엔진에서 어떤 데이터소스를 사용하게 될지 명시를 해줘야하기 때문에

기존 설정인 "spring.datasource.hikari.xxx"설정은 감추고 "engine.ds.xxx" 형식으로 지원을 처리하고 있었습니다.

 

그리고 설정이 되어있지 않다면 H2를 메모리 방식으로 동작하는것을 의도해서

코드를 아래와같이 했었습니다.

 

	
    @Bean("xxxDS")
    @Primary
    @ConfigurationProperties(prefix = "engine.ds")
    public DataSource xxxDS() {
        HikariDataSource ds = DataSourceBuilder.create().type(HikariDataSource.class).build();
        if(!ds.isRunning()){
            ds = memDataSource();
        }

        return ds;
    }

그런데 아래와 같이 에러가 떨어집니다..

 

일부..

Caused by: java.lang.IllegalStateException: Unable to set value for property jdbc-url
at org.springframework.boot.context.properties.bind.JavaBeanBinder$BeanProperty.setValue(JavaBeanBinder.java:349)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:96)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:79)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:56)
at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$5(Binder.java:452)
at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:572)
at org.springframework.boot.context.properties.bind.Binder$Context.withDataObject(Binder.java:558)
at org.springframework.boot.context.properties.bind.Binder$Context.access$400(Binder.java:513)
at org.springframework.boot.context.properties.bind.Binder.bindDataObject(Binder.java:450)
at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:391)
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:320)
... 123 more
Caused by: java.lang.reflect.InvocationTargetException
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.boot.context.properties.bind.JavaBeanBinder$BeanProperty.setValue(JavaBeanBinder.java:346)
... 133 more
Caused by: java.lang.IllegalStateException: The configuration of the pool is sealed once started. Use HikariConfigMXBean for runtime changes.
at com.zaxxer.hikari.HikariConfig.checkIfSealed(HikariConfig.java:1015)
at com.zaxxer.hikari.HikariConfig.setJdbcUrl(HikariConfig.java:504)
... 138 more

HikariDataSource를 보니 HikariConfig를 생성자로 받아 생성된경우거나 getConnection()이 한번 불려서 seal처리가 되어있어서 setter를 부를수 없어 발생하는 에러였습니다

 

아마도 이 경우에는 HikariConfig 를 arg로 받는 생성자가 불려 seal 처리 된거겠지요

그런데.. 저는setter를 불러준적이 없습니다..

물론 그렇다면 DataSourceBuilder쪽에서 build 할때 발생하는 거겠지요

 

하지만.. 실제로는 DataSourceBuilder쪽에서 build할때 리플렉션으로 setter를 부르는게 아니라

@ConfigurationProperties에 의해 이미 Bean이 된 engineDS에 setter를 부르고 있었습니다..

 

정리하면!

HikariDataSource ds = DataSourceBuilder.create().type(HikariDataSource.class).build();

에 생성된 ds는 빈껍데기고

체크하는 로직에 당연히 들어오니 memDataSource가 만들어진 후 memDataSource가 engineDS로 등록되었고

그후 @ConfigurationProperties에 의해 setter가 불리는 상황..

 

그래서 어떻게할까 하다. memDataSource를 만들때 HikariConfig를 생성자로 넘기지 않고

HikariDataSource 에 setter들을 불러 생성하면 사용하기 전까지는 seal처리가 되지 않기에 이 방법으로 우회했습니다.

어차피 기본값도 정의를 할 부분이였기 때문에..

(fastPool을 안쓰는거지요, getConnection을 보니 한번은 락을 잡지만 pool 생성후에는 잡지 않으니 문제는 없을것 같습니다.)

(추후 다른 방법을 찾으면 변경하는걸로..)

 

역시 어노테이션을 통한 리플렉션이 편리한건 맞지만..

동작을 모르면 예상치 못한 상황이 나온다는 교훈을... 또 얻어 갑니다.

댓글()

SpringBatch에 external Configuration Class 검토-2

JAVA/SpringBatch|2020. 4. 23. 19:38

지난번 SpringBoot에서 SpringBatch를 동작시키는데, External Class들을 잡는데 생기는 문제점들이 있어서

 

지난번 글에 MANIFEST.MF에 org.springframework.boot.loader.JarLauncher 가 Main-Class 에 적혀 있고

실제 개발한 코드의 메인은 Main-Class에 적힌 JarLauncher가 호출해준다는 부분을 언급했습니다.

 

JarLauncher 코드를 보며 클래스로더를 봤을때는 "클래스로더로 커스텀으로 구성하던지 해야겠구나" 로 생각했는데

검색해보니 역시나 해결방법은 있었습니다.

 

org.springframework.boot.loader.JarLauncher 말고도 빌드된 jar파일을 보면 다른 클래스들이 함께 들어있는데

다른 여러 클래스들이 있지만 Launcher 구현체 는 WarLauncher와 PropertiesLauncher 두가지가 추가로 있습니다.

org.springframework.boot.loader.PropertiesLauncher 소스를 열어보니 처음 시도하던 -cp로 잡은 클래스패스와 BOOT-INF를 같은 클래스로더로 구성하는 방식은 아니지만 클래스패스 경로를 지원하는 설정을 통해 BOOT-INF와 별도로 설정한 클래스 패스 경로를 설정할 수 있도록 작성되어있습니다

 

그설정은 바로 "loader.path" 입니다.

저는 실행시 -Dload.path=userLib/ 형식으로 주거나 별도 설정 파일로 영역을 열어줄 예정입니다.

"loader.path"의 특이사항으로는 해당 영역의 클래스와 jar 그리고 리소스를 모두 클래스 패스에 한번에 잡아준다는 부분이며 해당 클래스패스에 있는 Configuration 클래스들도 Bean으로 잘 등록되는것을 확인했습니다.

 

물론 이 부분이 해결됬다고 하더라도 추가적으로 해야할 부분은 많이 남았습니다.

외부 클래스들을 클래스 패스로 제공하면 현재는 패키지가 동일한 클래스를 테스트 했지만

다른 패키지라면 componentScan을 어떻게 제공할 것인지

xml 설정은 어떻게 커버할 것인지 등.. 하나씩 시도해보고 

막히는 부분이나 기록할 부분이 있으면 기록을 남길 예정입니다.

(작성하다보니 이건 SpringBatch 라기보다는 SpringBoot 활용 관련 이슈군요..)

댓글()