HttpUrlConnection을 이용한 외부서버 통신

프로젝트 내에서 외부 library를 사용하는데 제한이 조금 있어서 Spring이 지원하는 RestTemplate 같은 건 사용할 수 없었고, 대신 java.net.HttpUrlConnection 으로 외부 서버와 통신할 수 밖에 없었다..

 

makeParams(..) :

map에 담겨온 파라미터들을 get방식 뒤에 붙이는 url?key=value 형식으로 만드는 역할을 하는 메소드.

makeJsonParams(..) :

map에 담겨온 파라미터들을 { "key" : "value" } 와 같이 json 포맷으로 만들어 주는 메소드

httpUrlConnection(..) :

실제 외부와 connection 을 하는 메소드.

header 정보를 담아 호출해야하는 경우, json 형식으로 파라미터를 넘겨야 하는 경우 등 상황에 따라 호출하는 데이터 형식 및 호출 방식이 달라지기 때문에 오버로딩하여 구현

 

소스는 아래와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
package ;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Map;
import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.stereotype.Component;
import com.google.gson.Gson;
/** @author ljpyo */
@Component("httpUtil")
public class HttpUtil {
    
    private static final Logger logger = LoggerFactory.getLogger(HttpUtil.class);
    public static final String POST = "POST";
    public static final String GET = "GET";
    public static final String DELETE = "DELETE";
    
    private String makeParams(Map<String, Object> params){
        String param = null;
        StringBuffer sb = new StringBuffer();
        
        if(params != null){
           for ( String key : params.keySet() ){
               logger.info(" key : " + key + " / value : " + params.get(key));
               sb.append(key).append("=").append((params.get(key)==null?"":params.get(key)).toString().trim()).append("&");
           }
        }
        param = sb.toString().substring(0, sb.toString().length()-1);
        return param;
    }
    
    private String makeJsonParams(Map<String, Object> params){
        String json = "";
        if(params != null){
            json = new Gson().toJson(params);
        }
        return json;
    }
    
    public String httpUrlConnection(String getpost, String targetUrl, Map<String, Object> params) throws Exception {
       String returnText = this.httpUrlConnection(getpost, targetUrl, params, nullfalse);
       return returnText;
    }
    
    public String httpUrlConnection(String getpost, String targetUrl, Map<String, Object> params, boolean isJson) throws Exception {
        String returnText = this.httpUrlConnection(getpost, targetUrl, params, null, isJson);
        return returnText;
     }
    
    public String httpUrlConnection(String getpost, String targetUrl, Map<String ,Object> params, Map<String, Object> header, boolean isJson) throws Exception {
       URL url = null;
       HttpURLConnection conn = null;
       
       String jsonData = "";
       BufferedReader br = null;
       StringBuffer sb = null;
       String returnText = "";
       JSONObject jobj = null;
       
       String postParams = "";
       
       try{
           
           if(getpost.equalsIgnoreCase(POST) || getpost.equalsIgnoreCase(DELETE)){
               url = new URL(targetUrl);
           } else if(getpost.equalsIgnoreCase(GET)){
               url = new URL(targetUrl + ((params!=null)?"?"+makeParams(params):""));
           }
           logger.info("request url : " + url);
           conn = (HttpURLConnection) url.openConnection();
//         conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
//         conn.setRequestProperty("Accept", "application/json");
           
           if(header != null){
               conn.setRequestProperty(header.get("headerKey").toString(), header.get("headerValue").toString());
               logger.info("header : " + header.get("headerKey").toString() + "  /  headerValue : " +header.get("headerValue").toString());
           }
           
           if(isJson){
               conn.setRequestProperty("Content-Type""application/json");
           }
           
           conn.setRequestMethod(getpost);
           conn.setConnectTimeout(5000);
           conn.setReadTimeout(5000);
           conn.setDoOutput(true);
           
           if(getpost.equalsIgnoreCase(POST) || getpost.equalsIgnoreCase(DELETE)){
               if(params != null){
                   if(isJson){
                       postParams = makeJsonParams(params);
                   } else {
                       postParams = makeParams(params);
                   }
                   logger.info("isJson : " + isJson);
                   logger.info("postParam.toString()  : " + postParams);
                   logger.info("post param : " + postParams.getBytes("UTF-8").toString());
                   conn.getOutputStream().flush();
               }
           } 
           
           br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
           
           sb = new StringBuffer();
           
           while((jsonData = br.readLine()) != null){
               sb.append(jsonData);
           }
           
           returnText = sb.toString();
           
           try{
               jobj = new JSONObject(returnText);
               if! jobj.has("responseCode") ){
                   jobj.put("responseCode", conn.getResponseCode());
               }
           } catch (JSONException e){
               jobj = new JSONObject();
               jobj.put("responseCode", conn.getResponseCode());
           }
           
       } catch (IOException e){
           logger.debug("exception in httpurlconnection ! ", e);
           throw new APIException("exception in httpurlconnection !");
       } finally {
           try {
               if (br != null) br.close();
           } catch(Exception e){
               logger.warn("finally..br.close()", e);
           }
           br = null;
           try {
           if(conn!=null)
               conn.disconnect();
           } catch(Exception e){
               logger.warn("finally..conn.disconnect()", e);
           }
           conn = null;
       }
       return jobj != null ? jobj.toString() : null;
    }
    
}
 
cs

 

처음엔 GET POST만 짜놓으면 될 줄 알았는데 나중에 DELETE 방식 요청을 추가적으로 구현해야했다.

POST 처럼 날리면 될 줄 알았더니 DELETE 호출방식엔 outputstream을 사용할 수 없다는 예외가 발생하여 애 좀 먹었다.. (https://developyo.tistory.com/8 참고..) 

 

1년도 안된 신입이 짠 유틸을 꽤 큰 프로젝트에서 공통으로 사용하고 있으니 불안해 죽겠다.

아직 큰 문제 없는 걸 보니 그냥 잘 돌아가고 있는듯...

 

일단 본 프로젝트에선 connectionTimeout , readTimeout Exception이 발생했을 때 재시도(retry) 없이 Customizing한 Exception을 내뱉으며 접속을 종료 시키지만

공부할 겸 retry 기능을 넣어봐야겠다.

 

추후 retry 기능을 넣어 재포스팅하겠다.

 

 

* RETRY (재시도) 설정 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package ;
 
@Component("httpUtil")
public class HttpUtil {
    
    public String httpUrlConnection(~) throws Exception {
               
       //재시도 추가 190220
       for(int i=0; i < (retry<1?1:retry); i++){
               
           try{
               //파라미터 세팅
               //호출 
               //생략~               
 
               if(conn.getResponseCode() != HttpStatus.OK){
                    //응답코드가 실패인 경우 Exception 고의로 발생(catch 에서 continue 로 처리)
                    throw new CustomException();    //customized exception 사용
              }  
              //성공은 for문 나감
              break;
             
              //응답 값 파싱
           } catch (SocketTimeoutException ste){
               errMsg = ste.getMessage();
               logger.debug(errMsg);
           } catch (CustomExceptione ce){
               errMsg = ce.getMessage();
               logger.debug(errMsg);
           } catch (Exception e){
               
           } finally {
               //자원 해제
               try {
                   if (br != null) br.close();
               } catch(Exception e){
                   logger.warn("finally..br.close()", e);
               }
               br = null;
               try {
               if(conn!=null)
                   conn.disconnect();
               } catch(Exception e){
                   logger.warn("finally..conn.disconnect()", e);
               }
               conn = null;
           }
       }
       
       if(jobj!=null){
           return jobj.toString();
       } else {
           throw new APIException(errMsg, ConstantsAPI.APIResult.E_NETWORK.getCode());
       }
    }
}
cs

 

호출받는 쪽에 connect가 오래걸려 connectTimeOut 이 나거나, connect는 되었으나 내부 처리가 오래걸려 readTimeOut이 발생한 경우 특정 횟수(n)만큼 재시도를 하도록 소스를 조금 수정했다.

(보통 한번 안되면 몇 번을 다시 시도해도 안되는 것 같지만...)

 

retry 횟수를 파라미터로 받고,

for문을 돌린다.

timeout 예외 및 응답코드가 200이 아닌 경우(CustomException)가 발생하면( SocketTimeoutException ) catch 문에서 Error 를 먹어버리고 for문이 retry 횟수만큼 이어서 진행된다.

응답코드가 200인 경우 break; 구문을 타게 되어 for문이 종료한다.

* 예외가 발생하지 않을 경우 return 하고 해당 메소드는 끝남.

 

 

요청하고 있는 외부 API 에서 HttpStatus 를 제멋대로 (보통 200을 사용) 담아 리턴하고 있어서 소스 전체를 올리진 못했다. 

(결과코드를 일일히 매핑해야했기 때문에.. 소스가 요상함)

 

여튼 for문 써서 connection 객체로부터 getResponseCode()로 연결상태 확인 하여 처리하면 된다.

반응형

jboss/wildfly 는 설치시 bin 디렉토리 밑에

서버 시작 스크립트(standalone.sh)가 존재하나, 서버 종료 스크립트가 없다.

 

jboss home directory/bin/standalone.sh 로 서버 실행 후 ctrl+c 로 서버 종료가 가능하나,

특별한 경우가 아니라면 standalone.sh & 와 같은 옵션을 주어 백그라운드에서 서버를 실행시킨다.

이 경우, 서버를 어떻게 죽여야 할까?

 

[1. 프로세스 죽이기]

ps -ef | grep wildfly 

로 프로세스 찾은 후

 

kill -9 프로세스No.

와 같이 서버를 죽일 수 있다..

하지만 뭔가 찜찜하니 매뉴얼을 읽어본다.

 

[2. jboss/wildfly 매뉴얼 따라하기]

jboss/wildfly 매뉴얼에 따르면

 

The first thing to do after the CLI has started is to connect to a managed WildFly instance. This is done using the command connect, e.g.

./bin/jboss-cli.sh
You are disconnected at the moment. Type 'connect' to connect to the server
or 'help' for the list of supported commands.
[disconnected /]
 
[disconnected /] connect
[domain@localhost:9990 /]
 
[domain@localhost:9990 /] quit
Closed connection to localhost:9990

localhost:9990

 is the default host and port combination for the WildFly CLI client.

The host and the port of the server can be provided as an optional parameter, if the server is not listening on localhost:9990.

./bin/jboss-cli.sh
You are disconnected at the moment. Type 'connect' to connect to the server
[disconnected /] connect 192.168.0.10:9990
Connected to standalone controller at 192.168.0.1:9990

The :9990 is not required as the CLI will use port 9990 by default. The port needs to be provided if the server is listening on some other port.

 To terminate the session type quit.

 

The jboss-cli script accepts a --connect parameter: ./jboss-cli.sh --connect

The --controller parameter can be used to specify the host and port of the server: ./jboss-cli.sh --connect --controller=192.168.0.1:9990

Help is also available:

[domain@localhost:9990 /] help --commands
Commands available in the current context:
batch               connection-factory  deployment-overlay  if                  patch               reload              try
cd                  connection-info     echo                jdbc-driver-info    pwd                 rollout-plan        undeploy
clear               data-source         echo-dmr            jms-queue           quit                run-batch           unset
command             deploy              help                jms-topic           read-attribute      set                 version
connect             deployment-info     history             ls                  read-operation      shutdown            xa-data-source
To read a description of a specific command execute 'command_name --help'.

해석&정리하자면 

 

1. jboss-cli.sh 스크립트 실행

> jboss home directory/bin/jboss-cli.sh 실행

 

2. 관리자 접속

기본 아이피:포트 사용하고 있는 경우

> connect

 

connect 시 기본 아이피:포트 를 사용하지 않는 경우 

> connect 할당한 아이피:포트 

 

* 여기서 아이피와 포트는 관리자 포트와 아이피를 적어주어야 한다. 

(jboss home dir/standalone/configuration/standalone.xml 파일의 제일 밑에 쪽에 위치한 management-http 참고. (vi 모드에서 파일 끝으로 이동은 :$ (깨알팁))

<socket-binding name="management-http" interface="management" port="${jboss.management.http.port:9992}"/>  )

(domain으로 관리하고 있을 경우 domain.xml 을 참고하면 될 것같고, https 사용하는 경우 management-https 에 할당된 아이피 및 포트를 확인하면 될 것 같다)

3. shutdown 명령어로 서버 죽이기.

 

> ./jboss-cli.sh --connect --controller=localhost:port shutdown 와 같이 한 줄로 서버를 죽일 수도 있다.

매번 쓰기 힘드니 stop.sh 이름으로 쉘스크립트를 작성하여 사용하면 편하다..

 

 

[ stop.sh 작성 ]

#!/bin/bash
./jboss-cli.sh --connect --controller=localhost:port shutdown

 

반응형

java.net.HttpUrlConnection 을 사용한 GET/POST 방식 호출 함수를 작성 후,

호출 방식(httpMethod) 만 DELETE 방식으로 바꿔서 함수를 동작시킬시

HTTP method DELETE doesn't support output ~

과 같은 exception 이 발생한다.

 

https://bugs.openjdk.java.net/browse/JDK-7157360

위의 URL에 기재된 jdk bug 리포트에 따르면..

When using HttpURLConnection, if I set the request method to "DELETE" and attempt to get to the output stream to write the entity body, I get:

Exception in thread "main" java.net.ProtocolException: HTTP method DELETE doesn't support output

at sun.net.www.protocol.http.HttpURLConnection.getOutputStream(HttpURLConnection.java:1004)

As it turns out, sun.net.www.protocol.http.HttpURLConnection explicitly denies access to the output stream if the request

method is DELETE.

>> httpurlconnection 을 사용하여, DELETE request 메소드로 사용하고 (.setRequestMethod("DELETE") 의미) 바디를 쓰기위해 output stream 을 가

    져오면, HTTP 메소드 DELETE는 output 을 지원하지 않는다는 exception이 발생한다.

>> sun.net.www.protocol.http.HttpURLConnection 은 request 메소드가 DELETE라면 output stream 접근을 막는다.

한줄로 정리하자면 jdk 1.7에선 http DELETE 방식에 OutputStream을 지원하지 않는다(Exception이 발생한다).

 

해결책 1.

jdk 버전을 1.8로 올려주면 된다. (참고)

>> jdk1.8버전에 수정된 버그인 듯 하나, 직접 실험해보진 않았다.. (프로젝트 자체가 jdk1.7이었고 다른 방법을 강구해야 했다)

 

해결책 2.

조건문을 걸어 파라미터가 존재하지 않을 경우 OutputStream을 사용하지 않는다.

DELETE 방식의 호출인 경우 파라미터가 없어야 정상으로 알고 있다.

(restful api 에서의 delete 요청은 uri 에 파라미터(key)를 넘긴다)

 

[SAMPLE]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if(getpost.equalsIgnoreCase(POST) || getpost.equalsIgnoreCase(DELETE)){
   if(params != null){
       if(isJson){
           postParams = makeJsonParams(params);
       } else {
           postParams = makeParams(params);
       }
       logger.info("isJson : " + isJson);
       logger.info("postParam.toString()  : " + postParams);
       conn.getOutputStream().write(postParams.getBytes("UTF-8"));
       conn.getOutputStream().flush();
   }
 
cs

(참고 : https://developyo.tistory.com/10?category=688588)

* 결과값은 GET/POST 와 같은 다른 HttpMethod과 상관없이 InputStream을 가져와서 읽어주면 된다.

 

 

쉽게 해결한 듯 보이나 실제론 2시간 가까이 삽질했다.. 

 

반응형

MYSQL에서 데이터 형식 바꾸기(DATE_FORMAT)

 

oracle , sybase , mysql 전부 데이터 형식 바꾸는 표현식이 전부 다르다..

 

1. mysql (출처 : https://www.w3schools.com/sql/func_mysql_date_format.asp)

SELECT DATE_FORMAT(NOW(), '%y/%m/%d')

return : 18/12/10

 

SELECT DATE_FORMAT(NOW(), '%Y/%m/%d')

return : 2018/12/10

 

%Y : 4자리 년도

%y : 2자리 년도

%H : 00~23

%h : 00~12

%Y%m%d%H%i%s : yyyyMMddHH24miss

%y%m%d%h%i%s : yyMMddHHmiss

 

2. ORACLE

SELECT TO_DATE(SYSDATE, 'YY/MM/DD') 

return : 18/12/10

 

3. SYBASE ( http://infocenter.sybase.com/help/index.jsp?topic=/com.sybase.infocenter.dc38151.1520/html/iqrefbb/Convert.htm 참고 )

SELECT CONVERT(varchar, GETDATE(), 111)

return : 2018/12/10

 

뭐 sybase야 다시 쓸 일이 없을 것 같긴 하지만..

거지같다복잡하다

 

 

반응형

보통 개발환경은 최소 2개 이상으로 나뉜다.

local, dev(개발), oper(운영) 등등..

 

was(웹어플리케이션서버)에 .war(컴파일된 소스)를 deploy(반영/배포) 할 때마다 환경(호출해야하는 이미지서버 도메인주소 등)이 달라지므로 디플로이하는 환경에 맞춰 읽어들여야 할 property 파일들을 달리 처리 해야 한다. (local일땐 local.property를, 개발일땐 dev.property를 ... )

이를 매번 반영할 때마다 환경에 맞게 필요없는 프로퍼티 파일들을 제거한 후 deploy 하기도 번거롭고 

deploy 해놓고 vi (리눅스 visual editor) 로 property 불러오는 스프링 context.xml 을 수정하기도 번거롭다.

 

이런 번거로운 수고를 덜기 위해, 동적으로 프로퍼티를 적용하는 방법은 아래와 같이 몇가지가 있다.

 

1. 서버(WAS)의 ip 값을 기준으로 property 를 분기처리 하는 방법

was 에서 ip를 얻어올 땐 java.net.InetAddress클래스의 getHostAddress() 사용.

가져온 ip주소와 하드코딩 해놓은 was ip 비교하여 property 파일을 분기처리.

서버정보가 바뀔 경우 소스레벨에서의 수정이 필요하므로 그닥 좋은 방법 같진 않다..

[SAMPLE CODE]

1
2
3
4
5
6
InetAddress iAddr = InetAddress.getLocalHost(); 
String adr = iAddr.getHostAddress();
String prefix = ""
if(adr.indexOf("127.0.0.1"> -1){ 
prefix = "local" 
else if (//... 생략
cs

 

2. 서버(WAS)의 환경 변수를 기준으로 property를 분기처리 하는 방법

2-1. was서버 설정

1) 보통 was 실행하는 쉘 스크립트인 start.sh 을 vi 로 들여다 보면 JAVA_OPTS 을 선언한 부분을 찾을 수 있다.

    없다면 start.sh 내에서 호출하는 다른 쉘을 vi 로 또 한 번 들여다보자. 

    찾았다면 JAVA_OPTS="$JAVA_OPTS -Dspring.profiles.active=dev" 와 같이 한 줄을 추가한다.

    못 찾겠으면 

./start.sh -Dspring.profiles.active=dev &

    와 같이 서버구동용 쉘스크립트에 위와 같은 옵션을 주어 실행하면

    위와 같이 할 경우, spring.profiles.active 라는 key 에 dev 라는 value를 갖는 환경변수를 설정한 셈이 된다.

 

2) 스프링 property 설정 수정

1
<util:properties id="properties" location="classpath:sample/properties/conf_#{systemProperties['spring.profiles.active']}.properties" />
cs

이와 같이 설정시

sample/properties/conf_환경변수.properties 프로퍼티를 주입하게 된다.

(환경변수는 로컬 vm arguments(2-1)와 서버별 was 환경변수로 지정한 spring.profiles.active 값)

별다른 코드를 짜지 않아도 위와 같은 설정만으로 서버 환경별 property 를 동적으로 주입받게 되는 것이다.

 

2-2. 로컬설정

사용중인 서버(jboss) 더블클릭 > Open launch configuration >  VM arguments 에

-Dspring.profiles.active=local 추가 (톰캣도 같다)

※ 만약 로컬 환경에서 사용 할 프로퍼티 파일명을 sample/properties/conf_.properties 와 같이 사용할 경우 환경변수를 잡아 줄 필요가 없다.

※ 환경변수(JAVA_OPTS)로 선언할 땐 -Dspring ~ 과 같이 D를 붙여주고 그 외(소스)에는 D를 빼주어야 한다는 걸 주의

 

[사용예]

서버 환경 별로 사용하는 js 가 달라지는 경우 (서버 환경 별로 js 내의 목적지 ip 가 달라지거나 하는 경우)

@Controller :

1
2
@Value("#{systemProperties['spring.profiles.active'].js})"
private String jsFileNm;
cs

JSP :

1
<script src="/resources/js/${jsFileNm}" />
cs

와 같이 사용하여 서버 환경 별로 달라지는 js 파일명을 동적으로 가져다 사용할 수 있다.

 

 

반응형

+ Recent posts