:: 오픈채팅방-배열과 맵을 이용한 데이터 핸들링
1. 문제 개요
문제출처
- 2019년 카카오 신입공채 1차 온라인 코딩 테스트 해설
- 온라인 테스트: https://www.welcomekakao.com/learn/courses/30/lessons/42888
- 정답률: 59.91%
시작하기
카카오 개발자 블로그에서 제공하는 공식 해설도 있지만 좀 더 설명이 필요한 초보자를 대상으로 비교적 자세한 풀이과정과 2-3 가지 구현 예를 서로 비교해 보도록 하겠습니다.
- 먼저 온라인 테스트 링크에서 문제를 확인하고 자신이 있으면 바로 풀어 봅니다.
- 언어는 본인이 희망하는 언어를 선택 할 수 있습니다. 여기서는 자바언어(JDK11)를 기준으로 합니다.
- 다양한 문제풀이 방법이 있지만 비교적 초보자들도 이해가 가능한 수준을 우선으로 구현하고 좀 더 고수준 풀이도 다뤄 봅니다.
문제 요약
제시된 문제는 일종의 채팅방 로그 관리로 볼 수 있습니다. 채팅방에 들어오거나 나가는 기록이 남아 있는 것이고 다음의 두가지 조건에 의해 로그 메시지는 변경될 수 있습니다.
- 기존유저가 다시 들어오면서 이름을 바꿔 들어오는 경우(아이디 는 유지)
- 채팅방에 들어온 유저가 이름을 바꾸는 경우
입력 메시지 구조
구현 해야할 메서드는 String[] solution(String[String record]) 이며 인자와 리턴은 모두 문자열 배열 입니다.
cmd id [nickname]
예) Enter uid1234 Hong , Enter uid4567 Hong , Leave uid1234, Enter uid1234 Kang, Change uid4567 Hwang
배열의 데이터는 각각 명령어 아이디 닉네임
의 구조이며 명령어는 Enter, Leave, Change
중에 하나이고 닉네임의 경우 옵션으로 Leave
에서는 사용되지 않습니다.
출력 메시지 구조
관리되거나 리턴되는 String[] 의 메시지는 다음과 같이 구성 됩니다.
Hong님이 들어왔습니다.
Kang님이 나갔습니다.
...
2. 문제 해결 전략
너무 급하게 서둘지 말고 머리속에서 생각을 정리하고 메모장을 이용해 자료구조를 도식화 하거나 프로그램의 흐름, 필요한 알고리즘등을 정리해야 합니다.
이번 문제는 특별한 알고리즘등을 고려하지 않아도 되는 비교적 단순한 문제 입니다. 다만 한가지 문제해결에 방해되는 요소가 있습니다.
이름이 변경되는 경우(두가지 조건, 앞에서 설명) 기존 메시지의 닉네임도 수정
입력 메시지 는 id 값이 포함되어 있고 출력 메시지는 id 값이 없으므로 기존 메시지의 닉네임을 변경하기 어렵습니다. 단순히 제공 코드에서 명시한 리턴값인 String[] answer
배열만 사용하려 한다면 지나치게 복잡한 코드가 될 수 있고 효율이 떨어집니다.
Step-1
기본적인 접근은 사용자 목록과 출력메시지를 별도로 관리하고 마지막에 최종 변경된 메시지를 String[]로 변환해 리턴하도록 하는 것입니다.
먼저 전체 입력으로 부터 사용자 정보를 따로 관리해서 닉네임이 최신으로 유지되도록 합니다. 여기에는 Key:Value
형태의 저장이 가능한 Map 자료구조가 사용됩니다.
HashMap<String,String> userlist = new HashMap<>();
for(String r : record) {
StringTokenizer st = new StringTokenizer(r," ");
String cmd = st.nextToken();
String id = st.nextToken();
if(cmd.equals("Enter") || cmd.equals("Change")) {
String name = st.nextToken();
userlist.put(id, name);
}
}
- 입력 배열값의 원소들로 부터 메시지를 분리하기 위해
StringTokenizer()
를 사용(String 의 split() 도 사용가능). - 입력 메시지로 부터 Enter 와 Change 의 경우 id, name 을 userlist 에 저장.
- Map 의 특징으로 키의 중복이 허용되지 않기 때문에 기존에 id가 있다면 새로운 값으로 업데이트 됨.
Step-2
입력값에 기초해 메시지를 빌드 합니다.
사용자 목록 관리와 메시지 빌드를 같은 루프에서 처리 하는 것이 효율적으로 보이지만 중간에 닉네임 변경이 발생하기 때문에 솔루션-1 에서와 같이 먼저 아이디와 닉네임 정보
를 처리한 뒤 메시지를 빌드하는 것이 좋습니다.
메시지 빌드는 record 배열로 부터 Change
를 제외하고 각각 명령어에 따라 메시지를 만들어 ArrayList 에 추가 합니다. 여기서 answer[]를 바로 사용하지 않고 ArrayList를 사용하는 것은 주어진 메시지의 길이에 따라 동적인 목록을 편하게 구성하기 위함입니다. 모든 메시지가 만들어지면 return 시에 배열로 변환해 주면 됩니다.
ArrayList<String> logs = new ArrayList<>();
for(String msg : record) {
String cmd = msg.split(" ")[0];
String id = msg.split(" ")[1];
if(cmd.equals("Enter")) {
logs.add(String.format("%s님이 들어왔습니다.", userlist.get(id)));
} else if(cmd.equals("Leave")) {
logs.add(String.format("%s님이 나갔습니다.", userlist.get(id)));
}
}
- 앞에서와 같이
Enter
,Leave
메시지만 출력 문장을 구성해 ArrayList 인 logs 에 저장. - 메시지 분리는 앞에서와 같이 StringTokenizer 를 사용해도 되지만 여기서는
split
를 사용. - 메시지 원소가 많고 예시와 같이 반복해서 split()을 사용하는 형태는 성능상 문제가 될 수 있습니다.
마지막으로 리턴 처리 입니다. 리턴 타입은 모든 로그가 담긴 String[] 이므로 앞에서 생성한 ArrayList 를 String[] 로 바꿔줘야 합니다. ArrayList 의 toArray() 함수를 사용할 수 있습니다만 그냥 사용하면 예외가 발생 합니다. 이유는 ArrayList 의 원소들은 기본적으로 Object 타입이기 때문에 형변환을 해주어야 합니다. 그러나 단순히 (String[])logs.toArray()
과 같이 일반적인 형변환을 사용하면 컴파일 에러는 발생하지 않지만 실행시 문제가 됩니다.
따라서 다음과 같이 두가지 방법중 하나를 사용해야 합니다.
return logs.toArray(new String[logs.size()]);
return logs.toArray(String[]::new); //jdk11
3. 실행 및 검증
기본 코드는 다음과 같습니다. 학습을 위해서는 Eclipse 등의 IDE에서 작업하는 것이 좋고 실전 연습을 위해서는 코딩 테스트 사이트를 이용하도록 합니다. 코딩 테스트 사이트에는 solution() 메서드 부분만 작성하면 됩니다. main 에는 결과와 함께 간단하게 실행 시간을 계산하도록 처리해 두었습니다.
import java.util.*;
public class L1_Exam1 {
HashMap<String,String> userlist = new HashMap<>();
ArrayList<String> logs = new ArrayList<>();
public static void main(String[] args) {
long start = System.currentTimeMillis();
String[] input = { "Enter uid1234 Muzi", "Enter uid4567 Prodo", "Leave uid1234", "Enter uid1234 Prodo", "Change uid4567 Ryan" };
L1_Exam1 app = new L1_Exam1();
String[] result = app.solution(input);
for(String s : result) {
System.out.println(s);
}
long end = System.currentTimeMillis();
System.out.println((end - start)/1000.0);
}
public String[] solution(String[] record) {
// build userlist
for(String msg : record) {
StringTokenizer st = new StringTokenizer(msg," ");
String cmd = st.nextToken();
String id = st.nextToken();
if(cmd.equals("Enter") || cmd.equals("Change")) {
String name = st.nextToken();
userlist.put(id, name);
}
}
// build message
for(String msg : record) {
String cmd = msg.split(" ")[0];
String id = msg.split(" ")[1];
if(cmd.equals("Enter")) {
logs.add(String.format("%s님이 들어왔습니다.", userlist.get(id)));
} else if(cmd.equals("Leave")) {
logs.add(String.format("%s님이 나갔습니다.", userlist.get(id)));
}
}
return logs.toArray(String[]::new);
//return logs.toArray(new String[logs.size()]);
}
}
Junit 을 이용한 테스트 케이스 검증은 다음과 같이 작성할 수 있습니다. 테스트케이스를 이용하는 경우 구현 클래스에서 main() 은 필요 없습니다.
import static org.junit.Assert.*;
import org.junit.Test;
public class L1_Exam1Test {
@Test
public void test() {
L1_Exam1 app = new L1_Exam1();
String[] msg = {"Enter uid1234 Muzi", "Enter uid4567 Prodo", "Leave uid1234", "Enter uid1234 Prodo", "Change uid4567 Ryan"};
String[] ret = {"Prodo님이 들어왔습니다.","Ryan님이 들어왔습니다.","Prodo님이 나갔습니다.","Prodo님이 들어왔습니다."};
String[] result = app.solution(msg);
assertEquals(ret.length, result.length);
assertArrayEquals(ret, result);
}
}