티스토리 뷰

Spring

[Spring] Lombok 사용시 주의사항

roopreDev 2023. 4. 20. 20:53

@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 어노테이션을 붙인 효과를 발생시켜 모든 멤버 필드에 대해서 매개변수를 받는 기본 생성자를 만듭니다. 하지만 받지말아야할 데이터인 idcreatedAt, 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 의 문제라기 보다는 변경 가능한 필드에 이를 남발하면서 생기는 문제이다. 이러한 문제를 방지하기 위해서는 아래와 같은 방법들이 있다.

  1. Immutable(불변) 클래스를 제외하고는 아무 파라미터 없는 @EqualsAndHashCode사용을 금지한다.
  2. 일반적으로 비교에서 사용하지 않는 단순 클래스의 경우에는 equals()와 hashCode()를 구현하지 않는게 낫다.
  3. 항상 @EqualsAndHashCode(of={"name"}) 형태로 동등성 비교에 필요한 필드를 명시하는 형태로 사용한다.

여러 어노테이션 사용 금지 설정

lombok.config를 사용하여 여러 어노테이션 사용 금지를 강제할 수 있다. 예를 들어, 아래와 같은 설정으로 @Data, @AllArgsConstructor,  @RequiredArgsConstructor등의 사용을 금지할 수 있다. 사용할 경우 컴파일 오류가 발생한다.

config.stopBubbling = true
lombok.data.flagUsage=error
lombok.allArgsConstructor.flagUsage=error
lombok.requiredArgsConstructor.flagUsage=error

 

참고

https://lkhlkh23.tistory.com/159

https://siahn95.tistory.com/170

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함