Spring

[Spring] lombok 어노테이션 정리와 주의사항

자바바 2023. 10. 24. 22:42

Lombok

Lombok이란 자바 컴파일 시점에서 특정 어노테이션으로 해당 코드를 추가해주는 라이브러리이다.

코드의 간결함과 가독성 및 유지 보수에 많은 도움이 된다. 하지만 그만큼 잘못 사용할 수도 있어 간단한 어노테이션 설명과 좋은 사용 방법에 대해 정리했다.

 

Lombok에는 @Getter, @Setter외에 다양한 어노테이션들이 있다.

 

@NoArgsConstructor

매개변수가 없는 기본 생성자 구현

@NoArgsConstructor(AccessLevel.PROTECTED) 이렇게 사용할 경우 모든 필드에 대한 값이 들어가야함을 보장하고 싶을 때, 기본 생성자 호출을 막음으로서 이후에 발생할 수 있는 문제를 사전 차단할 수 있다.
@NoArgsConstructor
public Class Sample {

	private String attr1;
        
	public Sample(String attr1) {
		this.attr1 = attr1;
	}
}

실제 자바 코드에는

public Sample() {

}

이 추가되어 들어간다.

주의점

필드들이 final로 생성되어 있는 경우에는 필드를 초기화할 수 없기 때문에 생성자를 만들 수 없어 Error가 발생한다.

이 때, @NoArgsConstructor(force=true) 옵션을 줘서 final 필드를 0, false, null 등으로 강제 초기화시켜 생성자를 만들 수 있다.

@AllArgsConstructor

클래스에 존재하는 모든 필드에 대한 생성자를 자동으로 생성

@AllArgsConstructor
public Class Sample {

	private String attr1;
	private String attr2;	
	@NonNull    
	private String attr3
    
}

실제 자바 코드에는

public Sample(String attr1, String attr2, @NonNull String attr3) {
		if(attr3 == null) {
			throw new NullPointerException("attr3 is marked non-null but is null");
		} else {
			this.attr1 = attr1;        
			this.attr2 = attr2;
			this.attr3 = attr3;
		}
	}

이 추가된다.

@RequiredArgsConstructor

final, @NonNull인 필드들에 대한 생성자 구현

@RequiredArgsConstructor
public Class Sample {

	private String attr1;
	private final String attr2;	
	@NonNull    
	private String attr3
    
}

실제 자바 코드에는

public Sample(String attr2, @NonNull String attr3) {
		if(attr3 == null) {
			throw new NullPointerException("attr3 is marked non-null but is null");
		} else {
			this.attr2 = attr2;
			this.attr3 = attr3;
		}
	}

이 추가된다.

 

생성자 어노테이션 주의점: static 필드는 스킵된다. AccessLevel을 명시해주지 않으면 public으로 설정된다.

@EqualsAndHashCode

equal, hashCode를 자동 생성해준다.

equals : 두 객체의 문자열이 같은지 동등성 비교

hashCode: 두 객체가 같은 객체인지 동일성 비교

 

@Builder

해당 클래스에 자동으로 빌더를 추가해준다.

다수의 필드를 가지는 복잡한 클래스일 경우, 생성자 대신 빌더를 사용하는 경우가 많다.

@Builder
public class User {
	private Long id;
	private String username;
	private String password;
	@Singular
	private List<Integer> scores;
}
User user = User.builder()
	.id(1L)
	.username("dale")
	.password("1234")
	.score(70)
	.score(80)
	.build();
// User(id=1, username=dale, password=1234, scores=[70, 80])

 

@ToString

객체의 내용을 문자열로 나타내는 toString 메소드 코드를 생성해준다.

public class Sample {
  private String id;
  private String name;
  private Integer age;

  @Override
  public String toString() {
    return "Sample(id=" + this.id + ", name=" + this.name + ")");
  }
}

주의! ToString 사용 시 멤버 변수 중 객체 타입이 있고, 순환 참조가 있다면 무한 루프가 발생한다.

classA의 멤버 변수로 classB를 가지고 있고, classB의 멤버 변수로 classA를 갖고 있다면 = 양방향 연관 관계라면,

ToString의 무한 반복이 발생한다.

 

이럴 때는, exclude 키워드를 사용하여 명시적으로 해당 필드를 제외해줘야 한다.

@ToString(exclude="classA")

 

@Data

@ToString, @EqualsAndHashCode, @Getter, @Setter, @RequiredArgsConstructor

다음 어노테이션을 한번에 설정해준다.

 

@Value

@Data와 비슷하지만 불변(immutable 객체)으로 만들어준다.

생성 시 생성자에 따라 값이 설정되고, 그 이후는 값 변경이 불가능. 즉, 한번 생성하면 변경할 수 없는 불변 객체를 만들기 위한 클래스 선언할 때는 @Data대신 @Value 사용

 

@Value를 사용하면 @Getter, @ToString, @EqualsAndHashCode, @AllArgsConstructor이 부여된다.

클래스를 final로 설정하고, 모든 필드를 private final로 설정. Setter를 생성하지 않는다.

 

@NonNull

자동으로 null 체크하고 null인 경우 NullPointException을 발생시킨다.

 


💡 Lombok 주의사항

@Data는 지양하자

@Data는 @ToString, @EqualsAndHashCode, @Getter, @Setter, @RequiredArgsConstructor을 한번에 사용하는 강력한 어노테이션이다. 강력한 어노테이션인만큼 부작용도 많다.

무분별한 Setter 남용

@Data를 사용하면 자동으로 Setter를 지원하게 된다. ➡ 

간단히 말하면, Setter는 그 의도가 분명하지 않고 객체를 언제든지 변경할 수 있는 상태가 되어서 객체의 안전성이 보장받기 힘들다. 

 

ToString으로 양방향 연관관계 시 무한 순환 참조 문제

예를 들어, Member 객체와 Coupon 객체가 양방향 연관관계일 경우, ToString을 호출하면 무한 순환 참조가 발생한다.

JPA를 사용하면 객체를 JSON으로 직렬화 하는 과정에서 발생하는 문제와 동일한 이유.

 

해결 방법으로는,

@ToString(exclude = "coupons")
public class Member {...}

좋은 Lombok 사용법

Lombok 사용법 예시

@Entity
@Table(name = "member")
@ToString(exclude = "coupons")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode(of = {"id", "email"})
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(name = "email", nullable = false)
    private String email;

    @Column(name = "name", nullable = false)
    private String name;

    @CreationTimestamp
    @Column(name = "create_at", nullable = false, updatable = false)
    private LocalDateTime createAt;

    @UpdateTimestamp
    @Column(name = "update_at", nullable = false)
    private LocalDateTime updateAt;

    @OneToMany
    @JoinColumn(name = "coupon_id")
    private List<Coupon> coupons = new ArrayList<>();

    @Builder
    public Member(String email, String name) {
        this.email = email;
        this.name = name;
    }
}

@ NoArgsConstructor 접근 권한을 최소화하자

JPA에서는 프록시를 생성을 위해서 기본 생성자를 반드시 하나를 생성해야 한다. 이때 접근 권한이 protected 이면 된다. 외부에서 생성을 열어둘 필요가 없다.

@NoArgsConstructor(access = AccessLevel.PUBLIC)

을 하게 되면 id 값은 null이 되게 된다. 이처럼 기본 생성자를 아무 이유 없이 열어두는 것은 객체 생성 시 안전성을 낮출 수 있다.

 

@NoArgsConstructor(access = AccessLevel.PROTECTED)를 사용하면 객체 생성 시 안전성을 어느정도 보장받을 수 있다. 다만, 외부에서 해당 생성자를 접근할 수 없으므로 아래 생성자를 통해서 객체를 생성해야한다.

@Builder
public Product(String name) {
    this.id = UUID.randomUUID().toString();
    this.name = name;
}

객체에 대한 생성자를 하나 두고 그것을 @Builder를 통해서 사용하는 것이 좋다.

 

Builder 사용시 클래스 매개변수를 최소화하자

@Builder
public class Member {...}

클래스 위에 @Builder를 사용시 @AllArgsConstructor 어노테이션을 붙인 효과를 발생시켜 모든 멤버 필드에 대해 매개변수를 받는 기본 생성자를 만들게 된다.

이럴 경우, 객체 생성지 받지 않아야 할 데이터들이 Builder를 사용하게 되면 다 넘겨받게 된다.

 

public class Member {

    @Builder
    public Member(String email, String name) {
        this.email = email;
        this.name = name;
    }
}

그렇기 때문에, 이렇게 받아야 하는 생성자를 필요 조건에 따라 지정하고 그 위에 @Builder를 붙이는게 좋다.

위 코드로 하면 매개변수 name, email만 넘겨 받을 수 있게 된다.

✨ 요약

  • @NoArgsConstructor(access = AccessLevel.PROTECTED) JPA에서는 프록시 객체가 필요하므로 기본 생성자 하나가 반드시 있어야 한다. 이때 접근제한자는 protected로 설정한다.
  • @Data는 사용하지 말자.
  • @Setter 지양하자. 객체는 변경 포인트를 남발하지 말자.
  • @ToString은 무한 참조가 생길 수 있다. @ToString(of = {" "}) 권장
  • 클래스 상단의 @Builder (X). 생성자 위에 @Builder (O)

 


출처

https://haenny.tistory.com/387

https://github.com/cheese10yun/blog-sample/tree/master/lombok