네이버 신입 사원 교육 중 람다로 구현한 소스 코드

2015-02-13 10:33

요구사항은 다음과 같다.

  • 컨퍼런스의 강의 시간을 의미하는 기준 시간이 있다.
  • 컨퍼런스의 각 강의 시간에 해당하는 세션이 있다.
  • 그런데 강의 시간별로 모든 세션이 존재하지 않는 상태가 발생하는데 이와 같이 이빨이 빠지는 경우의 강의 시간에 대해 빈 세션을 추가한다.

위 기능을 구현하기 위해 신입사원이 2중 for문과 싸우고 있길 때 기능을 축소해 구현해 본 기능이다.

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;


import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;


public class CollectionTest {
    private static final Logger logger = LoggerFactory.getLogger(CollectionTest.class);


    @Test
    public void transform() {
        List<Session> sessions = Arrays.asList(new Session("10:00"), new Session("10:03"), new Session("10:10"));
        List<String> standardTimes = Lists.newArrayList();
        for (int i = 0; i < 10; i++) {
            standardTimes.add("10:0" + i);
        }


        List<Session> filteredSessions = standardTimes.stream().filter(s -> {
            for (Session session : sessions) {
                if (session.isSameSessionTime(s)) return false;
            }
            return true;
        }).map(s -> new Session(s)).collect(Collectors.toList());
        logger.debug("sessions : {}", filteredSessions);


        List<Session> newSessions = Lists.newArrayList(Iterables.concat(sessions, filteredSessions));
        Collections.sort(newSessions, (s1,  s2) -> s1.getSessionTime().compareTo(s2.getSessionTime()));
        logger.debug("sessions : {}", newSessions);
    }


    private class Session {
        private String sessionTime;


        Session(String sessionTime) {
            this.sessionTime = sessionTime;
        }


        boolean isSameSessionTime(String standardTime) {
            if( standardTime == null ) {
                return false;
            }


            return standardTime.equals(this.sessionTime);
        }


        String getSessionTime() {
            return sessionTime;
        }


        @Override
        public String toString() {
            return "Session [sessionTime=" + sessionTime + "]";
        }
    }
}

위와 같이 구현한 소스 코드를 공유했더니 같이 교육을 담당하고 있는 정상혁님이 람다를 최대한 사용해 모든 if/for문을 제거하는 코드를 구현해 주었다. 이 맛에 소스 코드를 공유한다. 정상혁님의 코드를 통해 나 또한 많이 배웠다. 정상혁님이 위 소스 코드에 변경한 내용은 다음과 같다.

  • sessions 생성에 Session:new 사용
  • standardTimes 생성에 for문 대신 IntStream 사용
  • s -> new Session(s))대신 Session:new로 메서드 레퍼런스 사용
  • isSameSessionTime()로 검사하는 for, if문 대신 Stream.nonMatch()사용
  • Guava 미사용 (Stream.concat 등으로 대체)
  • Collections.sort대신 Stream.sorted와 Comparator.comparing, 메서드 레퍼런스 사용
  • isSameSessionTime안의 null 검사로직을 Optional.orElse로 대체
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;


import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


public class CollectionTest {
    private static final Logger logger = LoggerFactory.getLogger(CollectionTest.class);


    @Test
    public void transform() {
        List<Session> sessions = Stream.of("10:00", "10:03","10:10")
                .map(Session::new)
                .collect(Collectors.toList());


        Stream<String> standardTimes = IntStream.range(0, 10).boxed().map(i -> "10:0" + i);
        Stream<Session> filteredSessions = standardTimes
            .filter(
                t -> sessions.stream().noneMatch(
                    s -> s.isSameSessionTime(t)
                )
            )
            .map(Session::new);


        List<Session> newSessions = Stream.concat(filteredSessions, sessions.stream())
                .sorted(Comparator.comparing(Session::getSessionTime))
                .collect(Collectors.toList());
        logger.debug("sessions : {}", newSessions);
    }


    private class Session {
        private String sessionTime;


        Session(String sessionTime) {
            this.sessionTime = sessionTime;
        }


        boolean isSameSessionTime(String standardTime) {
            return Optional.of(standardTime)
                .map(s-> s.equals(this.sessionTime))
                .orElse(false);
        }


        String getSessionTime() {
            return sessionTime;
        }


        @Override
        public String toString() {
            return "Session [sessionTime=" + sessionTime + "]";
        }
    }
}

1개의 의견 from FB

9개의 의견 from SLiPP

2015-02-13 13:53

toString등 완벽하게 동일하지는 않지만 거의 유사한 코드를 스칼라로 작성해보았습니다.

class CollectionTest {
  @Test def transform = {
    val sessions = List(Session("10:00"), Session("10:03"), Session("10:10"))
    val filteredSessions = 0 to 9 map (x => Session("10:0" + x)) filter (!sessions.contains(_))


    Logger.info((sessions ++ filteredSessions).sortBy(_.sessionTime).toString)
  }


  case class Session(sessionTime: String)
}
2015-02-13 15:12

스칼라 for안에 억지스럽게 다 구겨 넣은 버젼 입니다 ㅋ

for {
      session <- (0 to 9).map(i => Session("10:0" + i))
      if !(Session("10:00") :: Session("10:03") :: Session("10:10") :: Nil contains session)
    } yield session
2015-02-13 16:05

@자바지기 ㅎㅎ예 돌려봤을 때 결과는 동일하게 나왔어요. 언어의 기능을 이용해 간결하게 하는 부분도 있고, 기본 라이브러리를 적극 활용해서 간결하게 하는 부분도 있었습니다. 하지만 언어 자체의 차이를 통해 간결하게 할 수 있는 부분이 많아 보여요.

2015-02-14 02:07

차차님이 한것과 같은 방식으로 한 Groovy코드도 올려봅니다~

package deview.kr;


import groovy.transform.EqualsAndHashCode
import org.junit.Test
import org.slf4j.Logger
import org.slf4j.LoggerFactory


class GrCollectionTest {
	private static final Logger logger = LoggerFactory.getLogger(GrCollectionTest.class);


	@Test public void transform() {
		def sessions = ["10:00", "10:03","10:10"].collect{new Session(sessionTime: it)}
		def standardTimes = (0..9).collect{new Session(sessionTime:"10:0" + it)}


		def filteredSessions = standardTimes.findAll{ !sessions.contains(it)}
		logger.debug("sessions : {}", (sessions + filteredSessions).sort{it.sessionTime} )
	}


	@EqualsAndHashCode
	private class Session {
		String sessionTime
		@Override
		public String toString() {
			return "Session [sessionTime=" + sessionTime + "]"
		}
	}
}

Java에서도 Session.isSameSessionTime() 대신 equals를 활용하고 Collection.contains등을 이용하면 조금더 코드가 줄어들긴합니다. 그런데 처음 예제에서 isSameSessionTime를 따로 정의하신게 equals()는 또 다르게 정의하는 상황을 염두에 두셨을것 같기는하네요.

같은 방식으로도 Groovy나 Scala가 Java보다 코드를 더 줄일수 있는 점은 아래와 같은 요소가 생각나네요.

  • equals를 편하게 정의할수 있는 @EqualsAndHashCode 같은 요소
  • Collection의 결합에 "+" 사용가능
  • "=="으로 쓰면 알아서 null에 안전하게 .equals를 호출
  • 파라미터가 1개일때 쓸 수 있는 축약기호 ( Scala의 "_"이나 Groovy의 it)
  • Java에는 익명함수를 쓰기 위해서 Stream, Collection간이 상호변환이 빈번히 필요한데, Groovy의 List나 Scala의 Buffer 등에는 별다른 변환이 없이 바로 익명함수를 적용 가능 ( 심지어 java.util.List로 변환할 때도 Groovy의 List는 java.util.List의 구현체라서 별다른 변환이 필요없고, Scala는 'import scala.collection.JavaConversions._' 선언을 이용한 암묵적 변환으로 매끄럽게 이어짐. )

Java와 다른 발전된 JVM언어와 결정적인 격차는 과거 Collection interface와는 별도로 Stream인터페이스를 도입했다는 점과 연산자 오버로딩을 지원하지 않는다는 점에서 생기는것 같습니다. java8에서 디폴트 메서드를을 통해서 인터페이스의 하위호환성을 유지하면서 기능을 더할만한 여지가 많아지긴 했지만, 그래도 Collection을 전면 개조하기는 부담스럽기 때문에, Stream 도입은 레가시를 이끌어가야하는 Java의 어쩔수 없는 선택인듯합니다.

Groovy에 it같은건 java에도 언젠가 도입이 되지 않을까 싶어요. Kotlin같은 다른 JVM 언어에도 있는거라서요.

그런데 연산자 오버로딩이 안 되는건 꼭 단점만은 아니라고 저는 생각하긴 하는데, 아마 제가 C++의 연산자 오버로딩이 굉장히 부정적인 평가를 받았을때 Java를 배우고, 20대를 보냈기 때문에 그런게 아닌가 싶어요; 형님처럼 40대에도 열린 생각을 가진 개발자가 되어야겠어요 ^^;

얼마전에 JVM의 여러 언어로 filtering, sorting, mapping 수행하는 클래스를 만들어보면서 많은걸 느꼈는데, 예제는 아래에 있어요.

2015-02-14 08:29

@benelog 정리 잘 했다. 좋은 글이다.

과거 네이버에서 SPS 관련 문서를 작성할 때도 그렇고, 이번 신입사원 교육에서 답변을 달 때도 느끼는 것이지만 너는 한 가지 사항에 대해 다양한 가능성을 제시하는 능력이 뛰어나다는 생각을 종종 한다. 지금까지의 역사를 거슬러 올라가면서 다양한 내용들, 그와 관련한 링크들을 어떻게 기억하고 관리하는지가 궁금해질 정도이다. 본인이 어떻게 이런 정보들을 관리하는지 공유해 주는 것도 주변 개발자들에게도 많은 도움이 되겠다.

2015-02-14 10:37

안녕하세요, @차민창 형이 링크를 공유해주셨는데 재밌어 보여서 저도 한번 Javascript로 비슷한 동작을 하는 코드를 구현해보았습니다. 함수형 구현을 도와주는 Underscore를 사용하였고, (본 코드의 목적과는 다른 방향으로 구현이 되었을지도 모르지만,) 선언을 제외한 구현부를 한줄로 구현해보는 것을 목적으로 만들어 보았습니다 :)

var _ = require("underscore");
var sessions = ["10:00", "10:03", "10:10"];


console.log("sessions : ",
  _.range(10)
  .map(function (it) {
    return "10:0" + it;
  })
  .filter(function (session) {
    return !_.contains(sessions, session);
  })
  .concat(sessions)
  .sort()
  .map(function(session) {
    return {
      sessionTime  : session
    };
  })
);
2015-02-15 01:22

@조밍

Java도 같은 방식으로는 아래와 같이~

	@Test
	public void transform() {
		List<String> sessions = Arrays.asList("10:00", "10:03","10:10");


		logger.debug("sessions : {}",
				Stream.concat(
					IntStream.range(0, 10).boxed()
						.map(i -> "10:0" + i)
						.filter(t -> ! sessions.contains(t)) ,
					sessions.stream()
				)
				.sorted()
				.map(Session::new)
				.collect(Collectors.toList())
		);
	}


	private class Session {
		private String sessionTime; // getter등 생략
		Session(String sessionTime) {
			this.sessionTime = sessionTime;
		}
		@Override
		public String toString() {
			return "Session [sessionTime=" + sessionTime + "]";
		} 
	}

그런데 그 코드의 영감(?)이 된 신입 프로젝트에서는 Session객체에는 sessionTime 외에도 다양한 속성이 있었고, DB에서 읽어온 List을 바탕으로 빈 시간대를 채워야했기 때문에 단순하게 Collection.contains를 호출하거나 Collection.contains를 쓰기위해 Session.equals를 sessionTime를 비교하는 것만으로 재정의할 수는 없었던 것 같아요.

2015-02-15 01:45

@자바지기

체계화해서 검색 가능한곳에 넣어두려고 하는데, 다른 많은 개발자들이 하는것처럼, 위키나 웹페이지에 정리하는 것 이상의 특별한 건 없는것 같아요. 재성형님이나 권남님등이 더 정리를 잘 하시는듯..

sprignote 이후로는 서비스가 언제든지 망할수 있다는 생각에 파일 기반으로도 많이 정리하려고하는데, 그래서 근래에는 게시판에 올린것도 markdown파일로도 많이 남겨두고 있어요. local에서 검색할때는 요즘은 ack ( http://beyondgrep.com/ )를 애용하고 있고요.

개인적인 메모는 Markdown + Dropbox 조합으로 많이 시도해봤었어요 ( https://gist.github.com/benelog/3194442 에서 설명한 작업도 그 중 일부.. ) 이 밖에 다양한 도구와 방식을 계속 해보고 있어요. Evernote, getpocket, gist 등 이것저것 다 써보고는있네요. 목적별로 도구를 구분하려고는 하지만, 어디에 두었더라.. 하고 헷갈리는 경우도 많아요 ^^:

뭐 늘 고민하는 주제인데, 새로운 정보를 받아들일때 기존 정보와의 연결고리를 여러 경로로 이어 보려 하고, 언젠가는 그 정보가 포함된 어떤 주제로 남에게 설명을 하거나 글을 쓸 수도 있다는 점을 의식하면 기록이나 기억에 도움이 되었던것 같아요.

의견 추가하기

연관태그

← 목록으로