발생일: 2010.02.16

문제:
정확히 어디서부터 이 얘기가 시작되었는 지는 잘 모르겠다.

프렌드 홍과 스트럿츠의 복잡한 요청에 대한 ActionForm 구현에 대해 이야기하다가,
자연스럽게 관계형 DB 에서 지연 로딩 객체 구현에 대한 주제로 넘어가게 됐다.

일반적인 OR Mapping 툴에서 대부분 lazy loading 을 지원한다는데,
(실제 사용해 본 적이 없어 정확히는 모르겠으나, iBatis 의 lazy loading 부분을 책에서 읽어봐 대충 감이 잡혀있긴 하다.)
얘네들은 아무래도 객체형 DB 에 적합할 것 같다는 생각이 들었다.

그럼 객체 자체에 지연 로딩을 적용해 보면 어떨까.

예를 들어, user 객체는 userId 만 가지고 있다가,
userUserName() 과 같은 요청이 들어왔을 때 DB 에 접근해서 사용자 이름을 가져오는 방식으로 말이다.


해결책:
객체 자체에 지연 로딩을 적용해보기 전에 일단 VO 객체가 올바르게 객체 지향으로 재사용되도록 잡아줘야 할 것 같다.

예를 들어, 데이터베이스에 아래와 같은 정보가 있다고 가정해보자.

User (userId, userName, deptId, ... ) // 사용자 정보
Dept (deptId, deptName, ... ) // 부서 정보

각 사용자는 자신의 부서 정보를 포함하고 있다.

몇몇 급조된 프로젝트 - 적어도 홍과 내가 겪었던 - 에서는 사용자 정보와 매핑된 부서 정보를 가져오는 객체를
아래와 같이 한 객체에 정의했다.

class User {
    private String userId;
    private String userName;
    private String deptId; // 사용자 객체에 부서 코드도 포함되어 있다.
    private String deptName; // 부서 이름을 가져오기 위해 DB 에서 부서명도 함께 조회한다.

    // 그 외 getter/setter
}

class Dept { } // 때로 부서 정보만 조회할 때에는 따로 Dept 클래스를 사용하곤 한다.


만약, 게시판 내용을 가져오는 BoardList 라는 VO 객체를 만들었다고 한다면,
위 User 객체를 재사용하지 않고 보통은 아래와 같이 해버리곤 한다. (대부분 '바쁘니까...')

class BoardList { // 게시판 목록 VO 객체
    // 등록자 정보
    private String regUserId;
    private String regUserName;
    private String regUserDeptId;
    private String regUserDeptName;
    // 최근 수정자 정보
    private String updateUserId;
    private String updateUserName;
    private String updateUserDeptId;
    private String updateUserDeptName;

    // 그 외 getter/setter
}


가끔은 deptId 부분이 쓰이지 않을 것 같으면 그냥 deptId 에 deptName 을 넣기도 한다.


이 User 객체를 아래와 같이 좀 더 객체 지향으로 리팩토링 할 수 있다.

class User {
    private String userId;
    private String userName;
    private Dept dept; // 부서 정보는 부서 객체를 사용한다.

    // getter / setter
}

class Dept {
    private String deptId;
    private String deptName;

    // getter / setter
}

BoardList 에서도 User 객체를 활용한다.

class BoardList {
    private User regUser;
    private User updateUser;
}



이제 어느 정도 객체 지향적인 모습을 띄었다.
이제 이 객체에 지연 로딩(lazy loading) 을 적용해 보려고 한다.

기본적으로 primary key 가 되는 값만 전달해주고, 추가 요청이 생길 경우 상세 내용을 DB에서 가져오는 방식이다.

class User {   
    String userId;
    String userName;
    Dept dept;
   
    public User(String userId) {
        this.userId = userId;
    }
   
    public void set() {
        // DB 에서 user 정보를 가져오고, 같은 row 내 deptId 로 dept 객체를 생성해둔다.
        // set 하는 부분은 구현하기 나름이겠다.
        Map result = dao.getUserMap(userId);
        userName = (String) result.get("userName");
        dept = new Dept((String) result.get("deptId"));
    }
   
    public String getUserId() {
        return userId;
    }
   
    public Strin getUserName() {
        if (userName == null) set(); // userName 등 상세정보가 없을 경우 set() 한다
        return userName;
    }
   
    public Dept getDept() {
        return dept;
    }
}

class Dept() {   
    String deptId;
    String deptName;
   
    public Dept(String deptId) {
        this.deptId = deptId;
    }
   
    void set() {
        // 디비에서 deptId에 해당하는 부서 정보를 가져온다.
        dao.getDept(deptId);
    }
   
    public String deptName() {
        if (deptName == null) set(); // 역시 lazy loading
        return deptName;
    }
}


이렇게 할 경우, userId 만 있으면 필요한 시점에 상세 정보를 DB 에서 가져올 수 있다.
코드가 간략해지고 짧아짐은 물론이다.

만약, 전체 목록을 가져오는 등의 상황을 고려한다면 퍼포먼스 측면에서 그리 추천할 만한 방법은 아닐 것 같다.
(홍은 목록이 10~20 여개 정도라면 크게 문제되지 않을 것 같다고 한다)

아직 실제로 구현해보지는 않았으나, 이런 방식으로 빈을 만든다면 상세 정보 호출에 대한 수고가 크게 줄어들 게 된다.

예를 들어, 게시물에 대한 Board 라는 VO 를 생각해보자.

class Board {
    private String title;
    private String content;
    private User author;

    public void set() {
        // Board 객체도 set() 메서드가 있다고 가정하고,
        // DB 에서 author 에 대한 userId 값을 가지고 user 객체를 생성해뒀다고 치자.
        ... (설정 중략) ...
        ahthor = new User((String) dao.getBoardDetail("author_id")); // author_id 가 작성자 id라고 가정
    }
    // getter / setter
}

board 객체만 가져오면 그 이후부터는 필요할 때마다 객체에서 직접 상세 내용을 가져오도록 할 수 있다.

board.getAuthor().getUserName()
board.getAuthor().getDept().getDeptName()

과 같이 상세 내용을 호출하면 필요할 때 그 내용을 직접 가져오게 된다.

코드도 굉장히 짧아지고, 각자 자기 역할을 충실히 해 낸다.

공통되는 set() 부분이나 null check 부분을 인터페이스로 뽑아내도 좋을 것 같고, (LazyBean 같은 이름으로)
아니면 dao 를 가지고 있는 부모 클래스를 두고 확장하도록 해도 괜찮을 것 같다.

iBatis 에서는 result-map 을 이용해 서브 쿼리를 필요할 때 실행해서 가져오도록 하던데,
이 방법도 적합하게 사용된다면 아주 유용할 것이라 생각한다.





저작자 표시 비영리 변경 금지
Posted by ohgyun
발생일: 2010.02.16

문제:
오랜만에 회사 온라인 강의를 신청해 들어볼까 싶어서 교육 사이트를 둘러보다가,
'알기 쉬운 UML' 강좌의 화려한 플래시에 혹~ 해서 신청 버튼을 눌러버렸다.

UML 은 이미 대략적인 개요를 알고 있고, (여러 다이어그램 중 클래스 다이어그램만 마음 먹은대로 쓸 수 있는 수준이다)
업무를 포함하여 평소에는 다이어그램을 쓸 일이 거~~의 없어서 그다지 관심있는 편은 아니었다.
다만, 얼마 전 IBM 의 UML 컬럼 강좌을 보고서는,

'+ methodName ( ParameterName: ParameterType ) : ReturnType ...'
'요고요고 띄어쓰기도 좋고, 깔끔하니 있어 보이는데~'

하는 생각이 들어서 작성 기법에 개인적으로 후한 점수를 주기는 했다.

여튼, (서론이 굉장히 길었다... )
강의 시작이 보름이나 지난 오늘에야 접속해서 내용을 훑어보던 중,
이런 생각이 들었다.

'TDD 를 하다보면 Test Case 를 먼저 작성하는데게 되는데, 그럼 UML 자체가 의미없지 않은가?'
'XP 에서 추구하는 가치 측면에서 보면 UML 도 어쩌면 비효율적이고 구시대적인 문서가 아닌가?


해결책:
아래 포스트에서 해답이 될 만한 답변을 얻었다.



너무 근시안적으로 사고하지 않았나 싶다.
다이어그램을 그리는 주목적을 잊고 도구적인 측면으로만 생각하고 있었다.

특히, 애니메이션을 하다 프로그래밍에 뛰어든 나는,
습관적으로 '어떻게 하면 더 예쁘게 그릴까'에만 너무 많은 초점을 두고 있었다...
(이건 코드를 작성할 때도 마찬가지다. 너무 "깔끔"한 코드에만 연연하고 있는 건 아닐까.. 란 생각도 들었다)


"먼저 여러분이 다이어그램을 무엇을 위해 그리고 있는지 명심하라.
첫 번째 가치는 의사소통이다.
효율적인 의사소통은 중요한 것을 선택하고 덜 중요한 것은 무시하는 것을 의미한다."



우리가 UML 을 그리고자 한다면, 그건 효율적인 커뮤니케이션을 위함일 것이다.
위에서 했던 질문에 대한 답변을 자연스레 얻을 수 있다.

저작자 표시 비영리 변경 금지
Posted by ohgyun
발생일: 2010.02.10

문제:
iBatis 에 parameterClass 로 List 를 넘긴 후,
iterate 태그 사용을 시도하였을 경우 아래와 같은 예외가 발생한다.

Error getting ordinal list from JavaBean. Cause java.lang.StringIndexOutOfBoundsException


이 때 사용된 iBatis 구문은 아래와 같다.

<select id="selectSomething" parameterClass="list">
    // select something and use iterate
    <iterate property="someList">
        #someList[]#
    </iterate>
</select>


잘못된 게 없는 것 같은데, 왜 이럴까.


해결책:
정확한 원인은 모르겠으나, 일단 패러미터를 Map 형태로 전달해주면 문제 없이 작동한다.

queryForList 등의 메서드를 호출하기 전,
패러미터로 전달할 list 객체를 Map 객체로 감싼 후 전달하도록 한다.

<select id="selectSomething" parameterClass="map">
    // select something and use iterate
    <iterate property="someList">
        #someList[]#
    </iterate>
</select>


BUT, 아래 더 올바른 해결 방법이 있다.
=================================================================
# 추가. 2010.02.11

list 를 패러미터로 전달 시 iterate 태그에서 property 를 설정하면,
전달된 list 에서 해당 property 를 찾으려고 시도하는 것 같다.
<iterate> 태그에서 property 속성을 제거하면 정상적으로 작동한다.

<select id="selectSomething" parameterClass="list">
    // select something and use iterate
    <iterate> // iterate 태그 내 property 속성을 제거하라
        #someList[]#
    </iterate>
</select>


참고: iBatis: Support for Array or List Parameter with SQL IN Keyword



저작자 표시 비영리 변경 금지
Posted by ohgyun
발생일: 2010.02.10

문제:
업무 공유를 위해 팀원끼리 사용할 간단한 게시판을 하나 만들고 있다.

한 게시물에 여러 개의 태그를 달 수 있는 게시판이며, 태그는 < 게시물 1 : n 태그 > 형태로 구성되어 있다.
또한 gmail 의 목록처럼, 전체 목록을 뿌릴 때 엮여 있는 모든 태그를 보여준다.


업무 게시판

업무 게시판




좌측 메뉴에서 태그를 선택할 경우, 아래와 같이 해당 태그에 해당하는 게시물만 필터링하려고 한다.





그러다 매핑 테이블에서 and 조건으로 조회를 하려다가 난관에 봉착했다.
매핑 테이블에서 AND 조건을 추가하기가 쉽지 않다.

task 테이블과 tag 테이블이 존재하며 매핑 테이블은 아래와 같이 구성되어 있다.

[task_tag_map]
task_id
tag_id

데이터 샘플은 아래와 같다.
(task_id, tag_id)
(1, 2)
(1, 4)
(2, 1)
(2, 4)
(3, 1)
(3, 4)
(3, 5)

기존에는 아래와 같은 형태로 tag_id 를 받아와 조회했는데,
이 경우 OR 조건으로 선택한 태그를 포함한 모든 목록을 보여준다.

SELECT *
FROM task
WHERE
    task_id IN (
        SELECT task_id
        FROM task_tag_map
        WHERE tag_id IN (1, 4) -- 이 부분의 tag_id 를 패러미터로 가져와 넣는다
    )



이 경우, 위 데이터 샘플을 기준으로 조회하면 아래와 같은 결과가 나온다.

(task_id, tag_id)
(1, 4)
(2, 1)
(2, 4)
(3, 1)
(3, 4)

원하는 대로라면 2번과 3번 게시물만 조회되어야 하는데 말이다.

어떤 식으로 해야할까....


해결책:
한 게시물 당, 중복된 태그는 없기 때문에 count 를 조건에 추가했다.
1번과 4번 태그를 포함한 결과라면 결과가 2건 이상이어야 하기 때문이다.

아래와 같이 수정해서 문제를 해결했다.

SELECT *
FROM task
WHERE
    task_id IN (
        SELECT task_id
        FROM task_tag_map
        WHERE
            tag_id IN (1, 4) -- 이 부분의 tag_id 를 패러미터로 가져와 넣는다
        GROUP BY task_id
        HAVING COUNT(*) >= 2 -- 이 값은 tag_id 의 개수의 값으로, 유동적이다
    )



수정한 쿼리로 조회하면, 원하는 결과를 얻을 수 있다.

(task_id, tag_id)
(2, 1)
(2, 4)
(3, 1)
(3, 4)


저작자 표시 비영리 변경 금지
Posted by ohgyun