Java의 break, continue 구문 대신 Scala의 Option을 사용해 구현해 보기

2015-12-30 10:06

현재 slipp.net 프로젝트를 java 소스 코드에서 scala 소스 코드로 변경하고 있다. 변경 과정에서 난감한 것 중의 하나는 Scala는 java에서 지원하는 break와 continue를 지원하지 않고 있다.

어제 리팩토링 중 정확하게 continue를 사용하는 코드가 등장했다. 경우에 따라 break, continue 대신 재귀 함수를 통해 해결할 수도 있는데 어제 발생한 코드는 재귀 함수로 해결하기 힘든 구조여서 고민 끝에 Option으로 해결해 봤다.

Scala로 변환하기 전의 Java 코드는 다음과 같다.

public Set<Tag> processGroupTags(Set<FacebookGroup> groupTags) {
    Set<Tag> tags = Sets.newHashSet();
    for (FacebookGroup each : groupTags) {
        if (each.isEmpty()) {
            continue;
        }
        
        Tag tag = tagRepository.findByGroupId(each.getGroupId());
        if (tag != null) {
            tags.add(tag);
            continue;
        }
        
        tag = tagRepository.findByName(each.getName());
        if (tag != null) {
            tag.moveGroupTag(each.getGroupId());
            tags.add(tag);
            continue;
        }
        
        Tag newTag = Tag.groupedTag(each.getName(), each.getGroupId());
        tags.add(tagRepository.save(newTag));
    }
    return tags;
}

while문을 속에서 DB에서 조회한 값이 존재하느냐에 따라 continue를 사용하는 일반적인 코드다. 위 코드를 Scala의 Option과 Collection의 map method를 사용해 다음과 같이 리팩토링했다.

private def getTagByFacebookGroup(facebookGroup: FacebookGroup) = {
    Option(tagRepository.findByGroupId(facebookGroup.getGroupId))
      .getOrElse(Option(tagRepository.findByName(facebookGroup.getName))
        .getOrElse {
          val newTag: Tag = Tag.groupedTag(facebookGroup.getName, facebookGroup.getGroupId)
          tagRepository.save(newTag)
        })
}

def processGroupTags(groupTags: Set[FacebookGroup]): Set[Tag] = {
    groupTags.map(getTagByFacebookGroup)
}

위와 같이 리팩토링을 한 결과 코드를 보면 Option의 getOrElse를 이중으로 사용해 해결하고 있다. 이 코드가 가독성 측면에서 괜찮은지 모르겠다. 물론 Scala의 Option에 익숙하면 이전 코드보다 좀 더 직관적일 수 있다고 생각하지만..

이와 다른 방식의 접근이 있다면 조언해 주면 좋겠다. 이런 방식으로 이전 코드를 Scala로 변환하면서 Scala를 학습해 나가는 것도 나름 재미있다.

0개의 의견 from FB

11개의 의견 from SLiPP

2015-12-30 11:23

orElse를 이용해서 중첩을 뺄 수 있겠네요.

Option(tagRepository.findByGroupId(facebookGroup.getGroupId))
  .orElse(Option(tagRepository.findByName(facebookGroup.getName)))
  .getOrElse(tagRepository.save(Tag.groupedTag(facebookGroup.getName, facebookGroup.getGroupId))))

옮기시는 과정에서 tag.moveGroupTag(each.getGroupId()) 가 빠졌네요. map으로 처리하면 됩니다.

자바로 이런 식으로 해봤는데 쓰기나 읽기나 더 좋은지는 모르겠어요. 작성도 어렵고 잠시 후에 다시 봐도 뭐하는 코드인지 한참 봐야하더라구요.

2015-12-30 12:41

@eungju.park.1 네 피드백을 반영해서 다음과 같이 수정했다.

private def getTagByFacebookGroup(facebookGroup: FacebookGroup) = {
    Option(tagRepository.findByGroupId(facebookGroup.getGroupId))
      .orElse(Option(tagRepository.findByName(facebookGroup.getName)))
      .map(t => t.moveGroupTag(facebookGroup.getGroupId))
      .getOrElse(tagRepository.save(Tag.groupedTag(facebookGroup.getName, facebookGroup.getGroupId)))
}

def processGroupTags(groupTags: Set[FacebookGroup]): Set[Tag] = {
    groupTags.map(getTagByFacebookGroup)
}

orElse 써서 중첩 줄이니까 좋네. Option에서 바로 get 하면 어떻게 처리할지 막막할텐데 map 써서 처리 가능하니 좋네. scala의 막강함을 다시 한번 느끼네 되네

2015-12-30 13:17

제가 볼때는 애초에 findXxxxx 메소드 자체가 Option타입을 반환하는게 더 좋을거 같습니다. find 메소드의 경우 찾는 데이터가 있을수도 있고 없을수도 있기 때문에 null을 반환할 가능성이 생기는데, Scala를 쓰시면 아예 null을 취급 안 하는 방식이 가장 좋습니다. 그래서 저에게 나머지 코드가 있는게 아니니 그냥 대충 trait로 뽑아보자면,

trait TagRepository {
  def findByGroupId(groupId: Long): Option[Tag]
  def findByName(name: String): Option[Tag]
  def save(tag: Tag): Tag
}

이렇게 만들고 위의 코드는 이렇게 변경이 가능하겠죠.

private def getTagByFacebookGroup(facebookGroup: FacebookGroup): Tag =
  tagRepository.findByGroupId(facebookGroup.getGroupId)
               .orElse(tagRepository.findByName(facebookGroup.getName)
                                    .map(_.moveGroupTag(facebookGroup.getGroupId)))
               .getOrElse(tagRepository.save(Tag.groupedTag(facebookGroup.getName, facebookGroup.getGroupId)))

def processGroupTags(groupTags: Set[FacebookGroup]): Set[Tag] = groupTags.map(getTagByFacebookGroup)

저의 경우는 자바로 코딩할때도 Repositoryfind 메소드들이 Optional타입을 반환하게 만듭니다. 물론 자바8을 사용하고 있으니 가능한건데 8을 못쓰면 Guava 같은걸 써서 해야겠죠.

2015-12-30 13:30

@자바지기 findByName 성공일 때만 moveGroupTag를 해야하니 map을 orElse 안으로 넣어야 기존 의미와 더 일치합니다.

    Option(tagRepository.findByGroupId(facebookGroup.getGroupId))
      .orElse(Option(tagRepository.findByName(facebookGroup.getName))
        .map(t => t.moveGroupTag(facebookGroup.getGroupId)))
      .getOrElse(tagRepository.save(Tag.groupedTag(facebookGroup.getName, facebookGroup.getGroupId)))

이런 패턴이 자주 보이니 Optional[R]을 리턴하는 함수 목록을 받아서 차례로 실행하여 첫 Some을 돌려주는 함수를 만들면 좋겠네요.

blah[R](producers: List[() => Optional[R]]): Optional[R]
2015-12-30 13:33

@Kevin 저도 find 메서드에서 Option을 반환하는 방식이 좋다고 생각합니다. 그런데 Repository에 해당하는 놈들이 spring data jpa 기반의 프레임워크로 동작하고 있는데요. 이 부분을 Optional로 반환할 수 있을까요? 제 코드가 Spring 프레임워크 기반하에서 Scala를 사용하다보니 Option을 극대화해서 사용하는데는 한계가 있는 듯 해서요. 혹시 아이디어 주시면 도전해 볼께요.

2015-12-30 13:48

@자바지기 아... Spring을 쓰고 계시군요. ㅡ_ㅡ; Scala를 사용하셔서 Spring을 쓰고 계실거란 생각을 못 했네요. 그렇다면 Spring측에서는 Scala를 제대로 사용하도록 만들지 못했단 거군요. :( 전 Java 쓸 경우도 GenericRepository를 직접 만들어서 쓰고 있어서 (아마 스프링에 있는거랑 거의 비슷할겁니다), Spring Data를 쓰면서 이문제를 어떻게 쉽게 해결할 수 있을지 잘 모르겠습니다. GenericRepository를 직접 만들어 쓰시는건 어떨까요? 사실 제 책(Scala가 아니라 Java8이지만)에 집어 넣을 내용이기도 합니다만, 쉽게 직접 만들어 쓰실 수 있을거 같습니다. 뭐 정 안되면 Wrapper 같은걸 만들어서 내부적으로는 Spring의 Repository를 사용하시는 방법도 있겠죠. 제가 Scala와 Spring을 같이 쓰는거에 대한 고민을 해본적이 없어서, 현재로서는 더 나은 대답이 생각나지 않는군요. 혹시 찾게 되시면 공유해 주시면 감사하겠습니다. :)

2015-12-30 14:01

음..점심시간에 잠깐 생각해본건데 컴파일은 안해봤습니다.. 요구 사항을 다음과 같다고 혼자 추측하고;;; // facebook group을 뒤지면서.. // groupId가 empty면 넘어가고.. // groupId가 있으면 group id로 tag를 찾아서 있으면 해당 tag 리스트에 추가.. // groupId가 있으면 group id로 tag를 찾았는데 tag가 없으면 이름으로 tag를 찾고 찾으면 리스트에 추가.. // 두가지 방법으로 찾았는데 관련 Tag 없으면 하나 만들어서 넣는다.

“repository의 "find계열의 함수의 return type이 Option이라면, save의 경우 save된 tag라면" 이라고 생각하고 작성해봤습니다. 저의 경우 보통 그렇게 사용하거든요..” => 요 부분은 글 쓰는 동안 댓글이 달렸으므로..무효..Option으로 처리해봤습니다..허허허..

제 기준으로 이해가 잘 될만한 naming 및 코드 레벨로 정리해본건데..컴파일도 안돌려본거라..ㅎㅎ 참고 부탁드려요^^;;

private processGroupTags(groupTags: Set[Facebookgroup]): Set[Tag] =
     groupTags.filter(_.isEmpty == false).map { each =>
          val groupId = each.getId
          val groupName = each.getName

          Option(tagRepository.findByGroupId(groupId)).getOrElse {
               Option(tagRepository.findByName(groupName)) match {
                     case Some(tag) => tag.moveGroupTag(groupId); tag
                     case None => tagRepository.save(Tag.groupedTag(groupName, groupId))
                }
          }
     }
2015-12-30 17:18
  • IDE에서 검증하지 않아 문법상 오류는 있습니다.
  • Tag 클래스 내부를 잘 몰라 관련 로직은 그대로 사용했습니다. 어떻게 되어있는지 좀 더 안다면 다른 방식으로도 할 수 있겠는데, 지금으로선 이게 한계네요.
def processGroupTags(groupTags: Set[FacebookGroup]): Set[Tag] = {
  val tags = scala.collection.mutable.Set[Tag]()
  val filteredTags = groupTags.filter(!(_.isEmpty))
  filteredTags.map(Option(tagRepository.findByGroupId(_.getGroupId)) map { tag => tags.add(tag) })
  filteredTags map { each =>
    Option(tagRepository.findByName(each.getName)) map { tag => tag.moveGroupTag(each.getName); tags.add(tag) }
  }
  filteredTags.diff(tags).map { each =>
    val newTag = Tag.groupedTag(each.getName(), each.getGroupId())
    tags.add(tagRepository.save(newTag));
  }
  tags
}
2015-12-30 20:21

@자바지기 이거 한번 보세요. :) https://github.com/spring-projects/spring-data-examples/tree/master/jpa/java8

일단 Optional로 받으신후에 Optional에서 Option으로의 변환은 scala-java8-compatimport scala.compat.java8.OptionConverters._를 사용하시면 쉽게 될거 같습니다만, 직접 해보지 않아서 모르겠네요. ^^;

근데 Spring에서 Optional 타입을 지원하기 시작했으면 Spring Scala에서도 Option을 지원해야 할거 같은데, 아직인가 보죠? 오히려 Spring Scala의 경우는 Java8지원 이전부터 Option을 지원했어야 정상으로 보입니다만...

2015-12-30 22:45

@Kevin 감사합니다. Optional 지원하고 있으니 Scala와도 어떤 식으로든 연계할 수 있을거 같네요. 한번 시도해 볼께요. spring data jpa가 된다면 spring mvc에서도 사용자가 입력한 값에 대해 Optional 처리가 될 수도 있겠네요. 그런 날이 오기를 기대해 봅니다.

의견 추가하기

연관태그

← 목록으로