/**

* private key 내용을 얻어옴

* - -----BEGIN PRIVATE KEY----- 또는 -----END PRIVATE KEY----- 와 같은 가이드라인 줄은 제외하고 실제 사용하는 부분만 파일에서 가져옴

*

* @param privateKeyFile

* @return

*/

private String getPrivateKeyBody(MultipartFile privateKeyFile) {

 

try (BufferedReader br = new BufferedReader(new InputStreamReader(privateKeyFile.getInputStream()))) {

 

String line;

StringBuilder sb = new StringBuilder();

while ((line = br.readLine()) != null) {

if (line.contains("PRIVATE KEY")) { //guard line은 pass

continue;

}

sb.append(line);

}

 

return sb.toString();

 

} catch (Exception e) {

log.error(e.getMessage(), e);

throw new RuntimeException(e.getMessage(), e);

}

}

 

 

 

 
참고 링크
 
  1. 배경 및 목적
    1. 요즘 MSA 등의 형태로 서비스를 만들다보니 외부 API호출하는 경우가 많음(또는 AWS 서비스 호출)
    2. 네트웤 문제나 스로틀링 등의 문제로 가끔 실패가 발생하는 경우가 있음. 실패가 발생할 경우 재 처리가 필요하여 사용
 
 
 
예제 소스 (from AWS)
  •  지수 백오프의 기본 아이디어는 오류 응답이 연이어 나올 때마다 재시도 간 대기 시간을 점진적으로 늘린다는 것.
  • 최대 지연 간격과 최대 재시도 횟수를 구현해야 함. 최대 지연 시간 간격과 최대 재시도 횟수는 고정 값일 필요가 없으며, 수행 중인 작업과 다른 로컬 요인(예: 네트워크 지연 시간)을 기반으로 설정
java 버전의 재 시도 대기시간 구하는 메소드 샘플
* 참고로 jitter를 적용하는게 좋음(위 aws글 참고
Thread.sleep(RetryUtil.getRetrySleepTime(retryCnt, 2, 1000, 2000)); //대기처리

	
/**
* 재 시도시 대기시간(sleep) 구하기
*  - 지수 백오프 알고리즘 기반
*
* @param retryCount 요청하는 재시도 횟수
* @param increaseBase 증가 처리의 지수의 밑(ex. 2^4 에서 2)
* @param increaseMillis 매 재 요청마다 증가하는 시간(밀리시간초)
* @param maxWaitMillis 최대 대기시간(밀리시간초). 매 증가하는 대기 시간은 최대 대기시간을 넘을 수 없음
* @return
*/
public static long getRetrySleepTime(int retryCount, double increaseBase, long increaseMillis, long maxWaitMillis) {

    long waitTime = ((long)Math.pow(increaseBase, retryCount) * increaseMillis); //ex) ((long)Math.pow(2, retryCount) * 100L);  => 100, 200, 400, 800, 1600과 같이 증가함
    return Math.min(waitTime, maxWaitMillis); //두번째 argment은 최대 대기시간(밀리세컨드)
}



 
AWS 샘플 소스
public enum Results {
    SUCCESS,
    NOT_READY,
    THROTTLED,
    SERVER_ERROR
}
 
 
/*
* Performs an asynchronous operation, then polls for the result of the
* operation using an incremental delay.
*/
public static void doOperationAndWaitForResult() {
 
 
    try {
        // Do some asynchronous operation.
        long token = asyncOperation();
 
 
        int retries = 0;
        boolean retry = false;
 
 
        do {
            long waitTime = Math.min(getWaitTimeExp(retries), MAX_WAIT_INTERVAL);
 
 
            System.out.print(waitTime + "\n");
 
 
            // Wait for the result.
            Thread.sleep(waitTime);
 
 
            // Get the result of the asynchronous operation.
            Results result = getAsyncOperationResult(token);
 
 
            if (Results.SUCCESS == result) {
                retry = false;
            } else if (Results.NOT_READY == result) {
                retry = true;
            } else if (Results.THROTTLED == result) {
                retry = true;
            } else if (Results.SERVER_ERROR == result) {
                retry = true;
            }
            else {
                // Some other error occurred, so stop calling the API.
                retry = false;
            }
 
 
        } while (retry && (retries++ < MAX_RETRIES));
    }
 
 
    catch (Exception ex) {
    }
}
 
 
/*
* Returns the next wait interval, in milliseconds, using an exponential
* backoff algorithm.
*/
public static long getWaitTimeExp(int retryCount) {
 
 
    long waitTime = ((long) Math.pow(2, retryCount) * 100L);
 
 
    return waitTime;
}
 

 

import java.util.Properties;
import javax.mail.Flags;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.URLName;
import javax.mail.search.FlagTerm;
/**
 * IMAP 이메일을 가져오는 샘플 소스
 *  - 주의: 간단히 POC한 소스라서 예외처리 및 부가처리는 완벽히 안되어 있음
 *
 * @author 엄승하
 */
public class IMAPEmailGetSample {
     public static void main(String[] args) throws Exception {
          System.out.println("-- IMAP Emal 가져오기: Start\n\n");
          String host = "입력필요"; //imap 호스트 주소. ex) imap.gmail.com
          String userEmail = "입력필요"; //유저 이메일 주소
          String password = "입력필요"; //유저 암호
          IMAPMailService mailService = new IMAPMailService();
          mailService.login(host, userEmail, password);
          int messageCount = mailService.getMessageCount();
          //테스트 목적이라서 5개 초과이면 5개만 처리: TODO 삭제
          if (messageCount > 5) {
               messageCount = 5;
          }
          Message[] msgArray = mailService.getMessages(false);
          for (int i = 0; i < messageCount; i++) {
               Message msg = msgArray[i];
               if (msg.getSubject() != null) {
                    System.out.println(String.format("컨텐츠타임: %s", msg.getContentType()));
                    System.out.println(String.format("발신자[0]: %s", msg.getFrom()[0]));
                    System.out.println(String.format("메일제목: %s", msg.getSubject()));
                    String mailText = mailService.getEmalText(msg.getContent());
                    System.out.println(String.format("메일내용: %s", mailText));
               }
          }
          mailService.logout(); //로그아웃
          System.out.println("\n\n-- IMAP Emal 가져오기: 종료");
     }
}
/**
 * IMAP 관리 inner클래스
 *  - 참고: https://javapapers.com/java/receive-email-in-java-using-javamail-gmail-imap-example/
 * 
 * @author 엄승하
 */
class IMAPMailService {
     private Session session;
     private Store store;
     private Folder folder;
     // hardcoding protocol and the folder
     // it can be parameterized and enhanced as required
     private String protocol = "imaps";
     private String file = "INBOX";
     public IMAPMailService() {
     }
     public boolean isLoggedIn() {
          return store.isConnected();
     }
     /**
      * 메일 본문 텍스트 내용을 가져옴
      *
      * @param content
      * @return
      * @throws Exception
      */
     public String getEmalText(Object content) throws Exception {
          //TODO: 개발 필요
          System.out.println("####  컨텐츠 타입에 따라서 text body 또는 멀티파트 처리 기능 구현이 필요");
          if (content instanceof Multipart) {
               System.out.println("Multipart 이메일임");
          } else {
               System.out.println(content);
          }
          return null;
     }
     /**
      * to login to the mail host server
      */
     public void login(String host, String username, String password) throws Exception {
          URLName url = new URLName(protocol, host, 993, file, username, password);
          if (session == null) {
               Properties props = null;
               try {
                    props = System.getProperties();
               } catch (SecurityException sex) {
                    props = new Properties();
               }
               session = Session.getInstance(props, null);
          }
          store = session.getStore(url);
          store.connect();
          folder = store.getFolder("inbox"); //inbox는 받은 메일함을 의미
          //folder.open(Folder.READ_WRITE);
          folder.open(Folder.READ_ONLY); //읽기 전용
     }
     /**
      * to logout from the mail host server
      */
     public void logout() throws MessagingException {
          folder.close(false);
          store.close();
          store = null;
          session = null;
     }
     public int getMessageCount() {
          //TODO: 안 읽은 메일의 건수만 조회하는 기능 추가
          int messageCount = 0;
          try {
               messageCount = folder.getMessageCount();
          } catch (MessagingException me) {
               me.printStackTrace();
          }
          return messageCount;
     }
     /**
      * 이메일 리스트를 가져옴
      *
      * @param onlyNotRead 안읽은 메일 리스트만 가져올지 여부
      * @return
      * @throws MessagingException
      */
     public Message[] getMessages(boolean onlyNotRead) throws MessagingException {
          if (onlyNotRead) {
               return folder.search(new FlagTerm(new Flags(Flags.Flag.SEEN), false));
          } else {
               return folder.getMessages();
          }
     }
}
메모 목적의 글입니다.

19년 9월 기준으로 기존에 자주 사용하던 apache http 컴포넌트에서는 http 2를 정식으로 지원하지 않는 것 같음
현재 개발 기술 스택인 java+spring 환경에서, 애플 APNS와 통신시 http 2로 통신해야해서 통신 모듈을 변경함(java도 가능하면 11로 올리세요. 8에서는 문제 있음)


  1. okhttp 디펜더시 추가
        <dependency>
               <groupId>com.squareup.okhttp3</groupId>
               <artifactId>okhttp</artifactId>
               <version>4.1.0</version>
          </dependency>

  1. Spring restTemplate에 셋팅
     
    private RestTemplate restTemplate = new RestTemplate();
     
     @PostConstruct //init 시점 및 방법은 선호하는 방법에 따라서 셋팅
     private void init() {
          // @formatter:off
          OkHttpClient client = new OkHttpClient.Builder()
               .readTimeout(5, TimeUnit.SECONDS)
               .connectTimeout(7, TimeUnit.SECONDS)
               .connectionPool(new ConnectionPool(30, 10, TimeUnit.MINUTES)) //커넥션풀 적용
               .build();
          // @formatter:on
          OkHttp3ClientHttpRequestFactory crf = new OkHttp3ClientHttpRequestFactory(client);
          restTemplate.setRequestFactory(crf);
     }


  1. 사용하는 소스 예(일부분)

               HttpHeaders headers = new HttpHeaders();
               headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
               //ex) Bearer test.test.test
               headers.set("Authorization", authorization);
               headers.set("apns-id", apnsId); //A canonical UUID that identifies the notification. example UUID is as follows:123e4567-e89b-12d3-a456-42665544000
              
             //If the value is 0, APNs treats the notification as if it expires immediately and does not store the notification or attempt to redeliver it.
               headers.set("apns-expiration", "0");
               headers.set("apns-priority", "10"); //The priority of the notification. 10은 즉시, 5는 배터리상태 고려해서 발송
               headers.set("apns-topic", apnsTopic);
               log.debug("IOS APNS푸시 요청headers\n{}", headers);
              
             Map<String, Object> bodyMap = new HashMap<String, Object>();
               bodyMap.put("aps", aPNSPayloadAPS);
               String bodyJson = OM.writeValueAsString(bodyMap);
               log.debug("IOS APNS푸시 요청bodyJson\n{}", bodyJson);
               
            //http 200 응답이면 API요청 성공
               ResponseEntity<String> rslt = restTemplate.exchange(apiURL, HttpMethod.POST, new HttpEntity<String>(bodyJson, headers), String.class);
               log.debug("IOS APNS 푸시발송 요청 리턴 결과\n{}", rslt);

  • 주의
    • 해당 설정은 개인 프로젝트의 디렉토리 경로 등에 디펜더시되어 있으며, 서비스 환경에서 로그 출력시 성능이 저하될 수 있으니 개발 환경에서 디버깅 용으로만 사용

  1. maven 라이브러리 추가
        <!-- sql로그를 남기기 위한 라이브러리 -->
        <dependency>
               <groupId>org.bgee.log4jdbc-log4j2</groupId>
                <artifactId>log4jdbc-log4j2-jdbc4.1</artifactId>
               <version>1.16</version>
          </dependency>

  1. jdbc driverClassName을 "net.sf.log4jdbc.sql.jdbcapi.DriverSpy"로 변경
  2. resources 디렉토리에 "log4jdbc.log4j2.properties" 파일 추가 후 아래 내용 작성
log4jdbc.spylogdelegator.name =  net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
#sql문을 최대 몇 라인까지 출력할지 여부: 0은 제한 없음
log4jdbc.dump.sql.maxlinelength=0

  1. logback  설정에 출력 및 미출력할 로그 어펜더 정의(샘플)
<logger name="jdbc" level="OFF"/>
    <logger name="jdbc.audit" level="OFF"/>
    <logger name="jdbc.connection" level="OFF"/>
    
    <!-- SQL문과 해당 SQL을 실행시키는데 수행된 시간  정보(milliseconds)를 포함한다. -->  
    <logger name="jdbc.sqltiming" additivity="false">
        <level value="DEBUG" />
        <appender-ref ref="CONSOLE" />
    </logger>
    
      <!-- SQL 결과 조회된 데이터의 table을 로그로 남긴다 -->
     <logger name="jdbc.resultsettable" additivity="false">
        <level value="DEBUG" />
        <appender-ref ref="CONSOLE" />
    </logger>
    
        <!-- SQL문만을 로그로 남기며, PreparedStatement일 경우  관련된 argument 값으로 대체된 SQL문이 보여진다 -->
<!--      <logger name="jdbc.sqlonly" additivity="false">  -->
<!--         <level value="DEBUG" /> -->
<!--         <appender-ref ref="CONSOLE" /> -->
<!--     </logger> -->
    <!-- ResultSet을 포함한 모든 JDBC 호출 정보를 로그로 남기므로  매우 방대한 양의 로그가 생성된다 -->
<!--      <logger name="jdbc.resultset" additivity="false">  -->
<!--         <level value="DEBUG" /> -->
<!--         <appender-ref ref="CONSOLE" /> -->
<!--     </logger> -->


tda를 이용했었는데 온라인 사이트도 존재함


https://fastthread.io/index.jsp

pinpoint 설치 방법- 1.8.4 버전 설치방법 메모

서비스 일부가 JAVA 11을 사용하게 되어서 Pinpoint 버전업이 필요하게 됨(Pinpoint 1.8.3 부터인가 java 11을 지원함)



유저홈/apps 디렉토리를 기준으로 설치 함

#디렉토리 생성
mkdir ~/apps && cd ~/apps

#다운로드 후 압축 해제, 심볼릭 링크 처리
wget 'https://github.com/naver/pinpoint/archive/1.8.4.tar.gz' && tar -xzf 1.8.4.tar.gz && rm 1.8.4.tar.gz && ln -s pinpoint-1.8.4 pinpoint


#인스톨(JAVA 6,7,8, 9 필요)
cd ~/apps/pinpoint && ./mvnw install -Dmaven.test.skip=true

#참고: JAVA설치
* 아래 wget이 인증 만료로 진행되지 않는다면, 각 다운로드 페이지에 로그인 후 fiddler를 통해 다운로드 URL을 구하면 됨

cd ~/apps

wget 'http://download.oracle.com/otn/java/jdk/6u45-b06/jdk-6u45-linux-x64.bin?AuthParam=1565679254_d8f8f965c05a5c02f7b05dcf12cd591a' -O  ~/apps/jdk-6u45-linux-x64.bin
chmod 755 jdk-6u45-linux-x64.bin && ./jdk-6u45-linux-x64.bin && rm jdk-6u45-linux-x64.bin
ln -s jdk1.6.0_45 jdk_6

wget 'http://download.oracle.com/otn/java/jdk/7u80-b15/jdk-7u80-linux-x64.tar.gz?AuthParam=1565679323_817db680a1f10b44f3ea966c24fda0fb' -O  ~/apps/jdk-7u80-linux-x64.tar.gz
tar -xzf jdk-7u80-linux-x64.tar.gz && rm jdk-7u80-linux-x64.tar.gz
ln -s jdk1.7.0_80 jdk_7

wget 'http://download.oracle.com/otn/java/jdk/8u221-b11/230deb18db3e4014bb8e3e8324f81b43/jdk-8u221-linux-x64.tar.gz?AuthParam=1565679401_a0a5d03078d4a324b9376891541aae33' -O ~/apps/jdk-8u221-linux-x64.tar.gz
tar -xzf jdk-8u221-linux-x64.tar.gz && rm jdk-8u221-linux-x64.tar.gz && ln -s ~/apps/jdk1.8.0_221 jdk && ln -s ~/apps/jdk1.8.0_221 jdk_8

wget 'http://download.oracle.com/otn/java/jdk/9.0.4+11/c2514751926b4512b076cc82f959763f/jdk-9.0.4_linux-x64_bin.tar.gz?AuthParam=1565679522_413bd7c661de37ea7e7059de455f0b5c' -O ~/apps/jdk-9.0.4_linux-x64_bin.tar.gz
tar -xf jdk-9.0.4_linux-x64_bin.tar.gz  && rm jdk-9.0.4_linux-x64_bin.tar.gz && ln -s ~/apps/jdk-9.0.4 jdk_9


#환경변수 등록
vi ~/.bashrc 후에

export JAVA_HOME=~/apps/jdk
export JAVA_9_HOME=~/apps/jdk_9
export JAVA_8_HOME=~/apps/jdk_8
export JAVA_6_HOME=~/apps/jdk_6
export JAVA_7_HOME=~/apps/jdk_7
export PATH=$JAVA_HOME/bin:$PATH

source ~/.bashrc

#참고 : JAVA DNS TTL Modify
echo 'networkaddress.cache.ttl=60' >> ~/apps/jdk/jre/lib/security/java.security



#참고 : OS 디스크외 AWS EBS를 추가로 마운트해서 데이터를 저장한다면 pinpoint data디렉토리 변경(/data에 EBS 마운트)
cd ~/apps/pinpoint/quickstart && ln -s /data/pinpoint data
sudo su
mkdir /data/pinpoint && chown 유저계정:유저계정 /data/pinpoint



Hbase 설치 및 시작
-- HBase 저장기간을 줄임(1일=86400초로 줄이는데 상황에 따라서 적당한 수치로 조정)
 vi ./quickstart/conf/hbase/init-hbase.txt 후 아래 입력(2일간 데이터 보관)
 :%s/5184000/172800/g

 :%s/5184000/259200/g  <<3일로 설정할 경우

-- Hbase 다운로드 및 시작
./quickstart/bin/start-hbase.sh

-- Hbase 테이블 초기화
./quickstart/bin/init-hbase.sh



Pinpoint 데몬들 설정 후 시작

-- 컬렉터로그 레벨을 조정해서 적게 남기도록 함
vi ./quickstart/collector/src/main/resources/log4j.xml
:%s/DEBUG/INFO/g
:%s/TRACE/INFO/g

-- pinpoint 웹의 로그레벨 조정
vi ./quickstart/web/src/main/resources/log4j.xml
:%s/DEBUG/INFO/g

-- 컬렉터(데이터 수집 프로세스)  시작
./quickstart/bin/start-collector.sh

-- 웹 UI 시작
./quickstart/bin/start-web.sh


agent 수정(해당 agent가 실제 어플리케이션 시작시 사용됨)

agent 홈 경로 : ~/apps/pinpoint/agent/target/pinpoint-agent-버전

cd ~/apps/pinpoint/agent/target/pinpoint-agent-버전

-- agent 로그 조정( 미 조정시 DEBUG레벨로 로그가 남아서 대상 프로그램의 성능 하향이 발생)
vi lib/log4j.xml 후 아래 명령 수행(로그레벨 DEBUG를 INFO로 조정)
:%s/DEBUG/INFO/g

아래내용은 삭제
<appender-ref ref="console" />

-- pinpoint.config파일에서 Collector server의 IP 및 포트 수정


vi ./quickstart/agent/src/main/resources/pinpoint.config
cd ~/apps/pinpoint/agent/target/pinpoint-agent-1.7.0-SNAPSHOT && vi pinpoint.config

-- 어플리케이션 서버로 복사를 위해서 압축해둠(예)
ex) tar -czf pinpoint-agent-1.7.0-SNAPSHOT_171023.tar.gz pinpoint-agent-1.7.0-SNAPSHOT

이후 /home/integtool/apps/pinpoint/quickstart/web/target/deploy 경로에 파일을 복사하면 웹에서 다운로드 가능



이후 APM으로 모니터링하고 싶은 서버에 Agent 복사 후 프로세스 실행시 agent 사용하도록 셋팅
 - 방법은 정리 예정이며 필요시 구글 검색하세요





Spring API 개발시 예외(에러) 처리 방법을 메모합니다. 실제로 회사 등의 프로젝트에서 제가 선호하는 방식입니다.

 -  특정 케이스, 레거시의 하위호환성을 유지해야하는 경우는 어쩔수 없지만ㅠ (ex. 갑에서 에러도 200으로 응답해달라고 요청)


[참고 링크]

1. 트위터 : https://developer.twitter.com/en/docs/basics/response-codes

2. 카카오 : https://developers.kakao.com/docs/restapi/quick-reference#응답-코드

3. 기타

  - 어떤분이 내가 사용하는 방법과 유사하게 Spring 기반으로 정리해두신 링크입니다.

  - 다른점은, 저는 다른 개발자와 커뮤니케이션& 개발자가 정의서를 따로 보지 않도록 하기 위해서 에러코드를 숫자보다 문자로 처리하는걸 선호합니다.

  - https://cheese10yun.github.io/spring-guide-exception/#undefined



+ Recent posts