TIL_030 | Clean Architecture, spyk, mockk, 약간의 잡설

묘한묘랑·2024년 2월 12일
0

TIL

목록 보기
30/31

Service를 Test하는 과정에서 생긴 일이다.


@Entity
@Table(name = "post")
class PostEntity(
	val title: String,

	val description: String,

	@Type(value = ListArrayType::class)
	@Column(columnDefinition = "text[]")
	val tagList: List<String>,

	@ManyToOne(fetch = FetchType.EAGER)
	@JoinColumn(name = "member_id")
	val member: MemberEntity,
) {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	val id: Long? = null

	@CreatedDate
	val createdAt: LocalDateTime = LocalDateTime.now()

	companion object {
		fun of(dto: AddPostReq, member: MemberEntity) =
			PostEntity(
				title = dto.title,
				description = dto.description,
				tagList = dto.tagList,
				member = member
			)
	}

	fun toResponse() =
		PostRes(
			title = title,
			postId = id!!,
			description = description,
			tagList = tagList,
			writer = member.nickname,
			createdAt = createdAt
		)
}

현재 Entity Class의 상태

1. 문제 인지

Service의 Unit Test를 진행하려 하니 문제점에 하나 봉착하게 된다.
위 Entity에서 현재 id를 개발자의 실수로 변경되는 것을 막고 있는데, Service Test Code를 작성하려 하니 Hibernate의 도움 없이는 Id가 없는 빈 깡통 Entity로만 작업을 하게 된다.

2. 해결을 위해 무엇을 해야할까?

2.1 Clean Architecture

가장 먼저 떠올랐던 방법은 Clean Architecture 였다.

사실 이 방식을 떠올린 이유는 최근 Haxagonal Arichitecture와 Clean Architecture를 계속 생각하다 보니 가장 먼저 떠오른게 큰 것 같다.

우선 Clean Architecture를 도입하게 된다면, mock 객체를 만들지 않고 순수하게 객체를 만들어 테스트를 진행할 수 있다는 장점이 있다.


잡설

이 부분을 인지하고 나서부터 내가 생각하고 있던 의문들이 풀려나가기 시작했다.
최근 Entity가 domain 내부에 있고, repository 구현체는 infra 쪽으로 빼놓았었는데, 이렇게 한다고 한들 Entity는 종속적으로 가져가고 있다는 사실에 제대로 된 분리가 아닌 그저 패키지를 나눠놨을 뿐이라는 느낌이 계속 들었었다.
그렇다고 jpa와 관련된 annotation을 전부 때버리고 Model과 Entity를 나누게 된다면 부수적인 Class들이 많이 생겨 생산성을 떨어트리고 너무 과해지는 것이 아닌가라는 생각이 머릿속을 계속 맴돌았었다.
그러던 도중, 튜터분과의 대화를 통하여 Clean Architecture와 Haxagonal Architecture에 대하여 알게 되었는데 그 때 내가 느낀 감정은 "아, 이미 체계화 된 방식이 존재하구나!" 였다.

이전에 적었어야할 글을 지금 적어보려 한다.

사실 나는 이전에는 개발을 무색 퍼즐으로 보고 있었다. 무색 퍼즐에 틀을 만들고, 그 틀에 맞춰 퍼즐 조각들을 끼워 맞추고 그림을 하나하나 그려나가는 것이 개발이라고 생각하고 있었다.

하지만 튜터분과의 질의를 통하여 거대한 서비스의 경우 평면보다는 입체적으로 보아야 한다는 사실을 깨달았다.
그리고 머릿속으로 입체적인 모형을 하나 생각해보았을 때 작업 순서를 어떻게 가져가야 하는가에 대하여 생각을 해보았을 때 가장 먼저 떠올린 방식은 외부의 살을 먼저 덧붙이는 방식이었다.
그런데 이 방식에는 문제가 있다. 내부는 빈 깡통이므로 무너지기 가장 쉬운 형태가 되어버린다.
그렇다면 어떻게 해야하는가?
내부부터 덧붙여 가는 형식으로 개발을 진행하는 것이 가장 합리적인 방식이었다.
자, 이것을 코드로 풀게 된다면 core에 해당하는 객체나 함수들을 미리 정의하고 만들어 놓은 후 그것을 기반으로 덧붙이는 코드들을 만들어 나가게 된다면 core만큼은 다른 모든 것들을 다 때어내어도 core 만큼은 제 역할을 할 수 있었다.
또한 이것은 코드적인 부분만이 아닌 infra 쪽에서도 가져야 할 관점이었다.
사실 이런 생각이 들었던 것은 Redis를 통하여 동기화를 해결 하는 활용법에 대한 이야기를 들었을 때, 코드만으로 해결하는 것이 아닌 다른 서비스를 사용하여 문제를 해결하는 방식이 있다는 사실에서 부터 출발하였다.

물론 서비스 모델에 따라 어떤 관점으로 코드를 작성해야 할지는 달라지겠지만 새로운 관점을 볼 수 있게 되었다는 사실만으로도 나는 만족한다.


하지만 그 만큼 프로젝트가 커진다는 단점이 생기는데 학습으로써는 좋겠다만은 현재 문제만을 놓고 봤을 때 오버엔지니어링으로 넘어간다고 생각한다.

2.2 UUID

이후 Scale Out이나 UUID를 필요로 하는 도메인이 되거나, 생길지도 모르지만 지금 현재로써는 테스트만을 위하여 UUID로 변경을 하는 것이기 때문에 내키지는 않았기에 금방 포기하였다.

2.3 Refrection

테스트코드에서 Refection을 사용하면 filed 조작도 가능하고 문제는 없다지만 이걸 사용해서 하는 것이 유일한 답인가..? 라는 의문만이 남겨졌다.

그런데... Refrection에 대해 생각하다보니까... 잠깐만... mock은 bytecode 조작이니까...? 어라...?

2.4 mockk

이것을 왜 뒤늦게 떠올렸냐라고 묻는다면 계층에 대한 테스트를 진행할 때 mock객체를 쓰다보니 선입견이 생겨버렸다. 이내 시간이 지나고 mock객체를 만들면 되는거 아니야? 라는 생각에 도달하여 mockk를 사용하여 Entity를 mock하였다.

val postEntity = mockk<PostEntity>()

every { postEntity.id } returns postId
... // postEntity 값 할당

하지만 결과가 이상하다.

PostEntity에 있는 toResponse를 호출하는데 every로 등록된 값이 나오지 않는다.

이것도 문제인데 every로 하나하나 값을 할당하는 것이 Refrection이랑 크게 다를 바 없고 일단 귀찮다...

또한 어째서 toResponse에서 이상한 결과를 배출해내는가에 대하여 알아보니 mock객체 자체가 원본 객체를 그대로 유지하는 것이 아닌 모든 부분이 mock 객체로 치환되는 형태였다.

그래서 나는 객체를 하나 만들고, 특정 부분만 mock 하는 방법이 없을까에 대하여 코드를 만지작 거리다가 검색을 해보았다.

2.5 spyk

이것이 그 정답이었다.

val postEntity = spyk<PostEntity>(PostEntity(
	title = "테스트 제목",
	description = "테스트 내용",
	tagList = listOf("태","그","리","스","트"),
	member = memberEntity
))
every { postEntity.id } returns postId


결론 - 답은 여러가지다. 상황에 맞는 답을 택하자.

profile
상황에 맞는 기술을 떠올리고 사용할 수 있는 개발자가 되고 싶은 개발자

0개의 댓글