인프런 워밍업 클럽 2기 - 백엔드 프로젝트(Kotlin, Spring) / 2주차 발자국

인프런 워밍업 클럽 2기 - 백엔드 프로젝트(Kotlin, Spring) / 2주차 발자국

image

1주 동안 배운 내용을 정리하고 회고하는 시간을 가져보자.

 

6 ~ 7일차 - Repository, Test

FetchType과 N+1 문제

강의에서 FetchType에 따른 쿼리 작동 과정을 보여주셨고 이 때 발생할 수 있는 N+1 문제에 대해서 언급이 되었다.

N+1 문제는 JPA를 사용하여 Entity를 조회할 때 발생할 수 있는 문제로 아래와 같이 부모 엔티티를 조회할 때, 연관 되어있는 자식 엔티티들의 수 N 만큼의 쿼리가 발생하여 성능에 지장을 줄 수 있는 문제다.

 

지연로딩을 사용했을 때는 각 엔티티를 실제 사용할 때 마다 쿼리가 발생하였고, 즉시로딩으로 변경하자 모든 엔티티의 정보를 한꺼번에 수집하여 두 경우 모두 N+1 문제가 발생함을 볼 수 있었다.

image

이를 해결하기 위해 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을 사용하여 연관관계를 한번에 조회할 수 있었고, 단 한번의 쿼리로 줄어든 것을 볼 수 있다.

image

하지만 이 경우에도 @~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 만큼씩 자식 엔티티를 조회할 수 있다.

image

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를 설계하도록 노력해봤고 결과물은 아래 리포지토리에서 볼 수 있다.

https://github.com/ppusda/MML

 

총 회고

이번 2주차는 쉬는 날 겹쳐있어 진도가 많이 나가질 못했다.

하지만 의외로 많은 걸 배울 수 있었다.

단순히 Kotlin으로 Spring을 접근하는 방법 뿐만 아니라 약간의 복습과 거들어 N+1 문제, Test code와 같이 아직 부족한 부분에 대해서 좀 더 학습할 수 있었다.

특히 Test code에 대한 부분은 조금 공부해보니 흥미가 더 생겨서 향후에 Mockito 동작 과정에 대해서 자세하게 뜯어볼 의향도 생겼다.

 

앞으로도 부족한 부분을 채워나가면서 학습해나가야겠다.

모두 화이팅! (o゚v゚)ノ

 

참고

https://jojoldu.tistory.com/457

댓글을 작성해보세요.

채널톡 아이콘