Java에서는 다음과 같이 가변 길이 파라메터를 지원한다. 이 가변길이 파라메터는 재밌는 특성들이 있는데 이를 활용하는 방법을 알아보자.


우선 기본적인 사용법 부터 알아보도록 한다.

public static void target(String...strings){

    String[] array = strings; // 배열화 된다.

    for(String str : strings) System.out.println(str);

}

이 함수의 사용법은 아래와 같다.

target("A", "B", "C", "D");

target();

실행 결과는 아래와 같다.

A

B

C

D


이와 같이 0~* 개의 동일한 타입을 파라메터로 받을 수 있다. 만약 또 다른 파라메터와 함께 사용하고 싶다면 가변길이 파라메터는 맨 뒤에 위치 시켜야 한다.


자 여기서부터 재밌는 실험을 해보도록 하자. 혹시 가변길이 파라메터로 받은 인자를 가변길이 파라메터 함수에 다시 넣을 수 있을까? 코드는 아래와 같다.

public static void wrap(String...strings){

    target(strings); // 받은 가변 인자 파라메터를 그대로 넘김

}

      

public static void target(String...strings){

    String[] array = strings; // 배열화 된다.

    for(String str : strings) System.out.println(str);

}

다음과 같이 실행해 본다.

wrap("A", "B", "C", "D");

wrap();

실행 결과는 위와 같다. 즉, 가변인자 파라메터로 받은 인자를 다시 가변인자 파라메터로 넘기는 것이 가능하다.

재밌는 것은 가변길이 파라메터에 집어 넣은 파라메터들이 무엇으로 변환 되는지이다. target() 함수 두번째 줄에 보면 가변길이 파라메터들이 배열로 변환됨을 알 수 있다. 이것이 의미하는 바는 무엇인가? String...strings와 같은 가변인자 파라메터 형식은 여러 String 객체가 나열된 형식("a", "b", "c"와 같은)도 인자로 인정해 주고, String[] 형식 즉, 배열 한 개도 인자로 인정해 준다는 의미이다. 따라서 아래와 같은 사용도 가능하다.

String[] array = new String[]{"a", "b"};

target(array);

결과는 예상할 수 있을 것이다.


이 특성을 이용하면 몇가지 유틸리티 성 함수를 만들어 볼 수 있다. 우선 여러 개별 객체를 받아 배열로 변환해 주는 toArray(String...strings)와 같은 함수를 만들어 볼 수 있겠다.

public static String[] toArray(String...strings){

    return strings;

}


또한 객체의 배열을 인자로 받는 함수를 이용하기 쉽게 만들 수도 있다. 만약 제공된 API가 다음과 같은 모양이라고 가정하자.

public void api(Object[]);

이런 경우 가장 손쉽게 사용하는 방법은 api(new Object{......})와 같은 형식이다. 하지만 위의 toArray() 함수와 같이 한번 가변인자 파라메터로 통과 시켜 주면 더 간편하게 api() 함수를 사용할 수 있다.

Posted by 이세영2
,

복사해서 붙여 넣으면 끝!

중복 코드가 나쁘다는 것은 익히 알고 있지만 문제는 중복 코드가 자주 나타난다는 점이다. 여러가지 이유가 있겠지만 가장 큰 이유는 복사해서 붙여 넣기가 너무 쉽기 때문이다. 조금 다른 코드를 작성하는 경우에도 복사해서 붙여 넣고 일부만 수정해 주면 끝이다. 즉, 중복을 만드는 것은 매우 쉬운 일인데 상대적으로 중복을 없애기는 어렵다. 중복은 머리가 없어도 만들 수 있지만 중복을 없애는 것은 머리가 없이 할 수 없는 일이다.


중복에 대한 인식

중복에 대해서 크게 문제 삼지 않는 경우이다. 중복 코드에 대한 인식 문제는 프로그래밍에 대한 자세와 연관이 깊다. 코드를 만들고 돌아가면 끝이고 다시는 그 코드를 들여다 보고 싶지 않은 개발자들이 보통 이런 사고를 가지고 있다.


객체지향 설계 및 디자인 패턴에 대한 지식 부재

요컨데 중복 코드를 제거하는 도구와 정형화 된 방법들에 대한 이해가 부족하기 때문이다. 객체지향의 겉모습은 온톨로지(Ontology)를 기반으로 한 실제 세계에 대한 인식을 컴퓨터 상에 모사한 것으로 보이지만, 깊숙히 들여다 보면 중복을 효율적으로 제거하는 도구들로 채워져 있다. 문제는 객체지향을 이해하는 것보다 코드를 만드는 일이 더 선행된다는 점이다. 즉, 중복을 제거할 수 있는 도구는 모른체 코드를 만들기 시작한다. 당연히 코드는 중복으로 넘쳐날 수 밖에 없다.


발견하기 어려운 경우

중복 코드가 너무 멀리 있는 경우, 중복 코드를 만든지 너무 오래된 경우, 중복 코드가 너무 짧은 경우, 중복 코드가 다른 코드에 뭍혀 있는 경우, 중복 코드 블럭 중간에 다른 코드가 들어가 있는 경우 등이다. 아래 코드를 보자.

class Example{

    private int data;

    public Example(int data){

        this.data = data; // 중복

    }

    public void setData(int data){

        this.data = data; // 중복

    }

}

이 코드는 중복이 있다. 아래와 같이 고쳐 줘야 한다.

class Example{

    private int data;

    public Example(int data){

        setData(data); // 중복 제거

    }

    public void setData(int data){

        this.data = data;

        /* data 갱신시 수행할 일들 추가 */

    }

}

얼핏 보면 우스운 일이다. 단 한 줄의 코드고, 직접 수행하던 코드를 함수까지 써가면서 수정했다. 코드는 줄지도 않았고 오히려 함수 콜에 의한 연산만 증가했다. 하지만 기존의 코드는 명백한 중복 코드이다. 만약 data 값이 변경 되었을 때 해야 할 일이 생겼다면 어떻게 할 것인가? 기존의 코드에서는 생성자와 setter 함수 모두 그 일을 수행하도록 변경해야 할 것이다. 하지만 아래 코드에서는 setData() 함수 내부만 수정해 주면 된다. 코드는 한 줄 바뀌었지만 "data가 갱신 되었을 경우 해야 할 일"에 대한 코드가 들어가야 할 위치가 setter 함수 쪽으로 단일화 되었다. 단 한 줄의 코드가 중복인 경우라도 수정에 닫혀 있지 않다면 중복이다. 그리고 중복을 해결하면 코드가 짧아질 것이라는 선입견을 버려야 한다. 중복 코드를 없애는 것은 코드를 짧게 줄이는 것이 아니고 중복 코드가 발생시킬 수 있는 문제를 차단하는 것이다.


다 똑같은데 일부만 다른 경우

대표적인 예가 순회(방문) 코드일 것이다. 즉, 여러 객체들을 방문해서 어떤 작업을 수행하는 코드이다. 이 때 방문 코드가 매우 길어지면 상대적으로 방문해서 할 작업 코드는 짧아진다. 그러면 다른 작업이 추가되면 방문 코드는 복사해서 붙여 넣게 된다. 이런 형태의 코드들은 발견해도 바로 수정하기는 어렵다.


다른 코드와 섞여 있는 경우

중복 코드가 블럭 A와 블럭 B의 연속이라고 하자. 그리고 이 중복 코드가 두 군데 이상 존재하는데, 어느 한 쪽에서 블럭 A와 블럭 B 사이에 흐름과 관계 없는 코드를 집어 넣었다고 가정하자. 이런 경우에 중복 코드를 발견해 내기가 어려울 수 있다. 그리고 상황에 따라서는 삽입된 코드가 중복 코드들과 유사하거나 어떤 영향이 있는지를 알기 힘들어서 분리해 내기가 어려울 수도 있다.

List<String> list1 = new ArrayList<String>();

List<String> list2 = new ArrayList<String>();

List<String> list3 = new ArrayList<String>();

public void example(){

    for(int i = 0; i < list1.size(); i++){

        /* list1에 대한 연산 */

        /* list1 + list2 + list3에 대한 연산 */

    }

} 

list1에 대한 연산이 중복 코드일 경우, 아래에 있는 코드와의 관계를 재빨리 파악하기는 힘들다. 왜냐하면 둘 모두 같은 for 문에 묶여 있기 때문이다. 이 경우 코드를 유심히 들여다 보지 않고는 중복 코드가 있는지 발견하기 어렵다.


매개가 필요한 경우

이 경우가 해결하기 어려운 문제 중 하나이다. 분명 코드는 중복인 것처럼 보이지만 다들 조금씩 다르고 해결하기에는 쉬워 보이지 않는다. 아래 코드를 보자.

class Boundary{

    int northLimit = 100;

    int southLimit = 50;

    int eastLimit = 20;

    int westLimit = 10;

   

    public int getNorthLimit() { return northLimit; }

    public int getSouthLimit() { return southLimit; }

    public int getEastLimit() { return eastLimit; }

    public int getWestLimit() { return westLimit; }

    public void setNorthLimit(int northLimit) { this.northLimit = northLimit; }

    public void setSouthLimit(int southLimit) { this.southLimit = southLimit; }

    public void setEastLimit(int eastLimit) { this.eastLimit = eastLimit; }

    public void setWestLimit(int westLimit) { this.westLimit = westLimit; }

}

각 데이터들에 대해서 반복적인 패턴이 나타난다. 즉, 데이터 선언과 getter / setter 선언이 그것이다. 이러한 중복은 얼핏 해결이 불가능해 보인다. 완벽하게 동일한 코드가 아니고 유사한 코드들의 나열이기 때문이다. 이것을 해결하기 위해서는 매개체가 필요하다.

enum Direction{

    NORTH,

    SOUTH,

    EAST,

    WEST

    ;

    public static int size(){ return values().length; }

}

class Boundary{

    int[]boundaries = new int[Direction.size()];

    public int getLimit(Direction direction) { return boundaries[direction.ordinal()]; }

    public void setLimit(Direction direction, int limit) { boundaries[direction.ordinal()] = limit; }

}

이것이 중복의 해결책인 이유는 다음과 같다. 만약 Direction이 추가되었을 경우, Boundary 클래스는 수정이 전혀 필요하지 않게 된다. 기존의 코드에서는 새로운 데이터가 추가될 때 getter/setter가 추가 되어야 했다는 점을 주목하자. 이렇게 매개체가 필요한 형태의 코드들은 중복을 발견해 내기가 어렵다.

Posted by 이세영2
,

많은 책에서 코드 중복이 나쁘다는 말이 나온다. 이 글에서 코드 중복이 발생시키는 문제들을 정리해 보도록 하겠다.


완벽하게 논리적인 사고로 만들어진 버그

중복 코드가 있을 경우, 개발자가 완벽하게 논리적인 사고를 한다고 해도 버그가 발생하게 된다. 이 문제를 첫번째로 놓은 이유는 그만큼 이 문제가 심각한 영향을 미치기 때문이다. 개발자의 능력이 아무리 뛰어나고 논리적 사고를 잘 한다고 해도 중복 코드가 있으면 비 논리적인 코드를 만들어 내게 된다.

아래 코드를 보자.

public void function1(){

    task1();

}

public void function2(){

    task1();

} 

위와 같이 두 개의 함수가 있다고 가정하자. 편의상 두 함수를 나란히 배치했지만, 두 함수가 멀리 떨어져 있다고 가정하자. 중복된 코드는 task1()이다. 여기서 task2()를 추가한다고 가정해 보자. 논리적으로 task1() 이후에는 항상 task2()가 와야 한다. 그러면 각 함수의 task1() 이후에 task2()가 실행되도록 수정되어야 할 것이다.

그래서 개발자는 function1()에서 task1() 이후 task2()를 실행하도록 코드를 수정한다. 하지만 가정했듯이 두 함수가 아주 멀리 떨어져 있다면 function2() 함수가 있는지 모를 수도 있고, 그래서 task2()를 실행하도록 수정하지 않았다면 그 코드는 버그로 남게 된다.

이 문제는 중복 코드가 들어 있는 함수 간의 거리, 중복 코드를 만든 후 지나간 시간, 중복 코드의 개수, 중복 코드의 길이에 비례하여 커진다.


중복의 강요

중복된 코드는 중복을 강요한다. Unit Test를 한다면 중복된 코드에 대해서 중복으로 테스트를 만들어야 한다. 중복된 코드는 중복된 주석, 문서, 설명을 요구한다. 복사 해서 붙여 넣기는 쉽지만 그것을 계속 유지하기는 어렵다.


OCP(Open Close Principle) 위배

코드는 수정에 닫혀 있어야 한다는 SOLID 원칙에 위배 된다. 하나의 문제를 수정하기 위해 여러 코드를 수정해야 하기 때문이다.


코드량 증가

중복 코드는 코드의 양을 증가 시킨다. 코드의 양이 늘어나면 코드를 읽는데 걸리는 시간이 늘어나고, 수정이나 디버깅 이슈가 발생할 가능성이 높아진다. 같은 기능을 구현한다면 짧고 간결한 코드를 작성하는 것이 좋다.


중복 코드 동일성 검사

우스운 일일지 모르지만 중복 코드들은 모두 동일해야 한다. 그래서 개발자들은 종종 중복 코드가 완벽하게 동일한지를 검사한다. 이런 일이 발생하는 이유는 중복 코드를 발견하기는 쉽지만 생각보다 제거하기는 더 어렵기 때문이다. 어쨌든 이런 검사도 불필요한 비용을 발생시킨다.


Posted by 이세영2
,