티스토리 뷰

이전에 이런 포스팅을 한 적이 있다. ( kotlin(+JPA) entity 에서 setter 를 막을 수 있을까 ) 결론을 암울하게 끝낸 것 같아서 몇가지 해결법을 포스팅하고자 한다.

 

1. 프레임워크에 의존하지 않는 domain layer

@Entity
@Table(name = "product")
class Product(
    @Column(name = "product_name")
    var name: String,

    @Column(name = "product_price")
    var price: Long,

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "product_id")
    val id: Long? = null,
) {
    fun update(name: String, price: Long) {
        this.name = name
        this.price = price
    }
    
    // domain logics...
}

이런 product 엔티티가 있다고 생각해보자. 상품의 이름이나 가격은 변경될 수 있기에 var 로 선언했다. 지속적으로 점점 많은 개발자들이

객체지향적인 코드를 작성하고, 도메인 객체에 로직을 담으려고 노력하는 모습을 심심찮게 볼 수 있다. 그리고 그런 노력의 일환으로 JPA 가 이전에 비해 훨씬 대중화되었다.(국내에서 JPA 가 이 정도로 대중화된건 정말 몇 년 되지 않았다)

 

그리고 도메인 객체로 JPA 의 Entity 를 활용하게 되는데, 사실 문제는 여기서 발생한다. 도메인 레이어는 다른 외부 기술에 의존하지 않는 순수한 레이어로 존재해야 하는데, JPA 에 의존해버리니 이런 문제가 발생하는 것이다. 도메인 레이어와 JPA 의 연관관계를 끊는다면 JPA 엔티티는 여전히 setter 를 감출 수 없지만 우리는 애초에 setter 를 감출 필요가 사라지게 될 것이다. 다만 레이어를 한 단계 추가하게 되면 필연적으로 각 레이어를 연결해줄 매핑코드가 늘어나게 되고, 이는 곧 코드 관리 비용으로 연결된다.

@Entity
@Table(name = "product")
class JpaProduct(
    @Column(name = "product_name")
    val name: String,

    @Column(name = "product_price")
    val price: Long,

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "product_id")
    val id: Long? = null,
)

class Product(
    val id: ProductId,
    name: String,
    price: Long,
) {
    var name: String = name
    private set

    var price: Long = price
    private set

    fun update(name: String, price: Long) {
        this.name = name
        this.price = price
    }

    // domain logics
}

 

2. 인터페이스 구현

위 방법외에 엔티티가 인터페이스를 구현하도록 하는 방법이 있다.

@Entity
@Table(name = "product")
class JpaProduct(
    name: String,

    price: Long,

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "product_id")
    override val id: Long? = null,
) : Product {
    @Column(name = "product_name")
    override var name: String = name
    private set

    @Column(name = "product_price")
    override var price: Long = price
    private set

    fun update(name: String, price: Long) {
        this.name = name
        this.price = price
    }
}

interface Product {
    val id: Long?
    val name: String
    val price: Long
}

private setter 를 만들지 못하는 이유는 JPA 가 클래스 기반 상속 프록시를 구현하기 위해 클래스에 open 을 붙여야 되기 때문이다. 결국 open 과 private 이 서로 공존할 수 없는 이유인 것이다. JPA 가 프록시를 구현할때 클래스 상속 기반이 아니라 인터페이스 기반으로 동작하게 한다면 JPA 엔티티에 private setter 를 선언할 수 있다. 인터페이스를 이용했을때 가능하게 하려면 인터페이스에 선언할 프로퍼티들은 모두 val 이어야한다. 인터페이스부터 var 로 선언하게 되면 이전 문제와 똑같은 문제가 발생한다.

 

인터페이스에 val 로 선언하게 되면 JPA 가 프록시를 만들때도 클래스가 아니라 인터페이스를 활용하기 때문에 프록시를 만들때도 getter 만 필요로 할뿐 setter 를 필요로 하지 않기 때문에 private setter 가 가능해지는 것이다. 다만 이 방식을 이용하면 인터페이스 기반의 프록시를 만들게되므로 클래스에 open 을 붙일 필요가 없다. 즉 allopen 과 같은 플러그인도 제거해줘야하고, 모든 JPA 엔티티에 인터페이스를 만들어 줘야 한다.

 

인터페이스에 val 로 선언한 프로퍼티를 하위 구현체에서 var 로 바꾸는 것에 의아함을 가질 수 있으나 가만히 생각해보면 val 이 var 가 되는게 맞고 그 반대는 안되는게 맞다. 자바 기반으로 생각해보면 val 로 선언한다는 것은 인터페이스에 getter 만 만들어 놓는 것이다. getter 만 만들어놓은 인터페이스를 구현하면서 구현 클래스에 setter 를 추가하는건 이상할게 없는 코드다. 오히려 인터페이스에 getter, setter 를 모두 만들어놨는데 구현체에서 setter 를 없앨 수 있는 방법은 없다.

 

결론

첫 번째 방법은 애초에 JPA 엔티티에 private setter 를 만들 필요가 사라지게 하는 방법이고, 두 번째 방법은 결국 private setter 를 만들게 하는 방법이다. 두 번째 방법은 코틀린을 쓰면서도 코드양이 너무 많아지기 때문에 어차피 많은 코드를 작성해야 하는거라면 첫 번째 방법이 낫다고 생각한다. 레이어를 분리하고, 순수하게 가져갈수록 레이어간에 매핑코드가 많아지는건 필연적이므로 trade off 비용을 고려하면서 알맞은 방법을 선택하는게 좋을 것 같다. 두 가지 방법 중 어떤 방법을 선택하든 많아지는 코드양으로 인해 동료들을 설득하기가 쉽지 않아 보이기도 한다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함