과거의 기록

Java8 Stream과 기존 코드 작성시 성능 비교 연구

이병록 2016. 12. 3. 07:23

이 연구는 Java8의 Stream Api를 공부하는 도중에 단순 연산과 Stream Api 사용했을 때 성능비교에 대한 호기심으로 시작되었다.

  

 

1. 개요

 

최초 items.stream().map(Item::getNum).reduce(0, Integer::sum);을 실행했을 때, 실행결과 시간은 얼마나 걸릴까 하고 측정하고, 단순 for문으로 계산했을 때의 시간은 얼마나 걸릴까 측정 후 결과 시간이 현저히 차이나는 것을 보고 의문을 가지게 되었다.

  

부족한 실력과 내공으로 이런 글을 쓰는 것도 웃기지만 배우는 자세로 도전해 보았다.

  

유지보수, 생산성 VS 성능

  

곰곰이 생각해볼 문제이다.

  

먼저 프로그램을 실행하기 위한 하드웨어 스펙과, 소프트웨어 환경은 이렇다.

CPU : Intel(R) Core(TM) i5-4200U CPU @ 1.60GHz 2.30 GHz

RAM : 8.00GB

OS : Windows 7(64bit)

JDK : 1.8.0_20 (Server VM)

  

JVM의 태그는 생략했다.

  

*먼저 필자는 퍼포먼스 개발자가 아니며 평범한 인간이다, 또한 이 연구는 전문적인 측정 방식은 아니며, 단순 실행결과만 비교하였다.

  

*그리고 디자인패턴을 제외한 단순 계산 메서드만 만들었다. 디자인 패턴을 사용하여 코딩하려다보니 Stream 구조와 비슷하게 만들어질 것 같아서 제외하였다.

  

*가능하면 추상화를 안했다. 그 이유는 추상화 하려다보니 결국엔 함수형 디스크립터와 다를게 없다고 느껴졌었다. 

  

*결과 값은 밀리세컨드이다.

 

 

 


2. 준비물

1) Student Object

public class Student {  
private final int studentNum; 
private final String studentName; 
private final int studentGrade; 
private final String studentCollege; 
private final Sex studentSex;  

public enum Sex {  MALE, FEMALE }  

public Student(int studentNum, String studentName, int studentGrade, String studentCollege, Sex studentSex) {  
    this.studentNum = studentNum;  
    this.studentName = studentName;  
    this.studentGrade = studentGrade;  
    this.studentCollege = studentCollege;  
    this.studentSex = studentSex; 
}  

public int getStudentNum() {  
    return studentNum; 
}      

public String getStudentName() {  
    return studentName; 
}      

public int getStudentGrade() {  
    return studentGrade; 
}      

public String getStudentCollege() {  
    return studentCollege; 
}      

public Sex getStudentSex() {  
    return studentSex; 
}
 

 

2) main

public class Main {
    public static void main(String[] args) {
        List<Student> students = new ArrayList();

        students.add(new Student(1, "roka", 4, "경영학", Student.Sex.MALE, 90));
        students.add(new Student(2, "roka1", 3, "컴퓨터공학", Student.Sex.FEMALE, 20));
        students.add(new Student(3, "roka2", 2, "세무학", Student.Sex.MALE, 40));
        students.add(new Student(4, "roka3", 1, "유아교육학", Student.Sex.MALE, 42));
        students.add(new Student(5, "roka4", 4, "국제무역학", Student.Sex.MALE, 60));
        students.add(new Student(6, "roka5", 4, "부동산학", Student.Sex.FEMALE, 100));
        students.add(new Student(7, "roka6", 2, "건축학", Student.Sex.MALE, 15));
        students.add(new Student(8, "roka7", 1, "수학", Student.Sex.FEMALE, 85));
        students.add(new Student(9, "roka8", 1, "경영학", Student.Sex.MALE, 60));
        students.add(new Student(10, "roka9", 3, "수학", Student.Sex.FEMALE, 92));
        students.add(new Student(11, "roka10", 4, "국제무역학", Student.Sex.MALE, 75));
        students.add(new Student(12, "roka11", 2, "유아교육학", Student.Sex.MALE, 64));
        students.add(new Student(13, "roka12", 2, "세무학", Student.Sex.FEMALE, 44));
        students.add(new Student(14, "roka13", 3, "경영학", Student.Sex.MALE, 10));

    }
}

 

 

3) StudentFunction

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

public class StudentFunction {

    
    public static List<Student> findNamesFilter(List<Student> list, String find) {
        // TODO : Student 리스트를 받아와서 find(이름) 조건에 맞는 Student 리스트를 반환한다.
        List<Student> results = new ArrayList();
        // 굳이 Iterator<E> 화 하지않았다.
        for (Student students : list) 
            if (students.getStudentName().equals(find))
                results.add(students);
        return results;
    }
    
    public static List<String> findNameMap(List<Student> list) {
        // TODO : Student 리스트를 받아와서 find(성별) 조건에 맞는 Student 리스트를 반환한다.
        List<String> results = new ArrayList();
        // 굳이 Iterator<E> 화 하지않았다.
        for (Student students : list) 
            results.add(students.getStudentName());
        return results;
    }
    

    public static List<Student> findGradeFilter(List<Student> list, StudentFunctionInterface function) {
        // TODO : 고민하다가 결국엔 인터페이스 리턴.. Integer 조건을 지정하기위해..
        return function.map(list);
    }
    

    public static List<Student> findSexFilter(List<Student> list, Student.Sex sex) {
        // TODO : Student 리스트를 받아와서 find(성별) 조건에 맞는 Student 리스트를 반환한다.
        List<Student> results = new ArrayList();
        // 굳이 Iterator<E> 화 하지않았다.
        for (Student students : list) 
            if (students.getStudentSex() == sex)
                results.add(students);
        return results;
    }
    
    public static List<String> findSexMap(List<Student> list) {
        // TODO : Student 리스트를 받아와서 find(성별) 조건에 맞는 Student 리스트를 반환한다.
        List<String> results = new ArrayList();
        for (Student students : list) 
            results.add(students.getStudentName());
        return results;
    }
    
    public static List<Student> findCollegeFilter(List<Student> list, String college) {
        // TODO : Student 리스트를 받아와서 find(학부) 조건에 맞는 Student 리스트를 반환한다.
        List<Student> results = new ArrayList();
        // 굳이 Iterator<E> 화 하지않았다.
        for (Student students : list) 
            if (students.getStudentCollege().equals(college))
                results.add(students);
        return results;
    }
    
    public static List<String> findCollegeMap(List<Student> list) {
        // TODO : Student 리스트를 받아와서 find(학부) 리스트를 반환한다.
        List<String> results = new ArrayList();
        for (Student students : list) 
            results.add(students.getStudentCollege());
        return results;
    }
    
    public static List<String> distinctStr(List<String> list) {
        List<String> results= new ArrayList<String>(new HashSet<String>(list));
        return results;
    }
    
    public static List<Integer> distinctInteger(List<Integer> list) {
        List<Integer> results= new ArrayList<Integer>(new HashSet<Integer>(list));
        return results;
    }
    
    public static List<Student> findScoreFilter(List<Student> list, StudentFunctionInterface function) {
        return function.map(list);
    }
    
    public static List<Integer> findScoreMap(List<Student> list) {
        List<Integer> results = new ArrayList();
        // 굳이 Iterator<E> 화 하지않았다.
        for (Student students : list) 
            results.add(students.getScore());
        return results;
    }
    
    public static int sum(List<Integer> list) {
        int sum = 0;
        for (Integer integer : list)
            sum += integer;
        return sum;
    }
    
    public static int min(List<Integer> list) {
        int min = Integer.MAX_VALUE;
        if (list.size() == 0)
            throw new ArrayIndexOutOfBoundsException();
        for (Integer integer : list) {
            if (min > integer)
                min = integer;
        }
        return min;
    }
    
    public static int max(List<Integer> list) {
        int max = 0;
        for (Integer integer : list) {
            if (max < integer)
                max = integer;
        }
        return max;
    }
}
 

 

4) StudentFunctionInterface

import java.util.List;

public interface StudentFunctionInterface {
    public List<Student> map(List<Student> list);
}
 

 


 

 

3. 단순 비교 문제

 

1) 성별이 MALE인 학생의 이름 리스트를 반환하여라.

 

(1) 비교 코드

List<String> selectStudent = students.stream().filter(student -> student.getStudentSex()==Student.Sex.MALE).map(Student::getStudentName).collect(Collectors.toList()); 

List<String> selectStudent2 = StudentFunction.findNameMap(StudentFunction.findSexFilter(students, Student.Sex.MALE));

 

  

(2) 결과

 

Stream사용 걸린시간 : 61

[roka, roka2, roka3, roka4, roka6, roka8, roka10, roka11, roka13]

 

일반메서드 걸린시간 : 1

[roka, roka2, roka3, roka4, roka6, roka8, roka10, roka11, roka13]

  

 

 

2) 성별이 MALE이고 학년이 2학년 이상인 사람의 이름 리스트를 반환하여라.

 

(1) 비교 코드

List<String> selectStudent = students.stream().filter(s -> s.getStudentSex()==Student.Sex.MALE).filter(s -> s.getStudentGrade() >= 2).map(Student::getStudentName).collect(Collectors.toList());  

List<String> selectStudent2 = StudentFunction.findNameMap(StudentFunction.findGradeFilter(StudentFunction.findSexFilter(students, Student.Sex.MALE), 
    new StudentFunctionInterface() {
        public List<Student> map(List<Student> list) {// TODO Auto-generated method stub
            List<Student> result = new ArrayList();
            int i =0;
            for (Student student : list)
                if (student.getStudentGrade() >= 2)
                    result.add(student);return result;
        }
    }
));
 

 

  

(2) 결과

 

Stream사용 걸린시간 : 63

[roka, roka4, roka10, roka13]

 

일반 메서드 걸린시간 : 1

[roka, roka4, roka10, roka13]

  

 

 

3) 중복되지 않은 College의 리스트를 반환하여라.

 

(1) 비교코드

List<String> selectStudent = students.stream().map(Student::getStudentCollege).distinct().collect(Collectors.toList()); 

List<String> selectStudent2 = StudentFunction.distinctStr(StudentFunction.findCollegeMap(students));

 

 

 

 

(2) 결과

 

Stream사용 걸린시간 : 55

[경영학, 컴퓨터공학, 세무학, 유아교육학, 국제무역학, 부동산학, 건축학, 수학]

 

일반 메서드 걸린시간 : 1

[국제무역학, 컴퓨터공학, 세무학, 유아교육학, 경영학, 건축학, 수학, 부동산학]

  

 

 

 

4) 학생들의 점수를 모두 더하여라. ( 박싱, 언박싱 비용포함)

 

(1) 비교코드

int sum = students.stream().map(Student::getScore).reduce(0, Integer::sum); 

int sum2 = StudentFunction.sum(StudentFunction.findScoreMap(students));
 

 

  

(2) 결과

 

Stream사용 걸린시간 : 100

797

일반 메서드 걸린시간 : 1

797

  

(3) 심화

 

*케이스 10,000개

Stream사용 걸린시간 : 114

100000

일반 메서드 걸린시간 : 7

100000

  

*케이스 100,000개

Stream사용 걸린시간 : 134

1000000

일반 메서드 걸린시간 : 27

1000000

  

*케이스 1,000,000개

Stream사용 걸린시간 : 162

10000000

일반 메서드 걸린시간 : 44

10000000

  

 

5) 학생들의 점수중 최소값을 구하여라.

 

(1) 비교코드

 

Optional<Integer> min= students.stream().map(Student::getScore).reduce(Integer::min); 

int sum2 = StudentFunction.min(StudentFunction.findScoreMap(students));

 

 

(2) 결과

 

Stream사용 걸린시간 : 117

10

일반 메서드 걸린시간 : 1

10

  

 


  

4. 결론

  

1) 테스트 문제점

 

(1) 필자 내공의 부족

(2) 테스트 케이스가 다양하지 못한점

(3) 테스트 케이스의 개수

(4) 비 추상화

(5) Not 병렬

  

  

2) 후기

 

(1) 추상화 하는 만큼 비용이 들어갈 수 밖에 없는 것 같다. 많은 메서드를 스택에 쌓는 만큼 연산비용이 올라갈 수 밖에 없는 것 같다. 

(2) 코드가 자주 변할 일이 없으며, 레거시 코드에 대한 걱정이 없을 만한 단순 연산은 Stream을 굳이 사용할 필요가 없을 것 같다. 

(3) 물론 Java8의 대부분 내장된 함수형 인터페이스로 인해 직접 인터페이스를 생성하지 않아도 되며, 생산성 측면이나 코드 관리, 가독성 부분에선 확실히 Java8이 강력하다. 데이터베이스 쿼리마냥 코드를 작성할 수 있다는 것은 굉장하다.

(4) 생산성, 유지보수 VS 성능, 현업에서 개발로만 봤을 때 생산성과 유지보수는 절대로 무시 못한다. 다만 앞에서 남겼듯이 단순 연산 부분이라면 직접 만드는 것이 나을 것 같다. 밸런스를 잘 맞춰야 겠다.

(5) JVM단에서 Stream API를 성능을 얼마나 받춰 주는지 좀 더 연구를 해봐야할 것 같다.

  

  

3) 최종 결론

(1) 생산성, 유지보수, 성능에 대한 밸런스를 각 개발 상황에 따라 잘 맞춰야겠다.

 

(2) 좀 더 시간을 들여서 연구를 해볼 가치가 있다.