1. 배경
JPA를 사용하여 엔티티를 작성할 때 우리는 보통 정형화된 방식으로 작성합니다. 예를 들어 아래와 같은 형태가 일반적입니다.
@Entity
@Table(
name = "board",
indexes = [Index(name = "idx_fts", columnList = "title, content", unique = false)]
)
class Board(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null
@Column(nullable = false)
var title: String,
@Column(columnDefinition = "TEXT", nullable = false)
var content: String,
@Column(nullable = false)
var category: BoardCategory,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
var user: User,
@OneToMany(fetch = FetchType.LAZY, cascade = [CascadeType.ALL], orphanRemoval = true)
@JoinColumn(name = "board_id")
var comments: List<Comment> = emptyList()
) : BaseEntity() {
fun updateBoard(boardEditRequest: BoardEditRequest) {
this.title = boardEditRequest.title
this.content = boardEditRequest.content
this.category = boardEditRequest.category
}
}
@MappedSuperclass
abstract class BaseEntity {
@Column(updatable = false)
var createdAt: LocalDateTime = LocalDateTime.now()
var updatedAt: LocalDateTime? = null
@PreUpdate
fun setUpdatedAt() {
updatedAt = LocalDateTime.now()
}
}
일반적으로 엔티티 작성 시 고려하는 요소는 다음과 같습니다
- @Id를 통한 PK 설정
- 비즈니스 도메인에 필요한 필드(프로퍼티) 정의
- 연관 관계 설정 (@ManyToOne, @OneToMany 등)
- 생성일/수정일 필드 추가
하지만, 입력 값에 대한 유효성 검사 또는 변환 로직까지 고려하는 경우는 드뭅니다.
검증을 위한 별도 클래스를 만들 수도 있지만, 매번 해당 클래스를 호출하는 방식은 비효율적일 수 있습니다. 이에 따라 Kotlin의 기능을 활용해, 객체 생성 시점에서 유효성 검사를 강제하고, 무결성을 보장하는 패턴을 소개하고자 합니다.
물론 다음과 같은 엔티티 개선 사항은 이후에 따로 다룰 예정입니다.
- equals, hashCode 오버라이딩
- allOpen, no-args-constructor Gradle 설정
- PK 타입의 UUID 전환
- 프로퍼티 임의 변경 제한 (접근 제어자 등)
이번 글에서는 그중에서도 팩토리 메서드 패턴을 중심으로 어떻게 개선했는지 살펴보겠습니다.
2. 팩토리 메서드 패턴이란?
1) 개요
팩토리 메서드 패턴(Factory Method Pattern)은 객체 생성을 캡슐화하여, 객체를 생성하는 책임을 별도의 메서드 또는 클래스에 위임하는 디자인 패턴입니다. 이는 객체 생성 로직을 클라이언트 코드로부터 분리하여, 코드의 유연성과 유지보수성을 높이는 데 목적이 있습니다.
주요 특징으로는,
추상화된 생성: 객체 생성 로직을 직접 호출하지 않고, 팩토리 메서드를 통해 객체를 생성합니다.
확장성: 새로운 클래스를 추가하거나 생성 로직을 변경할 때 클라이언트 코드를 수정하지 않아도 됩니다.
캡슐화: 객체 생성 시 필요한 유효성 검사, 초기화 로직 등을 한 곳에서 관리할 수 있습니다.
팩토리 메서드 패턴은 주로 객체 생성이 복잡하거나, 특정 조건에 따라 다른 방식으로 객체를 생성해야 할 때 사용됩니다.
2) 배경 -> "객체 생성" 이라는 문제
일반적으로 객체를 생성할 때 우리는 생성자(constructor)를 사용합니다.
val user = User("이름이름", 28)
이렇게 직접 생성자를 호출하는 방식은 직관적이고 간단하지만, 몇몇 문제를 일으킬 수 있습니다.
- 객체 생성 시 복잡한 로직이 필요한 경우(ex. 값 검증, 외부 자원 초기화 등)
- 생성 시점에 유효성 검사가 필요한 경우
- 하위 클래스에 따라 다른 객체를 반환해야하는 경우
위와 같은 상황의 경우, 직접 생성자를 호출하는 방식은 한계를 가집니다.
3) 해결 -> 팩토리 메서드 패턴 등장
위와 같은 어려움을 해결하기 위해 팩토리 메서드 패턴이 등장합니다.
팩토리 메서드 패턴이란, 객체 생성을 캡슐화하여, 생성 방법을 서브클래스나 외부 메서드로 위임하는 패턴입니다.
이말은 즉, new나 생성자 직접 호출을 감추고, 별도 메서드를 통해 객체를 생성한다는 뜻입니다.
// 일반 생성자 호출
val user = User("이름이름", 28)
// 팩토리 메서드 패턴
val user = User.create("이름이름", 28)
4) 실제 코틀린에서는 어떻게 쓰일까?
코틀린에서는 주로 private constructor + companion object를 활용하여 쓰입니다.
위와 같이 작성할 경우 아래와 같은 이점이 있습니다.
- 객체 생성 시 항상 유효성 검사 수행. 팩토리 메서드 패턴 내 객체 생성 시 유효성 검사 로직을 포함시킬 수 있기 때문
- 캡슐화 : 외부에서는 오직 팩토리 메서드 패턴을 거쳐서만 객체 생성 가능. 다른 루트는 없음
3. 팩토리 메서드 패턴이 적용된 Board Entity
아래 코드를 통해 구체적으로 어떻게 적용되었는지 살펴보겠습니다.
@Entity
@Table(
name = "board",
indexes = [Index(name = "idx_fts", columnList = "title, content", unique = false)]
)
class Board private constructor(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
@Column(nullable = false)
var title: String,
@Column(columnDefinition = "TEXT", nullable = false)
var content: String,
@Column(nullable = false)
var category: BoardCategory,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
var user: User,
@OneToMany(fetch = FetchType.LAZY, cascade = [CascadeType.ALL], orphanRemoval = true)
@JoinColumn(name = "board_id")
var comments: List<Comment> = emptyList()
) : BaseEntity() {
fun updateBoard(boardEditRequest: BoardEditRequest) {
require(boardEditRequest.title.isNotBlank()) { "제목은 빈 값일 수 없습니다." }
require(boardEditRequest.content.isNotBlank()) { "내용은 빈 값일 수 없습니다." }
this.title = boardEditRequest.title
this.content = boardEditRequest.content
this.category = boardEditRequest.category
}
companion object{
@JsonCreator
fun from(
title: String,
content: String,
category: BoardCategory,
user: User,
comments: List<Comment>
) : Board {
check (title.isNotBlank()) {throw IllegalStateException("제목은 빈 값일 수 없습니다.") }
check (content.isNotBlank()) {throw IllegalStateException("내용은 빈 값일 수 없습니다.")}
return Board(title = title, content = content, category = category, user = user, comments = comments)
}
operator fun invoke(
title: String,
content: String,
category: BoardCategory,
user: User,
comments: List<Comment>
) : Board = from(title, content, category, user,comments)
}
}
1) 생성자 접근 제한
class Board private constructor(...) { ... }
2) companion object + @JsonCreator
companion object{
@JsonCreator
fun from(
title: String,
content: String,
category: BoardCategory,
user: User,
comments: List<Comment>
) : Board {
check (title.isNotBlank()) {throw IllegalStateException("제목은 빈 값일 수 없습니다.") }
check (content.isNotBlank()) {throw IllegalStateException("내용은 빈 값일 수 없습니다.")}
return Board(title = title, content = content, category = category, user = user, comments = comments)
}
}
from 메서드는 Board 객체를 생성하는 팩토리 메서드의 핵심입니다.
이 메서드는 필요한 매개변수(title, content, category, user, comments)를 받아 Board 객체를 생성하며, 내부적으로 생성자 호출을 처리합니다. 또한 Jackson의 @JsonCreator 어노테이션을 사용하여 JSON 역직렬화 시 이 메서드를 호출하도록 설정했습니다.
3) invoke 연산자 오버로딩
operator fun invoke(
title: String,
content: String,
category: BoardCategory,
user: User,
comments: List<Comment>
): Board = from(title, content, category, user, comments)
Kotlin의 invoke 연산자를 사용하여 Board(...)처럼 자연스러운 호출 방식으로 객체를 생성할 수 있게 했습니다.
내부적으로 from 메서드를 호출하므로, 생성 로직은 여전히 from 메서드에 캡슐화되어 있습니다.
4) 객체 생성 시 유효성 검사 강화
companion object{
@JsonCreator
fun from( ... ) : Board {
check (title.isNotBlank()) {throw IllegalStateException("제목은 빈 값일 수 없습니다.") }
check (content.isNotBlank()) {throw IllegalStateException("내용은 빈 값일 수 없습니다.")}
return Board(title = title, content = content, category = category, user = user, comments = comments)
}
객체 생성 시 from 메서드에서 check를 사용하여 필드 값의 유효성을 검사합니다. 이를 통해 런타임 무결성을 강제합니다.
4. 효과
팩토리 메서드 패턴의 적용은 다음과 같은 효과를 가져옵니다
- 객체 생성 제어 및 중앙화
- 객체 생성 로직이 companion object의 from과 invoke 메서드에 집중되어, 생성 로직을 한 곳에서 관리할 수 있습니다.
- private constructor로 인해 외부에서 임의로 객체를 생성하지 못하므로, 생성 과정에서 의도하지 않은 상태
(예: 유효하지 않은 title 또는 content)를 방지합니다.
- 유효성 검사 강제화
- 팩토리 메서드 패턴 내부에서 check를 통해 title과 content의 비어 있음 여부를 확인하므로, 모든 Board 객체는 유효한 상태로 생성됩니다.
- 이는 데이터 무결성을 보장하며, 런타임 오류를 줄이는 데 기여합니다.
- 유연성과 확장성
- from 메서드에 추가적인 초기화 로직이나 유효성 검사를 쉽게 추가할 수 있습니다. 예를 들어, category의 특정 값에 따라 다른 초기화 로직을 적용하거나, comments에 대한 추가 검증을 삽입할 수 있습니다.
- 새로운 생성 방식을 추가하거나 기존 로직을 수정할 때 클라이언트 코드를 변경하지 않아도 됩니다.
- 가독성과 Kotlin 관용 스타일 활용
- invoke 연산자를 통해 Board(...)처럼 자연스러운 호출 방식을 제공하여, 코드 가독성을 높였습니다.
- JSON 역직렬화 일관성 확보
- @JsonCreator를 사용해 JSON 데이터를 Board 객체로 변환할 때 from 메서드를 호출하도록 설정했습니다. 이는 REST API나 JSON 기반 데이터 처리 시 객체 생성의 일관성을 보장합니다.
5. 마무리
이번 글에서는 JPA 엔티티 작성 시, 단순히 필드를 나열하는 것을 넘어 생성 시점에서의 유효성 검사와 무결성 확보를 위한 패턴으로 팩토리 메서드 패턴을 어떻게 활용했는지 소개했습니다.
이 패턴은 복잡한 도메인 객체를 다룰 때 특히 유용하며, 다음 글에서는 equals/hashCode, UUID 도입, 접근 제어 등의 고급 설계 주제를 더 깊이 다뤄보겠습니다.
참고 : https://tech.kakaopay.com/post/katfun-joy-kotlin/#user-content-fn-1
'Project > CANCER-FINE_암환자를 위한 정보 제공 사이트' 카테고리의 다른 글
[CANCER-FINE] 프로젝트 init (0) | 2025.04.29 |
---|