인프런 워밍업 클럽 2기 - 백엔드 프로젝트(Kotlin, Spring) / 2주차 발자국
⭐ 1주 동안 배운 내용을 정리하고 회고하는 시간을 가져보자.
6 ~ 7일차 - Repository, Test
FetchType과 N+1 문제
강의에서 FetchType에 따른 쿼리 작동 과정을 보여주셨고 이 때 발생할 수 있는 N+1 문제에 대해서 언급이 되었다.
N+1 문제는 JPA를 사용하여 Entity를 조회할 때 발생할 수 있는 문제로 아래와 같이 부모 엔티티를 조회할 때, 연관 되어있는 자식 엔티티들의 수 N 만큼의 쿼리가 발생하여 성능에 지장을 줄 수 있는 문제다.
지연로딩을 사용했을 때는 각 엔티티를 실제 사용할 때 마다 쿼리가 발생하였고, 즉시로딩으로 변경하자 모든 엔티티의 정보를 한꺼번에 수집하여 두 경우 모두 N+1 문제가 발생함을 볼 수 있었다.
이를 해결하기 위해 JPQL에서 Fetch Join 쿼리를 작성하고 application.yml
을 수정하게 되었다.
Fetch Join과 Fetch Size
JPA에서 쿼리를 직접 작성하기 위해서는 JPQL을 사용할 수 있다.
아래는 강의에서 작성했던 쿼리 부분이다.
@Query("select e from Experience e left join fetch e.details where e.isActive = :isActive")
fun findAllByIsActive(isActive: Boolean): List<Experience>
Fetch Join을 사용하여 연관관계를 한번에 조회할 수 있었고, 단 한번의 쿼리로 줄어든 것을 볼 수 있다.
하지만 이 경우에도 @~ToMany
의 관계를 갖는 자식 엔티티가 여러 개인 경우에는 적용할 수 없다는 한계가 있다.
이는 MultipleBagFetchException
이 발생하기 때문인데, 그 이유는 다수의 자식 엔티티를 Fetch Join하게 될 경우에 중복이 발생하고 일관성이 떨어지게 된다.
그렇기에 JPA에서 이를 방지하기 위해 MultipleBagFetchException
를 통해 두 개 이상의 자식 엔티티를 Fetch Join 하는 것을 막아두었다.
이를 해결하기 위해 Fetch Size를 조정하여 해결할 수 있다.
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 10
기존의 문제점은 자식 엔티티가 여러 개일 경우 하나의 Fetch Join만 사용가능하며, 그로 인해 N개의 쿼리가 더 발생한다는 점이었다.
default_batch_fetch_size
를 조정하여 되면 부모 엔티티의 Key를 이용하여 in
절을 통해 조정한 default_batch_fetch_size
만큼씩 자식 엔티티를 조회할 수 있다.
Test
6~7일차에는 이러한 내용들을 테스트 해볼 수 있는 Repository 테스트 코드를 작성하게 되었다.
아래 내용은 강의에서 작성한 코드의 일부분이다.
@DataJpaTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS) // 클래스 간 독립적으로 실행 됨.
class ExperienceRepositoryTest(
@Autowired val experienceRepository: ExperienceRepository
) {
@DataJpaTest
JPA 관련 테스트를 위한 설정을 제공하는 어노테이션이다.
그렇기에 데이터에 접근할 수 있는 레이어인 리포지토리 테스트 시 많이 사용된다.
내장 데이터베이스를 설정하고,
@Entity
및@Repository
어노테이션이 부여된 클래스들을 통해 테스트 환경을 구성하는 역할을 한다.
@TestInstance
테스트 인스턴스의 라이프사이클을 지정하기 위해 사용된다.
기본적으로 JUnit 5는 각 @Test 메서드마다 새로운 테스트 인스턴스를 생성하게 되어있다.
이는 테스트 환경을 어떻게 구성할 것이느냐에 따라 달라지겠고, 상황에 맞춰 사용하면 될 것 같다.
8 ~ 10일차 - DTO, Service, Test
DTO
Kotlin에서는 Java의 Record
처럼 data class
를 통해 DTO를 선언해 줄 수 있었다.
추가적인 생성자를 선언하기 위해서는 constructor
를 이용하여 만들어 줄 수 있었다.
map, filter와 같은 컬렉션 함수를 적용할 때 람다식에서 이름을 지정해주지 않아도 it
으로 사용할 수 있다.
data class ProjectDTO(
// 생략
val details: List<ProjectDetailDTO>,
val skills: List<SkillDTO>?
) {
constructor(project: Project) : this(
// 생략
details = project.details.filter { it.isActive }.map { ProjectDetailDTO(it) },
skills = project.skills.map { it.skill }.filter { it.isActive }.map { SkillDTO(it) }
)
}
Service, Repository
도메인에서 관리해야할 Repository가 많아짐에 따라 한 번에 의존관계를 주입받을 수 있는 Repository를 생성했다.
각각 필요한 부분만 주입받게 되면 후에 관리하기가 힘들어 지는 상황을 예방할 수 있다.
추가적으로 각 리포지토리의 기능들을 래핑하여 캡슐화 하는 형태의 코드를 작성하여 Service 단에서 사용하기 유용하도록 코드를 작성하였다.
@Repository
class PresentationRepository( // Presentation 에서 필요한 리포지토리들을 한 번에 주입받아서 활용하기 위함.
// A, B, C 형태로 따로따로 주입받으면 후에 관리하기가 힘들기 때문
private val achievementRepository: AchievementRepository,
// 생략
) {
fun getActiveAchievements(): List<Achievement> {
return achievementRepository.findAllByIsActive(true)
}
// 생략
}
Test - Mockito
8~10일차에는 Service에 구현한 기능들에 대해서 테스트 코드를 작성하게 되었다.
아래 내용은 강의에서 작성한 코드의 일부분이다.
@ExtendWith(MockitoExtension::class) // Mockito Extension 추가
class PresentationServiceTest {
@InjectMocks // Mock을 주입받을 대상, 테스트를 할 대상
lateinit var presentationService: PresentationService // Mock을 만든 이후 초기화를 진행하기 위해 lateinit
@Mock
lateinit var presentationRepository: PresentationRepository
}
@ExtendWith
테스트 확장을 지원하는 어노테이션으로 Mock 객체의 생성 및 초기화를 자동으로 처리하게 해주는 역할을 해준다.
@InjectMocks
Mockito에서 테스트 대상이 되는 클래스에 인스턴스를 생성하고,
@Mock
이 사용된 필드를 찾아 객체를 자동으로 주입하기 위해 사용된다.위에서는 테스트할 대상인
PresentationService
의 인스턴스를 생성하며,PresentationRepository
에 Mock 객체를 주입하기 위한 용도로 사용된다.
@Mock
Mockito에서 Mock 객체를 생성할 때 사용한다.
Mock 객체는 모의 객체로 실제 객체의 동작을 흉내낼 수 있다.
아래는 강의에서 사용된 Mock 객체가 실제 객체의 동작을 흉내낸 부분이다.
Mockito.`when`(presentationRepository.getActiveIntroductions())
.thenReturn(activeIntroductions)
when에서 정의한 내용을 시도했을 때, activeIntroductions
의 내용을 반환 하도록 Mocking을 한 것이다.
presentationRepository
가 실제 데이터베이스와 상호작용이 발생하지 않도록 동작을 했다고 속이는 것이며, 이러한 동작을 통해 테스트를 독립적이고 일관되게 유지할 수 있다.
서브 미션
2주차의 서브 미션에는 API 설계가 예정되어 있었다.
이를 위해 RESTful 하도록 API를 설계하도록 노력해봤고 결과물은 아래 리포지토리에서 볼 수 있다.
총 회고
이번 2주차는 쉬는 날 겹쳐있어 진도가 많이 나가질 못했다.
하지만 의외로 많은 걸 배울 수 있었다.
단순히 Kotlin으로 Spring을 접근하는 방법 뿐만 아니라 약간의 복습과 거들어 N+1 문제, Test code와 같이 아직 부족한 부분에 대해서 좀 더 학습할 수 있었다.
특히 Test code에 대한 부분은 조금 공부해보니 흥미가 더 생겨서 향후에 Mockito 동작 과정에 대해서 자세하게 뜯어볼 의향도 생겼다.
앞으로도 부족한 부분을 채워나가면서 학습해나가야겠다.
모두 화이팅! (o゚v゚)ノ
참고
댓글을 작성해보세요.