분류 전체보기
-
[Kotlin] 08. 클래스 계층 이해하기2022.11.01
-
[Spring] Custom Mapper with MapStruct2022.11.01
-
GIT merge2022.06.21
-
GIT rebase2022.06.21
[MongoDB] Spring MongoTemplate Search for ISODate
MongoTemplate Search for ISODate
Spring MongoTemplate을 사용하여 MongoDB에서 ISODate를 가져오는 방법에 대하여 소개합니다.
일반적으로 MongoDB와 통신하기 위한 Query 작성시 Spring Data MongoDB Criteria를 사용하고 있습니다.
MongoDB의 Date 필드를 검색하기 위한 방법은 다음과 같습니다.
MongoDB test Collection
{
"_id": {
"$numberLong": "20588"
},
"title": "json document date type",
"real_yn": "Y",
"use_yn" : "Y",
"view_date": {
"$date": {
"$numberLong": "1668674055000" //2022-11-17T08:34:15.000+00:00
}
}
Mongo shell에서는 일반적으로 ISODate 형식으로 감싸주면 원하는 결과를 얻을 수 있습니다.
db.test.find{"real_yn":"Y", "view_date":{"$lte":ISODate("2022-11-17T08:34:15.000+00:00")}}
이제 MongoTemplate에서 동일한 결과를 얻기 위해 Query를 작성해 보겠습니다.
MongoTemplate#find에서는 아래와 같이 Criteria를 생성하고, Java.util.Date or Java.time.LocalDateTime을 사용하면 $date field에 날짜를 기준으로 결과를 확인 할 수 있습니다.
Criteria criteria = new Criteria();
LocalDateTime today = LocalDateTime.parse(LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME));
criteria.and("real_yn").is("Y").and("view_date").lte(today); // lte(new Date())
mongoTemplate.find(Query.query(criteria), test.class);
하지만 Java Console에 출력된 Query를 Mongo shell에서 실행하면 원하는 결과를 얻을 수 없으니 이점을 유의해서 개발하시면 됩니다.
2022-11-17 22:33:52.907 DEBUG 17749 [8090-exec-1] o.s.d.m.c.MongoTemplate :find using
query: { "real_yn": "Y", "view_date" : { "$lte": { "$date" : "2022-11-17T13:33:52.906Z"}}}
fields: {} for class: class domain.test.Test in collection: Test
MongoTemplate에서 $date field를 Query 하는 방법 및 그 차이점에 대하여 알아봤습니다.
'Database & NoSQL > NoSQL' 카테고리의 다른 글
[Kotlin] 08. 클래스 계층 이해하기
클래스 상속 개념을 소개하고 하위 클래스를 정의하는 방법을 설명하며, 추상 클래스, 인터페이스, 클래스 위임, 봉인된 클래스를 통해 클래스 계층 구조를 알아보자.
- 상속과 오버라이딩
- 타입 검사와 캐스트
- 추상 클래스
- 인터페이스
- 봉인된 클래스
- 위임
8.1 상속
자바와 마찬가지로 코틀린 클래스는 단일 상속만을 지원한다.
8.1.1 하위 클래스 선언
클래스를 정의하면서 주생성자 뒤에 콜론(
:
)으로 표기한다.
- 자바의 클래스와 메서드는 default로 상속에 열려있고, 상속을 금지하려면 final을 명시
- 코틀린은 default로 final 이고, 상속을 허용하려면 클래스 앞에 open 지시어 선언
open class Vehicle {
var currentSpeed = 0
fun start() {
println("I'm moving")
}
fun stop() {
println("Stopped")
}
}
open class FlyingVehicle : Vehicle() { // Vehicle는 open 지시어를 통해 상속에 열려 있음
fun takeOff() {
println("Take off")
}
fun land() {
println("Landed")
}
}
class Aircraft(val seats: Int) : FlyingVehicle()
// error: this type is final, so it cannot be inherited from
class Airbus(seats: Int) : Aircraft(seats) // Aircraft는 default final 이기에 상속에 대해 닫혀 있음
하위 클래스 인스턴스는 상위 클래스의 멤버를 모두 상속한다.
val aircraft = Aircraft(100)
val vehicle: Vehicle = aircraft
vehicle.start()
vehicle.stop()
aircraft.start()
aircraft.takeOff()
aircraft.land()
aircraft.stop()
println(aircraft.seats)
data 클래스는 항상 final 이기에 open 선언이 불가하다.
// error: modifier 'open' is incompatible with 'data'
open data class Person(val name: String, val age: Int)
인라인 클래스는 상속 및 상위 클래스 역할 불가하다.
class MyBase
// error: inline classes can be only final
open value class MyString(val value: String)
// error: inline class cannot extend classes
value class MyStringInherited(val value: String): MyBase()
객체(동반 객체 포함)는 클래스 상속이 가능하다.
open class Person(val name: String, val age: Int) {
companion object : Person("Unknown", 0)
}
object JohnDoe : Person("John Doe", 30)
상위 클래스의 open으로 지정된 멤버를 하위 클래스에서 오버라이드 가능하고,
자바와 차이점은 구현하는 메서드 앞에 override 지시어를 붙여야 함
open class Vehicle2 {
open fun start() {
println("I’m moving")
}
fun stop() { // final 이기에 상속만 가능하고 하위 메서드에서 오버라이드 불가
println("Stopped")
}
}
class Car : Vehicle2() {
override fun start() {
println("I'm riding")
}
}
class Boat : Vehicle2() {
override fun start() {
println("I'm sailing")
}
}
오버라이드하는 멤버를 final로 선언하면 더 이상 하위 클래스에서 오버라이드 불가하다.
open class Vehicle6 {
open fun start() {
println("I’m moving")
}
}
open class Car5 : Vehicle6() {
final override fun start() {
println("I'm riding a car")
}
}
// Car5 클래스에서 start 멤버 함수를 final로 선언했기 때문에 Car5클래스를 오버라이드할 수 없음
class Bus : Car5() {
// error: 'start' in 'Car' is final and cannot be overridden
override fun start() {
println("I'm riding a bus")
}
}
프로퍼티 오버라이드는 본문 구현 외에 주생성자 파라미터 선언으로 오버라이드 가능하다.
open class Entity {
open val name: String get() = ""
}
class Person2(override val name: String) : Entity()
불변 프로퍼티를 가변 프로퍼티로 오바라이드할 수 있다.
(단, 꼭 필요한 경우가 아니라면 상위 클래스의 선언한 값을 그대로 사용)
open class Entity2 {
open val name: String get() = ""
}
class Person3() : Entity2() {
override var name: String = "" // val -> var
}
자바와 마찬가지로 코틀린도 멤버의 영역을 하위 클래스의 영역으로만 제한하는 특별한 접근 변경자(protected)를 제공한다.
- Java: 동일한 패키지 안에서 속한 코드에서 멤버 접근을 허용
- Kotlin: 동일 클래스 및 그 클래스를 상속한 하위 클래스에서만 멤버 접근이 허용
open class Vehicle7 {
protected open fun onStart() { }
fun start() {
println("Starting up...")
onStart()
}
}
class Car6 : Vehicle7() {
override fun onStart() {
println("It's a car")
}
}
fun main3() {
val car = Car6()
car.start() // Ok
// error: cannot access 'onStart': it is protected in 'Car'
// car.onStart() 상속한 하위 클래스에서만 접근 가능
}
함수나 프로퍼티를 오버라이드한 상위 클래스를 재사용해야 하는 경우 super 키워드를 사용한다.
open class Vehicle {
open fun start(): String? = "I'm moving"
}
open class Car : Vehicle() {
override fun start() = super.start() + " in a car"
}
fun main() {
println(Car().start())
}
// I'm moving in a car
8.1.2 하위 클래스 초기화
특정 클래스의 인스턴스 생성 시 자신의 상위 클래스 생성자를 호출하며 최상위 클래스에 이를 때까지 연쇄적으로 일어나고, 최상위클래스로 부터 하위 클래스 순서로 초기화가 진행된다.
open class Vehicle {
init {
println("Initializing Vehicle")
}
}
open class Car : Vehicle() {
init {
println("Initializing Car")
}
}
class Truck : Car() {
init {
println("Initializing Truck")
}
}
fun main() {
Truck()
}
// result
Initializing Vehicle
Initializing Car
Initializing Truck
위임 호출(delegating call)을 사용하면 인자를 상위 클래스의 일반생성자 및 주,부생성자에게 전달할 수 있다.
// 일반생성자에 전달
open class Person(val name: String, val age: Int)
class Student(name: String, age: Int, val university: String) :
Person(name, age)
fun main() {
Student("Euan Reynolds", 25, "MIT")
}
// 주, 부생성자에 전달
open class Person {
val name: String
val age: Int
constructor(name: String, age: Int) {
this.name = name
this.age = age
}
}
class Student(name: String, age: Int, val university: String) :
Person(name, age)
Student 클래스에서 부생성자를 사용할 경우 위임 호출할 클래스를 생성자 뒤에
( )
괄호없이 작성한다.
open class Person(val name: String, val age: Int)
class Student : Person {
val university: String
constructor(name: String, age: Int, university: String) :
super(name, age) { // super 키워드는 부생성자가 상위 클래스의 생성자를 위임 호출하는것을 컴파일러에게 전달
this.university = university
}
}
상위 클래스의 초기화 코드가 호출되고 하위 클래스에서 오버라이드한 함수가 아직 초기화되지 않은 상태일 경우 this 누출(leaking this) 문제가 발생할 수 있다.
open class Person10(val name: String, val age: Int) {
open fun showInfo() {
println("$name, $age")
}
init {
showInfo() // 잠재적인 위험 요소
}
}
class Student7(
name: String,
age: Int,
val university: String
) : Person10(name, age) {
override fun showInfo() {
println("$name, $age (student at $university)")
}
}
fun main() {
// Euan Reynolds, 25 (student at null)
Student7("Euan Reynolds", 25, "MIT")
}
8.1.3 타입 검사와 캐스팅
코틀린의 타입 검사와 캐스팅 연산에는 is를 사용한다.
val objects = arrayOf("1", 2, "3", 4)
// objects 는 Any 로 이뤄진 배열에 원소에 String or Int 연산을 바로 할 수 없음
for (obj in objects) {
println(obj*2) // error: unresolved reference
}
for (obj in objects) {
println(obj is Int) // is 연산자를 통해 타입 검사가 가능하다
}
// result
false
true
false
true
null값에 대한 is 연산 처리
- Java instanceof 와 유사하고 차이점은 null 에 대해 항상 false
- 연산자 오른쪽에 널이 될 수 있는지 여부에 따라 다르다(스마트 캐스트 지원)
//null이 될 수 없는 타입
println(null is Int) // false
//null이 될 수 있는 타입
println(null is String?) // true
!is 연산자는 is와 반대인 연산을 제공한다.
val o: Any = ""
println(o !is Int) // true
println(o !is String) // false
값의 타입을 자동으로 세분화해서 Null이 될 수 없는 타입으로 바꿔준다.
val objects2 = arrayOf("1", 2, "3", 4)
var sum = 0
for (obj in objects2) {
if (obj is Int) {
sum += obj // 여기서는 obj의 타입을 `Int`로 세분화한다
}
}
println(sum) // 6
is/!is 검사를 in/!in처럼 특별한 조건으로 사용할 수 있는 식 내부에서도 스마트 캐스트가 지원된다.
- 컴파일러는 검사 시점과 사용 시점 사이에 변수가 변경되지 않는다고 확신할 수 있을 때만 스마트 캐스트를 허용한다.
val objects3 = arrayOf("1", 2, "3", 4)
var sum2 = 0
for (obj in objects3) {
when (obj) {
is Int -> sum2 += obj // 여기서 obj는 Int 타입이다
is String -> sum2 += obj.toInt() // 여기서 obj는 String 타입이다
}
}
println(sum2) // 10
- 프로퍼티나 커스텀 게터가 정의된 변수에는 스마트 캐스트 사용이 불가하다. (값이 변경될 수 있다고 예측될 때)
class Holder {
val o: Any get() = "" // 게터 통해서 값이 변경될 수 있음
}
fun main9() {
val o: Any by lazy { 123 }
if (o is Int) {
println(o*2) // error: smart cast to 'Int' is impossible
}
val holder = Holder()
if (holder.o is String) {
println(holder.o.length) // error: smart cast to 'String' is impossible
}
}
- 가변 지역 변수의 경우 런타임 시점에 변수가 변경될지 예측할 수 없을 경우 스마트 캐스트 지원이 불가하다.
fun main() {
var o: Any = 123 // 위임이 없는 불변 지역변수는 가능
if (o is Int) {
println(o + 1) // Ok: Int로 스마트 캐스트
o = ""
println(o.length) // Ok: String으로 스마트 캐스트
}
if (o is String) {
val f = { o = 123 } // 람다 안에서 변수를 변경할 경우 지원 불가
// println(o.length) // error: smart cast to 'String' is impossible
}
}
- 스마트 캐스트를 쓸 수 없는 경우 (as와 as?)명시적인 연산자를 사용해 값의 타입을 강제로 변환할 수 있다.
val o: Any = 123
println((o as Int) + 1) // 124
println((o as? Int)!! + 1) // 124
println((o as? String ?: "").length) // 0 변환하려는 타입과 일치하지 않치만 안전한 as?
//println((o as String).length) // java.lang.ClassCastException 실제 타입과 일치하지 않아 예외
8.1.4 공통 메서드
Kotlin.Any 클래스는 코틀린 클래스 계층 구조의 루트로 모든 클래스는 직간적접으로 상속하고 기본 연산자를 제공한다.
open class Any {
public open operator fun equals(other: Any?): Boolean // 구조적 동등성 (== 나 !=)
public open fun hashCode(): Int // HashSet, HashMap
public open fun toString(): String // String 변환
}
디폴트로 정의된 모든 클래스에는 Any에서 상속받은 참조 동등성만 구현하기에 두 인스턴스가 같은 객체로 간주되지 않는다.
fun main() {
val addresses = arrayOf(
Address("London", "Ivy Lane", "8A"),
Address("New York", "Kingsway West", "11/B"),
Address("Sydney", "North Road", "129")
)
// -1
println(addresses.indexOf(Address("Sydney", "North Road", "129")))
}
equal() 메서드를 오버라이드하여 값 비교 (동등성) 문제를 해결할 수 있다.
class Address(
val city: String,
val street: String,
val house: String
) {
// Address에 정의한 equals 함수
override fun equals(other: Any?): Boolean {
if (other !is Address) return false
return city == other.city &&
street == other.street &&
house == other.house
}
}
fun main() {
// 2
println(addresses.indexOf(Address("Sydney", "North Road", "129")))
}
일반 값에 대한 동등성 비교는 ==와 !=, 참조 동등성은 === 사용하여 비교한다.
- 코틀린에서 ==는 내부적으로 equals 호출 (=자바 equals)
- 자바 주소 값 비교인 == 와 코틀린의 === 동일한 역할
val addr1 = Address2("London", "Ivy Lane", "8A")
val addr2 = addr1 // 같은 인스턴스
val addr3 = Address2("London", "Ivy Lane", "8A") // 다른 인스턴스지만, 동등함
println(addr1 === addr2) // true
println(addr1 == addr2) // true
println(addr1 === addr3) // false
프로퍼티는 각자의 equals()와 hashCode() 오버라이드하여 해시와 동등성을 계산 할 수 있다.(배열은 타입은 예외)
class Address(
val city: String,
val street: String,
val house: String
) {
// Address에 정의한 equals 함수
override fun equals(other: Any?): Boolean {
if (other !is Address2) return false
return city == other.city &&
street == other.street &&
house == other.house
}
// 위 equals와 호환이 되는 해시코드 정의
override fun hashCode(): Int {
var result = city.hashCode()
result = 31 * result + street.hashCode()
result = 31 * result + house.hashCode()
return result
}
}
fun main() {
val addr1 = Address("London", "Ivy Lane", "8A")
val addr3 = Address("London", "Ivy Lane", "8A")
// true
println(addr1 == addr3)
}
8.2 추상 클래스와 인터페이스
8.2.1 추상 클래스와 추상 멤버
자바와 동일하게 추상 클래스를 지원하고, abstract라는 키워드를 붙여야 한다.
abstract class Entity(val name: String)
// Ok: 하위 클래스에서 위임 호출
class Person(name: String, val age: Int) : Entity(name)
// 인스턴스 생성 불가하고 부모 클래스로만 사용
// error: cannot create an instance of an abstract class
val entity = Entity("Unknown")
추상 클래스는 추상 멤버를 정의할 수 있다.
import kotlin.math.PI
abstract class Shape {
abstract val width: Double
abstract val height: Double
abstract fun area(): Double
}
class Circle(val radius: Double) : Shape() {
val diameter get() = 2*radius
override val width get() = diameter // 추상 클래스 멤버를 오버라이드해서 구현
override val height get() = diameter
override fun area() = PI*radius*radius
}
class Rectangle(
override val width: Double,
override val height: Double
) : Shape() {
override fun area() = width*height
}
fun Shape.print() {
println("Bounds: $width*$height, area: ${area()}")
}
fun main() {
// Bounds: 20.0*20.0, area: 314.1592653589793
Circle(10.0).print()
// Bounds: 3.0*5.0, area: 15.0
Rectangle(3.0, 5.0).print()
}
추상 클래스 제약사항은 다음과 같다.
- 추상 프로퍼티를 초기화할 수 없고 명시적인 접근자나 by 절을 추가할 수 없다.
- 추상 함수에는 본문이 없어야 한다.
- 추상 프로퍼티와 함수 모두 명시적으로 반환 타입을 적어야 한다.
- 추상 멤버는 암시적으로 열려 있어 open 생략 가능하다.
8.2.2 인터페이스
코틀린 인터페이스 개념은 자바의 인터페이스와 비슷하고,자바 8에 디폴트 메서드가 도입된 이후로 더 많이 비슷해 졌다.
- default 키워드 없이 메서드 구현
- 프로퍼티, 함수 구현 추가 가능
interface Vehicle9 {
val currentSpeed: Int // 디펄트가 추상 멤버이고, abstract 지시어는 생략 가능
fun move()
fun stop()
}
인터페이스는 클래스나 다른 인터페이스의 상위 타입이 될 수 있다.
- 코틀린에서는 모든 상속(클래스, 인터페이스)을 똑같은 기호 ( : )를 사용해 표시한다.
interface FlyingVehicle2 : Vehicle9 {
val currentHeight: Int
fun takeOff()
fun land()
}
class Car8 : Vehicle9 {
override var currentSpeed = 0 // Vehicle9에 추상 멤버에 대한 구현을 제공해야 한다
private set
override fun move() {
println("Riding...")
currentSpeed = 50
}
override fun stop() {
println("Stopped")
currentSpeed = 0
}
}
class Aircraft2 : FlyingVehicle2 {
override var currentSpeed = 0 // FlyingVehicle2, Vehicle9에 추상 멤버에 대한 구현을 제공해야 한다
private set
override var currentHeight = 0
private set
override fun move() {
println("Taxiing...")
currentSpeed = 50
}
override fun stop() {
println("Stopped")
currentSpeed = 0
}
override fun takeOff() {
println("Taking off...")
currentSpeed = 500
currentHeight = 5000
}
override fun land() {
println("Landed")
currentSpeed = 50
currentHeight = 0
}
}
인터페이스 안의 함수와 프로퍼티에 구현을 추가할 수도 있다.
interface Vehicle10 {
val currentSpeed: Int
val isMoving get() = currentSpeed != 0
fun move() // 암시적으로 열려 있고, final로 정의하면 컴파일 오류 발생
// error: modifier 'final' is not applicable inside 'interface'
final fun move() {}
fun stop()
fun report() {
println(if (isMoving) "Moving at $currentSpeed" else "Still")
}
}
인터페이스 내부에는 초기화 코드나 위임이 붙은 프로퍼티는 금지된다.
interface Vehicle13 {
// error: property initializers are not allowed in interfaces
val currentSpeed = 0
// error: delegated properties are not allowed in interfaces
val maxSpeed by lazy { 100 }
}
추상 클래스와 달리 인터페이스에 대한 생성자는 금지된다.
// error: property initializers are not allowed in interface
interface Person(val name: String)
interface Vehicle {
// error: delegated properties are not allowed in interface
constructor(name: String)
}
한 타입이 동일한 시그니처를 가지는 멤버가 들어있는 다른 인터페이스를 둘 이상 상속할 때는 super<상위타입>.메서드 호출을 사용한다.
interface Car11 {
fun move()
}
interface Ship2 {
fun move()
}
class Amphibia : Car11, Ship2 {
override fun move() {
println("I'm moving")
}
}
interface Car12 {
fun move(){
println("I'm riding")
}
}
interface Ship3 {
fun move()
}
class Amphibia2 : Car12, Ship3 {
override fun move() {
super.move() // Car에서 상속받은 메서드를 호출하는 문제가 생김
super<Car11>.move() // Car11에서 상속받은 메서드를 호출
super<Ship2>.move()
}
}
fun main() {
Amphibia2().move() // I'm riding
}
8.2.3 봉인된 클래스와 인터페이스
Enum 클래스는 미리 정의된 상수 집합을 표현하여 이외의 값이 들어오는걸 막을 수 있다.
enum class Result {
SUCCESS, ERROR
}
fun runComputation(): Result {
try {
println("Input a int:")
val a = readLine()?.toInt() ?: return Result.ERROR
println("Input a int:")
val b = readLine()?.toInt() ?: return Result.ERROR
println("Sum: ${a + b}")
return Result.SUCCESS
} catch (e: NumberFormatException) {
return Result.ERROR
}
}
fun main() {
val message = when (runComputation()) { // when식을 통해 미리 정의된 상수 처리 및 이외의 값이 들어올 수 없는 것을 컴파일 단에서 감지
Result.SUCCESS -> "Completed successfully"
Result.ERROR -> "Error!"
}
println(message)
}
하지만 특정 케이스의 경우 각 종류별로 애트리뷰트가 다를 수 있어, 추상 클래스를 사용하여 성공 및 하위 클래스의 특정 케이스를 처리할 수 있지만, Enum 처럼 type의 제한을 할 수 없다.
abstract class Result2 {
class Success(val value: Any) : Result2() {
fun showResult() {
println(value)
}
}
class Error(val message: String) : Result2() {
fun throwException() {
throw Exception(message) // type 을 다양하게 처리 가능함
}
}
}
fun runComputation2(): Result2 {
try {
println("Input a int:")
val a = readLine()?.toInt()
?: return Result2.Error("Missing first argument")
println("Input a int:")
val b = readLine()?.toInt()
?: return Result2.Error("Missing second argument")
return Result2.Success(a + b)
} catch (e: NumberFormatException) {
return Result2.Error(e.message ?: "Invalid input")
}
}
fun main() {
val message = when (val result = runComputation2()) {
is Result2.Success -> "Completed successfully: ${result.value}"
is Result2.Error -> "Error: ${result.message}"
else -> return // result 결과를 제한할 수 없어 else 절이 필요함
}
println(message)
}
코틀린은 (sealed)봉인된 클래스나 인터페이스(코틀린 1.5 이상)를 통해 이런 문제를 극복할 수 있다.
- 상속하는 클래스는 내포된 클래스 or 객체로 정의, 같은 파일 내에 최상위 클래스로 정의돼야만 한다.
- 이 밖에 영역에서는 봉인된 클래스 상속 불가하다. (final 클래스와 동일 효과)
- 직접 인스턴스를 만들 수 없어 기본적으로 추상 클래스 이다.
sealed class Result { // Result 클래스는 Success 와 Error로 제한했다
class Success(val value: Any) : Result() {...}
class Error(val message: String) : Result() {...}
}
Enum과 마찬가지로 불필요한 else를 사용 안해도 되는 빠진 부분이 없는 when 형태를 지원한다.
// compile 시점에 Result의 하위 클래스는 Success와 Error만 존재한다고 인식
val message = when (val result = runComputation()) {
is Result.Success -> "Completed successfully: ${result.value}"
is Result.Error -> "Error: ${result.message}"
}
// 봉인된 클래스 확장시 when 구문에 실수로 코드를 누락하는일이 발생하지 않는다
class Confirm(val message: String) : Result() {...}
상속 제한은 봉인된 클래스를 직접 상속한 클래스에 대해서만 성립한다.
// Result.kt
sealed class Result { // Result 클래스는 Success 와 Error로 제한했다
class Success(val value: Any) : Result()
open class Error(val message: String) : Result()
}
// Util.kt
class FatalError(message: String): Result.Error(message)
데이터 클래스가 봉인된 클래스 계층에 속할 수도 있다.
sealed class Expr
data class Const(val num: Int): Expr() // 봉인된 클래스의 하위 계층으로 사용
data class Neg(val operand: Expr): Expr()
봉인된 클래스를 객체로 구현할 수도 있다.
sealed class Result {
object Completed: Result()
class ValueProduced(val value: Any) : Result()
class Error(val message: String) : Result()
}
8.2.4 위임
기존 클래스의 확장이나 변경이 어려울 경우 위임 패턴을 사용할 수 있다.
interface PersonData {
val name: String
val age: Int
}
open class Person17(
override val name: String,
override val age: Int
): PersonData
data class Book(val title: String, val author: PersonData) {
override fun toString() = "'$title' by ${author.name}"
}
fun main() {
val valWatts = Person17("Val Watts", 30)
val introKotlin = Book("Introduction to Kotlin", valWatts)
println(introKotlin) // 'Introduction to Kotlin' by Val Watts
}
// 메서드나 프로퍼티를 다른 객체에 전달하기 위해 작성해야하는 준비 코드가 너무 많은 것이 단점
class Alias(
private val realIdentity: PersonData,
private val newIdentity: PersonData
) :PersonData {
override val name: String // 인터페이스의 프로퍼티를 오버라이드한 커스텀 게터 작성
get() = newIdentity.name
override val age: Int
get() = newIdentity.age
}
fun main21() {
val valWatts = Person17("Val Watts", 30)
val johnDoe = Alias(valWatts, Person17("John Doe", 25))
val introJava = Book("Introduction to Java", johnDoe)
println(introJava) // 'Introduction to Java' by John Doe
}
코틀린은 위와 같은 위임을 처리하는 기능을 내장하고 있다.
- 상위 인터페이스 이름 바로 뒤 (by) 키워드 작성 후 위임할 인스턴스를 작성한다
class Alias(
private val realIdentity: PersonData,
private val newIdentity: PersonData
): PersonData by newIdentity // newIdentity 인스턴스에 있는 이름으로 메서드가 구현된다
구현을 바꾸고 싶은 경우 직접 멤버를 오버라이드할 수 있다.
class Alias(
private val realIdentity: PersonData,
private val newIdentity: PersonData
): PersonData by newIdentity {
override val age: Int get() = realIdentity.age
}
클래스는 인터페이스 멤버를 구현할 때만 위임을 쓸 수 있다.
open class Person(
override val name: String,
override val age: Int
)
class Alias(
private val realIdentity: PersonData,
private val newIdentity: PersonData
): Person by newIdentity // error: only interface can be delegated to // 클래스는 위임 불가
클래스 위임은 번거로운 준비 코드없이 객체 합성과 상속의 이점을 살릴 수 있다.
'Programming > Kotlin' 카테고리의 다른 글
[Kotlin] Spring Boot와 Armeria를 이용한 gRPC Server 톺아 보기 (0) | 2023.12.12 |
---|---|
[Kotlin] ERROR: Query failed with error code 2 and error message ‘Field ‘locale’ is invalid. (3) | 2023.03.27 |
[Kotlin] Kotlin + Spring Webflux + gRPC 환경 구성 (0) | 2023.03.24 |
[Kotlin] ERROR: Only the Kotlin standard library is allowed to use the 'kotlin' package (0) | 2023.03.22 |
[Kotlin] 13. Kotlin에서 Coroutine이란? (0) | 2022.12.10 |
[Spring] Custom Mapper with MapStruct
Custom Mapper with MapStruct
개요
MapStruct를 사용한 사용자 정의 매퍼 방법에 대하여 정리해 봅니다.
현재 실무에서 MapStruct 라이브러리를 활용해 Java Bean 유형 간의(DTO <> Domain) 매핑 구현에 사용하고 있습니다.
유사한 기능의 ModelMapper 라이브러리도 존재하지만 맵핑이 일어나는 시점에 리플렉션 API 방식으로 처리되기 때문에 B2C or 대용량 트래픽을 핸들링하는 사이트에서는 성능 이슈로 사용 하기에는 적절하지 않습니다. 반면에 MapStruct는 컴파일 시점에 구현체가 만들어지는 Code Generated 방식으로 성능상의 월등한 차이로 많이 활용하고 있습니다.
MapStruct 및 Bean Object Patten
일반적으로 Java 애플리케이션에서 POJO를 다른 POJO로 변환하는 로직을 흔히 볼 수 있습니다. 예를 들어, 도메인에서 가져오는 Entity와 클라이언트로 전달하는 DTO간에 일반적인 유형의 변환이 발생하는데, 이것을 손쉽게 해결하는 것이 MapStruct입니다. 뭐, 수동으로 Bean mapper를 만들 수도 있겠지만 시간이 많이 소요되고 노가다성 코드가 발생하게 됩니다. 그러나 MapStructs는 자동으로 Bean mapper Class를 생성할 수 있습니다.
Dependency
maven 에 아래 종속성을 추가해 줍니다.
MapStruct의 안정적인 최신 릴리스 버전은 maven central repository에서 사용할 수 있습니다.
Processor는 컴파일 시점에 구현체를 생성하는 역할을 담당하게 됩니다.
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.4.1.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.4.2.Final</version>
</dependency>
MapStruct 활용
Entity 및 DTO 객체를 만들어 보겠습니다.
@Getter @Setter
public class Purchase {
private String orderName;
private String orderDescription;
}
@Getter @Setter
public class PurchaseDTO {
private String orderName;
private String orderDescription;
}
Mapper Interface
객체 맵핑을 위해 아래와 같은 인터페이스만 생성해 주면 됩니다. 그 이유는 구현체는 MapStruct-Processor 가 알아서 생성해 주기 때문입니다. 다만, 아래와 같은 인터페이스를 별도로 생성해주는 작업은 modelMapper에 비해 약간의 번거로움이 있습니다. 그럼에도 애플리케이션의 성능 향상을 위해서는 필수 요소라고 생각이 됩니다.
@Mapper
public interface PurchaseMapStruct {
PurchaseMapStruct INSTANCE = Mappers.getMapper(PurchaseMapStruct.class);
PurchaseDTO toPurchaseDto(Purchase entity);
}
Genernat 된 mapper
Maven clean install을 실행하면 /target/generated-sources/annotations/ 경로에 아래와 같은 구현체가 생성이 됩니다. MapStruct가 자동으로 생성해주는 클래스입니다.
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2022-10-28T16:52:20+0900",
comments = "version: 1.4.2.Final, compiler: javac, environment: Java 1.8.0_282 (AdoptOpenJDK)"
)
public class PurchaseMapStructImpl implements PurchaseMapStruct {
@Override
public PurchaseDTO toPurchaseDto(Purchase dto) {
if ( dto == null ) {
return null;
}
PurchaseDTO purchaseDTO = new PurchaseDTO();
purchaseDTO.setOrderName( dto.getOrderName() );
purchaseDTO.setOrderDescription( dto.getOrderDescritpion() );
return purchaseDTO;
}
}
Mapping Fields With Different Field Names
Enitity와 DTO 필드의 이름이 다른 경우에도 Java Bean 객체를 자동으로 매핑할 수 있습니다. 아래 예제를 보겠습니다.
@Getter @Setter
public class Purchase {
private String ordNm;
private String ordDesc;
private String ordDate;
private OrdItem ordItem;
}
@Getter @Setter
public class PurchaseDTO {
private String orderName;
private String orderDescription;
private Date orderDate;
Private OrderItem orderItem;
}
Mapper Interface 선언
다른 필드 이름을 매핑할 때 @Mapping 애너테이션을 추가하여 source와 target을 정의해 주면 됩니다.
@Mapper
public interface PurchaseMapStruct {
PurchaseMapStruct INSTANCE = Mappers.getMapper(PurchaseMapStruct.class);
@Mapping(target="orderName", source="entity.ordNm")
@Mapping(target="orderDescription", source="entity.ordDesc")
PurchaseDTO toPurchaseDto(Purchase entity);
}
Mapping With Type And Value Conversion
String Type의 날짜를 Date 객체로 변환할 수도 있습니다.
@Mapping(target="orderDate", source="entity.ordDate", dateFormat="yyyy-MM-dd HH:mm:ss")
PurchaseDTO toPurchaseDto(Purchase entity);
때로는 Value에 대한 커스터 마이징이 필요할 경우도 있습니다. Interface 내에 default method를 정의 하여 다른 빈에 대한 참조로 새로운 맵핑을 할 수도 있습니다.
@Mapping(target="orderDate", source="entity.orderDate", qualifiedByName="toOrderYear")
@Mapping(target="orderItem", source="entity.ordItem", qualifiedByName="toOrderItem")
PurchaseDTO toPurchaseDto(Purchase entity);
@Named("toOtderYear")
default String toOrderYear(String ordDate) {
if(ordDate == null){
return null;
}
return ordDate.substring(0,4);
}
@Named("toOrderItem")
default OrderItem toOrderItem(OrdItem item){
if(item == null){
return null;
}
OrderItem orderItem = new OrderItem();
orderItem.setItemName(item.getItemNm());
orderItem.setOrderPrice(item.getPrice());
return orderItem;
}
Service Layer 호출
Stream에서 map과 함께 entity to DTO 변환에 아래와 같이 적용 할 수 있습니다.
public List<PurchaseDTO> getPurchases() {
return purchaseComponent.getPurchases(purchaseReq)
.stream()
.map(PurchaseMapStruct.INSTANCE::toPurchaseDto)
.collect(Collectors.toList());
}
결론
MapStruct를 활용한 Java Bean 객체간의 전환에 대하여 기본적인 표현식과 다른 객체를 맵핑하는 방법에 대하여 소개하였습니다.
'Programming > Spring' 카테고리의 다른 글
Spring Annotation @Componenet, @Configuration 차이 (0) | 2022.06.21 |
---|---|
[Spring] interceptor login check (0) | 2015.02.11 |
[Spring] org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'HEAD' not supported (0) | 2015.02.05 |
mybatis cache 설정 (0) | 2014.05.14 |
[Spring] org.xml.sax.SAXParseException: Document root element "configuration", must match DOCTYPE root "mapper" (0) | 2014.05.13 |
GIT merge
merge 란?
- git merge는 다른 브랜치를 현재 Checkout된 브랜치에 Merge 하는 명령으로, Merge 하고 나서 현재 브랜치가 Merge 된 결과를 가리키도록 옮긴다.
merge 명령어
- 서로 다른 branch 병합 하기
git checkout {branchA}
git merge {branchB} //A에 B 변경 내역을 병합
git merge --continue // 병합 저장
git merge --abort // 병합 롤백
- 특정 브랜치에서 file 1개만 merge 하기
git checkout {branchA} {filename} //A branch 에서 file 1개 merge
'Infra > git' 카테고리의 다른 글
GIT rebase (0) | 2022.06.21 |
---|---|
GIT push (0) | 2022.06.21 |
GIT commit (0) | 2022.06.21 |
GIT stash (0) | 2022.06.21 |
GIT clean (0) | 2022.06.21 |
GIT rebase
rebase
- 서로 다른 두브랜치를 병합시킬 때
- 여러개의 commit 이력을 1개의 새로운 commit 으로 합칠 때(되감기)
- git merge 전략 중 하나.
- 내가 작업한 브랜치에 master의 최신 이력을 적용시킬 때(브랜치 현행화)
- 개인 브랜치는 commit history 깔끔하게 관리하고 싶을 경우 수행 추천.
- 공동 작업 브랜치 or master에는 수행하지 말자(remote master에 merge된 경우 기존의 commit tree가 완전히 달라지기에 동료들로 부터.....)
rebase 활용
- feature 브랜치에서 작업을 하는 동안 main 브랜치가 release, hotfix 등 변경되었고 merge 하려 할 때 충돌이 발생, feature 브랜치에서 main 브랜치를 rebase 진행한 후 main으로 merge를 수행한다.
- main 브랜치의 내용을 현재 작업 중인 feature 브랜치로 병합할 때(최신 commit으로 base 옴겨주기)
git rebase -i HEAD~2,3 //합칠 개수
git rebase -i HEAD~3 //마지막부터 몇번째 commit 까지 개수
pick -> squash 변경 후 vi 저장 //합칠경우 (lastest 부터 역순으로!!!!)
pick -> drop 변경 후 vi 저장 //특정 commit 삭제
git log //합쳐진 commit 확인 or git reflog
git commit -m "주석 수정" //합친 후 주석 수정 or 새로운 commit 안남길 경우 건너뜀
git rebase --continue //rebase 완료
git rebase --abort //rebase 롤백시
git push origin {branch} -f //force remote push
- rebase invalid upstream 'HEAD~n' 오류 발생시 처리
git rebase --interactive HEAD~5 //위 순서에 맞춰 vi 편집 후 force push