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”) 로 감싸주면 됩니다.

Sep 28, 2020 - (Spring) Spring Boot 2.0에서 lombok 으로 인한 로깅 환경 구축하기

lombok을 디펜던시에 추가했습니다.

compileOnly('org.projectlombok:lombok')

lombok에서 사용하는 log를 그대로 사용합니다.

logging:
  file:
    name: /home/xxx/log/xxxx/
  level:
    com:
      xxx:
        xxxx: INFO

특정 경로의 폴더 명을 정의합니다.
logging.file.name 의 경우, 예전에 만들어졌던 logging.file 과 동일합니다.

resources 에 logback-spring.xml 에 file.xml 과 console.xml 로 추가합니다.

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="log/console.xml" />
    <include resource="log/file.xml" />
    <root level="info">
        <springProfile name="local">
            <appender-ref ref="CONSOLE"/>
            <appender-ref ref="dailyRollingFileAppender"/>
        </springProfile>
        <springProfile name="prod">
            <appender-ref ref="dailyRollingFileAppender"/>
        </springProfile>
    </root>
</configuration>

console.xml

<?xml version="1.0" encoding="UTF-8"?>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
        <charset>UTF-8</charset>
        <pattern>[%-5p][%d{yyyy/MM/dd HH:mm:ss}] %C{1} [%L] %m %n</pattern>
    </encoder>
</appender>

file.xml

<?xml version="1.0" encoding="UTF-8"?>
<appender name="dailyRollingFileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${LOG_FILE}xxxx.log</file>
    <encoder>
        <charset>UTF-8</charset>
        <pattern>[%-5p][%d{yyyy/MM/dd HH:mm:ss}] %C{1} [%L] %m %n</pattern>
    </encoder>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>${LOG_FILE}xxxx.%d{yyyy-MM-dd}-%i.log</fileNamePattern>
        <maxHistory>30</maxHistory>
        <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
            <maxFileSize>100MB</maxFileSize>
        </timeBasedFileNamingAndTriggeringPolicy>
    </rollingPolicy>
</appender>

제가 좋아하는 패턴입니다.
패턴레이어를 보시고 본인이 원하는 패턴을 추가하시면됩니다.
패턴레이어주소
maxHistory를 30일로 설정해서 30일이 지난 로그들은 삭제됩니다.
maxFileSize를 100MB로 설정해서 로그 파일 용량이 100MB를 초과하면 자동으로 인덱스가 증가하면서 새로운 로그 파일이 생성됩니다. (시작은 0) 로 하고 %i 로 처리합니다.

2020/10/13 추가 내용 ${LOG_FILE} 말고 application.yml 에서 값을 받아오고 싶을 경우 configuration 안에,

<springProperty scope="context" name="LOAD_BALANCING" source="server.port"/>

springProperty 값을 추가하여, 처리하시면 됩니다.