사실 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()
}
}
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는 동적으로 사용자를 추가 제거할 수 있는 대가로 코드가 조금 늘었다.
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. 하위 클래스에서는 생성자에서 초기화하거나 중간에 동적으로 변경하거나 유연하게 사용할 수 있다.
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를 상속받아 만들어 사용할 수 있다.
그 기능을 여러 곳에서 사용하여도 중복코드없이 구성할 수 있다.
또한 알고리즘이 변경이 되어도 사용하는 부분에 영향없이 수정이 가능한 장점이 있다.
스트래티지 패턴
알고리즘군을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다.
스트래티지를 활용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.
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")
}
}