inflearn logo
강의

강의

N
챌린지

챌린지

멘토링

멘토링

N
클립

클립

로드맵

로드맵

지식공유

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

river_bori
0

일주일간 학습한 내용 요약

개발 - Domain

프로젝트 생성

Jar로 해야지 스프링부트에서 제공하는 내장 키트를 사용할 수 있다.

Dependensies: 프로젝트에서 쓸 외부 라이브러리들을 추가해 주는 작업

6개의 라이브러리 추가

  1. Spring web: MVC사용 등

  2. Thymeleaf: 템플릿과 데이터를 합쳐서 최종적으로 완성된 html 파일을 만들어준다. (없으면 개발자가 html 파일까지 코드를 짜야함)

  3. Spring Data JPA: JPA에 껍데기를 씌서 사용성을 높임

  4. My SQL Driver

  5. H2 Database: 인메모리 DB로 스프링이 켜질때 같이 켜짐(스프링과 같은 메모리 사용), 꺼질때 데이터 사라짐

  6. Validation: 검증기능

그외

Spring Security: 로그인 기능에 사용하지만 지금 설치하면 스프링 킬때마다 로그인 해야해서 일단 설치 제외 

IntelliJ 설정

 

Git과 Github

Git 용어

Git 명령어

터미널에서
'pwd' 입력 => 폴더의 경로 확인
'git init' 입력 => 깃 폴더 초기화
'git status'입력 => 깃에서 관리하지 않는 파일들이 빨간색으로 표시됨. 그 중 관리하지 않아도 된는 파일들을 배제하고 등록해줌.

프로젝트 환경 변수 설정

데이터 소스와 jpa설정 => 설정해놓은 값을 복붙함.

위와 관련된 중요 개념
이런 설정 값들은 보통 상수(=변하지 않는 값)다.

자바 코드에 " "(literal, 문자열 방식)으로 관리해도 되지만, 같은 값을 여러 클래스에서 사용할 때, 값이 수정되면 모든 클래스에서 사용한 값들을 다 찾아서 수정해줘야 한다. 이때, 하나라도 놓치면 에러가 난다.

실제 운영할 때는 개발용 서버, 운영용 서버, 개발용 DB, 운영용 DB로 나눠서 사용한다.

서로 다른 서버 컴퓨터에서 똑같은 프로그램이 돌아가는데 서로 다른 DB서버에 붙어있다. 개발DB와 운영DB의 주소는 다르다 => 환경변수(=환경마다 바뀌는 값)

개발서버에게 개발DB URL을, 운영서버에게 운영DB URL을 알려줘야하는데 " "(문자열 방식)으로는 관리가 어렵다.

Spring Profile과 application.yml

yml

클래스 생성

도메인 패키지에서 개발할 클래스들을 미리 껍데기만 만듦

포트폴리오 패키지

entity 패키지 (11개 클래스)

BaseEntity(추상클래스):모든 테이블들이 공통적으로 갖는 Created Date Time, Updated Date Time 컬럼들은 각 클래스에 직접 넣지 않고 상속을 활용할 예정

Achievement: BaseEntitiy클래스를 상속받는다.

Achievement 클래스를 복사해서 다른 클래스들을 만든다. (@Column의 name등 바꾸기)

 

repository 패키지 (8개 인터페이스)

Spring Data JPA Repository

 AchievementRepository

 나머지 엔티티에 대응하는 레퍼지토리 만들기

 

엔티티 개발 - 연관관계 없음

BaseEntity.kt

(다른 엔티티와) 연관관계가 없는 엔티티


생성자(영어: constructor, 혹은 약자로 ctor)는 객체 지향 프로그래밍에서 객체의 초기화를 담당하는 서브루틴을 가리킨다. 생성자는 객체가 처음 생성될 때 호출되어 멤버 변수를 초기화하고, 필요에 따라 자원을 할당하기도 한다. 객체의 생성 시에 호출되기 때문에 생성자라는 이름이 붙었다.

[위키백과]


Achievement.kt

Skill.kt

HttpInterface.kt


HTTP 쿠키(HTTP cookie)란 웹 서버에 의해 사용자의 컴퓨터에 저장되는, '이름을 가진 작은 크기의 데이터'이다. 인터넷 사용자가 어떠한 웹사이트를 방문할 경우 사용자의 웹 브라우저를 통해 인터넷 사용자의 컴퓨터나 다른 기기에 설치되는 작은 기록 정보 파일을 일컫는다. 쿠키, 웹 쿠키, 브라우저 쿠키라고도 한다. 이 기록 파일에 담긴 정보는 인터넷 사용자가 같은 웹사이트를 방문할 때마다 읽히고 수시로 새로운 정보로 바뀐다. 이 수단은 넷스케이프의 프로그램 개발자였던 루 몬툴리가 고안한 뒤로 오늘날 많은 서버 및 웹사이트들이 브라우저의 신속성을 위해 즐겨 쓰고 있다. (=> 신속성 = 서버크기 예측..?)

쿠키는 소프트웨어가 아니다. 쿠키는 컴퓨터 내에서 프로그램처럼 실행될 수 없으며 바이러스를 옮길 수도, 악성코드를 설치할 수도 없다. 하지만 스파이웨어를 통해 유저의 브라우징 행동을 추적하는데에 사용될 수 있고, 누군가의 쿠키를 훔쳐서 해당 사용자의 웹 계정 접근권한을 획득할 수도 있다.

[위키백과]


 

엔티티 개발 - 연관관계 있음

Experience.kt
생성자에 초기값을 넣는다.
필드를 선언한다.
Experience Entity는 ExperienceDetail과 1:N의 관계
jpa에서는 List로 N쪽에 해당하는 필드를 가져올 수 있다.

@OneToMany(targetEntity = ExperienceDetail::class, 
           fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
@JoinColumn(name = "experience_id")
var details: MutableList<ExperienceDetail> = mutableListOf()

ExperienceDetail.kt: 연관관계 없는 엔티티와 비슷
experienceDetail만 가지고는 experience를 찾을 수 없는 일대다 단방향 연관관계

Project.kt, ProjectDetail.kt 는 experience, ...detail과 비슷

@OneToMany(mappedBy = "project",
    fetch = FetchType.LAZY,
    cascade = [CascadeType.PERSIST])
var skills: MutableList<ProjectSkill> = mutableListOf()

ProjectSkill.kt: 다대일의 관계라서 프로젝트와 스킬을 각각 연결

@ManyToOne(targetEntity = Project::class, fetch = FetchType.LAZY)
@JoinColumn(name = "project_id", nullable = false)
var project: Project = project

@ManyToOne(targetEntity = Skill::class, fetch = FetchType.LAZY)
@JoinColumn(name = "skill_id", nullable = false)
var skill: Skill = skill

 

데이터베이스 초기화

프로필

데이터 초기화 코드 작성

도메인 패키지 안에 DataInitializer.kt 생성 (개발 편의를 위해 임의로 만든 것임)

experience1.addDetails(
    mutableListOf(
        ExperienceDetail(content = "GPA 4.3/4.5", isActive = true),
        ExperienceDetail(content = "소프트웨어 연구 학회 활동", isActive = true)
    )
)

experience2.addDetails(
    mutableListOf(
        ExperienceDetail(content = "유기묘 위치 공유 서비스 개발", isActive = true),
        ExperienceDetail(content = "신입 교육 프로그램 우수상 수상", isActive = true)
    )
)
// 방법1
project1.addDetails(
    mutableListOf(
        ProjectDetail(content = "구글 맵스를 활용한 유기묘 발견 지역 정보 제공 API 개발", url = null, isActive = true),
        ProjectDetail(content = "Redis 적용하여 인기 게시글의 조회 속도 1.5초 → 0.5초로 개선", url = null, isActive = true)
    )
)
// 방법2 => 다양한 방법이 있다
project1.skills.addAll(
    mutableListOf(
        ProjectSkill(project = project1, skill = java),
        ProjectSkill(project = project1, skill = spring),
        ProjectSkill(project = project1, skill = mysql),
        ProjectSkill(project = project1, skill = redis)
    )
)

val: 불변(Immutable) 변수로, 값의 읽기만 허용되는 변수. 값(Value)의 약자이다.
변수를 선언할 때 지정한 값에서 더이상 변경하지 않는 경우

var: 가변(Mutable) 변수로, 값의 읽기와 쓰기가 모두 허용되는 변수. 변수(Variable)의 약자이다.
변수의 값을 바꿔야 하는 경우

출처: https://kotlinworld.com/173

 

리포지토리 개발

JAP엔티티를 미리 정의해 두고 인터페이스만 만들면 Spring이 실행되면서, 리포지토리 인터페이스를 기반으로 리포지토리 클래스들을 만들어서 Spring Bean으로 등록한다.

@Entity
class Experience(...

interface AchievementRepository : JpaRepository<Achievement, Long> {...

서비스 Bean에서 리포지토리 빈들을 주입받아서 바로 사용 가능하다. 이때 사용하는 기능들은 Insert, Update, ID로 조회하기, ID로 삭제하기 등이 있다. 특정 컬럼 조회하기 등은 기본 메소드에 없다.
인터페이스에 미리 정해진 규칙대로 메소드 이름을 정의해주면, 메소드 이름을 기반으로 쿼리를 작성해준다. => A부터 Z까지의 컬럼이 있을 때, 개발자가 A, B를 조회하고 싶다면 'Find by A and B' 이런 식으로 메소드 이름을 정의하고 파라미터로 A와 B를 넣어 주도록 인터페이스에 메소드를 정의하면 된다.

// select * from achievement where is_active = :isActive
fun findAllByIsActive(isActive: Boolean): List<Achievement>

SkillRepository.kt 에는 메소드를 하나 더 만든다.

// select * from skill where lower(name) = lower(:name) and skill_type = :type
fun findByNameIgnoreCaseAndType(name: String, type: SkillType): Optional<Skill>

 

리포지토리 테스트 코드 작성

테스트 코드는 매우 정말 중요하다.
=> 강의용 프로젝트같이 규모가 작은 경우에는 덜 중요할 수 있다.

IntelliJ는 특정 클래스의 테스트 클래스를 쉽게 만들어주는 기능을 제공한다.

DataInitializerTest.kt

[테스트할 클래스 -> 마우스 오른쪽 -> Generate -> test]
도메인 등 원래 클래스가 있던 것과 같은 경로test패키지 안에 test 클래스가 생성된다.
=> DataInitializerTest.kt 삭제 (테스트할 대상이 Spring Data JPA Repository Interface 이기 때문)
인터페이스여서 테스트 클래스를 만들 수 없고 같은 규칙으로 직접 만듦

test>kotlin>com>bohui>portfolio>domain 안에 패키지 '리포지토리'를 만든다.

테스트 코드 작성은 많은 작업이 필요해서 오래 걸린다. => 때문에 찐 테스트 코드를 작성하지 않고, 작성하는 방식을 보여줄 예정

Experience와 Project Repository 에 대해서만 테스트 클래스 생성

ExperienceRepositoryTest.kt

@DataJpaTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ExperienceRepositoryTest(
    @Autowired val experienceRepository: ExperienceRepository // 테스트할 대상을 주입받음
) 
// 테스트 데이터 초기화
@BeforeAll
fun beforeAll() {
    println("----- 데이터 초기화 이전 조회 시작 -----")
    val beforeInitialize = experienceRepository.findAll()
    assertThat(beforeInitialize).hasSize(0) // 테스트를 검증하는 메소드
    println("----- 데이터 초기화 이전 조회 종료 -----")
    println("----- 테스트 데이터 초기화 시작 -----")
    val experiences = mutableListOf<Experience>()
    for (i in 1..DATA_SIZE) {
        val experience = createExperience(i)
        experiences.add(experience)
    }
    experienceRepository.saveAll(experiences)
    println("----- 테스트 데이터 초기화 종료 -----")
}
@Test
fun testFindAll() {
    println("----- findAll 테스트 시작 -----")
    val experiences = experienceRepository.findAll()
    assertThat(experiences).hasSize(DATA_SIZE)
    println("experiences.size: ${experiences.size}")
    for (experience in experiences) {
        assertThat(experience.details).hasSize(experience.title.toInt())
        println("experience.details.size: ${experience.details.size}")
    }
    println("----- findAll 테스트 종료 -----")

 

리포지토리 성능 개선

JPQL의 fact join을 활용해 jpa에서 발생하는 n+문제를 해결하고, ProjectRepositoryExperienceRepository의 성능을 개선.

ExperienceRepositoryTest.ktfun testFindAllByIsActive() 실행

ExperienceRepository.ktfun findAllByIsActive위에 @Query("select e from Experience e left join fetch e.details where e.isActive = :isActive") 달아줌
- 'e' alias 별칭

ProjectRepository.kt
projectSkill, projectDetail과 관계를 맺고 있다.

패치조인의 단점, 한계점이 위와 같이 여러 개의 엔티티와 관계를 맺고 있을 때, 이것들을 한꺼번에 조회할 수 없다.
=> 네이티브 쿼리로 풀거나, 쿼리 DSL or something
=> yml의 default_batch_fetch_size: 10 을 통해 어느정도의 성능 문제를 해결 => n+1의 완전히 해결하는 것이 아닌 fetchSize의 값에 따라, m의 팻치사이즈가 n번 나가는 쿼리를 n/m으로 줄여준다.

백엔드 SpringBoot Kotlin Web

답변 0