지금껏 플젝을 진행하면서 SpringBoot의 cron Scheduler만 사용을 하다가, 특이한 요구사항(?)으로 인해 fixedDelay, fixedRate를 사용하게 되었다. 사용을 해보니 fixedDelay만을 사용하게 되었지만, 이 참에 정리해보고자 한다.

 

일단 fixedDelay와 fixedRate의 구현은 다음과 같이 진행한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.scheduling.annotation.Scheduled;
 
@Scheduled(fixedDelay = 1000)
public void fixedDelayScheduler() {
  logger.info("FixedDelay Start!");
  try {
    Thread.sleep(600);
  } catch {
    e.printStackTrace();
  }
  logger.info("FixedDelay End!");
}
cs

 

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.scheduling.annotation.Scheduled;
 
@Scheduled(fixedRate = 1000)
public void fixedRateScheduler() {
  logger.info("FixedRate Start!");
  try {
    Thread.sleep(600);
  } catch {
    e.printStackTrace();
  }
  logger.info("FixedRate End!");
}
cs

사실 구현에 있어 fixedDelay , fixedRate의 차이는 존재하지 않는다. 다만, 어떻게 동작하느냐가 매우 다르므로 사용상에 유의할 필요가 있다.

 

먼저, fixedDelay는 현재 Schedule 상에 걸린 작업을 모두 끝난 이후에 설정된 시간이 카운팅되는 형태이며

fixedRate는 현재 Schedule 상에 걸린 작업의 완료 여부와 상관 없이 Scheduler가 시작한 시간으로부터 카운팅되는 형태이다.

 

위의 소스에 대해 로그를 살펴보자.

1
2
3
4
5
6
7
### Fixed Delay ###
2021-06-21 14:05:36.094  INFO 5536 --- [   DUMMY-1] com.example.DummyController   : FixedDelay Start!
2021-06-21 14:05:36.695  INFO 5536 --- [   DUMMY-1] com.example.DummyController   : FixedDelay END!
2021-06-21 14:05:37.698  INFO 5536 --- [   DUMMY-1] com.example.DummyController   : FixedDelay Start!
2021-06-21 14:05:38.300  INFO 5536 --- [   DUMMY-1] com.example.DummyController   : FixedDelay END!
2021-06-21 14:05:39.301  INFO 5536 --- [   DUMMY-1] com.example.DummyController   : FixedDelay Start!
2021-06-21 14:05:39.903  INFO 5536 --- [   DUMMY-1] com.example.DummyController   : FixedDelay END!
cs

 

1
2
3
4
5
6
7
### Fixed Rate ###
2021-06-21 14:06:42.762  INFO 5536 --- [   DUMMY-1] com.example.DummyController   : FixedRate Start!
2021-06-21 14:06:43.363  INFO 5536 --- [   DUMMY-1] com.example.DummyController   : FixedRate END!
2021-06-21 14:06:43.763  INFO 5536 --- [   DUMMY-1] com.example.DummyController   : FixedRate Start!
2021-06-21 14:06:44.365  INFO 5536 --- [   DUMMY-1] com.example.DummyController   : FixedRate END!
2021-06-21 14:06:44.765  INFO 5536 --- [   DUMMY-1] com.example.DummyController   : FixedRate Start!
2021-06-21 14:06:45.367  INFO 5536 --- [   DUMMY-1] com.example.DummyController   : FixedRate END!
cs

두 소스 모두 Start와 End 사이에 600ms의 Sleep을 주었다. 그 후에 End log가 찍히며, 이후 설정된 시간인 1000ms(1sec) 이후에 스케쥴러가 재실행되는 구조로 되어있다.

 

FixedDelay의 경우에는 14:05:36.094에 Start, 600ms Sleep 이후인 14:05:36:695에 End가 찍혔으며 End log가 찍힌 1000ms 이후인 14:05:37:698에 해당 스케쥴러가 실행되었다. 첫번째 Start 로그와 두번째 Start 로그의 시간차는 중간에 Thread.sleep으로 걸어준 600ms가 반영된 1600ms 만큼 차이가 존재한다.

 

FixedRate의 경우에는 14:06:42.762에 Start, 600ms Sleep 이후인 14:06:43:363에 End가 찍혔으나 그 시간과는 상관없이 Start 로그는 첫번째 Start 로그가 찍힌 시간에서 1000ms 이후인 14:06:43:763에 Start 로그가 찍혔다. 즉, 첫번째 Start 로그와 두번째 Start 로그의 시간차는 도중의 Thread.Sleep이 무시가 된 1000ms로 동일하다.

 

이는 스케쥴러 작업에 아주 중요한 영향을 끼친다. 내가 이번 프로젝트에서 fixedRate를 배제하고 fixedDelay를 사용하게 된 이유는 다음과 같다.

 

조건1. A 스케쥴러는 5분(300초)의 주기로 작동한다
조건2. A 스케쥴러에서 수행하는 작업은 짧게는 10초, 길게는 10분 이상이 소요된다
조건3. 조건2의 작업에서, DB의 Insert/Update 작업이 수시로 발생한다

위와 같은 작업상황에서 fixedRate로 수행을 할 경우, 조건 1의 10분정도 걸리는 작업을 수행하는 경우, 5분이 지난 시점에 동일한 A 스케쥴러가 작동을 하게 될 것이며, 같은 작업을 두 번 이상 수행하는 결과를 낳게 된다.

이는 의도된 행동과는 다르게 동작하기에, 동일한 데이터에 대해 한 번만 처리가 되어야 하는 작업을 여러번 처리하게 되므로 오작동을 하게 된다.

 

이에, 수행시간동안에는 스케쥴러가 동작하지 않고, 작업이 끝난 이후 설정된 시간 텀을 주는 fixedDelay를 사용하게 된 이유라고 볼 수 있다.

 

추가로, fixedDelayString , fixedRateString으로도 선언이 가능하며, 이는 long형 대신 String 형으로 선언을 받는다는 차이가 있다. application.yml에 선언하고 쓰기에 좋음ㅋ

 

*** 하나 더 추가

Springboot 에서 @Scheduled 을 사용하기 위해서는 메인 클래스에 @EnableScheduling 을 선언해주어야 정상 동작한다.

블로그 이미지

김생선

세상의 모든것을 어장관리

,

SpringBoot로 프로젝트를 진행하면서 앞으로 쓰일 여러가지들을 정리해본다.

 

1. 단일 항목 가져오기

application.yml과 application.property는 문법이 조금 다르다지만 뭐 그런건 크게 중요하지 않다.

일단 단일 항목을 가져오는 방법에 대해 서술한다.

 

1
2
3
4
5
### Saimple application.yml ###
 
async:
 coreName: Test
 coreSize: 10
cs

 

1
2
3
4
5
6
7
import org.springframework.beans.factory.annotation.Value;
 
@Value("${async.coreName}")
private String asyncCoreName;
 
@Value("${async.coreSize}")
private int asyncCoreSize;
cs

요러케 하면 String/Int 형으로 각각의 선언된 항목을 가져올 수 있다.

 

2. Array 항목 가져오기

1
2
3
4
### Saimple application.yml ###
 
async:
 intervalTime: 0910111213
cs
 
1
2
3
4
5
6
7
8
import org.springframework.beans.factory.annotation.Value;
 
@Value("${async.intervalTime}")
private String[] strAsyncIntervalTime;
 
@Value("${async.intervalTime}")
private int[] intAsyncIntervalTime;
 
cs

 

Array로 지정된 항목들에 대해서는 sample application.yml과 같이 쉼표, 공백문자열을 이용해 구분해준다.

String/Int 형태의 Array로 변환이 가능하다.

블로그 이미지

김생선

세상의 모든것을 어장관리

,

0. 현재상황

중계서버를 만들고 있다. 개발하다 보면 흔히 있는 일인데, 앞단(통칭 A)에서 전달해주는 JSON Key와 우리가 전달해줘야 하는 뒷단(통칭 B)의 JSON Key가 다른 경우가 있다. 같은 의미의 파라미터라고 하더라도, A서버는 "testEMail" 이라는 Key로 전달해줄 때, B서버는 "test_email"이라는 key로 받아야 한다. 그럴 땐 대충 아래와 같이 작업하는 편이다.

1
2
3
4
5
Map<String, Object> bMap = new HashMap<String, Object>();
//bMap에 담아서 B서버로 전달
if ( jsonResult.containsKey("testEMail")) {
   bMap.put("test_email", jsonResult.get("testEMail"));              
}
cs

뭐 근데 쓰다보면 불편한 점이 참 많다. 간단하게 한두개야 이렇게 한다손 치더라도, 후에는 오류가 생길 수 있다. 담당 개발자가 바뀐다거나 또는 Parameter Key가 변경된다거나 하는 정도로. 그래서 이래저래 고민하다 찾아보니 Enum이 있더라. 그래서 냅다 바꿨다.

 

1. Enum class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Getter
public enum aParamMapper {
    //좌측 - a서버 , 우측 - b서버
    testNm("testNm""test_nm"),    //사용자명
    testTelNo("testTelNo""test_tel_no"), //사용자전화번호
    testEMail("testEMail""test_email"), //사용자이메일
    ;
 
    private String aParam;
    private String bParam;
 
    bParamMapper(String aParam, String bParam) {
        this.aParam = aParam;
        this.bParam = bParam;
    }
}
 
cs

enum class를 하나 만들어서, 4~6번 라인과 같이 매칭시켜줄 것들을 만든다. 타입에는 arraylist도 들어가고 많은데, 아직 그런 활용성은 생기지 않았다.

 

2. 사용

1
2
3
4
5
6
7
8
Map<String, Object> bMap = new HashMap<String, Object>();
//bMap에 담아서 B서버로 전달
if ( jsonResult.containsKey(bParamMapper.testNm.getbParam())) {
   bMap.put(
       bParamMapper.testNm.getbParam()
       , jsonResult.get(bParamMapper.testNm.getaParam())
        );
}
cs

* 소스가 좀  길어져서 임의로 줄바꿈 함

0. 현재상황 의 4번 라인은 key/value 값이 각각 String 인자값으로 하드코딩 되어있는데 반해, 2.사용 의 5~6번 라인은 key/value 값을 enum을 통해 가져오는 방식으로 바뀌었다.

한두군데에서 쓰는 방식이라면 굳이 enum을 써야하나 싶긴 한데, 이를 활용하면 공통되는 부분을 모두 대체할 수 있는 데다가, 향후 Parameter Key가 변경되었을 때의 대처방안, 대소문자 구분자에 대한 해결방안 등도 모두 대응할 수 있을 것으로 생각된다. 

블로그 이미지

김생선

세상의 모든것을 어장관리

,

RESTful API를 개발하다가 좀 멘붕에 오는 상황을 발견했다. postman으로 아무리 날려도 파라미터는 계속 false만 찍는 것이었다. 대략적으로 다음과 같았다.


1
2
3
4
private boolean isBusiness;     // 개인용 false / 법인용 true
private String userName;        // 개인용 - 사용자명 / 법인용 - 법인명
private String userSex;         // 개인용만 사용, 성별
private String userInfo;        // 개인용 - 사용자 태그 / 법인용 - 법인 태그
cs


로그는 다음과 같다.

2020-05-13 18:30:47.801  INFO 17740 --- [nio-9988-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 5 ms

[사용자정보] UserIssueVO: UserIssueVO(isBusiness=false, userName=TEST, userSex=M, userInfo=TEST_tag)


파라미터를 바꾸면 잘되고 해서 대체 뭐가 잘못인가. 대충 30분 정도 고민하다가 개발자들의 꿈과 희망, 스택오버플로우에서 검색하니 다음과 같은 링크가 뜬다.


JSON Post request for boolean field sends false by default

https://stackoverflow.com/questions/21913955/json-post-request-for-boolean-field-sends-false-by-default


그래서 찾아보니, lombok과 같은 어플리케이션으로 getter/setter를 생성할 때에는 boolean 필드이름에 'is'를 쓰지 않는 것이 맞다고 한다. jackson의 java bean 네이밍 규칙이라고.

어쩐지 이후 로직을 짤 때 세팅하는 쪽에서 isBusiness로만 찍히기에 조금 의아하긴 했다.


여튼 한가지 알아둘 점은 lombok 사용시 boolean 필드명은 is를 쓰지 않는다는 것. 이거 하나면 됐다.

블로그 이미지

김생선

세상의 모든것을 어장관리

,

별건 아니지만 왠지 정리해야 할 것 같은 느낌이다.

UUID는 Universally Unique IDentifier의 약자로, 범용 고유 식별자라는 뜻을 갖고 있다. 유니크한 ID를 만들거나 뭐 활용도가 무궁무진하다. 사용법은 JAVA에서 그냥 import 후 사용하면 된다.


1
2
3
4
5
6
7
8
9
10
public static String uuid() throws Exception {
    try {
        String uuid = UUID.randomUUID().toString();
        System.out.println(uuid);        
        return uuid;
    } catch (Exception e) {
        // TODO: handle exception
    }
    return null;
}
cs


대충 요런식. 그러면 뭐 유니크한 ID가 나오게 된다.

별거 없다.

블로그 이미지

김생선

세상의 모든것을 어장관리

,

이클립스에서 dll 라이브러리를 활용하여 빌드를 할 일이 있었다.

비단 이클립스 뿐만 아니라 인텔리제이도 마찬가지였고. dll 라이브러리를 추가하는 방법을 몰라 헤맸는데, 간단히 해결할 수 있었다.


관련 dll 파일들을 모두 C:\Windows\System32 디렉토리에 넣어주면 된다. 

일각에서는 dll 파일들의 path를 잡아주면 된다고는 하는데 방법을 잘 몰라서 이렇게 무식한 방법으로 해결함...

블로그 이미지

김생선

세상의 모든것을 어장관리

,
개발환경
OS : macOS 10.14
JAVA : JDK 1.8

URL API 연동을 하는데 다음과 같은 에러를 보게 되었다.

sun.security.validator.ValidatorException:
PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

찾아보니 JAVA 에서 HTTPS로 연결시, 해당 사이트의 SSL 인증서가 신뢰하는 기관 인증서목록에 없거나 SSL/TLS암호화 버전이 맞지 않는다거나 하는 이유들.
다른 URL로는 정상적으로 연결이 되는 걸 보아서는 인증서 목록에 누락된 것에 무게를 두고 해결방법을 찾았다. 

해결방법은 JDK의 Cert Keystore에 해당 URL인증서를 넣어주면 된다.


1. InstallCert.java 다운로드
구글 코드에서 InstallCert.java 를 다운로드한다. 링크를 클릭하면 다운로드가 시작된다.


2. InstallCert.java 컴파일
다운로드 받은 InstallCert.java 를 컴파일 한다.
cd [다운로드 디렉토리]
javac InstallCert.java



3. InstallCert.java 구동

URL은 접속이 안되는 URL을 입력한다. 아래 예제에서는 naver.com을 샘플로 입력했다.

sudo java -cp ./ InstallCert [URL 명]

ihowon-ui-MacBook-Pro:downloads ihowon$ sudo java -cp ./ InstallCert naver.com
Loading KeyStore jssecacerts...
Opening connection to naver.com:443...
Starting SSL handshake...
[생략]
 4 Subject CN=AddTrust External CA Root, OU=AddTrust External TTP Network, O=AddTrust AB, C=SE
   Issuer  CN=AddTrust External CA Root, OU=AddTrust External TTP Network, O=AddTrust AB, C=SE
   sha1    02 fa f3 e2 91 43 54 68 60 78 57 69 4d f5 e4 5b 68 85 18 68
   md5     1d 35 54 04 85 78 b0 3f 42 42 4d bf 20 73 0a 3f
Enter certificate to add to trusted keystore or 'q' to quit: [1]

위의 cmd 화면에서 1을 입력 후 엔터키를 누르면 설정된다.


4. alias 설정
[앞부분 생략]
Added certificate to keystore 'jssecacerts' using alias 'naver.com-1’

위에서 보이는 alias 명을 꼭 기억해야 한다. 이 부분을 통해 앞으로 설정을 계속할 것이다. 


5. Cert Explort 해당 명령어를 통해 jssecacert를 export 한다
keytool -exportcert -keystore jssecacerts -storepass changeit -file output.cert -alias [naver.com-1] 

keystore의 jssecacerts 는 certs파일명, storepass의 changeit은 certs파일의 암호명이다. 이는 등록된 list를 수정/삭제할 때 필요하므로 기억하는것이 좋다.
또한, output.cert는 Java Cert에 Import할 때 필요하다.


6. jssecacert를 Java Cert에 Import
sudo keytool -importcert -keystore ${JAVA_HOME}/jre/lib/security/cacerts -storepass changeit -file [4.에서 설정된 jssecacert.cert] -alias [URL Alias-1]

이부분에서 jssecacert.cert는 사용자가 직접 입력하는 cert파일명이다. 이 파일명이 설명하는 사이트마다 당연히 다르고, 이 부분에 대해 크게 언급이 없어서 꽤 삽질했다. 이를 사용하는 JDK의 디렉토리 내에 Import를 시켜준다고 보면 된다. 사용하는 JDK 버전이 여러개라면, 각각의 버전에 대해 설정을 해줘야 할 것이라 생각된다.


>> 18.11.23 추가내용
개발하는데 접속해야 할 사이트의 SSL인증서가 변경되어, 위와 같은 방법으로 적용하려는데 새로운 오류를 보게 되었다. 해당 오류는 6. jssecacert를 Java Cert에 Import 하는 부분에서 발생하였고, 오류내용은 다음과 같다.

X.509 인증서가 아닙니다.

해당 오류에 대해 원인은 정확히 파악할 수 없었으나, 다음과 같은 방법으로 오류를 해결했다.

5. Cert Export 해당 명령어를 통해 jssecacert를 export 항목에서 생성된 jssecacerts 파일을 ${JAVA_HOME}/jre/lib/security/ 디렉토리에 복사하였더니 정상적으로 접근이 가능했다.


블로그 이미지

김생선

세상의 모든것을 어장관리

,

Java 에서 PrintWriter 를 사용하여 한글 출력할 때 한글이 깨지는 경우가 존재한다.

한글인코딩이 맞지 않아서 생기는 일.


1
2
3
4
5
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=UTF-8");
PrintWriter outs = response.getWriter();
outs.println"<script>alert('로그인 권한이 존재하지 않습니다..');history.back();</script>" );
return;
cs


주의할 점은, PrintWriter 에서 out 으로 꺼내기 전에 response의 인코딩 타입을 미리 정해줘야 한다는 것. 


블로그 이미지

김생선

세상의 모든것을 어장관리

,