본문 바로가기

Web

(jquery-treeview) 2-1. async를 이용한 다이나믹 트리(서버)


이전 글에서 작성한 코드는 단순히 Ajax로 모든 데이터를 가져와 한번에 전체 트리를 구성하는 방식이였다.

트리기능을 하나의 도메인에서만 사용하고 노드의 양이 많지 않다면 큰 문제는 없어보인다.


하지만 내가 담당한 화면에서만해도 4개의 화면에서 계층구조로 데이터를 표현해야했다.

그래서 단순히 도메인마다 CTRL + C, V(일명 복붙신공)을 사용했는데...

아~ 알겠지만 굉장히 지루한 작업이다.


해서 어떻게하면 재사용성을 높일수 있을까 고민해봤다.

나는 마침 어제 헤드퍼스트 디자인패턴이라는 책의 1독을 마쳤다.

계층구조하면 떠오르는 컴포지트패턴 + 이터레이터패턴? 좋아 이거야.



컴포지트 패턴


컴포지트 패턴을 이용하면 객체들을 트리 구조로 구성하여 부분과 전체를 나타내는 계층구조로 만들 수 있다.

책에 나온 컴포지트 패턴의 정의다.

자식 원소가 있는 원소는 노드(Node)혹은 내부 노드(Inner Node)(이 패턴에서는 composite),

자식 원소가 없는 원소는 잎(Leaf)라고 부른다고 한다.


클래스 구조도 팩토리 뭐 이런거에 비해 복잡하지도 않아 많은 클래스를 만들지 않아도 될것같다.

이 패턴의 용도는 leaf와 composite를 똑같은 방법으로 다룰 수 있다는데 있다고 한다.

클라이언트에서는 이 객체가 leaf인지 composite인지 상관없이 그냥 메소드를 호출하기만 하면 된다.


근데 현재 시스템에서는 leaf와 composite가 다른 기능을 지원한다거나 다른 상태를 갖지 않는다.

그저 단순히 자식노드가 있으면 자식을 가졌는지에 대한 여부만 지원하면 된다.

그래서 굳이 Leaf와 Composite라는 객체로 나누지 않아도 될 것같다.

물론 내 판단이지만 ;;


이러한 판단하에 다른거 다 고려안하고 재사용성?이 맞나 모르겠지만 어쨋든 여러 클래스에서 같은 방법으로,

웹에서 트리를 표현하기 위해 필요한 클래스와 과정을 생각해보았다.



1. 공통 클래스 정의

(1). 트리로 표현될 수 있는 도메인(vo)은 HTML에 표시하기 위해 id, parentId, text, hasChildren, lvl와 같은 인스턴스 변수가 필요하다

- 클라이언트로 전송하기 위한 DTO = TreeVo

- 추가로 DOM에 클래스 속성을 정의하기 위한 classes와 펼친상태를 설정할 expanded가 필요하다.


(2). 각 도메인을 트리로 변환하는 역할을 담당하는 클래스가 필요하다

- 각 도메인이 구현해야할 getId(), getParentId.......와 같은 인터페이스 = Treeable

- 트리의 자식소유여부 판단, 도메인을 TreeVo로 변환하는 기능을 위한 TreeService


2개의 인터페이스와 1개의 클래스로 구성해보았다.



TreeVo

/**

 * 트리를 그릴때 사용되는 Vo

 */

@Getter

@Setter

public class TreeVo {


private String id;

private String parent;

private String text;

private boolean expanded;

private boolean hasChildren;

private String classes;

private int lvl;

private List<TreeVo> children = new ArrayList<>();

public TreeVo() {

this("0", "전체", "treeView", 1);

}

public TreeVo(String id, String text) {

this(id, "0", text, "", 0, false);

}

public TreeVo(String id, String text, boolean expanded) {

this(id, "0", text, "", 0, expanded);

}

public TreeVo(String id, String parent, String text, int lvl) {

this(id, parent, text, "", lvl, false);

}

public TreeVo(String id, String parent, String text, String classes, int lvl) {

this(id, parent, text, classes, lvl, false);

}

public TreeVo(String id, String parent, String text, String classes, int lvl, boolean expanded) {

this.id = id;

this.parent = parent;

this.text = text;

this.classes = classes;

this.lvl = lvl;

this.expanded = expanded;

this.hasChildren = false;

}

public void addChild(TreeVo childTree) {

hasChildren = true;

children.add(childTree);

}

public void removeChild(TreeVo childTree) {

Iterator<TreeVo> iter = children.iterator();

while(iter.hasNext()) {

TreeVo tree = iter.next();

if(tree.id == childTree.id)

children.remove(tree);

}

if(children.size() == 0)

hasChildren = false;

}

}


Treeable

/**

 * 트리로 표현할 수 있는 Vo가 구현해야할 인터페이스

 */

public interface Treeable {

// 트리의 ID

public String getTreeId();

// 트리의 부모ID

public String getTreeParent();

// 트리가 표시될 이름

public String getTreeText();

// 트리의 계층레벨

public int getLvl();

// 자식 소유여부

public boolean getTreeHasChildren();

}


TreeService

/**

 * 트리로 표현가능한 Vo의 서비스 계층

 */

public interface TreeService {


// 트리의 자식 소유여부를 판단할 로직

public boolean getTreeHasChildren(Object parent);

// Vo를 트리형식으로 변환할 로직

public List<? super TreeVo> parseToTree(List<? extends Treeable> target) throws Exception;

}



2. 변환 클래스 정의

구현보다는 인터페이스 위주로 프로그래밍하라는 디자인 패턴원칙이 생각났다.

뭐 누가 내 소스본다고 중요하겠느냐만..

그렇다고 어떠한 패턴을 사용한건 아니다;

단순히 변환하는 역할을 맡은 클래스를 기능 확장과 같은 경우를 고려해야할까?? 나중에 생각해봐야겠다.


최상위 Parser

/**

 * @param <T> 변환 타겟의 타입

 * @param <R> 변환 결과의 타입

 */

public abstract class Parser<T, R> {

public R parse(T target, Map<String, Object> parameters) throws Exception {

throw new NotSupportedException();

}

public R parse(T target) throws Exception {

throw new NotSupportedException();

}

}



오브젝트를 List로 변환하기 위한 Parser

/**

 * @param <T> extends Object

 * @param <R> extends List

 */

public abstract class ListParser<T extends Object, R extends List<?>> extends Parser<T, R> {


@Override

public R parse(T target) throws Exception {

return super.parse(target);

}

@Override

public R parse(T target, Map<String, Object> parameters) throws Exception {

return super.parse(target, parameters);

}

}


도메인의 리스트를 받아 TreeVo의 리스트로 변환하는 Parser

/**

 * @param <T> List<? extends Treeable>

 * @param <R> List<? super TreeVo>

 */

public class TreeParser extends ListParser<List<? extends Treeable>, List<? super TreeVo>>{

TreeDao treeDao;

List<Treeable> voList;

public TreeParser(TreeDao treeDao) {

super();

this.treeDao = treeDao;

}

@Override

public List<? super TreeVo> parse(List<? extends Treeable> target, Map<String, Object> parameters)

throws Exception {

String classes = "";

if(parameters != null && parameters.containsKey("classes")) {

classes = parameters.get("classes").toString(); 

}

List<TreeVo> list = new ArrayList<>();

Iterator<? extends Treeable> iter = target.iterator();

while(iter.hasNext()) {

Treeable element = iter.next();

TreeVo treeNode = new TreeVo(

element.getTreeId(),

element.getTreeParent(),

element.getTreeText(),

classes,

element.getLvl()

);

if(treeDao.getTreeHasChildren(element.getTreeId())) {

treeNode.setHasChildren(true);

}

list.add(treeNode);

}

return list;

}

@Override

public List<? super TreeVo> parse(List<? extends Treeable> target) throws Exception {


return parse(target, null);

}

}


사전준비가 끝났다.

이제는 트리를 표현할 수 있는 도메인에서는 Treeable과 TreeService를 구현하고

컨트롤러에서 도메인서비스의 parseToTree() 메서드를 호출해서 반환하기만 하면된다.


3. 각 도메인에서 Treeable, TreeService를 구현


코드 도메인

@Getter
@Setter

public class Code implements Treeable{


private String codeId;

private String parentId;

private int codeLvl;

private String codeNm;


@Override

public String getTreeId() {


return codeId;

}

@Override

public String getTreeParent() {

return parentId;

}

@Override

public String getTreeText() {

return codeNm;

}

@Override

public int getLvl() {

return codeLvl;

}

@Override

public boolean getTreeHasChildren() {

return false;

}

}


코드 서비스

public interface CodeService extends TreeService {


}


public class CodeServiceImpl implements CodeService {

@Override

public List<? super TreeVo> parseToTree(List<? extends Treeable> target) throws Exception {

Parser<List<? extends Treeable>, List<? super TreeVo>> parser = new TreeParser(this);

Map<String, Object> parameters = new HashMap<>();

parameters.put("classes", "code");

return parser.parse(target, parameters);

}

@Override

public boolean getTreeHasChildren(Object parent) {

if(codeDao.getChildCount(parent) != 0)

return true;

else

return false;

}

}


Dao쪽은 Mybatis를 사용하든 jdbc statement를 사용하든 자유롭게 구현해도 좋다.

parseToTree() 메서드 내부에서 parser를 생성해서 변환하고있다.


* 메소드 서명에서 parser를 강제하는게 좋을까?

* CodeServiceImpl의 멤버로 Parser를 구성요소로 사용하는게 좋을까?

* Treeable에서 parseToTree를 default 메소드로 만들고 parse를 오버라이딩 하게 하는게 좋을까?

-> 이를 템플릿 메소드 패턴이라고 하던가..?


대충 세가지 의문이 생긴다. 이는 좀 더 고민해봐야 될 것 같다.

숨겨진 입력(부원인)과 숨겨진 출력(부작용)? 을 얼핏 들었는데.., 아 지금 나는 FP가 아닌 OOP중이다.


4. 컨트롤러에서 트리데이터를 요청받았을 경우 모델에게 넘겨주기 위한 메소드 정의

// 트리뷰

@ResponseBody

@RequestMapping("/getTreeview")

public List<? super TreeVo> test( @RequestParam("parent") String parent) throws Exception {

List<Code> list = codeCatService.findCodeListByParent(parent);

return codeService.parseToTree(list);

}


오 뭔가 HTML 개발자가 아닌 0년차쯤의 자바 개발자가 된 느낌이다.

우선 서버쪽은 끝났다. 다음은 웹에서 jqeury-treeview를 사용해야되는데...

얘는 절망스럽게도 api가 없다. 내가 못찾는건가?

그래서! jquery.treeview.async.js를 뜯어봐야할 것 같다. 


(jquery-treeview) 1. 트리를 표현하는 방법

(jquery-treeview) 2-1. async를 이용한 다이나믹 트리(서버)

(jquery-treeview) 2-2. async를 이용한 다이나믹 트리(클라이언트)