1. 반복적인 코드를 피하라
Java는 훌륭한 언어이지만 때로는 코드에서 수행해야 하는 일반적인 작업이나 일부 프레임워크 방식을 준수하기에는 너무 장황해질 수 있습니다. 이것은 종종 우리 프로그램의 비즈니스 측면에 실질적인 가치를 제공하지 않으며, Lombok이 우리를 더 생산적으로 만드는 곳입니다.
작동 방식은 빌드 프로세스에 연결 하고 코드에 도입한 여러 프로젝트 어노테이션에 따라 Java 바이트 코드를 .class 파일에 자동 생성 하는 것입니다.
어떤 시스템을 사용하든 빌드에 포함하는 것은 매우 간단합니다. Project Lombok의 프로젝트 페이지 에는 세부 사항에 대한 자세한 지침이 있습니다. 내 프로젝트의 대부분은 maven 기반이므로 일반적으로 제공된 범위 에서 의존성을 삭제하고 진행하는 것이 좋습니다.
<dependencies>
...
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<scope>provided</scope>
</dependency>
...
</dependencies>
여기에서 사용 가능한 최신 버전을 확인할 수 있습니다 .
Lombok에 의존하는 것은 런타임이 아닌 순수한 빌드 의존성이므로 .jar 사용자도 의존하지 않습니다.
2. 게터/세터, 생성자 – 너무 반복적
공개 getter 및 setter 메소드를 통해 객체 속성을 캡슐화하는 것은 Java 세계에서 일반적인 관행이며 많은 프레임워크가 이 "Java Bean" 패턴에 광범위하게 의존합니다(빈 생성자와 "속성"에 대한 get/set 메소드가 있는 클래스).
이것은 매우 일반적이어서 대부분의 IDE는 이러한 패턴(및 그 이상)에 대한 자동 생성 코드를 지원합니다. 그러나 이 코드는 소스에 있어야 하며 새 속성이 추가되거나 필드 이름이 변경될 때 유지되어야 합니다.
JPA 엔터티로 사용하려는 이 클래스를 고려해 보겠습니다.
@Entity
public class User implements Serializable {
private @Id Long id; // will be set when persisting
private String firstName;
private String lastName;
private int age;
public User() {
}
public User(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
// getters and setters: ~30 extra lines of code
}
이것은 다소 간단한 클래스이지만 getter 및 setter에 대한 추가 코드를 추가했다고 상상해보십시오. 우리는 관련 비즈니스 정보보다 더 많은 상용구 0값 코드가 있는 정의로 끝났을 것입니다. "사용자는 이름과 성, 나이를 가지고 있습니다."
이제 이 클래스를 롬복화해 보겠습니다 .
@Entity
@Getter @Setter @NoArgsConstructor // <--- THIS is it
public class User implements Serializable {
private @Id Long id; // will be set when persisting
private String firstName;
private String lastName;
private int age;
public User(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
}
@Getter 및 @Setter 어노테이션 을 추가 하여 클래스의 모든 필드에 대해 이를 생성하도록 Lombok에 지시했습니다. @NoArgsConstructor 는 빈 생성자를 생성합니다.
이것은 전체 클래스 코드이며 // getters 및 setters 어노테이션 이 있는 위의 버전과 달리 생략하지 않습니다 . 세 가지 관련 속성 클래스의 경우 코드를 크게 절약할 수 있습니다!
User 클래스 에 속성(속성)을 더 추가 하면 동일한 일이 발생합니다. 기본적으로 모든 필드를 염두에 두도록 유형 자체에 어노테이션을 적용합니다.
일부 속성의 가시성을 개선하려면 어떻게 해야 합니까? 예를 들어, 엔터티의 id 필드 수정자 패키지 또는 보호된 개체를 계속 읽을 수 있도록 유지하고 싶지만 애플리케이션 코드에서 명시적으로 설정하지 않으 려면 이 특정 필드에 대해 더 세분화된 @Setter 를 사용할 수 있습니다 .
private @Id @Setter(AccessLevel.PROTECTED) Long id;
3. 게터 게터
응용 프로그램은 종종 값비싼 작업을 수행하고 나중에 사용할 수 있도록 결과를 저장해야 합니다.
예를 들어 파일이나 데이터베이스에서 정적 데이터를 읽어야 한다고 가정해 보겠습니다. 일반적으로 이 데이터를 한 번 검색한 다음 응용 프로그램 내에서 메모리 내 읽기를 허용하도록 캐시하는 것이 좋습니다. 이렇게 하면 응용 프로그램이 값비싼 작업을 반복하지 않아도 됩니다.
또 다른 일반적인 패턴은 처음 필요할 때만 이 데이터 를 검색하는 것 입니다. 즉, 해당 getter를 처음 호출할 때만 데이터를 얻습니다. 이것을 지연 로딩이라고 합니다.
이 데이터가 클래스 내부의 필드로 캐시되었다고 가정해 보겠습니다. 클래스는 이제 이 필드에 대한 모든 액세스가 캐시된 데이터를 반환하는지 확인해야 합니다. 이러한 클래스를 구현하는 한 가지 가능한 방법은 필드가 null인 경우에만 getter 메서드가 데이터를 검색하도록 하는 것 입니다. 우리는 이것을 Lazy getter 라고 부릅니다 .
Lombok은 위에서 본 @ Getter 어노테이션 의 lazy 매개변수를 사용하여 이를 가능하게 합니다 .
예를 들어 다음과 같은 간단한 클래스를 고려하십시오.
public class GetterLazy {
@Getter(lazy = true)
private final Map<String, Long> transactions = getTransactions();
private Map<String, Long> getTransactions() {
final Map<String, Long> cache = new HashMap<>();
List<String> txnRows = readTxnListFromFile();
txnRows.forEach(s -> {
String[] txnIdValueTuple = s.split(DELIMETER);
cache.put(txnIdValueTuple[0], Long.parseLong(txnIdValueTuple[1]));
});
return cache;
}
}
이것은 파일에서 Map 으로 일부 트랜잭션을 읽습니다 . 파일의 데이터는 변경되지 않으므로 한 번 캐시하고 getter를 통한 액세스를 허용합니다.
이제 이 클래스의 컴파일된 코드를 보면 캐시가 null인 경우 캐시를 업데이트 한 다음 캐시된 데이터를 반환 하는 getter 메서드를 볼 수 있습니다 .
public class GetterLazy {
private final AtomicReference<Object> transactions = new AtomicReference();
public GetterLazy() {
}
//other methods
public Map<String, Long> getTransactions() {
Object value = this.transactions.get();
if (value == null) {
synchronized(this.transactions) {
value = this.transactions.get();
if (value == null) {
Map<String, Long> actualValue = this.readTxnsFromFile();
value = actualValue == null ? this.transactions : actualValue;
this.transactions.set(value);
}
}
}
return (Map)((Map)(value == this.transactions ? null : value));
}
}
Lombok이 AtomicReference 에서 데이터 필드를 래핑 했다는 점을 지적하는 것은 흥미롭습니다 . 이것은 트랜잭션 필드 에 대한 원자 업데이트를 보장 합니다. getTransactions () 메서드는 경우 파일을 읽을 확인합니다 거래 입니다 널 (null)이.
클래스 내에서 직접 AtomicReference 트랜잭션 필드를 사용하지 않는 것이 좋습니다 . 필드에 액세스하려면 getTransactions() 메서드를 사용하는 것이 좋습니다 .
우리가 같은 다른 롬복 어노테이션을 사용하는 경우 이러한 이유로, ToString을 같은 클래스에서 , 그것은 사용 ) getTransactions를 ( 직접 필드에 액세스하는 대신.
4. 가치 등급/DTO
복잡한 "값"을 "데이터 전송 개체"로 나타내는 유일한 목적으로 데이터 유형을 정의하려는 많은 상황이 있습니다. 대부분의 경우 한 번 만들고 절대 변경하고 싶지 않은 불변 데이터 구조의 형태입니다.
성공적인 로그인 작업을 나타내는 클래스를 디자인합니다. 우리는 모든 필드가 null이 아니길 원하고 객체가 변경 불가능하여 해당 속성에 스레드로부터 안전하게 액세스할 수 있기를 바랍니다.
public class LoginResult {
private final Instant loginTs;
private final String authToken;
private final Duration tokenValidity;
private final URL tokenRefreshUrl;
// constructor taking every field and checking nulls
// read-only accessor, not necessarily as get*() form
}
다시 말하지만, 어노테이션 처리된 섹션에 대해 작성해야 하는 코드의 양은 캡슐화하려는 정보보다 훨씬 더 많은 양입니다. 이를 개선하기 위해 Lombok을 사용할 수 있습니다.
@RequiredArgsConstructor
@Accessors(fluent = true) @Getter
public class LoginResult {
private final @NonNull Instant loginTs;
private final @NonNull String authToken;
private final @NonNull Duration tokenValidity;
private final @NonNull URL tokenRefreshUrl;
}
@RequiredArgsConstructor 어노테이션 을 추가하면 선언한 대로 클래스의 모든 최종 필드에 대한 생성자를 얻을 수 있습니다. 속성에 @NonNull 을 추가하면 생성자가 null 허용 여부를 확인하고 그에 따라 NullPointerException이 발생 합니다. 이것은 필드가 최종이 아니고 @Setter 를 추가한 경우에도 발생 합니다.
우리 는 우리의 속성에 대해 지루하고 오래된 get*() 형식을 원합니까? 이 예제에서 @Accessors(fluent=true) 를 추가했기 때문에 "getters"는 속성과 동일한 메서드 이름을 갖습니다. getAuthToken() 은 단순히 authToken() 이 됩니다.
이 "유창한" 형식은 속성 설정자를 위한 최종 필드가 아닌 필드에 적용되고 연결 호출을 허용합니다.
// Imagine fields were no longer final now
return new LoginResult()
.loginTs(Instant.now())
.authToken("asdasd")
. // and so on
5. 핵심 자바 상용구
유지해야 하는 코드를 작성하게 되는 또 다른 상황은 toString() , equals() 및 hashCode() 메서드를 생성할 때 입니다. IDE는 클래스 속성 측면에서 템플릿을 자동 생성하는 데 도움을 주려고 합니다.
다른 Lombok 클래스 수준 어노테이션을 사용하여 이를 자동화할 수 있습니다.
- @ToString :모든 클래스 속성을 포함하는 toString() 메서드를생성합니다. 데이터 모델을 강화할 때 직접 작성하고 유지 관리할 필요가 없습니다.
- @EqualsAndHashCode :기본적으로 모든 관련 필드를 고려 하고 의미 체계 에 따라 equals() 및 hashCode() 메서드를모두 생성합니다.
이 발전기는 매우 편리한 구성 옵션을 제공합니다. 예를 들어, 어노테이션이 달린 클래스가 계층의 일부인 경우 callSuper=true 매개변수를 사용 하면 메서드의 코드를 생성할 때 부모 결과가 고려됩니다.
이를 보여주기 위해 사용자 JPA 엔터티 예제에 이 사용자와 관련된 이벤트에 대한 참조가 포함되어 있다고 가정해 보겠습니다 .
@OneToMany(mappedBy = "user")
private List<UserEvent> events;
@ToString 어노테이션을 사용했기 때문에 사용자 의 toString() 메서드를 호출할 때마다 전체 이벤트 List이 덤프 되는 것을 원하지 않습니다 . 대신 @ToString(exclude = {“events”}) 과 같이 매개변수화할 수 있으며 그렇게 하지 않습니다. 이것은 예를 들어 UserEvent 에 User 에 대한 참조가 있는 경우 순환 참조를 피하는 데 도움이 됩니다 .
를 들어 LoginResult의 예를 들면, 우리는 우리의 클래스에서 자체 토큰이 아닌 다른 최종 속성의 측면에서 평등과 해시 코드 계산을 정의 할 수 있습니다. 그런 다음 @EqualsAndHashCode(of = {“authToken”}) 와 같은 것을 간단히 작성할 수 있습니다 .
지금까지 검토한 어노테이션의 기능이 관심이 있는 경우 @Data 및 @Value 어노테이션도 검사할 수 있습니다. 이는 마치 해당 집합이 클래스에 적용된 것처럼 동작하기 때문입니다. 결국, 이러한 논의된 사용법은 많은 경우에 매우 일반적으로 결합됩니다.
5.1. ( 아니요 ) JPA 엔터티와 함께 @EqualsAndHashCode 사용
기본 equals() 및 hashCode() 메서드를 사용해야 하는지 아니면 JPA 엔터티에 대한 사용자 지정 메서드를 만들어야 하는지 여부는 개발자들 사이에서 자주 논의되는 주제입니다. 우리가 따를 수 있는 여러 접근 방식 이 있으며 각각 장단점이 있습니다.
기본적으로 @EqualsAndHashCode 에는 엔터티 클래스의 최종이 아닌 모든 속성이 포함됩니다. Lombok이 엔터티의 기본 키만 사용하도록 하기 위해 @EqualsAndHashCode 의 onlyExplicitlyIncluded 속성을 사용하여 이 문제를 "수정"할 수 있습니다 . 그래도 생성된 equals() 메서드는 몇 가지 문제를 일으킬 수 있습니다. Thorben Janssen 은 자신의 블로그 게시물 중 하나 에서 이 시나리오에 대해 자세히 설명합니다 .
일반적으로 JPA 엔터티에 대한 equals() 및 hashCode() 메서드 를 생성하기 위해 Lombok을 사용하는 것을 피해야 합니다.
6. 빌더 패턴
다음은 REST API 클라이언트에 대한 샘플 구성 클래스를 만들 수 있습니다.
public class ApiClientConfiguration {
private String host;
private int port;
private boolean useHttps;
private long connectTimeout;
private long readTimeout;
private String username;
private String password;
// Whatever other options you may thing.
// Empty constructor? All combinations?
// getters... and setters?
}
클래스 기본 빈 생성자를 사용하고 모든 필드에 대해 setter 메서드를 제공하는 것을 기반으로 하는 초기 접근 방식을 가질 수 있습니다. 그러나, 우리가 이상적으로 다시 수없는 구성을 원하는 설정을 효과적으로 불변하고, 그들이 내장 (인스턴스화)하고 나면. 따라서 우리는 setter를 피하고 싶지만 잠재적으로 긴 args 생성자를 작성하는 것은 안티 패턴입니다.
대신, 우리는 도구에 빌더 패턴 을 생성하도록 지시할 수 있습니다 . 이는 단순히 @Builder 어노테이션을 ApiClientConfiguration에 추가하여 추가 Builder 클래스 및 관련 유창한 setter와 유사한 메서드 를 작성하지 않아도 되는 것을 무효화합니다 .
@Builder
public class ApiClientConfiguration {
// ... everything else remains the same
}
위와 같은 클래스 정의(선언 생성자 또는 설정자 + @Builder 없음 )를 남겨두고 다음과 같이 사용할 수 있습니다.
ApiClientConfiguration config =
ApiClientConfiguration.builder()
.host("api.server.com")
.port(443)
.useHttps(true)
.connectTimeout(15_000L)
.readTimeout(5_000L)
.username("myusername")
.password("secret")
.build();
7. 확인 예외 부담
많은 Java API가 여러 검사된 예외를 throw할 수 있도록 설계되었습니다. 클라이언트 코드는 catch 하거나 throws 로 선언해야 합니다. 이런 일이 일어나지 않을 것이라고 알고 있는 예외를 몇 번이나 바꾸었습니까?:
public String resourceAsString() {
try (InputStream is = this.getClass().getResourceAsStream("sure_in_my_jar.txt")) {
BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
return br.lines().collect(Collectors.joining("\n"));
} catch (IOException | UnsupportedCharsetException ex) {
// If this ever happens, then its a bug.
throw new RuntimeException(ex); <--- encapsulate into a Runtime ex.
}
}
컴파일러가 만족하지 않기 때문에 이 코드 패턴을 피하고 싶다면( 확인된 오류가 발생할 수 없다는 것을 알고 있음 ) 적절하게 이름이 지정된 @SneakyThrows를 사용 하세요 .
@SneakyThrows
public String resourceAsString() {
try (InputStream is = this.getClass().getResourceAsStream("sure_in_my_jar.txt")) {
BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
return br.lines().collect(Collectors.joining("\n"));
}
}
8. 리소스 공개 확인
Java 7은 java.lang을 구현하는 모든 인스턴스가 리소스를 보유하도록 보장하기 위해 try-with-resources 블록을 도입했습니다 . 종료 시 AutoCloseable 이 해제됩니다.
Lombok은 @Cleanup을 통해 이를 달성하기 위한 대안적이고 보다 유연한 방법을 제공합니다 . 리소스가 해제되었는지 확인하려는 모든 지역 변수에 사용할 수 있습니다. 특정 인터페이스를 구현할 필요는 없습니다. close() 메서드만 호출하면 됩니다.
@Cleanup InputStream is = this.getClass().getResourceAsStream("res.txt");
릴리스 방법의 이름이 다른가요? 문제 없습니다. 어노테이션을 맞춤설정하면 됩니다.
@Cleanup("dispose") JFrame mainFrame = new JFrame("Main Window");
9. 로거를 얻기 위해 클래스에 어노테이션 달기
우리 중 많은 사람들이 우리 가 선택한 프레임워크에서 Logger 의 인스턴스를 생성하여 코드에 로깅 문을 드물게 추가 합니다. SLF4J를 가정해 보겠습니다.
public class ApiClientConfiguration {
private static Logger LOG = LoggerFactory.getLogger(ApiClientConfiguration.class);
// LOG.debug(), LOG.info(), ...
}
이것은 Lombok 개발자가 단순화한 일반적인 패턴입니다.
@Slf4j // or: @Log @CommonsLog @Log4j @Log4j2 @XSlf4j
public class ApiClientConfiguration {
// log.debug(), log.info(), ...
}
많은 로깅 프레임워크 가 지원되며 물론 인스턴스 이름, 주제 등을 사용자 지정할 수 있습니다.
10. 스레드보다 안전한 방법 작성
Java에서는 동기화된 키워드를 사용하여 중요한 섹션을 구현할 수 있습니다. 그러나 이것은 100% 안전한 접근 방식이 아닙니다. 다른 클라이언트 코드도 결국 우리 인스턴스에서 동기화되어 예기치 않은 교착 상태가 발생할 수 있습니다.
여기서 @Synchronized 가 사용됩니다. 메서드(인스턴스 및 정적)에 어노테이션을 추가할 수 있으며 구현에서 잠금에 사용할 자동 생성된 비공개 비공개 필드를 얻을 수 있습니다.
@Synchronized
public /* better than: synchronized */ void putValueInCache(String key, Object value) {
// whatever here will be thread-safe code
}
11. 객체 구성 자동화
Java에는 "조성 상속 선호" 접근 방식을 매끄럽게 하는 언어 수준 구성이 없습니다. 다른 언어에는 이를 달성하기 위해 Traits 또는 Mixins 와 같은 기본 제공 개념 이 있습니다.
Lombok의 @Delegate 는 이 프로그래밍 패턴을 사용하고자 할 때 매우 편리합니다. 예를 들어 보겠습니다.
- 우리는 사용자 와 고객 이 이름 지정 및 전화 번호에 대한 몇 가지 공통 속성을 공유하기를 원합니다 .
- 이러한 필드에 대해 인터페이스와 어댑터 클래스를 모두 정의합니다.
- 모델이 인터페이스와 @Delegate 를 어댑터에 구현 하도록 하여 연락처 정보로 모델을 효과적으로 구성 합니다.
먼저 인터페이스를 정의해 보겠습니다.
public interface HasContactInformation {
String getFirstName();
void setFirstName(String firstName);
String getFullName();
String getLastName();
void setLastName(String lastName);
String getPhoneNr();
void setPhoneNr(String phoneNr);
}
이제 지원 클래스 로서의 어댑터 :
@Data
public class ContactInformationSupport implements HasContactInformation {
private String firstName;
private String lastName;
private String phoneNr;
@Override
public String getFullName() {
return getFirstName() + " " + getLastName();
}
}
이제 흥미로운 부분입니다. 연락처 정보를 두 모델 클래스로 구성하는 것이 얼마나 쉬운지 확인하십시오.
public class User implements HasContactInformation {
// Whichever other User-specific attributes
@Delegate(types = {HasContactInformation.class})
private final ContactInformationSupport contactInformation =
new ContactInformationSupport();
// User itself will implement all contact information by delegation
}
고객 의 경우는 너무 유사하여 간결성을 위해 샘플을 생략할 수 있습니다.
12. 롬복 롤백?
짧은 대답: 전혀 그렇지 않습니다.
프로젝트 중 하나에서 Lombok을 사용하는 경우 나중에 해당 결정을 되돌리고 싶을 수도 있다는 걱정이 있을 수 있습니다. 잠재적인 문제는 해당 클래스에 대해 어노테이션이 달린 많은 수의 클래스가 있다는 것입니다. 이 경우 동일한 프로젝트 의 delombok 도구 덕분에 문제가 해결 되었습니다.
코드 를 delombok-ing 함으로써 Lombok이 빌드한 바이트코드와 정확히 동일한 기능을 가진 자동 생성된 Java 소스 코드를 얻습니다. 그런 다음 원래 어노테이션이 달린 코드를 이러한 새 deomboked 파일 로 간단히 대체할 수 있으며 더 이상 의존하지 않습니다.
이것은 우리가 빌드에 통합 할 수 있는 것 입니다.
13. 결론
이 기사에서 소개하지 않은 몇 가지 다른 기능이 있습니다. 우리는에 깊은 다이빙이 걸릴 수 있습니다 기능 개요 자세한 내용과 사용 사례를.
게다가, 우리가 보여준 대부분의 기능에는 우리가 편리하게 찾을 수 있는 많은 사용자 정의 옵션이 있습니다. 사용 가능한 내장 구성 시스템 도 이에 도움이 될 수 있습니다.
이제 Lombok에 Java 개발 도구 세트를 사용할 수 있는 기회를 제공하여 생산성을 높일 수 있습니다.
예제 코드는 GitHub 프로젝트 에서 찾을 수 있습니다 .