티스토리 뷰
@AllArgsConstructor, @RequiredArgsConstructor 사용 주의
@AllArgsConstructor
@RequiredArgsConstructor
@ToString
public class User {
public String id;
public String password;
}
위와 같이 User 클래스를 생성했다고 했을 때 보통 아래와 같이 코드를 작성합니다.
public void addUser(String id, String password) {
User user = new User(id, password);
userRepository.save(user);
}
생성자의 첫번째 자리에는 id, 두번째에는 password가 잘 들어갑니다. 이 후 요구사항의 변경으로 User의 멤버 변수가 추가로 들어오고 삭제되다가 아래와 같이 순서가 변경될 수 있습니다.
@AllArgsConstructor
@RequiredArgsConstructor
@ToString
public class User {
public String password;
public String id;
}
id와 password가 같은 String 타입이기 때문에 프로그램적인 이슈는 없지만 id에 password가 들어가고 password에 id가 들어가는 논리적 이슈가 생길 수 있습니다. id와 password라는 간단한 예시를 들었지만 실제 프로젝트에서는 다양한 멤버변수가 있기 때문에 자기도 모르게 이러한 문제가 발생할 수 있습니다. 이를 방지하기 위해 어떤 멤버 변수에 어떤 값이 들어갈지 확실하게 하는 @Builder를 사용하는 것이 좋습니다.
하지만 @Builder의 경우에도 @AllArgsConstructor를 가지고 있기 때문에 클래스에 붙이는 것 보다는 아래와 같이 직접 만든 생성자에 붙이는 것이 좋습니다.
@Builder
public User(String id, String password) {
this.id = id;
this.password = password;
}
@Data는 지양하기
@Data 는 @ToString, @EqualsAndHashCode, @Getter, @Setter, @RequiredArgsConstructor 을 한번에 사용하는 강력한 어노테이션입니다. 많은 기능을 한번에 사용하는 만큼 그에 따른 부작용도 많습니다.
무분별한 @Setter 남용
Setter 메소드는 의도를 갖기 힘듭니다. 아래 setter들은 회원 정보를 변경하기 위한 나열이라서 메소드들의 의도가 명확히 드러나지 않습니다.
public void updateMyUser(UserReq userReq) {
User user = userRepository.findById(id);
user.setId = userReq.getId();
user.setPassword = userReq.getPassword();
}
아래와 같이 updateMyUser 메서드를 이용해 단순한 setter의 나열보다는 명확한 의도를 가지는 메서드를 사용하는 것이 바람직합니다.
public void updateMyUser(UserReq userReq) {
User user = findById(userReq.getId());
user.updateMyUser(userReq);
}
// User 도메인 클래스
public void updateMyUser(UserReq userReq) {
this.id = userReq.getId();
this.password = userReq.getPassword();
}
또한, setter 메서드가 있을 때 모든 곳에서 객체의 변수들이 언제든지 변경할 수 있기 때문에 객체의 일관성을 유지하기 어렵습니다.
@ToString으로 인한 양방향 연관관계시 순환 참조 문제
@ToString
@Entity
public class User {
@OneToMany
@JoinColumn(name = "coupon_id")
private List<Coupon> coupons = new ArrayList<>();
}
@ToString
@Entity
public class Coupon {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
private User user;
public Coupon(User user) {
this.user = user;
}
위와 같이 User 객체와 Coupon 객체가 양방향 연관관계일 경우 @ToString을 호출하면 순환 참조가 발생합니다. JPA를 사용하다 보면 객체를 Json으로 직렬화하는 과정에서 발생하는 문제와 동일한 이유입니다. 이처럼 무분별하게 @ToString를 사용하게 되면 이러한 문제를 만나기 쉽습니다. 아래와 같이 coupons를 제외시키면서 문제를 해결할 수 있습니다.
@ToString(exclude = "coupons")
public class Member {
...
}
@ToString으로 인한 양방향 연관관계시 순환 참조 문제
JPA에서는 프록시 생성을 위해서 기본 생성자 하나를 반드시 생성해야합니다. 이 때 접근 권한이 protected로 생성해 외부에서 생성을 막는 것을 권장합니다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Product {
@Id
private String id;
private String name;
@Builder
public Product(String name) {
this.id = UUID.randomUUID().toString();
this.name = name;
}
}
생성자를 통해 id는 항상 null이 아니길 기대하지만 만약 public 생성자를 이용해 객체를 생성하면 id값은 null이 되게 됩니다. 이처럼 기본 생성자를 아무 이유 없이 열어두는 것은 객체 생성 시 안전성을 심각하게 떨어트린다고 생각합니다. 이때 @NoArgsConstructor(access = AccessLevel.PROTECTED)를 사용하면 객체 생성 시 안전성을 어느 정도 보장받을 수 있습니다.
기본 생성자 접근을 protected으로 변경하면 외부에서 해당 생성자를 접근할 수 없으므로 아래 생성자를 통해서만 객체를 생성해야 합니다.
@Builder
public Product(String name) {
this.id = UUID.randomUUID().toString();
this.name = name;
}
해당 생성자 코드에는 UUID 생성 코드가 있어 객체를 생성할 시 반드시 id값을 보장받을 수 있습니다.
@Builder 사용시 매개변수를 최소화하기
@Builder
public class User {
@Id
@GeneratedValues(access = AccessLevel.PROTECTED)
private Long id;
private String password;
private String name;
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
위와 같이 클래스 위에 @Builder 를 사용 시 @AllArgsConstructor 어노테이션을 붙인 효과를 발생시켜 모든 멤버 필드에 대해서 매개변수를 받는 기본 생성자를 만듭니다. 하지만 받지말아야할 데이터인 id와 createdAt, updatedAt도 받게되는 경우가 생깁니다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
@Id
@GeneratedValues(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String password;
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
@Builder
public User(String name, String password) {
this.name = name
this.password = password
}
}
Class에 @Builder를 적용하기보다는 위와 같이 필요한 생성자 메서드를 직접 만들어서 @Builder를 적용하면 문제를 해결할 수 있습니다.
무분별한 @EqualsAndHashCode 사용 자제
@EqualsAndHashCode는 상당히 고품질의 equals()와 hashCode()메서드를 만들어주지만 아래와 같은 경우에는 문제가 생길 수 있다.
@EqualsAndHashCode
public class Product {
private long price;
private String name;
public Product(long price, String name) {
this.price = price;
this.name = name;
}
public void setPrice(long price) {
this.price = price;
}
}
Product product = new Product(1000L, "product");
Set<Product> products = new HashSet<>();
products.add(product);
products.contains(product); // true
product.setPrice(2000L);
products.contains(product); // false
위와 같이 동일한 객체여도 Set에 저장한 뒤에 필드 값을 변경하면 hashCode()가 변경되면서 찾을 수 없게 된다. 이는 @EqualsAndHashCode 의 문제라기 보다는 변경 가능한 필드에 이를 남발하면서 생기는 문제이다. 이러한 문제를 방지하기 위해서는 아래와 같은 방법들이 있다.
- Immutable(불변) 클래스를 제외하고는 아무 파라미터 없는 @EqualsAndHashCode사용을 금지한다.
- 일반적으로 비교에서 사용하지 않는 단순 클래스의 경우에는 equals()와 hashCode()를 구현하지 않는게 낫다.
- 항상 @EqualsAndHashCode(of={"name"}) 형태로 동등성 비교에 필요한 필드를 명시하는 형태로 사용한다.
여러 어노테이션 사용 금지 설정
lombok.config를 사용하여 여러 어노테이션 사용 금지를 강제할 수 있다. 예를 들어, 아래와 같은 설정으로 @Data, @AllArgsConstructor, @RequiredArgsConstructor등의 사용을 금지할 수 있다. 사용할 경우 컴파일 오류가 발생한다.
config.stopBubbling = true
lombok.data.flagUsage=error
lombok.allArgsConstructor.flagUsage=error
lombok.requiredArgsConstructor.flagUsage=error
참고
'Spring' 카테고리의 다른 글
[JPA] 엔티티 설계 시 주의점 (0) | 2023.12.17 |
---|---|
[Spring] ShedLock을 활용하여 스케줄러 동기화하기 (0) | 2022.12.20 |