프로그래밍언어/Kotlin 2020. 9. 19. 14:47

오늘 다루어볼 내용은 코틀린의 with와 apply 함수이다. 바로 예제로 들어간다.

 

with 함수
fun alphabet(): String {
    val result = StringBuilder()
    for (letter in 'A'..'Z') {
        result.append(letter)
    }
    result.append("\nNow I Know the alphabet!")
    return result.toString()
}

 

위 코드는 알파벳을 출력해주는 함수이다. 뭔가 StringBuilder를 생성하여 특정 변수에 담고, 해당 변수를 사용해 함수를 호출하고 뭔가 군더더기가 많이 붙어있는 느낌이다. 해당 코드를 with 함수를 통해 리팩토링이 가능하다.

 

fun alphabet() = with(StringBuilder()) {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I Know the alphabet!")
    toString()
}

 

코드가 뭔가 훨씬 깔끔해졌다. 눈에 띄는 것이 with 함수이다. 간단하게 with 함수는 하나의 인자를 받는 함수 같지만, 실제로는 2개의 인자를 받는 함수이다.

 

  • 첫번째 인자 : 수신객체
  • 두번째 인자: 첫번째에 받은 인자가 수신 객체인 람다

 

람다에서는 첫번째 인자로 받은 StringBuilder의 인스턴스가 수신객체가 되기때문에 람다안에서 마치 StringBuilder의 내부 함수처럼 사용이 가능하다.(this 키워드로 참조가 가능하지만, 겹치는 함수 이름이 없어 this를 생략하였다.) 그리고 해당 with 함수의 반환값은 람다의 반환값이 된다. with 함수의 내부를 살펴보자.

 

@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}

 

우선 with 함수는 T 타입을 인자로 받고 R타입을 리턴하는 함수이다. 내부 코드를 보면 조금 특이하다. 두번째 인자로 받은 람다가 마치 첫번째로 받은 인자 타입이 수신객체인 확장함수처럼 동작을 하고 있는 것이다. 이런 with 함수를 이용하면 특정 상황에서 훨씬 깔끔한 코드가 나올 수 있다.

 

apply함수

with 함수는 람다의 반환값이 with의 반환값이 된다. 하지만, 람다는 람다대로 수행하고 실제 수신 객체가 함수의 리턴값이 되길 바랄때가 있는데 이럴때 apply 함수를 이용한다.

 

fun alphabetUsingApply() = StringBuilder().apply {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I Know the alphabet!")
}.toString()

 

위 예제는 with 예제와는 다르게, 우선 람다대로 수행하고 리턴값이 수신 객체의 인스턴스 자기 자신이 되는 것이다.

 

  • with : 람다의 마지막 식(결과)가 with함수의 반환값이 된다. 즉, with의 두번째 인자 람다의 반환은 특정 객체를 리턴해야하는 것이다.
  • apply : 람다는 람다대로 수행하고, 수신 객체 자기자신을 반환한다. 즉, apply의 인자인 람다의 반환은 Unit(void)이다.

 

apply의 함수 내부를 살펴보자.

 

@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

 

apply 함수 내부를 보면, apply를 호출한 인스턴스 타입이 수신 객체인 확장 함수 람다를 인자로 받는다. 해당 람다의 반환은 Unit이다. 실제 코드 내부에서는 해당 람다를 수행하고 자기자신(this)를 리턴하고 있다. 실제 apply를 활용하여 코틀린이 구현한 유용한 함수의 예제로 아래와 같다.

 

/**
 * buildString은 StringBuilder를 만들어주는 것과 toString을 호출하는것을 대신해준다.
 */
fun alphabetUsingBuildString() = buildString {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I Know the alphabet!")
}

@kotlin.internal.InlineOnly
public inline fun buildString(builderAction: StringBuilder.() -> Unit): String =
    StringBuilder().apply(builderAction).toString()

 

여기까지 코틀린의 with와 apply에 대해 간단히 다루어보았다.

posted by 여성게
:
프로그래밍언어/Kotlin 2020. 9. 17. 10:09

 

이전까지 간단하게 코틀린에 대한 간략한 문법들을 다루어봤는데, 이번 포스팅은 코틀린의 클래스, 객체, 인터페이스에 대해 다루어본다.

 

인터페이스

자바의 인터페이스와 크게 다르지 않다.

 

interface SampleInterface1 {
    val property: String
    fun method1()
    fun method3() = println("method2")
}

interface SampleInterface2 {
    fun method2()
    fun method3() = println("method2")
}

class SampleImpl(): SampleInterface1, SampleInterface2 {
    override val property: String
        get() = TODO("Not yet implemented")

    override fun method1() {
        TODO("Not yet implemented")
    }

    override fun method3() {
        super<SampleInterface2>.method3()
    }

    override fun method2() {
        TODO("Not yet implemented")
    }
}

 

자바처럼 여러 인터페이스를 한 클래스에서 구현할 수도 있다. 또한 자바의 default 메서드처럼 코틀린에서는 그냥 함수 구현체를 정의하면 그게 default 함수가 된다. 하지만 유의할점은, 구현하고 있는 인터페이스 두개에서 같은 이름의 함수가 있다면, 구현하고 있는 하위 함수에서 오버라이드 해주어야한다.(혹은 특정 상위 인터페이스의 구현을 사용한다고 명시할 수도 있다.) 

 

자바는 @Override 어노테이션을 사용하지만 코틀린은 override 키워드를 사용한다. 마지막으로 자바와는 다르게 코틀린은 인터페이스에서 프로퍼티를 선언할 수 있다. 만약 인터페이스에 프로퍼티가 선언되어 있다면, 구현체에서 Getter를 오버라이드 해주면 된다.

 

코틀린의 열린 클래스(클래스상속)

자바에서는 기본적으로 클래스를 상속하면 다른 클래스에서 상속을 받을 수 있는 구조이다(public, not final) 하지만, 코틀린은 기본적으로 클래스가 모두 final 이기때문에 상속을 받을 수 없다. 하지만, 일반 클래스도 상속을 허용하기 위해 열린 클래스를 정의할 수 있다.

 

open class SampleOpen(val value: String): SampleInterface1 {
    override val property: String
        get() = TODO("Not yet implemented")

    fun disable() {}
    open fun animate() {}
    override fun method1() {
        TODO("Not yet implemented")
    }
}

class SampleOpenImpl(val key: String, value: String): SampleOpen(value) {
    override fun animate() {
        super.animate()
    }
}

 

open이라는 키워드를 붙여 상속이 가능한 형태의 클래스를 만들 수 있다. 하지만 자바와는 조금 문법이 다르게 하위 클래스에서 상위 클래스의 생성자 인자만큼 괄호안에 프로퍼티를 넣어줘야하는데, 그 프로퍼티를 생성자 시점에 넘겨줘야한다.(자바와 비슷) 하지만 자바와는 다르게 하위클래스의 생성자 프로퍼티 명을 그대로 상위 클래스 생성자 아규먼트로 넣어주면 된다. 또한 open class는 구현체 함수도 들어갈 수 있고, 상속을 강제하기 위해 open 키워드를 붙인 함수를 정의할 수 있다.

 

확장함수의 접근제한자

코틀린에서는 확장함수라는 아주 편한 문법이 존재한다. 하지만 하나 유의해야할 점이 확장함수를 정의할때 수신 객체 타입의 접근제한자의 레벨과 같거나 낮아야한다. 그말은 코틀린의 internal(같은 모듈에서만 사용가능)으로 수신객체의 접근제한자로 정의해놓았다면, 확장 함수는 반드시 internal 제한과 같거나 낮아야한다.(public으로는 internal 수신 객체의 확장함수를 정의할 수 없다.)

 

internal class SampleInternal {
    
}

internal fun SampleInternal.expendFunction() {} -> ok
fun SampleInternal.expendFunction() {} -> 컴파일 에러

 

sealed 클래스

아래와 같은 코드가 있다고 가정하자.

 

interface InterfaceExam {}
class InterfaceImpl1: InterfaceExam
class InterfaceImpl2: InterfaceExam
class InterfaceImpl3: InterfaceExam

fun whenExam(obj: InterfaceExam) = when (obj) {
    is InterfaceImpl1 -> println("interfaceImpl1")
    is InterfaceImpl2 -> println("interfaceImpl2")
    is InterfaceImpl3 -> println("interfaceImpl3")
    else -> println("els")
}

 

when 식에서 인터페이스 구현체의 타입을 분기하는 로직인데, 실제로 모든 구현체를 다 명시했음에도 else 구문이 들어가야한다. 하지만 코틀린에서 sealed 타입을 구현함으로써 else문을 명시하지 않아도된다.(sealed로 상위 타입을 명시함으로써 when 식에 모든 타입이 들어왔으니 else 구문이 오지 않을 것이라라고 강제할 수 있어 else 구문이 필요없다.)

 

sealed class SampleSealed {
    open fun method() {}
}

class SampleSealedImpl1: SampleSealed() {
    override fun method() {
        super.method()
    }
}
class SampleSealedImpl2: SampleSealed() {
    override fun method() {
        super.method()
    }
}

class SampleSealedImpl3: SampleSealed() {
    override fun method() {
        super.method()
    }
}

 

클래스 생성자

코틀린에서 주 생성자는 자바보다 간결하게 정의가능하다.

 

class User1(val name: String)

#java
public final class User1 {
	private final String name;
    
    public User1(String name) {
    	this.name = name
    }
}

 

위 정의는 모든 필드가 final이고, 그 필드를 모든 필드를 인자로 받는 생성자로 초기화하는 것과 같은 것이다. 위와 같은 생성자는 코틀린에서 펼친 표현으로는 아래와 같다.

 

class User2 constructor(name: String){
    val name: String
    init {
        this.name = name
    }
}

class User3(name: String){
    val name: String = name
}

 

복잡한 초기화 로직을 적용해야하는 클래스는 주 생성자말고 부 생성자를 여러개 정의할 수 있다.

 

class Secretive private constructor() {}

open class View {
    constructor(title: String): this(title, "")
    constructor(title: String, content: String) {}
}

class MyButton: View {
    constructor(title: String): super(title){

    }

    constructor(title: String, content: String): super(title, content) {

    }
}

 

인터페이스 프로퍼티 오버라이드

코틀린의 인터페이스에서는 프로퍼티를 선언할 수 있는데, 그 예로 아래와 같다.

 

interface Book {
    val name: String
}

class EBook1(override val name: String): Book
class EBook2: Book {
    override val name: String
        get() = TODO("Not yet implemented")
}
class EBook3(val category: String): Book {
    override val name = getBookName(category)
    private fun getBookName(category: String) = "ebook"
}

 

데이터 클래스

자바의 레코드 클래스와 비슷하게 코틀린에는 데이터 클래스가 있다. 데이터 클래스는 코틀린에서 필요한 함수를 모두 숨겨서 구현해주고 있다.( equal(), hashCode(), toString() , copy())

 

data class BookDto(val name: String, val price: Int) {}

 

위임클래스

코틀린에서는 delegate pattern을 아주 쉽게 구현할 수 있는 수단이 존재한다. 만약 자바에서 HashMap을 상속하여 커스텀한 로직을 넣은 클래스를 만든다면 여러 불필요한 코드가 많고, 실제 상위 클래스의 메서드에 의존해야한다라는 단점이 있는데, 코틀린은 이를 아주 우하하게 해결해주었다.

 

/**
 * 자바와 달리 데코레이터 패턴을 아주 쉽게 구현할 수 있다.
 * 보통 특정 클래스를 상속할때 상위 클래스의 기능이 변경되면, 하위 클래스의 동작에도 문제가 생기는데
 * 아래처럼 위임 관계를 만들어주면 실제 상위 클래스의 구현에 의존하지 않고, 위임하기때문에 상위클래스 변경에
 * 안전하다.
 */
class DelegatingCollection<T>(
        private val innerList: Collection<T> = ArrayList<T>()
): Collection<T> by innerList {

}

class BookNamePriceMap(
        private val map: Map<String, Int> = hashMapOf()
): Map<String, Int> by map {

}

 

만약 오버라이드해야하는 함수가 있다면 오버라이드하면 되고, 나머지는 "by map"이라는 키워드로 인해 자동으로 상위 클래스의 함수로 위임해준다. 

 

객체의 선언과 생성을 동시에 해주는 Object

코틀린에는 자바와는 다르게 object라는 타입이 존재한다. 간단하게는 싱글톤을 만들어주고, 마치 static한 유틸클래스처럼 이용가능한 타입이다.

 

object Payroll {
    const val companyName = "company"
    init {
    }
}

 

선언과 생성이 동시에 이루어지기 때문에, 상태값을 담은 프로퍼티 선언을 하지 않고 보통은 상수정도의 프로퍼티를 갖는다. object 타입에 함수를 정의하면 Payroll.method() 처럼 정적인 클래스처럼 이용가능하다.

 

동반객체(companion object)

특정 클래스의 생성을 담당할 팩토리 클래스 등을 만들때 아주 좋은 코틀린의 기능이 있다. 또한 마치 확장함수와 같은 기능을 제공하기도 하는데 아래 예제를 살펴보자.

 

class OuterClass private constructor(val name: String) {
    //내부 객체선언이 동반 객체선언이라면, 내부 객체 선언의 private 멤버 혹은 함수 접근이 가능하다.
    private fun method2() = method()
    private fun method3() = value
    
    companion object OuterClassFactory {
        private const val value = ""
        
        fun newOuterClass() = OuterClass("outerName")
        
        private fun method() = println("")
        
        fun companionMethod() = println("")
    }
}

OuterClass.newOuterClass()
OuterClass.companionMethod()

 

자바와는 달리 코틀린의 내부클래스는 외부 클래스를 참조하지 않는다. 또한 companion 키워드를 넣어주면, 바깥 클래스에서 내부 객체의 private한 멤버도 참조가능하다. 그렇다면 그냥 object로 선언된 내부 객체는 어떠할까

 

class OuterClass private constructor(val name: String) {
    private fun method() = println("private method")
    //내부 객체선언이 일반 객체선언이라면, 내부 객체 선언의 private 멤버 혹은 함수 접근이 안된다.
    private fun method2() = OuterClassFactory.method() //컴파일에러
    
    object OuterClassFactory {
        fun newOuterClass() = OuterClass("outerName")
        
        private fun method() = println("")
    }
}

 

일반 object 내부 객체로 정의되어있다면, 외부 클래스에서 내부 객체의 private 멤버에 접근할 수 없다. 이를 잘 활용하면 팩토리 클래스를 따로 분리할 필요없이 클래스 내부에 정의가 가능해지는 것이다. 또한 이와 비슷한 느낌의 로직도 한곳에 다모이게 되어 관리가 쉬워질 것이다.

 

여기까지 코틀린의 클래스, 인터페이스, 객체에 대해 간단히 다루어보았다.

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 여성게
:
인프라/Docker&Kubernetes 2020. 8. 30. 17:02

오늘 다루어볼 내용은 kustomize이다. Kustomize는 kustomization 파일을 이용해 kubernetes 오브젝트를 사용자가 원하는 대로 변경(customize)하는 도구이다.

 

모든 예제는 아래 깃헙 kube-kustomize 디렉토리에 있다.

 

yoonyeoseong/kubernetes-sample

Kubernetes(쿠버네티스) sample. Contribute to yoonyeoseong/kubernetes-sample development by creating an account on GitHub.

github.com

 

kustomization 파일을 포함하는 디렉터리 내의 리소스를 보거나 실제 클러스터에 리소스를 적용하려면 다음 명령어를 이용한다.

 

#kustomize가 적용된 설정파일 결과를 보여준다. 
> kubectl kustomize <kustomization_directory> 
#실제 kustomize 리소스를 클러스터에 적용한다. 
> kubectl apply -k <kustomization_directory>

 

Kustomize

Kustomize는 쿠버네티스 구성을 사용자 정의화하는 도구이다. 이는 애플리케이션 구성 파일을 관리하기 위해 다음 기능들을 가진다.

  • 다른 소스에서 리소스 생성
  • 리소스에 대한 교차 편집 필드 설정
  • 리소스 집합을 구성하고 사용자 정의

 

교차 편집 필드 설정

프로젝트 내 모든 쿠버네티스 리소스에 교차 편집 필드를 설정하는 것은 꽤나 일반적이다. 교차 편집 필드를 설정하는 몇 가지 사용 사례는 다음과 같다.

  • 모든 리소스에 동일한 네임스페이스를 설정
  • 동일한 네임 접두사 또는 접미사를 추가
  • 동일한 레이블들을 추가
  • 동일한 어노테이션들을 추가

 

 

yoonyeoseong/kubernetes-sample

Kubernetes(쿠버네티스) sample. Contribute to yoonyeoseong/kubernetes-sample development by creating an account on GitHub.

github.com

# deployment.yaml을 생성
cat <<EOF >./deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx
EOF

cat <<EOF >./kustomization.yaml
namespace: my-namespace
namePrefix: dev-
nameSuffix: "-001"
commonLabels:
  app: bingo
commonAnnotations:
  oncallPager: 800-555-1212
resources:
- deployment.yaml
EOF
> kubectl kustomize ./kube-kustomize/kustomize-upsert-field

apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    oncallPager: 800-555-1212
  labels:
    app: bingo
  name: dev-nginx-deployment-001
  namespace: my-namespace
spec:
  selector:
    matchLabels:
      app: bingo
  template:
    metadata:
      annotations:
        oncallPager: 800-555-1212
      labels:
        app: bingo
    spec:
      containers:
      - image: nginx
        name: nginx

 

구성(composition)

한 파일에 deployment, service 등을 정의하는 것은 일반적이다. kustomize는 서로 다른 리소스들을 하나의 파일로 구성할 수 있게 지원한다.

 

 

 

yoonyeoseong/kubernetes-sample

Kubernetes(쿠버네티스) sample. Contribute to yoonyeoseong/kubernetes-sample development by creating an account on GitHub.

github.com

 

# deployment.yaml 파일 생성
cat <<EOF > deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  selector:
    matchLabels:
      run: my-nginx
  replicas: 2
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - name: my-nginx
        image: nginx
        ports:
        - containerPort: 80
EOF

# service.yaml 파일 생성
cat <<EOF > service.yaml
apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  ports:
  - port: 80
    protocol: TCP
  selector:
    run: my-nginx
EOF

# 이들을 구성하는 kustomization.yaml 생성
cat <<EOF >./kustomization.yaml
resources:
- deployment.yaml
- service.yaml
EOF
> kubectl kustomize /kube-kustomize/kustomize-composition

apiVersion: v1
kind: Service
metadata:
  labels:
    run: my-nginx
  name: my-nginx
spec:
  ports:
    - port: 80
      protocol: TCP
  selector:
    run: my-nginx
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      run: my-nginx
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
        - image: nginx
          name: my-nginx
          ports:
            - containerPort: 80

 

 

사용자 정의(user patch define)

패치는 리소스에 다른 사용자 정의를 적용하는 데 사용할 수 있다. Kustomize는 patchesStrategicMerge와 patchesJson6902를 통해 서로 다른 패치 메커니즘을 지원한다. patchesStrategicMerge는 파일 경로들의 리스트이다. 각각의 파일은 patchesStrategicMerge로 분석될 수 있어야 한다. 패치 내부의 네임은 반드시 이미 읽혀진 리소스 네임(ex. deployment.yaml 안의 이름)과 일치해야 한다. 한 가지 일을 하는 작은 패치가 권장된다. 예를 들기 위해 디플로이먼트 레플리카 숫자를 증가시키는 하나의 패치와 메모리 상한을 설정하는 다른 패치를 생성한다.

 

 

 

yoonyeoseong/kubernetes-sample

Kubernetes(쿠버네티스) sample. Contribute to yoonyeoseong/kubernetes-sample development by creating an account on GitHub.

github.com

 

 

yoonyeoseong/kubernetes-sample

Kubernetes(쿠버네티스) sample. Contribute to yoonyeoseong/kubernetes-sample development by creating an account on GitHub.

github.com

 

# deployment.yaml 파일 생성
cat <<EOF > deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  selector:
    matchLabels:
      run: my-nginx
  replicas: 2
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - name: my-nginx
        image: nginx
        ports:
        - containerPort: 80
EOF

# increase_replicas.yaml 패치 생성
cat <<EOF > increase_replicas.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  replicas: 3
EOF

# 다른 패치로 set_memory.yaml 생성
cat <<EOF > set_memory.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  template:
    spec:
      containers:
      - name: my-nginx
        resources:
        limits:
          memory: 512Mi
EOF

cat <<EOF >./kustomization.yaml
resources:
- deployment.yaml
patchesStrategicMerge:
- increase_replicas.yaml
- set_memory.yaml
EOF
> kubectl kustomize /kube-kustomize/kustomize-patchesStrategicMerge

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      run: my-nginx
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
        - image: nginx
          name: my-nginx
          ports:
            - containerPort: 80
          resources:
            limits:
              memory: 512Mi

 

모든 리소스 또는 필드가 patchesStrategicMerge를 지원하는 것은 아니다. 임의의 리소스 내 임의의 필드의 수정을 지원하기 위해, Kustomize는 patchesJson6902를 통한 JSON 패치 적용을 제공한다. Json 패치의 정확한 리소스를 찾기 위해, 해당 리소스의 group, version, kind, name이 kustomization.yaml 내에 명시될 필요가 있다. 예를 들면, patchesJson6902를 통해 디플로이먼트의 리소스만 증가시킬 수 있다. 또한 patchesStrategicMerge, patchesJson6902를 같이 혼합해서 사용도 가능하다.

 

#deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  selector:
    matchLabels:
      run: my-nginx
  replicas: 2
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
        - name: my-nginx
          image: nginx
          ports:
            - containerPort: 80
          resources:
            limits:
              memory: 256Mi

#patch-replica.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  replicas: 3

#patch-resource.yaml
- op: replace
  path: /spec/template/spec/containers/0/resources/limits/memory
  value: 512Mi

#kustomization.yaml
resources:
  - deployment.yaml

patchesStrategicMerge:
  - patch-replica.yaml

patchesJson6902:
  - target:
      kind: Deployment
      name: my-nginx
      group: apps
      version: v1
    path: patch-resource.yaml
> kubectl kustomize /kube-kustomize/kustomize-patchesJson6902

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      run: my-nginx
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
        - image: nginx
          name: my-nginx
          ports:
            - containerPort: 80
          resources:
            limits:
              memory: 512Mi

 

patchesJson6902는 "replace"라는 오퍼레이션 말고, add, remove, move, copy, test라는 오퍼레이션도 존재한다.

patch images

patch 파일을 생성하지 않고, 컨테이너의 이미지를 재정의 할 수 있다.

 

 

yoonyeoseong/kubernetes-sample

Kubernetes(쿠버네티스) sample. Contribute to yoonyeoseong/kubernetes-sample development by creating an account on GitHub.

github.com

 

cat <<EOF > deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  selector:
    matchLabels:
      run: my-nginx
  replicas: 2
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - name: my-nginx
        image: nginx
        ports:
        - containerPort: 80
EOF

cat <<EOF >./kustomization.yaml
resources:
- deployment.yaml
images:
- name: nginx
  newName: my.image.registry/nginx
  newTag: 1.4.0
EOF
> kubectl kustomize /kube-kustomize/kustomize-patch-images

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      run: my-nginx
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
        - image: my.image.registry/nginx:1.4.0
          name: my-nginx
          ports:
            - containerPort: 80

 

Base&Overlay

Kustomize는 base와 overlay의 개념을 가지고 있다. base는 kustomization.yaml과 함께 사용되는 디렉터리다. 이는 사용자 정의와 관련된 리소스들의 집합을 포함한다. kustomization.yaml의 내부에 표시되는 base는 로컬 디렉터리이거나 원격 리포지터리의 디렉터리가 될 수 있다. overlay는 kustomization.yaml이 있는 디렉터리로 다른 kustomization 디렉터리들을 bases로 참조한다. base는 overlay에 대해서 알지 못하며 여러 overlay들에서 사용될 수 있다. 한 overlay는 다수의 base들을 가질 수 있고, base들에서 모든 리소스를 구성할 수 있으며, 이들의 위에 사용자 정의도 가질 수 있다.

 

 

 

yoonyeoseong/kubernetes-sample

Kubernetes(쿠버네티스) sample. Contribute to yoonyeoseong/kubernetes-sample development by creating an account on GitHub.

github.com

# base를 가지는 디렉터리 생성
mkdir base
# base/deployment.yaml 생성
cat <<EOF > base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  selector:
    matchLabels:
      run: my-nginx
  replicas: 2
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - name: my-nginx
        image: nginx
EOF

# base/service.yaml 파일 생성
cat <<EOF > base/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  ports:
  - port: 80
    protocol: TCP
  selector:
    run: my-nginx
EOF
# base/kustomization.yaml 생성
cat <<EOF > base/kustomization.yaml
resources:
- deployment.yaml
- service.yaml
EOF

 

이 base는 다수의 overlay에서 사용될 수 있다. 다른 namePrefix 또는 다른 교차 편집 필드들을 서로 다른 overlay에 추가할 수 있다. 다음 예제는 동일한 base를 사용하는 두 overlay들이다.

 

> mkdir dev

cat <<EOF > dev/kustomization.yaml
#구버전 base 불러오는 방법
bases:
  - ../base
#resources:
#- ../base/kustomization.yaml

namespace: dev-my-nginx

patchesStrategicMerge:
  - patch-replica.yaml

patchesJson6902:
  - target:
      kind: Deployment
      name: my-nginx
      group: apps
      version: v1
    path: patch-resource.yaml

images:
  - name: nginx
    newName: my.image.registry/nginx
    newTag: 1.4.0
EOF

mkdir prod
cat <<EOF > prod/kustomization.yaml
#구버전 base 불러오는 방법
bases:
  - ../base
#resources:
#- ../base/kustomization.yaml

namespace: prod-my-nginx

patchesStrategicMerge:
  - patch-replica.yaml

patchesJson6902:
  - target:
      kind: Deployment
      name: my-nginx
      group: apps
      version: v1
    path: patch-resource.yaml

images:
  - name: nginx
    newName: my.image.registry/nginx
    newTag: 1.4.0
EOF

 

추가적으로 patch 파일들을 몇가지 작성하였다.

 

> cd dev
> kubectl kustomize ./

#dev
apiVersion: v1
kind: Service
metadata:
  labels:
    run: my-nginx
  name: my-nginx
  namespace: dev-my-nginx
spec:
  ports:
    - port: 80
      protocol: TCP
  selector:
    run: my-nginx
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
  namespace: dev-my-nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      run: my-nginx
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
        - image: my.image.registry/nginx:1.4.0
          name: my-nginx
          ports:
            - containerPort: 80
          resources:
            limits:
              memory: 512Mi

> cd ../prod
> kubectl kustomize ./

#prod
apiVersion: v1
kind: Service
metadata:
  labels:
    run: my-nginx
  name: my-nginx
  namespace: prod-my-nginx
spec:
  ports:
    - port: 80
      protocol: TCP
  selector:
    run: my-nginx
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
  namespace: prod-my-nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      run: my-nginx
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
        - image: my.image.registry/nginx:1.4.0
          name: my-nginx
          ports:
            - containerPort: 80
          resources:
            limits:
              memory: 1024Mi

 

여기까지 쿠버네티스 설정 파일들을 관리하기 위한 방법으로 kustomize에 대해 간단히 다루어보었다.

posted by 여성게
:

 

오늘은 간단하게 클러스터 모니터링을 위한 API 몇개를 정리해본다.

 

  • http://es-host:9200/_cat/allocation?v -> 클러스터 디스크 현황
  • http://es-host:9200/_cluster/health?pretty -> 클러스터 헬스체크
  • http://es-host:9200/_cat/indices?v -> 인덱스 상태 확인
  • http://es-host:9200/_cat/shards -> 모든 샤드 상태 확인
  • http://es-host:9200/_cat/shards/{index_name}?v -> 특정 인덱스의 샤드 상태확인

 

https://brunch.co.kr/@alden/43

 

ElasticSearch status 바로 알기

ElasticSearch | 오늘은 ElasticSearch (이하 ES)의 status 에 대한 이야기를 해볼까 합니다. ES의 status는 무엇을 의미하는지, 그리고 어떤 값들이 있으며 어떻게 확인할 수 있는지 살펴보겠습니다. ES status ��

brunch.co.kr

 

posted by 여성게
:
인프라/Docker&Kubernetes 2020. 8. 24. 22:35

이번 포스팅에서는 쿠버네티스 로깅 파이프라인 구성에 대해 다루어볼 것이다. 저번 포스팅에서는 Fluentd + ES + Kibana 조합으로 클러스터 로깅 시스템을 구성했었는데, 이번 시간에는 Fluentd + kafka + ELK 조합으로 구성해본다.

<fluentd + ES + kibana logging>

 

 

Kubernetes - Kubernetes 로깅 운영(logging), Fluentd

오늘 다루어볼 내용은 쿠버네티스 환경에서의 로깅운영 방법이다. 지금까지는 쿠버네티스에 어떻게 팟을 띄우는지에 대해 집중했다면 오늘 포스팅 내용은 운영단계의 내용이 될 것 같다. 사실

coding-start.tistory.com

중간에 카프카를 두는 이유는 여러가지가 있을 수 있을 것 같다. 첫번째 버퍼역할을 하기때문에 어느정도 파이프라인의 속도 조절이 가능하다. 두번째 로그를 카프카 큐에 담아두고, 여러 컨슈머 그룹이 각기의 목적으로 로그데이터를 사용가능하다. 바로 실습에 들어가보자.

 

구성

 

 

구성은 위 그림과 같다. fluentd는 컨테이너 로그를 tail하고 있고, tail한 데이터를 카프카로 프로듀싱한다. 그리고 아웃풋으로 로그스태시로 보내고 로그 스태시는 엘라스틱서치에 색인을하게 된다.

 

실습이전에 본 실습에서 진행하는 예제중 카프카 구성과 엘라스틱서치의 구성은 별도로 옵션 튜닝 및 물리머신에 구성하는 것이 좋다. 필자는 구성의 편의를 위해 아무런 옵션을 튜닝하지 않은채 같은 쿠버네티스 클러스터에 카프카와 엘라스틱서치를 구성하였다.

 

kafka install & deploy on kubernetes unsing helm
 

TheOpenCloudEngine/uEngine-cloud-k8s

Contribute to TheOpenCloudEngine/uEngine-cloud-k8s development by creating an account on GitHub.

github.com

<헬름 설치>

> curl https://raw.githubusercontent.com/kubernetes/helm/master/scripts/get | bash
> kubectl --namespace kube-system create sa tiller
> kubectl create clusterrolebinding tiller --clusterrole cluster-admin --serviceaccount=kube-system:tiller
> helm init --service-account tiller
> helm repo update

위 명령어로 헬름을 다운로드 받는다.

 

<카프카 헬름 차트 설치 및 배포>

> kubectl create ns kafka
> helm repo add incubator http://storage.googleapis.com/kubernetes-charts-incubator
> helm install --name my-kafka --namespace kafka incubator/kafka

 

kafka라는 별도의 네임스페이스를 생성하여 그 안에 카프카를 배포하였다.

 

<헬름차트 삭제>

차트 삭제가 필요하면 아래 명령어를 이용하자.

# --purge 옵션으로 관련된 모든 정보를 지운다. 
helm delete my-kafka --purge

 

<fluentd가 데이터를 보낼 토픽생성>

> kubectl -n kafka exec my-kafka-0 -- /usr/bin/kafka-topics \
--zookeeper my-kafka-zookeeper:2181 --topic fluentd-container-logging \
--create --partitions 3 --replication-factor 3

Created topic "fluentd-container-logging".

 

"fluentd-container-logging"이라는 이름으로 토픽을 생성하였다.

 

<생성된 topic 확인>

> kubectl -n kafka exec my-kafka-0 -- /usr/bin/kafka-topics --zookeeper my-kafka-zookeeper:2181 --list

fluentd-container-logging

 

토픽리스트를 조회해서 우리가 생성한 토픽이 있는지 조회해본다.

 

<fluentd가 보낸 데이터가 큐로 잘들어오는지 확인하기 위해 컨슘머 실행>

> kubectl -n kafka exec -ti my-kafka-0 -- /usr/bin/kafka-console-consumer \
--bootstrap-server my-kafka:9092 --topic fluentd-container-logging --from-beginning

 

이제 실제로 카프카와 주키퍼가 쿠버네티스에 잘 떠있는지 확인해보자 !

 

> kubectl get pod,svc -n kafka
  NAME                       READY   STATUS    RESTARTS   AGE
  pod/my-kafka-0             1/1     Running   2          4m14s
  pod/my-kafka-1             1/1     Running   0          116s
  pod/my-kafka-2             1/1     Running   0          78s
  pod/my-kafka-zookeeper-0   1/1     Running   0          4m14s
  pod/my-kafka-zookeeper-1   1/1     Running   0          3m32s
  pod/my-kafka-zookeeper-2   1/1     Running   0          3m
  NAME                                  TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                      AGE
  service/my-kafka                      ClusterIP   10.108.104.66   <none>        9092/TCP                     4m14s
  service/my-kafka-headless             ClusterIP   None            <none>        9092/TCP                     4m14s
  service/my-kafka-zookeeper            ClusterIP   10.97.205.63    <none>        2181/TCP                     4m14s
  service/my-kafka-zookeeper-headless   ClusterIP   None            <none>        2181/TCP,3888/TCP,2888/TCP   4m14s

 

위와 같이 팟과 서비스 목록이 보인다면 다음으로 넘어간다.

 

ELK Stack 구성

<elasticsearch 실행>

아래 deployment와 service 설정파일을 이용하여 쿠버네티스 위에 엘라스틱서치를 구성한다.

 

apiVersion: v1
kind: Service
metadata:
  name: elasticsearch
  namespace: elk-stack
spec:
  selector:
    app: elasticsearch
  ports:
    - port: 9200
      protocol: TCP
      targetPort: 9200
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: elasticsearch
  namespace: elk-stack
  labels:
    app: elasticsearch
spec:
  replicas: 1
  selector:
    matchLabels:
      app: elasticsearch
  template:
    metadata:
      labels:
        app: elasticsearch
    spec:
      containers:
      - name: elasticsearch
        image: elastic/elasticsearch:6.8.6
        ports:
        - containerPort: 9200
          name: http
        - containerPort: 9300
          name: tcp

 

위 설정 파일은 볼륨을 구성하지 않아서 일회성(테스트)로만 가능하다. 실제로 운영환경에서는 물리머신에 클러스터를 구성하던가, 혹은 쿠버네티스 볼륨을 붙여서 구성하자.

 

> kubectl apply -f ./kube-logging/fluentd-elasticsearch/elasticsearch.yaml
> kubectl get pod,svc -n elk-stack
  NAME                                 READY   STATUS    RESTARTS   AGE
  pod/elasticsearch-654c5b6b77-l8k2z   1/1     Running   0          50s
  NAME                    TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
  service/elasticsearch   ClusterIP   10.101.27.73   <none>        9200/TCP   50s

 

<kibana 실행>

키바나는 아래 설정파일을 예제로 구성하였다.

 

apiVersion: v1
kind: Service
metadata:
  name: kibana
  namespace: elk-stack
spec:
  selector:
    app: kibana
  ports:
  - protocol: TCP
    port: 5601
    targetPort: 5601
  type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: kibana
  namespace: elk-stack
  labels:
    app: kibana
spec:
  replicas: 1
  selector:
    matchLabels:
      app: kibana
  template:
    metadata:
      labels:
        app: kibana
    spec:
      containers:
      - name: kibana
        image: elastic/kibana:6.8.6
        ports:
        - containerPort: 5601
          name: http

 

위 설정중 조금 살펴봐야할 것은 서비스 타입을 NodePort로 준 점이다. 실제로 외부로 포트를 개방해 localhost로 접근 가능하다. 실제 운영환경에서는 ingress까지 구성하여 배포하자.

 

> kubectl apply -f ./kube-logging/fluentd-elasticsearch/kibana.yaml
> kubectl get pod,svc -n elk-stack | grep kibana
  NAME                                 READY   STATUS    RESTARTS   AGE
  pod/kibana-6d474df8c6-fsfc7          1/1     Running   0          24s
  NAME                                 READY   STATUS    RESTARTS   AGE
  service/kibana          NodePort    10.97.240.55   <none>        5601:30578/TCP   24s

 

http://localhost:30578로 접근해 키바나가 잘 떠있는지와 엘라스틱서치와 잘 연동되었는지 확인하자.

 

<logstash 실행>

로그스태시는 아래 예시 설정 파일로 구성하였다.

 

apiVersion: v1
kind: ConfigMap
metadata:
  name: logstash-configmap
  namespace: elk-stack
data:
  logstash.yml: |
    http.host: "127.0.0.1"
    path.config: /usr/share/logstash/pipeline
    pipeline.workers: 2
  logstash.conf: |
    # all input will come from filebeat, no local logs
    input {
      kafka {
        bootstrap_servers => "my-kafka.kafka.svc.cluster.local:9092"
        topics => "fluentd-container-logging"
        group_id => "fluentd-consumer-group"
        enable_auto_commit => "true"
        auto_offset_reset => "latest"
        consumer_threads => 4
        codec => "json"
      }
    }

    output {
        elasticsearch {
          hosts => ["http://elasticsearch.elk-stack.svc.cluster.local:9200"]
          manage_template => false
          index => "kubernetes-container-log-%{+YYYY-MM-dd}"
        }
    }
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: logstash-deployment
  namespace: elk-stack
spec:
  replicas: 1
  selector:
    matchLabels:
      app: logstash
  template:
    metadata:
      labels:
        app: logstash
    spec:
      containers:
        - name: logstash
          image: docker.elastic.co/logstash/logstash:5.6.0
          ports:
            - containerPort: 5044
          volumeMounts:
            - name: config-volume
              mountPath: /usr/share/logstash/config
            - name: logstash-pipeline-volume
              mountPath: /usr/share/logstash/pipeline
      volumes:
        - name: config-volume
          configMap:
            name: logstash-configmap
            items:
              - key: logstash.yml
                path: logstash.yml
        - name: logstash-pipeline-volume
          configMap:
            name: logstash-configmap
            items:
              - key: logstash.conf
                path: logstash.conf
---
apiVersion: v1
kind: Service
metadata:
  name: logstash-service
  namespace: elk-stack
spec:
  selector:
    app: logstash
  ports:
    - protocol: TCP
      port: 5044
      targetPort: 5044
  type: ClusterIP

 

설정에서 잘 살펴볼 것은 input과 output의 호스트 설정이다. 우리는 모든 모듈을 같은 클러스터에 설치할 것이기 때문에 쿠버네티스 내부 DNS를 사용하였다.(실습에 편의를 위한 것이기도 하지만, 실제 운영환경에서도 내부 시스템은 종종 클러스터 내부 DNS를 사용하기도 한다. 그러면 실제로 통신하기 위해 클러스터 밖으로 나갔다 오지 않는다.)

 

또 한가지 설정은 Deployment에 볼륨을 마운트 하는 부분이다. 실제 쿠버네티스에서 ConfigMap은 볼륨으로 잡히기 때문에 그 ConfigMap을 logstash pod 내부로 마운트하여 실행시점에 해당 설정파일을 물고 올라가도록 하였다.

 

> kubectl apply -f ./kube-logging/fluentd-elasticsearch/logstash.yaml
> kubectl get pod,svc -n elk-stack | grep logstash
  NAME                                       READY   STATUS    RESTARTS   AGE  
  pod/logstash-deployment-556cfb66b5-6xrs6   1/1     Running   0          34s
  service/logstash-service   ClusterIP   10.96.13.170   <none>        5044/TCP         33s

 

<fluentd 실행>

이제는 실제 컨테이너 로그를 tail하여 수집하는 fluentd를 실행시켜보자.

 

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd
  namespace: kube-system
  labels:
    app: fluentd-logging
    version: v1
    kubernetes.io/cluster-service: "true"
spec:
  selector:
    matchLabels:
      app: fluentd-logging
  template:
    metadata:
      labels:
        app: fluentd-logging
        version: v1
        kubernetes.io/cluster-service: "true"
    spec:
      tolerations:
        - key: node-role.kubernetes.io/master
          effect: NoSchedule
      containers:
        - name: fluentd
          image: 1223yys/fluentd-kafka:latest
          imagePullPolicy: Always
          env:
            - name: FLUENT_KAFKA_BROKERS
              value: "my-kafka.kafka.svc.cluster.local:9092"
            - name: FLUENT_KAFKA_DEFAULT_TOPIC
              value: "fluentd-container-logging"
            - name: FLUENT_KAFKA_OUTPUT_DATA_TYPE
              value: "json"
            - name: FLUENT_KAFKA_COMPRESSION_CODEC
              value: "snappy"
            - name: FLUENT_KAFKA_MAX_SEND_LIMIT_BYTES
              value: "4096"
          resources:
            limits:
              memory: 200Mi
            requests:
              cpu: 100m
              memory: 200Mi
          volumeMounts:
            - name: varlog
              mountPath: /var/log
            - name: varlibdockercontainers
              mountPath: /var/lib/docker/containers
              readOnly: true
      terminationGracePeriodSeconds: 30
      volumes:
        - name: varlog
          hostPath:
            path: /var/log
        - name: varlibdockercontainers
          hostPath:
            path: /var/lib/docker/containers

 

fluentd 설정파일은 몇가지 짚고 넘어갈 것들이 있다. 첫번째는 컨테이너를 tail하기 위해 마운트한 설정이다. /var/log, /var/lib/docker/container를 마운트하였다. 실제 호스트머신에 해당 디렉토리에 들어가면 파일이 보이지 않을 것이다. 만약 파일을 보고 싶다면 아래 설정을 통해 도커 컨테이너를 실행시키고 볼 수 있다.

 

> docker run -it --rm -v /var/lib/docker/containers:/json-log alpine ash

 

위 도커이미지를 실행한후 /json-log 디렉토리에 들어가면 호스트머신에 쌓인 컨테이너 로그들을 볼 수 있다.

 

두번째, tail한 로그를 내보내기 위한 env 설정이다. 아웃풋은 카프카로 두었고, 역시 도메인은 내부 클러스터 DNS로 잡아주었다. 그리고, 우리가 미리 생성한 토픽에 데이터를 보내고 있고 타입은 json으로 보내고 있다.(사실상 튜닝할 설정은 많지만 실습의 편의를 위해 대부분 기본 설정으로 잡았다.)

 

그리고 필자가 fluentd 이미지를 새로 빌드한 이유는 카프카로 보내는 로그 포맷을 수정하기 위하여 fluentd 설정파일들을 조금 수정하였기 때문이다. 혹시나 fluentd 설정 파일들이 궁금하다면 포스팅 마지막 Github을 참조하자.(https://github.com/yoonyeoseong/kubernetes-sample/tree/master/kube-logging/fluentd-kafka)

 

> kubectl apply -f ./kube-logging/fluentd-kafka/fluentd-kafka-daemonset.yaml
> kubectl get pod,daemonset -n kube-system | grep fluentd
  NAME                                         READY   STATUS    RESTARTS   AGE
  pod/fluentd-bqmnl                            1/1     Running   0          34s
  daemonset.extensions/fluentd      1         1         1       1            1           <none>                        34s

 

이제 로그 출력을 위해 샘플 앱을 실행시켜보자. 로그 출력을 위한 앱은 꼭 아래 필자가 빌드한 웹 어플리케이션을 실행시킬 필요는 없다. 만약 아래 애플리케이션을 실행시키려면 ingress 설정 혹은 service node port를 설정하자.

 

> kubectl apply -f ./kube-resource/deployment-sample.yaml
> kubectl get pod
  NAME                                 READY   STATUS    RESTARTS   AGE
  sample-deployment-5fbf569554-4pzrf   0/1     Running   0          17s

 

이제 요청을 보내보자.

 

> kubectl get svc -n ingress-nginx
  NAME                                 TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)                      AGE
  ingress-nginx-controller             NodePort    10.97.27.106   <none>        80:30431/TCP,443:31327/TCP   21d
  ingress-nginx-controller-admission   ClusterIP   10.96.76.113   <none>        443/TCP                      21d
> curl localhost:30431/api

 

이제 키바나에 접속해보면 앱에서 출력하고 있는 로그 데이터를 볼 수 있다. 모든 예제 설정 및 코드는 아래 깃헙을 참고하자 !

 

 

yoonyeoseong/kubernetes-sample

Kubernetes(쿠버네티스) sample. Contribute to yoonyeoseong/kubernetes-sample development by creating an account on GitHub.

github.com

 

posted by 여성게
:
인프라/Web Server & WAS 2020. 8. 22. 15:43

오늘 포스팅해볼 내용은 Web server 중 하나인 Nginx의 설치 및 사용방법에 대해 다루어본다. 우선 Nginx는 무엇인가 알아보자.

 

예제 설정은 아래 깃헙사이트에 있다.

 

yoonyeoseong/kubernetes-sample

Kubernetes(쿠버네티스) sample. Contribute to yoonyeoseong/kubernetes-sample development by creating an account on GitHub.

github.com

 

Wiki(https://ko.wikipedia.org/wiki/Nginx)
Nginx(엔진 x라 읽는다)는 웹 서버 소프트웨어로, 가벼움과 높은 성능을 목표로 한다. 웹 서버, 리버스 프록시 및 메일 프록시 기능을 가진다.
2017년 10월 기준으로 실질적으로 작동하는 웹 사이트(active site)들에서 쓰이는 웹 서버 소프트웨어 순위는 아파치(44.89%), 엔진엑스(20.65%), 구글 웹 서버(7.86%), 마이크로소프트 IIS(7.32%)순이다.[1] 이 조사에서 생성은 되어있으나 정상적으로 작동하지 않는 웹 사이트들은 배제되었으며[2] 특히 MS의 인터넷 정보 서비스(IIS)를 설치한 웹 사이트들의 상당수가 비활성 사이트였다. 그런 사이트들도 포함하면 MS IIS가 1위이다. 2017년 6월 현재 Nginx는 한국 전체 등록 도메인 중 24.73%가 사용하고 있다.[3]
Nginx는 요청에 응답하기 위해 비동기 이벤트 기반 구조를 가진다. 이것은 아파치 HTTP 서버의 스레드/프로세스 기반 구조를 가지는 것과는 대조적이다. 이러한 구조는 서버에 많은 부하가 생길 경우의 성능을 예측하기 쉽게 해준다.

 

또한 nginx는 하나의 마스터 프로세스와 여러 워커 프로세스가 있고, 마스터 프로세스는 주로 설정 파일을 읽고 적용하며 워커 프로세스들을 관리하는 역할을 하게 된다. 워커 프로세스는 실제 요청에 대한 처리를 하게 된다. nginx는 event driven 모델을 메커니즘으로 사용하여 실제 워커 프로세스간 요청을 효율적으로 분산한다.

 

실습은 Mac os 기준으로 실습을 진행해 볼것이다. 우선 nginx를 설치해보자.

 

Nginx install
> brew install nginx

 

brew로 설치를 아래와 같은 디렉터리들이 생성된다. 우선 아래 디렉토리를 실습을 진행하면서 전부 알아볼 것이다.

 

Docroot is: /usr/local/var/www

The default port has been set in /usr/local/etc/nginx/nginx.conf to 8080 so that
nginx can run without sudo.

nginx will load all files in /usr/local/etc/nginx/servers/.

To have launchd start nginx now and restart at login:
  brew services start nginx
Or, if you don't want/need a background service you can just run:
  nginx
==> Summary
🍺  /usr/local/Cellar/nginx/1.19.2: 25 files, 2.1MB
==> Caveats
==> nginx
Docroot is: /usr/local/var/www

The default port has been set in /usr/local/etc/nginx/nginx.conf to 8080 so that
nginx can run without sudo.

nginx will load all files in /usr/local/etc/nginx/servers/.

To have launchd start nginx now and restart at login:
  brew services start nginx
Or, if you don't want/need a background service you can just run:
  nginx

 

Nginx 구동 명령어(nginx -s <signal>
  • nginx : 서버시작
  • nginx -s stop : 서버종료(워커들이 요청을 처리중이더라도 그냥 종료한다.)
  • nginx -s quit : 워커 프로세스가 현재 요청 처리를 완료할 때까지 대기하고 모두 처리완료된 후에 서버 종료.
  • nginx -s reload : nginx config를 새로 로드한다. 마스터 프로세스가 설정을 다시 로드하라는 요청을 받으면 설정 유효성 검사후 새로운 워커 프로세스를 시작하고, 이전 워커 프로세스에게 종료 메시지를 보내게 되고 이전 워커 프로세스는 요청을 완료하게 되면 종료된다.

위 명령어로 nginx를 시작 해보자 !

 

> nginx
> lsof -i:8080
COMMAND   PID         USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
nginx   88891 yun-yeoseong    6u  IPv4 0x7370b7ed168f296f      0t0  TCP *:http-alt (LISTEN)
nginx   88892 yun-yeoseong    6u  IPv4 0x7370b7ed168f296f      0t0  TCP *:http-alt (LISTEN)
#실행중인 모든 nginx 프로세스 목록을 가져온다.
> ps -ax | grep nginx
88891 ??         0:00.00 nginx: master process nginx
88892 ??         0:00.01 nginx: worker process
89201 ttys000    0:00.03 vi nginx.conf
89695 ttys001    0:00.00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn nginx

 

디폴트 포트인 8080으로 nginx 프로세스가 잘 떠있다. 이제 웹브라우저에서 localhost:8080으로 접속해보자.

 

> curl localhost:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

 

 

브라우저에 welcome to nginx가 보인다면 설치 및 실행이 잘된 것이다 ! 어 그렇다면, 여기서 조금 의아한 것이 있을 것이다. 과연 저 html은 어디서 응답을 준것일까?

 

Docroot

답은 도큐먼트 루트에 있다. 설치를 하면 아래와 같은 로그가 출력되어있을 것인데, 해당 디렉토리 내에 html 파일이 존재한다.

 

Docroot is: /usr/local/var/www

 

기본적으로 웹서버는 다른 서버로 프록시 하지 않는 이상 uri로 명시한 path로 도큐먼트 루트 디렉토리를 찾아서 응답을 주게 된다. 사실 localhost:8080은 localhost:8080/index.html과 같다고 보면된다. 그렇다면 index.html의 위치를 바꾸면 어떻게 될까?

 

> cd /usr/local/var/www
> mkdir backup
> mv index.html ./backup

 

이제 아래 요청을 보내보자.

 

> curl localhost:8080/index.html
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.19.2</center>
</body>
</html>

 

우리는 index.html을 다른 디렉토리로 옮겼기 때문에 404 not found가 뜨게 된다. 그렇다면 옮긴 디렉토리 path를 명시해서 요청을 보내보자.

 

> curl http://localhost:8080/backup/index.html
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

 

응답이 잘 도착하였다. 보통 도큐멘트 루트는 어떻게 사용이 될까? 보통은 정적인 리소스 파일(css, html)을 위치시키게 된다. 그렇다면 정적인 리로스 파일을 위치시키는 이유는 무엇일까? 만약 WAS에 해당 정적인 리소스 파일을 위치시키게 되면, 사실상 서버 동작과 관련이 적은 정적 리소스를 가져오기 위한 요청도 모두 WAS로 들어가기 때문에 앱에 부하가 많이 가게 될수 있다. 그렇기 때문에 보통 정적인 리소스는 nginx(웹서버)에서 처리하고 WAS는 백엔드 데이터만 제공하게 하여 WAS의 부담을 줄여줄 수 있다.

 

이제는 본격적으로 Nginx의 설정을 커스터마이징해보자.

 

Configuration file's structure

nginx의 설정 파일은 simple directives(단순 지시문)과 block directives(블록 지시문)으로 나뉜다. 단순 지시문을 공백으로 구분 된 이름과 매개변수로 구성되며 세미콜론(;)으로 끝난다. 블록 지시문은 단순 지시문과 구조가 동일하지만 세미콜론 대신 중괄호({})로 명령 블록을 지정한다. 또한 블록지시문을 블록지시문의 중첩구조로도 이루어 질 수 있다. 이러한 지시문으로 nginx에 플러그인 된 여러 모듈을 제어하게 된다.

 

Nginx Configuration

nginx.conf 파일에는 nginx의 설정 내용이 들어간다. 해당 파일의 전체적인 구조(모듈)는 아래와 같이 이루어져있다.

 

user  nginx;
worker_processes  1;

error_log  logs/error.log;

events {
    worker_connections  1024;
}
http { 
    include       mime.types;
    #응답의 기본 default mime type을 지정
    default_type  application/octet-stream;
    
    charset utf-8;
    
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    '$status $body_bytes_sent "$http_referer" '
    '"$http_user_agent" "$http_x_forwarded_for"';
    
    access_log  /var/log/nginx/access.log  main;
    
	#지정된 에러 코드에 대해 응답나갈 document root의 html 파일을 지정
    #docroot의 html말고 다른 URL로 리다이렉션 가능하다.
    error_page 500 502 503 504 /50x.html;
    #error_page 500 502 503 504 http://example.com/error.html
    
    sendfile        on;
    tcp_nopush     on;
    
    keepalive_timeout  65;
    #keepalive로 유지되는 커넥션으로 최대 처리할 요청수를 지정
    #keepalive_requests 100;    
    
    #nginx의 버전을 숨길 것인가에 대한 옵션이다. 보안상 활성화하는 것을 권장한다.
    server_tokens            on;
    #응답 컨텐츠를 압축하는 옵션, 해당 옵션말고 gzip관련 다양한 옵션 존재(압축 사이즈 등등)
    gzip  on;
    
    #context : http, server, location
    #클라이언트 요청 본문을 읽기 위한 버퍼 크기를 설정 64bit platform default 16k
    client_body_buffer_size 16k;
    #클라이언트 요청 본문을 읽기 위한 타임아웃 시간 설정
    client_body_timeout 60s;
    #클라이언트 요청 헤더를 읽기위한 버퍼 크기 설정
    client_header_buffer_size 1k;
    client_header_timeout 60s;
    #클라이언트가 보낸 요청 본문의 최대 사이즈
    client_max_body_size 1m;
    
    server {
        listen       80;
        location / {
            root   html;
            index  index.html index.htm;
        }
    }
}

 

  • Core 모듈 설정 : 위 예제의 worker_processes와 같은 지시자 설정 파일 최상단에 위치하면서 nginx의 기본적인 동작 방식을 정의한다.
  • http 모듈 블록 : 밑에서 설명할 server, location의 루트 블록이라고 할 수 있고, 여기서 설정된 값을 하위 블록들은 상속한다. http 블록은 여러개를 사용할 수 있지만 관리상의 이슈로 한번만 정의하는 것을 권장한다. http, server, location 블록은 계층구조를 가지고 있고 많은 지시어가 각각의 블록에서 동시에 사용될 수 있는데,  http의 내용은 server의 기본값이 되고, server의 지시어는 location의 기본값이 된다. 그리고 하위의 블록에서 선언된 지시어는 상위의 선언을 무시하고 적용된다.
  • server 블록 : server 블록은 하나의 웹사이트를 선언하는데 사용된다. 가상 호스팅(vhost)의 개념이다.
  • location 블록 : location 블록은 server 블록 안에 정의하며 특정 URL을 처리하는 방법을 정의한다. 예를 들어 uri path마다 다르게 요청을 처리하고 싶을 때 해당 블록 내에 정의한다.
  • events 블록 : nginx는 event driven을 메커니즘으로 동작하는데, 이 event driven 동작 방식에 대한 설정을 다룬다.

 

nginx.conf

"user"

user의 값이 root로 되어 있다면 일반 계정으로 변경하는 것이 좋다. nginx는 마스터 프로세스와 워커 프로세스로 동작하고, 워커 프로세스가 실질적인 웹서버의 역할을 수행하는데 user 지시어는 워커프로세스의 권한을 지정한다. 만약 user의 값이 root로 되어 있다면 워커 프로세스를 root의 권한으로 동작하게 되고, 워커 프로세스를 악의적으로 사용자가 제어하게 된다면 해당 머신을 루트 사용자의 권한으로 원격제어하게 되는 셈이기 때문에 보안상 위험하다.

 

user 설정의 값으로는 대표성있는 이름(nginx)로 사용하고, 이 계정은 일반 유저의 권한으로 쉘에 접속할 수 없어야 안전하다.

 

> useradd --shell /sbin/nologin www-data

 

"worker_process"

worker_process는 워커 프로세스를 몇개 생성할 것인지를 지정하는 지시어이다. 이 값이 1이라면 모든 요청을 하나의 프로세스로 실행하겠다는 뜻인데, 여러개의 CPU 코어가 있는 시스템이라면 CPU 코어수만큼 지정하길 권장한다.

 

"events.worker_connections"

이 값은 몇개의 접속을 동시에 처리할 것인가를 지정하는 값이다. 이 값과 worker_process의 값을 조합해 동시에 최대로 처리할 수 있는 커넥션의 양을 산출할 수 있다.(worker_process*worker_connections)

 

"http.incloud"

가상 호스트 설정이나, 반복되는 설정들을 파일로 저장해놓고, incloude를 통해 불러올 수 있다.

 

"http.log_format"

access 로그에 남길 로그 포맷을 지정한다. 보통 어떠한 장애가 났을 때, 가장 먼저보는 것이 로그 파일이기 때문에 디버깅하기 위해 유용한 값들을 로그에 남겨두는 것이 중요하다. 특히나, 여러 프록시 서버를 지나오는 서버 구성인 경우에는 x-forwarded-ip 등을 지정하면 지나온 프록시들의 아이피들을 할 수 있다.

 

"http.access_log"

access로그를 어느 디렉토리에 남길지 설정한다.

 

"http.keepalive_timeout"

소켓을 끊지 않고 얼마나 유지할지에 대한 설정이다. 자세한 내용은 keepalive 개념을 확인하자.

 

"http.server_tokens"

nginx의 버전을 숨길 것인가에 대한 옵션이다. 보안상 활성화하는 것을 권장한다.

 

기타 설정들은 위 예제 파일에 주석으로 달아놓았다.

 

다음은 실제 프록시 설정이 들어가는 server 블록 설정을 다루어 보자.

 

server {
    listen 80;
    server_name levi.local.com;
    access_log  logs/access.log;
    error_log   logs/error.log;
    error_page  500 502 503 504 /50x.html;
    charset     utf-8;
    
    location / {
    	proxy_pass  http://app;
    }
}

upstream app {
	server localhost:8080;
}

 

위 설정은 http 블록 하위로 들어가게 된다. 크게 어려운 설정은 없고, "levi.local.com:80/"으로 요청이 들어오면 upstream(요청받는 서버)으로 요청을 리버스 프록시 한다라는 뜻이다. 실제로 앱하나를 띄워보고 프록시 되는지 확인해보자. 

 

> curl levi.local.com/api
new api ! - 7

 

위처럼 응답이 잘오는 것을 볼 수 있다. 그런데 사실 server 블록이 하나일때는 server_name에 적혀있는 도메인으로 오지않아도 응답을 준다. server_name이 진짜 도메인네임을 구분하기 위한 server_name으로 사용되기 위해서는 listen 포트가 같은 server 블록이 두개 이상 존재할때 이다. 아래 예제를 보자.

 

    server {
        listen 80;
        server_name levi.local.com;
        #access_log  logs/access.log;
        #error_log   logs/error.log;
        error_page  500 502 503 504 /50x.html;
        charset     utf-8;

        location / {
            proxy_pass  http://app;
        }
    }

    upstream app {
        server localhost:8080;
    }

    server {
        listen 80;
        server_name local.yoon.com;
        #access_log  logs/access.log;
        #error_log   logs/error.log;
        error_page  500 502 503 504 /50x.html;
        charset     utf-8;

        location / {
            proxy_pass  http://app2;
        }
    }

    upstream app2 {
        server localhost:7070;
    }

 

위와 같이 설정하고, 각 도메인을 분리해서 요청을 보내보자. server_name으로 분리되어 요청이 프록시 될것이다.

 

Nginx cache

마지막으로 location 블록에 대한 설정중 nginx cache에 설정에 대해 주로 다루어보자.

 

 

  • /path/to/cache ==> 캐시 내용이 local disk 에 저장될 위치
  • levels=1:2 ==> directory depth 와 사용할 name 길이.
    • ex ) /data/nginx/cache/c/29/b7f54b2df7773722d382f4809d65029c
  • keys_zone ==> 캐시 키로 사용될 이름과 크기. 1MB 는 약 8천개의 이름 저장. 10MB면 8만개.
  • max_size ==> 캐시 파일 크기의 maximum. size 가 over 되면 가장 오래전에 사용한 데이터 부터 삭제한다.
  • inactive ==> access 되지 않았을 경우 얼마 뒤에 삭제 할 것인가.
  • use_temp_path ==> 설정된 path 외에 임시 저장 폴더를 따로 사용할 것인가? 따로 설정하지 않는 것이 좋다.
  • proxy_cache <namev> ==> 캐시로 사용할 메모리 zone 이름.
  • proxy_cache_methods ==> request method를 정의한다. default : GET, HEAD
  • proxy_cache_key ==> 캐시 할 때 사용할 이름.
  • proxy_cache_bypass ==> 예를 들어 "http://www.example.com/?nocache=true" 이러한 요청이 왔을 때 캐싱되지 않은 response 를 보낸다. 이 설정이 없다면 nocache 아규먼트는 동작하지 않는다. http_pragma==> 헤더 Pragma:no-cache
  • proxy_cache_lock ==> 활성화 시키면 한 번에 단 하나의 요청만 proxy server로 전달되어 proxy_cache_key 에 따라 캐싱된 데이터로 사용합니다. 다른 request 들은 캐싱된 데이터를 사용하거나 proxy_cache_lock_timeout의 설정에 따라 proxy server로 전달 될 수 있습니다.
  • proxy_cache_valid ==> 기본적으로 캐싱할 response code 와 시간을 정의한다.

 

 

예제 설정으로는 아래와 같다.

 

proxy_cache_path /usr/local/etc/nginx/cache levels=1:2 keys_zone=myapp:10m max_size=10g inactive=60s use_temp_path=off;

server {
    listen 80;
    server_name levi.local.com;
    access_log  logs/access.log;
    error_log   logs/error.log;
    error_page  500 502 503 504 /50x.html;
    charset     utf-8;
    
    location / {
        proxy_cache myapp;
        proxy_cache_methods GET;
        proxy_cache_key "$uri$is_args$args";
        proxy_cache_bypass $cookie_nocache $arg_nocache $http_pragma;
        proxy_ignore_headers Expires Cache-Control Set-Cookie;
        #proxy_cache_lock on;
        #200ok인 응답을 1분동안 캐싱
        proxy_cache_valid 200 1m; 
        
        proxy_pass  http://app;
    }
}

upstream app {
	server localhost:8080;
}

 

실제로 캐싱이 잘되는지 요청을 보내보고 실제 캐싱이 저장되는 디렉토리로 들어가보자.

 

> cd /usr/local/etc/nginx/cache
> ls
8
> cd 8
> ls
68
> cd 68
> ls
5d198634e5fa00f3cf3a478fcdf57688
> vi 5d198634e5fa00f3cf3a478fcdf57688
^E^@^@^@^@^@^@^@û½@_^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@ÿÿÿÿÿÿÿÿ¿½@_^@^@^@^@#Y|^V^@^@d^Aè^A^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@
KEY: /api?arg=args
HTTP/1.1 200 ^M
Content-Type: text/html;charset=UTF-8^M
Content-Length: 13^M
Date: Sat, 22 Aug 2020 06:39:59 GMT^M
Connection: close^M
^M
new api ! - 5

 

응답이 잘 캐싱된것을 볼수 있다. 그리고 대략 1분후에는 해당 캐싱 파일 지워져있다.

 

여기까지 간단하게 Nginx 설치 및 사용방법에 대해 다루어보았다. 맘 같아선 캐싱에 대해 더 자세히 다루고 싶었다. 대규모 웹사이트 같은 경우는 정말 장비를 늘리는 것으로는 트래픽을 받는데 한계가 있기 때문에 사실상 캐싱 싸움이 될것이기 때문이다. 이번 포스팅에서는 Nginx에 대해 맛보기 정도만 하였지만, 다음 시간에는 조금더 딥한 내용까지 다루어 볼 계획이다.

 

참조

 

nginx cache

1. cache dir 설정 proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=cache:2m 2. cache 사용 설정 server { listen  80; server_name cached.test.co.kr; access_log /var/log/nginx/cache-access.log c..

semode.tistory.com

 

posted by 여성게
: