Scala collection 사용해 알림 사용자 찾는 코드 구현하기

2016-01-12 10:02

slipp의 페이스북 알림 사용자 목록을 구하는 요구사항은 다음과 같다.

  • 질문을 한 사용자가 페이스북 사용자인 경우
  • 답변을 한 사용자 중 페이스북 사용자인 경우
  • 현재 로그인한 사용자는 제외

위 요구사항을 만족하기 위해 구현한 자바 코드는 다음과 같다.

    public Set<SocialUser> findNotificationUser(SocialUser loginUser) {
        Answers newAnswers = new Answers(this.answers);
        Set<SocialUser> notifierUsers = newAnswers.findFacebookAnswerers();
        if (this.writer.isFacebookUser) {
            notifierUsers.add(this.writer);
        }
        return Sets.difference(notifierUsers, Sets.newHashSet(loginUser));
    }

Answers 코드는 다음과 같다. 위의 Sets는 guava 라이브러리를 사용했다.

public class Answers {
    private List<Answer> answers;

    public Answers(List<Answer> answers) {
        this.answers = answers;
    }

    public Set<SocialUser> findFacebookAnswerers() {
        Set<SocialUser> answerers = Sets.newHashSet();
        for (Answer answer : answers) {
            if (answer.isFacebookWriter()) {
                answerers.add(answer.getWriter());
            }
        }
        
        return answerers;
    }
}

위와 같이 구현한 이유는 slipp 개발을 시작하는 시점에 JDK 8이 나오지 않은 상태라 람다를 적용하지 못했더니 더 복잡하다. 이번에 scala로 전환하면 다음과 같이 구현했다.

  def findNotificationUser(loginUser: SocialUser) = {
    val participants = answers.map(a => a.getWriter) :+ this.writer
    participants.toSet.filter(u => u.isFacebookUser && !u.isSameUser(loginUser))
  }

물론 JDK 8의 람다를 적용하면 위와 비슷한 코드량으로 구현할 수 있으리라 생각한다.

위 코드가 친숙하지 않은 사람들에게는 가독성 측면에서 좋지 않을 수도 있다. 하지만 친숙함의 차이 아니겠는가? 위와 비슷한 구현을 계속해서 본다면 어렵지 않게 파악할 수 있으리라 생각한다.

일단 map, filter, reduce에 대해서만 명확히 이해하고 있어도 많은 도움이 되리라 생각한다.

0개의 의견 from FB

2개의 의견 from SLiPP

2016-01-12 11:51

Java 8을 쓰면 대충 아래와 같이 될거 같네요. 안타깝게도 Java 8의 Stream에는 element을 추가하는 메소드가 없어서 flatMap과 identity function을 활용해야 할거 같습니다.

Stream.of(
  answers.stream().map(Answer::getWriter),
  Stream.of(this.writer)
)
.flatMap(Function.identity())
.filter(SocialUser::isFacebookWriter)
.filter(user -> !user.isSameUser(loginUser))
.collect(Collectors.toSet())

근데 .filter(user -> !user.isSameUser(loginUser)) 이부분은 사실 결과가 Set이기 때문에 최종적으로 하나만 빼버리면 되는데도 불구하고 위와 같이 하면 전체를 다 비교 해야하니 차라리

final Set<SocialUser> participants = Stream.of(
  answers.stream().map(Answer::getWriter),
  Stream.of(this.writer)
)
.flatMap(Function.identity())
.filter(SocialUser::isFacebookWriter)
.collect(Collectors.toSet());

participants.remove(loginUser);
return participants;

이런식으로 하는게 더 나을거 같습니다. mutation이 일어나긴 하지만, 안타깝게도 애초에 자바의 Set자체가 immutable collection이 아니니까요.

근데 스칼라 코드의 경우도 마찬가지 이유로 그냥

  def findNotificationUser(loginUser: SocialUser) = {
    val participants = answers.map(_.getWriter) :+ this.writer
    participants.toSet.filter(_.isFacebookUser) - loginUser
  }

이렇게 해도 될거 같구요. 이경우 Set에 대한 mutation이 아니라 loginUser를 뺸 새로운 Set이 생성되는거라서 Java쪽보다 더 낫다고 봐야겠죠.

개인적으로는 이런식으로 할거 같습니다.

  def findNotificationUser(loginUser: SocialUser) =
    (for {
      user <- answers.map(_.getWriter) :+ this.writer
      if user.isFacebookUser
    } yield user).toSet - loginUser
2016-01-12 12:55

@Kevin 구체적인 답변 감사합니다. 저도 아직 scala 경험이 많지 않아 이렇게 공유하면 꼭 피드백 주는 분들 때문에 많이 배우는 맛에 공유하게 되네요. 오늘도 @Kevin 님 통해서 한 수 배우고 가네요.

자바에 람다가 추가되기는 했지만 코드의 복잡도가 Scala에 비해 높기는 하네요.

의견 추가하기