2019년에 했던 '스프링 가이드' (스터디 자료로 공유된 내용은 비공개이므로 해당 repository를 공유) 스터디 내용을 바탕으로 내용을 정리하고자 한다.
2회차 내용은 lombok 사용법라는 주제였다. 그리고 객체지향에 대한 내용도 다루었다. 그때 당시 객체의 사용과 객체 지향에 대해 관심이 많았지만, 이해가 부족했던 부분들이 '객체지향의 사실과 오해'를 읽고 다시 발표자료를 보니 더욱 공감가는 내용들이 많았다.
아래에서 다룰 내용은 lombok 사용법이지만, 나아가서 객체를 올바르게 사용하려면? 이라는 생각을 가지고 읽어나가면 lombok을 사용하지 않는 누군가에게도 분명히 도움이 될 내용이라고 생각된다.
compile
시점에 어노테이션으로 특정 코드 추가할 수 있게 도와주는 java library@Data
지양하기/**
* Generates getters for all fields, a useful toString method, and hashCode and equals implementations that check
* all non-transient fields. Will also generate setters for all non-final fields, as well as a constructor.
* <p>
* Equivalent to {@code @Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode}.
* <p>
* Complete documentation is found at <a href="https://projectlombok.org/features/Data">the project lombok features page for @Data</a>.
*
* @see Getter
* @see Setter
* @see RequiredArgsConstructor
* @see ToString
* @see EqualsAndHashCode
* @see lombok.Value
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Data {
...
}
@Data
는 위의 docs에서 보이느 것과 같이 @Getter
, @Setter
..를 한방에 주입할 수 있는 만능 어노테이션이다. 언젠간 대가를 치루게 된다는 단점이 있다.
한방에 사용할 수 있다는 장점도 있을 수 있겠지만, 수면위에 드러나지 않는 문제가 많을 수 있기 때문에 지양하자.
setter
는 코드의 의도를 가지기 힘들다.setter
(변경포인트)가 제공되지 않는 것이 맞다. 즉, 인스턴스의 변경 포인트를 제공하지 않음으로써, 변경 기능이 없다는 것을 명시적으로 전달할 수 있음setter
가 열린다는 말은, immutable(불변)하지 않다는 것이다.)💁♀️ @setter
대신에 이렇게 사용하자
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();
}
}
}
위의 코드는 회원의 회원가입을 위한 객체
👉 회원 가입 시 일관성 있게, 객체를 생성할 수 있다.
@ToString
양방향 순환 참조 문제 피하기ToString
을 호출하게 되면 무환 순환 참조 발생👉 @ToString(of={"..."})
권장
exclude
보다 비용이 적음 (새로 객체를 추가해 줄 때마다 수정 비용이 적다.)@ToString
사용할 때 특정 항목을 제외하여 사용할 수 있음@EqualsAndHashCode
남발 지양하기Set
자료 구조)@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();
}
}
reflection
의 이유로 기본 생성자가 꼭 필요한 경우가 있다.
하지만, 기본 생성자를 만든다는 것은, 우리에게 또다른 null
처리의 의무를 부여하게 된다. (다른 어딘가에서 기본생성자를 호출하여 null
이 되면 안되는 필드가 null
이 되어 돌아다닐 수 있다..)
👉 가장 낮은 레벨로 지정하여 사용하자
@NoArgsConstructor(access = AccessLevel.PRIVATE)
# 해당 어노테이션을 사용한다면 Build 시 error 발생하여 어노테이션 제한 가능
lombok.setter.flagUsage=error
lombok.allArgsConstructor.flagUsage=error
lombok.data.flagUsage=error
쿠폰을 사용하기 위해,
@Data
public class Coupon {
private CouponCode couponCode;
private boolean used;
private double discount;
private LocalDateTime expirationDate;
private LocalDateTime createAt;
private LocalDateTime updateAt;
private Account account;
@Builder
public Coupon(CouponCode couponCode, double discount, LocalDateTime expirationDate, Account account) {
this.couponCode = couponCode;
this.discount = discount;
this.expirationDate = expirationDate;
this.account = account;
this.used = false;
}
public boolean isExpiration() {
return LocalDateTime.now().isAfter(expirationDate);
}
public void use() {
verifyExpiration();
verifyUsed();
this.used = true;
}
private void verifyExpiration() {
if(isExpiration()) throw new CouponAlreadyUsedException();
}
private void verifyUsed() {
if(used) throw new CouponExpireException();
}
}
Coupon.use()
만 호출하면 된다.