PageSourceSearch

https://subji.github.io/assets/js/lunr/lunr-store.js

js subji.github.io collected 2026-10-03 10:00:06 UTC 347,990 bytes, 496 lines download raw bytes

1var store = [{
2        "title": "Welcome to Jekyll!",
3        "excerpt":"You’ll find this post in your _posts directory. Go ahead and edit it and re-build the site to see your changes. You can rebuild the site in many different ways, but the most common way is to run jekyll serve, which launches a web server and auto-regenerates your site when a file is updated.   To add new posts, simply add a file in the _posts directory that follows the convention YYYY-MM-DD-name-of-post.ext and includes the necessary front matter. Take a look at the source for this post to get an idea about how it works.   Jekyll also offers powerful support for code snippets:   ​python def print_hi(name):   print(\"hello\", name) print_hi('Tom') ​   Check out the Jekyll docs for more info on how to get the most out of Jekyll. File all bugs/feature requests at Jekyll’s GitHub repo. If you have questions, you can ask them on Jekyll Talk.   ","categories": ["posts"],
4        "tags": ["index"],
5        "url": "https://subji.github.io/posts/2019/05/25/first-post",
6        "teaser":null},{
7        "title": "Git 비밀번호 입력 없애는 방법",
8        "excerpt":"1. Git Credential 정보 저장  git config --global credential.helper store  2. Git Credential 정보 캐시에 저장 (시간 설정 가능)  git config --global credential.helper cache & 'cache --timeout 86400(1일)'  Window 의 경우에는 아래 주소의 프로그램을 받아 설치 후 적용해본다.     https://github.com/microsoft/Git-Credential-Manager-for-Windows   ","categories": ["posts"],
9        "tags": ["git"],
10        "url": "https://subji.github.io/posts/2019/05/30/git-credential-helper",
11        "teaser":null},{
12        "title": "SPRING 에서 PROXY 된 IP 까지 받아오는 방법",
13        "excerpt":"기본적으로 HttpServletRequest.getRemoteAddr() 로 받아 올 수 있지만   Proxy 된 IP 의 경우 제대로 받아오지 못한다.   그 이유는 웹서버 또는 WAS 이전에 Load Balancers (L4, …), Proxy Server, Caching Server, HTTP 서버용 WAS Connector 등이 있을 경우   앞서 언급한 기타 Server 들의 경우 웹서버 & WAS 에 HTTP 또는 AJP(전용 프로토콜) 으로 요청을 보낸 후 받은 결과를 클라이언트에 재전송하게 된다.   이로 인해 “getRemoteAddr()” 은 null 을 출력한다. 그러면 어떻게 방법은 없는 걸까? 하고 찾아보니 X-Forwarded-For 등 HTTP Header 에서 받아오는 방법이 있었다.   아래는 X-Forwarded-For 의 간략한 설명과 예제 코드이다.   X-Forwarded-For (XFF) 는 HTTP Header 중 하나로 HTTP Server 에 요청한 Client IP 를 식별하기 위한 방법 중 하나이다.   일반적으로 Java 에서 Client IP 를 받아오기 위해선 아래와 같은 방법으로 작성한다.   HttpServletRequest.getHeader(\"X-Forwarded-For\"); HttpServletRequest.getHeader(\"Proxy-Client-IP\"); HttpServletRequest.getHeader(\"WL-Proxy-Client-IP\"); HttpServletRequest.getHeader(\"HTTP_CLIENT_IP\"); HttpServletRequest.getHeader(\"HTTP_X_FORWARDED_FOR\");   위 코드를 사용해도 로컬 환경에서는 “0:0:0:0:0:0:0:1” 과 같이 IPv6 로 출력된다. 해당 값은 IPv4 에서 “127.0.0.1” 과 같은 값이다.   Tomcat 의 경우 “{“tomcat directory”}/bin/catalina.bat” 파일에 다음과 같이 작성한다.   :noJuliConfig set \"JAVA_OPTS=%JAVA_OPTS% %LOGGING_CONFIG%\" -Djava.net.preferIPv4Stack=true :noJuliManager set \"JAVA_OPTS=%JAVA_OPTS% %LOGGING_MANAGER%\" -Djava.net.preferIPv4Stack=true  ","categories": ["posts"],
14        "tags": ["spring"],
15        "url": "https://subji.github.io/posts/2019/06/18/getclientiponspring",
16        "teaser":null},{
17        "title": "브라우저에서 ES6 사용 방법",
18        "excerpt":"vuejs 를 사용하다가 import & export 문을 사용할 경우가 생겨 사용하던 도중   정의되지 않은 식별자라고 오류가 나와서 찾아보았다.   검색 결과 이전에 es6 구문을 사용하기 위해서 필요했었던 babel, webpack 없이도   사용할 수 있었다.   방법은 간단하다. 아래 코드에서 처럼 스크립트 태그에 “module” 만 추가해주면 된다.   <script type=\"module\" src=\"...\">   지원하는 브라우저     [IE] X   [Edge] 17+   [Edge Chromium] 75+   [Chrome] 72+   [Firefox] 66+   [Safari] 12.1+  ","categories": ["posts"],
19        "tags": ["javascript"],
20        "url": "https://subji.github.io/posts/2019/06/21/howtousees6onbrowser",
21        "teaser":null},{
22        "title": "ZSH에서 Vi & Emacs 모드 하는 방법",
23        "excerpt":"ZSH 에서 아래와 같은 명령어를 입력한다.   # Emacs mode bindkey -e # Vi mode bindkey -v  ","categories": ["posts"],
24        "tags": ["linux"],
25        "url": "https://subji.github.io/posts/2019/06/25/howtosetvimodeonzsh",
26        "teaser":null},{
27        "title": "tail 명령어로 여러개의 로그파일 보기",
28        "excerpt":"아래 명령어를 입력하면 여러개의 로그파일ì
28„ 로깅할 수 있다.  로그 내용의 변화가 있을 경우 파일명과 로깅내용이 나오게 된다.  (로그파일에 변화가 없는 경우 출력 X)  tail -f *.log  ","categories": ["posts"],
29        "tags": ["linux"],
30        "url": "https://subji.github.io/posts/2019/06/27/howtoshowmultiplelog",
31        "teaser":null},{
32        "title": "Vue.js external script(외부 스크립트) 추가 방법",
33        "excerpt":"스프링 & 뷰 프로젝트를 하던 도중 굳이 외부 스크립트를 추가해야 할 일이 생겼다.   이런저런 방법을 동원해도 스크립트가 추가가 안되었는데 다음과 같이 했더니   잘 동작 되었다.   // vue.config.js module.export = {   publicPath: process.env.NODE_ENV === 'production' ? './' : '', // 정적 리소스의 경로를 상대 경로로 지정한다.   outputDir: '../backend/src/main/resources/static', // 정적 파일 생성 위치   assetsDir: '', // js, css, img 파일 생성 위치   indexPath: '../templates/index.html', // index.html 파일이 생성되는 위치   devServer: {     proxy: {       '/': {         target: 'localhost:8090',         ws: true,         changeOrigin: true       }     }   } }   vue 설정은 위와 같이 하고   vue-cli 디렉토리 중에 public 디렉토리에 원하는 리소스 파일들을 넣는다.   그리고 아래와 같이 vue 파일에 작성한다.   // xxx.vue export default {   methods: {     addScript: function () {       let scripts = [         '스크립트 상대 경로',         'ex) libs/jquery/jquery.js',       ];        for (let i = 0, len = scripts.length; i < len; i++) {         let script = document.createElement('script');         script.setAttribute('type', 'text/javascript');         script.setAttribute('src', scripts[i]);         script.async = true;         document         .getElementsByTagName('body or head')[0]         .appendChild(script);       }     }   },   created () {     this.addScript();   } }  마지막으로 빌드하면 스프링 부트 resources 디렉토리에 static & templates 디텍토리안에 vue 에서 빌드한 파일이 생긴다.   npm run build   이제 스프링을 실행하면 성공이다.  ","categories": ["posts"],
34        "tags": ["vue"],
35        "url": "https://subji.github.io/posts/2019/07/05/vuejs-appendingscript-howto",
36        "teaser":null},{
37        "title": "Uncaught (in promise) TypeError Cannot read property 'protocol' of undefined 에러 처리",
38        "excerpt":"“Uncaught (in promise) TypeError Cannot read property ‘protocol’ of undefined”   Vue 2.x 버전에서는 axios 를 사용할 때, Vue.use(axios) 와 같이 사용하면 위와 같은 에러가 발생한다.   그러므로 다음과 같이 사용해야 한다.   Vue.use(axios); // Incorrect  Vue.prototype.$http = axios; // Correct  ","categories": ["posts"],
39        "tags": ["vue"],
40        "url": "https://subji.github.io/posts/2019/07/05/vuejs-crossorigin-error",
41        "teaser":null},{
42        "title": "Apache + Tomcat 연동 및 SSL 적용",
43        "excerpt":"첫번째로 apache 와 tomcat 을 연동하기 위해선 jk_mod 라는 아파치 플러그인을 설치해야 한다.   sudo apt install libapache2-mod-jk  ê·¸ 다음 jk_mod 가 설치된 디렉토리로 이동하여 파일 하나를 수정한다.  cd /etc/libapache2-mod-jk  vim worker.properties  # worker.properties worker.list=\"{사용자 정의 모듈 이름}\" ex) ajp13_worker  worker.\"{사용자 정의 모듈 이름}\".port=\"포트 번호 (Port Number)\" worker.\"{사용자 정의 모듈 이름}\".host=\"아이피 주소 (IP Address)\" worker.\"{사용자 정의 모듈 이름}\".type=\"ajp13\" worker.\"{사용자 정의 모듈 이름}\".type  worker.\"{사용자 정의 모듈 이름}\".lbfactor=1   다 작성했으면 아파치가 설치된 폴더로 이동하자  # ubuntu cd /etc/apache2  첫번째로 위에서 작성한 worker.properties 파일의 경로를 확인한다.  vim mod-availables/jk.conf  # jk.conf  JkWorkerFile /etc/libapache2-mod-jk/workers.properties   위에서는 기본적으로 jk_mod 에서 설치된 프로퍼티 파일경로 되어있ì
43§€ë§Œ 다른 경로에 추가하였을 경우 해당 경로로 변경해야 한다.   다음으로 sites-available 에 가상 호스트를 추가한다. 여기에서는 톰캣과의 연동과 SSL 인증서를 추가한다.  vim sites-available/\"{xxx}\".conf  # \"{xxx}\".conf  <IfModule mod_ssl.c>     <VirtualHost _default_:443>     ServerName \"{도메인 네임 (Domain name)}\"     DocumentRoot \"{톰캣의 ROOT 디렉토리 경로를 설정한다 (Tomcat Directory path)}\"      JkMount /* ajp13_worker <!-- Apache + Tomcat 연동 -->      SSLEngine on <!-- SSL 연동 -->      SSLProtocol all -SSLv2 -SSLv3     SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128:DHES-RSA-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHAAES256-SHA:AES:CAMESIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA     SSLHonorCipherOrder on     <!-- SSL 인증서 및 키 파일 설정 -->     SSLCertificateFile \"{xxx}\".crt     SSLCertificateKeyFile \"{xxx}\".key     SSLCertificateChainFile \"{xxx}\".pem     SSLCACertificateFile \"{xxx}\".crt     <!-- default-ssl.conf 기본 설정 -->     SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire      <FilesMatch \"\\.(cgi|shtml|phtml|php)\">         SSLOptions +StdEnvVars     </FilesMatch>     <Directory /usr/lib/cgi-bin>         SSLOptions +StdEnvVars     </Directory>     <!-- 톰캣 디렉토리에 대한 다음과 같은 설정을 해야 SSL 이 적용된다. -->     <Directory \"{Tomcat ROOT Directory path}\">         Options Indexes FollowSymLinks         AllowOverride None         Order allow,deny         Allow from all         Require all granted     </Directory> </VirtualHost>  마지막으로 톰캣 디렉토리에서 server.xml 을 수정한다.  vim conf/server.xml  #server.xml  <Connector port=\"8009\" protocol=\"AJP/1.3\" redirectPort=\"8443\" /> <!-- 주석 제거 -->   이제 아파치와 톰캣으로 재시작 하면 적용된다.  # restart apache sudo systemctl restart apache2 # restart tomcat sh shutdown.sh sh startup.sh   마지막으로 http 로 오는 접속을 https 로 리다이렉트해주는 방법이다. ```bash  “{user defined configuration file in apache sites-available}”.conf  <VirtualHost :80>     ServerName “{Domain name}”     ServerAlias “{Domain name alias}”     ProxyPass / “{url}”     ProxyPassReverse / “{reverse url}”     # HTTP -> HTTPS Redirect     RewriteEngine On     RewriteCond %{SERVER_PORT} !443 # ‘%{HTTPS} off’ is not work     RewriteRule ^(/(.))?$ https://%{HTTP_HOST}/$1 [R=301,L] # ^(/(.*))?$ = all url patterns   SetOutputFilter DEFLATE SetEnvIfNoCase Request_URI \"\\.(?:gif|jpe?g|png)$\" no-gzip  </VirtualHost>  ","categories": ["posts"],
44        "tags": ["ssl","tomcat","apache"],
45        "url": "https://subji.github.io/posts/2019/07/08/ssl-apacheandtomcat-howto",
46        "teaser":null},{
47        "title": "Oracle 날짜 관련 쿼리",
48        "excerpt":"참고 함수 및 포맷  # REF FUNCTION TO_CHAR(TARGET, FORMAT) : 숫자 또는 날짜를 지정한 포맷에 맞게 변환 TO_DATE(TARGET)         : 문자를 DATE로 변환  # FORMATS 'YYYY'  :   4자리 년도로 표시  'YY'    :   끝의 2자리 연도로 표시 ('2019' -> '19') 'YEAR'  :   연도를 알파뱃으로 표시  'Q'     :   해당월의 분기 'MM'    :   달을 두자리 숫자로 표시 'MON'   :   달을 알파뱃 약어로 표시  'MONTH' :   달을 알파뱃으로 표시 'W'     :   해당 월의 주차 'WW'    :   현재 년도의 1일부터 7일단위의 주차 'IW'    :   날짜가 해당한 주가 하루라도 다음해인 경우에는 그달의 첫주로 한다 'DD'    :   일자를 두자리 숫자로 표시 'DAY'   :   일자에 해당하는 요일 'DY'    :   일자에 해당하는 요일의 약어 'HH'    :   12시간으로 표시 (1 - 12) 'HH24'  :   24시간으로 표시 (0 - 23) 'MI'    :   분을 표시 'SS'    :   초를 표시 'AM, PM':   오전, 오후를 표시  날짜 계산  # EX) SYSDATE = \"2019-08-27 13:24:00\"  # 특정 날짜와 현재 날짜와의 차이를 NUMBER 형으로 반환 #   = 0.050020534647550776583
480346475507765830346 SELECT MONTHS_BETWEEN(SYSDATE, TO_DATE('2019-08-26'), 'YYYYMMDD') FROM DUAL;   # 특정 날짜에 N 개월을 더하기 #   = 2019-12-27 13:13:51 SELECT ADD_MONTHS(SYSDATE, 4) FROM DUAL;  # 특정 날짜의 다음주 특정 요일 구하기 #   ! 시스템 장소 정보에 따라 한국일 경우 두번째 인자를 한글로, 영어권일 경우 영어로해야 #     에러가 발생하지 않는다. #   = 2019-09-02 13:26:27 SELECT NEXT_DAY(SYSDATE, '월요일') FROM DUAL;  # 특정 날짜의 반올림 EX) 특정 날짜의 시간이 오후면 반올림 #   = 2019-08-28 00:00:00 SELECT ROUND(SYSDATE, 'DD') FROM DUAL;  # 전년도의 12월 31일 SELECT TRUNC(SYSDATE, 'IY') FROM DUAL;  # 같은 년도의 첫날 SELECT TRUNC(SYSDATE, 'YY') FROM DUAL;  # 같은 년도의 마지막날 #   = 2019-12-31 00:00:00 SELECT ADD_MONTHS(TRUNC(SYSDATE, 'YEAR'), 12) - 1 FROM DUAL;  # 분기의 첫달 1일 SELECT TRUNC(SYSDATE, 'Q') FROM DUAL;  # 특정 날짜가 속한 달의 첫날  #   = 2019-08-01 00:00:00 SELECT TRUNC(SYSDATE, 'MONTH') FROM DUAL;  # 특정 날짜가 속한 달의 마지막 날 #   = 2019-08-31 13:23:53 SELECT LAST_DAY(SYSDATE) FROM DUAL;  # 특정 달의 일수 #   = 31 SELECT CAST(TO_CHAR(LAST_DAY(SYSDATE), 'DD') AS INT) FROM DUAL;  # YYYY-MM-01 표시 SELECT TRUNC(SYSDATE, 'MM') FROM DUAL;  # YYYY-MM-DD 표시 SELECT TRUNC(SYSDATE, 'DD') FROM DUAL;  # 분기 계산 SELECT TRUNC(SYSDATE, 'Q') 'N분기' FROM DUAL;  # 주차 구하기  # 일요일 기준 SELECT TRUNC((TRUNC(SYSDATE) - TRUNC(TRUNC(SYSDATE, 'YY'), 'D')) / 7) + 1  # 월요일 기준 SELECT TRUNC((TRUNC(SYSDATE) - TRUNC(TRUNC(SYSDATE, 'YY'), 'IW')) / 7) + 1   출처:   오라클 TRUNC 날짜 기능         SYSDATE 기준 일자관련 다양한 표현2         날짜 함수 및 날짜 구하기         오라클 날짜계산         년 주차 구하기  ","categories": ["posts"],
49        "tags": ["Oracle"],
50        "url": "https://subji.github.io/posts/2019/08/27/oracle-getweeknumberofyear-howto",
51        "teaser":null},{
52        "title": "Git error: src refspec master does not match any",
53        "excerpt":"Git 푸시 도중 아래와 같은 에러가 발생하였다.  error: src refspec master does not match any error: failed to push some ref to 'URL'  위 에러는 푸시를 할때, 커밋된 내용이 없기때문에 깃 서버에 추가할  브랜치가 존재하지 않아 발생하는 것이다.   즉, 빈 리포지토리 (예. 초기에 리포지토리를 생성한 경우)일 때, 아무런 커밋 없이 푸시를 하였을때 발생한다.   이는 커밋을 하고 푸시를 하면 쉽게 해결이 가능하다.  ","categories": ["posts"],
54        "tags": ["git"],
55        "url": "https://subji.github.io/posts/2019/08/28/git-commiterror-solve",
56        "teaser":null},{
57        "title": ".gitignore가 적용되지 않는다",
58        "excerpt":"git 푸시를 할때 가끔 gitignore 가 제대로 동작하지 않는 경우가 있다.   이 경우 git 캐시를 지우고 다시 푸시해주자  git rm -r --cached . git add . git commit -m '깃 이그노어가 적용되지 않아서 다시 올려요~'  출처: My .gitignore file is ignored by git and it does not work  ","categories": ["posts"],
59        "tags": ["git"],
60        "url": "https://subji.github.io/posts/2019/08/28/git-gitignorenotwork-solve",
61        "teaser":null},{
62        "title": "/tmp/spring.log (Permission denied)",
63        "excerpt":"하나의 프로젝트에서 여러개의 프로파일을 관리할때 /tmp/spring.log 가 같이 사용되어 권한 오류가 발생하는 경우가 있다.   보통 logback 의 기본설정 파일을 가져오면서 같은 위치를 가리키게 되어 생기는 오류인데,   이럴 경우 아래와 같이 해결할 수 있다.  <property name=\"LOG_TEMP\" value=\"Log/\" /> <include resource=\"org/springframework/boot/logging/logback/base.xml\"/>  위 문구를 개별 logback 파일에 추가해주며, 주의사항을 반드시 property 태그가 base.xml 를 포함하는 태그보다   위에 와야 한다.  ","categories": ["posts"],
64        "tags": ["spring"],
65        "url": "https://subji.github.io/posts/2019/11/12/springboot-logbackpermissiondenied-solve",
66        "teaser":null},{
67        "title": "How to access Resources (images, etc...) on spring boot (외부에서 파일 접근 방법)",
68        "excerpt":"스프링 부트를 도커에 올려서 사용하다가 외부에서 이미지 접근이 안된다..   로컬에서는 잘 동작하지만 클라이언트가 서버상의 경로를 몰라서 인지 404에러를 뱉어냈다.   이럴 경우 다음과 같이 디렉토리 경로를 지정해주면 된다.  @Configuration public class WebConfig extends WebMvcConfigurerAdapter {  \t@Value(\"${'파일이 있는 디렉토리 경로'}\")   private String path;    @Override   public void addResourceHandlers(ResourceHandlerRegistry registry) { \t\tregistry \t\t\t.addResourceHandler(path + \"/**\") // url 접근 경로 \t\t\t.addResourceLocations(\"file:\" + path + \"/\"); // 디렉토리 경로 (반드시 file: 을 붙여주어야 한다.)   } }  이로서 클라이언트에서도 서버의 리소스에 쉽게 접근이 가능해졌다.  ","categories": ["posts"],
69        "tags": ["spring boot"],
70        "url": "https://subji.github.io/posts/2019/11/21/springboot-external-reources-solve-copy-2",
71        "teaser":null},{
72        "title": "List or Map value print just string (vscode debugger에서 list 또는 map이 문자열로만 나올 경우)",
73        "excerpt":"스프링 프로젝트 중 디버깅이 필요해서 vscode 에서 지원하는 디버깅을 사용했다.   그런데.. list 나 map 이 값으로 조회되는게 아니라 문자열로 “arraylist@xxx size=0” 이런식으로 나오는게 아닌가??!   그래서 열심히 구글링을 해보니 튜토리얼에 제공
73이 되어있어 쉽게 해결할 수 있었다.  {   \"version\": \"0.2.0\",   \"configurations\": [     {       \"name\": \"(lldb) Launch\",       \"type\": \"cppdbg\",       \"request\": \"launch\",       \"program\": \"${workspaceFolder}/helloworld.out\",       \"args\": [],       \"stopAtEntry\": true,       \"cwd\": \"${workspaceFolder}\",       \"environment\": [],       // set debugger config for printing map or list value       // ========================       \"externalConsole\": true,       \"MIMode\": \"lldb\",       \"logging\": {         \"trace\": true,         \"traceResponse\": true,         \"engineLogging\": true       }       // ========================     }   ] }  표시된 곳을 디버거 툴 옆 톱니바퀴를 눌러 나오는 configuration 에 입력해주면 된다.  ","categories": ["posts"],
74        "tags": ["Visual Studio Code"],
75        "url": "https://subji.github.io/posts/2019/11/21/vscode-debuggerlistandmapvalue-solvecopy",
76        "teaser":null},{
77        "title": "Spring Security Cross Domain",
78        "excerpt":"프로젝트를 하다보면 항상 마주하는 Cross Domain 문제   이번에도 ì
78–´ê¹€ì—†ì´ 문제가 생겼다.   같은 서버에서 포트가 다른 두개의 웹서버가 있고, 한 웹 서버에서 다른 웹 서버가 관리하는 이미지를 불러올때 발생하는 문제였다.   항상 발생하는데 적어두질 않으니 매번 찾아보기를 반복한다.   그래서 찾으면서 해결? 조치 방법을 기록해놓았다.   1. CORS 는 HTTP Header 의 OPTIONS 요청은 무시한다?   </br>HTTPMethod.OPTIONS 를 허용해보았다.   </br>결과는..   </br>아무 상관이 없었다..   2. CorsFilter 를 추가?   </br>인증과정을 거치기전에 Filter 를 추가해서 사전에 CORS 검사를 해보았다.   </br>이쯤에서 해결되면 다행이다.   </br>하지만…   </br>역시나 안된다. 무엇이 문제일까..   3. 같은 도메인 혹은 같은 아이피에서 쿠키를 공유함에 있어서 생기는 문제?   </br>이번에 처음 겪게된 경우다.   </br>스택오버플로우에서 검색하고 찾아봐서 이리저리 해봐도 안되던게 단지 서버에서 관리되는 쿠키 때문이었다.   </br>그래서 curl 요청 시 쿠키를 제거하고 요청하니 CORS 에러가 발생하지 않는것이었다!   해결 / 조치 :   </br>어렵지 않았다. 단지 이미지를 제공해주는 웹서버에서 세션관리를 STATELESS 하게 하면된다.   소스 코드 :   /** * Web Security Config */  @Override public void configure (WebSecurity web) throw Exception {   web.ignore().antMatcher(\"${URL_PATH}\"); }  @Override protected void configure (HttpSecurity http) throw Exception {   http       .csrf().disable()     .and()       .sessionManagement()       .sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 중요! }  /** * Web MVC Config (CORS 등록) */ @Override public void addCorsRegistry (CorsRegistry registry) {   registry     .addMapping(\"/*\")     .allowedOrigins(\"*\")     .allowedMethods(\"GET\", \"POST\", \"OPTIONS\", \"PUT\")     .allowedHeaders(\"Content-Type\", \"X-Requested-With\", \"accept\", \"Origin\",\"Access-Control-Request-Method\", \"Access-Control-Request-Headers\")     .exposedHeaders(\"Access-Control-Allow-Origin\", \"Access-Control-Allow-Credentials\")     .allowCredentials(true)     .maxAge(3600); }   ","categories": ["posts"],
79        "tags": ["Spring Security"],
80        "url": "https://subji.github.io/posts/2019/12/06/springsecurityignorecors",
81        "teaser":null},{
82        "title": "Spring Getter & Setter 보안취약성 관련",
83        "excerpt":"웹 어플리케이션의 보안취약성 점검을 받고   다음과 같은 문제가 있다는것을 보고받았다.   “Public 메소드부터 반환된 Private 배열 - private로 선언된 배열이 public 메소드를 통해 반환이 되고 있음”   알고보니 Getter & Setter 가 정의된 VO 혹은 Domain 파일이었다.   다른 곳은 아무 이상없는데 왜 저기 특정 메소드에만 문제가 있는것인가 하는 의문이 생겼다.   자세히 보니 다른 메소드에서는 기본형 타입을 반환하고 있었고, 문제가 된 해당 메소드에서는 참조(객체) 타입을   반환하고 있었다.   즉, 기본형 타입 (int, String, .. 등) 이 아닌 참조(객체) (Array, … 등) 은 주소 정보가 전달되기 때문에   이를 그대로 반환하는 것보단 새로 복사를 해서 반환해야 한다는 것이었다.   그래서 다음과 같이 수정했다.   소스 코드 :  private String[] test;  public String[] setTest (String[] test) {   String[] temp = new String[test.length];   System.arraycopy(test, 0, temp, 0, test.length);   this.test = temp; }  public String[] getTest (String[] test) {   String[] temp = new String[test.length];   System.arraycopy(test, 0, temp, 0, test.length);   return temp; }  ","categories": ["posts"],
84        "tags": ["spring"],
85        "url": "https://subji.github.io/posts/2019/12/20/springprivategettersetter-solve",
86        "teaser":null},{
87        "title": "Docker Got permission denied while trying to connect to the Docker daemon socket at... 에러 해결",
88        "excerpt":"도커를 처음설치하고 실행 후 “docker ps -a” 를 입력해보니   **“Got permission denied while trying to connect to the Docker daemon socket at” 에러를 출력했다   권한이 없다는 문제인데 간단하게 현재 로그인한 유저에게 도커 권한을 주면 되었다.   소스 코드 :  sudo usermode -a -G docker $USER sudo reboot  ","categories": ["posts"],
89        "tags": ["Docker"],
90        "url": "https://subji.github.io/posts/2019/12/22/dockerGot-permission-denied-while-trying-to-connect-to-the-Docker-daemon-socket/.-solve",
91        "teaser":null},{
92        "title": "자바 & 스프링 실행 명렁어에서 속성값을 변경하는 실행 커맨드(h
92ow to change property when use java -jar command with option)",
93        "excerpt":"| 문제  스프링 부트 프로젝트를 하면서 application.yml 내의 프로퍼티값을 변경 & 추가해주어야 하는 상황이 발생했다.   | 해결  보통 war 파일을 실행할 때 java -jar file.war 로 실행한다.   이때, -Drun.arguments=–{프로퍼티명=값} 을 추가해주면 application.yml 내의 값이 변경 & 추가가 된다.   처음에는 -D프로퍼티명=값 으로 했는데 적용이 안되었다.   그래서 찾아보니 -D 는 시스템 프로퍼티를 설정하는 옵션이라고 한다.   그래서 시스템 프로퍼티인 run.arguments 를 사용하여 커스텀 프로퍼티를 넘겨주니 잘 된다.   주의할 점은 스프링부트의 메인 함수에서 다음과 같이 작성이 되어있어야 한다.   | 소스 코드  java -jar -Drun.arguments=--test.test=test application.war  public static void main(String[] args) {   // args 를 반드시 파라미터로 넘겨주어야 한다.   SpringApplication.run(MainApplication).class, args); }  ","categories": ["posts"],
94        "tags": ["java","spring"],
95        "url": "https://subji.github.io/posts/2020/01/10/java-jar-with-option-override-property-solve",
96        "teaser":null},{
97        "title": "Crontab 적용 시 주의사항(Precautions to apply Crontab)",
98        "excerpt":"| 문제  ShellScript 파일을 Crontab 에 적용하는데 있어서 실행이 안되는 경우가 발생하였다.   | 해결     가급적이면 ‘vi crontab’ 대신 ```crontab -e’ 를 사용하자 ‘vi’ 로 실행하는 경우는 계정 권한에 따른 설정이 달라지는데, ‘crontab -e’ 로 실행하는 경우는 이를 고려하지 않아도 되기 때문이다.   Crontab 에는 절대경로 / 환경변수 설정을 꼭 해주어야 한다.   가급적이면 ShellScript 로 만들고, 파일 상단에 ‘/bin/bash, /…/bin/python’ 등과 같이 환경변수 설정을 해주자   ShellScript 는 이왕이면 실행가는한 파일로 만들자.     chmod +x 스크립트파일.sh           Crontab 의 최소 시간은 1분이며, 초단위로 하고자 할때는 * * * * sleep {second}; {명령어} 의 형태로 사용하자.  ","categories": ["posts"],
99        "tags": ["linux"],
100        "url": "https://subji.github.io/posts/2020/01/15/crontab-cautions",
101        "teaser":null},{
102        "title": "Java 특정 요일을 기준으로 주차 구하기(Get number of week where first day of some day)",
103        "excerpt":"| 문제  년도를 기준으로 주차를 구하거나 달의 주차를 구할때 Calendar 객체에 기본값으로 정해진 주차를 구하다보니 원하는 주차가 나오지 않는 경우가 발생하였다.   | 해결     Calendar 의 인스턴스를 가져올때 파라미터로 로케일값을 주는 방법이있다.     // 예제 -  Calendar cal = Calendar.getInstance(new Locale(\"en\", \"US\")); // 일요일 기준 주차 구하기 Calendar cal = Calendar.getInstance(new Locale(\"en\", \"UK\")); // 월요일 기준 주차 구하기 cal.get(Calendar.WEEK_OF_YEAR);           Calendar 객체의 setFirstDayOfWeek, minimalDaysInFirstWeek 를 적용하는 방법이있다.  minimalDaysInFirstWeek 메소드는 해당 연도의 첫주에 포함할 최소 일 수를 설정하는 것이다.  setFirstDayOfWeek 에 기준요일을 설정하고, minimalDaysInFirstWeek 에 포함할 일 수를 설정하면된다.     // 예제 - 일요일을 시작요일로 설정하고 주차를 계산 Calendar cal = Calendar.getInstance(); cal.setFirstDayOfWeek(Calendar.SUNDAY); cal.minimalDaysInFirstWeek(7); cal.get(Calendar.WEEK_OF_YEAR);          ","categories": ["posts"],
104        "tags": ["java"],
105        "url": "https://subji.github.io/posts/2020/01/15/java-get-numberofweekstartwithday-solve",
106        "teaser":null},{
107        "title": null,
108        "excerpt":"| 문제  웹 어플리케이션을 만들면서 현재페이지에서 다음페이지로 이동후에 이전페이지로 가기위한 Referer 헤더를 받아오는데, 주소를 직접 입력해서 접근하면 이전페이지 정보인 Referer 를 Null 로 받아왔다.   | 과정          “Referer” 란?             Referer 요청 헤더는 현재 요청된 페이지의 링크 이전의 웹 페이지 주소를 포함하는 헤더이다.           Interceptor 에서 가로채므로 Interceptor 에서 Referer 를 받아와보았다. => 실패   혹여나 세션은 받아올 수 있지 않을까하고 세션에 추가해서 받아보았다. => 실패   | 결론     구글링한 결과 Referer 는 다음과 같은 경우에는 받아올 수 없다고한다.            주소를 직접 입력한 경우       즐겨찾기로 이동한 경우       location.href 로 이동한 경우       참조되는 리소스가 로컬 “파일” 혹은 “데이터”의 URI인 경우       안전하지 않은 HTTP 요청이 사용되고 참조 페이지가 보안 프로토콜(HTTPS)로 수신된 경우          ","categories": [],
109        "tags": null,
110        "url": "https://subji.github.io/2021/12/09/2020-01-16-refererhttpheader-why",
111        "teaser":null},{
112        "title": "Git username, password 저장 방법 (how to save username and password in git)",
113        "excerpt":"| 문제  git pull, git push 하는데 있어서 계속 비밀번호를 입력해야되서 번거로웠다. 그래서 비밀번호를 입력하지 않고 저장해서 사용하는 방법을 찾아보았다.   | 과정     git config credential.password={password} => 아예 명령어가 안된다.   | 결론  다음 명령어를 먼저 실행한다. 전체에 공통 유저이름과 비밀번호를 저장하고 싶으면 –global 메뉴를 붙여준다.    git config (--global) credential.helper store  그리고 나서 다음 명령어를 실행한다.    git pull  이제 마지막으로 username 과 password 를 입력하면 저장이 된다.  ","categories": ["posts"],
114        "tags": ["git"],
115        "url": "https://subji.github.io/posts/2020/01/17/git-hotwtosaveusernameandpassword-solve",
116        "teaser":null},{
117        "title": "Javascript SNS(Social Media) 공유 방법 (How to share social media in javascript.)",
118        "excerpt":"| 문제  프로젝트 개발 중 페이지를 SNS (Social Media) 로 공유하는 기능을 추가해야 했다.   공유할 SNS는 Facebook, Twitter, Kakao, 그리고 간단하게 링크를 클립보드로 복
118사하는 정도이다.   | 과정  간단하다.   Facebook 이나 Twitter 의 경우 공유 API 가 존재하기 때문에, 해당 API 로 파라미터만 넘겨주면 된다.   반면 카카오의 경우에는 카카오 개발자 페이지에서 앱(or 웹) 등록을 해주어야 한다.   공유할때 그냥 링크만 달랑 공유할 수 있지만 그렇기에는 밋밋하므로 미리보기 페이지가 같은게 나오면 좋다.   미리보기 페이지는 Facebook 에서 만든 OpenGraph 가 있다. 사실상 표준으로 굳어지는 듯 하다.   오픈그래프는 간단하게 메타태그에 Prefix 로 “og” 가 붙은 태그를 말하며, 페이지를 크롤링 하는 봇들이 해당 태그를 읽어서 미리보기로 만들어주는 것이다.   오픈그래프를 설정할 때는 다음과 같이 중요한 점이 있다.      외부에 공개된 도메인 또는 고정 아이피로 된 페이지어야 한다.   Javascript 로 동적으로 변경되는 오픈 그래프는 적용되지 않는다. 처음 불러온 html 페이지의 og 속성만을 가져온다.   동적 메타태그를 생성하기 위해서는 서버사이드 렌더링 또는 아파치 nginx 와 같은 웹서버에서 따로 Redirect 를 해주어야 한다.   og:image 의 경우 “http://” 를 포함한 절대 경로를 입력하며 상대 경로를 입력하면 안된다.   og:image 의 경우 최소 크기가 200x200 픽셀이다.   Twitter 전용 Open Graph 가 있지만 Facebook 의 OpenGraph 도 공용으로 사용된다.   | 결론  // Facebook 공유 window.open(   'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent('공유할 주소') ); // Twitter 공유 window.open(   'https://twitter.com/intent/tweet?url=' + encodeURIComponent('공유할 주소') ); // Kakao 공유 Kakao.init('카카오 개발자 페이지에서 발급받은 키'); Kakao.Link.sendDefault({   objectType: 'feed',   content: {      title: '공유 미리보기 제목',      imageUrl: '공유 미리보기 썸네일',      link: { webUrl: '공유할 주소' }    },   buttons: [{      title: '링크 열기', link: { webUrl: '공유할 주소' }    }]; });   <!-- Facebook (Common) Open Graph --> <meta property=\"og:type\" content=\"미리보기 종류\" /> <meta property=\"og:url\" content=\"페이지 URL\" /> <meta property=\"og:title\" content=\"미리보기 제목\" /> <meta property=\"og:description\" content=\"미리보기 설명\" /> <meta property=\"og:image\" content=\"미리보기 이미지 주소\" /> <!-- Twitter Open Graph --> <meta name=\"twitter:card\" content=\"트위터 미리보기 종류\" /> <meta name=\"twitter:url\" content=\"페이지 URL\" /> <meta name=\"twitter:title\" content=\"미리보기 제목\" /> <meta name=\"twitter:description\" content=\"미리보기 설명\" /> <meta name=\"twitter:image\" content=\"미리보기 이미지 주소\" />  ","categories": ["posts"],
119        "tags": ["git"],
120        "url": "https://subji.github.io/posts/2020/01/17/javascriptsnsshare",
121        "teaser":null},{
122        "title": "Javascript SNS(Social Media) 공유 방법 (How to share social media in javascript.)",
123        "excerpt":"| 문제  Eclipse 에서 Spring 프로젝트를 Tomcat 위에서 실행하였을때 다음과 같은 에러 화면이 나왔다.   “404 Not Found”   “The origin server did not find a current representation for the target resource or is not willing to disclose that one exists.”   | 과정  별다른 에러 로그가 출력이 나오지 않아 구글 검색을 하였다.   문제는 간단했다. 컨트롤러가 어떤 경로를 못찾는것.   컨트롤러 주소 매핑이 잘못되었나했지만 아니었다.   찾아보니 Web Context Path 가 잘못되있었다.   | 결론  IDE 또는 Context Path 를 수정하자.  ","categories": ["posts"],
124        "tags": ["git"],
125        "url": "https://subji.github.io/posts/2020/01/17/Theoriginserverdidnotfind.",
126        "teaser":null},{
127        "title": "정규표현식 정리 (진행중..) Regex",
128        "excerpt":"| 문제  비밀번호를 검사하는 정규식을 만드는데 이런저런 시행착오를 겪다가   내친김에 앞으로 자주 사용되는 정규식을 여기에 정리해보자 하고 만들게 되었다.   | 정규표현식 목록     비밀번호 정규표현식         특수문자, 소문자, 대문자, 숫자 1개 이상.     특수문자, 소문자, 대문자, 숫자 외 다른 입력 제외     8자 이상, 16자 이하       /^(?=.*[a-zA-z0-9])(?=.*[$`~!@$!%*#^?&\\\\(\\\\)\\-_=+])(?!.*[^a-zA-z0-9$`~!@$!%*#^?&\\\\(\\\\)\\-_=+]).{8,16}$/g     ","categories": ["posts"],
129        "tags": ["regular expression"],
130        "url": "https://subji.github.io/posts/2020/02/11/regexpopular",
131        "teaser":null},{
132        "title": "Git fatal: refusing to merge unrelated histories 오류",
133        "excerpt":"| 문제     기존에 있던 로컬 디렉토리에 깃 리포지토리를 연결하고 Push 또는 Pull 을 할때   “fatal: refusing to merge unrelated histories” 오류가 발생하여 동작하지 않았다.   | 과정     이 오류는 로컬 디렉토리와 깃 리포지토리의 기록을 비교했을때 소스코드의 차이가 심할 경우 병합 (Merge) 의 오류가 발생할 것을 대비하여 발생시키는 ì˜
133¤ë¥˜ì´ë‹¤.   | ê²°ë¡      두가지 방법이 있다.   첫번째, 현재 디렉토리의 내용을 전부 삭제하고 깃 리포지토리를 Clone 하는 방법   두번째, –allow-unrelated-histories 를 git pull 뒤에 입력하여 받아 오는 방법이다.   두번째의 경우 관련없는 두 가지 (로컬 디렉토리, 깃 리포지토리) 의 기록의 병합을 허용하는 옵션이다.     $ git pull origin {branch name} --allow-unrelated-hitories  ","categories": ["posts"],
134        "tags": ["git"],
135        "url": "https://subji.github.io/posts/2020/02/12/Git-fatalrefusing-to-merge-unrelated-histories-error",
136        "teaser":null},{
137        "title": "Spring Boot (Maven) with React Study day 1",
138        "excerpt":"  목적 ===     Spring boot 와 React 를 활용하여 간단한 웹 페이지를 만들어보고자 한다.   1. React JS 란?      페이스북에서 개발한 라이브러리   UI 를 컴포넌트화하여 재사용에 용이함   Virtual DOM 이라는 개념을 사용   JSX     1-1. Virtual DOM?       UI 의 가상의 DOM 객체를 메모리에 저장하고 이를 라이브러리를 통해 실제 DOM 에 동기화 하는 프로그래밍 개념   이와 같은 과정을 재조정이라고 한다.   기존의 DOM 의 경우 페이지를 로드할때 재구성되는 과정에서 성능이 낮은데, Virtual DOM 의 경우 동적인 페이지 로드 환경에서 이전값과 현재값을 비교하여 차이가 있는 부분만 변경해주기 때문에 성능상 이점이 있다.   단, DOM 의 경우 무조건 느린 것이 아닌 ReFlow 현상이 잦은 페이지에서 성능 저하가 나오며 그외의 ReDraw 상황 또는 데이터 변화가 적은 환경에서는 성능 차이가 없다.   Virtual DOM 의 경우 데이터의 잦은 변화가 있는 페이지에서 유용하다는 점을 주목하여 이에 맞게 사용하는 편이 낫다.     1-2. JSX?       Javascript 확장 문법   Javascript 코드 안에서 UI 관련작업을 할때 도움이 되며, React 의 에러 처리에도 도움   React 의 이벤트 처리방식, 상태가 변하는 방식, 데이터를 화면에 보여줄 방식등 로직과 마크업을 컴포넌트에 묶어 사용     const name = 'Tester'; const element = <h1>Hellow, {name}</h1> // JSX, name 변수를 JSX 안에 넣어주었다.  ReactDOM.render(element, document.getElementById('root'));           모든 Javascript 표현식을 사용할 수 있다. ```javascript function formatName (user)  { return user.firstName + ‘ ‘ + user.lastName; }   const user = { firstName: ‘First’, lastName: ‘last’ };   const element = (       Hello, {formatName(user)}      )   ReactDOM.render(element, document.getElementById(‘root’));   - JSX 도 하나의 Javascript 객체이며, **if,for loop** 로 사용이 가능하다. - MarkUp 과 같이 Attribute 를 태그안에 넣어줄수 있으며, 변수 역시 삽입이 가능하다. 단, 변수 삽입 시에는 **\"\"** 를 제외하고 중괄호로 감싼다. - Attribute 이름은 JSX 의 특성상 Javascript 에 가깝기 때문에 camelCase 를 사용. ```javascript const element = <img src={user.avatarUrl}></img>;     ReactDOM 은 렌더링 전 모든 값을 이스케이프(&lt, …) 하므로, 애플리케이션에서 명시적으로 작성되지 않은 내용은 주입되지 않는다.   모든 항목은 렌더링전 문자열로 변환되며, 이러한 특성으로 XSS 공격을 방지 할 수 있다.   2. React JS 체험   2-1. 세팅     NodeJS 설치 : https://nodejs.org/ko/ 에서 LTS 버전으로 설치하였다.   NPM 설치 : NodeJS 를 설치하면 NPM 이 자동으로 설치된다.   설치 확인     $ node -v  $   v10.16.0 $ npm -v $   6.13.7          2-2. React 프로젝트 생성       NPX : NPM 패키지를 실행하는 명령어   create-react-app 로 프로젝트 생성     $ npx create-react-app my-app           다음과 같은 프로젝트 구조가 생성              “npm start” 명령어로 실행     $ npm start           http://localhost:3000 으로 접속하고 다음 화면이 나오면 정상적으로 동작된것이다.           정리      React JS 는 라이브러리이다.   JSX, Virtual DOM 등을 사용한다.   설치방법   다음에 정리할 내용      Spring Boot 설치   Spring Boot & React 연동  ","categories": ["posts"],
139        "tags": ["spring boot","maven","react"],
140        "url": "https://subji.github.io/posts/2020/02/12/react-with-spring-1",
141        "teaser":null},{
142        "title": "Git Log 보기",
143        "excerpt":"| 문제  git history 를 보고싶어졌다. 간단하게 git log 를 이용하여 확인 할 수 있지만   좀 더 원하는 대로 출력할 수 있지 않을까 하는 생각에 찾아보게 되었다.   | 과정  구글검색을 하
143였다.   | 결론  git log 에서 내용을 보면 commit 옆에 해쉬 값이 출력되는데 해당 값에서 앞 8자리를 사용해 HEAD 를 이동할 수 있다.  $ git log  $ commit 3089658e7229cd1d130a1a89095f8184b1eb53c5   Author: ...   Date: ...  –stat 옵션은 기존 로그에서 어떤 파일에 수정이 일어났는지를 덧붙여준다.  $ git log --stat  –pretty 옵션은 email, format, full, fuller, medium, oneline, raw, short 설정이 가능하다.  아래는 한 줄로 간략하게 보여주는 설정을 추가한 예이다.  $ git log --pretty=oneline $ f617baa0cf80a331b6b97c6d1e792749399b5588 202002121305   ddbc68766b4a4ad1c0a291c178ad72f8876ba53e 202002111257   bb5b51571a010c86dbb7d9b292d44282e3cc8a6f 202002041210   22423b1e00444b7fa8274c66f28dc5de993baa26 202001301720  format 설정은 커밋 정보를 format 에 맞추어 보여준다. 자세한 내용은 아래에 있다.     % – raw %                    D – ref name                 N – notes                    a – author details           d – ref name in brackets     g – reflog                   n – newline                  t – abbreviated tree hash  B – raw body                 G – GPG details              P – parent hash              b – body                     e – encoding                 h – abbreviated commit hash  p – abbreviated parent hash  w – switch line wrapping   C – color                    H – commit hash              T – tree hash                c – committer details        f – sanitized subject        m – mark                     s – subject                  x – hex code     -p -{숫자} 숫자만큼 상제한 내용을 더 보여주는 옵션이다.  $ git log -p -10 $ commit 3089658e7229cd1d130a1a89095f8184b1eb53c5 (HEAD -> master, origin/master, origin/HEAD)   Author: ...   Date:   Wed Feb 12 13:54:09 2020 +0900        202002121354    diff --git a/_posts/2020-02-12-Git fatal: refusing to merge unrelated histories error.md b/_posts/2020-02-12-Git fatal: refusing to merge unrelated histories error.md   new file mode 100644   index 0000000..f05290e   --- /dev/null   +++ b/_posts/2020-02-12-Git fatal: refusing to merge unrelated histories error.md          @@ -0,0 +1,23 @@   +---   +title: \"Git fatal: refusing to merge unrelated histories 오류\"   +date: 2020-02-12 ...  마지막으로 –graph 옵션은 GUI 가 입혀진 로그를 출력해준다.  ","categories": ["posts"],
144        "tags": ["git"],
145        "url": "https://subji.github.io/posts/2020/02/13/git-log",
146        "teaser":null},{
147        "title": "Apache 의 동작 방식",
148        "excerpt":"| Apache?  우선 아파치는 HTTP Server 로 오픈소스이다.   Nginx 도 많이 사용하지만 아직까지는 Apache 의 사용률이 우세인것 같다.   보통은 Web Application Server 인 톰캣등에서 사용되는 정적 리소스 (Static Resource) 들을 처리할때 사용한다.   또한 모듈을 추가하여 사용할 수도 있다. 대표적으로 SSL 과 같은 모듈을 사용하여 HTTPS 를 적용하곤한다.   | Apache 의 동작  아파치는 다양한 환경, 플랫폼에서 사용할 수 있게 설계되었다.     그래서 각각의 다른 환경, 플랫폼에 대응할 수 있게 모듈화된 설계를 사용했다.     이처럼 유연한 모듈화된 설계방식을 아파치에서는 시스템쪽으로도 확장하여    네트워크의 처리를 Root 와 Child 를 두어 분산시키는 다중처리모듈(Multi-Processing Modules, MPMs) 를 사용하였다.     아파치의 동작 방식은 http -V 를 입력하여 확인할 수 있다.  $ http -V    Server version: Apache/2.2.15 (Unix)   Server built:   Jul 23 2014 14:17:29   Server's Module Magic Number: 20051115:25   Server loaded:  APR 1.3.9, APR-Util 1.3.9   Compiled using: APR 1.3.9, APR-Util 1.3.9   Architecture:   64-bit   Server MPM:     Prefork     threaded:     no       forked:     yes (variable process count)   Server compiled with....  위 명령어에 대한결과에서 보면 MPM 이라고 적힌 부분 옆의 Prefork 가 현재 구동중인 아파치의 동작방식이며   아파치 설치 후 초기의 동작방식이다.  | Apache 동작 방식의 종류  1. Prefork     요청 하나당 하나의 프로세스를 할당하는 방식   요쳥에 대한 처리가 각각의 프로세스에서 처리되므로, 하나의 프로세스에서 오류가 발생해도 다른 프로세스에 영향을    미치
148지 않는다.   요청에 많아짐에 따라 프로세스가 늘어나므로 시스템의 메모리, CPU 등 리소스에 부하가 생길 가능성이 높다.     2. Worker       각각의 요청에 대해 프로세스가 여러개가 생성되는 것이 아닌 프로세스의 쓰레드를 여러개 생성하여 할당하는 방식   다른 요청이라도 같은 프로세스내의 쓰레드를 사용하므로 메모리를 공유하게 된다. 그러므로 서로 영향을 미치게 된다.   Prefork 에 비해 적은 프로세스를 사용하므로 시스템 리소스의 부하가 비교적 적다.   단일 프로세스에서 여러개의 쓰레드를 관리하므로 프로세스에 오류가 발생했을 경우에 그 안의 쓰레드에 영향을 미친다.   한 프로세스의 쓰레드 한계치에 해당하는 대량의 연결이 발생하는 서버에서 적합하다.     3. Event       nginx 에 비해 뒤떨어지는 퍼포먼스로 인해 Apache 2.4 버전부터 추가된 동작 방식   이전의 두 방식은 프로세스가 요청에 대해 연결이 끊어지기 전까지 계속 연결을 유지하였다. 이와 같은 방식은 대량 접속의 경우에는 많은 시스템 리소스의 손실이 오게 된다.   Event 방식에서는 이러한 문제를 해결하기 위해 요청을 받는 쓰레드를 분리하여 분산화하였다.   | Apache 동작 설정  /etc/httpd/conf/httpd.conf 파일 또는 /etc/apache2/conf/httpd.conf 파일에서 설정이 가능하다.   위 파일을 열어보면 Prefork 와 Worker 밖에 안보이는데, Event 방식은 Worker 와 설정이 같기 때문에    Event 방식을 적용하고자 한다면 Worker 의 설정을 수정해주면된다.  # prefork MPM # StartServers: number of server processes to start # MinSpareServers: minimum number of server processes which are kept spare # MaxSpareServers: maximum number of server processes which are kept spare # ServerLimit: maximum value for MaxClients for the lifetime of the server # MaxClients: maximum number of server processes allowed to start # MaxRequestsPerChild: maximum number of requests a server process serves <IfModule prefork.c>   StartServers       8   MinSpareServers    5   MaxSpareServers   20   ServerLimit      256   MaxClients       256   MaxRequestsPerChild  4000 </IfModule> # worker MPM # StartServers: initial number of server processes to start # MaxClients: maximum number of simultaneous client connections # MinSpareThreads: minimum number of worker threads which are kept spare # MaxSpareThreads: maximum number of worker threads which are kept spare # ThreadsPerChild: constant number of worker threads in each server process # MaxRequestsPerChild: maximum number of requests a server process serves <IfModule worker.c>   StartServers         4   MaxClients         300   MinSpareThreads     25   MaxSpareThreads     75   ThreadsPerChild     25   MaxRequestsPerChild  0 </IfModule>  ","categories": ["posts"],
149        "tags": ["apache"],
150        "url": "https://subji.github.io/posts/2020/02/13/whatistheapache",
151        "teaser":null},{
152        "title": "Spring Boot (Maven) with React Study day 2",
153        "excerpt":"  목적 ===     Spring boot 와 React 를 활용하여 간단한 웹 페이지를 만들어보고자 한다.   이전 내용  이전에는 React 의 간단한 소개와 더불어 주요개념중에 일부인 VirtualDOM ê³¼ JSX 를 알아보았다.   그리고 create-react-app 명령어로 첫 react 앱을 실행시켜보았다.   Spring Boot (Maven) with React Study day 1     이번 시간에는 Maven 빌드툴을 기반으로한 Spring Boot 환경을 설정하고 실행을 해보자.   1. Spring Boot ?   기존의 Spring MVC 의 경우에는 개발자가 두루두루 사용되는 라이브러리들의 설정을 직접 하나하나 해줘야했다.   매 프로젝트마다 같은 작업을 반복하는 것은 참으로 지겹고도 고된 일이다.   그래서 나온게 Spring Boot 다.   Spring Boot 는 간단히 말해 Java 로 어플리케이션을 빠르고 간편하게 개발할 수 있도록 도와주는 도구이다.   1-1. Why Spring Boot ?   그렇다면 그냥 Spring MVC 를 사용해서 만들어도 되는데 굳이 Spring Boot 라는 것을 또 배워서 적용해야 될까?   기존의 Spring 개발방식을 고집한다면 말릴순 없다. 틀린건 아니니까..   하지만 나처럼 반복되는 일을 지겨워한다면 또는 기본적인 설정정도는 알아서 해줬으면 하는 개발자라면 한번쯤 사용해볼만하다.   Spring Boot 를 사용함으로써 얻는 이점은 다음과 같다.      Was 구축이 편리하다.            Spring MVC 에서는 톰캣을 다운로드해서 IDE 에 추가하고 어플리케이션과 연결도 해야했다.    하지만 Spring Boot 의 경우에는 내장 톰캣을 지원하여 Spring Boot 프로젝트를 생성하고 실행만하면 된다.           자주 사용되는 또는 다양한 종류의 라이브러리들이 지원된다.   기존의 Spring 방식보다 가볍고 쉽다.            기존의 Spring 에서는 xml 파일ì
153„ 만들고 설정을 작성하는 어렵고도 고된 방식이였다. Spring Boot 에서는   이러한 복잡하고도 어려운 방식을 .properties 또는 .yml 파일에 모아 쉽고 간편하게 작성이 가능하다.           2. Install Spring Boot   Spring Boot 를 설치해보고 실행시켜보자.  2-1. Install VSCode   VSCode 설치 에서 자신의 OS 에 맞는 VSCode 를 다운로드한다.   2-1. Install Spring Plugin   Ctrl + Shift + P (Mac 의 경우 Cmd + Shift + P) 를 눌러 아래 확장프로그램을 설치한다.     Debugger for Java   Language Support for Java   Java Test Runner   Maven for Java   Spring Boot Tools   Spring Initializer Java   Spring Boot DashBoard   Spring Boot Extentions   설치가 완료되면 VSCode 의 지시에 따라 재시작을 하고 다시 Ctrl + Shift + P 를 누른다.       위처럼 “Spring initializer: Generate a Maven Project” 로 프로젝트를 생성한다.   처음에는 어떤 언어로 개발할 것인지를 묻는다 (Java, Kotlin, Groovy)   나는 Java 만 알고있기 때문에 Java 를 선택하였다.   이후에는 GroupId 와 ArtifactId 를 입력하고 진행한다.   ê·¸ 다음으로는 중요한 항목인데         위처럼 Dependency 를 선택하는 창이 나온다. 여기서 개발자가 원하는 Dependency 를 선택하여 추가할 수 있다.   정말 간편하지 않은가?   그리고 마지막으로 프로젝트를 생성할 디렉토리를 선택하면 프로젝트가 생성된다.   3. Run Spring Boot   왼쪽 아래 “SPRING-BOOT DASHBOARD” 를 확인하면 생성한 프로젝트명이 표시된다.       화살표 버튼을 누르거나 프로젝트명을 우클릭하여 run/debug 모드로 실행할 수 있다.   정리      Spring Boot 는 간편하다.   Spring Boot 프로젝트 생성 및 실행   다음에 정리할 내용      Spring Boot & React 연동  ","categories": ["posts"],
154        "tags": ["spring boot","maven","react"],
155        "url": "https://subji.github.io/posts/2020/02/14/react-with-spring-2",
156        "teaser":null},{
157        "title": "Spring Boot 필드 인젝션이 아닌 생성자 인젝션을 해야 하는 이유",
158        "excerpt":"Field Inject 대신 생성사 주입을 해야하는 이유?   의존성 주입 (Dependency Indject) ?  프로그래밍에서 구성요소간의 의존 관계가 소스코드 내부가 아닌 외부의 설정파일등을 통해 정의되게 하는 디자인 패턴중의 하나.   의존성 ?  쉽게 말해 의지한다는 표현으로 받아 들일 수 있다.   B 클래스가 A 클래스를 의존하고 있고, A 클래스가 변하게 된다면 B 클래스 역시 변하게 되는 것을 말한다.   따라서 클래스간 영향을 많이 받을 경우 의존성이 높다고하며, 영향을 적게 받을 경우 의존성이 낮다고 한다.  주입 ?  내부가 아닌 외부 설정파일등을 통해 객체를 생성하고 무언가를 넣어주는 것을 말한다.   의존성이 높은 경우  public Class A {    public String str = \"DI test\";    public void printA () {     System.out.println(str);   } }  public Class B {   public void printB ()  {     A a = new A();     a.printA();   } }  B b = new B(); b.printB();  위의 코드에서 보듯이 클래스 B 는 클래스 A 에 의존성이 강한 형태이다.   무슨말인가하면 클래스 B 의 printB 함수는 클래스 A 의 printA 함수를 호출해준다.   따라서 printA 의 함수의 변화에 따라 printB 함수의 값이 달라지게 된다.   그러므로 클래스 B 가 클래스 A 에 의존해있다고 ë³¼ 수 있다.     정리하면 위처럼 클래스 B 내부에서 클래스 A 를 직접 생성하고 있고 클래스 A 가 아닌 다른    객체로 변경하고 할 때에 클래스 B 를 수정해야 하는 형태를 클래스 B 가 클래스 A 에 의존한하고 하며   강한 결합 이라고 한다.   의존성이 낮은 경우  위와 같은 관계에서는 상위 객체인 B 가 하위 객체인 A 에 의존하고 있다.   하지만 이럴 경우 의존도가 높아져 재사용성, 단위테스트 등의 의존성 주입의 장점이 무색하게 된다.   그래서 의존도를 낮춰야 하는데 ê·¸ 방법으로는 의존 관계 역전 원칙이 있다.     의존관계 역전 원칙 이란 상위 객체가 하위 객체에 의존하는 관계를 분리하여 각각을 모듈 형태로 나누는 것이다.   즉, 전통적인 상하위 관계의 의존성을 반전(역전) 시킴으로써 상위 객체가 하위 객체의 구현으로부터 독립되게 하는 것이다.     이 원칙은 다음과 같은 내용을 가지고 있다.      첫째. 상위 객체는 하위 객체에 의존해서는 안되고 모두 추상화에 의존해야 한다.    둘쨰. 추상화는 세부 사항에 의존해서는 안되고 세부사항이 추상화에 의존해야 한다.   -> 인터페이스를 활용한 의존성 주입 방법    인터페이스 (Setter) 주입 ?  의존성 주입은 객체 생성을 외부로부터 주입받는 것이다.    ê·¸ 중 의존성을 낮출 수 있는 방법중에 하ë
158‚˜ì¸ 인터페이스 주입은 다음과 같이 사용할 수 있다.   public interface TestInterface {   void print (String text); }  public class A implements TestInterface {    @Override   public void print (String text) {     System.out.println(\"A: \" + text);   }  }  public class B implements TestInterface {    @Override    public void print (String text) {     System.out.pritln(\"B: \" + text);   }  }  public class MainClass ()  {    private TestInterface testInterface;    public void setTestInterface (TestInterface testInterface)  {     this.testInterface = testInterface;   }    public void printText () {     TestInterface.print(\"Test\");   }  }  MainClass m = new MainClass(); TestInterface interA = new A(); m.setTestInterface(interA); m.printText();  이처럼 구현했지만 뭔가가 부족하다. 아직 MainClass 객체와 A 객체는 소스코드 내부에서 생성을 해주고 있다.   이를 또 다시 밖으로 분리하려면 xml 파일을 사용하여 bean 으로 객체를 생성해주는 방법 또는 Spring Annotation 을 사용하는 방법이 있다.    Bean 을 사용하여 주입하는 방법은 다음과 같다.   <?xml version=\"1.0\" encoding=\"UTF-8\"?> <beans xmlns=\"http://www.springframework.org/schema/beans\" \txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" \txsi:schemaLocation=\"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd\">    <!-- A a = new A(); -->   <bean id=\"A\" class=\"A.class\" />   <!-- B b = new B(); -->   <bean id=\"B\" class=\"B.class\" />   <!-- MainClass m = new MainClass(); -->   <bean id=\"Main\" class=\"Main.class\">     <!-- m.setTestInterface(interA) -->     <property name=\"interA\" ref=\"A\" />   </bean>  </beans>  이제 호출하는 쪽에서는 다음과 같이 사용한다.   public static void main (String args) {   ApplicationContext ctx = new ClassPathXmlApplicationContext(\"test.xml\");   MainClass m = (MainClass) ctx.getBean(\"Main\");   m.printText(); }  Interface 주입 방법 (Setter 를 이용한 주입 방법) 은 이처럼 객체를 분리하여 하위 객체의 메소드를 가져오는 상위 객체의 의존성을 낮춰준다.    하지만 문제점은 하위 객체를 오로지 Setter 라는 메소드로만 받으므로 상위 객체는 빈 객체로 생성이 될 수도 있다는 점이다. 이럴경우 상위 객체안의 하위 객체 메소드를 호출하는 메소드를 호출하게 되면 Null Pointer 오류가 발생한다.   이때 상위 객체의 메소드를 불러오면 아무값도 정의가 되지 않은채 호출이 되므로 NullPointerException 이 발생한다.   이를 해결할 방법이 필요한데 그것은 다음에 살펴볼 생성자를 통한 주입이다.   생성자 주입 ?  public interface TestInterface {   void print (String text); }  public class A implements TestInterface {    @Override   public void print (String text) {     System.out.println(\"A: \" + text);   }  }  public class B implements TestInterface {    @Override    public void print (String text) {     System.out.pritln(\"B: \" + text);   }  }  public class MainClass ()  {    private TestInterface testInterface;    public MainClass (TestInterface testInterface)  {     this.testInterface = testInterface;   }    public void printText () {     TestInterface.print(\"Test\");   }  }  MainClass m = new MainClass(new A()); m.printText();  생성자를 통한 주입은 위 코드 내용처럼 생성자의 파라미터로 하위 객체를 넘겨주지 않는 이상 Null 에러는 발생할 수 없다.   추가로 final 을 사용할 수 있다. final 은 객체의 생성과 동시에 초기화가 되므로 setter 함수를 통해서는 초기화가 될 수 없다.     스프링에서는 인터페이스를 통한 주입, 생성자를 통한 주입 외에도 필드 인젝션이라는 인터페이스를 통한 주입과 비슷한 동작으로 이루어지는 방법이 있다.   흔히 사용되는 @Autowired 로 불러오는 방식이다.   그러면 왜 인터페이스 주입 (수정자 (setter) 주입) 말고 생성자 주입을 추구해야되는건가 ?  스프링에서는 순환참조라는게 있다고 한다.   객체 A 의 메소드에서 객체 B 의 메소드를 호출하고 객체 B 의 메소드에서 객체 A 의 메소드를 호출하며 순환되는것인데,   인터페이스 주입 (수정자 주입) 의 경우 런타임 후에 비즈니스 로직상에서 주입이 이뤄지므로 stackoverflow 가 날때까지 계속 반복하게 된다.   반면 생성자 주입의 경우 런타임에서 객체 생성이 이뤄지기 때문에 이때 순환참조가 일어날 경우 곧바로 에러를 출력하며 실행이 되지 않는다.   이처럼 런타임이 순환참조임을 알리며 실행이 되지 않는 것과 아무것도 모르고 실행이 되어 오류를 뱉는 경우   어느 방법이 좀 더 정확한 에러를 파악하기에 쉬을것인가에 대해 생각해보면 생성자 주입을 추구하는게 맞는것 같다.   생성자 주입은 테스트 코드 작성에도 용이하다.  수정자 주입의 경우 스프링의 IOC 컨테이너 (제어의 역전 (의존성 역전)) 에서 객체를 생성해주는 방식이다.   그러므로 테스트 시 사용자가 필요한 객체를 생성하여 코드를 작성할 수 없다.   그래서 수정자 주입이 된 메소르를 단위 테스트를 해보면 NullPointerException 이 발생한다.     생성자 주입의 경우에는 객체를 생성해서 주입해주기 때문에 Null 에러가 나지 않고, 수월한 테스트가 가능하다.   정리     DI 는 의존성을 낮추는 편이 좋다.   DI 는 수정자 주입, 생성자 주입으로 나뉜다.   수정자 주입            Setter 함수를 활용한 방식       필드 인젝션 또한 같은 방식으로 동작           생성자 주입            생성자에 객체를 넘겨주는 방식           생성자 주입의 이점            의존관계 설정의 유무 확인 가능       final 선언 가능       순환참조 발견의 용이성       테스트 코드 작성이 용이           앞으로는 생
158성자 주입을 사용해보도록 하자.  ","categories": ["posts"],
159        "tags": ["spring boot"],
160        "url": "https://subji.github.io/posts/2020/02/17/whatistheDI",
161        "teaser":null},{
162        "title": "Java equals & hashCode",
163        "excerpt":"| 문제  스프링 시큐리티를 적용하다가 Entity 객체의 값을 비교하는 과정에서 equals 와 hashCode 메소드를 재정의 해야 한다는 것을 알았다. 그러면 두 메소드를 재정의하는 이유는 무엇일까..?   | equals  equals 는 객체를 비교한다. 쉬운 예로 Java 에서 문자열 비교가 있다.   String a = \"test\";  if (a.equals(\"test\")) {   System.out.println(\"Same!\"); } else {   System.out.println(\"Wrong!\"); }  위 코드처럼 문자열에 대해서는 쉽게 비교가 가능하다. 하지만 객체를 예로들면 어떠할까?   class A {     private String a;          public A (String a) {         this.a = a;     } }  class Main {   public static void main (String args[]) {     A a = new A(\"a\");     A aa = new A(\"a\");      System.out.println(a.equals(aa)); // false   } }  위 코드에서보면 A 라는 객체에 초기값으로 “a” 라는 문자열을 생성하고 있다. 그러면 두 객체는 같은것일까?   결론은 아니다 객체는 생성됨에따라 서로 다른 주소값을 가지고 있다.   그러므로 같은 값으로 초기화되어도 같을 수가 없다.     그러면 같은 값을 가지는 두 객체를 같다고 비교할순 없을까?   그 방법은 객체내에 equals 메소드를 Override 하는것이다.   class A {   private String a;      public A (String a) {       this.a = a;   }    @Override   public boolean equals(Object obj) {     if (this == obj)  {       return true;     }          if (obj == null)  {       return false;     }      if (getClass() != obj.getClass()) {       return false;     }      A other = (A) obj;      if (a == null) {       if (other.a != null)  {         return false;       }     } else if (!a.equals(other.a))  {       return false;     }      return true;   } }  class Main {   public static void main (String args[]) {     A a = new A(\"a\");     A aa = new A(\"a\");      System.out.println(a.equals(aa)); // true   } }  equals 메소드를 A 객체내에 Override 하여 구현하였다.   코드를 보면 객체를 파라미터로 받고 그 객체가 지금의 객체 그 자신이면 true 를 반환한다.   또한 파라미터 객체의 값이 null 이거나 getClass 의 함수값이 다르면 false 를 반환한다.   위 과정을 통과하고 나면 중요한 부분인 비교할 객체와 기존 객체의 비교를 진행한다.   코드에서는 a 라는 객체의 프로퍼티로 비교를 한다.   a 프로퍼티는 null 이거나 비교객체(파라미터 객체) 와 값이 다르면 false 를 반환한다.   위 조건들에 해당이 되지 않는다면 최종적으로 같은 객체라고 판단하여 true 를 반환한다.   위 내용을 보면 처음에 생성되는 객체는 주소값이 다르기때문에 엄연히 다른 객체라고 말했는데, 정확히 보면 저것도 다른 객체가 아닌가요? 라고 할 수 있다.   물론 맞는 말이야 엄연히 보면 다른 객체이다.   하지만 객체 자체를 하나의 값으로 보고 비교하고 이것을 토대로 다른 추가, 삭제, 수정등의 기능을 수행하기 위해서는   객체가 지닌 프로퍼티를 비교하여 같은 객체인지의 여부를 판단하는것도 하나의 방법인 것이다.   이와같은 방법은 주로 JPA 사용시 Entity 객체를 비교하고자 할때 사용된다.   | hashCode  hashCode 또한 위에서 설명한 equals 와 같은 맥락이다.   객체의 내용으로 두 객체가 같은지를 판단하는 메소드이다.   추가적으로 말하자면 equals 객체를 재정의할때는 반드시 hashCode 메소드로 재정의해주어야 한다.   그 이유는 다음과 같은 hashCode 규약으로 확인할 수 있다.          프로그램 실행 중 같은 객체의 hashCode 를 여러번 호출하는 경우, equals 가 사용하는 정보들이 변경되지 않는다면, 언제나 동일한 값이 반환되어야 한다. 하지만 프로그램이 종료되어 다시 실행 될 경
163우에는 그럴 필요가 없다.     equals 메소드가 같다고 판정되면 hashCode 메소드도 같아야 한다.     equals 메소드가 다르더라도 hashCode 값이 같을 수도 다를 수도 있지만 hashTable 의 성능 향상을 위해서는 다른 것이 낫다.      위 예제 코드에서 작성한 equals 메소드가 포함된 객체는 같은 정보를 가지고 있기때문에 같은 객체라고 판정되었다.   하지만 아직까지 엄연히 다른 객체이다.   이는 앞서 말한 hashCode 의 두번쨰 규약을 위반하는 내용이다.   class A {   private String a;      public A (String a) {       this.a = a;   }    @Override   public boolean equals(Object obj) {     if (this == obj)  {       return true;     }          if (obj == null)  {       return false;     }      if (getClass() != obj.getClass()) {       return false;     }      A other = (A) obj;      if (a == null) {       if (other.a != null)  {         return false;       }     } else if (!a.equals(other.a))  {       return false;     }      return true;   } }  class Main {   public static void main (String args[]) {     A a = new A(\"a\");     A aa = new A(\"a\");      System.out.println(a.hashCode());   // 918221580     System.out.println(aa.hashCode());  // 2055281021   } }   그러므로 더 명확하게 같은 객체임을 판정하기 위해서는 hashCode 메소드를 재정의 해주어야 한다.   만약 HashMap, HashSet, HashTable 등의 객체를 사용할 때 이 객체들에 위애서 equals 만 재정의한 객체를 추가한다면 서로 다른 hashCode 를 가지는 객체이므로 같은 객체가 중복되어 추가되게 된다.   그러므로 hashCode 값을 비교 해줄 수 있게 hashCode 메소드를 재정의 해주어야 한다.   class A {   private String a;      public A (String a) {       this.a = a;   }    @Override   public boolean equals(Object obj) {     if (this == obj)  {       return true;     }          if (obj == null)  {       return false;     }      if (getClass() != obj.getClass()) {       return false;     }      A other = (A) obj;      if (a == null) {       if (other.a != null)  {         return false;       }     } else if (!a.equals(other.a))  {       return false;     }      return true;   }      @Override   public int hashCode() {     final int prime = 31;     int result = 1;     result = prime * result + ((a == null) ? 0 : a.hashCode());     return result;   } }  class Main {   public static void main (String args[]) {     A a = new A(\"a\");     A aa = new A(\"a\");      System.out.println(a.hashCode());   // 128     System.out.println(aa.hashCode());  // 128   } }   결론적으로 VO, DTO, Entity 등의 객체를 비교하고자 할때에는 위처럼 equals, hashCode 메소드를 재정의 해주어야한다.   추가적으로 lombok 을 사용할 경우에는 @EqualsAndHashCode Annotation 을 사용하여 코드를 간단하게 할 수 있다.  ","categories": ["posts"],
164        "tags": ["java"],
165        "url": "https://subji.github.io/posts/2020/02/19/whyusehashcodeandequals",
166        "teaser":null},{
167        "title": "자주 찾아보는? 사용되는 자바스크립트 코드 모음 (Useful Javascript code)",
168        "excerpt":"| 자주 찾아보는 자바스크립트 코드 모음이다.   Javascript 버전   /**  * 숫자값이 정수인지 여부를 반환하는 함수  * @param { Number } value : 숫자 값  * @return { Boolean } : 정수(true), 정수 아닐 경우(false)  */ function isInt(value) {   let parse = parseFloat(value);   return !isNaN(value) && (parse | 0) === parse; } /**  * 정수부에 3자리 마다 ',' 를 체크해주는 함수  * @param { Integer } value : 정수 또는 실수 값  * @return { String } : ',' 가 적용된 실수 또는 정수 문자열 값  */ function addComma(value) {   if (!isInt(value)) {     let integer = value | 0;     let floater = (value - integer).toFixed(2).toString().substring(1);     return addComma(integer) + floater;   } else {     return value.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',');   } } /**   * 윤년 계산 메소드  * @param { Integer }
168 year : Date 객체의 년도  */ Date.isLeafYear = function (year) {   return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0)); }; /**  * 윤년 계산 인터페이스  */ Date.prototype.isLeafYear = function () {   return Date.isLeafYear(this.getFullYear()); }; /**  * 윤년을 포함한 해당 년도의 각 월수를 반환하는 메소드  * @param { Integer } year : Date 객체의 년도  * @param { Integer } month : Date 객체의 월 (0-11)  */ Date.getDaysInMonth = function (year, month) {   return [31, (Date.isLeafYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]; }; /**  * 윤년을 포함한 해당 년도의 각 월수를 반환하는 인터페이스  * @param { Integer } year : Date 객체의 년도  * @param { Integer } month : Date 객체의 월 (0-11)  */ Date.prototype.getDaysInMonth = function (year, month) {   return Date.getDaysInMonth(this.getFullYear(), this.getMonth()); }; /**  * Date 객체에서 N 개월만큼을 이동하는 메소드  * @param { Integer } n : 변경할 개월 수  */ Date.prototype.changeMonth = function (n) {   let day = this.getDate();   this.setDate(1);   this.setMonth(this.getMonth() + n);   this.setDate(Math.min(day, this.getDaysInMonth()));   return this; }; Date.prototype.changeWeek = function (n) {   let day = this.getDate();   this.setDate(day + (7 * n));   return this; }; /**  * YYYY(구분자) MM(구분자) DD 와 같은 형태로 날짜를 반환하는 메소드, 기본 구분자는 '-'  * @param { String } delimiter : 구분자  */ Date.prototype.formatDate = function (delimiter) {   let d = (this.getDate() > 9 ? '' : '0') + this.getDate();   let m = ((this.getMonth() + 1) > 9 ? '' : '0') + (this.getMonth() + 1);   delimiter = delimiter || '';   return this.getFullYear() + delimiter + m + delimiter + d; };  /**  * YYYY-MM-DD hh24:mi:ss 변환  */ Date.prototype.formatDateYMDHMS = function () {   let d = (this.getDate() > 9 ? '' : '0') + this.getDate();   let m = ((this.getMonth() + 1) > 9 ? '' : '0') + (this.getMonth() + 1);   let h = (this.getHours() > 9 ? '' : '0') + this.getHours();   let mi = (this.getMinutes() > 9 ? '' : '0') + this.getMinutes();   let s = (this.getSeconds() > 9 ? '' : '0') + this.getSeconds();   return this.getFullYear() + \"-\" + m + \"-\" + d + \" \" + h + \":\" + mi + \":\" + s; };   /**  * 날짜 변경 함수  * @param { Date } beforeDate : 기준 날짜  * @param { int } addNumber : 더하거나 뺄 값  * @param { String } type : 년(y) 월(m) 일(d)   */ function dateAddDel(beforeDate, addNumber, type) {   var yy = parseInt(beforeDate.substr(0, 4), 10);   var mm = parseInt(beforeDate.substr(5, 2), 10);   var dd = parseInt(beforeDate.substr(8), 10);    if (type == \"d\") { //일     d = new Date(yy, mm - 1, dd + addNumber);   } else if (type == \"m\") { //월     d = new Date(yy, mm - 1, dd + (addNumber * 31));   } else if (type == \"y\") { //년     d = new Date(yy + addNumber, mm - 1, dd);   }    yy = d.getFullYear(); // 19를 2019로 변경   mm = d.getMonth() + 1;   mm = (mm < 10) ? '0' + mm : mm; //월 변경  +1 하는 이유는 자바스크립트에서 0이 1월이라    dd = d.getDate();   dd = (dd < 10) ? '0' + dd : dd; //10일 이전이면 숫자 자릿수 맞추기 위함    return yy + '-' + mm + '-' + dd; }  /**  * 해당 달의 첫째날로 변환  */ Date.prototype.getFirstDay = function () {   return new Date(this.getFullYear(), this.getMonth(), 1); }; /**  * 해당 달의 마지막날 반환  */ Date.prototype.getLastDay = function () {   return new Date(this.getFullYear(), this.getMonth() + 1, 0); }; /**  * 날짜의 주차를 반환하는 메소드  * @param { String } dowOffset   */ Date.prototype.getWeek = function (dowOffset) {   /*getWeek() was developed by Nick Baicoianu at MeanFreePath: http://www.meanfreepath.com */   dowOffset = typeof (dowOffset) == 'int' ? dowOffset : 0; //default dowOffset to zero    let newYear = new Date(this.getFullYear(), 0, 1);   let day = newYear.getDay() - dowOffset; //the day of week the year begins on day = (day >= 0 ? day : day + 7);    let dayNum = Math.floor((this.getTime() - newYear.getTime() - (this.getTimezoneOffset() - newYear.getTimezoneOffset()) * 60000) / 86400000) + 1;   let weekNum; //if the year starts before the middle of a week    if (day < 4) {     weekNum = Math.floor((dayNum + day - 1) / 7) + 1;     if (weekNum > 52) {       let nYear = new Date(this.getFullYear() + 1, 0, 1);       let nDay = nYear.getDay() - dowOffset;       nDay = nDay >= 0 ? nDay : nDay + 7; /*if the next year starts before the middle of the week, it is 
168week #1 of that year*/       weekNum = nDay < 4 ? 1 : 53;     }   } else {     weekNum = Math.floor((dayNum + day - 1) / 7);   }   return weekNum; }; /**  * 날짜의 일단위 변경 함수  */ Date.prototype.changeDate = function (num) {   this.setDate(this.getDate() + num);   return this; }; /**  * 해당 날짜가 속한 주의 일요일 ~ 토요일 날짜를 문자열로 반환하는 함수  */ Date.prototype.getFirstAndLastOfWeek = function (precision) {   let day = this.getDay();   let sunday = 0 - day;   let saturday = 6 - day;    let first = new Date(this.getFullYear(), this.getMonth(), this.getDate() + sunday);   let last = new Date(this.getFullYear(), this.getMonth(), this.getDate() + saturday);    if (precision) {     first = this > first ? this : first;     last = new Date() < last ? new Date() : last;   }    return {     first: first,     last: last   }; }; /**  * YYYYMMDD 형태의 문자열 날짜를 Date 객체로 변환하는 함수  * @param { String } dateStr   */ function dateParser(dateStr) {   dateStr = dateStr.replace(/\\W/g, '');   let y = dateStr.substring(0, 4);   let m = dateStr.substring(4, 6);   let d = dateStr.substring(6, 8);   return new Date(y, m - 1, d); } /**  * 문자열의 가로, 세로 길이를 반환하는 함수  * @param { Integer } fontSize  */ String.prototype.getMeasureText = function (fontSize) {   let $temp = document.getElementById('measureTextWidth');   let type = typeof (fontSize);   if (type === 'undefined' || type === 'boolean' || type === 'object' || type === 'function' || type === 'symbol') {     throw Error('지원하지 않는 형식입니다.');   }   fontSize = '' + fontSize;   $temp.innerText = this;   $temp.style.fontSize = fontSize.indexOf('em') > -1 ? fontSize : (fontSize + 'em');      return {     width: ($temp.clientWidth + 1),     height: ($temp.clientHeight + 1)   }; } /**  * 공백 전체 제거  */ String.prototype.trimAll = function () {   return this.replace(/(\\s+)/g, ''); }; /**  * 종성 받침을 반환하는 함수 (으로 / 로 결정을 도와줌)  * false 는 '로', true 는 '으로'  * @param {*} word   */ function checkBatchimEnding(word) {   const t = [     '', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ',     'ㄷ', 'ㄹ', 'ㄺ', 'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ',     'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ', 'ㅆ',     'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'   ];    if (typeof word !== 'string') {     return null;   }    let lastLetter = word[word.length - 1];   let unicode = lastLetter.charCodeAt(0);    if (unicode < 44032 || unicode > 55203) {     return null;   }    let endChar = t[(unicode - 44032) % 28];   let addChar = (endChar !== '' && endChar !== 'ㄹ') ? '은' : '는';    return addChar; } /**  * 기간 별 날짜 문자열을 반환하는 함수  * @param {*} period   */ function getDateStringByPeriod(str, period, options) {   if (period === 0) {     return dateParser(str).formatDate('-');   } else if (period === 1) {     let date = dateParser(str);     let weeks = date.getFirstAndLastOfWeek(options.precision);     return weeks.first.formatDate('-') + ' ~ ' + weeks.last.formatDate('-');   } else if (period === 2) {     let result = null;     if (str.length < 7) {       result = str.substring(0, 4) + '-' + str.substring(4, 6);     } else {       let date = dateParser(str).formatDate('');       result = date.substring(0, 4) + '-' + date.substring(4, 6);     }      return result;   } } /**  * 우클릭 & 복사 방지 함수  */ function preventPointerEvents() {   $('.prevent-pointer-events')     .mousedown(function (e) {       if (e.button == 2) {         alert('내용을 복사 할 수 없습니다.');         return false;       } else {         return true;       }     })     .on('contextmenu', function () {       return false;     })     .on('dragstart', function () {       return false;     })     .on('selectstart', function () {       return false;     }); } /**  * Canvas DataURL 을 Blob 으로 변경하는 함수  * @param {*} dataUrl   */ function dataURLtoBlob(dataUrl) {   let arr = dataUrl.split(',');   let mime = arr[0].match(/:(.*?);/)[1];   let bstr = atob(arr[1]);   let n = bstr.length;   let u8arr = new Uint8Array(n);    while (n--) {     u8arr[n] = bstr.charCodeAt(n);   }    return new Blob([u8arr], {     type: mime   }); } /**  * target (input or textarea, ...) 의 값을 클립보드에 복사하는 함수  * @param {*} target   * @param {*}
168 callback   */ function setClipBoard(value, callback) {   try {     let text = document.createElement('textarea');     document.body.appendChild(text);     text.value = value;     text.select();     document.execCommand('copy');     document.body.removeChild(text);     return callback(true);   } catch (e) {     return callback(null);   } } /**  * 쿠키 설정 함수  * @param {*} key   * @param {*} value   * @param {*} expireDays   */ function setCookie (name, value, expireDays) {   let expire = new Date();   expire.setDate(expire.getDate() + expireDays);   let cookieString = escape(value);   cookieString += '; path=/';   cookieString += (expireDays === null ? '' : '; expires=' + expire.toUTCString());   document.cookie = name + '=' + cookieString; } /**  * 쿠키 값을 가져오는 함수  * @param {*} name   */ function getCookie (name) {   let result = null;    document.cookie.split(';').forEach(function (cookie)  {     let split = cookie.split('=');     let n = split[0].replace(/ /g, '');     let v = unescape(split[1]);      if (n === name) {       result = v;     }   });    return result; } /**  * 을/를 포맷 함수  * @param {*} text   */ function formatJosa (text)  {   let lastChar = text.charAt(text.length - 1);    let lmn = ['l', 'm', 'n'];   let afhiorsuvwxyz = ['a', 'f', 'h', 'i', 'o', 'r', 's', 'u', 'v', 'w', 'x', 'y', 'z'];   let bcdegkpt = ['b', 'c', 'd', 'e', 'g', 'k', 'p', 't'];   let bckpt = ['b', 'c', 'k', 'p', 't'];   let deg = ['d', 'e', 'g'];   let aeiou = ['a', 'e', 'i', 'o', 'u'];    let lastIndex = '';     if (lastChar < 0xAC00 || lastChar > 0xD7A3) {      if ((lastChar > 0x0041 && lastChar < 0x005A) || (lastChar > 0x0061 && lastChar < 0x007A)) {        if (lmn.indexOf(lastChar) > -1) {          lastIndex = \"을\";        } else if (afhiorsuvwxyz.indexOf(lastChar) > -1) {          lastIndex = \"를\";        } else if (bcdegkpt.indexOf(lastChar) > -1) {          if (text.length() < 2) {            lastIndex = \"를\";          } else {            let beforeLastChar = text.charAt(text.length - 2);             if (bckpt.indexOf(lastChar) > -1) {              if (lastChar == 'k' && beforeLastChar == 'c') {                lastIndex = \"을\";              } else if (lastChar == 'b' && beforeLastChar == 'm') {                lastIndex = \"을\";              } else if (aeiou.indexOf(beforeLastChar) > -1) {                lastIndex = \"을\";              } else {                lastIndex = \"를\";              }            } else if (deg.indexOf(lastChar) > -1) {              if (lastChar == 'e' && beforeLastChar == 'l') {                lastIndex = \"을\";              } else if (lastChar == 'e' && beforeLastChar == 'n') {                lastIndex = \"을\";              } else if (lastChar == 'e' && beforeLastChar == 'm') {                lastIndex = \"을\";              } else if (lastChar == 'g' && beforeLastChar == 'n') {                lastIndex = \"을\";              } else if (lastChar == 'd' && aeiou.indexOf(beforeLastChar) > -1) {                lastIndex = \"을\";              } else if (lastChar == 'g' && aeiou.indexOf(beforeLastChar) > -1) {                lastIndex = \"을\";              } else {                lastIndex = \"를\";              }            }          }        }      } else {        lastIndex = \"을/를\";      }    } else {      // 0 보다 크면 받침이 있으므로 \"을\"      // 0 보다 작거나 같으면 \"를\"      lastIndex = (lastChar - 0xAC00) % 28 > 0 ? \"을\" : \"를\";    }     return lastIndex; } /**  * 쿠키를 삭제하는 함수  * @param {*} name   */ function deleteCookie (name)  {   document.cookie = name + '= ; expires=' + new Date().toUTCString() + '; path=/'; }  function checkIe () {   let agent = navigator.userAgent.toLowerCase();    if ((navigator.appName == 'Netscape' && agent.indexOf('trident') != -1) || (agent.indexOf(\"msie\") != -1)) {     return true;   } else {     return false;   } }    Typescript 버전   /**  * 숫자값이 정수인지 여부를 반환하는 함수  * @param { Number } value : 숫자 값  * @return { Boolean } : 정수(true), 정수 아닐 경우(false)  */ function isInt (value:any): boolean {     const parse: number = parseFloat(value);     return !isNaN(value) && (parse | 0) === parse; } /**  * 정수부에 3자리 마다 ',' 를 체크해주는 함수  * @param { Integer } value : 정수 또는 실수 값  * @return { String } : ',' 가 적용된 실수 또는 정수 문자열 값  */ function addComma (value:any): string {   if (!isInt(value)) {     let integer:number = value | 0;     let floater:string = (value - integer).toFixed(2).toString().substring(1);     return addComma(integer) + floater;   } else {     return value.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',');   } } /**   * 윤년 계산 메소드  * @param { Integer }
168 year : Date 객체의 년도  */ Date.isLeafYear = function (year) {   return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0)); }; /**  * 윤년 계산 인터페이스  */ Date.prototype.isLeafYear = function () {   return Date.isLeafYear(this.getFullYear()); }; /**  * 윤년을 포함한 해당 년도의 각 월수를 반환하는 메소드  * @param { Integer } year : Date 객체의 년도  * @param { Integer } month : Date 객체의 월 (0-11)  */ Date.getDaysInMonth = function (year, month) {   return [31, (Date.isLeafYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]; }; /**  * 윤년을 포함한 해당 년도의 각 월수를 반환하는 인터페이스  * @param { Integer } year : Date 객체의 년도  * @param { Integer } month : Date 객체의 월 (0-11)  */ Date.prototype.getDaysInMonth = function (year, month) {   return Date.getDaysInMonth(this.getFullYear(), this.getMonth()); }; /**  * Date 객체에서 N 개월만큼을 이동하는 메소드  * @param { Integer } n : 변경할 개월 수  */ Date.prototype.changeMonth = function (n) {   let day = this.getDate();   this.setDate(1);   this.setMonth(this.getMonth() + n);   this.setDate(Math.min(day, this.getDaysInMonth()));   return this; }; /**  * Date 객체에서 N 주 만큼을 이동하는 메소드  * @param { Integer } n : 변경할 개월 수  */ Date.prototype.changeWeek = function (n) {   let day = this.getDate();   this.setDate(day + (7 * n));   return this; }; /**  * YYYY(구분자) MM(구분자) DD 와 같은 형태로 날짜를 반환하는 메소드, 기본 구분자는 '-'  * @param { String } delimiter : 구분자  */ Date.prototype.formatDate = function (delimiter) {   let d = (this.getDate() > 9 ? '' : '0') + this.getDate();   let m = ((this.getMonth() + 1) > 9 ? '' : '0') + (this.getMonth() + 1);   delimiter = delimiter || '';   return this.getFullYear() + delimiter + m + delimiter + d; };  /**  * YYYY-MM-DD hh24:mi:ss 변환  */ Date.prototype.formatDateYMDHMS = function () {   let d = (this.getDate() > 9 ? '' : '0') + this.getDate();   let m = ((this.getMonth() + 1) > 9 ? '' : '0') + (this.getMonth() + 1);   let h = (this.getHours() > 9 ? '' : '0') + this.getHours();   let mi = (this.getMinutes() > 9 ? '' : '0') + this.getMinutes();   let s = (this.getSeconds() > 9 ? '' : '0') + this.getSeconds();   return this.getFullYear() + \"-\" + m + \"-\" + d + \" \" + h + \":\" + mi + \":\" + s; };  /**  * 날짜 변경 함수  * @param { Date } beforeDate : 기준 날짜  * @param { int } addNumber : 더하거나 뺄 값  * @param { String } type : 년(y) 월(m) 일(d)   */ function dateAddDel(beforeDate, addNumber, type) {   var yy = parseInt(beforeDate.substr(0, 4), 10);   var mm = parseInt(beforeDate.substr(5, 2), 10);   var dd = parseInt(beforeDate.substr(8), 10);    if (type == \"d\") { //일     d = new Date(yy, mm - 1, dd + addNumber);   } else if (type == \"m\") { //월     d = new Date(yy, mm - 1, dd + (addNumber * 31));   } else if (type == \"y\") { //년     d = new Date(yy + addNumber, mm - 1, dd);   }    yy = d.getFullYear(); // 19를 2019로 변경   mm = d.getMonth() + 1;   mm = (mm < 10) ? '0' + mm : mm; //월 변경  +1 하는 이유는 자바스크립트에서 0이 1월이라    dd = d.getDate();   dd = (dd < 10) ? '0' + dd : dd; //10일 이전이면 숫자 자릿수 맞추기 위함    return yy + '-' + mm + '-' + dd; }  /**  * 해당 달의 첫째날로 변환  */ Date.prototype.getFirstDay = function () {   return new Date(this.getFullYear(), this.getMonth(), 1); }; /**  * 해당 달의 마지막날 반환  */ Date.prototype.getLastDay = function () {   return new Date(this.getFullYear(), this.getMonth() + 1, 0); }; /**  * 날짜의 주차를 반환하는 메소드  * @param { String } dowOffset   */ Date.prototype.getWeek = function (dowOffset) {   /*getWeek() was developed by Nick Baicoianu at MeanFreePath: http://www.meanfreepath.com */   dowOffset = typeof (dowOffset) == 'int' ? dowOffset : 0; //default dowOffset to zero    let newYear = new Date(this.getFullYear(), 0, 1);   let day = newYear.getDay() - dowOffset; //the day of week the year begins on day = (day >= 0 ? day : day + 7);    let dayNum = Math.floor((this.getTime() - newYear.getTime() - (this.getTimezoneOffset() - newYear.getTimezoneOffset()) * 60000) / 86400000) + 1;   let weekNum; //if the year starts before the middle of a week    if (day < 4) {     weekNum = Math.floor((dayNum + day - 1) / 7) + 1;     if (weekNum > 52) {       let nYear = new Date(this.getFullYear() + 1, 0, 1);       let nDay = nYear.getDay() - dowOffset;       nDay = nDay >= 0 ? nDay : nDay + 7; /*if the next year starts before the middle of the week, it is 
168week #1 of that year*/       weekNum = nDay < 4 ? 1 : 53;     }   } else {     weekNum = Math.floor((dayNum + day - 1) / 7);   }   return weekNum; }; /**  * 날짜의 일단위 변경 함수  */ Date.prototype.changeDate = function (num) {   this.setDate(this.getDate() + num);   return this; }; /**  * 해당 날짜가 속한 주의 일요일 ~ 토요일 날짜를 문자열로 반환하는 함수  */ Date.prototype.getFirstAndLastOfWeek = function (precision) {   let day = this.getDay();   let sunday = 0 - day;   let saturday = 6 - day;    let first = new Date(this.getFullYear(), this.getMonth(), this.getDate() + sunday);   let last = new Date(this.getFullYear(), this.getMonth(), this.getDate() + saturday);    if (precision) {     first = this > first ? this : first;     last = new Date() < last ? new Date() : last;   }    return {     first: first,     last: last   }; }; /**  * YYYYMMDD 형태의 문자열 날짜를 Date 객체로 변환하는 함수  * @param { String } dateStr   */ function dateParser(dateStr) {   dateStr = dateStr.replace(/\\W/g, '');   let y = dateStr.substring(0, 4);   let m = dateStr.substring(4, 6);   let d = dateStr.substring(6, 8);   return new Date(y, m - 1, d); } /**  * 문자열의 가로, 세로 길이를 반환하는 함수  * @param { Integer } fontSize  */ String.prototype.getMeasureText = function (fontSize) {   let $temp = document.getElementById('measureTextWidth');   let type = typeof (fontSize);   if (type === 'undefined' || type === 'boolean' || type === 'object' || type === 'function' || type === 'symbol') {     throw Error('지원하지 않는 형식입니다.');   }   fontSize = '' + fontSize;   $temp.innerText = this;   $temp.style.fontSize = fontSize.indexOf('em') > -1 ? fontSize : (fontSize + 'em');      return {     width: ($temp.clientWidth + 1),     height: ($temp.clientHeight + 1)   }; } /**  * 공백 전체 제거  */ String.prototype.trimAll = function () {   return this.replace(/(\\s+)/g, ''); }; /**  * 종성 받침을 반환하는 함수 (으로 / 로 결정을 도와줌)  * false 는 '로', true 는 '으로'  * @param {*} word   */ function checkBatchimEnding(word) {   const t = [     '', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ',     'ㄷ', 'ㄹ', 'ㄺ', 'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ',     'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ', 'ㅆ',     'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'   ];    if (typeof word !== 'string') {     return null;   }    let lastLetter = word[word.length - 1];   let unicode = lastLetter.charCodeAt(0);    if (unicode < 44032 || unicode > 55203) {     return null;   }    let endChar = t[(unicode - 44032) % 28];   let addChar = (endChar !== '' && endChar !== 'ㄹ') ? '은' : '는';    return addChar; } /**  * 기간 별 날짜 문자열을 반환하는 함수  * @param {*} period   */ function getDateStringByPeriod(str, period, options) {   if (period === 0) {     return dateParser(str).formatDate('-');   } else if (period === 1) {     let date = dateParser(str);     let weeks = date.getFirstAndLastOfWeek(options.precision);     return weeks.first.formatDate('-') + ' ~ ' + weeks.last.formatDate('-');   } else if (period === 2) {     let result = null;     if (str.length < 7) {       result = str.substring(0, 4) + '-' + str.substring(4, 6);     } else {       let date = dateParser(str).formatDate('');       result = date.substring(0, 4) + '-' + date.substring(4, 6);     }      return result;   } } /**  * 우클릭 & 복사 방지 함수  */ function preventPointerEvents() {   $('.prevent-pointer-events')     .mousedown(function (e) {       if (e.button == 2) {         alert('내용을 복사 할 수 없습니다.');         return false;       } else {         return true;       }     })     .on('contextmenu', function () {       return false;     })     .on('dragstart', function () {       return false;     })     .on('selectstart', function () {       return false;     }); } /**  * Canvas DataURL 을 Blob 으로 변경하는 함수  * @param {*} dataUrl   */ function dataURLtoBlob(dataUrl) {   let arr = dataUrl.split(',');   let mime = arr[0].match(/:(.*?);/)[1];   let bstr = atob(arr[1]);   let n = bstr.length;   let u8arr = new Uint8Array(n);    while (n--) {     u8arr[n] = bstr.charCodeAt(n);   }    return new Blob([u8arr], {     type: mime   }); } /**  * target (input or textarea, ...) 의 값을 클립보드에 복사하는 함수  * @param {*} target   * @param {*}
168 callback   */ function setClipBoard(value, callback) {   try {     let text = document.createElement('textarea');     document.body.appendChild(text);     text.value = value;     text.select();     document.execCommand('copy');     document.body.removeChild(text);     return callback(true);   } catch (e) {     return callback(null);   } } /**  * 쿠키 설정 함수  * @param {*} key   * @param {*} value   * @param {*} expireDays   */ function setCookie (name, value, expireDays) {   let expire = new Date();   expire.setDate(expire.getDate() + expireDays);   let cookieString = escape(value);   cookieString += '; path=/';   cookieString += (expireDays === null ? '' : '; expires=' + expire.toUTCString());   document.cookie = name + '=' + cookieString; } /**  * 쿠키 값을 가져오는 함수  * @param {*} name   */ function getCookie (name) {   let result = null;    document.cookie.split(';').forEach(function (cookie)  {     let split = cookie.split('=');     let n = split[0].replace(/ /g, '');     let v = unescape(split[1]);      if (n === name) {       result = v;     }   });    return result; } /**  * 을/를 포맷 함수  * @param {*} text   */ function formatJosa (text)  {   let lastChar = text.charAt(text.length - 1);    let lmn = ['l', 'm', 'n'];   let afhiorsuvwxyz = ['a', 'f', 'h', 'i', 'o', 'r', 's', 'u', 'v', 'w', 'x', 'y', 'z'];   let bcdegkpt = ['b', 'c', 'd', 'e', 'g', 'k', 'p', 't'];   let bckpt = ['b', 'c', 'k', 'p', 't'];   let deg = ['d', 'e', 'g'];   let aeiou = ['a', 'e', 'i', 'o', 'u'];    let lastIndex = '';     if (lastChar < 0xAC00 || lastChar > 0xD7A3) {      if ((lastChar > 0x0041 && lastChar < 0x005A) || (lastChar > 0x0061 && lastChar < 0x007A)) {        if (lmn.indexOf(lastChar) > -1) {          lastIndex = \"을\";        } else if (afhiorsuvwxyz.indexOf(lastChar) > -1) {          lastIndex = \"를\";        } else if (bcdegkpt.indexOf(lastChar) > -1) {          if (text.length() < 2) {            lastIndex = \"를\";          } else {            let beforeLastChar = text.charAt(text.length - 2);             if (bckpt.indexOf(lastChar) > -1) {              if (lastChar == 'k' && beforeLastChar == 'c') {                lastIndex = \"을\";              } else if (lastChar == 'b' && beforeLastChar == 'm') {                lastIndex = \"을\";              } else if (aeiou.indexOf(beforeLastChar) > -1) {                lastIndex = \"을\";              } else {                lastIndex = \"를\";              }            } else if (deg.indexOf(lastChar) > -1) {              if (lastChar == 'e' && beforeLastChar == 'l') {                lastIndex = \"을\";              } else if (lastChar == 'e' && beforeLastChar == 'n') {                lastIndex = \"을\";              } else if (lastChar == 'e' && beforeLastChar == 'm') {                lastIndex = \"을\";              } else if (lastChar == 'g' && beforeLastChar == 'n') {                lastIndex = \"을\";              } else if (lastChar == 'd' && aeiou.indexOf(beforeLastChar) > -1) {                lastIndex = \"을\";              } else if (lastChar == 'g' && aeiou.indexOf(beforeLastChar) > -1) {                lastIndex = \"을\";              } else {                lastIndex = \"를\";              }            }          }        }      } else {        lastIndex = \"을/를\";      }    } else {      // 0 보다 크면 받침이 있으므로 \"을\"      // 0 보다 작거나 같으면 \"를\"      lastIndex = (lastChar - 0xAC00) % 28 > 0 ? \"을\" : \"를\";    }     return lastIndex; } /**  * 쿠키를 삭제하는 함수  * @param {*} name   */ function deleteCookie (name)  {   document.cookie = name + '= ; expires=' + new Date().toUTCString() + '; path=/'; }  function checkIe () {   let agent = navigator.userAgent.toLowerCase();    if ((navigator.appName == 'Netscape' && agent.indexOf('trident') != -1) || (agent.indexOf(\"msie\") != -1)) {     return true;   } else {     return false;   } }  ","categories": ["posts"],
169        "tags": ["javascript"],
170        "url": "https://subji.github.io/posts/2020/02/20/usefulljavascriptcodes",
171        "teaser":null},{
172        "title": "Spring Boot (Maven) with React Study day 3",
173        "excerpt":"  목적 ===     Spring boot 와 React 를 활용하여 간단한 웹 페이지를 만들어보고자 한다.   이전 내용  이전에는 스프링부트의 설치하는 방법과 실행방법에 대해서 살펴보았다.   Spring Boot (Maven) with React Study day 2     이번에는 create-react-app ê³¼ 설치된 스프링 부트 앱의 빌드 및 배포 연동을 해보도록 하겠다.   1. 연동 방법   스프링부트의 static 디렉토리 안에 .html 파일이 있다.   해당 파일ì
173„ 리액트의 파일로 대체해주는 방법이 있다.   즉, SPA 인 리액트를 빌드해서 만들어진 js, css, html 파일들을 스프링부트의 정적 리소스 디렉토리에 복사해주는 것이다.   그러면 스프링부트에서는 classpath 를 훑으며 자연스레 리액트 파일을 실행할 것이다.   2. 구글링   내가 찾은 방법으로는 대략 2가지 정도가 있었다.   첫번째는 webpack 으로 만들어진 리액트 프로젝트를 스프링부트 프로젝트 내 별도의 디렉토리에 두고    Maven 또는 Gradle 에서 스프링부트의 빌드와 동시에 리액트 앱도 빌드해서 완료된 파일들을 복사해주는 방법이 있다.   두번쨰는 Maven 의 경우인데, frontend-maven-plugin, maven-antrun-plugin 을 활용하는 방법이다.   여기서는 Maven 으로 프로젝트를 생성했으므로 두번째 방법을 사용해보도록 하자.   다음 프로젝트에서는 ê¼­ Gradle 도 사용해보자.   3. 스프링부트 & 리액트 빌드   frontend-maven-plugin Version : Recently Version 링크를 통해 해당 메이븐 플러그인의 최신버전을 사용하거나 원하는 버전을 사용하자.  <plugin>     <groupId>com.github.eirslett</groupId>     <artifactId>frontend-maven-plugin</artifactId>     <version>1.7.6</version> <!-- frontend-maven-plugin Version -->     <configuration>         <workingDirectory>frontend</workingDirectory> <!-- React JS 가 설치된 디렉토리 -->         <installDirectory>target</installDirectory> <!-- war & jar 가 생성되는 메이븐 빌드의 타겟 디렉토리 -->     </configuration>     <executions>         <execution>             <id>install node and npm</id>             <goals>                 <goal>install-node-and-npm</goal> <!-- node & npm 설치 -->             </goals>             <configuration>                 <nodeVersion>v12.16.1</nodeVersion> <!-- 설치할 nodeJs 의 버전 -->                 <npmVersion>6.14.0</npmVersion>  <!-- 설치할 npm 의 버전 -->             </configuration>         </execution>         <execution>             <id>npm install</id>    <!-- npm install 로 package.json 의 모듈을 설치한다. -->             <goals>                 <goal>npm</goal>             </goals>             <configuration>                 <arguments>install</arguments>             </configuration>         </execution>         <execution>             <id>npm run build</id>  <!-- React Js Build -->             <goals>                 <goal>npm</goal>             </goals>             <configuration>                 <arguments>run build</arguments>             </configuration>         </execution>     </executions> </plugin>  위에 코드를 작성하고 터미널 또는 cmd 를 열고 “mvn clean install” 을 해보자.   아래 그림과 같은 진행사항이 표시된다.   그리고 나서 리액트가 설치된 디렉토리를 ë³´ë©´ “build” 라는 디렉토리가 생기고 그안에 js, css, html 이 생성되었을 것이다.               이렇게 되면 이제 마지막 단계만 남았다.   아래 코드를 pom.xml 에 추가해주자.   <plugin>   <artifactId>maven-antrun-plugin</artifactId>   <executions>     <execution>       <phase>generate-resources</phase> <!-- 리소스 생성 -->       <configuration>         <target>           <copy todir=\"${project.build.directory}/classes/public\">  <!-- 복사할 디렉토리 설정 -->             <fileset dir=\"${project.basedir}/frontend/build\"/>  <!-- 가져올 디렉토리 및 파일 -->           </copy>         </target>       </configuration>       <goals>         <goal>run</goal>  <!-- 복사 실행 -->       </goals>     </execution>   </executions> </plugin>  위에 코드를 작성하고 다시 “mvn clean install” 을 실행해보자.   그러면 다음과 같이 target 디렉토리 아래에 생성될 것이다.         이제 모든게 다 되었다.   target 아래에 생성된 .war 파일ì
173„ “java -jar *.war” 로 실행하자.   java -jar *.war   이제 실행된 주소로 접속해보면 다음 화면이 나온다!         정리      Spring Boot 와 React 연동 방법   React 를 빌드해서 Spring Boot Target 디렉토리에 복사   마무리  간단하게 리액트와 스프링부트를 연동해보았다.   이를 사용해서 다음에는 리액트를 개발하고 스프링부트는 API 로 활용하여 빠른개발에 도움이 될거 같다.  ","categories": ["posts"],
174        "tags": ["spring boot","maven","react"],
175        "url": "https://subji.github.io/posts/2020/02/26/react-with-spring-3",
176        "teaser":null},{
177        "title": "JSP 렌더링 오류 (java.lang.NoSuchMethodError: org.springframework.web.servlet.tags.form.InputTag.setReadonly(Ljava/lang/String;)V)",
178        "excerpt":"| 문제  프로젝트를 업데이트하고 배포하였는데, 특정 페이지만 안들어가진다..   더군다나 해당 페이지는 건드린적도 없었다.   레거시 프로젝트라 하나하나 다 살펴봐야 하는 상황..   다음과 같은 에러 발생         | 체크리스트     컨트롤러에서 뷰로 매핑이 잘못되었나 ? - NO   뷰리졸버를 설정하는 과정에서 오류가 있나 ? - NO   뷰 파일의 위치가 잘못되었나 ? - NO   후.. 도대체 뭘까..   하고 포기하면서 로그나 읽어보자 했는데..!?   로그 마지막 4문장 정도를 보면 configuration.jsp 라는 파일에서 input 태그의 readonly 속성이 잘못됬다고 나와있었다!   다시 구글링해서 다음과 같이 명쾌한 해답을 얻었다.   | 결론            태그의 readonly 속성이 잘못되었던 것       readonly 는 readonly=”true” 로 설정할 것   그외에 속성을 설정하면 에러 발생!   코드를 보니까  로 되어있었다..   후.. 레거시 코드 …   그래도 고쳐서 다행이다.  ","categories": ["posts"],
179        "tags": ["jsp"],
180        "url": "https://subji.github.io/posts/2020/02/27/fomtinputreadonly",
181        "teaser":null},{
182        "title": "Spring Boot 가 아닌 프로젝트에서 Actuator 사용하기 (How to use Actuator without Boot",
183        "excerpt":"| 문제  레거시 프로젝트들의 모니터링 시스템을 만들던중 Spring Boot 에서는 간단한 설정만으로 해당 어플리케이션의 정보를 가져오는 Actuator 를 적용시키고자 하였다.   하지만 Actuator 는 Spring Boot 프로젝트의 일환이였던것..   그럼 기존의 레거시 시스템에는 어떻게 적용을 해야하는건가..   | 과정  구글링에서는 없는게 없다.   라는 확신을 갖고 열심히 찾아보았다.   먼저 pom.xml 에 actuator Dependency 를 추가하고..  xml 설정으로는 너무 어려우니까 관리가 편한 자바 설정으로 해봐야겠네..   열심히 찾고 테스트하고 확인한 결과 다음과 같이 사용하면 된다는걸 알았다.      POM.xml 에 의존성을 추가   Java Configuration 을 사용해서 원하는 EndPoint 를 설정해주는 객체를 생성   Security 외에 다른 보안 처리를 작업중이었다면 /health, /metrics 등의 접근을 허용   | 결론  POM.xml   <!-- actuator --> <dependency>   <groupId>org.springframework.boot</groupId>   <artifactId>spring-boot-actuator</artifactId>   <version>1.2.1.RELEASE</version> </dependency>  Java   @Configuration @Import({ \tEndpointAutoConfiguration.class,  // Endpoint 자동 설정 객체 \tPublicMetricsAutoConfiguration.class, // Metrics 객체 \tHealthIndicatorAutoConfiguration.class  // Health 객체 }) public class ActuatorConfig { \t   @Bean   @Autowired   public EndpointHandlerMapping endpointHandlerMapping(Collection<? extends MvcEndpoint> endpoints) {       return new EndpointHandlerMapping(endpoints);   }    /**     * Metrics 접근 객체 Bean 등록     */   @Bean   @Autowired   public EndpointMvcAdapter metricsEndPoint(MetricsEndpoint delegate) {       return new EndpointMvcAdapter(delegate);   }   /**     * Health 접근 객체 Bean 등록     */   @Bean   @Autowired   public EndpointMvcAdapter healthEndPoint (HealthEndpoint delegate)\t{     return new EndpointMvcAdapter(delegate);   } }  ","categories": ["posts"],
184        "tags": ["spring"],
185        "url": "https://subji.github.io/posts/2020/02/27/howtouseactuatorwithoutboot",
186        "teaser":null},{
187        "title": "React WebFonts 적용하기 (Applied WebFonts on React)",
188        "excerpt":"| 문제  기존 프로젝트에서 Web Fonts 를 사용하고 있었다.   리액트로 전환하는 도중 초기에 셋업과정에서 웹폰트 적용에 있어 ì˜
188¤ë¥˜ê°€ 발생하였다.   기존 프로젝트에서는 웹폰트를 불러오는데 별다른 제약이 없었지만 리액트에서는 따로 처리를 해줘야하는것 같았다.   그래서 구글링을 통해 찾아보았는데 몇가지 방법이 있었다.   | 어떻게?  첫번쨰 는 index.html 에 css 파일을 링크해주고 뒤에 웹폰트를 명시해주는 방법이다.   <link rel=\"stylesheet\" href=\"../fonts/fonts.css?family=Titillium+Web:300,400,700\" media=\"all\">   코드에서 보듯이 index.html 에 임포트하는 css 파일 뒤에 폰트패밀리로 웹폰트를 파라미터로 해주었다. 뒤에 숫자는 폰트의 사이즈를 의미한다.   위에 방법은 확실히 간단한 방법이다. 하지만 다음과 같은 이슈가 있다고 한다.   해당 라인에 대해서 퍼포먼스에 관련된 이슈이다.   Render Blocking CSS   그래서 두번째 로 다음과 같은 방법을 사용한다.   이 방법은 자체 로컬에 설치된 자체 웹폰트나 외부 웹폰트를 모두 사용가능하다.   Web Font Loader   먼저 라이브러리를 설치해주자. 설치할때는 항상 package.json 에 저장되게 –save 옵션을 넣어주자.   npm install --save webfontloader   다음과같이 index.js 등 원하는 스크립트 파일에서 작성한다.   import WebFont from 'webfontloader';  WebFont.load({   google: {     families: ['Titillium Web:300,400,700', 'sans-serif']   } });   웹폰트로더를 임포트하여 웹폰트를 불러오는 로직이다.   예시는 구글의 웹폰트를 불러오며 라이브러리의 깃허브 페이지를 ë³´ë©´ 알 수 있듯이 여러가지 웹폰트도 불러올수 있다.   WebFont.load({   google: {     families: ['Titillium Web:300,400,700', 'sans-serif']   },   custom: {     families: ['curstom-bold'],     urls: ['../fonts/fonts.css']   } });   사용자 정의 웹폰트는 예시처럼 설정할 수 있다.   Before        After       ","categories": ["posts"],
189        "tags": ["react"],
190        "url": "https://subji.github.io/posts/2020/03/02/howtoappliedwebfontsinreact",
191        "teaser":null},{
192        "title": "React Image Import 하는 방법 (How to import image on React JS)",
193        "excerpt":"리액트로 프로젝트를 만들면서 이미지를 어떻게 불러와야 하느냐에 대한 고민이 있었다.   처음에는 js, css 처럼 임포트문을 사용해서 넣어도 봤지만 이렇게하다가는 이미지가 한두개도 아니라서 코드가 복잡하고   불편해질거 같았다.   그래서 찾아보니 public 디렉토리를 자동으로 리액트 앱에서 검색하는거 같았다.   결과적으로 public 디렉토리 아래에 images 디렉토리를 만들고 해당위치를 상대경로로하여 불러올 수 있었다.   <img src=\"/images/logo.png\" />   위의 코드는 public 디렉토리 아래에 images 디렉토리가 있을 경우에 작동한다.  ","categories": ["posts"],
194        "tags": ["react"],
195        "url": "https://subji.github.io/posts/2020/03/03/howtoimportimageonreact",
196        "teaser":null},{
197        "title": "React props Default value 적용 방법 (How to set default value of props React JS)",
198        "excerpt":"props 에 대하여 기본값을 지정해야 할 필요가 있을때가 있다.   그럴떈 다음처럼 사용할 수 있다.      사용방법   Component.defaultProps = {   propsOne: 'Test',   ... };  export default Component;  ","categories": ["posts"],
199        "tags": ["react"],
200        "url": "https://subji.github.io/posts/2020/03/03/howtovalidatepropertiesdefault",
201        "teaser":null},{
202        "title": "React props type Validation 방법 (How to validate props type React JS)",
203        "excerpt":"props 를 받으면서 타입 체크를 할 필요가 있을때가 있다.   그럴때는 간단하게 다음과 같이 사용할 수 있다.      props-type import   import PropTypes from 'prop-types';      가능한 유효 타입 및 작성 방법   Component.propTypes = {   anyProp: PropTypes.any,           // 타입 상관 X   boolProp: PropTypes.bool,         // Boolean 타입   numberProp: PropTypes.number,     // Number 타입   stringProp: PropTypes.string,     // String 타입   funcProp: PropTypes.func,         // Function 타입   arrayProp: PropTypes.array,       // Array 타입   objectProp: PropTypes.object,     // Object 타입   symbolProp: PropTypes.symbol      // Symbol 타입 };  export default Component;  ","categories": ["posts"],
204        "tags": ["react"],
205        "url": "https://subji.github.io/posts/2020/03/03/howtovalidatepropertiestype",
206        "teaser":null},{
207        "title": "unclosed regular expression 에러 처리 방법",
208        "excerpt":"리액트앱을 만들다가 앱에 영향을 미치는 ì˜
208¤ë¥˜ëŠ” 아니지만   jshint 가 읽는과정에서 에러로 처리하는 경우가 있다.   보통 jsx 를 사용할때 생기는 ê±° 같은데 에디터나 IDE 에서 jshint 의 전역 설정을 할 수 있지만   간단하게 주석처리만으로도 해결이 가능하다.   jshint 의 예외를 추가해주는 방법인데 원하는 코드영역 위, 아래에 아래와 같은 주석을 추가해준다.     return (     /* jshint ignore:start */     <div>     </div>     /* jshint ignore:end */   );  ","categories": ["posts"],
209        "tags": ["react"],
210        "url": "https://subji.github.io/posts/2020/03/09/unclosedregularexpressionerror-react",
211        "teaser":null},{
212        "title": "WSL 에서 lxd package broken 으로 인해 apt upgrade 안될 때",
213        "excerpt":"윈도우 서브 시스템 리눅스 (WSL) 은 snap 이 연동이 안된다.   어느날 apt update && apt upgrade 를 실행하는데 아래와 같은 에러가 발생하였다.     Reading package lists... Done   Building dependency tree          Reading state information... Done   You might want to run 'apt --fix-broken install' to correct these.   The following packages have unmet dependencies:   lxd : Depends: lxd-client (= 3.0.3-0ubuntu1~18.04.1) but 1:0.7 is installed   lxd-client : Depends: lxd (>= 1:0.7) but 3.0.3-0ubuntu1~18.04.1 is installed   E: Unmet dependencies. Try 'apt --fix-broken install' with no packages (or specify a solution).   내용을 보면 lxd, lxd-client 는 각각 3.0.3, 1:0.7 버전 이상을 지원하는데 현재 설치된 버전은   그 이하여서 안된다는것이다.   그래서 로그에 출력된대로 fix 파라미터를 실행해보았지만     Interacting with snapd is not yet supported on Windows Subsystem for Linux.   This command has been left available for documentation purposes only.   위와 같은 메세지를 출력했다.   내용은 WSL 에서는 snap 을 지원하지 않는다는 얘기같다.   그래서 어떻게 강제로 lxd, lxd-client 를 설치하는 방법이 없을까하고 찾아보다가   WSL 공식 깃허브에 누군가가 해결방법을 작성해놓았다.     sudo dpkg --force depends -P lxd   sudo dpkg --force depends -P lxd-client   dpkg 는 패키지와 관련된 명령어로 설치, 삭제 등을 파라미터로 두고 사용할 수 있다.   –force 옵션은 강제 옵션이다.   -P 옵션은 lxd, lxd-client 의 패키지와 해당 패키지의 설정파일을 삭제한다는 뜻이다. (Purge)   depends-version, depends 옵션은 –force 와 주로 같이쓴다고하는데 첫번째의 경우 버전 의존성 문제를   경고로 넘어가고 두번째 옵션은 모든 의존성 문제를 경고로 넘어간다는 뜻이다.   위 명령어들을 실행하니 apt update && apt upgrade 가 잘된다!  ","categories": ["posts"],
214        "tags": ["wsl"],
215        "url": "https://subji.github.io/posts/2020/03/12/wslbrokenlxdwithsnap",
216        "teaser":null},{
217        "title": "외부 Jar 추가 방법",
218        "excerpt":"스프링 부트를 패키징하고 배포하는 과정에서 외부 ojdbc 를 사용해야 하는데   배포후에 적용이 안되서 실행이 안됬다.   로컬에서는 mvn install:install-file 을 사용해서 로컬 PC 의 리포지토리에서   사용했는데 배포한 서버의 로컬 리포지토리에는 ojdbc 가 없었다..   또한 패키징 과정에서 ojdbc 파일도 누락되었다.   두가지 방법이 있는데, 하나는 서버의 로컬 리포지토리에 파일을 설치하는 방법   다른 하나는 패키징할때 프로젝트 디렉토리안에 자체 리포지토리를 만드는 것이다.   프로젝트 리포지토리를 만드는 방법은 간단하다.   다음과 같이 lib 아래에 외부 jar 파일을 옮겨놓는다.         그리고 pom.xml 에 만들어놓은 디렉토리를 리포지토리로 지정한다.     <repositories>     <repository>         <id>local-repository</id>         <name>local repository</name>         <url>file://${project.basedir}/lib</url>     </repository>   </repositories>   마지막으로 외부 ojdbc 에서 선언한 아래와 같은 내용을 주석처리하고 다시 패키징한다.     <scope>system</scope> \t<systemPath>${basedir}/src/lib/ojdbc7.jar</systemPath>  ","categories": ["posts"],
219        "tags": ["maven"],
220        "url": "https://subji.github.io/posts/2020/03/16/externalojdbcpackaging",
221        "teaser":null},{
222        "title": "Type safety: Unchecked cast from Object to List<>",
223        "excerpt":"List list = (List) otherList; 에서   Type safety: Unchecked cast from Object to List&lt;&gt;   경고가 발생하였다.   변수의 유형을 확실하게 하
223지 못해 발생하는 경고이다.   Java 는 일반적으로 컴파일을 할때 변수의 유형을 알고있어야 한다.   그러므로 불확실한 유형의 변환은 경고를 발생시킨다.   이를 해결하는 방법은 임시로 해결하는 방법과 조금은 더 근본적으로 해결하는 방법이 있다.   임시적으로 해결하는 방법은 다음과 같다.   @SuppressWarnings() // 컴파일러가 일반적으로 경고하는 내용을 제외할 때 사용   하지만 이는 어디까지나 임시방편일뿐 이를 해결하기 위해서 명확한 유형을 정해주어야한다.   List&lt;?&gt; result = (List&lt;?&gt;) results.values; for (Object object : result) {     if (object instanceof MyObject) {         tempList.add((MyObject) object);     } }  ","categories": ["posts"],
224        "tags": ["java"],
225        "url": "https://subji.github.io/posts/2020/03/17/TypesafetyUncheckedcastfromObjecttoList",
226        "teaser":null},{
227        "title": "Java Capitalize",
228        "excerpt":"자바에서 “command” =&gt; “Command” 로 변경하는 방법   public String capitalize (String text)  {   if (text == null) {     return text;   }    return text.substring(0, 1).toUpperCase() + text.substring(1); }  ","categories": ["posts"],
229        "tags": ["java"],
230        "url": "https://subji.github.io/posts/2020/03/17/javacapitalize",
231        "teaser":null},{
232        "title": "Git Push 취소하는 방법",
233        "excerpt":"잘못 Push 한 깃을 취소하려한다.   하지만 그 전에 꼭 같은 팀원과 상의해야하며 자기 마음대로 해버리다간 일이 커진다..   방법은 간단하다.   말을 취소지만 현재 Branch 의 HEAD 값을 이동시키는 것이다.   또 하나 주의할 점은 기존에 만들어놓은 코드가 HEAD 값의 이동으로 변경된다는것이다.   작업중이던 파일이 있으면 반드시 백업을 만들어놓고 Push 취소를 하자.   $ ~ git reset --hard HEAD^ or git reset --hard [commit id]  $ ~ git push --force  ","categories": ["posts"],
234        "tags": ["git"],
235        "url": "https://subji.github.io/posts/2020/03/20/git-reset-pssh",
236        "teaser":null},{
237        "title": "D3 여러 버전 사용하는 방법 (How to use multiple version d3.js)",
238        "excerpt":"D3 를 사용하는데 레거시 프로젝트에서 v3 과 v4 를 같이 사용하고 있었다.   기존 레거시 프로젝트는 다른 jsp 에서 각각 다른 버전을 사용하고 있어서 오류가 없었다.   하지만 같은 페이지에서 v3 과 v4 를 써야하는 상황이오니 v3 -&gt; v4 에서 버전업 되면서 발생하는 호환성 이슈가 폭발했다.   그래서 2개를 각각 사용하는 방법을 생각해봤다.   전역으로 불러오거나 지역으로 불러오거나 상관없이 다음과 같이 해주자.   &lt;script type=\"text/javascript\" src=\"d3.v3.js\"&gt;&lt;/script&gt; &lt;script&gt;   d3v3 = d3;   window.d3 = null; &lt;/script&gt; &lt;script type=\"text/javascript\" src=\"d3.v4.js\"&gt;&lt;/script&gt; &lt;script&gt;   d3v4 = d3;   window.d3 = null; &lt;/script&gt;  ","categories": ["posts"],
239        "tags": ["javascript","d3js"],
240        "url": "https://subji.github.io/posts/2020/03/23/d3usemultipleversion",
241        "teaser":null},{
242        "title": "Git add -P 와 commit -v 사용",
243        "excerpt":"일반적으로 git add . 또는 git add –all 을 사용해서 한번에 변경 내용을 스테이징영역으로 올린다.   하지만 어떤 변경사항은 스테이징에 반영되면 안되거나 혹은 반영시키고 싶지 않은 경우가 있다.   그럴때 위에 언급한 명령어는 위험하다.   그래서 자기가 원하는 변경내용만 반영하는 방법이 있다.   아래 명령어를 입력하자   $ ~&gt;  git add -p        위 그림과 같이 추가/수정/삭제가 된 부분이 하이라이팅되어 표시된다.   그리고 변경내용을 반영할지 안할지를 묻는 곳에서 원하는대로 선택할 수 있다.   주요 선택 옵션의 내용은 다음과 같다.   다음 내용에서 hunk 는 변경내용의 단위를 가리킨다.      y: 현재 hunk 를 스테이징 영역으로 이동   n: 현재 hunk 를 스테이징 영역으로 보내지 않는다.   q: add 과정을 종료한다.   이외에도 여러 옵션이 있다. 자세한 내용은 ? 를 입력하여 확인해보자.   그리고 다음은 commit -v 에 대한 내용인데   위에서 스테이징으로 추가된 내용을 다시한번 확인 할 수 있다.   $ ~&gt; git commit -v   를 입력하면 기본 에디터 화면이 열리면서 변경된 내용이 출력된다.   git diff –staged 라는 비스무리한 명령어도 있는데 이것도 확인해봐야 겠다.  ","categories": ["posts"],
244        "tags": ["git"],
245        "url": "https://subji.github.io/posts/2020/03/27/gitaddpandcommitv",
246        "teaser":null},{
247        "title": "nohup 명령어 .log 파일없이 실행",
248        "excerpt":"일반적으로 명령어 끝에 “&amp;” 를 붙여도 백그라운드 실행이 된다.   그렇지만 ssh 등 세션을 끊더라도 계속 백그라운드에서 유지하려면 nohup 을 사용하게 된다.   nohup 명령어를 실행하면 out 파일이 생기는데 이 파일ì
248„ 안생기게 하는 방법과 백그라운드 명령어 각각의 의미를 알아보자.      명령어 끝에 &amp; : 명령어를 백그라운드 작업으로 실행   2&gt;&amp;1 : stderr (표준에러) 를 stdout (표준출력) 으로 이동합니다. (&amp;1 부분을 특정 파일 경로로 바꾸어 로그를 출력할 수 있다.)   /dev/null : stdout (표준출력) 을 /dev/null 로 이동, 지운다는 의미로 보통 알고있음   nohup : stty 가 끊긴 후에도 프로세스를 종료시키지 않음   # nohup.out 파일 생성 없이 백그라운드 작업 실행 $ nohup example.sh 1&gt; /dev/null 2&gt;&amp;1 &amp;  ","categories": ["posts"],
249        "tags": ["linux"],
250        "url": "https://subji.github.io/posts/2020/04/12/nohupwithoutlog",
251        "teaser":null},{
252        "title": "Comparison method violates its general contract! 에러",
253        "excerpt":"간혹 정렬을 하고자 할때 a &gt; b ? 1 : -1 과 같이 쓸때가 있다.   java 9 이전 버전에서는 위와 같이 쓰면 == 에 해당하는 경우가 명시되어있지 않기때문에 오류가 발생한다.   방법은 여러가지가 있다.   첫째는 자바 버전을 올리는 것이다.   하지만 버전을 현업에서는 내가 맘대로 올릴수도 없으니..   둘째는 조금 코드량이 많아지지만 다음과 같이 작성하는 방법이 있다.     if (a &gt; b)  {     return 1;   } else if (a &lt; b) {     return -1;   } else if (a == b)  {     return 0;   }   한 두곳이나 저렇게 쓸 수 있지 여러곳이면 손가락 아플거 같다.   마지막은 조금 더 간단하게 작성하는 방법이다.     return Long.compare(a, b);   래핑 객체를 비교하면 더 낫다.  ","categories": ["posts"],
254        "tags": ["java"],
255        "url": "https://subji.github.io/posts/2020/04/14/javacomparezeroerror",
256        "teaser":null},{
257        "title": "Java Equals '==' 차이",
258        "excerpt":"보통 String 객체의 경우 equals 를 사용하고   int 변수의 경우 == 을 사용한다.   이유는 무엇일까?   그 차이는 객체와 값에 있다.   전자인 String 객체 비교의 경우 값이 아닌 객체를 비교한다.   객체를 비교한다는 것은 Call By Reference 로 참조값을 비교하는 것이다.   후자인 int 의 경우 Wrapping 객체가 아닌 Primitive 타입이므로 Call By Value 로 값을 비교하게 된다.   예를들어     String a = \"aaa\";   String b = a;   String c = \"aaa\";    a == b // True   b == c // False   위에서 보면 분명 a (“aaa”) 의 값을 b 로 복사해주었으니 당연히 c (“aaa”) 와 같을거라 생각할수 있다.   하지만 다르다.   a 와 c 의 값은 같다. 하지만 그 값을 가지고 있는 객체가 다르다.   다만 a 객체를 b 객체로 복사해주었으므로 a 와 b 는 같은 객체이다.   String 은 Primitive 타입이 아닌 객체 이므로 a 객체의 값 (“aaa”) 를 비교하는 것이 아닌 그 값을 가진   객체를 비교하는 것이다.   이때 비교하게 되는게 주소값인데, 객체는 그 각자가 서로 다른 주소값을 가지게 된다.   그러므로 a 를 할당받은 b 는 a 와 같은 주소값을 가지므로 “==” 연산자로 비교할떄 주소값으로 비교해 같게 되는것이다.   그래서 객체의 값을 비교하는 메소드인 equals 를 써야 정확하게 객체를 비교할 수 있다.   다음으로 int 형의 경우 Integer 라는 Wrapping 객체가 있지만 int 형 자체는 원시 타입 (Primitive Type) 이기에 단순히 값을 비교할 수 있는 것이다.  ","categories": ["posts"],
259        "tags": ["java"],
260        "url": "https://subji.github.io/posts/2020/04/27/javaequals",
261        "teaser":null},{
262        "title": "URLDecoder: Illegal hex characters in escape (%) pattern",
263        "excerpt":"Ajax 요청을 보내던 중 “URLDecoder: Illegal hex characters in escape (%) pattern” 라는 에러가 발생했다.   무슨 에러인가 하고 보니 % ê³¼ 16진수 문자열에 관한 내용인거 같다.   찾아보니 URL Decode 과정에서는 % 뒤에 오는 문자열을 16진수로 인식하는데 이 문자열이 변환이 되지 않았을때 발생하는 ì˜
263¤ë¥˜ë¼ê³  한다.   URL 에서 %가 붙은 문자열을 Escape 문자열이라고 한다.   왜 Escape 문자열을 사용하는건가? 라는 질문에는 다음과 같이 간단하게 답할 수 있다.   예를들어 GET 방식으로 파라미터를 보낼 때 다음과 같이 파라미터 값에 공백 값이 하나 있다고 치자   ex) ?key=valu e   이 경우 요청을 받는 곳에서는 온전하게 “valu e” 라는 값을 받아오지 못한다. 즉 URL 에 포함된 특수한 문자들에 대해선 인식을 하지 못하는 것이다.   그래서 특수문자를 사용했다는것을 알려주어야 하는데, 이때 사용하는게 Escape 문자이다.   위 예에서 “valu e” 를 인식해주기 공백을 나타내는 Escape 문자열인 “%20” 을 공백부분에 바꿔넣는다.   ex) ?key=valu%20e   그러면 URL Decoder 에서는 자연스레 공백으로 인식할 것이다.   에러인 “%” 에 대해서도 아래 표에서 나왔듯이 “%25” 로 바꿔주면 에러가 나지 않을 것이다.    Escape 특수문자 표             공백     %20     `     %60     -     %2D     &lt;     %3C           [     %5B     ~     %7E     _     %5F     &gt;     %3E           ]     %5D     !     %21     =     %3D     .     %2E           {     %7B     @     %40     +     %2B     ,     %2C           }     %7D     #     %23     \\     %5C     ?     %3F           (     %28     $     %24     |     %7C           )     %29     %     %25     /     %2F                     ^     %5E     :     %3A                     &amp;     %26     ;     %3B                     *     %2A     '     %27                               \"     %22     ","categories": ["posts"],
264        "tags": ["javascript"],
265        "url": "https://subji.github.io/posts/2020/04/28/urldecodepercenterror",
266        "teaser":null},{
267        "title": "Can't exec \"locale\": No such file or directory at /usr/share/perl5/Debconf/Encoding.pm",
268        "excerpt":"WSL 을 사용하면서 19.10 에서 20.04 로 판올림을 하는데 중간에 실수로 꺼서 그런지   system 쪽에서 충돌이 발생하여 제목과 같은 ì˜
268¤ë¥˜ê°€ 발생하였다.   깨진 파일들을 복구하거나 지우고 새로 설치를 해야된다.   첫번째로 찾은 방법은 다음과 같다.     $ apt-get download libc-bin   $ dpkg -x libc-bin*.deb unpackdir/      $ sudo cp unpackdir/sbin/ldconfig /sbin/   $ sudo apt-get install --reinstall libc-bin   $ sudo apt-get install -f   명령어는 실행되는데 변화가 없다.   오류도 그대로 발생중이다..   그래서 찾은 다른 방법     $ sudo mv /var/lib/dpkg/info /var/lib/dpkg/info_old        $ sudo mkdir /var/lib/dpkg/info                $ sudo apt-get update &amp;&amp; sudo apt-get -f install   $ sudo mv /var/lib/dpkg/info/* /var/lib/dpkg/info_old       $ sudo rm -rf /var/lib/dpkg/info                $ sudo mv /var/lib/dpkg/info_old /var/lib/dpkg/info     명령어를 보니      기존의 dpkg 내용들을 백업   dpkg 내용들을 담을 디렉토리 생성   패키지 업데이트 &amp; 강제 설치   새로 받은 패키지를 백업 패키지 디렉토리에 덮어씌우기   새로 만든 디렉토리 삭제   백업 디렉토리를 원래 패키지 디렉토리로 변경   그냥 쉽게 말해 다시 새로 받은것이다..ㅎ  ","categories": ["posts"],
269        "tags": ["linux"],
270        "url": "https://subji.github.io/posts/2020/05/06/fixedlinuxerrorlocaleperl5",
271        "teaser":null},{
272        "title": "Windows10 여러개의 VPN 설정하는 방법",
273        "excerpt":"작업을 하다보면 여러개의 VPN 을 사용해야 할 때가 있다.   매번 하나의 연결을 끊고 다시 연결하고를 반복할 수가 없는 노릇이다.   그래서 이번에는 여러개의 VPN 을 연결하는 방법을 알아보자.   먼저 TAP-Windows 어댑터를 추가해줘야한다.   TAP-Windows 어댑터란 가상 네트워크 커널 장치로 하드웨어 네트워크 어댑터가 아닌 소프트웨어 차원에서 지원된다.   Open VPN 의 경우 해당 어댑터를 사용하는데 기본적으로 하나의 어댑터가 추가되어있고 여러개의 VPN 을 사용하기   위해서는 TAP-Windows 를 늘려주면된다.   ê·¸ 방법은 아래와 같다.   먼저 아래의 경로로 이동한다.     C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\TAP-Windows\\Utilities   그러면 아래와 같이 TAP-Windows 를 추가 / 삭제할 수 있는 배치 파일이 보인다.          가상 TAP 어댑터를 추가해야 하니 “Add a new TAP virtual ethernet adapter” 파일ì
273„ 우클릭하여 관리자로 실행시키자.          다음과 같은 진행 상황 화면이 나오며 “계속하려면 아무 키나 누르십시오…” 문구가 나오면 아무버튼이나 눌러서 종료한다.   여기까지 됬으면 가상의 TAP-Windows 어댑터가 추가된것이다.   이제 확인하러 “제어판 - 네트워크 및 인터넷 - 네트워크 연결” 으로 이동해보자.   ë³´ë©´ “로컬 영역 연결 1 ~ 4” 부분이 새롭게 추가된 TAP 어댑터이다.   이제 VPN 파일(.opvn) 을 열어 다음과 같은 명령어를 추가해주자.     dev-node \"TAP-Windows 이름\" # 여기서는 로컬 영역 연결 1 ~ 4   마지막으로 VPN 연결 해주었던 방식대로 연결해주면 여러개의 VPN을 사용할 수 있게 된다.   ","categories": ["posts"],
274        "tags": ["vpn","windows"],
275        "url": "https://subji.github.io/posts/2020/05/06/howtomultiplevpnforwindows",
276        "teaser":null},{
277        "title": "CRLF (Carriage Return & Line Feed) 란?",
278        "excerpt":"Git add 를 하다가 다음 경고가 출력되었다.   “he file will have its original line endings in your working directory warning: CRLF will be replaced by LF in”   CRLF 는 개행문자인데 무언가 잘못된건가 싶었다.   찾아보니 큰 문제는 아니었고 OS 별로 CRLF 를 판정하는 기준이 조금씩 달라서 생긴 경고였다.   이 경고는 간단하게 다음 명령어를 입력하면 수정가능하다.   git config --global core.autocrlf false   그럼 본론으로 들어가서 위에서 언급한 CRLF 는 무엇일까?   간단하게 개행문자라고 할 수 있다.   보통 글을 쓰다보면 다음 행으로 내려서 쓰는 경우가 있다. 이때 다음행으로 넘어가는 행위를 개행이라고 하는데,   컴퓨터는 사람처럼 자동으로 알아서 다음 줄을 만들어내질 못한다.   그래서 입력해주는 사람이 컴퓨터에게 알려주어야 한다. 이것이 바로 개행문자인데 ASCII 코드에도 10, 13 번으로 명시되어 있다.   CRLF 는 Carriage Return 과 Line Feed 로 이루어진 줄임말인데, Line Feed 는 Enter 키 입력   즉, 다음줄로 넘어가 달라는 명령문자이다.   Carriage Return 은 커서의 위치를 현재 줄의 맨 처음으로 이동시키는 명령문이다.   이 두가지 명령문자들은 타자기에서 유래했다고 한다.   영화속 타자기를 보면 주인공이 타자를 치다가 왼쪽 또는 오른쪽의 레버(?) 같은 모양을 내리거나 올리거나 한다.   이 동작이 바로 타자기에서 사용되는 Line Feed 알림인 것이다!   또한 같은 장면에서 종이가 왼쪽으로 점점 이동하면서 글을 작성하는데, 위에서 처럼 레버를 내리고 새로운 줄의 처음으로   이동하기 위해 또다른 어떤 동작을 한다. 이것이 바로 Carriage Return 이다.  ","categories": ["posts"],
279        "tags": ["computer theory"],
280        "url": "https://subji.github.io/posts/2020/05/18/carragereturnandlinefeed",
281        "teaser":null},{
282        "title": "Oracle OVER 명령어",
283        "excerpt":"쿼리 작업을 하다보면 SELECT 절에서 COUNT() 값을 같이 조회해서 내보내고자 할때가 있다.   이전에는 다음과 같이 서브쿼리를 사용해서 작성했었다.     SELECT TITLE,          (            SELECT COUNT(*)              FROM T_TEST_ONE          )     FROM T_TEST_TWO   그런데 오라클에서는 좀 더 간단하게 표현하는 방법이 있었다.   바로 OVER() 함수다.   OVER 함수는      GROUP BY 또는 ORDER BY 와 같이 그룹 조회를 할때 이용되는 서브쿼리를 개선하기 위해 나온 함수이다.   COUNT(), SUM(), AVG(), RANK(), ROW_NUMBER() 등의 집계함수 또는 분석함수와 같이 사용된다.   아래는 일반적으로 발생할 수 있는 오류 상황이다.     SELECT COL_ONE,          COUNT(*)     FROM T_TEST_ONE   무엇이 잘못되었을까?   COUNT() 함수는 집계함수다. 즉, 전체를 그룹으로 하거나 특정 Column 을 그룹으로 만들어 사용할 수 있다.   위에 코드에서는 COL_ONE 이라는 Column 과 테이블 전체를 그룹으로 하는 COUNT 함수를 조회하고 있다.   즉, 집계함수를 사용하는 SELECT 문에서는 GROUP BY 로 지정되어 있지 않은 Column 을 조회할 수 없다.   만일 COL_ONE 이라는 Column 을 조회하고 싶으면 다음과 같이 작성해야 한다.     SELECT COL_ONE,          COUNT(*)     FROM T_TEST_ONE    GROUP BY COL_ONE   자, 확인해보자!   실행결과는 오류없이 나오는것 같다. 하지만 문제는 GROUP BY 로 묶여진 COL_ONE 별 COUNT() 함수의 값이 집계 된다는 것이다.   내가 원하는 것은 COUNT() 함수는 전체의 값을 COL_ONE 은 각각의 값을 내보내는 것이었다.   그래서 사용할 수 있는 것이 서브쿼리! 또는 OVER() 함수이다   OVER() 함수의 사용법은 간단하다.     # COL_ONE 과 테이블 전체의 COUNT() 값 조회   SELECT COL_ONE          COUNT(*) OVER()     FROM T_TEST_ONE    # 중복 제외   SELECT COL_ONE          COUNT(*) OVER(DISTINCT COL_TWO)     FROM T_TEST_ONE    # COL_TWO 정렬    SELECT COL_ONE          COUNT(*) OVER(ORDER BY COL_TWO)     FROM T_TEST_ONE    # GROUP BY   SELECT COL_ONE          COUNT(*) OVER(PARTITION BY COL_THREE)     FROM T_TEST_ONE    서브쿼리 사용도 좋지만 OVER() 함수와 같이 사용하기 편한 표현도 사용해보자.  ","categories": ["posts"],
284        "tags": ["db","oracle"],
285        "url": "https://subji.github.io/posts/2020/05/18/oracleoverfunction",
286        "teaser":null},{
287        "title": "MyBatis DBMS 별 LIKE 절",
288        "excerpt":"DBMS 별로 MyBatis 에서 LIKE 절을 사용하는 방법이 조금 다르다.   각 사용방법은 아래와 같다.   MySql     SELECT *     FROM TABLE    WHERE COLUMN LIKE CONCAT('%', #{ Keyword }, '%')   Oracle     SELECT *     FROM TABLE    WHERE COLUMN LIKE '%' || #{ Keyword } || '%'   MsSql     SELECT *     FROM TABLE    WHERE COLUMN LIKE '%' + #{ Keyword } + '%'  ","categories": ["posts"],
289        "tags": ["sql","mybatis"],
290        "url": "https://subji.github.io/posts/2020/05/19/mybatislike",
291        "teaser":null},{
292        "title": "Git 에러 error: pathspec { branch 명 } did not match any file(s) known to git.",
293        "excerpt":"Repository 에 새로운 브랜치를 생성하고 사용하던중 다른 피시에서 새로 생성된 브랜치를 불러오는데   제목과 같은 오류가 발생하였다.   다른 피시에 있는 브랜치는 새로 생성된 브랜치를 가지기 전의 브랜치로 이를 업데이트해줘야 했다.   방법은 다음 명령어를 실행해보자   git remote update git fetch git checkout {branch}  ","categories": ["posts"],
294        "tags": ["git"],
295        "url": "https://subji.github.io/posts/2020/05/20/gitdidnotmatchanyfilknowntogit",
296        "teaser":null},{
297        "title": "Headless 란",
298        "excerpt":"Headless 란?   Headless 란 직역을 해보면 머리가 없다는? 말인데.. 머리가 없다는게 도대체 무슨말일까?   일반적으로 사용자들이 웹페이지를 접속할때 브라우저(크롬, IE, 엣지, 사파리 등등) 을 사용한다. 이때 사용되는 브라우저가 앞서 말한 Head 를 가리키는 부분이다. 다시말해 Headless 란 브라우저가 없는 흔히 개발자들 사이에서 사용되는 Command Line 에서 확인가능한 방법을 가리킨다.   Headless 를 사용하는 이유는 뭘까?   Headless 가 주요사용되는 영역은 다양하다. 그중에서도 대표적으로 크롤링에서 많이 사용되는데, Headless 를 사용하면 사용자 인증등의 자바스크크립트로 이뤄지는 동작을 사용해볼 수 있다는 점이다.   한가지 예를 들면 자주 사용하는 curl 이란 명령어로 어느 특정 페이지를 가져온다고 하자.   만일 해당 페이지가 인증을 하고난 후에 접근이 가능하다면 curl 의 결과는 단순히 로그인 페이지를 가져올것이다. 하지만 Headless 를 사용하면 인증 동작을 사용하여 로그인 후 원하는 페이지의 정보를 가져올 수 있을 것이다.   이외에도 스크린샷 캡쳐, PDF 파일 생성 등에서 많이 쓰인다.   Headless 의 장점? 단점?  개발자 측면에서 봤을때 Headless 는 활용방안이 정말 많아 보인다. 그런데 꼭 좋은점만 있는 것은 아니다. 일반적으로 사용자가 브라우저에서 접근해서 사용하는 방식이 HttpClient 방식인데, 이는 주소 입력과 동시에 HTML Contents 를 불러오게 됩니다. 하지만 Headless 의 경우 로컬 또는 서버의 백그라운드에서 실제 브라우저를 사용하는 방식이기 때문에 속도적인 측면에서 기존의 HttpClient 방식에 비해 낮다고 볼 수 있습니다.   Headless 라이브러리  널리 알려저있는 것은 PhantomJS 이다. 하지만 Chrome 최신 버전에서도 headless-mode 를 사용할 수 있게 되었다. 윈도우는 60, 그외 OS 는 59 버전 이상에서 사용할 수 있다.  다만 PhantomJS 의 경우 OS 에 구애받지 않아 사용하기 용이하다.  ","categories": ["posts"],
299        "tags": ["headless"],
300        "url": "https://subji.github.io/posts/2020/06/01/headless",
301        "teaser":null},{
302        "title": "Blocking & Non-Blocking, Syncronous & Asyncronous",
303        "excerpt":"Blocking &amp; Non-Blocking 과 Syncronous &amp; Asyncronous 를 간단히 알아보고 차이점을 확인해보자.   Blocking 은 어떤 요청을 클라이언트가 주었을때, 서버에서 완료 혹은 오류 처리를 반환하기 전까지 클라이언트가 다른 호출을 할 수 없이 대기하는 상태이다.   Non-Blocking 은 어떤 요청을 클라이언트가 주었을때, 서버에서 완료 혹은 오류와 상관없이 클라이언트에 다른 호출을 할 수 있게 해주는 것이다.   Syncronous 는 C, Java 와 같은 프로그래밍에서 볼 수 있는 순차적 프로그래밍 방식이라고 생각하면 쉽다. 처음 호출된 함수 또는 코드가 완료되기 전까지 다음 함수 또는 코드가 실행되지 않는 방식이다.   Asyncronous 는 Syncronous 와 달리 함수 또는 코드의 완료 여부와 상관없이 다음 함수 또는 코드가 실행되는 방식이다.  ","categories": ["posts"],
304        "tags": ["javascript"],
305        "url": "https://subji.github.io/posts/2020/06/03/blockingnonblockingaysncsync",
306        "teaser":null},{
307        "title": "Javascript 비동기란?",
308        "excerpt":"비동기란?   영어로는 Asyncronous, 특정 함수 또는 코드의 실행 완료와 상관없이 다음 코드 및 함수를 실행하는 방식이다.   반대말로는 Syncronous, 동기 방식이 있다. 동기방식은 가까운 Java 를 ë³´ë©´ 알 수 있다.   다음 코드를 확인해보면 출력이 println() 함수의 호출 순서대로 나오는것을 ë³¼ 수 있다. 이처럼 순차적으로 코드가 실행되는 방식을 동기 방식이라고 한다.    public static void main (String[] args) {   System.out.println(\"1\");   System.out.println(\"2\"); }  // 1 // 2    그러면 비동기 방식은 어떻게 동작할까?   다음 에제를 보자.   console.log('1');  setTimeout(function ()  {   console.log('2'); }, 1000);  console.log('3');  // 1 // 3 // 2  위 코드는 자바스크립트에서 대표적인 비동기 처리 함수ë¡
308œ ë³¼ 수 있는 setTimeout 를 사용한 예제이다.   그렇다면 위 예제에서의 출력 순서는 어떻게 될까? 일반적인 경우라면 당연히 위에서부터 아래로 읽어내려가며 1 -&gt; 2 -&gt; 3 으로 생각될 것이다.   하지만 비동기 방식에서는 그렇지 않다. 첫번째 1 이 출력되고 2가 출력되기전까지의 1초를 기다려주지 않고 다음 3을 먼저 출력한다.   이렇게 순서에 상관하지 않고 코드를 실행하는 방식을 비동기방식이라고 한다.      비동기 처리를 동기? 적으로 사용하고 싶을떄   앞서 말한 비동기 방식을 동기방식으로 사용하고 싶을때 사용할 수 있는 몇가지 방법이 있다.   첫째로 콜백을 사용하는 방식이다.   자바스크립트에서 함수는 일급객체로서 파라미터로 사용할 수 있다.   그래서 함수 호출이 파라미터로 콜백함수라는 또다른 함수를 포함시켜 동기식으로 사용하는 방법이있다.   JQuery 함수로 익숙한 Ajax 를 활용한 예제 코드를 보자.   참고로 Ajax 는 클라이언트에서 서버로 XMLHttpRequest 를 비동기로 처리하는 통신방식이다.   // 1.  function main ()  {   let result = null;    $.get('/test', function (res) {     result = res;   });    return result; }  console.log(main());    // 2. function main (callback)  {   $.get('/test', function (res) {     callback(res);   }); }  main(function (res) {   console.log(res); });    1번과 2번 에제를 비교해보자. 1번은 비동기방식으로 처리되는 코드를 그대로 사용한 것이다. “console.log(main())” 의 결과는 어떻게 나올까? 응닶 값이 나올까? 아니면 null 값이 나올까? 답은 null 값이 나온다. 이유는 비동기 방식에서 찾아 ë³¼ 수 있다. $.get 으로 호출한 함수를 자바스크립트는 해당 요청이 완료될때까지 기다려주지 않고 바로 다음 코드인 “return result” 를 실행한다. 그래서 결과적으로 마지막 로그에서는 null 이 출력되는 것이다.   이러한 문제점을 해결하고자 하는 방식중에 하나가 콜백함수를 이용한 방식이다. 같은 Ajax 예제인 2번을 ë³´ë©´ 이름이 없는 함수를 파라미터로 main 함수에 전달해주고 있다. 자바스크립트에서 함수는 일급객체이므로 파라미터로 사용이 가능하다.   이 실행 순서를 1번 예제와 비교해보자.      main 함수 호출 -&gt; $.get 요청 -&gt; ê²°ê³¼(result) 반환 -&gt; ê²°ê³¼(result) 출력 -&gt; $.get 요청 응답? (순서가 마지막이 될지 언제가 될지 모른다.)   main 함수 호출 -&gt; $.get 요청 -&gt; 응답 -&gt; 콜백함수로 반환   순서를 ë³´ë©´ 2번의 경우 일반적으로 생각하기 쉬운 처리 방식인 위에서부터 아래로 읽어내려가는 방식이다. 반면에 1번의 경우는 도통 이해가 잘 가지 않는 방식이다.   콜백 방식은 비동기방식을 조금이나마 동기에 가깝게? 사용할 수 있도록 도와준다. 하지만 ê¼­ 좋은점만 있는건 아니다.      콜백의 단점?   콜백함수의 경우 비동기방식을 조금이나마 해소할 수 있는 방법이다. 하지만 가장 큰 문제로 꼽히는것이 콜백지옥이라고 불리는 문제이다.   다음 예제로 확인해보자.   function main (callback)  {   callback(); }  main(function ()  {   main(function ()  {     main(function ()  {       ...     });     ...   }); });  예제에서는 같은 함수를 계속 호출하였지만 각각 다른 함수를 호출할 수도 있다. 그럼 이게 왜 지옥이라고 불리느냐? 이유는 간단하다. 지금이야 몇개 안되는 함수를 콜백으로 호출하기 때문에 알아보기 쉽지만 만약 10개가 넘어가고 또 20개가 넘어가고 하다보면 알아보기 힘든 코드가 될 것이다.   더군다나 디버깅에도 어려움이 있다.   이를 해소할 수 있는 방법이 몇가지 또 있는데 첫째로 앞서 비동기 처리방식을 동기방식으로 해결하고자 도움을 줄 몇가지 중에서 Promise 와 Async/Await 가 있다.     Promise ?   조금 낯선? 용어다. Promise? 약속? 뭘 약속한다는거지?   이전에 봤듯이 콜백함수를 사용한 비동기 처리에서는 콜백지옥에 ë¹ ì ¸ 코드를 디버깅하기도 읽기도 어려워진다는 단점이 있었다. 이를 해결하기 위해서 사용되는것이 Promise 라는 함수이다.   Promise 는 총 3가지의 상태가 있다.     대기 (Pending)   이행 (Fullfill)   실패 (Reject)      대기 상태            new Promise() 로 선언된 상태이다. ```ts new Promise(); new Promise(function (resolve, reject) {           });  2. 이행 상태 - 콜백함수를 실행시켜주는 단계이다. ```ts new Promise(function (resolve, reject)  {   // 이행 콜백 함수 호출   resolve(); });  function main() {   return new Promise(function (resolve, reject) {     resolve('Success');   }); }  main() // 이행 콜백 함수  .then(function (result) {   console.log(result); });     실패 상태            이행 상태는 Resolve 호출, 실패는 Reject 함수를 호출한다. ```ts new Promise(function (resolve, reject)  { // 이행 콜백 함수 호출 resolve(); // 실패 콜백 함수 호출 reject(); });           function main() {   return new Promise(function (resolve, reject) {     resolve(‘Success’);     reject(‘Error’);   }); }   main() // 이행 콜백 함수  .then(function (result) {   console.log(result); }) // 실패 콜백 함수 .catch(function (error)  {   console.error(error); });   위 예제들 처럼 성공시 이행을 동작하고자 할때에는 resolve 함수를 호출하고 then 메소드에서 처리하며, 실패를 동작하고자 할때에는 reject 함수를 호출하고 catch 에서 해당 실패 이유를 확인한다.  이렇게 간단하게 예제를 알아보았다. 조금 더 알아보자면, 한개의 Promise 를 여러개의 then 을 사용하는 방법과 여러개의 Promise 를 연결하는 방법이 있는데 ê·¸ 예는 다음과 같다.  ```ts function main ()  {   return new Promise(function (resolve,reject) {     resolve();   }); } // 한개의 Promise 에 여러개의 then 을 사용하는 방법 main().then(function () { return result; }).then(function (result) { return result; })  function subPromise ()  {   return new Promise(function (resolve, reject) {     resolve();   }); }  main().then(subPromise).then()...    마지막으로 Promise 에서의 에러처리는 catch() 함수를 사용하도록 하자.     Async/Await   콜백함수, Promise 와 같이 비동기 처리 방법이다. ES6 에서 새로이 추가되었다.   사용법은 매우 간단하다. 한가지만 주의하면 되는데 await 로 받는 함수는 반드시 Promise 객체로 리턴을 해줘야한다는 것이다.   예제를 하나보자.   function returnResult ()  {   return new Promise (function (resolve, reject)  {     resolve('Success');   }); }  async function main ()  {   let result = await returnResult();    console.log(result); }   동작 방식은 간단하다. Promise 의 then 을 await 가 처리해주는 것이다. async/await 방식은 주로 여러개의 비동기를 처리할때 많이 쓰인다. 예외처리는 기본적인 try/catch 를 사용한다.  ","categories": ["posts"],
309        "tags": ["javascript"],
310        "url": "https://subji.github.io/posts/2020/06/03/javascriptasyncronous",
311        "teaser":null},{
312        "title": "SyntaxError: Cannot use import statement outside a module",
313        "excerpt":"NodeJs 와 Typescript 를 사용해서 개발을 하던 도중 제목과 같은 ì˜
313¤ë¥˜ë¥¼ 마주하게 되었다.   module 밖에서 추가된 코드를 사용할 수 없다는 말인거 같다.   그래서 첫째로 tsconfig.json 을 살펴보았다. module: commonjs 를 ë³´ë©´ commonjs 를 활용해서 module 을 사용하고 있었다. commonjs 라 함은 기억이 가물가물하지만 require(‘’) 로 선언해서 외부 라이브러리를 가져오는 방법인걸로 알고있다.   그러면 뭐가 문제일까..?   nodejs 공식문서를 ë³´ë©´ 다음과 같은 문장이 있다.   If the nearest parent package.json lacks a \"type\" field, or contains \"type\": \"commonjs\", .js files are treated as CommonJS. If the volume root is reached and no package.json is found, Node.js defers to the default, a package.json with no \"type\" field.   package.json 에서 “type” 필드에 별도의 값이 없거나 “commonjs” 로 설정되어있으면 기본 모듈로 “commonjs” 를 사용한다고 나와있다. 그럼 어떻게 import 문을 사용할 수 있을까? ê·¸ 답은 쉽게 찾을 수 있었다. package.json 에서 “type”: “module” 로 설정하면 되는것이었다.   문제해결!  ","categories": ["posts"],
314        "tags": ["javascript","typescript","node"],
315        "url": "https://subji.github.io/posts/2020/06/03/nodetypescripterror1",
316        "teaser":null},{
317        "title": "WSL1 cannot read realtime clock: Invalid argument 오류",
318        "excerpt":"WSL 을 사용하다가 sleep 명령어에서 다음과 같은 ì˜
318¤ë¥˜ê°€ 발생하였다.   “sleep:cannot read realtime clock: Invalid argument”   실제 시간이 안맞춰져서 인가? 싶어 찾아보았다.   WSL 20.04 이전버전에서 발생하는 버그로 20.04 로 업데이트 하는 과정 또는 ê·¸ 이전 버전에서 발생하는 버그였다.   그래서 20.04 로 설치되면서 꼬인 라이브러리들을 재 설치 하고 sleep 명령어도 재 복사 해주었다.   # 잘못 설치된 라이브러리 수정 wget https://launchpad.net/~rafaeldtinoco/+archive/ubuntu/lp1871129/+files/libc6_2.31-0ubuntu8+lp1871129~1_amd64.deb sudo dpkg --install libc6_2.31-0ubuntu8+lp1871129~1_amd64.deb sudo apt-mark hold libc6 sudo apt --fix-broken install sudo apt full-upgrade   # sleep 명령어 복사 sudo -i sudo mv /bin/sleep /bin/sleep~ ; touch /bin/sleep ; chmod +x /bin/sleep  ","categories": ["posts"],
319        "tags": ["wsl","linux"],
320        "url": "https://subji.github.io/posts/2020/06/09/wslrealteimclock",
321        "teaser":null},{
322        "title": "Apache Kafka 설치",
323        "excerpt":"Apache Kafka 설치 방법   kafka 는 Scala 로 작성되어 있습니다. 따라서 자바가 설치되었는지 확인 후 다음을 진행해야됩니다.   kafka 클러스터 관리등의 이유로 zookeeper 가 선행되어야 합니다. 다행이도 zookeeper 는 kafka 설치파일에 포함되어 있습니다.   나중에 실행할때도 zookeeper 를 먼저 실행하고 kafka 를 실행해줍니다.   1) Windows   Kafka 공식 홈페이지에서 바이너리 파일을 받습니다.   압축 해제 후 사용자가 원하는 디렉토리로 이동시키고 bin 디렉토리 안에 kafka-server-start.bat 으로 실행합니다.   2) Linux (Ubuntu)   # Kafka 다운로드 wget https://www.apache.org/dist/kafka/2.5.0/kafka_2.13-2.5.0.tgz   # 파일 압축 해제 tar zxf kafka_2.13-2.5.0.tgz  # kafka 디렉토리 이동 (선택사항) mv kafka_2.13-2.5.0 /usr/local/kafka   리눅스의 경우 실행하기 쉽게 systemctl 명령어로 등록을 해줍니다.   # zookeeper 실행 명령어를 등록 vim /etc/systemd/system/zookeeper.service  [Unit] Description=Apache Zookeeper server Documentation=http://zookeeper.apache.org Requires=network.target remote-fs.target After=network.target remote-fs.target  [Service] Type=simple ExecStart=/usr/local/kafka/bin/zookeeper-server-start.sh /usr/local/kafka/config/zookeeper.properties ExecStop=/usr/local/kafka/bin/zookeeper-server-stop.sh Restart=on-abnormal  [Install] WantedBy=multi-user.target  # kafka 명령어 등록 vim /etc/systemd/system/kafka.service  [Unit] Description=Apache Kafka Server Documentation=http://kafka.apache.org/documentation.html Requires=zookeeper.service  [Service] Type=simple Environment=\"JAVA_HOME={java home directory}\" ExecStart=/usr/local/kafka/bin/kafka-server-start.sh /usr/local/kafka/config/server.properties ExecStop=/usr/local/kafka/bin/kafka-server-stop.sh  [Install] WantedBy=multi-user.target  # 새로 만든 zookeeper, kafka 명령어 등록 sudo systemctl daemon-reload  # zookeeper 실행 sudo systemctl start zookeeper  # kafka 실행 sudo systemctl start kafka   이렇게 설치 및 실행을 해보았습니다.  ","categories": ["posts"],
324        "tags": ["kafka"],
325        "url": "https://subji.github.io/posts/2020/06/11/kafkahowtoinstallUbuntu",
326        "teaser":null},{
327        "title": "Spring Security 권한별 Maximum Sessions 결정 방법",
328        "excerpt":"스프링 시큐리티를 사용하다보면 중복 로그인을 막거나 해제해야되는 경우가 있습니다.   이경우에는 일반적으로 maximumSessions() 와 maxSessionPreventsLogin() 을 사용하곤 합니다. 그런데 조금 더 상세하게 설정을 하고 싶을떄가 있습니다. 예를들어 관리자의 경우에는 중복로그인을 허용하고 그외 사용자의 경우에는 중복로그인을 막는 것과 같이 말이죠.   그런데 위 설정만으로는 전체적으로 중복로그인을 막는다거나 혹은 특정 세션수만큼 풀어준다거나 그것도 아니면 아예 중복로그인을 허용한다거나 하는 것 밖에는 없어보입니다.   그래도 혹시 다른 무언가 방법이 있지 않을까해서 찾아보니 “ConcurrentSessionControlAuthenticationStrategy” 라는 클래스를 사용하면 된다는 군요. 스프링 시큐리티의 기본 Maximum Session 설정은 전역으로 적용되게 되어있습니다. 하지만 앞서 말한 “ConcurrentSessionControlAuthenticationStrategy” 의 “getMaximumSessionsForThisUser” 메소드를 사용하면 아주 쉽게 권한 또는 사용자의 정보에 따라서 세션수를 조절하는게 가능하다고 합니다.   더불어 세션 정책?전략? 에 대해서 사용자가 임의로 정하고 싶을때에는 “SessionAuthenticationStrategy” 의 구현체를 작성하거나 위에서 언급한 “ConcurrentSessionControlAuthenticationStrategy” 의 메소드인 “getMaximumSessionsForThisUser” 를 오버라이드하여 구현하면 되겠습니다.   아래는 getMaximumSessionsForThisUser 를 Override 하는 예제입니다.   public class CustomConcurrentSessionControlAuthenticationStrategy extends ConcurrentSessionControlAuthenticationStrategy {     // 최대 세션 수 권한별로 적용     protected int getMaximumSessionsForThisUser(Authentication authentication) {         boolean admin = // Check authentication.getAuthorities() for the admin role         return admin ? 8 : 1;      } }  위에서 만든 MaximumSession 메소드의 구현체를 Bean 으로 등록합니다.  @Bean public CustomConcurrentSessionControlAuthenticationStrategy sessionControlStrategy() {     return new CustomConcurrentSessionControlAuthenticationStrategy(new SessionRegistryImpl()); }  등록한 Bean 을 SessionManager 의 SessionAuthenticationStratege 에 등록합니다.  sessionManagement().sessionAuthenticationStrategy(se
328ssionControlStrategy());  권한별로 최대 세션 허용 수를 설정하는 법을 알아보았습니다.   출처 : Spring Security concurrent session control per User group or per single user   ","categories": ["posts"],
329        "tags": ["spring security"],
330        "url": "https://subji.github.io/posts/2020/06/16/springsecuritymaximumsessionbyroles",
331        "teaser":null},{
332        "title": "MSA Spring Cloud Config",
333        "excerpt":"MSA 마이크로서비스 아키텍쳐의 줄임말입니다.   이번에는 MSA 의 개념이 아닌 실습 내용으로 Spring Cloud Config 를 사용하여 MSA 내에 전체적인 설정을 관리하는 서비스를 실습해보고자 합니다.   1. 설치  Spring initializer 에서 Spring Cloud Config, Spring Security 를 추가해서 다운로드합니다.   2. Config 서버 활성화  Configuration 서버임을 알리는 Annotation 을 Main Class 에 작성합니다.  @SpringBootApplication @EnableConfigServer public class ConfigApplication {   public static void main(String[] args) {     SpringApplication.run(ConfigApplication.class, args);   } }   3. 기본 Application.yml &amp; Application.properties 설정  흔히들 8888 번을 Configuration 서버의 포트로 많이 사용합니다. 딱히 이유는 없는것 같습니다.  server:   port: 8888  다음으로 Config 파일들을 올릴 Git 주소를 입력합니다.  spring:   cloud:     config:       server:         git:           uri: ${Git (Github / others) Url}  이렇게 했으면 Git 에 Commit 을 해서 올려줍니다.   여기서부터는 선택사항입니다.   # Config 파일을 찾을 위치를 지정합니다. spring:   cloud:     config:       server:         native:           search-locations: classpath:/shared  # 위에서 적어는 server 의 profile 인 native 를 활성화합니다. spring:   profiles:     active: native \t\t # 나중에 인증 &amp; 인가 서버추가시 필요한 Security 설정입니다. spring:   security:     user:       name: ${username}       password: ${password}  이렇게 작성하면 Config 서버의 간단한 설정은 끝입니다.   실행하고 http://localhost:8888 로 접속하면 Security 설정을 한 경우에는 아이디/비밀번호를 묻는 창이 나오고 그 외에는 아무것도 나오지 않습니다.   이제 임의의 서비스가 사용할 Config 파일을 만들어봅니다.   위에서 classpath 아래 shared 디렉토리를 설정파일 경로로 지정해주었으니 그 경로에 파일을 하나 만들어줍니다.   이때, 파일이름이 중요합니다. {application name}-{profiles name}.yml 로 작성하는것이 좋습니다.   Config 서버가 Config 파일을 반환할때 보통 주소의 Context 를 찾아 반환하게 됩니다. 예를들어   http://config:8888/service/dev 는 application 이름이 service, 해당 어플리케이션의 profiles 이름이 dev 인 파일을 찾아 해당 설정을 반환합니다.   그러면 임시의 서비스의 포트만 반환하는 간단한 파일을 만들어줍니다. 저는 service-dev.yml 로 작성하였습니다.   # service-dev.yml server:   port: 8090   그리고 Config 서버의 내용은 Commit 하고 실행해봅니다.   이렇게하면 간단하게 Config 서버 세팅은 끝이 났습니다.   4. 테스트  curl -X GET http://locahost:8888/service/dev 로 확인해봅니다.   5. Config Client 세팅  Spring initializer 에서 Spring Config Client, Spring Security 를 추가 후 다운로드 합니다.   그리고 application.properties 를 bootstrap.yml 로 변경합니다. 그 이유는 bootstrap 의 경우 application.yml 이전에 불러오기 때문입니다.   이제 남은건 설정입니다.   spring:   application:     name: service   profiles:     active: dev        cloud:     config:       uri: http://localhost:8888       fail-fast: true       password: password       username: user   이전에 말했다시피 application 이름과 profiles 이름을 작성하였고, Config 서버의 주소를 작성해주었습니다.   이제 Config Server 를 실행한 상태로 Service 를 실행해봅니다.   그리고 http://locahost:8090 으로 접속해봅니다.   접속이 잘되면 설정을 잘 가져온 것입니다.  ","categories": ["posts"],
334        "tags": ["msa"],
335        "url": "https://subji.github.io/posts/2020/06/17/msa2",
336        "teaser":null},{
337        "title": "MSA (Micro Service Architecture) 란",
338        "excerpt":"1. MSA 란?   대규모 서비스에서 많이 사용되는 MSA 가 무엇일까? 말 그대로 풀이해보면 작은 서비스로 이루어진 구조이다. 작은 서비스로 이루어진 구조는 또 무엇일까?   MSA 는 SOA (Service Oriented Architecture) 라는 구조의 작은? 의미라고도 한다.   SOA 는 기업 (Enterprise) 단위에서 사용되었던 서비스 기반으로 이루어진 소프트웨어 구조이다. MSA 는 SOA 개념을 기반으로 대ê
338·œëª¨, 대용량 서비스를 이용하기 위한 SOA 보다는 조금 가벼운 구조라고 ë³¼ 수 있다.   MSA 는 작은 서비스로 이루어진 구조라고 하였다. 이 개념이 낯선 이유는 무엇일까? ê·¸ 이유는 가까이에서 찾을 수 있다. 아직도 많이 사용하는 웹 서비스 개발 구조를 보자. 하나의 프로젝트를 생성하고 ê·¸ 프로젝트 안에 기능 A, 기능 B, 기능 …, 을 둔다. 또한 하나의 프로젝트에는 하나의 DB 가 연결되어 있으며, WAS 역시 하나로 구성되어 있다. 익숙한 이 구조를 모놀리틱 구조 (Monolithic Architecture) 라고 한다.   모놀리틱 구조 (Monolithic Architecture)        그러면 기존의 잘 사용하던 모놀리틱 구조를 왜 굳이 MSA 를 도입하려하고 또 이미 사용하는 기업들이 많은 걸까? 모놀리틱 구조의 단점을 ë³´ë©´ 알 수 있다. 모놀리틱 구조는 하나의 프로젝트 안에 여러 기능(서비스) 가 들어가 있다고 했다. 만약 여기서 자주 배포되는 프로젝트의 경우는 어떻게 될까? 서비스 A 에서 코드를 조금 수정했다고 해보자 모놀리틱 구조의 경우 프로젝트 전체를 다시 배포해야 된다. 즉, 뭘 해도 전체를 다시 시작해야 된다는 것이다.   또한 여러 서비스들이 서로 간의 Dependency 가 높을 수 있기 때문에 서비스 A 의 기능을 변경할 경우 어떤 서비스에 영향을 미칠수도 있다. 같은 이유로 어느 한 서비스에 새로운 버전의 기능 및 다른 구조를 적용하기도 어려워지며, 서비스의 확장면에도 다른 서비스들과의 Dependency 를 고려해야하기 때문에 어렵다는 단점이있다.   이러한 모놀리틱 구조의 단점들을 해결하고자 사용되는 것이 MSA 구조이다. 위에 모놀리틱 구조의 그림과는 조금 다른 구조이다.   MSA 구조 (Micro Service Architecture)              2. MSA 장단점   MSA 는 하나의 서비스를 최소의 기능만을 수행하는 단위로 나누고 이에 연결되는 DB, WAS 를 각각 하나씩 두는 여러개의 프로젝트로 이루어진 구조이다. 그러면 MSA 는 어떤 장점들이 있으며 또 어떤 단점들이 있을까. MSA 의 장점은 모놀리틱 구조의 단점의 반대되는 내용이 많다.   첫쨰, 배포 시간 단축. 하나의 서비스는 최소한의 단위로 이루어져 있다. 그러면 당연히 사이즈도 그에맞게 작다. 즉, 빌드할 내용이 적으니 빌드의 속도도 빠르다. 빌드가 빠르면 배포 또한 오래걸리지 않는다.   둘쨰, 하나의 기술에 얽매이지 않는다. 각각의 서비스는 서로 다른 프로그래밍 언어로 구성해도 되고 아니면 서로 다른 구조를 사용해도 된다. 이를 일컬어 폴리글랏 구조라고 한다.  MSA 에서는 이러한 점이 다른 서비스에 영향을 미치지 않는다. 왜냐하면 서비스들 각각이 상대 서비스의 내부 구조를 알지도 못할 뿐더러 알필요도 없기 때문이다.     “폴리글랏 (Polyglot) : 서로 다른 프로그래밍 언어와 기술을 사용한 구조”   셋쨰, 확장성. 모놀리틱 구조에서는 서버 과부하에 따른 서비스 확장시에 하나의 프로젝트를 여러개 생성하여 L4 스위치를 이용해 로드 밸런싱을 구성한다. 하지만 MSA 에서는 다르다. 서비스 A 에 많은 과부하가 있으면 서비스 A 만 여러개로 구성하면 된다. 로드 밸런싱 구성은 나중에 Ribbon 이라는 기술을 사용하여 Client 쪽에서 구성할 수 있고 기존의 L4 스위치를 이용해서 구성해도 된다.   이외에도 여러가지 장점이 있지만 위 세가지는 여러 블로그, 책 등에서 나오는 대표적인 장점들이다. MSA 는 장점만 있는 것일까? 물론 단점도 존재한다. MSA 의 단점은 반대로 모놀리틱 구조의 장점이 될 수도 있다.   첫째, 통합 테스트의 어려움 모놀리틱 구조에서는 하나의 프로젝트에 전체 서비스가 들어가 있기 때문에 해당 프로젝트 내에서 테스트 코드를 작성해서 테스트를 쉽게 할 수 있다. 반면에 MSA 구조에서는 서비스가 각각 개별로 존재하기 때문에 프로젝트 전체의 통합 테스트를 하기가 어려운 부분이 있다.   둘째, 성능 모놀리틱 구조는 서비스들이 한 프로젝트 안에 있으므로 서로 Function ë
338˜ëŠ” Method 를 호출해서 참조할 수 있다. 반면에 MSA 는 분리된 서비스 구조이기 때문에 REST, Message 등의 기술로 내용을 주고 받아야한다. 집 앞에 있는 가게에 가서 직접 물건을 사오는 것과 집 앞에 있는데 굳이 배달을 이용해서 배달 지연등등의 상황을 고려해가며 물건을 받는 것의 차이일까?   셋쨰, 트랜잭션 관리 MSA 는 각각 서비스들을 호출해가며 트랜잭션을 진행한다.   MSA 구조의 트랜잭션        위 그림에서 ë³´ë©´ 서비스 A -&gt; 서비스 B 로 가는 트랜잭션 A 가 있고, 서비스 C 에서 서비스 B 로 가는 트랜잭션 B 가 있다. 각각의 트랜잭션은 서로 별개의 트랜잭션이기 때문에 이를 관리해하는데 어려움이 있다. 보통 모놀리틱 구조에서는 하나의 데이터의 이동을 따라 하나의 트랜잭션으로 구성되기 때문에 관리가 쉽지만 MSA 구조에서는 그렇지 않다. 이를 해결하기 위해 보상 트랜잭션, 분산 트랜잭션등의 기술이 있다.         3. MSA 구성   MSA 는 어떻게 구성되어 있을까? 여러 글을 봤지만 딱 이거다! 라고 내린 정의된 구성은 없었다. 다만 어느정도 중복되는 개념은 있었다.   모놀리틱 구조의 í
338Œ€êµ¬ì„±                Gateway 영역   하위 항목으로 Security 를 가지고 있다. Security 는 인증 &amp; 인가 역할을 수행하며 Gateway 에서는 인증 &amp; 인가 내용을 바탕으로 맞는 서비스를 호출 또는 접근 불가 동작을 수행한다.            Discovery 영역   MSA 에 구성된 서비스들을 등록한다. 등록된 서비스들은 Health Check 및 다른 서비스의 호출 요청에 원하는 서비스를 찾는데 도움을 준다.            CI/CD 영역   서비스의 지속적인 배포 및 통합을 관리한다.            Testing 영역   서비스 각각의 테스트 또는 전체적인 테스트를 관리 및 실행한다.            Communication 영역   서비스들간의 통신처리를 담당한다. 통신 처리는 AMQP 와 같은 메세지 프로토콜이 될수도있고 JSON 을 사용하는 REST 방식일 수도있다.            Observing 영역   서비스들 각각의 모니터링을 담당한다.       MSA 구성을 간단하게 작성해 보았다. 다시 말하지만 MSA 구성에는 정답이 없으며 나또한 많이 사용되는 부분을 참고하여 작성한 것이다.   MSA 구성 관련해서는 가트너에서 언급한 구성도 있고, microservice.io 라고 Chris Richardson 이라는 분이 운영하는 페이지에서도 확인 가능하다. 그외에도 검색을 통해 여러 사람들이 다양하게 구성한 MSA 구조를 ë³¼ 수 있다. 여러 곳을 참고하여 자신이 운영할 서비스에 맞는 구조를 구성하도록 하자.         4. MSA 도입   이렇게 ë³´ë©´ 하루빨리 MSA 구조를 도입해야만 할 것 같다. 하지만 또 무조건적인건 아니다. 모놀리틱 구조는 절대 나쁜 구조가 아니다. 그렇다고 MSA 구조 또한 절대 좋은 구조가 아니다. 서로 다른 구조라고 보는게 맞는것 같다. 예를들어 크지 않은 경량 서비스의 경우에는 오히려 모놀리틱 구조가 유용할 수 있다. 이는 성능 상의 이점을 가져올수도 있기 때문이다.   개인적으로는 MSA 는 모놀리틱 구조와 같이 어울리는 것 같기도하다. MSA 의 작은 서비스 단위 하나가 모놀리틱 구조로 설계 될 수도 있지 않은가?   MSA 구조는 팀의 구성 또한 중요하다. 모놀리틱 구조의 í
338Œ€ 구성을 ë³´ë©´ 대부분 기획/개발/디자인/운영 등으로 이루어져 있다.   모놀리틱 구조의 팀구성        반면에 MSA 구조의 팀 구성은 각각의 서비스에 7~10 명 정도의 인원으로 구성되며, ê·¸ 안에서 기획/개발/디자인/운영으로 이루어진다. 최종적으로 이러한 소규모의 인원으로 구성된 팀이 하나의 서비스를 담당하며 ê·¸ 팀들이 여러개로 구성되어 있다.   MSA 구조의 팀구성        물론 이게 정답은 아닐 것이다. 대규모 인원으로 이루어진 하나의 팀으로 MSA 를 진행할 수도 있고 소규모의 인원으로 구성된 여러개의 팀으로 모놀리틱 구조를 진행 할 수도 있다. 하지만 위에 나온 그림들은 각 소프트웨어 구조에 그나마? 최적화된 구성이 아닐까 싶다.         5. 끝   낯선 내용은 언제나 처음이 어렵다. 하지만 찬찬히 하나씩 적용해가며 이해하면 또 금방이다. 스마트폰 보급으로 인터넷 사용률이 증가하고 있는 세상에서 어떠한 서비스던지 많은 사용량을 기록할 수도 있다. 무조건적으로 보류할만한 내용은 아니다. 자신이 개발한 서비스 또는 팀이 개발한 서비스가 커질것이라고 믿고 MSA 를 공부해보자!  ","categories": ["posts"],
339        "tags": ["msa"],
340        "url": "https://subji.github.io/posts/2020/06/19/msa1",
341        "teaser":null},{
342        "title": "Non-ASCII character 'ë' in file run.py on line {N}, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details",
343        "excerpt":"파이썬에서 주석등 한글을 사용할 때 발생하는 인코딩관련 오류이다.   일단 에러메세지에 나온대로 http://python.org/dev/peps/pep-0263/ 에 접속해보자.   Problem 항목을 들여다보자.    In Python 2.1, Unicode literals can only be written using the Latin-1 based encoding \"unicode-escape\".    여기가 핵심이다. 파이썬 2.x 버전대에서는 유니코드 리터럴을 Latin-1 기반 인코딩 “unicode-escape” 로 씌여져 있다고 나와있다. 우리가 쓴 한글은 Latin-1 기반에서는 지원되지 않는다.   해결방법은 그 바로 아래에 잘 나와있다. 기본 인코딩을 파이썬 파일에 정의하라고 되어있다. 적용방법은 다음과 같다.   파이썬 파일 제일 위에 다음 Comment 를 추가한다. 한글을 지원해야 하므로 UTF-8 을 사용했다.   #!/usr/bin/python # -*- coding: &lt;encoding name&gt; -*-  #!/usr/bin/python # -*- coding: utf-8 -*-  ","categories": ["posts"],
344        "tags": ["python"],
345        "url": "https://subji.github.io/posts/2020/06/19/python2.xencoding",
346        "teaser":null},{
347        "title": "TDD(Test Driven Development) 란 (1)",
348        "excerpt":"아래 내용은 ‘테스트 주도 개발: TDD 실천법과 도구’(https://repo.yona.io/doortts/blog/issue/1)를 보고 주요 내용을 개인적으로 정리하여 작성했습니다.   TDD ?   테스트 주도 개발의 줄임말이다. 빠른 개발을 진행하며 매번 해야지만 생각하고 막상 실천하지는 못했다. 테스트 주도 개발이 뭘까? 말 그래도 해석해보자면 테스트를 우선시 하는 개발이라고 할 수 있다. ‘그래서 그게 도대체 무슨말인데..’   자바를 예로 들면 일반적으로 클래스를 생성하고 프로퍼티와 메소드를 생성하고 코드를 실행한다. 그리고 콘솔이나 다른 방법으로 해당 코드가 잘 실행이 되었는지 눈으로 확인한다. 테스트 주도 개발은 조금 다르다. 가장 큰 차이점은 코드를 작성하기전에 테스트 코드를 먼저 작성한다는 것이다.   테스트 코드를 먼저 작성한다는게 무슨 의미를 가질까?   좀 더 깔끔하고 단단한 코드를 작성하게 되는 밑거름이 될 수 있다. 그렇다고 테스트 코드를 먼저 작성하지 않고 작성한 코드가 잘못 됬다는 것은 아니다. 다만 테스트 코드를 먼저 작성함으로써 오는 이점이 조금 더 크다는 점이다. 그렇다면 테스트 코드를 작성하면 얻는 이점이 뭐가 있으며 또 단점은 뭐가 있을까?       TDD 장단점?   TDD 의 장점은 위에 언급 했었던 깔끔하고 단단한 코드를 만들 수 있다는 점이다. 또한 개발 코드의 수정 후 QA 및 기타 테스트를 수행하는 과정에서 유지보수가 용이하다는 점이다.   예를들어보자. 결제 프로젝트에서 결제 실행 코드 부분을 조금 수정했다고 하자. 코드 수정이 끝난 후에는 다시 테스트가 진행되어야 한다. 이때 수정된 부분에서만 테스트가 진행되는게 아니라 수정된 부분과 연결된 기존의 실행 과정 자체도 테스트가 되어야 한다. 만약 수정된 부분이 다른 코드와 종속적인 관계에 있다면 또 그 부분에서 오류가 발생한다면 과연 빠르게 찾아 낼 수 있을까? 확실하게 찾아낼 수 있을까? 여기서 테스트 코드의 장점이 발휘된다.   개발자는 수정된 코드를 테스트 하기 위해 기존의 테스트 코드를 실행한다. 테스트 코드가 전체 완료되면 성공인 것이고, 실패 되는 부분이 있으면 그곳을 다시 확인하면된다. 즉, 프로젝트의 유지보수에 용이하다.   또한 테스트 코드는 시나리오를 바탕으로 작성하게 되는데, 이는 추후 인수인계 및 코드 설명에서 유용한 자료가 된다. 잘 짜여진 테스트 시나리오는 다른 개발자들에게 좋은 매뉴얼이 될 수 있다. 많은 양이 작성된 코드를 여기저기 찾아가며 따라가는것이 빠를 것 같은가? 아니면 누군가가 만들어놓은 매뉴얼을 따라가며 확인하는 것이 빠를 것 같은가?   마지막으로 소프트웨어 설계에 좋은 영향을 미친다. 예를들어 클래스를 하나 설계할때도 변수 명, 메소드 명, 파라미터, Arguments 등에서 다시한번보게 되고 더 깔끔하게 작성되도록 도와준다.   이외에도 여러 장점이 있을것이다. 다음은 단점을 꼽아야 하는데 사실 단점이라고 할게 개인적으로 일정이 짧은 프로젝트에서는 많은 시간을 할당하기 어렵다는 점? 뿐이다. 하지만 이정도의 작은 단점으로 위에서 말한 큰 장점들을 포기하기엔 아쉽다.       TDD 작성 방법   TDD 의 작성방법은 큰 틀에서 다음과 같다. 먼저 시나리오를 하나 선택한다. QA 및 기획팀에서 만들어 놓은 시나리오가 있으면 그걸 따라가면 된다. 하지만 그렇지 않다면 개발자가 스스로의 테스트 시나리오를 작성해야 한다.   시나리오를 하나 선택했으면 그 시나리오에 대한 결과를 작성한다. 결과는 성공 또는 실패가 될 수 있다. 아니면 테스트 코드 자체의 오류가 될 수 있다. 가급적 오류는 만들지 않도록 작성하자.   결과의 성공은 선택한 시나리오가 맞는 값, 의도된, 예측된 값등을 출력했다는 것을 의미하고, 실패는 맞지 않는, 의도하지 않은 값, 예측에 실패 한 값등이 될 것이다.   끝으로 리팩토링이 있다. 시나리오를 선택하고 결과값을 판단하는 코드를 만들었다면 이를 바탕으로 개발 코드를 리팩토링해야 한다.       끝   TDD 는 개념이다. 어떤 프레임워크나 라이브러리를 사용하는 특정 기술이 아니라. 해당 기술을 부수적으로 사용하는 소프트웨어 설계 개념이라고 생각한다.  ","categories": ["posts"],
349        "tags": ["tdd"],
350        "url": "https://subji.github.io/posts/2020/06/22/tdd-java-1",
351        "teaser":null},{
352        "title": "TDD(Test Driven Development) - JUnit 5 (2)",
353        "excerpt":"아래 내용은 ‘테스트 주도 개발: TDD 실천법과 도구’(https://repo.yona.io/doortts/blog/issue/1)를 보고 주요 내용을 개인적으로 정리하여 작성했습니다.   JUnit ?   Java 에서 사용되는 Testing 프레임워크이다. 단위 테스트를 할 때 사용되는 도구이며 테스트 결과와 예상값을 확인시켜주는 Assert 와 테스트 실행을 도와주는 Test Runner 등으로 구성되어 있다.   xUnit 이라고 해서 자바 외에 다른 언어에서 사용되는 테스트 도구가 있다. 예를들어 C 의 경우 CUnit C++ 의 경우 CppUnit 등이 있다.       JUnit, Hamcrest 설정    JUnit 4 버전에서는 단일 jar 만 사용되었었는데 JUnit 5 로 넘어오면서 여러개의 jar 로 분리되었다고 한다.   JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage      JUnit Platform : JVM 위에서 실행되는 테스트 프레임워크 이다. JUnit 과 Client Build Tool 사이에서 강력한 인터페이스를 제공한다. 또한 JUnit 플랫폼 위에서 실행되는 테스트 프레임워크를 개발하기 위한 TestEngine API 를 제공한다. 이를 사용해서 써드파티 테스트 라이브러리를 JUnit 에 직접 플러그인 할 수 있다.   JUnit Jupiter : @BeforeEach, @AfterEach, 등의 5버전에서 새로이 추가된 프로그래밍 모델과 확장 모델들을 포함하고 있다. JUnit 4 에서 추가되거나 변경된 Annotation 은 다음과 같다.            @TestFactory : 동적 테스트를 위한 테스트 팩토리 메소드를 가리킨다.       @DisplayName : 테스트 클래스 또는 메소드의 사용자 정의 이름을 표시한다.       @Nested : 해당 Annotation 이 표시된 클래스가 중첩된 비 정적인 테스트 클래스임을 나타낸다.       @Tag : 필터링 테스트를 위한 태그를 선언       @ExtendWith : 사용자 정의 확장명을 등록하는 데 사용       @BeforeEach : 테스트 메소드 이전에 실행되는 메소드(@Before)       @AfterEach : 테스트 메소드 후에 실행되는 메소드(@After)       @BeforeAll : 현재 클래스의 모든 메소드 이전에 실행되는 메소드(@BeforeClass)       @AfterAll : 현재 클래스의 모든 메소드 이후에 실행되는 메소드(@AfterClass)       @Disable : 테스트 클래스 또는 메소드를 테스트에서 제외 또는 비활성화 하는데 사용(@Ignore)                JUnit Vintage : TestEngine 에서 JUnit 3 &amp; JUnit 4 버전의 테스트 코드를 실행해준다.       JUnit 5 Features :            JUnit Jupiter 에서 테스트 코드를 작성       JUnit 4 버전과 Jupiter 통합       테스트 실행       JUnit Jupiter 를 위한 확장 모델 제공       JUnit Platform 실행 API, JUnit Platform Test Kit 제공           JUnit 5 의 대략적인 변경점과 특징을 알아보았다. 이제 Maven 또는 Gradle 에서 Dependency 를 추가해보자.   Maven  &lt;dependencies&gt;     &lt;!--        https://mvnrepository.com/artifact/org.junit.       jupiter/junit-jupiter      --&gt;     &lt;dependency&gt;         &lt;groupId&gt;org.junit.jupiter&lt;/groupId&gt;         &lt;artifactId&gt;junit-jupiter&lt;/artifactId&gt;         &lt;version&gt;5.6.2&lt;/version&gt;         &lt;scope&gt;test&lt;/scope&gt;     &lt;/dependency&gt; &lt;/dependencies&gt;   Gradle  // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter testCompile group: 'org.junit.jupiter', name: 'junit-jupiter', version: '5.6.2'       JUnit 5 예제   import static org.junit.jupiter.api.Assertions.assertEquals;  import java.util.logging.Logger;  import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest;  @SpringBootTest class DemoApplicationTests {    private static final Logger log = Logger.getLogger(DemoApplicationTests.class.getName());    // BeforeAll 은 현재 테스트 클래스 또는 전체 테스트 메소드 실행이전에 실행되야 하므로 static 으로 작성한다.   @BeforeAll   static void setup() {     log.info(\"@BeforeAll - executes once before all test methods in this class\");   }    @BeforeEach   void init() {     log.info(\"@BeforeEach - executes before each test method in this class\");   }      // assertEquals 사용   @Test   @DisplayName(\"1 + 1 = 2\")   void addTwoNumbers ()\t{     Calculator calculator = new Calculator();     assertEquals(2, calculator.add(1, 1), \"1 + 1 should equal 2\");   }    // 테스트 항목 이름 설정   @DisplayName(\"Single test successful\")   @Test   void testSingleSuccessTest() {     log.info(\"Success\");   }    // JUnit4 버전의 @Ignore 와 동일한 @Disabled 사용   @Test   @Disabled(\"Not implemented yet\")   void testShowSomething() {   }    // Java 8 이상의 버전에서는 Lambda 식을 사용할 수 있다.   @Test   void lambdaExpressions() {     List&lt;Integer&gt; numbers = Arrays.asList(1, 2, 3);      assertTrue(numbers       .stream()       .mapToInt(i -&gt; i)       .sum() &gt; 5, () -&gt; \"Sum should be greater than 5\");   }    // 위 Lambda 식을 사용하면 lazy 로딩을 이용하여 좋은 퍼포먼스를 보일 수 있다.   // 하지만 아래처럼 사용할 경우 보다 더 정확한 위치의 오류를 확인할 수 있다.   @Test   void groupAssertions() {     int[] numbers = {0, 1, 2, 3, 4};      assertAll(\"numbers\",       () -&gt; assertEquals(numbers[0], 1),       () -&gt; assertEquals(numbers[3], 3),       () -&gt; assertEquals(numbers[4], 1)     );   }    @Test   void trueAssumption() {     // True 경우를 가정하는 테스트 문     assumeTrue(5 &gt; 1);     assertEquals(5 + 2, 7);   }    @Test   void falseAssumption() {     // False 일 경우를 가정하는 테스트 문     assumeFalse(5 &lt; 1);     assertEquals(5 + 2, 7);   }    @Test   void assumptionThat() {     String someString = \"Just a string\";     // assumingThat 의 경우 첫번째 인자인 가정문이 맞지 않을 경우 실행되지 않는다.     // 만약 첫번째 인자의 가정문이 맞다고 하면 뒤에 테스트 문이 실행된다.     assumingThat(someString.equals(\"Just a string\"), () -&gt; assertEquals(2 + 1, 4));   }    // 예외에 대한 테스트   @Test   void shouldThrowException() {     // 예외의 세부사항에 대한 테스트     Throwable exception = assertThrows(UnsupportedOperationException.class, () -&gt; {       throw new UnsupportedOperationException(\"Not supported\");     });     assertEquals(exception.getMessage(), \"Not supported\");   }    @Test   void assertThrowsException() {     String str = null;     // 예외의 유형에 대한 테스트     assertThrows(IllegalArgumentException.class, () -&gt; {       Integer.valueOf(str);     });   }    // 아래 두 예제는 Test suite 예제인데, @SelectPackage 같은 경우에는 테스트 패키지를 테스트하는 것이고   // @SelectClasses 의 경우에는 선택된 클래스만 테스트하는 것이다.   // @RunWith(JUnitPlatform.class)   // @SelectPackages(\"com.baeldung\")   // public class AllUnitTest {}   // @RunWith(JUnitPlatform.class)   // @SelectClasses({AssertionTest.class, AssumptionTest.class, ExceptionTest.class})   // public class AllUnitTest {}    // 아래 예제는 동적인 테스트에 관한 예제이다.    // 일반적으로 위에서 보았던 테스트의 경우 정해진 정적인 값을 비교하는 테스트들이었다.   // 하지만 아래는 고정된 값이 아닌 배열을 조회하며 각각의 값을 테스트한다.   private List&lt;String&gt; in = new ArrayList&lt;&gt;(Arrays.asList(\"A\", \"B\", \"C\"));   private List&lt;String&gt; out = new ArrayList&lt;&gt;(Arrays.asList(\"aaa\", \"bbb\", \"ccc\"));    @TestFactory   public Stream&lt;DynamicTest&gt; translateDynamicTestsFromStream() {     return in.stream().map(word -&gt;
353 DynamicTest.dynamicTest(\"Test translate \" + word, () -&gt; {       int id = in.indexOf(word);       assertEquals(out.get(id), translate(word));     }));   }    private String translate(String word) {    if (\"A\".equalsIgnoreCase(word)) {       return \"aaa\";     } else if (\"B\".equalsIgnoreCase(word)) {       return \"bbb\";     } else if (\"C\".equalsIgnoreCase(word)) {       return \"ccc\";     }     return \"Error\";   }    @AfterEach   void tearDown() {       log.info(\"@AfterEach - executed after each test method.\");   }    @AfterAll   static void done() {       log.info(\"@AfterAll - executed after all test methods.\");   }  }    위에 코드를 보면 @Test 로 표시된 메소드를 테스트한다. 테스트 항목의 이름은 “1 + 1 = 2” 로 선언하였고 Equals 를 이용해 add(1, 1) 의 결과가 2 가 맞는지 확인한다.   해당 예제는 JUnit5 공식 사이트에 있다.   간단하게 JUnit 5의 특징과 예제를 알아보았다. 다음에는 Jupiter 의 여러 Annotation 과 테스트를 좀더 유연하게 사용하도록 도와줄 Hamcrest 를 알아보자.  ","categories": ["posts"],
354        "tags": ["tdd","java","junit"],
355        "url": "https://subji.github.io/posts/2020/06/23/tdd-java-2",
356        "teaser":null},{
357        "title": "TDD(Test Driven Development) - Hamcrest (3)",
358        "excerpt":"아래 내용은 ‘테스트 주도 개발: TDD 실천법과 도구’(https://repo.yona.io/doortts/blog/issue/1)를 보고 주요 내용을 개인적으로 정리하여 작성했습니다.   Hamcrest ?   단순히 값 비교, 참/거짓 비교 등을 떠나 좀 더 유연하게 테스트를 할 수 있게 지원해주는 Matcher 라이브러리이다. Matcher 라이브러리는 ‘필터나 검색 등을 위해 값을 비교할 때 좀 더 편리하게 사용할 수 있게 도와주는 라이브러리.’ 라고 한다. 예전에는 JUnit 라이브러리와 같이 쓰면 좋은 정도의 외부 라이브러리였지만 편리함과 많은 사람들의 쓰임으로 인해 JUnit 라이브러리에 아예 포함되었다. 하지만 JUnit 5 에 와서는 Hamcrest 에서 사용되는 assertThat 문이 빠지게 되었다.   JUnit 5 공식문서에 보면 다음과 같이 나와있다.     For example, the combination of matchers and a fluent API can be used to make assertions more descriptive and readable. However, JUnit Jupiter’s org.junit.jupiter.api.Assertions class does not provide an assertThat() method like the one found in JUnit 4’s org.junit.Assert class which accepts a Hamcrest Matcher. Instead, developers are encouraged to use the built-in support for matchers provided by third-party assertion libraries.    자세한 이유는 안나와있어서 잘 모르겠다만 결과적으로 assertThat 과 같은 구문을 사용하고 싶으면 써드파티 라이브러리를 사용하라는 말이다. 얘기를 쭉 이어 나가자면 왜 Hamcrest 같은 Matcher 라이브러리가 좀 더 유연하고 문법적으로 친숙하냐고 궁금할 수 있다. 그럼 잠시 Hamcrest 예제와 JUnit 예제를 비교해보자   // JUnit  assertEquals(true, true); // Hamcrest (Matcher) assertThat(true, is(true));   딸랑 두개뿐인 메소드지만 잘 보면 차이를 느낄 수 있다. ‘assertEquals’ 와 같은 경우 프로그래밍 적으로는 친숙하다. 뭐 나쁘지 않다. 하지만 아래 ‘assertThat, is’ 메소드를 보면 뭔가 책을 읽는 듯한 느낌? 이다. 즉, 한글이 아닌 영어라서 조금 받아들이기 어렵겠지만 문법적으로는 아래쪽이 더 나아보인다. 이러한 이유에서 Hamcrest 와 같은 Matcher 라이브러리를 사용한다고 한다.       Hamcrest 설정    Hamcrest 를 사용하기 위한 사전조건이 몇가지 있다. 첫째는 Hamcrest 라이브러리를 추가하는 것이고, 둘째는 추가된 라이브러리를 static 으로 import 하는 것이다. 다른 IDE 나 툴들은 잘 모르겠지만 내가 현재 사용하는 vscode 에서는 static 메소드에 대해서는 자동으로 import 가 잘 안될때가 있다.   Maven De
358npendency 추가  &lt;dependency&gt;     &lt;groupId&gt;org.hamcrest&lt;/groupId&gt;     &lt;artifactId&gt;hamcrest-library&lt;/artifactId&gt;     &lt;version&gt;2.1&lt;/version&gt;     &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt;   Static method import  // Matcher method import static org.hamcrest.Matchers.is;  import static org.hamcrest.MatcherAssert.assertThat;   Matcher 메소드는 is() 외에도 여러개가 있으니 필요에 따라서 추가해주도록 하자.       Hamcrest 사용   Hamcrest 의 assertThat 은 세가지의 파라미터를 받는다.      Assential 내용이 실패했을 떄 출력되는 메세지   실제로 출력되는 값   출력 예상 값   @Test public void testTemp() { \tassertThat(\"Date test: \", \"2020-06-25\", is(\"2020-06-24\")); }   위 코드의 실행 결과는 다음과 같다.         보면 “Expected: is “2020-06-24” 라고 2020-06-24를 예상했다. 하지만 실제 값은 “but: was 2020-06-25” 라고 실패했다고 한다. is, but, was 등의 문구는 좀 더 사람에게 친숙하게 느껴진다. 이것이 Hamcrest 를 쓰는 이유이다.  ","categories": ["posts"],
359        "tags": ["tdd","java","junit","hamcrest"],
360        "url": "https://subji.github.io/posts/2020/06/29/tdd-java-3",
361        "teaser":null},{
362        "title": "TDD(Test Driven Development) - Mock (4)",
363        "excerpt":"아래 내용은 ‘테스트 주도 개발: TDD 실천법과 도구’(https://repo.yona.io/doortts/blog/issue/1)를 보고 주요 내용을 개인적으로 정리하여 작성했습니다.   Mock ?   자동차를 만들기전에 나무를 사용해서 실제 자동차와 1:1 비율로 만들때가 있다. 실제 자동차를 제작하려면 비싼 재료가 들어가기도 하고 또 그런 상황이 발생하면 비용적인 측면에서도 많은 수고를 들이기 때문이다. 그래서 이러한 수고를 덜고자 값이 비교적 싼 나무를 사용해서 실제 자동차와 같은 비율로 만들어 테스트를 한다. 이떄 나무로 만들어진 자동차 모형을 Mock 이라고 한다. 쉽게 말해 가짜 객체이다.   Mock 객체는 실제 객체를 만드는데 시간이 오래 걸리거나 여러 객체가 의존성으로 묶여 있어 구현이 힘들때 사용한다.   그렇다면 Mock 객체가 필요한 때는 언제일까?      환경설정이 필요할 떄            일반적으로 DB, WAS 등의 설정이 필요할 떄 사용한다.           특정 모듈이 “아직” 존재하지 않을 때            예를들어 A 라는 메소드가 아직 구현이 안된 상태에서 어떤 값이 출력될지 예상이 되면 Mock 객체를 만들고 결과값으로 임시적인 예상 값만 추가해서 사용할 수 있다.           다른 팀 간의 협의 및 정책이 필요할 때            팀 간 이용하는 네트워크가 다를 때 접근이 불가능한 모듈의 경우 Mock 객체를 사용해서 테스트를 할 수 있다.           이외에도 테스트시간이 너무 오래 걸리거나, 특정 순간이나 경우에 의존적일 경우에도 사용한다.   Mock 객체는 메스자로스 제라드라는 사람이 테스트 더블 이라는 용어를 사용하여 테스트 더블의 하위 분류로 나누었다.             Test Double   그렇다면 테스트 더블의 하위 분류들을 알아보도록 하자.   (1) Dummy Object   더미 객체는 말 그대로 모조품, 단순한 껍데기에 해당한다.  인터페이스를 오로지 인스턴스화 시키기 위한 이유로만 생성되는 객체이다. 더미 객체는 기능적인 면은 구현하지 않고 객체의 인스턴스 확인에만 사용되므로 테스트 코드 작성 시에 추가적인 예외처리가 필요하다. 하지만 이렇게 단순히 인스턴스만 가지고는 기능적인 테스트를 할 수 없다. 그래서 더미 객체에서 조금 더 발전되어 메소드에 기능을 추가한 객체를 Test Stub 이라고 한다.   class Dummy implements User {    public void getDummy() {}  }  class DummyTest {    @Test   public void getDummy_더미객체() {     // User 인터페이스의 가짜 구현 객체 인 Dummy 객체를 사용     User user = new Dummy();   } }    (2) Test Stub   더미 객체가 마치 실제처럼 동작하는 것처럼 보이게 만든 객체이다.  더미 객체와의 차이점은 더미 객체는 단순 인스턴스만 반환하고 스텁 객체는 인스턴스화된 객체가 특정 상태 값을 가지는 것이다. 다음은 로직이 실제 구현된 것처럼 보이는 Fake Object 이다.   class Stub implements User {      public void getUser() {     return \"stub\";   }  }  class StubTest {    @Test   public void getUser_스텁객체() {     User user = new Stub();     // 임의의 값을 넣어 실제처럼 동작하게 만든 stub 메소드 호출     assertThat(user.getUser(), is(\"stub\"));   } }   (3) Fake Object   Stub 객체와 조금은 비슷하지만 인스턴스화 된 객체가 여러개인 경우 또는 복잡한 구현이 들어가 있는 형태 stub 의 경우에는 하드코딩된 값이 들어가 있어서 하나의 인스턴스만을 대표했다. 즉 하나의 인스턴스화 된 객체로만 역할을 수행했다. 하지만 어떤 상황이나 조건에 의해 값이 변하거나 할 경우에는 여러개의 인스턴스에 대응할 수 있어야 한다. 이때 사용되는게 Fake Object 이다. 예를들어 DB 를 조회하는 로직의 경우 실제 DB 를 조회하는 것이 아닌 DB 를 조회하는 것처럼 List 또는 Map 등의 객체를 사용하는 것이다. 특징으로는 외부 객체의 의존성을 단순화 하거나 복잡한 로직을 단순화 한것이라고 볼 수 있다.   class Fake implements User {    public void getUser (String userName) {     List&lt;String&gt; userList = new ArrayList&lt;String&gt;();     userList.add(\"a\");     userList.add(\"b\");     userList.add(\"c\");      String targetUser = null;      for (String user : userList)  {       if (user.equals(userName))  {         targetUser = user;       }     }      return targetUser;    } }  class FakeTest {    @Test   public void getUser_페이크객체() {     User user = new Fake();      assertThat(user.getUser(\"a\"), is(\"b\"));    }  }   (4) Test Spy   특정 객체 또는 메소드의 정상 호출 여부를 확인 테스트 더블 하위 분류 전체에 사용이 가능하다. 예를들어 Fake 객체에서 만든 메소드가 몇번 불렸나를 확인하기 위해 Fake 객체에 메소드 호출 횟수를 반환하는 스파이 메소드를 만들곤 한다. 여기서 실제 객체에는 없는 스파이 메소드를 만들어도 되냐하는 의문이 드는데 어짜피 가짜 객체이기 때문에 상관 없다고 한다.   테스트 스파이는 굳이 애써서 만들 필요는 없으며 요즘 있는 Mock 라이브러리에서는 왠만해서는 지원을 한다고 한다.   (5) Mock Object      들어가기 앞서 “상태 기반 테스트” 와 “행위 기반 테스트” 에 대해 알아보도록 하자. 상태 기반 테스트의 경우 말 그대로 객체의 그 당시의 상태를 가지고 테스트하는 것이다. 만약 Setter 로 상태를 변경했으면 Getter 로 변경된 상태를 테스트해야한다. 반면 행위 기반 테스트의 경우 A 메소드에서 B 메소드를 호출하는 행위에 집중해서 테스트한다.    일반적인 테스트 더블은 상태를 기반으로 테스트 하지만 Mock 의 경우 행위를 기반으로 테스트 한다.   이렇게 Mock 객체에 대한 간략한 내용과 Test Double 에 대해서 알아보았다. 다음에는 Mokito 를 활용한 예제를 알아보도록 하자.  ","categories": ["posts"],
364        "tags": ["tdd","java","junit"],
365        "url": "https://subji.github.io/posts/2020/06/29/tdd-java-4",
366        "teaser":null},{
367        "title": "TDD(Test Driven Development) JUnit 5 - Mockito (5)",
368        "excerpt":"아래 내용은 ‘테스트 주도 개발: TDD 실천법과 도구’(https://repo.yona.io/doortts/blog/issue/1)를 ë³´ê³  주요 내용을 개인적으로 정리하여 작성했습니다.   Mockito ?   특징 및 동기 에서 ë³´ë©´ JMock, EasyMock 보다 가볍고 직관적인 라이브러리라고 설명하고 있다. 또한 기존의 예상-실행-검증 이 아닌 방식으로 행위 보다 상태에 조금더 집중했다고 되어있다.   문법은 EasyMock ê³¼ 비슷하며 따라서 EasyMock 에서의 리팩토링에 í
368° 어려움이 없다. 테스트 스파이 또한 구현되어 있다. 아래 몇가지 추가적인 특징이 있다.      인터페이스 만큼 견고한 Mock 객체   간단한 어노테이션 지원 ex) @Mock   검증 에러에 대한 깔끔한 출력 지원   유연한 검증 가능 ex) 원하는 것만 검증하거나, 특정 검증을 제외 시키는 것   횟수 및 최소 한번 실행에 대한 검증 제공   인수를 이용한 stub ê³¼ 유연한 검증 제공 ex) 리플렉션을 사용한 anyObject(), anyString(), refEq() 등   사용자 정의 Matcher 함수 또는 기존의 Hamcrest Matcher 사용 가능       Mockito 설치   Maven  &lt;dependency&gt;   &lt;groupId&gt;org.mockito&lt;/groupId&gt;   &lt;artifactId&gt;mockito-all&lt;/artifactId&gt;   &lt;version&gt;1.10.19&lt;/version&gt;   &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt;   Gradle  testCompile group: 'org.mockito', name: 'mockito-all', version: '1.10.19'       Mockito 예제   Mockito 는 위에서 말했듯이 예상-실행-검증의 간단한 절차를 가진다. 그러면 이런 과정을 어떻게 작성해야 할까?   첫번째로 Mock 객체를 생성한다.   List mockedList = mock(List.class);   ê·¸ 다음으로는 stub 을 정의한다. 대부분 when-then 으로 이루어져있다.   when(mockedList.get(0)).thenReturn(\"item\");   마지막으로 실행에 대한 검증을 한다. 검증에는 호출횟수를 나타내는 함수(times(), atLeastOnce(), 등) 가 있다 또한 두번쨰 줄의 검증처럼 add 함수에 인자로 Matcher 함수를 넘겨줄 수 있다.   verify(mockedList, times(1)).add(\"item\");  verify(mockedList).add(any())   그외에도 Mock 객체의 호출 순서를 지정하는 방법, void 메소드의 검증방법 등이 참고했던 페이지에 나와있다.       Mockito 예제 - spy   실제 객체를 Mock 객체로 사용할 수 있게 도와주는 기능이다. final 메소드는 spy 함수로 만들 수 없다.   마치며   Mockito 는 위에 나온 기능 외에도 BDD 지원, SMART_NULL 지원등의 다양한 기능들이 있다. 차근차근 기본적인 것부터 추가하며 여러 기능을 사용해봐야 겠다. 하지만 무엇보다 중요한건 테스트는 최대한 엄격하고 세세하지만 간단하게 작성해야겠다.  ","categories": ["posts"],
369        "tags": ["tdd","java","junit","mockito"],
370        "url": "https://subji.github.io/posts/2020/06/30/tdd-java-5",
371        "teaser":null},{
372        "title": "역인덱스",
373        "excerpt":"역인덱스   하나의 row 에 긴 텍스트가 있고 이런 row 들이 수만, 수십만개로 이루어진 테이블에서 “LIKE %…%” 같은 구문을 이용해서 검색을 한다고 해보자.   결과는 어떻게 될까? LIKE 절은 인덱스를 타지 않는다고 한다. 그러면 검색 속도를 올리고 싶을 떄는 어떻게 해야할까?   많은 방법이 있겠지만 여기서 소개할 방법은 역인덱스라고 불리는 방법이다. 역인덱스라는 말이 ì°¸ 처음 듣기에 어려워 보이는데 개념은 간단하다. 예를들어 “안녕 나는 누구누구야” 라는 문장이 있다고 하자. 보통의 테이블은 “안녕 나는 누구누구야” 라는 내용이 담긴 컬럼 자체에 인덱스를 설정한다. 하지만 역인덱스는 해당 문장을 분해해서 각각 분해된 단어 또는 문자들에 인덱스를 설정하는 것이다.   위에서 예를든 문장 “안녕 나는 누구누구야” 의 경우 2개 단위로 문자를 자른다고 하면 역인덱스 테이블은 다음과 같다.   CREATE TABLE T_INVERSE_INDEX (   original_seq NOT NULL, # 원래 문장의 시퀀스 및 인덱스 정보   separated_word NOT NULL # 분리된 문자 )   역인덱스 테이블에서 중요한건 separated_word column 이다. 분리된 문자에 인덱스를 거는 것이다.! 이렇게 하면 “LIKE ‘%누구%’” 라고 검색을 할때 원본 테이블이 아닌 역인덱스 테이블에서 인덱싱된 separated_word column 을 조회해서 속도를 올릴수 있다.       장/단점   장점은 뚜렷하게 증가하는 속도일 것이다. 하지만 단점도 있다. 역인덱싱을 위한 테이블을 따로 추가해줘야 하는 것과 텍스트가 1000개의 단ì
373–´ë¡œ 이루어져 있고 이를 2개씩 자른다고하면 엄청난 수의 row 가 생길것이며, 그로인해 역인덱스 테이블의 용량은 엄청나게 커질 것이다. 이외에도 여러 장/단점이 있을 수 있다.       FULLTEXT INDEX   검색속도를 올리고자 하려면 FULLTEXT INDEX 를 사용하는 방법도 있다고 한다.  ","categories": ["posts"],
374        "tags": ["db","sql"],
375        "url": "https://subji.github.io/posts/2020/07/03/query-inverse-index",
376        "teaser":null},{
377        "title": "SQL Pivot (여러개의 Row 를 하나의 Row 의 Column 들로 변경하기)",
378        "excerpt":"Data Pivot   여러개의 로우를 가진 값을 하나의 로우에 여러개의 컬럼으로 바꾸는 예제이다.   주어진 테이블을 다음과 같다.   Table 1  |ID | Name |  |—|—| |1 | Jim |  |2 | Bob |  |3 | John |   Table 2  |ID | key | value | |—|—|—| |1 | address | “X Street” | |1 | city | “NY” |  |1 | region | “NY” |  |1 | country | “USA” |  |1 | postal_code |  |  |1 | phone | “123456789” |   위 두개의 테이블을 다음과 같이 보여주고 싶다.   Result | ID |  Name | address | city | region | country | postal_code | phone | |—|—|—|—|—|—|—|—| |1 | Jim | X Street | NY | NY | USA | NULL | 123456789 | |2 | Bob | NULL | NULL | NULL | NULL | NULL | NULL | |3 | John | NULL | NULL | NULL | NULL | NULL | NULL |   방법은 찾아보니 3가지 정도가 있다. 첫번째는 GROUP BY 와 MAX 함수를 이용한 방법. 단, 이방법은 같은 컬럼에 값이 여러개일 경우 오류를 발생할 수 있고 또한 너무 많은 로우를 변환하고자 할때에는 코드가 길어질 수 있다.   SELECT t1.id,        t1.name,        MAX(CASE WHEN t2.`key` = 'address' THEN t2.value END) address,        MAX(CASE WHEN t2.`key` = 'city' THEN t2.value END) city,        MAX(CASE WHEN t2.`key` = 'region' THEN t2.value END) region,        MAX(CASE WHEN t2.`key` = 'country' THEN t2.value END) country,        MAX(CASE WHEN t2.`key` = 'postal_code' THEN t2.value END) postal_code,        MAX(CASE WHEN t2.`key` = 'phone' THEN t2.value END) phone   FROM table1 t1 LEFT JOIN table2 t2 ON t1.id = t2.id  GROUP BY t1.id, t1.name   두번째 방법은 첫번째 방법의 단점 중 하나인 값이 여러개를 가지는 컬럼이 존재할 경우를 대비한 방법이다. 다만 이방법도 역시 로우가 많아질수록 코드가 길어지고 관리가 힘들다는 단점이 있다.   SELECT t1.id,        t1.name,        t2a.value address,        t2c.value city,        t2r.value region,        t2y.value country,        t2pc.value postal_code,        t2p.value phone   FROM table1 t1      LEFT JOIN table2 t2a ON t1.id = t2a.id AND t2a.`key` = 'address'     LEFT JOIN table2 t2c ON t1.id = t2c.id AND t2c.`key` = 'city'      LEFT JOIN table2 t2r ON t1.id = t2r.id AND t2c.`key` = 'region'      LEFT JOIN table2 t2y ON t1.id = t2y.id AND t2c.`key` = 'country'      LEFT JOIN table2 t2pc ON t1.id = t2pc.id AND t2pc.`key` = 'postal_code'      LEFT JOIN table2 t2p ON t1.id = t2p.id AND t2p.`key` = 'phone';   마지막 방법은 위의 단점들을 모두 해결할 수 있는 방법이다. 단, 여기에는 프로시져 또는 함수를 알아야한다는 전제조건이 필요하다.   SET @sql = NULL; SELECT   GROUP_CONCAT(DISTINCT     CONCAT(       'max(case when t2.`key` = ''',       `key`,       ''' then t2.value end) AS `',       `key`, '`'     )   ) INTO @sql from Table2;  SET @sql      = CONCAT('SELECT t1.id, t1.name, ', @sql, '                from table1 t1               left join table2 t2                 on t1.id = t2.id               group by t1.id, t1.name;');  PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;   그러면 결과는 다음과 같이 나온다.   Query Result | ID | NAME |  ADDRESS |   CITY | REGION | COUNTRY | POSTAL_CODE |     PHONE | |—-|——|———-|——–|——–|———|————-|———–| |  1 |  Jim | X Street |     NY | (null) |  (null) |      (null) | 123456789 | |  2 |  Bob |   (null) | (null) | (null) |  (null) |      (null) |    (null) | |  3 | John |   (null) | (null) | (null) |  (null) |      (null) |    (null) |   참고: MySQL Pivot  ","categories": ["posts"],
379        "tags": ["sql"],
380        "url": "https://subji.github.io/posts/2020/07/03/sql-pivot",
381        "teaser":null},{
382        "title": "Git Revert & Reset, 그리고 Working Directory 되돌리기",
383        "excerpt":"Intro   Git push 할때 실수로 올라가지 말아야할 것이 올라갔을 때가 있다. 이때 잘못된 내용을 올린 푸시 이전으로 돌아가고 싶을때 사용하는 것이 Reset &amp; Revert 명령어이다. 여기에 더불어 아직 staging 영역에 추가 되지 않은 내용을 다시 되돌리는 방법도 알아보자.   Checkout   git checkout {branchName} 으로 자주 사용되는 명령어이다. 하지만 이 명령어로 staging 영역에 올라가지 않는 즉, 커밋은 했지만 변경사항이 추가되지 않았을때 (git add . 사용 전) 해당 변경사항을 취소하고 이전으로 돌아갈 수 있도록 도와주기도 한다.   예제를 보자   먼저 기존 내용을 변경하고 git add 이전에 commit 을 먼저하자.   git commit -m 'Checkout 되돌리기 테스트'         이 상태에서 변경사항 이전으로 돌아가고 싶으면 다음 명령어를 입력하자.   git checkout -- {fileName}
383    위 명령어는 특정 파일을 변경사항 이전으로 돌리는 것이고, 현재 Working Directory 의 모든 파일 또는 특정 Directory 만 돌리고 싶다면 다음처럼 작성하자.   # Working Directory 내 모든파일 git checkout .  # 특정 Directory 이름 git checkout {DirectoryName}   위 방법은 Staging 영역으로 올라가지 않은 즉, 변경사항이 커밋에 추가되지 않는 건에 대해서만 가능하다. 그렇다면 git add 를 하고 Staging 영역으로 올라간 내용은 어떻게 취소할 수 있을까? 방법은 다음과 같다. 다음에 볼 reset 명령어를 사용할 수 있다.   git reset {FileName}   이 명령어를 사용하면 변경사항은 유지한채 staging 영역에 올라간 커밋을 취소할 수 있다.       Reset   특정 커밋으로 되돌아가는 명령어. 단, 되돌아간 커밋 이후의 커밋들은 히스토리에서 삭제된다.   –hard 옵션     특정 Commit 으로 되돌리는 데 변경된 사항도 모두 삭제한다.     git reset --hard {Commit id}           –soft 옵션     특정 Commit 으로 되돌리는 데 커밋 이력은 삭제하고, 변경사항들에 대해서는 git add 에 올려놓는다.     git reset --soft {Commit id}           –mixed 옵션     특정 Commit 으로 되돌리는 데 커밋 이력은 삭제되고, 변경사항은 Commit 상태로 변경한다.     git reset --mixed {Commit id}               Revert   특정 커밋으로 되돌아가는 것은 Reset 과 같다. 하지만 되돌아간 커밋 이후의 커밋들은 삭제되지 않고 남아있다.   git revert {Commit id}   Reset 과 비슷하게 특정 커밋으로 되돌아 가지만 커밋 이력을 삭제하지 않고 Revert 라는 새로운 커밋 이력을 남긴다. Reset 의 경우 커밋이력을 삭제하기 때문에 충돌이 발생하지 않지만 Revert 의 경우 커밋 이력이 남기 때문에 충돌이 발생 할 수 있다. 이 부분에 대해서는 직접 수정을 해줘야한다.       Cancel merge   개발 브랜치 또는 기타 브랜치들을 master 브랜치로 합치기 위해 merge 를 사용한다. 이때 합쳐지만 안될 내용이 합쳐질떄가 있는데, 이것을 취소할떄도 Revert &amp; Reset 을 사용하면 된다.   Reset 을 사용하는 경우 merge 할 때 최신 커밋에 포인터를 지정한다. (ORIG_HEAD) 그러므로 병합을 취소할 때는 포인터가 가리키는 최신 커밋으로 이동하면 된다. 최신 커밋이란 merge 를 수행할 때, git 에서 합쳐지기 전 마지막 커밋 Id 를 가리킨다.   git reset -merge ORIG_HEAD   위에 처럼 명령어를 사용하면 master 브랜치의 최신 커밋으로 이동하며 merge 가 취소된다. Revert 를 사용하는 방법은 조금 다르지만 간단한 옵션하나만 추가 해주면된다. 단, 여기서 주의할 점은 Rebase 는 git 이력에 merge 에 대한 내용을 남기지 않으므로 되돌릴 수 없다.   Revert 는 Reset 처럼 마지막 커밋이 merge 일때만 사용할 수 있는 것이 아닌 특정 Merge 에 대해서도 취소할 수 있다. 다음 옵션을 추가하여 사용 할 수 있다.   git revert -mainline {1/2} {To Cancel Merge Commit ID}   mainline 은 merge 취소 후 어느쪽으로 갈것이냐? 즉, master 가 1, 자식 브랜치가 2라고 할때 -mainline 뒤에 1 또는 2 옵션으로 돌아가고자 하는 쪽으로 돌아갈 수 있게 해준다. 숫자는 git 그래프 그림에서 왼쪽부터 1로 매긴다.       End   reset 은 원격 저장소를 관리할 수 없다. 왜냐하면 커밋이력이 삭제되기 때문이다. 그래서 원격 저장소에 푸시 되기 전에 사용해야한다. revert 는 커밋이력이 존재하기 때문에 푸시가 된 상태에서도 사용할 수 있다.  ","categories": ["posts"],
384        "tags": ["git"],
385        "url": "https://subji.github.io/posts/2020/07/06/gitrevertresetandturnbackcheckout",
386        "teaser":null},{
387        "title": "MSA 인증 서비스 Keycloak 소개",
388        "excerpt":"Keycloak 이란?   Redhat 에서 개발한 서비스를 대상으로 하는 인증 &amp; 권한 부여 오픈소스이다. 기반은 JBoss 와 WildFly 로 구성되어 있다. SSO, 소셜 로그인, LDAP, RDBMS 등의 User Federation 도 지원한다. ê·¸ 외에도 adapter, SPI, REST API 등을 제공한다.   기존에 OAuth 를 Spring Security 로 사용하려 했지만 oauth provider 쪽에서 예제가 많이 없어서 애를 먹었다. 그러다가 많은 예제들에서 나온것이 Keycloak 하고 Okta. 둘다 비슷한 인증 &amp; 권한 부여 솔루션이지만 keycloak 이 더 많은 기능을 제공한다는 점에서 Keycloak 을 사용해 보려고 한다.       keycloak 설치 &amp; 실행   Keycloak 다운로드 에 접속해서 ZIP or TAR.GZ 파일ì
388„ 다운로드 받는다.                        ZIP       TAR.GZ 를 받았으면 압축을 해제한다.           $ unzip keycloak-10.0.2.zip   $ tar -zxvf keycloak-10.0.2.tar.gz   압축 해제가 끝나면 “bin” 디렉토리로 이동해서 “standalone.sh” 또는 “standalone.bat” 을 실행한다.   # For Linux/Unix $ cd bin $ sh standalone.sh # or ./standalone.sh  # For Windows $ \\bin\\standalone.bat   이러면 실행은 끝났다. 아주 쉽다. 이제 브라우저를 열고 http://localhost:8080/auth 으로 접속하자.         관리자 콘솔 화면이 나온다. localhost 로 실행했을 경우 보안적인 이유로 인해 계정이 없는 상태로 실행된다. 만약 계정이 추가된 상태로 실행하고 싶으면 bin/add-user.sh or bin/add-user.bat 을 실행하자.         위 화면에서 “Administration Console &gt;” 을 선택하면 로그인 화면이 나온다. 그러면 이전에 가입한 관리자 이름과 비밀번호를 입력하자.   관리자 콘솔의 관리자 이름과 비밀번호를 입력하고 로그인을 하면 keycloak 관리자 화면이 나온다.         여기까지 오면 keycloak 실행까지는 끝난것이다.       Keycloak 관련 용어      OIDC : OAuth 가 권한 부여만 다루는 것이라면 OIDC 는 OAuth 를 포함하여 인증과 권한부여를 모두 포함한 것이다. SSO 의 구현을 위한 수단으로 사용된다.   Realm : 인증, 권한 부여가 적용되는 범위를 나타내는 단위이다. SSO 를 적용한다고 했을때 해당 SSO 가 적용되는 범위는 Realm 단위이다.   Client : 인증, 권한 부여 행위를 대행하도록 맡길 어플리케이션을 나타내는 단위이다. ê·¸ 단위는 웹사이트 혹은 REST API 를 제공하는 서비스도 될 수 있다. 하나의 Realm 에 n개의 Client 를 생성, 관리할 수 있다.   User : Client 에 인증을 요청할 사용자를 나타낸다. 하나의 Realm 에는 Realm 에 종속된 n개의 User 를 생성하고 관리할 수 있다. 기본적으로 User 는 Username, Email, FirstName, LastName 으로 구성되어 있지만 Custom User Attribute 를 사용하면 사용자가 원하는 속성을 추가할 수 있다.   Role : User 에게 부여할 권한 내용을 나타낸다. 여기에는 Keycloak 의 REST API 를 사용할 권한을 부여할 수 있고 사용자가 정의한 권한을 부여할 수도 있다.   참고 : https://jsonobject.tistory.com/445       Keycloak 설정   MSA 에서 인증, 권한 부여 서비스로 keycloak 을 사용하기 위해서는 다음 설정들을 해줘야 한다.      Realm 생성   Client 생성   User 생성   Realm 은 초기에는 Master Realm 만 존재한다. 하지만 우리는 Master 가 아닌 MSA 에서 사용할 렐름이 필요하기 때문에 생성해준다.   keycloak 관리자 화면에서 왼쪽 위에 Master 를 누르고 add realm 버튼을 누르자.         렐름을 생성하면 바로 새로 생성된 렐름의 관리 페이지로 이동한다.   Realm Settings 에서 설정할 수 있는건 일반설정, 로그인 화면 설정, 키 암호화 설정, 이메일 서비스 설정, 로그인/회원가입 등의 테마 설정, 캐시 설정, 토큰 설정등이 있다. 지금 당장은 바꿔야 할 부분은 없고 필요시 공식문서를 참고하자.   이제 client 를 생성하자.   마찬가지로 왼쪽에서 “Clients” 탭을 선택하자. 그러면 기본적인 클라이언트 목록이 나오는데 건드릴 필요는 없고 우측 위에 보이는 “Create” 버튼을 눌러 Client 를 생성하자.         여기 역시 필요시 공식문서를 참고하여 설정하면된다. 지금 당장 필요한 옵션은 2가지가 있다. 첫번째는 Client-secret 을 생성하기 위한 Access Type 탭. 해당 값을 public 에서 confidential 으로 변경한다. 그리고 나중에 User 관련 REST API 를 사용하기 위해 필요한 Service Accounts Enabled 탭을 활성화 시켜준다.   마지막으로 “*” 처리가 되어있는 Valid Redirect URIs 부분이 있는데 인증 후 리다이렉트 될 주소를 입력해주면된다. 나의 경우는 gateway 주소로 입력했다.   전부 완료됬으면 save 버튼을 눌러 저장하자.   client 설정이 끝났다. 이제 client-secret 이 잘 생성되었는지 Clients -&gt; Credentials 탭에서 확인해보자.         마지막으로 인증할때 필요한 계정을 하나 추가해주자.         “Users” 탭을 클릭하면 유저 목록이 나오는데 당연히 하나도 없다. 그래서 오른쪽에 “Add user” 를 클릭해서 유저를 추가하자.         위에서 말했듯이 기본적으로 Username, Email, First Name, Last Name 이 있다. 그런데 비밀번호 입력칸은..? 뭐 일단 Username 만 별표가 있으니 그것만 입력하고 저장하자.   저장이 완료되면 페이지가 새로고침되며 “Credentials” 탭이 생긴것을 ë³¼ 수 있다. 탭을 클릭하면 비밀번호를 입력하는 곳이 나온다. 여기서 유저의 비밀번호를 추가해주자.         이제 Gateway 를 위한 keycloak 설정을 끝이 났다.       Gateway 설정   위에서 설정한 keycloak 을 이용하기 위해 Gateway 에서도 설정을 해주자.   config 서버에 있는 gateway 설정파일에 다음과 같이 코드를 입력하자.   spring:   security:     oauth2:       client:         provider:           keycloak:             issuer-uri: http://{keycloak address}/auth/realms/{realm name}             user-name-attribute: preferred_username         registration:            keycloak:             client-id: {client name}             client-secret: {client secret}     gateway:       default-filters:         - TokenRelay=   그리고 Gateway 프로젝트에서 src/main/java/com/{groupId}/{artifactId}/config 밑에 클래스파일을 하나 추가하자.   @Order(99) @Configuration @EnableWebFluxSecurity public class GatewayWebSecurityConfiguration {      @Bean   public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ReactiveClientRegistrationRepository clientRegistrationRepository) {     // Authenticate through configured keycloak SSO Provider     http.oauth2Login();
388     // Also logout at the keycloak SSO Connect Provider     http.logout(logout -&gt; logout.logoutSuccessHandler(       new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository)     ));     // Require authentication for all requests     http.authorizeExchange().anyExchange().authenticated();     // Allow showing /home within a frame     http.headers().frameOptions().mode(Mode.SAMEORIGIN);     // Disable CSRF in the gateway to prevent conflicts with proxied server CSRF     http.csrf().disable();      return http.build();   }    }  지금 개발중인 MSA 프로젝트에서 Gateway 는 Spring Cloud Gateway 를 사용하였다. 기본적으로 비동기적으로 처리되기 때문에 WebFlux 를 사용하였다.   모두 작성 후 오류가 날 경우에는 다음 두개의 Dependency 를 추가하자.      spring-boot-starter-oauth2-client   spring-cloud-starter-security   코드 작성이 완료되었으면 config, discovery, gateway 서비스를 실행해보자. gateway 로 접속하면 이전에 보았던 keycloak 로그인 화면이 보일 것이다.   일단 Gateway 까지는 keycloak 이 잘 적용된 것이다.       Service 설정   이제 마지막으로 REST API 서비스등에 적용할 설정을 보자.   config 서버에서 service 의 설정파일에 다음과 같이 작성하자.   spring:   security:     oauth2:       resourceserver:         jwt:           issuer-uri: http://{keycloak address}/auth/realms/{realm name}   Dependency 는 다음과 같다.      spring-security-oauth2-jose   spring-security-oauth2-resource-server   spring-cloud-starter-oauth2   spring-cloud-starter-security   이제 시큐리티 설정 클래스 파일을 작성하자   @Configuration @EnableWebSecurity public class SecurityConfiguration extends WebSecurityConfigurerAdapter {    @Override   protected void configure(HttpSecurity http) throws Exception {     // Validate tokens through configured OpenID Provider     http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter());     // Require authentication for all requests     http.authorizeRequests().anyRequest().authenticated();     // Allow showing pages within a frame     http.headers().frameOptions().sameOrigin();   }    private JwtAuthenticationConverter jwtAuthenticationConverter() {     JwtAuthenticationConverter converter = new JwtAuthenticationConverter();     // Convert realm_access.roles claims to granted authorities, for use in access decisions     converter.setJwtGrantedAuthoritiesConverter(new KeycloakRealmRoleConverter());     return converter;   }    class KeycloakRealmRoleConverter implements Converter&lt;Jwt, Collection&lt;GrantedAuthority&gt;&gt; {     @Override     public Collection&lt;GrantedAuthority&gt; convert(Jwt jwt) {       final Map&lt;String, Object&gt; realmAccess = (Map&lt;String, Object&gt;) jwt.getClaims().get(\"realm_access\");       return ((List&lt;String&gt;) realmAccess.get(\"roles\")).stream()         .map(roleName -&gt; \"ROLE_\" + roleName)         .map(SimpleGrantedAuthority::new)         .collect(Collectors.toList());     }   } }   그리고 테스트를 위해 컨트롤러에 코드를 하나 작성하자.   @RestController @RequestMapping(value = \"/\") public class UserController {  \tprivate final UserService userService;  \tpublic UserController(UserService userService)\t{ \t\tthis.userService = userService; \t}  \t@PreAuthorize(value = \"#oauth2.hasScope('email')\") \t@GetMapping(value = \"users/{userId}\") \tpublic UserInfoDTO getUserInfo(@PathVariable String userId) throws Exception {  \t\tUserInfoDTO userInfoDTO = new UserInfoDTO(); \t\tuserInfoDTO.setId(userId);  \t\treturn userService.getUserInfo(userInfoDTO); \t} \t }   위 내용은 OAuth, JWT 로 인증을 처리한다는 것이며 권한은 email 일때만 Users API 를 조회할 수 있다.   이제 service 까지 실행해서 컨트롤러로 요청을 해보자. 로그인이 안되었다면 로그인 후 값이 출력될 것이다. 로그인 유저와 비밀번호는 이전에 keycloak 관리 페이지에서 만든 Username 과 Password 이다.       마치며   현재 keycloak 은 h2 데이터베이스를 사용해서 파일로 사용자를 관리하고 있다. 하지만 나는 내가 만들어놓은 사용자 테이블을 사용해서 인증을 하고 싶다. 다음에는 사용자 외부 테이블을 keycloak 에 연결하는 것을 알아보자.  ","categories": ["posts"],
389        "tags": ["msa","keycloak"],
390        "url": "https://subji.github.io/posts/2020/07/08/keycloak1",
391        "teaser":null},{
392        "title": "MSA 인증 서비스 Keycloak 설치 및 설정",
393        "excerpt":"Keycloak 서버 요구사항      Java 가 실행될 수 있는 OS   Java 8 JDK 이상   zip or gzip and tar 명령어 실행 가능   최소 512M 램   최소 1G 이상의 디스크 공간   공유 외부 DB (MySQL, Oracle, PostgreSQL, …) 을 실행할 수 있고 클러스터를 구성할 수 있는 환경   ê·¸ 밖에 네트워크 및 리눅스의 추가 적인 요구사항은 공식문서를 참고하자.       keycloak 서버 디렉토리 구성            bin/ : 서버를 부팅하거나 서버에서 다른 관리 작업을 수행하는 다양한 스크립트가 포함되어 있다.            domain/ : 다음에 설명한 모드 중 도메인 모드 에서 keycloak 을 실행할 때 구성 파일과 Working Directory 가 포함된다.            modules/ : 서버에서 사용하는 Java 라이브러리를 포함한 디렉토리. 나중에 설명하겠지만 외부 RDBMS 를 연결할때 필요한 jdbc driver 도 여기에 위치하게 된다.            standalone/ : 독립 실행 형 모드 에서 keycloak 을 실행할 때 구성 파일과 Working Directory 가 포함된다.            standalone/deployments/ : keycloak 에 대한 extension 들을 추가하는 공간.            themes/ : 로그인, 회원가입 등의 UI 에 대한 파일 포함. UI 는 Freemarker 로 되어 있다.       참고 : 공식 문서 - 디렉토리 구조       Keycloak 실행 모드   keycloak 은 기본적으로 WildFly Application Server 에서 구성되었고 아래 내용은 keycloak 의 배포에 대한 기본적인 내용만 설명한다. 자세한 내용은 WildFly 18 Document 를 참고하자.           Standalone Mode (독립 실행 형 모드) : 하나의 keycloak 서버 인스턴스 만 실행하려는 경우에 유용하다. keycloak 에서 사용되는 모든 캐시는 클러스터로 구성되어 있지 않기 때문에 분배되지 않으며 로컬 전용이다. 프로젝트를 운영에서 사용하게 될 경우 독립 실행 형 모드는 단일 인스턴스이기 때문에 오류 발생 시 모든 서비스가 인증 &amp; 권한 부여 서비스를 사용할 수 없게 되어 치명적인 장애가 발생한다. 따라서 해당 모드는 테스트 시에 사용하도록 권유한다.   설정 파일은 standalone/configuration/standalone.xml 이다.       Standalone Mode 실행 방법       $ bin/standalone.sh --server-config=standalone.xml # bin\\standalone.bat                Standalone Clustered Mode (독립형 클러스터 모드) : 독립 실행 형 모드를 클러스터로 구성하는 모드이다. 이 모드는 각각의 keycloak 인스턴스의 구성을 변경해줘야 한다는 점에서 시간이 많이 소요되며, 오류를 발생할 수 도 있다. 또한 반드시 공유 DB 연결을 구성하고 로드 밸런서를 구성해야 한다.   설정 파일은 standalone/configuration/standalone.ha.xml 이다.       Standalone Clustered Mode 실행 방법       $ bin/standalone.sh --server-config=standalone-ha.xml # bin\\standalone.bat           Domain Clustered Mode (도메인 클러스터 모드) : 독립형 클러스터 모드의 단점을 해결할 수 있는 방법이다. 중앙에서 keycloak 인스턴스를 관리하는 모드이다. 초기 설정이 복잡하고 시간이 많이 소요될 수 있지만 추후 관리적인 측면에서는 앞서말한 독립형 클러스터 모드보다 낫다.   도메인 클러스터 모드는 다음 내용들로 구성되어있다.            Domain Controller : 클러스터의 각 노드에 대한 일반 구성을 저장, 관리 및 게시하는 프로세스. 이 프로세스는 클러스터 노드가 구성을 얻는 중심점 역할을 한다.       Host Controller : 특정 물리 서버에서 여러개의 서버 인스턴스들을 관리한다. 도메인 컨트롤러는 여러 컴퓨터에 설치된 호스트 컨트롤러와 상호 작용하여 클러스터를 관리할 수 있다. 또한 하나의 서버에서 호스트, 도메인같이 여러 프로세스가 실행되는걸 방지하고 싶을 때는 도메인 컨트롤러로 호스트 컨트롤러 역할을 줄 수 있다.       Domain Profile : 클러스터에 속한 여러 노드들의 설정들이다. 도메인 컨트롤러에서 다른 노드들의 프로필을 정의할 수 있다.       Server Group : 말 그대로 서버 그룹이다. 하나의 서버 그룹에 도메인 프로파일을 할당 할 수 있으며, 그룹 내의 모든 서비스는 같은 도메인 프로파일을 사용한다.           ì
393„¤ì • 파일은 domain/configuration/domain.xml 이다. 도메인 클러스터 모드의 자세한 내용은 다음을 공식문서에 나와있다.       Cross-Datacenter Replication Mode : 내용도 많이 길고 해서 간단하게 소개 정도만 하겠다. 자세한 내용은 공식문서 를 참고하자. 교차 데이터센터 복제 모드는 아직 완전히 지원되지 않는 모드이다. 위에 말한 3가지 모드는 하나의 공유 데이터베이스를 사용했지만 여기서는 여러개의 데이터베이스를 서로의 클러스터가 사용하는 모드이다.   참고 : 공식 문서 - 실행 모드       Keycloak RDBMS 설정   이제 keycloak 에서 h2 데이터베이스를 사용하는 것이 아니라 사용자의 DB 의 데이터를 사용하여 로그인하는 설정을 추가해보자.           jdbc driver 설치 (여기서는 MariaDB) : 다운로드 받은 driver 파일을 modules 디렉토리 아래 위치시킨다. modules 디렉토리는 keycloak 의 클래스 경로이며, Dependency 를 정의한 곳이다. jdbc driver 를 다음과 같이 경로를 생성하여 위치시키자.       modules/system/layers/keycloak/org/{database name}/main/                       위에서 말한 경로를 ë³´ë©´ module.xml 파일이 보이는 데 여기에 내가 추가한 driver 를 keycloak 의 모듈로 사용하겠다라고 설정해준다.         &lt;?xml version=\"1.0\" ?&gt;    &lt;!-- name 에 org.{database name} 을 적어준다 --&gt;    &lt;module xmlns=\"urn:jboss:module:1.3\" name=\"org.mariadb\"&gt;         &lt;resources&gt;            &lt;!-- 실제 driver jar 파일의 이름 --&gt;            &lt;resource-root path=\"mariadb-java-client-2.6.1.jar\"/&gt;        &lt;/resources&gt;         &lt;dependencies&gt;            &lt;module name=\"javax.api\"/&gt;            &lt;module name=\"javax.transaction.api\"/&gt;        &lt;/dependencies&gt;     &lt;/module&gt;           Driver 선언 및 로드 : 이제 추가한 jdbc driver 모듈을 서버에 로드하기 위한 설정을 추가하자.   standalone.xml / standalone-ha.xml / domain.xml 설정 파일      &lt;subsystem xmlns=\"urn:jboss:domain:datasources:5.0\"&gt;    &lt;datasources&gt;     &lt;drivers&gt;        &lt;driver name=\"h2\" module=\"com.h2database.h2\"&gt;            &lt;xa-datasource-class&gt;org.h2.jdbcx.JdbcDataSource&lt;/xa-datasource-class&gt;        &lt;/driver&gt;        &lt;!-- 여기서 추가한 jdbc driver 작성 --&gt;        &lt;!-- driver 이름을 작성, module 은 1번에서 name 에 설정한 이름 --&gt;        &lt;driver name=\"mariadb\" module=\"org.mariadb\"&gt;            &lt;xa-datasource-class&gt;org.mariadb.jdbc.MariaDbDataSource&lt;/xa-datasource-class&gt;        &lt;/driver&gt;     &lt;/drivers&gt;   &lt;/datasources&gt;  &lt;/subsystem&gt;                keycloak 의 데이터 소스를 변경 : 기본 H2 를 사용하는 데이터 소스를 mariadb 를 사용하는 데이터 소스로 변경하자. 같은 파일에서 다음 부분을 수정하면된다. 여기서 주의할 점이 하나 있는데 MySQL, MariaDB 의 경우 기본적으로 utf8mb4 로 되어 있는데, 반드시 utf8 로 변경 후 다음을 작성하자. how to change utf8mb4 to utf8        &lt;subsystem xmlns=\"urn:jboss:domain:datasources:5.0\"&gt;    &lt;datasources&gt;      &lt;datasource jndi-name=\"java:jboss/datasources/KeycloakDS\" pool-name=\"KeycloakDS\" enabled=\"true\" use-java-context=\"true\"&gt;          &lt;!-- 사용자 DB 의 주소 (인코딩 UTF8 로 변경) --&gt;          &lt;connection-url&gt;jdbc:mariadb://localhost/keycloak?characterEncoding=UTF-8&lt;/connection-url&gt;          &lt;!-- 데이터 소스 설정의 이름 --&gt;          &lt;driver&gt;mariadb&lt;/driver&gt;          &lt;pool&gt;              &lt;max-pool-size&gt;20&lt;/max-pool-size&gt;          &lt;/pool&gt;          &lt;security&gt;              &lt;user-name&gt;{username}&lt;/user-name&gt;              &lt;password&gt;{password&lt;/password&gt;          &lt;/security&gt;      &lt;/datasource&gt;    &lt;/datasources&gt;  &lt;/subsystem&gt;
393           서버 데이터베이스 구성 : 이제 keycloak 서버의 데이터 베이스를 구성해보자. 역시 같은 파일에서 다음 부분을 수정하면 된다.      &lt;subsystem xmlns=\"urn:jboss:domain:keycloak-server:1.1\"&gt;    &lt;spi name=\"connectionsJpa\"&gt;    &lt;provider name=\"default\" enabled=\"true\"&gt;        &lt;properties&gt;            &lt;property name=\"dataSource\" value=\"java:jboss/datasources/KeycloakDS\"/&gt;            &lt;property name=\"initializeEmpty\" value=\"false\"/&gt;            &lt;property name=\"migrationStrategy\" value=\"manual\"/&gt;            &lt;property name=\"migrationExport\" value=\"${jboss.home.dir}/keycloak-database-update.sql\"/&gt;        &lt;/properties&gt;    &lt;/provider&gt;    &lt;/spi&gt;  &lt;/subsystem&gt;                 dataSource : 데이터 소스 이름, 3번 설정에서 jndi-name 부분이다.       driver : 초기 설정에서는 안보인다. hibernate 가 자동으로 감지하기 때문에 따로 설정할 필요가 없다.       initializeEmpty : value 가 false 일 경우 수동으로 초기화 한다.       migrationStrategy : update, manual, validate 가 있다. update 는 자동으로 keycloak 관련 테이블을 생성. manual 의 경우 수동으로 생성해줘야 한다. validate 는 단순히 데이터베이스의 스키마가 최신인지만 검사한다.       migrationExport : 데이터베이스 초기화 / 마이그레이션 파일을 작성하는 경로       showSql : Default false, SQL 을 출력할지 여부       formatSql : Default true SQL 포맷화 여부           이렇게 해서 RDBMS 와의 연결이 끝났다. 서버를 실행 한 후 데이터베이스를 확인해보면 여러개의 테이블이 추가된 것을 확인할 수 있다.       마치며   다음은 Keycloak 에서 제공하는 REST API 를 요청하고 받는 방법과 Custom User Attribute 를 사용하기 위한 User Storage SPI 를 활용해보는 걸 알아보자.  ","categories": ["posts"],
394        "tags": ["msa","keycloak"],
395        "url": "https://subji.github.io/posts/2020/07/08/keycloak2",
396        "teaser":null},{
397        "title": "Throttling 과 Debouncing 에 대해서",
398        "excerpt":"들어가며   DOM 이벤트 중 스크롤에 관한 것이 있다. 스크롤을 할때마다 발생하는 이벤트 같은 것이다. 이벤트 안에 로그를 찍어보면 스크롤을 할때마다 수없이 많은 로그가 출력된다. 이렇게 될 경우 이벤트 함수 안에 ajax 호출등과 같은 많은 리소스가 소모되는 동작을 하게 된다면 페이지 성능에 영향을 미치게 된다. 그래서 대안? 이라고 할 수 있는 방법이 Throttling 과 Debouncing 이다.       Throttling   Throttling 은 이벤트 안에 있는 함수의 호출 최대 횟수를 제한하는 것이다. 예를들면 다음과 같다.   스크롤 이벤트를 두고 10초동안 스크롤 동작을 수행하였을 때 함수가 총 1000번 실행된다고 하자. 한 번의 실행이 1ms 라고 했을떄, 10초 동안 총 10000ms 의 시간이 소요된다. 이때 Throttling 을 사용한다. 총 10000ms 에서 100ms 마다 한번씩 실행을 강제하자. 그러면 총 100번의 함수가 실행된다. 10초라는 시간내에 Throttling 기법을 사용해서 1/10의 시간이 줄어든게 된것이다.   Throttling 은 “100ms 초에 한번씩 실행하십시오” 와 같이 총 실행 시간 내에서 함수 호출의 최대 횟수를 제한하는 것이다.       Debouncing   Debouncing 은 Throttling 과 조금은 비슷하다. Debounce 는 “그룹화” 가 가능하다. 이것이 무슨말이냐하면 Debounce 의 개념을 보면 알수 있는데, Debounce 는 N 초가 지난 후 한번 실행되는 방법이다. 즉, N 초가 지나기 전까진 함수가 실행되지 않는다. 즉, 여러번 이벤트가 발생하더라고 N 초가 지난 후의 한번. 단 한번만 실행하는 것이다. 이후 다시 타이머가 재설정되고 또다시 기다렸다가 N 초후에 한번 발생하는 것이다.   이때 Debounce 의 특징 중 하나인 “그룹화” 를 볼 수 있다. N 초 까지 기다린 이벤트 내용들이 그룹지어져 N 초 후에 한번 실행되는 것이다.       Throttling, Debounce 차이   차이는 간단하다. Throttling 의 경우 N 초마다 실행되는 것. Debounce 의 경우 N 초 후 한번 실행되는 것.       마치며   간략하게 여러 글들을 읽은 바탕으로 정리했다. 자세한 내용은 다음을 참고하자.   CSS-Tricks : The difference between throttling and debouncing  ","categories": ["posts"],
399        "tags": ["javascript"],
400        "url": "https://subji.github.io/posts/2020/07/13/throttlinganddebouncingwithjs",
401        "teaser":null},{
402        "title": "MSA 인증 서비스 Keycloak User Storage SPI",
403        "excerpt":"SPI ?   Keycloak 에서 제공하는 모듈과 같은 개념이다. SPI 는 Service Provider Interface 의 줄임말로 Keycloak 에 있는 각 기능들을 개발자가 직접 구현할 수 있게 도와준다. 일반적으로 Provider 파일과 ProviderFactory 파일로 구현한다. 구현 가능한 기능들을 로그인 &amp; 회원가입등의 테마, LDAP, 인증 등이 있다.       User Storage SPI    User Storage SPI 는 기본적으로 H2 데이터베이스를 사용하는 Keycloak 을 자신의 외부 DB 에도 연결하도록 도와준다. 즉, 외부에 설치된 DB 를 이용해서 인증을 할 수 있게 된다.   참고: User Storgae SPI       User Storage 구현    User Storage 디렉토리 구조            adapter/CustomUserAdapter
403: 외부 DB 컬럼과 Keycloak DB 를 매핑시켜주는 어댑터   entity/AuthoritiesMapping: 외부 DB 의 권한 매핑 엔티티   entity/AuthoritiesMappingKey: 외부 DB 의 권한 매핑 복합키 엔티티   entity/AuthoritiesType: 외부 DB 의 권한 엔티티   entity/UserInfo: 외부 DB 의 사용자 엔티티   repository/CustomUserStorageRepository: 외부 DB 리포지토리 (Hibernate 사용)   CustomUserStorageProvider: 외부 DB 의 데이터를 사용하여 인증, 사용자 관리등을 구현   CustomUserStorageProviderFactory: Provider Id 를 설정하고 Keycloak 과 연결   resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory: 반드시 만들어야되는 파일로 Keycloak 에서는 이 파일로 Provider 를 찾아 적용한다.   resources/META-INF/persistence.xml: Hibernate 설정 파일   이제 하나하나 코드를 보며 알아보자. 먼저 가장 우선으로 구현해야 되는것이 META-INF 아래 파일과 Provider, ProviderFactory 이다.   먼저 META-INF/services 아래 파일을 보면 파일 이름은 Keycloak 의 UserStorageProviderFactory 클래스의 패키지 경로를 포함한 이름이다. 파일을 만들면 내용은 간단하게 개발자가 만든 Custom User Storage Provider 의 패키지 경로와 파일명을 적어주면 된다.   resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory  # {package path}.{provider factory file name} com.demo.storage.CustomUserStorageProviderFactory   다음으로 Provider Factory 를 보자.   CustomUserStorageProviderFactory  import javax.naming.InitialContext;  import org.keycloak.component.ComponentModel; import org.keycloak.models.KeycloakSession; import org.keycloak.storage.UserStorageProviderFactory;  public class CustomUserStorageProviderFactory implements UserStorageProviderFactory&lt;CustomUserStorageProvider&gt; {    @Override   public CustomUserStorageProvider create(KeycloakSession session, ComponentModel model) {     try {       InitialContext ctx = new InitialContext();       // 1) java:global/{jar 파일명}/Provider 클래스명       CustomUserStorageProvider provider = (CustomUserStorageProvider) ctx.lookup(\"java:global/custom-user-storage-jpa/\" + CustomUserStorageProvider.class.getSimpleName());       provider.setModel(model);       provider.setSession(session);        return provider;     } catch (Exception e) {       throw new RuntimeException(e);     }   }    @Override   public String getId() {     // 2) configuration.xml 에서 &lt;spi name=\"{id값}\"&gt; 에 작성     return \"custom-user-storage\";   }    }      JBoss(WildFly) 에서 설정 내용을 찾는다. 여기서는 전역 설정중 개발자가 구현한 User Storage Provider 를 찾으며, 주의해야 할 점은 “java:global/” 뒤에 반드시 개발자가 빌드한 jar 의 파일명으로 작성해야 한다는 것이다. WildFly 가 처음인 내게는 반나절을 꼬박 보낼정도로 찾기 힘들었다.. 마지막으로 뒤에 Provider 클래스 명을 입력하자.   여기도 중요한 부분인데 Keycloak 에서는 standalone.xml 파일에 SPI 들이 선언된 부분이 있는데, 여기에 개발자가 만든 SPI 도 작성하게 된다. 이때 Keycloak 은 무언가를 이용해서 개발자가 만든 SPI 를 찾거나 사용해야 하는데 이때 선언하는게 Id 값이다. 나중에 standalone.xml 파일에도 작성하겠지만 어떤 양식이 정해진게 아니어서 마음대로 작성해도 된다. 하지만 중요한점은 여기에 입력한 Id 값이랑 standalone.xml 에 작성할 Id 값이랑 같아야 한다.   위 두개 파일은 단지 Keycloak 과 매칭시켜주는 역할을 중점으로 한다. 다음은 본격적으로 SPI 의 내용을 구현하는 클래스다. 여기는 구현된 메소드가 조금 많아서 따로 설명하기 보다는 코드에 주석을 달아서 간략하게 설명을 하겠다.   CustomUserStorageProvider   import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors;  import javax.ejb.Local; import javax.ejb.Remove; import javax.ejb.Stateful;
403 import javax.persistence.EntityManager; import javax.persistence.PersistenceContext;  import com.daumsoft.storage.userStorage.adapter.CustomUserAdapter; import com.daumsoft.storage.userStorage.entity.UserInfo;  import org.keycloak.component.ComponentModel; import org.keycloak.credential.CredentialInput; import org.keycloak.credential.CredentialInputUpdater; import org.keycloak.credential.CredentialInputValidator; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; import org.keycloak.models.credential.OTPCredentialModel; import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.storage.StorageId; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.user.UserLookupProvider; import org.keycloak.storage.user.UserQueryProvider; import org.keycloak.storage.user.UserRegistrationProvider; import org.mindrot.jbcrypt.BCrypt;  // StateFul Bean 으로 선언 @Stateful // EJB 클라이언트와 동일한 환경에 있을 경우 선언 즉, 여기선 WildFly 에 배포되기 때문에 동일한 환경으로 간주함 @Local(CustomUserStorageProvider.class)           // User Storage SPI  public class CustomUserStorageProvider implements UserStorageProvider,                                                    // 사용자 조회                                                   UserLookupProvider,                                                    // 사용자 조회                                                   UserQueryProvider,                                                    // 사용자 등록                                                   UserRegistrationProvider,                                                    // 비밀번호 검사                                                   CredentialInputValidator,                                                    // 비밀번호 변경                                                    CredentialInputUpdater  {    protected ComponentModel model;   protected KeycloakSession session;    // 외부 DB 쿼리를 사용할 EntityManager 사용, Spring Boot JPA 를 사용하려 했지만 안되서 그냥 Hibernate 를 사용   @PersistenceContext   protected EntityManager entityManager;    // 빈 생성자 생성, 안해주면 에러가 난다.   public CustomUserStorageProvider() {}   // Keycloak 의 사용자 세션과 컴포넌트 모델을 가져옴   public CustomUserStorageProvider(KeycloakSession session, ComponentModel model) {     this.model = model;     this.session = session;   }      // 작업이 끝나면 EntityManager 세션 종료등을 한꺼번에 수행하는 메소드   @Remove   @Override   public void close() {}    /**    * 사용자가 로그인을 할때 입력된 Id 로 사용자 정보를 조회    */   @Override   public UserModel getUserById(String keycloakId, RealmModel realm) {     System.out.println(\"\\n\\ngetUserById\\n\\n\");      String userId = StorageId.externalId(keycloakId);     UserInfo userInfo = entityManager.createNamedQuery(\"findByUserId\", UserInfo.class)     .setParameter(\"userId\", userId)     .getResultList().stream().findFirst().orElse(null);          return new CustomUserAdapter(session, realm, model, userInfo);   }   /**    * 사용자가 로그인을 할때 입력된 username 으로 사용자 정보를 조회    */   @Override   public UserModel getUserByUsername(String username, RealmModel realm) {     System.out.println(\"\\ngetUserByUsername : \" + username + \"\\n\");          UserInfo userInfo = entityManager.createNamedQuery(\"findByUserId\", UserInfo.class)     .setParameter(\"userId\", username)     .getResultList().stream().findFirst().orElse(null);      return userInfo == null ? null : new CustomUserAdapter(session, realm, model, userInfo);   }    /**    * 사용자가 로그인을 할때 입력된 Email 로 사용자 정보를 조회    */   @Override   public UserModel getUserByEmail(String email, RealmModel realm) {     System.out.println(\"\\ngetUserByEmail: \" + email + \"\\n\");      UserInfo userInfo = entityManager.createNamedQuery(\"findByUserEmail\", UserInfo.class)     .setParameter(\"userEmail\", email)     .getResultList().stream().findFirst().orElse(null);      return userInfo == null ? null : new CustomUserAdapter(session, realm, model, userInfo);   }    /**     * 사용자 추가, 외부 DB 에 들어가는 것이 아니라 Keycloak DB 에 추가된다.    */    @Override   public UserModel addUser(RealmModel realm, String username) {     // System.out.println(\"\\naddUser\\n\");     // System.out.println(username);     // System.out.println(\"\\naddUser\\n\");     // UserInfo userInfo = new UserInfo();     // userInfo.setUserId(username);     // userInfo.setDelYn(\"N\");     // entityManager.persist(userInfo);     // return new CustomUserAdapter(session, realm, model, userInfo);     return null;   }    /**    * 사용자 삭제, 역시 Keycloak DB 에서 제거하는 용도    */    @Override   public boolean removeUser(RealmModel realm, UserModel user) {     return false;   }      /**    * 사용자 비밀번호 업데이트    */   @Override   public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {     System.out.println(\"updatecredential\\n\\n\");     System.out.println(user.toString());     System.out.println(input);     System.out.println(\"updatecredential\\n\\n\");     return true;   }    /**    * 비밀번호 종류(Otp, Password, 등) 을 비활성화    */   @Override   public void disableCredentialType(RealmModel realm, UserModel user, Str
403ing credentialType) {     System.out.println(\"disableCredentialType\\n\\n\");     System.out.println(user.toString());     System.out.println(credentialType);     System.out.println(\"disableCredentialType\\n\\n\");   }    /**    * 비활성화 된 비밀번호 종류(Otp, Password) 조회, 여기서 OTP 는 사용하지 않을 것이므로 OTP 를 반환    */   @Override   public Set&lt;String&gt; getDisableableCredentialTypes(RealmModel realm, UserModel user) {     System.out.println(\"getDisableableCredentialTypes\\n\\n\");      Set&lt;String&gt; set = new HashSet&lt;&gt;();     set.add(OTPCredentialModel.TYPE);      return set;   }    /**    * 지원되는 비밀번호 종류 (Password, Otp, ...) 가 현재 사용한 종류와 맞는지 확인    */   @Override   public boolean supportsCredentialType(String credentialType) {     System.out.println(\"\\n\\n supportsCredentialType: \" + credentialType + \"\\n\\n\");      return PasswordCredentialModel.TYPE.equals(credentialType);   }    /**    * 정확히 어떤 기능인지는 모르지만 비밀번호 인증에 어떤 설정을 해주는? 메소드    */   @Override   public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {     System.out.println(\"\\n\\n isConfiguredFor: \" + credentialType + \"\\n\\n\");     return false;   }    /**    * 비밀번호 검사 메소드, 여기서는 Bcrypt 를 사용했다.    */   @Override   public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) {     if (!(credentialInput instanceof UserCredentialModel))       return false;     if (supportsCredentialType(credentialInput.getType())) {       UserInfo userInfo = (UserInfo)session.getAttribute(\"userInfo\");        return BCrypt.checkpw(credentialInput.getChallengeResponse(), userInfo.getUserPassword());     } else {       return false;      }   }    /**    * 사용자 수를 조회    */   @Override   public int getUsersCount(RealmModel realm) {     return 0;   }    /**    * 사용자 목록을 조회, Keycloak REST API 등에서 사용    */   @Override   public List&lt;UserModel&gt; getUsers(RealmModel realm) {     System.out.println(\"\\ngetUsers\\n\");     return null;   }    /**    * firstResult 페이지에서 maxResults 개의 사용자를 조회    */   @Override   public List&lt;UserModel&gt; getUsers(RealmModel realm, int firstResult, int maxResults) {     System.out.println(\"getUsers\\n\\n\");     System.out.println(firstResult + \", \" + maxResults);     System.out.println(\"getUsers\\n\\n\");     return null;   }    /**    * Keycloak 관리자 대시보드에서 검색한 사용자 목록을 조회    */   @Override   public List&lt;UserModel&gt; searchForUser(String search, RealmModel realm) {     System.out.println(\"\\nsearchForUser 1 :\" + search + \"\\n\\n\");     return null;   }    /**    * Keycloak 관리자 대시보드에서 검색한 사용자 목록을 조회    */   @Override   public List&lt;UserModel&gt; searchForUser(String search, RealmModel realm, int firstResult, int maxResults) {     System.out.println(\"\\nsearchForUser 2 :\" + search + \"\\n\");          List&lt;UserInfo&gt; result = entityManager.createNamedQuery(\"findByUserIdLike\", UserInfo.class).setParameter(\"userId\", search).getResultList();      // AuthoritiesType authType = result.get(0).getAuthoritiesMapping().get(0).getAuthoritiesType();          // System.out.println(\"searchForUser 2 Role: \");     // System.out.println(authType.getAuthorityType());     // System.out.println(\"searchForUser 2 end\");      return result     .stream()     .map(user -&gt; new CustomUserAdapter(session, realm, model, user))     .collect(Collectors.toList());   }    @Override   public List&lt;UserModel&gt; searchForUser(Map&lt;String, String&gt; params, RealmModel realm) {     System.out.println(\"\\nsearchForUser 3 : \\n\\n\");     return null;   }    /**    * Keycloak 관리자 대시보드에서 전체 사용자 목록을 조회    */   @Override   public List&lt;UserModel&gt; searchForUser(Map&lt;String, String&gt; params, RealmModel realm, int firstResult, int maxResults) {     System.out.println(\"\\nsearchForUser 4 :\\n\\n\");          List&lt;UserInfo&gt; result = entityManager.createNamedQuery(\"findAll\", UserInfo.class)     .setFirstResult(firstResult)     .setMaxResults(20)     .getResultList();      return result.stream().map(user -&gt; new CustomUserAdapter(session, realm, model, user))         .collect(Collectors.toList());   }    /**    * Keycloak 대시보드에서 검색한 그룹 멤버를 조회    */    @Override   public List&lt;UserModel&gt; getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {     return null;   }    /**    * Keycloak 대시보드에서 전체 그룹 멤버를 조회    */    @Override   public List&lt;UserModel&gt; getGroupMembers(RealmModel realm, GroupModel group) {     return null;   }    /**    * Keycloak 대시보드에서 사용자 Attribute 를 조회    */    @Override   public List&lt;UserModel&gt; searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm) {     return null;   }    /**    * Provider 에서 사용되는 Setter    */   public void setModel(ComponentModel model) {     this.model = model;   }    /**    * Provider 에서 사용되는 Setter    */   public void setSession(KeycloakSession session) {     this.session = session;   }    }   Provider 클래스는 간단한 인증 외에도 Keycloak 대시보드에서 사용되는 사용자 관련 조회, 등록, 변경, 삭제등을 직접 구현할 수 있게 되어있다. 다음은 외부 DB 의 내용들을 Keycloak DB 에 매핑할 수 있게 도와주는 Adapter 클래스를 보자. 역시 적지 않은 메소드로 인해 별도로 설명보다는 간단한 주석으로 처리했다.   CustomUserAdapter   import 
403com.daumsoft.storage.userStorage.entity.AuthoritiesType; import com.daumsoft.storage.userStorage.entity.UserInfo;  import org.keycloak.component.ComponentModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.storage.StorageId; import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage;                                         // User Adapter Federated Storage 추상클래스 상속 public class CustomUserAdapter extends AbstractUserAdapterFederatedStorage {    protected UserInfo userInfo;   protected String keycloakId;    // Keycloak DB 와 매핑될 Adapter 생성자   public CustomUserAdapter(KeycloakSession session, RealmModel realm, ComponentModel model, UserInfo userInfo) {     super(session, realm, model);      this.userInfo = userInfo;      if (userInfo != null) {       // 외부 DB 에서 조회한 사용자 정보가 null 이 아닐 경우 외부 DB 의 사용자 PK 를 Keycloak 사용자 DB 의 PK 값으로 사용       this.keycloakId = StorageId.keycloakId(model, String.valueOf(userInfo.getUserSeq()));       // Provider 에서 사용하기 위해 세션에 사용자 정보 값을 저장       session.setAttribute(\"userInfo\", userInfo);     }   }      /**    * Keycloak DB 의 username 입력, 조회 일반적으로 사용되는 id 개념이다    */   @Override   public String getUsername() {     return userInfo != null ? userInfo.getUserId() : null;   }    @Override   public void setUsername(String username) {     userInfo.setUserId(username);   }   /**    * Keycloak DB 의 Email 입력, 조회    */   @Override   public String getEmail() {     return userInfo != null ? userInfo.getUserEmail() : null;   }    @Override   public void setEmail(String email) {     userInfo.setUserEmail(email);   }    /**    * 외부 DB 의 Role 을 가져와 Keycloak 에서 사용할 수 있게하는 메소드    */    @Override   public void grantRole(RoleModel role) {     AuthoritiesType authType = userInfo.getAuthoritiesMapping().get(0).getAuthoritiesType();      System.out.println(\"grantRole\\n\");     System.out.println(authType.getAuthorityType());     System.out.println(\"\\ngrantRole\");   }    }   3개의 주요 클래스와 하나의 설정파일들을 보았다. 엔티티와 리포지토리의 경우는 외부 DB 환경에 맞춰서 구현하면 될거 같다. 추가로 하나더 알아보자면 이전에 Keycloak standalone.xml 설정파일에서 작성한 외부 DB 소스가 있다. 이걸 사용하려면 persistance.xml 에 다음을 추가해주면 된다.   persistance.xml   &lt;?xml version=\"1.0\" encoding=\"UTF-8\"?&gt; &lt;persistence xmlns=\"https://xmlns.jcp.org/xml/ns/persistence\"    xmlns:xsi=\"https://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"https://xmlns.jcp.org/xml/ns/persistence              https://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd\" version=\"2.1\"&gt;    &lt;persistence-unit name=\"custom-user-storage-jpa\"&gt;     &lt;description&gt;Hibernate Entity Manager Example&lt;/description&gt;     &lt;!-- standalone.xml 에서 작성한 DB 설정들을 가져온다. --&gt;     &lt;jta-data-source&gt;java:jboss/datasource/UserStoreXADS&lt;/jta-data-source&gt;      &lt;properties&gt;       &lt;property name=\"hibernate.show_sql\" value=\"true\" /&gt;       &lt;property name=\"hibernate.hbm2ddl.auto\" value=\"none\" /&gt;       &lt;property name=\"hibernate.dialect\" value=\"org.hibernate.dialect.MariaDBDialect\" /&gt;       &lt;!-- &lt;property name=\"hibernate.physical_naming_strategy\" value=\"org.ohjic.hfinSync.PhysicalNamingStrategyImpl\" /&gt; --&gt;     &lt;/properties&gt;    &lt;/persistence-unit&gt;  &lt;/persistence&gt;       Keycloak standalone.xml 설정    Provider 를 적용하기 위해서 몇가지 사전에 설정파일에 작성해야 하는것이 있다. 많지는 않고 두가지? 정도 된다.   첫째로 개발자가 만든 spi 를 찾기 위한 선언코드, 이전에 ProviderFactory 에서 작성한 Id 값을 여기에서 사용한다.   standalone.xml  &lt;!-- Provider Factory 에서 작성한 id 값을 입력 --&gt; &lt;spi name=\"custom-user-storage\"&gt; &lt;/spi&gt;   둘째는 persistance.xml 에서 Keycloak 설정파일에서 작성한 DB 속성을 사용하기 위해 설정한 DataSource 내용이다. 이전 글에서 작성한 Datasource 부분에서 “xa-“ 부분을 추가했다. 이 부분이 persistance.xml 에서 사용되는 DB 설정이다.   standalone.xml   &lt;subsystem xmlns=\"urn:jboss:domain:datasources:5.0\"&gt;   &lt;datasources&gt;     ...     &lt;xa-datasource jndi-name=\"java:jboss/datasource/UserStoreXADS\" pool-name=\"UserStoreXADS\"&gt;         &lt;xa-datasource-property name=\"ServerName\"&gt;             {external db host}         &lt;/xa-datasource-property&gt;         &lt;xa-datasource-property name=\"DatabaseName\"&gt;             {external db name}         &lt;/xa-datasource-property&gt;         &lt;driver&gt;mariadb&lt;/driver&gt;         &lt;security&gt;             &lt;user-name&gt;{external db username}&lt;/user-name&gt;             &lt;password&gt;{external db password}&lt;/password&gt;         &lt;/security&gt;     &lt;/xa-datasource&gt;     &lt;drivers&gt;       &lt;driver name=\"mariadb\" module=\"org.mariadb\"&gt;           &lt;xa-datasource-class&gt;org.mariadb.jdbc.MariaDbDataSource&lt;/xa-datasource-class&gt;       &lt;/driver&gt;     &lt;/drivers&gt;   &lt;/datasources&gt;   ... &lt;/subsystem&gt;       How to use?    개발도 다 되었고, 설정파일도 작성했다. 그럼 이제 빌드를 할것이고, jar 파일ì
403„ 만들것이다. 그럼 이제 이걸 어떻게 해야하는거지? 라는 고민을 했고 찾아보았다. 멀지 않은데서 찾을 수 있었다. 공식문서에 잘 나와있었다. 방법은 간단하다. standalone/deployments 아래에 jar 파일을 복사해주면 된다. 단! 앞에서 말한 파일명을 Provider Factory 클래스에서 작성한 “java:global” 뒤에 오는 이름과 같게 해줘야 한다는점 주의하자.   이제 Keycloak 서버를 재실행하거나 실행중이라면 가만두면 자동으로 Deploy 가 된다. 그리고서는 Keycloak 대시보드에서 잘 적용되었는지 확인해보자.   먼저 User Federation 탭을 선택하고 Select box 에서 자신이 추가한 Id 값을 찾는다.         선택한 후 save 버튼을 누르면 아래와 같이 잘 추가된 화면을 ë³¼ 수 있다. 아래 이미지는 공식문서에서 가져온것이므로 만약 위에 작성한 jar 를 추가해줬다면 ID 에 “custom-user-storage” 가 보일 것이다.             마치며    일단 구현을 하고보니 조금 어려운 점이 많았다. 사용자 추가되는 점에서 외부 DB 와 Keycloak DB 가 다르다거나 사용되는 Key 들이 너무 달라서 구현하는데 리소스가 많이 들어가게 되었다. 그래서 경험삼아 구현을 했다고 치고 다른 방법을 찾기로 했다. 어떻게 해야 할까 고민하다가 어차피 Keycloak 에서 사용자 관련 모든 기능을 제공하니 차라리 Keycloak DB 를 사용하자고! 그리고는 헐레벌떡 파이썬으로 Keycloak REST API 를 이용해서 외부 DB 내용을 입력해주었다.   REST API 는 다음 편에서 간단히 소개하고, OIDC 를 사용하는 Keycloak 에서 Naver 로그인을 연동하는 방법도 잠깐 소개할 것이다.  ","categories": ["posts"],
404        "tags": ["msa","keycloak"],
405        "url": "https://subji.github.io/posts/2020/07/24/keycloak3",
406        "teaser":null},{
407        "title": "MSA 인증 서비스 Keycloak REST API, Naver 로그인",
408        "excerpt":"Keycloak REST API   공식문서에 보면 정말 다양한 API 들이 제공된다. 그중 여기서 다뤄볼 API 는 access_token 을 발급받는 것과, 사용자 생성, Role 매핑, 사용자 조회 API 이다.   그리고 잠깐 Identity Provider 를 사용해서 OIDC(Open Id Connect) 를 지원하지 않는 네이버 로그인을 설명하고자 한다.       Keycloak REST API    Access Token 발급 API   curl http://localhost:8080/auth/realms/{keycloak realm}/protocol/openid-connect/token \\  -dclient_id={keycloak client id} \\  -dclient_secret={keycloak client secret} \\  -dscope=profile \\  -dgrant_type=client_credentials   위에서 따로 입력해야 하는 부분은 realm, clientid, client secret 정도이다. 물론 client_credentials 를 사용할 경우 clientid, client secret 이 필요하다. 결과는 다음과 같이 출력된다.         이제 받아온 access token 으로 Keycloak 인증을 하고 다양한 API 를 사용할 수 있다. 위에서 말한 몇가지를 사용한 예제를 보자.   Create User  curl http://localhost:8080/auth/admin/realms/{keycloak realm}/users \\ -H \"Authorization: bearer {access_token}\" \\ -X POST \\ --data '{     \"username\": \"{username}\",     \"enabled\": \"true\",     \"credentials\": [         {             \"value\": \"{password}\"         }     ] }'   추가로 공식문서에 보면 realmRoles 라고 있는데, 작동이 안된다. 왠지는 모르겠다. 개발을 안한건가? 아무튼 Role 를 추가하고자 할때 조금 힘들지만 Keycloak 대시보드에서 Role 을 직접 추가해서 API 로 사용자와 Role 을 매핑시켜줘야 한다. Role 매핑 API 는 다음과 같다. 여기서는 Client Role 매핑을 사용했다. Realm Role 매핑과는 Path 하나의 차이만 있다.   Client Role Mapping  curl http://localhost:8080/auth/admin/realms/sometrend-realm/users/{keycloak user id}/role-mappings/clients/{keycloak client} \\ -H \"Authorization: bearer {access_token}\" \\ -X POST \\ --data '[     {         \"id\": \"{realm/client role id}\",         \"name\": \"{role name}\",         \"clientRole\": \"true\",         \"containerId\": \"{realm/client id}\"     } ]'   Realm 매핑을 사용하려면 뒤에 clients/{keycloak client} 를 제거해주면 된다.   사용자를 추가하고 Role 매핑까지 해줬으니 이제 추가된 사용자를 조회해보자. 다음은 전체 사용자를 조회하는 요청이다. 만약 username 또는 Email 등으로 조건을 주고 조회를 하고싶으면 다양한 Parameter 가 공식문서에 나와있다.   Get User Parameter   Find User  curl http://localhost:8080/auth/admin/realms/{keycloak realm}/users       Naver 로그인   간단하게 API 를 이용해서 사용자를 Keycloak DB 에 추가했다. 이제 로그인도 잘된다. 여기서 추가로 소셜 로그인을 추가하고 싶다. 뭐 예를들어 구글이라던가 페이스북이라던가.. 다행이도 Keycloak 은 OIDC 를 지원하는 다양한 소셜 로그인들을 지원한다. 아래에 그림을 보면 웬만한 소셜 로그인은 지원한다고 볼 수 있다.         그런데 중요한 것은 여긴 한국이다. 한국은 네이버, 카카오 사용자 수를 무시할 수 없다. 그러기에 네이버 로그인, 카카오 로그인을 추가해줘야 한다. 근데 위 사진에서 보듯이 네이버랑 카카오는 눈을 씻고 찾아봐도 없다. 이유는 네이버와 카카오의 경우 OIDC 를 지원하지 않고 OAuth 만을 지원하기 때문이다. 그래서 이부분을 개발자가 직접 Identity Provider 를 구현해서 OAuth 만을 사용하는 소셜 로그인을 연결해줘야 한다.   네이버 용 Identity Provider 를 만들어보자. 아래 구현은 다음 내용을 참고했다.   참고: Baidu Identity Provider         먼저 프로젝트 구조는 다음과 같다. 이전에 User Storage SPI 를 구현할때 처럼 META-INF/services 아래 파일과 Provider, ProviderFactory 가 존재하는 것을 볼 수 있다. 여기는 외부 DB 와 연결해줘야하는 복잡한 구현은 없기때문에 파일도 몇개 안된다. 그러면 하나하나 보자.   먼저 META-INF/services 아래 파일들을 보자. UserStorageSPI 와 다른점이라면 Mapper 파일이 추가됬다. 일단 정확히 무슨역할을 하는지 모르니 Keycloak 클래스명과 패키지명으로 파일명을 작성하고 내용으로는 개발자가 만든 매퍼와 팩토리 클래스의 패키지명과 파일명을 적어주도록 하자.   Gradle Dependency    compile 'org.keycloak:keycloak-services:10.0.2'   compile 'org.keycloak:keycloak-server-spi:10.0.2'   compile 'org.keycloak:keycloak-server-spi-private:10.0.2'   org.keycloak.broker.provider.IdentityProvi
408derMapper  moe.saru.keycloak.modules.naver.NaverUserAttributeMapper   org.keycloak.broker.social.SocialIdentityProviderFactory  moe.saru.keycloak.modules.naver.NaverIdentityProviderFactory   NaverUserAttributeMapper  import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;  /**  * User attribute mapper.  */ public class NaverUserAttributeMapper extends AbstractJsonUserAttributeMapper {  \tprivate static final String[] cp = new String[] { NaverIdentityProviderFactory.PROVIDER_ID };  \t@Override \tpublic String[] getCompatibleProviders() { \t\treturn cp; \t}  \t@Override \tpublic String getId() { \t\treturn \"naver-user-attribute-mapper\"; \t}  }    정확히 매퍼가 하는 역할은 확실치 않다. 다만 예상컨데 사용자 속성 매퍼라는 점… 아마 Identity Provider 정보 입력 화면에서 보이는 속성들을 매핑시켜주는 것 같다.   NaverIdentityProviderFactory   import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; import org.keycloak.broker.provider.AbstractIdentityProviderFactory; import org.keycloak.broker.social.SocialIdentityProviderFactory; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession;  public class NaverIdentityProviderFactory extends AbstractIdentityProviderFactory&lt;NaverIdentityProvider&gt; implements SocialIdentityProviderFactory&lt;NaverIdentityProvider&gt; {      public static final String PROVIDER_ID = \"naver\";      @Override     public String getName() {         return \"naver\";     }      @Override     public NaverIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {         return new NaverIdentityProvider(session, new OAuth2IdentityProviderConfig(model));     }      @Override     public String getId() {         return PROVIDER_ID;     }      @Override     public OAuth2IdentityProviderConfig createConfig() {         return new OAuth2IdentityProviderConfig();     } }   친숙한 Factory 클래스다. 역시 Id 값을 넣어주며, Provider 를 생성해준다. 주의할 점은 아래 createConfig 가 없으면 에러가 난다는 점이다. 반드시 작성해주자 여기서는 OAuth2 IdentityProvider 설정을 생성하였다.   NaverIdentityProvider  import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import java.util.Iterator; import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider; import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper; import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.social.SocialIdentityProvider; import org.keycloak.events.EventBuilder; import org.keycloak.models.KeycloakSession;  public class NaverIdentityProvider extends AbstractOAuth2IdentityProvider implements SocialIdentityProvider {  \tpublic static final String AUTH_URL = \"https://nid.naver.com/oauth2.0/authorize\"; \tpublic static final String TOKEN_URL = \"https://nid.naver.com/oauth2.0/token\"; \tpublic static final String PROFILE_URL = \"https://openapi.naver.com/v1/nid/me\"; \tpublic static final String DEFAULT_SCOPE = \"basic\";  \tpublic NaverIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) { \t\tsuper(session, config); \t\t \t\tconfig.setAuthorizationUrl(AUTH_URL); \t\tconfig.setTokenUrl(TOKEN_URL); \t\tconfig.setUserInfoUrl(PROFILE_URL); \t}  \t@Override \tprotected boolean supportsExternalExchange() { \t\treturn true; \t}  \t@Override \tprotected String getProfileEndpointForValidation(EventBuilder event) { \t\treturn PROFILE_URL; \t}  \t@Override \tprotected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) { \t\t// getJsonProperty 는 Oidc 관련 파싱만 가능하므로 JsonNode 의 get 메소드를 이용해서 가져온다. \t\t// BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, \"response\")); \t\tBrokeredIdentityContext user = new BrokeredIdentityContext(profile.get(\"response\").get(\"id\").asText());  \t\tString email = profile.get(\"response\").get(\"email\").asText();  \t\tuser.setIdpConfig(getConfig()); \t\tuser.setUsername(email); \t\tuser.setEmail(email); \t\tuser.setIdp(this);  \t\tAbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());  \t\treturn user; \t}  \t@Override \tprotected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) { \t\ttry { \t\t\tJsonNode profile = SimpleHttp.doGet(PROFILE_URL, session).param(\"access_token\", accessToken).asJson();  \t\t\tBrokeredIdentityContext user = extractIdentityFromProfile(null, profile);  \t\t\treturn user; \t\t} catch (Exception e) { \t\t\tthrow new IdentityBrokerException(\"Could not obtain user profile from naver.\", e); \t\t} \t}  \t@Override \tprotected String getDefaultScopes() { \t\treturn \"\"; \t} }   먼저 상수로 선언된 문자열들을 각각 네이버의 인증 URL, 토큰 발급 URL ê³¼ Profile 을 가져ì˜
408¤ëŠ” URL 들을 설정했다. Scope 는 네이버 개발가이드를 ë³´ë©´ 따로 설정을 안해줘도 된다고 해서 작성은 했지만 아래 getDefaultScopes 에서 빈 문자열을 반환하게 했다.   생성자에서는 Identity Provider 의 기본 URL 설정을 해주었다.      getProfileEndpointForValidation: 네이버 Profile Endpoint 주소 반환   extractIdentityFromProfile: 네이버 Profile 내용 반환   doGetFederatedIdentity: 실제로 네이버에 인증 요청을 하고 토큰을 받아오는 역할, 토큰을 이용해 Profile 을 가져오는 역할을 수행하는 메소드이다.   getDefaultScopes: 네이버 개발가이드에도 나와있듯이 scope 값이 필요없기 때문에 여기서는 빈 문자열을 반환   User Storage SPI 에 비하면 구현이 비교적 간단하다. 자 그럼 여기서 끝? 위에 프로젝트 구조 그림을 다시보자. src 와 같은 레벨에 resources 디렉토리가 있다. ë³´ë©´ .html 확장자를 가진 파일 두개가 있다. 이것은 다음 화면을 찾아가는데 필요한 파일이지 않을까 싶다. (아는 분이 있으면 알려주세요..)         주의할 점은 각 html 파일명에서 마지막에 오는 naver 이부분이다. 앞서 Factory 클래스에서 작성한 Id 값을 여기에다 적어줘야 한다. 적어주지 않을 경우 위 그림의 화면으로 연결이 안된다.   이제 프로젝트를 빌드해보자. 여기에서는 Gradle 프로젝트로 개발되었기때문에 “gradle bulid” 를 하자. 그러면 build 디렉토리 아래에 jar 가 생성된다. 그럼 이제 이 jar 를 이전처럼 standalone/deployments 아래에 넣어주자. 그리고 Keycloak 서버가 재 실행 되면 대시보드로 접속해서 Identity Provider 를 보자.         네이버가 잘 추가되었다. 굿! 이제 naver 를 선택해보자 혹시 에러가 발생하거나 페이지 이동이 안된다면 하나 확인해야할 점이 있다. 위에서 말한 .html 파일들을 keycloak 프로젝트에서 themes/base/admin/resources/partials/ 밑에 둬보자. 그런뒤 Keycloak 서버를 재시작하고 다시 naver 를 선택하면 다음과 같은 화면이 나온다.         여기서 입력해줘야할 것은 네이버 개발자에서 발급받은 Client ID, Client Secret 두개뿐이다. 입력이 완료되었으면 네이버 개발자에 등록해야 되는 Callback URL 인 Redirect URL 을 복사해두고 save 를 하자.   마지막으로 네아로(네이버 아이디로 로그인) 개발자 화면에서 이런저런 등록을 하고 API 설정탭에서 다음 두부분을 입력해줘야 한다. 하나는 서비스가 실행되는 URL ê³¼ 방금 복사한 Redirect URL 을 넣어줄 Callback URl 이다. Callback URL 에 복사한 Redirect URL 을 넣어주고 서비스 URL 도 마저 채워주자.         테스트를 위해 다음 URL 로 접속을 하자 http://localhost:8080/auth/realms/{realm}/account/   그러면 다음과 같은 로그인 화면이 나오고 네이버를 선택해 네이버 로그인을 진행하면 된다.       마치며    OIDC 와 OAuth 의 차이점을 공부한 내용도 올려야겠다. 점점 OIDC 로 변하는 추세여서 그런지 오픈소스들도 속속들이 반영을 하는가보다.  ","categories": ["posts"],
409        "tags": ["msa","keycloak"],
410        "url": "https://subji.github.io/posts/2020/07/24/keycloak4",
411        "teaser":null},{
412        "title": "JPA 무한 순환참조 Stack over flow 오류",
413        "excerpt":"Json 순환참조   JPA 를 사용하면서 Team -&gt; Member 와 같이 OneToMany -&gt; ManyToOne ê³¼ 같은 양방향 관계를 가지는 구조에서 많이 발생하는 Json 순환참조에 대해서 알아보자.   Json 순환참조는 양뱡향 관계에서 객체를 직렬화 하는 과정에서 양쪽 모두 직렬화 하려할때 무한참조가 되어 StackOverFlow 를 발생시킨다.   무심코 Lombok 을 사용하며 @Data 를 사용하거나 @ToString 및 hashcode&amp;equals 어노테이션을 사용하면서 발생하기도 한다. ToString 이나 HashCode&amp;Equals 에서 양 객체가 서로의 프로퍼티를 계속해서 참조하면서 무한 순환참조가 발생한다.   즉, 가급적이면 Entity 에서 @Data, @ToString 등의 어노테이션 사용을 지양하자.   다음은 몇가지 순환참조 해결방법이다.      @JsonIgnore   ToString Override   @JsonManagedReference, @JsonBackReference   @JsonIdentityInfo   첫ë²
413ˆì§¸ @JsonIgnore 의 경우 해당 어노테이션이 적용된 부분을 Json 직렬화에서 아예 제외시키므로 좋은 방법이 아니다.   두번째 역시 매번 새로 Override 를 해줘야 하기 떄문에 생산성 측면에서 좋은 방법이 아니다. 그러면 남은 방법은 3, 4번째다. 3 번째는 Jackson 라이브러리 1.x 버전에서 4 번째는 2.x 버전 이상에서 사용가능하다.   3번째의 경우 예를 들어보겠다. 다음 예시의 Entity 두개를 보자.   @Entity public class User implements Serializable{    @Id  @GeneratedValue(strategy=GenerationType.IDENTITY)  private long id;    @Column(name=\"name\")  private String name;   @ManyToMany  @JoinTable(name=\"users_roles\",joinColumns=@JoinColumn(name = \"user_fk\"),  inverseJoinColumns=@JoinColumn(name = \"role_fk\"))  @JsonManagedReference  private Set&lt;Role&gt; roles = new HashSet&lt;Role&gt;();  ...  @Entity public class Role implements Serializable {   @Id   @GeneratedValue(strategy=GenerationType.IDENTITY)  private int id;   @ManyToMany(mappedBy=\"roles\")  @JsonBackReference  private Set&lt;User&gt; users = new HashSet&lt;User&gt;();  ...   여러명의 User 가 여러개의 Role 을 갖는 양방향 다대다 구조이다. 양방향 구조이기 때문에 무한 순환참조가 발생할 것이다. User 에 있는 *Set roles = new HashSet();* 부분이 Jackson 에서 가져오는 **Forward part of Reference**. 즉, Json 형식으로 변환하는 부분이다. 이렇게 앞에서 참조하는 부분을 가져와 Json 으로 변환하는 것을 마샬링 프로세스라고 한다.   그리고 Role Entity 에서 *Set users = new HashSet();* 부분은 **Backward part of Reference** 라고 뒤에서 참조되는 부분이다. Jackson 이 부분을 직렬화하지 않고 그냥 두어야 무한 순환참조가 발생하지 않게 된다.   그래서 주인 Entity 인 User, JoinColumn 이 선언된 곳에서는 Json 참조를 발생시키고, 자식 Entity 인 Role, MappedBy 가 선언된 곳에서는 Json 직렬화를 방지하게 된다.   마지막으로 @JsonIdentityInfo 에 대해서 간단한 사용방법을 소개하고 마치겠다.   @Entity @JsonIdentityInfo(generator = IntSequenceGenerator::class, property = \"id\") public class User implements Serializable{    @Id  @GeneratedValue(strategy=GenerationType.IDENTITY)  private long id;    @Column(name=\"name\")  private String name;   @ManyToMany  @JoinTable(name=\"users_roles\",joinColumns=@JoinColumn(name = \"user_fk\"),  inverseJoinColumns=@JoinColumn(name = \"role_fk\"))  private Set&lt;Role&gt; roles = new HashSet&lt;Role&gt;();  ...  @Entity @JsonIdentityInfo(generator = IntSequenceGenerator::class, property = \"id\") public class Role implements Serializable {   @Id   @GeneratedValue(strategy=GenerationType.IDENTITY)  private int id;   @ManyToMany(mappedBy=\"roles\")  private Set&lt;User&gt; users = new HashSet&lt;User&gt;();  ...   사용법은 간단하다. 각 Entity 에 @JsonIdentityInfo(generator = IntSequenceGenerator::class, property = “id”) 를 추가하면된다. generator 에는 정수형 ID 를 설정하는 IntSequenceGenerator 와 문자열 ID 를 설정하는 StringGenerator 와 UUID 형태로 설정하는 UUIDGenerator 등이 있다. 또한 property 속성에 별다른 값을 정하지 않으면 직렬화 할때 @id 라는 속성이름으로 객체의 Id 가 설정된다.       마치며    차근차근 공부하며 사용하는 JPA 인데 어려움이 많다. 그래도 많이 사용하고들 있어서인지 공부할 자료도 많고 문제해결 자료도 많아 도움이 많이 된다.       참고   Json and Jackson Stackoverflow 문제   Infinite Recursion with Jackson JSON and Hibernate JPA issue  ","categories": ["posts"],
414        "tags": ["jpa"],
415        "url": "https://subji.github.io/posts/2020/08/06/infiniterecusionofjpa",
416        "teaser":null},{
417        "title": "Java RestTemplate File Upload",
418        "excerpt":"How to   RestTemplate 를 이용해서 파일을 전송하고자 할때 일반적인 방법으로 전송하게 되면 Jackson 라이브러리에서 직렬화에 실패했다고 에러 메세지를 보낸다. 그러면 어떻게 할 수 있을까? 파일 자체를 바이트로 변경해서 이것을 Multivaluemap 에 담아 보내는 방법이 있다. 코드는 다음과 같다.   HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.MULTIPART_FORM_DATA);  MultiValueMap&lt;String, Object&gt; body = new LinkedMultiValueMap&lt;String, Object&gt;();  for (MultipartFile file : files)  {   if (!file.isEmpty())  {     body.add(\"file\", new ByteArrayRes
418ource(file.getBytes()));   } }  HttpEntity&lt;MultiValueMap&lt;String, Object&gt;&gt; entity = new HttpEntity&lt;MultiValueMap&lt;String, Object&gt;&gt;(body, headers);  ResponseEntity&lt;Map&lt;String, Object&gt;&gt; result = restTemplate.exchange(   \"{api uri}\",   HttpMethod.POST,   entity,    new ParameterizedTypeReference&lt;Map&lt;String, Object&gt;&gt;() {}); }   위 코드대로 보내면 Jackson FormHttpMessageConverter 관련 에러가 발생한다. 몇시간을 무슨 에러인지 헤매고 있었다. 자세히 에러 스택트레이스를 보니 무슨 컨버터가 자꾸 없다고 한다. 컨버터가 없다는게 무슨뜻이지.. 하고 생각하다가 아? RestTemplate 에 컨버터를 설정한 부분이 있었다는게 생각났다. 그래서 다시 구글링을 하니 다음과 같이 File 을 보내고자 할때에는 Resource 관련 컨버터를 추가하라는 말이 있었다.   HttpMessageConverter&lt;Object&gt; jackson = new MappingJackson2HttpMessageConverter(); HttpMessageConverter&lt;Resource&gt; resource = new ResourceHttpMessageConverter(); FormHttpMessageConverter formHttpMessageConverter = new FormHttpMessageConverter(); formHttpMessageConverter.addPartConverter(jackson); formHttpMessageConverter.addPartConverter(resource);   RestTemplate restTemplate = new RestTemplate(Arrays.asList(jackson, resource, formHttpMessageConverter));   이 코드를 추가하니까 보내는건 잘보내진다. 하지만 받을때 에러가 발생! 뭐 이거는 어렵지 않게 해결할 수 있었다. 메세지를 보낼때 필요한 컨버터가 있다면 받을때도 내용을 가져올 컨버터가 설정이 되어 있거나 해야할것이다. 그래서 받는 API 에도 위 설정을 추가해주니 잘 되었다.   추가로 new ByteArrayResource 를 사용했는데, 스프링 5.1 버전 이후에서는 file.getResource() 를 사용하여 리소스를 보낼수 있다고 한다.   최종 수정 코드   HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.MULTIPART_FORM_DATA);  MultiValueMap&lt;String, Object&gt; body = new LinkedMultiValueMap&lt;String, Object&gt;();  for (MultipartFile file : files)  {   if (!file.isEmpty())  {     body.add(\"file\", file.getResource());   } }  HttpEntity&lt;MultiValueMap&lt;String, Object&gt;&gt; entity = new HttpEntity&lt;MultiValueMap&lt;String, Object&gt;&gt;(body, headers);  ResponseEntity&lt;Map&lt;String, Object&gt;&gt; result = restTemplate.exchange(   \"{api uri}\",   HttpMethod.POST,   entity,    new ParameterizedTypeReference&lt;Map&lt;String, Object&gt;&gt;() {}); }       참고    How to send Multipart form data with restTemplate Spring-mvc   Resttemplate form/multipart: image + JSON in POST  ","categories": ["posts"],
419        "tags": ["java"],
420        "url": "https://subji.github.io/posts/2020/08/10/fileuploadusingresttemplate",
421        "teaser":null},{
422        "title": "Eureka self-preservation",
423        "excerpt":"Eureka    Eureka 는 CAP 정리 측면에서 ë³¼ 수 있는 AP 시스템이다. 여러개의 네트워크 파티션 있다고 했을때 각각의 파티션들의 레지스트리 값은 일치하지 않게 된다. Eureka 의 Self-preservation 기능은 이러한 불일치를 최소화 해주는 특징이다.       Eureka Self-Preservation ?    Self Preservation 은 Eureka 서버에서 특정 시간이나 횟수등의 값을 초과하는 heartbeat 를 수신하지 못할때 Eureka 레지스트리에서 해당 인스턴스의 만료를 중지하는 기능이다.   heartbeat 는 각각의 Eureka 클라이언트로 어떠한 상태값이나 특정 값을 받아 해당 클라이언트의 상태를 알 수 있는 정보이다.       Eureka Self-Preservation Flow       Eureka Server 1, Server 2 가 존재   Eureka Client Intance 1, 2, 3, 4, 5 =&gt; Eureka Server 1 에 등록   Eureka Server 1 -&gt; Server 2 로 등록 복사   각각의 서버 레지스트리에 Instance 1, 2, 3, 4, 5 가 존재   Eureka Client Instance 2 는 Instance 4 와 연결 중이다.   Eureka Client Instance 4, 5 가 네트워크 파티션으로 분리   Eureka Server 는 Instance 4, 5 의 Heartbeat 가 더이상 수신되지 않는것을 확인하고 레지스트리에서 Instance 4, 5 제거   Eureka Server 에서 갑자기 15% 이상의 Instance 가 종료됨을 알고 Self-Preservation 모드 실행   이제부터 Eureka Server 는 나머지 1, 2, 3 Instance 가 종료되더라도 레지스트리에서 만료될 경우에만 Instance 를 중지   즉, Instance 3 을 종료시키더라도 서버 레지스트리에는 활성 상태로 남아있다. 또한 Instance 3 자리에 새로운 Instance 등록을 허용한다.       Why Self-Preservation ?    Self-Preservation 모드의 활성화 이유?      클라이언트의 종료가 아닌 잘못된 네트워크 파티션 분리로 인한 heartbeat 가 수신이 안되는 현상일 수 있다. 이 경우 잘못된 네트워크 파티션을 수정할 경우 원래대로 등록이 가능하므로 무조건적으로 레지스트리에서 제거하는 것은 옳지 않을 수 있다.   일부 Instance 의 연결이 끊어지더라도 또 다른 Instance 와 연결이 있을 경우 강제로 레지스트리에서 제거할 때 오류가 발생할 수 있다. 따라서 특정 시간 또는 횟수 동안 레지스트리에 활성 상태로 두는 것이 낫다.       About Self-Preservation configuration       eureka.instance.lease-renewal-interval-in-seconds = 30   Eureka Client 가 여전히 활성 상태임을 나타내기 위해 Eureka Server 에 heartbeat 를 보내는 빈도를 나타내며 기본값은 30초이다. 즉, 30초 마다 한번씩 heartbeat 를 수신하게 된다. 가급적 기본값인 30초 사용을 권장한다.   eureka.instance.lease-expiration-duration-in-seconds = 90   Server 레지스트리에서 Client Instance 를 제거하기 전에 마지막 heartbeat 를 받은 후 서버가 대기하는 시간을 나타낸다.   이값은 앞서 말한 lease-renewal-interval-in-seconds 보다 커야한다. 이 값을 너무 길게 설정하면 레지스트리의 활성 상태가 설정값에 따라 달라지므로 이전에 설명한 lease-renewal-interval-in-seconds 의 ë
423¶„당 실제 heartbeat 계산의 정밀도에 영향을 주게 된다고 한다. 또한 너무 작게 설정하면 시스템이 일시적인 네트워크 결함을 견딜 수 없게 된다고 한다.   eureka.server.eviction-interval-timer-in-ms = 60 * 1000   이 설정은 Instance 가 종료되고 만료가 되었을때 레지스트리에서 Instance 를 제거하는 스케쥴러가 이 빈도로 실행된다. lease-expiration-duration-in-seconds 의 값을 너무 길게 설정하면 시스템이 자체 Self-Preservation 으로 들어가는 것이 지연된다.   eureka.server.renewal-percent-threshold = 0.85   이 값은 분당 예상 heartbeat 를 계산하는 데 사용된다.    이값의 계산은 Eureka 대시보드를 ë³´ë©´ 알 수 있는데, Renews Threshold 와 Renews (last min) 부분을 ë³´ë©´ 알 수 있다. 첫번째는 갱신 기준이 되는 임계값이며, 두번째는 마지막 갱신된 값이다. 즉 “EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.” 와 같은 에러가 나면서 Instance 가 종료되지 않는다면 Renews Threshold 값이 Renews (last min) 보다 클 경우에 발생하는 에러이다. 임계 값이 더 클 경우 Self-Preservation 모드가 실행되며 만료된 Instance 가 종료되지 않기 때문이다.   계산 방법은 다음과 같다.      Renews Threshold =&gt; 분당 heartbeat 수 (기본값인 30초에 한번으로 가정) * Instance 수 + Spring 에서 구성하는 최소 Instance 수 (1) * renewal-percent-threshold 값 계산 후 반올림을 한다.            ex: 2 * 1 + 1 * 0.85 = 3           Renews (last min) =&gt; 분당 heartbeat 수 (기본값인 30초에 한번으로 가정) * Instance 수            ex: 2 * 1 = 2           즉, 테스트 환경이나 별도의 운영환경 구성에서 앞서말한 경고를 없애고자 할때 퍼센트 값을 낮추면 되겠다.      eureka.server.renewal-threshold-update-interval-ms = 15 * 60 * 1000   레지스트리에서 Instance 를 제거하는 스케줄러는 분당 예상 heartbaet 를 계산하는 것이 여기서 설정된 빈도 값 만큼 실행된다.   eureka.server.enable-self-preservation = true   Self-Preservation 모드를 켜고 끌 수 있는 설정이다. 가급적이면 운영 상황에서는 끄지 않는 것을 권장한다.       참고    The Mystery of Eureka Self-Preservation  ","categories": ["posts"],
424        "tags": ["msa","eureka"],
425        "url": "https://subji.github.io/posts/2020/08/11/springcloudeurekaregistry",
426        "teaser":null},{
427        "title": "java.sql.SQLException: Field 'id' doesn't have a default value 문제 해결",
428        "excerpt":"Problem   분명 연관관계 매핑도 제대로하고 테이블에도 Not Null 로 설정을 해두었다. 그런데 자꾸 ‘doesn’t have a default value’ 문제가 발생한다.       How to solve?   아무리 찾아도 코드상에는 오류가 없었다. 그렇게 끙끙 앓다가 스택오버플로우에서 한줄기 빛을 찾게 되었다.   Hibernate: “Field ‘id’ doesn’t have a default value”   여기에서 100 표를 넘게 받은 답변을 보면 알 수 있다.   “경우에 따라서 모델이나 ORM은 데이터베이스의 변경사항이 정확하게 반영되지 않을 수 있다. (SchemeUpdate),    코드상의 오류나 정확히 파악할 수 있는 오류가 아니고 원인을 알 수 없는 오류의 경우에는 데이터베이스 또는 테이블을 다시 만들고 재실행을 하는것을 권한다. (SchemeExport)”   그렇다. 나의 경우에도 정확히 원인을 뭐라고 말할 수 없을뿐더라 찾을 수도 없었기에 위 답변이 맞는말이었다. 그래서 오류가 나는 컬럼이 포함된 테이블을 드랍하고 다시 추가하였다.   서버를 재실행하니 잘된다..       Why?   그렇다면 뭐가 문제일까 도대체?   대부분의 글에선 Hibernate 에서 발생하는 오류라기보단 특징? 이라고 한다. hibernate 는 DB를 추상화하는 도구로써 테이블이나 컬럼에 대해서 아주 정확한 매핑을 하
428지 않는다고 한다.   그래서 데이터 타입이 다른데도 매핑시키거나 한다고 한다. 그래서 중간에 테이블이나 컬럼의 설정을 변경하면 반영이 안되는 것일 수도 있다고 생각한다.   추가로 테이블 또는 컬럼의 변경사항을 좀 더 잘 적용되게 하려면 hbm2ddl.auto 옵션에서 update 를 사용하면 된다고한다. 하지만 update 옵션에도 hibernate 의 느슨한 매핑 특징은 완전히 처리가 안되는것같다.   hbm2ddl.auto 의 옵션은 다음과 같다.      create: SessionFactory 시작시 테이블 삭제하고 다시 생성   create-drop: create + SessionFactory 종료시 테이블 삭제   update: SessionFactory 시작시 컬럼 추가/삭제 수행. 데이터 삭제 안함   validate: SessionFactory 시작시 테이블 컬럼 확인하여 다르면 예외 발생       참고    하이버네이트의 hbm2ddl.auto에 update가 좋을까? validate가 좋을까?  ","categories": ["posts"],
429        "tags": ["jpa","hibernate"],
430        "url": "https://subji.github.io/posts/2020/08/20/jpadoesnthaveadefaultvalue",
431        "teaser":null},{
432        "title": "MapStruct Dependency 추가 시 빌드 오류 문제",
433        "excerpt":"Problem   MapStruct 를 사용하기 위해 의존성을 추가하고 mvn clean install 을 실행하는데, “cannot find symbol” 오류를 마구 뱉어냈다.       How to solve?   먼저 MapStruct 를 사용하기 위해서는 pom.xml 에 다음과 같이 추가해줘야한다.   &lt;properties&gt;   &lt;org.mapstruct.version&gt;1.3.1.Final&lt;/org.mapstruct.version&gt;   &lt;org.projectlombok.version&gt;1.18.12&lt;/org.projectlombok.version&gt; &lt;/properties&gt;  &lt;dependencies&gt;   &lt;dependency&gt;     &lt;groupId&gt;org.mapstruct&lt;/groupId&gt;     &lt;artifactId&gt;mapstruct&lt;/artifactId&gt;     &lt;version&gt;${org.mapstruct.version}&lt;/version&gt;   &lt;/dependency&gt;   &lt;dependency&gt;     &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;     &lt;artifactId&gt;lombok&lt;/artifactId&gt;     &lt;version&gt;${org.projectlombok.version}&lt;/version&gt;     &lt;scope&gt;provided&lt;/scope&gt;   &lt;/dependency&gt;   &lt;!-- ... --&gt; &lt;/dependencies&gt;  &lt;plugins&gt;   &lt;plugin&gt;     &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;     &lt;artifactId&gt;maven-compiler-plugin&lt;/artifactId&gt;     &lt;configuration&gt;       &lt;!-- mapstruct 를 사용할 때 lombok 과 충돌이 발생한다. 그래서 lombok 에 대한 path 도 추가해준다. --&gt;       &lt;annotationProcessorPaths&gt;         &lt;path&gt;           &lt;groupId&gt;org.mapstruct&lt;/groupId&gt;           &lt;artifactId&gt;mapstruct-processor&lt;/artifactId&gt;           &lt;version&gt;${org.mapstruct.version}&lt;/version&gt;         &lt;/path&gt;       &lt;/annotationProcessorPaths&gt;     &lt;/configuration&gt;   &lt;/plugin&gt; &lt;/plugins&gt;   여기서 빌드를 하면 앞서 말한 오류가 마구 발생한다. 원인을 찾아보니 누군가가 친절하게 설명해줬다. 답은 간단하다. mapstruct-processor dependency 가 조금은 다른 방식으로 호출 되기에 annotaionProcessorPaths 에 작성하게 된다. 이떄, 만약 lombok 을 같이 사용하고 있다면, 충돌이 발생한다고 한다. 정확한 이유는 더 찾아봐야겠다.   방법은 두가지다.      lombok dependency 도 annotationProcessorPaths 에 추가해준다.   scope 를 provided 로 작성하는 방법이다.   Lombok/Mapstruct problem: Cannot find symbol   이를 토대로 다시 pom.xml 을 작성해보자. 아래는 1번 방법을 적용한 코드이다.   &lt;properties&gt;   &lt;org.mapstruct.version&gt;1.3.1.Final&lt;/org.mapstruct.version&gt;   &lt;org.projectlombok.version&gt;1.18.12&lt;/org.projectlombok.version&gt; &lt;/properties&gt;  &lt;dependencies&gt;   &lt;dependency&gt;     &lt;groupId&gt;org.mapstruct&lt;/groupId&gt;     &lt;artifactId&gt;mapstruct&lt;/artifactId&gt;     &lt;version&gt;${org.mapstruct.version}&lt;/version&gt;   &lt;/dependency&gt;   &lt;dependency&gt;     &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;     &lt;artifactId&gt;lombok&lt;/artifactId&gt;     &lt;version&gt;${org.projectlombok.version}&lt;/version&gt;     &lt;scope&gt;provided&lt;/scope&gt;   &lt;/dependency&gt;   &lt;!-- ... --&gt; &lt;/dependencies&gt;  &lt;plugins&gt;   &lt;plugin&gt;     &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;     &lt;artifactId&gt;maven-compiler-plugin&lt;/artifactId&gt;     &lt;configuration&gt;       &lt;!-- mapstruct 를 사용할 때 lombok 과 충돌이 발생한다. 그래서 lombok 에 대한 path 도 추가해준다. --&gt;       &lt;annotationProcessorPaths&gt;         &lt;path&gt;           &lt;groupId&gt;org.mapstruct&lt;/groupId&gt;           &lt;artifactId&gt;mapstruct-processor&lt;/artifactId&gt;           &lt;version&gt;${org.mapstruct.version}&lt;/version&gt;         &lt;/path&gt;         &lt;path&gt;           &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;           &lt;artifactId&gt;lombok&lt;/artifactId&gt;           &lt;version&gt;${org.projectlombok.version}&lt;/version&gt;         &lt;/path&gt;       &lt;/annotationProcessorPaths&gt;     &lt;/configuration&gt;   &lt;/plugin&gt; &lt;/plugins&gt;  더 알아보니 mapstruct 공식 github 에도 참고할 만한 코드가 몇개 올라와있었다.       참고    mapstruct/mapstruct-examples  ","categories": ["posts"],
434        "tags": ["lombok","mapstruct","maven"],
435        "url": "https://subji.github.io/posts/2020/08/20/mapstructandlombokcrash",
436        "teaser":null},{
437        "title": "로그인 다시 안하고 사용자 권한 업데이트 & 변경하기",
438        "excerpt":"Problem   회원가입, 멤버십 구매등의 로직을 개발하면서 구매 완료시점에서 사용자의 권한을 변경 또는 추가를 해줘야했다. 단순한 방법으로는 가입 및 구매 완료 후 다시 로그인 화면으로 옮기는 방법이 있다.   하지만 이 방법은 사용자로 하여금 불편한? 과정일 것이다. 그래서 찾아보니 로그아웃 - 로그인 없이 Spring 의 Security 에 있는 SecurityContext 의 값을 변경해주면 되는 방법이 있었다.   다음은 간단한 사용방법의 예이다.       How to solve?   Authentication auth = SecurityContextHolder.getContext().getAuthentication();  List&lt;GrantedAuthority&gt; updatedAuthorities = new ArrayList&lt;&gt;(auth.getAuthorities()); updatedAuthorities.add(...); //add your role here [e.g., new SimpleGrantedAuthority(\"ROLE_NEW_ROLE\")]  Authentication newAuth = new UsernamePasswordAuthenticationToken(auth.getPrincipal(), auth.getCredentials(), updatedAuthorities);  SecurityContextHolder.getContext().setAuthentication(newAuth);   나는 Keycloak 을 사용하고 있어서 Keycloak 버전도 올려본다.     List&lt;GrantedAuthority&gt; newRoles = new ArrayList&lt;&gt;();    if (result.getStatusCode().is2xxSuccessful()) {     newRoles = result.getBody().stream().map(       res -&gt; {         // {keycloak auth url}/auth/admin/realms/{realm name}/users/{user id(uuid)}/role-mappings/clients/{clientid{uuid}, containerid};         // 요청 후 받아온 값에서 Security 의 기본 접두사인 ROLE_ 이 생략되어 있으므로 추가해주어야 한다.          // 추가하지 않으면 Security 에서 원할한 검사가 되지 않는다.         String role = \"ROLE_\" + ((String) res.get(\"name\"));         return new KeycloakRole(role);       }     )     .collect(Collectors.toList());   }    Authentication auth = SecurityContextHolder.getContext().getAuthentication();   KeycloakAuthenticationToken existToken = (KeycloakAuthenticationToken) auth;    List&lt;GrantedAuthority&gt; oldRoles = auth.getAuthorities().stream().map(authority -&gt; new KeycloakRole(authority.getAuthority())).collect(Collectors.toList());   newRoles.addAll(oldRoles);    Authentication newAuth = new KeycloakAuthenticationToken(existToken.getAccount(), existToken.isInteractive(), newRoles);   SecurityContextHolder.getContext().setAuthentication(newAuth);       참고    How to reload authorities on user update with Spring Security  ","categories": ["posts"],
439        "tags": ["spring security","spring"],
440        "url": "https://subji.github.io/posts/2020/09/03/updaterealtimeroleinsecuritywithoutlogin",
441        "teaser":null},{
442        "title": "이클립스 프로젝트를 VSCODE 에 Import 할 때 이슈",
443        "excerpt":"Problem   이클립스에서 만든 스프링 부트 프로젝트를 vscode 에서 사용할때 classpath 를 못찾는다는 ì˜
443¤ë¥˜ê°€ 나와 실행이 안된다.       How to solve?   ~ mvn eclipse:eclipse    위에 명령어를 사용하면 이클립스 프로젝트로 변환이 되며 classpath 파일이 추가된다.       참고    Visual Studio Code - Java Classpath is incomplete. Only syntax errors will be reported  ","categories": ["posts"],
444        "tags": ["vscode","eclipse"],
445        "url": "https://subji.github.io/posts/2020/09/06/vscodeimporteclipse",
446        "teaser":null},{
447        "title": "CentOS 에서 Redis 여러개 실행하기",
448        "excerpt":"Topic   master-slave-sentinal 구조로 Redis 를 구성하기 위해서 여러 방법을 생각해봤다. 첫째는 하나의 서버에 여러개의 slave 서버를 띄우는 방법. 두번째는 여러개의 서버에 각각 하나씩 slave 를 띄우는 방법.   각각의 장단점은 있다. 첫번째는 여러 서버를 확인하지 않아도 되는 관리하는 관점에서 쉽다는 이점이 있다. 두번째는 그래도 첫번째보다는 더 나은 안정성? 이 있을 수 있겠다.   Redis 를 여러개로 구성하는 이유에 따라 나뉘겠지만, 나는 여기서 Redis 를 단순히 세션 클러스터링으로 사용하기로 했기 때문에 하나의 서버에 여러개의 Redis 를 띄우기로 했다.       How to solve?   1. redis rdb 파일이 작성되는 디렉토리 생성     rdb 파일 경로의 경우 Redis 에서 자동으로 생성해주지 않기 때문에 직접 만들어 주고 사용자, 그룹 권한(redis) 도 주어야 한다.   다음은 redis2 라는 새로운 서버를 생성하는 경우의 예제이다.     mkdir -p /var/lib/redis2/ chown redis /var/lib/redis2/ chgrp redis /var/lib/redis2/ # 또는 chown redis:redis /var/lib/redis2/          2. 설정 파일 추가       기본으로 설치된 설정 파일을 복사해 몇가지만 수정한다.   slaveof 에서 master 의 host 와 port 를 적는다.   masterauth 와 requirepass 에 master 에서 설정한 패스워드를 적는다.   나머지는 구글링으로 필요한 내용을 작성했다.   사용자 권한을 부여한다. 권한을 부여하지 않으면 실행 스크립트에서 오류가 발생한다.     cp /etc/redis.conf /etc/redis2.conf chown redis /etc/redis2.conf  slaveof 127.0.0.1 6379 masterauth {password} requirepass {password} --- redis2.conf          3.서비스 스크립트 생성       서비스 스크립트의 실행 시작, 종료 부분을 변경한다.   daemonize no 를 추가하지 않으면 데이터 삽입 시 충돌이 발생한다.     cp /usr/lib/systemd/system/redis.service /usr/lib/systemd/system/redis2.service  ------ redis2.service file ------ [Service] ExecStart=/usr/bin/redis-server /etc/redis2.conf --daemonize no ExecStop=/usr/bin/redis-shutdown redis2 ---------------------------------  systemctl enable redis2 systemctl start redis2               참고    [REDIS] 를 CentOS에 레디 스의 여러 인스턴스를 실행  ","categories": ["posts"],
449        "tags": ["redis"],
450        "url": "https://subji.github.io/posts/2020/10/08/howtomultipleinstanceofredisoncentos",
451        "teaser":null},{
452        "title": "Apache 설정 (with SSL)",
453        "excerpt":"아파치(httpd) 설정      Ubuntu 에서는 /etc/apache2 에 있고 CentOS 같은 Redhat 계열은 /etc/httpd 아래에 존재한다.   가상 호스트(Virtual Host) 별 Config 파일을 sites-availiable 밑에 생성하고, sites-enabled 에 소프트 링크를 추가하였다.   httpd.conf   # virtual host setting IncludeOptional sites-enabled/*.conf   가상호스트 SSL 설정      mod_ssl 을 반드시 설치하고 활성화 해야한다.   checking mobile device 는 모바일 기기로 접속하였을 때, 다른 도메인으로 이동시키기 위한 설정이다.            모바일 기기 리다이렉팅은 조금 고생했는데. virtualhost:80 번에서 모바일 기기를 검사하는 RewriteCond,Rule 설정을 추가해도 계속 같은 페이지만 불러오고 모바일 페이지로는 이동이 되지 않았다.       곰곰히 생각해보니 앞에서 http 에서 https 로 보내는 설정이 있었다. 그렇다면 모바일 기기 체크는 http 가 아니라 https 에서 해야되는게 아닌가?       그래서 80 포트에 작성한 설정을 443 번으로 옮겼다.       결과는 성공했다. 다만 무한 리다이렉트라는 또다른 문제가 발생했다.       그래서 R=301 설정으로 강제 리다이렉팅하고, L 로 rule 을 끝내버렸다. NE 는 이스케이프 문자를 허용하지 않는 조건이다.(여기서는 크게 상관없는듯하다).           Redirect http to https 는 http 로 접속했을 때, 강제로 https 로 리다이렉트하는 설정이다.   example.conf ```sh           &lt;VirtualHost {domain name(DNS)}:443&gt;                 ServerName {domain name(DNS) include \"www\"}                 ServerAlias {domain name(DNS)}                 DocumentRoot {web application path}                  SSLEngine on                 SSLProtocol All -SSLv2 -SSLv3                 SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128:DHES-RSA-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHAAES256-SHA:AES:CAMESIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA                 SSLHonorCipherOrder on \t\t\t\t\t\t\t\t# case of Setigo RSA                 SSLCertificateFile {SSL file path}/_wildcard_{...}.crt                 SSLCertificateKeyFile {SSL file path}/_wildcard_{...}.key                 SSLCertificateChainFile {SSL file path}/ChainCA/rsa-dv.chain-bundle.pem                 SSLCACertificateFile {SSL file path}/ChainCA/AddTrustExternalCARoot.crt                  SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire                  ProxyPass / http://127.0.0.1:{PORT}/ connectiontimeout=900 timeout=900                 ProxyPassReverse / http://127.0.0.1:{PORT}/                 ProxyPreserveHost On                  RequestHeader set X-Forwarded-Proto \"https\"                 RequestHeader set X-Forwarded-Port \"443\"                  # CORS Config                 Header always set Access-Control-Allow-Origin \"*\" \t\t\t\t\t                 ErrorDocument 503 {error html file path} \t\t\t\t\t\t\t\t# Checking Mobile device                 RewriteEngine On                 RewriteCond %{QUERY_STRING} !^desktop                 RewriteCond %{HTTP_USER_AGENT} \"android|blackberry|googlebot-mobile|iemobile|ipad|iphone|ipod|#opera mobile|palmos|webos\" [NC,OR]                 RewriteCond %{HTTP_USER_AGENT} \"acs|alav|alca|amoi|audi|aste|avan|benq|bird|blac|blaz|brew|cell|cldc|cmd-\" [NC,OR]                 RewriteCond %{HTTP_USER_AGENT} \"dang|doco|eric|hipt|inno|ipaq|java|jigs|kddi|keji|leno|lg-c|lg-d|lg-g|lge-\" [NC,OR]                 RewriteCond %{HTTP_USER_AGENT}  \"maui|maxo|midp|mits|mmef|mobi|mot-|moto|mwbp|nec-|newt|noki|opwv\" [NC,OR]                 RewriteCond %{HTTP_USER_AGENT} \"palm|pana|pant|pdxg|phil|play|pluc|port|prox|qtek|qwap|sage|sams|sany\" [NC,OR]                 RewriteCond %{HTTP_USER_AGENT} \"sch-|sec-|send|seri|sgh-|shar|sie-|siem|smal|smar|sony|sph-|symb|t-mo\" [NC,OR]                 RewriteCond %{HTTP_USER_AGENT} \"teli|tim-|tosh|tsm-|upg1|upsi|vk-v|voda|w3cs|wap-|wapa|wapi\" [NC,OR]                 RewriteCond %{HTTP_USER_AGENT} \"wapp|wapr|webc|winw|winw|xda|xda-\" [NC,OR]                 RewriteCond %{HTTP_USER_AGENT} \"up.browser|up.link|windowssce|iemobile|mini|mmp\" [NC,OR]                 RewriteCond %{HTTP_USER_AGENT} \"symbian|midp|wap|phone|pocket|mobile|pda|psp\" [NC]                 RewriteRule ^(.*)$ https://m.%{HTTP_HOST}$1 [R=301,NE,L]                  &lt;Directory \"{web application path}\"&gt;                         Options none                         AllowOverride none                         Order allow,deny                         Allow from all                         Require all granted                 &lt;/Directory&gt; \t\t\t\t&lt;/VirtualHost&gt;         &lt;VirtualHost *:80&gt;                 ServerName {domain name(DNS) include \"www\"}                 ServerAlias {domain name(DNS)}                 DocumentRoot {web application path}                  Header always set Access-Control-Allow-Origin \"*\" \t\t\t\t\t\t\t\t# Redirect http to https                 RewriteEngine On                 RewriteCond %{HTTPS} !=on [NC]                 RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R,L]                  ProxyPreserveHost On                 &lt;Directory \"{web application path}\"&gt;                         Options none                         AllowOverride none                         Order allow,deny                         Allow from all                         Require all granted                 &lt;/Directory&gt;         &lt;/VirtualHost&gt;  ```   참고      Apache2.4 - mod_rewrite Reference   mod_rewrite 로 URL 다루기   Using multiple rewrite rules?   https://stackoverflow.com/questions/3680463/mobile-redirect-using-htaccess  ","categories": ["posts"],
454        "tags": ["apache","httpd"],
455        "url": "https://subji.github.io/posts/2020/11/27/goodtoknowregex",
456        "teaser":null},{
457        "title": "알아두면 좋은 정규식(Regular Expression) 모음",
458        "excerpt":"업데이트 일자 2020-11-27      학교 메일 검사 (ex. “ac.kr”, “x.{학교 도메인}.ac.kr”, 등등)     /[\\@]{1}[a-z0-9A-Z\\.]*(?=\\.ac\\.kr)/gi      ","categories": ["posts"],
459        "tags": ["regular expression"],
460        "url": "https://subji.github.io/posts/2020/11/27/howtosetapachesettingwithssl",
461        "teaser":null},{
462        "title": "MacOS BigSur 에서 Java 환경변수 못찾는 오류",
463        "excerpt":"문제   BigSur 로 업데이트를 하고나서 VSCode 에서 스프링 개발좀 하려고 하는데 스프링 부트 대시보드가 보이지 않고 자바 커맨드도 찾을 수 없다는 ì˜
463¤ë¥˜ê°€ 나왔다.         해결   BigSur 로 업데이트되고 나서 바뀐 자바 환경 때문에 발생하는 오류였다. BigSur 에서는 기본적으로 /usr/bin/javac 를 자바 기본 환경변수로 정해져있다. 즉, jdk 경로를 심볼릭 링크로 정해놓지 않는다. 그래서 java 커맨드를 사용할때 올바른 동작을 할 수 없었다.       먼저 현재 설치된 jdk 의 경로를 보기위해 아래 커맨드를 입력한다. 그러면 설치된 jdk 의 경로가 나온다.  /usr/libexec/java_home -V  Matching Java Virtual Machines (1):     15.0.1 (x86_64) \"AdoptOpenJDK\" - \"AdoptOpenJDK 15\" /Library/Java/JavaVirtualMachines/adoptopenjdk-15.jdk/Contents/Home /Library/Java/JavaVirtualMachines/adoptopenjdk-15.jdk/Contents/Home # jdk 경로      jdk 경로를 확인했다면 이제 JAVA_HOME 을 설정해서 /usr/bin/javac 의 환경변수를 연결해준다. Mac 은 zsh 가 기본 쉘로 설정되어있기 때문에 .zshrc 파일을 열어서 다음 값을 입력하고 저장한다.  vi ~/.zshrc  # .zshrc file  JAVA_HOME=`/usr/libexec/java_home`  source ~/.zshrc      마지막으로 JAVA_HOME 이 잘 설정되었는지 확인한다. 결과가 jdk 경로로 나온다면 성공이다.  echo $JAVA_HOME  /Library/Java/JavaVirtualMachines/adoptopenjdk-15.jdk/Contents/Home      참고   Error in every java file after updating to macOS Big Sur  ","categories": ["posts"],
464        "tags": ["java","mac"],
465        "url": "https://subji.github.io/posts/2020/12/25/bigsurjavaerror",
466        "teaser":null},{
467        "title": "Gradle : Could not initialize class org.codehaus.groovy.runtime.InvokerHelper Error",
468        "excerpt":"문제   Gradle Test 를 하는 과정에서 Could not initialize class org.codehaus.groovy.runtime.InvokerHelper Error 오류가 발생하였다.       해결   Gradle 5.x 버전에서 jdk14 버전을 지원히지 않는 과정에서 발생하는 오류였다.   Gradle 버전을 5.x 에서 6.x 버전으로 업그레이드하면 해결된다.   gradle/wrapper/gradle-wrapper.properties  # Gradle distribution version url  # old version # distributionUrl=https\\://services.gradle.org/distributions/gradle-5.6.4-bin.zip  # new version # distributionUrl=https\\://services.gradle.org/distributions/gradle-6.3-bin.zip  ","categories": ["posts"],
469        "tags": ["gradle"],
470        "url": "https://subji.github.io/posts/2020/12/28/GradleCouldnotinitializeclassorgcodehausgroovyruntimeInvokerHelper",
471        "teaser":null},{
472        "title": "Java 월 주차 구하기",
473        "excerpt":"월 주차 계산 방법   우리나라 기준으로 월 주차는 한 주의 시작을 월요일로 한다. 매달 첫째주는 이전달의 마지막주이냐 이번달의 첫째주이냐가 특히 고민이다. 간단하게 확인하는 방법이 있다. 해당 주에 목요일로 판단할 수 있다.   예를들어 다음 그림에 있는 첫째주는 9월의 마지막 주일까 10월의 첫째주 일까?      정답은 10월의 첫째주이다. 앞서 말한대로 10월 1일이 목요일부터 시작함으로써 10월에 첫째주가 되는 것이다. 반대로 9월 30일이 목요일이면 9월의 마지막주가 되는것이다.  사실 목요일로 구분하는건 굉장히 간단한 방법이다. 정확한 방법은 첫주에서 4일이상을 차지하는 달의 주로 결정되는 것이다. 즉 월요일 부터 한주가 시작하는 우리나라의 경우 목요일부터 일요일까지 4일이 되므로 간단하게 목요일로 결정 할 수 있는 것이다.   그럼 내 나름대로 이리저리 월의 주차를 계산하는 코드를 작성해보았다.   public class CalendarUtil {    /**    * 파라미터로 전달 된 날짜의 1일의 주차 계산     * 1일이 목요일(5) 보다 클 경우 첫째 주 이므로 0을 반환    * 1일이 월 ~ 목 이외의 날짜 일 경우 -1 을 반환.    * 1일이 목요일(5) 보다 작으면 첫째 주가 아니므로 1을 반환    * @param year    * @param month    * @param day    * @return    */   public static int subWeekNumberIsFirstDayAfterThursday(int year, int month, int day)  {     Calendar calendar = Calendar.getInstance(Locale.KOREA);     calendar.set(year, month - 1, day);     calendar.set(Calendar.DAY_OF_MONTH, 1);     calendar.setFirstDayOfWeek(Calendar.MONDAY);      int weekOfDay = calendar.get(Calendar.DAY_OF_WEEK);      if ((weekOfDay &gt;= Calendar.MONDAY) &amp;&amp; (weekOfDay &lt;= Calendar.THURSDAY)) {       return 0;     } else if (day == 1 &amp;&amp; (weekOfDay &lt; Calendar.MONDAY || weekOfDay &gt; Calendar.TUESDAY))  {       return -1;     } else {       return 1;     }   }    /**    * 해당 날짜가 마지막 주에 해당하고 마지막주의 마지막날짜가 목요일보다 작으면     * 다음달로 넘겨야 한다 +1    * 목요일보다 크거나 같을 경우 이번달로 결정한다 +0    * @param year    * @param month    * @param day    * @return    */   public static int addMonthIsLastDayBeforeThursday(int year, int month, int day) {     Calendar calendar = Calendar.getInstance(Locale.KOREA);     calendar.setFirstDayOfWeek(Calendar.MONDAY);     calendar.set(year, month - 1, day);      int currentWeekNumber = calendar.get(Calendar.WEEK_OF_MONTH);     int maximumWeekNumber = calendar.getActualMaximum(Calendar.WEEK_OF_MONTH);      if (currentWeekNumber == maximumWeekNumber) {       calendar.set(year, month - 1, calendar.getActualMaximum(Calendar.DAY_OF_MONTH));       int maximumDayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);        if (maximumDayOfWeek &lt; Calendar.THURSDAY &amp;&amp; maximumDayOfWeek &gt; Calendar.SUNDAY) {         return 1;       } else {         return 0;       }     } else {       return 0;     }   }    /**    * 해당 날짜의 주차를 반환    * @param year    * @param month    * @param day    * @return    */   public static String getCurrentWeekOfMonth(int year, int month, int day)  {     int subtractFirstWeekNumber = subWeekNumberIsFirstDayAfterThursday(year, month, day);     int subtractLastWeekNumber = addMonthIsLastDayBeforeThursday(year, month, day);      // 마지막 주차에서 다음 달로 넘어갈 경우 다음달의 1일을 기준으로 정해준다.      // 추가로 다음 달 첫째주는 목요일부터 시작하는 과반수의 일자를 포함하기 때문에 한주를 빼지 않는다.     if (subtractLastWeekNumber &gt; 0) {       day = 1;       subtractFirstWeekNumber = 0;     }      if (subtractFirstWeekNumber &lt; 0)  {       Calendar calendar = Calendar.getInstance(Locale.KOREA);       calendar.set(year, month - 1, day);       calendar.add(Calendar.DATE, -1);        return getCurrentWeekOfMonth(calendar.get(Calendar.YEAR), (calendar.get(Calendar.MONTH) + 1), calendar.get(Calendar.DATE));     }      Calendar calendar = Calendar.getInstance(Locale.KOREA);     calendar.setFirstDayOfWeek(Calendar.MONDAY);     calendar.setMinimalDaysInFirstWeek(1);     calendar.set(year, month - (1 - subtractLastWeekNumber), day);      int dayOfWeekForFirstDayOfMonth = calendar.get(Calendar.WEEK_OF_MONTH) - subtractFirstWeekNumber;      return (calendar.get(Calendar.MONTH) + 1) + \",\" + dayOfWeekForFirstDayOfMonth;   }  }  ","categories": ["posts"],
474        "tags": ["java"],
475        "url": "https://subji.github.io/posts/2021/01/06/javagetweeknumberinkr",
476        "teaser":null},{
477        "title": "Spring REST Docs 소개 및 예제",
478        "excerpt":"Spring Rest Docs   생성일: 2021년 1월 5일 오전 9:19   해당 내용은 공식 문서 2.0.5.RELEASE 버전을 참고 및 토대로 작성하였습니다.   Overview     Spring REST Docs 는 RESTful 서비스의 문서화를 도와주는 도구이다. Spring REST Docs 는 문서 ì
478ž‘성 도구로 기본적으로 Asciidoctor 를 사용하며, 이것을 사용해 HTML 을 생성한다. 필요한 경우 Markdown 을 사용하도록 변경할 수 있다.   Spring REST Docs 는 Spring MVC 의 테스트 프레임 워크, Spring WebFlux WebTestClient 또는 Rest Assured 3 로 작성된 테스트 코드에서 생성된 Snippet 을 사용한다. 테스트 기반 접근 방식은 서비스 문서의 정확성을 보장해준다. Snippet 이 올바르지 않을 경우 테스트가 실패하기 때문이다.   RESTful 서비스를 문서화 하는 것은 해당 리소스를 설명하는 것이다. 리소스 설명의 핵심은 HTTP 요청과 HTTP 응답의 세부정보이다. Spring REST Docs 를 사용하면 리소스와 HTTP 요청 및 응답을 사용하여 서비스 내부의 세부 정보로부터 문서를 보호할 수 있다. 즉, 서비스 내부 코드에 추가 또는 수정과 같이 영향을 주지 않고도 별도의 문서화 작업을 진행할 수 있게 된다. 이는 서비스 구현보다 API 를 문서화 하는데 도움이 된다. 또한 문서를 재 작업 하지 않고도 버전에 따라 변경할 수 있다.   많이 사용되는 RESTful API 문서도구로는 대표적으로 Swagger 가 있다. 아래 표는 Swagger 와 Spring REST Docs 의 비교 내용이다.                  Spring REST Docs       Swagger                       코드의 추가 및 수정이 없다.       코드에 어노테이션등을 추가해야 한다.                 테스트 코드 작성이 필요하며, 테스트 성공 시 문서가 생성된다.       테스트 코드 없이 서비스 쪽 코드 및 어노테이션 추가로 문서를 생성할 수 있다.                 버전 변화에 유연하고 정확성이 높다.       버전 변화에 맞춰 재 작성해야 하며 이를 하지 않을 때 정확성이 낮다.                                     자세한 내용은 여기를 참고한다.          시작하기    샘플 애플리케이션   여러 샘플 응용 프로그램을 사용할 수 있다.     MockMvc  | Sample | Build System| Description | ————–| — | — | |Spring Data REST|Maven|Spring Data REST 를 사용| |Spring HATEOAS|Gradle|Spring HATEOAS 를 사용|    WebTestClient  | Sample | Build System| Description | ————–| — | — | |WebTestClient|Gradle|Spring WebFlux 의 WebTestClient 를 사용|    REST Assured  | Sample | Build System| Description | ————–| — | — | |REST Assured|Gradle|Spring WebFlux 의 WebTestClient 를 사용|    고급  | Sample | Build System| Description | ————–| — | — | |Slate|Gradle|Markdown 및 Slate 를 사용| |TestNG|Gradle|TestNG 사용| |JUnit 5|Gradle|JUnit 5 를 사용|      요구 사항      Java 8 &gt;   Spring Framework 5 (5.0.2) &gt;   빌드 시스템 설정           Maven         &lt;!--    \t\"test\" scope 를 가지는 spring-restdocs-mockmvc 의존성 추가    \t또는 spring-restdocs-webtestclient, spring-restdocs-restassured 추가   --&gt;   &lt;dependency&gt;    \t&lt;groupId&gt;org.springframework.restdocs&lt;/groupId&gt;   \t&lt;artifactId&gt;spring-restdocs-mockmvc&lt;/artifactId&gt;   \t&lt;version&gt;{project-version}&lt;/version&gt;   \t&lt;scope&gt;test&lt;/scope&gt;   &lt;/dependency&gt;    &lt;build&gt;   \t&lt;plugins&gt;   \t\t&lt;!-- asciidoctor 플러그인 추가 --&gt;   \t\t&lt;plugin&gt;    \t\t\t&lt;groupId&gt;org.asciidoctor&lt;/groupId&gt;   \t\t\t&lt;artifactId&gt;asciidoctor-maven-plugin&lt;/artifactId&gt;   \t\t\t&lt;version&gt;1.5.8&lt;/version&gt;   \t\t\t&lt;executions&gt;   \t\t\t\t&lt;execution&gt;   \t\t\t\t\t&lt;id&gt;generate-docs&lt;/id&gt;   \t\t\t\t\t&lt;!-- \"prepare-package\" 옵션은 패키지내에 API 문서를 포함할 수 있게 한다. --&gt;   \t\t\t\t\t&lt;phase&gt;prepare-package&lt;/phase&gt;    \t\t\t\t\t&lt;goals&gt;   \t\t\t\t\t\t&lt;goal&gt;process-asciidoc&lt;/goal&gt;   \t\t\t\t\t&lt;/goals&gt;   \t\t\t\t\t&lt;configuration&gt;   \t\t\t\t\t\t&lt;backend&gt;html&lt;/backend&gt;   \t\t\t\t\t\t&lt;doctype&gt;book&lt;/doctype&gt;   \t\t\t\t\t&lt;/configuration&gt;   \t\t\t\t&lt;/execution&gt;   \t\t\t&lt;/executions&gt;   \t\t\t&lt;dependencies&gt;   \t\t\t\t&lt;!--    \t\t\t\t\t\"asciidoctor\" 의 의존성으로 spring-restdocs-asciidoctor 추가    \t\t\t\t\t.adoc 파일이 target/generated-snippets 아래에 생성된 Snippet 을 가리키는    \t\t\t\t\t설정이 추가된다.    \t\t\t\t\t또한 operation 블록 매크로를 사용할 수 있다.    \t\t\t\t--&gt;   \t\t\t\t&lt;dependency&gt;    \t\t\t\t\t&lt;groupId&gt;org.springframework.restdocs&lt;/groupId&gt;   \t\t\t\t\t&lt;artifactId&gt;spring-restdocs-asciidoctor&lt;/artifactId&gt;   \t\t\t\t\t&lt;version&gt;{project-version}&lt;/version&gt;   \t\t\t\t&lt;/dependency&gt;   \t\t\t&lt;/dependencies&gt;   \t\t&lt;/plugin&gt;   \t&lt;/plugins&gt;   &lt;/build&gt;                Gradle         plugins {    \t// Asciidoctor 플러그인 적용   \tid \"org.asciidoctor.convert\" version \"1.5.9.2\"   }    dependencies {   \t/*    \t\tasciidocker 에 대한 spring-restdocs-asciidocker 의존성 추가   \t\tMaven 처럼 build/generated-snippets 밑에 생성된 Snippet 을 .adoc 파일이 자동으로   \t\t가리키도록 하는 설정 추가.   \t\toperation 블록 매크로 사용 가능   \t*/   \tasciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor:{project-version}'    \t// Maven ê³¼ 같이 test Scope 에 대한 mockMvc 의존성을 추가 (WebClient, Assured 사용가능)    \ttestCompile 'org.springframework.restdocs:spring-restdocs-mockmvc:{project-version}'    }
478    ext {    \t// Snippet 의 생성 위치를 지정   \tsnippetsDir = file('build/generated-snippets')   }    test {    \t// Snippets 디렉토리를 출력으로 작업하도록 설정   \toutputs.dir snippetsDir   }    asciidoctor {    \t// Snippets 디렉토리를 Input 디렉토리로 설정   \tinputs.dir snippetsDir    \t// 문서 생성 전 테스트가 실행되도록 test 에 종속 설정   \tdependsOn test    }           문서 패키징   생성된 API 문서를 jar 또는 war 에 패키징하기 위한 설정이다. 이는 Spring Boot 에 의해 정적 콘텐츠로 제공된다.      jar 가 빌드되기 전에 문서가 생성.   생성된 문서는 jar 에 포함.           Maven         &lt;plugin&gt;    \t&lt;!-- Asciidoctor 플러그인 선언 --&gt;   \t&lt;groupId&gt;org.asciidoctor&lt;/groupId&gt;   \t&lt;artifactId&gt;asciidoctor-maven-plugin&lt;/artifactId&gt;   &lt;/plugin&gt;   &lt;plugin&gt;    \t&lt;!--   \t\t문서가 패키지로 복사되기 전에 생성되야 하므로 위에 Asciidoctor 플러그인 뒤에 선언한다.   \t--&gt;   \t&lt;artifactId&gt;maven-resources-plugin&lt;/artifactId&gt;   \t&lt;version&gt;2.7&lt;/version&gt;   \t&lt;executions&gt;   \t\t&lt;execution&gt;   \t\t\t&lt;id&gt;copy-resources&lt;/id&gt;   \t\t\t&lt;phase&gt;prepare-package&lt;/phase&gt;   \t\t\t&lt;goals&gt;   \t\t\t\t&lt;goal&gt;copy-resources&lt;/goal&gt;   \t\t\t&lt;/goals&gt;   \t\t\t&lt;configuration&gt;    \t\t\t  &lt;!-- 생성된 문서를 static/docs 에 복사 --&gt;   \t\t\t\t&lt;outputDirectory&gt;   \t\t\t\t\t${project.build.outputDirectory}/static/docs   \t\t\t\t&lt;/outputDirectory&gt;   \t\t\t\t&lt;resources&gt;   \t\t\t\t\t&lt;resource&gt;   \t\t\t\t\t\t&lt;directory&gt;   \t\t\t\t\t\t\t${project.build.directory}/generated-docs   \t\t\t\t\t\t&lt;/directory&gt;   \t\t\t\t\t&lt;/resource&gt;   \t\t\t\t&lt;/resources&gt;   \t\t\t&lt;/configuration&gt;   \t\t&lt;/execution&gt;   \t&lt;/executions&gt;   &lt;/plugin&gt;                Gradle         bootJar {   \t// 빌드 전 문서 생성 확인   \tdependsOn asciidoctor   \t// 생성된 문서를 static/docs 에 복사    \tfrom (\"${asciidoctor.outputDir}/html5\") {    \t\tinto 'static/docs'   \t}   }           문서 조각 생성   Spring REST Docs 는 테스트 프레임워크, WebTestClient 등을 사용해서 문서화에 필요한 서비스에 요청을 한다. 그 다음 요청 및 결과 응답에 대한 문서 Snippet 을 생성한다.   테스트 설정   테스트 프레임워크에 따라 다르다. 여기에서는 JUnit 5 에 대한 설정을 다룬다.      JUnit 5 테스트 설정                     RestDocumentationExtension 을 테스트 클래스에 적용한다.             @ExtendWith(RestDocumentationExtension.class)   public class JUnit5ExampleTests {}                   RestDocumentationExtension 은 Maven 의 경우 “target/generated-snippets” Gradle 의 경우 “build/generated-snippets” 를 자동으로 출력 디렉토리로 설정되어있다.                        일반적인 Spring Application 의 경우에는 SpringExtension 을 적용한다.             @ExtendWith({ RestDocumentationExtension.class, SpringExtension.class })   public class JUnit5ExampleTests {}                           JUnit 5.1 을 사용하는 경우 클래스의 필드로 등록해서 출력 디렉토리를 임의로 지정할 수 있다.   public class JUnit5ExampleTests {  \t@RegisterExtension \tfinal RestDocumentationExtension restDocumentation = new RestDocumentationExtension (\"custom\");  }   다음은 MockMvc 를 사용한 설정 방법이다.   import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;  private MockMvc mockMvc;  @BeforeEach public void setUp(WebApplicationContext webApplicationContext, \t\tRestDocumentationContextProvider restDocumentation) { \tthis.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) \t.apply(documentationConfiguration(restDocumentation))  \t.alwaysDo(document(\"{method-name}\", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()))) \t.build(); }   documentationConfiguration 은 기본값이 있지만 사용자 정의를 위한 다양한 설정도 제공한다. 내용은 여기를 참고한다.   추가로 alwaysDo() 메서드에서 문서화로 보여줄 때 이쁘게 보여주기 위한 옵션도 추가한다.   JUnit 없이 테스트 설정      JUnitRestDocumentation 대신 ManualRestDocumentation 을 사용   @Rule 어노테이션 제거   private MockMvc mockMvc;  @Autowired private WebApplicationContext context;  private ManualRestDocumentation restDocumentation = new ManualRestDocumentation();   @BeforeMethod public void setUp(Method method) { \tthis.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) \t\t\t.apply(documentationConfiguration(this.restDocumentation)) \t\t\t.build(); \tthis.restDocumentation.beforeTest(getClass(), method.getName()); }   마지막으로 각 테스트 후에 ManualRestDocumentation.afterTest 를 호출해야 한다.   @AfterMethod public void tearDown() { \tthis.restDocumentation.afterTest(); }   RESTful 서비스 호출           RESTful 호출 예제         // 서비스의 루트(\"/\") 로 요청하고 응답 타입은 application/json 으로 한다.   this.mockMvc.perform(get(\"/\").accept(MediaType.APPLICATION_JSON))    \t// 응답 확인   \t.andExpect(status().isOk())    \t// 서비스 호출을 문서화 하는데 Snippet 이름을 index 로 지정.    \t// 
478Snippet 은 \"RestDocumentationResultHandler\" 에 의해 작성되며, 클래스의 인스턴스는   \t// \"org.springframework.restdocs.mockmvc.MockMvcRestDocumentation\" 의 \"document\"      // 메서드를 참고한다.   \t.andDo(document(\"index\"));           기본적으로 6개의 Snippet 이 작성된다. 그 외에 Snippet 에 대한 정보는 여기를 참고한다.      &lt;output-directory&gt;/index/curl-request.adoc   &lt;output-directory&gt;/index/http-request.adoc   &lt;output-directory&gt;/index/http-response.adoc   &lt;output-directory&gt;/index/httpie-request.adoc   &lt;output-directory&gt;/index/request-body.adoc   &lt;output-directory&gt;/index/response-body.adoc   Snippet 사용   테스트 후 생성된 Snippet 을 사용하기위해 .adoc 파일을 생성해야 한다. 파일명은 아무렇게해도 상관없다.  그리고 생성된 snippet 을 연결해주면 된다.   include::{snippets}/index/curl-request.adoc[]   만들어 놓은 .adoc 과 생성된 Snippet 의 연결이 끝났다면 maven install 또는 gradle build 로 .html 파일을 생성해준다.                     빌드도구       소스 파일       생성 된 파일                       Maven       src/main/asciidoc/*.adoc       target/generated-docs/*.html                 Gradle       src/docs/asciidoc/*.adoc       build/asciidoc/html5/*.html                Simple Example     코드는 여기를 참고 한다.      의존성 추가       spring-boot-starter-web 과 Spring REST Docs 를 위한 spring-restdocs-mockmvc 를 추가한다.   &lt;dependency&gt; \t&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; \t&lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; \t&lt;groupId&gt;org.springframework.restdocs&lt;/groupId&gt; \t&lt;artifactId&gt;spring-restdocs-mockmvc&lt;/artifactId&gt; \t&lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt;      Plugin 설정            Asciidoctor 플러그인을 설정       생성 된 Snippet 과 연결된 .adoc 을 빌드 했을 때 만들어지는 HTML 파일의 위치 설정       HTML 파일을 복사하여 웹에서 확인할 수 있는 디렉토리로 이동       maven-resources-plugin 은 Snippet 이 생성된 이후에 동작(파일 복사) 되어야 하므로 asciidoctor-maven-plugin 뒤에 오도록한다.           &lt;plugin&gt; \t&lt;groupId&gt;org.asciidoctor&lt;/groupId&gt; \t&lt;artifactId&gt;asciidoctor-maven-plugin&lt;/artifactId&gt; \t&lt;version&gt;1.5.8&lt;/version&gt; \t&lt;executions&gt; \t\t&lt;execution&gt; \t\t\t&lt;id&gt;generate-docs&lt;/id&gt; \t\t\t&lt;phase&gt;prepare-package&lt;/phase&gt; \t\t\t&lt;goals&gt; \t\t\t\t&lt;goal&gt;process-asciidoc&lt;/goal&gt; \t\t\t&lt;/goals&gt; \t\t\t&lt;configuration&gt; \t\t\t\t&lt;backend&gt;html&lt;/backend&gt; \t\t\t\t&lt;doctype&gt;book&lt;/doctype&gt; \t\t\t\t&lt;attributes&gt; \t\t\t\t\t&lt;snippets&gt;${snippetsDirectory}&lt;/snippets&gt; \t\t\t\t&lt;/attributes&gt; \t\t\t\t&lt;sourceDirectory&gt;${basedir}/src/main/docs/asciidocs&lt;/sourceDirectory&gt; \t\t\t\t&lt;outputDirectory&gt;${project.build.directory}/generated-docs&lt;/outputDirectory&gt; \t\t\t&lt;/configuration&gt; \t\t&lt;/execution&gt; \t&lt;/executions&gt; \t&lt;dependencies&gt; \t\t&lt;dependency&gt; \t\t\t&lt;groupId&gt;org.springframework.restdocs&lt;/groupId&gt; \t\t\t&lt;artifactId&gt;spring-restdocs-asciidoctor&lt;/artifactId&gt; \t\t\t&lt;version&gt;${spring-restdocs.version}&lt;/version&gt; \t\t&lt;/dependency&gt; \t&lt;/dependencies&gt; &lt;/plugin&gt; &lt;plugin&gt; \t&lt;artifactId&gt;maven-resources-plugin&lt;/artifactId&gt; \t&lt;version&gt;2.7&lt;/version&gt; \t&lt;executions&gt; \t\t&lt;execution&gt; \t\t\t&lt;id&gt;copy-resources&lt;/id&gt; \t\t\t&lt;phase&gt;prepare-package&lt;/phase&gt; \t\t\t&lt;goals&gt; \t\t\t\t&lt;goal&gt;copy-resources&lt;/goal&gt; \t\t\t&lt;/goals&gt; \t\t\t&lt;configuration&gt; \t\t\t\t&lt;outputDirectory&gt; \t\t\t\t\t${basedir}/src/main/resources/static/html/docs \t\t\t\t&lt;/outputDirectory&gt; \t\t\t\t&lt;resources&gt; \t\t\t\t\t&lt;resource&gt; \t\t\t\t\t\t&lt;directory&gt; \t\t\t\t\t\t\t${project.build.directory}/generated-docs \t\t\t\t\t\t&lt;/directory&gt; \t\t\t\t\t&lt;/resource&gt; \t\t\t\t&lt;/resources&gt; \t\t\t&lt;/configuration&gt; \t\t&lt;/execution&gt; \t&lt;/executions&gt; &lt;/plugin&gt;      예제 Controller, Service, DTO 추가              DTO         @Data   public class RestDocsTest {            private String id;     private String name;     private String body;    }                Service         @Service   public class RestDocsTestService {            public ResponseEntity&lt;RestDocsTest&gt; readContent(RestDocsTest restDocsTest) {       RestDocsTest result = new RestDocsTest();                try {         result.setId(\"ID\");         result.setName(\"Name\");         result.setBody(\"Body\");          return new ResponseEntity&lt;&gt;(result, HttpStatus.OK);       } catch (Exception e) {         return new ResponseEntity&lt;&gt;(result, HttpStatus.BAD_REQUEST);       }     }    }                Rest API Controller         @RestController   @RequestMapping(\"/test\")   public class RestDocsRestController {      private final RestDocsTestService restDocsTestService;      public RestDocsRestController(RestDocsTestService restDocsTestService)  {       this.restDocsTestService = restDocsTestService;     }            @GetMapping(value = { \"\" })     public ResponseEntity&lt;RestDocsTest&gt;
478 readRestDocsTest(@RequestBody(required = false) RestDocsTest restDocsTest)  {       ResponseEntity&lt;RestDocsTest&gt; result = restDocsTestService.readContent(restDocsTest);       return result;     }    }              포트 설정(필요 시) 및 View Resolver 확장자 설정      server:    port: 8080  spring:   mvc:     view:       suffix: .html      테스트 코드 작성      import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;  import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.restdocs.payload.PayloadDocumentation; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext;  @ExtendWith({ RestDocumentationExtension.class, SpringExtension.class }) @SpringBootTest(classes = ExampleSprintRestDocsApplication.class) class ExampleSprintRestDocsApplicationTests {  \tprivate MockMvc mockMvc;  \t@Test \tvoid contextLoads() { \t}  \t@BeforeEach \tpublic void setUp( \t\tWebApplicationContext webApplicationContext,  \t\tRestDocumentationContextProvider restDocumentationContextProvider)\t{ \t\tthis.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) \t\t\t.apply(documentationConfiguration(restDocumentationContextProvider)) \t\t\t.alwaysDo(document(\"{method-name}\", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()))) \t\t\t.build(); \t}  \t@Test \tpublic void readExample() throws Exception { \t\tthis.mockMvc.perform(get(\"/test\")) \t\t.andExpect(status().isOk()) \t\t.andDo( \t\t\tdocument(\"test-example\",  \t\t\t\tpreprocessRequest(prettyPrint()),  \t\t\t\tpreprocessResponse(prettyPrint()),  \t\t\t\tPayloadDocumentation.responseFields( \t\t\t\t\tPayloadDocumentation.fieldWithPath(\"id\") \t\t\t\t\t\t.description(\"ID 필드 입니다.\"),  \t\t\t\t\tPayloadDocumentation.fieldWithPath(\"name\") \t\t\t\t\t\t.description(\"이름 필드 입니다.\"),  \t\t\t\t\tPayloadDocumentation.fieldWithPath(\"body\") \t\t\t\t\t\t.description(\"데이터 필드 입니다.\") \t\t))); \t\t\t// links( \t\t\t// \t// linkWithRel 은 텍스트를 링크화 한다.  \t\t\t// \tlinkWithRel(\"test\").description(\"The Test resource\")),  \t\t\t// \t\t//subsectionWithPath 를 사용해서 메서드에 대한 응답을 문서화 했다. \t\t\t// \t\tresponseFields(subsectionWithPath(\"_links\") \t\t\t// \t\t\t.description(\"Links to other resources\")), \t\t\t// \t\t// 응답에 \"Content-type\" 헤더를 추가하기 위해 headerWithName 을 사용 \t\t\t// \t\tresponseHeaders(headerWithName(\"Content-Type\") \t\t\t// \t\t\t.description(\"The Content-Type of the payload, e.g. `application/hal+json`\"))) \t}  }      빌드 및 Snippet 확인      만들어진 Snippet 들을 연결해줄 사용자 정의 .adoc 파일을 “src/main/docs/aciidocs” 경로에 만들고 다음과 같이 작성한다.   = RESTful Notes API Guide :doctype: book :icons: font :source-highlighter: highlightjs :toc: left :toclevels: 4 :sectlinks:  include::{snippets}/test-example/curl-request.adoc[]  include::{snippets}/test-example/http-request.adoc[]  include::{snippets}/test-example/http-response.adoc[]  include::{snippets}/test-example/httpie-request.adoc[]  include::{snippets}/test-example/request-body.adoc[]  include::{snippets}/test-example/response-body.adoc[]  include::{snippets}/test-example/response-fields.adoc[]   이제 Snippet 들을 만들기 위한 빌드를 실행한다.   mvn clean install   빌드 및 테스트가 완료되면 target/generated-snippets 밑에 기본 파일들이 생긴다. 해당 Snippet 들은 자동으로 .adoc 과 연결된다. 또한 static/html/docs 밑에 html 이 생성된 것을 볼 수 있다.         HTML 웹 페이지 연결      이제 만들어진 HTML 을 웹 페이지로 볼 수 있게 연결해보자.   @Controller @RequestMapping(\"/docs\") public class RestDocsController {      @GetMapping   public String docs()  {     return \"html/docs/rest-test-api-guide\";   }  }      웹 페이지 확인      스프링 부트 애플리케이션을 실행하고 다음 주소로 접속하면  Spring REST Docs 로 만들어진 문서를 볼 수 있다.   http://localhost:8080/docs     ","categories": ["posts"],
479        "tags": ["spring rest docs"],
480        "url": "https://subji.github.io/posts/2021/01/06/springrestdocsexample",
481        "teaser":null},{
482        "title": "Spring Boot JUnit 에서 한글 깨지는 오류",
483        "excerpt":"JUnit 에서 응답 값의 결과 중 한글이 깨지는 오류  JUnit 으로 mockmvc.perform 으로 응답값을 가져와서 확인하려던 중 결과에 한글이 포함되었을 때 깨지는 오류가 발생하는 것을 확인하였다.   찾아보니 Spring 5.2 버전 이후 Spring Boot 의 경우 2.2 버전 이후 부터 기본 인코딩 방식으로 UTF-8 을 더이상 지원하지 않는다고 한다. 이는 기본값이 ISO-8859-1 로 변경되었기 때문이라고 한다.   &lt;/br&gt; &lt;/br&gt; &lt;/br&gt;   해결방법  @before 의 setup 함수에서 UTF-8 로 인코딩을 해주는 필터를 추가해주면된다.  @Before public void setUp() { \tthis.mockMvc = MockMvcBuilders \t\t.webAppContextSetup(this.context) \t\t.addFilter(((request, response, chain) -&gt; { \t\t\tresponse.setCharacterEncoding(\"UTF-8\"); \t\t\tchain.doFilter(request, response); \t\t})) \t\t.apply(springSecurity()).build(); }   참고     Spring Boot MVC Test - MockMvc is always null   MockMvc no longer handles UTF-8 characters  ","categories": ["posts"],
484        "tags": ["spring boot","junit","test"],
485        "url": "https://subji.github.io/posts/2021/02/04/springbootmockmvccharset",
486        "teaser":null},{
487        "title": "Intellij 열린 에디터 선택 시 해당 파일 가리키기 Select opened file or editor",
488        "excerpt":"문제  이전에 vscode 를 사용하면서 열린 파일을 선택했을때, 왼쪽에 Sidebar 에서 자동으로 파일로 포커싱이 되는 기능이 있었다. 프로젝트 구조가 길어지면서 스크롤을 하면서 찾아가기 힘들때 아주 많은 도움이 되었었다. 이번에 Intellij 를 사용하면서 당연히 해당 기능이 있을거라 생각했는데, 내가 무슨 설정을 잘못한건지 아니면 기본값이 그런건지 동작하지 않았다. 뭐 물론 Ctrl + Shift + N 을 눌러서 파일을 직접 찾는 방법도 있다. 하지만 그냥 지금 열린 파일을 자동으로 프로젝트 구조에서 선택되게 해주면 더 편하기에 방법을 이리저리 찾아보았다.   &lt;/br&gt; &lt;/br&gt; &lt;/br&gt;   해결방법  방법은 간단하다. 수동으로 포커싱 해주는 단축키도 있다(Alt + f1). 하지만 그보다 항상 자동으로 되는걸 바랬기에 다음 설정을 체크하도록 한다.        ","categories": ["posts"],
489        "tags": ["intellij"],
490        "url": "https://subji.github.io/posts/2021/02/25/intellijselectopeneditor",
491        "teaser":null},{
492        "title": "java.lang.IllegalArgumentException: 'Content-Type' cannot contain wildcard type '*'",
493        "excerpt":"오류  RestTemplate 으로 요청을 하는데 온전히 전송되지도 않고 오류를 뱉어냈다. 오류 내용을 그대로 ë³´ë©´ “Content-Type” 에는 “*” 와 같은 와일드 카드를 포함할 수 없다고 나온다. 그래서 코드를 살펴보았는데,   HttpHeaders headers = new HttpHeaders(); HttpEntity&lt;Object&gt; entity = new HttpEntity&lt;&gt;(params, headers);   라고 되어있었다. 다만 궁금한 점은 아래 코드 부분인데,   public void setContentType(@Nullable MediaType mediaType) {   if (mediaType != null) {     Assert.isTrue(!mediaType.isWildcardType(), \"Content-Type cannot contain wildcard type '*'\");     Assert.isTrue(!mediaType.isWildcardSubtype(), \"Content-Type cannot contain wildcard subtype '*'\");     set(CONTENT_TYPE, mediaType.toString());   }   else {     remove(CONTENT_TYPE);   } }  Content-Type 을 설정하지 않았을 경우에 또는 알 수 없는 경우에 “*” 와 같은 와일드카드가 설정되는가이다.   찾아보니 Accept 헤더를 설정하지 않거나 사용하지 않는 경우에 혹은 MediaType.ALL 로 설정한 경우에는 응답 타입을 알지 못한다. 그래서 제목과 같은 ì˜
493¤ë¥˜ë¥¼ 발생한다고 한다.   오류 해결 방법은 간단했다. Header 에 Content-Type 을 지정해주면 된다.   HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON);          참고  Problem to Understand correct configuration for Http GET Requests on Spring Integration  ","categories": ["posts"],
494        "tags": ["springbood"],
495        "url": "https://subji.github.io/posts/2021/03/30/cannotcontainwildcardstar",
496        "teaser":null}]

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.