객체를 객체를 생성하는 방법은 여러 가지가 있습니다. 각 방식은 특정 상황에서 유리하게 작용하며, 코드의 가독성, 유지보수성, 불변성 등에 영향을 미칩니다.
이번 포스팅에서는 자바에서 객체를 생성하는 주요 방식인 생성자, @Setter, 그리고 @Builder 패턴에 대해 알아보고, @Builder 어노테이션을 사용해야 하는 이유에 대해서 알아보고자 합니다.
생성자(Constructor)
public class Person {
private String name;
private int age;
private String email;
// Constructor
public Person(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
// Getters
public String getName() { return name; }
public int getAge() { return age; }
public String getEmail() { return email; }
}
// Usage
public class Main {
public static void main(String[] args) {
Person person = new Person("John Doe", 30, "john.doe@example.com");
System.out.println(person.getName() + ", " + person.getAge() + ", " + person.getEmail());
}
}
장점
필수 필드 보장
- 생성자를 통해 모든 필수 필드를 한 번에 설정할 수 있습니다. 이는 객체가 항상 유효한 상태로 생성됨을 보장합니다.
불변성
- 생성자에서 모든 필드를 초기화하면, 객체가 불변(immutable)일 수 있습니다 (특히 final 필드를 사용할 경우).
단점
매개변수 순서
- 생성자의 매개변수 순서가 중요합니다. 매개변수의 순서가 변경되면 새로운 생성자를 작성해야 합니다.
Person person = new Person("John Doe", "john.doe@example.com", 30);
Person() 생성자는 순서가 name, age, email로 받아야 합니다. 그러나 위 코드와 같이 name, email, age로 생성하게 된다면 컴파일 오류가 발생하진 않지만 객체가 잘못된 상태로 생성됩니다.
다양한 조합
- 필드의 조합이 많을 경우, 다양한 생성자를 작성해야 하므로 코드가 복잡해질 수 있습니다.
// Constructor with all fields
public Person(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
// Constructor with name and age, email has a default value
public Person(String name, int age) {
this.name = name;
this.age = age;
this.email = "not_provided@example.com"; // Default value
}
// Constructor with only name, age and email have default values
public Person(String name) {
this.name = name;
this.age = 0; // Default value
this.email = "not_provided@example.com"; // Default value
}
필드의 조합에 따른 다양한 생성자 때문에 코드가 길어졌습니다. 위 경우 3개의 필드만 존재하지만 필드의 수가 많아지게 되면 코드가 아주 복잡해지게 됩니다.
@Setter 어노테이션
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Person {
private String name;
private int age;
private String email;
}
// Usage
public class Main {
public static void main(String[] args) {
Person person = new Person();
person.setName("John Doe");
person.setAge(30);
person.setEmail("john.doe@example.com");
System.out.println(person.getName() + ", " + person.getAge() + ", " + person.getEmail());
}
}
장점
간편한 데이터 설정
- 객체 생성 후 setter 메서드를 통해 필드를 설정할 수 있습니다. 객체 생성 시 인자를 전달할 필요가 없습니다.
순서 무시
- 필드를 설정할 때
순서가 중요하지 않습니다.
단점
불변성 부족
- @Setter를 사용하면 객체의 상태를 외부에서 변경할 수 있으므로,
불변성을 보장할 수 없습니다.
public class Main {
public static void main(String[] args) {
Person person = new Person();
person.setName("John Doe");
person.setAge(30);
person.setEmail("john.doe@example.com");
// 필드 값을 변경할 수 있음
person.setName("Jane Doe");
person.setAge(25);
person.setEmail("jane.doe@example.com");
System.out.println(person.getName() + ", " + person.getAge() + ", " + person.getEmail());
}
}
위의 코드에서 객체 person의 상태를 변경하면 출력 결과가 바뀌게 됩니다. 이는 객체가 불변하지 않다는 것을 의미합니다.
불변성이 보장되지 않으면 객체의 상태가 예기치 않게 변경될 수 있습니다. 예를 들어, 여러 곳에서 같은 객체를 사용하고 이 객체의 상태를 변경하면, 객체를 사용하는 다른 부분에서도 변경된 상태를 보게 됩니다. 이로 인해 데이터의 일관성 문제를 초래할 수 있습니다.
불필요한 API 노출
- 모든 필드에 대해 setter가 생성되므로, 객체의 내부 상태가 외부에 노출됩니다.
Builder 패턴 (@Builder)
Builder 패턴은 객체 생성 과정에서 복잡한 객체를 단계별로 구성하고 설정할 수 있는 패턴입니다. 이 패턴은 객체의 생성 과정과 그 객체의 표현을 분리하여 복잡한 객체를 더 간단하게 생성할 수 있도록 돕습니다. 주로 다음과 같은 상황에서 유용합니다.
- 객체 생성 시 많은 매개변수가 필요한 경우
- 매개변수의 순서가 중요할 때
- 선택적인 매개변수가 있는 경우
- 객체의 불변성을 보장하고 싶은 경우
Builder 패턴
public class Person {
private final String name;
private final int age;
private final String email;
// Private constructor to prevent direct instantiation
private Person(PersonBuilder builder) {
this.name = builder.name;
this.age = builder.age;
this.email = builder.email;
}
// Getters
public String getName() { return name; }
public int getAge() { return age; }
public String getEmail() { return email; }
// Builder class
public static class PersonBuilder {
private String name;
private int age;
private String email;
public PersonBuilder name(String name) {
this.name = name;
return this;
}
public PersonBuilder age(int age) {
this.age = age;
return this;
}
public PersonBuilder email(String email) {
this.email = email;
return this;
}
public Person build() {
return new Person(this);
}
}
}
// Usage
public class Main {
public static void main(String[] args) {
Person person = new Person.PersonBuilder()
.name("John Doe")
.age(30)
.email("john.doe@example.com")
.build();
System.out.println(person.getName() + ", " + person.getAge() + ", " + person.getEmail());
}
}
@Builder 어노테이션 사용
import lombok.Builder;
import lombok.ToString;
@Builder
@ToString
public class Person {
private String name;
private int age;
private String email;
}
// Usage
public class Main {
public static void main(String[] args) {
Person person = Person.builder()
.name("John Doe")
.age(30)
.email("john.doe@example.com")
.build();
System.out.println(person);
}
}
장점
가독성
Person person = Person.builder()
.name("John Doe")
.age(30)
.email("john.doe@example.com")
.build();
- @Builder를 사용하면 객체 생성 시 필드를 명시적으로 설정할 수 있어 코드의 가독성이 좋습니다.
- 필드를 설정하는 순서가 명확하며, 생성자 호출 시 매개변수의
순서에 신경쓸 필요가 없습니다.
불변성 보장
- 생성자에서 final로 선언된 필드만 사용하면, 객체의 불변성을 보장할 수 있습니다.
순서 무시
- 빌더 패턴은
필드 설정 순서가 중요하지 않으며, 필요한 필드만 설정할 수 있습니다.
유연성
@Builder
public class Person {
private final String name;
private int age = 0; // 기본값
private String email = "not_provided@example.com"; // 기본값
}
- 필드의 기본값을 설정하거나, 필요한 필드만 설정할 수 있어 유연합니다.
체이닝 지원
- 빌더 메서드가 체이닝 방식으로 호출되므로, 코드가 깔끔하고 자연스럽습니다.
단점
추가적인 클래스 생성
- @Builder는 내부적으로 빌더 클래스를 생성하므로, 코드베이스에 추가적인 클래스가 생깁니다. 그러나 이 점은 대부분의 경우 장점으로 작용합니다.
각 객체 생성 방식은 특정 상황에서 장단점이 있습니다.
@Builder 패턴은 특히 복잡한 객체를 생성할 때 유용하며, 코드의 가독성과 유지보수성을 높이는 데 큰 도움이 됩니다.
@Setter는 간단한 객체의 경우 편리하지만, 불변성을 보장하지 않으며 객체의 상태를 쉽게 변경할 수 있는 단점이 있습니다.
생성자는 필수 필드를 보장하고 불변성을 유지할 수 있지만, 다양한 필드 조합에 대해 생성자를 추가로 작성해야 하는 번거로움이 있습니다.
코드와 객체 설계의 요구 사항에 맞게 적절한 객체 생성 방식을 선택하는 것이 중요합니다.