앱을 개발하다보면, 99% 확률로 리스트 를 만나게 될 것 입니다. 리스트 없는 앱? 진짜 1% 에 속하려나요...? 있긴..하겠죠?
오늘은 List 를 그리는 3가지 방법을 알아볼까 합니다.
단순히 스크롤이 가능하게 하기 위한 Scroll 위젯들은 이번에 다루지는 않습니다. ^^
ListView 를 그리는 방법은 총 3가지가 있으며, 이 3가지는 각기 다른 차이가 존재합니다.
그리고 앞으로는 몰라서, 귀찮아서, 그냥 이렇게 하라길래 가 아닌 정확한 차이를 알고 썼으면 좋겠습니다.
먼저, 세가지 방법에 대해서 코드와 실행결과를 확인해주시면 될 것 같아요~!
builder 를 이용하여 짜여진 ListView
separated 를 이용하여 짜여진 ListView
그렇다면, 이렇게 짜여진 세가지 방법은 서로 어떤 차이가 있는 걸까요?? 그리고 어떤 상황에서, 대체 뭘 써야 할까요????
ListView
ListView.builder
ListView.separated
스크롤 가능 여부
가능
가능
가능
구분자 적용의 편안함
불편함
보통
편함
렌더링 시점
첫 생성시 모든 아이템 렌더링
동적으로 필요에 따른 렌더링
동적으로 필요에 따른 렌더링
특정 아이템만 리렌더링
불가능
가능
가능
성능
하
상
상
언제 사용?
모든 아이템을 생성시에 미리 렌더링해놓고, 그 아이템 개수가 적을 때 사용
동적으로 아이템을 삽입/삭제 해야 하거나, 아이템 개수가 많은 경우에 사용
동적으로 아이템을 삽입/삭제 해야 하거나, 아이템 개수가 많으면서 아이템과 아이템 사이에 구분이 필요한 경우에 사용
ListView.builder 와 ListView.separated 의 차이가 구분자 처리 하나뿐이라면 ListView.builder 는 구분자 처리를 못하나요?? -> 그렇지 않습니다. ListView.builder 도 구분자 처리를 할 수 있습니다. 오히려 구분자가 매우 단순하다면, ListView.builder 가 좋습니다.
ListView.builder 보다 ListView.separated 가 항상 더 좋은거 아닌가요???? 구분자가 해당 위젯을 비워두어도 되니까??? -> 그렇지 않습니다.
1. 구분자를 그려야하는지 말아야하는지 판단하기 위한 내부로직이 동작해야하는데, 이때 구분자를 그릴필요없다면, 이는 불필요한 오버헤드로 이어집니다.
2. 각 구분자가 매우 단순하거나, 동일한 경우에는 ListView.builder 가 좋습니다. 각 구분자가 100개까지는 A 타입, 100개 이후부터 B타입 등으로 다른 구분자를 필요로 하는 등 복잡한 구분자라면, ListView.separated 가 좋습니다.
3. 구분자도 결국 메모리를 필요로 합니다. 리스트의 아이템개수가 너무 많은 경우에 이러한 구분자를 위한 메모리도 그만큼 늘어납니다.
저는 왠만하면 ListView.builder 혹은 ListView.separated 둘중 하나를 사용하는 것을 추천합니다. 그와중에도
구분자가 복잡한 디자인을 가지고 있는 경우, 구분자가 index에 따라서 다르게 처리되는 경우 엔 ListView.separated 를 추천합니다.
구분자가 단순히 회색선, 모든 구분선이 동일 한 경우에는 ListView.builder 를 추천합니다.
Flutter 의 핵심은 위젯기반의 UI 툴킷 입니다. ( Android 에서 Compose 도 UI 툴킷인것 처럼) 하나의 코드로 여러 플랫폼에서 같은 결과... 네이티브와 유사한 성능 .. 렌더링이 어쩌구.. 다른것도 많지만 일단은 꼭 알아두세요! Flutter 의 핵심기능중 하나는 위젯 기반으로 UI 를 그리는 것 입니다.
StatefulWidget , StatelessWidget 이 뭐고 FunctionWidget 은 쓰면 안되나요?
StatefulWidget 과 StatelessWidget 은 Flutter 를 개발하려면 반드시 만나게 되는 기초중의 기초 Widget 입니다.
결론부터 말씀드리자면, StatefulWidget 과 StatelessWidget 가 아닌 그냥 Widget 은 직접 정의해서 사용하지 말라! 입니다.
StatefulWidget 은 상태(Status) 를 저장하는 Widget 입니다.
StatelessWidget 는 상태(Status) 를 저장하지 않는 Widget 입니다.
상태(Status) 를 저장한다? 저장하지 않는다?
플러터는 Widget 을 기반으로 렌더링을 하면서 UI 를 그리게 됩니다.
완전 느린 컴퓨터가 있고, 이미지 1장 그릴때마다 1초가 걸린다고 생각해봐요 이미지 1장을 1초에 걸쳐서 그렸고, 새로운 이미지를 추가해서 1장을 더 그리려는데, 갑자기 이미 그렸던 이미지를 다시 1초에 걸쳐서 다시 그리고, 그다음 이미지를 1초에 걸쳐서 또 그려요.. 그렇게 3장 4장 추가하다보면, 화면 하나 다 그리는게 3초, 4초 로 늘어나겠죠?
우리는 당연히 "이미 그렸는데, 얘를 왜 다시그림?" 이라고 생각할 수 있지만, 컴퓨터는 생각보다 멍청해서 "이럴때 그리라고 했지, 너가 이미 그린경우엔 다시 그리지말라고는 안했잖아. 시킨대로 그리는거야"
그래서 플러터에서는 "다시 그리지 않아야 하는 경우" 를 알려주기 위해서 상태(Status) 를 사용해요. 상태(Status) 를 저장해두고, 이걸 보면서 다시 그릴지 말지 결정하고, 상태(Status) 를 저장하지 않는 경우에는 한번 그려지면, 다시그리지 않아요.
StatefulWidget다시 그려야하는 시점을 파악하고, 때에 따라 다시 그리기 위해 상태(Status) 를 저장하는 Widget 입니다.
StatelessWidget 는 한번 그리면 다시 그리지 않을거여서상태(Status) 를 저장하지 않는 Widget 입니다.
FunctionWidget 은 Widget getItem() { ... } 처럼 Widget 자체를 반환하는 함수라고 하겠습니다.
1. 만약 여러분이 만든 StatefuleWidget 에서 상태가 변경되면 (setState 호출), 자식트리에 있는 StatefulWidget, StatelessWidget, FunctionWidget 전부 다시 그리려고 시도합니다.
2. 다시 그리려고 시도는 하지만, 내부적으로 const 로 선언된 Widget 은 다시 그리지 않고, 재사용을 하게됩니다. ( 다시 그리려고 시도한다고해서 무조껀 다시 그리는게 아니라는 거죠! )
3. 하지만 FunctionWidget 인 경우에는 얘기가 달라집니다. FunctionWidget 내부에 사용된 모든 Widget( 자식트리포함) 은 재사용을 하지 않습니다. FunctionWidget 안에서 return Text('123') 처럼 단일 StatelessWidget 하나라면 성능상으로 차이를 못느낄 수 있겠지만, 우리가 개발을 하다보면, 위젯 하위에 위젯. 그리고 그것을 확장하고, 또 다른 위젯을 파생시키는 등... 엄청나게 변경가능성이 크죠?
4. 따라서 FunctionWidget 은 그냥 사용하지 않는 것이 좋습니다. (최소 Production 에서는 절대 쓰지 않는다고 생각하는게 좋아요)
너의 뇌피셜아니야? 알고보니 그냥 써도 성능상 차이 없는거 아니야? 플러터 팀에서 직접 만든 영상을 보고 스스로 판단하셔도 좋습니다!
저는 커넥팅더닷츠 에서 째깍악어 앱 Android 개발자 였습니다~ 2021년 6월 ~ 22023년 3월 까지 Android ( with Kotlin) 으로 앱 개발을 담당했고, 2023년 3월 ~ 2023년 6월 동안 그동안 만들어서 상용중이던 앱을 Flutter 로 전환(QA 이후 7월초 게시)하였습니다. (사실상.. 저와 제 개발인생 첫 후임 둘이서 전환을 했습니다. - 시작전 Flutter 지식 보유 Zero. )
기존 앱 개발자 (다른동료) 들은 전부 기간내에 불가능하다... 이 소식을 들은 전 CTO 분께서도 불가능하다.. 솔직히 기운 빠지는 말들이 참 많았습니다...
그럼에도 불구하고 시작한 이유는 저와 후임 모두 최종적으로 "우리는 할 수 있고, 우리니까 할 수 있었다." 라고 말할 수 있는 자신감이 있었던 것 같습니다. (실패했을때 잃는 것보다 성공했을때 얻는게 훨씬 큰 것 도 사실이지 않나....? )
사실 Android 개발자였던 저희 둘은 Flutter 가 힘들기보단, iOS 때문에 많이 힘들었습니다. ( 아무래도 기존 상용앱을 전환하는거라, 모든 기능이 다 탑재되어야 했고, 네이티브 기능을 필요로 하는 경우에는 직접 플러그인을 만들어야 하는 것들... Push Service 가 Nhn Toast, Channel Talk, Braze(Marketing Tool) 처럼 여러곳에 대한 Notification ... 이런것들이 참 어려웠죠
최종 Page 개수는 약 140 Page (Navigator 전환 기준) .. 하나하나 기능을 나열하면 정말 너무 길어서.. 생략하고..
그럼에도 불구하고, 저희가 전환시킨 앱은 2024년이 되기 전까지! 약6개월의 기간동안 Server 이슈가 아닌 순수 앱에서 발생한 이슈는 10건 미만! (그마저도 서비스 자체에 영향을 끼치지는 않는 위험도가 낮은 이슈)
그렇게 째깍악어(선생님) 앱 은 Flutter 전환이 성공적으로 되었습니다.
이제는 보상이라도 받는걸까요? 버그를 너무 없앴을까요? 시간이 여유롭네요.. ㅋㅋ 그동안 겪고, 개발이후에도 공부했던 것들을 토대로 다시 블로그를 시작해볼까 합니다~!
3. 아직 실무에 Compose 를 도입하지 않았다면, JetPack Compose 를 준비하기에는 정말 좋은 시기 입니다.
4. Compose 는 직관적이고, 개발 가속화를 위하여 설계되었습니다. (훨씬 적은 코드로 UI 를 개발할 수 있습니다.)
5. 기기성능은 향상되고, 앱에 대한 기대가 커진만큼 UI는 훨씬 동적이고, 표현이 풍부해졌습니다.
6. Views 로도 충분히 만들어낼 수 있지만, 최신 아키텍처를 기반으로 하고 있는 Kotlin 을 활용하는 현대적 도구키트를 원하는 의견으로 인해 Jetpack Compose 가 등장했습니다.
7. Jetpack Compose 는 선언적 독립형 도구 키트입니다.
8. 선언적(Declarative) : 요즘 앱은 데이터가 동적이고, 실시간으로 업데이트 됩니다. 기존에는 xml 에 UI 를 선언하고, 데이터가 바뀌면 UI 도 업데이트 해주어야 합니다. 변형도 필요합니다. 이 과정을 위하여 View를 조회하고, 속성을 설정해야 합니다. 애플리케이션 상태가 바뀔때마다 (데이터베이스, 네트워크 호출 로드, 사용자 상호작용 등) 새로운 정보를 이용하여 UI 를 업데이트하여 데이터를 동기화해야합니다. View 마다 상태가 다르고, 각각 업데이트 해야하므로 그 과정이 복잡하고, 모델과 UI 를 동기화 하는 과정에서 버그가 엄청나게 발생할 수 있습니다. 이는 온전히 개발자가 책임지고 모든걸 업데이트 해야합니다. 앱과 UI 가 복잡해진 만큼, 당연히 오류가 생기기가 더 쉬워졌습니다.
Compose 는 xml 방식과 다른 방식을 사용합니다. Compose 는 "상태를 UI 로 변환합니다." UI 는 변경이 불가능하고, 한번 생성되면 업데이트가 불가능합니다. 앱상태가 바뀌면, 새로운 상태를 새로운 표현으로 변환합니다. 상태에 따른 데이터 동기화 문제가 완전히 해결됩니다. UI 전체를 다시 생성하기 때문입니다. 물론, Compose 는 매우 지능적이고 효율적이어서 변경되지 않은 요소에 대한 작업은 건너뜁니다. 그러나 개념적으로 특정 상태에 따라 UI 를 새로 생성하는 것과 같습니다.
코드는 그저 특정 상태에 따른 UI 형태를 설명할뿐, 생성 방법을 지정하지 않습니다.
Q. 어떻게 상태를 UI 로 변환할까? A. Compose 에서 UI 구성요소는 구성가능한 주석이 달린 함수일 뿐입니다. 그래서 UI 구성 요소를 빠르고 쉽게 생성 할 수 있습니다.
따라서, 재사용가능한 요소로 구성된 라이브러리로 UI 를 나누는 것이 좋습니다. (중복코드 방지)
위 함수는 값을 반환하는 것이 아닌, UI 를 전달합니다. (Compose 라이브러리의 Column 과 Text Composable 사용합니다.)
Text 를 수직으로 배열하고, 간단한 Text Lable 을 표시합니다.
만약, 특정 조건에 따라서 요소를 표시하려면, 즉 message 가 없을때 Lable 을 표현하고 싶다면, if 문을 추가하면 됩니다.
@Composable
fun MessageList(messages: List<String) {
Column {
if (messages.size == 0) {
Text(text = "No messages")
} else {
messages.forEach { message ->
Text(text = message)
}
}
}
}
Composable 은 매개변수를 받을 수 있고, 사실 받아야 합니다.
데이터를 UI 로 변환한다는 것이 이런 뜻입니다. Composable 은 데이터를 함수 매개변수로 받아서 UI 를 전달합니다.
이렇게되면, UI 가 동기화 상태에서 벗어나지 않습니다. (메시지가 없다는 Text 를 삭제하지 않는 등의 실수를 하지 않을 수 있습니다.) -> 메시지데이터가 있다면, 메시지가 없는 UI 를 제거하고, 메시지가 있는 UI 를 보여주어야하는데, 둘다 보여주는 실수를 하지 않을 수 있다. 는 것입니다. 상태가 바뀌었을때, 이 함수를 실행한다면 새로운 UI 가 생성됩니다. 이러한 행위를 리컴포징(recomposing)이라고 합니다.
메시지 목록은 어떻게 바뀌는가?
-> 콜 스택을 처리하는 동안 ViewModel 이 메시지의 LiveData 를 노출합니다. 이 데이터를 관찰할 수 있고, message 필드를 읽는 Composable 은 새로운 데이터가 입력될때마다 리컴포저블(recomposable) 됩니다
직접 감시 객체를 설정할 필요가 없습니다. Compose 컴파일러는 어느 Composable 이 상태를 읽는지 추적하고, 상태가 바뀌면 자동으로 다시 실행합니다. 여기서 Compose 가 지능적이기 때문에 입력이 변경된 Composable 만 다시 실행하고, 나머지는 건너뜁니다.
@Composable
fun ConversationScreen() {
val viewModel: ConversationViewModel = viewModel()
val messages by viewModel.messages.observeAsState()
MessageList(messages)
}
@Composable
fun MessageList(messages: List<String) {
...
}
위 코드를 보면 이전과 달린 viewModel.messages.observe { ... } 는 필요없고, Compose 컴파일러가 observeAsState() 로 된 messages 를 추적하기 때문에, 해당 상태가 바뀌면 자동으로 ConversationScreen() 를 다시 실행하고, 이 과정에서 입력이 변경된 MessagesList(message: List<String) 도 다시 실행됩니다. 만약, 변경된 데이터가 없는 Composable 함수는 건너뛰는 것으로 이해하면 될 것 같습니다.
각 Composable 은 변경할 수 없습니다. Composable 을 참조하거나, 나중에 쿼리하거나, 내용을 업데이트 할 수 없습니다.
정보입력을 원할땐, 모두 매개변수로 Composable 에 전달해야 합니다.
하지만, 모든 Composable 이 고정되어 있는 것은 아닙니다. 아래 코드가 그 예시입니다.
해당 내용은 김진태님의 강의에서 배운 내용을 요약 한 것입니다. 요약된 내용보다 강의내용은 훨씬 방대하고, 훌륭했음을 미리 알려드립니다.
코드품질 확보가 왜 필요한가?
<문제 Case1>
<문제 Case2>
<문제 Case3>
즉, 코드품질이 확보되지 않으면 나쁜코드 양산의 악순환이 발생하고, 나쁜코드는 다음과 같은 악순환을 야기한다.
나쁜코드 양산
변경시 엉뚱한 곳에서 문제 발생
생산성 저하
프로젝트 인력 추가 투입
시스템 설계를 이해하지 못함
설계 의도와 상반되는 변경 코드 작성
소스코드 품질을 확보하기 위해서 어떻게 할 수 있는가?
클린코드 : 코드를 사람이 이해할 수 있도록 작성 및 개선하는 활동
코드리뷰 : 프로젝트 참여자들이 소스코드에 대해 토론하고, 잠재된 결함을 찾는 활동
리팩토링 : 결함제거, 코드 및 아키텍처 구조 개선 등 기능의 변경없이 코드를 개선하는 활동
단위 테스트 : 작성된 함수/메소드가 정사적인 동작을 수행하는지 확인하기 위해 테스트 코드 작성 및 결함 수정을 수행하는 활동
정적분석 : 소스코드를 분석하는 도구를 이용하여 컴파일 단계에서 잠재된 결함을 찾는 활동
ch01. 클린코드
사람이 쉽게 이해할 수 있도록 코드를 작성하고, 지속적으로 개선하는 활동
비야네 스트롭스트룹(C++창시자) : 나는 우아하고 효율적인 코드를 좋아한다. 논리가 간단해야 버그가 숨어들지 못한다. 의존성을 최대한 줄여야 유지보수가 쉬워진다. 오류는 명백한 전략에 의거해 철저히 처리한다. 성능을 최적으로 유지해야 사람들이 원칙 없는 최적화로 코드를 망치려는 유혹에 빠지지 않는다. 클린 코드는 한 가지를 제대로 한다.
그래디 부치(OOAD with Application 저자) : 클린 코드는 단순하고 직접적이다. 클린코드는 잘 쓴 문장처럼 읽힌다. 클린 코드는 결코 설계자의 의도를 숨기지 않는다. 오히려 명쾌한 추상화와 단순한 제어문으로 가득하다.
데이브 토마스 (이클립스 전략의 대부) : 클린 코드는 작성자가 아닌 사람도 읽기 쉽고 고치기 쉽다. 의미있는 이름이다. 특정한 목적을 달성하는 방법은 하나만 제공한다. 의존성은 최소이며 각 의존성을 명확히 정의한다. API는 명확하며 최소로 줄였다. 때로는 정보 전부를 코드만으로 명확하게 드러내기 어려우므로 언어에 따라 문학적 표현이 필요하다.
존 제프리 (Exterme Programming Installed의 저자) : 클린 코드는 중복이 없다. 표현성이 뛰어나다. 작게 추상화되어 있다.
마틴파울러 : 컴퓨터가 이해하는 코드는 어느 바보나 다 짤 수 있다. 훌륭한 프로그래머는 사람이 이해할 수 있는 코드를 짠다.
사람이 이해하는 코드란?
가독성이 뛰어나다.
간단하고 작다.
의존성을 최대한 줄였다.
의도와 목적이 명확한 코드.
타인에 의해 변경이 용이한 코드
중복이 없는 코드
개체(Class, Method)가 한가지 작업만 수행하는 코드
일반적인 SW 개발은 기존 코드에서 시작 ( 기존코드에서 발전 )
따라서, 코드를 Write 하는 시간보다 Read 하는 시간이 더 많다.
SW 가 3년정도 지나면, Feature 가 향상, 수정 등으로 SW 가 진화됨. 이런 과정에서 코드가 변질되기 시작하고, 최초 의한 설계에서부터 멀어지기 시작함.
그러나 현실은 BaseCode 에 필요한 작업을 추가할뿐, BaseCode 자체를 개선하는 활동을 하지 않으면서 코드는 Dirty Code 가 되고, SW 복잡도향상을 가속화시킨다.
이런경우 SW 노후화가 되고, SW 노후화는 품질을 급격하게 떨굼.
어떻게 해야 사람이 이해하는 코드를 보다 쉽게 작성할 수 있을까?
이름짓기 : 변수, 클래스, 인수, 상수, 패키지, 파일, 디렉토리 등 이름을 명확하게 지어야 한다.
의도를 분명히 하라 : 불필요한 주석을 제거하라. (주석이 필요없도록 코드를 작성하라.)
보통 주석은 코드가 무엇을 하는가? 로 작성하는데, 여기서 문제는 코드가 수정되어도 주석이 수정되지 않는 경우가 많고, 이경우 오히려 주석이 코드해석을 방해하는 경우가 생긴다.
그릇된 정보를 피하라 : 길고 흡사한 이름은 피하라.
함수명이 길지만 몇글자 다른경우. : IDE 자동완성에서 실수의 여지가 있음.
문맥에 맞는 단어를 사용하라.
이해하는데 시간을 소모하지 않기 위해 일관성 있는 단어를 사용하라.
함수를 작게 만들어라
함수 자체는 작게 만들기 위해 이름을 명확하게 지었지만, 함수 내의 코드가 상세하게 작성되면서 추상화 레벨이 확 낮아지면서 작성되는 경우가 있다.
한가지만 하도록 만들어라
하나의 함수 내에 작성된 코드가 추상화 수준이 하나인 단계만 수행하라.
인수를 적게 하라.
사람은 숫자 5까지 직관적으로 파악이 가능한데, 3개 이상의 인수를 직관적으로 파악할 수 없다. ( type : name, type : name, type: name) type 과 name 의 합이 총 6개가 되면서 직관적인 파악이 어렵다. 즉, 직관적으로 인지하기 위해 3개 이상의 인수는 피하라.
중복하지 마라.
변경 시 여러 부분을 손대야 한다. 오류가 발생할 확률이 높아지면서 수정이 어려워진다.
ch02. UML 기초
UML (Unified Modeling Language) : 소프트 웨어 개발 과정에서 산출되는 산출물을 명세화, 시각화 문서화 하기 위한 표준 모델링 언어
방법론과 UML 은 다르다.
방법론은 어떠한 작업을 할 때 이러저러한 절차를 가지고 작업을 하면 된다. 라는 것이면,
UML 은 어떤 방법론을 적용하더라도, UML 정의에 의거하여 동일한 결과물이 나온다.
UML 구성요소
Things (사물)
Relation (관계)
Diagram (다이어그램)
Things(사물)
ClassName + Attribute + Operation() 으로 이루어짐.
ClassName : 클래스나 객체가 가져하는 이름
Attribute : 클래스나 객체가 가져야하는 정보 (값, 변수)
Operation : 클래스의 인스턴스인 객체의 행위를 나타냄
: Public
: Private
: Protected,
<<>> : Sterotype == Memo (우측상단이 접혀있는 사각형)
일반화 : 상속의 개념
실체화 : 인터페이스
연관 : 관계가 있음.
방향성을 가진 직접연관 : 의존성 - A에서 경우에 따라 B가 메모리에 탑재 - 약결합
집합연관 : A 가 메모리에 탑재된 이후에 B가 메모리에 탑재 ( new ) - 중결합
복합연관 : A와 B의 LifeCycle 이 동일함 ( Constructor ) - 강결합
Relationship(관계)
public class Shape {
private Origin origin;
protected void move(){}
public void resize(){}
public void display(){}
}
public class Circle extends Shape {
private float radius;
}
public class Polygon extends Shape {
private List points;
@Override
public void display() {}
}
위 코드를 UML 로 표현한 경우
public class User {
public Scedule makeSchedule() {
return new Schdule();
}
public String getScheduleDate(Schedule schdlue) {
String date = schedule.getDate();
return date;
}
}
위 코드를 UML 로 표현한 경우
public interface OutputInterface{}
public class Monitor implements OutputInterface {
...
}
위 코드를 UML 로 표현한 경우
public class Teacher {
private List<Student> students;
}
위 코드를 UML 로 표현한 경우
public class Laptop {
private Mouse wheelMouse;
}
위 코드를 UML 로 표현한 경우
public class Window {
private Frame mainFrame;
public Window() {
mainFrame = new Frame();
}
}
위 코드를 UML 로 표현한 경우
Diagram(다이어그램)
UML 을 정밀하게 하는 것은 프로그래밍하는 행위가 그 비용이 발생하기 때문에 전부 쓰진 않고 자주 쓰이는 것들이 있다.
UseCase Diagram : 행위
Class Diagram : 구조적
Activity Diagram : 행위
Sequence Diagram : 행위
Component Diagram : 행위
Deployment Diagram : 구조적
ch03~04. OOAD
객체지향 : 현실세계를 객체와 그들간의 상호관계로 이해
즉, 코드를 작성할때, 그것을 명령어들의 모음으로 보는 것이 아니라 현실세계를 객체로 이해한것처럼 코드를 객체로 이해하여 작성
장점
재사용성 향상
생산성 향상
확장 및 유지보수성 향상 : 변경은 어렵고 확장은 쉬워야 한다.
객체지향 구성요소
객체
실세계에 존재하는 것 : 트럭, 자동차 //누구나 찾을 수 있다.
개념으로 존재하는 것 : 공정, 주문 //도메인 지식이 있어야 한다.
소프트웨어 세계에만 나타는 것 : 연결리스트, 변수 //소프트웨어 지식이 있어야 한다.
클래스
동일 범주의 객체들의 특성(상태, 행위)을 정의
객체들의 추상화된 형태
추상화란, 복잡한 자료,모듈,시스템 등으로부터 핵심적인 개념 또는 기능을 간추려 내는 것을 말한다.
관계
객체와 객체, 클래스와 클래스의 관계
객체지향 특성
캡슐화
외부 호출자로부터 내부의 동작이나 데이터를 숨기는 것 (가시성)
상속
부모클래스에 기능을 추가 또는 변경(확장) 하여 새로운 자식 클래스를 생성하는 것
여러 클래스에 중복 또는 비슷한 코드가 존재하면 상속으로 해결할 가능성이 있다.
다형성
동일한 이름의 오퍼레이션이 그 오퍼레이션이 정의된 클래스에 따라 각기 다른 행동을 수행해야 함
draw() 는 모두 동일하게 사용되지만, 실제 동작하는 구현은 서로 다르게 가능하다
오버로딩(같은이름, 서로 다른 파라미터)과 오버라이딩(재정의)으로 해결할 수 있다.
4+1 View : 객체지향 설계를 위하여 알아야 함.
Logical View : 아키텍처가 어떤것인지 등, 기능에 초점을 두고 있음
Process View : 시스템이 동작할때 어느정도의 퍼포먼스를 두고있는지에 초점을 두고 있음.
Implementation View : 소프트웨어를 매니지먼트 할때 사용
Deployment View : 시스템 토폴로지(네트워크 요소들을 물리적으로 연결해둔 것)에 초점을 두고 있음.
Use-Case View : 시스템이 사용자에게 제공할 기능에 초점을 두고 있음. (요구사항)
순서는 Use-Case View → Logical View → Process or Implementation View → Deployment View
Use-Case View
시스템이 사용자에게 제공할 기능을 상호작용을 중심으로 봄
시스템 전체의 요구사항을 표현(단, 비기능 요구사항은 제외)
UML Diagram : Usecase diagram, Activity diagram, + class diagram(필요시)
Use Case Modeling
시스템에 요구되는 행위를 파악하여 표현하기 위한 방법
사용자 관점의 시스템 행위를 의사소통하기 위한 방법
시스템과 상호작용하는 외부 사용자 또는 시스템을 식별함
시스템의 범위 표현
프로젝트 계획의 도구
Use Case Model 작성 순서
Actor 를 식별하고 설명 기술
<Actor를 찾기 위한 질문>
누가 시스템을 사용하는가
누가 시스템으로부터 정보를 취득하는가
누가 시스템에 정보를 제공하는가
누가 시스템을 운용하는가
시스템과 연동되는 외부 시스템이 있는가
Use Case 를 식별하고 설명 기술
<Use Case 찾기 위한 질문 : 각 Actor 에 대해 다음 질문>
시스템이 수행하기를 바라는 기본 작업은 무엇인가
Actor가 시스템의 데이터를 등록/수정/삭제 하는가? 왜?
Actor는 외부변경을 시스템에 알릴 필요가 있는가?
Actor는 시스템 내에서 발생된 사건에 대해 알 필요가 있는가?
Actor는 시스템을 구동시키거나 종료시키는가?
<비즈니스프로세스에서 식별>
Use Case 명세 작성 (전체 Use Case의 우선순위를 파악하고, 우선순위별 명세 작성)
Use Case 구조화 (Include/Extend 관계 파악)
Use Case 작성시 발생하는 흔한 실수
사용과 관련된 시나리오를 쓰기 보다는 기능적 요구사항을 표현하려 한다.
액터와 시스템의 상호작용에 초점을 두지 않는다.
시스템 반응은 무시한 채, 사용자의 요구만 기술한다.
대안 흐름을 생략한다.
Usecase 내부 처리 방법에 대해 언급한다.
Use Case View 에서 사용하는 Activity diagram 예시
Logical View
시스템이 사용자에게 제공할 기능과 핵심적인 아키텍처 요소를 표현
3 Level 정도로 class / package 가 혼용되어 나타날 수 있다.
정적인 구조(Structure)를 표현할때는 Analysis Class 의 구조를 분석하며 Class Diagram 을 사용하여 도식한다.
동적인 구조(Behavior)를 표현할 때는 Analysis Class 의 행위를 분석하며 Sequence Diagram or Collaboration Diagram 을 사용하여 도식한다.
간단하게, Logical View 는 Use-Case View 로부터 시작되어 정제된 class diagram 이 나오는 것
만약 다음과 같은 조건으로 User 라는 Class 를 만들어야 하는 경우를 생각해 보겠습니다.
1. 이름을 의미하는 문자열 Type 의 name filed 를 포함한다. 2. 나이를 의미하는 정수 Type 의 age filed 를 포함한다. 3. 생성하는 순간에 name 과 age를 parameter 로 전달받고, 전달받은 name 과 age 로 초기화한다. 4. 한번 생성된 User라는 객체의 name 과 age filed 는 read only 이다. 5. 출력을 위하여 toString() 을 호출하는 순간 User{name='value', age=value} 형태로 출력한다. 6. 값을 비교하기 위하여 eqauls() 를 구현한다.
7. 객체의 값이 일치하다면, 같은 주소값을 반환하는 hashCode() 를 구현한다.
<Java 로 구현하기>
public class User {
private final String name;
private final int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age &&
Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
<Kotlin 으로 구현하기>
data class User(
val name: String,
val age: Int
)
1. toString()
Java 에서는 기본적으로 객체의 toString() 은 이름@[16진수로 표시한 hashcode] 를 반환합니다. 만약, 위에서 작성한 User 라는 Java class 에서 hashCode() 를 아래와 같이 수정한다면,
출력결과는 User@0 이 됩니다. 따라서 Java 에서는 toString() 을 Override 하여 원하는 출력결과를 명시해야합니다. (IDE 가 좋아졌기 때문에, 자동완성 기능으로 조건에서 언급한 기준처럼 작성은 되지만, 반드시 Override 해야하는 것은 변하지 않습니다.)
@Override
public int hashCode() {
return 0;
}
그러나 Kotlin 의 data class 은 이를 사용자가 직접 작성하지 않더라도, 조건에서 언급한 기준으로 동작합니다. 만약 Kotlin 의 data class 에서 toString() 을 아래와 같이 Override 하는 순간, Java 의 객체 toString() 과 동일하게 동작하여 이름@[16진수로 표시한 hashcode] 를 반환합니다.
override fun toString(): String {
return super.toString()
}
2. eqauls() Objecet 의 equals() 는 주소값 비교입니다. 즉, 만들어진 객체가 완전히 같은 주소값을 가리키고 있으면 true, 아니면 false 를 반환합니다.
흔히들 Java 의 equals 는 값비교, == 은 주소비교 라고 하지만, 재정의된 Class 에 한에서만 위와 같이 동작합니다.
* String 에서 equals 는 이미 재정의되어 있기 때문에 값비교가 성립하는 것입니다. 모든 Class 가 성립하지 않습니다.
Kotlin data class 는 이러한 equals 가 이미 재정의되어 값비교를 할 수 있게 되어 있습니다.
즉, Kotlin 의 data class 에서 아래와 같이 사용자가 직접 Override 하는 순간, 그 조건이 Object 의 주소비교가 됩니다.
override fun equals(other: Any?): Boolean {
return super.equals(other)
}
3. hashcode() 객체를 식별할 수 있는 값을 반환합니다. (주소값은 유니크해서 일반적으로 주소값을 반환하지만, 주소값이 아닐 수 있습니다. 중요한건 해당 값은 "유니크" 해야 한다는 점입니다.)
"유니크" 하다는 것은 Key <-> Value 로 치면, Key 로 접근했을 때 항상 동일한 Value 에 접근할 수 있어야 한다는 것입니다. 그러나, 주소값만 반환한다면, 논리적으로 동일한 값을 가졌음에도 불구하고 새로 생성하는 객체마다 늘 다른 값을 반환합니다.
이러한 이유로 Java 의 class 는 hashcode 를 Override 하여 논리적으로 같을 경우 같은 hashcode를 반환하도록 작성하고,
Kotlin 의 data class 는 해당경우에 같은 hashcode 를 반환하도록 되어 있습니다.
Java Class
Kotlin data Calss
toString()
Override 하지 않으면 이름@16진수 hashcode 반환
Override 할 때 super.toString() 만 작성하면 이름@16진수 hashcode 반환
Override 하지 않으면 자동생성되는 String 을 반환
Override 할 때 super.toString() 만 작성하면 이름@16진수 hashcode 반환
equals()
Overrdie 하지 않으면 객체가 주소값이 같은지만 판단
Overrride 할 때 super.equals(other) 만 작성하면 역시 객체가 완전히 같은지만 판단
Override하지 않으면 객체의 값이 같은지를 판단
Override 할 때 super.equals(other) 만 작성하면 역시 객체가 완전히 같은지만 판단
hashcode()
Override 하지 않으면 서로 다른 객체의 value 가 같더라도, 다른 유니크값을 반환합니다.
Override 할 때 super.hashCode() 를 작성하면 서로 다른 객체의 value 가 같더라도, 다른 유니크값을 반환합니다.
Override 할 때 value 를 포함하여 작성하면 서로 다른 객체의 value 가 같으면, 같은 유니크값을 반환합니다.
Override 하지 않으면 value 를 포함하도록 작성되어있어서 서로 다른 객체의 value 가 같으면, 같은 유니크값을 반환합니다.
Override 할 때 super.hashCode() 를 작성하면 서로 다른 객체의 value 가 같더라도, 다른 유니크값을 반환합니다.
Override 할 때 value 를 포함하여 작성하면 서로 다른 객체의 value 가 같으면, 같은 유니크값을 반환합니다.
* 값이 같은지?? 논리적으로 같은지?? 어짜피 equals 가 true 이면 그 값이 동일하다는 건데, hashcode 도 true 아닌가요???????
User 라는 class 와 그 내용이 완전히 같은 User2 라는 클래스가 있다고 가정하겠습니다. 아래처럼 eqauls() 는 값 뿐만 아니라 class 도 확인합니다. 즉 생성할때 object 값이 같더라도, class 가 다르면 서로 다르다고 판단합니다.
그러나, hashCode() 는 논리적으로 같음을 보기 때문에, 생성하는 class 가 다르더라도, object 값이 같다면, 서로 같은 유니크값을 반환하게 됩니다.
val a = User("DevHyeon", 28)
val b = User("DevHyeon", 28)
val c = User2("DevHyeon", 28)
//true
println(
a == b
)
//true
println(
a.hashCode() == b.hashCode()
)
//false
println(
a == c
)
//true
println(
a.hashCode() == c.hashCode()
)
너무나도 헷갈리고, 평소에 자주 사용하지 않는 한 나중에 다시보면 아! 이랬지! 싶은 내용입니다만, 헐? 그런거야? 보다는 아! 이랬지!
AndroidProject 를 진행해본 사람이라면, 누구나 익숙한 아래와 같은 코드를 보았을 것입니다.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
그리고 우리는 이러한 Activity 에서 매번 반복되는 것을 조금더 쉽게 하고자, 상속을 받은 Activity 를 사용하곤 합니다. 예를 들면, 아래와 같이 Toast 를 띄우기 위해 매번 생성하는 수고를 덜기 위하여 BaseActivity 에서 생성 및 보여줌을 담당하고 해당 BaseActivity 를 상속받은 Activity 는 문자열만을 전달하여 호출한다고 하겠습니다.
class BaseActivity : AppCompatActivity() {
fun showShortToast(message: String) {
Toast(this).apply {
setText(message)
duration = Toast.LENGTH_SHORT
}.show()
}
}
class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
showShortToast("onCreate()")
}
}
당장의 코드는 굉장히 편해보이고, 쉽게 사용할 수 있을 것 같지만,
BaseActivity , ToastActivity, BackActivity, SafetyActivity 등등... 작성자가 순간에 필요하다고 판단한것을
작성해 나가다 보면, 이후 Project 에 참여한 사람이나, 함께 코드를 작성하던 동료들이
"그래서 어떤 Activity 를 상속받으란거지?" "이 기능을 하는 코드가 상위 클래스중에 있는게 있나?" "이거 없겠지~ 그냥 작성하지뭐~"
라는 생각들이 겹치면, 결국 이도저도 아닌 중복코드가 나오게 되고, 누군가는 상속받아 사용하고, 누군가는 그냥 새롭게 작성하여 사용하면서 코드 추적도 어려워지게 되는 것 같습니다.
상속받아서 사용해야 하는 상위 클래스가 정확히 어떤기능을 하는지, 정말 상속받아서 사용할정도의 필요함이 있어서 누구나 쉽게 파악이 가능하도록 짜는것이 가장 좋겠지만
그것이 어렵고, 제대로 관리가 안되는 것이라면
BaseActivity , ToastActivity, BackActivity, SafetyActivity 등등... 무분별하게 사용되는 것은 "지양" 하고 싶다는 생각이 들었습니다.
역시 난이도는 신규개발보다 기존코드를 유지보수하며 추가해나가는 것이 더 어려운것 같습니다.. 그리고 그러한 과정중에 더 많은것을 느끼고, 배우는게 아닐까란 생각도 듭니다..
(지금의 제 생각이 틀릴 수도 있겠지만, 그 조차 정답에 찾아가기 위한 스텝이라고 생각하며.. 마치겠습니다.)