프로그래밍 PROGRAMMING/자바 JAVA AND FRAMEWORKS

[JAVA] Java 생성자 사용법: 올바른 설계와 실전 활용법

매운할라피뇨 2026. 2. 13. 08:11
반응형
Java 생성자 사용법

 

개요

Java 개발자는 생성자를 매일 사용하지만, 대부분 제대로 설계하지 못합니다. 많은 코드에서는 수십 개의 파라미터를 받는 생성자, 중복된 초기화 로직, IDE 자동 생성 코드에 의존하는 모습을 봅니다. 이런 설계는 코드 가독성을 떨어뜨리고, 유지보수를 어렵게 만듭니다.
생성자 설계의 흔한 문제:

  • 파라미터가 10개 이상인 생성자 (객체 생성 시 순서 실수 위험)
  • 중복된 초기화 로직으로 인한 일관성 문제
  • null 체크 누락으로 인한 NullPointerException
  • 선택적 파라미터 처리를 위한 여러 오버로드 생성자 (explosion)

이 가이드에서는 생성자 체이닝, Builder 패턴, 팩토리 메서드, 불변성 보장 등 생성자 설계의 모범 사례를 실전 코드로 배웁니다. 각 패턴의 장단점을 이해하면, 프로젝트에 맞는 최적의 생성자 설계를 할 수 있습니다.


1. 생성자 설계의 핵심 원칙

파라미터 개수 제한과 가독성

생성자 설계의 첫 번째 원칙은 파라미터 개수를 제한하는 것입니다. 파라미터가 많을수록 다음과 같은 문제가 발생합니다:

  1. 파라미터 순서 실수: IDE 자동완성에 의존하면 순서를 헷갈리기 쉬움
  2. 코드 가독성 저하: 각 파라미터의 의미를 파악하기 어려움
  3. 선택적 파라미터 처리 복잡: 생성자 오버로드가 기하급수적으로 증가
  4. 유지보수 어려움: 새 필드 추가 시 모든 생성자를 수정해야 함

생성자가 받는 파라미터가 많을수록 사용하기 어렵습니다. 일반적으로 파라미터 3개 이상이면 Builder 패턴을 고려해야 합니다.
변경전: 너무 많은 파라미터

// ❌ 파라미터가 8개! 순서를 기억하기 어려움
public class User {
    private String name;
    private String email;
    private String phone;
    private String address;
    private String city;
    private String country;
    private int age;
    private boolean active;

    public User(String name, String email, String phone, String address,
                String city, String country, int age, boolean active) {
        this.name = name;
        this.email = email;
        this.phone = phone;
        this.address = address;
        this.city = city;
        this.country = country;
        this.age = age;
        this.active = active;
    }
}

// 사용 시:
User user = new User("Alice", "alice@example.com", "010-1234-5678",
                     "Seoul", "Gangnam-gu", "Korea", 25, true);
// 파라미터 순서 헷갈림! age와 city 순서를 헷갈리기 쉬움

변경후: Builder 패턴으로 가독성 확보

// ✅ 각 필드의 의미가 명확함
User user = User.builder()
    .name("Alice")
    .email("alice@example.com")
    .phone("010-1234-5678")
    .address("Seoul, Gangnam-gu, Korea")
    .age(25)
    .active(true)
    .build();

// 또는 필수 파라미터만 생성자에, 나머지는 setter
User user = new User("Alice", "alice@example.com");
user.setPhone("010-1234-5678");

Output:

✅ 코드 가독성 향상: 각 파라미터의 의미 명확
✅ 파라미터 순서 실수 제거
✅ 선택적 파라미터 처리 간단

2. 생성자 체이닝과 this() 활용

중복 로직 제거하기

생성자 오버로드가 많을 때는 코드 중복이 심각해집니다. 특히 null 체크, 기본값 설정, 유효성 검증 같은 초기화 로직이 각 생성자마다 반복됩니다. 이때 this() 키워드를 사용하여 생성자 체이닝을 하면, 초기화 로직을 한 곳에 집중할 수 있고, 유지보수도 훨씬 쉬워집니다. 여러 개의 오버로드 생성자가 있을 때, this()를 사용하여 중복을 제거할 수 있습니다:

변경전: 각 생성자마다 중복된 초기화

public class Product {
    private String name;
    private double price;
    private int quantity;
    private boolean available;

    // 기본 생성자
    public Product() {
        this.name = "Unknown";
        this.price = 0.0;
        this.quantity = 0;
        this.available = false;  // 중복!
    }

    // 이름과 가격만 받는 생성자
    public Product(String name, double price) {
        this.name = name;
        this.price = price;
        this.quantity = 0;
        this.available = false;  // 중복!
    }

    // 모든 필드를 받는 생성자
    public Product(String name, double price, int quantity, boolean available) {
        this.name = name;
        this.price = price;
        this.quantity = quantity;
        this.available = available;
    }
}

변경후: this()로 생성자 체이닝

// ✅ 중복 제거, 유지보수 용이
public class Product {
    private String name;
    private double price;
    private int quantity;
    private boolean available;

    // 가장 완전한 생성자
    public Product(String name, double price, int quantity, boolean available) {
        this.name = name != null ? name : "Unknown";
        this.price = price > 0 ? price : 0.0;
        this.quantity = quantity > 0 ? quantity : 0;
        this.available = available;
    }

    // 이름과 가격만 받는 생성자
    public Product(String name, double price) {
        this(name, price, 0, false);  // ✅ this() 호출
    }

    // 기본 생성자
    public Product() {
        this("Unknown", 0.0);  // ✅ this() 호출
    }
}

// 사용:
Product p1 = new Product();  // Unknown, 0.0, 0, false
Product p2 = new Product("Apple", 1.5);  // Apple, 1.5, 0, false
Product p3 = new Product("Orange", 2.0, 10, true);  // 모든 필드 지정

Output:

✅ Product() called → this("Unknown", 0.0) → this(...) 전체 초기화
✅ 일관된 초기화 로직: null 체크, 범위 체크 한 곳에만 존재
✅ 유지보수 용이: 초기화 로직 변경 시 한 곳만 수정

3. Builder 패턴으로 유연한 객체 생성

선택적 파라미터와 불변성

데이터베이스 설정, API 클라이언트 옵션 같이 선택적인 파라미터가 많을 때는 Builder 패턴이 매우 효과적입니다. 생성자 오버로드로는 모든 조합을 커버할 수 없고, 코드도 복잡해집니다. Builder 패턴은 메서드 체이닝을 통해 필요한 필드만 설정할 수 있게 해주며, 동시에 생성된 객체를 불변으로 만들어 스레드 안전성까지 보장합니다. Builder 패턴은 파라미터가 많거나 선택적일 때 가장 효과적입니다:
변경전: 여러 오버로드 생성자 (생성자 폭발)

public class DatabaseConfig {
    private String host;
    private int port;
    private String username;
    private String password;
    private int connectionTimeout;
    private int queryTimeout;
    private boolean useSSL;

    public DatabaseConfig(String host, int port) { }
    public DatabaseConfig(String host, int port, String username) { }
    public DatabaseConfig(String host, int port, String username, String password) { }
    public DatabaseConfig(String host, int port, String username, String password, int connectionTimeout) { }
    // ... 계속 추가됨
}

변경후: Builder 패턴

// ✅ 체인 방식으로 필요한 필드만 설정
public class DatabaseConfig {
    private final String host;
    private final int port;
    private final String username;
    private final String password;
    private final int connectionTimeout;
    private final int queryTimeout;
    private final boolean useSSL;

    private DatabaseConfig(Builder builder) {
        this.host = builder.host;
        this.port = builder.port;
        this.username = builder.username;
        this.password = builder.password;
        this.connectionTimeout = builder.connectionTimeout;
        this.queryTimeout = builder.queryTimeout;
        this.useSSL = builder.useSSL;
    }

    public static class Builder {
        private String host = "localhost";
        private int port = 3306;
        private String username = "root";
        private String password = "";
        private int connectionTimeout = 30000;
        private int queryTimeout = 60000;
        private boolean useSSL = false;

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

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

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

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

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

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

        public Builder useSSL(boolean useSSL) {
            this.useSSL = useSSL;
            return this;
        }

        public DatabaseConfig build() {
            return new DatabaseConfig(this);
        }
    }
}

// 사용: 매우 직관적이고 가독성 좋음
DatabaseConfig config = new DatabaseConfig.Builder()
    .host("db.example.com")
    .port(5432)
    .username("admin")
    .password("secure123")
    .useSSL(true)
    .build();

Output:

✅ 코드 가독성: 각 설정이 무엇인지 명확
✅ 유연성: 필요한 필드만 설정 가능
✅ 불변성: 생성 후 값 변경 불가능 (final 필드)
✅ 유지보수: 새 필드 추가 시 Builder에만 추가하면 됨

4. 불변 객체와 방어적 복사

null 안전성과 불변성 보장

생성자는 객체의 안전성을 보장하는 마지막 방어선입니다. 생성자에서 적절한 검증을 하지 않으면 이후 코드에서 다양한 문제가 발생할 수 있습니다:

  1. null 참조 문제: 생성자에서 null 체크를 하지 않으면, 언제 어디서 NullPointerException이 터질지 예측 불가능
  2. 외부 수정 문제: List나 Date 같은 가변 객체를 그대로 저장하면, 외부에서 이를 수정할 수 있음
  3. 불변성 위반: 생성 후 필드를 수정할 수 있으면 멀티스레드 환경에서 문제 발생
  4. 계약 위반: 객체가 생성 후 예상과 다른 상태가 될 수 있음

생성자 단계에서 Objects.requireNonNull(), 방어적 복사, final 키워드를 통해 이런 문제들을 원천적으로 차단할 수 있습니다:

// ✅ null 안전성과 불변성을 모두 보장
public class ImmutableUser {
    private final String name;
    private final List<String> emails;
    private final LocalDate birthDate;

    public ImmutableUser(String name, List<String> emails, LocalDate birthDate) {
        // null 체크: 필수 필드는 반드시 존재
        this.name = Objects.requireNonNull(name, "name cannot be null");

        // 방어적 복사: 외부에서 전달된 리스트 변경 방지
        this.emails = Collections.unmodifiableList(
            new ArrayList<>(Objects.requireNonNull(emails, "emails cannot be null"))
        );

        // LocalDate는 불변이므로 그대로 할당 가능
        this.birthDate = Objects.requireNonNull(birthDate, "birthDate cannot be null");
    }

    // Getter는 불변 뷰만 반환
    public String getName() {
        return name;
    }

    public List<String> getEmails() {
        return emails;  // unmodifiableList이므로 안전
    }

    public LocalDate getBirthDate() {
        return birthDate;  // 불변 객체
    }
}

// 사용:
ImmutableUser user = new ImmutableUser("Alice",
    Arrays.asList("alice@example.com", "alice.work@example.com"),
    LocalDate.of(1995, 5, 15));

// ✅ 안전함
String name = user.getName();

// ❌ 컴파일 에러: 수정 불가능
// user.emails.add("newemail@example.com");  // UnsupportedOperationException 발생

Output:

✅ null 체크로 NullPointerException 방지
✅ 방어적 복사로 외부 수정 방지
✅ Collections.unmodifiableList()로 불변 보장

맺음말

생성자 설계의 핵심 요점

1. 파라미터 개수 제한 - 3개 이상이면 Builder 패턴 고려
2. 생성자 체이닝 - this()로 중복 로직 제거, 일관성 보장
3. 불변성 보장 - final 필드, Collections.unmodifiableList() 사용
4. null 안전성 - Objects.requireNonNull()로 null 체크
5. 선택적 파라미터 - Builder 패턴으로 가독성 확보
상황별 선택 가이드:

  • 파라미터 3개 이하: 일반 생성자로 충분
  • 파라미터 4~7개: 필수 필드만 생성자에, 나머지는 setter 또는 Builder
  • 파라미터 8개 이상: 반드시 Builder 패턴 사용
  • 선택적 필드가 많음: Builder 패턴 권장
  • 불변 객체 필요: 생성자에서 방어적 복사 + final 필드

이 원칙들을 따르면 안전하고 유지보수하기 쉬운 생성자를 설계할 수 있습니다. 특히 팀 프로젝트에서 다른 개발자가 생성자를 사용할 때, 의도를 명확하게 전달하고 오류를 방지할 수 있습니다.

반응형