Lombok에 대해

많이 사용할수록 매워지는데... 이게 맞나?

·

4 min read

결론

  • 컴파일단계에서는 구현코드가 줄어드나, 실행코드가 많은 것은 여전함

  • 코드 자동 생성으로 인해 개발자의 의도와는 다르게 구현될 수 있음(ex 순환참조)

  • Lombok 이 개발자를 도와주는 도구임에는 변함없음
    모든 기술이 역사적으로 그래왔듯이, 사용자 하기 나름

  • java record를 사용하되, 필요한 기능만 구현해도 무방함
    (필자는 delombok 후 record + builder 라이브러리로 재구성했음)

  • Lombok 사용자는 지금도 늘고 있으며 커뮤니티도 활발함
    향 후 패치에서 점진적으로 개선될 것으로 보임


시작

Student 객체로 데이터를 설계해보자

public class Student {
    private String name;
    private Integer age;
    private Grade grade;

    private enum Grade {
        A, B, C, D, F
    }
}

그리고 아래의 추가 요청사항이 있다고 하자

  1. get/set 설정

  2. 동일한 데이터에 대해 처리하는 로직 추가

  3. 객체 로그 출력 이쁘게

  4. 빌더 패턴 적용

  5. .....

import java.util.Objects;

public class Student {
    private String name;
    private Integer age;
    private Grade grade;

    private Student(Builder builder) {
        setName(builder.name);
        setAge(builder.age);
        setGrade(builder.grade);
    }

    public static Builder builder() {
        return new Builder();
    }

    private enum Grade {
        A, B, C, D, F
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Grade getGrade() {
        return grade;
    }

    public void setGrade(Grade grade) {
        this.grade = grade;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return Objects.equals(name, student.name) && Objects.equals(age, student.age) && grade == student.grade;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", grade=" + grade +
                '}';
    }

    public static final class Builder {
        private String name;
        private Integer age;
        private Grade grade;

        private Builder() {
        }

        public Builder name(String val) {
            name = val;
            return this;
        }

        public Builder age(Integer val) {
            age = val;
            return this;
        }

        public Builder grade(Grade val) {
            grade = val;
            return this;
        }

        public Student build() {
            return new Student(this);
        }
    }
}

WOW... 😱 BoilerPlate 코드가 많아졌다...

하지만 lombok 을 사용하면

import lombok.*;

@Data
@Builder
public class Student {
    private String name;
    private Integer age;
    private Grade grade;

    private enum Grade {
        A, B, C, D, F
    }
}

코드가 순식간에 줄어들었다.
불변 객체로써 쓰고 싶다면 클래스 위에 @Value(staticConstructor = "of") 붙이면 된다.

아래와 같이 lombok이 잘 적용된 것을 알 수 있다.

public class ImmutableObjTest {
    @Test
    void lombokTest() {
        Student fred1 = Student.of("Fred", 21, Student.Grade.A);
        Student fred2 = Student.of("Fred", 21, Student.Grade.A);
        Student martie = Student.of("Martie", 20, null);

        assertEquals(fred1, fred2);
        assertNotEquals(fred1, martie);

        System.out.println("I am a "+fred1);
        System.out.println("I am a "+fred2);
        System.out.println("I am a "+martie);
    }
}
I am a Student(name=Fred, age=21, grade=A)
I am a Student(name=Fred, age=21, grade=A)
I am a Student(name=Martie, age=20, grade=null)

자주 사용되는 기능

@Data

  • 아래의 애너테이션이 모두 포함되어 있음

    • @Getter

    • @Setter

    • @RequiredArgsConstructor

    • @ToString

    • @EqualsAndHashCode

@Value

  • 불변 객체를 만들 때 사용

  • 아래의 애너테이션이 모두 포함되어 있음

    • @Getter

    • @Getter

    • @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)

    • @AllArgsConstructor

    • @ToString

    • @EqualsAndHashCode


주의점

  • 편한 만큼 과도한 애너테이션의 남용으로 실제 실행 코드 증가

  • 관례 기반 코드 스타일로 컴파일 단계에서 잠재적 오류를 파악 하기 힘듦

잠재된 오류 예제

아래에 어떤 개발자가 name 만큼은 꼭 받고 싶어서 final 선언했다고 가정하자

@Data
@Builder
public class Student {
    private final String name;
    private Integer age;
    private Grade grade;

    public enum Grade {
        A, B, C, D, E
    }
}
public class FinalFieldButGeneratedTest {
    @Test
    void lombokTest() {
        Student fred1 = Student.builder()
                .name("fred")
                .age(20)
                .grade(Student.Grade.A)
                .build();
        Student fred2 = Student.builder()
                .name("fred")
                .age(20)
                .grade(Student.Grade.A)
                .build();
        Student martie = Student.builder()
                .age(30)
                .grade(Student.Grade.B)
                .build();

        assertEquals(fred1, fred2);
        assertNotEquals(fred1, martie);

        System.out.println("I am a " + fred1);
        System.out.println("I am a " + fred2);
        System.out.println("I am a " + martie);
    }
}

실행하면 아래와 같이 null 값이 들어가게 된다.

I am a Student(name=fred, age=20, grade=A)
I am a Student(name=fred, age=20, grade=A)
I am a Student(name=null, age=30, grade=B)

@ToString 순환 참조 문제도 있다. 코드가 길어질 것 같으니 pass..


대체재. Java record type

자바 14에 첫 등장하여 16에 안정화가 된 record 타입도 있다.

public record Student(
        String name,
        Integer age,
        Grade grade
) {
    public static Student of(String name, Integer age, Grade grade) {
        return new Student(name, age, grade);
    }

    public enum Grade {
        A, B, C, D, F
    }
}

아까 전의 테스트코드와 완벽 호환된다. 값 호출도 get이 아닌 칼럼 명 그대로 호출한다.

public class ImmutableObjTest {
    @Test
    void lombokTest() {
        Student fred1 = Student.of("Fred", 21, Student.Grade.A);
        Student fred2 = Student.of("Fred", 21, Student.Grade.A);
        Student martie = Student.of("Martie", 20, null);

        assertEquals(fred1, fred2);
        assertNotEquals(fred1, martie);

        System.out.println("I am a " + fred1);
        System.out.println("I am " + fred1.age() + " years old");
        System.out.println("I got " + fred1.grade());
    }
}
I am a Student[name=Fred, age=21, grade=A]
I am 21 years old
I got A