1일 1커밋🌱
Lombok
1. Lombok이란?
Lombok
은 Java 기반에서 기계적으로 작성하는 VO, DTO, Entity 관련 작업을 손쉽게 하게 해주는 도구이다. 보통 Model 클래스나 Entity 같은 도메인 클래스 등에는 수많은 멤버변수가 있고 이에 대응되는 getter, setter 그리고 toString 메서드와 멤버변수에 따른 수많은 생성자를 만들어 주는데 이는 번거로운 작업이 될 수 있다.
Lombok을 사용하면 여러가지 어노테이션을 제공하고 이를 기반으로 코드를 컴파일과정에서 생성해주는 방식으로 동작하는 라이브러리이다. 즉 코딩 과정에서는 롬복과 관련된 어노테이션만 보이고 getter와 setter 메서드 등은 보이지 않지만 실제로 컴파일된 결과물(.class)에는 코드가 생성되어 있다.
2. Lombok과 일반 Java 코드의 차이
기본 소스 - Lombok 미사용
- 주로 DTO, VO, Entity 관련 Class
- 물론 IDE(Eclipse, IntelliJ 등)에서도 자동으로 쉽게 작성 가능하다.
public class NonLombokModel {
private String name;
private String age;
private String address;
public NonLombokModel(String name, String age, String address){
super();
this.name = name;
this.age = age;
this.address = address;
}
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
public String getAge(){
return age;
}
public void setAge(String age){
this.age = age;
}
public String getAddress(){
return address;
}
public void setAddress(String address){
this.address = address;
}
@Override
public String toString(){
return "NonLombokModel [name=" + name + ", age=" + age + ", address=" + address + "]";
}
@Override
public int hashCode(){
final int prime = 1;
int result = 1;
result = prime * result + ((address == null) ? 0 : address.hashCode());
result = prime * result + ((age == null) ? 0 : age.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj){
if(this == obj){
return true;
}
if(obj == null){
return false;
}
if(getClass() != obj.getClass()){
return false;
}
NonLombokModel other = (NonLombokModel) obj;
if(address == null){
if(other.address != null){
return false;
}
}else if(!address.equals(other.address)){
return false;
}
if(age == null){
if(other.age != null){
return false;
}
}else if(!age.equals(other.age)){
return false;
}
if(name == null){
if(other.name != null){
return false;
}
}else if(!name.equals(other.name)){
return false;
}
return true;
}
}
기본 소스 - Lombok 사용
- 매우 직관적으로 깔끔한 코드를 작성할 수 있다.
- 일반적으로 @Data 어노테이션만 사용하는 경우가 많다.
//@Data로 사용 가능
@Getter
@Setter
@ToString
@EqualsAndHashCode
@Builder
public class LombokModel {
private @NonNull String name;
private @NonNull String age;
private @NonNull String address;
}
기본 소스 - 실행
- 실행 결과는 같다.
public class MainClass {
public static void main(String[] args){
LombokModel lombokModel = new LombokModel.LombokModelBuilder()
.name("Park")
.address("seoul")
.age("33")
.build();
NonLombokModel nonLombokModel = new NonLombokModel("kim", "Pusan", "34");
// Lombok 사용
System.out.println("lombok library 사용!");
System.out.println("lombokModel : " + lombokModel);
System.out.println("lombokModel : " + lombokModel.toString());
System.out.println("lombokMoel : " + lombokModel.hashCode());
// Java Bean(Classic Way) 사용
System.out.println("lombok library 미사용!");
System.out.println("nonlombokModel : " + nonLombokModel);
System.out.println("nonlombokModel : " + nonLombokModel.toString());
System.out.println("nonlombokModel : " + nonLombokModel.hashCode());
}
}
실행 결과
lombok library 사용!
lombokModel : LombokModel(name=Park, age=33, address=seoul)
lombokModel : LombokModel(name=Park, age=33, address=seoul)
lombokMoel : 153051665
lombok library 미사용!
nonlombokModel : NonLombokModel [name=kim, age=Pusan, address=34]
nonlombokModel : NonLombokModel [name=kim, age=Pusan, address=34]
nonlombokModel : 77588684
또는 위에서 사용했던 @Getter, @Setter, @ToStrig, @EqualsAndHashCode, @Builder 5가지의 어노테이션을 붙이는 것 대신에 @Data
어노테이션 하나만 붙여서 사용할 수도 있다.
3. Lombok 컴파일된 클래스 파일
package lombok;
public class LombokModel {
@NonNull
private String name;
@NonNull
private String age;
@NonNull
private String address;
LombokModel(@NonNull String name, @NonNull String age, @NonNull String address) {
if (name == null) {
throw new NullPointerException("name is marked non-null but is null");
} else if (age == null) {
throw new NullPointerException("age is marked non-null but is null");
} else if (address == null) {
throw new NullPointerException("address is marked non-null but is null");
} else {
this.name = name;
this.age = age;
this.address = address;
}
}
public static LombokModel.LombokModelBuilder builder() {
return new LombokModel.LombokModelBuilder();
}
@NonNull
public String getName() {
return this.name;
}
@NonNull
public String getAge() {
return this.age;
}
@NonNull
public String getAddress() {
return this.address;
}
public void setName(@NonNull String name) {
if (name == null) {
throw new NullPointerException("name is marked non-null but is null");
} else {
this.name = name;
}
}
public void setAge(@NonNull String age) {
if (age == null) {
throw new NullPointerException("age is marked non-null but is null");
} else {
this.age = age;
}
}
public void setAddress(@NonNull String address) {
if (address == null) {
throw new NullPointerException("address is marked non-null but is null");
} else {
this.address = address;
}
}
public String toString() {
String var10000 = this.getName();
return "LombokModel(name=" + var10000 + ", age=" + this.getAge() + ", address=" + this.getAddress() + ")";
}
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (!(o instanceof LombokModel)) {
return false;
} else {
LombokModel other = (LombokModel)o;
if (!other.canEqual(this)) {
return false;
} else {
label47: {
Object this$name = this.getName();
Object other$name = other.getName();
if (this$name == null) {
if (other$name == null) {
break label47;
}
} else if (this$name.equals(other$name)) {
break label47;
}
return false;
}
Object this$age = this.getAge();
Object other$age = other.getAge();
if (this$age == null) {
if (other$age != null) {
return false;
}
} else if (!this$age.equals(other$age)) {
return false;
}
Object this$address = this.getAddress();
Object other$address = other.getAddress();
if (this$address == null) {
if (other$address != null) {
return false;
}
} else if (!this$address.equals(other$address)) {
return false;
}
return true;
}
}
}
protected boolean canEqual(Object other) {
return other instanceof LombokModel;
}
public int hashCode() {
int PRIME = true;
int result = 1;
Object $name = this.getName();
int result = result * 59 + ($name == null ? 43 : $name.hashCode());
Object $age = this.getAge();
result = result * 59 + ($age == null ? 43 : $age.hashCode());
Object $address = this.getAddress();
result = result * 59 + ($address == null ? 43 : $address.hashCode());
return result;
}
public static class LombokModelBuilder {
private String name;
private String age;
private String address;
LombokModelBuilder() {
}
public LombokModel.LombokModelBuilder name(@NonNull String name) {
if (name == null) {
throw new NullPointerException("name is marked non-null but is null");
} else {
this.name = name;
return this;
}
}
public LombokModel.LombokModelBuilder age(@NonNull String age) {
if (age == null) {
throw new NullPointerException("age is marked non-null but is null");
} else {
this.age = age;
return this;
}
}
public LombokModel.LombokModelBuilder address(@NonNull String address) {
if (address == null) {
throw new NullPointerException("address is marked non-null but is null");
} else {
this.address = address;
return this;
}
}
public LombokModel build() {
return new LombokModel(this.name, this.age, this.address);
}
public String toString() {
return "LombokModel.LombokModelBuilder(name=" + this.name + ", age=" + this.age + ", address=" + this.address + ")";
}
}
}
특히 Lombok 라이브러리의 @Builder를 사용했을 때의 코드를 유의깊게 보자. Lombok 클래스의 Inner 클래스인 LombokModelBuilder 클래스가 static으로 선언되어 있고 클래스 안의 각 메서드는 this(LombokModelBuilder)를 반환한다. Static inner class를 생성할때는 일반적인 static 멤버에 접근할때와 같이 클래스명.멤버명으로 접근해서 생성한다. MainClass를 보면 LombokModel.lombokModelBuilder()와 같이 접근해서 static inner Builer 클래스에 접근함을 확인할 수 있다. Builder 패턴을 사용해 lombokModel 객체를 생성하는 마지막 부분을 보면 build() 메서드가 있는데, 코드를 보면 public LombokModel build() { return new LombokModel(this.name, this.age, this.address); } 라고 되어 있고, 이를 보면 이전의 .name(), .age(), .address() 메서드를 통해 Builder 객체의 인스턴스 변수에 값을 할당한 후 새로운 LombokModel 객체를 반환함을 알 수 있다.
4. 자주 사용되는 Lombok 어노테이션
1. @Getter @Setter : 접근자/설정자 자동 생성
- @Getter와 @Setter 어노테이션을 클래스명 위에 명시하면, 필드값에 대한 getters / setters 메소드가 자동 생성된다.
- “AccessLevel”을 통해 접근 제한자 설정이 가능하다. (Default : Public) ex) @Getter(access=AccessLevel.PROTECTED)
- 클래스에 선언시 모든 필드에 대해 생성, 필드에 선언시 선언된 필드에 대해서만 getter, setter를 생성한다.
2. @NoArgsConstructor @AllArgsConstructor @RequiredConstructor : 생성자 자동 생성
- “AccessLevel”을 통해 접근 제한자 설정이 가능하다. (Default : Public) ex) @NoArgsConstructor(access=AccessLevel.PROTECTED)
- @NoArgsConstructor : 기본 생성자를 자동으로 생성해준다.
- @AllArgsConstructor : 필드값을 모두 포함한 생성자를 자동 생성해준다.
- RequiredConstructor : final이나 @NonNull인 필드값만 파라미터로 받는 생성자를 만들어준다.
3. @ToString : toString 메소드 자동 생성
- toString() 메소드를 자동 생성해준다.
- 클래스명(필드명1=값1, 필드명2=값2…) 형태로 출력된다.
- exclude 키워드를 이용하여 출력을 원치 않는 필드 지정이 가능하다.
- 필드의 명을 출력하고 싶지 않을 때 : includeFieldNames를 false 처리. 제외 제외시키고 싶은 필드는 exclude 파라미터에 추가하면 된다.
- of 파라미터를 이용하여 원하는 필드만 출력할 수도 있다. 부모 클래스의 toString을 출력하고 싶은 경우에는 callSuper 파라미터를 true로 설정하여 포함시킬 수도 있다.
4. @EqualsAndHashCode : equals, hashCode 메소드 자동 생성
- equals()와 hashCode()를 자동으로 생성해준다.
5. @NotNull
- 멤버필드에 선언해 주면, 해당 변수 Setter에 null값이 들어올 때 NullPointerException을 발생시킨다.
6. @cleanup
- try~finally에서 close()를 대신 호출해주는 어노테이션이다.
7. @synchronized
- 자바의 synchronized를 사용할 때 deadlock이 발생하는 경우가 종종 발생하는 것을 방지하기 위해 Lombok이 메소드가 실행되기 전에 잠글 $lock이라는 잠금 필드를 생성한다.
- lock 오브젝트를 자동으로 생성, synchronized를 손쉽게 사용할 수 있게 해준다.
8. @Data
- @Getter, @Setter, @NotNull, @EqualsAndHashCode, @ToString에 대한 걸 모두 해주는 Annotation이다.
- @NonNull 또는 final 필드를 매개변수로 사용하는 public 생성자가 생성된다.
- @Data는 staticConstructor라는 하나의 파라미터 옵션만 제공하는데, 파라미터를 이름으로 하는 static factory 메소드를 생성해준다. ex) @Data(staticConstructor=”of”)
9. @Value
- Value는 Immutable Class를 생성해준다.
- @Data와 비슷하지만 모든 필드를 기본적으로 Private 및 Final로 하고, Setter 함수를 생성하지 않고, Class 또한 Final로 지정하는 것만 빼고 동일하다.
10. @Slf4j, @Log 등
- 해당 어노테이션을 선언하면, log 관련 static 메소드를 클래스별로 선언할 필요가 없다.
11. Builder
- 어노테이션을 선언하면 생성자 대신 빌더를 사용할 수 있다.
- @Singular : collection 타입에 선언하게 되면 파라미터를 하나씩 추가할 수 있다.
@Builder public class TestVo {
private String id; private String name;
@Singular private List listTest;
@Singular private Map<String, String> maps;
/* TestVo test = TestVo.builder().id("goddaehee") .name("GodDaeHee")
.listTest("List")
.listTest("List Test")
.maps("age","20")
.maps("gender","male")
.build();
*/
}
5. 주의할 점
Lombok은 자바 컴파일 시점에서 특정 어노테이션으로 해당 코드를 추가할 수 있는 라이브러리이다. 이는 코드의 다이어트, 가독성 및 유지보수에 맣은 도움이 되지만 잘못 사용하기 쉽다.
1. @Data는 지양하자.
@Data는 @ToString, @EqualsAndHashCode, @Getter, @Setter, @RequireArgsConstructor를 한 번에 사용하는 강력한 어노테이션이다. 강력한 어노테이션인 만큼 그에 따른 부작용도 많다.
2. 무분별한 Setter 남용
@Data를 사용하면 자동으로 Setter를 지원하게 된다. 그로 인해서 생기는 문제점들이 있다.
Setter는 그 의도가 분명하지 않고, 객체를 언제든지 변경할 수 있는 상태가 되어 객체의 안전성이 보장받기 힘들다. 따라서 변경되지 않는 인스턴스에 대해서는 setter(변경포인트)가 제공되지 않는 것이 많다. 즉, 인스턴스의 변경 포인트를 제공하지 않음으로써, 변경 기능이 없다는 것을 명시적으로 전달할 수 있다. 또한 객체의 일관성/안정성이 보장받기 힘들다.(setter가 열린다는 말은, immutable(불변)하지 않다는 것이다.
=> Builder 패턴을 사용하자.
public class MyAccountDto {
public static class SignUpReq {
private Email email;
private String name;
private String password;
private Address address;
@Builder
public SignUpReq(Email email, String name, String password, Address address) {
Assert.notNull(email, "email must not be null");
Assert.notNull(name, "name must not be null");
Assert.notNull(password, "password must not be null");
Assert.notNull(address, "address must not be null");
this.email = email;
this.name = name;
this.password = password;
this.address = address;
}
public Account toEntity() {
return Account.builder()
.email(this.email)
.name(this.name)
.password(Password.builder().value(this.password).build())
.address(this.address)
.build();
}
}
}
하지만 클래스 상단의 @Builder는 지양해야 한다.
- 클래스 위에 @Builder 사용 시 @AllArgsConstructor 효과가 발생하여 모든 멤버 필드에 대해 매개 변수를 받는 기본 생성자를 생성한다.
따라서 생성자 위 @Builder에 적절한 책임을 부여해야 한다.
- 주문에 대해 신용카드 취소, 계좌 기반 환불이 있다고 가정하자. = 신용카드 취소 / 계좌 기반 환불에 대해 각각 정의하여 @Builder 설계
public class Refund {
private Long id;
private Account account;
private CreditCard creditCard;
private Order order;
@Builder(builderMethodName = "of", builderClassName = "of")
private Refund(Account account, CreditCard creditCard, Order order) {
this.account = account;
this.creditCard = creditCard;
this.order = order;
}
// 계좌 기반 환불
@Builder(builderClassName = "byAccountBuilder", builderMethodName = "byAccountBuilder")
public static Refund byAccount(Account account, Order order) {
Assert.notNull(account, "account must not be null");
Assert.notNull(order, "order must not be null");
return Refund.of().account(account).order(order).build();
}
// 신용카드 환불
@Builder(builderClassName = "byCreditBuilder", builderMethodName = "byCreditBuilder")
public static Refund byCredit(CreditCard creditCard, Order order) {
Assert.notNull(creditCard, "creditCard must not be null");
Assert.notNull(order, "order must not be null");
return Refund.of().creditCard(creditCard).order(order).build();
}
}
따라서 생성자 위에 Builder 사용시에도 필요조건에 따라 매개변수를 최소화해야 한다.
3. 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;
}
}
public Class MemberTest {
@Test
public void test_01(){
final Member member = new Member("asd@asd.com", "name");
final Coupon coupon = new Coupon(member);
final List<Coupon> coupons = new ArrayList<>();
coupons.add(coupon);
member.setCoupons(coupons);
member.toString();
}
}
JPA를 사용하다보면 객체를 직렬화하는 과정에서 발생하는 문제와 동일한 이유이다. 이처럼 무분별하게 @Data를 사용하게 되면 이러한 문제를 만나기 쉽다.
=> @ToString(exclude=”…“)처럼 어노테이션을 사용해 제외하자. => @ToString(of=”…“)처럼 어노테이션을 사용해 제외하자.
4. NoArgsConstructor 접근 권한을 최소화하자.
JPA에서는 프록시 생성을 위해 기본 생성자 반드시 하나를 생성해야 한다. 하지만, 기본 생성자를 만든다는 것은, 우리에게 또다른 null 처리의 의무를 부여하게 된다.(다른 어딘가에서 기본 생성자를 호출하여 null이 되면 안되는 필드가 null이 되어 돌아다닐 수 있다.) 이때 접근 권한이 protected이면 된다. 굳이 외부에서 생성을 열어둘 필요가 없다. (access = AccessLevel.PROTECTED)
6. Maven + IntelliJ에서 Lombok 라이브러리 사용법
1. Lombok Dependency 설정
Maven
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<scope>provided</scope>
</dependency>
2. Lombok Plugin 설정
- 최초로만 설정하면 된다.
- 설정
- Windows : File > Setting(Ctrl + Alt + S)
- Plugins 선택 후 Browse repositories에서 Lombok 검색
- Lombok Plugin install
- IntelliJ Restart
3. Enable annotation 설정
- 설정
- Windows : File > Setting(Ctrl + Alt + S)
- Build, Execution, Deployment > Compiler > Annotation Processings
- Enable annotation processing 체크
참고
- https://www.popit.kr/%EC%8B%A4%EB%AC%B4%EC%97%90%EC%84%9C-lombok-%EC%82%AC%EC%9A%A9%EB%B2%95/
- https://niceman.tistory.com/99
- https://dololak.tistory.com/783
- https://goddaehee.tistory.com/208
- https://www.daleseo.com/lombok-popular-annotations/
- https://gmlwjd9405.github.io/2018/11/29/intellij-lombok.html
- https://cheese10yun.github.io/lombok/
- https://velog.io/@dahye4321/%EC%8A%A4%ED%94%84%EB%A7%81-%EA%B0%80%EC%9D%B4%EB%93%9C-2
- https://johngrib.github.io/wiki/builder-pattern/
- https://atoz-develop.tistory.com/entry/JAVA-static-class%EC%99%80-Builder-Pattern%EB%B9%8C%EB%8D%94-%ED%8C%A8%ED%84%B4
댓글남기기