본문 바로가기

우테코/level1(코틀린)

[우테코]02/14 level1 세번째 수업 (두번째 미션 자동차경주) TDD,OOP

🐗TDD🐗

이제야 제대로된 첫수업이다. TDD라니 설렌다.

TDD 쉽게 말하면 테스트가 먼저 선행되는 개발인건 맞지만 테스트만 먼저 짠다고 TDD라기 보다는 결국  Development가 중요하지 않는가 라고 코치님은 생각하신다고 한다.

 

개발을 할때 설계(design)와 개발(development) 중 무엇이 먼저 선행되어야하는가?

-> 설계가 당연히 설계가 먼저 진행되어야 할것이다.

 

TDD의 약자는 Test Driven Development 이다 . 즉 개발의 영역이므로 설계는 알아서 선행하고 난 이후에 진행되는것이다

즉 TDD에 앞서 객체지향적 설계이든 함수형 설계이든 설계의 기본적인 과정이 선행되어야한다.

 

그래서 여태까지 매번 기능목록을 짜고 그 기능목록을 보충하고 했던것들이다.(설계적인 의미로) 

 

TDD를 통해서 얻을수있는 큰 장점중 하나로 자신감이 있다고한다.(실제로 이게 용어처럼 쓰이나보다 자신감이라니 ㅋㅋㅋㅋㅋㅋㅋ )

-> 개발을 테스트로 검증되었고 매번 필요한것들을 필요에의해 검증하며 개발하고 의도대로 개발이 진행되므로 그에대한 자신감이 뿜뿜한다는 뭐 그런의미이다.

 

 

TDD 의 진행 프로세스

순서대로 살펴보자면

 

기능목록 등 설계가 미리 선행되고 진행된다.

 

1. 빨간과정

실패하는 작은테스트를 작성한다 컴파일이 안될수도 있다.

일단 테스트를 작성하기 때문에 클래스 이런거 생성자 함수조차 안 만들었으므로 빨간줄이 막 뜨고 이런 빨간줄을 버티는게 힘들다.

어쩃든 일단 의도한대로 클래스,생성자,함수명 이런것들을 선제적으로 정해서 호출하고 검증하는 형태로 테스트 부터 우선 짜게된다.

-> 이때도 설계는 미리 진행되어있어야한다. 어떤객체를 만들건지, 기능목록 이런 전체적인 설계는 완료된 상태이다.

 

2.초록과정

빨리 테스트가 통과하게끔 만든다. 이를 위해 어떠한 죄악을 저질러도 좋다.

진짜 테스트 통과를 위한 코드라도 작성해서 통과하는것이다. 이때 진짜 아무런 기능없이 테스트만 통과하도록 거지같이 짜도된다.

예를들어 assertThrow 테스트이고 테스트 항목은 클래스를 만드는것이였다면 그냥 클래스 init블록에다가 throw를 던져버리도록 이렇게 진짜 죄악이라는 말이 맞는 코드를 짜도된다. -> 이제 바로 리펙토링 할것이니까

 

3. 파랑과정

자 이제 테스트는 통과했으니 테스트를 계속 통과하며 내가 목적했던 기능들과 진짜 예쁜코드를 위해 리펙터링을 계속 진행한다.

-> 이게 즉 제일 중요한 요소이다.

 

이러한 원칙을 지키며 개발을 하면된다.

 

 

 

-> 결국 이짓을 왜하는가?

두려움을 관리하는 방법의 일환이다.

 

프리코스 할때를 생각해보면 어디서부터 개발을 시작해야하나 라는 의문이 들고 했을것이다.(사실 난 안이럼)

TDD를 적용한다면 테스트가 있으니 검증을 받을수있고 올바른 방향으로 나아가고 있다는 안도감을 얻는것이다.

 

 


라이브 코딩

자동차 경주를 TDD에 맞춰서 라이브 코딩을 해주셨다.

 

 

일단 시작점에서 감정 상태가 어디서 어떻게 시작해야할지 모르는 막막함이 있을것이라고 한다.

 

그럴때 타개책으로 시작해야하는것이

요구사항 분석 및 설계를 해보는것이다

-> 개발 이전에 설계가 우선되어야 앞에서도 언급되었듯이

디자인 관련 행위부터 시작해야 한다고 한다.

객체를 어떻게 짤지 생각하고

 

 

어려운 DB와 UI는 냅두고 도메인 로직부터 설계한다고한다.-> 도메인은 테스트가 걍 단위테스트 갈기면 되니까 오히려 쉬운편에 속한다.

 

 

이제 한번 racingcar에 대입해서 한번 생각해보자

 

객체를 뭘 만들어야한까?

car,racinggame 을 일단 만들어야겠네 이런식으로 생각이 날것이다.

이걸 기능목록을 작성하고 car의 기능목록대로 하나하나 도메인 로직을 작성해 나가면 되는것이다.

 

 

이런데도 막막하다면 

그냥 일단 아무 잣대없고 생각없이 일단 그냥 아무렇게나 개발해본다고한다

-> 이러면 그 경험을 통해 도메인에대한 지식이 쌓일테고 (그게임의 룰같은거 4이상은 전진이네)

그렇게 얻을것을 얻고난후 기존 얼기설기 코드를 다 날려버린다 -> 처음부터 다시 구현하게 될것이다.

그리고 다시 구현할 기능목록부터 시작해서 처음에 하려던 과정을 시작하면 되는것이다,

 

 

이후 라이브 코딩을 진행하셨는데 그 중 기억해야 하는 것들을 정리해보았다. 

 

1. 진짜 기능목록부터 빠르게 작성한다.

2. 테스트명을 기능목록 짜놓은거 그대로 쓴다.(뭐 엿장수 맘대로지만 의미도 빠르게 전달되고 구현해야하는 기능을 바로 보여줄수 있어 좋은것같다).

3. 빨간색과정 시작 테스트 일단 그냥 빨간줄이 뜨든 뭐하든 그냥 내가 생각하는 테스트에서 이뤄져야하는 사항을 만든다

class CarTest{

    @Test

     fun 자동차는 이름을 가지고 있다(){

     val car = Car("json")

     assertThar(car.name).isEqualTO("json")

     }

}

-> 이름을 가지고있으니 이름을 검사하는 테스트이다.

4. 초록색 과정 진짜 돌아가게만 코드를 짠다 여기서 중요한건 진짜 돌아가게만 짜든 어쩌든 빨리짜면 장떙이다.

5. 파란색과정 이제 리펙토링하는데 테스트 코드도 리펙토링한다.

6. 커밋은 TDD사이클 단위로? 아니면 단위별로? 알아서 기준을 잡으면된다.

7. 중간에 테스트 하다가 다른것들을 생성하거나 테스트 해야하는 일이 있다면 ex) 자동차 move기능 검증시 position 이라는 프로퍼티가 처음에 없을것이다 그럼 자동차는 자기자신의 위치를 알수있다 라는 기능목록이 누락된것이다.

기능목록이 누락된것이고 기능목록을 업데이트하고 그것을 또 테스트하면 된다.

8. 테스트를 하며 다음 코드를 짜는 근거를 만들어낸다.

ex) 4이상이면 전진이다의 초록색 과정에서는 if문없이 그냥 position 값을 ++ 해도 통과하지만 3일때 정지한다는 테스트를 통과하기 위해 if문을 넣어주는 근거를 얻어간다. -> 적절한 테스트도 다 들어가야 할것이다.

9. 처음에는 프로세스에 맞춰서 하지만 숙련되다보면 퀀텀 점프를 하면 된다고 한다. -> 테스트 이후 바로 제대로 구현

 


질문사항 및 답변

 

TDD를 쓰면 개발속도가 느려지는가?

-> 전체적으로 유지보수나 다른 과정들을 종합하면 최종적으로는 빨라진다(지금 당장은 느려보여도)

 

TDD의 장점은 무엇인가?

-> 코치님의 경험담으로 TDD로 개발을하니 테스트가 감시하기 때문에 술을 엄청먹고 개발한적이 있는데 코드가 맛이 안갔다고 한다.

물론 리펙토링은 필요 이런식으로 코드의 검증이 이루어지니 항상 올바른 방향으로 나아갈수있다.

 

테스트를 잘못짜면 어떻게 대처하는가?

-> 다른 테스트를 통해 테스트가 올바른지 검증하게 될것이다.

그리고 테스트가 잘못되었다면 그냥 리펙터링 하면된다.(모든것은 리펙터링으로 귀결)

 

테스트를 위한 메서드를 만드는것은 옳은일인가?

-> 옳지 않다. 

이에대한 꼬리질문 생성자는 괜찮은가? 이것 또한 테스트를 위한거나 다름 없지 않는가?

-> 생성자는 설계의 영역이다. 클라이언트(사용자)가 편하게 사용하도록 제공하는 것이지 테스트를 위한것이 아니라고 볼수있다.

생성자가 많은것은 함수가 많은것보다 객체의 응집도를 올릴수있는 좋은 방법이다

 


테스트를 자세히 뜨ㄸ어보면

given,when,the 이 등장하는데

given 은 조건( 시나리오) ex) 포비차가 4번 이동 멧돼지 차는 10000번 이동 즉 멧돼지 승

when 은 로직 상황)

then 은 검증 -> assert로 시작하는 검사하는 부분

 

이렇게 테스트를 짜게 될것인데 누군가 우승하는 것에 대한 테스트를 짠다고 가정해보자

 

given 과정에서 car객체를 생성해놓고 막 move를 여러번 할것이다 car.move 를 포비는 4번 멧돼지는 10000번 할것이다.

이러면 한눈에 누가 우승했는지 알수가 없다(가독성 개똥망)

 

그리고 애초에 테스트 자체가 누가 우승했는가가 중요한거지 움직이는것을 테스트 하는것이 아니다.

move를 할필요없이 애초에 기능목록 자체도 가장 멀리간 자동차 한대가 우승한다 이다.

-> move를 여러번 할게아니라 car의 생성자를 통해 애초에 position 을 조작해서 car을 생성하고 우승자를 가려내는 식으로 한다.

 

추후에 테스트에 대해서 좀더 공부해보자

JUNIT 공부 키워드

@ArgumentSource

@ParameterizedTest

@MethodSource


역컴파일

이제 자바와 역일일이 있을때 어떻게 세상이 돌아는지 알려주는 고맙지만 킹받는 기능이다. 자바좀 그만 만나고싶다.(코틀린 넌 왤케 의존적이니)

 

인텔리제이에서 파일을 쭉보다보면

build 와 out 이라는게 있을텐데

 

build 은 gradle 가 만드는거고

out은 인텔리제이가 만드는것이다

 

이두 폴더는 코틀린 컴파일러가 만든 코드들이다.

 

이제 여기 들어가서 내가 작성한 코드들 찾아서 커맨드 쉬프트 a 누르고 decompile 검색해서 실행하면 그 파일이 자바코드로 어떻게 생겼는지 볼수있다.

동등성 동일성 검사할때 같이 거지같은 상황에서 보게된다.

 


원시값 포장

이제 로또 미션에 원시값 포장이 프로그래밍 요구사항으로 추가되었다. 

원시값 포장의 이점과 용도는 무엇일까?

 

예를들어 car의 이름을 carname으로 래핑하지 않고 그냥 name 으로 사용한다고 생각해보자

 

이름이 5글자가 넘지않는다는 조건이 car클래스의 init 로직 에서 한번만 검사 하는것이 아닌 이름이 바뀔수 있는 로직들이 생겨나면 (ex: changeName 함수) 거기서 항상  5글자가 넘는지 검증해줘야하는 로직이 선행되어야 할것이다. -> 이는 코드의 중복을 일으키게 된다. (만약 검증을 하지 않는다면 이름을 바꿨을때는 이름이 8글자건 9글자건 설정되어 비정상적인 접근이 가능해지는 것이다.)

 

한번 예시를 봐보자

 

package racingcar.domain

class Car(var name: String, var position: Int = 0){
    init {
        require(name.length <= MAXIMUM_NAME_LENGTH){"자동차 이름의 길이는 5를 넘어서는 안됩니다"}
    }
    
    fun changeName(nme:String){
        require(name.length <= MAXIMUM_NAME_LENGTH){"자동차 이름의 길이는 5를 넘어서는 안됩니다"}
        this.name =name
    }
    
    companion object{
        private const val MAXIMUM_NAME_LENGTH = 5
    }
}

이런식으로 코드를 짜게되면 위에서 설명했듯

require(name.length <= MAXIMUM_NAME_LENGTH){"자동차 이름의 길이는 5를 넘어서는 안됩니다"}

이 로직이 매번 중복될것이다.

 

일단 제일 기본적인 방법으로 메서드로 중복된 로직을 추출하는 방법이있는데 이또한 계속 호출해주고 중복된행위를 해야한다.

 

 

이러한 중복을 없애기위해 이름 자체에 책임을 부여하는것인데

name을 포장해서 값검사등 중복해서 일어날 일들을 시킨다. -> carname 객체에서 기본 검사들을 하고 중복을 없앨수 있다.

 

 

이것을 리펙토링 입장에서 보면 기존에 string 값들을 받아서 써왔으므로 기존 코드에서 대응이 안되는문제가 생길수 있다.

이럴때 부생성자를 이용해서 기존의 코드에 대응해야한다.

// 부생성자
constructor(name: String, position: Int): this(CarName(name), position))

그리고 이제 carname 변수를 따로 만들어줘서 String 값을 담아 String 을 직접 사용하는것에 대응하게 해준다.

 

 

class Car(private val carName: CarName) {

	val name1: String = carName.name
	val name2: String
		get() = carName.name

	constructor(name: String, position: Int) : this(CarName(name), position)

	fun changeName(name: String) {
		this.name = CarName(name)
	}
}



class CarName(val name: String) {
	init{
			require(...)
		}
	companion object{
		...
	}
}

이런식으로 처리한다.

 

https://tecoble.techcourse.co.kr/post/2020-05-29-wrap-primitive-type/

 

원시 타입을 포장해야 하는 이유

변수를 선언하는 방법에는 두 가지가 있다. 원시 타입의 변수를 선언하는 방법과, 원시 타입의 변수를 객체로 포장한 변수를 선언하는 방법이 있다. (Collection…

tecoble.techcourse.co.kr

원시값 포장의 이점을 설명한 테코블 글이다. 참고해보자

 

 

자바는 원시값 포장방법이 클래스로 감싸는게 유일하다고 하는데 코틀린은 api가 많아서 여러방법이 있다고한다.