채팅 서버를 구현하는 것은 대형 프로젝트로, 면접장에서 끝낼 수 있는 일의 범위를 넘어섭니다. 따라서, 실제와 일치할 필요는 없지만, 실제 구현을 꽤 잘 반영하는 수준이 되도록 설계하면 됩니다.
채팅에 필요한 모든 기능을 만드는 것 보다 사용자 관리와 사용자 간 대화 에 관련된 핵심 기능에 초점을 맞추도록 하겠습니다.
- 사용자 추가하기
- 대화 시작하기
- 사용자 상태 갱신하기
구현할 채팅 시스템은 그룹 채팅뿐 아니라 일대일(private) 채팅도 지원합니다. 음성 채팅이나 화상 채팅, 파일 전송 등의 기능은 제외하겠습니다.
어떤 종류의 기능을 지원해야 하는가?
이는 스스로 몇 가지 예상되는 기능을 나열해 보고, 면접관과 토의해야 합니다.
- 온라인/오프라인 알림
- 친구 추가 요청(요청 전송, 요청 수락, 요청 거부)
- 상태 메시지의 갱신
- 일대일/그룹 채팅 세션 생성
- 시작된 채팅 세션에 새로운 메시지 추가
허락된 시간 내에서 가능한 기능들을 생각해보고 추가하면 될 것 같습니다.
시스템의 핵심 컴포넌트는 무엇인가?
시스템은 아마도 데이터베이스, 사용자들 그리고 서버들로 구성될 것입니다. 이런 부분은 객체 지향 설계에 포함시키지 않을 것이지만, 시스템의 전반적인 형태로서 토론할 수 있습니다.
데이터베이스는 지속적으로 보관되어야 하는 자료들, 즉 사용자 리스트나 채팅 내역 등을 보관하기 위해 사용될 것입니다. SQL 데이터베이스도 좋지만 규모확장성이 필요한 경우에는 BigTable과 유사한 시스템들을 사용할 수도 있을 것입니다.
클라이언트와 서버 간 통신에는 XML을 사용하면 좋을 것입니다. 가장 잘 압축된 형태의 데이터는 아니지만(이 점을 면접관에게 지적해야 합니다) 컴퓨터가 해석하기에도 좋고 사람이 읽기에도 좋습니다. XML을 사용하면 디버깅이 한층 쉬어질 것이고, 이는 꽤 중요한 이점입니다.
서버는 여러 대로 구성될 것입니다. 데이터는 이 서버들에 분할될 것이며, 따라서 데이터를 찾아 서버 사이를 오락가락할 수도 있습니다. 가능하다면, 탐색 오버헤드를 최소화하기 위해 어떤 데이터는 여러 서버에 복제해 둘 수도 있을 것입니다. 이때 중요하게 따져봐야 할 제약 조건은 SPOF(Single-Point-Of-Failure) 를 없애는 것입니다. 가령 어떤 하나의 서버가 모든 사용자 로그인을 처리한다면, 그 기계가 네트워크에서 사라질 경우 잠재적으로는 수백만 사용자의 접속이 불가능해질 것입니다.
핵심 객체와 메서드는 무엇인가?
사용자, 대화 그리고 상태 정보 메시지 등의 개념이 시스템의 핵심 객체들을 구성할 것입니다.
// UserManager는 사용자와 관련된 핵심적 기능을 구현하는 장소입니다.
public class UserManager {
private static UserManager instance;
private HashMap<Integer, User> userById; // 사용자 id를 사용자에게 대응시킨다.
private HashMap<String, User> userByAccountName; // 계정 이름을 사용자에게 대응시킨다.
private HashMap<Integer, User> onlineUsers; // 사용자 id를 온라인 상태인 사용자에 대응시킨다.
public static UserManager getInstance() {
if (instance == null) instance = new UserManager();
return instance;
}
public void addUser(User fromUser, String toAccountName) { ... }
public void approveAddRequest(AddRequest req) { ... }
public void rejectAddRequest(AddRequest req) { ... }
public void userSignedOn(String accountName) { ... }
public void userSignedOff(String accountName) { ... }
}
User 클래스의 메서드 receivedAddRequest는 사용자 B에게 사용자 A가 친구 승인을 요청했음을 알립니다. 사용자 B는 그 요청을 승인하거나 거부할 수 있습니다(UserManager.approveAddRequest 또는 rejectAddRequest). 그리고 UserManager는 사용자를 다른 이의 연락처 목록에 추가하는 일을 처리합니다.
User Manager는 AddRequest를 사용자 A가 처리해야 하는 요청 목록에 추가하기 위해 User 클래스의 sentAddRequest를 호출합니다. 따라서 다음과 같은 흐름으로 처리가 이루어집니다.
- 사용자 A가 사용자 B에 대해 Add User 버튼을 클릭하면, 해당 요청은 서버로 날아갑니다.
- 사용자 A가 requestAddUser(사용자 B)를 호출합니다.
- 이 메서드는 다시 UserManager.addUser를 호출합니다.
- UserManager는 사용자 A.sentAddRequest와 사용자 B.receivedAddRequest를 호출합니다.
이 흐름은 사용자 간 상호작용을 설계하는 방법 중 하나입니다. 유일한 방법도 아니고, 다른 좋은 방법이 없는 것도 아닙니다.
public class User {
private int id;
private UserStatus status = null;
private HashMap<Integer, PrivateChat> privateChats; // 상대방의 id를 채팅방에 대응시킨다.
private ArrayList<GroupChat> groupChats; // 그룹 채팅 id를 그룹 채팅에 대응시킨다.
private HashMap<Integer, AddRequest> receivedAddRequest; // 다른 사용자의 id를 AddRequest 객체에 대응시킨다.
private HashMap<Integer, AddRequest> sentAddRequest; // 다른 사용자의 id를 AddRequest 객체에 대응시킨다.
private HashMap<Integer, User> contacts; // 사용자 id를 사용자 객체에 대응시킨다.
private String accountName;
private String fullName;
public User(int id, String accountName, String fullName) {...}
public boolean sendMessageToUser(User to, String content) {...}
public void setStatus(UserStatus status) {...}
public UserStatus getStatus() {...}
public boolean addContact(User user) {...}
public void receivedAddRequest(AddRequest req) {...}
public void sentAddRequest(AddRequest req) {...}
public void removeAddRequest(AddRequest req) {...}
public void requestAddUser(String accountName) {...}
public void addConversation(PrivateChat conversation) {...}
public void addConversation(GroupChat conversation) {...}
public int getId() {...}
public String getAccountName() {...}
public String getFullName() {...}
}
Conversation 클래스는 추상 클래스입니다. 모든 Conversation은 반드시 GroupChat이거나 PrivateChat이어야 하며, 이들 클래스는 각기 고유한 기능을 갖기 떄문입니다.
public abstract class Conversation {
protected ArrayList<User> participants;
protected int id;
protected ArrayList<Message> messages;
public ArrayList<Message> getMessages() {...}
public boolean addMessage(Message m) {...}
public int getId() {...}
}
public class GroupChat extends Conversation {
public void removeParticipant(User user) {...}
public void addParticipant(User user) {...}
}
public class PrivateChat extends Conversation {
public PrivateChat(User user1, User user2) {...}
public User getOtherParticipant(User primary) {...}
}
public class Message {
private String content;
private Date date;
public Message(string content, Date date) {...}
public String getContent() {...}
public Date getDate() {...}
}
AddRequest와 UserStatus는 많은 기능을 갖지 않는 간단한 클래스입니다. 이 클래스들의 주된 목적은 다른 클래스들이 사용할 데이터를 한데 묶어 놓는 것입니다.
public class AddRequest {
private User fromUser;
private User toUser;
private Date date;
RequestStatus status;
public AddRequest(User from, User to, Date date) {...}
public RequestStatus getStatus() {...}
public User getFromUser() {...}
public User getToUser() {...}
public Date getDate() {...}
}
public class UserStatus {
private String message;
private UserStatusType type;
public UserStatus(UserStatusType type, String message) {...}
public UserStatusType getStatusType() {...}
public String getMessage() {...}
}
public enum UserStatusType {
offline, Away, Idle, Available, Busy
}
public enum RequestStatus {
Unread, Read, Accepted, Rejected
}
가장 풀이 어려운 문제는?
Q1. 어떤 사용자가 온라인인지 어떻게 알 수 있는가? 정말로 확실하게 알 수 있는가?
Q2. 정보 불일치 문제는 어떻게 처리해야 하는가?
Q3. 서버의 규모확장성은 어떻게 확보해야 하나?
Q4. DOS 공격은 어떻게 막아야 하나?