본문 바로가기

신입 개발자 면접 기초

개발자 기술 과제, 라이브 코딩 테스트 후기(자바 스트림 활용 능력 with flatMap)

과제 겸 라이브 코딩

1. 후기

제가 생각하는 일반적인 개발자 채용 프로세스는 아래와 같습니다.

서류전형 → 코딩테스트(온라인) → 기술면접 → 임원면접 → 최종합격

여기에 조금 추가되면 코딩테스트를 여러번 본다든지 오프라인 코딩테스트를 한 번 더 본다든지하는 경우가 있습니다.

또한 경우에 따라 기술 면접 전에 필기 문제를 푸는 경우도 있고, 코딩테스트 대신 과제 전형이나 기술 면접 중 수도코드 작성, 아주 드물게 기술 면접 단계에서 라이브 코딩을 할 수도 있죠.

그러한 여러 과정 중 제가 이번에 "라이브 코딩"을 봐서 후기를 남기려고 합니다.

조쉬 롱(josh long)만큼 라이브 코딩을 할 수 있다면 두려울 것이 없겠지만 제 현실은 나약한 주니어 개발자죠.... 그래도 나름 깨달은 것이 있어서 공유하고자 합니다.

2. 1시간 짜리 라이브 코딩에서 보여줄 수 있는 것

코딩 테스트도 보통 2~3시간 정도는 소요됩니다. 과제는 보통 1주일 정도는 시간이 있죠.

제가 이번에 본 라이브 코딩 테스트는 "1시간" 안에 요구하는 기능을 구현하는 것이었습니다.

(뜬금 없지만 우아한(?) 어떤 회사의 프론트엔드 개발자 과제 테스트도 4시간이었던 걸로 기억합니다...)

1시간이기 때문에 과도하게 어려운 기능을 구현하라고는 보통은 못 할 거라고 예상합니다.

사설이 길어지는데요, 제가 본 과제는 csv 형식의 데이터가 있고, 그것을 읽어서 가공하거나 통계를 내보는 기능이었습니다.

(제가 이번에 시험을 본 회사의 이름과 문제는 유출하면 안 되기 때문에 나름대로 기억하고 변형한 것임을 미리 알려드립니다.)

개인적으로 문제를 받아보는 순간 스트림 API를 얼마나 잘 다루는 지 보려는 것 같은데? 했습니다.

3. 일부 문제 공유 겸 코드 연습

//member.csv 파일 일부
이름, 취미, 소개
김프로, 축구:농구:야구, 구기종목 좋아요
정프로, 개발:당구:족구, 개발하는데 뛰긴 싫어
앙몬드, 피아노, 죠르디가 좋아요 좋아좋아너무좋아
죠르디, 스포츠댄스:개발, 개발하는 죠르디
...

csv파일은 콤마(,)로 구분되는 데이터고 한 줄에 한 명의 정보가 저장되어 있습니다.

위와 같은 구조의 csv파일이 있다고 가정합니다.

취미는 여러 개가 올 수 있고 콜론(:)으로 구분하는 구조입니다.

3.1 취미별 인원 수를 구하라

public void printMemberCountGroupByHobby() {
    List<List<String>> persons = CsvReader.getLines();
    //첫 줄 제거
    persons.remove(0);
    //결과를 담을 해시맵 구성
    Map<String, Integer> result = new HashMap<>();
    persons.stream()
            .flatMap(member -> Arrays.stream(member.get(1).split(":"))) //취미를 플랫하게 스트림으로 분리
            .forEach(hobby -> result.merge(hobby, 1, (oldValue, newValue)->++oldValue));
    //출력
    result.entrySet().forEach(entry-> System.out.println(entry.getKey() + " " + entry.getValue()));
}

csv파일을 읽은 것을 저는 List의 List로 읽어왔습니다.

한 라인에 하나의 멤버 정보가 담겨있기 때문에 첫 번째 리스트는 Member를 나타내고, Member의 정보는 콤마(,)로 구분된 데이터들을 List로 나타냈습니다.

csv파일 정보를 읽는 것은 읽어서 파싱하는 개발자 마음이므로 이차원 배열로 가져올 수도 있고 저 처럼 List로 가져올 수도 있습니다. (배열이든 리스트든 오브젝트든 스트림화 시킬 수 있어서 무관함)

그 다음에 첫 줄은 해당 컬럼에 대한 설명(이름, 취미, 소개)이기 때문에 지웠습니다.

그 후에는 스트림처리로 했습니다.

먼저 관심있는 데이터 즉, 1번 인덱스에 있는 취미를 콜론(:)기준으로 split 메서드를 이용해 나눴고, 그 값을 flatMap을 이용해 플랫한 스트림으로 변경하였습니다.

그러면 중간에 변경한 스트림은 아래와 같이 변경될 것입니다.

//before
축구:농구:야구, 개발:당구:족구, 피아노, 스포츠댄스:개발, ...
//after
축구, 농구, 야구, 개발, 당구, 족구, 피아노, 스포츠댄스, 개발, ...

그 후 Map에 결과를 담으면 되겠습니다.

containsKey로 확인하고 추가하는 방법이 있겠지만 코드를 간결하게 하기위해서 평소에 써본 적도 없는 merge메서드를 써봅니다...

default V merge(K key, V value,
            BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
    Objects.requireNonNull(remappingFunction);
    Objects.requireNonNull(value);
    V oldValue = get(key);
    V newValue = (oldValue == null) ? value :
               remappingFunction.apply(oldValue, value);
    if(newValue == null) {
        remove(key);
    } else {
        put(key, newValue);
    }
    return newValue;
}

merge메서드의 default 구현은 위와 같습니다.

key로 값을 찾아서 없으면 value로 넣고 key로 찾은 값이 있으면 remappingFunction 메서드 처리에 따라 map에서 찾은 값과 value로 넣어준 값 중에 어떻게 처리할지 정하는 거라고 보시면 됩니다.

그렇게 1번을 처리했습니다. (실제 라이브코딩 중에는 엉망진창으로 했습니다... 블로그에 올릴 때는 나름대로 간결하게 짰다는 것을 알려드립니다...)

3.2 취미별 정씨 성을 갖는 멤버 수를 구하라

public void printMemberCountGroupByJeongMember() {
    List<List<String>> lines = CsvReader.getLines();
    lines.remove(0);
    Map<String, Integer> result = new HashMap<>();
    lines.stream()
            .filter(member-> member.get(0).startsWith("정"))//정씨 성을 갖는 멤버를 필터링
            .flatMap(member -> Arrays.stream(member.get(1).split(":")))
            .forEach(hobby -> result.merge(hobby, 1, (oldValue, newValue)->++oldValue));
    //출력
    result.entrySet().forEach(entry-> System.out.println(entry.getKey() + " " + entry.getValue()));
}

아까 코드와 달라진게 filter가 추가된 것 밖에 없습니다.

정씨 성을 갖는 사람만 대상이므로 아까 작성한 스트림에 filter로 name.startsWith("정")만 걸어주면 됩니다.

3.3 소개 내용에 '좋아'가 몇 번 등장하는지 구하라

모든 멤버의 소개를 탐색해서 '좋아'라는 문자열이 총 몇 번 등장하는지 계산하는 것입니다.

public void printLikeCount() {
    List<List<String>> lines = CsvReader.getLines();
    lines.remove(0);
    final String word = "좋아";
    int result = lines.stream()
            .map(member -> countFindString(member.get(2), word))
            .reduce(0, Integer::sum);
    //출력
    System.out.println(word+" "+result);
}
//recursive search
private int countFindString(String source, String target){
    int idx = source.indexOf(target);
    if(idx == -1){
        return 0;
    }else{
        return 1 + countFindString(source.substring(idx + 1), target);
    }
}

특정 문자열을 찾는 부분에서는 map, reduce 패턴으로 처리했습니다.

특정 문자열이 몇 번 등장하는지를 세는 메서드로 따로 빼고 map함수로 스트림을 변경하고 reduce로 숫자를 모두 더하는 방식으로 처리했습니다.

4. 정리

추가적으로 유사한 2문제가 더 있었지만 생략하겠습니다...

그리고 다시 한 번 말씀드리지만 실제로 라이브코딩때 위와 같이 하지는 못했습니다.

회고 차원에서 어떻게하면 더 깔끔하게 할 수 있을까하면서 정리한 것이고, 위의 코드가 정답도 아닙니다.

고수분들은 훨씬 더 깔끔하게 처리하실 수 있겠지만, 주니어 개발자 입장에서 한 번 써본거라 여겨주시고 코드 리뷰는 환영합니다! 노하우 좀 알려주세요!

이번에 배운 것은 스트림에서 flatMap을 잘 활용하면 코드가 깔끔해질 수 있다?! 이 정도로 정리하였습니다.

ps. 문제를 낸 회사가 어딘지도 밝히지 않았고 나름대로 문제도 변경하였으나, 문제 유출의 위험이 있으면 언제든지 이 포스트는 비공개로 전환될 수 있다는 것을 알려드립니다...