Oct 23, 2020 - setEnv

layout: post
title: "crontab -e 로 java를 실행하기"
date: 2010-12-28 09:15:00 +0900
comments: true

2010년 crontab 으로 java를 실행시키기라는 글을 올린 적이 있습니다.
10년이 지났지만, 참 유용하게 사용합니다.

사람의 기억력은 미화하거나, 잊혀지지만,
잘 정리된 글은 필요할 때, 다시 봐도 항상 그 자리에서 저를 도와줍니다.

스프링부트의 임베디드 톰캣이 활성화(?)되기 전에,
모든 자바 어플리케이션은 tomcat을 별도 구성하고,
별도의 tomcat 폴더의 bin 에 있는 catalina.sh 을 실행하여야했습니다.

순수 설정을 정의하고자할 때, CATALINA_OPTS 같은 변수를 export 해야만 했습니다.
예를 들어, jconsole 등을 사용하기 위해 xmxremote 등을 설정해야했고,
scouter 를 하기 위해서는 javaagent 등의 독자적인 설정을 각 sh 파일에 export 해야했습니다.

요근래 스프링부트로 단독으로 올리게 되어,
아예 start 와 stop shell 을 구성하게 되었습니다.

#!/bin/sh or #!/bin/bash

SCOUTER_BASE=/home/xxx/scouter
export XXX_OPTS="-Duser.timezone=Asia/Seoul -javaagent:$SCOUTER_BASE/scouter.agent.jar -verbosegc -server -Xms1g -Xmx1g -Duser.timezone=GMT+09:00 -Dscouter.config=./scouter.conf"

scouter 로 트래픽 및 성능 및 모니터링을 해야하므로,
기본 설정값으로 setEnv.sh 을 구성하였고,
각각의 start, stop 의 쉘을 구성하였습니다.

#!/bin/sh

PID="$(ps -ef|grep java | grep -i xxx | grep -i 8082  | grep -v grep | awk '{print $2}')"
if [[ "" !=  "$PID" ]]; then
  echo "killing $PID"
  kill -s TERM $PID
fi

특정 ‘xxx’ 의 해당 port가 떠있으면 kill 한 후,

#!/bin/bash
usage=`ps -ef | grep java | grep toDsp | grep '8082' | wc -l`
echo $usage

DEFAULT_BASE=/home/xxx/data
. $DEFAULT_BASE/setEnv.sh

if [ $usage -lt 1 ];  then
        echo "ssp to Dsp  재 실행!";
        nohup java -jar -server -XX:+PrintGCDetails $XXX_OPTS -Xloggc:/home/xxx/log/gc/xxxx/gc8082.log -Dspring.profiles.active=prod -Dserver.port=8082 ../xxx-0.0.1-SNAPSHOT.jar 1>/dev/null 2>&1 &

else
        echo "ssp to Dsp 실행 중!";
fi

해당 서비스의 실행하도록 구축하였습니다.
gc 를 모니터링하기 위해 PrintGCDetails, loggc 를 정의하였는데,
조금더 깔끔한 부분이 있는 지 고민해봐야할 거 같습니다.

Oct 21, 2020 - https PKIX path building failed:

https PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target; nested exception is javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

nginx 에서, 무중단 서비스를 구성하기 위해,

upstream xxxapp {
        server 127.0.0.1:8080;
        server 127.0.0.1:8090;
    }

upstream 을 여러개 두어, 한 서버에 두개의 어플리케이션이 동작하도록 구현이 되어있습니다.
해당 서비스는, 내부 서버에 호출을 하는데, 내부 호출을 https 로 호출시 발생하는 문제로 파악되었습니다.

내부 로컬에서 내부 로컬로 호출할 시, 굳이 443 으로 호출할 필요가 없을 듯 하여,

		server {
        listen  80;
        server_name  localhost;
        location /xxx {
                rewrite ^/xxx(/.*)$ $1 break;
                proxy_pass http://xxxapp;
        }
    }

    server {
        listen  443 ssl http2;
        server_name  xxxx.net;
        root         /usr/share/nginx/html;
        server_tokens   off;
        ssl_certificate /etc/nginx/SSL_2020/STAR.mobon.net.crt;
        ssl_certificate_key     /etc/nginx/SSL_2020/STAR.mobon.net.key;
        ssl_session_timeout     5m;
        ssl_ciphers HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers on;
        include /etc/nginx/default.d/*.conf;
				

server 에서 443 의 별개로, 로컬시에는 80 포트의 server 를 구축하였습니다. service nginx reload 시 1s 미만의 지연이 있을 수 있다고 하는데, scouter 셋팅 시 별도의 네트워크 오류는 발견되지 못했습니다.

Oct 20, 2020 - 3Depth Ehcache redis

예전 CommonDaoPattern 이라고 하여, 프로세스는 다음과 같습니다.

  1. ehcache를 검색합니다. 2-1. ehcache에 데이터가 없을 시 레디스를 검색합니다. 2-2. ehcache에 데이터가 있을 시 ehcache의 데이터를 return 합니다. 3-1. redis에 데이터가 없을 시 DB를 검색합니다. 3-2. redis에 데이터가 있을 시 ehcache에 해당 데이터를 넣고 데이터를 return 합니다. 4-1. DB에 검색한 데이터가 없을 시 5분간 해당 primary key를 캐시에 담아 return null 시킵니다. 4-2. DB에 검색한 데이터가 있을 시, 레디스랑 ehcache에 해당 데이터를 담습니다.
public class CommonDaoPattern<T1> {
	private static final Logger LOG = LoggerFactory.getLogger(CommonDaoPattern.class);
	protected static final Random random = new Random();
	private CommonEHCacheManager<T1> cacheManager;
	private CommonRedisManager<T1> redisManager;
	private static final Cache emptyKeyFromDB;

	static {
		emptyKeyFromDB = CacheManager.create().getCache(EHCacheConstants.EMPTY_KEY_CACHE);
	}

	public CommonDaoPattern(CommonEHCacheManager<T1> cacheManager, CommonRedisManager<T1> redisManager) {
		this.cacheManager = cacheManager;
		this.redisManager = redisManager;
	}

	public CommonDaoPattern(CommonEHCacheManager<T1> cacheManager) {
		this.cacheManager = cacheManager;
	}

	public CommonDaoPattern(CommonRedisManager<T1> redisManager) {
		this.redisManager = redisManager;
	}

	public CommonDaoPattern() {
	}

	public void setCacheManager(CommonEHCacheManager<T1> cacheManager) {
		this.cacheManager = cacheManager;
	}

	protected CommonEHCacheManager<T1> getCacheManager() {
		return this.cacheManager;
	}

	public void setRedisManager(CommonRedisManager<T1> redisManager) {
		this.redisManager = redisManager;
	}

	protected CommonRedisManager<T1> getRedisManager() {
		return this.redisManager;
	}

	protected Cache getEmptyKeyFromDB() {
		return emptyKeyFromDB;
	}

	protected boolean isDBSearch() {
		boolean result = false;
		try {
			result = PropertyHandler.getBoolean("USE_LOCAL_QUERYING");
		} catch (Exception e) {
			if (LOG.isDebugEnabled())
				LOG.debug("{}", e.getStackTrace().toString());
		}
		return result;
	}

	public T1 retrieveData(String key) {
		return retrieveData(key, null);
	}

	public <T2> T1 retrieveData(String key, T2 t2) {
		T1 returnObject = null;
		try {

			SEARCH_TYPE searchType = SEARCH_TYPE.NOTHING;

			if (!commonValidation(key) || !doValidation(key, t2)) {
				if (LOG.isDebugEnabled())
					LOG.debug("validation is failed... please check >>>key:{}, DBdata:{}", key, t2);
				return null;
			}

			// 1. get from EHcache
			if (cacheManager != null) {
				searchType = SEARCH_TYPE.CACHE;
				returnObject = cacheManager.selFromCache(key);
			}

			// 2. get from REDIS
			if (redisManager != null && returnObject == null) {
				searchType = SEARCH_TYPE.REDIS;
				returnObject = redisManager.get(key);
			}

			// 3. fetch from DB
			if (returnObject == null) {
				searchType = SEARCH_TYPE.DB;
				try {
					returnObject = t2 == null ? selFromDB() : selFromDB(t2);
					if (returnObject == null)
						returnObject = t2 == null ? selFromDB(key) : selFromDB(key, t2);

					if (returnObject == null) {
						String emptyKey = _generateEmptyKey(key);
						if (emptyKey != null) {
							emptyKeyFromDB.put(new Element(emptyKey, null));
							if (LOG.isDebugEnabled())
								LOG.debug("empty key is put into EHCache>>>{}", emptyKey);
						}
					}
				} catch (Exception e) {
					LOG.error("key>>> {}, {}", key, t2, e);
				}
			}

			// 4. restore data to ehcache, Redis
			if ((searchType == SEARCH_TYPE.REDIS || searchType == SEARCH_TYPE.DB) && cacheManager != null && returnObject != null) {
				cacheManager.storeIntoCacheWithKey(key, returnObject);
			}

			if (searchType == SEARCH_TYPE.DB && redisManager != null && returnObject != null) {
				redisManager.set(key, returnObject);
			}

		} catch (Exception e) {
			e.printStackTrace();
			LOG.error(ErrorLog.getStack(e, String.format("key>>>%s, %s", key, t2)));
		}
		return returnObject;
	}

	public void loadData(String key, T1 t1) {
		try {
			// 1. load to EHcache
			if (cacheManager != null) {
				cacheManager.storeIntoCacheWithKey(key, t1);
				LOG.debug("complete load to CACHE");					
			}

			// 2. load to REDIS
			if (redisManager != null) {
				redisManager.set(key, t1);
				LOG.debug("complete load to REDIS");					
			}

		} catch (Exception e) {
			LOG.error(ErrorLog.getStack(e, ""));
		}
	}

	/**
	 * DB에서 데이터를 조회한뒤 캐시와 레디스에 로딩한다.
	 *
	 * @param key
	 * @return
	 */
	public <T2> void loadDataFromDB(String key, T2 t2) {
		try {
			T1 t1 = selFromDB(t2);
			// 1. load to EHcache
			if (cacheManager != null) {
				cacheManager.storeIntoCacheWithKey(key, t1);
				LOG.debug("complete load to CACHE");
			}

			// 2. load to REDIS
			if (redisManager != null) {
				redisManager.set(key, t1);
				LOG.debug("complete load to REDIS");
			}

		} catch (Exception e) {
			LOG.error("error key={}", key, e);
		}
	}

	public <T2> void loadRedisDataFromDB(String key, T2 t2) {
		try {
			T1 t1 = selFromDB(key, t2);
			if (redisManager != null) {
				redisManager.set(key, t1);
				LOG.debug("complete load to REDIS");
			}
		} catch (Exception e) {
			LOG.error("error key={}", key, e);
		}
	}

	protected boolean doValidation(String key, Object t2) {
		return true;
	}

	// key 체크는 필수로 한다. empty key 가 있는지 체크한다.
	protected boolean commonValidation(String key) {
		if (StringUtils.isEmpty(key))
			return false;
		if (emptyKeyFromDB.isKeyInCache(_generateEmptyKey(key)))
			return false;
		return true;
	}

	// 디비 로직을 쓴다면 반드시 오버라이딩해서 구현.
	public T1 selFromDB() throws SQLException {
		return null;
	}

	// 디비 로직을 쓴다면 반드시 오버라이딩해서 구현.
	public T1 selFromDB(Object obj) throws SQLException {
		return null;
	}

	public T1 selFromDB(String key) throws SQLException {
		return null;
	}

	public T1 selFromDB(String key, Object obj) throws SQLException {
		return null;
	}

	protected String _generateEmptyKey(String key) {
		if (cacheManager != null) {
			return cacheManager.getCacheName() + GlobalConstants.GUBUN + key;
		} else if (redisManager != null) {
			return redisManager.generateKey(key);
		}
		return null;
	}
}

해당 클래스를 처리하기 위해 selFromDB 를 overide 받아 쿼리를 작성하는 형태로 구현하였습니다. 3Depth 기능을 스프링부트에서 구현한다고 한다면, 다음과 같이 구성할 수 있습니다.


import org.springframework.cache.CacheManager;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class CacheConfig {

  @Bean(name = "redisCacheManager")
  public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
    return RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(defaultConf())
            .withInitialCacheConfigurations(confMap())
            .build();
  }

  @Bean(name = "ehCacheManager")
  @Primary
  public CacheManager cacheManager() {
    return new EhCacheCacheManager(ehCacheCacheManager().getObject());
  }

  @Bean
  public EhCacheManagerFactoryBean ehCacheCacheManager() {
    EhCacheManagerFactoryBean cmfb = new EhCacheManagerFactoryBean();
    cmfb.setConfigLocation(new ClassPathResource("/config/ehcache.xml"));
    cmfb.setShared(true);
    return cmfb;
  }

  private RedisCacheConfiguration defaultConf() {
    return RedisCacheConfiguration.defaultCacheConfig()
//            .serializeKeysWith(fromSerializer(new StringRedisSerializer()))
//            .serializeValuesWith(fromSerializer(new GenericJackson2JsonRedisSerializer()))
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
            .entryTtl(Duration.ofMinutes(1));
  }

  private Map<String, RedisCacheConfiguration> confMap() {
    Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
    cacheConfigurations.put("findBySspNo", defaultConf().entryTtl(Duration.ofMinutes(30L)));
    cacheConfigurations.put("findByDspList", defaultConf().entryTtl(Duration.ofMinutes(1L)));
    cacheConfigurations.put("findBycodeTpIdAndCodeId", defaultConf().entryTtl(Duration.ofMinutes(1L)));
    cacheConfigurations.put("findByGoogleSspNo", defaultConf().entryTtl(Duration.ofMinutes(30L)));
    return cacheConfigurations;
  }
}

위와 같이 redisCacheManager, ehCacheManager 을 구현한 후, @Cacheable(cacheManager = “ehCacheManager”) 를 @Cacheable(cacheManager = “redisManager”) 로 감싸주면 됩니다.