✅ 새로 알게 된 점
자연스럽게 클래스의 생성자를 정의하고 바깥에 @Builder를 붙였는데 에러가 발생했다.
@Entity
@Getter
@Table(name = "card")
@Builder // ---------> 문제 발생
public class Card {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@EqualsAndHashCode.Include
@Column(name = "id")
Long id;
@Column(name = "name", nullable = false)
String name;
@Column(name = "email")
String email;
@Column(name = "phoneNumber")
String phoneNumber;
@Column(name = "memo")
String memo;
public Card(String name, String email, String phoneNumber) {
this.name = name;
this.email = email;
this.phoneNumber = phoneNumber
}
}
찾아보니 클래스 내의 모든 멤버 변수가 생성자에서 초기화되지 않으면,
@Builder를 해당 생성자 바로 위에 붙여야한다고 한다.
@Entity
@Getter
@Table(name = "card")
public class Card {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@EqualsAndHashCode.Include
@Column(name = "id")
Long id;
@Column(name = "name", nullable = false)
String name;
@Column(name = "email")
String email;
@Column(name = "phoneNumber")
String phoneNumber;
@Column(name = "memo")
String memo;
@Builder // 해결
public Card(String name, String email, String phoneNumber) {
this.name = name;
this.email = email;
this.phoneNumber = phoneNumber
}
}
편하다고 너무 생각 없이 쓰고 있는 것 같아서 객체 생성과 관련된 자바 패턴을 공부해봤다.
✅ 생성자 오버로딩 (점층적 생성자)
자바에는 인스턴스를 생성할 때 사용할 생성자를 정의할 수 있다.
만약 인스턴스를 생성할 때 다양한 형태의 매개변수를 이용해서 생성하고자 하면,
오버로딩을 이용해서 패턴 수만큼의 생성자를 만들어야 한다.
public class Computer {
// 필드 선언
private String cpu;
private int ram;
private int storage;
private String gpu;
// 필수 필드만 포함하는 생성자
public Computer(String cpu, int ram) {
this.cpu = cpu;
this.ram = ram;
}
// 선택적 필드 storage 추가
public Computer(String cpu, int ram, int storage) {
this(cpu, ram); // 기본 생성자 호출
this.storage = storage;
}
// 선택적 필드 gpu 추가
public Computer(String cpu, int ram, int storage, String gpu) {
this(cpu, ram, storage); // 다른 생성자 호출
this.gpu = gpu;
}
}
이는 코드 가독성을 해칠 뿐만 아니라
사용자가 생성자의 매개변수 구조를 알고 있어야하는 등의 문제가 발생한다.
✅ 빈즈 패턴
각 멤버의 Setter 메소드를 정의하는 Beans 패턴이 있다.
생성시 빈 깡통 인스턴스를 생성하고,
Setter 메소드를 사용하여 원하는 멤버 변수를 초기화하는 방식이다.
public class Computer {
private String cpu;
private int ram;
private int storage;
private String gpu;
// 기본 생성자
public Computer() {}
// 세터 메소드
public void setCpu(String cpu) { this.cpu = cpu; }
public void setRam(int ram) { this.ram = ram; }
public void setStorage(int storage) { this.storage = storage; }
public void setGpu(String gpu) { this.gpu = gpu; }
}
// 사용 예시
public class Main {
public static void main(String[] args) {
Computer computer = new Computer();
computer.setCpu("Intel i5");
computer.setRam(16);
computer.setStorage(256);
computer.setGpu("NVIDIA GTX 1660");
}
}
하지만 이 경우 사용자에게 많은 자유를 주기 때문에 문제가 발생한다.
1. 객체의 일관성이 떨어지며 불완전하다.
간단하게 말해서 인스턴스 생성의 자유도가 높아, 일관되지 않고 불완전하다는 말이다.
로직상 필수 멤버여도 Setter 호출을 깜빡하는 등 초기화가 되지 않으면 불완전한 인스턴스가 생성된다.
2. 객체 불변성을 해친다.
모든 멤버 변수에 대해 Setter 함수가 정의되어 있기 때문에
이후 로직에서 바뀌지 말아야 할 객체의 상태가 언제든 바뀔 수 있다.
이는 객체 불변성을 해치게 되며, 어떤 문제를 일으킬지 예측할 수 없게 된다.
이외에도 멤버 변수가 많아지면 개수만큼 Setter 메소드를 호출하기 때문에 코드 가독성을 해친다는 문제도 있다.
✅ 빌더 패턴
위 두 패턴의 장점을 살린 빌더 패턴이 있다.
스프링부트에서는 @Builder라는 어노테이션을 생성자에 붙이면 알아서 생성되지만,
실제 구현은 대상 클래스와 빌더 클래스가 개별로 구현된다.
다음은 ChatGPT가 작성한 예시 코드이다.
class Computer {
// 필수 매개변수
private String CPU;
private String RAM;
// 선택적 매개변수
private int storage;
private boolean isGraphicsCardEnabled;
private boolean isBluetoothEnabled;
// 생성자와 빌더 클래스는 외부에서 직접 접근할 수 없도록 private으로 설정
private Computer(Builder builder) {
this.CPU = builder.CPU;
this.RAM = builder.RAM;
this.storage = builder.storage;
this.isGraphicsCardEnabled = builder.isGraphicsCardEnabled;
this.isBluetoothEnabled = builder.isBluetoothEnabled;
}
// Builder 클래스
public static class Builder {
// 필수 매개변수
private String CPU;
private String RAM;
// 선택적 매개변수의 기본값
private int storage = 256; // 기본 스토리지
private boolean isGraphicsCardEnabled = false;
private boolean isBluetoothEnabled = false;
// 필수 매개변수를 받는 생성자
public Builder(String CPU, String RAM) {
this.CPU = CPU;
this.RAM = RAM;
}
// 선택적 매개변수를 설정할 수 있는 메소드들
public Builder setStorage(int storage) {
this.storage = storage;
return this;
}
public Builder setGraphicsCardEnabled(boolean isGraphicsCardEnabled) {
this.isGraphicsCardEnabled = isGraphicsCardEnabled;
return this;
}
public Builder setBluetoothEnabled(boolean isBluetoothEnabled) {
this.isBluetoothEnabled = isBluetoothEnabled;
return this;
}
// 최종적으로 Computer 객체를 반환하는 build() 메소드
public Computer build() {
return new Computer(this);
}
}
@Override
public String toString() {
return "Computer [CPU=" + CPU + ", RAM=" + RAM + ", Storage=" + storage +
"GB, GraphicsCardEnabled=" + isGraphicsCardEnabled +
", BluetoothEnabled=" + isBluetoothEnabled + "]";
}
}
// 사용 예시
public class Main {
public static void main(String[] args) {
Computer computer = new Computer.Builder("Intel i7", "16GB")
.setStorage(512)
.setGraphicsCardEnabled(true)
.setBluetoothEnabled(true)
.build();
System.out.println(computer);
}
}
기본적인 생성자를 가진 타겟 클래스를 작성한다.
타겟 클래스의 Setter 함수들을 빌더 클래스에 작성한다.
이를 통해, 선택적으로 초기화 가능하도록 하고,
객체의 불변성을 지킬 수 있게 한다.
각 Setter 함수의 리턴 값을 빌더 객체로 하여 예시 코드와 같이 체인 형태로 계속해서 호출할 수 있게 한다.
최종적으로 build() 함수를 호출하면 선택적으로 초기화한 멤버 값이 담긴 빌더 객체를 이용해 타겟 객체를 생성하게 된다.
사실 빌더 패턴을 이용하면 코드 가독성이 좋아지고, 불변성이 유지되고, 유연성이 확보 된다는 것은 알겠는데,
일관성과 불완전성을 피한다는 점은 잘 이해가 가지 않는다.
빌더 클래스의 생성자에서 필수 멤버의 초기화를 강제하는 등 구조상 강제하기가 편하다는 것 같은데,
빈즈 패턴에서도 같은 방식으로 충분히 강제가 가능해보이며,
강제하기 편하다는 이유로 일관성, 불완전성을 피했다는 점이 이해가 가질 않는다.
추후 알게 되면 추가하도록 하겠다.
'스프링부트 메모' 카테고리의 다른 글
[스프링부트] GenerationType.IDENTITY vs GenerationType.SEQUENCE (0) | 2024.11.07 |
---|---|
[스프링부트] 자동 답변 봇 구현 (AI 서버와의 연결) (2) | 2024.09.25 |
[스프링부트] 이메일 전송 기능 구현하기 (0) | 2024.09.23 |
[스프링부트] 디스코드 웹훅 연동하기 (1) | 2024.09.22 |
[스프링부트] 메모 시작 (0) | 2024.09.22 |