애플리케이션 성능 관리(APM) 시장 분석 및 전망 2018-2019

6월 11, 2019 By irene
2018년 제니퍼소프트가 전년에 이어 업계 1위 입지를 유지했다. 2018년엔 137.8억원의 매출을 달성하며 소폭 성장했다. 이 회사의 시장점유율은 68%였다. 제니퍼소프트는 지난해 공공시장에서 선전했고, 금융기관에서도 추가 매출을 거두며 성장을 이어갔다. 제니퍼소프트는 본지가 APM 시장조사를 시작한 2009년 이래 이 회사는 APM 시장에서 선두를 이어오고 있다. 2018년 WAS 모니터링 APM시장의 총매출은 엔드-유저 라이선스 구매 기준으로 204.6억 원을 기록했다. 이는 전년 대비 1.99% 성장한 수치다. 시장은 지난 5년간 200억 원 내외의 매출을 기록하며 성숙한 모습을 보여주고 있다. 시장에 변화를 줄 만한 요소가 크지 않아 안정된 흐름을 이어가고 있다. 2018년 WAS APM 벤더별 신규 라이선스 매출 현황 (시장 매출액 기준) 이 회사는 WAS 모니터링 영역에 대한 기술 투자를 기반으로 대용량 데이터를 원활히 모니터링 하는 등 기술 우위를 기반으로 시장에서의 입지를 강화하고 있다. 특히 클라우드 및 가상화 대응 기능도 이미 제품에 반영한 상태다. 이 회사의 2018년 누적 국내 고객의 수는 1,014개사로 전년 대비 31개 고객사 추가했다. 일본에서의 사업 성과도 돋보인다. 이 회사는 라쿠텐, 손보재팬 등을 포함해 2018년까지 고객사 250개를 확보했다. 또한 2016년 시장매출 30억원을 넘긴 이래 매년 안정적으로 성장하고 있다. <보고서 주요 내용> 2018 년 APM 마켓 분석 벤더별 매출  분석APM (WAS+EUM) 매출 분석산업별 APM 마켓 분석 제니퍼 구매 고객에 한하여 데일리그리드 시장 분석 보고서를 무료로 제공하고 있습니다.  보고서 관련 문의는 마케팅 김윤희 부장에게 메일로 연락주시기 바랍니다. ( irene.kim@jennifersoft.com)

[개발자 아티클]PHP Session Locking: PHP Session 장애 재현 및 해결 방법

3월 20, 2019 By taylor
글쓴이 제니퍼소프트 PHP 개발자 홍철의(Talyor) 이 글은 PHP Session Hang 장애를 직접 재현해 보고 왜 이런 문제가 발생하는지, 어떻게 이 문제를 해결할 수 있는지 설명합니다. 글 하단의 결론만 읽으셔도 충분합니다. 이제 장애를 재현해 보겠습니다. Session을 사용하지 않았을 때 다음 PHP 스크립트를 작성합니다. ab 명령을 이용해 first.php로 동시에 10개 요청을 서버에 보냅니다. 결과는 다음과 같습니다. 1초가 걸리는 요청을 동시에 10개 수행했으니, 위 ab 명령어의 수행 시간은 당연히 1초입니다. PHP Session을 사용했을때 이제 Session을 사용해 요청자의 페이지 방문 횟수를 보여주는 기능을 추가하겠습니다. curl 호출을 통해 단일 요청의 결과를 확인했을때 결과는 다음과 같습니다. 응답으로 visit_count=1이 주어졌습니다. HTTP 응답 쿠키에  PHPSESSID=lmrfduhgfm6oik62fi2c9q4ek7이 추가된 것으로 보아 PHP 기본 Session 핸들러는 Cookie를 사용하며, PHPSESSID 쿠키 값이 Session 식별자로 사용됨을 알 수 있습니다. 다시 한번 curl을 요청을 보내되, Cookie에 PHPSESSID를 넣어 보내겠습니다. 응답으로 visit_count=2 를 얻었습니다. 새로 만든 기능은 잘 동작하고 있네요. 이제 웹 서버에 어떤 일이 벌어졌는지 확인해보겠습니다. ls /tmp 를 실행해보면 lmrfduhgfm6oik62fi2c9q4ek7 파일이 존재함을 알 수 있습니다. 해당 파일의 내용은 다음과 같은데, 이것으로 세션이 어떻게 저장되는지 확인할 수 있습니다. PHP 세션 장애 재현 Case 1 Cookie로 PHPSESSID를 지정해 동시에 10개 요청을 보냅니다. 그리고 어떤 일이 벌어지는지 확인해보겠습니다. Session기능을 넣지 않았을때 ab 명령의 수행 시간은 1초였습니다. 같은 Session ID로 동시에 10개의 요청을 보내니 10초가 걸렸습니다. 이때 JENNIFER X-View는 다음과 같습니다. 위 그림에서 X 축은 요청 시간(Request Time)이며, Y축은 처리 시간(Elapsed Time)입니다. 10개 요청은 모두 동일한 시간에 들어왔으나 처리된 시간은 각각 다르다는 사실을 알 수 있습니다. 트랜잭션의 상세 내용을 보면 시작시간은 모두 16:12:52 090 으로 차이가 없으나 응답시간은 1초, 2초, 3초, 4초, … 10초 입니다. 그리고 마지막으로 처리된 요청의 session_start 함수의 수행 시간이 9초가 걸렸네요. 위 현상이 관찰되는 까닭은 File session handler가 Race Condition을 피하기 위해 Session 파일을 잠그기 때문입니다. (링크) 이는 실시간 콜스택을 확인해 보면 확실히 드러납니다. (현재 실행중인 함수가 Lock을 얻기 위해 사용되는 flock 임에 주목해 주세요) 사용자가 동시에 요청을 보내지 않으니, 이런 일은 벌어지지 않는다구요? 그렇지 않습니다. 이런 시나리오를 상상해 봅니다. 로그인한 사용자가 브라우저로 요청을 보냈습니다. 그런데 해당 사이트의 응답이 느려 사용자가 F5를 눌러 재 요청을 보냅니다. 그래도 응답이 오지 않아 계속 F5를 반복해 누릅니다. 그 결과 사용자가 재 요청 보낸 수 만큼 아파치 프로세스가 생성되며(아파치 Prefork 모드의 경우) 이는 자원 고갈로 이어질 수 있습니다. (100번 재요청 했다고 상상해보세요)  PHP 세션 장애 재현 Case 2 문제를 좀 더 심각하게 만들어 보겠습니다. useful_function에 기능을 추가하되, 그 기능을 위해 db connection이 필요하다고 가정하겠습니다. session_start() 전에 mysql_connect를 호출했습니다. 이 경우 한 Session의 요청이 동시에 N번 호출되면 DB Connection 수 또한 N개 만큼 증가 합니다. 이를 피하기 위해선 session_start 전에 자원을 획득하지 않아야 합니다. DB 자원을 session_start() 호출 후 얻게 코드를 수정 했을때 Active DB Connection 차트는 다음과 같습니다. DB 커넥션 수가 1로 유지됨을 알 수 있습니다. session_write_close를 이용한 문제 해결 PHP.net은 위 사항에 대해 경고 하고 있고 이에 대한 해결 방법도 제안하고 있습니다. read_and_close 옵션과 함께 session_start 함수를 사용하라. (PHP 7.0 이상만 이 옵션을 사용할 수 있습니다.)세션 데이터를 수정한 후 가능한 빨리 session_commit 또는 session_write_close를 사용해 session lock을 해제해라. 여기서는 2번 방법을 택해 문제를 해결하도록 하겠습니다. 이렇게 프로그램을 수정한 후 동시에 10개 요청을 보내겠습니다. 문제가 해결되었네요. 동시에 모든 요청이 처리되었습니다. 단, 주의점이 있습니다. 위 코드는 session_write_close() 호출 이후 $_SESSION[‘visit_count’]를 수정하고 있습니다. 어떻게 동작할까요? Jennifer 프로파일을 확인해보겠습니다. $_SESSION[‘visit_count’] 값이 'write data after session_write_close()'로 수정된것을 확인할 수 있습니다. 이제 /tmp 에 저장된 sess파일의 내용을 확인해봅시다. visit_count 값이 ‘write data after session_write_close()‘가 아니라 40인 것을 알 수 있습니다. 이것으로 session_write_close() 호출 이후 수정된 $_SESSION 변수 값은 요청이 처리되는 동안 유효하나, Session 데이터에 기록되지 않는 다는 사실을 알 수 있습니다. 이것이 session_write_close 해법의 단점입니다. 만약 session을 수정하고 사용하는 코드가 여러 곳에 흩어져 있다면 이 방법을 적용하기 까다로우며, 실수했을 경우 사용자가 보는 값과 실제 Session에 저장된 값이 다를 수 있습니다. Redis Session Handler를 사용해 문제 해결 이번에는 Session을 잠그지 않는 Redis 세션 핸들러를 사용해 문제를 해결해보겠습니다.(option으로 Lock을 사용하게 할 수도 있습니다.) 위에 정의한 session_test2.php(session_write_close를 사용하지 않는 스크립트)를 동시에 10번 호출합니다. Lock으로 인한 문제는 사라졌습니다. 모든 요청이 동시에 처리되었네요. Session Data도 확인해 봅니다. Session 데이터가 저장되어 있음을 알 수 있습니다. 그런데 visit_count가 10이 아니라 6으로 기록 되어 있습니다. Lock을 사용하지 않았기에 동시성 문제가 발생한 것입니다. 이것이 Lock을 사용하지 않는 Session Handler의 단점입니다. Redis Session Handler를 Lock 없이 사용하기로 했다면 이를 알고 있어야 하며 Session에 저장된 값이 무결(Integrity)할 것이란 기대를 하지 않아야 합니다. 그리고 무결성을 요구하는 데이터를 Session에 저장하지 않아야 합니다. Framework, Library의 단순한 사용은 해결책이 될 수 없습니다. 위는 CodeIgniter에서 session 핸들러로 database를 지정했을때 확인 가능한 프로파일 입니다. session_handler로 database를 사용하게 했습니다. 따라서 session_start 함수 전에 DB_CONNECTION이 호출 되어야만 합니다. SQL Query를 확인해보니 Lock을 사용하고 있으며 session_start() 메소드 호출이 2초 이상 걸린 것으로 보아 Lock으로 인한 문제가 발생하고 있음을 알 수 있습니다. 위를 재현하기 위해 사용한 CodeIgniter Controller 코드는 아래와 같습니다. 이 단순한 코드에 session_start 와 session_write_close가 감춰져 있다는 사실이 놀랍습니다. 혹시 CodeIgniter Session을 사용하고 있다면 CodeIgniter/A note about concurrency 단락을 읽어보시기 바랍니다. 저 단락은 해당 문서의 다른 단락과는 달리 다소 격정적인 문체로 쓰여 있습니다. CodeIgniter가 Session Lock으로 많은 고심(또는 고생)을 했다고 추측할 수 있습니다. (CodeIgniter 3.0 Session Library는 완전히 재 작성되었다고 하네요) 그 고심 끝에 CodeIgniter는 다음 문장을 내놓습니다. Locking is not the issue, it is a solution. Your issue is that you still have the session open, while you’ve already processed it and therefore no longer need it. So, what you need is to close the session for the current request after you no longer need it. CodeIgniter - A note about concurrency 단락에서 발췌 위에 인용한 내용이 CodeIgniter가 Session Lock을 바라보는 관점입니다. 이와 달리 Laravel의 Session기능은 session_start함수를 사용하지 않으며 Session Lock도 하지 않습니다. 다만 이로 인해 동시성 문제가 발생합니다. 이 이슈를 살펴 보시기 바랍니다. Fraemework, 또는 Library의 Session 기능을 사용하고 있다면 Session Lock 여부에 대해 알아야 합니다. Lock을 한다면 Session을 수정한 후 가능한 빨리 Lock을 해제해야 합니다. Session Lock을 사용하지 않는 Framework라면 동시성 문제가 발생할 수 있음을 알고 있어야 합니다. 결론 지금까지 Session Lock으로 인한 장애를 재현해보고 해결해보았습니다. 이 과정을 통해 다음 권고 사항을 도출할 수 있습니다. 사용하고 있는 Session Handler의 Lock 정책이 무엇인지 알아야 한다.PHP 기본 Session Handler는 file이며, Session Data를 Lock한다.Redis Session Handler는 Lock을 하지 않는다. 옵션을 통해 Lock 정책을 변경할 수 있다.Memcached Session Handler는 Lock을 한다. 옵션을 통해 Lock 정책을 변경할 수 있다.Session Handler가 Lock을 사용하고 있다면 Session을 수정한 후 반드시 session_write_close로 Lock을 해제해 주어야 한다.Session Handler가 Lock을 하고 있지 않다면 동시성 문제가 발생할 수 있다는 사실을 알아야 하며, 무결성을 요구하는 데이터를 Session에 저장하지 말아야 한다. 마지막에 살펴봤듯이 위 권고는 Library, Framework에 있는 Session기능을 사용할 때도 적용되어야 합니다. 여러분이 사용하는 Library의 Session은 어떻게 구현되어 있나요? Lock을 사용하나요? 동시성 문제가 발생하지는 않나요?

제니퍼의 다이나믹 메소드 프로파일 기능 어떻게 활용하면 될까요?

3월 4, 2019 By khalid
다이나믹 메소드 프로파일(Dynamic method profile)은 애플리케이션 서버를 재시작하지 않고도 트랜잭션의 프로파일링 레벨을 증가시키거나 감소시킬 수 있는 JENNIFER의 강력한 기능입니다. 우선 프로파일이란 무엇일까요? 소셜 네트워크에서 말하는 프로파일이란 사람을 개인 프로필로서 설명하고 윤곽을 나타내기 위해 주어진 정보를 말합니다. 이와 유사한 방식으로 트랜잭션 프로파일에서는 개별 메소드의 응답 시간, 매개 변수 및 반환 값 등과 같은 트랜잭션에 대한 정보가 포함되어 있습니다. 프로파일 정보에서는 코드의 클래스가 트랜잭션을 완료하기 위해 사용하는 트랜잭션 동작, 실행된 메소드 및 각 메소드를 실행하는 데 걸리는 시간, 마지막으로 데이터베이스에 대해 실행된 SQL 쿼리를 실행하는데 걸리는 시간을 담고 있습니다. 이 모든 데이터는 트랜잭션의 작동 방식을 이해하고 문제의 근본 원인을 찾는 데 매우 유용하게 사용됩니다. 이번 아티클에서는 JENNIFER의 Dynamic Method Profiling 기능을 사용하는 방법에 대해 알아보겠습니다. Transaction 자바 애플리케이션 서버의 트랜잭션은 브라우저에서 요청된 URL의 최종 결과 페이지를 만드는 데 필요한 일련의 프로세스를 뜻합니다.  다음 이미지는 트랜잭션 처리 예를 보여 주고 있습니다. 트랜잭션이 처리되는 과정 브라우저의 요청은 여러 클래스에서 처리되며, 이 경우 ClassA에서 classD로 처리되고 두 클래스가 데이터베이스(E)를 호출합니다. 모든 클래스에 대해 수집된 프로필 데이터는 다음과 같습니다. 그러나 일반적인 상황에서 모든 클래스의 응답 시간을 측정하면 응답시간을 저하시키는 과도한 오버 헤드가 발생할 수 있습니다. 따라서 JENNIFER는 기본 조건에서 최소 로직의 응답 시간만 측정합니다. 위의 그림에서는 빨간색으로 표시된 항목만 추적됩니다. 그런 다음 다음 프로파일 데이터를 얻을 수 있습니다. 경우에 따라 기본(최소) 프로파일 정보가 충분하지 않을 수 있으며 현재 진행 중인 거래에 대한 추가 정보가 필요할 수 있습니다. 이 경우 Dynamic Profile을 사용하여 추가 정보를 얻을 수 있습니다. Using the Dynamic Method Profile Dynamic profile 기능을 설명하기 위해 다음 예를 확인해 보겠습니다. 사용자가 자신의 계정 영역에 액세스하려고 합니다. 하지만 그가 자신의 계정에 로그인 하려고 하니 서버의 응답 시간이 오래 걸립니다. 만약 JENNIFER가 시스템에 설치되어 시스템을 모니터링하고 있다면, JENNIFER의 dynamic method profile 을 사용하여 이 문제의 원인을 찾을 수 있습니다.  응답 시간이 느린 원인은 느린 트랜잭션(높은 응답시간)인 것 같으므로, X-View 차트 상위에 위치한 트랜잭션을 확인해 보겠습니다. 시스템 평균 응답 시간 및 TPS에 따라 X-View 차트의 상단 쪽에 트랜잭션 수가 많을 수 있습니다. X-View차트를 변경하여 검색 범위를 좁히거나 필터 기능을 사용하여 특정 기준으로 트랜잭션을 검색할 수 있습니다. 이 기능은 나중에 사용해 보도록 하겠습니다. 이 시스템에서는 JENNIFER가 HTTP 세션에서 사용자 ID를 추출하도록 구성되어 있으므로, 우리는 그의 USER ID를 사용하여 문제를 겪고 있는 특정 사용자와 관련된 트랜잭션을 검색하려고 합니다. 트랜잭션을 검토하고 응답 시간 별로 정렬하면 welcome page의 응답 시간이 응용 프로그램의 평균 응답 시간에 비해 상당히 오래 걸린다는 것을 즉시 관찰할 수 있습니다. 그러나 앞부분에서 설명한 바와 같이(일반적인 상황에서 모든 클래스의 응답 시간을 측정하면 응답시간을 저하시키는 과도한 오버 헤드가 발생할 수 있기에) JENNIFER는 기본 프로파일 정보만 표시합니다. 시작 페이지의 프로파일을 확인하면 트랜잭션 시간의 99%가 애플리케이션 로직(코드 레벨)에서 발생했음을 알 수 있습니다.  JENNIFER는 응답 시간 백분율과 "Not Profiled" 메시지에 대한 디테일한 정보를 보여주지 않습니다. 현시점에서는 우리는 응답 시간에 대한 문제가 데이터베이스나 외부 호출과 관련이 없으며 애플리케이션 로직 그 자체와 관련이 있음을 알 수 있습니다. 일부 클래스에서는 메소드가 높은 응답 시간을 발생시키고 있으며 우리는 Dynamic method profile을 사용하여 트랜잭션에 대한 추가 정보를 얻을 수 있습니다. Dynamic profile 기능을 적용하는 방법은 여러 가지이며 특정 메소드, 특정 클래스 또는 전체 패키지에 대한 프로파일을 적용할 수도 있습니다. 프로파일은 많은 클래스와 메소드에 대한 정보를 수집하기 때문에 오버헤드의 원인이 될 수 있으므로 패키지 범위에 적용하는 것은 권장되지 않습니다. 몇 분 동안 활성화할 수 있지만 분석을 마친 후에는 비활성화해야 합니다. 프로파일링 설정을 적용한 후 X-View로 돌아가서 새 트랜잭션이 도착할 때까지 기다려 봅니다. 이전과 동일한 필터 방법을 적용하여 원하는 트랜잭션을 신속하게 확인할 수 있습니다. 하지만, 이번에는 우리에게 이용 가능한 추가 정보를 확인할 수 있습니다. 이 트랜잭션으로 실행되는 각 클래스/방법과 각 클래스의 소요 시간을 확인할 수 있습니다. 이를 통해 우리는 "loadProfile" 방법이 응답 시간이 높아지는 이유를 알 수 있습니다. Method Param/Return JENNIFER는 수많은 사용자 분석을 통해 사용자들이 Profile을 분석하면서 메소드의 Parameter와 Return값을 쉽게 설정하여 보고자 한다는 사실을 알게 되었습니다. 이를 위해 콜트리에서 원하는 매서드를 선택하여 쉽게 설명할 수 있는 기능을 제공하고 있습니다.  예를 들어 함수에 전달된 매개 변수를 알고 싶으면 X-View 호출 트리에서 메소드를 마우스 오른쪽 버튼으로 클릭하고 "메소드 프로파일"을 선택하여 메소드 프로파일 팝업을 열 수 있습니다. 반환 값 또는 매개 변수를 추적하거나 둘 모두를 선택합니다. 다음에 메소드를 호출할 때 JENNIFER는 X-View 콜 트리의 메소드 옆에 파라미터 또는 반환 값을 표시하는 것을 확인할 수 있습니다. 이번 아티클에서는 제니퍼의 강력한 기능인Dynamic method profiled의 사용과 활용법에 대해 정리해 봤습니다. 제니퍼 Dynamic method의 활용법이 많은 도움을 드렸는지 모르겠습니다. 제니퍼 기능에 대한 궁금중이나 질문이 있다면 제니퍼소프트 기술팀으로 연락하여 주시기 바랍니다. 이메일: support.ko@jennifersoft.com

제니퍼 닷넷 (JENNIFER .NET)의 Azure App Service 웹 앱 지원

12월 5, 2018 By irene
 글쓴이  제니퍼소프트 닷넷 개발자 정성태(kevin)마이크로소프트의 Azure 환경에서 웹 응용 프로그램을 운영하는 방법은 크게 다음과 같이 2가지 유형으로 나뉩니다.IaaS (Infrastructure-as-a-service) 방식: 가상 머신(VM)에서 웹 응용 프로그램 구동(AWS의 경우 EC2 서비스에 해당)PaaS (Platform-as-a-service) 방식: App Service에서 웹 응용 프로그램 구동(AWS의 경우 Elastic Beanstalk에 해당)기존 응용 프로그램의 구조를 최대한 변경하지 않고 Azure에 올릴 수 있는 방법은 IaaS 방식을 사용하는 것이지만 이런 경우 일반적인 호스팅 업체에서 서비스를 하는 것과 비교해 물리적인 서버 시스템에 대한 관리 비용을 없앤 정도의 효용성만 있습니다. 반면 클라우드의 최대 장점인 손쉬운 Scale-out 서비스를 이용하려면 Azure App Service의 웹 앱 방식으로 마이그레이션을 해야 합니다. 단지 기 구축된 시스템을 웹 앱으로 이전하는 것이 경우에 따라 쉽지 않은데다 PaaS의 특성에 대한 이해도가 부족해 초기 시점에는 웹 앱 응용 프로그램이 많이 활성화되지는 못했습니다.하지만 근래 들어 PaaS의 장점이 대두되면서 새로 구축하는 웹 응용 프로그램의 경우 가상 머신이 아닌 웹 앱으로 만드는 사례가 늘어가고 있습니다. 예를 들어, 국내에서는 ㈜제이와이피엔터테인먼트에서 아시아 팬들을 위한 이벤트 정보 및 예약 안내를 담당하는 JYP NATION 투어 안내 사이트를 웹 앱으로 개설했고 최근에는 AI와 챗봇 서비스를 도입한 SSG.COM에서 “Zero VM”을 목표로 가상 머신이 아닌 FaaS, BaaS, PaaS 만으로 서비스를 구축하는 등 점차로 PaaS 이후의 단계로 이전하는 서비스가 늘어가고 있습니다.클라우드에서 제공하는 PaaS의 특성 상 성능이라는 관점에서 자동적으로 누릴 수 있는 혜택이 있습니다. 대표적으로는, 자유로운 Scale-out으로 인해 On-premise 환경과 비교해 대역폭과 관련한 문제가 쉽게 해결됩니다. 예를 들어, 웹 서버로 유입되는 트랜잭션의 증가로 요청 처리가 늦어지는 경우 서버 인스턴스를 동적으로 증가시키면 다시 서비스를 정상 수준으로 처리할 수 있습니다. 또한 서비스 지역을 단일 지점이 아닌, 클라우드 서비스 업체가 제공하는 전 세계의 데이터 센터를 활용할 수 있으므로 네트워크 트래픽에서도 자유로울 수 있습니다. 하지만 단순히 Scale-out으로 해결되지 않는 문제도 여전히 많습니다. 예를 들어, 전자 상거래 사이트의 장바구니 페이지를 방문 시 평소 응답 시간이 5초가 걸린다고 가정하는 경우 이것은 서비스 인스턴스의 수를 늘린다고 해서 선형적으로 반응 시간이 줄어들지는 않습니다. 즉, PaaS가 해결해 줄 수 있는 것은 일반적으로 유입 트래픽의 변동으로 인한 기존 서비스의 반응 시간을 유지해 주는 것입니다. 따라서 여전히 클라우드 환경에서도 유의미한 성능 문제가 발생하며 이런 문제의 원인을 찾기 위해 APM의 역할이 필요하게 됩니다.제니퍼소프트는 이러한 웹 응용 프로그램의 환경 변화와 여전히 APM이 필요할 수 있다는 요건에 따라 클라우드 환경에서의 APM 지원을 꾸준히 보완해 나가고 있습니다. 이미 자바 플랫폼의 AWS에 대한 Elastic Beanstalk 지원을 추가했고 최근에는 Azure 환경에서 JENNIFER .NET의 웹 앱 지원을 추가했습니다.JENNIFER .NET 설치가상 머신(VM) 상에서 구동하는 웹 응용 프로그램은 기존의 On-premise 환경의 설치 방식과 다른 점이 없습니다. 하지만 PaaS 유형에서는 Sandbox로 보호된 환경의 제약으로 인해 가상 머신과 같은 방식으로 설치할 수 없습니다. 이러한 설치 환경의 제약은 App Service 웹 응용 프로그램들에 대한 부가 서비스를 제공하는 3rd-party 응용 프로그램의 생태계에 영향을 미치게 되는데, 마이크로소프트는 이에 대한 해법으로 “웹 앱 확장(Web App Extension)” 환경을 제공하는 Kudu 서비스를 오픈 소스로 공개하고 Azure App Service 환경에 통합시켜 운영하고 있습니다. 그리고 JENNIFER .NET은 바로 그 웹 앱 확장의 하나로 등록되어 있기 때문에 기존 웹 앱 사용자라면 언제든지 해당 확장을 웹 앱과 연동할 수 있습니다.현재 웹 앱 확장은 Azure 포탈의 콘솔 화면을 통해 “Extensions” 메뉴를 이용하면 자유롭게 설치 및 삭제할 수 있습니다.[그림 1: 설치 대상이 되는 웹 앱의 “Extensions” 메뉴] 만약 JENNIFER .NET 에이전트를 여러분들의 Web App에 설치하고 싶다면 다음과 같이 Extensions 하위의 “Add extension” 메뉴로 JENNIFERSOFT에서 등록한 “JENNIFER .NET Agent”를 선택해 추가하면 됩니다. [그림 2: 확장 목록 중 “JENNIFER .NET Agent” 선택] 설치 후에는 JENNIFER .NET 에이전트가 JENNIFER 데이터 서버로 수집 데이터를 전송하기 위해 몇 가지 설정이 필요합니다. 기존 설치 방식에서는 사용자 설정을 conf 파일에 지정했지만 Azure App Service 환경에서는 해당 App Service의 “Application settings” 메뉴로, [그림 3: 대상 웹 앱의 환경 변수 설정 메뉴]표 1과 같은 환경 변수를 설정해야만 합니다. Figure 1 에이전트 구동을 위한 환경 변수 설정 표 1의 설정 중에서 사용자 환경에 따라 달라지는 값은 “ARIES_DOMAIN_ID”,  “ARIES_SERVER_ADDR”, “ARIES_SERVER_PORT”이며 나머지 값들은 변경하지 않고 그대로 설정해야 합니다.이후 웹 앱을 재시작하면 대시 보드에 해당 웹 앱 인스턴스를 대표하는 ID가 동적으로 할당되어 나타나고 Scale-out 설정에 따라 인스턴스의 수가 자동으로 늘고 주는 것을 확인할 수 있습니다.제니퍼 닷넷 사용 및 설치와 관련하여 문의 사항이 있으시면 tech@jennifersoft.com으로 문의 주시기 바랍니다. 정성태(kevin jung), 제니퍼소프트 닷넷 개발자 

[개발자 아티클] 레거시 시스템의 프론트엔드 개발환경 최신화 후기

12월 3, 2018 By irene
글쓴이 제니퍼소프트 프론트엔드 개발자 홍재석(Alvin) 필자의 올해 목표 중 하나는 오랜기간 조금씩 진행해왔던 제니퍼 뷰서버 플랫폼화를 마무리하는 것이었다. 기획했던 플랫폼 요소는 여러가지가 있지만 그 중에서도 제니퍼 화면을 독립적인 개발환경에서 구현할 수 있게 하는 기능이 제일 중요했다. 그래서 생각해낸 것이 서버 환경은 스프링부트로 심플하게 구성하고, 모던한 프론트엔드 개발을 위해 모듈 번들러로 웹팩을 선택했다. 관련해서는 필자가 쓴 “웹팩+스프링부트 기반의 프론트엔드 개발환경 구축하기”를 참고하자. 새로 갖춰진 개발환경에서 기존의 제니퍼 뷰서버 플러그인 중 일부를 테스트 삼아 마이그레이션 해보니까 나름 신선하더라. 기존의 JSP 템플릿에 마크업과 공존하고 있던 자바스크립트 코드는 ES6 스펙에 맞춰서 수정하고, 모듈 별로 분리를 하고나니 필자가 사용하고 있는 IntelliJ IDEA의 정적 코드 분석이나 메소드 힌트 같은 기능들을 제대로 활용할 수 있게 되었다. 요즘 같은 세상에서는 너무 당연한 것이지만 수년간 쌓아온 레거시란 벽은 이미 넘을 수 없을만큼 높아진 상태였다. 필자는 지난 8월에 “레거시(Legacy) 시스템에 웹팩 개발환경 적용하기”를 쓰면서 언젠가는 실무에 꼭 적용해보겠다는 다짐을 했었다. 그리고 얼마지나지 않아 여유 일정이 생겼고, 더 이상 미룰 수 없는 일이기에 바로 시작했다. (1) 마이그레이션 대상 선택하기 제니퍼 화면은 크게 메인 대시보드와 사용자정의 대시보드, 리얼타임, 분석, 통계, 관리, 보고서 템플릿, 사용자 메뉴로 나눌 수 있다. 참고로 사용자정의 대시보드와 보고서 템플릿은 화면 단위가 아니라 컴포넌트 단위의 기능으로 복잡하게 얽혀있어서 마이그레이션 대상에서 제외했다. 다음은 제니퍼에서 제공하는 타입 별 화면 개수이다. 메인 대시보드 6종, 리얼타임 8종, 분석 21종, 통계 6종, 관리 44종 제니퍼는 한달에 최소 두번 이상의 마이너 버전이 릴리즈 되는 온-프레미스(On-premise) 제품이다보니 문제가 되는 버전이 고객사에 설치되면 되돌리기가 쉽지 않다. 서비스형 제품처럼 피드백이 즉각적으로 나타나진 않지만 수없이 많은 과거 버전들 속에서 다양한 문제에 직면하게 된다. 필자는 지난 수년간 수많은 고객사에 설치되어 어느 정도 안정성이 확보된 85종의 화면들을 마이그레이션 해야한다. (2) 목표 설정하기 모든 일의 시작은 목표를 잘 정하는 것이다. 너무 당연한 말이지만 일의 규모가 크거나 앞에서 말한 것처럼 이미 검증된 일을 뒤엎고 새로운 것을 적용하는 일은 진행하는 사람이나 직책자 또는 구성원들에게 큰 부담을 안겨준다. 그래서 필자는 다음과 같은 목표를 정하고, 일의 당위성을 확보하기 위한 논리를 정리했다. 유닛 테스트와 스냅샷 테스트가 가능해야 함 툴에서 디버깅이 가능한 코드를 개발할 수 있어야 함 화면 단위로 프레임워크나 라이브러리를 자유롭게 사용할 수 있어야 함 마이그레이션 된 화면과 기존의 화면이 모두 잘 동작해야 함 4번에 대해 조금 더 설명을 하자면 한번에 모든 화면을 마이그레이션 할 수 있다면 정말 좋겠지만 현실적으로 불가능한 일이기 때문에 새로 갖춰진 개발환경에서는 기존의 화면과 마이그레이션 된 화면이 모두 잘 동작해야 한다. (3) 기술 스펙 정하기 목표 설정이 끝났으니 이제는 새로 갖춰질 개발환경의 기술 스펙을 정해야 한다. 본문에서는 각각의 기술 스펙에 대한 설명은 생략하겠으며, 서론에서도 언급한 필자가 작성한 글을 참고하면 도움이 될 것이다. 모듈 번들러 : webpack 4 개발환경 서버 : webpack-dev-server JavaScript 컴파일러 : babel 6 JavaScript 프레임워크 : vuejs 2 CSS 컴파일러 : sass 테스트 도구 : jest 기타 : eslint, prettier              (4) 레이어 기반의 화면 정리하기 목표와 기술 스펙이 정해졌으니 현재 시점에서 바로 본론으로 넘어가야 하는데, 뜬금없이 레이어 기반의 화면에 대한 설명을 보게 되서 당혹스럽겠지만 제니퍼는 언제 어디서든 관리 화면을 띄울 수 있도록 레이어 기반으로 구현되었다. 즉, 마이그레이션 대상 화면의 절반이 레이어 기반인 것이다. 필자의 계획은 화면 단위(URL 별)로 엔트리를 설정하고, 공통 모듈을 제외한 아웃풋 파일들을 화면 별 디렉토리 안에 생성해두려고 했었다. 하지만 관리 화면들이 레이어 기반이기 때문에 시작부터 큰 난관에 부딪치게 되었다. <제니퍼 관리 화면>  고민 끝에 필자는 모든 관리 화면을 iFrame 기반으로 변경하기로 했다. 물론 리소스 중복 로드에 따른 로딩 속도 문제나 컨텐츠에 따라 iFrame 크기를 유동적으로 변경해야 하는 등의 몇가지 문제점이 있었지만 어렵지 않게 해결할 수 있었다. 간단하게 정리하자면 다음과 같다. 관리 화면 특성상 다른 종류의 화면에 비해 공통 모듈이나 라이브러리를 적게 사용하기 때문에 마이그레이션이 완료되면 로딩 속도 문제가 어느 정도 개선될 것이다. “iFrame Resizer”라는 완성도가 높은 라이브러리를 사용했기 때문에 컨텐츠에 따른 iFrame 크기 조절을 자연스럽게 처리 할 수 있었다. 참고로 페이지 기반으로 화면이 변경되면서 고정 크기의 윈도우 컴포넌트에서 벗어나 별도의 팝업이나 URL로 접근할 수 있게 되어 사용성이 많이 개선되었다. (5) 레이아웃 구조 살펴보기 제니퍼 화면은 JSTL 커스텀 태그로 공통 레이아웃을 화면 별로 구성하는데, 화면 타입 별로 조금씩 다르게 처리되어 있다. 문제는 마크업 뿐만이 아니라 템플릿, 자바스크립트, 스타일까지 함께 포함되어 있기 때문에 우선 자바스크립트를 분리하면서 ES6 모듈로 마이그레이션을 진행해야 한다. 일단 기존의 화면 타입 별 레이아웃에 대한 설명을 하자면 다음과 같다. 화면_타입_header.jsp : default_css.jsp와 default_js.jsp를 로드함 화면_타입_body_start.jsp : toolbar.jsp를 로드함 화면_타입_body.end.jsp : 화면 타입 별로 공통으로 사용되는 마크업과 스크립트가 들어가고, footer.jsp를 로드함 common_ui.jsp : 제니퍼에서 사용되는 모든 컴포넌트들에 대한 템플릿과 스크립트가 포함되어 있음 다음은 화면 타입 별 레이아웃 내부에서 로드하는 공통 레이아웃에 대한 설명이다. default_css.jsp : 제니퍼 화면 구성에 필요한 css 파일과 JUI 라이브러리의 css 파일들을 로드함 default_js.jsp : 제니퍼 캔버스 차트와 유틸리티 js 파일과 jQuery나 JUI 같은 라이브러리의 js 파일들을 로드함 toolbar.jsp : 제니퍼 화면 상단에 보이는 툴바 영역에 대한 마크업과 스크립트가 포함되어 있음 예를 들어 EVENT 분석 화면은 /analysis/event으로 접근할 수 있는데, 해당 JSP 템플릿 파일은 /WEB-INF/jsp/analysis/event.jsp에 위치한다. <EVENT 분석 화면의 레이아웃 구조> (6) 레이아웃 구조 분리하기 일단 default_css.jsp는 화면 별 스타일과 ES6로 마이그레이션 된 컴포넌트 스타일만 분리했기 때문에 생각보다 간단하게 끝났다. 하지만 문제는 default_js.jsp와 toolbar.jsp, analysis_body_end.jsp, common_ui.jsp였다. 기존의 화면들은 잘 동작해야하므로 그대로 두고, 마이그레이션 대상 화면에 대해서만 레이아웃을 다르게 구성하기로 했다. 필자는 모듈 번들러로 웹팩을 선택했기 때문에 development 모드와 production 모드에 따라 output.path를 다르게 생성했다. development 모드 : ${project.basedir}/.webpack/bundles production 모드 : ${project.basedir}/src/main/webapp/bundles JSP 템플릿에서는 pageConext 내장 객체를 사용할 수 있는데, 요청 헤더 정보 중에 request.getServletPath() 메소드를 사용하여, 화면 타입과 화면 이름을 분류했다. 제니퍼 뷰서버는 다음과 같이 단순한 URL 구조를 가진다. URL : http://127.0.0.1:8080/analysis/event (화면 타입은 analysis, 화면 이름은 event) 만약에 development 모드이고, EVENT 분석 화면이라면 ${project.basedir}/.webpack/bundles/analysis/event에 디렉토리가 생성된다. 사용자가 특정 화면에 접근했을 때, 앞에서 말한 pageContext 내장 객체를 사용하여 화면 타입과 화면 이름을 분류하고, output.path에 해당 디렉토리가 존재하는지 확인한다. 만약에 디렉토리가 존재한다면 마이그레이션 대상 화면이라고 간주하고, 다음과 같은 레이아웃 구조로 변경한다. <새로운 EVENT 분석 화면의 레이아웃 구조> 기존에는 모든 화면에서 default_js.jsp에 정의된 js 파일들을 로드했었다. jquery나 moment, lodash 같은 유명한 라이브러리도 포함되어 있고, 제니퍼 캔버스 차트나 유틸리티, JUI 등 내부에서 사용되는 모듈들도 포함된다. 필자는 마이그레이션 대상 화면에서 의존성이 너무 높은 jquery를 제외하고, 번들 파일들만 로드할 수 있도록 제니퍼 뷰서버를 대대적으로 수정하였다. (7) 웹팩 기본 설정하기 본문에서는 웹팩 설정 방법에 대해서 자세히 다루지는 않고, 중요하다고 생각하는 부분만 짚고 넘어가려고 한다. 먼저 모드에 따라 output.path를 다르게 설정해주고, 마이그레이션 대상 화면은 계속 늘어날 것이기 때문에 entry를 멀티로 설정해야 한다. <웹팩 설정 파일> 먼저 clientPath에는 JSP 템플릿 파일에서 분리한 자바스크립트 코드를 ES6 모듈로 마이그레이션 한 index.js 파일들이 위치한다. 화면 별 index.js 파일들은 entry가 되는데, 여기서 entry 키에 주목하자. output.filename이 [name].js로 설정되어 있는데, [name]은 entry 키로 치환되어 output.path 디렉토리에 생성된다. 만약에 analysis/event/index.js 파일을 production 모드에서 빌드를 하면 번들링 된 파일 경로는 다음과 같다. ${project.basedir}/src/main/webapp/bundles/analysis/event/app.js 참고로 output.publicPath를 /bundles로 설정했기 때문에 웹에서는 다음과 같이 접근할 수 있다. http://127.0.0.1:8080/bundles/analysis/event/app.js 이제 entry 모듈에서 화면 별로 필요한 라이브러리만 import해서 사용할 수 있게 되었다. 물론 jquery는 여전히 webpack_default_js.jsp에서 로드되고 있기 때문에 다음 설정을 통해서 번들 파일에 포함되지 않게 해야한다. <웹팩 설정 파일 2> (8) 웹팩 개발서버 설정하기 필자는 development 모드일 때, webpack-dev-server를 사용하기로 결정했는데, 이유는 HMR(Hot Module Replacement)를 적용해보고 싶었기 때문이다. 하지만 몇가지 문제로 인해 현재는 Live-Reload만 적용한 상태이다. webpack-dev-server는 번들 파일을 메모리 상에서 제공하기 때문에 output.path로 설정한 .webpack 디렉토리가 필요없다. 하지만 특정 화면으로 접근했을 때, 제니퍼 뷰서버가 마이그레이션 대상 화면인지 판단하기 위해서는 실제 파일이 필요했고, JSP 템플릿에서 로드해야 하는 파일들은 화면 별로 조금씩 다르기 때문에 구분이 필요했다. 그래서 마음에 들진 않지만 webpack 명령어를 함께 사용했다. webpack --watch --env=development & webpack-dev-server --env=development 실은 webpack-dev-server가 컴파일 할 때, 번들 파일 경로를 얻어오는 방법을 열심히 알아봤으나 아직까지도 답을 찾지 못했다. 하지만 Express는 webpack-dev-server를 미들웨어로 추가하면, 번들 파일 경로를 가지고 올 수 있어서 참 아쉬웠다. (제니퍼 뷰서버는 자바 스프링을 사용함 ㅜㅜ) <웹팩 설정 파일 3> webpack-dev-server 포트로 제니퍼 화면에 접근했을 때, 웹소켓으로 데이터를 가져오는 대시보드가 제대로 동작하지 않았다. 그래서 조금 헤맸었는데, http와 ws 프록시 컨텍스트만 겹치지 않게 설정하면 해결되는 문제였다. (9) 공통 모듈 청크하기 앞에서 entry를 멀티로 설정하여 화면 별로 번들 파일을 생성하는 방법에 대해 알아봤다. 하지만 네비게이션 바나 사용자 메뉴, 알림 등 모든 화면에서 공통으로 사용되는 기능이나 유틸리티 모듈들은 어떻게 번들링 될까? 웹팩 기반으로 개발하는 사람은 누구나 알고 있는 splitChunks 옵션을 사용하면 되는데, 일단 다음 설정을 보자. <웹팩 설정 파일 4> 여기서 중요한 부분은 cacheGroups 모듈의 name 설정 부분인데, 앞에서 설명한 output.filename의 [name]과 치환되어 output.path 디렉토리에 번들 파일이 생성되는 것이다. production 모드일 때, 번들링 된 파일 경로는 다음과 같다. ${project.basedir}/src/main/webapp/bundles/base/common.js ${project.basedir}/src/main/webapp/bundles/base/modules.js (10) 이미지 파일 관리하기 보통은 스프라이트 이미지를 사용하는데, 부득이하게 특정 스타일에 단일 이미지를 사용해야 하는 경우가 종종 발생한다. 그래서 필자는 웹팩으로 번들링 할 때, development 모드에서는 url-loader를 사용하고, production 모드에서는 file-loader를 사용했다. 이미지가 base64로 인코딩되서 CSS 파일에 포함되기 때문에 개발할 때는 매우 편리하지만 용량이 많이 커지는 문제가 있다. 그래서 개발이 완료되면 file-loader를 사용하기로 했다. 다만 여기서 주의할 점은 이미지 파일들은 화면 별 디렉토리에 포함되어 있지만 배포 할때는 다른 화면의 이미지 파일과 함께 동일한 디렉토리에 복사된다. <웹팩 설정 파일 5> 이미지가 복사되는 디렉토리는 앞에서 설명한 output.path에 options.outputPath가 더해진 경로이므로 다소 헷갈릴 수도 있기 때문에 주의해서 설정해야 한다. ${project.basedir}/src/main/webapp/bundles/images 모든 화면의 이미지 파일이 동일한 디렉토리에 복사되기 때문에 파일명이 겹치지 않도록 options.name을 [name] 대신 [hash]로 변경하였다. 그리고 options.publicPath를 /bundles/images로 설정했기 때문에 웹에서는 다음과 같이 접근할 수 있다. http://127.0.0.1:8080/bundles/images/[hash].jpg (11) JavaScript 프레임워크 선택하기 Vue.js를 선택한 이유는 여러가지가 있는데, 일단 필자는 템플릿 방식을 선호한다. 2.x 버전부터 render 함수를 지원하지만 이건 개인 취향이니 그냥 넘어가고, 가장 큰 이유는 기존의 레거시 화면과 공존해야 하는 특수한 경우라서 경량 프레임워크를 선택하는 것이 옳은 판단이라고 생각했다. JSP 템플릿에 포함된 자바스크립트 코드는 ES6 모듈로 마이그레이션 했지만 여전히 마크업 코드는 남아있었다. 하지만 싱글 파일 컴포넌트에 거의 수정하지 않고 옮길 수 있었기 때문에 마이그레이션 속도가 많이 향상되었다. 참고로 제니퍼 화면은 서버에서 넘겨주는 필수 데이터들이 많아서 JSP 같은 서버 템플릿을 완전히 제거할 수는 없었다. 그래서 최소한의 마크업만 남겨두고, 최대한 뷰 컴포넌트 단위로 화면을 분리해서 개발했다. 어차피 테스트가 가능한 코드를 만드는 것이 최종 목표이기 때문에 뷰 컴포넌트 단위로 테스트를 진행하기로 결정했다. 그리고 필자는 Vue.js 하위 프로젝트인 “vue-test-utils”를 아주 잘 사용하고 있다. (12) 뷰 컴포넌트로 마이그레이션 하기 제니퍼 화면에서는 자체 개발한 수많은 컴포넌트들을 사용한다. 크게 대시보드와 리얼타임 화면에서 사용되는 캔버스 차트가 있고, 분석이나 통계, 보고서 템플릿 화면에서 사용되는 SVG 차트가 있다. 그리고 모든 화면에서 그리드, 달력, 콤보박스 등등 수많은 컴포넌트들을 두루 사용하고 있다. JUI 라이브러리는 그 중에서 일부를 공개한 것이다. 막상 Vue.js로 화면 개발을 하다보니 ES6 모듈로 마이그레이션 된 기존의 컴포넌트들을 사용하기가 어려웠다. 아무래도 제니퍼 화면은 컴포넌트 비중이 높기 때문에 Vue.js가 제공하는 기능들을 제대로 활용하지 못했다. 그래서 먼저 JUI 라이브러리를 뷰 컴포넌트로 마이그레이션 하기로 결정했다. 불행(?) 중 다행으로 차트는 몇달 전부터 시작해서 어느 정도 마무리가 된 상태였다. JUI 라이브러리가 가지는 기존의 색은 모두 버리고, 최대한 Vue.js 특성에 맞게 마이그레이션 하려고 신경썼다. 쉽지 않은 일이었지만 결국 차트(23종), 그리드(2종), UI(13종)의 뷰 컴포넌트를 제공하게 되었고, GitHub에 프로젝트를 공개했다. 현재는 제니퍼 화면에 의존성이 높은 전용 컴포넌트들을 마이그레이션 하고 있다. <제니퍼 EVENT 분석 화면> (13) Jest 설정시 주의사항 테스트 프레임워크는 요즘 많이 사용하고 있는 Jest를 선택했는데, 기본 설정을 하는 과정에서 몇일동안 삽질한 부분만 짧막하게 짚고 넘어가려고 한다. 만약에 테스트 대상 모듈에서 NPM으로 설치한 모듈을 사용하고 있다면 SyntaxError: Unexpected identifier 에러가 발생한다. 필자가 공식 매뉴얼만 제대로 읽었다면 쉽게 해결할 수 있는 간단한 문제였다. 그것은 바로 transformIgnorePatterns 옵션의 기본값이 [“/node_modules/”]로 설정되어 있기 때문이다. 간단하게 빈 배열로 변경하거나 각자의 프로젝트에 맞는 패턴을 설정하면 된다. 글을 마치며… 결과는 만족스러웠지만 조금만 더 빨리 시작했으면 좋았을텐데, 후회도 아쉬움도 많이 남는 일이었다. 일을 진행하면서 문제가 생겨 몇번의 핫픽스 버전을 릴리즈 했었다. 그만큼 레거시 시스템을 엎는다는건 조심스럽고 예민한 일이다. 하지만 앞으로 몇년을 생각하면 언젠가는 해야만 하는 일이다. 늦으면 늦을수록 위험부담이 커지기 때문에 기회가 오면 바로 시작해야 한다.