} else if (rq.action == "삭제") {
val id = rq.getParamValueAsInt("id", 0)
if (id == 0) {
println("id를 정확히 입력해주세요.")
continue
}
val wiseSaying = wiseSayings.firstOrNull() { it.id == id }
if (wiseSaying == null) {
println("${id}번 명언은 존재하지 않습니다.")
continue
}
wiseSayings.remove(wiseSaying)
println("${id}번 명언을 삭제하였습니다.")
}
val id = rq.getParamValueAsInt("id", 0)
-> id 값을 가져옴 (id가 없으면 0 반환)
if (id == 0)
-> id가 0이면 "id를 정확히 입력해주세요." 출력 후 continue , 삭제 진행 X
val wiseSaying = wiseSayings.firstOrNull() { it.id == id }
-> wiseSayigs 리스트에서 id가 일치하는 첫 번째 요소 찾기
-> 없으면 null 반환
wiseSaying == null
-> "id번 명언은 존재하지 않습니다." 출력 후 continue, 삭제 진행 X
wiseSayings.remove(wiseSaying)
-> 찾은 명언을 리스트에서 제거
✅
id확인 → 존재하는지 체크 → 리스트에서 삭제 → 성공 메시지 출력
class WiseSayingController {
private var lastId = 0
private val wiseSayings = mutableListOf<WiseSaying>();
fun actionWrite(rq: Rq) {
print("명언 : ")
val content = readlnOrNull()!!.trim()
print("작가 : ")
val author = readlnOrNull()!!.trim()
val id = ++lastId
wiseSayings.add(WiseSaying(id, content, author))
println("${id}번 명언이 등록되었습니다.")
}
fun actionList(rq: Rq) {
if (wiseSayings.isEmpty()) {
println("등록된 명언이 없습니다.")
return
}
println("번호 / 작가 / 명언")
println("----------------------")
wiseSayings.forEach {
println("${it.id} / ${it.author} / ${it.content}")
}
}
fun actionDelete(rq: Rq) {
val id = rq.getParamValueAsInt("id", 0)
if (id == 0) {
println("id를 정확히 입력해주세요.")
return
}
val wiseSaying = wiseSayings.firstOrNull() { it.id == id }
if (wiseSaying == null) {
println("${id}번 명언은 존재하지 않습니다.")
return
}
wiseSayings.remove(wiseSaying)
println("${id}번 명언을 삭제하였습니다.")
}
// fun actionModify(rq: Rq) {
//
// }
}
class App {
fun run() {
val wiseSayingController = WiseSayingController()
val systemController = SystemController()
println("== 명언 앱 ==")
while (true) {
print("명령) ")
val input = readlnOrNull()!!.trim()
val rq = Rq(input)
when (rq.action) {
"종료" -> {
systemController.actionExit(rq)
break
}
"등록" -> wiseSayingController.actionWrite(rq)
"목록" -> wiseSayingController.actionList(rq)
"삭제" -> wiseSayingController.actionDelete(rq)
}
}
}
}
각 기능을 메서드로 분리
이렇게 분리하면 뭐가 좋은가?
-> 코드의 재사용성을 높이고, 가독성을 개선하며, 유지보수를 쉽게 만든다.
fun actionDelete(rq: Rq) {
val id = rq.getParamValueAsInt("id", 0)
if (id == 0) {
println("id를 정확히 입력해주세요.")
return
}
val wiseSaying = wiseSayings.firstOrNull() { it.id == id }
if (wiseSaying == null) {
println("${id}번 명언은 존재하지 않습니다.")
return
}
// 여기서부터는 wiseSaying 변수가 nullable이 아니다.
// 스마트 캐스트
wiseSayings.remove(wiseSaying)
println("${id}번 명언을 삭제하였습니다.")
}
스마트 캐스트는 Kotlin에서 변수를 별도의 캐스팅 없이
안전하게 사용할 수 있도록 해주는 기능이다.
wiseSaying 변수는 firstOrNull { it.id == id } 를 사용해서 찾았기 때문에
nullable 타입 (wiseSaying?)이 된다.
val wiseSaying = wiseSayings.firstOrNull { it.id == id }
만약 id에 해당하는 명언이 없으면? null 이 반환
그러므로 wiseSaying 은 WiseSaying? (nullable) 타입
if (wiseSaying == null) {
println("${id}번 명언은 존재하지 않습니다.")
return
}
// 여기서부터는 wiseSaying 변수가 nullable이 아니다.
// 스마트 캐스트
wiseSayings.remove(wiseSaying)
wiseSaying == null 인지 체크
if(wiseSaying == null) { return } 이 실행되면 함수가 종료
즉 , wiseSaying 이 null 인 경우 이후 코드 실행 ❌
wiseSaying 이 null 이 아니라는 것이 확정
if 문을 통과한 이후에는 wiseSaying 이 null 이 될 수 없음
따라서, Kotlin이 자동으로 wiseSaying 을 WiseSaying 타입으로 변환 (캐스트)
명시적 캐스팅 없이 wiseSaying 을 사용할 수 있음
원래 nullable 타입을 사용할 때는 wiseSaying!! 같은 명시적 캐스팅이 필요할 수 있음
하지만, 스마트 캐스트 덕분에 안전하게 non - null 타입으로 간주되고,
그냥 wiseSaying 을 사용할 수 있음
만약 스마트 캐스트가 없었다면, wiseSaying 이 nullable 타입이므로
!! 나 ?. 를 사용
wiseSayings.remove(wiseSaying!!) // 강제 캐스트
wiseSaying!! 을 사용하면 null이 들어오면서 예외 발생 (NullPointerException)wiseSaying?.let {
wiseSayings.remove(it)
}
wiseSaying 이 null 이 아닐 때만 remove(it) 실행
하지만 이미 null 체크를 했기 때문에 불필요한 코드
스마트 캐스트 덕분에 불필요한 캐스팅 없이 안전하게 변수를 사용할 수 있음
Kotlin이null체크를 통과한 변수를 자동으로 non - null 타입으로 캐스팅해주므로!!같은 강제 캐스트 없이wiseSaying을 바로 사용 가능
fun actionModify(rq: Rq) {
val id = rq.getParamValueAsInt("id", 0)
if (id == 0) {
println("id를 정확히 입력해주세요.")
}
val wiseSaying = wiseSayings.firstOrNull() { it.id == id }
if (wiseSaying == null) {
println("${id}번 명언은 존재하지 않습니다.")
return
}
println("명언(기존) : ${wiseSaying.content}")
print("명언 : ")
val content = readlnOrNull()!!.trim()
println("작가(기존) : ${wiseSaying.author}")
print("작가 : ")
val author = readlnOrNull()!!.trim()
wiseSaying.update(content, author)
println("${id}번 명언이 수정되었습니다.")
}
println("명언(기존) : ${wiseSaying.content}")
println("작가(기존) : ${wiseSaying.author}")
기존에 있던 명언, 작가는 wiseSaying 에서 content 불러오기
val content = readlnOrNull()!!.trim()
null 이 될 수 없다고 내가 보장한다 라는 뜻
fun update(content: String, author: String) {
this.content = content
this.author = author
}
수정을 위한 update 메서드
class WiseSayingService {
private var lastId = 0
private val wiseSayings = mutableListOf<WiseSaying>()
fun write(content: String, author: String): WiseSaying {
val id = ++lastId
return WiseSaying(id, content, author).apply {
wiseSayings.add(this)
}
}
fun isEmpty(): Boolean {
return wiseSayings.isEmpty()
}
fun findAll(): List<WiseSaying> {
return wiseSayings
}
fun findById(id: Int): WiseSaying? {
return wiseSayings.find { it.id == id }
}
fun delete(wiseSaying: WiseSaying) {
wiseSayings.remove(wiseSaying)
}
fun modify(wiseSaying: WiseSaying, content: String, author: String) {
wiseSaying.modify(content, author)
}
}
fun actionWrite(rq: Rq) {
print("명언 : ")
val content = readlnOrNull()!!.trim()
print("작가 : ")
val author = readlnOrNull()!!.trim()
val wiseSaying = wiseSayingService.write(content, author)
println("${wiseSaying.id}번 명언이 등록되었습니다.")
}
fun actionList(rq: Rq) {
if (wiseSayingService.isEmpty()) {
println("등록된 명언이 없습니다.")
return
}
println("번호 / 작가 / 명언")
println("----------------------")
wiseSayingService.findAll().forEach() {
println("${it.id} / ${it.author} / ${it.content}")
}
}
fun actionDelete(rq: Rq) {
val id = rq.getParamValueAsInt("id", 0)
if (id == 0) {
println("id를 정확히 입력해주세요.")
return
}
val wiseSaying = wiseSayingService.findById(id)
if (wiseSaying == null) {
println("${id}번 명언은 존재하지 않습니다.")
return
}
wiseSayingService.delete(wiseSaying)
println("${id}번 명언을 삭제하였습니다.")
}
fun actionModify(rq: Rq) {
val id = rq.getParamValueAsInt("id", 0)
if (id == 0) {
println("id를 정확히 입력해주세요.")
}
val wiseSaying = wiseSayingService.findById(id)
if (wiseSaying == null) {
println("${id}번 명언은 존재하지 않습니다.")
return
}
println("명언(기존) : ${wiseSaying.content}")
print("명언 : ")
val content = readlnOrNull()!!.trim()
println("작가(기존) : ${wiseSaying.author}")
print("작가 : ")
val author = readlnOrNull()!!.trim()
wiseSayingService.modify(wiseSaying, content, author)
println("${id}번 명언이 수정되었습니다.")
}
표준 입력과 출력을 리다이렉트하여 테스트를 진행하는 방식인 두 코드가 있는데
차이점이 궁금해서 알아봤따
object TestRunner {
private val originalIn: InputStream = System.`in`
private val originalOut: PrintStream = System.out
fun run(input: String): String {
// 표준 입력 리다이렉팅
// 키보드 입력 => 문자열 입력
System.setIn(
ByteArrayInputStream(
("${input.trimIndent()}\n종료")
.toByteArray()
)
)
// 표준 출력 리다이렉팅
// 콘솔 출력 => 문자열 출력
val outputStream = ByteArrayOutputStream()
val printStream = PrintStream(outputStream)
System.setOut(printStream)
App().run()
// 표준 출력 결과를 문자열로 변환
val result = outputStream
.toString()
.trim()
.replace(Regex("\\r\\n"), "\n") // 개행문자 차이 표준화
// 다시 표준 입력으로 복구
System.setIn(originalIn)
// 다시 표준 출력으로 복구
System.setOut(originalOut)
return result
}
}
object TestRunner {
private val originalIn: InputStream = System.`in`
private val originalOut: PrintStream = System.out
fun run(input: String): String {
val formattedInput = input
.trimIndent()
.plus("\n종료")
return ByteArrayOutputStream().use { outputStream ->
PrintStream(outputStream).use { printStream ->
try {
System.setIn(
ByteArrayInputStream(
formattedInput.toByteArray()
)
)
System.setOut(printStream)
App().run()
} finally {
System.setIn(originalIn)
System.setOut(originalOut)
}
}
outputStream
.toString()
.trim()
.replace("\r\n", "\n")
}
}
}
표준 입력 (System.in) 리다이렉트
ByteArrayInputStream 을 사용해서 키보드 입력 대신
문자열을 입력하도록 설정
마지막에 "종료" 를 추가하여 프로그램이 종료되도록 유도
표준 출력 (System.out) 리다이렉트
ByteArrayInputStream 을 사용하여 콘솔 출력이 문자열로 저장되도록 변경출력 결과 가공
outputStream.toString().trim().replace("\r\n", "\n") 을 사용해기본적으로 프로그램 실행 (App().run()) 후 원래 상태 복구
System.setIn(originalIn) , System.setOut(originalOut) 을 호출하여// 표준 입력 리다이렉팅
System.setIn(
ByteArrayInputStream(
("${input.trimIndent()}\n종료") // input을 정리하고 마지막에 "종료" 추가
.toByteArray()
)
)
// 표준 출력 리다이렉팅
val outputStream = ByteArrayOutputStream()
val printStream = PrintStream(outputStream)
System.setOut(printStream)
App().run()
// 표준 출력 결과를 문자열로 변환
val result = outputStream
.toString()
.trim()
.replace(Regex("\\r\\n"), "\n") // 개행문자 표준화
// 표준 입력/출력 복구
System.setIn(originalIn)
System.setOut(originalOut)
return result
System.setIn() → System.setOut() → App().run() 실행
→ 그 후 원래대로 복구
출력값을 result 변수에 저장한 후 반환
개행문자 변환을 정규식 (Regex("\\r\\n")) 을 사용해서 처리
val formattedInput = input
.trimIndent()
.plus("\n종료") // input을 정리하고 "종료" 추가
return ByteArrayOutputStream().use { outputStream -> // ByteArrayOutputStream을 use 블록에서 사용
PrintStream(outputStream).use { printStream -> // PrintStream을 use 블록에서 사용
try {
System.setIn(ByteArrayInputStream(formattedInput.toByteArray()))
System.setOut(printStream)
App().run()
} finally {
System.setIn(originalIn)
System.setOut(originalOut)
}
}
outputStream
.toString()
.trim()
.replace("\r\n", "\n")
}
use 블록을 사용하여 ByteArrayOutputStream 과 PrintStream 을
자동으로 close
try-finally 를 사용하여 System.setIn() 과 System.setOut() 을
원래대로 복구
출력값을 outputStream.toString() 에서 직접 반환 (별도의 변수 저장 없이)
개행문자 변환을 정규식 없이 replace("\r\n", "\n") 로 처리
| 차이점 | 첫 번째 코드 | 두 번째 코드 |
|---|---|---|
| 리소스 관리 | 명시적으로 System.setIn()/setOut() 복구 | use 블록과 try-finally 사용하여 자동 복구 |
| 출력값 반환 방식 | result 변수에 저장 후 반환 | outputStream.toString()에서 직접 반환 |
| 개행 문자 변환 방식 | 정규식 (Regex("\\r\\n")) 사용 | 단순 replace("\r\n", "\n") 사용 |
| 가독성 | 코드가 길고 명시적 | use 블록으로 더 깔끔하고 안전 |
결론은 2번째 코드가 더 안전하고 가독성이 좋다
use 블록을 활용해서 자동으로 스트림을 닫고 (리소스 관리 자동화)
try-finally 를 사용하여 System.setIn() , System.setOut() 을 안전하게 원복하고,
개행 문자 변환도 정규식 없이 단순 replace() 사용으로 더 간결해진다
하지만 첫 번째 코드도 동작하는 데는 문제없음
👉 다만 "리소스를 명시적으로 관리해야 하는 코드" 라서 좀 더 길고
복잡한 느낌이 있다