24.05.29. 엔티티 리팩토링

develemon·2024년 6월 20일

Doran

목록 보기
6/13
post-thumbnail

오랜만의 게시물 작성! 그간 알바와 다른 일들에 신경쓰고 또 쌓였던 기술부채에 허덕이다가 잠깐이나마 시간내서 기록해보기로 한다. MSA 구조로 설계한만큼 고려해야할 복잡한 요구사항들이 있었고, 그 외의 리팩토링이 필요한 부분들도 있었다. 이번 포스팅의 주제는 리팩토링이다.

Intro


이 리팩토링의 시작은 매핑 방식에 있어서 ModelMapper보다 MapStruct를 사용하는 것이 성능상 더 유리하므로 매핑 방식을 교체하려는 데에 있었다. 왜 MapStruct가 더 유리한지는 나중에 알아보도록 하고, 리팩토링 과정을 천천히 돌아보면서 단계별로 포스팅하고자 한다.

우선 ModelMapper를 사용함에 있어서, 객체 간의 동일한 필드 이름에 따라 매핑이 되도록 하는 부분과 간결한 사용방식이 쉽게 사용하기 좋은 매퍼였다. 그러나 매핑이 필요한 객체들이 많아지게 되면 매핑에 필요한 로직이 모두 분산되어 코드를 관리하기가 어려워진다. 뿐만 아니라 매핑 방식에 있어서 성능상 더 유리한 MapStruct라는 것도 존재한다. 그래서 서비스의 규모가 커진다면 ModelMapper에 대한 방안을 고려해야 한다.

그러나 그에 앞서서 내 코드에는 문제점이 있었다. 엔티티 클래스에서는 보통 @Setter@NoArgsContructor, @AllArgsContructor를 두지 않는 게 원칙인데 그 원칙들이 지켜지지 않았다. 이게 원칙인 이유는 아무래도 엔티티 객체가 리포지토리에 직접적으로 연결되어 있다보니 엔티티의 값을 함부로 주입 및 초기화하는 게 보안 상으로 부적절하기 때문이다. 하지만 코드를 작성하다보면 엔티티에 대한 생성자와 필드값의 편리한 주입이 필요하다보니 이를 어떻게 해결하면 좋을지 고민에 빠지게 되었다.

한편으로는 @Setter@NoAgrsConstructor, @AllArgsConstructor들이 있었기에 ModelMapper의 사용에 있어서도 막힘이 없었다. 그러나 이들을 제거하면서 매핑하려고 하면 생성자가 없어 초기화를 할 수 없다는 에러를 받곤 했다. 물론 MapStruct에서는 @Setter가 필요 없지만, 처음 적용해보다보니 ModelMapper와 MapStruct의 동작 방식에 대해서도 이해가 필요했고, 이 과정에서는 결국 엔티티 리팩토링도 불가피했다.

엔티티 리팩토링


@NoArgsConstructor 제거

@Entity, @Embeddable 어노테이션을 사용하는 클래스는 DB의 테이블과 매핑할 클래스이기 때문에 기본 생성자를 필요로 한다. 그래서 엔티티 클래스에서 @NoArgsContructor를 붙이지 않으면 Class 'Item' should have [public, protected] no-arg constructor라는 에러 메세지를 받게 된다. 그렇다고 @NoArgsContructor로 파라미터 없는 생성자를 외부에서 사용할 수 있게 열어주면 안되기 때문에

@NoAgrsContructor(access = AccessLevel.PROTECTED)

위와 같이 access 옵션에 값을 PROTECTED로 설정해주면 외부에서는 사용하지 못하지만 @Entity로서의 조건에 맞춰지고 또 자식 클래스에서도 부모 클래스의 기본 생성자를 사용할 수 있게 된다.

@AllArgsContructor, @Setter 제거

@NoArgsContructor는 필드값 주입을 받지는 않다보니 대체방안이 쉽게 나오지만, 주입 및 초기화는 반드시 필요하다. 그러나 @AllArgsContructor@Setter를 모두 사용해서는 안된다는 조건에 대해 초보자라면 당황할 수 있다. '어떻게 이 어노테이션들을 안쓰고 필드값 주입을 받을 수 있는데?'

필요한 생성자에 대한 @Builder 사용

코드 작성에 약간의 수고를 더 들이긴 해야하지만 @Builder 어노테이션을 사용함으로써 해결할 수 있다. @RequiredArgsContructor를 사용하는 것과 비슷하게, 필요한 생성자를 직접 만들어주고, 그 생성자 위에 @Builder 어노테이션을 붙이는 것이다. 대신 @RequiredArgsConstructor를 사용하는 것도 결국 @NoAgrsContructor@AllArgsConstructor를 사용하는 것과 다를 바 없어지기 때문에, 이를 사용하지 말고 엔티티에 최대한 제한적으로 접근할 수 있도록 필요한 생성자를 직접 만들어주도록 해야한다. 코드를 같이 확인해보자.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DiscriminatorValue("BOOK")
public class Book extends Item {

    private String author;
    private String isbn;
    private int pages;
    private Date publicationDate;
    private String contentsTable;
    private String bookReview;

    @Builder
    public Book(String itemUuid, String itemName, int price, int stockQuantity, String itemImageUrl, Category category,
            String author, String isbn, int pages, Date publicationDate, String contentsTable, String bookReview) {
        super(itemUuid, itemName, price, stockQuantity, itemImageUrl, category);
        this.author = author;
        this.isbn = isbn;
        this.pages = pages;
        this.publicationDate = publicationDate;
        this.contentsTable = contentsTable;
        this.bookReview = bookReview;
    }
}

Book 엔티티 클래스를 보면, 필요한 생성자를 직접 만들어주고 해당 생성자에 한해 @Builder가 적용되게 함으로써 필드값 주입 및 초기화를 문제 없이 수행할 수 있다.

추가로 Book 엔티티 클래스는 Item 엔티티 클래스를 상속받는데, Book 엔티티 클래스의 객체를 통해 Item 엔티티 필드값도 주입받길 원한다면 super() 호출을 통해 부모 클래스의 필드값도 입력받을 수 있다. 대신 부모 클래스에서도 super()와 일치하는 형태의 생성자를 별도로 작성해주어야 한다.

엔티티 클래스 내 메서드 생성

물론 답답함을 느낄 수 있는 게, 특정 필드의 값을 변경하고자 할 때마다 빌더를 통해 접근하는 것도 다소 이상하긴 하다. 이럴 때에는 @Setter는 사용하지 않는 대신에 엔티티 클래스 내에 특정 필드에 대한 별도의 메서드를 만들어주면 된다.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype")
public abstract class Item extends AuditingFields {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "item_id")
    private Long id;
    private String itemUuid;
    private String itemName;
    private int price;
    private int stockQuantity;
    private String itemImageUrl;
    @Enumerated(EnumType.STRING)
    private Category category;

    public void addStock(int quantity) {
        this.stockQuantity += quantity;
    }

    public void removeStock(int quantity) {
        int restStock = this.stockQuantity - quantity;
        if (restStock < 0) {
            return;
        }
        this.stockQuantity = restStock;
    }

	// ...
}

위와 같이 addStock()removeStock() 메서드를 통해 특정 필드에 접근할 수 있으면서도 동시에 @Setter와는 달리 필드값 주입이 남발될 가능성을 제한함으로써 필요를 적절히 채울 수 있게 된다.

이로써 엔티티 리팩토링 정리는 해결하였다. 다음 포스팅에서는 ModelMapperMapStruct의 비교를 통해 왜 MapStruct가 더 유리한지 살펴보도록 한다.

profile
유랑하는 백엔드 개발자 새싹 블로그

0개의 댓글