그러면 이전 포스팅에 만들어둔 Auditing 엔티티를 상속받아 사용할 도메인 엔티티를 만들어보자.
이 프로젝트는 간단한 블로그 형식으로 만들 것이기에, 일단 세가지의 엔티티만 만들어두고 있다.
Member 엔티티
package simpleblog.domain.member
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.Table
import simpleblog.domain.AuditingEntity
@Entity
@Table(name = "Member")
class Member(
email: String,
password: String,
role: Role
) : AuditingEntity() {
@Column(name = "name", nullable = false)
var email: String = email
protected set
@Column(name = "password", nullable = false)
var password: String = password
protected set
@Enumerated(EnumType.STRING)
var role: Role = role
protected set
override fun toString(): String {
return "Member(email='$email', password='$password', role=$role)"
}
companion object {
fun createFakeMember(memberId: Long): Member {
val member = Member(
"",
"",
Role.USER
)
member.id = memberId
return member
}
}
}
fun Member.toDto() =
MemberRes(
this.id!!,
this.email,
this.password,
this.role
)
enum class Role {
ADMIN,
USER
}
자 그럼 다시 어노테이션부터 보자
- @Entity
- 이 클래스가 데이터베이스랑 매칭되는 엔티티라는 것을 알려주는 어노테이션
- @Table
- name : 이 엔티티가 데이터베이스에서 어떤 이름으로 저장될 지 설정할 수 있다.
- @Enumrated
- 이 필드는 ENUM 속성을 갖는다고 설정함
- EnumType.STRING : 이 필드에 enum이 저장될 때, 숫자로 저장될건지 string으로 저장될 건지 선택할 수 있는데, 웬만하면 string으로 저장하는 것이 정신건강에 좋다.
companion object
자바의 static 에 대응되는 개념이지만, 조금 다른 점이 있다.
자바의 경우 static은 클래스에 고정된 변수나 메소드이지만, companion object는 실제로 하나의 싱글톤 객체로 만들어진다.
따라서 인터페이스나 다른 클래스를 상속받는것도 가능하다 !!
더 쉽게 위 코드의 companion object를 자바로 개념적으로 바꿔보자.
public static final class Companion {
private Companion() {}
public final Member createFakeMember(long memberId) {
Member member = new Member("", "", Role.USER);
member.setId(memberId);
return member;
}
}
아마 이런식으로 싱글톤 객체로 만들어질 것이다.
확장 함수
아마 C++에서도 이런 함수가 있었던 것 같다. 클래스 내부에 선언되지는 않지만 마치 클래스의 멤버처럼 동작할 수 있도록 새로운 함수를 추가해주는 기능이다.
fun Member.toDto() =
MemberRes(
this.id!!,
this.email,
this.password,
this.role
)
위의 코드에서는 toDto 함수가 확장 함수이다. Member 함수를 MemberRes라는 Dto 클래스로 바꿔주는 함수인데, 클래스 내부에 선언하지 않고도 이렇게 추가할 수 있다.
이를 활용해서 기존에 있는 라이브러리 클래스들(String, Int 등등)을 직접 수정할수도 있다 !!
val Int.isEven: Boolean
get() = this % 2 == 0
바로 이렇게 짝수인지 아닌지를 표현하는 함수를 만들수 있다.
확장 함수는 클래스를 수정하는 것이 아니라, 컴파일 시점에 static 메소드로 변환된다고 한다.
그러니까 저 toDto 함수도 아마 자바라면 Member.kt 파일 내의 public static final 함수로 바뀔 듯 하다. (Member 클래스 내에 있는 것이 아닌 외부 메소드임!! 따라서 Member의 private나 protected 접근자에는 접근 불가)
단일 표현식 함수
그런데 조금 이상한 문법이 보인다. 중괄호로 감싸는 것이 아니라 바로 = 연산자를 썼다 !!
코틀린의 문법이 자바에 비해 매우 간결하다고 했던 것 중에 하나다. 따로 반환타입, 중괄호와 retrun 문을 작성하지 않고도 컴파일러가 반환 타입을 추론하여 넣어줘서 굉장히 편하다. 아마 정석대로 썼다면 아래 코드와 같았을 거다
fun Member.toDto(): MemberRes { // 반환 타입 명시 필수
return MemberRes(
this.id!!,
this.email,
this.password,
this.role
)
}
Post 엔티티
package simpleblog.domain.post
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.FetchType
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.ManyToOne
import jakarta.persistence.Table
import simpleblog.domain.AuditingEntity
import simpleblog.domain.member.Member
import simpleblog.domain.member.toDto
@Entity
@Table(name = "Post")
class Post(
title: String,
content: String,
member: Member
) : AuditingEntity() {
@Column(name = "title", nullable = false)
var title: String = title
protected set
@Column(name = "content", nullable = false)
var content: String = content
protected set
@ManyToOne(fetch = FetchType.LAZY, targetEntity = Member::class)
var member: Member = member
protected set
override fun toString(): String {
return "Post(title='$title', content='$content', member=$member)"
}
}
fun Post.toDto() =
PostRes(
this.id!!,
this.title,
this.content,
this.member.toDto()
)
음.. 딱히 새로 보이는 개념은 나오지 않는다. 관계를 설정하는 JPA 어노테이션 하나만 설명하고 넘어가겠다.
- @ManyToOne
- 다른 엔티티와의 관계를 나타내기 위해 사용한다. 이 어노테이션이 붙은 필드인 Member와 Post 엔티티는 1 : N 관계라는 것을 나타낸다
- fetch = FetchType.LAZY : 이 엔티티가 DB로부터 조회될때, 이 엔티티와 연관관계가 있는 이 필드, Member 엔티티가 같이 불러올것인지 설정 가능하다. LAZY로 설정해두면 바로 Member를 DB로부터 조회하는 것이 아니라, 처음에는 Member의 ID만 가지고 있는 프록시 객체를 가져왔다가 나중에 실제로 접근되었을 때 쿼리를 날려서 조회를 실행한 다음, 프록시 객체를 실제 객체로 교체한다. 이 전략은 LazyInitializationException 이 발생하기 쉬운데, 이 프록시 객체에 접근 할 때, 트랜잭션 내에서 접근하지 않았을 때 흔하게 발생하기에 주의를 요한다.
기본 전략은 EAGER 전략인데, Post 엔티티가 조회되는 즉시 LEFT OUTER JOIN을 사용해서 한번의 쿼리로 연관된 Member 엔티티까지 함께 가져온다. 하지만 성능상의 문제로 잘 사용하지 않고, 연관된 엔티티가 한번에 조회되어야 할 때는 JOIN FETCH를 자주 사용한다. - targetEntity = Member::class : 어느 엔티티와 연관되어있는지를 표시한다. 생략 가능
Comment
package simpleblog.domain.comment
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.FetchType
import jakarta.persistence.ManyToOne
import jakarta.persistence.Table
import simpleblog.domain.AuditingEntity
import simpleblog.domain.member.Member
import simpleblog.domain.post.Post
@Entity
@Table(name = "Comment")
class Comment(
title:String,
content: String,
post: Post
) : AuditingEntity() {
@Column(name = "title", nullable = false)
var content: String = content
protected set
@ManyToOne(fetch = FetchType.LAZY, targetEntity = Post::class)
var post: Post = post
protected set
}
여기서도 새로운 개념은 없다.
생성자
아 그리고 자바와 달리 코틀린에서는 클래스에 ()가 붙을 수 있는데, 이는 주 생성자를 나타낸다.
자바와 코틀린의 코드를 다시 비교해보자.
// val과 var을 붙이면 초기화까지 해줌 !! getter, setter도 자동으로
class Person(val name: String, var age: Int)
// 자바 코드
public class Person {
private final String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
정말 짧고 간편하다!!
물론 저 생성자에 초기화나 유효성 검사같은 로직을 추가할수는 없지만, init 블록을 통해 해결 가능하다.
class Person(val name: String, var age: Int) {
init {
// 주 생성자의 로직 (유효성 검사 등)
require(age > 0) { "나이는 0보다 커야 합니다." }
println("init 블록: ${this.name} 객체가 생성되었습니다.")
}
}
이렇게 사용하면 init 블록은 주 생성자의 일부로 실행된다.
'Spring > Kotlin' 카테고리의 다른 글
| 마이그레이션 계획 (0) | 2025.12.03 |
|---|---|
| 서비스, API 작성 (2) | 2025.10.01 |
| 리포지토리 만들기 (0) | 2025.09.13 |
| AuditingEntity 작성 (2) | 2025.08.31 |
| 코틀린 + 스프링 프로젝트 (1) | 2025.08.31 |