문제
레벨 2의 팀 프로젝트 미션으로 SNS(Social Networking Service)를 만들고자 하는 팀이 있다. 팀에 속한 크루 중 평소 알고리즘에 관심이 많은 미스터코는 친구 추천 알고리즘을 구현하고자 아래와
같은 규칙을 세웠다.
사용자 아이디 user와 친구 관계 정보 friends, 사용자 타임 라인 방문 기록 visitors가 매개변수로 주어질 때, 미스터코의 친구 추천 규칙에 따라 점수가 가장 높은 순으로 정렬하여 최대 5명을
return 하도록 solution 메서드를 완성하라. 이때 추천 점수가 0점인 경우 추천하지 않으며, 추천 점수가 같은 경우는 이름순으로 정렬한다.
| user | friends | visitors | result |
|---|---|---|---|
| "mrko" | [["donut", "andole"], ["donut", "jun"], ["donut", "mrko"], ["shakevan", "andole"], ["shakevan", "jun"], ["shakevan", "mrko"]] | ["bedi", "bedi", "donut", "bedi", "shakevan"] | ["andole", "jun", "bedi"] |
구현 전 고려한 내용
로직
구현 과정 및 피드백 내용
main 에서 solution 메서드를 실행한다.
public class Main {
public static void main(String[] args) {
new Controller().solution();
}
}
solution 메서드에서는 순서대로 로직을 실행한다.
public void solution() {
// 정보 입력받기
String userName = inputInfo(INPUT_USER_NAME);
String inputFriends = inputInfo(INPUT_FRIENDS_LIST);
String inputVisitors = inputInfo(INPUT_VISITORS_LIST);
// 사용자, 유저 리스트 생성
User user = users.findOrCreateUser(userName);
users.addUser(user);
// 방문자 목록이 주어졌다면, 방문자목록 파싱 및 유저의 방문자목록에 추가
if (inputVisitors != null && inputVisitors.length() != 0) {
String[] visitors = parser.visitorsParsing(inputVisitors);
users.addVisitorsToUser(user, visitors);
}
// 친구 목록 파싱해서 이차원배열로 만들기
String[][] friends = parser.friendsParsing(inputFriends);
// 아이디 A의 친구목록에 아이디 B 추가, 아이디 B의 친구목록에 아이디 A 추가
users.addFriends(friends);
// 추천친구 5명 선정 및 출력
List<User> top5User = pointManager.recommendFriends(user, users);
if (top5User == null) {
output.message("❌ 추천할 친구가 없습니다.");
return;
}
output.result(top5User);
}
이 같은 검증과 기능 로직은 User 클래스에 넣었다.
2, 3은 일급컬렉션인 Friends 와 Visitors 클래스 내부에서 실질적인 데이터 변경이 이루어진다.
public class User {
private final String name;
private final Friends friends;
private final Visitors visitors;
public User(String name, Queue<User> friends, Queue<User> visitors) {
validateUserNameLength(name);
validateUserNameFormat(name);
this.name = name;
this.friends = new Friends(friends);
this.visitors = new Visitors(visitors);
}
public String getName() {
return name;
}
public Queue<User> getFriends() {
return friends.friends();
}
public Queue<User> getVisitors() {
return visitors.visitors();
}
private void validateUserNameLength(String userName) {
if (userName == null || userName.length() > USER_NAME_MAX_LENGTH) {
throw new IllegalArgumentException("사용자 이름은 1자 이상 30자 이하여야 합니다.");
}
}
private void validateUserNameFormat(String userName) {
for (int i = 0; i < userName.length(); i++) {
if (userName.charAt(i) < LOWERCASE_A || userName.charAt(i) > LOWERCASE_Z) {
throw new IllegalArgumentException("사용자 이름은 소문자 영어로 이루어져야합니다.");
}
}
}
public void addFriend(User user) {
friends.addFriend(user);
}
public void addVisitor(User user) {
visitors.addVisitor(user);
}
}
public record Friends(Queue<User> friends) {
public Friends {
validateSize(friends);
}
@Override
public Queue<User> friends() {
return new LinkedList<>(friends);
}
private void validateSize(Queue<User> friends) {
if (friends.size() == 0) return;
if (friends.size() > MAX) {
throw new IllegalArgumentException("친구 목록은 1명 이상 10000명 이하여야 합니다.");
}
}
public void addFriend(User user) {
friends.add(user);
}
}
public record Visitors(Queue<User> visitors) {
public Visitors {
validateSize(visitors);
}
@Override
public Queue<User> visitors() {
return new LinkedList<>(visitors);
}
private void validateSize(Queue<User> visitors) {
if (visitors.size() == 0) return;
if (visitors.size() > MAX) {
throw new IllegalArgumentException("방문자 목록은 0명 이상 10000명 이하여야 합니다.");
}
}
public void addVisitor(User user) {
visitors.add(user);
}
}
Controller 에서는 UserList 라는 유저 리스트를 관리하는 일급컬렉션이 있다.
private 를 통해 UserList 를 다른 데에서 수정하지 못하도록 막았고, 오직 Controller 에서만 UserList 를 관리할 수 있다.
UserList 에서는 현재 존재하는(입력받은) 유저들을 반복문으로 돌면서 사용자에게 입력받은대로 방문자를 추가하고, 각 유저들의 친구 목록에 친구를 추가하는 역할을 수행한다.
// Controller 의 UserList
private final UserList users;
// UserList 클래스 내부의 컬렉션
private final List<User> users;
유저리스트를 관리하는 게 조금 어려웠는데, 유저리스트에 이미 이름이 같은 유저가 존재하는 경우에는 해당 유저를 반환하고, 존재하지 않는 경우에는 새로 유저를 생성하고 유저리스트에 추가하는 방법을 선택했다.
// 유저를 반환하는 기능
public User findOrCreateUser(String usersName) {
for (User user : users) {
if (user.getName().equals(usersName)) {
return user;
}
}
User newUser = new User(usersName, new LinkedList<>(), new LinkedList<>());
users.add(newUser);
return newUser;
}
// 위를 기반으로, 친구를 추가하는 기능
public void addFriends(String[][] friends) {
for (String[] friend : friends) {
User userA = findOrCreateUser(friend[0]);
User userB = findOrCreateUser(friend[1]);
userA.addFriend(userB);
userB.addFriend(userA);
}
}
친구 및 방문자를 추가하는 기능은 모두 구현되었고, 이제 포인트를 부여하는 기능을 구현하면 되었다.
서비스에서 하는 것 보다는 일급컬렉션을 통해 포인트가 함부로 수정되는 것을 막고 가독성을 향상시키는 구현 방법을 택했다.
각 유저에게 포인트를 부여하는 것이므로, Map 자료구조가 맞다고 생각했다.
public class Point {
private Map<User, Integer> point;
public Point() {
this.point = new HashMap<>();
}
public List<User> getTopUsersByPoint(User user, UserList users) {
addPointByFriend(user, users);
addPointByVisitor(user);
List<User> topUsersByPoint = removeZeroPointUser();
// 내림차순 정렬
if (point.size() != 0) {
Collections.sort(topUsersByPoint, (v1, v2) -> (point.get(v2).compareTo(point.get(v1))));
}
return topUsersByPoint;
}
private List<User> removeZeroPointUser() {
List<User> users = new ArrayList<>(point.keySet());
for (User user : users) {
if (point.get(user) == 0) {
users.remove(user);
}
}
return users;
}
private void addPointByFriend(User user, UserList users) {
for (User userFriend : user.getFriends()) {
for (User otherUser : users.getUsers()) {
if (otherUser == user) continue;
if (otherUser.getFriends().contains(userFriend)) {
point.put(otherUser, point.getOrDefault(user, 0) + POINT_UP_BY_FRIEND);
}
}
}
}
private void addPointByVisitor(User user) {
for (User visitor : user.getVisitors()) {
point.put(visitor, point.getOrDefault(visitor, 0) + POINT_UP_BY_VISITOR);
}
}
}
주 기능을 모두 구현했으니, 이제 서비스에서 순서대로 실행만 하면 되었다.
RecommendFriendService 를 만들어서
를 수행하는 메서드를 만들었다.
public class RecommendFriendService {
private Point point;
public RecommendFriendService() {
point = new Point();
}
public List<User> recommendFriends(User user, UserList users) {
List<User> recommendationList = point.getTopUsersByPoint(user, users);
recommendationList = removeMe(user, recommendationList);
recommendationList = removeKnownFriend(user, recommendationList);
if (recommendationList.size() > RECOMMEND_LENGTH_MAX) {
recommendationList = recommendationList.subList(0, RECOMMEND_LENGTH_MAX);
}
return recommendationList;
}
private List<User> removeMe(User me, List<User> recommendationList) {
for (User user : recommendationList) {
if (user.getName().equals(me.getName())) {
recommendationList.remove(me);
}
}
return recommendationList;
}
private List<User> removeKnownFriend(User me, List<User> recommendationList) {
for (User recommendationFriend : recommendationList) {
for (User knownFriend : me.getFriends()) {
if (recommendationFriend.getName().equals(knownFriend.getName())) {
recommendationList.remove(recommendationFriend);
}
}
}
return recommendationList;
}
}
실행결과

알게된 점