난 죽어도 쓰지 않을것같던 코틀린을 배우고있다.
자바와 Lombok이 친숙한 사람들은 스프링 부트에서 엔티티를 다음처럼 짤것이다.
@Entity
@Getter @Setter
public class Menu {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "place_id")
private Place place;
private String menuName;
private int menuPrice;
@Column(columnDefinition = "text", nullable = true)
private String menuImg;
}
Entity에 Setter를 지양해야 하는 이유같은 글을 써놓고 @Setter를 붙인건 당장은 무시하고, 수많은 Getter, Setter가 우리 코드를 침범하는걸 막기 위해 Lombok을 통해 많이들 구현한다.
디버깅이나, 로깅등을 위해 toString, hashcode등을 인텔리제이 시켜서 작성도 간간히 한다.
코틀린은 나름 최신 언어답게 이런 불편함을 제거해준다.
@Entity
data class Article(
@Id
@GeneratedValue
val articleId: Long,
var createdAt: LocalDateTime,
var updatedAt: LocalDateTime,
var content: String,
var title: String,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
val user: User,
)
엔티티 구성은 조금 다르지만.. class 앞에 data 한마디 붙여준것만으로
생성자, getter, setter, toString, hashCode, equals, copy(deep copy임.)를 지원시켜준다. 자바와 달리 여러분의 alt+insert키의 수명을 10년은 늘려줄것이다.
위 메서드들을 지원하게 하고싶은 필드를, data class의 생성자 위치에 써주면 된다.
class Myclass(필드1, 필드2, 필드3...)
참고로, 코틀린은 생성자를 위와 같이 클래스에 괄호로 붙여서 선언한다.
data class는 이런 편의사항이 있지만, 제약도 있다.
abstract와 open(해당 클래스를 상속하는것을 허용하는 키워드)이 불가능하다. Data Class는 immutable
을 목적으로 만들어진 클래스기 때문이다.
추가적으로, 필드가 var
이면 Setter를 사용가능하고, val
이면 사용 불가능하다. 개인적으로 무지성 val을 쓴다음 필요 할 때 var로 바꿔주는 방법을 선호한다.
양방향 매핑(@ManyToOne 이후 @OneToMany(mappedBy=...) )를 사용할때 아주 주의해야한다.
자바로 코드를 짤때도, 양방향 매핑시 toString() 메서드는 주의해서 override 했어야했다.
순환 참조
때문인데, 다음 예시를 보자. PK는 생략한다.
@Entity
public class Parent {
private String name;
@OneToMany(mappedBy="parent")
private List<Child> children = new ArrayList();
@Override
public String toString() {
return "Menu{" +
"id=" + id +
", children=" + children +
'}';
}
}
@Entity
public class Child {
private String name;
@ManyToOne
@JoinColiumn(name = "parent_id")
private Parent parent;
@Override
public String toString() {
return "Menu{" +
"name=" + name +
", Parent=" + Parent +
'}';
}
}
위 엔티티가 있다고 할때, 개발중 log등에서 log.info("child = {}", child)
를 써버렸다고 하자.
그러면 child에서 toString이 호출되고, 그 안에 있는 parent때문에 parent의 toString이 호출되고, 또 그 안에 있는 children때문에 child의 toString이 호출되고...
함수 호출이 무한히 이루어진다. 재귀함수를 배웠다면 미래가 보일것이다. StackOverFlow
Exception을 무조건 발생시킨다.
그런데, 코틀린의 data class
가 지원하는 메서드중 toString()도 존재한다. 그래서 무심코 생성자 자리에 양방향 매핑을 시켜버리면, 로그를 찍지 않더라도 사용 과정에서 내부 로직등에 의해 반드시 순환 참조가 발생한다. 진짜 무조건 발생한다.
근데 스프링부트와 JVM이 어떻게든 관리하고 버텨내서 당장 티가 안나는거지만, 이론상 함수 CALL 한번마다 스택 오버플로우가 한번씩 터져서 쓰레드가 하나씩 사망할것이다.
쓰레드는 스택공간을 공유하지 않기때문에 다행인지 아닌지 겉으로 크게 티는 안나지만, 실제 서비스 단계에서 발생한 일이면 끔찍한 미래가 확정이다..
따라서, Data Class에서 @OneToMany는 생성자가 아닌, 클래스 내부에 집어넣자. 다음처럼.
@Entity
data class Article(
@Id
@GeneratedValue
val articleId: Long,
var createdAt: LocalDateTime,
var updatedAt: LocalDateTime,
var content: String,
var title: String,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
val user: User,
) {
@OneToMany(mappedBy = "article", cascade = [CascadeType.REMOVE], orphanRemoval = true)
val comments: MutableList<Comment> = mutableListOf()
}