전체 글 (356)

async(비동기) 처리를 위한 ThreadPoolTaskExecutor

ThreadPoolTaskExecutor를 이용하여 비동기처리하는 방법을 알아보겠습니다.

ThreadPoolTaskExecutor는 스프링에서 제공해주는 클래스로 org.springframework.scheduling.concurrent 패키지에 속해있습니다.

생성자도 기본생성자 하나만 존재합니다. 이름에서 알 수 있듯이 쓰레드풀을 이용하여 멀티쓰레드 구현을 쉽게 해주는 클래스입니다.

 

그럼 어떻게 사용하는지 살펴보겠습니다.

	public static void main(String[] args) {
		ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
		executor.initialize();
	}

ThreadPoolTaskExecutor 를 생성하고 사용할 수 있도록 initialize()를 호출했습니다. 왜냐면 이니셜라이즈하기 전에는 executor를 사용을 할 수 없기 때문입니다. 만약 이니셜라이즈 하기 전에 사용하려고 한다면 아래와 같은 오류 메시지를 보게 됩니다.

 

Exception in thread "main" java.lang.IllegalStateException: ThreadPoolTaskExecutor not initialized

 

그럼 이제 아래처럼 코드를 좀 더 추가한 뒤 실제로 쓰레드를 실행시켜 보겠습니다.

	public static void main(String[] args) {
		ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
		executor.initialize();
		
		log.info("executing threads....");
		Runnable r = () -> {
			try {
				log.info(Thread.currentThread().getName() + ", Now sleeping 10 seconds...");
				Thread.sleep(10000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		};

		for (int i = 0; i < 10; i++) {
			executor.execute(r);
		}
	}

 

출력 결과를 한번 볼까요?

07:42:09.450 [main] INFO com.keichee.test.service.TestService - executing threads....
07:42:09.460 [ThreadPoolTaskExecutor-1] INFO com.keichee.test.service.TestService - ThreadPoolTaskExecutor-1, Now sleeping 10 seconds...
07:42:19.464 [ThreadPoolTaskExecutor-1] INFO com.keichee.test.service.TestService - ThreadPoolTaskExecutor-1, Now sleeping 10 seconds...
07:42:29.465 [ThreadPoolTaskExecutor-1] INFO com.keichee.test.service.TestService - ThreadPoolTaskExecutor-1, Now sleeping 10 seconds...
07:42:39.470 [ThreadPoolTaskExecutor-1] INFO com.keichee.test.service.TestService - ThreadPoolTaskExecutor-1, Now sleeping 10 seconds...
07:42:49.472 [ThreadPoolTaskExecutor-1] INFO com.keichee.test.service.TestService - ThreadPoolTaskExecutor-1, Now sleeping 10 seconds...
07:42:59.477 [ThreadPoolTaskExecutor-1] INFO com.keichee.test.service.TestService - ThreadPoolTaskExecutor-1, Now sleeping 10 seconds...
07:43:09.483 [ThreadPoolTaskExecutor-1] INFO com.keichee.test.service.TestService - ThreadPoolTaskExecutor-1, Now sleeping 10 seconds...
07:43:19.489 [ThreadPoolTaskExecutor-1] INFO com.keichee.test.service.TestService - ThreadPoolTaskExecutor-1, Now sleeping 10 seconds...
07:43:29.491 [ThreadPoolTaskExecutor-1] INFO com.keichee.test.service.TestService - ThreadPoolTaskExecutor-1, Now sleeping 10 seconds...
07:43:39.496 [ThreadPoolTaskExecutor-1] INFO com.keichee.test.service.TestService - ThreadPoolTaskExecutor-1, Now sleeping 10 seconds...

로그가 출력된 시간을 보면 10초마다 출력이 되고있고 쓰레드도 ThreadPoolTaskExecutor-1 하나가 모두 처리한 것을 알 수 있습니다. 즉, 지금 위 코드는 멀티쓰레드로 돌아간게 아니란 얘기죠. ThreadPoolTaskExecutor는 몇 가지 설정값들을 가지고 있습니다. 그 중 corePoolSize 값이 동시에 실행할 쓰레드의 개수를 의미하는데 이 설정의 default 값이 1로 세팅되어 있기 때문에 위 처럼 corePoolSize 설정없이 그대로 사용하면 싱글쓰레드로 돌아가게 됩니다. 

 

설정값

그럼 설정값들에 어떤 것들이 있는지 그것들이 의미하는게 무엇인지 setter 메서드를 기준으로 중요한 값들만 한번 살펴보고 넘어가겠습니다.

 

메서드

설명

기본값

setCorePoolSize

corePoolSize 값을 설정함. corePoolSize는 동시에 실행시킬 쓰레드의 개수를 의미함

1

setAllowCoreThreadTimeOut

코어 쓰레드의 타임아웃을 허용할 것인지에 대한 세터 메서드. true로 설정할 경우 코어 쓰레드를 10으로 설정했어도 일정 시간(keepAliveSeconds)이 지나면 코어 쓰레드 개수가 줄어듦.

false

setKeepAliveSeconds

코어 쓰레드 타임아웃을 허용했을 경우 사용되는 설정값으로, 여기 설정된 시간이 지날 때까지 코어 쓰레드 풀의 쓰레드가 사용되지 않을 경우 해당 쓰레드는 terminate 된다.

60초

setMaxPoolSize

쓰레드 풀의 최대 사이즈

Integer.MAX

setQueueCapacity

쓰레드 풀 큐의 사이즈. corePoolSize 개수를 넘어서는 task가 들어왔을 때 queue에 해당 task들이 쌓이게 된다. 최대로 maxPoolSize 개수 만큼 쌓일 수 있다.

Integer.MAX

 

여기서 corePoolSize, maxPoolSize, queueCapacity 이 세 가지 설정값이 가장 중요합니다.

우선 위에서 봤던 첫 번째 예제에서는 이 세 가지 값에 대해서 별도로 설정을 하지 않았었습니다. 그래서 싱글 쓰레드로 작업이 이루어졌죠.

 

corePoolSize

이번에는 corePoolSize를 올려보겠습니다.

    public static void main(String[] args) {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.initialize();

        log.info("executing threads....");
        Runnable r = () -> {
            try {
                log.info(Thread.currentThread().getName() + ", Now sleeping 10 seconds...");
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        for (int i = 0; i < 10; i++) {
            executor.execute(r);
        }
    }

 

corePoolSize를 5로 설정 후 실행해보았습니다. (queueCapacity와 maxPoolSize값은 현재 기본값인 Integer.MAX 입니다)

08:52:50.423 [main] INFO com.keichee.test.service.TestService - executing threads....
08:52:50.456 [ThreadPoolTaskExecutor-3] INFO com.keichee.test.service.TestService - ThreadPoolTaskExecutor-3, Now sleeping 10 seconds...
08:52:50.456 [ThreadPoolTaskExecutor-2] INFO com.keichee.test.service.TestService - ThreadPoolTaskExecutor-2, Now sleeping 10 seconds...
08:52:50.456 [ThreadPoolTaskExecutor-5] INFO com.keichee.test.service.TestService - ThreadPoolTaskExecutor-5, Now sleeping 10 seconds...
08:52:50.456 [ThreadPoolTaskExecutor-1] INFO com.keichee.test.service.TestService - ThreadPoolTaskExecutor-1, Now sleeping 10 seconds...
08:52:50.456 [ThreadPoolTaskExecutor-4] INFO com.keichee.test.service.TestService - ThreadPoolTaskExecutor-4, Now sleeping 10 seconds...
08:53:00.460 [ThreadPoolTaskExecutor-1] INFO com.keichee.test.service.TestService - ThreadPoolTaskExecutor-1, Now sleeping 10 seconds...
08:53:00.460 [ThreadPoolTaskExecutor-2] INFO com.keichee.test.service.TestService - ThreadPoolTaskExecutor-2, Now sleeping 10 seconds...
08:53:00.461 [ThreadPoolTaskExecutor-3] INFO com.keichee.test.service.TestService - ThreadPoolTaskExecutor-3, Now sleeping 10 seconds...
08:53:00.461 [ThreadPoolTaskExecutor-4] INFO com.keichee.test.service.TestService - ThreadPoolTaskExecutor-4, Now sleeping 10 seconds...
08:53:00.461 [ThreadPoolTaskExecutor-5] INFO com.keichee.test.service.TestService - ThreadPoolTaskExecutor-5, Now sleeping 10 seconds...

실행되자마자 5개의 쓰레드가 실행되고 10초 후에 또 다른 5개의 쓰레드가 실행된 것을 확인할 수 있습니다. 즉, queueCapacity와 maxPoolSize값을 기본값으로 해놓으면 corePoolSize의 값만큼 쓰레드가 동시에 실행되는 것을 알 수 있습니다.

 

corePoolSize와 queueCapacity

그럼 이번에는 corePoolSize는 default 값인 1로 놔두고 queueCapacity와 maxPoolSize 값을 5로 설정해보겠습니다. 그리고 10개의 task가 실행될 때 poolSize, activeSize, queueSize 가 어떻게 변하는지 확인할 수 있게 출력해보겠습니다. 또, 출력을 좀 짧게 하기위해서 쓰레드명 prefix를 "my-"로 변경했습니다.

    public static void main(String[] args) {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setThreadNamePrefix("my-");
        executor.setQueueCapacity(5);
        executor.setMaxPoolSize(5);
        executor.initialize();

        log.info("executing threads....");
        Runnable r = () -> {
            try {
                log.info(Thread.currentThread().getName() + ", Now sleeping 10 seconds...");
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        for (int i = 0; i < 10; i++) {
            executor.execute(r);
            System.out.print("poolSize:" + executor.getPoolSize());
            System.out.print(", active:" + executor.getActiveCount());
            System.out.println(", queue:" + executor.getThreadPoolExecutor().getQueue().size());
        }
    }

 

어떤 결과가 나올 것 같은지 한번 생각을 해보고 아래 결과를 확인해보시기 바랍니다.

09:02:22.864 [main] INFO com.keichee.test.service.TestService - executing threads....
poolSize:1, active:1, queue:0
poolSize:1, active:1, queue:1
poolSize:1, active:1, queue:2
poolSize:1, active:1, queue:3
poolSize:1, active:1, queue:4
poolSize:1, active:1, queue:5
poolSize:2, active:2, queue:5
09:02:22.886 [my-1] INFO com.keichee.test.service.TestService - my-1, Now sleeping 10 seconds...
09:02:22.887 [my-2] INFO com.keichee.test.service.TestService - my-2, Now sleeping 10 seconds...
poolSize:3, active:3, queue:5
09:02:22.887 [my-3] INFO com.keichee.test.service.TestService - my-3, Now sleeping 10 seconds...
poolSize:4, active:4, queue:5
09:02:22.887 [my-4] INFO com.keichee.test.service.TestService - my-4, Now sleeping 10 seconds...
poolSize:5, active:5, queue:5
09:02:22.887 [my-5] INFO com.keichee.test.service.TestService - my-5, Now sleeping 10 seconds...
09:02:32.888 [my-1] INFO com.keichee.test.service.TestService - my-1, Now sleeping 10 seconds...
09:02:32.890 [my-2] INFO com.keichee.test.service.TestService - my-2, Now sleeping 10 seconds...
09:02:32.890 [my-4] INFO com.keichee.test.service.TestService - my-4, Now sleeping 10 seconds...
09:02:32.890 [my-5] INFO com.keichee.test.service.TestService - my-5, Now sleeping 10 seconds...
09:02:32.890 [my-3] INFO com.keichee.test.service.TestService - my-3, Now sleeping 10 seconds...

 

 

위 출력 결과를 보면 10개의 task를 실행할 때 queue 사이즈가 0에서 5까지 올라가고 그 이후에 poolSize와 active 사이즈가 증가하는 것을 알 수 있습니다. corePoolSize가 1 이라서 2번째 task부터 6번째 task까지는 queue에 들어가게 됩니다. 그리고 7번째 task부터 10번째 task까지 4개의 task는 maxPoolSize 를 넘어서지 않는 한 추가로 쓰레드를 생성하여 pool에 넣고 해당 쓰레드로 각 task를 실행하게 됩니다. 

그럼 만약 maxPoolSize를 넘어설 만큼 많은 양의 task들이 들어온다면 어떻게 될까요?

Exception in thread "main" org.springframework.core.task.TaskRejectedException: Executor [java.util.concurrent.ThreadPoolExecutor@32eff876[Running, pool size = 5, active threads = 5, queued tasks = 5, completed tasks = 0]] did not accept task: com.keichee.test.service.TestService$$Lambda$22/0x00000008000ce840@5e0826e7
	at org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor.execute(ThreadPoolTaskExecutor.java:324)

TaskRejectedException 오류가 발생하게 됩니다. 

따라서 얼마나 많은 양의 task를 소화해야 하는지를 정확히 알고 corePoolSize, queueCapacity, maxPoolSize에 적절한 값을 세팅하여 사용해야 합니다. 기본 값으로 사용하면 TaskRejectedException을 볼 일은 거의 없겠지만 그 대신 queue에 어마어마한 양의 task가 쌓이겠죠. 그러다가 애플리케이션의 배포나 어떤 이유에 의해서 재기동이 필요하게 되면 해당 queue에 쌓여있던 task들은 사라지게 됩니다. 

 

스프링부트에서 사용하실 분들은 아래 포스팅을 참고하시면 좋습니다.

스프링 부트에서 ThreadPoolTaskExecutor를 설정 및 사용하는 방법

 

스프링 RestTemplate 타임아웃 설정을 하는데 타임아웃 시간이 설정한대로 적용되지 않는 듯 하여 테스트 해봄..

보통 HttpComponentsClientHttpRequestFactory 와 SimpleClientHttpRequestFactory 를 사용하여 설정을 함

스프링의 RestTemplate 기본적으로 SimpleClientHttpRequestFactory를 사용함

SimpleClientHttpRequestFactory를 이용해서 설정을 하면 정상적으로 세팅한 값에 타임아웃이 발생함.

하지만 HttpComponentsClientHttpRequestFactory 를 이용하면 설정한 시간보다 4배 긴 시간이 흐른 뒤에야 타임아웃이 발생하였음

@Test
public void 타임아웃_테스트() {

HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setReadTimeout((int) TimeUnit.SECONDS.toMillis(10));
factory.setConnectTimeout((int) TimeUnit.SECONDS.toMillis(5)); // 4배의 시간이 걸린 뒤에야 타임아웃 발생
factory.setConnectionRequestTimeout(5 * 1000);

// SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
// factory.setReadTimeout((int) TimeUnit.SECONDS.toMillis(10));
// factory.setConnectTimeout((int) TimeUnit.SECONDS.toMillis(5)); // 세팅한 시간대로 타임아웃 발생

// RequestConfig config = RequestConfig.custom()
// .setSocketTimeout((int) TimeUnit.SECONDS.toMillis(1))
// .setConnectTimeout((int) TimeUnit.SECONDS.toMillis(5))
// .setConnectionRequestTimeout((int) TimeUnit.SECONDS.toMillis(10))
// .build();
// CloseableHttpClient client = HttpClientBuilder
// .create()
// .setDefaultRequestConfig(config)
// .build();
// HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(client);

RestTemplate restTemplate = new RestTemplate(factory);

// 1. connect timeout 테스트
long start = System.currentTimeMillis();
try {
ResponseEntity<String> result = restTemplate.getForEntity("https://abc.com:81/test", String.class);
} catch (ResourceAccessException e) {
log.error("타임아웃!! {}", TimeUtils.printDuration(System.currentTimeMillis() - start), e);
}

}


아직 원인은 확인하지 못함.


push까지 해버린 git commit 원복하는 방법


공동 repository에 push까지 해버린 commit을 되돌리는 방법은 경우에 따라 두 가지가 있는 것 같습니다.
커밋 개수가 한 두 건 일 때 git revert를 사용하는 방법이 있고, 커밋 개수가 너무 많아 하나씩 revert하기 힘든 경우가 있을 수 있습니다. 여기서는 git revert를 사용하지 않고 원복하는 방법을 알려드립니다.

1. commit log 확인

2. reset (원하는 시점으로 되돌아가기)

3. revert (특정 시점 이후의 변경사항 되돌리기)

4. force push (되돌린 내용을 공동 repo에 반영하기)


1. commit history에서 원복 시점의 커밋 확인

keichee$ git log -5 --pretty=format:"%h - %an, %ar : %s"

148444a6a - keichee, 4 days ago : Merge branch 'dev-sentry' into stage

eebdd9202 - keichee, 4 days ago : dev 환경 로깅 sentry 연동

1c74ca53e - john, 5 days ago : Merge pull request #1238 in test/repo from release/200202 to stage

6544cd10a - john, 5 days ago : Merge pull request #1237 in test/repo from feature/200202 to release

754046d47 - tom, 5 days ago : test commit 1


git log 명령어를 이용해서 commit 히스토리를 확인 할 수 있습니다.

이때 옵션으로 -5 를 입력해주면 최신순으로 5개 까지만 조회를 할 수 있고 

옵션을 주지 않으면 페이징 처리되어 계속 조회할 수 있습니다.

뒤에 pretty 옵션을 주면 위 결과처럼 조회가 되는데 pretty 옵션을 안주면 commit 하나의 내용이 총 6줄에 걸쳐서 표시가 되기 때문에 한눈에 보기가 힘듧니다.


2. 원하는 시점으로 Reset 

위에서 조회한 5개의 커밋들 중에 john이 5일 전에 커밋한 내용까지만 적용하고 keichee가 커밋한 내용을 revert시켜보겠습니다.

commit it가 1c74ca53e인 것을 확인하고 아래와 같이 reset 합니다.

keichee$ git reset 1c74ca53e

Unstaged changes after reset:

M       src/main/resources/logback-spring.xml


3. Revert

reset을 하면 1c74ca53e 커밋까지 완료된 상태로 되돌아 갑니다.

그리고 그 이후의 커밋에 대해서는 수정상태로 변경됩니다.

이때 git status로 확인을 해보면 아래와 같이 확인 할 수 있습니다.

keichee$ git status

On branch test-revert

Changes not staged for commit:

  (use "git add <file>..." to update what will be committed)

  (use "git checkout -- <file>..." to discard changes in working directory)


        modified:   src/main/resources/logback-spring.xml

이제 이렇게 수정된 내용들에 대해서 원복해보겠습니다.

keichee$ git checkout -- src/main/resources/logback-spring.xml

파일들이 너무 많아 이렇게 하기가 불편하다면 아래와 같이 할 수도 있습니다.

# Revert changes to modified files.

git reset --hard


# Remove all untracked files and directories.

# '-f' is force, '-d' is remove directories.

git clean -fd

출처 : https://stackoverflow.com/questions/5807137/how-to-revert-uncommitted-changes-including-files-and-folders


4. Force push

keichee$ git push -f

Total 0 (delta 0), reused 0 (delta 0)

remote: 

remote: Create pull request for test-revert:

remote:   https://git.repository/compare/commits?sourceBranch=refs/heads/test-revert

remote: 

To https://git.repository

 + 61495b45b...1c74ca53e test-revert -> test-revert (forced update)



이상입니다.

오늘도 즐코딩하세요~


Converting timestamp to date time string format and vice versa

 

실제 업무에서 java로 프로그래밍 할 때 날짜/시간을 다뤄야 할 때가 참 많습니다.

데이터 업데이트 시간을 기록하거나 로깅을 하거나 등등 말이죠.

그래서 오늘은 시간 변환에 대해서 짧게 한가지만 알려드립니다.

자바 프로그래밍을 시작한지 얼마 안되었을 때 System.currentTimemillis()를 이용해서 내가 작성한 알고리즘이 얼마나 빨리 돌아가는지 확인을 했었는데요, 저 함수의 결과는 long형입니다. 

1970년 1월 1일 UTC 자정 이후로 몇 밀리초가 지났는지를 반환해주죠.

반환값에 대한 정확한 정의는 아래와 같습니다.

   * @return  the difference, measured in milliseconds, between
   *          the current time and midnight, January 1, 1970 UTC.

 

그런데 이 long형 숫자는 길이도 길고 이게 도대체 그래서 몇 일 몇 시 라는 건지를 알아보기가 힘들죠

그래서 이 long형 숫자를 가독성있게 우리가 일반적으로 사용하는 시간 포맷에 맞게 String 타입으로 변환하려면 어떻게 해야 할까요? 그리고 그렇게 얻은 String 타입 시간값을 다시 long형으로 변환하려면 또 어떻게 해야 할까요?

 

우선 long타입 시간을 원하는 일시 포맷으로 변경하는 것은 Date와 SimpleDateFormat을 이용하면 쉽게 변환할 수 있습니다. 아래와 같이 말이죠.

 public static void main(String[] args) {
     Date d = new Date(vv);
     DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
     String bb = format.format(d);
     System.out.println(bb);
 }

 

반대로 String으로 된 일시를 long으로 변환할 때는 아래와 같이 java.sql.Timestamp를 이용하면 쉽게 변환이 가능합니다.

public static void main(String[] args) {
    String prdt = "2020-01-10 10:38:09.419";
    long vv = Timestamp.valueOf(prdt).getTime();
    System.out.println(vv);
}

 

하지만 Timestamp.valueOf(String) 메서드는 "yyyy-mm-dd hh:mm:ss[.fffffffff]" 형태의 string만 입력 가능합니다.

일자만 있거나 시간값만 있으면 사용이 불가능 하다는 것 명심하세요.

만약 일시 포맷을 잘못 입력하면 아래와 같은 오류가 발생합니다.

Exception in thread "main" java.lang.IllegalArgumentException: 
		Timestamp format must be yyyy-mm-dd hh:mm:ss[.fffffffff]

 

java.sql.Timestamp 대신에 LocalDateTime을 이용할 수도 있습니다.

public static void main(String[] args) {

    String prdt2 = "2020-01-11 00:00:00";
    System.out.println(Timestamp.valueOf(prdt2).getTime());
    System.out.println(LocalDateTime.parse("2020-01-11T00:00:00")
        .atZone(ZoneId.of("Asia/Seoul"))
        .toInstant()
        .toEpochMilli());
}

---출력결과---
1578668400000
1578668400000

 

하지만 LocalDateTime의 경우 parse할 때 들어가는 string형태의 일시값에 'T'구분자를 포함해줘야 합니다.

그렇지 않을 경우 아래와 같이 파싱오류가 발생합니다.

Exception in thread "main" java.time.format.DateTimeParseException: 
		Text '2020-01-11 00:00:00' could not be parsed at index 10

 

 

Summary

이번 포스팅에서는 자바에서 long타입의 timestampe를 String 타입의 가독성 좋은 형태로 변환하는 작업과 그렇게 변환된 String을 다시 long으로 변경하는 부분에 대해서 알아보았습니다.

long -> String -> long 변경을 했을 때 동일한 값이 나오는지도 아래 테스트를 통해서 확인할 수 있었습니다.

public static void main(String[] args) {

    // String to long conversion 1
    String prdt = "2020-01-11 10:24:09.419";
    long l1 = Timestamp.valueOf(prdt).getTime();
    System.out.println(l1);

    // String to long conversion 2
    String prdt2 = "2020-01-11T10:24:09.419";
    long l2 = LocalDateTime.parse(prdt2)
        .atZone(ZoneId.of("Asia/Seoul"))
        .toInstant()
        .toEpochMilli();
    System.out.println(l2);

    // long to String conversion
    DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    System.out.println(format.format(new Date(l1)));
}

---출력결과---
1578705849419
1578705849419 <-- Timestamp를 이용해서 얻은 결과와 동일함을 확인 
2020-01-11 10:24:09.419 <-- long으로 변환했던 String 형태의 시간과 동일하게 출력되는지 확인

 

이상으로 Java에서 datetime을 long에서 String으로 다시 String에서 long으로 변환하는 방법에 대해서 알아보았습니다.

 

유용했다면 공감 꾹 부탁해요~

 

💻 Programming

[AWS DynamoDB] Conditional Check Failed Exception

현재 AWS 다이나모 DB를 이용해서 작업하는 부분이 있는데 

레가시 코드의 dynamoDB에 저장하는 부분에서 

ConditionalCheckFailedException 예외를 잡아서 debug 로깅을 하고 있는 부분이 있었다. 

 

이걸 왜 error 레벨로 로깅하지 않고 debug 레벨로 로깅을 하고 무시하고 있을까? 

실제로 얼마나 해당 로그가 남는지 확인해보니 매일 같이 10번 이상 발생하고 있었다. 

우선 throughput은 전혀 상관이 없어보였다.

stackoverflow를 찾아봐도 딱히 시원한 답을 얻을 수는 없었는데

그러다가 AWS 공식 문서를 찾게 되었다.

AWS DynamoDB Conditional Constraints

 

키포인트 한줄만 발췌해보면 아래와 같다.

You could specify a version attribute for your mapped objects, and the mapper would automatically apply conditional constraints to give you optimistic locking, but you couldn’t explicitly specify your own custom conditional constraints with the mapper.

 

위 페이지를 읽어보면 

dynamo DB는 기본적으로 version을 기준으로 logical condition을 검사한 뒤 

테이블에 데이터를 저장한다고 나와있다. 

실제 legacy코드에도 기존 데이터를 조회해서 version을 읽어와서 

새 데이터 입력할 때 세팅해주고 있었다. 

ok 그럼 뭔가 version충돌로 인해 데이터를 저장하지 못하고 있다는 추측을 해볼 수 있었다. 

왜 충돌이 날까? 

분산시스템에서 동일한 데이터의 업데이트 요청을 동시에 여러 개 받게 되면?

dynamo 테이블의 동일한 데이터를 동시에 업데이트를 하려고 시도를 하게 될 텐데,

실제로 업데이트 하기 전에 기존 version을 조회해와서 신규 데이터에 version 세팅을 해주고

업데이트를 시도하는데 처음 업데이트 시도는 성공! (이때 해당 데이터는 version이 올라가게 된다)

그 이후는 version충돌로 ConditionalCheckFailedException 예외가 발생하게 되는 것이다.

 

 

 

 

 

이클립스에서 프로젝트 import를 해보겠습니다.

import를 한다는 것은 불러오기를 한다는 것입니다.

당연히 기존에 생성했던 프로젝트나 파일이 있어야겠죠

이번 시연에 사용된 이클립스 버전은 oomph 2019-06 (4.12.0)입니다.

이클립스 버전


프로젝트 불러오기는 아래 두 가지 방법이 있습니다.

1. 이클립스 좌측의 package explorer에서 Import projects...를 선택

2. 이클립스 도구메뉴(상단메뉴)의 File > Import 를 선택

패키지탐색기

이클립스 파일 메뉴


어떤 방법을 선택하던지 동일한 아래 창이 뜨게 됩니다.

프로젝트 불러오기 팝업창 - 종류선택

위 창에서 내가 만들었던 프로젝트의 종류를 선택하면 됩니다.

여기서는 Existing Projects into Workspace를 선택하겠습니다.

이렇게 할 경우 현재 workspace가 아닌 

다른 경로에 위치한 프로젝트를 현재 workspace로 가져오게 됩니다.


프로젝트 불러오기 팝업창 - 경로탐색

여기서는 프로젝트의 경로를 선택하면 됩니다.


프로젝트 경로 선택창

import할 프로젝트의 경로를 찾아서 프로젝트 디렉토리를 선택해줍니다.

저는 SampleProject라는 프로젝트를 선택하고 Open을 클릭하였습니다.


프로젝트 불러오기 팝업창 3

프로젝트의 디렉토리 경로가 표시됨가 동시에 

Projects 목록에 SampleProject가 표시되었습니다.

이제 Finish 버튼을 클릭합니다.


프로젝트 불러오기 완료 모습

Package Explorer에 위와 같이 프로젝트가 import되었습니다.


이상으로 이클립스 IDE에서 프로젝트 불러오기를 해보았습니다.




스프링 프로젝트에서 logback을 이용하여 환경별로 로그 설정을 다르게 할 때 

<if>, <elseif> 등의 태그를 이용할 수 있는 줄 알고 아래처럼 설정을 해보았다.

<if condition="property('myproject.profile').equals('live')">
<logger name="com.myproject" level="debug">
<appender-ref ref="STDOUT"/>
</logger>
<logger name="org.springframework" level="WARN"/>
<root level="info"/>
</if>
<elseif condition="property('myproject.profile').equals('dev')">
<logger name="com.myproject" level="debug">
<appender-ref ref="STDOUT"/>
</logger>
<logger name="org.springframework" level="WARN"/>
<root level="info"/>
</elseif>
<else condition="property('myproject.profile').equals('local')">
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</else>

위 처럼 설정했는데 앱 기동 시 아래처럼 logback에서 에러로그를 찍었다.


14:46:24,662 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [STDOUT]

14:46:24,668 |-WARN in ch.qos.logback.core.ConsoleAppender[STDOUT] - This appender no longer admits a layout as a sub-component, set an encoder instead.

14:46:24,668 |-WARN in ch.qos.logback.core.ConsoleAppender[STDOUT] - To ensure compatibility, wrapping your layout in LayoutWrappingEncoder.

14:46:24,668 |-WARN in ch.qos.logback.core.ConsoleAppender[STDOUT] - See also http://logback.qos.ch/codes.html#layoutInsteadOfEncoder for details

14:46:24,673 |-ERROR in ch.qos.logback.core.joran.conditional.IfAction - Could not find Janino library on the class path. Skipping conditional processing.

14:46:24,673 |-ERROR in ch.qos.logback.core.joran.conditional.IfAction - See also http://logback.qos.ch/codes.html#ifJanino

14:46:24,673 |-ERROR in ch.qos.logback.core.joran.spi.Interpreter@31:64 - no applicable action for [logger], current ElementPath  is [[configuration][if][logger]]

14:46:24,674 |-ERROR in ch.qos.logback.core.joran.spi.Interpreter@32:42 - no applicable action for [appender-ref], current ElementPath  is [[configuration][if][logger][appender-ref]]


원인은 명확해보였다. Janino라는 라이브러리가 없어서였다.

<if>, <elseif> 등의 태그문법을 Janino 라이브러리에서 해석을 해주는데 해당 라이브러리가 없어서 logback 설정파일해석을 제대로 못한 상태였다.

그래서 현재일자 가장 최신버전으로 의존성을 추가했다.

<dependency>
<groupId>org.codehaus.janino</groupId>
<artifactId>janino</artifactId>
<version>3.0.14</version>
</dependency>


그런데 또 오류가 났다 ㅠㅠ

 Failed to parse condition [property('myproject.profile').equals('live')] org.codehaus.commons.compiler.CompileException: Line 1, Column 45: Closing single quote missing

at org.codehaus.commons.compiler.CompileException: Line 1, Column 45: Closing single quote missing


조건으로 준게 파싱오류가 났단다. 그래서 컨디션을 아래처럼 바꾸었다.

<if condition='property("myproject.profile").equals("local")'>

쌍따옴표와 홑따옴표를 바꾼거다.

그리고 if-else if - else 를 하기위해선 nested if를 써야했다.

하지만 nested <if> 를 쓰기엔 안쪽으로 인덴트 되는 게 많아져서 가독성이 떨어진다.

어차피 조건을 equals로 비교하고 있으니 

결과적으로 if문만 써서 profile값을 비교해주어도 if-elseif의 효과는 볼 수 있으므로 아래처럼 바꿨다.

<if condition='property("myproject.profile").equals("live")'>
<then>
<logger name="com.myproject" level="debug">
<appender-ref ref="ELASTIC"/>
</logger>
<logger name="org.springframework" level="WARN"/>
<root level="info"/>
</then>
</if>
<if condition='property("myproject.profile").equals("dev")'>
<then>
<logger name="com.myproject" level="debug">
<appender-ref ref="ELASTIC"/>
</logger>
<logger name="org.springframework" level="WARN"/>
<root level="info"/>
</then>
</if>
<if condition='property("myproject.profile").equals("local")'>
<then>
<logger name="com.myproject" level="debug">
<appender-ref ref="STDOUT"/>
</logger>
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
</then>
</if>

이제 잘 동작한다.


java.lang.NoSuchMethodError: com.fasterxml.jackson.core.JsonStreamContext.<init>(II)V

위 오류는 jackson-core 버전과 jackson-bind 버전이 서로 다를 경우 발생할 가능성이 높다.

오류발생원인 

메이븐 프로젝트에서 일부 의존성(dependency)을 신규로 추가했을 때, 해당 dependency가 jackson-core의 2.8.0버전을 포함하고있었다. 기존에 jackson-core, jackson-databind 버전을 2.9.0 을 사용하도록 의존성을 관리하고 있었는데 신규 추가 의존성이 jackson-core 2.8.0 버전을 포함하고있어 앱 기동 시 2.8.0버전이 물려올라가면서 발생하였다.

오류해결방법

신규로 추가한 의존성에 아래와 같이 exclusion 처리함.

<dependency>
<groupId>com.internetitem</groupId>
<artifactId>logback-elasticsearch-appender</artifactId>
<version>1.6</version>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</exclusion>
</exclusions>
</dependency>


참고자료 : 깃헙 jackson-core 이슈

 

둥근 토글 버튼

본 포스팅에서는 html, css, javascript를 이용하여 둥근 toggle버튼을 만들고, 버튼의 상태가 변경될 때마다 상태를 출력하는 기능까지 만들어 봅니다. 본 보스팅에 사용되는 기본코드는 w3schools에서 가지고 온 것입니다. w3schools에서는 단순히 css를 이용해서 토글버튼처럼 보이는 것을 만드는 것 까지만 보여주었는데, 저는 그렇게 토글이 될 때마다 자바스크립트를 이용해서 어떤 기능이 실행되는 부분까지 확장해서 포스팅합니다.

 

우선 toggle.html 파일에 input tag를 이용해서 아래와 같이 작성해줍니다.

<!DOCTYPE html>
<html>

<head>
</head>

<body>
    <label class="switch">
        <input type="checkbox" />
        <span class="slider round"></span>
    </label>
</body>

</html>

 

그리고 toggle.css 파일을 하나 만들어서 그 안에 아래와 같이 작성을 해줍니다.

/* 슬라이더 외부를 감싸는 라벨에 대한 스타일 */
.switch {
  position: relative;
  display: inline-block;
  width: 60px;
  height: 34px;
}

/* HTML 기본 체크박스 숨기기 */
.switch input {
  opacity: 0;
  width: 0;
  height: 0;
}

/* 슬라이더 - 실제로 토글될 부분 */
.slider {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: #ccc;
  -webkit-transition: .4s;
  transition: .4s;
}

.slider:before {
  position: absolute;
  content: "";
  height: 26px;
  width: 26px;
  left: 4px;
  bottom: 4px;
  background-color: white;
  -webkit-transition: .4s;
  transition: .4s;
}

input:checked + .slider {
  background-color: #2196F3;
}

input:focus + .slider {
  box-shadow: 0 0 1px #2196F3;
}

input:checked + .slider:before {
  -webkit-transform: translateX(26px);
  -ms-transform: translateX(26px);
  transform: translateX(26px);
}

/* 슬라이더를 동그랗게 보여주기 위한 부분 */
.slider.round {
  border-radius: 34px;
}

.slider.round:before {
  border-radius: 50%;
}

 

이제 HTML 파일의 <head></head> 부분에 위 css 파일을 링크시켜줍니다.

<head>
	<link rel="stylesheet" type="text/css" href="toggle.css" />
</head>

 

이제 toggle.html 파일을 더블클릭해서 브라우저에서 열어보면 아래 영상처럼 움직이는 것을 확인할 수 있습니다.

 

둥근 토글 버튼 움직이는 영상

 

자, 이제 여기에 자바스크립트를 이용하여 기능을 추가해보도록 할건데요,

이 토글버튼은 input 태그의 checkbox를 이용한 것이므로 토글이 될 때마다 checked 속성이 변경되도록 되어있습니다.

이 기본적인 내용을 기억하고 자바스크립트에서 토글버튼의 checked 속성을 기반으로 특정 문자열을 출력하도록 해보겠습니다.

자바스크립트 파일은 따로 만들지 않고 그냥 html 파일에 추가하도록 할게요.

우선, input 태그에 onclick 속성을 이용하여 toggle이라는 함수를 호출하도록 하고, toggle 함수를 구현하겠습니다.

아래처럼 input  태그의 onclick 속성을 넣어주세요. 

<label class="switch">
    <input type="checkbox" onclick="toggle(this)">
    <span class="slider round"></span>
</label>

이렇게 해주면 토글버튼(체크박스)가 클릭될 때마다 toggle이라는 함수를 호출하면서 자기자신을 파라미터로 넘겨주게 됩니다.

 

자, 이제 body 아래쪽에 자바스크립트를 이용하여 스크립트를 작성해보겠습니다.

<script>
    function toggle(element) {
        console.log(element.checked);
    }
</script>

위 코드는 <head></head> 에 위치해도 되고 <body></body> 사이에 위치해도 됩니다.

당연히 별도 파일로 분리하여 작성해도 됩니다.

분리하여 작성하는 것과 관련해서는 제가 작성한 다른 포스트에서 확인 가능합니다. -> https://keichee.tistory.com/356

저는 <body></body> 사이에 넣어놓았습니다.

<body>
    <label class="switch">
        <input type="checkbox" onclick="toggle(this)">
        <span class="slider round"></span>
    </label>

    <script>
        function toggle(element) {
            console.log(element.checked);
        }
    </script>
</body>

 

자, 여기까지 html 파일 전체 코드가 어떻게 되는지 다시 한번 보여드릴게요.

<!DOCTYPE html>
<html>

<head>
    <link rel="stylesheet" type="text/css" href="toggle.css" />
</head>

<body>
    <label class="switch">
        <input type="checkbox" onclick="toggle(this)">
        <span class="slider round"></span>
    </label>

    <script>
        function toggle(element) {
            console.log(element.checked);
        }
    </script>
</body>

</html>

 

여기까지 완성하여 실행하면 체크박스(토글버튼)을 클릭할 때마다 toggle함수가 실행되면서 현재 체크박스의 상태가 어떻게 바뀌었는지를 출력하게 됩니다.

확인을 위해서 브라우저에서 toggle.html 파일을 새로고침하여 다시 열어주시고,

화면에서 우클릭 > 검사 (inspect)를 선택하여 개발자도구(developer tools)를 열어서 console탭을 열어주세요.

그리고 버튼을 누를때마다 어떤 값이 출력이 되는지 확인해보도록 하겠습니다.

 

토글 버튼 동작 영상

 

자, 이렇게 자바스크립트를 이용해서 토글버튼의 상태값을 출력해보았습니다.

어떤가요? 별로 어렵지 않죠? css 스타일을 해석하기 힘드실 순 있겠으나, 여기서 그런 부분을 자세하게 다루진 않겠습니다.

좀 더 나아가서 토글버튼의 상태값에 따라 ajax 요청을 서버에 날려 실제 DB를 업데이트하거나, 

또는 html의 특정 element를 hide하거나 show하는 등의 기능을 만들어 보세요.

 

그럼 오늘도 즐거운 코딩하시길 바래요 ^-^

 

- 깐깐한 개발자 -

웹사이트를 만들다보면 HTML파일 크기가 커지기 마련입니다.

웹사이트가 dynamic한 동적 사이트라면 더더욱 그럴 가능성이 커집니다.

이런 저런 기능을 자바스크립트로 구현을 하다보면 주체할 수 없이 커지는 html 파일을 볼 수도 있는데요

이럴 때는 자바스크립트 부분을 별도의 .js 파일로 분리한 뒤 html 파일에 링크를 걸어서

마치 html 파일에 자바스크립트를 직접 작성한 것처럼 사용할 수 있습니다.

 

방법도 매우 간단합니다.

아래 html 예제를 먼저 보도록 하겠습니다.

<!DOCTYPE html>
<html>

<head>
    <link rel="stylesheet" type="text/css" href="toggle.css" />
</head>

<body>
    <label class="switch">
        <input type="checkbox" onclick="toggle(this)">
        <span class="slider round"></span>
    </label>

    <script>
        function toggle(element) {
            console.log(element.checked);
        }
    </script>
</body>

</html>

위 코드는 토글버튼 만들기 시간에 사용했던 코드입니다.

위 코드에서 <script> 태그로 감싼 부분을 .js 파일로 분리해내고 해당 파일을 html파일에 링크(import, include라고 얘기하기도 함)를 걸어서 기능이 정상적으로 돌아가도록 해보겠습니다.

우선 html 문서의 <script> 태그 내에 있던 내용을 복사하여 toggle.js 파일을 만들어 넣어줍니다.

function toggle(element) {
    console.log(element.checked);
}

이제 <head>태그 내에 아래와 같이 toggle.js 파일을 연결시켜주고, <body>안에 있던 script는 제거합니다.

<!DOCTYPE html>
<html>

<head>
    <link rel="stylesheet" type="text/css" href="toggle.css" />
    <script src="toggle.js"></script>
</head>

<body>
    <label class="switch">
        <input type="checkbox" onclick="toggle(this)">
        <span class="slider round"></span>
    </label>
</body>

</html>

<script> 태그가 사라지니 화면에 그림을 그려주는 요소들만 <body>에 남아있게 되었습니다.

위 코드 자체가 워낙 작은 코드라 깔끔해졌다는 느낌을 받지 못할 수 있으나, 

위 처럼 짧은 html 문서는 테스트용밖에 없을 것입니다. 

개발자가 되고싶다면 항상 .js 파일을 분리하여 링크걸어 사용하길 추천합니다.

 

뭔가 길게 설명드렸지만, html파일에 자바스크립트 파일을 연결/링크/import/include 시키는 방법은 <head>태그 내에 아래와 같이 한 줄만 추가해주시면 됩니다.

<script src="toggle.js"></script>

여기서 .js 파일의 위치는 html 파일의 위치에서 상대경로로 지정해주시면 됩니다.

또는 웹상에 있는 파일일 경우 URL 주소가 들어올 수도 있으니 참고하시기 바랍니다.