🐗점진적 리팩터링🐗
강의 자료에 있는것들을 다 배워서 제이슨의 라이브코딩으로 우리를 혼내줬다.
라이브코딩을 보고있는데 흡사 마술쇼를 보는것 같았다.
내 입장에서 빨라서 잘인식을 못했고 (다른 애들은 잘 이해함 킹받네) 기억마저 가물가물하지만 일단 남는거라도 정리해본다.
1.리펙터링할 부분의 복제본을 만들고 수정해서 대체하라
로또를 통해 이 상황의 예시를 들자면(수업에서 로또로 했으니까)
리펙토링이 필요한 코드에서 로또번호로 Int값을 그냥 사용하고 있다고 가정해보자
이번 리펙토링에서 원시값을 포장해서 Int를 LottoNumber 로 바꿔줘야하는 상황이다.
이 상황에서 함수에서 인자로 받던 Int값을 LottoNumber로 바꾼다면 함수의 사용처에서 다 터져버릴것이다.
만약 작은 로또앱이라면 그냥 바꾸고 빨간줄 뜬곳가서 교체하는데 얼마 안걸리겠지만
규모가 큰 상용앱이라면 사용처가 몇개가 될지도 모르고 외부에서도 사용하고 있을지도 모른다.
예를들어 함수의 사용처가 1000000개 라고 가정해보자 하루안에 다 일일히 바꿔줄수 있겠는가?
퇴근못하고 야근하고 해결할때까지 집을 못가면 리펙터링에 대한 혐오감이 쌓일것이다.
이런 혐오감을 없애기위해 점진적인 리펙터링이 필요한것이다.
그래서 어떻게 문제가 안생기면서 점진적으로 리펙터링이 가능하게 할수있는가?
예를들어 로또 번호를 Int 자료형의 인자로 받고있는 있는 match 라는 함수가 있다고 가정하자
로또번호를 Int로 사용하고 있으니 리펙터링의 대상이 될것이다.
그럼 그냥 같은 내용을 복붙해서 match2를 만든다.
(함수 오버로딩을 이용해서 같은 이름을 써도되지만 명시적으로 리펙토링중임을 명시하기위해 이름을 바꾸는것이 좋지않을까 싶다.)
만약 match 함수에 하위함수들이 있다면 다 똑같이 2를 붙여서 복붙해준다.
그후 match2의 인자를 LottoNumber로 바꿔주고 그에 맞게 로직을 수정해준다.
그럼 이상황에서는 match 가 살아있기 때문에 빌드하는데 전혀 문제가 없다.
-> 이상황을 과도기적 단계라고 한다.
match2를 내가 원하는 대로 다 변경했다면 이제 match 의 사용처를 암살자마냥 하나하나 찾아가서 match2로 변경해준다.
테스트 코드, 프로덕션 코드 모두 하나하나씩 돌아다니면서 바꿔주면 되는것이다.
이렇게 match2로 다 대치되어서 match 함수가 사용하지 않게되면(회색으로 떠있으면) 기존 match 함수를 삭제하고
match2를 match 함수로 개명시켜주면 리펙토링이 완료되는것이고
나는 저녁있는 삶을 점진적 리펙토링을 통해 만끽하며 퇴근 할수있는것이다.
2. 확장함수를 통해 객체안으로 옮길수있는지 간보기
제목이 자극적이지만 이렇게 자극적인게 기억에 잘 남는다.
확장함수를 통해 리펙터링을 해보고 상황에 맞는다면 객체에게 메시지를 던져서 처리할 수 있도록 리펙터링 할 수 있다.
어떤 방식으로 리펙터링을 하는지 한번 살펴보겠다.
함수의 인자로 들어오는 것들중 하나를 확장함수 형태로 확장해주면 그 인자를 받지 않고 this를 통해서 접근할수 있으며 this는 생략도 가능하기에 인자수를 줄일 수 있고 간결하게 만들어줄 수 있다.
그리고 이를 통해서 직관적으로 함수가 객체에 들어가도 되는지 판단할수있다.
예를들어 Lotto객체에서 Set<LottoNumber>를 프로퍼티로 가지고있다고 가정해보자.
그리고 다른 클래스의 메서드 혹은 최상위 함수에서 List<littiNumber> 를 인자로 받아서 어떤 작업을 하고있다고 생각해보자
private fun test(lottoNumbers: Set<LottoNumber>, bonusNumber: LottoNumber) {
// 알랄라 살랄라 작업중
}
이런식으로 외부에 함수가 존재하고 있는 상태이다. 그렇다면 이 함수를 확장함수로 변경해볼 수 있을것이다.
위함수를 확장함수로 리펙터링 한다면 아래와 같다.
private fun Set<LottoNumber>.test(bonusNumber: LottoNumber) {
// 알랄라 살랄라 작업중
}
그렇다면 이 함수를 호출해본다면 어떠한 방식으로 호출되는지 살펴보자
val lotto = Lotto(setOf(LottoNumber(1), LottoNumber(2)))
// 리펙토링 전 일반함수
test(lotto.lottoNumbers, LottoNumber(3))
// 확장함수 형태
lotto.lottoNumbers.test(LottoNumber(2))
일반적인 함수와는 다르게 확장함수로 리펙토링할시 호출하는 모습이 꼭 로또객체 안에있는 메서드를 호출하는 형태와 비슷하게 변한다.
이말은 보이는바와 같이 확장함수를 Lotto내부로 옮겨도 된다는 말이다.
이렇게 확장함수를 통해서 적절히 객체 안으로 메서드를 옮길수 있는지 확인하고
옮길수 있다면 옮기고 난후 기존 메서드의 this 를 로또안의 Set<LottoNumber> 프로퍼티 값으로 대체해주면 될것이다.
private fun Set<LottoNumber>.test(bonusNumber: LottoNumber) {
// 알랄라 살랄라 작업중
this.map{ /** 알랄라 살랄라*/ }
}
이랬던 확장함수를
class Lotto(val lottoNumbers: Set<LottoNumber>) {
private fun test(bonusNumber: LottoNumber) {
// 알랄라 살랄라 작업중
lottoNumbers.map { /** 알랄라 살랄라*/ }
}
}
이런형태로 변경해서 함수를 객체 내부로 적절히 옮기고 객체에 메시지를 던지는 형태로 변경할수 있게된다.
매번 메시지를 던지는 형태로 설계해야겠지만 헷갈려서 외부로 메서드를 빼놨을수 있고 이때 옮기는것이 적절한가의 판단으로 확장함수를 이용해 보는것도 좋은 방법이라고 생각된다.
그리고 항상 객체에 메시지를 던지는 형태로 리펙터링을 하자
그리고 한가지 장점으로 이렇게 객체내부로 메서드를 옮긴다면 private를 벗는경우가 많고 그렇다는것은 테스트가 가능하게 변한다는 이야기이다. -> 테스트 측면에서도 긍정적이다.
이런 의문이 들은적이 있을수 있다 원시값 포장하면서 왜 포장은 했는데 함수가 하나도 없지? -> 객체가 상태를 가지고있다면 상태관련된 로직이 다 객체로 들어가야하는건데 제대로 리펙토링이 덜된것이니 이렇게 확장함수를 통해서 간을 봐보자.
점진적인 리펙터링의 의미
점진적 리펙터링을 왜하는가? -> 테스트와 함께하는 좀 더 질좋은 삶을 위해서(퇴근이 있는 삶을 위해서)
이게 무슨 소리냐하면
TDD 를 이용해서 코딩을 하는데 리펙터링을 시도하고 있을때 테스트가 걸리적 거린적이 있을것이다.
뭔가 조금 바꿨는데 테스트를 다 통과 못해서 테스트 자체에 반감이 들수가 있기 때문에
실제 실무에서 상황을 보면 테스트가 다 주석처리 되어있거나 @Disabled해놓고 그냥 넘어가는 경우들이 있다고 한다.
그리고 팀원들이 테스트 싫어 빼애애애애애액 하고 있는 경우가 있을수도 있다고 하는데
이렇게 테스트 혐오자가 되지않기 위해 점진적 리펙터링을 통해 테스트에서 유연하게 리펙터링을 할 수 있도록 하는것이다.
점진적인 리펙터링을 시도하면 중간에 다 빌드도 할수있고 과도기적 과정에서 실행하는데 전혀 문제가 없기 때문에
리펙터링 중간에 휴가를 갔다와도 그냥 배포해버려도 상관이 없는것이다.
결국 이말 == 퇴근 하고싶어서 인것 같다.
ps. 테스트 커버리지 라는것이 있는데 현재 몇퍼센트가 테스트로 체크되고 있는지 확인할 수 있다.
테스트에서 저 메뉴를 실행하면
이런식으로 어떤 클래스가 몇퍼센트 테스트로 체크 되는지 확인할수 있다.
의존성 관련
의존성에 대래서 두려워 말라 이런 의미가 강했던것 같다.
의존성이 생기는것은 당연하자 하지만 고려해야하는것은 의존성의 방향이다.
단방향인지 양방향인지 항상 생각해보고 고려해야한다.
한방향으로 흐르고있는 의존성은 문제가 되지않는다. 하지만 양방향 의존관계가 있다면 끊어내야 할것이다.
뭐 리펙토링 관점에서 함수가 인자 세개를 받는 함수인데 각 각각 3개의 객체에서 하나씩 갖고있는 상태라 생각하면 객체안에 메시지를 던지는 형태로 리펙터링할때 고려해야할 요소로는 의존성이 양방향이 걸리는지 생각해봐야할것이다.
테스트코드를 간결화 하는 방법 (feat. 생성자에대한 생각)
테스트 코드를 만들고 있는데 객체생성이 굉장히 더러워지는 경우가 많다.
val lotto = Lotto(
setOf(
LottoNumber(1),
LottoNumber(2),
LottoNumber(3),
LottoNumber(4),
LottoNumber(5),
LottoNumber(6),
),
),
로또 테스트 코드에서 이런 코드들이 즐비했다(어후 보기만해도 힘들다
물론 프로덕션코드에서는 이럴일이 없거나 적다.
이런경우 간결화 시킬 수 있는 로직 팩토리 함수에작성해서 사용할수있다.
또한 이런 팩토리 함수를 테스트 파일에작성하고 private로 접근을 막아 단순 테스트에서만 사용할수 있도록 할수있다.
companion object {
private fun lottoNumberSetOf(vararg numbers: Int): Set<LottoNumber> =
numbers.map { LottoNumber(it) }.toSet()
}
// 객체 생성이 간단해진다
lottoNumberSetOf(4, 3, 12)
이때 한가지 더 고려해볼 수 있는 방법으로 fake constructor 를 이용하는것이다.
함수 이름과 생성자의 컨벤션은 생성자는 대문자로 시작하고 함수는 소문자로 시작한다.
하지만 함수를 대문자로 시작해도 동작한다 (노란색 주의는 뜬다)
어쩃든 이렇게 생성자처럼 보일수있는 팩토리 함수를 생성해서 가짜 생성자를 만든다면
-> 실제 생성자를 추가하거나 하지않고 객체를 생성할수 있고 생성방법을 간단하게 만들거나 어떤 행동을 중간에 할 수 있으며
접근제한자를 private로 놓고 test 파일에 위치시켜 테스트 이외의 접근 (테스트 전용) 막아버릴수도 있다.
이런것 사용하면 팀원들에게 극렬한 저항을 받을수있다 .
이럴때 내가 쓰는것이 옳다고 생각하고 써야겠다 싶으면(물론 난 별로 싸우고 싶지않아서 안쓸듯 그냥 팩토리함수 쓰면되지 뭘싸워)
어쨋든 못쓸건아니고 좋은것이라는 근거를 대야하니 그에대한 근거로
이미 코틀린에서 잘만들어놓은 기본 api에서도 fake constructer를 쓰는데 코틀린 만든 개발자보다 잘하면 반박하셈 이러고 공격하면된다(물론 팀원과 싸우지말자)
예를들어 우리는 리스트를 생성할때
val testlist = List(3) { /**알랄라 살랄라*/ }
이런식으로 리스트를 생성하기도 한다
이거 보면 생성자 같이 생겼는데 막상 타고 들어가보면 생성자가 아니라 function이다.
@SinceKotlin("1.1")
@kotlin.internal.InlineOnly
public inline fun <T> List(size: Int, init: (index: Int) -> T): List<T> = MutableList(size, init)
이렇듯 이미 코틀린 api에도 존재하니 충분히 써도 뭐 나쁠건없다. 이렇게 팀원들을 설득해 나가보자
이런거 쓰면 리뷰 이렇게 달릴것이다(재미를 위해 각색한거임 이런거 달리면 전투신청이니 안심하라)
팀원1 리뷰 : 근본도 없는 네이밍쓰네 왜 함수에 대문자쓰고 그럼?
fake constructor 원리주의자: 너 코틀린 언어 개발자보다 개발잘함? 코틀린 언어 개발자보다 잘하고 말해라
근데 사실 제이슨의 조언은 그냥 진짜 생성자를 쓰는게 더 좋다고 생각한다고한다. 그리고 그냥 이런거 부생성자로 만들어주면 되지않는가 라고 제시했고 또한 객체를 생성하는데 있어서 생각해보아야 할 점들을 던져주었다. 이에 대해 생각을 많이 해보았고 정리해보자면
1. 객체에 생성자가 많아도됨?
나는 객체생성에 관해 미션을 진행하고 특히 마지막 오목미션에서 sqlite로 상태를 저장했다가 복귀시키는 행위를 해보며
일단 왠만한 상태값은 생성자(특히 컨텍스트 스위칭하듯 중간지점까지 도달해있는 객체를 되살릴수있도록 해야하는 상황)이 있다면 그와 관련된 상태는 죄다 생성자로 가는게 옳다고 생각했다.
그에 따라 생성자 부생성자를 만드는 행위는 자유롭게 해야하고 생성자 혹은 부생성자가 많아도 상관없다고 생각한다.
또한 제이슨의 생성자는 많아도 오히려 객체의 응집도를 올려준다는 말이 이해가 되었다.
예를 들어 오목을 하다 중간에 sqlite에 저장을 했다고 가정하자
구현방법에 따라 다르겠지만 오목게임이 분명있었을 것이고 게임을 끝낸시점의 객체의 상태를 스냅샷을 떠서 저장해놨을것이다.
그리고 게임을 다시 시작할때 그 스냅샷의 상태를 sqlite에서 꺼내서 롤백시켜줘야할텐데 생성자로 각 상태값들을 전달할수 없다면 곤란해질것이다.
적절한 상태값을 받을수있는 생성자 부생성자가 없다면 각 롤백시켜줄수 있는 함수를 만들거나 setter를 통해서 새롭게 설정해줘야하는데
이는 로직도 번잡하고 객체의 응집도를 떨어트리는 행위라고 생각한다.
뭐 어쨋든 이런상황을 겪어보고 나니 이제 생성자는 객체를 사용하는 사용자가 객체를 편하게 사용할수 있도록 만들어주는 수단이니 많든 적든 적절하게 이용한다면 많아지는것에 겁낼필요가 없다고 생각한다(사용자에게 사용할수있는 여러방법을 제공하면 더 좋은거니까!!!)
2. 객체 생성에 로직이 들어가는거 맞음?
팩토리 함수에다가 로직을 꾸역꾸역 넣고있으니 생성자(혹은 팩토리함수 같이 객체를 생성하는 행위)에 로직이 들어가는게 옳은지 생각해보라고 제이슨이 말해주었다.
그래서 처음에는 뭔소리지? 하고 일단 알겠다고 했는데 궁금해서 찾아보니까
엘레강트 오브젝트 라는 책(자바 checked exception 공부할때 뜨거운 감자로 등장했던) 에 이 내용이 나와있음을 찾아냈다.
그 책에서 생성자에 로직을 넣는것에 대한 문제점을 정리한 블로그글을 하나 첨부한다.
https://jessyt.tistory.com/134
생성자에 로직을 비워야하는 이유가 쭉 정리되어있는데
간단히 이야기하자면 생성자에 로직을 넣는경우 성능 최적화가 어려워진다는 것이다.
객체가 생성될때 생성자에 포함된 로직을 실행하기 때문에 객체의 관련 상태값이 사용되지않는 경우에도 생성자로직을 실행해서 성능에 좋지 못하다는 것인데
생성자에 로직이 무겁지 않거나 지연초기화를 적절히 이용한다면 성능이슈가 크게 발생할것 같지않다고 생각한다.
그리고 애초에 생성자에 로직이 들어가는편이 직관적이라고 생각해서 성능이 진짜 중요한 이슈가 아니라면 요즘 정말좋아진 폰성능을 믿고
생성자에 로직을 넣는 방향으로(물론 객체생성 로직이 무겁거나 객체가 0.1초에 한번씩 생성되는 미친경우라면 생성자에서 로직을 뺄것이다.) 진행할것같다.
방어적 복사(어둠의 마법 방어술)
방어적 복사는 어둠의 마법 방어술 교육과 같다고한다. 어떤 미친짓을해서 뚤어내는걸 다 쫓아가서 막아내야한다고 한다.
근데 사실 방어적복사는 양도 방대하고 따로 공부하고 글을 써야할것 같으니 수업에서 백킹 프로퍼티 관련 방어를 뚫어내는 것을 한번 보고 제대로 막아야겠구나 라고 느끼면 될것같다.
backing property 를 뚫을 방법은 없을까?
-> as 를 통해 Mutable로 강제 캐스팅해서 뚫기
-> 애초에 넣어줄때 어떤 변수에 담아서 참조값을 갖고있는 상태에서 넣어주면 참조값을 통해 오염시킬수있다.
이런 상황을 위해 여기저기 tolist() 같은 함수로 객체를 깊은복사를 통해 사용해서 사용자가 미쳐가는걸 막아야한다.
근데 이것도 사실 다 리플렉션같은것으로 뚫을 수 있다고 한다.(아니 근데 방어적으로 복사해놨는데 그짓을 왜하냐고 팀원이 아니라 해커임?)
어쨋든 이렇게 뚫는 방법은 많으니 방어적복사를 좀더 학습하고 집중해서 막아보자
ps. 백킹 프로퍼티는 객체지향 생활체조 원칙에서 프로퍼티 두개 갖는거임?
-> 이게 생활체조 원칙이 자바로 된거라서 그렇다고 한다. -> 코틀린으로 코딩하면 이원칙에서 가끔 벗어나야하는 시점이 온다(즉 1개로 생각하자)
결론:
점진적인 리펙토링의 목적 결국 최대한 컴파일 에러를 내지 않는것이다.
퇴근은 해야하는데 리펙터링한다고 까불며 잘못 건드려서 코드가 벌집이된다면 집에 못가니까(야근맛좀볼래?)
이번 수업의 핵심!!!!! 새로운 함수를 만들어놓고 거기서 리펙하고 천천히 고쳐라
'우테코 > level1(코틀린)' 카테고리의 다른 글
[우테코]02/21 level1 다섯번째 수업 (로또 피드백) (0) | 2023.04.06 |
---|---|
[우테코] 템플릿 메서드 패턴 간단요약 (0) | 2023.03.16 |
[우테코]02/17 level1 네번째 수업(로또 피드백) (5) | 2023.02.23 |
[우테코]로또 미션 코드리뷰 관련 학습내용 (0) | 2023.02.22 |
[우테코]02/14 level1 세번째 수업 (두번째 미션 자동차경주) TDD,OOP (0) | 2023.02.20 |