분류 전체보기 (356)

💻 Programming

AWS Elasticsearch ISM을 활용한 인덱스 자동삭제

안녕하세요 오랜만에 포스팅을 하게 되었습니다.

 

한동안 노가다성 작업만 하다보니 딱히 포스팅할 만한 내용이 없었네요.

 

오늘은 AWS Elasticsearch(이하 ES)의 인덱스를 자동으로 삭제하는 방법에 대해서 알려드립니다.

 

저는 AWS ES를 로깅을 위해서 사용하고 있습니다. 로그 검색 속도가 빨라 디버깅시 원인파악을 빠르게 할 수 있어서 좋죠. 특정 트래킹 ID로 특정 프로세스의 로그들을 하나로 묶어주면 해당 프로세스의 로그만 검색이 가능하여 편리합니다.

 

아무튼, 이렇게 로그를 쌓기 위해서 사용하는 ES가 용량이 부족해지면 별다른 에러나 알람을 주지않고 더이상 로그가 쌓이지 않게되는 현상이 있습니다. 운영 환경에서는 그런 일이 없도록 하기 위해서 큐레이터 설정을 하여 사용중입니다. 이 설정은 인프라 전문팀과 논의하여 설정을 했었습니다. 큐레이터를 이용하려면 좀 귀찮은 작업들이 수반됩니다. 그래서 개발환경에서는 큐레이터 설정 없이 수동으로 가끔씩 인덱스를 삭제해주곤 했죠. ES 버전이 올라가면서 이제는 키바나에서 간단히 설정해주면 자동으로 특정 기간이 지난 인덱스를 삭제할 수 있게끔 되었습니다. 참고로 오리지날 ES에서는 이 기능을 ILM (Index Lifecycle Management) 기능이라고 부르는데 AWS에서는 ISM (Index State Management)라고 합니다.

 

우선 제가 사용중인 AWS ES 버전은 7.9 입니다. 동일한 7.9 버전이라도 패치버전에 따라 키바나 메뉴가 조금 상이합니다.

상이한 메뉴는 각각 스샷첨부드리겠습니다.

 

이제부터 우리가 할 것은 30일이 지난 인덱스를 자동으로 삭제하는 설정을 하는 것입니다.

작업은 3스텝으로 진행합니다.

  1. ISM 정책 생성
  2. 기존 인덱스들에 생성한 정책 적용
  3. 신규 인덱스들에 정책 적용

 

우선 키바나에 접속해서 메뉴를 살펴보겠습니다.

구 버전(좌) 메뉴와 최신 버전(우) 메뉴

 좌측은 패치버전이 구버전인 케이스이고, 우측이 최신버전 (게시글 작성일 기준 R20210331) 패치를 적용한 키바나 메뉴입니다.

 

여기서 Index Management 메뉴를 선택하여 들어갑니다.

그러면 또 메뉴가 아래처럼 다르게 나옵니다.

구 버전(좌) 메뉴와 최신 버전(우) 메뉴

좌측 처럼 메뉴가 나올 경우 Index Policies 메뉴를 선택, 우측 처럼 메뉴가 나올 경우 State management policies 메뉴를 선택합니다.

 

그 다음부터는 거의 동일합니다.

구 버전 스샷
신 버전 스샷

제목만 다를 뿐 내용은 모두 동일합니다. 여기서 create policy를 선택합니다.

 

Name policy 에는 원하는 이름을 명명해주시면 됩니다. 저는 30일이 지난 인덱스를 삭제한다는 의미로 delete_old_indexes_30d라고 명명했습니다. 그리고 define policy 에는 아래 json 포멧 데이터를 적어주시면 됩니다.

{
    "policy": {
        "policy_id": "delete_old_indexes_30d",
        "description": "delete old indexes",
        "default_state": "hot",
        "states": [
            {
                "name": "hot",
                "actions": [],
                "transitions": [
                    {
                        "state_name": "delete",
                        "conditions": {
                            "min_index_age": "30d"
                        }
                    }
                ]
            },
            {
                "name": "delete",
                "actions": [
                    {
                        "delete": {}
                    }
                ],
                "transitions": []
            }
        ]
    }
}

지금 우리는 인덱스 상태를 관리하는 설정을 하는 것입니다. 따라서 policy 내에 상태에 대한 정의를 해주고 있습니다.

policy_id는 Name policy에 명명해주었던 이름을 그대로 사용하고, default_state 를 hot 이라는 상태로 설정해주었습니다.

여기서 우리는 두 가지 상태를 정의해서 사용하고 있습니다. hot 과 delete.

인덱스의 기본상태로는 hot을 정의를 해주었고 그 상태의 인덱스들은 transitions에 설정되어있는 조건에 따라 상태를 변경하게 됩니다. 즉 max_index_age가 30d (30일)이 지나면 상태를 delete로 변경(transition)하게 되죠. 그렇게 delete 상태가 된 인덱스들은 delete 상태 정의의 actions에 있는 delete 설정으로 인해 삭제되게 됩니다.

 

이제 ISM 정책 생성을 완료하였습니다.

이렇게 정책을 생성한 뒤에는 기존 인덱스들에 대해서 해당 정책을 적용해주어야 합니다.

정책을 저장한 뒤에 좌측 메뉴에서 Indices 로 들어가보면 현재 인덱스들 목록이 출력됩니다.

인덱스 목록

목록에서 하나 이상의 인덱스를 선택하면 우상단의 Apply policy 버튼이 활성화 됩니다. 버튼을 클릭하면 정책이 반영되고 새로고침을 해보시면 Managed by Policy 값이 No 에서 Yes로 변경됩니다. .kibana 인덱스처럼 잘 모르면 건들지 말아야하는 인덱스들도 여기 목록에 조회가 되기 때문에 전체 선택하여 정책을 적용시킬 때에는 그런 인덱스들이 포함되어있지 않은지 잘 확인하셔야 합니다. 만약 잘못 적용했다면 적용 취소도 가능합니다.

정책을 적용한 인덱스들을 여기서는 Managed Index라고 부릅니다. 따라서 적용을 취소하려고 할 때에는 Managed Indices 또는 Policy managed indices 메뉴로 들어가서 인덱스를 조회하여 처리합니다.

구 버전 화면
최신 버전 화면 (R20210331)

인덱스를 선택하고 Remove policy(삭제) 하거나 Change policy(변경)이 가능합니다.

 

이상으로 AWS ES에서 ISM을 활용하여 인덱스 자동삭제 정책을 생성 및 적용하는 방법을 알아보았습니다.

 

 

 

궁금한 점이나 잘못된 내용이 있다면 언제든지 댓글달아주세요~

 

감사합니다.

💻 Programming

SpringBoot 2 http response utf-8 설정하기

스프링부트 1 을 사용하던 엔터프라이즈 앱을 MSA 적용 작업을 통해 여러 앱으로 분리하면서 스프링부트 2로 업그레이드를 진행했습니다.

이 과정에서 다른 회사와 API 연동하고 있던 부분에서 인코딩 문제가 발생했습니다.

소스 이전을 진행하면서 빼먹은 부분이 있어서였죠.

이슈가 발생한 회사측에서 원인으로 얘기한 것은 응답 헤더의 Content-Type에 utf-8 인코딩 설정이 빠져있다는 것이었습니다.

 

그래서 확인해보니 스프링부트 1 기반 앱에서는 아래와 같은 설정이 있었습니다.

	@Bean
	public HttpMessageConverter<String> responseBodyConverter() {
		return new StringHttpMessageConverter(Charset.forName("UTF-8"));
	}

	@Bean
	public Filter characterEncodingFilter() {
		CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
		characterEncodingFilter.setEncoding("UTF-8");
		characterEncodingFilter.setForceEncoding(true);
		return characterEncodingFilter;
	}

그리고 테스트 결과 응답헤더의 Content-Type에 application/json;charset=utf-8 이라고 되어있는 것을 확인할 수 있었습니다.

하지만 신규 앱에서는 charset 정보가 빠져있었습니다.

 

그래서 위 설정을 그대로 스프링부트 2 기반의 신규로 개발한 앱쪽에 넣으려 했더니 앱 기동 시 오류가 발생했습니다.

***************************
APPLICATION FAILED TO START
***************************

Description:

The bean 'characterEncodingFilter', defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/HttpEncodingAutoConfiguration.class], could not be registered. A bean with that name has already been defined in com.yanolja.affiliate.out.api.AffiliateOutApiApplication and overriding is disabled.

Action:

Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

이미 동일한 이름의 빈이 존재하니 오버라이드 할 수 있도록 설정을 변경하라는 얘기였죠.

근데 생각해보니 굳이 이렇게 해야하나 라는 생각이 들어 좀 찾아보니,

자바 소스에 저렇게 추가하지 않고 스프링 설정 만으로도 헤더의 인코딩 설정은 가능했습니다.

바로 server.servlet.encoding.charset 설정을 추가해주면 되었습니다. 

하위 버전에서는 이 설정이 spring.http.encoding.charset 으로 사용되었었습니다.

 

자바 소스에서 characterEncodingFilter 빈 생성로직을 제거하고, application.yml 파일에 아래와 같이 설정을 추가합니다.

server:
  servlet:
    encoding:
      charset: UTF-8

 

그리고 앱을 재부팅 해주면 응답헤더의 Content-Type 값이 application/json;charset=utf-8 으로 변경된 것을 확인할 수 있습니다.

만약 값이 바뀌지 않는다면 server.servlet.encoding.force-response 또는 server.servlet.encoding.force 설정값을 true로 설정해서 다시 해보시기 바랍니다.

 

 

참고문서: docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#server.servlet.encoding.charset 

자바에서 비동기 호출하기 (AsyncRestTemplate, ListenableFuture)

안녕하세요, 최근에 관리자용 앱을 개선하다가 비동기 처리에 대해서 공유하고 싶은 것이 있어 포스팅을 작성합니다.

제가 관리중인 관리자용 앱은 Spring Framework 4, Java 8, Bootstrap 3 기반으로 작성되어있습니다.

오래된 라이브러리들을 업그레이드 하고 싶지만 메인 업무가 아니다보니 짬날때 조금씩 업그레이드 해주고 있는데 부트스트랩이나 스프링 버전을 올리기엔 작업량이 많아보여서 안건드리고 있네요.

 

아무튼 자바에서 비동기로 호출을 하는 방법은 크게 두 가지가 있는 것 같습니다.

 

첫 번째는 callee 메서드 쪽에서 @Async 를 붙여서 비동기로 동작하게 하는 방법이 있고요

이 방법은 callee가 외부 팀이거나 외부 회사일 경우 불가능하거나 일이 빨리 진행하기 어려울 수 있습니다.

굳이 특별한 이유가 있지 않은이상 callee쪽에 요청을 할 필요는 전혀 없습니다.

하지만 callee가 내부에 있다면 아키텍처 구조상 이 방법을 선택할 수도 있습니다.

 

두 번째는 caller가 호출방식을 직접 비동기로 호출하는 방법입니다.

callee가 어떻게 처리하던지 caller는 신경쓸 필요가 없죠. 

단지 응답만 잘 받아서 처리해주면 됩니다.

 

오늘 소개해 드릴 내용은 두 번째 방식인 caller 입장에서 비동기로 호출하는 방법입니다.

스프링을 사용하신다면 API 호출하실 때 RestTemplate을 사용하실 겁니다.

 

근데 스프링에서는 비동기 호출을 위해서 이미 AsyncRestTemplate 을 제공하고 있습니다.

AsyncRestTemplate 을 이용해서 API를 호출하는 방법은 RestTemplate과 다를게 없죠.

그리고 당연히도 응답을 기다리지 않고 다음 프로세스를 처리하게 됩니다.

그리고 그 응답은 ListenableFuture<ResponseEntity<T>> 타입으로 받습니다.

간단한 예제를 한번 보겠습니다.

 

    // Autowired로 주입된 상태라 가정합니다.
    private final AsyncRestTemplate asyncRestTemplate;
	
    public void test() {
        URI uri = UriComponentsBuilder.fromUriString("http://localhost")
                .path("/test")
                .build()
                .toUri();

        HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");

        HttpEntity<String> requestEntity = new HttpEntity<>(headers);
        HttpMethod httpMethod = HttpMethod.POST;

        ListenableFuture<ResponseEntity<Void>> res = 
        	asyncRestTemplate.exchange(uri, httpMethod, requestEntity, Void.class);
        log.info("호출 성공!!");
    }

위 코드는 http://localhost/test 를 POST 방식으로 호출하고 있습니다. 헤더에 application/json;charset=UTF-8 정보도 넣어주고 있고요 응답메시지에 대해서는 Void로 처리하고 있습니다. 어떤 메시지를 응답으로 주던지 신경안쓴다는 거겠죠.

그리고 호출에 대한 결과를 ListenableFuture<ResponseEntity<Void>> res 에 받게 됩니다.

하지만 응답을 받기 전에 호출하자마자 호출 성공!! 로그를 출력하게 됩니다.

 

그러면 그렇게 받은 응답의 결과에 따라 정상과 실패를 구분해서 예외처리를 해줘야 할텐데 그건 어떻게 할까요?

res 에 callback 함수를 넣어주면 됩니다.

콜백을 추가한 예제를 보겠습니다.

 

    // Autowired로 주입된 상태라 가정합니다.
    private final AsyncRestTemplate asyncRestTemplate;

    public void test() {
        URI uri = UriComponentsBuilder.fromUriString("http://localhost")
                .path("/test")
                .build()
                .toUri();

        HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");

        HttpEntity<String> requestEntity = new HttpEntity<>(headers);
        HttpMethod httpMethod = HttpMethod.POST;

        ListenableFuture<ResponseEntity<Void>> res = asyncRestTemplate.exchange(uri, httpMethod, requestEntity, Void.class);
        res.addCallback(new ListenableFutureCallback<ResponseEntity<Void>>() {
            @Override
            public void onFailure(Throwable th) {
                log.error("실패");
            }

            @Override
            public void onSuccess(ResponseEntity<Void> voidResponseEntity) {
                if (HttpUtil.isNot2xxSuccessful(voidResponseEntity)) {
                    log.error("실패, 응답코드:{}", voidResponseEntity.getStatusCodeValue();
                } else {
                    log.info("성공");
                    // TODO: 추가 작업
                }
            }
        });

    }

여기서는 annonymous ListenableFutureCallback 클래스를 만들어서 addCallback 메서드에 넘겨주었습니다.

onFailure 와 onSuccess 메서드를 구현해주면 되는데요 이때 주의할 점이 하나 있습니다.

onSuccess 메서드가 호출되는 케이스가 2xx 성공인 케이스만이 아니라는 점입니다.

4xx 케이스도 onSuccess 메서드를 호출하게 됩니다.

onFailure는 SocketTimeout과 같은 예외가 발생하면 호출됩니다.

따라서 실질적으로 우리가 원하는 성공 은 response entity에서 응답코드로 확인해줘야 합니다.

 

참고로 위 코드에서 HttpUtil.isNot2xxSuccessful(voidResponseEntity) 메서드는 아래처럼 코딩되어 있습니다.

public class HttpUtil {

    public static boolean is2xxSuccessful(ResponseEntity responseEntity) {
        return HttpStatus.Series.valueOf(responseEntity.getStatusCodeValue())
        	.equals(HttpStatus.Series.SUCCESSFUL);
    }

    public static boolean isNot2xxSuccessful(ResponseEntity responseEntity) {
        return !is2xxSuccessful(responseEntity);
    }
}

ListenableFuture<ResponseEntity<T>> 에서 제공하는 addCallback 메서드 시그니처는 아래 두 가지가 있습니다.

 

void addCallback(ListenableFutureCallback<? super T> var1);
void addCallback(SuccessCallback<? super T> var1, FailureCallback var2);

 

위 예제에서는 첫 번째 addCallback 메서드를 사용했습니다만, 만약 SuccessCallback와 FailureCallback을 나눠서 구현하여 사용하고 싶다면 각각 구현해서 addCallback 메서드에 파라미터로 전달해주셔도 됩니다.

 

추가로 AsyncRestTemplate 을 스프링에서 제공해주는 기본 클래스를 사용해도 되지만 타임아웃 등 설정을 바꿔서 사용해야한다면 아래 코드를 참고하시면 도움이 되실겁니다.

    @Bean
    public AsyncRestTemplate asyncRestTemplate2() {
        SimpleAsyncTaskExecutor simpleAsyncTaskExecutor = new SimpleAsyncTaskExecutor();
        simpleAsyncTaskExecutor.setTaskDecorator(new MdcTaskDecorator());

        SimpleClientHttpRequestFactory simpleClientHttpRequestFactory = new SimpleClientHttpRequestFactory();
        simpleClientHttpRequestFactory.setConnectTimeout(30_000);    // 밀리초 단위
        simpleClientHttpRequestFactory.setReadTimeout(30_000);       // 밀리초 단위
        simpleClientHttpRequestFactory.setTaskExecutor(simpleAsyncTaskExecutor);

        AsyncRestTemplate asyncRestTemplate = new AsyncRestTemplate(simpleClientHttpRequestFactory);

        return asyncRestTemplate;
    }

    static class MdcTaskDecorator implements TaskDecorator {

        @Override
        public Runnable decorate(Runnable runnable) {
            // Web thread context
            Map<String, String> contextMap = MDC.getCopyOfContextMap();
            return () -> {
                try {
                    // @Async thread context
                    MDC.setContextMap(contextMap);
                    runnable.run();
                } finally {
                    MDC.clear();
                }
            };
        }
    }

 

이상으로 AsyncRestTemplate을 이용하여 자바에서 비동기 호출하기에 대해서 알아봤습니다.

 

하지만 AsyncRestTemplate은 Spring 5로 넘어가면서 deprecated 되었고, WebClient를 사용하도록 권장하고 있습니다.

Deprecated as of Spring 5.0, in favor of org.springframework.web.reactive.function.client.WebClient

WebClient에 대해서는 다음 포스팅에서 다루도록 하겠습니다.

 

오늘도 즐프행프하세요~

 

 

💻 Programming/Java

SpringBoot에서 ThreadPoolTaskExecutor 설정하기

이번 포스팅에서는 SpringBoot에서 ThreadPoolTaskExecutor를 어떻게 간단하게 설정하여 사용할 수 있는지 알려드립니다.

SpringBoot 프로젝트는 이미 구성되어 있다는 전제하에 Java11, SpringBoot 2.4.2 기준으로 작성했습니다.

참고로 ThreadPoolTaskExecutor의 설정 및 구체적인 사용법에 대해서는 이전에 포스팅한 문서를 참고하시기 바랍니다.

 

ThreadPoolTaskExecutor의 사용법

 

ThreadPoolTaskExecutor를 이용하여 비동기 처리하기(멀티쓰레드)

async(비동기) 처리를 위한 ThreadPoolTaskExecutor ThreadPoolTaskExecutor를 이용하여 비동기처리하는 방법을 알아보겠습니다. ThreadPoolTaskExecutor는 스프링에서 제공해주는 클래스로 org.springframework.s..

keichee.tistory.com

ThreadPoolTaskExecutor Bean 등록하기

우선 ThreadPoolTaskExecutor를 빈으로 등록하도록 하겠습니다. 

TaskExecutorConfig 클래스를 만들고 @Configuration 어노테이션을 추가하여 설정을 위한 클래스를 하나 만들었습니다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
public class TaskExecutorConfig {

	@Bean(name = "executor")
	public ThreadPoolTaskExecutor executor() {
		ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
		executor.setThreadNamePrefix("my-");
		return executor;
	}
}

그리고 executor() 메서드를 하나 만들어서 ThreadPoolTaskExecutor를 생성하여 반환해주도록 하고 @Bean으로 등록하면서 name 속성을 지정해주었습니다. 참고로 name 속성의 값은 메서드명과 달라고 무관합니다. 또한, 해당 executor를 이용하여 쓰레드를 실행시켰을 때 내가 만든 ThreadPoolTaskExecutor가 사용되는 것인지 확인할 수 있도록 쓰레드명prefix를 "my-"로 세팅해주었습니다. 

이전 포스팅을 읽어보셨거나 또는 ThreadPoolTaskExecutor를 직업 new 키워드로 생성해서 사용해보신 분은 눈치채셨을 수도 있을텐데요, 여기서 initialize()를 하지 않았습니다. 공식문서에는 initialize 메서드를 호출하는 것으로 나와있지만 굳이 하지 않아도 빈으로 등록될 때 initialize를 하더군요.

 

@Autowired 로 ThreadPoolTaskExecutor 사용하기

아래는 빈으로 등록한 executor를 서비스 레이어에서 autowired하여 사용하는 예제입니다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class TestService {

  @Autowired
  @Qualifier("executor")
  private ThreadPoolTaskExecutor executor;

  public void executeThreads() {
      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);
      }
  }
}

TestService라는 서비스 클래스에서 멤버변수 executor에 위에서 빈으로 등록한 객체를 쓰도록 @Qualifier로 executor 이름을 명시해주었습니다.

위 코드를 실행해보면 쓰레드 명이 my-1 로 출력되는 것을 확인하실 수 있습니다. corePoolSize를 default 값인 1 로 사용하여 싱글 쓰레드로 실행되기 때문에 my-2, my-3은 보이지 않습니다. corePoolSize 를 2 이상으로 세팅해주면 my-2, my-3 등의 쓰레드도 확인가능합니다.

2021-01-26 18:16:23.147  INFO 16390 --- [           my-1] com.keichee.test.service.TestService     : my-1, Now sleeping 10 seconds...
2021-01-26 18:16:33.150  INFO 16390 --- [           my-1] com.keichee.test.service.TestService     : my-1, Now sleeping 10 seconds...
2021-01-26 18:16:43.152  INFO 16390 --- [           my-1] com.keichee.test.service.TestService     : my-1, Now sleeping 10 seconds...
2021-01-26 18:16:53.157  INFO 16390 --- [           my-1] com.keichee.test.service.TestService     : my-1, Now sleeping 10 seconds...

 

@Async로 ThreadPoolTaskExecutor 사용하기

@Async 어노테이션을 이용하여 위에서 빈으로 등록한 ThreadPoolTaskExecutor를 사용하려면 @EnableAsync 도 추가해주어야 합니다.

따라서 TaskExecutorConfig 클래스에 @EnableAsync 어노테이션을 추가해줍니다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@EnableAsync
@Configuration
public class TaskExecutorConfig {

	@Bean(name = "executor")
	public ThreadPoolTaskExecutor executor() {
		ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
		executor.setThreadNamePrefix("my-");
		return executor;
	}
}

 

그리고 서비스 레이어의 메서드를 하나 추가하겠습니다.

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class TestService {

    @Async("executor")
    public void task() {
        try {
            log.info(Thread.currentThread().getName() + ", Now sleeping 10 seconds...");
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

이제 controller 쪽에서 task() 메서드를 loop 돌면서 호출해보면 @Autowired 를 이용한 방법과 동일하게 로그가 출력되는 것을 확인하실 수 있을 겁니다.

 

이상으로 SpringBoot에서 ThreadPoolTaskExecutor 설정하여 사용하는 방법에 대해서 알아보았습니다.

간략하게 다시 정리해보자면

 

1. @Configuration 으로 등록한 클래스에 executor @Bean 추가 (@Async 를 이용할 경우 @EnableAsync 도 추가)

2. @Autowired @Qualifier 로 주입하여 사용하거나 또는 메서드 레벨에 @Async 를 붙여 사용

 

이렇게 정리할 수 있겠네요.

 

그럼 오늘도 행복한 코딩하세요~

프로그래밍을 하다보면 DB 조회 시에 정렬해서 조회해오는 방법도 있고, 애플리케이션 레벨에서 정렬해야 할 때도 있습니다.

오늘은 애플리케이션에서 배열, 리스트, 맵, 셋에 대하여 각각 오름차순, 내림차순 정렬하는 방법에 대해서 알아보도록 하겠습니다.

1. 배열 정렬하기

우선 아래와 같이 int 배열이 있다고 가정해보겠습니다.

int[] arr = new int[] { 5, 1, 89, 255, 7, 88, 200, 123, 66 };

배열의 정렬은 java.util.Arrays 클래스에 있는 sort() 메서드를 이용하여 정렬할 수 있습니다.

이 메서드를 이용하면 모든 primitive 타입에 대한 정렬이 가능합니다. 물론 원시타입이 아닌 Object[] 도 정렬이 가능합니다.

int[] arr = new int[] { 5, 1, 89, 255, 7, 88, 200, 123, 66 };
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));

Arrays.sort(배열) 호출을 하면 인자로 넘겨준 배열을 정렬해줍니다. 

그리고 출력을 해보면 아래와 같이 잘 정렬이 된 것을 확인할 수 있습니다.

[1, 5, 7, 66, 88, 89, 123, 200, 255]

그럼 이 메서드가 내부적으로 사용하는 정렬 알고리즘은 어떤 것일까요?

Java8 문서에는 듀얼 피봇 퀵소트(Dual-Pivot Quick Sort) 알고리즘을 사용한다고 나와있습니다.

듀얼 피봇 퀵소트는 모든 데이터 셋에 대해서 O(n log(n)) 의 퍼포먼스를 보이고 일반적으로 기존의 One-Pivot 퀵소트 정렬보다 빠르다고 합니다.

Java7 문서, Java14 문서에도 동일한 알고리즘을 사용하는 것으로 나와있습니다.

1.1 부분정렬

위 예제는 배열의 전체 요소를 정렬하는 반면 배열의 일부분만 정렬을 할 수도 있습니다.

일부분만 정렬할 때는 Arrays.sort(int[] a, int fromIndex, int toIndex) 메서드를 이용하시면 됩니다.

int[] arr = new int[] { 5, 1, 89, 255, 7, 88, 200, 123, 66 };
Arrays.sort(arr, 0, 4);
System.out.println(Arrays.toString(arr));

출력을 해보면 아래와 같이 0번 인덱스부터 3번 인덱스까지(4는 exclude입니다) 정렬이 된 것을 확인할 수 있습니다.

[1, 5, 89, 255, 7, 88, 200, 123, 66]

 

1.2 병렬 정렬 (ParallelSort)

Arrays.sort()가 싱글쓰레드로 돌아간다면 Arrays.parallelSort()는 멀티쓰레드로 동작을 합니다. parallelSort는 배열을 sub-array로 나눈 뒤 각각을 별도의 쓰레드에서 처리를 하고 각 쓰레드에서는 Arrays.sort()를 실행하여 정렬을 하게 됩니다.

Arrays.parallelSort 의 결과는 당연히 Arrays.sort의 결과와 동일합니다. 멀티쓰레딩을 이용했느냐의 차이만 존재할 뿐입니다.

참고로 Arrays.parallelSort 도 부분정렬이 가능합니다. Arrays.parallelSort (int [] a, int fromIndex, int toIndex);

2. List 정렬하기

리스트를 정렬할 때는 java.util.Collections.sort(List<T> list) 메서드를 이용하는 방법이 있습니다.

int[] arr = new int[] { 5, 1, 89, 255, 7, 88, 200, 123, 66 };
List<Integer> toSortList = Ints.asList(arr);    // int[]을 Integer[]로 변환 (구글 guava 라이브러리 이용) 
Collections.sort(toSortList);

출력 결과는 Arrays.sort()를 사용한 것과 동일합니다.

기본적으로 오름차순으로 정렬을 합니다. 만약 내림차순으로 정렬하고 싶다면 comparator를 인자로 전달받는 sort​(List list, Comparator c) 메서드를 이용할 수 있습니다.

그리고 너무나도 기본적인 얘기지만 정렬을 하려면 두 요소(element)를 비교할 수 있어야 하기 때문에 리스트 내의 모든 요소들이 Comparable 인터페이스를 구현하고 있어야 합니다.

Collections.sort(List<T> list) 는 merge sort 알고리즘을 이용하여 정렬을 합니다. List.sort(Comparator c)에서 제공하는 기본 알고리즘과는 조금 다르다고 합니다만, 더 안좋게 변형된 알고리즘을 사용하는 건 아니겠죠. 그리고 기본적으로 List.sort()에서 사용하는 merge sort는 어느정도 부분 정렬이 되어있는 데이터에 대해서는 n log(n) 보다 훨씬 적은 횟수만 비교하며(거의 정렬이 되어있는 상태라면 거의 n번의 비교만으로도 정렬 가능), 랜덤하게 섞여있는 데이터 셋에 대해서는 전통적인 merge sort의 퍼포먼스를 낸다고 합니다. Java14 Collections.sort() 참고하시면 좋을 것 같습니다.

 

만약 역순(내림차순)으로 정렬을 하고 싶으면 Collections.sort(List list, Comparator c) 메서드를 이용하면 됩니다.

// 내림 차순 정렬
Collections.sort(list, Comparator.reverseOrder());

자바에서는 내림차순 정렬을 위해 Comparator.reverseOrder() 메서드를 통해 기본 comparator를 제공해주고 있습니다. 

3. Set 정렬하기

Set은 기본적으로 순서를 보장하지 않습니다. 하지만 LinkedHashSet은 순서를 보장하죠. 따라서 Set을 정렬한다는 것은 이 LinkedHashSet 처럼 순서를 보장하는 set 구현체에 대한 얘기입니다.

Set을 정렬할 때도 Collections.sort()를 이용합니다. 하지만 해당 메서드는 List만을 인자로 전달받고 있기 때문에 Set을 정렬하려면 List로 변경하는 작업이 필요합니다.

Set<Integer> set = new LinkedHashSet<>(Ints.asList(arr));
System.out.println(set);    // 정렬하기 전 출력

List<Integer> list = new ArrayList<>(set);
Collections.sort(list);
set = new LinkedHashSet<>(list);

System.out.println(set);    // 정렬 후 출력

위에서 보듯이 list로 변환 후 다시 set으로 변환하여 정렬을 할 수 있습니다.

4. Map 정렬하기

Map은 아시다시피 key와 value 쌍으로 이루어진 자료구조입니다. 따라서 Map을 정렬할 때는 key를 기준으로 정렬하는 방법과 value를 기준으로 정렬하는 방법, 두 가지 방법이 있습니다. 또한, Map도 Set과 마찬가지로 HashMap 같은 경우는 순서와는 무관한 자료구조이기 때문에 정렬을 위해서는 LinkedHashMap을 이용해야 합니다.

 

우선 key를 기준으로 정렬하는 방법에 대해서 알아보겠습니다. Map을 정렬할 때도 List, Set과 마찬가지로 Collections.sort(List list) 메서드를 이용할 수 있습니다. 따라서 list로 변환을 해주어야 하죠. 그리고 정렬 후에는 순서의 보장을 위해 list를 LinkedHashMap으로 변환해주어야 합니다. 또한 List.sort(Comparator c)를 이용할 수도 있습니다. 예제에서 두 가지 방법에 대해 모두 확인해보도록 하겠습니다.

// 기본 데이터 셋
HashMap<Integer, String> map = new HashMap<>();
map.put(5, "Orange");
map.put(8, "Apple");
map.put(2, "WaterMelon");
map.put(13, "Pear");
map.put(9, "Grape");
map.put(4, "Banana");


// key, value 를 모두 가져오기 위해서 entrySet()을 리스트로 변환
List<Map.Entry<Integer, String>> entries = new ArrayList<>(map.entrySet());

// 1. List.sort 를 이용하는 방법
entries.sort(Comparator.comparing(Map.Entry::getKey));

// 2. Collections.sort 를 이용하는 방법
Collections.sort(entries, Comparator.comparing(Map.Entry::getKey));

// 정렬된 데이터를 LinkedHashMap에 저장
Map<Integer, String> sortedByKey = new LinkedHashMap<>();
for (Map.Entry<Integer, String> entry : entries) {
	sortedByKey.put(entry.getKey(), entry.getValue());
}

// 정렬된 내용 출력
System.out.println(sortedByKey);

정렬할 때 List.sort(Comparator c) 와 Collections.sort(List list)를 이용하는 방법을 모두 소개해드렸습니다.

여기서 사용한 Comparator.comparing() 메서드는 comparator를 반환하는 메서드입니다. Comparator.comparing(Map.Entry::getKey) 는 Map의 key 값을 비교하는 comparator를 만들어주죠.

위 코드에서 출력되는 내용은 아래왁 같습니다.

{2=WaterMelon, 4=Banana, 5=Orange, 8=Apple, 9=Grape, 13=Pear}

key 값 순서대로 정렬이 된 것을 확인할 수 있습니다.

 

그럼 이번에는 위 코드를 조금 수정하여 value를 기준으로 정렬을 해보도록 하겠습니다.

// 1. List.sort 를 이용하는 방법
entries.sort(Comparator.comparing(Map.Entry::getValue));

// 2. Collections.sort 를 이용하는 방법
Collections.sort(entries, Comparator.comparing(Map.Entry::getValue));

Comparator.comparing() 메서드의 인자로 key가 아닌 value 값을 넘겨주기만 하면 됩니다.

수정된 코드 실행 결과는 다음과 같습니다.

{8=Apple, 4=Banana, 9=Grape, 5=Orange, 13=Pear, 2=WaterMelon}

value값으로 넣어주었던 과일명 순으로 정렬이 되어 출력된 것을 확인할 수 있습니다.

5. 커스텀 객체 정렬하기

이번에는 커스텀 객체를 정렬하는 방법에 대해 알아보겠습니다. 사실 커스텀 객체를 정렬하는 방법이라고 따로 있는건 아닙니다. 이미 위에서 설명드린 내용중에 comparator를 이용하는 방법을 이용해야 합니다.

일단 쇼핑몰에 판매중인 상품에 대한 객체 정의를 해보겠습니다.

@Getter
@Setter
@AllArgsConstructor
class Product {
    String name;        // 상품 명
    int price;          // 상품 가격
    float sellerRating; // 판매자 평점
    
    @Override
    public String toString() {
    	return "{" + this.name + ", " + price + ", " + sellerRating + "}";
    }
}

이제 이 객체를 리스트로 만들어 가격, 판매자 평점을 기준으로 각각 오름차순, 내림차순으로 정렬을 해보도록 하겠습니다.

// 기본 데이터 셋
List<Product> data = new ArrayList<>(Arrays.asList(
    new Product("Apple", 100, 4.3f),
    new Product("Apple", 200, 3.3f),
    new Product("Apple", 150, 4.8f)));

// 1. List.sort 를 이용하여 가격 오름차순 정렬
data.sort(Comparator.comparing(Product::getPrice));
System.out.println("\n1. List.sort 를 이용하여 가격 오름차순 정렬");
System.out.println(data);

// 2. List.sort 를 이용하여 가격 내림차순 정렬
data.sort(Comparator.comparing(Product::getPrice, Comparator.reverseOrder()));
System.out.println("\n2. List.sort 를 이용하여 가격 내림차순 정렬");
System.out.println(data);

// 3. Collections.sort 를 이용하여 판매자 편점 오름차순 정렬
Collections.sort(data, Comparator.comparing(Product::getSellerRating));
System.out.println("\n3. Collections.sort 를 이용하여 판매자 편점 오름차순 정렬");
System.out.println(data);

// 4. Collections.sort 를 이용하여 판매자 편점 내림차순 정렬
Collections.sort(data, Comparator.comparing(Product::getSellerRating, Comparator.reverseOrder()));
System.out.println("\n4. Collections.sort 를 이용하여 판매자 편점 내림차순 정렬");
System.out.println(data);

Product 리스트를 특정 필드를 기준으로 오름차순 내림차순으로 정렬하는 코드는 List.sort를 사용해도, Collections.sort를 사용해도 한 줄이면 됩니다. 매우 간단합니다. 내림차순으로 정렬을 하고 있을 때는 Comparator.comparing() 메서드에 Comparator.reverseOrder() 인자를 추가해주면 됩니다. 즉, Comparator.comparing(정렬기준, 정렬순서) 이렇게 사용하시면 되는거죠.

 

자, 이제 출력 결과를 확인해보면 다음과 같습니다.

1. List.sort 를 이용하여 가격 오름차순 정렬
[{Apple, 100, 4.3}, {Apple, 150, 4.8}, {Apple, 200, 3.3}]

2. List.sort 를 이용하여 가격 내림차순 정렬
[{Apple, 200, 3.3}, {Apple, 150, 4.8}, {Apple, 100, 4.3}]

3. Collections.sort 를 이용하여 판매자 편점 오름차순 정렬
[{Apple, 200, 3.3}, {Apple, 100, 4.3}, {Apple, 150, 4.8}]

4. Collections.sort 를 이용하여 판매자 편점 내림차순 정렬
[{Apple, 150, 4.8}, {Apple, 100, 4.3}, {Apple, 200, 3.3}]

Summary

여기까지 자바에서 정렬하는 방법에 대해서 알아보았습니다.

간략히 요약을 하면 다음과 같습니다.

 

배열의 정렬 -> Arrays.sort() 이용

List 정렬 -> Collections.sort(), List.sort() 이용

Set, Map 정렬 -> List로 변환 후 Collections.sort(), List.sort() 이용

기본적으로 오름차순 정렬

내림차순 정렬은 Comparator.reverseOrder() 이용

 

문장 내에서 사용된 distinct 문자 개수 구하기

한 문장 안에서 사용된 distinct 문자의 개수를 구해야 한다면 어떻게 할 수 있을까요?

stream이 나오기 전이었다면 그저 String을 캐릭터 배열로 만들어서 loop를 돌려 set에 넣은 뒤 set의 사이즈를 구하면 됐겠죠.

아래처럼 말이죠.

    String sentence = "Computer users take it for granted that their systems can do more than one thing at a time.";
    
    Set<Character> s = new HashSet<>();
    for (char c : sentence.toCharArray()) {
    	s.add(c);
    }
    
    System.out.println(s.size());

하지만 stream 을 이용하면 한줄로 해결이 가능합니다.

 

Stream을 이용한 distinct 문자 개수 구하기

stream에는 distinct() 메서드가 있으며 이는 해당 스트림내에서 distinct한 것들만 추출해줍니다. 그리고 count()메서드를 이용하여 그 개수를 셀 수 있습니다.

그래서 위에서 set과 for-loop를 사용한 부분을 stream을 이용하여 한줄로 줄이면 아래와 같이 바꿀 수 있습니다.

    String sentence = "Computer users take it for granted that their systems can do more than one thing at a time.";

    long distinctCharsCount = sentence.chars().distinct().count();

    System.out.println(distinctCharsCount);

훨씬 간단하게 구할 수 있습니다.

 

 

 

이번 포스팅에서는 jQuery를 이용하여 사용자가 선택한 라디오 버튼이나 체크박스를 해제하는 방법에 대해서 알아봅니다.

라디오 버튼 해제하기

우선 아래와 같은 화면을 하나 만들어 줍니다.

html 은 아래처럼 작성했습니다.

<div>
    <input type="radio" name="fruit" value="orange"><label>오렌지</label>
    <input type="radio" name="fruit" value="apple"><label>사과</label>
    <input type="radio" name="fruit" value="banana"><label>바나나</label>
    <button onclick="deselect()">선택해제</button>
</div>

여기서 name 값이 중요합니다. 라이오 버튼을 하나의 그룹으로 묶어주는 속성이기 때문이죠.

처음엔 아무런 버튼이 선택이 안되어있습니다. 

 

그리고 스크립트는 아래처럼 작성합니다.

function deselect() {
  // name이 fruit인 라디오 버튼 일괄 해제
  $("input:radio[name='fruit']").prop('checked', false);
}

선택해제 버튼을 클릭하면 deselect 함수를 호출하고 함수 내에서는 radio 버튼타입의 input 중에서 name값이 fruit인 것들의 체크상태를 false로 설정합니다. 즉, fruit name에 속한 모든 체크상태가 해제되게 됩니다.

 

자바스크립트로 라디오 버튼 체크 해제하기

jQuery를 사용하지 않고 자바스크립트로 직접 구현하려면 아래와 같이 작성할 수 있습니다.

function deselect() {
    var fruits = document.getElementsByName("fruit");
    for (var i = 0; i < fruits.length; i++) {
      if (fruits[i].getAttribute('type') === 'radio') {
      		fruits[i].checked = false;
      }
    }
}

 

체크박스 선택 해제하기

만약 라디오 버튼이 아니라 체크박스일 경우 선택자(selector)만 아래와 같이 변경해주면 됩니다.

function deselect() {
  // name이 fruit인 체크박스 일괄 해제
  $("input:checkbox[name='fruit']").prop('checked', false);
}

 

체크박스/라디오버튼 일괄 해제하기

그럼 만약 라디오 버튼과 체크박스를 모두 해제하려면 어떻게 할까요?

selector에서 radio나 checkbox라고 명시한 부분만 빼주면 됩니다.

단, 동일한 name 값으로 묶여있어야 합니다.

function deselect() {
  // name이 fruit인 모든 input 의 체크 상태 일괄 해제
  $("input[name='fruit']").prop('checked', false);
}

이렇게 말이죠.

 

간단하죠?

 

오늘도 즐코하세요~

 

💻 Programming

[5분 코딩] Spring4 + JCache (feat. Ehcache)

자바에서 초간단 캐시 사용하기

 

안녕하세요, 오늘은 캐시 관련 내용을 들고 왔습니다.

회사에서 관리중인 관리자 웹앱에서 특정 기능을 하는 버튼을 광클릭 하시는 전직 프로게이머 같은 분이 계셔서 자꾸 에러가 나길래 개선 방안을 고민하다가 한 번 클릭해서 정상적으로 실행이 되었다면 1분 동안 재처리를 하지 않도록 캐시를 이용해보면 좋겠다는 생각이 들었습니다. 즉, 특정 API를 호출할 때 캐시에 관련 데이터를 저장해놓고 1분 내에 동일한 내용으로 API 요청이 들어올 경우 에러 메시지를 출력하고 해당 기능을 실행하지 않도록 구현하면 되겠다 싶었죠. 그리고 예전에 카카오톡과 연동하여 G사의 매니저들이 사용하는 챗봇을 만든 적이 있는데 이 때 Ehcache 를 사용해본적이 있어서 이번에도 동일하게 Ehcache를 사용하면 되겠다 싶었습니다.

우선 현재 스펙은 백엔드 SpringFramework 4 기반이고 프론트엔드는 부트스트랩을 이용한 구식 앱입니다. (스프링 부트로 전환하고 싶지만 더 중요한 업무들이 많아 생각만 하고 있다는...ㅜㅜ) AWS의 Tomcat 8 플랫폼을 사용하여 서비스 중이죠.

 

Ehcache 에 대해서 오랜만에 구글링을 해보니 Jcache를 지원한다고 하여 표준인 Jcache를 사용할 수 있도록 구현해보았습니다. Jcache는 JSR-107 표준을 따르는 자바에서 제공하는 인터페이스입니다. 이 표준 인터페이스를 구현하고 있는 구현체들에는 Ehcache 뿐 아니라 Hazelcast, Oracle Coherence, Infinispan 등도 있습니다.

 

자바에서 간단하게 캐싱하기 (Ehcache를 이용한 Jcache 사용법)

우선 pom.xml 파일에 아래 두 개의 의존성을 추가합니다.

        <dependency>
            <groupId>org.ehcache</groupId>
            <artifactId>ehcache</artifactId>
            <version>3.9.0</version>
        </dependency>

        <dependency>
            <groupId>javax.cache</groupId>
            <artifactId>cache-api</artifactId>
            <version>1.1.1</version>
        </dependency>

ehcache, jcache 모두 포스팅 시점 최신버전으로 추가했습니다.

 

그리고 CacheConfig 클래스를 만들어서 아래처럼 작성했습니다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.cache.Cache;
import javax.cache.CacheManager;
import javax.cache.Caching;
import javax.cache.configuration.MutableConfiguration;
import javax.cache.expiry.CreatedExpiryPolicy;
import javax.cache.expiry.Duration;
import javax.cache.spi.CachingProvider;


@Configuration
public class CacheConfig {

    @Bean
    public Cache<String, String> syncCache() {

        CachingProvider provider = Caching.getCachingProvider();
        CacheManager cacheManager = provider.getCacheManager();
        MutableConfiguration<String, String> configuration =
                new MutableConfiguration<String, String>()
                        .setTypes(String.class, String.class)
                        .setStoreByValue(false)
                        .setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(Duration.ONE_MINUTE));
        return cacheManager.createCache("syncCache", configuration);
    }
}

이 설정 부분이 가장 중요한 부분일텐데요, 특히 캐시만료정책(expiry policy) 설정하는 부분은 잘 확인하시는 것을 권장드립니다.

jcache에서 제공하는 만료정책은 5가지가 있습니다.

 

- AccessExpiryPolicy

- CreatedExpiryPolicy

- EternalExpiryPolicy

- ModifiedExpiryPolicy

- TouchedExpiryPolicy

 

각각 특정 만료정책을 위해 기본적으로 제공하는 것들입니다. 예를들어 EternalExpiryPolicy는 캐싱된 데이터가 만료되지 않는 무기한 저장하는 정책이죠. 자세한 내용은 구글링 해보시면 많은 자료들이 있으니 참고하시기 바랍니다.

 

여기서는 CreatedExpiryPolicy를 사용해서 Duration으로 1분을 주었습니다. 기본적으로 많이 사용될만한 시간들이 static하게 정의되어 있으나 정의되어있지 않은 다른 시간을 사용하고자 할 경우 new Duration(TimeUnit timeUnit, long durationAmount) 을 이용하여 커스텀하게 정의해줄 수도 있습니다.

 

이제 저 캐시를 사용할 CacheService라는 서비스 클래스를 하나 만들겠습니다.

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import javax.cache.Cache;

@Service
@RequiredArgsConstructor
public class CacheService {

    private final Cache<String, String> syncCache;

    public boolean isCached(String key) {
        return syncCache.containsKey(key);
    }

    public void doCache(String key, String value) {
        syncCache.put(key, value);
    }

    public boolean remove(String key) {
        return syncCache.remove(key);
    }

}

 

이제 해당 서비스를 가져다 쓰기만 하면 됩니다.

꼼꼼하신 분들은 좀 이상한게 눈에 띌 수도 있습니다. 

ehcache 패키지에서 제공하는 클래스를 사용한 부분이 하나도 없다는 것이죠.

그럼 pom.xml 파일에서 ehcache 의존성을 제거 해볼까요??

그럼 아래 에러가 발생하는 것을 확인하실 수 있습니다.

Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [javax.cache.Cache]: Factory method 'syncCache' threw exception; nested exception is javax.cache.CacheException: No CachingProviders have been configured
	at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:189)
	at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:588)
	... 67 more
Caused by: javax.cache.CacheException: No CachingProviders have been configured
	at javax.cache.Caching$CachingProviderRegistry.getCachingProvider(Caching.java:391)
	at javax.cache.Caching$CachingProviderRegistry.getCachingProvider(Caching.java:361)
	at javax.cache.Caching.getCachingProvider(Caching.java:151)

 

즉, 실제로 사용하는 API는 javax.cache 패키지 내에 정의되어있는 것이지만 내부적으로 ehcache에서 구현한 구현체로 실행된다는 결론을 내릴 수 있습니다.

 

어떤가요? 정말 쉽게 구현이 가능하죠?

 

이상으로 ehcache를 활용한 jcache 사용법에 대한 포스팅을 마칩니다.

 

 

 

참고자료

- www.ehcache.org/

- www.ehcache.org/documentation/3.3/107.html

- www.baeldung.com/jcache

스프링 순환참조 문제

스프링으로 프로젝트를 진행하다보면 여러 빈들의 순환참조로 인해 앱 기동이 안되는 상황에 종종 맞딱드리게 됩니다. 최근에 젠킨스로 빌드한 앱이 순환참조 문제로 인해 정상적으로 기동이 되지 않는 문제가 발생했습니다. 그런데 웃긴건 로컬환경에서는 아무런 문제없이 잘 돌아간다는 것이었죠. 결국 담당자의 확인하에 젠킨스의 파이프라인 옵션 설정으로 해결은 했습니다만, setter injection (field injection) 이라도 순환참조가 발생하는 디자인은 좋지 않죠. 그래서 관련내용을 알아보다가 끄적여봅니다.

순환참조란? (What is Circular Reference ? )

순환참조란 서로 다른 여러 빈들이 서로 물고늘어져서 계속 연결되어 있음을 의미합니다.

즉, 아래처럼 A는 B에서 필요한데 B는 또 A에서 필요한 상태를 말합니다.

Bean A → Bean B → Bean A

 

만약 Bean A -> Bean B -> Bean C 처럼 연결되어있다면 스프링은 A를 먼저 만들고 A를 필요로 하는 B를 만들고 B를 필요로 하는 C를 만들게 됩니다. 하지만 순환참조가 발생하면 스프링은 어느 빈을 먼저 생성해야할지 결정하지 못하게되고 순환참조 오류가 발생하게 됩니다. 이렇게 순환참조가 발생한다는건 결국 설계가 잘못되었다는 것입니다. 하지만 그렇다고 설계를 다 뜯어고치자니 비용이 너무 많이 들어갈 수도 있죠. 순환참조 오류는 참고로 스프링의 의존성 주입방법 중에서도 특히 생성자 주입방법을 사용했을 때 발생합니다. 빈 생성시 필요한 다른 빈이 서로 물고늘어져 있으니 어떤 빈도 생성이 불가능한 상황이 되어버리는 것이죠.

순환참조 문제 해결방법

순환참조는 발생하지 않도록 해주는게 제일 좋긴 하지만 어쩔 수 없는 상황은 꼭 생기기 마련입니다. 그럼 어떻게 순환참조 문제를 해결할 수 있을까요? 가장 우선시 해야할 것은 순환참조의 고리를 끊어버리는 것입니다. 이것이 스프링에서 권장하는 방법이며 설계를 조금만 바꿔서 해결이 가능한 경우가 이에 해당합니다. 하지만 만약 설계의 변경이 힘든 경우라면 @Lazy 어노테이션을 사용 해볼 수 있습니다. (해볼 수 있다 라고 얘기한 이유는 아래에 나옵니다)

@Component
public class BeanA {

    private BeanB beanB;

    @Autowired
    public BeanA(BeanB beanB) {
        this.beanB = beanB;
    }
}

 

이렇게 되어있던 코드를 아래처럼 바꿔주는 것이죠.

@Component
public class BeanA {

    private BeanB beanB;

    @Autowired
    public BeanA(@Lazy BeanB beanB) {
        this.beanB = beanB;
    }
}

 

하지만 이런 접근 방식은 문제가 있습니다. Lazy initialization 에 대한 문제는 Spring 공식문서(Lazy Initialization)를 참고하시면 되겠습니다. 간단히 말씀드리면 앱 기동시점이 아닌 실제 해당 빈이 필요한 시점에 빈을 생성하기 때문에 특정 http 요청을 받았을 때 힙메모리가 증가할 수 있으며 메모리가 충분하지 않은 상황이었다면 장애로 이어질 수 있다는 얘기죠. 아무튼 스프링에서 권장하지 않는 방식이니 사용하지 않으시면 됩니다. 

 

또 다른 방법은 생성자 주입방법 대신에 setter 주입방법을 사용하시면 됩니다.

아래 setter 주입 예제를 한번 보시죠.

@Component
public class BeanA {

    private BeanB beanB;

    @Autowired
    public void setBeanB(BeanB beanB) {
        this.beanB = beanB;
    }

    public BeanB getBeanB() {
        return beanB;
    }
}

@Component
public class BeanB {

    private BeanA beanA;

    @Autowired
    public void setBeanA(BeanA beanA) {
        this.beanA = beanA;
    }
}

 

 

혹시나 주입방법에 다시 한번 짚고 넘어가고 싶으시다면 스프링 의존성 주입 포스팅을 참고하시기 바랍니다.

 

Conclusion

스프링으로 개발을 하다가 순환참조가 발생한다면 가장 좋은 해결책은 순환참조의 연결고리를 끊어버리는 것이며 그렇게 하는 것이 깔끔하게 설계된 디자인이라고 할 수 있습니다. 스프링에서도 권장하는 방식이죠. 그래서 setter주입이 아닌 생성자 주입방식을 사용하는 것이 권장됩니다. 우회적으로 순환참조를 잠시 피한다 하더라도 언제 또 똑같은 문제가 발생할지 아무도 알 수 없으며 메모리 부족(lazy initialization의 경우) 현상이 발생할 수도 있기 때문에 가급적 순환참조해야하는 설계는 피하도록 하는 것이 좋겠습니다.

 

 

 

 

Reference

- www.baeldung.com/circular-dependencies-in-spring 

- docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-dependencies

💻 Programming

스프링 의존성 주입(DI)

스프링 의존성 주입 (Spring Dependency Injection)

스프링에서 의존성을 주입하는 방법은 현재 세 가지가 있습니다.

첫 번째는 생성자 주입 방식이고 두 번째는 Setter 주입 방식, 그리고 마지막으로 Field 주입 방식이 있습니다.

순서대로 한번 보도록 하겠습니다.

 

1. 생성자 주입 (Constructor Based Dependency Injection)

말 그대로 생성자에 @Autowired로 다른 빈을 주입하는 방식입니다.

@Controller
public class MyController {
    private MyService myService;
 
    @Autowired
    public MyController(MyService myService) {
        this.myService = myService;
    }
}

주입해야하는 빈이 많을 경우 생성자 자체가 커지게 됩니다. 그런경우 만약 lombok 을 사용한다면 아래처럼 작성할 수 있습니다.

@Controller
@RequiredArgsConstructor
public class MyController {

    private final MyService myService;
 
}

@RequiredArgsConstructor 어노테이션을 class 레벨에 붙여주고 주입할 빈은 final 키워드를 붙여주면 lombok에서 자동으로 생성자 주입을 이용하여 빈들을 주입해줍니다. 필요한 빈들은 모두 final 키워드를 붙여서 필드로 추가만 해주면 되죠. 

2. Setter 주입 (Setter Based Dependency Injection)

Setter 메서드를 이용하여 주입하는 방식이죠. 

@Controller
public class MyController {

    private MyService myService;
    
    @Autowired
    public void setMyService(MyService myService) {
        this.myService = myService;
    }
}

특이한 경우를 제외하고는 이 방식은 거의 사용하는 것을 못봤네요

3. Field 주입 (사용하지 마세요)

예전부터 많이 사용되는 방식입니다. 하지만 스프링에서 권고하지 않는 방식이기도 하죠. 

@Controller
public class MyController {

    @Autowired
    private MyService myService;
}

필드 주입 방식은 공식문서에도 소개조차 하고있지 않습니다. 공식문서에는 두 가지 방식만 있다고 설명합니다. 생성자 주입 방식과 setter 주입방식이죠. 그 이유는 필드 주입을 사용하게되면 순환참조 문제를 우회할 수 있게되어 순환참조를 고려하지 않고 개발을 하게되고, 그렇게 설계된 클래스는 하나 이상의 기능을 하게되어 하나의 책임만 가져야 한다는 Single Responsibility 원칙에 위배될 가능성이 높아지게되죠. 또 다른 여러 단점들에 대해서 stackoverflow를 참고하시면 좋을 것 같습니다. 만약 필드 주입을 사용하고 계시다면 생성자 주입 방식으로 바꿔주세요. 그러면 순환참조를 찾아낼 수 있고 순환참조 고리 안에 있는 클래스들을 살펴보면 Single Responsibility 원칙을 위배하는 클래스가 보일겁니다. 그런 것들은 클래스 분리를 통해 개선해주시면 됩니다.

 

 

 

 

 

참고문서

docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-dependencies

stackoverflow.com/questions/39890849/what-exactly-is-field-injection-and-how-to-avoid-it

 

 

 

맥북 블로깅을 위한 초간단 그림그리기 앱 (맥북 버전 그림판)

안녕하세요, 오늘은 맥북용 그림그리기 앱을 소개해드리겠습니다.

포토샵이나 유사 앱들처럼 사용하기 어려운 그림그리기 앱이 아닌 정말 간단한 그림그리기 앱입니다.

윈도우에는 별도로 설치하지 않아도 기본적으로 딸려오는 그림그리기 프로그램이 있습니다.

바로 "그림판(영문으로는 paint)"이라는 프로그램 인데요

전 그 프로그램을 한번 살펴보고는 "누가 이런 프로그램으로 그림을 그리지?" 라고 생각했습니다.

그러다가 누군가가 그 프로그램을 사용하는 것을 보게되었는데요

그렇게 쉬울 수가 없더군요

그 후로는 저도 매우 애용했던 프로그램이 바로 그림판이었습니다.

 

그런데 맥북에서는 그런 프로그램을 찾기가 쉽지 않았습니다.

그런데 최근에 다시 찾아보니 페인트 X 라는 앱이 있더군요.

앱스토어에서 paintx 로 검색을 하시면 아래처럼 일반 버전과 프리미엄 버전이 나옵니다.

맥용 그림그리기 앱 페인트 X

저는 일반 버전을 무료로 다운로드 받았습니다.

실행시키면 UI 자체가 윈도우의 그림판과 매우 유사합니다.

페인트 X 실행화면

윈도우의 그림판과 한번 비교해볼까요?

와~ 어쩜 이렇게 비슷할까요~

생긴것 부터가 너무 비슷하고요, 기능역시 대부분 비슷합니다.

그림판을 자주 사용하셨던 분들은 쉽게 적응하실 수 있습니다.

 

하지만 역시 애플의 앱스토어에는 완전무료 앱이 거의 없죠.

무료로 다운받아서 사용은 가능하지만 그리는 그림의 크기가 일정 사이즈를 넘어가게되면 워터마크가 표시됩니다.

좀 더 정확히 말씀드리자면 가로 또는 세로 어느 한쪽이라도 200픽셀을 넘어가면 워터마크가 생깁니다.

즉, 1024 x 199 는 워터마크가 생기지 않지만 199 x 200 사이즈에서는 워터마크가 생깁니다.

페인트 X Lite 워터마크

 

해당 워터마크를 없애려면 우측 상단의 Remove Watermark & AD 를 클릭하여 일정 금액을 지불해야합니다.

페인트 X Lite 워터마크 & 광고 제거 팝업창

커피 한잔 값도 안되는 $5 를 결제하면 워터마크 및 광고를 제거할 수 있다고 합니다.

저는 그렇게 크게 그림을 그릴 일이 없어 일단 무료로 그냥 사용중입니다.

어떠신가요? 블로깅 용도로 자잘하게 그림을 그려야 할 때도 사용하면 좋을 것 같지 않나요?

 

이상으로 맥북용 그림그리기 앱 페인트X 포스팅을 마칩니다.

예외 처리/에러 핸들링 잘하기

안녕하세요, 오늘은 예외 처리(에러 핸들링)에 대해서 얘기를 해볼까 합니다.

책에 나오는 또는 인터넷 상에서 간략하게 설명하는 try-catch 예외처리에 대한 내용은 아니고 직접 개발을 진행하면서 경험한 내용을 토대로 이렇게 하면 좋겠다 싶은 내용들을 공유하는 차원에서 작성하는 글입니다. 혹여나 더 좋은 방법이나 공유해주시고 싶은 내용이 있다면 댓글도 환영합니다.

 

우선 자바 서적이나 구글링을 해서 찾는 에러 핸들링에 대한 내용들은 대부분 try-catch 문을 사용하면 된다에서 끝납니다.

하지만 단순히 그렇게 작업하면 에러 핸들링을 잘한다고 할 수 없습니다.

복잡한 비즈니스 로직을 구현하다보면 여러 클래스들을 넘나들면서 여러 메서드들을 거치게 됩니다.

비즈니스 로직의 메서드 콜 체인

위 메서드 콜 체인은 하나의 프로세스(기능)이 처리될 때의 과정의 일부입니다. 예를들면 controller -> service -> dao 뭐 이런 건데요, 아무리 복잡한 서비스라 하더라도 결국 요청에서부터 응답을 주기까지의 과정은 결국 위 그림처럼 한 줄로 연결이 가능하죠.

 

따라서 어느 시점에 try-catch 로 예외를 잡아야 할지 그리고 예외를 잡았다면 어떤 처리를 해야할지에 대해 많은 고민이 필요합니다. 하지만 실상 그런 고민을 별로 하지 않고 대충 에러로그 찍어놓는 개발자들이 많은 것이 사실이죠. 심지어 catch로 잡아서 로그만 남기고 다시 throw를 하기도 합니다. 무슨 폭탄돌리기 하는 것도 아니고 말이죠 -_-;; 최악입니다. 그렇게 개발된 서비스는 하나의 에러에 대해서 중복적으로 여러개의 로그를 남기는 것을 확인할 수 있고 디버깅도 비효율적으로 만들어 버립니다. 그럼 어떻게 하는 것이 좋은 것일까요?

 

첫 번째는 요청을 받아서 응답을 주기까지 하나의 체인에서 특정 예외에 대한 처리는 한 곳에서만 하도록 하는 것이 좋습니다. 즉, 위 체인 그림에서 메서드 1, 2, 3, 4 중 하나의 메서드에서만 A라는 예외에 대한 처리를 한다는거죠. 컨트롤러 부분에서 ExceptionHandler를 이용해서 처리할 수도 있고 서비스 레이어에서 try-catch 문을 이용해서 처리해도 됩니다. 메서드3에서 예외처리를 해놓았는데 불필요하게 메서드2에서 또 하고 있다거나 한다면 둘 중 하나를 제거하는게 유지보수하는데 큰 도움이 됩니다.

 

두 번째는 catch를 했으면 적절한 처리를 해줘야 한다는 겁니다. 여기서 "적절한" 이라는 말이 애매할 수 있는데요, 단순히 로그만 찍고 catch한 예외를 다시 throw 하고있다면 해당 try-catch문은 쓸모없는 것일 가능성이 99.9%입니다. 그런 경우라면 callee에서만 catch해서 로그를 남기고 적절한 처리를 하는게 더 깔끔합니다. 굳이 두 군데서 나누어 catch할 필요가 없다는 거죠. 또, 어떤 예외는 잡아서 로그만 남기고 무시할 수도 있고 (옵셔널 값이 없다거나 하는 경우), 어떤 예외는 디버깅용 로그를 남기고 비즈니스 로직을 롤백하는 처리를 해야할 수도 있습니다. 해당 프로세스를 진행함에 있어서 어떻게 처리해야 할 지는 API스펙 또는 정책에 따라 달라질 겁니다.

 

마지막으로, 예외가 발생했을 경우 디버깅용 로그를 남기기로 했다면 정말 필요한 정보만 가독성 있게 로그를 남기는 것입니다. 한 줄의 로그만 잘 남겨도 디버깅하기 쉽도록 작성하면 유지보수 효율성 뿐만 아니라 로깅 시스템에 부하도 줄일 수 있게 됩니다.

 

제 경험을 토대로 신입 또는 주니어 개발자 분들께서 이렇게 해보시면 좋겠다는 생각으로 끄적여 보았습니다.

 

혹시 이 글을 읽으시다가 멋진 노하우가 있으신 분들은 댓글로 공유 부탁드려요 ^-^

 

[자료구조] AVL 트리 특징 및 로테이션 기준 에서 AVL 트리의 특징 및 회전의 4가지 케이스들에 대해서 말씀드렸었는데요

로테이션을 어떻게 하는지에 대해서 좀 더 자세히 설명드리기 위해 추가 포스팅을 합니다.

AVL 트리 회전

우선 첫 번째 케이스였던 Left-Left 케이스에 대해서 살펴보도록 하겠습니다.

Left-Left 케이스는 부모노드와 pivot 노드가 모두 left heavy 인 케이스였죠. 

즉, 신규로 어떤 노드를 추가했을 때 해당 노드의 부모노드와 pivot 노드의 균형값(balance factor)이 (-)라는 얘기죠.

아래 예제를 한번 살펴보겠습니다.

 

figure1. Left-Left 에제 base tree

이런 트리가 있다고 가정해봅니다. 각 노드의 바깥쪽에 씌여진 숫자가 균형값(balance factor)입니다.

자, 이제 이 트리에 2번 노드를 추가합니다.

figure2. Left-Left 예제 신규노드 추가

새롭게 노드가 추가가 되면서 5번 노드와 10번 노드의 균형값에 변동이 생기고 10번 노드의 균형값이 -2가 되면서 AVL 트리의 조건에 위배됩니다. 따라서 회전을 해야하는데 이렇게 새로 추가한 노드의 부모 노드(5번 노드)와 pivot 노드(10번 노드)가 모두 left heavy인 경우에는 오른쪽으로 1회전을 해주면 됩니다.

figure3. Left-Left 예제 로테이션하기

오른쪽으로 회전을 해주게되면 모든 노드의 균형값이 0이 되면서 AVL 트리의 조건을 만족하게 됩니다.

 

두 번째로 왼쪽으로 회전해야하는 Right-Right 케이스를 살펴보겠습니다.

Right-Right 케이스는 부모노드와 pivot 노드가 모두 right heavy 인 케이스이죠.

아래 그림은 right-right 케이스에 대해서 왼쪽으로 회전하는 예제입니다.

figure4. AVL 트리의 왼쪽 회전 (출처: tutorialspoint.com)

최초에 A, B 노드가 있었고 C 노드가 추가가 되면서 right unbalanced 트리가 됩니다.

right-right 케이스에 해당하므로 왼쪽으로 1회전 해주면 balanced 트리가 되죠.

 

이제 세 번째 케이스인 left-right 케이스를 한번 보겠습니다.

left-right 케이스는 부모 노드와 pivot 노드의 균형값이 각각 right-heavy, left-heavy, 인 경우라고 했었습니다.

이 말은 결국 어떤 노드의 왼쪽 서브트리의 오른쪽 서브트리에 새로운 노드를 추가하게 되는 경우라고 이해하셔도 됩니다.

아래 그림을 보시면 이해가 더 빠를 겁니다.

figure5. left-right 케이스

최초에 C와 A 노드만 있었는데 B 노드가 추가가 됩니다. 그러면 A노드의 오른쪽 서브트리로 들어가게 되겠죠. 위 그림처럼 말이죠.

figure6. 왼쪽 회전

left-right 케이스라면 왼쪽으로 회전했다가 오른쪽으로 회전하면 되므로 위 그림처럼 왼쪽으로 회전을 시켜봅니다.

figure7. 왼쪽 회전의 결과

왼쪽으로 회전을 시키면 위 그림 처럼 됩니다. 어랏? 어디서 본 그림이네요?

맞습니다. 제일 처음에 봤던 left-left 케이스와 똑같아 졌습니다.

left-left 케이스면 오른쪽으로 회전을 시켜야겠죠?

figure8. 오른쪽 회전
figure9. 균형 트리

이제 균형이 모두 맞춰졌습니다.

 

마지막으로 right-left 케이스를 살펴보겠습니다.

간단하게는 left-right 케이스와 반대라고 생각하시면 됩니다. 

left-right 케이스가 왼쪽 서브트리의 오른쪽 서브트리에 새로운 노드가 추가되는 케이스였으니,

right-left 케이스는 오른쪽 서브트리의 왼쪽 서브트리에 새로운 노드가 추가되는 케이스겠죠.

그리고 그 말은 부모 노드와 pivot 노드의 균형값이 각각 left-heavy, right-heavy라는 것과 동일합니다.

그림으로 한번 살펴보겠습니다.

figure10. right-left 케이스

A, C 노드가 있었는데 B 노드가 새롭게 추가가 됩니다. C 노드의 왼쪽 서브트리로 추가가 되죠.

A노드의 오른쪽 서브트리(C가 루트노드인)의 왼쪽 서브트리에 새로운 노드가 추가가 됐습니다.

figure11. 오른쪽 회전

이런 경우 오른쪽으로 먼저 회전을 해줍니다.

figure12. 오른쪽 회전 결과

그러면?? 두 번째 케이스인 right-right 케이스가 됩니다. 그럼 왼쪽으로 회전시켜주면 되겠죠?

figure13. 왼쪽 회전
figure14. 균형 트리

자, 이렇게 마지막 네 번째 케이스도 균형 트리가 되었습니다.

 

AVL 트리의 4가지 회전 방법에 대해서 그림으로 설명을 해드렸는데요,

이해가 잘 되셨길 바랍니다.

 

이상으로 AVL 트리의 로테이션법에 대한 포스팅을 마치도록 하겠습니다.

 

오늘도 즐프하세요~

 

 

Note: left-left 케이스를 제외한 나머지 케이스들에 대한 이미지는 tutorialspoint.com 에서 가져왔습니다.

 

💻 Programming

[자료구조] AVL 트리 특징 및 로테이션 기준

AVL 트리 특징 및 로테이션 기준

AVL 트리는 이진탐색트리(Binary Search Tree, BST)의 한 종류입니다.

이진 탐색 트리(BST)는 이진트리(Binary Tree)의 한 종류이죠.

따라서 AVL 트리는 이진트리와 이진탐색트리의 특징들을 모두 갖고 있습니다.

그렇다면 그 특징들이 무엇일까요?

 

- 이진트리(Binary Tree)의 특징

a. 각 노드는 최대 2개의 자식 노드를 가질 수 있다.

b. 자식 노드는 보통 왼쪽과 오른쪽으로 구분한다 -> left child, right child

c. 각 노드는 데이터를 가지고 있다. (데이터를 저장하기 위한 자료구조 중 하나이니 당연한 말이다)

 

- 이진탐색트리(Binary Search Tree, BST)의 특징

a. 이진트리이므로 이진트리의 특성을 기본적으로 갖고 있다.

b. 각 노드의 값은 왼쪽 서브트리에 존재하는 모든 값들 보다 크고 오른쪽 서브트리에 존재하는 모든 값들 보다 작다.

c. 따라서 이진탐색트리는 정렬된 이진트리(sorted binary tree)라고도 한다. 

 

자, 그리고 여기에 추가로 아래와 같은 특징을 갖는 트리를 AVL 트리라고 부릅니다.

- AVL 트리의 특징

a. 어떤 노드라도 왼쪽 서브트리와 오른쪽 서브트리의 높이 차이가 1보다 크지 않다. (최대 높이 차이는 1 이다.)

=> Binary Tree이면서 최대 높이 차이가 1이라는 말은 height = O(log N) 이라는 것과 같다.

b. 만약 특정 노드를 추가하거나 삭제했을 때 왼쪽 서브트리와 오른쪽 서브트리의 높이 차이가 1보다 커지는 노드가 생긴다면, re-balancing을 통해 a 규칙에 어긋나지 않도록 한다.

=> 이 말은 AVL 트리는 자가 균형 이진 탐색 트리(self-balancing BST) 라는 말과 같다.

c. 균형 트리(balanced tree)이다. (좀더 정확히는 높이 균형 트리이다.)

=> 균형 트리는 아래 세 가지 조건을 모두 만족하는 트리이다.

   i. 왼쪽과 오른쪽 서브트리의 높이 차이가 최대 1이다.

  ii. 왼쪽 서브트리가 균형 트리이다.

 iii. 오른쪽 서브트리가 균형 트리이다.

d. 각 노드는 균형값(균형인수, balance factor)을 가지고 있으며 이 균형값은 오른쪽 서브트리의 높이에서 왼쪽 서브트리의 높이를 뺀 값이다. 이 값은 항상 {-1, 0, 1 } 셋 중 하나의 값이어야 한다.

e. 균형값(균형인수, balance factor)이 마이너스(-)이면 left-heavy, 플러스(+) 값을 가지면 right-heavy 라고 한다.

f. pivot 노드는 새로운 노드가 추가되었을 때 균형값에 변동이 발생하여 unbalanced 된 노드들 중 신규노드와 가장 가까운 노드를 말한다. 

 

자, 위와 같은 특징을 갖고 있는 AVL 트리에 노드를 추가 또는 삭제를 하게되면 스스로 균형을 맞추기 위해 트리의 회전(rotation)을 필요로 할 수도 있습니다. 노드가 추가 또는 삭제 되면서 일부 노드들의 균형값에 변동이 발생하기 때문입니다. 회전을 필요로 하는 경우는 크게 2가지(single rotation과 double rotation) 좀 더 세분화하면 아래와 같이 4가지로 나눌 수 있습니다.

  • 왼왼 (Left - Left) : 부모 노드와 pivot 노드의 균형값이 모두 left-heavy인 경우
    => Single Right rotation (오른쪽으로 1회전)
  • 오오 (Right - Right) : 부모 노드와 pivot 노드의 균형값이 모두 right-heavy인 경우
    => Single Left rotation (왼쪽으로 1회전)
  • 왼오 (Left - Right) : 부모 노드와 pivot 노드의 균형값이 각각 right-heavy, left-heavy, 인 경우
    => Double Left-Right rotation (왼쪽으로 한번, 오른쪽으로 한번 총 두 번의 회전)
  • 오왼(Right - Left) : 부모 노드와 pivot 노드의 균형값이 각각 left-heavy, right-heavy인 경우
    => Double Right-Left rotation (오른쪽으로 한번, 왼쪽으로 한번 총 두 번의 회전)

 

각각의 로테이션에 대한 그림은 wiki에 매우 간단하게 나와있어 gif 이미지를 첨부합니다.

AVL 트리의 4가지 로테이션 예제 (출처: https://en.wikipedia.org/wiki/AVL_tree)

로테이션 예제 그림이 너무 간단해서 위 4가지 케이스에 대해 설명이 잘 될지 모르겠습니다.

 

로테이션에 대한 추가 설명은 다음 포스팅을 기다려주세요~

 

추가로 AVL 트리에 새로운 노드를 추가하거나 삭제하는 동작은 최악의 상황에서도 O(log N) 의 시간복잡도를 보이며 검색 또한 O(log N) 으로 빠른 자료구조입니다.

 

 

 

 

AWS 로고

AWS 엘라스틱서치 키바나 에러 - Unable to find saved objects

최근 AWS의 엘라스틱서치 클러스터 이슈 때문에 시간을 많이 낭비했다.

AWS의 elastic search는 클러스터에 문제가 생기면 개발자가 어떻게 할 수 없기 때문에 매우 곤란할 수 있다.

클러스터도 키바나도 재시작을 할 수 없고 무조건 AWS internal team에서 확인해줘야한다.

support에 chat으로 문의해도 결국 internal team으로 넘어간다.

 

다행히 키바나가 접속이 된다면 클러스터 이슈는 아니다.

근데 키바나의 discover 메뉴에서 아무것도 조회가 안되고 하얀 화면이 뜨는 경우를 보게 되었다.

클러스터 상태와 키바나의 상태는 모두 GREEN 이었다.

특별히 설정을 변경하거나 하지 않았었는데도 갑자기 이런 현상이 생겼다.

ES 클러스터 인스턴스 타입을 변경하여 보았으나 현상은 동일했다.

그래서 키바나 인덱스를 삭제하여 설정을 초기화해보았다. 

인덱스 패턴을 새로 정의해주고 다시 discover로 들어갔는데....오잉?

다시 인덱스 패턴을 정의하라는 화면이 나온다.;;; 헐;;;

방금 정의해준 것에 대해 제대로 인식을 못한다;;;;

saved objects 를 들어가보니 에러가 발생한다.

 

Unable to find saved objects

 

이것저것 해보다가 AWS support에 live chat으로 문의했다.

GET _cat/aliases 를 실행해보라고 한다.

실행해보니 별다른 메시지는 없고 단순히 200 success 표시만 키바나 창 우상단에 표시되었다.

그 얘길 해주니 alias가 제대로 연결이 안된것 같다면서 아래 링크를 하나 던져주면서 모든 스텝을 그대로 따라서 해보라고 한다.

 

aws.amazon.com/ko/premiumsupport/knowledge-center/amazon-es-saved-objects-kibana/

 

Kibana에서 Amazon ES로부터 저장한 객체 찾기

Amazon ES 사용자들은 Amazon ES 버전 7.1로 업그레이드한 후에 가끔 400 Bad Request 오류를 경험할 수 있습니다. 업그레이드에 따라 사용자가 Kibana에서 저장된 객체를 찾을 수 없는 문제가 발생할 수 있

aws.amazon.com

스텝을 따라하면서 무슨 명령어인가 보았더니 새로운 인덱스를 만들고 alias를 연결해주고 기존 인덱스의 내용을 넣어주는 것이었다.

 

그리고 그렇게 따라하다가 제일 마지막 스텝인 백업 인덱스 삭제하기 전에 인덱스 패턴(saved object) 목록을 조회해보니 똑같은 인덱스 패턴이 여러개가 조회가 되었다. 그래서 동일한 패턴은 하나씩만 남겨놓고 모두 삭제하였다. 그리고 discover로 들어가니 정상적으로 조회가 되었다.

dev tools로 돌아와 GET _cat/aliases 를 실행해보니 200 success 표시와 함께 .kibana_1.kibana .kibana_1 - - - - 라고 메시지가 출력되었다.

 

오늘도 에러와 마주한 즐거운 하루였다.

reactjs 프론트와 java 백엔드 연동

이번에는 ReactJS로 구현한 프론트엔드와 Java + 스프링부트로 구현한 백엔드를 연동해보았습니다.

react를 쓰니 일반적인 자바스크립트 파일을 연결하는 것도 잘 모르겠고, ajax 호출을 jquery로만 했었는데 fetch 메서드를 쓰는 것이 공식문서의 예제로 나와있어서 그걸 이용했습니다. 

백엔드에 CORS 설정도 빼먹었었고요...

일단 화면에서 바뀐 점은 Blog > 다이어리 메뉴를 선택하면 검색을 위한 입력창과 버튼, 새글 등록을 위한 버튼을 넣었고(기능은 아직 구현못했네요), 리스트 목록 조회하는 부분만 간단하게 연결해놓았습니다. 소스코드는 많이 수정했는데 실제 화면에서 바뀐건 뭐 없네요 ㅡㅡㅋ

 

게시글 조회 기능 구현

 

앞으로 할 일들을 나열해봤습니다.

  • 등록기능 프론트 구현 및 백엔드와 연동
  • 검색기능 백엔드 구현 및 프론트와 연동
  • 목록의 헤더에 제목, 내용, 수정일 표시
  • 내용은 한 줄을 넘기지 않도록 일부만 출력되도록 수정
  • 등록일시 표시 -> 백엔드 수정
  • 내부적으로는 수정일시도 관리
  • 게시글 좌측에 게시글 번호 넣기 -> 몽고DB에 auto increment 기능이 없어 소스레벨에서 구현해야함

역시 프론트와 백엔드를 혼자 하려니 할 일이 너무 많네요 ㅜㅜ

그래도 재미있게 만들어 가고 있습니다.

요새 티스토리가 자체광고를 붙여서 광고가 자꾸 뜨는것 같네요...로그인할 때도 뜨고...

빨리 블로그 앱 만들어서 라이브해서 직접 사용해보고 싶네요

 

 

참고문서:

스프링부트 CORS 설정하기 >> spring.io/guides/gs/rest-service-cors/#global-cors-configuration

 

Enabling Cross Origin Requests for a RESTful Web Service

this guide is designed to get you productive as quickly as possible and using the latest Spring project releases and techniques as recommended by the Spring team

spring.io

리액트 ajax 호출하기 >> reactjs.org/docs/faq-ajax.html

 

AJAX and APIs – React

A JavaScript library for building user interfaces

reactjs.org

리액트 부모상태 업데이트하기 >> stackoverflow.com/questions/35537229/how-to-update-parents-state-in-react

 

How to update parent's state in React?

My structure looks as follows: Component 1 - |- Component 2 - - |- Component 4 - - - |- Component 5 Component 3 Component 3 should display some data depending on state of Component 5....

stackoverflow.com

 

💻 Programming

[5분코딩] 스프링 부트에 스웨거 v3.x 연동하기

자동생성된 스웨거 API 문서

스웨거는 백엔드에서 개발한 API를 문서화해주는 툴로 개발 진행중일 때 뿐만아니라 운영중에도 유용하게 사용되기도 하는데요

스웨거 버전이 3으로 올라가면서 설정 및 기본 url이 변경이 되었습니다.

스웨거 버전 업에 두어번 실패한 끝에 가장 간단히 해결할 수 있는 방법을 구해왔습니다.

 

스프링부트 스웨거 설정

버전은 스프링부트 2.4.0, 스웨거 3.0.0 입니다.

 

1. 스프링 부트와 스웨거 의존성 추가

메이븐프로젝트

<dependency>
	<groupId>io.springfox</groupId>
	<artifactId>springfox-boot-starter</artifactId>
	<version>3.0.0</version>
</dependency>
<dependency>
	<groupId>io.springfox</groupId>
	<artifactId>springfox-swagger-ui</artifactId>
	<version>3.0.0</version>
</dependency>

 

그래들 프로젝트

repositories {
	jcenter()
}

dependencies {
	implementation "io.springfox:springfox-boot-starter:3.0.0"
	implementation "io.springfox:springfox-swagger-ui:3.0.0"
}

 

2. 설정하기

SwaggerConfig 클래스를 하나 만들어서 아래와 같이 @Configuration 어노테이션을 붙여주고 Docket 빈을 하나 생성하면 됩니다.

좀 더 상세한 설정을 하려면 공식사이트에서 확인가능합니다.

@Configuration
public class SwaggerConfig {

	@Bean
	public Docket api() {
	return new Docket(DocumentationType.SWAGGER_2)
		.select()
		.apis(RequestHandlerSelectors.any())
		.paths(PathSelectors.any())
		.build();
	}
}

 

3. 접속 URL

기존의 2.x 버전까지는 host<:port>/<context root>/swagger-ui.html 로 접속을 했었다면, v3부터는 host<:port>/<context root>/swagger-ui/ 또는 host<:port>/<context root>/swagger-ui/index.html 로 접속을 하면 됩니다.

 

 

 

참고문서: https://springfox.github.io/springfox/docs/current/

이번 프로젝트에서는 Java + SpringBoot + Mongo DB 로 구현할 예정입니다.

각각 사용할 버전은 다음과 같습니다.

 

- Java 8 (오라클jdk가 상용목적으로는 유료화되어 추후 코틀린으로 변경하는 프로젝트를 진행해봐야겠네요)

- Springboot 2.4.0 (20년 11월 현재 최신버전)

- Mongo DB 4.4.2 Community Server (20년 11월 현재 최신버전)

 

몽고디비 설치 관련해서는 몽고DB 최신버전 설치하기 포스팅을 참고하시면 됩니다.

설치 후 사용자 계정 생성 및 데이터 베이스 생성 관련해서는 [몽고DB] 기본명령어 포스팅을 참고해주세요.

 

이제 그래들 자바 프로젝트를 하나 생성하여 Blog > 다이어리 메뉴에서 사용할 CRUD를 순서대로 작성해보겠습니다.

가장 먼저 프로젝트 구성을 한번 살펴보겠습니다.

블로그 만들기 백엔드 프로젝트

 

우선 build.gralde파일을 아래와 같이 작성했습니다.

plugins {
    id "org.springframework.boot" version "2.4.0"
    id 'java'
}

group 'com.keichee'
version '1.0-SNAPSHOT'
sourceCompatibility = "1.8"

repositories {
    jcenter()
}

ext {
    springVersion = '2.4.0'
}

dependencies {
    testImplementation group: 'junit', name: 'junit', version: '4.12'

    implementation 'org.mongodb:mongodb-driver-sync:4.1.1'
    testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3'

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

    compileOnly 'org.projectlombok:lombok:1.18.16'
    annotationProcessor 'org.projectlombok:lombok:1.18.16'

    testCompileOnly 'org.projectlombok:lombok:1.18.16'
    testAnnotationProcessor 'org.projectlombok:lombok:1.18.16'

    implementation "io.springfox:springfox-boot-starter:3.0.0"
    implementation 'io.springfox:springfox-swagger-ui:3.0.0'

    implementation 'com.google.guava:guava:30.0-jre'
}

test {
    useJUnitPlatform()
}

 

mongodb-driversync 4.1.1 을 사용했습니다.

로깅을 위해서 logback-classic을 추가했고 

api 를 쉽게 만들 수 있도록 spring-boot-starter-web을 추가

getter, setter 등의 어노테이션 사용을 위해 lombok 라이브러리를 추가하였고

스웨거 문서를 사용하려고 springfox 라이브러리들도 추가했습니다.

 

이제 몽고DB와 연결을 위한 설정을 아래와 같이 해줍니다.

@Configuration
public class MongoConfig {

    private static final String host = "localhost";
    private static final int port = 27017;
    private static final String database = "blogapp";

    @Bean
    public MongoDatabase blogAppDatabase() {
        MongoClient client = MongoClients.create(
                MongoClientSettings.builder()
                        .applyToClusterSettings(builder ->
                                builder.hosts(Collections.singletonList(new ServerAddress(host, port))))
                        .build());
        return client.getDatabase(database);
    }
}

 

컨트롤러에는 딱 4개의 Restful API를 만들었습니다.

@Slf4j
@RestController
@RequestMapping("/diary/life")
@AllArgsConstructor
public class DiaryLifeController {

    private final DiaryLifeService diaryLifeService;

    @ApiOperation("전체 목록 조회")
    @GetMapping
    public Response<List<Post>> getPosts() {
        log.info("전체 목록 조회");

        return new Response<>(diaryLifeService.getPosts(null));
    }

    @ApiOperation("다이어리 포스팅 신규생성")
    @PostMapping
    public Response<Void> savePost(@RequestBody Post post) {
        validatePostInput(post);
        diaryLifeService.createPost(post);
        return new Response<>();
    }

    @ApiOperation("다이어리 포스팅 업데이트")
    @PutMapping
    public Response<Void> updatePost(@RequestBody Post post) {
        validatePostInput(post);
        diaryLifeService.updatePost(post);
        return new Response<>();
    }

    @ApiOperation("다이어리 포스팅 삭제")
    @DeleteMapping
    public Response<Void> deletePost(@RequestParam String _id) {
        diaryLifeService.deletePost(_id);
        return new Response<>();
    }

    private void validatePostInput(Post post) {
        if (ObjectUtils.isEmpty(post.getTitle())) {
            throw new BadRequestException("No TITLE exists.");
        }
        if (ObjectUtils.isEmpty(post.getContent())) {
            throw new BadRequestException("No CONTENT exists.");
        }
    }

    @ExceptionHandler
    public ResponseEntity<Response<String>> badRequest(BadRequestException e) {
        log.error("Bad request.., e-msg:{}", e.getMessage(), e);
        return ResponseEntity.badRequest().body(new Response<>(e.getMessage()));
    }
}

 

스웨거로 보면 다음처럼 나오게 됩니다.

다이어리 API 스웨거

 

스웨거를 이용해서 기능 테스트를 한번 해보겠습니다.

 

 

 

현재 구현된 기능은 모두 정상적으로 동작하는 것 까지 확인했습니다.

 

여기까지 작업하면서 쉽게 풀리지 않았던 부분은 몽고DB를 처음 사용하다보니 몽고DB 클라이언트를 이용한 CRUD만드는 부분이었습니다. 기존 RDS와는 다르게 ObjectID 로 핸들링을 해야하는 부분이 있었고, auto increment pk 설정을 따로 할 수 없었습니다. 만약 게시글의 번호가 필요하다면 어떻게 데이터를 저장해야할지 고민이 좀 되는 부분입니다. 각 게시글마다 시퀀스값을 넣어줘야하는데 구글링해서 나오는 것들은 synchronized 가 안될 것 처럼 보여서 테스트도 좀 해봐야 할 것 같습니다.

 

여기까지 백엔드의 기본적인 구현을 완료했습니다.

다음 포스팅에서는 프론트와 백엔드를 연결하는 부분에 대해서 올리겠습니다.

💻 Programming

[몽고DB] 기본 쉘 명령어

MongoDB 기본명령어

 

- 현재 사용중인 데이터베이스 확인

> db

- 데이터베이스 목록 조회

> show dbs

admin 0.000GB

config 0.000GB

local 0.000GB

 

- 데이터베이스 변경

(존재하지 않는 새 데이터베이스로도 변경가능, 실제 생성은 컬렉션이 추가될 때 되는 듯)

> use blogapp

switched to db blogapp

> show dbs

admin 0.000GB

config 0.000GB

local 0.000GB

 

- 현재 데이터베이스의 컬렉션 목록 조회

> show collections

diary_life

 

- 컬렉션 및 데이터 추가 (데이터는 json format으로 넣는다)

db.<collection>.insertOne(<데이터>);

> db.diary_life.insertOne({title: "테스트", content:"테스트 게시글입니다"});

{ "acknowledged" : true, "insertedId" : ObjectId("5fb9a86c88b63c276fc5d50b") }

 

- 컬렉션의 모든 데이터 조회 (find메서드에 empty doc을 파라미터로 전달)

> db.diary_life.find({});

{ "_id" : ObjectId("5fb9a86c88b63c276fc5d50b"), "title" : "테스트", "content" : "테스트 게시글입니다" }

 

- 특정 데이터 업데이트

> db.diary_life.update({"title":"테스트"}, {title: "테스트", content:"업데이트완료"});

WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

> db.diary_life.find({});

{ "_id" : ObjectId("5fb9a86c88b63c276fc5d50b"), "title" : "테스트", "content" : "업데이트완료" }

 

- 컬렉션의 doc 개수 조회

> db.diary_life.count();

1

 

- 특정 데이터 삭제

> db.diary_life.deleteOne({"title":"테스트"});

{ "acknowledged" : true, "deletedCount" : 1 }

> db.diary_life.count(); 0

> db.diary_life.find({});

>

 

 

(참고: https://docs.mongodb.com/manual/reference/method/ )

아래 링크로 들어가서 오른쪽 하단의 Download 버튼을 클릭하여 tgz 파일을 다운로드 받아 압축을 풀면 설치가 완료된다.

 

몽고DB 커뮤니티 서버 최신버전 다운로드

 

 

몽고디비 최신버전은 자바버전 8 이상이면 사용가능하다.

추가적인 호환성 여부는 몽고DB 공식사이트에서 확인가능하다.

현재 사용하고 있는 자바 버전이 8이 아니라면 업그레이드가 필요하다.

몽고DB 드라이버 호환성 여부
몽고DB 자바버전 호환성 여부


몽고DB서버 실행하기 공식문서를 보고 따라하면되는데 그대로 따라하면 중간에 오류가 발생한다.

아래 명령어를 보고 실행하되 경로는 본인의 입맛에 맞게 변경해주면 된다.

keichee:BlogApp KH$ sudo mkdir -p /usr/local/mongodb/data-v4.4
Password:
keichee:BlogApp KH$ sudo mkdir -p /usr/local/mongodb/log
keichee:BlogApp KH$ sudo chown KH /usr/local/mongodb/*
keichee:BlogApp KH$ touch /usr/local/mongodb/log/mongo.log

-- 몽고db bin 디렉토리로 이동 --
keichee:bin KH$ ./mongod --dbpath /usr/local/mongodb/data-v4.4 --logpath /usr/local/mongodb/log/mongo.log --fork
about to fork child process, waiting until server is ready for connections.
forked process: 38133
child process started successfully, parent exiting
keichee:bin KH$ 

 

이제 서버는 정상적으로 기동이 되었고, 실제로 접속해서 명령어를 입력하려면 mongo 명령어를 실행하면 됩니다.

keichee:bin KH$ ./mongo
MongoDB shell version v4.4.2
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("b8192555-a975-457e-ab16-2b2c7675c72a") }
MongoDB server version: 4.4.2
---
The server generated these startup warnings when booting: 
        2020-11-22T08:12:57.220+09:00: Access control is not enabled for the database. Read and write access to data and configuration is unrestricted
        2020-11-22T08:12:57.221+09:00: This server is bound to localhost. Remote systems will be unable to connect to this server. Start the server with --bind_ip <address> to specify which IP addresses it should serve responses from, or with --bind_ip_all to bind to all interfaces. If this behavior is desired, start the server with --bind_ip 127.0.0.1 to disable this warning
        2020-11-22T08:12:57.221+09:00: Soft rlimits too low
        2020-11-22T08:12:57.221+09:00:         currentValue: 10240
        2020-11-22T08:12:57.221+09:00:         recommendedMinimum: 64000
---
---
        Enable MongoDB's free cloud-based monitoring service, which will then receive and display
        metrics about your deployment (disk utilization, CPU, operation statistics, etc).

        The monitoring data will be available on a MongoDB website with a unique URL accessible to you
        and anyone you share the URL with. MongoDB may use this information to make product
        improvements and to suggest MongoDB products and deployment options to you.

        To enable free monitoring, run the following command: db.enableFreeMonitoring()
        To permanently disable this reminder, run the following command: db.disableFreeMonitoring()
---
>