로버트 C. 마틴 제시한 5가지 원칙

물론 다 본인이 만든건 아니라고 한다.

LSP 법칙만 봐도 아니라는 것을 알 수 있다.

 

 

개발자라면 무조건 듣게 되는 원칙이라고 생각한다.

나도 많이 들었고 봤다가 잊어버렸다가를 반복하기에 정리하려 한다.

 

개인적으로는 완전히 이해하고 정리하기 쉽지 않았다.

(지금도 완전히 이해했는지 잘모름)

 

실무에서 상황상 적용하기 힘들 수도 있고

적용한 것이 더 안좋은 경우가 있을 수도 있다.

 

하지만 알고 적용하지 않는 것과 몰라서 적용하지 못하는 것은 다른 이야기이다.

 

 

 


위키피디아 내용

 

 

위키피디아에 써있는 한줄 설명이다.

 

 

SRP (Single Reponsibility Principle; 단일 책임 원칙)

한 클래스는 하나의 책임만 가져야 한다.

 

OCP (Open/Cloesed Priciple; 개방-폐쇄 원칙)

소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야 한다.

 

LSP (Liskov Substitution Principle; 리스코브 치환 법칙)

프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.

 

ISP (Interface Segregation Principle; 인터페이스 분리 법칙)

특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.

 

DIP (Dependency Inversion Principle; 의존 역전 법칙)

프로그래머는 “추상화에 의존해야지, 구체화에 의존하면 안된다.". DI(의존성 주입) 역시 이 원칙을 따르는 방법 중에 하나이다.

 

https://ko.wikipedia.org/wiki/SOLID_(%EA%B0%9D%EC%B2%B4_%EC%A7%80%ED%96%A5_%EC%84%A4%EA%B3%84)

 

SOLID (객체 지향 설계) - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전.

ko.wikipedia.org

 

 

완벽하게 이해하려고 각각의 원칙들의 위키를 읽다가 더 이해하기 힘들었다.

 

많은 포스팅을 보고 원칙이 전하려는 의미를 파악하는게 더 쉬울 것이라고 생각한다.

 

하지만 포스팅들을 보면 이해하는 것도 가지각색이고 아다르고 어다르게 설명하는 경우도 부지기수이다.

최대한 많이 보며 공통의 내용이 무엇인지 파악해보자.

다른 포스팅 잘못된 포스팅들을 보면서 공부하고 생각하다 보면 이 또한 공부가 된다.

 

 

 

 

 

'SoftwareDesign' 카테고리의 다른 글

OCP  (0) 2020.03.01
SRP  (0) 2020.03.01
3. Observer  (0) 2020.01.19
2. Strategy  (0) 2020.01.07
1. Singleton  (0) 2019.12.25

개발포스팅은 언제나 유통기한을 조심하세요.

 

 

Github : https://github.com/junghun9102/AndroidTemplate

Branch : base/internal_storage

 

junghun9102/AndroidTemplate

Contribute to junghun9102/AndroidTemplate development by creating an account on GitHub.

github.com

 

내부저장소와 외부저장소는 앱의 내부냐 외부냐의 차이일 뿐이다.

 

Android 10이 나오면서 외부저장소에 대한 보안이 강화되었다.

사실 대부분의 앱들이 굳이 외부저장소가 필요한가 싶다.

 

내부저장소를 사용하는 branch를 완성하기는 했는데 만들고 나니

별내용이 없는데 이걸 왜 굳이 작성했을까 하는 생각이 들었다.

 

사실 Context.filesDir.path에서 받아오는 내부저장소의 rootPath가 전부이다.

나머지는 파일을 이용하는 작업일 뿐이다.

 

내부저장소를 이용해 카메라 사진을 저장할 때 FileProvider를 사용하면 된다.

 

 

 

 

 


핵심 코드

 

MainActivity

    private val internalStoragePath: String by lazy { filesDir.path }
    private val saveFileDirectory = "/textFolder"

    private val saveStoragePath: String by lazy { "$internalStoragePath$saveFileDirectory" }
    private val fileName = "textFileName.txt"

 

 

 

 

 


부가 코드

 

데이터 로드

            // 1
            val savedNoteFile = File(saveStoragePath, fileName)
            val note = FileManager.getTextFromFile(savedNoteFile)
            showNote(note)

//            // 2
//            val inputStream = openFileInput(fileName)
//            val note = FileManager.getTextFromInputStream(inputStream)
//            inputStream.close()
//            showNote(note)

 

데이터 저장

            // 1
            val file = File(saveStoragePath, fileName)
            val textToSave = et_main_note.text.toString()
            FileManager.writeTextToFile(file, textToSave)

//            // 2
//            val outputStream = openFileOutput(fileName, Context.MODE_PRIVATE)
//            val textToSave = et_main_note.text.toString()
//            FileManager.writeTextToOutputStream(outputStream, textToSave)
//            outputStream.close()

 

주석처리해 놓은 코드와 같이 파일을 거치지 않고 바로 stream으로 받아올 수 있다.

단, 내부저장소의 기본 저장소를 사용해야만 하는 것 같다. 

 

 

FileManeger

object FileManager {

    @Throws(IOException::class)
    fun getTextFromFile(file: File): String {
        val inputStream = file.inputStream()
        val text = getTextFromInputStream(inputStream)
        inputStream.close()

        return text
    }

    @Throws(IOException::class)
    fun getTextFromInputStream(inputStream: InputStream): String {
        val br = inputStream.bufferedReader()
        val text = br.useLines { lines ->
            lines.fold("") { sum, line ->
                sum + if (sum.isNotBlank()) ", $line" else line
            }
        }

        br.close()
        return text
    }

    @Throws(IOException::class)
    fun writeTextToFile(file: File, text: String) {
        val outputStream = file.outputStream()
        writeTextToOutputStream(outputStream, text)
        outputStream.close()
    }

    @Throws(IOException::class)
    fun writeTextToOutputStream(outputStream: OutputStream, text: String) {
        val bw = outputStream.bufferedWriter()
        bw.write(text)
        bw.close()
    }

}

 

 

 

 

 

'창고' 카테고리의 다른 글

custom_dialog (+custom_toast)  (0) 2020.05.02
Activity간의 화면 전환  (0) 2020.03.11
permission  (0) 2020.02.26
DP(Device Independence Pixel)?  (0) 2019.06.09
프로그래밍 용어 정리  (0) 2019.05.27

개발포스팅은 언제나 유통기한을 조심하세요.

 

 

Github : https://github.com/junghun9102/AndroidTemplate

Branch : base/permission

 

junghun9102/AndroidTemplate

Contribute to junghun9102/AndroidTemplate development by creating an account on GitHub.

github.com

 

 

권한이 필요한 기능에 대한 동작 정의

 

1. Manifest에 권한을 추가한다.

2. 권한이 필요한 기능을 요청한다.

3-1. 권한이 있다면 그대로 기능을 실행 -> FIN

3-2. 권한이 없다면 권한 요청 -> 4

4-1. 권한 요청 수락하면 기능 실행

4-2. 권한 요청 거절하면 메시지 표시

 

 

 

전체코드 먼저 보기

더보기
object PermissionHelper {

    const val PERMISSIONS_CAMERA = 1

    private val permissionList = mapOf(
        PERMISSIONS_CAMERA to arrayOf(android.Manifest.permission.CAMERA)
    )

    private fun getPermissions(requestCode: Int) = permissionList[requestCode] ?: throw IllegalArgumentException("You must use requestCode defined at PermissionHelper")

    fun checkPermission(context: Context, requestCode: Int): Boolean {
        val permissions = getPermissions(requestCode)
        return checkPermissions(context, permissions)
    }

    private fun checkPermission(context: Context, permission: String): Boolean {
        return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
    }

    private fun checkPermissions(context: Context, permissions: Array<String>): Boolean {
        return permissions.fold(true, { acc, permission ->
            acc && checkPermission(context, permission)
        })
    }

    fun requestPermissions(activity: Activity, requestCode: Int) {
        val permissions = getPermissions(requestCode)
        ActivityCompat.requestPermissions(activity, permissions, requestCode)
    }

    fun onRequestPermissionResult(context: Context, requestCode: Int, funcAllowed: () -> Unit, funcNotAllowed: () -> Unit = {}) {
        val permissions = getPermissions(requestCode)
        if (checkPermissions(context, permissions)) {
            funcAllowed.invoke()
        } else {
            funcNotAllowed.invoke()
        }
    }
}

// PermissionHelper Extensions
fun Activity.requestPermissions(requestCode: Int) = PermissionHelper.requestPermissions(this, requestCode)

fun Context.checkPermissions(requestCode: Int) = PermissionHelper.checkPermission(this, requestCode)

fun Context.onRequestPermissionResult(requestCode: Int, funcAllowed: () -> Unit, funcNotAllowed: () -> Unit = {}) =
    PermissionHelper.onRequestPermissionResult(this, requestCode, funcAllowed, funcNotAllowed)

fun Activity.checkPermissionsAndDoFunctionOrRequest(requestCode: Int, funcAllowed: () -> Unit) {
    if (this.checkPermissions(requestCode))
        funcAllowed.invoke()
    else
        this.requestPermissions(requestCode)
}

 

 

 


필요한 권한 추가 (1)

 

AndroidManifest.xml

    <uses-permission android:name="android.permission.CAMERA"/>

 

권한 정리

    const val PERMISSIONS_CAMERA = 1

    private val permissionList = mapOf(
        PERMISSIONS_CAMERA to arrayOf(android.Manifest.permission.CAMERA)
    )

 

기능별로 필요한 권한들을 정리해둔다.

 

 

 

 

 


기능 요청 (2 ~ 3)

 

MainActivity

    private fun initViews() {
        btn_main_camera.setOnClickListener {
            checkPermissionAndStartCamera()
        }
    }

    private fun checkPermissionAndStartCamera() {
        checkPermissionsAndDoFunctionOrRequest(PermissionHelper.PERMISSIONS_CAMERA) {
            startCamera()
        }
    }

    private fun startCamera() {
        Toast.makeText(this, "카메라를 시작합니다 위이잉", Toast.LENGTH_SHORT).show()
    }

 

권한이 있다면 바로 기능 수행

 

 

extensions

fun Activity.requestPermissions(requestCode: Int) = PermissionHelper.requestPermissions(this, requestCode)

fun Context.checkPermissions(requestCode: Int) = PermissionHelper.checkPermission(this, requestCode)

fun Activity.checkPermissionsAndDoFunctionOrRequest(requestCode: Int, funcAllowed: () -> Unit) {
    if (this.checkPermissions(requestCode))
        funcAllowed.invoke()
    else
        this.requestPermissions(requestCode)
}

 

Activity.checkPermissionsAndDoFunctionOrRequest에서 퍼미션을 체크하고

모두 권한이 주어져있다면 funcAllowed가 아니라면 requestPermissions 수행

 

 

PermissionHelper

object PermissionHelper {

    const val PERMISSIONS_CAMERA = 1

    private val permissionList = mapOf(
        PERMISSIONS_CAMERA to arrayOf(android.Manifest.permission.CAMERA)
    )

    private fun getPermissions(requestCode: Int) = permissionList[requestCode] ?: throw IllegalArgumentException("You must use requestCode defined at PermissionHelper")

    fun checkPermission(context: Context, requestCode: Int): Boolean {
        val permissions = getPermissions(requestCode)
        return checkPermissions(context, permissions)
    }

    private fun checkPermission(context: Context, permission: String): Boolean {
        return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
    }

    private fun checkPermissions(context: Context, permissions: Array<String>): Boolean {
        return permissions.fold(true, { acc, permission ->
            acc && checkPermission(context, permission)
        })
    }

    fun requestPermissions(activity: Activity, requestCode: Int) {
        val permissions = getPermissions(requestCode)
        ActivityCompat.requestPermissions(activity, permissions, requestCode)
    }
    
    ...
    
}

 

코드가 길어 읽기 싫지만 별거없는 코드다.

 

 

 

 

 


권한 요청에 따른 반응 (4)

 

MainActivity

	override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        when (requestCode) {
            PermissionHelper.PERMISSIONS_CAMERA ->
                onRequestPermissionResult(PermissionHelper.PERMISSIONS_CAMERA, {
                    startCamera()
                }) {
                    showPermissionDeniedMessage()
                }
        }
    }

    private fun showPermissionDeniedMessage() {
        Toast.makeText(this, "권한을 허용하지 않아 카메라를 사용할 수 없습니다.", Toast.LENGTH_SHORT).show()
    }

 

권한 요청에 응답을 한 뒤에

다시 요청한 권한리스트를 확인하고 이에 따라 기능을 수행하거나 거부했을 때의 기능을 수행한다.

절대적으로 권한을 승락하게 하고 싶다면 Activity.checkPermissionsAndDoFunctionOrRequest를 사용하면 거절시 다시 요청하게 된다.

 

 

PermissionHelper

    fun onRequestPermissionResult(context: Context, requestCode: Int, funcAllowed: () -> Unit, funcNotAllowed: () -> Unit = {}) {
        val permissions = getPermissions(requestCode)
        if (checkPermissions(context, permissions)) {
            funcAllowed.invoke()
        } else {
            funcNotAllowed.invoke()
        }
    }

 

퍼미션에 관한 코드를 캡슐화함으로 MainActivity의 코드가 많이 줄었다.

권한과 관련된 유명한 TedPermission도 있지만 직접 내 코드를 만들고 싶었다.

 

 

 

 

 


전체 코드

 

object PermissionHelper {

    const val PERMISSIONS_CAMERA = 1

    private val permissionList = mapOf(
        PERMISSIONS_CAMERA to arrayOf(android.Manifest.permission.CAMERA)
    )

    private fun getPermissions(requestCode: Int) = permissionList[requestCode] ?: throw IllegalArgumentException("You must use requestCode defined at PermissionHelper")

    fun checkPermission(context: Context, requestCode: Int): Boolean {
        val permissions = getPermissions(requestCode)
        return checkPermissions(context, permissions)
    }

    private fun checkPermission(context: Context, permission: String): Boolean {
        return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
    }

    private fun checkPermissions(context: Context, permissions: Array<String>): Boolean {
        return permissions.fold(true, { acc, permission ->
            acc && checkPermission(context, permission)
        })
    }

    fun requestPermissions(activity: Activity, requestCode: Int) {
        val permissions = getPermissions(requestCode)
        ActivityCompat.requestPermissions(activity, permissions, requestCode)
    }

    fun onRequestPermissionResult(context: Context, requestCode: Int, funcAllowed: () -> Unit, funcNotAllowed: () -> Unit = {}) {
        val permissions = getPermissions(requestCode)
        if (checkPermissions(context, permissions)) {
            funcAllowed.invoke()
        } else {
            funcNotAllowed.invoke()
        }
    }
}

// PermissionHelper Extensions
fun Activity.requestPermissions(requestCode: Int) = PermissionHelper.requestPermissions(this, requestCode)

fun Context.checkPermissions(requestCode: Int) = PermissionHelper.checkPermission(this, requestCode)

fun Context.onRequestPermissionResult(requestCode: Int, funcAllowed: () -> Unit, funcNotAllowed: () -> Unit = {}) =
    PermissionHelper.onRequestPermissionResult(this, requestCode, funcAllowed, funcNotAllowed)

fun Activity.checkPermissionsAndDoFunctionOrRequest(requestCode: Int, funcAllowed: () -> Unit) {
    if (this.checkPermissions(requestCode))
        funcAllowed.invoke()
    else
        this.requestPermissions(requestCode)
}

 

 

 

 

 

 

 

 

 

 

'창고' 카테고리의 다른 글

Activity간의 화면 전환  (0) 2020.03.11
internal_storage  (0) 2020.02.26
DP(Device Independence Pixel)?  (0) 2019.06.09
프로그래밍 용어 정리  (0) 2019.05.27
xml로 도형 그리기  (0) 2019.05.26

 

 

프로젝트를 만드게 되면 진행하는 일련의 과정을

루틴으로 만들고 계속 개선하려고 한다.

 

1. 프로젝트 생성, GIT 연결

2. 개발환경 설정

3. 개발 구조

4. 테스트

 


프로젝트 시작

 

프로젝트 생성

 

 - 패키지 이름, 프로젝트 이름

 

 

Git

 

Git Issue 생성

 - 개발 환경 설정 생성

 

.gitignore 설정 https://www.gitignore.io/

 

gitignore.io

Create useful .gitignore files for your project

www.gitignore.io

 

gitflow(개념만 사용)

 - 생성 프로젝트 mater branch 업로드

 - develop branch 생성, 업로드

 

 

 

 

 


개발 환경 설정

 

feature/environment 생성

 

 

gradle 설정

 

gradle 버전 관리 파일(dependencies.gradle) 생성

 - 모든 버전 reference로 사용

 - 앱 버전 0.1 시작

 

문서화 : gradlw dokka

 - apply plugin: 'org.jetbrains.dokka'

 - classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokkaVersion"

 

코드 검사 : gradlew lint

 

사용할 라이브러리 추가

 - anko, koin, rxandroid, rxjava, rxbinding...

 

 

 

기본 파일 구성

 

resource 설정

 - strings(ko) 추가

   : 아주 조금이라도 해외에서 앱이 쓰일 수 있다면 default를 영어로 한글은 strings(ko)를 사용하자.

 - dimens 추가

 

BaseColor 설정

https://material.io/resources/color/#!/?primary.color=252F4A&secondary.color=22B94E&view.left=0&view.right=1

 

Color Tool - Material Design

Create and share color palettes for your UI, and measure the accessibility of any color combination.

material.io

 

패키지 구성 및 필요한 기본 클래스들 추가 (Rx 기반)

component

 - BaseActivity

 - BasePresenter

 - BaseView

 - BaseViewModel

ext

 - LifecycleExtensions

 - RxExtensions

 - ViewExtensions

util

 - AutoClearedDisposable

ui

 - custom

 - main

Constants(optional)

App

 

 

Manifest Warning 제거

 - fullBackupContent 설정

 - MainActivity View action 추가

 

 

 

 

 


개발 구조

 

 

MVP, MVVM, Clean Architecure, Rx, LiveData, AAC, DataBinding, Coroutine, DI

위의 기술들을 어떻게 접목하느냐에 따라 천차만별의 구조가 나오게 된다.

 

심지어 MVP만 보아도 사용하는 방법다 가지각색이다.

Contract interface 안에 View, Presenter interface를 사용할 수도 있고

View만 추상화해서 사용할 수도 있다.

 

하지만 결국 몇가지 공통적인 문제들을 해결하기 위한 표현이 다를뿐이라고 생각한다.

 

1. 뷰와 모델, 그리고 이 둘을 어떻게 연결할까?

2. 비동기 처리

3. 응집도는 강하게 결합도는 약하게

 

3번이 너무 추상적이긴 하지만 이 정도가 안드로이드의 가장 근본적인 문제인 것 같다.

 

하지만 천차만별인 구조들 중에

하나의 완성된 프로젝트를 만들고 나면 다른 기술을 적용하는 것이 그리 어렵지 않다고 생각한다.

 

그러니 나만의 완성된 구조를 만들고 계속 날카롭게 다듬자.

 

 

 

 

 


테스트 코드

 

좋은 구조를 가지지 않는 이상 테스트 코드 많은 일을 만들어 낸다.

좋은 구조부터 갖자

 

 

 

 

 

 

'Android > Common' 카테고리의 다른 글

수명주기; Lifecycle  (0) 2020.06.22
비동기  (0) 2020.05.14
안드로이드 개발자 로드맵  (2) 2020.05.07

 

Github : https://github.com/junghun9102/DesignPatternTemplate

branch : feature/observer

 

 

junghun9102/DesignPatternTemplate

Contribute to junghun9102/DesignPatternTemplate development by creating an account on GitHub.

github.com

 

 

날씨 정보(WeatherData)의 변경에 따라

WeatherBroadcast에서 변경된 날씨를 예보하고

WeatherStatistics에서 데이터를 분석하기 위해 저장하고 누적데이터를 보여주려한다.

 

 


Commit 708033d9

 

WeatherBroadcast

 - update, broadcast

 

WeatherStatistics

 - update, collectData, showCumulativeData

 

WeatherData

 - setMeasurements, mesurementsChanged

 

전체 코드보다 위의 함수에만 집중하면 된다.

 

 

class WeatherBroadcast {

    private lateinit var weatherData: WeatherData

    fun update(weatherData: WeatherData) {
        this.weatherData = weatherData

        broadcast()
    }

    private fun broadcast() {
        StringBuilder()
            .append("Current Weather is ")
            .append("Temperature(").append(weatherData.temperature).append("), ")
            .append("Humidity(").append(weatherData.humidity).append("), ")
            .append("Pressure(").append(weatherData.pressure).append(")")
            .let(::println)
    }
}

 

class WeatherStatistics {

    private val temperatureData: HashMap<Long, Float> = HashMap()
    private val humidityData: HashMap<Long, Float> = HashMap()
    private val pressureData: HashMap<Long, Float> = HashMap()

    fun update(weatherData: WeatherData) {
        collectData(weatherData)
    }

    private fun collectData(weatherData: WeatherData) {
        val currentTime = System.currentTimeMillis()
        temperatureData[currentTime] = weatherData.temperature
        humidityData[currentTime] = weatherData.humidity
        pressureData[currentTime] = weatherData.pressure

        showCumulativeData()
    }

    private fun showCumulativeData() {
        StringBuilder()
            .append("========== Cumulative Weather Data ==========\n")
            .append("temperature : ").append(temperatureData).append('\n')
            .append("humidity : ").append(humidityData).append('\n')
            .append("pressure : ").append(pressureData).append('\n')
            .let(::println)
    }

    private fun getDateStr(time: Long) = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(Date(time))
}

 

 

WeatherBroadcast와 WeatherStatistics는 weatherData 변경에 따라 추가적인 일을 하려한다.

 

update가 되면

WeatherBroadcast는 방송하고(broadcast)

WeatherStatistics는 데이터를 누적하고(collectData)누적데이터를 보여준다(showCumulativeData).

 

 

 

 

class WeatherData(
    private val weatherBroadcast: WeatherBroadcast,
    private val weatherStatistics: WeatherStatistics
) {

    var temperature: Float = 0f
        private set
    var humidity: Float = 0f
        private set
    var pressure: Float = 0f
        private set

    fun setMeasurements(temperature: Float, humidity: Float, pressure: Float) {
        this.temperature = temperature
        this.humidity = humidity
        this.pressure = pressure

        measurementsChanged()
    }

    private fun measurementsChanged() {
        weatherBroadcast.update(this)
        weatherStatistics.update(this)
    }

    override fun toString() = "t $temperature, h $humidity, p $pressure"

}

 

 

WeatherData는 측정된 데이터를 받아 변경하고

WeatherBroadcast와 WeatherStatistics에게 변경된 데이터를 알려준다.

 

 

잘 작동하지만 WeatherData는 누구에게 데이터를 전해줘야할지 알고 있어야한다.

강한 의존성이 생겼다.

 

 

만약 설계상 추가적으로 데이터를 전달할 사용자가 생긴다면?

-> 계속해서 WeatherData에 손을 대야한다.

 

만약 동적으로 데이터를 받고 싶거나 받고 싶지 않은 사용자가 생긴다면?

-> 사용자를 nullable하게 만들어 처리할 수 있으나 사용자가 명확히 정해져야 한다.

 

 

 


Commit d1151ce0

 

interface Observer<T> {
    fun update(data: T)
}

 

WeatherForecast와 WeatherStatistics의 공통적인 코드를 Interface로 정의했다.

 

 

interface Subject<T> {
    val observers: MutableList<Observer<T>>

    fun registerObserver(observer: Observer<T>)
    fun removeObserver(observer: Observer<T>)
    fun notifyObservers()
}

 

WeatherData에서 사용자들을 각각 구현체로 가지고 있던 부분을 observers로 묶고

동적으로 새로운 사용자 등록하고 제거할 수 있는 함수를 만들었다.

또 기존에 사용자들에게 각각 update했던 부분을 notifyObservers로 만들었다.

 

 

 

날씨 정보를 사용하는 사용자를 Observer, 즉 관찰자라고 할 수 있다. WeatherBroadcast, WeatherStatistics

날씨 정보는 Subject, Observable(관찰할 수 있는) 등으로 부른다. WeatherData

 

JavaAPI에서 Observable과 Observer가 있었으나 deprecated 되었다.

 

 

 

 

class WeatherBroadcast(weatherData: Subject<WeatherData>) : Observer<WeatherData> {

    init {
        weatherData.registerObserver(this)
    }

    private lateinit var weatherData: WeatherData

    override fun update(data: WeatherData) {
        this.weatherData = data

        broadcast()
    }

    private fun broadcast() {
        StringBuilder()
            .append("Current Weather is ")
            .append("Temperature(").append(weatherData.temperature).append("), ")
            .append("Humidity(").append(weatherData.humidity).append("), ")
            .append("Pressure(").append(weatherData.pressure).append(")")
            .let(::println)
    }
}

 

class WeatherStatistics(weatherData: Subject<WeatherData>) : Observer<WeatherData> {

    init {
        weatherData.registerObserver(this)
    }

    private val temperatureData: HashMap<Long, Float> = HashMap()
    private val humidityData: HashMap<Long, Float> = HashMap()
    private val pressureData: HashMap<Long, Float> = HashMap()

    override fun update(data: WeatherData) {
        collectData(data)
    }

    private fun collectData(weatherData: WeatherData) {
        val currentTime = System.currentTimeMillis()
        temperatureData[currentTime] = weatherData.temperature
        humidityData[currentTime] = weatherData.humidity
        pressureData[currentTime] = weatherData.pressure

        showCumulativeData()
    }

    private fun showCumulativeData() {
        StringBuilder()
            .append("========== Cumulative Weather Data ==========\n")
            .append("temperature : ").append(temperatureData).append('\n')
            .append("humidity : ").append(humidityData).append('\n')
            .append("pressure : ").append(pressureData).append('\n')
            .let(::println)
    }

}

 

class WeatherData : Subject<WeatherData>{

    override val observers: MutableList<Observer<WeatherData>> = mutableListOf()

    var temperature: Float = 0f
        private set
    var humidity: Float = 0f
        private set
    var pressure: Float = 0f
        private set

    fun setMeasurements(temperature: Float, humidity: Float, pressure: Float) {
        this.temperature = temperature
        this.humidity = humidity
        this.pressure = pressure

        measurementsChanged()
    }

    private fun measurementsChanged() {
        notifyObservers()
    }

    override fun toString() = "t $temperature, h $humidity, p $pressure"

    override fun registerObserver(observer: Observer<WeatherData>) {
        observers.add(observer)
    }

    override fun removeObserver(observer: Observer<WeatherData>) {
        observers.remove(observer)
    }

    override fun notifyObservers() {
        observers.forEach {
            it.update(this)
        }
    }

}

 

 

WeatherForcast와 WeatherStatistics는 생성자에

Subject를 받아 등록하는 것 외에는 동일하게 동작한다.

 

 

WeatherData는 동적으로 사용자를 추가 제거할 수 있는 대가로 코드가 조금 늘었다.

하지만 더 이상 누가 사용하는지 알지 못한다.

특정 구현체를 알지 못하기 때문에 영향을 받지 않는다.

 

 

 


옵저버 패턴

한 객체가 바뀌면 그 객체에 의존하는 다른 객체들한테

연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.

 

 

 

'SoftwareDesign' 카테고리의 다른 글

SRP  (0) 2020.03.01
SOLID 개요  (0) 2020.03.01
2. Strategy  (0) 2020.01.07
1. Singleton  (0) 2019.12.25
DesignPattern 왜 알아야 할까?  (0) 2019.12.23

 

Github : https://github.com/junghun9102/DesignPatternTemplate

branch : feature/strategy

 

junghun9102/DesignPatternTemplate

Contribute to junghun9102/DesignPatternTemplate development by creating an account on GitHub.

github.com

 

이번에는 Strategy 패턴이다.

Strategy는 직역하면 전략인데 어떤 패턴일까?

 

다양한 오리들을 가지고 있는 시스템을 만들고 있다.

청둥오리, 고무오리, 미끼용 오리 등 다양한 오리들을 만들어보려고 한다.

 


Commit 4ee19cf6

 

abstract class Duck {

    open fun quack() {
        println("quack! quack!")
    }

    fun swim() {
        println("swimming")
    }

    open fun fly() {
        println("I believe i can fly")
    }

    abstract fun display()

}
class MallardDuck : Duck() {
    override fun display() {
        println("It seems like MallardDuck")
    }
}

 

대부분의 오리들은 "꿱"하고 소리를 낼 것이며 수영을 할 것이고 날 것이다.

단 모두 다르게 생겼기 때문에 display만 추상 메소드로 만들었다.

 

모든 오리들이 위의 형태를 만족하면 좋았겠지만 그렇지 않은 오리가 발생했다.

 

고무 오리는 소리를 낼 수 있고 수영할 수 있지만 날 수 없다.

미끼용 나무 오리(DecoyDuck)는 소리를 낼 수 없고 날 수 없다.

 

 

 

class RubberDuck : Duck() {
    override fun display() {
        println("It seems like RubberDuck")
    }

    override fun fly() {

    }
}
class DecoyDuck : Duck() {
    override fun display() {
        println("It seems like DecoyDuck")
    }

    override fun quack() {

    }

    override fun fly() {

    }
}

 

고무오리는 날 수 없기 때문에 fly 메소드를 아무행동을 하지 않도록 오버라이딩했고

미끼용 나무 오리는 quack, fly 메소드를 빈 몸통으로 오버라이딩했다.

일단은 원하는데로는 동작한다.

 

하지만 날지못하는 오리가 더 늘어난다면 날지 못하도록 모두 오버라이딩해야하고 중복코드가 발생한다. 

"날지못한다"처럼 몸통을 비워 처리해서 중복코드가 많이 없어 보이지만

많약 예외 케이스의 코드양이 많다면 중복코드 양이 늘어날 것이다.

 

또한 오버라이딩을 통해 많은 서브클래스에서 처리한다면 신경쓸 범위가 늘어나는 것이다.

 

변경은 어쩔 수 없이 일어날 수 밖에 없다.

어떻게 해야 그 변경을 잘 받아들일 수 없는 구조를 만들 수 있을까?

 

 

 


Commit cf0b9c56

 

 

문제를 해결하기 위해서는 변경이 일어나는 부분과 그렇지 않을 부분을 구분하는 것에서 시작한다.

 

변경이 일어나는 부분 : quack, fly

변경이 일어나지 않는 부분 : swim, display

 

1. 변경이 일어나는 코드를 각각 캡슐화 한다.

2. 인터페이스를 만들고 이를 상속해 클래스 집합을 만든다.

3. 그리고 구성을 통해 가지고 있으며 변경이 일어나는 메소드에서 사용한다.

4. 하위 클래스에서는 생성자에서 초기화하거나 중간에 동적으로 변경하거나 유연하게 사용할 수 있다.

 

Class Diagram

 

abstract class Duck {

    lateinit var quackBehavior: QuackBehavior
    lateinit var flyBehavior: FlyBehavior

    fun quack() {
        quackBehavior.quack()
    }

    fun fly() {
        flyBehavior.fly()
    }

    fun swim() {
        println("swimming")
    }

    abstract fun display()

}

부모 클래스는 이런식으로 수정이 되었다.

 

interface FlyBehavior {
    fun fly()
}
class FlyWithWings : FlyBehavior {
    override fun fly() {
        println("fly with wings")
    }
}
class FlyNoWay : FlyBehavior {
    override fun fly() {

    }
}

하나의 클래스 집합

 

class MallardDuck : Duck() {

    init {
        quackBehavior = Quack()
        flyBehavior = FlyWithWings()
    }

    override fun display() {
        println("It seems like MallardDuck")
    }
}
class DecoyDuck : Duck() {

    init {
        quackBehavior = MuteQuack()
        flyBehavior = FlyNoWay()
    }

    override fun display() {
        println("It seems like DecoyDuck")
    }
}

초기화하여 사용한다.

 

 

기능이 추가되어도 Interface를 상속받아 만들어 사용할 수 있다.

그 기능을 여러 곳에서 사용하여도 중복코드없이 구성할 수 있다.

 

또한 알고리즘이 변경이 되어도 사용하는 부분에 영향없이 수정이 가능한 장점이 있다.

 

 


스트래티지 패턴

알고리즘군을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다.

스트래티지를 활용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.

 

 

 

 

 

 

'SoftwareDesign' 카테고리의 다른 글

SRP  (0) 2020.03.01
SOLID 개요  (0) 2020.03.01
3. Observer  (0) 2020.01.19
1. Singleton  (0) 2019.12.25
DesignPattern 왜 알아야 할까?  (0) 2019.12.23

 

Github : https://github.com/junghun9102/DesignPatternTemplate

branch : feature/singleton

 

junghun9102/DesignPatternTemplate

Contribute to junghun9102/DesignPatternTemplate development by creating an account on GitHub.

github.com

 

싱글톤은 가장 쉽게 접할 수 있는 디자인 패턴이라고 생각한다.

 

1. 인스턴스를 하나만 가져야 할 때

2. 인스턴스를 하나를 초과해 만들 필요가 없을 때

사용한다.

 

예시 프로젝트로 세계관을 가진 게임을 만드려고 한다.

이 세계에는 유일신만이 존재한다.

그렇기 때문에 God이라는 클래스의 인스턴스는 하나만 존재해야한다.

 


Commit c5019208

 

class God private constructor(private val index: Int) {

    companion object {
        private var uniqueInstance: God? = null

        fun getInstance(): God {
            uniqueInstance ?: run {
                uniqueInstance = God(count++)
            }

            return uniqueInstance!!
        }

        private var count = 0
    }

    fun bless(townName: String) {
        println("$index God bless $townName")
    }
}

 

짠! 가장 기본적으로 많이 보는 싱글톤 코드라고 생각한다.

나만 이렇게 사용했을 수도 있지만...

 

가장 기본적인 원칙

1. 여러개의 인스턴스를 만들 수 없게 생성자를 제한한다.

2. 인스턴스를 받을 수 있는 메소드에서 하나의 인스턴스만 만들어 리턴해준다.

 

전혀 문제될 부분이 없어 보인다.

물론 나도 그런줄 알고 이렇게 사용해 왔고..

 

 

 

하지만 이 코드는 인스턴스를 둘 이상 만들 수 있다.

 

class World {
    private val aTown = Town("ATown")
    private val bTown = Town("BTown")

    fun run() {

        Thread {
            aTown.pray()
        }.start()

        Thread {
            bTown.pray()
        }.start()
    }
}
class Town(private val name: String) {
    fun pray() {
        God.getInstance().bless(name)
    }
}

 

두 개 이상의 스레드에서 동시에 getInstance를 호출하면

 

와 같이 두개의 인스턴스가 만들어 지는 경우가 있다.

 

두 스레드가 동시에 null 체크를 통과할 수 있기 때문이다.

 

물론 비동기를 사용하지 않는다면 발생하지 않는다.

 

 

 


Commit 25874942

 

class God private constructor(private val index: Int) {

    companion object {
        private var uniqueInstance: God? = null

        @Synchronized
        fun getInstance(): God {
            uniqueInstance ?: run {
                uniqueInstance = God(count++)
            }

            return uniqueInstance!!
        }

        private var count = 0
    }

    fun bless(townName: String) {
        println("$index God bless $townName")
    }
}

 

딱 한 줄이 늘었다.

 

Synchronized 키워드를 통해

동시 접근할 수 없게 만들었기 때문에 인스턴스가 두개 만들어질 걱정은 없다.

 

하지만 동기화 키워드의 값을 비싸다.

그리고 처음 초기화를 위해 동기화 키워드가 필요로 하지만

초기화 이후에도 동기화로 인해 손해를 보고 있다.

 

 


Commit 18eff495

 

class God private constructor(private val index: Int) {

    companion object {

        @Volatile
        private var uniqueInstance: God? = null

        fun getInstance() = uniqueInstance ?: run {
            synchronized(God) {
                uniqueInstance ?: run {
                    uniqueInstance = God(count++)
                }
                uniqueInstance!!
            }
        }

        private var count = 0
    }

    fun bless(townName: String) {
        println("$index God bless $townName")
    }
}

 

null일 때만 동기화 블록에서 동시에 초기화할 수 없게 막아두었다.

 

이를 DCL(Double-Checking Locking)이라고 한다.

여기까지가 Head First Design Pattern에 나오는 Singleton이다.

 

 

하지만 싱글톤에 대해 찾아보다 DCL 역시 자바에서 문제가 생길 수 있다는 포스팅을 보았다.

 

uniqueInstance = God(count++)

 

uniqueInstance 에는 주소값이 할당되었지만 God 생성자가 돌지 않은 상태에서

다른 스레드가 getInstance를 호출해 not-null 상태인 uniqueInstance를 리턴하지만 제대로 된 객체를 받지 못한다는 이야기였다.

이에 관해 깊은 이해가 없어 마지막에 링크를 첨부했다.

 

  


Commit 42282b09

 

class God private constructor(private val index: Int) {

    companion object {

        private var count = 0

        private var uniqueInstance: God = God(count++)

        fun getInstance() = uniqueInstance
    }

    fun bless(townName: String) {
        println("$index God bless $townName")
    }
}

 

그렇기 때문에 getInstance 자체에 동기화를 거는 방식을 택하거나

위의 코드와 같이 전역변수를 선언하고 선언과 함께 초기화하는 방식으로 사용하면 된다.

 

사실 멀티스레드에서 싱글톤 객체를 경쟁적으로 호출하거나

늦은 초기화를 하는게 이점이 될만큼 무거운 싱글톤 객체를 사용해본 적이 없어

어떤 방식이든 상관이 없긴했다.

 

 

 

 


 

자바에서 DCL에서 생기는 문제점 포스팅

https://gampol.tistory.com/entry/Double-checked-locking%EA%B3%BC-Singleton-%ED%8C%A8%ED%84%B4

 

Double-checked locking과 Singleton 패턴

난이도 : 초급 Peter HaggarIBM 2002 년 5 월 01 일 2003 년 1 월 07 일 수정 모든 프로그래밍 언어에는 고유의 이디엄이 있다. 이중 대부분이 유용하다. 문제는 몇몇 이디엄의 경우 원래 표명했던 것이 아니라는..

gampol.tistory.com

 

Volatile 포스팅

https://nesoy.github.io/articles/2018-06/Java-volatile

 

Java volatile이란?

 

nesoy.github.io

 

 

 

 

'SoftwareDesign' 카테고리의 다른 글

SRP  (0) 2020.03.01
SOLID 개요  (0) 2020.03.01
3. Observer  (0) 2020.01.19
2. Strategy  (0) 2020.01.07
DesignPattern 왜 알아야 할까?  (0) 2019.12.23

 

 

프로그래밍을 잘 한다는 것은 무엇일까?

 

우선 만들고자 하는 기능을 만들 수 있어야 한다.

이건 너무 당연한 이야기이고

 

버그가 없어야 한다.

유지보수에 용이하게 변경과 확장에 유연해야 한다.

코드가 깔끔해야 한다.

성능이 좋아야 한다.

 

사람마다의 기준이 다를 것이고 추구하는 방향성이 다를 것이다.

 

그 많은 기준 중에 프로젝트를 구조적으로 잘 만들고 싶은 욕심이 있다면

공부해야하는 것이 디자인 패턴이라고 생각한다.

 

어떻게 하면 더 유연성이 있는 코드를 만들 수 있을까 고민하고 정의한 것을

패턴으로 만든 것이다.

 

머리가 너무 좋아서 추상클래스, 인터페이스, 상속, 포함 관계로

상황에 맞는 유연성 있는 좋은 코드를 만들 수 있다면 공부할 필요가 없다.

 

하지만 그렇지 않다면 상황에 맞는 패턴들을 공부하고 그 상황이 나왔을 때

사용함으로 더 좋은 코드를 만들 수 있다.

더 나아가 기존의 패턴에서 발전시킨 자신만의 패턴을 만들 수도 있을 것이다.

 

이미 알게모르게 많이 사용하는 패턴이라 이해하기 쉬울 수도 있을 것이고

그렇지 않은 경우에 와닿지 않는 패턴들도 있을 것이다

 

해당 디자인 패턴이 어떤 상황에서 필요한지 정리하고

꾸준히 그 패턴을 인지하고 있다면 언젠가 도움이 될 것이다.

 

 

 

Head First Desgin Pattern을 가지고 정리하려 한다.

 

Github에 패턴별로 branch를 나누고

Commit을 통해 어떻게 무엇을 개선하기 위해 발전했는지 과정을 보여주려고 한다.

 

코드를 돌려보며 확인하면 좋을 것 같다.

 

 

 

 

 

Strategy

알고리즘군을 정의하고 각각을 캡슐화하여 바꿔 쓸 수 있게 만든다. 스트래티지 패턴을 이용하면 알고리즘을 활용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있다.


Observer

한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의

 

Decorator

객체에 추가적인 요건을 동적으로 첨가한다. 데코레이터는 서브클래스를 만드는 것을 통해서 기능을 유연하게 확정할 수 있는 방법을 제공한다.


Factory

Factory

객체 생성하는 부분을 캡슐화하여 사용한다.

Factory Method

객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 만든다. 

Abstract Factory

인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 구상 클래스로 지정하지 않고도 생성할 수 있습니다.

 

Singleton

해당 클래스의 인스턴스가 하나만 만들어지고, 어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴


Command

요구 사항을 객체로 캡슐화할 수 있으며, 매개변수를 써서 여러 가지 다른 요구 사항을 집어넣을 수도 있다. 또한 요청 내역을 큐에 저장하거나 로그를 기록할 수도 있으며, 작업취소 기능도 지원 가능


Adapter

한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 다른 인터페이스로 변환한다. 어댑터를 이용하면 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있다.

 

Facade

서브시스템에 있는 일련의 인터페이스에 대한 통합 인터페이스를 제공한다.


Template Method

메소드에서 알고리즘의 골격을 정의한다. 알고리즘의 여러 단계 중 일부는 서브클래스에서 구현할 수 있다. 템플릿 메소드를 이용하면 알고리즘의 구조는 그대로 유지하면서 서브클래스에서 특정 단계를 재정의할 수 있다.


Iterator

컬랙션 구현 방법을 노출시키지 않으면서도 그 집합체 안에 들어있는 모든 항목에 접근할 수 있게 해 주는 방법을 제공해 준다.

EX) ArrayList, HashSet


Composite

객체들을 트리 구조로 구성하여 부분과 전체를 나타내는 계층구조로 만들 수 있다. 이 패턴을 이용하면 클라이언트에서 개별 객체와 다른 객체들로 구성된 복합 객체를 똑같은 방법으로 다룰 수 있다.

EX) Android의 View와 ViewGroup


Compound

객체의 내부 상태가 바뀜에 따라서 객체의 행동을 바꿀 수 있다. 마치 객체의 클래스가 바뀌는 것과 같은 결과를 얻을 수 있다.


Proxy

어떤 객체에 대한 접근을 제어하기 위한 용도로 대리인이나 대변인에 해당하는 객체를 제공하는 패턴

 

디자인 패턴의 공통점은 변하지 않는 본질을 추상화하는 것

 

 

 

 

'SoftwareDesign' 카테고리의 다른 글

SRP  (0) 2020.03.01
SOLID 개요  (0) 2020.03.01
3. Observer  (0) 2020.01.19
2. Strategy  (0) 2020.01.07
1. Singleton  (0) 2019.12.25

 

저는 commit할 때, merge 후 conflict 해결할 때만 IDE에서 작업하고

나머지는 Terminal에서 명령어로 처리합니다.

정리한 명령어들은 필수적으로 사용하는 것 같습니다.

 

 


Git 프로젝트 시작

git init : 처음 깃 설정
git clone : 원격 저장소에서 로컬에 복사


git remote add origin [git address] : 원격 저장소 주소를 연결

 

 


변경 후 원경 저장소에 업로드

git add * : 모든 변경 파일에 대해 인덱스에 저장
git commit -m "commit description" : 커밋 

git push origin [branch name] : 원격 저장소에 없는 경우 원격 저장소에 올리기
git push : 원격 저장소에 업데이트

 

 


브랜치 관리

git branch : 로컬 브랜치 리스트
git branch -r : 원격 브랜치 리스트
git branch -a : 모드 브랜치 리스트

git checkout [branch name] : 변경
git checkout -b [branch name] : 만들고 변경
git checkout -t [branch name]

git branch -D [branch name] : 로컬 브랜치 삭제
git push origin :[branch name] : 원격 브랜치 삭제


git merge [branch name] : 현재 브랜치에 지정 브랜치를 합침

 

 


브랜치 복구, 변경사항 임시저장

git stash : 하던 작업 임시 저장
git stash pop : 저장했던 작업 복구

git reset --hard [commit key] : 커밋 당시 형상으로 복구
git reset --soft [commit key] : 커밋 당시로 HEAD는 이동하지만 변경 코드는 남아있음

 

저는 작업 브랜치를 실수해서 다른 브랜치에서 작업하고 커밋한 경우

git reset --soft [전 커밋] 으로 돌아가고

git stash

git checkout [코드가 붙어야할 브랜치]

git stash pop

git commit

이렇게 사용합니다.

 

 


원격 저장소에서 받아오기

git pull : 해당 브랜치 변경사항 받아오기
git fetch : 브랜치 정보 업데이트

 

 


기타

git tag [tag name] : 중요한 커밋에 태그를 붙여 표시한다.

보통 master branch 에 release branch를 합치고 표시하는 방식으로 쓴다. 아마?

'Common' 카테고리의 다른 글

PorterDuff  (0) 2020.08.18
암호화와 해시  (0) 2020.06.19
Android Studio/IntelliJ 단축키 (Window)  (0) 2019.08.17
시간 표시  (0) 2019.06.16

 

제가 주로 사용하는 단축키

정리하면서 추가적으로 필요해 보이는 단축키들로 정리했습니다.

 


일반

ALT + 1 : Project

ALT + 6 : Logcat

ALT + 9 : Version Control

ALT + F12 : Command 창

CTRL + ALT + S : IDE Settings

CTRL + ALT + SHIFT + S : Project Settings

CTRL + ALT + Y : 동기화

CTRL + SHIFT + F12 : 편집기 최대화/최소화

 

 


Studio 내에서 탐색 및 검색

SHIFT 두번 : 모든 항목 검색

CTRL + R : 바꾸기

CTRL + SHIFT + R : 전체 파일에서 바꾸기

CTRL + SHIFT + N : 파일 찾기

CTRL + F : 찾기

CTRL + SHIFT + F : 경로에서 찾기

CTRL + F12 : 파일 구조 팝업 열기

CTRL + F4 : 활성 편집창 탭 닫기

ALT + LEFT/RIGHT : 탭 이동

CTRL + ALT + LEFT/RIGHT : 이전/이후 커서로 이동

ESC : 도구 창에서 편집기 창으로 돌아가기

 

 


코드 쓰기

ALT + Insert : 거의 모든 생성

CTRL + O : 메서드 재정의

CTRL + I : 메서드 구현

CTRL + +/- : 코드 블록 접기/펼치기

CTRL + D : 선택 블록 복사

CTRL + Space : 기본 코드 완성

CTRL + Q : 빠른 문서 조회

CTRL + P : 매개변수 표시

CTRL + B : 선언/사용으로 이동

CTRL + / : 줄 주석

CTRL + SHIFT + / : 블록 주석

CTRL + W : 계속 증가하며 코드 블록 선택

CTRL + SHIFT + W : 현재 선택 블록에서 영역 감소

CTRL + [ : 코드 블록 시작으로 이동

CTRL + ] : 코드 블록 끝으로 이동

CTRL + ALT + O : import 최적화

CTRL + J : 스마트 선 이음

SHIFT + ENTER : 새 줄 시작

ALT + UP/DOWN : 블록 단위 이동

CTRL + SHIFT + UP/DOWN : 현재/선택 영역 줄 바꿈

F2 : 다음 강조 표시된 오류

 

 


빌드 및 실행

CTRL + F9 : 빌드

SHIFT + F10 : 현재 선택되어 있는 작업 실행

CTRL + SHIFT + F10 : 현재 커서가 있는 곳에서 작업 실행

ALT + SHIFT + F10 : 실행 가능한 리스트 표시

 

 


ETC

CTRL + K : Git Commit

 

 

 

'Common' 카테고리의 다른 글

PorterDuff  (0) 2020.08.18
암호화와 해시  (0) 2020.06.19
Git 기본 명령어  (0) 2019.08.19
시간 표시  (0) 2019.06.16

+ Recent posts