알쓸코기: 알아두면 쓸데있는 코딩기술 – Generic

알쓸코기: 알아두면 쓸데있는 코딩기술 – Generic

개발을 하다보면 비슷한 구조나 패턴의 코드가 반복되는 경우를 많이 겪을 것이다. 이런 상황에서 좀더 깔끔하고 간략하게 코드를 정리하고 싶은데 딱히 떠오르지 않는다면 Java의 Generic이 해결책이 될 수 있다. 사실 대부분의 Java 개발자라면 Generic에 대해서 어느정도 알고 있으리라 생각된다. 특히 Collection의 클래스나 Stream의 경우 Generic을 활용하기 때문에 익숙할 것으로 생각된다. 하지만 이런 Generic을 이용하여 템플릿 형태의 메소드나 클래스를 직접 제작하거나 활용하는 경우는 그리 많지 않을 것이다. 이 글에서는 Generic을 활용하여 코드의 간결성과 재사용성을 높이는 방법에 대해 소개하고자 한다.

이전 글에서는 경로탐색을 예로들어 Deque 클래스인 LinkedList의 특성과 활용방법에 대해 알아보았다. 이번 글에서도 경로탐색을 예로들어 Generic을 설명해보고자 한다.

경로탐색 알고리즘이 목적지를 찾을 경우 출발지와 목적지 사이의 경로정보를 생성한다. 이때 생성된 경로는 도로네트워크의 Graph의 객체인 Node와 Link 그리고, Link와 Link간의 통행가능여부와 같이 연결성을 정의하는 Connection객체로 표현이 가능하다. 이들 각각을 그림으로 표현하면 아래와 같다.

node
link
connection

즉, Connection의 Collection이 경로탐색에서 생성되는 최종 경로정보로 생각하면 된다. 참고로 이때 생성되는 Connection에는 좌회전, 우회전, 회전각도와 같은 경로안내 정보도 같이 포함된다.

위의 그림을 자세히 보면 한가지 패턴을 발견할 수 있다. 즉, 노드와 노드를 연결하면 링크가 되고 링크와 링크를 연결하면 Connection이 된다. 사용하는 객체가 다를 뿐이지 개별 객체를 서로 연결해서 새로운 객체를 만들어 낸다는 측면에서 보면 동일한 패턴을 유지한다. 이런 패턴이 발견될 경우 Generic을 활용하면 정말 간결한 코드로 원하는 목적을 달성할 수 있다. 그럼 좀 더 쉬운 설명을 위해 Generic을 사용하지 않고 구현할 경우 어떻게 되는지 먼저 살펴보자 (이전 글에서 예로 들었던 hops() 메소드를 활용하도록 하겠다)

출발지와 목적지 사이의 모든 Node들 간에 형성되어 있는 링크객체를 추출하여 반환하는 메소드를 trail()이라고 하면 이 메소드는 아래와 같은 형태가 될 것이다.

public List<Link> trail() {
    final List<Link> links = new ArrayList<>();
    Iterator<Node> iter = hops().iterator();
    for (Node a = null, b = null; iter.hasNext(); a = b) {
        b = iter.next();
        if (a != null) links.add(a.linked(b));
    }
    return links;
}

참고로, 위 코드에서 a.linked(b)는 노드 a 와 노드 b 사이에 형성되어 있는 Link 객체를 반환한다.

이와 마찬가지로 Link와 Link를 연결하여 Connection 추출하는 메소드도 이와 동일한 패턴을 가질 것이며 이 메소드를 trace()라고 하면 아래와 같은 형태가 될 것이다.

public List<Connection> trace() {
    final List<Connection> connections = new ArrayList<>();
    Iterator<Link> iter = trail().iterator();
    for (Link a = null, b = null; iter.hasNext(); a = b) {
        b = iter.next();
        if (a != null) connections.add(a.connected(b));
    }
    return connections;
}

여기서도 위의 코드와 같이 a.connected(b)는 Link a와 Link b 사이에 형성되어 있는 Connection객체를 반환한다.

이렇게 보면 trail()trace() 메소드는 메소드 내에서 활용되는 객체(Node, Link, Connection)만 다를 뿐 동일한 패턴으로 동작한다. 이 경우 Generic을 이용하면 아래와 같이 템플릿 형태의 메소드를 작성할 수 있다.

private final static <A, T extends List<A>, C, R extends List<C>> R collect(T t, BiFunction<A, A, C> combiner) {
    final R list = (R) new ArrayList<>(t.size() - 1);
    Iterator<A> iter = t.iterator();
    for (A a = null, b = null; iter.hasNext(); a = b) {
        b = iter.next();
        if (a != null) list.add((C) combiner.apply(a, b));
    }
    return list;
}

이렇게 Generic을 활용하면 앞서 Link와 Connection 리스트를 만들기 위해 작성했던 두 메소드(trail, trace)를 하나의 메소드(collect)로 통합할 수 있다. 여기서 메소드간 차이가 나는 부분은 linked() 메소드와 connected() 메소드였는데 Java Function 패키지의 BiFunction을 이용하여 lambda 형태로 처리하면 깔끔하게 해결할 수 있다.

최종적으로 Generic을 활용한 템플릿 메소드 collect()를 활용하면 두 메소드(trail, trace)는 처음 작성했던 코드와 다르게 한결 간결한 형태로 바꿀 수 있다. 또한 이와 비슷한 경우가 발생하더라도 collect() 메소드를 활용하면 코드의 재사용성 또한 높게 가져갈 수 있다.

public List<Link> trail() {
    return collect(hops(), (a, b) -> a.linked(b));
}


public Collection<Connection> trace() {
    return collect(trail(), (a, b) -> a.connected(b));
}

지금까지 Java에서 제공하는 Generic을 활용하여 재사용성 높고 간결한 코드를 유지하는 방법에 대해 알아보았다. 이러한 기능들을 좀 더 능숙하게 활용함으로써 간결하면서 유지보수가 용이하고 오류발생 가능성이 낮으며 결과적으로 높은 품질의 코드로 이어지기를 바래본다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다