프로그래밍/Java 검색 결과

5개 발견
  1. 미리보기
    2018.11.13

    Eclipse에서 Darkest Dark Theme 적용 후 추가로 해줄것

  2. 미리보기
    2015.04.13

    Map과 VO(Value Object)의 해묵은 논쟁과 나의 결론..

  3. 미리보기
    2015.03.23

    JBoss Application Server 7에 Oracle Database Connection Pool 생성시 주의점..

  4. 미리보기
    2015.03.23

    Mybatis의 Plug-In을 이용한 SQL문 파라미터 바인딩 로그 출력..

  5. 미리보기
    2012.03.21

    Java Collection 정리된 내용

Eclipse에서 Darkest Dark Theme 적용 후 Git과 연동하면서 색상이 보기 흉하게 바뀌는 상황이 있었다. 이유는 Git으로 파일 관리를 하면서 파일의 상태가 바뀌었을때 이에 대해 표현하는 방법때문이었다. 예를 들면 파일을 수정할 경우 Commit이 아직은 안되어 있는 상태이기 때문에 Uncommited 인데 이때 이 자원을 나타내는 background 색상이 흰색으로 설정되어 있어서 전체적으로 검은색 테마에서 배경색을 흰색으로 주다보니 어울리지 않아 보기 싫은 상황이 생겼다. 암튼 Darkest Dark Theme를 사용하면서 배경색이 흰 색으로 되어 있는 부분은 튀어 보이는 상황이 벌어지는 것이 있는데 일단 예를 들은 경우를 수정할려면..(차후에 이런 상황들이 또 일어나면 여기에 정리할 예정이다)


1. Window -> Preference -> General -> Appearance -> Color And Fonts 에 간 뒤 Git 항목의 하위 항목으로 Uncommited Change(Background)를 찾아보면 현재 색상이 흰 색으로 되어 있는데 이것을 검정으로 바꿔준다.


2. Git의 ignored 파일에 속한 파일들도 그 배경색이 흰 색이기 때문에 Window -> Preference -> General -> Appearance -> Color And Fonts 에 간 뒤 Git 항목의 하위 항목으로 Ignored Resource (Background) 를 찾아서 이 색상을 검정으로 바꿔주면 된다.

트랙백을 확인할 수 있습니다

URL을 배껴둬서 트랙백을 보낼 수 있습니다

다른 카테고리의 글 목록

프로그래밍/Java 카테고리의 포스트 목록을 보여줍니다

프로그래머란 생활을 10여년 가까이 하면서 프로그래머들간에 이런저런 논쟁을 하게 되는 상황을 종종 보게 된다. 논쟁이란 것이 결론이 나는 것도 있고 그렇지 않은 것도 있다보니 진행중인 논쟁은 언제나 있기 마련이다. ORM 방식의 개발과 SQL 방식의 개발중 어느것이 나은가?, 왜 SpringFramework에 종속적이어야 하는가 등등 직간접적으로 몇가지 접하는 논쟁이 있다. 오늘은 그 중 데이터 전달을 Map으로 하는게 나을지, VO로 하는게 나을지에 대해 좀 얘기해보고자 한다.


먼저 이 논쟁이 나온 배경에 대해 이해를 할 필요가 있어 이 부분을 설명해보도록 하겠다. 우리가 어떤 엔티티(엔티티는 설계상의 용어이므로 잘 이해가 안되는 분은 클래스라고 생각하시길 바란다)를 구체화한 객체를 전송할때 이런식으로 구성할 것이다. 예를 들어 보자. 사용자가 쓴 게시물 데이터를 전달하는 방법을 생각해보자. 이 게시물은 제목과 내용, 단 두 가지의 항목으로 구성되어 있다. 그러면 우리가 흔히 이 데이터를 특정 함수에 전달해서 이 메소드에서 사용하도록 하고자 할 때 다음의 3 가지중 한 가지 방법을 사용할 것이다.


1. 메소드의 파라미터로 정의해서 넘기는법


public void writeBoard(String title, String content){
...
}

writeBoard("aaa", "bbb");


2. Map에 담은뒤에 이를 파라미터로 전달


public void writeBoard(Map<string, string> param){
...
}


Map<string, string> param= new HashMap<string, string>();
param.put("title", "aaa");
param.put("content", "bbb");

writeBoard(param);


3. VO(Value Object)에 담은뒤에 이를 파라미터로 전달


public class BoardVO{
     private String title;
     private String content;

     // getter, setter 메소드는 있다고 생각하고 여기서는 생략
}

public void writeBoard(BoardVO param){
...
}

BoardVO param= new BoardVO();
param.setTitle("aaa");
param.setContent("bbb");

writeBoard(param);


대강 메소드와 클래스의 정의, 그리고 이를 사용하는 방법으로 3가지 형태를 설명했다. 먼저 1의 방법은 구현은 정말 단순하다. 묶어서 어디다 넣을 필요도 없이 걍 1대1 대응으로 파라미터에 넣으면 되기 때문이다. 그러나 세상에 변하지 않는 것은 없다. 지금 당장 변하지 않는다고 정의가 되었더라도 변할 경우 대응할 여지는 남겨야 하는게 설계다. 우리가 코드 설계 및 구현을 하면서 항상 간과하는것이 있다. 세상에 변하지 않는건 없다. 나는 세상에 변하지 않는거라곤, 사람이든 사물이든 시간이 지나면 어떤식으로든 마지막을 맞이하는 원칙 말고는 확실히 결정된 원칙은 하나도 없다고 생각하는 사람이다. 비즈니스 로직또한 마찬가지이다. 지금 당장의 필요성으로 따지면 제목과 내용만 필요하니 그렇게 말했을지 모른다. 그러나 하루가 지나면 등록자 이름이 필요할 상황이 올수도 있고 1주일이 지나면 등록일이 필요한 상황이 올수도 있으며, 한달이 지나면 답글에 대한 정보가 필요한 상황이 올 수 있다. 우리가 만든 프로그램이 그런 변화가 발생할때 최소한의 코드 변화로 최대한 대처하게 할 수 있는가에 대한 고민을 해야 한다. 그런점에서 봤을때 1번은 정말 옆에 저렇게 개발한 개발자가 있다면 스트레이트 펀치를 날리고 싶은 설계이다. 단순히 생각하면 추가 수정사항이 발생할때마다 함수 파라미터를 늘리면 되지..이럴수도 있을 것이다. 그러나 함수의 정의를 고치면 그것을 사용하는 곳 또한 고쳐야 한다; 파라미터 갯수가 틀려지고 타입이 틀려지는데 그게 정상적으로 동작하겠는가? 더군다나 저 메소드를 호출한 곳이 1군데가 아니라 10군데, 100군데라고 가정해보자. 그러면 우리는 일일이 찾아서 저 함수를 호출한 부분을 고쳐야 한다. 또한 추가되거나 삭제되는 파라미터가 옵션, 즉 반드시 필요한게 아니라 해도 메소드 정의가 바뀌었기 때문에 일일이 찾아서 고쳐줘야 한다.


2번과 3번의 경우는 그나마 사정이 나은 편이다. Map이 됐든 VO로 만들든 수정사항이 발생할 경우 수정사항을 필요로 하는 곳에서만 데이터를 셋팅만 해주면 되고 그것을 사용하는 부분은 고칠 필요가 없다. 그런 점에서는 확실히 1번보다는 2번과 3번이 낫다.


문제는 2번과 3번, 즉 Map을 사용하는것이 나으냐, 아니면 VO를 사용하는 것이 나으냐..란 점이다. 방법 자체는 흠 잡을때가 없다. 실제로 디자인 패턴을 보면 Map이든 VO든 객체를 값으로 전달하는 것은 하나의 패턴으로 인식되어 얘기되어지고 있다. 머 디자인 패턴 자체를 얘기할려고 하는게 아니라 그만큼 검증은 되어 있다는 점에서 얘기하는 것이다. 2번과 3번 모두 문제는 딱히 없어보이는데도 사람들은 Map이 낫다고 하느니 VO가 낫다고 하느니 이거 가지고 논쟁을 벌이고 있고 아직도 진행중이다. 그럼 왜 우리가 이런 논쟁에 휩쓸리고 있는지 그 배경에 대해 생각 해 볼 필요성이 있다.


Map이 낫다고 주장하는 사람의 논리는 이렇다. 요구사항 변경시 수작업을 해야 할 부분이 많다는 것이다. 예를 들어 VO로 할 경우 A라는 VO에서 B 라는 VO로 바꿔야 할 경우 이 부분을 수정하는 것이 연쇄적으로 발생해서 이를 수정하는데 시간이 오래 걸린다는 것이다. 이 부분은 특히 우리나라 SI 환경에서는 민감하다. 왜냐면 우리나라 SI 플젝을 보면 열중 일곱여덟은 오픈하는 1주일전까지도 확정이 안되어있는 사항이 있을 정도로 변화무쌍하다는 것이다. 남은 시간이 얼마 남지 않은 상황에서 추가 변경 상황이 발생할 경우 Map으로의 전달은 대처하기가 쉽다는 것이다.


VO가 낫다고 주장하는 사람의 논리는 이렇다. Map의 경우는 put이나 get 메소드의 사용을 일일이 보지 않는 한에는 무엇을 전달할려고 하는지 그 의미를 알수가 없다는 것이다. VO일 경우 소스의 주석이나 변수명만 봐도 어떤 값들이 전달되는지 알 수 있으나 Map의 경우 실제 그 Map이 사용되는 곳을 봐야 알수 있는것이다.


양쪽 모두 틀린 얘기는 아니다. 모두 이론적인 얘기가 아닌 현실에서 발생하는 문제를 가지고 이슈제기를 하는 것이니까..나도 이 두 쪽의 의견에는 전적으로 공감은 하는 바이다. 그러나 우리가 앞으로 나가야 할 방향을 생각해야 한다면 나는 VO가 옳다고 생각한다. 이제 이렇게 생각하는 이유에 대해 얘기를 해보겠다.


근래에 하이버네이트를 공부하면서 나름 충격을 먹은게 하나 있었다. 하이버네이트 세미나 동영상 중 이런 내용이 있었다. 우리나라 개발자는 프로그램을 만드는데 있어 프로그램을 먼저 설계하는 것이 아니라 프로그램을 통해 만들어지는 값이 저장되는 DB 설계를 먼저 하고 프로그램설계를 그 나중에 한다는 것이다. 난 이걸 보고 정말 충격을 먹었다. 그럴수밖에 없었다. 왜냐면 당연히 했었어야 하는 자기 질문을 이제껏 한번도 안했기 때문이다. 나는 프로그래머고 프로그램을 만드는 사람이면 현실의 개념에 맞춰 프로그램을 설계해야 하는데 언제부턴가 프로그램을 통해 나오거나 또는 이용되는 값이 저장되는 DB에 의존적인 프로그램 설계를 하고 있는것이다. 예를 들어보자. 회원 정보를 보면 이름도 들어가고, 주소도 들어가고, 전화번호도 들어간다. 주소의 경우s,s 우편번호와 우편번호에 매핑되는 주소, 그리고 상세주소 속성이 있게 되고 이를 확장해서 사용할 가능성이 있는 회사 주소란 개념과 집 주소란 개념도 있다. 전화번호도 국번과 번호란 속성이 있으며 이용용도에 따라 핸드폰 번호, 집 번호, 회사번호 등 여러가지가 있다. 그러면 회원정보를 클래스로 설계할때 우편번호를 나타내는 zipcode와 우편번호와 엮어지는 주소인 address1, 상세주소를 의미하는 detailaddress라는 속성으로 구성되는 Address 란 주소를 의미하는 클래스를 만들고 이를 상속한 집주소를 의미하는 HomeAddress, 회사주소를 의미하는 JobAdrdess 이렇게 클래스를 만들어서 회원정보의 멤버필드로 HomeAddress 타입, JobAddress 타입의 멤버변수를 만들어서 설계하는것이 객체지향 방식의 설계이다. 그러나 DB는 이렇게 구현할수가 없다. 여러개의 속성을 하나로 묶어서 표현하게 되는 객체와 부모의 속성을 물려받아 다시 사용하는 상속이란 개념이 없기 때문에 원데이터를 그냥 넣어야 한다. 즉 문자열 타입의 ziipcode 컬럼과 address1 컬럼을 만들어야 한다. 즉 클래스의 속성들을 풀어서 테이블의 컬럼으로 구성해야 하는 것이다. 그러나 이런 구조이기 때문에 클래스와 DB 컬럼이 1대1로 매핑이 되지 않기 때문에 우리가 생각하는 객체지향적인 설계와 DB 구성이 Mismatch가 일어나게 된다. 또 이런 증상은 새로운 속성이 생기거나 기존 속성이 없어질때도 마찬가지로 벌어지게 된다. 그러다보니 결국은 클래스 설계를 DB 컬럼과 매핑이 되는 식으로 설계해서 유지보수 하기 용이하게끔 하고 있다. 즉 별도의 클래스로 따로 빼서 확장이 용이하게 설계할수 있는데도 불구하고 이런 문제땜에 일일이 풀어서 설계하고 있는 것이다.


이 얘기를 왜 하냐면 우리가 만드는 것이 무엇인가..에 대한 고민을 전혀 하지 않고 만들고 있기 때문에 그런것이다. 우리가 만드는 프로그램은 현실 생활의 변화에 맞춰 최소한의 변경만으로도 변화에 적응할수 있게끔 만들어야 하는데, 어느 순간부터 단지 DB에서 검색이 잘 되게끔 하기 위한 자료구조 형태에만 포커스를 맞추고 있기 때문이다. 즉 DB중심위주의 프로그래밍이 아닌 현실 위주의 객체지향적인 프로그래밍으로 전환해야겠다 생각하던 시점에 요근래 내가 잘 가는 커뮤니티에서 Map과 VO 논쟁을 겪었다. 그래서 현실 위주의 객체지향적인 프로그래밍 관점에서 낸 결론은 VO가 낫다는 것이다


최소한의 변화로 최대한의 수정효과를 거두는것에서 놓고 보면 VO가 단연 좋다. 해당 관심사의 집약체가 VO이기 때문에 바꿔말하면 VO에 대한 수정만 해주면 큰 문제가 없다. 오히려 Map의 경우는 이 수정을 할 경우 제대로 동작하는지 확인할 길이 없다. 버그가 내재되어 있다는 것이다. 여러개의 타입의 값을 전달해야 하기 때문에 Map은 선언 특성상 Value를 Object로 선언할수 밖에 없다.그러나 Object는 최상위 타입이기 때문에 타입 미스매치가 발생하는 상황이 있어도 실제로 돌려보지 않는 한에는 알 수가 없다. 그러나 VO에서는 멤버변수에 대한 구체적인 타입을 정할수 있기 때문에 만약 타입이 바뀔 경우 컴파일러에서 에러가 발생하므로 이를 바로 잡아낼수 있다.또한 반드시 들어가야 하는 값이라면 생성자의 파라미터로 필요한 값을 넣게끔 설계해주면 컴파일러에서 그렇게 맞추지 않았을 경우 에러가 발생하기 때문에 버그없이 구현될수 있다.


혹자는 이 점을 들어서 그게 VO의 단점이라고 얘기하는데 나는 그 얘기는 정말 단순한 발상에서 나온거라 생각한다. VO로 구현해서 수정할 경우 이클립스 같은 IDE 툴에서 VO 변경이 되었기 때문에 당연 에러가 쫙쫙 나올것이고 개발자는 그 에러를 고치느라 노가다를 할 것이다. 그러나 이것은 눈에 보이는 명확한 에러를 우리에게 보여주었기 때문에 에러를 수정하면 버그가 내재되지 않는다. 그러나 Map으로 했다고 가정해보자. 값이 들어가는 Value의 타입이 최상위 타입인 Object로 했기 때문에 모든 타입을 다 받아들일수 있어서 당장은 수정해야 할 부분은 없어보일수도 있다. 그러나 눈에은 안보이는 부가적인 Cast 연산이 추가로 발생되며 또한 타입에 대한 부정확성 때문에 버그가 내재될 소지도 크다. 또 만약 필수로 들어가야 하는 항목인데 put을 안했다면 논리적인 잘못이 있으나 컴파일러상에선 오류가 발생할 상황은 아니기 때문에 이 또한 버그의 소지를 안게된다


그리고 이 문제는 VO를 수정하는 상황이 없으면 이럴일도 발생하지 않는다. 위에서 잠깐 우리나라 SI 개발환경에 대해 언급을 했는데, 그런 개발환경땜에 문제가 없는 개발방법을 문제가 있다고 생각하는 사람들을 보면 정말 한숨뿐이 안나온다. 만약 설계사항이 모두 도출되었고(여기서는 변경의 여지가 있다..라는 얘기까지 나와서 변경이 될 경우의 설계까지도 반영되었다고 전제한다)..그에 맞춰 설계했다면 VO는 효율적이면 효율적이지 절대 비효율이 아니다. 확장성이 용이한데다가 관심사가 집중되기 때문에 여러군데 손을 댈 필요가 없다. 또한 타입이 분명하기때문에 적어도 타입관련 버그가 있을수가 없다. Collection 타입 조차도 Generic의 도입으로 안에 무슨 타입이 들어간다는걸 사전에 정의할 수 있기 때문에 컴파일러에서 조차 타입관련 에러를 잡아낼수 있다.


그러나 Map의 경우 값은 모든 타입을 다 받아들일수 있게끔 최상위 타입인 Object로 값 부분의 타입을 해야 한다. 물론 모든 타입을 String으로 할수도 있을것이다. 그러나 Object가 됐든 String이 됐든 이럴 경우 부가적인 Castring이 필요하다. 계산을 할려면 숫자형으로 변경해야 하며 변경을 하지 않을 경우 타입이 불분명하기 때문에 동작에 있어서 버그가 존재할 여지는 충분히 있다. 두리뭉실한 타입은 개발에서는 편할수 있으나 이것도 되고 저것도 되는 식의 타입은 자칫 동작에 있어서 오류를 가질 소지가 크기 때문이다. 이렇게 쓰면 Casting 작업이 비효율적인 작업이라고 오해하는 사람이 있겠으나 나는 비효율적인 작업이라고 하는게 아니라 불필요한 작업이라고 말하고 있는것이다. 타입을 명확하게 정의하면 이 작업이 필요가 없는데 타입을 명확하게 정의하지 않았기 때문에 이 작업이 필요하게 되는 것이다. VO로 개발하면 이런 작업이 필요가 없는데 왜 불필요한 작업을 하나 더 거치게 해서 전반적으로 비효율적인 작업으로 이르게끔 하냐는 것이다.


이런 조금만 생각해보면 되는것을 단지 우리나라 SI 환경에는 맞지가 않다고 VO를 안좋다고 말하는 사람들을 보면 답답하다. 개발을 하다보면 그런 현실적인 문제땜에 Map을 선택해서 개발할수도 있다. 현실적인 문제땜에 Map을 선택할수 밖에 없었던 것에 대해서는 머라 하진 않겠다. 그러나 그 방법이 옳다고 말할수는 없는것이다. 이건 마치 정당방위로 어쩔수 없이 사람을 죽였는다. 사람을 죽이는 행위는 올바른 것이다 와 다름없는 논리다. 상황땜에 그렇게 개발한것을 가지고 왜 그것을 VO로 개발하면 안되는거라고 주장하는가? 비정상적인 환경에서 도구가 쓰였다고 애초에 문제가 없는 도구를 문제삼을수는 없는 것이다. 설계를 할때 요구정의를 사전에 미리미리 결정을 짓고 충분히 검토하고 구현하면 그럴 일이 없는데 단지 그런 환경이 아닌 우리나라여서 그래서 VO를 이용한 개발방법은 문제가 있다는 식의 논리는 맞지가 않다는 것이다. 혹자는 왜 환경탓을 하냐고 하지만 이것은 환경탓이다. 그런 환경이라면 바꿔야 하는게 맞는거지 그런 환경에서까지 버그 없이 완벽한 개발방법은 존재하지 않기 때문이다. 


혹자는 이걸 보고 VO를 만능으로 보는것 아니냐고 생각하는데 나는 만능이라고 보진 않는다. 다만 현재 나와 있는 개발방법에서 버그가 없고 전달하는 의도가 분명하며 관심사에 변화가 발생했을시 최소한의 대응을 할수 있는 것을 VO라 보기 때문이다. 만약 VO보다 더 좋은 방법이 있음 그걸로 갈아 탈 것이다. 그러나 VO와 Map을 놓고 비교해보면 아직은 VO가 더 우선순위에 있다고 말하고 싶다. 그리고 만약 VO가 안좋다고 한다면 왜 대형 프로젝트에선 UML로 설계해서 나온 엔티티를 Map으로 구현할탠데 왜 그렇지 않은가? 이는 VO로의 구현 방법이 객체지향적인 설계 방법에서도 옳은 구현이기 때문이다. 관심사의 집중, 최소한의 변화로 최대의 효과..현재로썬 그걸 잘 구현한 방법은 VO라고 본다.


트랙백을 확인할 수 있습니다

URL을 배껴둬서 트랙백을 보낼 수 있습니다

다른 카테고리의 글 목록

프로그래밍/Java 카테고리의 포스트 목록을 보여줍니다
Spring을 사용하면서 Oracle Database Connection Pool을 등록할때마다 늘 발생하는 상황이지만..
Oracle 11g에 있는 ojdbc6.jar를 이용해서 Oracle Database Connection Pool을 구성하면 네트워크 관련 에러가 발생한다.
이 부분은 Spring 자체에서 Connection Pool을 만들든, Spring에서 별도의 Connection Pool 라이브러리(예를 들면 c3p0 같은 라이브러리..)를 사용하든, WAS에 Database Connection Pool을 만들어서 이용하든 항상 ojdbc6.jar를 사용하면 발생하게 되는데..
이것을 ojdbc14.jar(내가 알기론 JDK 1.4 버전에 맞춰진 Oracle용 JDBC 드라이버로 알고 있음)로 사용하면 에러가 발생하지 않는다.

이번에 프로젝트에 참여할 곳이 결정되어서 거기서 JBoss를 쓴다길래 한번도 사용해본 경험이 없어서 JBoss Application Server 7을 다운받아 셋팅을 시도해 보았다. 근데 여기서는 ojdbc14.jar를 사용하여 Database Connection Pool을 설정하는 방법대로 해도 되지를 않았다. 원인이 무엇인가 해서 열씨미 구글링 하여 해결책을 찾았다.

흔히 JBoss 설정파일에 Oracle JDBC 드라이버를 등록하면서 다음의 내용을 넣었을 것이다.(다음의 예는 ojdbc14.jar를 사용하다보니 module 디렉토리를 만들때 ojdbc14라는 네이밍을 썼다)

<driver name="OracleJDBCDriver" module="com.oracle.ojdbc14">
        <xa-datasource-class>oracle.jdbc.OracleDriver</xa-datasource-class>
</driver>


근데 구글링을 한 바로는 ojdbc14.jar는 jdbc4 규격에 호환이 되지 않는다고 한다. 그래서 다음과 같이 driver-class 태그를 따로 사용해서 등록해야 한다고 되어 있다.(아마 추측엔 ojdbc6.jar의 경우엔 oracle.jdbc.OracleDriver 클래스가 XA DataSource 역할도 같이 수행할수 있게끔 기능이 되어 있지만 ojdbc14.jar의 oracle.jdbc.OracleDriver 클래스의 경우엔 XA DataSource 역할을 할 수 없기 때문에 분리해서 등록해야 한다는 생각을 해본다)

<driver name="OracleJDBCDriver" module="com.oracle.ojdbc14">
        <driver-class>oracle.jdbc.OracleDriver</driver-class>
        <xa-datasource-class>oracle.jdbc.xa.client.OracleXADataSource</xa-datasource-class>
</driver>
이렇게 설정하면 JBoss에서 ojdbc14.jar를 이용하여 Database Connection Pool을 만들수가 있다.
관련 내용은 여기에서 참조했다(답글 달은 내용에 있음)


트랙백을 확인할 수 있습니다

URL을 배껴둬서 트랙백을 보낼 수 있습니다

다른 카테고리의 글 목록

프로그래밍/Java 카테고리의 포스트 목록을 보여줍니다

Mybatis에서는 Plug-In을 이용하여 Mybatis가 쿼리를 실행하는 시점에 간섭하여 사용자가 정의한 별도 작업을 진행할수 있다. 예를 들면 쿼리가 실행되기 전 또는 실행된 후에 해당 쿼리가 몇번 실행됐는지 그 실행횟수를 업데이트하는 그런 예를 들수 있다. 이 글에서는 쿼리를 실행하기 전에 로그에 파라미터가 바인딩된 쿼리 로그를 출력하는 Plug-In을 설명하고자 한다. 이 글에서는 Mybatis Plug-In에 대한 구체적인 설명은 하지 않는다. 다만 이 글에서 보여주는 Source의 주석으로 관련 내용을 설명했으니 참고하기 바란다. log4jsql이나 log4jdbc같은 괜찮은 로그툴이 있으나 굳이 이것을 만든것은 로그의 출력형태가 반드시 로그 파일 형태로만 갈수는 없기 때문이다. DB에 기록할수도 있고 별도 로직이 들어가야 하는 상황이 발생할 수도 있는데 이럴 경우엔 자신이 직접 이런 로그 기록하는 Plug-In을 구현해야 하기 때문이다. 이 소스에서 로그를 출력하는데 있어 사용한 라이브러리는 SLF4j 라이브러리를 사용했으나 다른 로그 라이브러리를 사용하고 있다면 해당 라이브러리에 맞춰 로그 출력 부분을 바꾸어주면 된다. 일단 전체 Source는 다음과 같다

(첨부파일 :  MybatisLogInterceptor.java)


package com.terry.boardprj32.common;

import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Intercepts({
    @Signature(type=StatementHandler.class, method="update", args={Statement.class})
    , @Signature(type=StatementHandler.class, method="query", args={Statement.class, ResultHandler.class})
})
public class MybatisLogInterceptor implements Interceptor {

    private Logger logger = LoggerFactory.getLogger(this.getClass());
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
	// TODO Auto-generated method stub
        StatementHandler handler = (StatementHandler)invocation.getTarget();
        
        BoundSql boundSql = handler.getBoundSql();
        
        // 쿼리문을 가져온다(이 상태에서의 쿼리는 값이 들어갈 부분에 ?가 있다)
        String sql = boundSql.getSql();
        
        // 쿼리실행시 맵핑되는 파라미터를 구한다
        Object param = handler.getParameterHandler().getParameterObject();
        
        if(param == null){				// 파라미터가 아무것도 없을 경우
            sql = sql.replaceFirst("\\?", "''");
        }else{						// 해당 파라미터의 클래스가 Integer, Long, Float, Double 클래스일 경우
            if(param instanceof Integer || param instanceof Long || param instanceof Float || param instanceof Double){
                sql = sql.replaceFirst("\\?", param.toString());
            }else if(param instanceof String){	// 해당 파라미터의 클래스가 String 일 경우(이 경우는 앞뒤에 '(홑따옴표)를 붙여야해서 별도 처리
                sql = sql.replaceFirst("\\?", "'" + param + "'");
            }else if(param instanceof Map){		// 해당 파라미터가 Map 일 경우
        	
        	// 쿼리의 ?와 매핑되는 실제 값들의 정보가 들어있는 ParameterMapping 객체가 들어간 List 객체로 return이 된다.
        	// 이때 List 객체의 0번째 순서에 있는 ParameterMapping 객체가 쿼리의 첫번째 ?와 매핑이 된다
        	// 이런 식으로 쿼리의 ?과 ParameterMapping 객체들을 Mapping 한다
        	List<ParameterMapping> paramMapping = boundSql.getParameterMappings();	
        	
        	for(ParameterMapping mapping : paramMapping){
        	    String propValue = mapping.getProperty();		// 파라미터로 넘긴 Map의 key 값이 들어오게 된다
        	    Object value = ((Map) param).get(propValue);	// 넘겨받은 key 값을 이용해 실제 값을 꺼낸다
        	    if(value instanceof String){			// SQL의 ? 대신에 실제 값을 넣는다. 이때 String 일 경우는 '를 붙여야 하기땜에 별도 처리
        	        sql = sql.replaceFirst("\\?", "'" + value + "'");
        	    }else{
        	        sql = sql.replaceFirst("\\?", value.toString());
        	    }
        	    
        	}
            }else{					// 해당 파라미터가 사용자 정의 클래스일 경우
        	
        	 // 쿼리의 ?와 매핑되는 실제 값들이 List 객체로 return이 된다.
        	 // 이때 List 객체의 0번째 순서에 있는 ParameterMapping 객체가 쿼리의 첫번째 ?와 매핑이 된다
        	 // 이런 식으로 쿼리의 ?과 ParameterMapping 객체들을 Mapping 한다
        	List<ParameterMapping> paramMapping = boundSql.getParameterMappings();
        	
        	Class<? extends Object> paramClass = param.getClass();

        	for(ParameterMapping mapping : paramMapping){
        	    String propValue = mapping.getProperty();			// 해당 파라미터로 넘겨받은 사용자 정의 클래스 객체의 멤버변수명
        	    Field field = paramClass.getDeclaredField(propValue);	// 관련 멤버변수 Field 객체 얻어옴
        	    field.setAccessible(true);					// 멤버변수의 접근자가 private일 경우 reflection을 이용하여 값을 해당 멤버변수의 값을 가져오기 위해 별도로 셋팅
        	    Class<?> javaType = mapping.getJavaType();			// 해당 파라미터로 넘겨받은 사용자 정의 클래스 객체의 멤버변수의 타입
        	    
        	    if(String.class == javaType){				// SQL의 ? 대신에 실제 값을 넣는다. 이때 String 일 경우는 '를 붙여야 하기땜에 별도 처리
        	        sql = sql.replaceFirst("\\?", "'" + field.get(param) + "'");
        	    }else{
        	        sql = sql.replaceFirst("\\?", field.get(param).toString());
        	    }
        	    
        	}
            }
            
        }
         
        logger.debug("=====================================================================");
        logger.debug("sql : {}", sql);
        logger.debug("=====================================================================");
        
        return invocation.proceed(); // 쿼리 실행
    }

    @Override
    public Object plugin(Object target) {
	// TODO Auto-generated method stub
	return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
	// TODO Auto-generated method stub

    }

}


Source안에 설명을 넣어놔서 Source를 보는데 불편함이 있을수 있으나 말하고자 하는 핵심은 사실 몇가지 안된다. 우리가 쿼리를 로그로 출력할때 알아야 할것은 2가지다.


1. 쿼리를 어디서 가져올수 있는가?

2. 쿼리에서 사용하는 파라미터의 값들은 어디서 가져올수 있는가?


첫번째 질문인 쿼리를 어디서 가져올수 있는가에 대한 답은 의외로 쉽다. invoke 함수의 파라미터로 넘어오는 Invocation 객체를 StatementHandler 객체로 캐스팅 한 뒤에 StatementHandler에서 제공하는 BoundSql 객체를 통해 다음과 같이 얻어올수 있다


        StatementHandler handler = (StatementHandler)invocation.getTarget();
        BoundSql boundSql = handler.getBoundSql();
        // 쿼리문을 가져온다(이 상태에서의 쿼리는 값이 들어갈 부분에 ?가 있다)
        String sql = boundSql.getSql();


그러면 이제 2번째 질문은 쿼리에서 사용하는 파라미터를 가져오는 부분이다. 이 부분을 찾느라 시간이 걸렸는데 mybatis에서 제공하는 자바 클래스의 javadoc 문서를 찾을수가 없었다. 메뉴얼 성격의 문서는 있는데 정작 클래스 관련 레퍼런스 문서를 찾을수가 없었다. 그래서 구글링을 통해 관련 내용을 검색하여 구현한뒤에 디버깅 모드로 일일이 추적해서 찾았다. 일단 쿼리에서 사용한 파라미터를 가져오는 것은 쉽다. 다음과 같이 하면 된다


        
        // 쿼리실행시 맵핑되는 파라미터를 구한다
        Object param = handler.getParameterHandler().getParameterObject();


그러나 이렇게 넘겨받은 파라미터는 Object 클래스 객체이기 때문에 어떤 클래스가 넘어온건지 알 수가 없다. 쿼리에서 사용한 파라미터가 1개 이상임을 감안한다면 넘겨받은 파라미터는 단순한 String 객체나 int 형 값일수도 있고, Map 객체일수도 있으며 프로그래머가 만든 별도 클래스의 객체일수도 있다. 그렇기 때문에 파라미터로 넘겨받은 객체가 어떤 클래스인지를 확인해야 할 필요가 있는 것이다. Java의 Reflection을 활용하면 어떤 클래스인지를 확인할 수 있다. 그래서 이 코드에서는 따로 기입을 안했으나 이 코드를 만드는 과정에서는 Reflection을 활용해서 클래스가 무엇인지를 확인한후에 instanceof 를 이용한 if 문으로 분기문을 만들었다


그러면 이렇게 파라미터로 넘겨받은 객체의 타입을 확인한 다음엔 넘겨받은 객체 안에 있는 실제 값을 쿼리에 매핑해줘야 한다. primitive 타입(int, long등)을 넘겨주었을 경우엔 Wrapper 클래스로 던져주기 때문에 파라미터 값으로 int 형을 주면 파라미터 객체의 클래스는 Integer 타입이 된다. 이것을 생각하고 보길 바란다. String이나 Integer, Long 같은 Wrapper 클래스일 경우엔 파라미터는 1개만 들어왔다는 것을 의미하지만 Map 일 경우나 프로그래머가 만근 별도 클래스일 경우엔 파라미터가 여러개가 있다는 것으로 생각할수가 있다. 1개만 있을땐 단순하다. toString 메소드를 실행하면 String 형태로 파라미터로 사용된 실제 값을 얻을수 있기 때문에 이것을 바로 쿼리 안에 있는 1개뿐인 ? 대신에 실제 값으로 replace 하면 되니까..그러나 Map이나 별도 클래스일 경우엔? Map 일땐 값이 들어가 있는 key를 알아야 key를 이용해서 값을 읽을수 있을테고 별도 클래스일 경우엔 변수명을 알아야 Reflection을 이용해서 꺼내올수 있을텐데.. 


바로 이럴때 Mybatis에서는 파라미터 값들을 읽어올수 있는 정보를 준다. BoundSql 클래스는 쿼리에서 사용되는 파라미터들의 정보가 들어있는 List 객체를 넘겨주는 함수인 getParameterMappings() 메소드를 제공해준다. Mybatis에서는 파라미터 정보를 제공하는 ParameterMapping 클래스가 있다. 이 클래스를 보면 해당 파라미터에 대한 여러가지 정보를 제공해주는데 그 중엔 파라미터의 타입(JavaType, JdbcType)과 파라미터 값을 읽어올수 있는 근거(파라미터로 넘겨받은 객체의 타입이 Map 일 경우엔 값을 읽어올수 있는 key, 별도 클래스일 경우엔 관련 변수명)를 제공해준다. 쿼리에서 여러개의 파라미터를 사용할 경우 물음표(?)가 여러개 있을것이다. 이럴때 해당 물음표에 어떤 파라미터로 매핑되어야 하는지가 List 객체에 있다. List 객체의 0번째에 있는 ParameterMapping 객체가 쿼리에서 보이는 첫번째 물음표와 매핑이 되고 List 객체의 1번째에 있는 ParameterMapping 객체가 쿼리에서 보이는 두번째 물음표와 매핑이 된다. 이런 식으로 쿼리에서 사용하는 물음표와 List 객체 안에 있는 ParameterMapping 객체를 매핑시킨다.


파라미터로 넘겨받은 객체가 Map일 때를 보자. Map 일 경우엔 key를 알아야 해당 key에 대한 값을 읽어올수 있다. ParameterMapping 클래스에서 제공하는 getProperty 메소드를 통해 Map에서 사용한 key 값을 읽어올수 있다. 그래서 다음과 같이 쿼리에 매핑하게 된다


        
             }else if(param instanceof Map){		// 해당 파라미터가 Map 일 경우
        	
        	    // 쿼리의 ?와 매핑되는 실제 값들의 정보가 들어있는 ParameterMapping 객체가 들어간 List 객체로 return이 된다.
        	    // 이때 List 객체의 0번째 순서에 있는 ParameterMapping 객체가 쿼리의 첫번째 ?와 매핑이 된다
        	    // 이런 식으로 쿼리의 ?과 ParameterMapping 객체들을 Mapping 한다
        	    List<ParameterMapping> paramMapping = boundSql.getParameterMappings();	
        	
        	    for(ParameterMapping mapping : paramMapping){
        	        String propValue = mapping.getProperty();		// 파라미터로 넘긴 Map의 key 값이 들어오게 된다
        	        Object value = ((Map) param).get(propValue);	// 넘겨받은 key 값을 이용해 실제 값을 꺼낸다
        	        if(value instanceof String){			// SQL의 ? 대신에 실제 값을 넣는다. 이때 String 일 경우는 '를 붙여야 하기땜에 별도 처리
        	           sql = sql.replaceFirst("\\?", "'" + value + "'");
        	        }else{
        	           sql = sql.replaceFirst("\\?", value.toString());
        	        }
        	    }
            }


그러면 별도 클래스일땐 어떻게 하는가? 별도 클래스일 경우엔 ParameterMapping 클래스에서 제공하는 getProperty 메소드를 통해 값이 들어 있는 별도 클래스 내부의 멤버변수명을 읽어올수 있다. 이렇게 읽어온 멤버변수명을 Java의 Reflection을 이용해 해당 변수에 저장되어 있는 값을 읽어와 쿼리에 매핑하게 된다


        
             }else{					// 해당 파라미터가 사용자 정의 클래스일 경우
        	
        	   // 쿼리의 ?와 매핑되는 실제 값들이 List 객체로 return이 된다.
        	   // 이때 List 객체의 0번째 순서에 있는 ParameterMapping 객체가 쿼리의 첫번째 ?와 매핑이 된다
        	   // 이런 식으로 쿼리의 ?과 ParameterMapping 객체들을 Mapping 한다
        	   List<ParameterMapping> paramMapping = boundSql.getParameterMappings();
        	
        	   Class<? extends Object> paramClass = param.getClass();

        	   for(ParameterMapping mapping : paramMapping){
        	       String propValue = mapping.getProperty();			// 해당 파라미터로 넘겨받은 사용자 정의 클래스 객체의 멤버변수명
        	       Field field = paramClass.getDeclaredField(propValue);	// 관련 멤버변수 Field 객체 얻어옴
        	       field.setAccessible(true);					// 멤버변수의 접근자가 private일 경우 reflection을 이용하여 값을 해당 멤버변수의 값을 가져오기 위해 별도로 셋팅
        	       Class<?> javaType = mapping.getJavaType();			// 해당 파라미터로 넘겨받은 사용자 정의 클래스 객체의 멤버변수의 타입
        	    
        	       if(String.class == javaType){				// SQL의 ? 대신에 실제 값을 넣는다. 이때 String 일 경우는 '를 붙여야 하기땜에 별도 처리
        	           sql = sql.replaceFirst("\\?", "'" + field.get(param) + "'");
        	       }else{
        	           sql = sql.replaceFirst("\\?", field.get(param).toString());
        	       }
        	   }
            }


지금까지 설명한 내용을 보고 위에 있는 전체 Source 코드와 주석들을 보면 전체적인 흐름을 이해할수 있을 것이다







  • BlogIcon misoboy 2014.12.31 11:59 신고

    안녕하세요. 지나가다 적어주신 코드에 대해 감사히 참고 하여 사용하고 있습니다.
    77 라인정도에 필드 값에 대한 조회해오는 부분이.. 상속관계일경우 값을 가져오지 못하더군요..ㅎㅎ
    재귀함수를 추가하면 되지 않을까 싶어 댓글을 남겼습니다.
    좋은 정보 감사합니다.

    /**
    * 클래스 필드 검색 재귀함수
    * @param paramClass
    * @param propValue
    * @return
    */
    private Field doDeclaredField(Class<? extends Object> paramClass, String propValue){
    Field field = null;
    try {
    /*
    * 해당 객체의 필드를 검색 한다.
    * 존재 하지 않을 경우 NoSuchFieldException 발생
    */
    field = paramClass.getDeclaredField(propValue);
    } catch ( NoSuchFieldException e ){
    // NoSuchFieldException 발생 할경우 상위 클래스를 검색 한다.
    field = doDeclaredField(paramClass.getSuperclass(), propValue);
    }
    return field;
    }

    1. BlogIcon 메이킹러브 2015.01.21 18:31 신고

      안녕하세요..댓글을 지금에서야 봤습니다..죄송해요..
      제가 상속 클래스에 대한 테스트는 진행해보질 않아서..그런 버그가 있을줄은 몰랐네요..지적 감사합니다..

      근데 좀더 보완을 한다면..
      지금 주신 코드도 버그는 있습니다..
      예를 들어 극단적으로 최상위 클래스..즉 Object 클래스까지 올라갔을때도 필드를 못찾았다면 어떻게 될까요?
      저도 원래 댓글에서는 catch에서 무한루프를 돌것 같다고 생각했지만..
      Object 클래스 객채에서 getSuperclass() 메소드를 호출하면 null 이 리턴된다고 하더군요..
      (http://stackoverflow.com/questions/2706069/java-object-superclass)
      그러면 다음번 doDeclaredField 메소드 실행시 paramClass가 null로 들어오기 때문에 paramClass.getDeclaredField(propValue); 부분에서 에러가 발생할겁니다..null인 객체에서 메소드를 실행시킬순 없으니까요..

      지금의 코드는 부모클래스에 반드시 필드가 있다는 전제하에서는 잘 돌겠지만..그렇지 않을 경우엔 최상위 Object 클래스까지 찾은 뒤 그 다음번 함수 호출에서 에러가 발생할 수 있으니 한번 테스트를 진행해보시면 좋을 듯 하네요..즉 필드가 없을 경우에 대한 테스트죠..

      다시 한번 코드 지적 고맙습니다..

  • BlogIcon 아타루 2018.11.05 10:06 신고

    안녕하세요.

    이코드는 어떻게 사용해야 하나요? xml에 따로 추가해야 되나요?
    아니면 JAVA 소스 어디에 넣어야 하는지 궁금합니다.
    systemMapper.xml 방식으로 sql 파싱을 하고있는데
    sql String 출력이 너무 어렵네요 ㅠㅠ 도움 부탁드려요..

    /WEB-INF/spring/mybatis-context.xml 이런 경로에

    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.dmc.ad.mapper
    ,com.dmc.ad.common.mapper
    ,com.dmc.ad.system.mapper" />
    </bean>

    이런식으로 쓰고 있습니다.

    1. BlogIcon 메이킹러브 2018.11.05 15:15 신고

      이 코드는 Mybatis plugin 형태로 사용되는거에요..
      mybatis xml 설정파일에서 plugin 태그를 사용할 수 있는데 거기게 다음과 같이 넣으면 됩니다..

      <plugins>
      <plugin interceptor="com.terry.boardprj32.common.MybatisLogInterceptor"/>
      </plugins>

      패키지와 클래스명은 본인이 사용하는거에 맞춰서 넣으시면 되요..
      mybatis 설정 xml 파일에 대한 설명에서 plugin 사용법에 대해 알아보시면 적용가능하실껍니다..

      그러나 요즘은 워낙 query 를 출력해주는 log 라이브러리들이 잘 나와 있어서 굳이 이걸 사용안하셔도 되요..log 라이브러리를 사용하시는 것이 더 좋습니다..

      http://log4jdbc.brunorozendo.com/

      여기 가시면 log4jdbc 사용 방법에 대해 아실수 있을꺼에요..
      대강의 사용방법을 말씀드리면 Spring 에서 DataSource를 만들때 log4jdbc에서 제공하는 클래스로 만들어줍니다..
      그러면 이것이 일종의 proxy 역할을 해서 이 DataSource를 이용하는 모든 SQL문에 대해 출력해주거든요..mybatis의 경우 변수에 값이 binding 된 상태로 출력해줍니다..

트랙백을 확인할 수 있습니다

URL을 배껴둬서 트랙백을 보낼 수 있습니다

다른 카테고리의 글 목록

프로그래밍/Java 카테고리의 포스트 목록을 보여줍니다
우연히 알게된 어떤분의 블로그 내용인데..
잘 정리되어 있어서 퍼옴..

http://blog.naver.com/windziel/60048694876

트랙백을 확인할 수 있습니다

URL을 배껴둬서 트랙백을 보낼 수 있습니다

다른 카테고리의 글 목록

프로그래밍/Java 카테고리의 포스트 목록을 보여줍니다