왓챠 과제전형을 진행하면서, 코루틴을 사용하는 도중에 lifecycle 과 coroutine 동작에 대한 학습이 부족함을 느꼈습니다.

그래서 다시한번 Coroutine 에 대해 정리를 하고자 합니다.

 

코루틴은 코틀린때문에 등장한 것이 아니라고 합니다.

코루틴의 개념은 훨씬 이전부터 존재해왔으며 (언제부터일까... 그것까진 안찾아봤어요.. )

오히려 저같은 후세대 개발자들은 안드로이드에서 코틀린을 공식언어로 적용하면서 만났을 것이라고 생각합니다.

 

 

1. MainRoutine 과 SubRoutine

코루틴에 대해서 알기 전에, 메인루틴과 서브루틴에 대해서 알아야 합니다.

메인루틴은 흔히, 메인메소드라고 생각하면 됩니다.

서브루틴은 메인함수 내에서 동작하는 메소드라고 생각하시면 됩니다.

 

    private fun mainRoutine() {
        val nameA = subRoutine1()
        val nameB = subRoutine2()
        val nameC = subRoutine3()
    }

    private fun subRoutine1() : String {
        return "subRoutine1"
    }
    private fun subRoutine2() : String {
        return "subRoutine1"
    }
    private fun subRoutine3() : String {
        return "subRoutine1"
    }

위 코드는 mainRoutine() 에서 순차적으로 subRoutine1() -> subRoutine2() -> subRoutine3()

으로 동작합니다.

 

순차적으로 동작하는 만큼, subRoutine1() 에서 while(true) 에 break; 가 없다면, subRoutine2() 과 subRoutine3()은 

절대 동작할리가 없겠죠? 만약 while 탈출 조건이 있다고 해도, 탈출하기 전까지는 다음 코드가 실행되지 않습니다.

 

이게 왜 중요한가???? 

Thread 동작

하나의 쓰레드 내부코드는 위에서 설명한 것처럼 모두 순차적으로 진행됩니다.

멀티쓰레드는 내부코드가 여러개로 나뉘는 것이 아닌, 순차적으로 진행되는 각 쓰레드를 여러개 두고 돌아가며 실행시키는 것이라고 보면 됩니다.

 

 

Routine 이 무엇인지 알았나요?

MainRoutine 내부에 SubRoutin, 그 안에 SubRoutine, 그 안에 SubRoutine ... 이렇게 존재한다고 하면,

결국 마지막 SubRoutine 의 코드부터 끝이 나야 그 다음 Soutine 들이 종료가 가능한거죠.. 음?.. 어...?

스택이 떠오르네요. Routine 은 스택으로 동작하는 군요!

 

어찌보면, 당연한 흐름이겠습니다만, 이런식으로 깊게 깊게 들어가면 끝이 없겠...네요... 이친구는 여기까지!

코루틴은 무엇일까?

코루틴은 시작부터 종료까지 suspend 와 resume 이 여러번 호출될 수 있습니다.

이게 무엇이냐면,

subRoutineA 가 실행된중에 잠시 suspend 하여 탈출하고, subRoutineB 를 실행하다가 subRoutineB 를 끝내거나,

subRoutineB 도 잠시 중지한다음 subRoutineA 를 다시 실행할 수 있다는 것입니다.

 

* 핵심은 return 이 아니기 때문에 subRoutineA 가 중지된 부분부터 이어서 실행이 가능하다는 것이죠!

 

    private fun mainRoutine() {
        subRoutine1()
        subRoutine2()
        subRoutine3()
    }

    private fun subRoutine1() {
        var count = 0
        while(count<5) {
            Thread.sleep(500)
            println("subRoutine1")
            count += 1
        }
    }

    private fun subRoutine2() {
        var count = 0
        while(count<5) {
            Thread.sleep(500)
            println("subRoutine2")
            count += 1
        }
    }

    private fun subRoutine3() {
        var count = 0
        while(count<5) {
            Thread.sleep(500)
            println("subRoutine3")
            count += 1
        }
    }

이 코드는 코루틴이 아닌 일반적인 코드입니다. 해당 코드들은 어떻게 동작할지 상상이 가죠?

 

그렇다면 다음 코드를 볼까요?

    private fun mainRoutine() {
        MainScope().launch {
            println("Start subRoutine1")
            subRoutine1()
        }

        MainScope().launch {
            println("Start subRoutine2")
            subRoutine2()
        }

        MainScope().launch {
            println("Start subRoutine3")
            subRoutine3()
        }
    }

    private suspend fun subRoutine1() {
        var count = 0
        while(count<5) {
            delay(500)
            println("subRoutine1")
            count += 1
        }
    }

    private suspend fun subRoutine2() {
        var count = 0
        while(count<5) {
            delay(500)
            println("subRoutine2")
            count += 1
        }
    }

    private fun subRoutine3() {
        var count = 0
        while(count<5) {
            Thread.sleep(500)
            println("subRoutine3")
            count += 1
        }
    }

subRoutine1, 2, 3 은 모두 코루틴으로 실행을 시켰습니다.

 

그럼, 1,2,3,1,2,3,1,2,3,1,2,3,1,2,3, 순으로 돌까요?

 

정답은 틀렸습니다.

 

subRoutineA 가 실행된중에 잠시 suspend 하여 탈출하고, subRoutineB 를 실행하다가 subRoutineB 를 끝내거나,

subRoutineB 도 잠시 중지한다음 subRoutineA 를 다시 실행하려면

 

suspend 로 메소드를 작성해야합니다.

 

즉, subRoutineA 와 subRoutineB 는 서로 잠시 탈출하고, 다음대상실행, 탈출 , 실행 으로 동잡합니다만,

 

subRoutineC 는 suspend 키워드가 없기 때문에  선점한 이후에 탈출하려면 subRoutineC 가 종료되어야만 가능합니다.

 

따라서 실행결과는 아래와 같습니다.

 

이러한 동작은 멀티쓰레드로 만들어낼수도 있습니다.

그러나, 엄청나게 큰 차이는

 

멀티쓰레드 : 쓰레드간 컨텍스트 스위칭으로 서로 다르게 동작

코루틴 : 단일쓰레드에서 함수를 잠시 일시정지하고 다른 함수를 실행하는 식으로 동작

 

Suspend 메소드는 어떤식으로 동작할까?

 

아래와 같은 suspend 메소드를 변경시켜보겠습니다.

정확하게 suspend 가 변하는 부분은 훨씬 복잡할 것입니다.

어떤 메소드를 사용할지 결정하는 등에 대한 부분도 고려가 될것으로 판단되구요..

중요한건 결국 메소드를 스위칭 한다는 것이죠

즉, 1과 2가 서로 어떻게 돌아가면서 실행되어야 하는데요,

 

따라서 아래와 같이 변경해 보았습니다.

label 을 두고, 현재 메소드 실행 이후에 어떤 메소드를 실행할기 결정시키는 것인데요, 공부한바에 의하면

코루틴의 suspend 는 저런식으로 label 가지고 결정한다고 합니다. 물론 훨씬 복잡하겠지만요 :)

 

멀티쓰레드 vs 코루틴  누가 좋을까?

 

멀티쓰레드를 스위칭하는 작업은 코루틴에서 함수를 스위칭하는것보다 훨씬 많은 메모리를 필요로 합니다.

즉, 사용 메모리 : 멀티쓰레드 > 코루틴

 

그러나, 코루틴은 단일쓰레드 내에서 여러개를 번갈아가며 실행시키는 것입니다.

(이는 단일코어에서 멀티쓰레드를 번갈아가며 실행시키는 것과 유사하다고 봐야할것 같습니다.)

 

즉 10개의 Task 가 있다면,

 

1. 단일코어 멀티쓰레드 : 1개의 코어에 10개의 쓰레드를 동작시키고...

  - 처리속도

멀티쓰레드의 컨텍스트 스위칭 시간 vs 코루틴 labeling 시간 중 누가 더 빠른가?

를 봐야할 것 같습니다. 개인적으로는 최소 같거나, 코루틴이 더 빠를것이라고 생각합니다.

 

2. 멀티코어 멀티쓰레드 : 3개의 코어가 3,3,4 의 쓰레드를 동작시키고...

 - 처리속도

멀티코어 멀티쓰레드 > 코루틴

 

그러나, 이 부분은 좀더 깊이있게 공부해서 어떤것이 좋은지 확인이 필요할것 같습니다.

제가 아직 이 판단은 잘 서지 않네요.. 

그러나, 쓰레드를 정말 제대로 잘짜는게 아니라면 코루틴이 좋지 않을까 싶...흡...

GlobalScope vs MainScope vs lifecycleScope 등등등

안드로이드에서 코루틴을 사용하다보면,

 

~~~Scope.launch {

...

}

 

이런식으로 많이 사용을 하게 되는데요.

대체 어떤 스코프를 써야하는지에 대한 생각이 들었던 적이 있었습니다. 다시한번 코루틴을 정리한 이유도 이때문이구요.

 

1. GlobalScope : 자체적인 쓰레드풀을 하나 갖고 있는 곳에서 실행됩니다.

즉 정말, Global 한 영역에서 동작시킨다고 봐야합니다.

MainActivity 에서 호출한다면, Application 이 해당 코루틴의 동작구간이 된다고 봐야합니다.

 

2. MainScope : MainThread 에 해당하는 영역에서 실행됩니다.

MainActivity 에서 호출한다면, MainActivity 가 해당 코루틴의 동작구간이 된다고 봐야합니다.

 

3. lifecycleScope : 호출되는 라이프사이클에 맞게 실행됩니다.

 

NullPointException 의 고통을 받고 싶지 않다면, 사용되는 코루틴이 LifeCycle 에 맞게 생성, 제거 될 수 있도록 신경을 써야 할 것 같습니다.

 

즉, 저는 앞으로 앱을 개발할 때, lifecycleScope 를 제일 많이 사용하게 될 것 같고, 그러지 못하는 경우에는 cancel 을 적절하게 사용하고, 제대로 동작할 수 있도록 고민하며 개발을 진행해야 한다고 판단했습니다.

의존성 주입(DI) 는 안드로이드 뿐만 아니라, 객체지향 프로그래밍을 할 때 한번쯤 듣는 단어입니다.

의존성이란

하나의 객체가 동작함에 있어서 다른 객체를 반드시 필요로 하는 경우를 말합니다.

A 클래스에서 B클래스를 반드시 필요로 한다. 즉, B클래스가 없으면 A클래스는 동작하지 않는다.

이런 경우 A클래스는 B클래스에 의존하고 있다. 라고 보면 될 것 같습니다.

의존성 주입(DI) 란,

의존성을 갖는 객체가 있다면, 그 객체가 의존하고 있는 객체를 외부에서 주입해주는 것입니다.

 

DI 를 사용하는 이유

지금부터 상황을 토대로 DI 를 사용하는 이유에 대해서 시작하겠습니다.

이 상황은 DI 설명을 위해 지어낸 상황입니다. (현실성에서 굉장히 떨어질 수 있습니다.)

 

Chapter1. 처음엔 1개 뿐이였어

요청 : 안드로이드 앱 개발자를 구하고 있습니다. 이 개발자는 AndroidStudio 를 사용해서 앱을 개발해야 합니다.
답변 : 안드로이드 개발자 클래스를 전달드리겠습니다. 필요하실 때마다 해당 클래스를 생성하시면, 안드로이드 앱 개발자를 얻게 되실겁니다.
class AndroidDeveloper {
  AndroidStudio androidStudio;
  
  AndroidDeveloper() {
  	androidStudio = new AndroidStudio();
  }

  void createAppAOS() {
  	androidStudio.createApp();
  }
}

Chapter2. 비슷하지만, 새로 만들면 돼

요청 : 저번에 안드로이드 개발자클래스를 잘 쓰고있습니다. 근데 이번엔 iOS 앱 개발자를 구하고 있습니다. iOS 앱 개발자는 Swift 를 사용해야 합니다.
답변 : iOS 개발자 클래스를 전달드리겠습니다... 역시 필요하실 때마다 해당 클래스를 생성하시면 됩니다.
class iOSDeveloper {
  xCode xCode;
  
  iOSDeveloper() {
  	xCode = new xCode();
  }

  void createAppiOS() {
  	xCode.createApp();
  }
}

Chapter3. 안좋은 예감이 들어

요청 : iOS 개발자가 그만두었습니다. 이참에 그냥 안드로이드 개발자가 iOS 개발도 할 수 있게 만들어주면 안되나요?
답변 : 네 알겠습니다... ㅂㄷㅂㄷ 수정해드릴게요
class AndroidDeveloper {
  AndroidStudio androidStudio;
  xCode xCode;
  
  AndroidDeveloper() {
  	androidStudio = new AndroidStudio();
  	xCode = new xCode();
  }

  void createAppAOS() {
  	androidStudio.createApp();
  }

  void createAppiOS() {
  	xCode.createApp();
  }
}

Chapter4. 이럴줄 알았어

질문 : 이거 혹시 그전 iOS 개발자 클래스도 그대로 쓸 수 있는거 맞죠?
답변 : 네 맞습니다...
요청 : 그냥 안드로이드 개발자, iOS 개발자, 두개 다 가능한 개발자 세개로 만들어주세요.
답변: 예예...
class AndroidDeveloper {
  AndroidStudio androidStudio;
  AndroidDeveloper() {
  	androidStudio = new AndroidStudio();
  }

  void createAppAOS() {
  	androidStudio.createApp();
  }
}

class iOSDeveloper {
  xCode xCode;
  iOSDeveloper() {
  	xCode = new xCode();
  }

  void createApp() {
  	xCode.createApp();
  }
}

class AppDeveloper {
  AndroidStudio androidStudio;
  xCode xCode;
  AppDeveloper() {
  	androidStudio = new AndroidStudio();
  	xCode = new xCode();
  }

  void createAppAOS() {
  	androidStudio.createApp();
  }

  void createAppiOS() {
  	xCode.createApp();
  }
}

Chapter5. 그렇게 1개에서 100개까지 늘어났지

요청 : 이번엔 Spring..
요청 : 이번엔 ~~~~~

...

요청 : 이번엔 풀스택~~

 

Chapter6. 절망의 순간

요청 : 보니까 개발자가 사용하는 툴을 생성하더라구요? 그 툴에서 사용하는 언어도 선택할 수 있게 해주세요.
답변 : 그러니까.. 이런식으로요?
class AndroidStudio {
Language language;
  AndroidStudio(Language language) {
  	this.language = language;
  }
  void createApp() {
	//...
  }
}
요청 : 어 맞아요. 그런데 어라... 이전에 제공해주신 코드들이 모두 에러가 나요

 

Chapter7. 회고의 순간

아.... 이거 사용하고 있는 코드들 다 찾아서 다 고쳐줘야하네...

 

이게 다 개발자 생성하는 클래스마다 싹다 툴 클래스에 의존하고있어서그래!!

애초에  개발자 클래스를 생성할 때 툴을 전달받게 했으면, 개발자 클래스를 생성하는 부분에서만 고치면 편할텐데 하............

 

다음에는 이런 부분은 진짜 고려해서 작성해야겠다!!

 

 

Chapter8. 이런 상황을 겪은 나에게 필요한 건 뭐다!?

"Constructor injection" , 바로 생성자 주입이라는 것이다!!!


 

Chapter9. 근데.. 만약에 생성자를 직접 안넣고  그냥 set 으로 전달받으면 어쩌지?

class AndroidDeveloper {
  AndroidStudio androidStudio;
  AndroidDeveloper(String lang) {
  	androidStudio = new AndroidStudio();
  	androidStudio.setLanguage(lang);
  }
  void createAppAOS() {
  	androidStudio.createApp();
  }
}

 

이렇게 AndroidStudio 객체가 언어를 필요로 하는데, 이 언어를 setLanguage 처럼 직접 넣어주는 경우를

"Filed injection" , 바로 필드 주입 이라고 한다.

 

 

 


의존성 주입의 장점

  • Unit Test가 용이해진다.
  • 코드의 재활용성이 높아진다.
  • 객체 간의 의존성(종속성)을 줄이거나 없앨 수 있다.
  • 객체 간의 결합를 낮추기 때문에 유연하게 코드를 작성하며, 유지보수가 쉬워진다.
  • 보일러 플레이크 코드가 줄어든다.

위 예시에서는 의존성 주입의 장점들을 모두 담아내지 못했습니다. (ex. TestCode , 재활용성 등 )

결국 클린하게 코드를 작성하기 위하여, 객체지향을 객체지향답게 작성하기 위하여

 

"의존성 주입" 은 선택이 아니라, 필수입니다.

 

아마 DI 를 도와주는 라이브러리를 사용하지 않아서, DI 를 모른다. 적용해본적 없다. 라고 생각하시는 분들이 계실지도 모르겠습니다.

그러나, DI 는 꼭 라이브러리가 있어야만 하는 것이 아닙니다. 여러분은 알게 모르게 사용하셨을 수도 있습니다. ( 의존성 주입만을 담당하는 Class 를 따로 둔다거나 하는 것까지 아니더라도)

겁먹지 말고, 클린코드를 위하여 편안한 유지보수로 미래의 "자신"이 더 행복하기 위하여 이제는 시작해봅시다.

 

'BackUp (관리중지) > CS 학습' 카테고리의 다른 글

개발자 기초지식 [ 내용요약 ]  (0) 2022.11.12
REST API  (0) 2021.04.28
Java GC  (0) 2021.04.28
GC ( Garbage Collection )  (0) 2021.04.28
동시성 이슈  (0) 2021.04.27

동적배열은 배열의 크기가 가변으로 변할 수 있는 배열입니다.

따라서 실행시간에 그 크기가 결정됩니다.

 

그러나, 동적배열도 배열이기 때문에 배열의 가장 중요한 특징은 일치합니다

 

그 특징은 "메모리 시작주소를 기준으로 연속적으로 할당되어 있다" 라고 볼 수 있습니다.

 

 

어떻게 메모리에 연속적으로 할당될 수 있죠?

다음과 같은 경우를 볼까요?

0x000000 USER[0]

0x000001 USER[1]

0x000002 USER[2]

0x000003 NAME[0]

0x000004 NAME[1]

0x000005

0x000006

0x000007

0x000008

 

이같은 경우에 USER[3] 이 필요하다면 어쩌죠?

NAME[0], NAME[1] 의 주소를 뒤로 옮기고 USER[3] 을 저장해야하나요?

 

아닙니다.

예시가 저럴뿐 저 사이에 얼마나 많은 메모리가 사용되고 있는지 모르는데, 그처럼 어마무시한 짓은 할 수 없습니다.

 

따라서 아래와 같은 순서로 진행합니다

1. 기존 USER 를 복사

0x000000 USER[0]

0x000001 USER[1]

0x000002 USER[2]

0x000003 NAME[0]

0x000004 NAME[1]

0x000005 USER[0]

0x000006 USER[1]

0x000007 USER[2]

0x000008

 

2. 새로운 USER[3] 을 추가

0x000000 USER[0]

0x000001 USER[1]

0x000002 USER[2]

0x000003 NAME[0]

0x000004 NAME[1]

0x000005 USER[0]

0x000006 USER[1]

0x000007 USER[2]

0x000008 USER[3]

 

3. 기존 USER 제거

0x000000

0x000001

0x000002

0x000003 NAME[0]

0x000004 NAME[1]

0x000005 USER[0]

0x000006 USER[1]

0x000007 USER[2]

0x000008 USER[3]

 

그럼 배열이 하나 추가될때마다 계속 복사하나요?

생각만해도 비효율적인 속도가 느껴지나요?

몇개가 추가될지 모르는데, 추가될때마다 복사하고 기존껏을 제거한다면, 시간복잡도가 상당해질 것이 예측이 됩니다.

 

그래서 동적배열의 경우 SIZE * 2 씩 미리 메모리 영역을 할당해둡니다.

 

0x000000 USER[0]

0x000001 USER[1] 을 위해 미리 차지해둠

0x000002 NAME[0]

0x000003 NAME[1]

 

이같은 경우가 있다면 USER[1] 을 추가하는 순간에는 복사 및 기존제거 행위는 일어나지 않습니다.

 

저 상황에서 USER[2] 를 추가한다면,

0x000000

0x000001 

0x000002 NAME[0]

0x000003 NAME[1]

0x000004 USER[0]

0x000005 USER[1]

0x000006 USER[2]

0x000007 USER[3] 을 위해 미리 차지해둠

0x000008

0x000009

 

마찬가지로 이런 상황에서 USER[3] 을 추가한다면

0x000000

0x000001

0x000002

0x000003 NAME[0]

0x000004 NAME[1]

0x000005

0x000006

0x000007

0x000008 USER[0]

0x00009 USER[1]

0x000010 USER[2]

0x000011 USER[3]

0x000012 USER[4] 를 위해 미리 차지

0x000013 USER[5] 를 위해 미리 차지

0x000014 USER[6] 를 위해 미리 차지

0x000015 USER[7] 를 위해 미리 차지

 

이렇게 동작한다고 보시면 됩니다.

 

 

'BackUp (관리중지) > 자료구조 & 알고리즘' 카테고리의 다른 글

[자료구조] 배열  (0) 2021.04.28

배열은 같은 종류의 데이터들이 순차적으로 저장되는 자료구조 입니다.

 

메모리 시작주소를 기준으로 자료형의 크기 * 데이터 개수  만큼 메모리에 연속적으로 할당되어 있습니다.

 

만약 자료형의 크기가 1bit 라고 한다면

0x000000 DATA[0]

0x000001 DATA[1]

0x000002 DATA[2]

0x000003 DATA[3]

0x000004 DATA[4]

0x000005 DATA[5]

0x000006 DATA[6]

0x000007 DATA[7]

 

처럼 할당됩니다.

0x000000 DATA[0]

0x000001 DATA[1]

0x000002 DATA[2]

0x000003

0x000004 DATA[3]

0x000005 DATA[4]

0x000006 DATA[5]

0x000007 DATA[6]

 

처럼 할당되는 경우는 없습니다.

 

배열은 처음 할당된 크기에서 가변적으로 늘릴 수 없습니다.

동적배열이라는 자료구조가 존재하지만, 그것은 다른 자료구조이기 때문에 기본적으로 배열은 처음 크기를 정해서 생성하고,

그 이후에는 그 크기를 늘리거나 줄일 수 없습니다.

'BackUp (관리중지) > 자료구조 & 알고리즘' 카테고리의 다른 글

[자료구조] 동적배열  (0) 2021.04.28

1. REST : HTTP 의 장점을 활용할 수 있도록 만든 아키텍처

2. REST API : REST 아키텍처에 맞게 설계한 API

3. RESTful API : API 가 REST의 제약을 잘 지킬수록 Restful 하다고 말합니다.

 

REST

REST (REpresentational State Transfer) 은, Roy Fielding 박사학위 논문에서 최초로 소개되었습니다.

HTTP 의 주요 저자 중 한 사람으로 그 당시에 HTTP 설계의 우수성에 비해 제대로 사용되어지지 못하는 모습에 장점을 최대한 활용할 수 있는 아키텍처로 REST 를 발표하였습니다.

 

REST 구성

ReSource

- 자원의 위치를 정의합니다.

- Client 는 URI 를 이용하여 자원에 접근할 주소를 최종 호출 단위까지 명확하게 특정합니다.

 

Verb

- 자원에 대한 행위를 정의합니다 (HTTP Method)

- Client 는 HTTP Method 를 이용하여 지정한 자원에 대한 조작을 요청합니다.

 

Representation

- 자원에 대한 행위의 내요을 정의합니다.

- Client가 Server에 자원에 대한 조작을 요청할 때, 조작에 필요한 데이터를 메시지로 표현하여 전송합니다.

 

HTTP Method ( REST API 에서 주로 사용되는 HTTP Method )

HTTP Method Type 설명 페이로드
PUT Replace 리소스 전체 교체  O
POST Create 리소스 생성 O
PATCH Modify 리소스 부분 교체 O
GET Read 리소스 조회 X
DELETE Delete 리소스 삭제 X

* 페이로드 : 여기서는 Message를 의미합니. 흔히, Body 에 Json Type 으로 Data 를 넘겨주는데 이같은 경우가 해당됩니다.

 

REST 데이터 포맷

흔히, 데이터를 JSON 형식으로 전송하고 JSON 형식으로 받기 때문에 JSON 만가능한 것으로 착각하는 경우가 있습니다.

 

그러나, JSON, HTML, XML, JavaScript, TEXT 등 다양한 포맷을 지원합니다.

 

 

'BackUp (관리중지) > CS 학습' 카테고리의 다른 글

개발자 기초지식 [ 내용요약 ]  (0) 2022.11.12
DI (Dependency Injection )  (0) 2021.04.29
Java GC  (0) 2021.04.28
GC ( Garbage Collection )  (0) 2021.04.28
동시성 이슈  (0) 2021.04.27

앞선 글에서, GC 는 프로그램 동적으로 할당했던 메모리 영역 중에서 필요없게 된 영역 해제 하는 메모리 관리 기법 이라고 하였습니다.

 

또한 Java에서 GC 는 JVM 의 가비지컬렉터 가 동작한다고 하였는데요,

 

이번 글에서는 Java GC 에 대해서 조금 더 알아보려고 합니다.

 

* Heap영역 : 자바에서 객체를 생성하면 Heap 영역에 저장

Minor GC & Major GC

JVM 은 Heap영역을 설계할 때 2가지 전제조건으로 설계 되었습니다.

1. 대부분의 객체가 금방 접근 불가능한 상태가 된다.

2. 오래된 객체에서 새로운 객체로의 참조는 드물게 존재한다.

 

이는 대부분의 객체가 일회성인 경우가 많고, 메모리에 오래 남아있는 경우가 드물다는 것을 의미합니다.

 

이에 따라 Young영역 , Old 영역 으로 구분을 하여 설계하였습니다.

 

Young 영역 : 새로운 객체의 영역

- 새로운 객체는 이곳에 할당되며, 대부분의 객체가 일회성인 만큼 대부분의 객체는 Young 영역에 오래 남아있지 않고 사라집니다.

- Young 영역을 관리하는 GC 를 Minor GC 라고 합니다.

또한, Young 영역의 구조는 아래와 같이 이루어져 있습니다.

- Eden 영역 : 새로 생성된 객체가 할당되는 영역

Eden 영역이 가득찰때마다, MinorGC 실행

- Survivor1 영역 : Eden 영역에서 1번 이상 살아남은 경우 할당되는 영역

- Survivor2 영역 : Survivor1 영역이 가득찬 경우 Survivor1 영역에서 살아남는 객체가 할당되는 영역

Survivor2 영역에서 계속 해서 살아남으면, 해당영역의 객체들은 Old 영역으로 복사

 

Old 영역 : 오래된 객체의 영역

- 새로운 객체가 Young 영역에서 사라질 때, Minor GC 를 거쳤지만, 해제되지 않고 살아남은 객체들이 이곳으로 복사됩니다.

- Old 영역에서도 언젠가는 사라져야하는데 이러한 Old 영역을 관리하는 GC 를 Major GC 라고 합니다.

- Old 영역은 Young 영역보다 크며, MinorGC 에 비해 MajorGC는 10배 이상의 시간을 필요로 합니다.

- 이는 MajorGC 가 자주 호출된다면, 성능에 영향을 끼칩니다.

- 그에 따라 MajorGC 는 Old 영역의 메모리가 부족해질 때 호출됩니다.

Card Table 

위에서 오래된 객체에서 새로운 객체로의 참조는 드물게 존재한다고 하였습니다.

즉, 오래된 객체가 새로운 객체를 참조할 수도 있다. 라는 것을 의미합니다.

 

그렇다면,

Minor GC 는 Young 영역에서 해제되어야하는 대상을 찾아야할 때, Old 영역도 확인해야 겠네? 라는 결론이 나오게 됩니다.

하지만 문제가 하나 있습니다. Old 영역에 있는 객체가 Young 영역에 있는 객체를 참조하는 경우는 드물게 발생한다 에서

Old 영역중에 0개 or 1개 의 객체만 Young 영역의 객체를 참조할 수 있다는 것입니다.

 

n 개의 객체를 제거대상인지 식별하기 위하여 m개의 객체를 검사한다면 n*m 번의 검사가 실행됩니다.

이러한 문제를 해결하기 위하여 Card Table 이 등장합니다.

 

Card Table : Old 영역의 객체가 Young 영역의 객체를 참조하는 경우에 그에 대한 정보를 저장합니다.

- Minor GC 가 동작할때 Young 영역에 있는 객체들은 Old 영역에 있는 객체와의 n*m 번의 검사가 아니라,

Card Table 에 있는 객체를 확인하여 Young 영역의 객체중 GC 의 대상에서 벗어나야 하는 객체를 식별합니다.

 

기본적인 GC 공통 동작 : Stop The World & Mark And Sweep

MinorGC 와 MajorGC 는 세부적으로 동작방식이 다릅니다.

간단하게 MinorGC 에서는 CardTable 도 사용하는 로직도 필요하다는 것을 예로 들 수 있겠죠?

 

하지만 기본적으로 GC 가 실행될 때 공통적으로 동작하는 방식이 있습니다.

 

1. Stop The World

- JVM 의 가비지컬렉터가 GC 를 실행하기 위하여 GC 를 실행하는 쓰레드를 제외한 나머지 쓰레드를 모두 정지시킵니다.

GC 작업이 종료되면, 나머지 쓰레드를 다시 동작합니다.

즉, Stop The World란 JVM 이 GC 를 실행시키기 위하여 다른 쓰레드를 정지&재개하는 동작입니다.

 

2. Mark And Sweep

- GC 가 동작할 때, 사용되는 메모리와 사용도지 않는 메모리를 식별하는 Mark 작업과 사용도지 않는 메모리가 식별된 경우, 이를 해제하기 위한 Sweep 작업입니다.

 

 

 

요약

자바의 GC 는 MinorGC 와 MajorGC 로 나뉩니다.

MinorGC : Young 영역에 대한 GC, Young 영역의 구조중 Eden영역이 가득차면 실행됨. 실행속도가 빠르다.

MajorGC : Old 영역에 대한 GC, Old 영역이 가득차면 실행됨. 실행속도가 느리다.

 

* 객체의 잘못된 사용으로 GC 의 대상이 되지 못하여 메모리 누수가 일어나는 경우 Old영역에 할당되는 객체가 늘어나게 되고,

Old영역이 가득찰때마다 MajorGC 가 실행되는데, MajorGC 에서 해제되는 메모리가 적어지는 만큼 Old영역이 가득차는 경우가 빈번해집니다. 따라서 MajorGC 가 자주 호출되게 되고, 이는 곧 성능에 치명적으로 다가옵니다.

그 이후, Old영역이 가득 찾지만, 더이상 MajorGC 의 대상에 속하지 못하여 사용할 수 있는 메모리가 없어진 순간

펑~ 펑~ 펑 .....

 

메모리누수를 조심합시당 :)

'BackUp (관리중지) > CS 학습' 카테고리의 다른 글

DI (Dependency Injection )  (0) 2021.04.29
REST API  (0) 2021.04.28
GC ( Garbage Collection )  (0) 2021.04.28
동시성 이슈  (0) 2021.04.27
쓰레드(Thread) / 프로세스(Process)  (0) 2021.04.27

GC란,

Java 를 경험한 사람들은 가비지컬렉션, 가비지컬렉션 하는 말들을 못들어봤을리가 없습니다.

그러나, Java 로 개발하는 사람들에게 가비지컬렉션이 뭔가요? 라는 질문을 하면 흠칫 하는 경우가 많습니다.

알고 있는데, 설명을 하지 못하는 것은 모른다고 보는게 맞습니다.

 

오늘은 이렇게 모르면 안되는, 하지만 모르는 경우가 많은

가비지 컬렉션(GC) 에 대하서 알아보려고 합니다.

 

GC (Garbage Collection) 란,

프로그램동적으로 할당했던 메모리 영역 중에서 필요없게 된 영역해제 하는 메모리 관리 기법입니다.

 

" 더이상 사용되지 않는 메모리를 제거합니다. "
" 참조되지 않는 메모리를 해제합니다."

" 자동으로 메모리를 관리해줍니다. "

와 같은 답변은 맞는듯 아닌듯 저 말만 듣고는 이해하기 쉽지 않습니다.

 

앞으로 GC 가 무엇인가요? 에 대한 답은 

프로그램 동적으로 할당했던 메모리 영역 중에서 필요없게 된 영역 해제 하는 메모리 관리 기법입니다.

로 했으면 합니다.

 

우리가 모르고 넘어가던 핵심포인트는 GC 는 메모리 관리 기법 중 하나 라는 것입니다.


GC 는 어쩌다 탄생했나?

기본적으로 C 와 C++ 등의 언어에서 사용되지 않는 메모리 영역을 해제하기 위해 수동으로 메모리를 관리하였습니다.

Lisp 라는 언어에서 이같은 수동 메모리 관리를 단순화하기 위하여 GC 가 발명되었습니다.

역사공부는 적당히 하고.... 결국

 

"수동 메모리 관리" -> "자동 메모리 관리" 를 하기 위해 탄생했다고 보면 됩니다.

 

GC 장단점

GC 를 기본적으로 제공하는 언어나, GC를 구현한 프로그램 에서는 다음과 같은 장단점이 존재합니다.

<장점>

아래와 같은 버그를 줄이거나, 방지할 수 있습니다.

 

1. 유효하지 않은 포인터 접근

- 이미 해제된 메모리에 접근하는 경우에 발생하는 버그입니다.

만약 메모리에 다른 값이 새로 할당되었다면, 해당 메모리를 바라보던 포인트는 전혀 다른 값을 반환하게 됩니다.

 

2. 이중 해제

- 이미 해제된 메모리를 다시 해제하는 경우에 발생하는 버그입니다.

ex) free() 같이 수동으로 메모리를 해제 하는 과정에서 이미 해제된 메모리를 한번더 해제 하면서 에러를 발생시킵니다.

 

3. 메모리 누수(메모리 릭)

- 프로그램이 더이상 필요로 하지 않는 메모리 영역이 해제되지 않고 남아있는 경우를 말합니다.

메모리 누수가 반복되는 경우 사용가능한 메모리 영역이 고갈나게되고, 그 경우 프로그램은 중단됩니다.

* 메모리 누수가 아님에도 메모리 영역이 고갈나는 경우는 GC 로도 막을 수 없습니다.

 

<단점>

 

1. 해제해야 하는 메모리를 결정하는 오버헤드

- GC 는 프로그램 동적으로 할당했던 메모리 영역 중에서 필요없게 된 영역 해제 하는 메모리 관리 기법입니다.

라고 하였습니다. 그렇다면 필요없게 된 영역을 누가 알려주죠? 개발자가 직접 알려주나요? 나 여기 필요없어.

그럴거면 수동으로 메모리를 관리하는 것과 무슨 차이가....

따라서, GC가 동작할 때, 필요없게 된 영역을 결정해야 하는 로직이 동작합니다.

이때 결정하기 위한 오버헤드가 발생합니다.

 

2. 할당된 메모리가 해제되는 시점을 알 수 없다.

- 일반적으로 GC 는 개발자가 호출하지 않습니다.(자바의 경우 JVM 의 가비지컬렉터 가 이를 실행합니다.) 즉, 개발자가 "변수=null" 을 통해서 더이상 사용하지 않기 위해 참조를 해제한다고 하더라도, GC가 동작하기 전까지는 메모리 영역에서 해제되지 않는다는 겁니다.

 

안드로이드를 기점으로 생각해볼까요? Activity 가 실행되고 있을 때, 해당 Activity 가 종료되면 Activity 가 사용하던 메모리 영역은 그 즉시 해제되나요? 확신할 수 있나요?

 

GC원리를 학습하면, 해제해야하는 대상을 결정하는 로직을 보게 됩니다. 그런 로직에 따라서 결정되는데 프로그램을 개발하는 순간에 GC 가 해제해야 하는 대상을 결정하는 로직과, GC가 일어나는 타이밍들을 100% 고려하여 작성할 수 있나요?

 

저희는 GC가 언제 동작하는지 예측하는것도 어려울 뿐더러, 그 GC 가 동작하는 순간에 어떤 메모리 영역을 해제해야 대상으로 선택했는지 에 대해서 알 수 없다고 봐야합니다.

 

 

< Java GC 에 대해서 좀 더 알아보고 싶다면 >

devhyeon0312.tistory.com/17 게시글을 확인해주세요 🤗

'BackUp (관리중지) > CS 학습' 카테고리의 다른 글

REST API  (0) 2021.04.28
Java GC  (0) 2021.04.28
동시성 이슈  (0) 2021.04.27
쓰레드(Thread) / 프로세스(Process)  (0) 2021.04.27
동기(Synchronous) / 비동기(Asynchronous)  (2) 2021.04.27

동시성 이슈는 결국 "공유자원" 으로 인해 발생한다고 보면 될 것 같습니다.

 

공유자원이란

그렇다면, "공유자원" 은 무엇일까요?

정말 단어 그대로 순수하게 자원을 공유한다고 보면 될 것같습니다.

서버와 클라이언트를 본다면, DB 의 데이터가 공유자원이 될수 있고,

프로세스 간에는 IPC 통신을 하면서 여러프로세스가 동일한 자원에 접근하는 경우에 해당 자원이 공유자원이 될 수 있고,

쓰레드간에는 여러쓰레드가 프로세스 힙메모리 등에 있는 자원에 접근하는 경우에 해당 자원이 공유자원이 될 수 있습니다.

 

단, 변하지 않는 Read Only 의 자원에 대해서는 동시성 이슈가 발생하지 않습니다.

그러나, 수정이 일어나는 자원에 대해서는 굉장히 조심해야 합니다.

 

실생활 예시

친구들과 팀프로젝트를 진행중입니다. 파일을 공유하기 위해 구글드라이브 사용하고 있네요.

팀장만 파일을 등록/삭제/수정 이 가능하다면, 팀원들이 받는 파일은 항상 팀장이 마지막으로 수정한 파일일 것입니다.

그러나 모두가 등록/삭제/수정 이 가능하다면?

0.팀원A와 팀원B가 파일을 각각 다운받았습니다.

1.팀원A가 파일에 1이라고 적혀있는 것을 0으로 고쳤습니다.

2.팀원B가 파일에 1이라고 적혀있는 것을 2로 고쳤습니다.

3.팀원A는 파일을 저장하였습니다.

4.팀원B도 파일을 저장하였습니다.

 

팀원A가 고친 값은 공중으로 사라지게 됩니다.

 

또다른 예를 볼까요?

0.팀원A와 B가 파일을 각각 다운받았습니다.

1. 팀원A는 발표횟수가 0이라고 되어있는 파일을 가지고 발표를 진행합니다.

2. 발표를 끝낸 팀원A는 발표횟수를 1로 올려두고 파일을 저장합니다.

3. 팀원B는 발표횟수가 0이라고 되어있는 파일을 가지고 발표를 진행합니다.

4. 발표를 끝낸 팀원B는 발표횟수를 1로 올려두고 파일을 저장합니다.

 

어라? 발표는 분명 2번 일어났는데, 최종적으로 발표는 1번만 한것이 됩니다.

 

Android Thread 에서 발생하는 동시성 이슈

Android 를 예시로 들겠습니다. 

안드로이드 프로젝트를 처음 동작시키면, UI Thread 와 Main Thread 가 존재합니다.

UI Thread 에서는 정말 UI만 그리는 작업을 진행합니다.

Main Thread 에서는 UI를 그리는데 필요한 데이터를 처리하는 작업을 진행합니다.

 

만약 Main Thread 에서 아직 처리되지 않아서 null 인데이터를 UI Thread 가 그리려고 하면 NullPointException 이 발생할 것입니다.

UI를 그리기 위해 필요한 Data 는 "공유자원" 에 해당합니다.

 

이처럼 당장 눈에 Thread 라는 것을 정의하지 않더라도, 자신이 사용하는 프레임워크라던가, 동작의 원리등에서 발생할 수 있는 "동시성 이슈" 는 항상 고민되어야 합니다.

 

Android API 동시성 이슈

간단한 API 를 단계별로 호출하고, 결과에 따라서 UI 를 update 하는 경우에는 크게 접하지 못했을 수도 있습니다.

그러나, 한 화면에 여러개의 API 가 호출되고, 여러개의 API 결과에 따라 UI 가 update 되는 경우에는 이런 문제가 발생할 수도 있습니다.

 

예를 하나 들어보겠습니다.

 

API 요청결과는 SUCCESS, FAIL 두가지로만 분류된다고 가정하겠습니다.

SUCCESS 인 경우에는 UI update 에 필요한 데이터가 null 인 경우는 없다고 가정하겠습니다.

또한 FAIL 의 경우에는 재시도 없이 FAIL UI 를 보여준다고 가정하겠습니다.

 

한개 API

1. API 요청 및 응답대기

2. SUCCESS : 필요한 데이터로 UI Update

2. FAIL : FAIL UI 표시

 

API 결과가 무엇이든, 동작에 문제는 없습니다.

 

여러 API

1. 첫번째 API 요청 및 응답대기

2. 두번째 API 요청 및 응답대기

 

3. 첫번째 API SUCCESS : 필요한 데이터로 UI Update // 두번째 API 결과로 얻는 Data 가 null 이라서 FatalError 발생

3. 첫번째 API FAIL : FAIL UI 표시

 

4. 두번째 API SUCCESS : FAIL UI 에서 SUCCESS UI 로 변경을 시도, // 첫번째 API 결과로 얻은 Data 가 null 이라서 FatalError 발생

5. 두번째 API FAIL : FAIL UI 표시

 

운좋게(?) 두개의 API 가 모두 FAIL 인 경우에는 FAIL UI 를 표시하면 되기 때문에 의도대로 동작한다고 볼수도 있습니다.

그러나, 한개의 API 라도 SUCCESS 가 일어나는 순간 저 로직은 FatalError 가 발생합니다.

 

해결과정을 알아볼까요?

Null 이 아닌 경우에만 해당 데이터가 사용되는 UI 를 그리라는 것입니다.

그러면 최소한 FatalError 에서는 벗어나겠지요?

 

그런데.. 저게 정말 맞는 해결방법일까요?

정답은. 때에 따라서 맞을수도, 틀릴수도 있다 입니다.

 

만약 정말 보이고자 하는 의도가 NULL 인 경우에 공백 등으로 UI 표시하는 것이 목적이라면, 저것은 맞다고 할 수 있습니다.

그러나, 의도된 UI or 다음 동작을 위해서 2개의 API 의 결과가 모두 필요한 경우라면, 저것은 틀리다고 할 수 있습니다.

 

만약 2개 API 의 결과가 필요하다면,  2개 API 의 반환된 결과에 따라서 처리로직이 구현되어야 합니다.

<첫번째 방법>

1. 첫번째 API 요청 및 응답대기

2. 첫번째 API SUCCESS 로 인해 대기

3. 두번째 API 요청 및 응답대기

4. 두번째 API SUCCESS 로 인해 UI Update

또는 첫번째 API FAIL or 두번째 API FAIL 의 경우에는 추가 요청없이 FAIL UI 표시

 

의 방법이 있겠네요. 이것은 여러 API 를 "순차적" 으로 사용하여 해결하는 방법입니다.

그러나, API 가 10개라면? 20개라면? 100개라면?

연관된 API 가 늘어날수록 위와같은 방법은 코드의 길이와 예외의 상황을 더이상 기억할수 없게 만들어버립니다.

 

<두번째 방법>

그럼 다음 방법을 살펴볼까요?

0. UI update 를 판단하는 쓰레드 동작

1. 첫번째 API 요청 및 응답대기

2. 두번째 API 요청 및 응답대기

3. 세번째 API 요청 및 응답대기

 

4. API 요청 결과가 모두 SUCCESS 인 경우 : 0번의 쓰레드의 SuccessCount = 3; 으로 UI Update

4. API 요청 결과가 부분 SUCCESS 인 경우 : 0번의 쓰레드의 SuccessCount < 3 && FailCount > 0 이므로 FAIL UI 표시

 

두번째 방법은 실제 사용시 Observer 를 사용한다거나, 편리하게 Rx를 사용한 API 콜을 처리하는 등으로 나아가게 됩니다.

 

이러한 과정은 결국 "동시성 이슈" 로 인해 나온 것이며, "왜" 필요한지 알게 되셨길 바랍니다.

 

'BackUp (관리중지) > CS 학습' 카테고리의 다른 글

Java GC  (0) 2021.04.28
GC ( Garbage Collection )  (0) 2021.04.28
쓰레드(Thread) / 프로세스(Process)  (0) 2021.04.27
동기(Synchronous) / 비동기(Asynchronous)  (2) 2021.04.27
HTTP (HyperText Transfer Protocol)  (0) 2021.04.26

+ Recent posts