티스토리 뷰

들어가며

"순서를 다시 세팅해서 불러오도록 해주세요"

듣자마자 꿀냄새가 진동했다.

쿼리에 ORDER BY 살짝 얹어주면 끝날거라 생각했기 때문이다.

하지만 역시 날먹을 허락해주지 않는다...

하필 적용해야하는 데이터가 VARCHAR 형식이라 ORDER BY를 적용하면

'1, 10, 11, 2, 3, ...'

'그럼 간단하게 숫자로 바꿔서 돌려볼까 ?' 라고 생각해도

'A01, A02, ...'

이런 문자 + 숫자 혼합된 애들도 있을 수가 있어서 바꾸기도 애매했다.

쿼리로는 해결이 안되니깐 API에서 가공을 해주기로 했다.

처음 생각했던 단순하면서도 수술같이 정교함을 요하는 아이디어는 이랬다.

1) 문자가 있는 경우 (숫자는 그냥 처리하면 되니깐!) 문자를 따로 분리해서 변수에 담아둠

(문자는 임의의 알파벳 하나만 고정될 것이므로 String 변수에 담으면 끝)

2) 숫자들로 이루어진 리스트를 int로 파싱 후 sorting

3) A01 같은 경우 숫자 길이 확인 후 앞에 0을 붙여줌

4) 따로 보관해둔 변수를 sorting된 객체에 다시 하나씩 붙여줌

끝!

소스로 바꾸면 더 늘어나서 지저분해지고 뭔가 도려내고 교정하고 다시 이어붙이고...

뭔가 수술실 같은 느낌이 물씬나는 복잡한 작업같아 보였다.

왠지 이런 고민을 나만 했던게 아닐텐데... 하는 생각에 구글링을 돌려본 결과

www.baeldung.com/java-sort-strings-contained-numbers

 

Sorting Strings by Contained Numbers in Java | Baeldung

A quick and practical tutorial to sorting Strings by contained numbers in Java.

www.baeldung.com

우리의 밸덩형이 친절하게 가이드라인을 제시해주셨다.

제목조차 정직하게 자바에서 숫자있는 스트링 정렬하기 !

원리는 간단하다.

1) 리스트 속 객체들에서 문자를 제거하고 숫자로 바꾼다. (숫자가 없다면 0으로 치환)

2) 1)번의 메소드를 Comparator.comparingInt 매개변수로 넣어줘서 정렬의 기준으로 잡는다.

3) List의 sorting 메소드의 매개변수로 2)번을 넣어주면 끝 !

1번이야 정규식 찾아보면 되고 파싱이야 뭐 자주하니깐 넘어간다쳐도

2번 3번은 정렬할 일이 많이 없는(하더라도 ORDER BY로 충분) 나에겐 조금 생소했어서 살짝 찍먹해봤다.

 

List.sort (Comparator<? super E> c)

List에는 sort 메소드가 있다.

default void sort(Comparator<? super E> c)

Sorts this list according to the order induced by the specified Comparator.All elements in this list must be mutually comparable using the specified comparator (that is, c.compare(e1, e2) must not throw a ClassCastException for any elements e1 and e2 in the list).If the specified comparator is null then all elements in this list must implement the Comparable interface and the elements' natural ordering should be used.This list must be modifiable, but need not be resizable.

대충 Comparator로 정렬한단 얘기

그리고 comparator가 null이어도 natural ordering이 된단다.

(natural ordering : 기계적이 아닌 자연 그대로 인간 친화적인 정렬 1, 2, 3... a, b, c... 등등)

    @Test
    public void 소팅기준이_널이어도_가능하다니() {
        List<String> testList = Arrays.asList("1", "5", "6", "3");
        List<String> expectedList = Arrays.asList("1", "3", "5", "6");
        testList.sort(null);
        assertEquals(expectedList, testList);
    }

당연하게도 테스트는 통과한다.

(문자로 해도 A, B, C... 순으로 나온다)

    @Test
    public void 소팅기준이_널이어도_가능하다니() {
        List<String> testList = Arrays.asList("1", "5", "6", "3");
        List<String> testList2 = Arrays.asList("1", "5", "6", "3");

        testList.sort(null);
        Collections.sort(testList2);

        assertEquals(testList2, testList);
    }

위의 값도 테스트는 통과한다.

Null을 넣어서 natural ordering으로 정렬된 값이나 Collections.sort로 정렬된 값이나 또이또이하다.

(둘다 오름차순으로 정렬된다.)

그럼 내츄럴이 아닌 비내츄럴로 하려면 어떡하면 될까

 

Interface Comparator<T>

비내츄럴로 sorting 기준을 잡아주려면 comparator에 대해서 알아야한다.

간단하게 함수형 인터페이스로 람다 적용 가-능하다.

빠르게 실전으로 가서 적용하며 익숙해져야겠다.

    @Test
    public void sort를_통해_comparator_실전적용() {
        List<String> testList = Arrays.asList("1", "5", "6", "3");

        List<String> expectedList = Arrays.asList("1", "3", "5", "6");

        testList.sort((num1, num2) -> num1.compareTo(num2));

//        testList.sort(new Comparator<String>() {
//            @Override
//            public int compare(String o1, String o2) {
//                return o1.compareTo(o2);
//            }
//        });

        assertEquals(expectedList, testList);
    }

주석친 부분은 arrow function을 풀어서 익명클래스로 만든건데,

Comparator의 JAVA API 문서를 참고하면 compare는 추상메소드라 반드시 사용해줘야 하므로 Override 해줬다.

compare 메소드에서 return되는 값은 음수, 0, 양수로 나뉘게 되고, 이 기준으로 순서를 정해 정렬한다.

(기준으로 사용하는 타입이 String이라 compare 파라미터가 String o1, String o2로 되어있는데, int compare(T o1, T o2) 즉, 제네릭이라 아무타입이나 사용 가능하다.)

여기서 Comparator의 compare메소드에서 리턴되는 음수, 0, 양수만 활용하면 어떤 값도 정렬할 수 있다는 결론이 된다.(이론상 가능...)

그렇다면 밸덩 형 소스에서 사용된 compareInt는 무엇일까 ?

docs.oracle.com/javase/8/docs/api/java/util/Comparator.html

 

Comparator (Java Platform SE 8 )

Compares its two arguments for order. Returns a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second. In the foregoing description, the notation sgn(expression) designates the mathematical s

docs.oracle.com

Java API Docs에서 추상메소드 외에도 여러 디폴트 메소드들이 Comparator에 내장되어 있는걸 볼 수 있다.

밸덩 형이 사용한 Comparator.comparingInt도 여기서 확인할 수 있다.

static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor)

Accepts a function that extracts an int sort key from a type T, and returns a Comparator<T> that compares by that sort key.The returned comparator is serializable if the specified function is also serializable.

파라미터로 ToIntFunction이란 함수형 인터페이스를 사용하는데,

매개변수는 제네릭, 리턴 타입은 int인 함수형 인터페이스 정도로만 알아도 될 듯 싶다.

(외에도 다양한 메소드들이 있는데 나중에 해당 타입으로 정렬하거나 혹은 해당 기능을 활용할 일이 생기면 그때 다시 활용하면서 되새김질하면 좋을듯하다)

그럼 이제 밸덩 형이 만든 parseStringToNumber 메소드를 활용해서 sorting을 완성해 볼 차례다.

    @Test
    public void comparingInt_sorting() {
        List<String> testList = Arrays.asList("A01", "A05", "A06", "A03");
        List<String> expectedList = Arrays.asList("A01", "A03", "A05", "A06");

        // 람다식을 이용한 버전
        testList.sort(Comparator.comparingInt((i) -> parseStringToNumber(i)));

        // 익명클래스 + 미리 만들어둔 메소드 활용
//        testList.sort(Comparator.comparingInt(new ToIntFunction<String>() {
//            @Override
//            public int applyAsInt(String value) {
//                return parseStringToNumber(value);
//            }
//        }));

        // 익명클래스 내부에 메소드 내용 구현
//        testList.sort(Comparator.comparingInt(new ToIntFunction<String>() {
//            @Override
//            public int applyAsInt(String value) {
//
//                String DIGIT_AND_DECIMAL_REGEX = "[^\\d.]";
//
//                final String digitsOnly = value.replaceAll(DIGIT_AND_DECIMAL_REGEX, "");
//
//                if("".equals(digitsOnly)) return 0;
//
//                try{
//                    return Integer.parseInt(digitsOnly);
//                }catch (NumberFormatException nfe){
//                    return 0;
//                }
//            }
//        }));
        assertEquals(expectedList, testList);
    }

확실히 익명클래스보다 람다를 활용하는게 소스가 간단명료해진다.

또한 기능적으로 정렬하는 부분과 문자와 숫자 분리 후 숫자로 변환하는 부분으로 나눠 유지보수에도 유용하다.

 

마치며

확실히 처음에 생각했던 아이디어 (문자 분리 -> 숫자 치환 -> 정렬 -> 문자로 치환 -> 문자 재조립) 보다 간결해졌고,

분리하고 붙이고 하는 과정에서 생길 수 있는 오류도 방지할 수 있게 되었다.

앞으로 API에서 정렬할 일이 얼마나 더 있을지 모르지만 그때마다 Comparator를 떠올린다면 확실히 간결한 코드가 나오지 않을까 생각된다.

댓글