Lombok에 대해 알아보자

Jaeyoung·2022년 11월 1일
0
post-thumbnail

LomBok이란?

LomBok은 자바 라이브러리로 자동적으로 에디터나 빌드 도구에 연결되며 자바 프로젝트의 생산성을 올려준다.대표적인 기능으로는 Class에 Getter 또는 Setter 또는 Equals 메소드 등을 작성하지 않더라도 어노테이션을 통해 자동으로 생성해준다. 이와 비롯해 여러가지 Stable된 Lombok이 제공하는 기능에 대해 알아보도록 하자.

Project Lombok 공식문서

val, var

자바에서 변수를 할당하거나 할때 타입을 적어줘야하는 번거로움이 있다. 하지만 LomBok val,var를 이용하면 타입 추론을 통해 번거롭게 타입을 따로 명시해주지 않아도 된다. 다만 지역변수에서만 지원이 가능하다.

그렇다면 val과 var은 어떤 차이점이 있는지 한번 알아보자 일단 val 부터 보면 우리가 불변성을 가지는 변수를 선언할 때 사용이 된다. 즉 자바에서는 final로 선언이 되는 변수를 뜻한다. 그럼 코드로 어떻게 사용되는지 알아보자

public class ValLomBok{
	
	// Use LomBok Val
	public String useVal(){
		val list = ArrayList<String>();
		list.add("Val");
		val result = list.get(0);
		return result;
	}

	// Not Use LomBok Val
	public String notUseVal(){
		List<String> list = ArrayList<String>();
		list.add("Val");
		String result = list.get(0);
		return result;
	}
}

val를 사용했을때랑 사용하지 않았을 때의 코드를 작성해 보았는데 확실히 번거로움을 줄일 수 있었다.

var같은 경우는 Mutable 즉 변경가능한 변수를 선언할 때 사용이 된다. JEP 286에 따라 작성이 되었다. 사용법은 val과 똑같다.

@NonNull

NonNull 어노테이션은 컴포넌트 혹은 메소드나 생성자의 파라미터에 선언할 수 있으며 이것은 해당 요소 맨 위에 널 체크 로직을 작성해 주어서 해당 컴포넌트나 파라미터가 Null이 될 수 없음을 보장하여 안전하게 처리 될 수 있게 해준다. 생성자의 경우에는 this()나 super()와 같은 명시적인 호출이 된후에 널 체크 로직이 작성된다. Record Componet인 경우에는 생성자가 없을 경우 아무 요소가 포함되어 있지 않는 Default 생성자에 널 체크 로직이 작성된다. 그리고 널 체크 로직이 맨 위에 있는 경우 널 체크 로직이 생성되지 않는다. 코드를 통해 어떻게 생성이 되는지 알아보자

 public class NonNullExample extends Something {
  private String name;
  
	
  public NonNullExample(@NonNull Person person) {
    super("Hello");
    this.name = person.getName();
  }

	// 변환된 코드
	public NonNullExample(@NonNull Person person) {
    super("Hello");
    if (person == null) {
      throw new NullPointerException("person is marked non-null but is null");
    }
    this.name = person.getName();
  }
}

@Cleanup

해당 어노테이션은 주어진 리소스에 대해 해당 스코프에서 코드가 실행되서 종료되기 전에 자동적으로 정리할 때 사용한다. 해당 설명만 봐서는 이해가 잘 되지 않을 것이다. 예를들면 FileInputStream을 통해 File에 대해 읽어 온다고 치자 그러면 우리는 명시적으로 close를 호출해서 해당 InputStream을 종료해야한다. 이런 close 호출을 자동적으로 try/finally 블록에서 처리해준다. 하지만 모든 객체가 close를 통해 리소스를 정리하는게 아님으로 @Cleanup(”정리 메소드 이름”) 이런 형식으로 명시적으로 정리 메소드를 통해 리소스를 정리 할 수 있다. 코드를 통해 한번 알아보도록 하자

public class CleanupExample {
  public void fileRead throws IOException {
    @Cleanup InputStream in = new FileInputStream(args[0]);
    @Cleanup OutputStream out = new FileOutputStream(args[1]);
    byte[] b = new byte[10000];
    while (true) {
      int r = in.read(b);
      if (r == -1) break;
      out.write(b, 0, r);
    }
  }
// 변환된 메소드
	public void fileRead throws IOException {
    InputStream in = new FileInputStream(args[0]);
    try {
      OutputStream out = new FileOutputStream(args[1]);
      try {
        byte[] b = new byte[10000];
        while (true) {
          int r = in.read(b);
          if (r == -1) break;
          out.write(b, 0, r);
        }
      } finally {
        if (out != null) {
          out.close();
        }
      }
    } finally {
      if (in != null) {
        in.close();
      }
    }
	}
}

@Getter, @Setter

해당 어노테이션은 Getter와 Setter을 자동으로 생성해주기 때문에 자바에서 아주 유용하게 사용할 수 있습니다. 또한 해당 Getter와 Setter의 접근레벨(public, protected, package, private)을 설정할 수 있습니다.

코드를 통해 한번 알아보자

public class GetterSetterExample {
  /**
   * Age of the person. Water is wet.
   * 
   * @param age New value for this person's age. Sky is blue.
   * @return The current value of this person's age. Circles are round.
   */
  @Getter @Setter private int age = 10;
  
  /**
   * Name of the person.
   * -- SETTER --
   * Changes the name of this person.
   * 
   * @param name The new value.
   */
  @Setter(AccessLevel.PROTECTED) private String name;
}
// 변환 후
public class GetterSetterExample {
  /**
   * Age of the person. Water is wet.
   */
  private int age = 10;

  /**
   * Name of the person.
   */
  private String name;
  
  /**
   * Age of the person. Water is wet.
   *
   * @return The current value of this person's age. Circles are round.
   */
  public int getAge() {
    return age;
  }
  
  /**
   * Age of the person. Water is wet.
   *
   * @param age New value for this person's age. Sky is blue.
   */
  public void setAge(int age) {
    this.age = age;
  }
  
  /**
   * Changes the name of this person.
   *
   * @param name The new value.
   */
  protected void setName(String name) {
    this.name = name;
  }
}

@ToString

@ToString 어노테이션 같은 경우는 Class에 있는 toString을 재정의 해준다. 기본적인 toString은 해당 객체 주소값을 return하게 되는데 @ToString 어노테이션을 사용하면 명시적으로 Class 안에 있는 필드들의 이름과 값을 포함하는 문자열을 return 해준다. @ToString.Exclude 어노테이션을 통해 원하지 않는 필드는 제외 시킬 수 있다. 또한 @ToString.Include 혹은 @ToString(onlyExplicitlyIncluded = true)를 통해 해당 필드들만 포함 시키도록 할 수 있다. 또한 @ToString(callSuper = true )설정을 해주게 되면 부모에 있는 ToString도 포함시키게 된다. @ToString.Include(name = “other name”) 설정을 해주게 되면 필드이름을 내가 원하는 이름으로 변경시킬 수 있다. @ToString.Include(rank = -1)설정을 해주게 되면 어떤게 먼저 출력되야 할지 우선순위를 정할 수 있게 해준다. 소스를 통해 이해해 보도록 하자

 @ToString
public class ToStringExample {
  private static final int STATIC_VAR = 10;
  private String name;
  private Shape shape = new Square(5, 10);
  private String[] tags;
  @ToString.Exclude private int id;
  
  public String getName() {
    return this.name;
  }
  
  @ToString(callSuper=true, includeFieldNames=true)
  public static class Square extends Shape {
    private final int width, height;
    
    public Square(int width, int height) {
      this.width = width;
      this.height = height;
    }
  }
}

// 변환 후
public class ToStringExample {
  private static final int STATIC_VAR = 10;
  private String name;
  private Shape shape = new Square(5, 10);
  private String[] tags;
  private int id;
  
  public String getName() {
    return this.name;
  }
  
  public static class Square extends Shape {
    private final int width, height;
    
    public Square(int width, int height) {
      this.width = width;
      this.height = height;
    }
    
    @Override public String toString() {
      return "Square(super=" + super.toString() + ", width=" + this.width + ", height=" + this.height + ")";
    }
  }
  
  @Override public String toString() {
    return "ToStringExample(" + this.getName() + ", " + this.shape + ", " + Arrays.deepToString(this.tags) + ")";
  }
}

@EqualsAndHashCode

해당 어노테이션은 equals메소드와 hashCode 메소드를 재정의 해주는 어노테이션입니다. 이것은 정적이지 않은 곳에서만 사용 가능하다. 보통 객체의 equals와 hashCode는 객체가 가지고 있는 필드들을 통해 작성이 되는데 특정 필드들을 제외하고 작성을 하고 싶을 때 유용하게 사용할 수 있는 어노테이션이다 . 하지만 다른 클래스를 확장하는 클래스인 경우 해당 어노테이션을 사용하게 되면 해당 기능은 많이 까다로워 진다고 한다. 일반적으로 이러한 클래스에 자동으로 equals와 hashCode를 생성 해주는 건 좋은 방법이 아니다 부모 클래스 또한 필드들을 가지고 있는데 부모 클래스에서는 해당 함수들이 생성되지 않기 때문이다. 그래서 callSuper 을 true로 설정해 주면 equals와 hashCode들이 생성이 된다. 그리고 자식 객체의 hash 알고리즘에 부모의 hashCode가 포함이 되고 equals에 부모객체와 비교하는 로직도 추가가 된다. 만약 아무것도 상속하지 않는다면 callSuper를 true로 설정 했을 때 컴파일할 때 에러가 발생하게 된다. 한번 코드를 통해 알아보자

@EqualsAndHashCode
public class EqualsAndHashCodeExample {
  private transient int transientVar = 10;
  private String name;
  private double score;
  @EqualsAndHashCode.Exclude private Shape shape = new Square(5, 10);
  private String[] tags;
  @EqualsAndHashCode.Exclude private int id;
  
  public String getName() {
    return this.name;
  }
  
  @EqualsAndHashCode(callSuper=true)
  public static class Square extends Shape {
    private final int width, height;
    
    public Square(int width, int height) {
      this.width = width;
      this.height = height;
    }
  }
}

// 변경 후
public class EqualsAndHashCodeExample {
  private transient int transientVar = 10;
  private String name;
  private double score;
  private Shape shape = new Square(5, 10);
  private String[] tags;
  private int id;
  
  public String getName() {
    return this.name;
  }
  
  @Override public boolean equals(Object o) {
    if (o == this) return true;
    if (!(o instanceof EqualsAndHashCodeExample)) return false;
    EqualsAndHashCodeExample other = (EqualsAndHashCodeExample) o;
    if (!other.canEqual((Object)this)) return false;
    if (this.getName() == null ? other.getName() != null : !this.getName().equals(other.getName())) return false;
    if (Double.compare(this.score, other.score) != 0) return false;
    if (!Arrays.deepEquals(this.tags, other.tags)) return false;
    return true;
  }
  
  @Override public int hashCode() {
    final int PRIME = 59;
    int result = 1;
    final long temp1 = Double.doubleToLongBits(this.score);
    result = (result*PRIME) + (this.name == null ? 43 : this.name.hashCode());
    result = (result*PRIME) + (int)(temp1 ^ (temp1 >>> 32));
    result = (result*PRIME) + Arrays.deepHashCode(this.tags);
    return result;
  }
  
  protected boolean canEqual(Object other) {
    return other instanceof EqualsAndHashCodeExample;
  }
  
  public static class Square extends Shape {
    private final int width, height;
    
    public Square(int width, int height) {
      this.width = width;
      this.height = height;
    }
    
    @Override public boolean equals(Object o) {
      if (o == this) return true;
      if (!(o instanceof Square)) return false;
      Square other = (Square) o;
      if (!other.canEqual((Object)this)) return false;
      if (!super.equals(o)) return false;
      if (this.width != other.width) return false;
      if (this.height != other.height) return false;
      return true;
    }
    
    @Override public int hashCode() {
      final int PRIME = 59;
      int result = 1;
      result = (result*PRIME) + super.hashCode();
      result = (result*PRIME) + this.width;
      result = (result*PRIME) + this.height;
      return result;
    }
    
    protected boolean canEqual(Object other) {
      return other instanceof Square;
    }
  }
}

@NoArgsConstructor, @RequiredArgsConstructor, @AllArgsConstructor

이 3가지 어노테이션은 1개의 특정 필드를 허용하고 간단하게 매개변수를 해당 필드에 할당해주는 생성자를 생성해 준다.

@NoArgsConstructor는 파라미터가 없는 생성자를 생성한다. 만약 final로 정의된 필드가 있다면 @NoArgsConstructor(force = true)를 사용하지 않았다면 컴파일 에러가 발생한다 만약 force를 true로 설정하게 되었으면 컴파일에러가 아닌 0,false,null로 초기화가 된다. @NonNull와 같은 필드에 제약이 있을 경우 해당 제약을 체크하는 로직이 생성되지 않는다 그렇기 때문에 해당 제약들은 해당 필드가 나중에 필드가 제대로 초기화 될 때 까지 충족되지 않을 것이다. 해당 어노테이션은 hibernate나 Service Provider Interface처럼 파라미터가 없는 빈 생성자를 필요로 하는 부분에 유용하게 사용이 된다.

@RequiredArgsConstructor은 특별하게 처리되어야 할 각 필드에 대해 1개의 매개변수가 있는 생성자를 생성한다. 모든 초기화 되지 않은 final필드와 선언되지 않아 초기화 되지 않은 @NonNull로 표시된 필드들을 포함한다. 만약 @NonNull으로 표시된 필드가 있다면 null check 로직도 해당 생성자에 생성이 된다. 그렇기 때문에 모든 필드가 @NonNull이고 파라미터가 null이 들어오게 되면 NullPointException을 발생 시킨다. 해당 매개변수의 순서는 필드가 선언된 순서를 따른다.

@AllArgsConstructor는 Class의 모든 필드에 대해 1개의 매개변수가 있는 생성자를 생성한다. @NonNull로 선언된 필드가 있다면 null check로직도 같이 생성이 된다.

각각의 어노테이션은 생성된 생성자가 항상 private이고 private 생성자를 통한 정적 팩토리 메소드를 생성하는 대체 적인 형식을 허용합니다. 이 모드는 @RequiredArgsConstructor(staticName=”of”)와 같은 staticName이라는 값을 제공합니다. 이와 같은 정적 팩토리 메소드는 제네릭을 추론합니다 일반 생성자와는 다르게 이것의 의미는 만약 너가 new MapEntry<String,Integer>(”foo”,5) 이런식으로 선언해야할 때 MapEntry.of(”foo”,5)로 선언이 가능하다는 것이다. 코드를 통해 알아 보도록 하자

@RequiredArgsConstructor(staticName = "of")
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class ConstructorExample<T> {
  private int x, y;
  @NonNull private T description;
  
  @NoArgsConstructor
  public static class NoArgsExample {
    @NonNull private String field;
  }
}

// 변환 후
public class ConstructorExample<T> {
  private int x, y;
  @NonNull private T description;
  
  private ConstructorExample(T description) {
    if (description == null) throw new NullPointerException("description");
    this.description = description;
  }
  
  public static <T> ConstructorExample<T> of(T description) {
    return new ConstructorExample<T>(description);
  }
  
  @java.beans.ConstructorProperties({"x", "y", "description"})
  protected ConstructorExample(int x, int y, T description) {
    if (description == null) throw new NullPointerException("description");
    this.x = x;
    this.y = y;
    this.description = description;
  }
  
  public static class NoArgsExample {
    @NonNull private String field;
    
    public NoArgsExample() {
    }
  }
}

@Data

해당 어노테이션은 @ToString, @EqualsAndHashCode, @Getter / @Setter 그리고 @RequiredArgsConstructor 어노테이션 집합에 대한 보일러플레이트를 줄일 수 있는 그런 편리한 shortcut 어노테이션이다. 그래서 @Data 어노테이션으로 대체가 가능하다. 다시말해 @Data 어노테이션은 해당 어노테이션들을 포함하고 있는 어노테이션이다. 하지만 callSuper, includeFieldNames 그리고 exclude는 @Data 어노테이션으로 설정할 수 없다. 이런 매개변수에 대한 설정을 하지 않는 경우는 그냥 @Data 어노테이션만 붙여주면 된다.

Getter와 Setter 같은 경우에는 항상 public으로 선언 되기 때문에 따로 Access Level을 설정해야 할때는 @Getter @Setter 어노테이션을 사용하도록 하자

모든 필드가 transient로 선언이 되어있다면 hashCode와 equals함수는 고려하지 않아도된다. 모든 static 필드는 전적으로 무시된다. (Getter, Setter 등등 생성하지 않음)

만약 클래스가 이미 같은 이름 및 매개변수 수가 동일한 메소드를 포함되고 있는 경우 메소드는 생성되지 않는다. 이는 생성자도 마찬가지로 적용된다. 코드로 한번 알아보자

@Data public class DataExample {
  private final String name;
  @Setter(AccessLevel.PACKAGE) private int age;
  private double score;
  private String[] tags;
  
  @ToString(includeFieldNames=true)
  @Data(staticConstructor="of")
  public static class Exercise<T> {
    private final String name;
    private final T value;
  }
}

//변환 후
public class DataExample {
  private final String name;
  private int age;
  private double score;
  private String[] tags;
  
  public DataExample(String name) {
    this.name = name;
  }
  
  public String getName() {
    return this.name;
  }
  
  void setAge(int age) {
    this.age = age;
  }
  
  public int getAge() {
    return this.age;
  }
  
  public void setScore(double score) {
    this.score = score;
  }
  
  public double getScore() {
    return this.score;
  }
  
  public String[] getTags() {
    return this.tags;
  }
  
  public void setTags(String[] tags) {
    this.tags = tags;
  }
  
  @Override public String toString() {
    return "DataExample(" + this.getName() + ", " + this.getAge() + ", " + this.getScore() + ", " + Arrays.deepToString(this.getTags()) + ")";
  }
  
  protected boolean canEqual(Object other) {
    return other instanceof DataExample;
  }
  
  @Override public boolean equals(Object o) {
    if (o == this) return true;
    if (!(o instanceof DataExample)) return false;
    DataExample other = (DataExample) o;
    if (!other.canEqual((Object)this)) return false;
    if (this.getName() == null ? other.getName() != null : !this.getName().equals(other.getName())) return false;
    if (this.getAge() != other.getAge()) return false;
    if (Double.compare(this.getScore(), other.getScore()) != 0) return false;
    if (!Arrays.deepEquals(this.getTags(), other.getTags())) return false;
    return true;
  }
  
  @Override public int hashCode() {
    final int PRIME = 59;
    int result = 1;
    final long temp1 = Double.doubleToLongBits(this.getScore());
    result = (result*PRIME) + (this.getName() == null ? 43 : this.getName().hashCode());
    result = (result*PRIME) + this.getAge();
    result = (result*PRIME) + (int)(temp1 ^ (temp1 >>> 32));
    result = (result*PRIME) + Arrays.deepHashCode(this.getTags());
    return result;
  }
  
  public static class Exercise<T> {
    private final String name;
    private final T value;
    
    private Exercise(String name, T value) {
      this.name = name;
      this.value = value;
    }
    
    public static <T> Exercise<T> of(String name, T value) {
      return new Exercise<T>(name, value);
    }
    
    public String getName() {
      return this.name;
    }
    
    public T getValue() {
      return this.value;
    }
    
    @Override public String toString() {
      return "Exercise(name=" + this.getName() + ", value=" + this.getValue() + ")";
    }
    
    protected boolean canEqual(Object other) {
      return other instanceof Exercise;
    }
    
    @Override public boolean equals(Object o) {
      if (o == this) return true;
      if (!(o instanceof Exercise)) return false;
      Exercise<?> other = (Exercise<?>) o;
      if (!other.canEqual((Object)this)) return false;
      if (this.getName() == null ? other.getValue() != null : !this.getName().equals(other.getName())) return false;
      if (this.getValue() == null ? other.getValue() != null : !this.getValue().equals(other.getValue())) return false;
      return true;
    }
    
    @Override public int hashCode() {
      final int PRIME = 59;
      int result = 1;
      result = (result*PRIME) + (this.getName() == null ? 43 : this.getName().hashCode());
      result = (result*PRIME) + (this.getValue() == null ? 43 : this.getValue().hashCode());
      return result;
    }
  }
}

@Value

해당 어노테이션은 @Data 어노테이션의 불변 버전이다 모든 필드가 기본적으로 private 그리고 final로 설정되며 setter는 생성되지 않는다. 그리고 불변은 하위 클래스에 강제될 수 있는 것이 아니기 때문에 기본적으로 클래스도 final으로 선언된다. @Data 처럼 유용하게 toString(), equlas() 그리고 hashCode()을 자동으로 생성해주고 각 필드는 Getter 메소드를 가진다 코드를 통해 알아보도록 하자

@Value public class ValueExample {
  String name;
  @With(AccessLevel.PACKAGE) @NonFinal int age;
  double score;
  protected String[] tags;
  
  @ToString(includeFieldNames=true)
  @Value(staticConstructor="of")
  public static class Exercise<T> {
    String name;
    T value;
  }
}

//변환 후
public final class ValueExample {
  private final String name;
  private int age;
  private final double score;
  protected final String[] tags;
  
  @java.beans.ConstructorProperties({"name", "age", "score", "tags"})
  public ValueExample(String name, int age, double score, String[] tags) {
    this.name = name;
    this.age = age;
    this.score = score;
    this.tags = tags;
  }
  
  public String getName() {
    return this.name;
  }
  
  public int getAge() {
    return this.age;
  }
  
  public double getScore() {
    return this.score;
  }
  
  public String[] getTags() {
    return this.tags;
  }
  
  @java.lang.Override
  public boolean equals(Object o) {
    if (o == this) return true;
    if (!(o instanceof ValueExample)) return false;
    final ValueExample other = (ValueExample)o;
    final Object this$name = this.getName();
    final Object other$name = other.getName();
    if (this$name == null ? other$name != null : !this$name.equals(other$name)) return false;
    if (this.getAge() != other.getAge()) return false;
    if (Double.compare(this.getScore(), other.getScore()) != 0) return false;
    if (!Arrays.deepEquals(this.getTags(), other.getTags())) return false;
    return true;
  }
  
  @java.lang.Override
  public int hashCode() {
    final int PRIME = 59;
    int result = 1;
    final Object $name = this.getName();
    result = result * PRIME + ($name == null ? 43 : $name.hashCode());
    result = result * PRIME + this.getAge();
    final long $score = Double.doubleToLongBits(this.getScore());
    result = result * PRIME + (int)($score >>> 32 ^ $score);
    result = result * PRIME + Arrays.deepHashCode(this.getTags());
    return result;
  }
  
  @java.lang.Override
  public String toString() {
    return "ValueExample(name=" + getName() + ", age=" + getAge() + ", score=" + getScore() + ", tags=" + Arrays.deepToString(getTags()) + ")";
  }
  
  ValueExample withAge(int age) {
    return this.age == age ? this : new ValueExample(name, age, score, tags);
  }
  
  public static final class Exercise<T> {
    private final String name;
    private final T value;
    
    private Exercise(String name, T value) {
      this.name = name;
      this.value = value;
    }
    
    public static <T> Exercise<T> of(String name, T value) {
      return new Exercise<T>(name, value);
    }
    
    public String getName() {
      return this.name;
    }
    
    public T getValue() {
      return this.value;
    }
    
    @java.lang.Override
    public boolean equals(Object o) {
      if (o == this) return true;
      if (!(o instanceof ValueExample.Exercise)) return false;
      final Exercise<?> other = (Exercise<?>)o;
      final Object this$name = this.getName();
      final Object other$name = other.getName();
      if (this$name == null ? other$name != null : !this$name.equals(other$name)) return false;
      final Object this$value = this.getValue();
      final Object other$value = other.getValue();
      if (this$value == null ? other$value != null : !this$value.equals(other$value)) return false;
      return true;
    }
    
    @java.lang.Override
    public int hashCode() {
      final int PRIME = 59;
      int result = 1;
      final Object $name = this.getName();
      result = result * PRIME + ($name == null ? 43 : $name.hashCode());
      final Object $value = this.getValue();
      result = result * PRIME + ($value == null ? 43 : $value.hashCode());
      return result;
    }
    
    @java.lang.Override
    public String toString() {
      return "ValueExample.Exercise(name=" + getName() + ", value=" + getValue() + ")";
    }
  }
}

@Builder

해당 어노테이션은 클래스에서 복잡한 Builder Api를 제공해 준다.

빌터 패턴이란 아래 코드와 같은 형태를 이야기한다.

Person.builder()
.name("Adam Savage")
.city("San Francisco")
.job("Mythbusters")
.job("Unchained Reaction")
.build();

@Builder 어노테이션은 클래스 또는 생성자, 메소드에 위치할 수 있다. 그래서 클래스와 생성자 모드가 가장 일반적인 사례인 반면 메소드 사용 사례로 가장 쉽게 설명된다.

@Builder 어노테이션이 붙은 메소드는 다음 7가지를 생성한다

  • static 메소드와 동일한 argument 타입을 가진 XXXBuilder라 불리는 inner static class
  • 빌더 안에서는 대상의 각 매개변수에 대해 하나의 private non-static, non-final 필드
  • 빌더 안에서는 아무런 매개변수가 없는 private 빈 생성자
  • 빌더 안에서는 대상의 각 매개변수에 대한 세터와 같은 매소드 그것은 해당 매개변수와 유형 및 이름이 동일하다. 그것은 setter 메소드가 연결되게 builder 그 자신을 return 한다.
  • 빌더 안에서는 build()라는 메소드가 있는데 이 메소드는 각 필드에게 설정한 값들을 넘겨주고 대상 객체를 return 하는 메소드이다.
  • 빌더안에서는 또한 합리적인 toString() 메소드를 구현한다
  • 빌더의 새 인스턴스를 생성하는 메소드인 builder()를 생성한다.

나열된 각 생성요소들은 해당요소가 이미 있는 경우에는 넘어갑니다. 만약 해당 클래스가 이미 존재한다면 lombok은 주입할 필드 메서드가 이미 존재하지 않는 한 이미 존재하는 클래스 내부에 필드와 메서드 주입을 시작한다.

빌더 클래스에 lombok 어노테이션을 생성하는 다른 메서드 또는 생성자를 넣을 수는 없다. 예를 들면 @EqualsAndHashCode같은 어노테이션

@Builder 어노테이션은 collection 필드 및 매개변수를 위한 단일 메소드라고 불리는 메소드를 생성할 수 있다. 이들은 전체 리스트 대신 1개의 요소를 취하고 요소를 리스트에 추가한다. 예를 들면 아래와 같다.

Person.builder()
.job("Mythbusters")
.job("Unchained Reaction")
.build();

해당 코드는 리스트에 2개의 문자열이 포함이 된다. 이러한 동작을 하기 위해서는 필드 및 매개변수에 @Singular 어노테이션을 작성해 주어야한다.

또한 기본적으로 Builder 패턴을 통해 처리되지 않은 필드들은 0,null,false로 초기화가 된다. 그렇기 때문에 내가 default 값을 넣고싶으면 @Builder.Default 어노테이션을 사용하면 된다.

코드로 한번 알아보도록 하자

@Builder
public class BuilderExample {
  @Builder.Default private long created = System.currentTimeMillis();
  private String name;
  private int age;
  @Singular private Set<String> occupations;
}

//변환 후
public class BuilderExample {
  private long created;
  private String name;
  private int age;
  private Set<String> occupations;
  
  BuilderExample(String name, int age, Set<String> occupations) {
    this.name = name;
    this.age = age;
    this.occupations = occupations;
  }
  
  private static long $default$created() {
    return System.currentTimeMillis();
  }
  
  public static BuilderExampleBuilder builder() {
    return new BuilderExampleBuilder();
  }
  
  public static class BuilderExampleBuilder {
    private long created;
    private boolean created$set;
    private String name;
    private int age;
    private java.util.ArrayList<String> occupations;
    
    BuilderExampleBuilder() {
    }
    
    public BuilderExampleBuilder created(long created) {
      this.created = created;
      this.created$set = true;
      return this;
    }
    
    public BuilderExampleBuilder name(String name) {
      this.name = name;
      return this;
    }
    
    public BuilderExampleBuilder age(int age) {
      this.age = age;
      return this;
    }
    
    public BuilderExampleBuilder occupation(String occupation) {
      if (this.occupations == null) {
        this.occupations = new java.util.ArrayList<String>();
      }
      
      this.occupations.add(occupation);
      return this;
    }
    
    public BuilderExampleBuilder occupations(Collection<? extends String> occupations) {
      if (this.occupations == null) {
        this.occupations = new java.util.ArrayList<String>();
      }

      this.occupations.addAll(occupations);
      return this;
    }
    
    public BuilderExampleBuilder clearOccupations() {
      if (this.occupations != null) {
        this.occupations.clear();
      }
      
      return this;
    }

    public BuilderExample build() {
      // complicated switch statement to produce a compact properly sized immutable set omitted.
      Set<String> occupations = ...;
      return new BuilderExample(created$set ? created : BuilderExample.$default$created(), name, age, occupations);
    }
    
    @java.lang.Override
    public String toString() {
      return "BuilderExample.BuilderExampleBuilder(created = " + this.created + ", name = " + this.name + ", age = " + this.age + ", occupations = " + this.occupations + ")";
    }
  }
}

@Synchronized

해당 어노테이션은 동기화 메서드의 더 안전한 변형이다. synchronized와 마찬가지로 어노테이션은 static 및 생성 매소드에서만 사용가능하다. 이 어노테이션은 synchronized 키워드와 비슷하게 동작한다 하지만 그것은 다른 객체에 lock를 건다. synchronized 키워드는 this에 lock을 걸지만 어노테이션은 private $lock이라는 필드에 lock을 건다. 만약 이 필드가 존재하지 않는다면 그것은 해당 필드를 만들어 줄것이다. 만약 정적 메서드에 어노테이션을 추가하면 어노테이션이 대신 $LOCK이라는 정적 필드에서 lock를 건다. 원한다면 너는 lock을 걸 객체에 대해 직접 생성할 수 있다. 그 $lock 그리고 $LOCK 필드가 이미 존재한다면 생성되지 않는다. 해당 어노테이션을 매개변수로 지정하여 다른 필드에 lock을 걸도록 선택할 수 있다. 이런 경우에는 필드가 자동으로 생성되지 않으며 직접 생성 해야한다. 그렇지 않으면 오류가 발생한다. this 또는 자신의 클래스 객체에 lock을 걸게 되면 side-effect가 발생 할 수 있다. 왜냐면 내가 통제하지 못하는 다른 코드도 이러한 객체를 잠글 수 있기 때문에 Race Condition이 발생하거나 쓰레딩 관련한 버그를 유발할수 있기 때문이다. 코드를 통해 한번 알아보자

public class SynchronizedExample {
  private final Object readLock = new Object();
  
  @Synchronized
  public static void hello() {
    System.out.println("world");
  }
  
  @Synchronized
  public int answerToLife() {
    return 42;
  }
  
  @Synchronized("readLock")
  public void foo() {
    System.out.println("bar");
  }
}

// 변환 후
public class SynchronizedExample {
  private static final Object $LOCK = new Object[0];
  private final Object $lock = new Object[0];
  private final Object readLock = new Object();
  
  public static void hello() {
    synchronized($LOCK) {
      System.out.println("world");
    }
  }
  
  public int answerToLife() {
    synchronized($lock) {
      return 42;
    }
  }
  
  public void foo() {
    synchronized(readLock) {
      System.out.println("bar");
    }
  }
}

@With

불변 프로퍼티와 같이 setter메소드를 적용할 수 없다 하지만 해당 프로퍼티만 변경한 값을 가지고 싶을 때 안전한 방법으로 변경하고자 하는 값을 가진 새로운 객체를 만들어 내는 방법이 있는데 해당 어노테이션이 그 부분을 지원해 준다. 코드를 통해 알아보도록 하자

public class WithExample {
  @With(AccessLevel.PROTECTED) @NonNull private final String name;
  @With private final int age;
  
  public WithExample(@NonNull String name, int age) {
    this.name = name;
    this.age = age;
  }
}

//변환 후
public class WithExample {
  private @NonNull final String name;
  private final int age;

  public WithExample(String name, int age) {
    if (name == null) throw new NullPointerException();
    this.name = name;
    this.age = age;
  }

  protected WithExample withName(@NonNull String name) {
    if (name == null) throw new java.lang.NullPointerException("name");
    return this.name == name ? this : new WithExample(name, age);
  }

  public WithExample withAge(int age) {
    return this.age == age ? this : new WithExample(name, age);
  }
}

몇개 빼먹은 어노테이션이 있긴한데 별로 중요하지 않아보여서 작성을 따로 하진 않았다 그리고 문서에 적힌대로 최대한 해석하긴 했지만 아마 빠져있는 내용도 있을 것이고 해석이 잘못 되었을수도 있다 그래서 다른 어노테이션이 궁금하거나 더 정확한 정보를 원한다면 https://projectlombok.org/features/ 해당 사이트에서 직접 봐보는 것을 추천한다. 이렇게 lombok에 대해 살펴보게 되었는데 자바의 불편함을 많이 해소시켜주는 것 같았다. 하지만 코틀린을 사용한 나로써는 코틀린에서 거의 지원하는 내용이어서 그렇게 막 신기하거나 그러진 않았다. 그래도 이제 자바를 많이 사용하게 될 것 같아서 알아 둘겸 다른 자바를 사용하는 사람들이 유용하게 볼수 있도록 작성해 보았는데 이런 불편한 부분들을 해소시켜주는 라이브러리를 나도 한번 만들어보고 싶었다.

profile
Programmer

0개의 댓글