티스토리 뷰
이전에 이런 포스팅을 한 적이 있다. ( 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 비용을 고려하면서 알맞은 방법을 선택하는게 좋을 것 같다. 두 가지 방법 중 어떤 방법을 선택하든 많아지는 코드양으로 인해 동료들을 설득하기가 쉽지 않아 보이기도 한다.
'kotlin' 카테고리의 다른 글
mockk 의 mocking 방식 (1) | 2022.12.24 |
---|---|
kotest 훑어보기 (1) | 2022.10.17 |
코틀린에서 logger 사용 (1) | 2022.06.01 |
생성자 필드에 애노테이션 선언시 리플렉션을 이용해 찾는 법 (0) | 2021.10.07 |
jpa entity 에 data class 를 사용해도 될까? (3) | 2021.07.17 |
- Total
- Today
- Yesterday
- DesignPattern
- frontcode
- Spring
- Design Pattern
- clean code
- frontend개발환경
- db
- Git
- toby
- JPA
- javascript
- mariadb
- code
- servlet
- Kotlin
- backend개발환경
- java8
- go-core
- 정규표현식
- Jackson
- generics
- TEST
- MySQL
- programming
- spring cloud
- java
- http
- EffectiveJava
- JavaScript Core
- OOP
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |