Lombok 사용 시 주의 사항
Lombok은 자바 컴파일 시점에서 특정 어노테이션으로 해당 코드를 추가할 수 있는 라이브러리이다.
간편한 코드 작성, 가독성, 유지 보수에 많은 도움이 되지만 편리한 만큼 잘못 사용하기 쉬운 것이 Lombok이다.
@Data 사용 지양
@Data는 @ToString, @EqualsAnHashCode, @Getter, @Setter, @RequiredArgsConstructor을 모두 포함하는 강력한 어노테이션임.
@Data 사용으로 인해 발생할 수 있는 문제점
1. 무분별한 Setter 남용
Setter는 그 의도가 분명하지 않고 객체를 언제든지 변경할 수 있는 상태가 되어서 객체의 안정성이 보장받기 힘들다.
불필요한 변경 포인트를 제공하지 않음으로써 안정성을 취할 수 있다.
2. @ToString : 양방향 연관관계 시 순환 참조
Member와 Coupon이 1:N 양방향으로 매핑되어 있는 상황을 가정. 이때, ToString을 호출하면 무한 순환 참조가 발생함.
@ToString(exclude = "coupons")처럼 어노테이션을 사용해서 특정 항목을 제외시키는 방법을 사용할 수 있다.
3. @EqualsAndHashCode
@EqualsAndHashCode는 상당히 고품질의 equals( )와 hasCode( ) 메서드를 만들어 줌, 잘 사용하면 좋지만 남발하면 심각한 문제가 생김
특히 문제가 되는 점은 Mutable 객체에 아무런 파라미터 없이 그냥 사용하는 경우임
@EqualsAndHashCode
public static class Order {
private Long orderId;
private long orderPrice;
private long cancelPrice;
public Order(Long orderId, long orderPrice, long cancelPrice) {
this.orderId = orderId;
this.orderPrice = orderPrice;
this.cancelPrice = cancelPrice;
}
}
Order order = new Order(1000L, 19800L, 0L);
Set<Order> orders = new HashSet<>();
orders.add(order); // Set에 객체 추가
System.out.println("변경전 : " + orders.contains(order)); // true
order.setCancelPrice(5000L); // cancelPrice 값 변경
System.out.println("변경후 : " + orders.contains(order)); // false
- 동일한 객체여도 필드 값을 변경시키면 hashCode가 변경되면서 찾을 수 없는 값이 됨
- 변경 가능한 필드에 이를 남발함으로써 생기는 문제
- Immutable 클래스를 제외하고는 아무 파라미터 없는 @EqualsAndHashCode 사용은 지양
- 항상 @EqualsAndHashCode(of={ "필드명시" }) 형태로 동등성 비교에 필요한 필드를 명시하는 형태로 사용
- 실무에서는 누군가는 이에 대해 실수할 수 있음, 사용을 완전히 금지시키고 꼭 필요한 필드를 지정하는 것이 나음
팁: @EqualsAndHashCode.Exclude 를 활용
@Getter
@Setter
@EqualsAndHashCode
public class TestUser {
public String id;
@EqualsAndHashCode.Exclude
public String password;
}
- @EqualsAndHashCode.Exclude 애노테이션을 이용하면 특정 필드만 뺄 수 있다. 직접 글자로 치는 것보다 오타없이 뺄 수 있어서 좋음
- @EqualsAndHashCode(onlyExplicitlyIncluded = true)를 사용하는 것도 가능
@Value 사용 금지
모든 필드를 private final로 변경해주는 기능이 있지만, 이 또한 @EqualsAndHashCode, @AllArgsConstructor 를 포함함.
@EqualsAndHashCode 는 애초에 필드 값 자체가 변경 불가능하게 바뀌기 때문에 상관이 없는데, @AllArgsConstructor 가 또 문제가 됨
@Builder는 웬만하면 직접 만든 생성자에서 사용
@Builder 를 편하게 사용하기 위해 @AllArgsConstructor 와 같이 써버리는데, 이렇게 해도 access = PRIVATE 을 하면 웬만해서는 실수할 일이 없지만 직접 만든 생성자에 @Builder를 붙이면 더욱 안전함
public class TestUser {
public String id;
public String password;
@Builder
public TestUser(String id, String password) {
this.id = id;
this.password = password;
}
}
- Builder 를 통해서 굳이 설정할 일이 없는 값 혹은 설정해서는 안되는 값을 제외하고 Builder 를 생성해줄 수 있음
@Log
@Log를 통해 각종 Logger 를 자동생성할 수 있음
기본적으로 private static final 로 생성하는데, static 이 아닌 필드로 만들고자 하거나 Logger 객체 이름을 변경하고자 한다면 lombok.config를 사용
lombok.log.fieldName=logger # 로거 객체 이름을 logger로 변경. 원래는 log
lombok.log.fieldIsStatic=false # 로거를 static이 아닌 필드로 생성
- 가급적이면 @Sl4j만 사용하고 나머지는 사용할 수 없게 금지시키는 것도 가능
@NonNull
불필요하게 branch converage를 증가시킴(프로젝트 코드 커버리지를 유지하고 싶다면 Null인 상황에서 오류발생과 그렇지 않은 상황에 대한 테스트를 모든 사용처에서 만들어야함)
이 코드를 일일이 테스트를 만들자니 이미 검증된 라이브러리의 기능을 사용하는 곳에서 모두 테스트를 만드는 것도 굉장히 소모적인 일임
- @NonNull을 사용하지 않고 Guava 의 Preconditions로 null을 검증하고 오류 처리하는 것도 가능
생성자 자동 생성 어노테이션 사용 지양
@NoArgsConstructor 접근 권한 최소화
JPA에서는 프록시 생성을 위해서 기본 생성자를 반드시 하나 생성해야 함, 이때 접근 권한이 protected이면 됨(굳이 외부에서 생성을 열어둘 필요가 없음).
접근 권한이 public인 경우에 문제 발생할 수 있음
@Entity
@Table(name = "product")
@Getter
@NoArgsConstructor(access = AccessLevel.PUBLIC) // 테스트를 위해 임시로 Public
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로 열어두길 권장하고 있음
- 객체에 대한 생성자를 하나로 두고, @Builder를 통해 사용하면 반드시 필요한 값이 있어야 객체가 생성됨을 보장할 수 있어 안정성을 높일 수 있음
@AllArgsConstructor 사용 지양
매우 편리하게 생성자를 만들어주지만, 별 생각없이 작성한 코드가 치명적인 버그를 만들어낼 수도 있음
@AllArgsConstructor
public static class Person {
private String firstName;
private String lastName;
}
// 성은 권, 이름은 현수
Person me = new Person("권", "현수");
- 위 클래스에 대해 자동으로 firstName, lastName 순서로 인자를 받는 생성자가 만들어짐, 누군가 lastName이 성인줄 알고 아래처럼 순서를 바꿀 경우
@AllArgsConstructor
public static class Person {
private String lastName;
private String firstName;
}
- IDE가 제공해주는 리팩토링이 전혀 작동하지 않고, lombok이 개발자도 인식하지 못하는 사이에 생성자의 파라미터 순서를 필드 선언 순서에 맞춰 변경해 버림
- 두 필드는 동일한 Type이라 기본 생성자 호출 코드에서는 인자 순서를 변경하지 않았음에도, 어떠한 오류도 발생하지 않음
- 바뀐 코드는 아무런 에러 없이 잘 작동하는 것처럼 보이지만 실제로는 Person이 이상한 값으로 바뀌어 버림
- 이러한 문제는 @AllArgsConstructor와 @RequiredArgsConstructor에 둘 다 존재함, 따라서 두 lombok 어노테이션은 사용하지 않도록 지양해야 함.
- @Builder 등을 이용하기 위함이라면, 접근 레벨을 낮춰놓자.
- 생성자를 직접 만들고 필요한 경우에 직접 만든 생성자에 @Builder 어노테이션을 붙이는 것을 권장(파리미터 순서가 아닌 이름으로 값을 설정하기 때문에 리팩토링에 유연하게 대응할 수 있다).
@AllArgsConstructor
public static class Person {
private String firstName;
private String lastName;
@Builder
private Person(String firstName, String lastName){
this.firstName = firstName;
this.lastName = lastName;
}
}
// 필드 순서를 변경해도 한국식 이름이 만들어진다.
Person me = Person.builder().lastName("현수").firstName("권").build();
System.out.println(me);
@RequiredArgsConstructor 지양
의존 관계를 주입하기 위해 사용하는 어노테이션
의존 주입 방식
- 생성자 주입 방식
- 필드 주입 방식
- Setter 주입 방식
공식 레퍼런스에서는 생성자 주입 방식을 권장하고 있음, 필수적인 의존성을 모두 만족해야만(파라미터로 모두 받아야만) 객체를 생성할 수 있도록 강제할 수 있기 때문
프로젝트 리팩토링 예시
리팩토링 전
@Entity
@NoArgsConstructor // 문제 상황 1. 기본 생성자의 접근 제어자가 불명확함
@AllArgsConstructor // 문제 상황 2. 부작용이 많은 모든 파라미터를 받는 생성자 자동 생성
@Builder // 문제 상황 3. 클래스 단위의 Builder 패턴 적용
@Getter
public class Board extends BaseTimeEntity {
@Id
@GeneratedValue
@Column(name="board_id")
private Long id;
@Enumerated(EnumType.STRING)
private Category category;
private String title;
private String content;
private String author;
@ManyToOne
@JoinColumn(name="member_id")
private Member member;
@Builder.Default @OneToMany(mappedBy="board") // 문제 상황 4. 잘못된 @Builder 위치로 인해 추가해야했던 초기화를 위해 불필요한 코드
private List<Comment> commentList = new ArrayList<Comment>();
public void edit(BoardForm boardForm){
content = changedInfo(content, boardForm.getContent());
}
private String changedInfo(String original, String changed){
return (changed == null || changed.equals("")) ? original : changed;
}
public void setMember(Member member){
if(this.member!=null){
this.member.getBoardList().remove(this);
}
this.member=member;
member.getBoardList().add(this);
}
// 문제 상황 5. 생성자 메소드 대신 생성해주는 메소드 사용
public static Board createBoard(Member member,BoardForm boardForm){
Board board =Board.builder()
.category(boardForm.getCategory())
.title(boardForm.getTitle())
.content(boardForm.getContent())
.author(member.getNickname())
.build();
board.setMember(member);
return board;
}
}
리팩토링 후
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 리팩토링 1. protected 접근 제어자로 생성자의 접근 제어
// @AllArgsConstructor 리팩토링 2. 불필요한 생성자 제거
@Getter
public class Board extends BaseTimeEntity {
@Id
@GeneratedValue
@Column(name="board_id")
private Long id;
@Enumerated(EnumType.STRING)
private Category category;
private String title;
private String content;
private String author;
@ManyToOne
@JoinColumn(name="member_id")
private Member member;
@OneToMany(mappedBy="board", cascade = CascadeType.REMOVE) // 리팩토링 3. 클래스 단위의 빌더 패턴 제거로 객체 생성 시 자동으로 List 초기화
private List<Comment> commentList = new ArrayList<Comment>();
public void edit(BoardForm boardForm){
content = changedInfo(content, boardForm.getContent());
}
private String changedInfo(String original, String changed){
return (changed == null || changed.equals("")) ? original : changed;
}
public void setMember(Member member){
if(this.member!=null){
this.member.getBoardList().remove(this);
}
this.member=member;
member.getBoardList().add(this);
}
@Builder // 리팩토링 4. 생성자 메소드 생성 후 본 메서드에 Builder 패턴 적용
public Board (Member member, BoardForm boardForm){
this.author = member.getNickname();
this.category = boardForm.getCategory();
this.title = boardForm.getTitle();
this.content = boardForm.getContent();
this.setMember(member);
}
}
'항해 99 > Spring' 카테고리의 다른 글
QueryDSL (0) | 2024.03.08 |
---|---|
Entity와 DTO의 분리 (0) | 2024.03.08 |
Controller, RestController 차이 (0) | 2024.03.05 |
메서드 명 find와 get의 차이 (0) | 2024.03.04 |
TDD(Test-Driven Development), JUnit (0) | 2024.03.03 |