Web/Spring 2021. 11. 17. 18:21

웹플럭스에서 블록킹 연산을 발생시키는 채널이 있다면, 이벤트 채널을 관리하는 이벤트 루프 자체에 블럭킹이 발생하기 때문에 전체적으로 요청 처리를 하나도 못하는 문제가 발생할 수 있다. 그렇기 때문에 블럭킹을 발생시키는 연산이 있을 경우 스케쥴을 분리시켜주는 것이 좋고, 실제로 리액터에서도 이러한 것을 고려해 스케쥴러 생성 팩토리 메서드를 제공한다.

위 두개의 팩토리 메서드는 non-blocking 연산을 위한 스케쥴러 팩토리 메서드이다. 오래 걸리는 연산 등을 이벤트 루프 쓰레드에서 분리하고 싶을 때 사용하며, 블럭킹 연산이 포함되지 않은 연산에서만 사용해야한다. 만약 블록킹 연산에 대해 스케쥴을 분리하고 싶다면 boundedElastic()을 이용하면 된다.

 

사용법은 아래와 같다.

 

 

그런데, 블록킹 연산에 대해 스케쥴 분리하면 된다는 것은 알겠는데 이걸 어떻게 일일이 찾아서 분리해줄까? 이것은 메서드 시그니쳐로 약속하자.

 

 

위 두개의 메서드는 동기 메서드이며, 첫번째 메서드는 동기코드이며 블럭킹을 유발시키는 코드이다.(InterruptedException 발생) 세번째 메서드는 블록킹 콜이 없는 비동기 메서드이다. 첫번째 두번째 메서드는 사용하는 쪽에서 스케쥴 분리를 신경쓰면 되고 세번째 메서드는 해당 메서드 안에서 스케쥴 분리 처리를 해주어야한다. 근데 진짜 위 메서드 시그니처를 지키면 블록킹 콜이 발생하지 않을까?.. 이것을 찾기위한 도구로 BlockHound가 있다.

 

 

BlockHound는 운영환경에 같이 띄우면 안된다.(실제 바이트 코드등의 영향을 줄 수 있어서) 그렇기에 테스트 코드와 결합하여 사전에 블록킹 연산을 찾아내는 용도로만 사용하자 !(비효율적인 연산 등을 잡는 역할로는 사용 불가)

 

출처: IfKakao

posted by 여성게
:
프로그래밍언어/Kotlin 2020. 9. 16. 13:05

 

오늘은 코틀린의 함수 정의와 호출에 대해 다루어 본다.

 

컬렉션 객체 만들기
fun createHashSet() = hashSetOf(1, 7, 53)

fun createArrayList() = arrayListOf(1, 7, 53)

fun createHashMap() = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-three")

 

기본적으로 코틀린은 컬렉션을 만드는 함수를 기본 라이브러리에 내장이 되어 있다. 또한 마지막에 hashMap을 만드는 함수 안에 "to"라는 키워드가 있는데, 사실 키워드가 아니고 일반 함수이다. 이점은 뒤에서 자세히 설명한다. 또한 생성하는 컬렉션 객체는 코틀린만의 컬렉션 객체가 아니고, 자바의 컬렉션 객체를 생성한다. 하지만 자바에서 제공하고 있는 기능보다 더 유용한 기능들을 제공한다.

 

fun collectionOperate() {
    val list = listOf("one", "two", "three")
    //리스트의 마지막 원소를 가져온다.
    println(list.last())
    
    val numbers = setOf(1, 14, 2)
    //숫자로 이루어진 컬렉션(Set)에서 가장 큰 값을 가져온다.
    println(numbers.max())
}

 

디폴트 파라미터 값
fun <T> defaultParamValue(collection: Collection<T>, separator: String = ", ", prefix: String = "", postfix: String = ""): String {
    val result = StringBuilder(prefix)
    for ((index, elem) in collection.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(elem)
    }
    result.append(postfix)
    return result.toString()
}

fun main() {
    println(defaultParamValue(list))
}

->
1, 2, 3

 

만약 이름있는 파라미터를 사용하면, 함수 호출시 파라미터 순서를 지정할 필요가 없어진다.

 

println(defaultParamValue(list, postfix = "#", prefix = ";"))

 

이러면, 순서와 상관없이 파라미터를 지정할 수 있고, seperate라는 파라미터도 디폴트 값이 적용된다.

 

최상위 함수와 프로퍼티
fun topLevelFunction() {
    println("top level function")
}

 

코틀린에서 클래스 밖에 함수를 정의하면 마치 자바의 static method와 같이 작동한다. 하지만 자바와는 다르게 클래스명을 임포트 할 필요 없이 함수명을 바로 임포트해서 사용할 수 있다. 어떻게 이렇게 실행 될 수 있을까? 이는 바로 JVM이 컴파일할때 새로운 클래스를 정의해주기 때문이다.(file이름을 기반으로 클래스 생성) 실제로 자바에서 해당 함수를 가져다 쓰려면 컴파일러가 만들어준 클래스명을 명시해서 사용해야 한다. ex) JoinKt.joinToString(..)

 

함수와 마찬가지로 프로퍼티도 파일의 최상위 수준에 놓을 수 있다. 기본적으로 최상위 프로퍼티도 다른 모든 프로퍼티처럼 접근자 메서드를 통해 자바 코드에 노출된다. 겉으론 상수처럼 보이지만, 실제로는 게터를 이용해야한다니 조금 이상하다. 그렇게 때문에 더 자연스럽게 자바처럼 public static final 같은 구문을 넣어주는 것이 자연스럽기 때문에 최상위 프로퍼티는 보통 아래와 같이 정의한다.

 

//Java : public static final String UNIX_LINE_SEPARATOR = "\n"
const val UNIX_LINE_SEPARATOR = "\n"

 

확장 함수와 확장 프로퍼티

기존 자바 API를 이용해 확장함수 및 확장 프로퍼티를 정의하는 것이 가능하다. 그 말은 우리가 지금까지 사용했던 setOf(), arrayListOf()등을 호출할 수 있었던 이유다. 다음 예제를 보자.

 

fun String.lastChar(): Char = this.get(this.length - 1)

fun main() {
    println("Kotlin".lastChar())
}

 

함수명 앞에 "String." 을 붙여서 확장할 클래스명을 넣어준다. 이것을 수신객체타입이라 부르고, 함수 내용에 "this" 키워드로 수신객체 타입을 참조할 수 있다. 이 this 키워드로 참조하는 객체를 수신객체(위에서 "Kotlin"이라는 문자열이 수신객체가 된다.)라고 부른다. 여기서 기억할 것은 확장 함수가 캡슐화를 깨지않는다는 것이다. 그 이유는 수신 객체의 private 메서드 같은 것은 접근할 수 없기 때문이다. 여기서 만약, 다른 패키지의 많은 확장함수를 임포트해서 사용하는데 함수의 시그니쳐 등이 모두 동일하면 충돌하는 현상이 발생되는데 이런 경우는 아래와 같이 "as" 키워드로 확장함수의 별칭을 지어줄 수 있다.

 

import com.example.kotlin.ch_3.lastChar as last

fun main() {
    println("Kotlin".last())
}

 

확장함수로 유틸리티 클래스 만들기

기존 자바 API 혹은 코틀린 클래스를 수신객체로 확장함수를 사용해 유틸클래스를 만들면 유용하다.

 

fun <T> Collection<T>.joinToString(seperate: String): String {
    val result = StringBuilder()
    for ((i, elem) in this.withIndex()) {
        if (i > 0) result.append(seperate)
        result.append(elem)
    }
    return result.toString()
}

fun main() {
    val list = listOf(1,2,3)
    println(list.joinToString(","))
}

 

이렇게 제네릭 타입으로 확장함수를 만들수 있고, 특정 타입으로 제한하여 확장함수를 만들 수 있다.

 

fun main() {
    val list = listOf(1,2,3)
    println(list.join2(","))
}

fun Collection<String>.join2(seperate: String): String {
    val result = StringBuilder()
    for ((i, elem) in this.withIndex()) {
        if (i > 0) result.append(seperate)
        result.append(elem)
    }
    return result.toString()
}

 

만약 String을 타입으로 받는 확장함수를 만들었는데, Int를 요소로 가지는 리스트에 대해 확장함수를 호출하면 컴파일 에러가 발생한다. 마지막으로 "확장함수는 함수 오버라이드가 불가능하다." 또한 확장함수와 그 수신객체의 함수의 시그니쳐가 동일하면 수신객체의 멤버 함수를 우선시 한다.

 

상속, 인터페이스 구현관계의 확장함수

아래와 같은 예제가 있다고 가정하자. 코틀린에서 클래스는 final 클래스와 같으므로, 그냥 상속이 안된다. 그렇기 때문에 class 앞에 open 키워드를 넣어준다. 이는 함수 레벨도 동일하다.

 

open class View {
    open fun click() = println("View clicked")
}

class Button: View() {
    override fun click() {
        println("Button clicked")
    }
}

fun main() {
    val view: View = Button()
    view.click()
}

->
Button clicked

 

위 예제는 상속을 예제로 코드를 작성하였는데, 결과로는 Button의 오버라이드된 메서드의 결과가 출력된다. 그렇다면 아래 예제는 어떨까?

 

fun View.showOff() = println("I'm a view !")
fun Button.showOff() = println("I'm a button !")

fun main() {
    val view: View = Button()
    view.showOff()
}

->
I'm a view !

 

위 결과는 상속과는 다르게 동작한다. 실제 변수에 담긴 타입은 Button()이지만, 결과는 View의 확장함수로 작동한다.

 

가변 인자 함수
fun <T> listOf(vararg values: T): List<T> {...}

fun main(args: Array<String>) {
    listOf("abc", *args)
}

 

위 listOf는 "vararg"라는 키워드로 가변인자를 받을 수 있는 파라미터를 정의했다. 자바에서는 "..."으로 배열인 가변인자를 받을 수 있지만, 코틀린은 vararg라는 키워드를 이용한다. 그리고 또 다른 자바와의 차이점은 배열자체를 인자로 넘기는 것이 아니고, 배열 변수 앞에 "*"를 붙여서 명시적으로 배열을 풀어서 넣는 것이다. 기술적으로는 스프레드 연산자가 그런 작업을 해준다. 그리고 또하나의 차이점은 자바와는 달리 배열 혹은 인자를 나열하는 것이 아니라, 인자+배열로 여러 값을 함께 인자로 넘길 수 있다.

 

값의 쌍을 다루기

이전에  Map 오브젝트를 만드는 예제에서 "to"라는 키워드를 사용한 적이 있었다. 여기서 "to"는 키워드가 아니고, 함수라고 이야기를 했는데, 이 함수를 바로 중위함수라는 특별한 방식으로 동작하는 함수이다.

 

fun pair() {
    val pair = 1 to "one"
    println(pair)
}

fun main() {
    pair()
}

->
(1, one)

 

여기서 to, 중위함수는 파라미터가 유일한 함수를 이렇게 호출할 수 있게 되는 것이다. 사실은 "1.to("one")"과 같은 구문이며, 실제로는 to는 파라미터가 하나로 유일한 함수인 것이다. 이것을 실제로 예제로 구현해보면 아래와 같다.

 

infix fun Int.pair(v: String): Pair<Int, String> {
    return Pair(this, v)
}

fun main() {
    1 pair "one"
}

 

함수의 정의를 보면, Int의 확장함수로 선언했으며 실제로 파라미터는 유일하게 하나만 받고 있다. 이러한 함수를 중위호출에 사용하려면 "infix"라는 키워드를 선언하면 된다.

 

구조 분해 선언
fun main() {
    val (left, right) = Pair(1, "one")
    val list = listOf(1, 2, 3)
    for ((index, value) in list.withIndex()) {}
}

 

Pair과 같이 값과 쌍으로 이루어진 객체의 값을 분해해서 받을 수 있는데, 이것을 구조 분해 선언이라고 한다.

 

문자열 나누기

자바의 split과 비슷하게 사용하면 되지만, 조금더 확장된 함수를 제공한다. 아래와 같이 문자열 하나 혹은 문자열 여러개를 넘겨 여러 구분 문자열로 나눌수도 있고, 혹은 직접 정규식을 생성해서 split의 구분문자 정규식을 넘길 수도 있다.

 

fun main() {
    val str = "12.345-6.A"

    println(str.split(".", "-"))
    println(str.split("\\.|-".toRegex()))
}

 

Stirng의 substring 함수
fun main() {
    val path = "/Users/levi/kotlin/chapter3.kt"
    val directory = path.substringBeforeLast("/")
    val fullName = path.substringAfterLast("/")
    val fileName = fullName.substringBefore(".")
    val extension = fullName.substringAfter(".")
    println("Dir : $directory, FullName : $fullName, FileName : $fileName, Extension : $extension")
}

->
Dir : /Users/levi/kotlin, FullName : chapter3.kt, FileName : chapter3, Extension : kt

 

 

로컬 함수와 확장

코틀린에서는 중복된 로직을 깔끔하게 리팩토링할 수 있는 해법이 있다. 코틀린에서는 함수에서 추출한 함수를 원 ㅎ마수 내부에 중첩시킬 수 있고, 그렇게 하면 문법적인 부가 비용을 들이지 않고 깔끔하게 코드를 조작할 수 있다. 바로 예시를 살펴보자.

 

class User(val id: Long, val name: String, val address: String) {
    fun saveUser(user: User) {
        if (user.name.isEmpty()) {
            throw IllegalArgumentException("empty user name")
        }
        
        if (user.address.isEmpty()) {
            throw IllegalArgumentException("empty user address")
        }
        
        // user를 데이터베이스에 저장하는 로직.
    }
}

 

위와 같은 코드가 있다. 해당 코드에서는 검즈하는 필드 값만 다를뿐 사실 같은 중복된 검증 로직(문자열이 비어있는지)을 가지고 있다. 이러한 중복된 코드 내용은 로컬 함수로 분리하면서 제거할 수 있다.

 

class User(val id: Long, val name: String, val address: String) {
    fun saveUser(user: User) {
        fun validate(user: User, value: String, fieldName: String) {
            if (value.isEmpty()) {
                throw IllegalArgumentException("empty ${user.id} $fieldName")
            }
        }
        
        validate(user, user.name, "name")
        validate(user, user.address, "address")

        // user를 데이터베이스에 저장하는 로직.
    }
}

 

위 예제는 중복된 검증 로직을 로컬 함수로 분리함으로써 중복을 제거하였다. 하지만, 뭔가 User 객체를 넘기는 것이 보기 싫다. 조금 더 개선하자면, 로컬 함수는 자신을 감싸고 있는 함수의 변수를 캡쳐링해서 가져갈 수있는 클로저로 동작하기 때문에 굳이 User 객체를 인자로 넘길 필요가 없이 아래처럼 리팩토링이 가능하다.

 

class User(val id: Long, val name: String, val address: String) {
    fun saveUser(user: User) {
        fun validate(value: String, fieldName: String) {
            if (value.isEmpty()) {
                throw IllegalArgumentException("empty ${user.id} $fieldName")
            }
        }

        validate(user.name, "name")
        validate(user.address, "address")

        // user를 데이터베이스에 저장하는 로직.
    }
}

 

여기서 멈추지 않고, 조금더 깔끔하게 확장함수를 활용해 리팩토링해보자.

 

class User(val id: Long, val name: String, val address: String) {
    private fun User.validate() {
        fun validateParam(value: String, fieldName: String) {
            fun validate(value: String, fieldName: String) {
                if (value.isEmpty()) {
                    throw IllegalArgumentException("empty ${this.id} $fieldName")
                }
            }            
        }
        validateParam(this.name, "name")
        validateParam(this.address, "address")
    }
    fun saveUser(user: User) {
        user.validate()
        // user를 데이터베이스에 저장하는 로직.
    }
}

 

클래스 내부에 확장함수를 정의해서 saveUser 함수 내부는 훨씬더 군더더기 없는 코드로 리팩토링 되었다. 클래스 내부에 적용한 확장함수는 해당 클래스 내부에서만 사용가능하다는 것을 유의하자(private 키워드를 붙이지 않더라도 해당 클래스 내부에서만 사용가능하다.)

 

여기까지 코틀린의 함수에 대한 내용은 간단히 다루어보았다. 사실 위 내용들보다 많은 내용이 있지만, 다음 포스팅들에서 다루어보지 못한 내용들을 더 많이 다루어볼 것이다.

posted by 여성게
:
프로그래밍언어/Kotlin 2020. 9. 15. 20:33

 

오늘은 코틀린에 대해 아주 기초를 다루어본다.

 

함수(Function)

코틀린에서 함수는 "fun"이라는 키워드로 정의한다. 간단하게 리턴값이 있고, 없는 함수와 바디 내용이 식으로만 이루어졌을때 함수를 간략화 하는 방법은 아래와 같다.

/**
 * 리턴이 없는 함수
 */
fun helloWorld() {
    println("hello, world")
}

/**
 * 리턴값이 있는 함수
 */
fun max(a: Int, b: Int): Int {
    //코틀린의 if문은 식(리턴 값이 존재)이지 문(block, return이 없음)이 아니다.
    return if (a > b) a else b
}

/**
 * max 함수와 간략 버전
 * 함수의 본문이 식으로만 이루어져있다면, 아래처럼 간략하게 바꿀 수 있다.
 * 또한 반환 타입도 생략이 가능하다. 그 이유는 식이 반환하는 타입을 컴파일러가 추론하기 때문이다.
 * fun max2(a: Int, b: Int): Int = if (a > b) a else b
 * ->
 * fun max2(a: Int, b: Int) = if (a > b) a else b
 */
fun max2(a: Int, b: Int) = if (a > b) a else b

 

변수(Variable)

크게 어색하지 않은 키워드이다. 하지만 val은 자바의 final과 거의 비슷한 느낌인데, 선언시 반드시 초기화 식이 들어가지 않아도 된다. 그 이유는 블록내에 컴파일러에게 오직 한번만 할당한다라는 보장을 줄 수 있다면, 블록내에 여러군대에서 할당하는 코드가 들어가도 된다.

//타입을 명시하지 않아도 컴파일러가 타입을 추론한다.
val question = "sample question" -> String
val num = 42 -> Int
val yearsToCompute = 7.5e6 -> Double

//변수의 타입을 직접 명시해줄 수도 있다.
val answer: Int = 42

//변경, 재할당이 가능한 변수
//한번 타입이 추론되면 다른 곳에서 어싸인할때 다른 타입으로 못넣음.
var variable = 1

/**
 * val 키워드지만, 컴파일러가 오직 하나의 초기화 문장만
 * 실행됨이 확실하면 여러 곳에서 할당하는 코드가 들어갈 수 있다.
 */
fun variableExam(b: Boolean): String {
    val message: String

    if (b) {
        message = "true"
    } else {
        message = "false"
    }

    return message
}

/**
* 타입이 생략가능하다지만, 다른 타입을 재할당 할 수는 없다.
*/
var intValue = 1
intValue = "123" -> 컴파일에러

 

마지막에 var에 다른 타입으로 재할당이 불가능하지만,  변환 함수나 강제 형변환을 통해 다른 타입의 값을 변수에 재할당 가능하다.

 

문자열 템플릿

 

문자열을 다룰때 "$" 키워드로 변수값을 그대로 넣을 수 있다. 마치 자바에서 "Hello" + name + "!" 과 비슷하지만 조금더 간결하게 작성가능하다. 또한 "${}"를 이용하여 변수명만 아니라, 복잡한 식도 넣을 수 있다.

//컴파일된 코드는 StringBuilder를 사용하고, 문자열 상수와 변수의 값을 append로 문자열 빌더 뒤에 추가한다.
fun main(args: Array<String>) {
    val name = if (args.isNotEmpty()) args[0] else "Kotlin"
    println("Hello, $name!")
}

fun stringTemplate(args: Array<String>) {
    if (args.isNotEmpty()) {
        println("Hello, ${args[0]}!")
    }
}

fun stringTemplate2(args: Array<String>) {
    if (args.isNotEmpty()) {
        println("Hello, ${if (args.isNotEmpty()) args[0] else "Kotlin"}!")
    }
}

 

클래스와 프로퍼티

자바의 클래스와 코틀린의 클래스 선언 방법을 비교해보자.

/**
 * Java class definition
 */
public class Person {
    private final String name;
    
    public Person(String name) {
        this.name = name;
    }
    
    public String getName() {
        return this.name;
    }
}

/**
 * Kotlin class definition
 */
class Person(val name: String)

 

자바에서는 프로퍼티가 증가할 수록 생성자에 매개변수가 증가함에 따라 내부 this 연산자로 프로퍼티를 초기화하는 코드도 증가한다.(물론 롬복이라는 라이브러리를 사용하면 되지만..) 코틀린은 프로퍼티가 많아져도 아주 간단하게 클래스를 정의할 수 있다. 또한 코틀린은 기본 접근제어자가 public 이기 때문에 public 키워드도 생략가능하다.

 

/**
 * Kotlin class definition
 */
class Person(val name: String,
             var age: Int)

fun main(args: Array<String>) {
    val person = Person("levi", 29)
    println(person.name)
    println(person.age)
    person.age = 30
}

 

생성자에서 var로 프로퍼티를 선언하면, 실제 변경가능한 프로퍼티로 생성된다. 자바와 달리 setxx, getxx이 필요없고, 실제 프로퍼티를 참조해 값을 가져오거나 값을 변경할 수 있다.(프로퍼티를 참조하면 내부적으로 getter, setter를 호출해준다.)

 

/**
 * Kotlin class definition
 */
class Person(val name: String,
             var age: Int) {
    val isProgrammer: Boolean
        get() {
            return name == "levi"
        }
        //get() = name == "levi"
}

fun main(args: Array<String>) {
    val person = Person("levi", 29)
    println(person.isProgrammer)
}

 

또한 위 예제처럼 실제 초기화 시점에는 값을 받을 필요가 없는 프로퍼티의 경우 직접 프로퍼티의 접근자(getter)를 정의해줄 수 있다. 함수로 fun isProgrammer() = name == "levi"를 정의할 수 있지만, 성능상 차이는 없고, 가독성 차이 뿐이다.

 

디렉토리와 패키지

코틀린은 자바와 패키지에 대한 개념이 비슷하다. 하지만 아래와 같이, 최상위 함수가 정의되어있다면 다른 패키지에서 해당 함수만 임포트해서 사용가능하다.

 

package com.example.kotlin.ch_1

class Rectangle(val height: Int, val width: Int) {
    val isSquare: Boolean
        get() = height == width

    fun sumHeightAndWidth() = height + width
}

fun createRandomRectangle(): Rectangle {
    val random = Random()
    return Rectangle(random.nextInt(), random.nextInt())
}

=========================================================

import com.example.kotlin.ch_1.createRandomRectangle

fun main(args: Array<String>) {
    var rectangle = createRandomRectangle()
}

 

또한 같은 패키지라면, 함수를 임포트할 필요없이 얼마든지 가져다 사용할 수 있다.

 

선택 표현과 처리: enum과 when

enum 클래스는 아래와 같이 선언한다.

 

enum class Color(val r: Int, val g: Int, val b: Int) {
    RED(255, 0, 0), ORANGE(255, 165, 0),
    YELLOW(255, 255, 0), GREEN(0, 255, 0);
    
    fun rgb() = (r * 256 + g) * 256 + b
}

 

when 절은 자바의 switch case와 상응하는 문법이다. if else와 같이 리턴값을 가지는 식이다.

 

fun getMnemonic(color: Color) = when (color) {
    Color.RED -> "GOOD"
    Color.ORANGE -> "NOT BAD"
    Color.GREEN, Color.YELLOW -> "BAD"
}

 

자바의 switch 문과 달리 코틀린의 when은 임의의 객체를 인자로 받을 수 있다.

 

#setOf Set 객체를 만드는 함수
fun mix(c1: Color, c2: Color) = when (setOf(c1, c2)) {
    setOf(Color.RED, Color.ORANGE) -> "first"
    setOf(Color.YELLOW, Color.GREEN) -> "second"
    else -> throw Exception("exception")
}

 

또한 when은 인자가 없이 만들 수도 있다.

 

fun mixOptimized(c1: Color, c2: Color) = when {
    (c1 == Color.RED && c2 == Color.YELLOW) -> "first"
    (c1 == Color.YELLOW && c2 == Color.GREEN) -> "second"
    else -> throw Exception("exception")
}

 

위처럼 인자가 없는 when 식을 사용하면, 각 분기의 조건이 Boolean을 반환하는 식을 넣으면 된다.

 

스마트 캐스트: 타입 검사와 타입 캐스트를 조합

자바와 달리 코틀린은 명시적으로 타입 캐스팅하는 코드를 넣을 필요가 없다.

 

interface Expr

class Num(val value: Int) : Expr

class Sum(val left: Expr, val right: Expr) : Expr

fun eval(e: Expr): Int =
        if (e is Num) {
            val num = e as Num
            num.value
        } else if (e is Sum) {
            eval(e.left) + eval(e.right)
        } else {
            throw IllegalArgumentException("Unknown expression")
        }

 첫번째 if문은 자바처럼 명시적으로 "as" 키워드를 사용해 타입 캐스팅을 하였다. 하지만 else if 문을 보면 딱히 타입 캐스팅을 하지 않았는데도, Sum 타입으로 변환이 되었다. 이것을 스마트 캐스팅이라고 한다.

 

이번엔 위 eval 함수를 when식로 리팩토링 해보자.

 

fun eval(e: Expr): Int =
        when (e) {
            is Num -> {
                val num = e as Num
                num.value
            }
            is Sum -> {
                eval(e.left) + eval(e.right)
            }
            else -> {
                throw IllegalArgumentException("Unknown expression")
            }
        }

 

코틀린에서 조금 특이하게 동등성 검사가 아닌 다른 기능에도 when식을 사용할 수 있다. 그리고 조금 특이한것이 블록 내에 return이 생략되어 있는 것을 볼 수 있다. 이 말은 "블록의 마지막 식이 블록의 결과"라는 규칙이 항상 성립되는 것이다.

 

for문

코틀린은 자바의 forEach문만 지원한다. 이말은 for(int i=0; i<10; i++) 같은 문법을 지원하지 않는것이다.

 

package com.example.kotlin.ch_2

fun forEach() {
    val list = listOf("a", "b", "c")

    for (str in list) {
        println(str)
    }
}

fun forEach1() {
	val list = arrayListOf(10, 11, 12)
    for ((index, elem) in list.withIndex()) {
    	println("$index, $elem")
    }
}

fun forEach2() {
    //range 1부터 10까지 포함
    val oneToTen = 1..10

    for (i in oneToTen) {
        println(i)
    }
}

fun forEach3() {
    for (i in 10 downTo 1 step 2) {
        println(i)
    }
}

fun main() {
    forEach()
    println("===============")
    forEach2()
    println("===============")
    forEach3()
}

->
a
b
c
===============
0, 10
1, 11
2, 12
===============
1
2
3
4
5
6
7
8
9
10
===============
10
8
6
4
2

 

특이하게 코틀린은 range라는 변수를 선언할 수 있어서 자바의 for문처럼 흉내를 낼 수 있다. 또한 downTo, step 문법을 지원해 역순으로 출력하는 것도 가능하고, withIndex로 리스트의 인덱스와 요소를 같이 받을 수도 있다.

 

맵에 대한 for문
fun forEach4() {
    val lowerUpperCaseMap = HashMap<Char, Char>()

    for (c in 'A'..'F') {
        val lowerCase = c.toLowerCase()
        //자바의 map.put(lowerCase, c)와 같다.
        lowerUpperCaseMap[lowerCase] = c
    }

    for ((lower, upper) in lowerUpperCaseMap) {
        println("$lower $upper")
    }
}

 

in으로 컬렉션이나 범위의 원소 검사
fun recognize(c: Char) = when (c) {
    //'0' <= c && c <= '9'
    in '0'..'9' -> "It's digit!"
    //'a' <= c && c <= 'z'
    in 'a'..'z', in 'A'..'Z' -> "It's a letter"
    else -> "I don't know"
}

 

Comparable 등이 구현되어 있다면, 일반 오브젝트에서도 "in" 사용이 가능하다.

 

코틀린의 예외 처리

코틀린은 자바와 같이 checked exception과 unchecked exception을 구분하지 않는다. 즉, 예외 처리를 강제하지 않아며, 메서드에 throws 문을 명시하지 않아도 된다.

 

fun readNumber(reader: BufferedReader): Int? {
    try {
        val line = reader.readLine() //checked exception이 발생
        return Integer.parseInt(line)
    } catch (e: NumberFormatException) {
        return null
    } finally {
        reader.close()
    }
}

 

위 코드의 경우 reader.readLine()이 IOException(checked exception)을 발생시키지만, 코틀린에서는 어디에도 다시 throw하거나, 캐치해서 처리하지 않는다. 즉, checked exception을 강제하지 않는 것이다.

 

또한 try-catch는 물론 식이므로 아래와 같이 코드를 변경할 수 있다.

 

fun readNumber2(reader: BufferedReader) {
    val number = try {
        Integer.parseInt(reader.readLine())
    } catch (e: NumberFormatException) {
        null
    }
    
    println(number)
}

fun readNumber3(reader: BufferedReader) {
    val number = try {
        Integer.parseInt(reader.readLine())
    } catch (e: NumberFormatException) {
        return
    }

    println(number)
}

 

하지만 조금 다른 것이 catch에서 null을 반환하냐, return 키워드를 쓰냐의 차이인데 return 키워드를 사용하면 catch문 아래의 로직은 수행하지 않고, 해당 메서드가 스택에서 반환된다.

 

여기까지 아주 간단한 코틀린 문법에 대해 다루어 보았다. 다음 포스팅에는 조금더 자세히 코틀린에 대해 다루어본다.

 

<참고 서적>

posted by 여성게
:
Web/Spring 2020. 5. 15. 14:12

 

MongoDbConfig를 작성할때, 몽고디비 서버 호스트관련하여 ClusterSettings.Builder를 작성해줘야하는데, mongo host에 모든 클러스터 서버 호스트를 명시하지 않고, 하나의 DNS(여러 서버를 하나로 묶은) 혹은 여러 서버 리스트 중 하나의 primary 호스트(ex. primary host를 명시하면 밑에 예외는 발생하지 않지만, 읽기 부하분산이 안된다.)만 명시한경우에는 반드시 multiple mode를 명시해주어야 한다. 내부적으로 host의 갯수를 보고 single mode인지 multiple mode인지 판단하기 때문이다. 해당 코드는 아래와 같다.

 

private ClusterSettings(final Builder builder) {
        // TODO: Unit test this
        if (builder.srvHost != null) {
            if (builder.srvHost.contains(":")) {
                throw new IllegalArgumentException("The srvHost can not contain a host name that specifies a port");
            }

            if (builder.hosts.get(0).getHost().split("\\.").length < 3) {
                throw new MongoClientException(format("An SRV host name '%s' was provided that does not contain at least three parts. "
                        + "It must contain a hostname, domain name and a top level domain.", builder.hosts.get(0).getHost()));
            }
        }

        if (builder.hosts.size() > 1 && builder.requiredClusterType == ClusterType.STANDALONE) {
            throw new IllegalArgumentException("Multiple hosts cannot be specified when using ClusterType.STANDALONE.");
        }

        if (builder.mode != null && builder.mode == ClusterConnectionMode.SINGLE && builder.hosts.size() > 1) {
            throw new IllegalArgumentException("Can not directly connect to more than one server");
        }

        if (builder.requiredReplicaSetName != null) {
            if (builder.requiredClusterType == ClusterType.UNKNOWN) {
                builder.requiredClusterType = ClusterType.REPLICA_SET;
            } else if (builder.requiredClusterType != ClusterType.REPLICA_SET) {
                throw new IllegalArgumentException("When specifying a replica set name, only ClusterType.UNKNOWN and "
                                                   + "ClusterType.REPLICA_SET are valid.");
            }
        }

        description = builder.description;
        srvHost = builder.srvHost;
        hosts = builder.hosts;
        mode = builder.mode != null ? builder.mode : hosts.size() == 1 ? ClusterConnectionMode.SINGLE : ClusterConnectionMode.MULTIPLE;
        requiredReplicaSetName = builder.requiredReplicaSetName;
        requiredClusterType = builder.requiredClusterType;
        localThresholdMS = builder.localThresholdMS;
        serverSelector = builder.packServerSelector();
        serverSelectionTimeoutMS = builder.serverSelectionTimeoutMS;
        maxWaitQueueSize = builder.maxWaitQueueSize;
        clusterListeners = unmodifiableList(builder.clusterListeners);
    }

 

ClusterSettings.Builder.build 메서드의 일부인데, mode를 set하는 부분에 mode를 명시적으로 넣지 않았다면 작성된 호스트 갯수를 보고 클러스터 모드를 결정한다. 만약 MonoDb 서버 여러개를 하나의 도메인으로 묶어 놓았다면, 보통 DNS하나만 설정에 넣기 마련인데, 이러면 write 요청이 secondary에 들어가게 되면 아래와 같은 에러가 발생하게 된다.(먄약 실수로 secondary host를 넣었다면 쓰기요청에 당연히 아래 예외가 계속 발생한다.)

 

MongoNotPrimaryException: Command failed with error 10107 (NotMaster): 'not master' on server bot-meta01-mongo1.dakao.io:27017. 

 

왠지, SINGLE모드일때는 secondary로 write요청이 들어왔을때 primary로 위임이 안되는듯하다.(이건 조사해봐야할듯함, 왠지 싱글모드이면 당연히 프라이머리라고 판단해서 그럴듯?..) 그렇기 때문에 클러스터는 걸려있고, 서버 리스트를 여러 서버를 묶은 DNS 하나만 작성한다면 반드시 ClusterSetting에 "MULTIPLE"을 명시해서 넣야야한다 !

posted by 여성게
:
Web/Spring 2019. 10. 23. 21:33

응답으로 나가는 Object에 getter가 존재하지 않아서 Response Content-type을 추론하지 못했다. 그래서 필터 단에서 임의로 DefaultMediaType으로 */*? * 로 넣어버리지만 사실 Content-Type에 wildCard는 들어가지 못한다. 즉, 예외발생!

posted by 여성게
:
Web/Spring 2019. 9. 11. 23:54

 

이번에 다루어 볼 포스팅은 Spring Cache이다. 스프링 3.1부터 빈의 메서드에 캐시 서비스를 적용할 수 있는 기능을 제공한다. 캐시 서비스는 트랜잭션(@Transaction)과 마찬가지로 AOP를 이용해 메서드 실행 과정에 우리가 모르도록 투명하게 적용된다. 또한 스프링은 장점으로 캐시 서비스 구현 기술에 종속되지 않도록 추상화된 서비스를 제공해주므로 환경이 바뀌거나 구현 기술이 바뀌어도 우리가 적용한 코드의 변경 없이 구현 기술을 바꿔줄 수 있다.

 

그렇다면 여기서 짚고 넘어갈 것이, "애플리케이션 빈의 메서드에 캐시를 적용하는 이유 혹은 목적"은 무엇일까?

 

캐시는 기본적으로 성능의 향상을 위해 사용한다. 여기서 성능 향상은 애플리케이션이 갑자기 막 빨라지고 하는 성능 향상이 아니다. 어떤 요청을 처리하는데 연산이 아주 복잡하거나 데이터베이스와 같은 백엔드 시스템에 많은 부하를 주거나, 외부 API 호출처럼 시간이 오래 걸린다면 캐시의 사용을 고려해볼 만하다. 하지만 결코 앞과 같은 상황에 직면했다고 캐시 기능을 사용하면 안된다.

 

캐시(Cache)는 임시 저장소라는 뜻이다. 복잡한 계산이나 데이터베이스 작업, 외부 API 요청의 처리 결과 등을 임시 저장소인 캐시에 저장해뒀다가 동일한 요청이 들어오면 복잡한 작업을 수행해서 결과를 만드는 대신 이미 캐시에 보관된 결과를 바로 돌려주는 방식이다. 만약 이런 상황이 있다고 생각하자.

 

사용자가 많은 웹사이트 서비스가 있다. 메인 페이지에 접속하면 항상 공지사항이 출력돼야 한다. 공지사항은 어떠한 추가적인 내용이 추가되거나 혹은 내용이 수정되기 전까지는 모든 사용자에게 동일한 내용을 보여준다. 그렇다면 사용자가 만명이 있고 사용자가 들어올때 마다 공지사항을 데이터베이스에 접근하여 보여준다면 총 만번의 데이터베이스 액세스가 이루어질 것이다. 사실 데이터베이스에 자주 접근하는 연산은 비용이 비싸다. 그만큼 데이터베이스에 부하가 간다는 뜻이다. 만약 이럴때 캐시를 적용한다면 첫 사용자가 접근할때만 데이터베이스에서 공지사항 내용을 가져오고 나머지 9999명은 캐시에 저장된 내용을 그대로 돌려준다면 만명의 사용자가 한번의 데이터베이스 액세스로 공지사항을 볼 수 있게 되는 것이다.

 

이렇게 캐시는 여러가지 장점을 가졌지만, 그렇다면 모든 상황에서 캐시를 사용하면 안된다. 

 

"값비싼 비용이 들어가는 요청, 데이터베이스 접근, 외부 요청 등에 캐시를 사용하지만 여기서 전제가 있다. 캐시는 반복적으로 동일한 결과를 주는 작업에만 이용해야 한다. 매번 다른 결과를 돌려줘야 하는 작업에는 캐시를 적용해봐야 오히려 성능이 떨어진다."

 

그렇다면 캐시를 사용하면 아주 주의깊게 체크해야 할 것이 있다.  바로, 캐시에 저장해둔 컨텐츠의 내용이 바뀌는 상황 혹은 시점을 잘 파악하는 것이다. 공지사항이 추가되거나 수정되었다면 캐시는 아직까지 이전 내용의 공지사항을 가지고 있을 것이므로, 이러한 상황에서는 캐시에 저장된 내용을 삭제하고 첫 사용자가 접근하여 캐시의 내용을 최신 내용으로 업데이트 해야한다.

 

이제 직접 코드를 보며 스프링에서 사용할 수 있는 캐시 서비스를 알아보자. 우선 캐시 기능을 사용하기 위해서는 @EnableCaching 애너테이션을 달아주어야 한다.

 

애너테이션을 이용한 캐시 기능 사용(@Cacheable)

스프링의 캐시 서비스 추상화는 AOP를 이용한다. 캐시 기능을 담은 어드바이스는 스프링이 제공한다. 이를 적용할 대상 빈과 메서드를 선정하고 속성을 부여하는 작업은 <aop:config>,<aop:advisor> 같은 기본 AOP 설정 방법을 이용할 수 있다. 하지만 이보다는 @Transaction 애노테이션을 이용하는 것처럼 캐시 서비스도 애너테이션을 이용할 수 있다.

 

캐시 서비스는 보통 메서드 단위로 지정한다. 클래스나 인터페이스 레벨에 캐시를 지정할 수도 있지만 캐시의 특성상 여러 메서드에 일괄적인 설정을 하는 경우는 드물다. 캐시에 저장할 내용과 캐시 설정 정보로 메서드의 리턴 값과 메서드 파라미터를 사용하기에 메서드 레벨로 적용하는 것이 수월하다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Product {
 
    private int productId;
    private ProductType type;
    private String productName;
    private boolean isBest;
 
}
 
@Slf4j
@Service
public class ProductService {
 
    @Cacheable("product")
    public List<Product> bestProductsByType(ProductType productType){
 
        log.info("ProductService.bestProductsByType");
 
        List<Product> bestProducts = new ArrayList<>();
        if(productType.equals(ProductType.TOP)){
            bestProducts.add(new Product(1,ProductType.TOP,"GUCCI SHIRTS",true));
        }else if(productType.equals(ProductType.PANTS)){
            bestProducts.add(new Product(1,ProductType.PANTS,"GUCCI PANTS",true));
        }else{
            bestProducts.add(new Product(1,ProductType.OUTER,"GUCCI OUTER",true));
        }
 
        return bestProducts;
    }
 
    @Cacheable(value = "product", key = "#productType")
    public List<Product> bestProduct(User user, LocalDateTime time, ProductType productType){
 
        log.info("ProductService.bestProduct");
 
        List<Product> bestProducts = new ArrayList<>();
        if(productType.equals(ProductType.TOP)){
            bestProducts.add(new Product(1,ProductType.TOP,"GUCCI SHIRTS",true));
        }else if(productType.equals(ProductType.PANTS)){
            bestProducts.add(new Product(1,ProductType.PANTS,"GUCCI PANTS",true));
        }else{
            bestProducts.add(new Product(1,ProductType.OUTER,"GUCCI OUTER",true));
        }
 
        return bestProducts;
    }
 
    @Cacheable(value = "product", condition = "#user.firstName == 'sora'")
    public List<Product> bestProductForGoldUser(User user){
 
        log.info("ProductService.bestProductForGoldUser");
 
        List<Product> bestProducts = new ArrayList<>();
        bestProducts.add(new Product(1,ProductType.OUTER,"CHANNEL OUTER",true));
        return bestProducts;
    }
 
    @Cacheable("product")
    public List<Product> allBestProduct(){
 
        log.info("ProductService.allBestProduct");
 
        List<Product> bestProducts = new ArrayList<>();
        bestProducts.add(new Product(1,ProductType.TOP,"GUCCI SHIRTS",true));
        bestProducts.add(new Product(1,ProductType.PANTS,"GUCCI PANTS",true));
        bestProducts.add(new Product(1,ProductType.OUTER,"GUCCI OUTER",true));
        return bestProducts;
    }
 
}
cs

 

위 코드는 @Cacheable 애너테이션을 이용하여 캐시 기능을 사용하는 예제 코드이다. 우선 첫번째로 ProductService 클래스의 첫번째 메서드를 살펴보자. 해당 메서드가 처음 호출되는 순간에 캐시에 "product"라는 이름의 공간이 생긴다. 그리고 파리미터로 productType(ProductType.TOP이라는 enum이 들어왔다 생각)이 들어오는데, 해당 값을 이용하여 키를 만든다. 이 말은 "product"라는 캐시 공간에 특정 키값으로 데이터를 캐시한다는 뜻이다. 그리고 첫번째 호출 이후에 동일한 메서드에 동일한 파라미터로 요청이 들어오면 "product"라는 캐시 공간에 ProductType.TOP을 이용한 키값을 가진 데이터가 있는 지 확인하고 만약 데이터가 존재하면 해당 메서드를 호출하지 않고 캐시에 있는 데이터를 돌려준다. 만약에 해당 키값으로 데이터가 없다면 메서드 로직을 수행한 후에 반환 값을 캐시에 적재한다.

 

그렇다면 두번째 메서드처럼 파라미터가 여러개인 메서드라면 어떻게 할까? 답은 바로 코드에 있다. 바로 key라는 설정 값에 SpEL식을 이용해 키값으로 사용할 파라미터 값을 명시할 수 있다. 그렇다면 이제 이 메서드는 명시한 파라미터로만 키값을 생성하게 된다. 만약 파라미터가 특정 객체로 들어온다면 key = "Product.productType" 과 같이 설정할 수 있다. 만약 키값으로 사용할 어떠한 설정 내용도 명시하지 않는 다면 어떻게 처리할까? 바로 여러개의 파라미터의 hashCode()를 조합하여 키값을 생성한다. 그렇지만 해시 코드 값의 조합이 키로서 의미가 있다면 문제가 없지만 대부분은 그렇지 않은 경우가 많기에 key 설정값을 이용하여 키로 사용할 파라미터를 명시해주는 것이 좋다.

 

세번째는 특정 조건에서만 캐싱을 하고 나머지 상황에서는 캐싱을 하고 싶지 않은 경우가 있다. 이럴 경우에는 세번째 메서드처럼 condition이라는 설정 값을 이용한다. 이 설정은 파라미터가 특정 값으로 들어올때만 캐시하고 싶을 때, 사용가능하다. 예제는 위와 같이 사용하면 된다.

 

마지막으로 메서드에 매개변수가 없다면 어떻게 될까? 이럴때는 디폴트 키 값으로 세팅이 되기 때문에 메서드가 처음 호출되면 무조건 데이터가 캐싱이 된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
@SpringBootTest
@RunWith(SpringRunner.class)
public class ProductServiceTest {
 
    @Autowired
    ProductService service;
    List<Product> bestProducts;
    User basicUser;
    User goldUser;
 
    @Before
    public void init(){
        bestProducts = new ArrayList<>();
        basicUser = new User(1,"yeoseong","yoon",28, Level.BASIC);
        goldUser = new User(1,"sora","hwang",30, Level.GOLD);
    }
 
    @Test
    public void noArgCacheTest(){
        bestProducts.addAll(service.allBestProduct());
 
        MatcherAssert.assertThat(bestProducts.size(), CoreMatchers.equalTo(3));
 
        service.allBestProduct();
    }
 
    @Test
    public void oneArgCacheTest(){
 
        bestProducts.addAll(service.bestProductsByType(ProductType.TOP));
 
        MatcherAssert.assertThat(bestProducts.get(0).getProductName(),CoreMatchers.equalTo("GUCCI SHIRTS"));
 
        service.bestProductsByType(ProductType.TOP);
 
        clearList(bestProducts);
 
        bestProducts.addAll(service.bestProductsByType(ProductType.PANTS));
 
        MatcherAssert.assertThat(bestProducts.get(0).getProductName(),CoreMatchers.equalTo("GUCCI PANTS"));
 
        service.bestProductsByType(ProductType.PANTS);
 
        clearList(bestProducts);
 
        bestProducts.addAll(service.bestProductsByType(ProductType.OUTER));
 
        MatcherAssert.assertThat(bestProducts.get(0).getProductName(),CoreMatchers.equalTo("GUCCI OUTER"));
 
        service.bestProductsByType(ProductType.OUTER);
 
    }
 
    @Test
    public void manyArgsCacheTest(){
        bestProducts.addAll(service.bestProduct(basicUser, LocalDateTime.now(),ProductType.TOP));
 
        MatcherAssert.assertThat(bestProducts.get(0).getProductName(),CoreMatchers.equalTo("GUCCI SHIRTS"));
 
        service.bestProduct(goldUser, LocalDateTime.now(),ProductType.TOP);
    }
 
    @Test
    public void conditionCacheTest(){
        bestProducts.addAll(service.bestProductForGoldUser(basicUser));
 
        MatcherAssert.assertThat(bestProducts.get(0).getProductName(),CoreMatchers.equalTo("CHANNEL OUTER"));
 
        service.bestProductForGoldUser(goldUser);
        service.bestProductForGoldUser(goldUser);
    }
 
    private void clearList(List<Product> products){
        products.clear();
    }
 
}
cs

 

위의 테스트 코드를 실행시키면 캐시가 적용된 이후에는 더 이상 ProductService 메서드의 로그가 출력되지 않을 것이다. 그 말은 캐시가 적용되어있다면 메서드 자체를 호출하지 않는 것을 알 수 있다.

 

캐시 데이터 삭제(@CacheEvict)

캐시는 적절한 시점에 제거돼야 한다. 캐시는 메서드를 실행했을 때와 동일한 겨로가가 보장되는 동안에만 사용돼야 하고 메서드 실행 결과가 캐시 값과 달리지는 순간 제거돼야 한다. 캐시를 적절히 제거해주지 않으면 사용자에게 잘못된 결과를 리턴하게 된다.

 

캐시를 제거하는 방법은 두 가지가 있다. 하나는 일정한 주기로 캐시를 제거하는 것과 하나는 캐시에 저장한 값이 변경되는 상황이 생겼을 때 캐시를 삭제하는 것이다.

 

캐시의 제거에도 AOP를 이용한다. 간단하게 메서드에 @CacheEvict 애너테이션을 붙여주면 된다. 캐시 삭제도 캐시 적재와 동일하게 키값을 기준해 적용한다. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    @CacheEvict("product")
    public void clearProductCache(){
        log.info("ProductService.clearProductCache");
    }
 
    @CacheEvict(value = "product", key = "#productType")
    public void clearProductCache(ProductType productType){
        log.info("ProductService.clearProductCache");
    }
 
    @CacheEvict(value = "product", condition = "#user.firstName == 'sora'")
    public void clearProductCache(User user){
        log.info("ProductService.clearProductCache");
    }
    //product의 모든 키값에 해당하는 캐시 데이터 삭제
    @CacheEvict(value = "product", allEntries = true)
    public void clearProductCacheAll(){
        
    }
cs

 

위의 코드를 이용하여 테스트 코드를 만들어 로그가 찍히는 횟수를 확인하자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@SpringBootTest
@RunWith(SpringRunner.class)
public class ProductServiceTest {
 
    @Autowired
    ProductService service;
    List<Product> bestProducts;
    User basicUser;
    User goldUser;
 
    @Before
    public void init(){
        bestProducts = new ArrayList<>();
        basicUser = new User(1,"yeoseong","yoon",28, Level.BASIC);
        goldUser = new User(1,"sora","hwang",30, Level.GOLD);
    }
 
    @Test
    public void clearCacheTest(){
        service.allBestProduct();
        service.clearProductCache();
        service.allBestProduct();
        System.out.println("===================================");
        service.bestProductsByType(ProductType.TOP);
        service.bestProductsByType(ProductType.PANTS);
        service.clearProductCache(ProductType.TOP);
        service.bestProductsByType(ProductType.TOP);
        service.bestProductsByType(ProductType.PANTS);
        System.out.println("===================================");
        service.bestProductsByType(ProductType.TOP);
        service.clearProductCache();
        service.bestProductsByType(ProductType.TOP);
        System.out.println("===================================");
        service.bestProductForGoldUser(goldUser);
        service.clearProductCache(basicUser);
        service.bestProductForGoldUser(goldUser);
        service.clearProductCache(goldUser);
        service.bestProductForGoldUser(goldUser);
    }
 
}
cs

 

이외에 @CachePut이라는 애너테이션이 존재한다. 이 애너테이션은 캐시에 값을 저장하는 용도로 사용한다. @Cacheable과 비슷하게 메서드 실행 결과를 캐시에 저장하지만 @CachePut은 캐시 데이터를 사용하는 것이 아니고 해당 애너테이션이 붙은 메서드를 호출 할때마다 결과 데이터를 캐시에 적재한다. 보통 한 번에 캐시에 많은 정보를 저장해두는 작업이나, 다른 사용자가 참고할 정보를 생성하는 용도로만 사용되는 메서드에 이용할 수 있다.

 

Cache Manager

스프링의 캐시 서비스는 AOP를 이용해 애플리케이션 코드를 수정하지 않고도 캐시 부가기능을 메서드에 적용할 수 있게 해준다. 동시에 캐시 기술의 종류와 상관없이 후상화된 스프링 캐시 API를 이용할 수 있게 해주는 서비스 추상화를 제공한다. 캐시 기능을 적용하는 AOP 어드바이스는 스프링이 제공해주는 것을 애너테이션을 통해 적용하면 되므로, 우리가 신경 쓸 부분은 적용할 캐시 기술을 선정하고 캐시 관련 설정을 넣어주는 것이다. 

 

캐시 추상화에서는 적용할 캐시 기술을 지원하는 캐시 매너저를 빈으로 등록해줘야 한다. 여기서는 자세한 설정법은 다루지 않고 캐시 매니저에는 무엇이 있는 지만 다루어 볼 것이다.

 

캐시 매니저 설명
ConcurrentMapCacheManager ConcurrentMapCache 클래스를 캐시로 사용하는 캐시 매니저다. ConcurrentHashMap을 이용해 캐시 기능을 구현한 간단한 캐시다. 캐시 정보를 Map 타입으로 메모리에 저장해두기 때문에 빠르고 별다른 설정이 필요없다는 장점이 있지만, 실제 서비스에서 사용하기에는 기능이 빈약하다. 캐시별 용량 제한이나 다양한 저장 방식 지원, 다중 서버 분산과 같이 고급 캐시 프레임워크가 제공하는 기능을 지원하지 않는다. 따라서 저장될 캐시 양이 많지 않고 간단한 기능에 적용할때 혹은 테스트 용도로만 사용해야 한다.
SimpleCacheManager 기본적으로 제공하는 캐시가 없다. 따라서 프로퍼티를 이용해서 사용할 캐시를 직접 등록해줘야 한다. 스프링 Cache 인터페이스를 구현해서 캐시 클래스를 직접 만드는 경우 테스트에서 사용하기 적당하다.
EhCacheCacheManager 자바에서 가장 인기있는 캐시 프레임워크의 하나인 EhCache를 지원하는 캐시 매니저다. 본격적으로 캐시 기능을 적용하려면 사용을 고려할만하다.
CompositeCacheManager 하나 이상의 캐시 매니저를 사용하도록 지원해주는 혼합 캐시 매니저다. 여러 개의 캐시 기술이나 캐시 서비스를 동시에 사용해야 할 때 이용할 수 있다. CompositeCacheManager의 cacheManagers 프로퍼티에 적용할 캐시 매니저 빈을 모두 등록해주면 된다.
NoOpCacheManager 아무런 기능을 갖지 않은 캐시 매니저다. 보통 캐시가 지원되지 않는 환경에서 동작할 때 기존 캐시 관련 설정을 제거하지 않아도 에러가 나지 않게 해주는 기능이다.

 

여기까지 스프링 캐시 추상화에 대해 다루어보았다. 아마 다음 포스팅은 직접 상용에서 사용할 수 있을 만한 캐시 매니저 구현를 이용해보는 포스팅이 될 것같다.

posted by 여성게
:
일상&기타/책 2019. 8. 11. 22:01

 

필자가 처음 스프링을 공부하기 위해 구입했던 책은 토비의 스프링이었다. 하지만 초급자에게 토비의 스프링을 완독하기란 쉽지 않을 일이였기에, 간단하게 스프링을 공부하기 위해 책을 찾던중 스프링 퀵 스타트라는 책을 알게되었다. 이 책은 스프링을 잘 모르고 처음 공부하는 사람에게 아주 좋은 책인 것 같다. 너무 깊지도 얕지도 않은 많이 사용하는 기술들을 이해하기 쉽게 알려주는 책이다.

posted by 여성게
:

퍼사드패턴 (facade pattern)

어떤 서브시스템의 일련의 인터페이스에 대한 통합된 인터페이스를 제공한다.
퍼사드에서 고수준 인터페이스를 정의하기 때문에 서브시스템을 더 쉽게 사용할수 있다.

 

퍼사드(Facede)는 '건물의 앞쪽 정면(전면)'이라는 사전적인 뜻을 가진다. 퍼사드패턴은 위의 그림과 같은 구조로 이루어지는 디자인패턴이다. 간단히 위의 그림을 설명하면 몇 개의 복잡한 서브시스템들과 클라이언트 사이에 Facede라는 객체를 세워놓음으로써 복잡한 관계를 정리 혹은 구조화하는 패턴이다. 

 

예를 들면 영화를 보기 위한 클라이언트가 있다. 조금은 억지스러운 예제이지만, 서브시스템으로는 Movie,Beverage라는 인터페이스와 이 인터페이스를 구현할 클래스가 있다.(영화를 보기 위해 음료를 구입하고 영화를 키고 음료를 다 마신다. 영화가 끝나면 영화를 끄고 다 먹은 음료컵을 버린다.) 물론 더 많은 서브시스템이 붙을 가능성도 있다. 퍼사드패턴을 적용하기 전에는 클라이언트가 영화를 보기위해서는 클라이언트 코드에 Movie,Beverage 등의 서브시스템을 의존하여 일일이 음료를 사고 영화를 보러 들어가고 등의 로직을 직접 호출해야한다.(만약 복잡한 서브시스템들이 더 많다면 더 많은 호출이 이루어질 것이다.) 그리고 하나의 기능을 수행하기 위해 여러개의 서브시스템들이 조합되어야 한다면 클라이언트는 여러개의 서비스를 직접 호출하여 로직을 만들어야 할 것이다.

 

그래서 퍼사드 패턴을 이용하여 모든 관계를 전면에 세워진 Facede 객체를 통해서만 이루어질 수 있게 단순한 인터페이스를 제공하는 것이다. 퍼사드 패턴을 이용하면 서브시스템 내부에서 작동하고 있는 많은 클래스들의 관계나 사용법을 의식하지 않고 퍼사드 객체에서 제공하는 단순화된 하나의 인터페이스만 사용하므로, 클래스 간의 의존 관계가 줄어들고 복잡성 또한 낮아지는 효과를 볼 수 있다.

 

여기서 퍼사드 객체는 클라이언트의 요청이 발생했을 때, 서브시스템 내의 특정한 객체에 요청을 전달하는 역할을 한다. 이 역할을 수행하려면 퍼사드 객체는 서브시스템의 클래스들에 어떤 기능이 있는지 알고 있어야 한다.(인스턴스 필드에 선언) 즉, 서브시스템은 자신의 기능을 구현하고 있으면 되고, 퍼사드 객체로부터 자신이 수행할 행위에 대한 요청을 받아 처리하면 되는 것이다. 또한 서브시스템은 퍼사드 객체의 어느 정보도 알고 있을 필요가 없다.

 

Facede Pattern의 장점

  • 퍼사드는 소프트웨어 라이브러리를 쉽게 사용할 수 있게 해준다. 또한 퍼사드는 소프트웨어 라이브러리를 쉽게 이해할 수 있게 해 준다. 퍼사드는 공통적인 작업에 대해 간편한 메소드들을 제공해준다.
  • 퍼사드는 라이브러리를 사용하는 코드들을 좀 더 읽기 쉽게 해준다.
  • 퍼사드는 라이브러리 바깥쪽의 코드가 라이브러리의 안쪽 코드에 의존하는 일을 감소시켜준다. 대부분의 바깥쪽의 코드가 퍼사드를 이용하기 때문에 시스템을 개발하는 데 있어 유연성이 향상된다.
  • 퍼사드는 좋게 작성되지 않은 API의 집합을 하나의 좋게 작성된 API로 감싸준다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
/**
 * 음료관련 서브시스템
 * @author yun-yeoseong
 *
 */
public interface BeverageService {
    
    public void prepare();
    public void takeout();
    public void gotoTrash();
    
}
 
public class BeverageServiceImpl implements BeverageService {
 
    @Override
    public void prepare() {
        System.out.println("음료준비");
    }
 
    @Override
    public void takeout() {
        System.out.println("음료주기");
    }
 
    @Override
    public void gotoTrash() {
        System.out.println("다먹은 음료컵 버리기");
    }
 
}
 
/**
 * 영화관련 서브 시스템
 * @author yun-yeoseong
 *
 */
public interface MovieService {
    
    public void prepare();
    public void turnOn();
    public void turnOff();
}
 
public class MovieServiceImpl implements MovieService {
 
    @Override
    public void prepare() {
        System.out.println("영화준비");
    }
 
    @Override
    public void turnOn() {
        System.out.println("영화틀기");
    }
 
    @Override
    public void turnOff() {
        System.out.println("영화끄기");
    }
 
}
 
 
/**
 * 영화,음료 시스템의 인터페이스를 하나의 인터페이스로
 * 몰아서 더 쉽게 서브 시스템들을 사용할 수 있게한다.
 * @author yun-yeoseong
 *
 */
public interface MovieFacede {
    
    public void prepareWatchMovie();
    public void watchMovie();
    public void endMovie();
    
}
 
public class MovieFacedeImpl implements MovieFacede {
    
    private MovieService movieService;
    private BeverageService beverageService;
    
    public MovieFacedeImpl(MovieService movieService,BeverageService beverageService) {
        
        this.movieService=movieService;
        this.beverageService=beverageService;
        
    }
    
    @Override
    public void prepareWatchMovie() {
        
        beverageService.prepare();
        beverageService.takeout();
        movieService.prepare();
        
    }
 
    @Override
    public void watchMovie() {
        
        movieService.turnOn();
        
    }
 
    @Override
    public void endMovie() {
        
        movieService.turnOff();
        beverageService.gotoTrash();
        
    }
 
}
cs

 

만약 Spring fw을 사용한다면 퍼사드 객체에 DI해주는 어노테이션을 붙이면 클라이언트에서 직접 의존 주입해줄 필요가 없어진다. 예제가 조금 억지스러운 면이 없지않지만 대략적으로 퍼사드패턴을 사용하는 이유와 사용법이 전달되었으면 좋겠다.

posted by 여성게
: