lombok을 잘 써보자! (1)

java 개발자에 있어 lombok은 아주 좋은 라이브러리이다. 어노테이션 하나로 자동으로 바이트코드를 만들어주니 더 할 것이 없는 라이브러리이다. 다른 언어들은 언어 자체에서 지원해주긴 하지만..
필자도 아주아주 잘 쓰지는 못하지만 필자가 아는 것만큼 포스팅을 해보자!

@Data

lombok을 사용한다면 제일 많이 사용하는 어노테이션이다. 이 어노테이션은 다재다능한 기능이다. 사용하는 사람은 알겠지만 getter, setter, toString, hasCode, equals, constructor 등 많은 부분을 자동으로 생성해준다.
각각 부분적으로는 밑에서 설명하도록 하겠다.
@Data 어노테이션에는 속성이 한개 있는데 staticConstructor 라는 속성이다. 말그대로 static한 생성자? 를 만들어 주는 것이다.

@Data(staticConstructor = "of")
public class DataObject {
  private final Long id;
  private String name;
}

위와 같이 선언한다면 다음과 같이 사용가능하다.

DataObject dataObject = DataObject.of(1L);

id 경우에는 final이라 필수 생성자에 포함되어 있다. 만일 위와 같이 사용한다면 new로 생성할 수 없다.

DataObject dataObject2 = new DataObject(); // compile error

위와 같이 new 를 이용해 생성할 시에는 컴파일 에러가 발생한다.

위의 여러 메서드를 제외하고 한개의 메서드가 더 생성이 되는데 그 메서드는 canEqual 이라는 메서드이다. 해당 메서드의 역할은 instanceof로 타입정도만 체크 한다. 하지만 메서드의 접근제한자는 protected 이다. 필자도 사용할 일이 없었다.

XXXXArgsConstructor

위의 어노테이션은 생성자를 생성해주는 어노테이션이다. 생성자를 생성해주는 어노테이션은 3가지가 있다. 첫 번째는 디폴트 생성자를 생성해주는 @NoArgsConstructor 두 번째는 모든 필드의 생성자를 생성해주는 @AllArgsConstructor 마지막으로 필수 생성자를 생성해주는 @RequiredArgsConstructor 가 있다.
속성에는 여러가지 있는데 자주 사용할 법한 속성들만 설명하겠다. (필자가 아는 것 만..)
1. staticName : 위에서 @Data 어노테이션의 staticConstructor 와 동일하다. static한 생성자를 만들어 준다.
2. access : 접근제한을 할 수 있다. PUBLIC, MODULE, PROTECTED, PACKAGE, PRIVATE 등으로 설정가능 하다.
3. onConstructor : 생성자에 어노테이션을 작성할 수 있다.
공통적인 속성으로는 위와 같이 3가지가 존재한다.

@RequiredArgsConstructor(staticName = "of", onConstructor = @__(@Inject))
public class ConstructorObject {
  private final Long id;
  private final String name;
}

만약 위와 같은 어노테이션을 작성할 경우에는 다음과 같은 코드가 나올 것이라고 예상해본다.

class ConstructorObjectNot {
  private final Long id;
  private final String name;

  @Inject
  private ConstructorObjectNot(Long id, String name) {
    this.id = id;
    this.name = name;
  }
  public static ConstructorObjectNot of(Long id, String name) {
    return new ConstructorObjectNot(id, name);
  }
}

staticName가 존재해 access는 별루 의미가 없어 사용하지 않았다.

@Getter와 @Setter

어노테이션 이름 그대로 getter와 setter를 생성해준다. 클래스 레벨에도 사용가능하며 필드 레벨에도 사용가능하다.
공통 속성으로는 value, onMethod 속성이 존재한다. value의 경우에는 접근 제한을 할 수 있으며 onMethod 메서드의 어노테이션을 작성할 수 있다.

public class GetSetObject {

  @Getter(value = AccessLevel.PACKAGE, onMethod = @__({@NonNull, @Id}))
  private Long id;
}

만약 위와 같은 코드를 작성하였을 경우에는 다음과 같은 코드가 작성 될 것이다.

class GetSetObjectOnMethod {
  private Long id;

  @Id
  @NonNull
  Long getId() {
    return id;
  }
}

물론 @Setter 어노테이션에도 onMethod를 사용할 수 있다.

@Setter(onMethod = @__({@NotNull}))

그리고 @Getter, @Setter 각각이 다른 속성들을 한개씩 가지고 있는데. @Getter인 경우에는 lazy 속성이고 @Setter의 경우에는 onParam 이라는 속성이다.
@Getter 의 lazy 속성은 속성명 그대로 필드의 값은 지연시킨다는 것이다.

@Getter(value = AccessLevel.PUBLIC, lazy = true)
private final String name = expensive();

private String expensive() {
  return "wonwoo";
}

lazy가 true일때는 무조건 final 필드어야만 한다. lazy 속성이 false 일 경우에는 객체를 생성할 때 expensive() 메서드를 호출하지만 속성이 true일 경우에는 getName() 메서드를 호출할 때 expensive() 메서드를 호출 한다.

다음은 @Setter의 onParam 속성이다. 이 속성은 파라미터의 어노테이션을 작성할 수 있는 속성이다.

@Setter(onParam = @__(@NotNull))
private Long id;

만약 다음과 같은 코드가 있을 경우에는 아래와 같은 코드가 작성될 것이라고 판단된다.

class GetSetObjectOnParam {
  private Long id;

  public void setId(@NotNull Long id) {
    this.id = id;
  }
}

아주 간단한 코드이다. (물론 만든 사람은 아니겠지만..)

IDEA에서는 onParamonMethod에 @Column 어노테이션의 속성을 넣으면 잘 동작하지 않는다. onParam 은 파라미터에 적용되니 @Column 자체가 들어 갈 수 없으니 그렇다 쳐도 onMethod는 왜안되는지.. 플러그인 문제인듯 싶다.

@EqualsAndHashCode 와 @ToString

@EqualsAndHashCode 어노테이션은 이름 그대로 hashcode와 equals를 생성해주는 어노테이션이고, @ToString도 마찬가지로 toString() 메서드를 생성해주는 어노테이션이다.
공통 속성으로는 4가지 있는데 exclude, of, callSuper, doNotUseGetters가 존재 한다. exclude는 제외시킬 변수명을 작성하면 되고 of는 포함시킬 변수명을 작성하면 된다. callSuper 속성은 상위 클래스의 호출 여부를 묻는 속성이다. 마지막으로 doNotUseGetters의 속성은 getter 사용여부 인듯 하나 제대로 동작하지는 모르겠다.

@EqualsAndHashCode(of = "id")
@ToString(exclude = "name")
public class HashCodeAndEqualsObject {
  private Long id;
  private String name;
}

만일 위와 같이 작성하였다면 hasCode, equals, toString 모두 id만 존재하게 된다.
각각의 속성으로는 @EqualsAndHashCode 는 onParam, @ToString 는 includeFieldNames 속성이 존재한다. onParam 은 equals에 작성되며 위의 onParam 속성과 동일하므로 생략한다. includeFieldNames는 toString의 필드 명을 출력할지 하지 않을지의 여부이다. 만일 위의 코드로 includeFieldNames을 false로 한다면 다음과 같이 출력 된다.

HashCodeAndEqualsObject(null)

참고로 canEqual 메서드도 @EqualsAndHashCode 메서드에 포함되어 있다.

@val 와 @var

스칼라, 코틀린 이외에 다른 언어들의 키워드와 동일하게 타입추론을 한다.

public class ValAndVarTests {
  @Test
  public void valVarTest() {
    val arrVal = Arrays.asList(1, 2, 3, 4, 5);
    arrVal = new ArrayList<>(); // compile error

    var arrVar = Arrays.asList(1, 2, 3, 4, 5);
    arrVar = new ArrayList<>();
  }
}

val 경우에는 final 키워드가 생성된다. 그래서 다시 어사인을 할 경우에 컴파일 에러가 발생한다. 마찬가지로 var는 final이 존재 하지 않으므로 다시 어사인이 가능하다. 위의 코드를 다시 만들어 보면 다음과 같을 것으로 예상된다.

final List<Integer> arrVal1 = Arrays.asList(1, 2, 3, 4, 5);
arrVal1 = new ArrayList<>();

List<Integer> arrVar1 = Arrays.asList(1, 2, 3, 4, 5);
arrVar1 = new ArrayList<>();

위와 동일한 바이트코드가 나올 것으로 예상해본다.

@UtilityClass

유틸리티 클래스에 적용하면 되는 어노테이션이다. 만약 이 어노테이션을 작성하면 기본생성자가 private 생성되며 만약 리플렉션 혹은 내부에서 생성자를 호출할 경우에는 UnsupportedOperationException이 발생한다.

@UtilityClass
public class UtilityClassObject {
  public static String name() {
    return "wonwoo;";
  }
}

만약 위의 코드를 다시 작성해보면 다음과 같다.

class UtilityClassObjectNot {
  private UtilityClassObjectNot() {
    throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
  }
  public static String name() {
    return "wonwoo;";
  }
}

이번시간에는 이정도로 마무리를 짓도록 하자. 오늘은 자주 사용하는 lombok에 대해서 알아봤다. 다음에는 자주 사용은 하지 않지만 특이하거나 재미있는 코드를 살펴보도록 하자. (물론 자주 사용하는 것도 있다.)

오늘은 여기까지!

javax.annotation.* (jsr 305)

오늘은 jsr305 스펙에 대해서 몇가지 알아보자.
jsr305 는 소프트웨어 결함 탐지를 위한 어노테이션이다. 이렇게 말하면 뭔말인지 모르니 한번 해보도록하자.
com.google.code.findbugs 의 jsr305를 디펜더스 받자. 왜 google로 되어있는지는 모르겠다. 만든 사람이 구글에 다니던 시절에 만들어서 그런가? findbugs 프로젝트보면 다양한게 많이 있는 듯한데.. 자세히는 보지 않았다.
일단 아래와 같이 디펜더시를 받자.

<dependency>
    <groupId>com.google.code.findbugs</groupId>
    <artifactId>jsr305</artifactId>
    <version>3.0.1</version>
    <scope>provided</scope>
</dependency>

그리고 나서 하나씩 살펴보도록 하자. 일단 기준은 IDEA이다. 이클립스나 넷빈즈는 어떻게 동작하지는 잘 모르겠다.
스펙들은 아주 많은데 그 중에 IDEA가 지원해주거나 혹은 자주 사용할 것 같은 어노테이션 위주로 살펴보도록 할 것이다.

@Nonnull

null이 아님을 의미한다. 이 어노테이션이 붙는 다면 null이 아니여야 한다. 파라미터나 리턴(메서드위)에 이 어노테이션을 넣을 수 있다. 인텔리 기준에서는 해당 메서드를 호출시 힌트가 발생한다.

public static int getLength(@Nonnull String name) {
  return name.length();
}

만약 위와 같은 코드가 있을 때 꼭 null이 아니여야 하므로 이 코드를 호출시에 null을 넣으면 경고(?) 힌트가 발생한다.

getLength(null); // null 에 힌트 

또 한 이렇게 메서드를 호출시에는 NPE이 발생하지 않고 다른 에러가 발생한다.

Exception in thread "main" java.lang.IllegalArgumentException: Argument for @Nonnull parameter 'name' of me/wonwoo/ClassTest.getLength must not be null

위와 같은 에러? 실제 컴파일을 해서 바이트코드를 보면 null 체크하는 코드가 삽입되서 들어간다. 파라미터뿐만 아니라 리턴타입에 넣어도 된다.

@Nonnull
public static Integer getLength(@Nonnull String name) {
  return null;  //null 에 힌트
}

@Nullable

@Nonnull 어노테이션과는 반대의 의미를 가지고 있다. null이 가능하다는 것을 의미한다. 역시 파라미터뿐 아니라 리턴타입에도 넣을 수 있다.

@Nullable
public static Integer getLength(@Nullable String name) {
  return name.length(); // 힌트
}

final Integer length = getLength("null");
length.toString(); //힌트

파라미터 name은 null이 들어 올 수 있으므로 NPE 발생할 여지가 있다. 그래서 length()를 그대로 호출 하면 안된다는 것이다.

@CheckReturnValue

위 어노테이션은 리턴 값 꼭 받아서 확인하라는 어노테이션이다.

@CheckReturnValue
public static Integer getLength(String name){
  return name.length();
}

getLength("null"); //힌트

getLength() 메서드를 호출할 때 리턴을 받지 않아 힌트가 주어졌다.

@OverridingMethodsMustInvokeSuper

이 어노테이션은 만약 슈퍼클래스의 메서드를 오버라이딩 할 때 꼭 슈퍼 클래스의 메서드를 호출 하라는 뜻 이다.

class SuperClass {
  @OverridingMethodsMustInvokeSuper
  public String name() {
    return "wonwoo";
  }
}

class SubClass extends SuperClass {

  @Override
  public String name() { //메서드 힌트
    return "won";
  }
}

SuperClass의 name 메서드를 오버라이딩 했지만 SuperClass의 name() 메서드를 호출하지 않아 메서드에 힌트가 발생하였다.

@ParametersAreNullableByDefault

@Nonnull 과 동일한 거 같은데? 이것은 파라미터에만 적용된다.

@ParametersAreNullableByDefault
public static List<String> lists(List<String> list) {
  if(list != null) {
    list.size();
  }
  list.size(); //힌트
  return list
}

파리미터 List<String> list 는 null이 가능하므로 if 문 밖에 있는 list.size()에 힌트가 주어졌다. null이 가능하므로 null 체크를 해줘야 할 것이다.

@ParametersAreNonnullByDefault

@ParametersAreNullableByDefault 이 어노테이션과는 반대되는 어노테이션이다. 파라미터가 null이 아니여야 한다는 뜻이다.

@ParametersAreNonnullByDefault
public static List<String> lists(List<String> list) {
  if(list != null) { //힌트
    list.size();
  }
  list.size();
  return list;
}

파라미터가 null이 아니여야 하므로 굳이 null 체크를 할 필요가 없다는 뜻이다. 그래서 null 체크 부분에서 힌트가 주어졌다.

일단 jsr305 어노테이션을 IDEA가 지원해주는 것은 여기까지 인 듯 하다. 더 있을 수는 있겠지만 그닥 사용하지 않을 듯 해서 살펴보지 않았다. 지원은 해주지 않지만 자주 사용할만한 어노테이션은 @Immutable, @NotThreadSafe, @ThreadSafe 이정도가 더 있을 듯하다.

이런 어노테이션을 통해 어느정도는 버그들을 잡을수(?) 있을 듯 하다. nullPointException만 잡아도 어느정도(?) 버그는 잡지 않을까? 예전에 어느 기사였나 블로그 였나? 자바의 가장 많이 발생하는 exception이 nullPointException이라 했던 기억이 어렴풋이 난다. 암튼 null이란..

아무튼 이렇게 jsr305 어노테이션을 사용해서 아니 @Nonnull, @Nullable 이정도면 사용해도 어느정도 괜찮을 듯 하다.
null 만이라도 잡자!

토비의 봄 (더블 디스패치)

이번 시간에는 저번시간에 이어서 더블 디스패치에 대해서 알아보자.
더블 디스패치라는 용어는 자바가 나오기 훨씬 전에 어떤 논문으로 발표된 용어이다. 내 기억에는 내가 태어날 때 나왔으니까 1986년도에 논문으로 발표된 것으로 기억한다. 굳이 자바뿐만이 아니라 싱글 디스패치인 언어에는 모두 포함되는 내용인 듯 싶다.

더블 디스패치 (Double Dispatch)

페이스북, 트위터에 사진과 텍스트를 올려주는 그런 요구사항이 들어왔다고 가정하자. 그래서 아래와 같이 만들었다.


interface Post { void postOn(SNS s); } static class Text implements Post { @Override public void postOn(SNS s) { System.out.println("text - " + s.getClass().getSimpleName()); } } static class Picture implements Post { @Override public void postOn(SNS s) { System.out.println("picture - " + s.getClass().getSimpleName()); } } interface SNS { } static class Facebook implements SNS { //... } static class Twitter implements SNS { //... } public static void main(String[] args) { List<Post> posts = Arrays.asList(new Text(), new Picture()); List<SNS> sns = Arrays.asList(new Facebook(), new Twitter()); posts.forEach(p -> sns.forEach(s -> p.postOn(s))); }

나름 확장성을 고려하여 Post와 SNS를 인터페이스로 만들고 Post는 SNS 인터페이스를 의존하게 만들었다. 출력 결과는 다음과 같다.

text - Facebook
text - Twitter
picture - Facebook
picture - Twitter

아주 맘에 든다. 그런데 가만보면 위의 코드는 아주 간단한 코드이다. 각각의 클래스명만 가져오는 동일한 코드가 들어 있다. 물론 동일한 로직이 들어있을 수도 있겠지만 보통은 각각 다른 비지니스를 정해 줄 때도 있다. 그래서 페이스북에 올릴 때와 트위터에 올릴때 각각 다른 비지니스를 넣어주려고 한다. 그래서 아래와 같이 바꾸었다.

//나머지는 동일 해서 생략

static class Text implements Post {
  @Override
  public void postOn(SNS s) {
    if (s instanceof Facebook) {
      System.out.println("text - facebook");
    }
    if(s instanceof Twitter){
      System.out.println("text - twitter");
    }
  }
}

static class Picture implements Post {
  @Override
  public void postOn(SNS s) {
    if (s instanceof Facebook) {
      System.out.println("picture - facebook");
    }
    if(s instanceof Twitter){
      System.out.println("picture - twitter");
    }
  }
}

변경된 부분만 살펴보자. 우리는 postOn안에 instanceof를 사용해서 Facebook일 때는 Facebook 비지니스로직, Twitter일때는 Twitter의 비지니스로직으로 변경하였다. 그렇게 썩 마음에 드는 코드는 아니지만 코드를 돌려보면 우리가 원하는 결과는 나온다. 그런데 갑자기 다른 SNS가 추가 되었다. Linkedin 이라는 SNS가 추가 되어 다시 개발하게 되었다.
그래서 다음과 같이 추가 하였다.

static class Linkedin implements SNS {
}
static class Text implements Post {
  @Override
  public void postOn(SNS s) {
    if (s instanceof Facebook) {
      System.out.println("text - facebook");
    }
    if(s instanceof Twitter){
      System.out.println("text - twitter");
    }
    if(s instanceof Linkedin){
      System.out.println("text - linkedin");
    }
  }
}

static class Picture implements Post {
  @Override
  public void postOn(SNS s) {
    if (s instanceof Facebook) {
      System.out.println("picture - facebook");
    }
    if(s instanceof Twitter){
      System.out.println("picture - twitter");
    }
  }
}

정상작동 할 것처럼 보이지만 실수로 우리는 Picture에 Linkedin을 만들지 않았다. 물론 간단하니까 그냥 한눈에 보이지만 어마어마하게 많은 로직이 숨어 있다면 찾기도 어려울지도 모른다. 또한 또다른 SNS가 추가 될 때 마다 맘에 안드는 if문 계속 추가 해야되는 단점이 숨어 있다. 물론 그렇게 해도 상관은 없다. 하지만 우리는 좀 더 나은 방법을 원한다. 그래서 아래와 같이 변경을 하였다.

interface Post {
  void postOn(Facebook facebook);
  void postOn(Twitter twitter);
}

static class Text implements Post {

  @Override
  public void postOn(Twitter twitter) {
    System.out.println("text - facebook");
  }

  @Override
  public void postOn(Facebook facebook) {
    System.out.println("text - twitter");
  }
}

static class Picture implements Post {

  @Override
  public void postOn(Twitter twitter) {
    System.out.println("picture - facebook");
  }

  @Override
  public void postOn(Facebook facebook) {
    System.out.println("picture - twitter");
  }
}

interface SNS {
}

static class Facebook implements SNS {
}

static class Twitter implements SNS {
}

Post를 SNS를 의존하는게 아니라 구현체인 Facebook과 Twitter를 의존하고 있다. 그렇게 나쁘지 않은 방식이다. 하지만 여기에서도 다른 SNS 추가 되면 Post 인터페이스를 수정해야 하고 그에 따른 구현체 TextPicture 클래스 모두 다 수정해야하는 단점이 있다. 더욱 문제가 있는 것은 실행하는 main메서드가 컴파일이 안된다.

public static void main(String[] args) {
  List<Post> posts = Arrays.asList(new Text(), new Picture());
  List<SNS> sns = Arrays.asList(new Facebook(), new Twitter(), new Linkedin());
  posts.forEach(p -> sns.forEach(s -> p.postOn(s)));
}

위의 코드를 작성해보면 컴파일 에러가 발생한다. p.postOn(s) 이부분에 에러가 발생하는데 에러를 보자면 SNS 타입을 받는 메서드를 찾을 수 없다고 나온다. 왜 일까? 메서드 오버로딩은 정적 디스패치를 한다. 런타임 시점이 아니라 컴파일하는 시점에 파라미터의 타입을 정확히 체크를 해서 해당하는 메서드를 정해놔야 한다. 하지만 우리는 SNS라는 추상화된 객체를 넣어서 컴파일 타임에 에러가 발생한 것이다. 까다롭다. 이대로 포기 하면 안된다.
기존 코드도 변경하지 않고 좀 더 확장성있게 만들 수는 없을까? 그래서 나왔다. 더블 디스패치 라는 것이다.

interface Post {
  void postOn(SNS s);
}

static class Text implements Post {
  @Override
  public void postOn(SNS s) {
    s.post(this);
  }
}

static class Picture implements Post {
  @Override
  public void postOn(SNS s) {
    s.post(this);
  }
}

interface SNS {
  void post(Text text);
  void post(Picture picture);
}

static class Facebook implements SNS {
  @Override
  public void post(Text text) {
    System.out.println("text - facebook");
  }

  @Override
  public void post(Picture picture) {
    System.out.println("picture - facebook");
  }
}

static class Twitter implements SNS {
  @Override
  public void post(Text text) {
    System.out.println("text - twitter");
  }

  @Override
  public void post(Picture picture) {
    System.out.println("picture - twitter");
  }
}

기존의 코드들은 냅두고 두번째가 타입을 결정해야 되는 Facebook 클래스와 Twitter 클래스로 비지니스 로직을 옮겨놨다. 그리고 Post를 구현하고 있는 클래스에는 s.post(this) 이와 같이 자기 자신을 파라미터로 넘겨주면 된다.
한마디로 첫번째 호출 되는 Post쪽에서 타입을 결정하고 두번째로 넘기는 그런 방식이다. 그래서 디스패치를 두번한다고 더블 디스패치라고 한다.
만약 여기서 아까와 같이 SNS가 추가 되었다고 가정하자. 그럼 우리는 또다른 SNS 클래스를 구현만 해주면 된다.

static class Linkedin implements SNS {
  @Override
  public void post(Text text) {
    System.out.println("text - linkedin");
  }

  @Override
  public void post(Picture picture) {
    System.out.println("picture - linkedin");
  }
}

위와 같이 Linkedin 클래스만 작성해서 구현해주면 된다. Post 인터페이스와 그에 따른 구현체들은 수정할 필요 없이 좀 더 확장성 있게 만들 수 있게 되었다.

우리는 좀 더 나은 방식으로 개발 할 수 있는 더블 디스패치라는 것을 배웠다. 쉽지 않은 기술이다. 비슷한 로직이 있다면 한번 써먹어 볼텐데 그런 로직이 있나 모르겠다. 언제 어디다 쓸지는 하다보면 나오겠지 뭐.. 하지만 쓸때되면 또 까먹겠지.

오늘 이렇게 더블디스패치에 대해서 조금이나마 알게 되었다!