Lombok 사용을 지양하라는 말을 들은 적이 있어서 사용을 할지 말지 고민했는데 찾아보니 주의사항이 있었다...
주의사항을 알아보고 잘 지켜서 사용하면 문제없이 개발에 좋은 영향을 줄 것 같다.
@AllArgsConstructor, @RequiredArgsConstructor 지양
@AllArgsConstructor
@RequiredArgsConstructor
@ToString
public class User {
private String id;
private String password;
}
위와 같이 User 클래스를 생성했을 때, 아래처럼 addUser 메서드를 작성한다.
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 {
private String password;
private String id;
}
id와 password가 같은 String 타입이기 때문에 프로그램적인 이슈는 없지만
id에 password가 들어가고, password에 id가 들어가는 논리적 이슈가 생길 수 있다.
이를 방지하기 위해 어떤 멤버 변수에 어떤 값이 들어갈지 확실하게 하는 `@Builder`를 사용하는 것이 좋다.
@Builder 지향
@Builder
public User(String id, String password) {
this.id = id;
this.password = password;
}
`@Builder`도 `@AllArgsConstructor`를 가지고 있기 때문에 클래스에 붙이는 것보다 생성자에 붙이는 것이 좋다.
@AllArgsConstuctor 참고
`@AllArgsConstructor`는 클래스의 모든 필드 값을 파라미터로 받는 생성자를 자동으로 생성한다. (클래스의 모든 필드를 한 번에 초기화할 수 있다.)
@AllArgsConstuctor
public class User {
private String id;
private String password;
/* 자동생성
public User(String id, String password) {
this.id = id;
this.password = password;
}
*/
}
@RequiredArgsConstructor 참고
Spring에서 DI(의존성 주입)의 방법 중 생성자 주입을 임의의 코드 없이 자동으로 설정해 주는 어노테이션이다.
초기화되지 않은 `final` 필드나, `@NonNull`이 붙은 필드에 대해 생성자를 생성해 준다.
새로운 필드를 추가할 때 다시 생성자를 만들어서 관리해야 하는 번거로움을 없애준다. (`@Autowired`를 사용하지 않고 의존성 주입)
@RestController
@RestMapping("/userExample")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
/* 자동생성
@Autowired
public userController(UserService userService) {
this.userService = userService;
}
*/
}
보통 DI(의존성 주입) 방식에는 필드(Field), 수정자(Setter), 생성자(Constructor) 주입(Injection) 3가지 방법이 있다.
여기서 생성자 주입 방식을 가장 권장한다.
@Data 지양
`@Data`는 아래와 같이 많은 기능을 한 번에 사용하는 어노테이션인 만큼 그에 따른 부작용도 많다.
- `@Data`
- `@ToString`
- `@EqualsAndHashCode`
- `@Getter`
- `@Setter`
- `@RequiredArgsConstructor`
@Setter 남용
// UserService
public void updateUser(UserReqeust userRequest) {
User user = userRepository.findById(id);
user.serId = userRequest.getId();
user.setPassword = userRequest.getPassword();
}
변경을 위한 나열이라서 setter 메서드는 의도를 명확히 알 수 없다.
// UserService
public void updateUser(UserRequest userRequest) {
User user = userRepository.findById(id);
user.updateUserInfo(userRequest);
}
// User
public void updateUser(UserRequest userRequest) {
user.setId = userRequest.getId();
user.setPassword = userRequest.getPassword();
}
단순한 setter 메서드의 나열보다, 명확한 의도를 가진 메서드를 도메인에 생성하여 사용하는 것이 좋다.
@ToString 양방향 연관관계 시 순환 참조 문제
@ToString
@Entity
public class User {
@OneToMany
@JoinComlum(name = "coupon_id")
private List<Coupon> couponList = 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 객체가 (1:N, N:1)로 양방향 연관관계일 경우, `@ToString`을 호출하면 순환 참조가 발생한다. (User -> Coupon -> User)
무분별하게 `@ToString`을 사용하게 되면 이러한 문제를 만나기 쉽다.
Entity 자체를 JSON으로 직렬화하여 반환할 경우에도 순환 참조 발생
이를 방지하기 위해 `@ToString`에 `exclude` 파라미터로 특정 항목을 제외시킨다.
@ToString(exclude = "couponList")
public class User {
...
}
다른 방법도 있는 것 같은데 프로젝트를 진행하면서 직접 부딪혀가며 해봐야겠다..
@NoArgsConstructor 접근 권한 최소화
JPA에서는 프록시 생성을 위해서 기본 생성자를 반드시 하나 생성해야 한다.
이때 굳이 외부에서 생성을 열어둘 필요가 없기 때문에 접근 권한이 `protected`이면 된다.
만약 접근 권한이 `public`인 경우에 문제가 발생할 수 있다.
@NoArgsConstructor(access = AccessLevel.PUBLIC)
// @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;
}
}
예를 들어, 기본 키 생성을 UUID로 가지도록 했지만, `public` 생성자를 통해 객체를 만들면 매개변수가 없는 기본 생성자를 바라보게 되어 id 값은 `null`이 된다.
이처럼 기본 생성자를 아무 이유 없이 열어두는 것은 객체 생성 시 안정성을 심각하게 떨어뜨린다.
반대로 `private`으로 되어있으면, JPA가 프록시를 만들 때 접근하지 못해서 객체를 생성하지 못한다.
따라서 기본 생성자 접근을 `protected`로 열어두기를 권장한다.
기본 생성자 접근을 `protected`로 변경하면 외부에서 접근할 수 없으므로, `@Builder`로 만든 생성자를 통해서 객체를 생성해야 한다.
@NoArgsConstructor 참고
파라미터가 없는 디폴트 생성자를 자동으로 생성한다.
클래스에 명시적으로 선언된 생성자가 없더라도 인스턴스를 생성할 수 있다.
@NoArgsConstructor
public class User {
private String id;
private String password;
/* 자동생성
public User() {}
*/
}
@Builder 사용 시 매개변수 최소화
@Builder
public class User {
@Id
@GeneratedValues(access = GenerationType.IDENTITY)
private Long id;
private String password;
private String name;
}
`@Builder`를 클래스 레벨에 적용하면, `@AllArgsConstructor` 효과를 발생시켜 모든 멤버 필드에 대해 매개변수를 받는 기본 생성자를 만든다. 이때 `id`와 같이 받지 말아야 할 데이터를 받는 경우가 생긴다.
`@Builder`를 메서드 레벨에 적용하면, 위의 문제를 해결할 수 있다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
@Id
@GeneratedValues(access = GenerationType.IDENTITY)
private Long id;
private String password;
private String name;
@Builder
public User(String name, String password) {
this.name = name;
this.password = password;
}
}
`@Builder`를 적용한 메서드로 필요한 멤버 필드만 사용하도록 한다.
@EqualsAndHashCode 남용
상당히 고품질의 equal()과 hashCode() 메서드를 만들어주지만, 무분별하게 사용하면 문제가 생길 수 있다.
특히 문제가 되는 점은 Mutable 객체에 아무런 파라미터 없이 그냥 사용하는 경우이다.
@EqualsAndHashCode
public class Product {
private long price;
private String name;
pulbic 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> productSet = new HashSet<>();
productSet.add(prodcut);
productSet.contains(product); // true
product.setPrice(2000L);
product.contains(product); // false
동일한 객체여도 `Set`에 저장한 뒤에 필드 값을 변경하면 hashCode()가 변경되면서 찾을 수 없게 된다.
이는 어노테이션 자체의 문제보다 변경 가능한 필드에 `@EqualsAndHashCode`의 무분별한 사용으로 생기는 문제이다.
Immutable 클래스를 제외하고는 아무 파라미터 없는 `@EqualsAndHashCode 사용은 지양한다.
@EqualsAndHashCode(of={"id"})
public class Product {
private Long id;
private long price;
private String name;
pulbic 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> productSet = new HashSet<>();
productSet.add(prodcut);
productSet.contains(product); // true
product.setPrice(2000L);
product.contains(product); // true
항상 `@EqualsAndHashCode(of={"필드명시"})` 형태로 동등성 비교에 필요한 필드를 명시하는 형태로 사용해야 한다.
여러 어노테이션 사용 금지 설정
lombok.config 파일로 여러 어노테이션 사용 금지를 강제할 수 있다.
config.stopBubbling=true
lombok.data.flagUsage=error
lombok.allArgsConstructor.flagUsage=error
lombok.requiredArgsConstructor.flagUsage=error
`@Data`, `@AllArgsConstructor`, `@RequiredArgsConstructor` 등의 사용을 금지할 수 있다.
설정 후 사용하면 컴파일 오류가 발생한다.
References
https://roopredev.tistory.com/14
https://cheese10yun.github.io/lombok/#google_vignette
https://dreamcoding.tistory.com/83