개발/개발관련

[개발관련]JAVA_ Getter/Setter vs Field Direct Access 비교

mabb 2024. 5. 17. 00:21
반응형

 회사에서 필드의 접근제한자를 private 가 아닌 public으로 사용하고, Getter/Setter 없이 직접 필드에 접근하여 연산처리하는 소스를 만났다. 아무래도 Getter/Setter 메서드를 호출할 때마다 Call Stack에 메서드가 쌓였다가 사라질 테니 필드에 직접 접근하여 연산하는 것보다 성능이 좋지 않을 것이라고 생각했다. 하지만, 은닉, 캡슐화를 위반하는 듯하여 이를 Getter/Setter로 변경하였을 때 성능이 얼마나 안 좋아질지 시간을 측정해보고자 하였다.
대략 간단하게
필드 직접 접근 10억번 반복 vs Getter/Setter 10억 번 반복을 비교해 보았다.

package practice;

public class GetterDirect {

    public static void main(String[] args) {
        Test t1 = new Test();
        long s1 = System.currentTimeMillis();
        for(int i=0; i<1_000_000_000; i++){
            t1.a += t1.a;
            t1.a -= t1.a;
            t1.a *= t1.a;
            t1.a -= t1.a;
        }
        System.out.println("t1.a = " + t1.a);
        long e1 = System.currentTimeMillis();
        System.out.println("(e1-s1) = " + (e1-s1));

        Test t2 = new Test();
        long s2 = System.currentTimeMillis();
        for(int i=0; i<1_000_000_000; i++){
            t2.setB(t2.getB()+t2.getB());
            t2.setB(t2.getB()-t2.getB());
            t2.setB(t2.getB()*t2.getB());
            t2.setB(t2.getB()-t2.getB());
        }
        System.out.println("t2.getB() = " + t2.getB());
        long e2 = System.currentTimeMillis();
        System.out.println("(e2-s2) = " + (e2-s2));
    }
    static class Test{
        public int a;
        private int b;

        public void setB(int b){
            this.b = b;
        }
        public int getB(){
            return b;
        }
    }
}

 
결과는 다음과 같았다.

어째서 게터세터를 사용하는 소스가 더 빠른 것일까.

Getter/Setter를 사용하는 소스가 3ms 빠르다.
어째서 게터세터를 사용하는 소스가 더 빠른 것일까.
뭔가 소스의 순서에 영향이 있을 것 같아서 순서를 바꿔보았다.

package practice;

public class GetterDirect {


    public static void main(String[] args) {
        Test t2 = new Test();
        long s2 = System.currentTimeMillis();
        for(int i=0; i<1_000_000_000; i++){
            t2.setB(t2.getB()+t2.getB());
            t2.setB(t2.getB()-t2.getB());
            t2.setB(t2.getB()*t2.getB());
            t2.setB(t2.getB()-t2.getB());
        }
        System.out.println("t2.getB() = " + t2.getB());
        long e2 = System.currentTimeMillis();
        System.out.println("(e2-s2) = " + (e2-s2));

        Test t1 = new Test();
        long s1 = System.currentTimeMillis();
        for(int i=0; i<1_000_000_000; i++){
            t1.a += t1.a;
            t1.a -= t1.a;
            t1.a *= t1.a;
            t1.a -= t1.a;
        }
        System.out.println("t1.a = " + t1.a);
        long e1 = System.currentTimeMillis();
        System.out.println("(e1-s1) = " + (e1-s1));

    }
    static class Test{
        public int a;
        private int b;

        public void setB(int b){
            this.b = b;
        }
        public int getB(){
            return b;
        }


    }
}

 
그 결과는 다음과 같았다.

이번에는 Getter/Setter를 사용하는 10억번의 연산이 약 12ms 느리다.
 
연산을 많이 넣어 보았다.

package practice;

public class GetterDirect {


    public static void main(String[] args) {
        Test t2 = new Test();
        long s2 = System.currentTimeMillis();
        for(int i=0; i<1_000_000_000; i++){
            t2.setB(t2.getB()+t2.getB());
            t2.setB(t2.getB()-t2.getB());
            t2.setB(t2.getB()*t2.getB());
            t2.setB(t2.getB()-t2.getB());
            t2.setB(t2.getB()+t2.getB());
            t2.setB(t2.getB()-t2.getB());
            t2.setB(t2.getB()*t2.getB());
            t2.setB(t2.getB()-t2.getB());
            t2.setB(t2.getB()+t2.getB());
            t2.setB(t2.getB()-t2.getB());
            t2.setB(t2.getB()*t2.getB());
            t2.setB(t2.getB()-t2.getB());
            t2.setB(t2.getB()+t2.getB());
            t2.setB(t2.getB()-t2.getB());
            t2.setB(t2.getB()*t2.getB());
            t2.setB(t2.getB()-t2.getB());
            t2.setB(t2.getB()+t2.getB());
            t2.setB(t2.getB()-t2.getB());
            t2.setB(t2.getB()*t2.getB());
            t2.setB(t2.getB()-t2.getB());
            t2.setB(t2.getB()+t2.getB());
            t2.setB(t2.getB()-t2.getB());
            t2.setB(t2.getB()*t2.getB());
            t2.setB(t2.getB()-t2.getB());
        }
        System.out.println("t2.getB() = " + t2.getB());
        long e2 = System.currentTimeMillis();
        System.out.println("(e2-s2) = " + (e2-s2));

        Test t1 = new Test();
        long s1 = System.currentTimeMillis();
        for(int i=0; i<1_000_000_000; i++){
            t1.a += t1.a;
            t1.a -= t1.a;
            t1.a *= t1.a;
            t1.a -= t1.a;
            t1.a += t1.a;
            t1.a -= t1.a;
            t1.a *= t1.a;
            t1.a -= t1.a;
            t1.a += t1.a;
            t1.a -= t1.a;
            t1.a *= t1.a;
            t1.a -= t1.a;
            t1.a += t1.a;
            t1.a -= t1.a;
            t1.a *= t1.a;
            t1.a -= t1.a;
            t1.a += t1.a;
            t1.a -= t1.a;
            t1.a *= t1.a;
            t1.a -= t1.a;
            t1.a += t1.a;
            t1.a -= t1.a;
            t1.a *= t1.a;
            t1.a -= t1.a;
        }
        System.out.println("t1.a = " + t1.a);
        long e1 = System.currentTimeMillis();
        System.out.println("(e1-s1) = " + (e1-s1));

    }
    static class Test{
        public int a;
        private int b;

        public void setB(int b){
            this.b = b;
        }
        public int getB(){
            return b;
        }


    }
}

 
그 결과는 다음과 같다.
Getter/Setter 가 65ms 느리다.

마지막 테스트에서는 Getter/Setter 를 총 720억 번 호출한다. 이 정도 연산량에 65ms이면 작은 것인가 큰 것인가. 성능이 조금이라도 빠른 것이 중요하다면 필드 직접 접근을 사용하는 것도 나쁘지 않을 것 같다.
같은 연산인데 소스의 순서에 따라 먼저 실행될 때 더 느린 이유는 무엇일까? JVM이 무언가 하고 있는 것일까.
마지막 테스트에서 또다시 순서를 바꿔서 필드 직접 접근 소스를 먼저 실행되도록 한 결과는 아래와 같다.

 
 
GPT 문의

간단한 소스임에도 성능 결과를 명확하게 이해하고 왜 그런 것인지 설명하기 어렵다. GPT 답변을 참고하여 JVM동작 원리 등을 공부해야겠다.
 
//todo:  소스 순서에 따라 성능 측정에 차이가 생기는 이유 찾기.

https://chat.openai.com/share/d3b2408b-8b20-48da-9f8c-f312bffc90ff

ChatGPT

A conversational AI system that listens, learns, and challenges

chat.openai.com

지피티 선생님의 답변으로 키워드를 얻을 수 있었다.

JIT최적화

나중에 실행되는 코드가 더 빠른 이유이다.
테스트 시에는 JIT최적화를 염두에 두고 진행해야 한다고 한다.

반응형