아래는 1년전 쯤 개발했던 httpUrlConnection 모듈이다

https://developyo.tistory.com/10?category=688588

 

시간이 지날 수록 소스는 누더기가 되어가고..

디자인패턴을 공부하고, 객체지향스러운 개발을 위해 이책 저책(클린코드 도서 등) 읽고나니

위 코드가 리펙토링이 필요하다는 것을 느껴 리펙토링을 해보았다.

 

상황]

1. 연동되는 서버가 계속해서 늘어났다.

2. 각각의 연동 서버에 맞춰야 되는 예외상황들이 늘어났다. (ex: timeout, charset, retry, responsecode..)

3. 예외상황들을 처리 하기 위한 파라미터들을 받기 위해 메소드의 인자값이 늘어났다.

4. 오버로딩되는 메소드가 늘어났다.

 

방향]

1. 객체를 받도록 처리하자. 

: 파라미터는 적을 수록 좋고,  파라미터가 너무 많이지면 객체를 파라미터로 받도록 소스를 리펙토링 해야한다.

2. boolean 값을 받는 메소드 1개는 두개의 메소드 따로 만들자. (도서 클린코드 참고)

: requestApi(boolean isJsonFormat, ...) 와 같이 boolean 값을 파라미터로 받은 후 분기처리 처리하지 말고

requestApiJsonFormat(...), requestApi(...) 와 같이 두개의 메소드로 분리해야 한다. (도서 클린코드 참고)

3. 필수값과 optional 값을 구분하기 위해 builder 패턴을 적용하여 객체를 설계해보자.

4. 전처리 후처리 로깅을 proxy 패턴을 적용하여 처리해보자. 

5. junit 을 사용하여 테스트해보자. (junit 관련 포스팅1, junit 관련 포스팅2)

 

1. RequestForm

httpUrlConnection 호출에 필요한 파라미터 정보 객체

빌더패턴을 적용하여 필수 파라미터(목적지주소, http method)는 생성자로,

그외 optional 파라미터는 setter 로 받도록 했다.

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
package com.jpp.web.util.HttpUtil;
 
import java.util.Map;
 
public class RequestForm {
   
   private final int tryCount;
   private final int connectionTimeOut;
   private final int readTimeOut;
   private final String httpMethod;
   private final String url;
   private final Map<String, Object> header;
   private final int httpStatus;
   private final String requestCharset;
   private final String responseCharset;
   
   public static class Builder {
      private String httpMethod; //required
      private String url;        //required
      private int tryCount = 1;  //optional
      private int connectionTimeOut = 1000;  //optional
      private int readTimeOut = 1000;     //optional
      private Map<String, Object> header; //optional
      private int httpStatus = 0;    //optional
      private String requestCharset = "UTF-8";
      private String responseCharset = "UTF-8";
      
      public Builder(String httpMethod, String url) {
         this.httpMethod = httpMethod;
         this.url = url;
      }
      
      public Builder setTryCount(int tryCount) {
         this.tryCount = tryCount<1?1:tryCount;
         return this;
      }
      
      public Builder setConnectionTimeOut(int connectionTimeOut) {
         this.connectionTimeOut = connectionTimeOut<100?1000:connectionTimeOut;
         return this;
      }
      
      public Builder setReadTimeOut(int readTimeOut) {
         this.readTimeOut =  readTimeOut<100?1000:readTimeOut;
         return this;
      }
      
      public Builder setHeader(Map<String, Object> header) {
         this.header = header;
         return this;
      }
      
      public Builder setExpectedHttpStatus(int httpStatus) {
         this.httpStatus = httpStatus;
         return this;
      }
      
      public Builder setRequestCharset(String requestCharset) {
         this.requestCharset = requestCharset;
         return this;
      }
      
      public Builder setResponseCharset(String responseCharset) {
         this.responseCharset = responseCharset;
         return this;
      }
      
      public RequestForm build() {
         return new RequestForm(this);
      }
   }
   
   public RequestForm(Builder builder) {
      this.httpMethod = builder.httpMethod;
      this.url = builder.url;
      this.tryCount = builder.tryCount;
      this.connectionTimeOut = builder.connectionTimeOut;
      this.readTimeOut = builder.readTimeOut;
      this.header = builder.header;
      this.httpStatus = builder.httpStatus;
      this.requestCharset = builder.requestCharset;
      this.responseCharset = builder.responseCharset;
   }
   
   public int getTryCount() {
      return tryCount;
   }
 
   public int getConnectionTimeOut() {
      return connectionTimeOut;
   }
 
   public int getReadTimeOut() {
      return readTimeOut;
   }
 
   public String getHttpMethod() {
      return httpMethod;
   }
 
   public String getUrl() {
      return url;
   }
   
   public Map<String, Object> getHeader(){
      return header;
   }
   
   public int getHttpStatus() {
      return httpStatus;
   }
 
   public String getRequestCharset() {
      return requestCharset;
   }
 
   public String getResponseCharset() {
      return responseCharset;
   }
   
}
 
cs

 

 

2. CustomHttpUrlConnection

java.net.HttpUrlConnection 객체를 사용하여 외부 서버 api를 호출하는 역할을 하는 클래스

1) 요청정보를 담고있는 RequestForm객체를 파라미터로 받도록 했다.

2) jsonFormat(body로 호출) 여부인 boolean 값을 파라미터로 받아 내부에서 분기처리하는게 아닌, 별도의 메소드로 각각 분리했다.

메소드를 작은 단위로 쪼개면 코드 중복이 많이 줄어들 것 같은데 나중에..

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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
package com.jpp.web.util.HttpUtil;
 
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Map;
 
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import com.google.gson.Gson;
import com.jpp.web.comm.CustomException;
import com.jpp.web.constants.Constants;
import com.jpp.web.constants.ConstantsEnum;
 
public class CustomHttpUrlConnection {
   
   private static final Logger logger = LoggerFactory.getLogger(CustomHttpUrlConnection.class);
   private static final String UTF_8 = "UTF-8";
 
   private int responseCode = 0;
   private long startTime = 0;
 
   private RequestForm reqForm;
   private Map<String, Object> reqParams;
   
   public CustomHttpUrlConnection(RequestForm reqForm, Map<String, Object> reqParams){
      this.reqForm = reqForm;
      this.reqParams = reqParams;
   }
   
   public int getResponseCode() {
      return responseCode;
   }
   
   public long getStartTime() {
      return startTime;
   }
   
   public String requestApi() {
      
      this.startTime = System.currentTimeMillis();
      
      String httpMethod = reqForm.getHttpMethod();  
      String surl = reqForm.getUrl();               
      int tryCnt = reqForm.getTryCount();   
      int readTimeOut = reqForm.getReadTimeOut(); 
      int connectionTimeOut = reqForm.getConnectionTimeOut();
      Map<String, Object> header = reqForm.getHeader();
      int expectedHttpStatus = reqForm.getHttpStatus();
      String requestCharset = reqForm.getRequestCharset();
      String responseCharset = reqForm.getResponseCharset();
      
      String reqCharset = requestCharset==null||requestCharset.isEmpty()?UTF_8:requestCharset;
      String resCharset = responseCharset==null||responseCharset.isEmpty()?UTF_8:responseCharset;
             
      URL url = null;
      HttpURLConnection conn = null;
      BufferedReader br = null;
      JSONObject jobj = null;
      String postParams = "";
      String errMsg = "";
      String returnText = "";
 
      
      for(int i=0; i < tryCnt; i++){
          
         try {
             
            if(httpMethod.equalsIgnoreCase(Constants.POST) || httpMethod.equalsIgnoreCase(Constants.DELETE)){
               url = new URL(surl);
            } else if(httpMethod.equalsIgnoreCase(Constants.GET)){
               url = new URL(surl + ((reqParams!=null)?"?"+makeUrlEncodedParams(reqParams, reqCharset):""));
            }
            
            conn = (HttpURLConnection) url.openConnection();
            
            if(header != null){
                for(String key : header.keySet()) {
                    conn.setRequestProperty(key, header.get(key)!=null?header.get(key).toString():"");
                }
            }
             
            conn.setRequestMethod(httpMethod);
            conn.setConnectTimeout(connectionTimeOut);
            conn.setReadTimeout(readTimeOut);
            conn.setDoOutput(true);
            
            if(httpMethod.equalsIgnoreCase(Constants.POST) || httpMethod.equalsIgnoreCase(Constants.DELETE)){
               if(reqParams != null){
                  postParams = makeUrlEncodedParams(reqParams, reqCharset);
                  conn.getOutputStream().write(postParams.getBytes(UTF_8));
                  conn.getOutputStream().flush();
               }
            }
            
            this.responseCode = conn.getResponseCode();
            
            if(expectedHttpStatus > 0){
               if(expectedHttpStatus!=conn.getResponseCode()){
                  throw new CustomException("successCode : {" + expectedHttpStatus + "}" + " , responseCode : {" + this.responseCode + "}", ConstantsEnum.API_RESULT.E_NETWORK.getCode());
               }
            }
            
            br = new BufferedReader(new InputStreamReader(conn.getInputStream(),resCharset));
            
            StringBuffer sb = null;
            sb = new StringBuffer();
            
            String jsonData = "";
            while((jsonData = br.readLine()) != null){
               sb.append(jsonData);
            }
            returnText = sb.toString();
            
            try{
               jobj = new JSONObject(returnText);
            } catch (JSONException e){
               throw new CustomException();
            }
            
            break;
            
         } catch (SocketTimeoutException se){
            logger.error("connection fail : " + se);
             errMsg = se.getMessage();
         } catch (CustomException e){
            logger.error("response fail : " + e);
             errMsg = e.getMessage();
         } catch (Exception e){
            throw new CustomException(e.getMessage().toString(), ConstantsEnum.API_RESULT.E_NETWORK.getCode());
         } 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 CustomException(errMsg, ConstantsEnum.API_RESULT.E_NETWORK.getCode());
      }
   }
   
   
   public String requestApiWithJsonForm() {
      
      this.startTime = System.currentTimeMillis();
      
      String httpMethod = reqForm.getHttpMethod();
      int tryCnt = reqForm.getTryCount();
      int readTimeOut = reqForm.getReadTimeOut();
      int connectionTimeOut = reqForm.getConnectionTimeOut();
      String surl = reqForm.getUrl();
      Map<String, Object> header = reqForm.getHeader();
      int expectedHttpStatus = reqForm.getHttpStatus();
      String responseCharset = reqForm.getResponseCharset();
      
      String resCharset = responseCharset==null||responseCharset.isEmpty()?UTF_8:responseCharset;
      
      URL url = null;
      HttpURLConnection conn = null;
      BufferedReader br = null;
      JSONObject jobj = null;
      String postParams = "";
      String errMsg = "";
      String returnText = "";
       
      for(int i=0; i < (tryCnt<1?1:tryCnt); i++){
          
         try {
             
            url = new URL(surl);
            
            conn = (HttpURLConnection) url.openConnection();
            
            if(header != null){
                for(String key : header.keySet()) {
                    conn.setRequestProperty(key, header.get(key)!=null?header.get(key).toString():"");
                }
            }
             
            conn.setRequestProperty("Content-Type""application/json");
            conn.setRequestMethod(httpMethod);
            conn.setConnectTimeout(connectionTimeOut);
            conn.setReadTimeout(readTimeOut);
            conn.setDoOutput(true);
            
            if(reqParams != null){
                postParams = makeJsonParams(reqParams);
                conn.getOutputStream().write(postParams.getBytes(UTF_8));
                conn.getOutputStream().flush();
            }
            
            this.responseCode = conn.getResponseCode();
            
            if(expectedHttpStatus != 0){
               if(expectedHttpStatus!=conn.getResponseCode()){
                  throw new CustomException("successCode : {" + expectedHttpStatus + "}" + " , responseCode : {" + this.responseCode + "}", ConstantsEnum.API_RESULT.E_NETWORK.getCode());
               }
            }
            
            br = new BufferedReader(new InputStreamReader(conn.getInputStream(), resCharset));
            
            StringBuffer sb = null;
            sb = new StringBuffer();
            
            String jsonData = "";
            while((jsonData = br.readLine()) != null){
               sb.append(jsonData);
            }
            returnText = sb.toString();
            
            try{
               jobj = new JSONObject(returnText);
            } catch (JSONException e){
               throw new CustomException();
            }
            
            break;
            
         } catch (SocketTimeoutException se){
            logger.error("connection fail : " + se);
             errMsg = se.getMessage();
         } catch (CustomException e){
            logger.error("response fail : " + e);
             errMsg = e.getMessage();
         } catch (Exception e){
            throw new CustomException(e.getMessage().toString(), ConstantsEnum.API_RESULT.E_NETWORK.getCode());
         } 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 CustomException(errMsg, ConstantsEnum.API_RESULT.E_NETWORK.getCode());
      }
   }
    
   
   private String makeUrlEncodedParams(Map<String, Object> params, String charset) throws Exception{
      String param = "";
      StringBuffer sb = new StringBuffer();
      
      if(params != null){
         for ( String key : params.keySet() ){
             try {
                sb.append(key).append("=").append((params.get(key)==null?"":URLEncoder.encode(params.get(key).toString(), charset)).toString().trim()).append("&");
             } catch (UnsupportedEncodingException e) {
                logger.error("ex while encoding : {}", e.getMessage());
                throw e;
             }
         }
         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;
   }
   
}
 
 
cs

 

 

3. HttpUtil

클라이언트에서 실제로 사용될 클래스

1) 프록시(proxy) 패턴을 적용하여 CustomHttpUrlConnection객체의 requestApi(), requestApiWithJsonForm() 메소드를 호출하기 전과 후에 logging 메소드가 호출되도록 했다

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
package com.jpp.web.util.HttpUtil;
 
import java.util.Map;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
public class HttpUtil {
    
   private static final Logger logger = LoggerFactory.getLogger(HttpUtil.class);
   
   private RequestForm reqForm;
   private Map<String, Object> reqParams;
   private CustomHttpUrlConnection httpUrlConnection;
   
   public HttpUtil(RequestForm reqForm, Map<String, Object> reqParams) {
      this.reqForm = reqForm;
      this.reqParams = reqParams;
      httpUrlConnection = new CustomHttpUrlConnection(reqForm, reqParams);
   }
   
   public String requestApi() {
      printBeforeLog();
      String resParams = httpUrlConnection.requestApi();
      printAfterLog(resParams);
      return resParams;
   }
   
   public String requestApiWithJsonForm() {
      printBeforeLog();
      String resParams = httpUrlConnection.requestApiWithJsonForm();
      printAfterLog(resParams);
      return resParams;
   }
   
   private void printBeforeLog() {
      logger.info("HTTPCONNECTION.REQUEST|URL:{}|IN_PARAMS:{}"
            , reqForm.getUrl(), reqParams);
   }
   
   private void printAfterLog(String resParams) {
      logger.info("HTTPCONNECTION.RESPONSE|URL:{}|TIME:{}|STATUS_CODE:{}|IN_PARAMS:{}|OUT_PARAMS:{}"
            , reqForm.getUrl()
            , System.currentTimeMillis() - httpUrlConnection.getStartTime()
            , httpUrlConnection.getResponseCode()<1?"":httpUrlConnection.getResponseCode()
            , reqParams
            , resParams);
   }
   
}
 
cs

 

4. test

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
package com.jpp.web.util.HttpUtil;
 
import java.util.HashMap;
import java.util.Map;
 
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
 
@RunWith(SpringRunner.class)
@SpringBootTest
public class HttpUtilTest {
   
   private static final Logger logger = LoggerFactory.getLogger(HttpUtil.class);
   
   @Test
   public void test() {
      RequestForm reqForm = new RequestForm.Builder("GET""127.0.0.1:8082/mobile/device")
            .setConnectionTimeOut(1000)
            .setReadTimeOut(1000)
            .setTryCount(3)
            .build();
      
      Map<String, Object> reqParams = new HashMap<String, Object>();
      reqParams.put("deviceType""1");
      reqParams.put("osVersion""10");
      
      String apiResult = new HttpUtil(reqForm, reqParams).requestApiWithJsonForm();
      logger.info(apiResult);
   }
 
   @Test
   public void test2() {
      RequestForm reqForm = new RequestForm.Builder("GET""127.0.0.1:8082/mobile/version")
            .setConnectionTimeOut(1000)
            .setReadTimeOut(1000)
            .setTryCount(3)
            .build();
      
      Map<String, Object> reqParams = new HashMap<String, Object>();
      
      String rs = new HttpUtil(reqForm, reqParams).requestApi();
      logger.info(rs);
   }
}
 
cs

 

소스리뷰를 받고싶다..

 

모든 코드는 제 github(https://github.com/develo-pyo/boot-mybatis)에 올려놓았습니다.

반응형

팩토리 패턴

팩토리라는 클래스에 객체 생성을 위임(캡슐화)하여 팩토리 클래스가 객체를 생성하도록 하는 방식.

어떤 클래스의 인스턴스를 생성할지 서브클래스에서 결정하도록 한다는게 팩토리 패턴의 핵심.

분류에 딱히 큰 의미는 없는 듯 하나, 팩토리 메소드 패턴과 추상 팩토리 메소드 패턴으로 나누고 있다.

"구상클래스에 의존하지 않게, 추상화 된 것에 의존하도록 개발" 을 따르는 패턴

 

 

1. 팩토리 메소드 패턴

: 객체 생성을 담당하는 팩토리 메소드 작성하여 객체 생성을 캡슐화

: 객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정한다.

 

[Mouse interface]

1
2
public interface Mouse {
}
cs

[LGMouse.java]

Mouse interface 구현

1
2
3
4
5
public class LGMouse implements Mouse {
    public LGMouse() {
        System.out.println("LG 마우스 생성");
    }
}
cs

[SamsungMouse.java]

Mouse interface 구현

1
2
3
4
5
public class SamsungMouse implements Mouse {
    public SamsungMouse() {
        System.out.println("Samsung 마우스 생성");
    }
}
cs

 

[Factory.java]

객체 생성을 맡아서 처리 하는 팩토리클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Factory {
    public static Mouse createMouse(String type) {
        Mouse mouse = null;
        switch(type) {
            case "LG":
                mouse = new LGMouse();
                break;
            case "SAMSUNG":
                mouse = new SamsungMouse();
                break;
        }
        return mouse;
    }
}
cs

 

[Client]

1
2
3
4
5
public class Client {
    public static void main(String[] args) {
        Factory.createMouse("LG");
    }
}
cs

[결과]

LG 마우스 생성

 

 

2. 추상 팩토리 패턴

: 연관된 서브 클래스를 그룹화할 수 있고 그룹을 자유롭게 교체할 수 있는 패턴

: 인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 구상 클래스를 지정하지 않고 생성할 수 있다

: 팩토리 메소드 패턴이 좀 더 캡슐화 되어있는 형태

 

[Mouse interface]

1
2
public interface Mouse {
}
cs

[LGMouse.java]

Mouse interface 구현

1
2
3
4
5
public class LGMouse implements Mouse {
    public LGMouse() {
        System.out.println("LG 마우스 생성");
    }
}
cs

[SamsungMouse.java]

Mouse interface 구현

1
2
3
4
5
public class SamsungMouse implements Mouse {
    public SamsungMouse() {
        System.out.println("SAMSUNG 마우스 생성");
    }
}
cs

 

[Keyboard interface]

1
2
public interface Keyboard {
}
cs

[LGKeyboard.java]

Keyboard interface 구현

1
2
3
4
5
public class LGKeyboard implements Keyboard {
    public LGKeyboard() {
        System.out.println("LG 키보드 생성");
    }
}
cs

[SamsungKeyboard.java]

Keyboard interface 구현

1
2
3
4
5
public class SamsungKeyboard implements Keyboard {
    public SamsungKeyboard() {
        System.out.println("SAMSUNG 키보드 생성");
    }
}
cs

 

[Computer interface]

1
2
3
4
public interface Computer {
    public Keyboard createKeyboard();
    public Mouse createMouse();
}
cs

[ComputerFactory.java]

Computer interface 구현

1
2
3
4
5
6
public class ComputerFactory {
     public void createComputer(Computer computer){
        computer.createKeyboard();
        computer.createMouse();
    }
}
cs

 

[Client.java]

1
2
3
4
5
6
7
public class Client {
    public static void main(String[] args) {
        ComputerFactory cf = new ComputerFactory();
        cf.createComputer(new SamsungComputer());
        cf.createComputer(new LGComputer());
    }
}
cs

[결과]

SAMSUNG 컴퓨터 생성
SAMSUNG 키보드 생성
SAMSUNG 마우스 생성
LG 컴퓨터 생성
LG 키보드 생성
LG 마우스 생성

 

참고 :

아래의 포스팅을 많이 참고했습니다. 훌륭한 예제를 보시려면 아래를 참고해주세요..

https://victorydntmd.tistory.com/300

 

 

 

반응형

어댑터 패턴은 실생활에서 사용하는 어댑터와 동일한 역할을 수행한다.

한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 다른 인터페이스로 변환한다.

 

어댑터 패턴 사용시 라이브러리를 직접 사용하는 것이 아닌, 

라이브러리를 캡슐화한 어댑터를 사용하게 되므로, 소스간 결합도가 약해지는 장점이 있다.

wrapper 와 slf4j(log4j 의 어댑터) 는 어댑터라 볼 수 있다.

 

 

[Adapter Pattern의 Class 다이어그램(참고)]

Client 는 Adapter 구상 클래스를 Target Interface를 통해 사용한다.

Adapter 클래스는 Target Interface를 구현한다.

Adapter 클래스는 Adaptee(새롭게 사용할 구상 클래스) 를 구상 클래스로 사용하며,

Adapter의 Request()는 Adaptee의 SpecificRequest()를 호출한다. (Adaptee를 캡슐화)

Client 는 Adapter 의 Request()를 사용하는 줄 알지만 실제론 Adaptee 의 SpecificRequest()를 사용하게 된다.

 

실제 Adaptor pattern 의 사용 예를 살펴보자.

 

 

1-1. Adapter 패턴을 사용하지 않은 설계

[OldLibraryIF]

1
2
3
public interface OldLibraryIF {
    public void sendPush(String input);
}
cs

 

[OldLibrary]

OldLibrary 인터페이스를 구현하는 클래스

1
2
3
4
5
6
7
public class OldLibrary implements OldLibraryIF {
    
    public void sendPush(String input) {
        System.out.println("send " + input + " using OldLibrary");
    }
    
}
cs

 

[Client]

OldLibrary를 사용하는 클라이언트

1
2
3
4
5
6
public class Client {
    public static void main(String[] args) {
       OldLibraryIF oldLib = new OldLibrary();
       oldLib.sendPush("message");
    }
}
cs

 

[결과]

send message using OldLibrary

위와 같이 모든 구현이 끝나있는 상태에서 기존의 라이브러리였던 OldLibrary 를 제거하고,

아래와 같은 새로운 라이브러리(NewLibrary)를 사용해야 하는 상황이 생긴다면 어떻게 해야할까?

교체대상인 OldLibrary를 사용하는 클라이언트를 일일히 찾아 수정해주어야 한다.

 

1-2. Adapter 패턴을 사용하지 않은 설계에서 OldLibrary를 NewLibrary로 교체

[NewLibraryIF]

1
2
3
4
public interface NewLibraryIF {
    public void sendMsg(String input);
}
 
cs

 

[NewLibrary]

NewLibrary 인터페이스를 구현하는 클래스

1
2
3
4
5
6
7
public class NewLibrary implements NewLibraryIF {
    
    public void sendMsg(String input) {
        System.out.println("send " + input + " using NewLibrary");
    }
    
}
cs

 

[Client]

OldLibrary 대신 NewLibrary를 사용하도록 기존의 Client 를 수정.

1
2
3
4
5
6
7
public class Client {
    public static void main(String[] args) {
        NewLibraryIF newLib = new NewLibrary();
        newLib.sendMsg("message");
    }
}
 
cs

 

[결과]

send message using NewLibrary

라이브러리 교체는 쉽게 끝이난 듯 보인다.

하지만 OldLibrary를 사용하는 곳이 수십, 수백곳이라면?!

애초에 어댑터 패턴을 사용했더라면 어땟을까?

 

2-1. Adapter 패턴을 사용한 설계

[Adapter]

OldLibrary를 캡슐화한 Adapter 클래스

1
2
3
4
5
6
7
8
9
10
public class Adapter implements OldLibraryIF{
    
    OldLibraryIF oldLibrary = new OldLibrary();
 
    @Override
    public void sendPush(String input) {
        oldLibrary.sendPush(input);
    }
    
}
cs

 

[Client]

1
2
3
4
5
6
public class Client {
    public static void main(String[] args) {
        OldLibraryIF adapter = new Adapter();
        adapter.sendPush("message");
    }
}
cs

 

[결과]

send message using OldLibrary

Client 에서 OldLibrary를 생성자로 직접 생성하여 사용하는 대신, Adapter 클래스를 사용했다.

이전과 마찬가지로 OldLibrary를 NewLibrary로 교체해보자.

 

2-2. Adapter 패턴을 사용한 설계에서 OldLibrary를 NewLibrary로 교체

[Adapter]

OldLibrary를 멤버변수로 사용하는 대신 NewLibrary를 멤버변수로 사용

1
2
3
4
5
6
7
8
9
10
11
public class Adapter implements OldLibraryIF{
    
    NewLibraryIF newLibrary = new NewLibrary();
 
    @Override
    public void sendPush(String input) {
        newLibrary.sendMsg(input);
    }
    
}
 
cs

 

[Client]

기존의 Client 코드와 동일한 코드를 사용해도 된다.

Adapter 클래스 코드만 수정했을 뿐, Client 코드는 바꾼게 없다.

1
2
3
4
5
6
7
public class Client {
    public static void main(String[] args) {
        OldLibraryIF adapter = new Adapter();
        adapter.sendPush("message");
    }
}
 
cs

 

[결과]

send message using NewLibrary

이와 같이 Adapter 패턴(캡슐화)을 사용하게 되면 라이브러리의 변화가 발생해도 클라이언트를 수정하지 않아도 된다.

도서 <Clean Code> 에도 외부라이브러리를 캡슐화 하여 사용하는 이점에 대해 언급되어있다.

 

 

참고 : Head First Design Patterns

반응형

원래 객체의 기능을 다른 객체가 대신 처리하도록 설계하는 패턴

AOP 등에 사용되는 패턴

 

 

Proxy 기본 패턴

[Subject interface]

1
2
3
public interface Subject {
    public String action();
}
cs

 

[RealSubject class]

실제 사용하는 객체

1
2
3
4
5
6
7
8
public class RealSubject implements Subject{
 
    @Override
    public String action() {
        return "Real Subject action()";
    }
}
 
cs

 

[Proxy class]

프록시 객체.

내부적으로 RealSubject 객체를 생성하여 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Proxy implements Subject {
    
    RealSubject realSubject;
    
    @Override
    public String action() {
        if(realSubject == null) {
            realSubject = new RealSubject();
        }
        return realSubject.action();
    }
}
 
cs

 

[Client class]

1
2
3
4
5
6
7
8
9
public class Client {
 
    public static void main(String[] args) {
        Subject subject = new Proxy();
        System.out.println(subject.action());
    }
 
}
 
cs

 

 

Proxy의 실제 사용

[AOP 흉내내기]

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
public class Proxy implements Subject {
    
    RealSubject realSubject;
    
    @Override
    public Object action() {
        if(realSubject == null) {
            realSubject = new RealSubject();
        }
        
        preProcess();
        Object result = realSubject.action();
        postProcess();
        
        return result;
    }
    
    private void preProcess() {
        System.out.println("선행작업");
    }
    
    private void postProcess() {
        System.out.println("사후작업");
    }
}
 
cs

 

[실제 AOP 소스]

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
@Component
@Aspect
public class ControllerAOP {
 
    protected static final Logger logger = LoggerFactory.getLogger(ControllerAOP.class);
    private final String CANNOT_PRINT_INPARAM = "{IN_PARAMS LENGTH IS TOO LONG TO PRINT}";
    private final String CANNOT_PRINT_OUTPARAM = "{OUT_PARAMS LENGTH IS TOO LONG TO PRINT}";
    private final String CANNOT_PRINT_VALUE = "VALUE LENGTH IS TOO LONG TO PRINT";
    private final String NOTHING_TO_PRINT = "RETURN TYPE IS VOID";
    
    @Around("execution(public void com.sample..*Controller.*(..))")
    public void voidAround(ProceedingJoinPoint pjp) {
        
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
 
        String inputParam = null;
        
        for (Object obj : pjp.getArgs()) {
            if (obj instanceof Map) {
                inputParam = obj.toString();
            } else if (obj instanceof String){
                inputParam = (String)obj;
            }
        }
 
        long start = System.currentTimeMillis();
 
        String controller = (String) pjp.getTarget().getClass().getSimpleName();
 
        String path = request.getRequestURI();
        String addr = request.getRemoteAddr();
        int port = request.getRemotePort();
 
        logger.info("##########################################################################");
        logger.info("# REQUEST | CONTROLLER = {} | METHOD = {} | REMOTEADDR = {} | PORT = {} | IN_PARAMS = {}",
                controller, path, addr, port, inputParam==null?"":(inputParam.length()>=1500?this.CANNOT_PRINT_INPARAM:inputParam));
        logger.info("##########################################################################");
        
        String result_msg = "success";
        
        try {
            pjp.proceed();
            result_msg = "success";
        } catch (APIException e) {
            result_msg = "fail";
        } catch (Throwable e) {
            result_msg = "fail";
        } finally {
            long end = System.currentTimeMillis();
            logger.info("##########################################################################");
            logger.info("# RESPONSE | CONTROLLER = {} | METHOD = {} | RESULT = {} | REMOTEADDR = {} | PORT = {} | TIME = {} ms | IN_PARAMS = {} | OUT_PARAMS = {}"
                    controller, path, result_msg, addr, port, end-start, inputParam==null?"":(inputParam.length()>=1500?this.CANNOT_PRINT_INPARAM:inputParam), 
                    this.NOTHING_TO_PRINT);
            logger.info("##########################################################################");
        }
 
 
    }
cs

위 AOP 코드 요약 : joinpoint(클라이언트에서 호출되는 모든 메소드) 중에서 pointcut으로(execution) 잡은 특정 메소드만 잡아 공통 관심사로 (advice(@Around)) 로 처리.

 

실제 호출되는 메소드를 인자(ProceedingJoinPoint)로 받아 프록시 패턴과 같이 처리한다.

pjp.proceed() 가 실제 메소드 실행부이며,

pjp.proceed() 위 아래로 로그를 처리하는 코드가 있다.

 

 

참고 :

도서 Head first design patterns,

https://effectiveprogramming.tistory.com/entry/Proxy-%ED%8C%A8%ED%84%B4%EA%B3%BC-%EA%B7%B8-%ED%99%9C%EC%9A%A9

 

반응형

"어떤 서브시스템의 일련의 인터페이스에 대한 통합된 인터페이스 제공.

퍼사드에서 고수준 인터페이스를 정의하기 때문에 서브시스템을 더 쉽게 사용할 수 있다."

 

사용할 메소드가 여러 클래스에 퍼져 있는 경우

필요한 클래스들을 모두 생성자로 생성하여 필요한 메소드를 각각 호출 하는 대신,

(최소 지식 원칙 (의존하는 클래스의 갯수를 최대한 적게) 위배)

별도의 클래스(퍼사드)에 사용할 클래스들을 멤버변수로 담고,

사용하고자 하는 멤버변수의 메소드들을 한곳에 묶은 새로운 메소드를 작성한다.

 

예를 들어, 메일 전송 로직을 다음과 같이 구현한다고 하자.

1. 수신인의 메일주소를 확인한다.

2. 메일을 전송한다.

3. 메일 전송 결과를 이력에 남긴다.

위 기능들이 각각 다른 클래스에 존재하는 메소드라고 한다면

위 세 기능을 묶은 하나의 메소드를 작성하여, 메인시스템에선 해당 메소드(서브시스템)만 호출한다.

 

[ValidationService class]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package design_pattern.fasade;
 
public class ValidationService {
    
    String mailAddr = "";
    
    public ValidationService(String addr){
        this.mailAddr = addr;
    }
    
    public boolean addrChk() {
        if(!mailAddr.isEmpty()) {
            System.out.println("mail validation check success!");
            return true;
        }
        return false;
    }
    
}
 
cs

 

[MailService class]

1
2
3
4
5
6
7
8
package design_pattern.fasade;
 
public class MailService {
    public void mailSend() {
        System.out.println("mail has been sent");
    }
}
 
cs

 

[HstService class]

1
2
3
4
5
6
7
8
package design_pattern.fasade;
 
public class HstService {
    public void hstSave() {
        System.out.println("sent mail saved");
    }
}
 
cs

 

[Facade.class]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package design_pattern.fasade;
 
public class Facade {
    private ValidationService vs;
    private MailService ms;
    private HstService hs;
    
    public Facade(ValidationService vs, MailService ms, HstService hs) {
        this.vs = vs;
        this.ms = ms;
        this.hs = hs;
    }
    
    public void facadeFunc() {
        if(vs.addrChk()) {
            ms.mailSend();
        } 
        hs.hstSave();
    }
}
 
cs

주시스템에서 사용하고자 하는 서브시스템들을 멤버변수로.

멤버변수 내에서 사용할 메소드들을 따로 모은, 새로운 메소드 구현(facadeFunc())

 

[main]

1
2
3
4
5
6
7
8
9
10
11
12
13
package design_pattern.fasade;
 
public class Main {
    
    public static void main(String args[]) {
        Facade f = new Facade(new ValidationService("receiver@gmail.com")
                            , new MailService()
                            , new HstService());
        f.facadeFunc();
    }
    
}
 
cs

 

디자인 패턴 중 가장 단순한(이해하기 쉬운..) 구조이며 가장 많이 쓰이는 패턴 중 하나

반응형

1. 변화하는 기능을 별도의 interface로 분리(캡슐화)

2. 각각의 기능(interfaceImple)들이 인터페이스를 구현(implements)

3. 기능을 사용하는 객체(class) 내에서 기능(interface)을 멤버변수로 포함

4. 기능을 사용하는 객체(class) 내에서 기능(interfaceImple)을 주입받는 setter를 선언하여 각각의 객체(class)가 다른 기능(interfaceImple) 을 사용할 수 있게 설계

 

* 무조건적인 상속은 소스의 중복을 야기, 소스 수정시 구현체들의 소스 수정을 야기

 

[ex]

[Shout interface]

소리지르기 비명지르기 기능이 있는 Shout 인터페이스 작성.

1
2
3
4
5
6
7
package design_pattern.strategy;
 
public interface Shout {
    public void yell();
    public void scream();
}
 
cs

 

[Human class]

Shout 인터페이스를 구현한 Human 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package design_pattern.strategy;
 
public class Human implements Shout{
    
    @Override
    public void yell() {
        System.out.println("으악");
    }
 
    @Override
    public void scream() {
        System.out.println("꺄");
    }
    
}
 
cs

 

[Dog class]

Shout 인터페이스를 구현한 Dog 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package design_pattern.strategy;
 
public class Dog implements Shout{
 
    @Override
    public void yell() {
        System.out.println("왈");
    }
 
    @Override
    public void scream() {
        System.out.println("왈");
    }
    
}
 
cs

 

[Throat class]

소리지르기, 비명지르기 기능을 갖고 있는 Shout 인터페이스를 멤버변수로 두고 있는 Throat 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package design_pattern.strategy;
 
public class Throat {
    
    Shout s;
    
    public void setThroat(Shout s) {
        this.s = s;
    }
    
    public void func1() {
        s.yell();
    }
    
    public void func2() {
        s.scream();
    }
    
}
 
cs

 

[Main class(client)]

기능을 사용하는 클라이언트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package design_pattern.strategy;
 
public class Main {
    public static void main(String args[]) {
        
        Throat throat = new Throat();
        
        throat.setThroat(new Human());
        throat.func1();
        throat.func2();
        
        System.out.println("------------------");
        
        throat.setThroat(new Dog());
        throat.func1();
        throat.func2();
        
    }
}
 
cs

 

[결과]

클라이언트 단에서 setter를 사용하여 클래스(기능)를 변경하여 사용 가능하다.

 

정리가 잘 되어있는 글

참고 도서

 

 

반응형

Head First : Design Patterns

 

자바스럽게 개발하려면 디자인 패턴에 대한 공부는 필수.

아직 1장 스트래터지 패턴 밖에 읽어 보지 못했지만, 이해하기 쉽게그래도 어렵다 정말 잘 쓴 책 같다.

공부하며 각각의 패턴들을 블로그에 따로 정리해보려고 한다.

 

실무에서 다양한 상황에 맞게 각각의 패턴들을 적용하여 개발하려면 내공이 더 쌓여야 가능하겠지만,

일단 각종 API, 라이브러리 및 고수 선배님들의 소스를 파악하는데 큰 도움이 될 듯 하다.

 

2019-06-03 시작.

반응형

'etc. > books' 카테고리의 다른 글

[Book] HTTP 완벽 가이드  (0) 2020.03.17
[Book] 객체지향의 사실과 오해  (0) 2020.02.23
[Book] 소프트웨어 장인  (0) 2020.01.16
[Book] Clean Code (클린 코드)  (0) 2019.12.28
[Book] 프로그래머의 길, 멘토에게 묻다  (0) 2019.11.22

+ Recent posts