오늘의 주제는 ORM의 "객체 매핑"을 커스텀 타입으로 내맘대로 주무를 수 있는 @Converter 와 해당 사항을 지시하는 플래그 @Convert 입니다.
다만... 오늘은 사정상 기차 안에서 작업을 하고 있어서 실습은 어려울 것 같습니다. DB 설정이나 이런 게 원체 번거로워서 노트북에 하기도 좀 그렇구요, 이번 글에서는 IDE에서 실제로 돌아가는지는 확인하지 않은 손코드들을 보여드릴 예정이니 이 점은 감안 부탁드립니다. 저는 경고했습니다.
Converter란?
: 엔티티의 필드 <-> "DB의 칼럼" 변환을 커스터마이징
ORM 은 프로그래밍 언어상의 자료구조를 DB로, DB에 있는 데이터를 프로그래밍 언어상의 자료구조로 상호변환해주는 역할을 합니다. JPA는 그 표준이구요. 상호변환하는 방법이 있다면, 그걸 커스터마이징할 수 있는 방법도 있지 않을까요? 네, 있습니다. 그게 Converter 입니다.
구체적으로, 이번 강의에서는 엔티티의 필드에 커스텀 타입을 지정해서, 만약 DB 에 number 가 딸랑 하나만 저장되어 있더라도 JVM 쪽에서 JPA 를 경유하여 매핑/로드된 객체에는 "BookStatus" 라는 객체로 매핑되게 만드는 것을 실습합니다.
생략
@Entity
public class Book {
생략
@Convert
BookStatus status;
// ^^^^^^^^^^
// 지금까지 이 위치에는 primary type, 혹은 boxed primary type 들만 사용해왔습니다.
// DB 에는 단일 number 하나로 저장되어 있다고 하더라도 JVM 코드에서는 커스텀 클래스를 쓰겠다는 거죠.
// 물론 저장될 때에는 number로 변환될 것입니다.
}
물론 컨버터가 객체의 필드에만 적용할 수 있는 것은 아닌 듯 합니다. 강의에서 가장 먼저 들어주셨던 예시가 이전에 nativeQuery 로 만들었던 findAll 이었거든요. 이것도 Hibernate 에 있는 컨버터를 기본으로 사용한 것이라고 합니다. 커스터마이징이 있다는 건 기본값이 있다는 말이겠죠.
interface BookRepo: JpaRepository던가...<Book, Long 이던가..> {
@Query(value = "select * from book", nativeQuery = true)
fun findAll(): MutableMap<String, Any>
}
내장 구현에 대해서 강의 때 좀 더 보여주셨는데요...
강의에서는 enum 의 변환을 얘기하시면서 Hibernate 의 내장 구현인 OrdinalEnumConverter, NamedEnumConverter 를 보여주셨고 이들은 EnumValueConverter를 구현하고, 이들의 부모에는 BasicValueConverter 가 있는 모양이더라구요. 이 BasicValueConverter 에서는 Jakarta Persistence (aka JPA) 의 AttributeConverter를 언급합니다. (강사님 말씀: "지원한다")
뭐 아무렴 어떻습니까. 지금으로서는 그냥 기본 구현체가 있고, 우리가 보통 쓰던 쿼리의 반환형 같은 건 기본 구현체를 거친다 정도만 알면 되겠습니다.
Converter 가 필요한 상황들
기본적으로도 쓰고 있지만, 커스텀 컨버터가 필요한 상황이 올 수도 있습니다.
- 레거시 시스템에 대응해야 하는 경우
- 우리 코드는 enum을 String 으로 저장하는 정책으로 개발했지만, 외부 코드는 int 인 "레거시 코드"일 때
- 외부 시스템의 값이 너무 낮은 수준 (low level) 일 때, 커스텀 타입으로 좀 더 추상화해서 다루고자 할 때
(low level: 어떤 언어의 개발자들은 이 표현을 욕하는 건 줄 안다는... 우스꽝스럽지만 차마 웃을 수 없는 얘기가 있습니다.)- 위에서 status = 200 같은 걸 BookStatus 라는 전용 클래스로 매핑하는 걸 (코드로) 살짝 맛만 봤었죠.
이번에 직접 해볼 케이스가 이 경우입니다.
- 위에서 status = 200 같은 걸 BookStatus 라는 전용 클래스로 매핑하는 걸 (코드로) 살짝 맛만 봤었죠.
Converter 개요 / 문서보기
들어가기에 앞서 Jakarta Persistence (aka JPA) 쪽의 문서들을 먼저 리스트업해야 할 것 같습니다. 하는 김에 기능도 좀 설명하구요.
- @Convert: 해당 필드를 DB와 매핑할 때 어떻게 상호변환할건지를 정합니다.
- 엔티티의 필드나 리포지터리의 쿼리 메소드(아마도)에 장식할 수 있습니다. 변환을 할 수 있는 곳에 넣는다는 느낌이네요.
- 문서에 굉장히 예시가 많습니다. 꼭 확인해봅시다.
- 커스텀 컨버터를 사용하고 있고, 해당 컨버터의 @Converter(autoConvert=true) 가 따로 지정되어 있지 않다면 필드에 꼭 붙여줘야 합니다. 아니면 오류가 납니다.
- @Converter: 커스텀 컨버터 로직 클래스에 붙여서, 그 클래스가 커스텀 컨버터임을 표시합니다.
- autoConvert=true 로 설정하면 굳이 필드에 @Convert 를 붙이지 않아도 됩니다... 만, 이건 어디서든 적용되므로 primary type 이나 primary type 을 box 한 것에는 사용하지 않도록 주의해야 합니다. 내가 원치도 않았는데 Long 이 이상한 처리가 되어서 DB에 저장되었다? 삐슝빠슝? 한 사태를 겪지 않으려면요.
- AttributeConverter<어트리뷰트_타입, DB_칼럼_타입>: 커스텀 컨버터는 이 인터페이스를 구현합니다.
- 이름이 어트리뷰트라는 점에서 알아챘어야 하는데, 엔티티의 필드를 어트리뷰트라고 부르는 모양입니다.
- JVM 객체 -> DB, DB -> JVM 객체로의 매핑 두 가지를 구현해야 합니다.
- 반드시 둘 다 구현하시기 바랍니다. 둘 중 하나 구현했다가 JPA가 의도치 않게 update() 를 치는, 그것도 null로 업데이트를 하는 대형사고에 대한 사례가 강의에서 언급되었었습니다.
원래는 이 이후로 실제 사용 사례... 강의에서 진행했던 내용이랑 마저 정리하려고 했는데, 아무래도 본가에 오니 상황이 녹록치 않네요. 밤이기도 하고 내일 강의를 정리하면서 같이 정리해야겠습니다 orz
본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.
패스트캠퍼스: https://bit.ly/37BpXiC
#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #한번에끝내는JavaSpring웹개발마스터초격차패키지Online
중간에 한 번 날려먹기도 했습니다... 인트로를 쓰는 중이었는데,,, 잘 안쓰는 맥북 상황이라... 여담이지만 개발자는 macOS 보다 Linux 입니다! macOS 빠들에게 속지 마세요! (합리적이긴 한데, 노트북이 아니라면 굳이 macOS를 쓸 필요가 없습니다. 앱을 만들지 않는 이상... 오히려 리눅스보다 불편하다구요.)
아무래도 맘이 편치 않아서 마저 하기로.
Converter 직접 만들어보기
위에서 커스텀 컨버터를 만드는데 필요한 구성요소를 쭉 살펴봤으니 이제 남은 일은 직접 만들어보는 것 뿐입니다.
백강이일여불코드 아니겠습니까.
커스텀 Converter 가 필요한 상황 살펴보기
먼저, 강의에서 예시로 들었던 커스텀 컨버터를 만들면 딱 좋겠다 싶은 상황을 살펴봅시다.
이전 강의에서도 쭉 활약해주셨던 엔티티 Book 씨를 모셔왔습니다.
@Entity
class Book {
생략
var status: int?
}
어째서인지 모르겠지만, Book 을 목록에 보여줄 수 있는지 아닌지를 http 의 상태코드같이 생긴 int 값으로 관리하고 있습니다. 이 값은 DB에 숫자로 들어가고, JVM 쪽 코드에도 마찬가지로 숫자로 다뤄질 것입니다. 그렇다면 개발자가 어떤 숫자가 가능한지 어떻게 알 수 있을까요?
그래서 DB에는 이처럼 int로 저장되어있는 값이지만, 아래의 전용 클래스로 타입을 매핑하기로 했습니다. 이러면 좀 더 관리하기 편해지겠죠?
data class BookStatus(
var statusCode: int?
) {
fun isDisplayable() = statusCode == 200
val description: String?
get() = when(statusCode) {
200 -> "표시 가능한 상품입니다."
410 -> "삭제된 상품입니다."
403 -> "조회할 권한이 없는 상품입니다."
else -> "상태에 문제가 있습니다."
}
}
커스텀 컨버터 만들기
이제 뭘 어떻게 변환해야 할지도 알겠으니 변환기 클래스를 만들어봅시다.
위에서 설명했던 대로 AttributeConverter 를 상속하셔서 메소드를 두 개 구현하시면 됩니다.
주의할 점은, DB에 근접해있는 코드인 만큼 null 체크를 꼭 해서 오류가 나는 일을 방지해주는 것 정도겠네요.
// 미리 설정했습니다. 이게 없으면 필드에 @Convert 도 지정해야 합니다.
// vvvvvvvvvvvvvv
@Converter(autoApply=true) // int가 아니라 boxed 인 Integer 필요
public class BoookStatusConverter // vvvvvvv
implements AttributeConverter<BookStatus, Integer> {
override가_들어가던가?
Integer convertToDatabaseColumn(BookStatus attribute) {
return attribute == null ? null : attribute.getStatusCode();
}
BookStatus convertToEntityAttribute(Integer dbData) {
return dbData == null ? null : new BookStatus(dbData);
}
}
미리 autoApply 도 적용했겠다, 필드에 적기만 하면 됩니다.
@Entity
class Book {
생략
var status: BookStatus?
}
Converter가 예상외로 동작하는 사례
강의에서 실제 사례 중 위험했던 사례가 나왔습니다.
커스텀 Converter 를 구현하는데, 레거시 DB에서 값만 읽어오면 되는 상황이었답니다. 그래서 귀찮기도 하고 DB에서 값을 읽어와서 객체에 매핑하는 부분만 구현을 했더랍니다. 그런데, 이것과 @Transactional, findAll 세 가지가 만나니, 트랜젝션이 끝나는 시점에 DB값과 비교하기 위해 DB의 값으로 바꿨는데, 대충 만든 값이든 예외값이든 만들어졌겠죠? 당연히 대충 때운 코드이니 DB와는 다르더랍니다. 그래서 Persistence Context가 옳타꾸나 이거 업뎃됐네 하고 지멋대로 update 쿼리를 날렸다고.
강의에서는 예시 코드로 직접 확인까지 했는데 여기선 그렇게까진 안 하겠습니다. 커스텀 컨버터를 구현해놨다면 @Transactional 이 있는 테스트 메소드에서 findAll() 만 호출해보면 쿼리에서 update 를 금방 확인할 수 있거든요.
본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.
패스트캠퍼스: https://bit.ly/37BpXiC
#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #한번에끝내는JavaSpring웹개발마스터초격차패키지Online
패캠마크 한번 더
23:46
'FastCampus - 한번에 끝내는 Java|Spring 웹 개발 > 04 JPA' 카테고리의 다른 글
JPA Ch 11 "N+1 문제" - 패스트캠퍼스 챌린지 43일차 (0) | 2022.03.07 |
---|---|
JPA Ch 10 @Embeddable - 패스트캠퍼스 챌린지 41일차 (0) | 2022.03.05 |
JPA Ch 9 @Query (3), NativeQuery - 패스트캠퍼스 챌린지 39일차 (0) | 2022.03.03 |
JPA Ch 9 @Query (1, 2) - 패스트캠퍼스 챌린지 38일차 (0) | 2022.03.02 |
JPA Ch 8 remove cascade / soft delete - 패스트캠퍼스 챌린지 37일차 (0) | 2022.03.01 |