본문 바로가기
  • 냥냥냥
Java

Setter Vs Builder 속도 차이

by 프로그래밍데 2025. 6. 23.

하도 Setter와 Builder의 차이점을 많이 봐서

글을 읽어보다가 문득 속도 측면에서는 Setter가 더 우수하다는 글을 보게 되어

그런가 ? 하고 글을 쓰러 왔습니다

그것은 바로 빌더가 생성방식에 있어 조금은 더 비효율적이라는 것인디요 ? 

 

1. 빌더 생성방식

=>주로 롬복 어노테이션으로 builder를 많이 쓰기 때문에

어노테이션 미사용시 어떤 식으로 Builder가 객체를 생성하는지 가시적으로 보이지 않습니다.

public class User {
  private final String name;
  private final String email;
  private final int age;

private User(Builder builder) {
  this.name = builder.name;
  this.email = builder.email;
  this.age = builder.age;
}

//1. 첫번째 호출
 public static Builder builder() {
   return new Builder();
 }

//2. 두번째 호출
 public static class Builder {
   private String name;
   private String email;
   private int age;

   public Builder name(String name) {
     this.name = name;
     return this;
   }

   public Builder email(String email) {
     this.email = email;
     return this;
   }

   public Builder age(int age) {
     this.age = age;
     return this;
   }

   //3. 세번째 호출
   public User build() {
     return new User(this);
   }
  }
 }

=> 3개의 필드에 대해서 Builder는 내부적으로

  • User.builder() 호출 → 내부 Builder 인스턴스 생성
  • Builder의 각 Setter 메서드 호출 → Builder 인스턴스 필드 값 설정
  • build() 호출 → Builder 내부 필드를 사용해 실제 User 객체 생성 (생성자 호출)

 

2. Setter 생성방식

User user = new User();
user.setName("밍데");
user.setEmail("programmingde00@gmail.com");
user.setAge(30);

 

3. 둘의 성능 차이 원인

 

                                                                   Builder 방식                                                                      Setter 방식

객체 생성 횟수 Builder 객체 + 최종 객체,총 2개 생성 최종 객체 1개만 생성
메서드 호출 횟수 Builder의 각 Setter 메서드+ 최종 객체 생성자 호출 객체의 Setter 메서드만 호출
추가 작업 Builder 내부에 임시 필드 저장,검증 로직 포함 가능 직접 필드 수정
  • Builder는 Builder라는 별도 객체 생성 비용이 추가
  • Builder 내부에 임시 상태를 저장하고, 최종 객체 생성 시 복사(또는 초기화)하는 비용 발생
  • Setter 방식은 한 객체에서 바로 필드 값 수정 → 상대적으로 가볍다

 

4. 실제 차이 측정

(JMH까지도 굳이라고 생각이 들어 nanoTime으로 측정했습니다~~[그랬었습니다]~~)

100만번 실행 기준

네 ?

몇 번을 해도 setter가 더 빠르다고 나와서

어쩔 수 없이 JMH를 선택 ㅋㅋ

 

build.gradle 추가

implementation 'org.openjdk.jmh:jmh-core:1.37'
annotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37'

 

@BenchmarkMode(Mode.Throughput)            // 초당 수행 횟수 측정
@OutputTimeUnit(TimeUnit.MILLISECONDS)    // ms 단위로 결과 출력
@Warmup(iterations = 5)                   // JVM 최적화를 위한 워밍업 5회
@Measurement(iterations = 10)              // 실제 측정 10회
@Fork(1)                                 // 별도 JVM 프로세스 1개로 실행
@State(Scope.Thread)                      // 각 스레드 별로 독립 상태 유지
public class UserBenchmarkJMH {

    @Benchmark
    public Users setterBenchmark() {
        Users user = new Users();
        user.setName("밍데");
        user.setEmail("ming@example.com");
        user.setAge(26);
        return user;
    }

    @Benchmark
    public Users builderBenchmark() {
        return Users.builder()
                .name("밍데")
                .email("ming@example.com")
                .age(26)
                .build();
    }
}

class Users {
    private String name;
    private String email;
    private int age;

    public Users() {}

    public void setName(String name) {
        this.name = name;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public void setAge(int age) {
        this.age = age;
    }

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {
        private String name;
        private String email;
        private int age;

        public Builder name(String name) {
            this.name = name;
            return this;
        }
        public Builder email(String email) {
            this.email = email;
            return this;
        }
        public Builder age(int age) {
            this.age = age;
            return this;
        }

        public Users build() {
            Users user = new Users();
            user.setName(name);
            user.setEmail(email);
            user.setAge(age);
            return user;
        }
    }
}

 

 

 

=> 결과 

 

벤치마크 메서드                                                    평균 처리 속도 (ops/ms)                     표준편차 (±오차)            비고

setterBenchmark  185,283 ops/ms ±1,255 더 빠름
builderBenchmark 103,753 ops/ms ±5,519 느림

 

Fork: 1 of 1

warmup Iteration 1: 93688.550 ops/ms

Warmup Iteration 2: 95958.480 ops/ms

Warmup Iteration 3: 104892.051 ops/ms

Warmup Iteration 4: 105217.199 ops/ms

Warmup Iteration 5: 103969.217 ops/ms

=> 벤치마크 실행 전에 JVM이 코드를 최적화하도록 미리 여러 번 실행하는 거에여

JVM은 JIT컴파일을 통해 코드를 점점 빠르게 최적화 하기 때문에

초반에는 인터프리터 모드로 실행하여 느릴 수 있습니답.

여러번 실행하면서 컴파일된 네이티브 코드로 변경되고 속도가 좋아져

워밍업 단계는 실제 성능 측정에 포함하지 않고 최적화된 상태에서 측정합니당.

Iteration 1: 185374.472 ops/ms

Iteration 2: 185394.954 ops/ms

Iteration 3: 186239.171 ops/ms

Iteration 4: 186300.425 ops/ms

Iteration 5: 185463.879 ops/ms

Iteration 6: 185351.042 ops/ms

Iteration 7: 185706.609 ops/ms

Iteration 8: 184725.054 ops/ms

Iteration 9: 184872.722 ops/ms

Iteration 10: 183402.014 ops/ms

Result "어쩌구.어쩌구.어쩌구.UserBenchmarkJMH.setterBenchmark":

185283.034 ±(99.9%) 1255.322 ops/ms [Average]

(min, avg, max) = (183402.014, 185283.034, 186300.425), stdev = 830.318

CI (99.9%): [184027.712, 186538.356] (assumes normal distribution)
Run complete. Total time: 00:05:02
REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on

why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial

experiments, perform baseline and negative tests that provide experimental control, make sure

the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.

Do not assume the numbers tell you what you want them to tell.

Benchmark Mode Cnt Score Error Units

UserBenchmarkJMH.builderBenchmark thrpt 10 103753.332 ± 5519.483 ops/ms

UserBenchmarkJMH.setterBenchmark thrpt 10 185283.034 ± 1255.322 ops/ms

Process finished with exit code 0

 

 

setterBenchmark

  • 1ms당 약 185,283번 실행됨.
  • 최소: 183,402 / 최대: 186,300
  • 오차범위(99.9% 신뢰수준): ±1,255 → 매우 안정적

➡ 정확도, 일관성, 속도 모두 우수함

➡ 단순 setter 방식이 JIT에 최적화되면서 빠르게 처리됨

 

builderBenchmark

  • 1ms당 약 103,753번 실행됨.
  • 오차 ±5,519 → 편차가 조금 큼 (변동성이 존재)
  • 최대 약 110,000, 최소 약 98,000선 추정 가능

➡ 상대적으로 느리고, 편차도 더 큼

➡ 내부적으로 Builder 객체 생성 + 체이닝 + build() 과정이 많아 약간의 오버헤드 발생

Setter 방식이 Builder보다 약 1.78배 빠름

185,283 ÷ 103,753 ≒ 1.78

즉, 성능 기준으로만 보면 Setter가 약 78% 더 빠름.

헉 78%? 이 정도 성능 차이가 실제 개발에 미치는 영향이라면

  • 대부분 애플리케이션에서 객체 생성 비용은 전체 처리 시간에서 극히 미미
  • 대부분의 일반적인 웹 백엔드 실무에서는 전혀 영향 없음.
    즉, DTO 객체를 만들 때 Setter가 Builder보다 1.7배 빠르다고 해도,성능 병목은
    대부분 I/O, DB, 네트워크, GC 등에서 발생하기 때문에이 정도 수준의 연산 속도 차이는 무시해도 된다고 본다.

 

사실 정량적으로 따져보면

  • Builder 방식: 약 100,000 ops/ms = 1억 개 객체를 약 1초에 생성
  • Setter 방식: 약 180,000 ops/ms = 1억 개 객체를 약 0.55초에 생성

=> 객체를 1억개 이상 생성할 게 아니라면 체감 불가

 

성능보다 중요한 것은 .. .

가독성, 유지보수성, 불변성, 객체 생성의 명확함 등등이다. ㅎ

결국은 그렇습니다. 그러니 저는 걱정하지 않고 Builder를 사용할 것인데

간단하게 setter와 Builder 중에서 Builder를 선호하는 이유를 좀 적어보고 끝내겠습니답.

 

 

5. Builder가 좋은 이유

1. 불변 객체 (Immutable Object)를 만들 수 있다 (final 키워드 사용시)

→ @Setter는 외부에서 값을 바꿔버릴 수 있다는 게 가장 큰 단점입니다.

→ Side Effect 위험이 있기 때문이져

Setter 기반 예시:

User user = new User(); 

user.setName("밍데"); 
user.setEmail("programmingde00@gmail.com"); 
user.setAge(26);  // 놓치면 NPE or 잘못된 객체 

user.setName("한선");
user.setName("샛별");
user.setName("성빈");

 

다음과 같이 기재될 경우 객체 상태는 Heap영역에 적재가 됩니다.

Heap 영역

+------------------------------------------------------------------+

| User 객체                                                                |

| - name: 참조 -> "밍데"                                             |

| - email: 참조 -> "programmingde00@gmail.com"  |

| - age: 86                                                                 |

+-----------------------------------------------------------------+

  • User 객체는 JVM Heap 영역에 할당됨
  • 각 필드는 문자열 객체(또는 기본형 값)를 참조함

단일 스레드에서 Setter 호출 순서

Thread 1 실행 흐름
User user = new User();

user.setName("밍데");
  └─> Heap의 user.name 필드가 "밍데"를 가리킴

user.setName("한선");
  └─> Heap의 user.name 필드가 "한선"을 가리킴

user.setName("샛별");
  └─> Heap의 user.name 필드가 "샛별"을 가리킴

user.setName("성빈");
  └─> Heap의 user.name 필드가 "성빈"을 가리킴
  • 한 스레드가 순차적으로 name 값을 변경
  • Heap의 name 필드는 계속 덮어쓰임 → 마지막 값이 최종 상태

멀티스레드 동시 실행 시 문제 발생 예시

Thread 1                                      Thread 2

user.setName("한선");                          user.setName("샛별");

Heap user.name 필드 → "한선"                   Heap user.name 필드 → "샛별" (덮어쓰기)

 

실행 순서 및 타이밍에 따라 결과가 불확실 !!!!!!!!!!!!

  • 두 스레드가 거의 동시에 setName() 호출
  • Heap 내 user.name 필드 참조가 경쟁 상태
  • 어느 스레드가 마지막에 값을 덮어쓸지 예측 불가
  • 메모리 가시성 문제로 한 스레드가 다른 스레드 변경을 즉시 못 볼 수도 있음

=> 따라서 회원 정보 수정 같은 경우에는 Setter를 지양하는 게 확실히 좋다..

이 외에도

  • 필드 누락 가능성 있음
  • 순서 헷갈릴 수 있음
  • 객체 생성과 설정이 분리돼 의도가 불분명해짐

 

 

Builder 기반 예시:

User user = User.builder()     
      .name("밍데")     
      .email("programmingde00@gmail.com")     
      .age(26)     
      .build();
  • 어떤 값을 넣었는지 명확
  • 중간에 빠진 필드도 가시적으로 보임
  • 한 번에 객체 완성 → 불변 객체화 용이

=> 그치만 알아야 할 점은, Builder 역시 내부 필드값을 final로 선언하지 않으면

User user = User.builder()
      .name("밍데")
      .email("programmingde00@gmail.com")
      .age(26)
      .build();

user.setAge(86);

=> 이렇게 값을 변경할 수 있다.

     따라서 불변을 보장하지는 않는다.

 

그럼에도 불구하고, 다음의 이유로 final 키워드 없이도 Builder를 많이 쓰고 나도 Builder를 좋아한다.

 

 

2. 가독성 + 유지보수성이 높다

  • 특히 필드가 많아질수록 가독성 차이가 매우 큼

 

예: 생성자가 많은 경우

User user = 
    new User("밍데, 
             "programmingde00@gmail.com", 
             26, 
             "서울", 
             "010-1234-5678", 
             "직장인", 
             "여성", 
             "2000-02-16");
  • 필드 순서 바뀌면 대참사
  • IDE 도움 없이는 필드가 어떤걸 의미하는지 파악 어려움
  • 지금 쉬운 값이라서 그렇지 User 객체 들어가서 순서 일치하게 필드값 확인 안하면 뭔지 모름.

→ Builder는 순서 신경 안 써도 되고, 명확하게 작성 가능

User.builder()     
    .name("밍데")     
    .email("programmingde00@gmail.com")     
    .age(26)     
    .city("서울")     
    .phone("010-1234-5678")     
    .job("직장인")     
    .gender("여성")     
    .birth("2000-02-16")     
    .build();

 

3. 필드 일부만 설정해도 된다

→ @Builder는 선택적 파라미터 설정이 가능

User.builder()
   .name("밍데")
   .build();  // 일부만 채워도 유효

 

반면

@AllArgsConstructor는 모든 필드를 넣어야 함

@Setter는 순서 없이 여러 번 호출해야 함

 

 

4. 불변 객체 + 스레드 안전한 객체 작성에 유리

  • 상태 변화가 없으므로 멀티스레드 환경에서 안전

=> 이는 위에서 언급했듯,

final 키워드로 사용시에 해당이다.

public class User {
  private final String mbrNm;
  private final String phonNo;
}

 

그리고 애초에 final 키워드로 필드를 작성시

setter는 사용할 수가 없다.

따라서 생성자 생성법이나 Builder 생성법 2개만 사용이 가능해진다.

그리고 킹갓 김영환 배민 개발자선생님도

DTO에서는 애초에 값을 전달하기 위한 class이기 때문에 setter를 사용해도 된다고 말했다.

Entity에서 지양하자 !!

 

 

6. 한 줄 요약을 하자면

 

@Builder는

  1. Final 키워드와 혼재 사용할 경우 명확하고 안전한 불변 객체 생성 방식이고,
  2. Final 키워드 없이 DTO에 사용할 경우 명확하고 읽기 좋은 객체 생성 방식

@Setter 는 임의의 상태 변경을 허용하는 유연한 방식이다.

 

 

 

즉,

불변이냐 가변이냐 or 가독성 의 차이입니다.

사실 생성자 방식을 권장하는 것도, 불변성 때문인데

Builder에 final 같이 병행하면 난 가독성도 챙기고 너무 좋은 거 같다.

최근댓글

최근글

skin by © 2024 ttuttak