Lombok
Lombok
Lombok은 자바 컴파일 시점에서 특정 어노테이션으로 해당 코드를 추가할 수 있는 라이브러리이다.
Reducing Bilerplate Code with Project Lombok
Lombok 실무에서 사용시 주의
~주관적인 생각. JPA Entity 객체 기반으로 설명드리겠습니다.
@Data는 지양하자
@Entity
@Table(name = "member")
@Data
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;
}
@Data는 @ToString, @EqualsAndHashCode, @Getter, @Setter, @RequiredArgsConstructor을 한번에 사용하는 강력한 어노테이션 입니다. 강력한 어노테이션인 만큼 그에 따른 부작용도 많다고 생각합니다.
무분별한 Setter 남용
@Data를 쓰면 자동으로 Setter 생성된다. 객체의 안전성이 보장받기 힘듬
ToString으로 인한 양방향 연관관계시 순환 참조 문제
@Entity
@Table(name = "member")
@Data
public class Member {
....
@OneToMany
@JoinColumn(name = "coupon_id")
private List<Coupon> coupons = new ArrayList<>();
}
@Entity
@Table(name = "coupon")
@Data
public class Coupon {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@ManyToOne
private Member member;
public Coupon(Member member) {
this.member = member;
}
}
위 코드 코드처럼 Member 객체와 Coupon 객체가 양방향 연관관계일 경우 ToString을 호출하면 무한 순환 참조가 발생합니다. JPA를 사용하다 보면 객체를 Json으로 직렬화 하는 가정에서 발생하는 문제와 동일한 이유입니다. 이 처럼 무분별하게 @Data를 사용하게 되면 이러한 문제를 만나기 쉽습니다.
쉬운 해결 방법으로는 다음과 같이 해당 어노테이션을 이용해서 ToString 항목에서 제외시키는 것입니다.
@ToString(exclude = "coupons")
public class Member {...}
바람직한 Lombok 사용법
제가 생각하는 바람직한 Lombok 사용법입니다. 코드를 하나 씩 설명해보겠습니다.
29
@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 이면 됩니다. 굳이 외부에서 생성을 열어둘 필요가 없습니다.
이렇게 디폴트 생성자 접근 권한을 설정하면 이러한 장점이 있습니다.
@Entity
@Table(name = "product")
@Getter
@NoArgsConstructor(access = AccessLevel.PUBLIC) // 테스트를 위해 임시로 Public, 의도한 코드는 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 생성 전략을 UUID로 가져 갔습니다.
Id는 항상 null이 아니길 기대하지만 pulbic 생성자를 통해서 객체를 생성하면 Id 값은 null 이 되게 됩니다.
이처럼 기본 생성자를 아무 이유 없이 열어두는 것은 객체 생성 시 안전성을 심각하게 떨어트린다고 생각합니다.
이때 @NoArgsConstructor(access = AccessLevel.PROTECTED)를 사용하면 객체 생성 시 안전성을 어느 정도 보장받을 수 있습니다.
기본 생성자 접근을 protected으로 변경하면 외부에서 해당 생성자를 접근 할 수 없으므로 아래 생성자를 통해서 객체를 생성 해야 합니다.
@Builder
public Product(String name) {
this.id = UUID.randomUUID().toString();
this.name = name;
}
해당 생성자 코드에는 UUID 생성 코드가 있어 객체를 생성할 시 반드시 Id 값을 보장받을 수 있습니다.
객체에 대한 생성자를 하나로 두고 그것을 @Builder을 통해서 사용하는 것이 더군다나 효율적이라고 생각합니다.
굳이 ID에 국환 되어 생각하지 객체 생성 시 반드시 생성되어야 하는 것들에 대한 안전성을 높이는 시각을 갖는 것이 중요하다고 생각합니다.
클린 코드, 유지보수 하기 좋은 코드들을 이런 사소한 객체 생성부터 생각해보는 것이 많은 도움이 될 것이라고 생각합니다.
Builder 사용시 매개변수를 최소화 하자
@Builder
public class Member {...}
클래스 위에 @Builder를 사용 시 @AllArgsConstructor 어노테이션을 붙인 효과를 발생시켜 모든 멤버 필드에 대해서 매개변수를 받는 기본 생성자를 만듭니다.
Builder AllArgsConstructor는 무슨 문제는 다음과 같습니다.
위 그림처럼 모든 멤버필드에 대한 매개변수를 허용하게 됩니다.
Member의 Id 생성전략은 데이터베이스의 auto_increment를 의존하고 있다고 가정했을 경우 Id를 넘겨받는 않아야 합니다.
또 createAt, updateAt 같은 경우는 @CreationTimestamp, @UpdateTimestamp 각각의 어노테이션이 해당 일을 담당하고 있습니다. 이 처럼 객채 생성시 받지 않아야 할 데이터들이 클래스 상단 @Builder를 사용하게 되면 발생하게 됩니다.
public class Member {
@Builder
public Member(String email, String name) {
this.email = email;
this.name = name;
}
}
이렇게 받아야 하는 생성자를 필요조건에 따라 지정하고 그 위에 @Builder를 붙이는게 바람직합니다
내 의견
위의 글처럼 바람직한 Lombok 사용법이란 본래 객체지향에 위배되지 않게 사용하는 것. 나는 실무에서 Lombok을 쓰지는 못하겠지만 마치 Lombok을 쓰듯이 저 원칙대로 클래스 구조를 설계해서 쓰다보면 나중에 Lombok을 쓰게될때도 도움이 될 것이고 그렇지 않더라도 습관을 들여놓는 것이 좋은 코드 컨벤션을 위해 필요할 것 같다.