val
val은 lombok 0.10에 도입되었습니다.
val은 실제로 변수 type을 작성하는 대신 지역 변수 선언의 type으로 사용할 수 있습니다. 이렇게 하면 initializer표현식에서 type이 유추됩니다. 지역 변수도 final 키워드가 붙어서 최종적으로 만들어집니다. 이 기능은 지역 변수와 foreach loop에서만 작동하며 필드에서는 작동하지 않습니다.
val은 실제로는 일종의 type이며, lombok 패키지에 실제로 클래스로 존재합니다.
1) lombok을 사용한 코드
import java.util.ArrayList;
import java.util.HashMap;
import lombok.val;
public class ValExample {
public String example() {
val example = new ArrayList<String>();
example.add("Hello, World!");
val foo = example.get(0);
return foo.toLowerCase();
}
public void example2() {
val map = new HashMap<Integer, String>();
map.put(0, "zero");
map.put(5, "five");
for(val entry : map.entrySet()) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
}
}
2) java
import java.util.ArrayList;
import java.util.HashMap;
import lombok.val;
public class ValExample {
public String example() {
final ArrayList<String> example = new ArrayList<String>();
example.add("Hello, World!");
final String foo = example.get(0);
return foo.toLowerCase();
}
public void example2() {
final HashMap<Integer, String> map = new HashMap<Integer, String>();
map.put(0, "zero");
map.put(5, "five");
for(final Map.Entry<Integer, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
}
}
var
var는 변수에 final 키워드가 적용되지 않는 점을 제외하면 정확히 val과 같습니다.
var는 val과 다르게 final 키워드가 적용되지 않기 때문에 변수에 추가적인 할당이 가능합니다. 하지만, 처음 초기화 표현식에만 타입 추론 기능이 작동하고, 추가적인 할당에 따른 추론기능은 동작하지 않습니다.
예를 들어, 아래와 같은 코드는 동작하지 않습니다.
var x = "Hello";
x = Color.RED;
x는 처음 표현식에서 java.lang.String으로 type이 추론됩니다. 하지만 x = Color.RED에서는 type 추론이 동작하지 않기 때문에 오류가 발생합니다.
@NonNull
@NonNull Annotation은 lombok v0.11.10에서 도입되었습니다.
@NonNull은 메서드 또는 생성자의 parameter를 통해 lombok에서 null-check 문을 생성하도록 할 수 있습니다.
null-check는 메서드의 맨 위에 삽입됩니다. 생성자의 경우 명시적 this() 또는 super() 호출 직후에 null-check가 삽입됩니다.
1) Lombok을 사용한 코드
public class NonNullExample {
@Getter @Setter @NonNull
private String name;
}
2) java
public class NonNullExample {
private String name;
public void setName(String name) {
if(name == null) {
throw new NullPointerExcrption("name is marked @NonNull but is null");
}
this.name = name;
}
}
@Getter, @Setter
이 Annotation은 getter와 setter method를 생성합니다.
lombok이 기본 getter/setter를 자동으로 생성하도록 하려면 모든 field에 @getter/@setter를 사용할 수 있습니다. 또는 Class에 넣을 수도 있습니다. 이 경우 모든 non-static field에 Annotation을 사용한 것과 같습니다.
기본 getter는 field명이 foo인 경우 getFoo()가 호출되지만, type이 boolean인 경우 isFoo()가 호출됩니다.
생성된 getter/setter method는 명시적으로 AccessLevel을 지정하지 않으면 public이 됩니다.
AccessLevel은 PUBLIC, PROTECTED, PACKAGE, PRIVATE, NONE이 있습니다.
AccessLevel.NONE 설정을 통해 getter, setter를 생성하지 않을 수도 있습니다. 이것은 Class에 Annotation을 override 할 수 있습니다.
1) Lombok
@Getter
@Setter
public class User {
private String name;
private Integer age;
}
2) java
public class User {
private String name;
private Integer age;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(Integer age) {
this.age = age;
}
public Integer getAge() {
return age;
}
}
이 밖에 아래와 같이 변수 별로 나눠서 지정할 수도 있습니다.
public class User {
@Getter @Setter private String name;
@Setter(AccessLevel.PROTECTED) private Integer age;
}
@Cleanup
@Cleanup을 사용하여 코드 실행 경로가 현재 범위를 벗어나기 전에 지정된 리소스가 자동으로 정리되도록 할수 있습니다. 다음과 같이 지역 변수 선언에 @Cleanup Annotation을 사용하여 이를 수행합니다.
@Cleanup InputStream in = new FileInputStream("some/file");
결과적으로 사용자가 속한 범위의 끝에서 in.close()가 호출됩니다. 이 호출은 try/finally 구성을 통해 실행됩니다.
정리하려는 객체 유형에 close() method가 없지만 parameter가 없는 다른 method가 있는 경우 다음과 같이 method의 이름을 지정할 수 있습니다.(하나 이상의 parameter를 사용하는 method는 @Cleanup을 통해 호출할 수 없습니다.)
@Cleanup("disable") org.eclipse.swt.widgets.CoolBar bar = new CoolBar(parent, 0);
단, try 에서 예외가 발생했고 cleanup method에서도 예외가 발생할 경우 cleanup method에서 발생한 예외는 완전히 무시됩니다. 그러므로 @Cleanup에 완전히 의존하는 것은 위험합니다.
1) lombok를 사용한 코드
import lombok.Cleanup;
import java.io.*;
public class CleanupExample {
public static void main(String [] args) throws IOException {
@Cleanup InputStream in = new FileInputStream(args[0]);
@Cleanup OuputStream out = new FileOuputStream(args[1]);
byte[] b = new byte[10000];
while(true) {
int r = in.read(b);
if(r == -1) {
break;
}
out.write(b, 0, r);
}
}
}
2) java
import java.io.*;
public class CleanupExample {
public static void main(String [] args) throws IOException {
InputStream in = new FileInputStream(args[0]);
try {
OuputStream out = new FileOuputStream(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();
}
}
}
}
@ToString
@ToString은 Class에 있는 멤버 변수를 문자열로 변환하여 return하는 toString() method를 생성합니다. 기본적으로 각 field와 함께 class name을 순서대로 쉽표로 구분하여 출력합니다.
includeFieldNames parameter를 true로 설정하면 toString()의 출력에 약간의 명확성(field name)을 추가할 수 있습니다.
기본적으로 모든 non-static field가 출력됩니다. 일부 field를 건너 뛰려면 field에 @ToString.Exclude를 사용할 수 있습니다. 또는 Class에 @ToString(onlyExplicitlyIncluded = true)을 사용한 다음 출력할 필드에 @ToString.Include를 사용할 수 있습니다.
출력할 이름을 변경하고 싶을 경우에는 @ToString.Include(name = "some other name")을 사용할 수 있습니다.
출력 순서를 변경하고 싶을 경우에는 @ToString.Include(rank = -1)를 사용할 수 있습니다.
1) lombock 사용
import lombok.ToString;
@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;
}
}
}
2) java
import java.util.Arrays;
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;
}
@Override public String toString() {
return "ToStringExample("
+ this.getName() + ", "
+ this.shape + ", "
+ Arrays.deepToString(this.tags)
+ ")";
}
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
+ ")";
}
}
}
@EqualsAndHashCode
@EqualsAndHashCode는 코드에서 객체 비교 등의 용도로 사용되는 equals(Object other), hashCode() 메소드의 코드를 생성해줍니다. @EqualsAndHashCode.Include, @EqualsAndHashCode.Exclude를 통해 사용될, 사용되지 않을 field를 지정할 수 있습니다.
1) lombok
import lombok.EqualsAndHashCode;
@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;
}
}
}
2) java
import java.util.Arrays;
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
@NoArgsConstructor는 매개 변수가 없는 생성자를 생성합니다. 이것이 가능하지 않은 경우 컴파일 오류가 발생합니다. @NoArgsConstructor(force=true)를 사용하지 않으면 모든 field가 0/false/null 로 초기화 됩니다.
@NonNull이 있는 경우 검사가 수행되지 않습니다. 따라서 해당 field가 제대로 초기화 될때까지 이러한 제약은 일반적으로 충족되지 않음을 유의해야 합니다.
@RequiredArgsConstructor는 초기화 되지 않은 final field나, @NonNull이 붙은 field에 대해 생성자를 생성합니다.
@AllArgsConstructor 는 Class의 각 Field에 대해 1개의 매개 변수가 있는 생성자를 생성합니다. @NonNull이 표시된 Field는 해당 매개 변수에 대해 null-check를 수행합니다.
이 3개의 Annotation 모두 static factory를 만들 수 있는 옵션이 있습니다.
staticName이라는 옵션을 사용하여 생성자를 private으로 생성하고, private 생성자를 감싸고 있는 static factory method를 추가할 수 있습니다.
1) lombok 사용
import lombok.RequiredArgsConstructor;
import lombok.NonNull;
@RequiredArgsConstructor(staticName = "of")
public class ConstructorExample {
private int x, y;
@NonNull private T description;
}
2) java
public class ConstructorExample {
private int x, y;
@NonNull private String description;
private ConstructorExample(String description) {
if (description == null) throw new NullPointerException("description");
this.description = description;
}
public static ConstructorExample of(String description) {
return new ConstructorExample(description);
}
}
private 생성자가 생성되고, private 생성자를 create 하는 static factory method가 생성된 것을 볼 수 있습니다.
생성자 Annotation은 주로 의존성 주입(Dependency Injection, DI) 편의성을 위해서 사용되곤 합니다.
Spring의 의존성 주입의 특징 중 한가지를 이용하는데 다음과 같습니다.
어떠한 빈(Bean)에 생성자가 오직 하나만 있고, 생성자의 parameter 타입이 Bean으로 등록 가능한 존재라면 이 Bean은 @Autowired Annotation 없이도 의존성 주입이 가능하다.
@Service
@RequiredArgsConstructor
public class RequiredArgsConstructorDIServiceExample {
private final FirstRepository firstRepository;
private final SecondRepository secondRepository;
private final ThirdRepository thirdRepository;
// ...
}
위 의 java 파일을 컴파일하면 아래와 같이 변경됩니다.
@Service
public class RequiredArgsConstructorDIServiceExample {
@ConstructorProperties({"firstRepository", "secondRepository", "thirdRepository"})
public RequiredArgsConstructorDIServiceExample(FirstRepository firstRepository, SecondRepository secondRepository, ThirdRepository thirdRepository) {
this.firstRepository = firstRepository;
this.secondRepository = secondRepository;
this.thirdRepository = thirdRepository;
}
}
Class 파일을 보면 @ConstructorProperties Annotation과 함께 final field를 매개변수로 하는 생성자가 생성되었습니다. 매개변수로 있는 3개의 repository는 Bean으로 등록이 가능한 존재이므로, @Autowired Annotation 없이 의존성 주입이 이루어지게 됩니다.
@Data
@Data는 Class에 정의된 모든 Field에 대한 getter, setter와 toString, equals, hashCode, final로 지정됐거나 @NonNull로 명시된 Field에 대한 값을 받는 생성자 메소드 코드를 생성해 줍니다. 즉, @Getter, @Setter, @NonNull, @EqualsAndHashCode, @ToString에 대한 것을 모두 해주는 Annotation입니다.
@Value
@Value는 @Data의 변형입니다. 모든 Field는 리본적으로 private 및 final로 만들어지며, setter는 생성되지 않습니다. Class 자체도 기본적으로 final로 만들어 지는데, 불변성은 하위 클래스로 강제할 수 있는 것이 아니기 때문입니다.
1) lombok
import lombok.AccessLevel;
import lombok.experimental.NonFinal;
import lombok.experimental.Value;
import lombok.experimental.Wither;
import lombok.ToString;
@Value public class ValueExample {
String name;
@Wither(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;
}
}
2) java
import java.util.Arrays;
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
다수의 Field를 가지는 복잡한 Class의 경우 생성자 대신 Builder를 사용하는 경우가 많습니다. 이것을 디자인 패턴 중 필더 패턴이라고 합니다.
@Builder Annotation 은 Class에 대한 복잡한 Builder 패턴을 자동으로 생성해 줍니다.
Builder Pattern먼저 Builder 패턴에 대해 살펴봅시다.
아래와 같이 User class가 있다고 가정합니다.
public class User {
private String userId;
private String userName;
private String password;
}
이 Class의 객체를 생성하고, setter를 호출해서 값을 setting할 수 있습니다.
User user = new User();
user.setUserId("hong");
user.setUserName("홍길동");
user.setPassword("1234");
setter를 이용한 값 주입은 실무에서도 어렵지 않게 볼 수 있는 코드입니다. 하지만 setter를 이용한 주입은 객체 생성 시점에 필요한 모든 값들을 주입하지 않아 개발자의 실수가 발생할 수 있으며, public으로 공개해놓은 set method는 코드 다른 부분에서 언제 호출되어 값이 바뀔지 알기 힘듭니다.
이런 문제를 해결하기 위해 아래와 같이 생성자를 이용해 값을 주입할 수 있습니다.
User user = new User(1, "홍길동", "1234");
생성자를 사용하면 값이 비어있는 객체가 생성될 걱정을 하지 않아도 됩니다. 하지만, Field 순서를 외우지 못하면 객체를 만들때마다 값의 순서를 확인해봐야 합니다.
이럴 때 사용할 수 있는 것이 Builder pattern 입니다.
순수 Java를 이용하여 만들어진 builder class는 아래와 같습니다.
public class User {
private String userId;
private String userName;
private String password;
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String userId;
private String userName;
private String password;
private Builder() { }
public Builder userId(String userId) {
this.userId = userId;
return this;
}
public Builder userName(String userName) {
this.userName = userName;
return this;
}
public Builder password(String password) {
this.password = password;
return this;
}
public User build() {
User user = new User();
user.userId = this.userId;
user.userName = this.userName;
user.password = this.password;
return user;
}
}
}
Builder Pattern의 핵심은 User 객체를 만드는 보조 내부 Class를 하나 만들고, 내부 Class의 객체를 이용해 User 객체를 생성하는 것입니다.
이 Builder Pattern을 사용하는 코드는 아래와 같습니다.
User user = User.builder()
.userId("hong")
.userName("홍길동")
.password("1234")
.build();
Builder 객체를 이용해 명시적으로 값을 setting하고 마지막에 build() mathod를 통해 객체를 생성합니다. 바깥에서는 Builder 타입을 참조할 일이 없으므로 build()를 호출하지 않으면 컴파일 에러를 발생시킵니다.
setter를 이용할 때 처럼 값이 누락된 객체를 생성할 일이 없고(단, 유효성 체크 코드 필요) 생성자를 이용할 때 처럼 순서를 외우거나 확인할 필요도 없습니다. 다만 작성해야 할 코드가 매우 길어지는 단점이 있는데, 이 때 @Builder Annotation을 사용하면 쉽게 Builder Class를 만들 수 있습니다.
@Builder
public class User {
private String userId;
private String userName;
private String password;
@Singular
private List<Integer> scores;
}
컬렉션으로 된 Field에는 @Singular Annotatioin을 선언하면 모든 원소를 한번에 넘기지 않고 하나씩 추가할 수 있습니다.
User user = User.builder()
.userId("hong")
.userName("홍길동")
.password("1234")
.score(70)
.score(80)
.build();
@SneakyThrows
@SneakyThrows는 Exception을 몰래 발생시키는데 사용할 수 있습니다.
1) lombok
import lombok.SneakyThrows;
public class SneakyThrowsExample implements Runnable {
@SneakyThrows(UnsupportedEncodingException.class)
public String utf8ToString(byte[] bytes) {
return new String(bytes, "UTF-8");
}
@SneakyThrows
public void run() {
throw new Throwable();
}
}
2) java
import lombok.Lombok;
public class SneakyThrowsExample implements Runnable {
public String utf8ToString(byte[] bytes) {
try {
return new String(bytes, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw Lombok.sneakyThrow(e);
}
}
public void run() {
try {
throw new Throwable();
} catch (Throwable t) {
throw Lombok.sneakyThrow(t);
}
}
}
@Synchronized
@Synchronized는 static 및 instance용 lock object를 자동으로 생성해주고, Annotation을 method에 적용할 경우에는 method body를 synchronized 키워드로 감싸줍니다. 여기서 관심있게 볼 부분은 new Object()가 serialize 되지 않는 것을 고려하여 $lock, instance용 object를 new Object[0]로 선언해준다는 것입니다. Annotation된 method가 정적이면 Lombock을 통해 $LOCK, Class object가 자동으로 작성되어 @Synchronized method와 동기화 시킵니다. 만약 별도의 lock 객체를 생성해서 특정 method에만 적용하고 싶다면 (read-lock 같은 경우를 위해) @Synchronized("lock-object-name") 형식으로 쓰면 됩니다. 대신 lock-object-name은 프로그래머가 직접 필드를 선언해야 합니다. 예를 들면, @Synchronized("myObject")는 myObject 오브젝트와 대조하여 @Synchronized method를 동기화 합니다.
1) lombok
import lombok.Synchronized;
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");
}
}
2) java
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");
}
}
}
[참고 사이트]
- https://projectlombok.org/features/all
- https://www.daleseo.com/lombok-useful-annotations/
- https://multifrontgarden.tistory.com/207
'웹 개발' 카테고리의 다른 글
javascript ajax 크로스 도메인 요청하기(CORS) (0) | 2020.09.30 |
---|---|
시간 기반 UUID 생성(Generate time based UUIDs) (0) | 2020.09.29 |
Annotation을 활용한 Spring AOP 활용 (0) | 2020.09.24 |
Java에서 날짜, 시간 제대로 사용하기(LocalDate, LocalTime, LocalDateTime) (0) | 2020.09.22 |
[Maven Error] you need to run build with jdk or have tools.jar on the classpath (0) | 2020.04.08 |