코틀린 백엔드 프로젝트 2기 - 2주차
이번주차는 섹션 3-7~ 섹션 4-5까지의 수업이 있었습니다.
섹션 3
데이터베이스 초기화, 리포지토리 개발, 테스트 코드 작성, 리포지토리의 성능 개선을 학습 하였습니다.
데이터베이스 초기화
데이터 베이스 초기화는 DataInitializer 클래스에서 진행을 해주었습니다.
해당 클래스를 빈으로 등록해주는 어노테이션인 @Component 를 사용 하였습니다.
생성자 주입을 통해 리포지토리 인터페이스를 주입 받고 @PostCunstruct 어노테이션을 이용한 메서드를 통해서 본격적인 데이터베이스 초기화를 실행하였습니다.
@PostConstruct
fun initializeData(){
}
엔티티 클래스들을 생성자를 통해 초기화 하여 생성한 뒤 주입받은 리포지토리의 기본적인 CRUD기능의 메서드를 이용하여 데이터베이스에 저장하였고 1:N 관계를 가지는 엔티티들은 각 필드의 리스트에 해당 엔티티들을 초기화 해주었습니다.
@PostConstruct
fun initializeData(){
...
val experience1 = Experience(
title = "캣홀릭대학교(CatHolic Univ.)",
description = "컴퓨터공학 전공",
startYear = 2018,
startMonth = 9,
endYear = 2022,
endMonth = 8, isActive = true,
)
experience1.addDetails(
mutableListOf(
ExperienceDetail(content = "GPA 4.3/4.5", isActive = true),
ExperienceDetail(content = "소프트웨어 연구 학회 활동", isActive = true)
)
)
val experience2 = Experience(
title = "캣카오",
description = "소셜 서비스팀 백엔드 개발",
startYear = 2022,
startMonth = 9,
endYear = null,
endMonth = null,
isActive = true,
)
experience2.addDetails(
mutableListOf(
ExperienceDetail(content = "유기묘 위치 공유 서비스 게발", isActive = true),
ExperienceDetail(content = "신입 교육 프로그램 우수상", isActive = true)
)
)
experienceRepository.saveAll(mutableListOf(experience1, experience2))
...
}
이 작업들을 통해서 데이터베이스 초기화를 진행해 주었습니다.
리포지토리 개발
스프링에서 제공하는 Spring Data JPA는 인터페이스를 상속하는것만으로 기본적인 CRUD 기능을 제공해줍니다.
또한 메서드 이름을 기반으로 하는 쿼리 생성 기능도 있습니다.(커스텀 메서드) findBy, deleteBy, countBy
같은 키워드 뒤에 필드들을 붙여주어 JPA가 자동으로 쿼리를 생성해줍니다.
// 커스텀 메서드
interface ExperienceRepository : JpaRepository<Experience, Long>{
@Query("select e from Experience e left join fetch e.details where e.isActive = :isActive")
fun findAllByIsActive(isActive : Boolean) :List<Experience>
@Query("select e from Experience e left join fetch e.details where e.id = :id")
override fun findById(id: Long): Optional<Experience>
}
@Query
Spring Data JPA에서 사용자 정의 JPQL (Java Persistence Query Language) 또는 네이티브 SQL 쿼리를 작성할 수 있게 해주는 어노테이션입니다.
jqpl에서 fetchJoin을 사용할 수 있는데 이 기능을 통해 지연로딩으로 받아오는 연관된 엔티티를 한번에 즉시로딩을 할 수 있으며 n+1문제를 해결할 수 있습니다.
N+1문제
대표적인 JPA의 단점으로 연관관계를 가진 두 개의 부모-자식 테이블에서 부모를 호출할 때 JPA는 엔티티의 연관관계를 바탕으로 조회해온 데이터의 갯수만큼 부모에 매핑된 자식 테이블의 데이터를 조회하기 위한 쿼리를 생성합니다.
예를 들어 프로젝트 - 프로젝트 멤버 의 관계에서 프로젝트가 3개 존재할 때 프로젝트를 조회하면 1번의 쿼리로 3개의 프로젝트가 조회 됩니다. 이때 각 조회된 프로젝트의 프로젝트 멤버를 1회 더 조회합니다. 프로젝트가 1회 조회될 때 3개의 프로젝트가 조회되었으므로 총 3번의 조회를 더 하게 됩니다.
이를 N+1 문제(1+N 문제)라고 합니다.
리포지토리 성능 개선
N+1문제가 발생하는 리포지토리의 성능을 개선하기위해선 엔티티에서 연관관계 매핑 시 설정가능한 FetchType의 종류에 대하여 알아야 합니다.
fetch = FetchType.LAZY
fetch = FetchType.EAGER
Lazy : 지연로딩 방법 부모 - 자식관계 엔티티에서 부모를조회하여도 자식을 호출하기 전까지는 조회하지 않습니다.
Eager : 부모 조회 즉시 자식 조회
N+1문제를 해결하기 위해 LAZY 타입을 사용합니다. 그 후 자식이 필요하면 @Query에 FetchJoin을 사용하여 부모와 자식의 데이터를 같이 조회하도록 할 수 있습니다.
테스트 코드
테스트 코드 작성은 운영 전 사이드 이펙트 감지를 위한 매우 중요한 기능입니다.
어노테이션
@DataJpaTest
JPA 관련 테스트 설정 제공, 내장 데이터 베이스 설정, @Entity, @Repository가 부여된 클래스들의 테스트 환경을 설정
@TestInstance
테스트 인스턴스의 라이프 사이클을 지정,Junit5는 각 테스트 메서드마다 새로운 인스턴스를 생성
@BeforeAll
테스트 클래스내의 모든 @Test메소드 실행전 한 번 실행되도록 함 테스트 데이터 초기화를 할 때 유용
@Test
테스트 메소드 지정
@Autowired
필드 주입 방식 사용, 테스트 코드에서는 필드주입을 사용
@DisplayName
테스트 실행 후 Run 창에서 실행한 테스트의 이름을 설정
테스트코드는 개발자가 더미데이터를 생성한 후 해당 더미데이터가 비즈니스 로직에 알맞게 들어갔는지 검증하면서 테스트를 진행합니다.
섹션 4
presentation패키지에 Controller와 DTO, Repository를 생성하였습니다.
Controller
레이어드 아키텍처에서 사용자의 요청이 진입하는 엔트리포인트로 실질적인 처리를 하는 service레이어로 넘겨주고 응답을 반환하는 기능을 합니다. @RequestMapping, @GetMapping 등 의 기능을 통해 진입 api를 설정합니다.
@Controller, @RestController 어노테이션이 있습니다.
@Controller
return 되는 문자열같은 html파일을 찾아 클라이언트에 응답
모델에 값을 담아두면 해당 html에서 모델에 담긴 값을 꺼내어 사용이 가능
@RestController
@Response와 @Controller가 합쳐진 기능
CSR방식의 웹 개발, 앱 개발, 데이터의 처리만 담당하는 API 개발 시 사용
return값은 그대로 HTTP 응답 메시지 바디에 들어가며 여러 타입으로 변환하여 반환 가능
Service
컨트롤러에서 받은 데이터를 비즈니스 로직에 맞게 처리하는 영역이며 필요에 따라 Repositry를 호출합니다.
Repository
데이터베이스와 상호작용 하는 영역
DTO
DTO(Data Transfer Object)는 데이터 전송 객체로, 주로 소프트웨어 애플리케이션에서 데이터의 전송 및 관리를 간편하게 하기 위해 사용되는 객체입니다. 데이터를 그룹화하여 전송할 때 유용합니다.
데이터 전송을 단순화하고 최적화하기 위함으로 비즈니스 로직이 없는 단순한 데이터 구조로 시스템간의 데이터 교환을 간소화 할 수 있다.
리포지토리 개발
이번 강의에서 리포지토리는 퍼사드 패턴을 적용하여 개발을 하였습니다. 퍼사드 패턴은 복잡한 여러 기능을 단순화해주는 소프트웨어 디자인 패턴입니다.
@Repository
class PresentationRepository (
private val achievementRepository: AchievementRepository,
private val introductionRepository: IntroductionRepository,
private val linkRepository: LinkRepository,
private val skillRepository: SkillRepository,
private val projectRepository: ProjectRepository,
private val experienceRepository: ExperienceRepository,
){
fun getActiveAchievements(): List<Achievement>{
return achievementRepository.findAllByIsActive(true)
}
fun getActiveExperiences(): List<Experience>{
return experienceRepository.findAllByIsActive(true)
}
fun getActiveAIntroductions(): List<Introduction>{
return introductionRepository.findAllByIsActive(true)
}
fun getActiveLinks(): List<Link>{
return linkRepository.findAllByIsActive(true)
}
fun getActiveSkills(): List<Skill>{
return skillRepository.findAllByIsActive(true)
}
fun getActiveProjects(): List<Project>{
return projectRepository.findAllByIsActive(true)
}
}
생성자 주입을 통해 필드로 리포지토리들을 보유하고 있으며 메서드를 구현하여 각 리포지토리에서 활성화된 객체를 가지고 옵니다.
서비스 개발
@Transactional 어노테이션을 이용하여 트랜잭션을 시작합니다.
reagOnly
일기전용 트랜잭션으로 설정, JPA를 사용시 더티체킹을 하지 않음
rollbackFor
어떤 예외시 롤백할지 설정
isolation
격리 수준을 정의
@Service
class PresentationService(
private val presentationRepository: PresentationRepository
) {
@Transactional(readOnly = true)
fun getIntroductions(): List<IntroductionDTO>{
val introductions = presentationRepository.getActiveAIntroductions()
return introductions.map { IntroductionDTO(it) }
}
@Transactional(readOnly = true)
fun getLinks(): List<LinkDTO>{
val links = presentationRepository.getActiveLinks()
return links.map { LinkDTO(it) }
}
@Transactional(readOnly = true)
fun getResume(): ResumeDTO{
val experiences = presentationRepository.getActiveExperiences()
val achievements = presentationRepository.getActiveAchievements()
val skills = presentationRepository.getActiveSkills()
return ResumeDTO(
experiences = experiences,
achievements = achievements,
skills = skills,
)
}
@Transactional(readOnly = true)
fun getProjects(): List<ProjectDTO>{
val projects = presentationRepository.getActiveProjects()
return projects.map { ProjectDTO(it) }
}
}
생성자 주입으로 리포지토리를 주입받아 서비스 레이어에서 리포지토리를 호출하여 비즈니스 로직을 실행하고 있습니다
서비스 테스트 코드 작성
@ExtendWith : Junit5에서 테스트 확장을 지원
@InjectionMocks, : Mockito에서 테스트 대상이 되는 클래스에 인서턴스 주입을 위해 사용
@Mock : Mockito에서 Mock 객체를 생성
Mock는 실제 객체를 대체하는 가짜 객체를 의미합니다.테스트에서 특정 객체의 모의 객체를 사용하게 해줍니다.
@Mock을 이용하여 모의 객체를 생성하고 @InjectionMocks를 사용하여 주입된 모의 객체를 사용하는 객체를 생성 합니다.
@AutoWired를 사용하지 않고 의존성 주입을 받으면서 실제 DB에 의존하지 않고 테스트를 진행할 수 있습니다
댓글을 작성해보세요.