[인프런 워밍업 클럽 3기 백엔드 ]발자국 2주차

[인프런 워밍업 클럽 3기 백엔드 ]발자국 2주차

해당 글은 ‘입문자를 위한 Spring Boot with Kotlin - 나만의 포트폴리오 사이트 만들기(정보근)’ 강의 를 수강하고 작성한 내용입니다.

https://www.inflearn.com/course/입문자-spring-boot-kotlin-포트폴리오/dashboard

📝 강의 내용 정리


[실습] 데이터베이스 초기화

@component 어노테이션

스프링 빈에 등록시킬 클래스라고 알려줌

그외 @controller, @service, @repository도 컴포넌트 중 하나(원본코드가면 component가 붙어잇음)

@Profile(value=[”default”])

프로필이 디폴트일때만 해당 클래스를 빈으로 등록해라(개발용 프로필 데이터 용)

생성자 주입시

먼저 등록시켜야할 클래스들 먼저 등록시킴

예를들어 현재 클래스에서 생성자 주입으로 레포지토리 클래스들을 주입 받으면

스프링은 해당 레포지토리 클래스들을 먼저 만들고 그다음에 해당 클래스를 생성

@PostConstruct

스프링 빈 주입 후 해당 어노테이션이 붙은 메서드들을 한번 더 실행하면

스프링 초기 빌드 끝나는거임(찾아봐 인터넷에 확실X)

출력문으로 해당 메서드가 언제 타는지 확인해보면

→ 스프링 빈들 적용 완료됐다는 로그 뒤에 찍힘

→ 해당 메서드 실행되고 난후에 톰캣 열림

<aside> 📌

실제 프로젝트는 출력할때 print쓰지 말고 log 사용하셈

</aside>

레포지토리 클래스의 saveAll 메서드

인터페이스인 JpaRespository 에 정의되어있음 그래서 우리가 따로 만들지 않았더라도 쓸 수 있음

(매우 기본적인 쿼리문은 jpa가 미리 정의해놔서 가져다 쓰면 됨)

experienceRepository.saveAll()로 데이터를 저장하는데

이렇게 하면 experience랑 experienceDetails랑 연관관계라

저거 saveall만 해도 experienceDetails에도 삽입 쿼리가 날라감(이거가 엔티티에서 cascadeType을 지정해줬기 때문)

=⇒ 쿼리문 로그에서 확인 해보셈

[실습] 리포지토리 개발

레포지토리 리턴값이 여러개 올것같으면 List형으로 받아오기

하나만 들어올때면 Optional로 받아오기

override fun findById(id: Long): Optional<Project

이거 jpa에 기본있는 메서드 명인데 다시 만들려면 앞에 override 붙여줘야함

<aside> 📌

프로젝트 클래스랑 익스페리언스 클래스 같이 연관관계가 있는 클래스들은 현재 코드로는 성능이 좋지 않음 다음 시간에 성능 개선용 코드를 다시 짤 것 같음

</aside>

[실습] 리포지토리 테스트 코드 작성

테스트 코드 작성 이유

큰 서비스 일 수록 의도하지 않은 결과가 나올 수 있기 때문에 테스트 코드를 촘촘하게 작성하여 서비스 전체 기능에 미치는 사이드 이펙트를 사전에 감지하는 것이 좋음

테스트 코드 작성법

  • 일반 class

  1. 테스트 코드를 작성할 클래스를 오른쪽 마우스 클릭

  2. Generate → Test

  3. test 패키지에 해당 테스트 클래스가 생성됨

  • 인터페이스(ex. 레포지토리)

  1. 테스트 패키지 안에 직접 만들어줘야함

  2. 테스트할 인터페이스 이름에 +Test 이름으로 클래스 생성(코틀린 클래스)

  3. 생성된 클래스 위에 어노테이션 추가

    • @DataJpaTest : jpa 관련 테스트를 위한 설정 제공

    • @TestInstance: 테스트 인스턴스의 라이프 사이클 지정. 각 메서드 마다 인스턴스를 생성하는게 아닌 모든 테이스에 적용될 작업을 beforeall 로 한번만 수행할 것이기 때문(—> 이건 좀 더 찾아봐야될)

      https://devs0n.tistory.com/40

    • @BeforeAll : 테스트 클래스 내의 모든 @Test 메서드 실행 전에 한 번 실행됨

  4. 의존성 주입은 클래스() 안에 @Autowired로 주입

  5. 테스트할 메서드에 @Test 어노테이션 달아야 테스트로 인식

<aside> 📌

assertThat( import assertj) 사용: 더 직관적

ctrl+ r 로 replace로 코드 한번에 변경 가능

</aside>

[실습] 리포지토리 성능 개선

N+1 문제

현재 테스트 코드에서

findAllByIsActive() 테스트를 돌려보면 로그에 굉장히 많은 쿼리문이 나가는 것을 확인할 수 있다

⇒ JPA N+1 문제

부모꺼 부르고 자식 것도 다 불러오는 문제

  • 코드에서는

    테스트 메서드 안에 있는 for문 돌때 experience.details 찾을때마다 쿼리가 날라감

    → 이유: 엔티티클래스에서 fetch타입이 lazy였기 때문에

    ⇒ 이걸 만약 eager로 바꾼다면

    → for문 말고 findBy 할때 여러개 쿼리가 날라감

    (lazy랑 eager랑 이 차이인거)

⇒ jpa proxy가 fetch 타입이 lazy인 경우 가짜 객체 상태로 있다가, 해당 객체가 호출됬는데 데이터가 없을 경우 쿼리를 날려서 그때 값을 가져오는거

해결법

  1. fetch join

    jpa에 의존하지 않고 직접 jpql쿼리를 보냄

    → 쿼리가 하나만 나감.(Good~)

    하지만 해당 방법은 onetomany나 manyto many관계의 자식 엔티티가 여러개일 경우 하나만 조인할 수 있다는 한계가 존재함

    <aside> 📌

    조인 테스트

    한쪽에만 데이터가 있는경우엔 left join 사용

    그냥 join은 양쪽에 다 데이터가 있어야지만 가져올 수 있음

    </aside>

  2. batch fetch size

    ex) ProjectRepository는 여러개가 참조되어있어서 1로 안됨

    단. project랑 p.skill이랑 조인하고 여기서 s.skill이랑 조인은 됨

    일단 자료에 있는 쿼리문 적은다음에 테스트 실행해보면 쿼리문이 여러개 나감 → 프로젝트랑 스킬즈는 join이 되었는데 디테일즈는 조인이 안되서 detail 호출할때마다 쿼리가 날라가는거래(=lazy)

    ⇒ 즉 여러개 조인은 못가져옴

    이걸 해결하려면 properties에서 default_batch_fetch_size를 수정 해야함

    → 이렇게 하면 나가는 쿼리가 줄음, 단 in으로 한번에 가져옴

    <결론>

    이거는 n번 나갈 쿼리를 batch size를 키워서 한번에 in으로 여러개 가져옴

    1+N 이 1+(N/batch_ferch_size) 로 줄일 수 있다아

    결론적으로 쿼리문을 줄일 순 있다아

 

해당 페이지 어노테이션 정리

  • @Controller

    컴포넌트 스캔의 대상이 되어 빈으로 등록됨. SSR(서버 사이드 랜더링) 방식으로 웹 개발 시 사용됨. 스프링 내부적으로 return된 문자열과 같은 이름을 갖는 html파일을 찾아 클라이언트에게 응답

  • @RestController

    컴포넌트 스캔의 대상이 되어 빈으로 등록됨. CSR(클라이언트 사이드 렌더링) 방식으로 웹 개발을 하거나, 데이터의 처리만을 담당하는 API를 개발할 때 사용됨. return되는 값은 그대로 HTTP 응답 메시지의 Body에 들어감. String 외의 타입으로 return 시 HTTP 헤더 값 설정에 따라 JSON으로 변환하여 Body에 넣음

  • @RequestMapping

    HTTP요청을 정의하는 역할. 클래스와 메소드에 붙일 수 있으며 클래스에 붙일 시 해당 경로가 클래스 내부 모든 메소드에 공통적으로 적용됨

  • @GetMapping

    @RequestMapping(method = [RequestMethod.GET], name =”/test”) 와 같고, 해당 경로로 GET요청을 했을 때 해당 Method가 호출됨.

  • @Service

    컴포넌트 스캔의 대상이 되며 서비스 레이어에 해당함을 명시

  • @Repository

    컴포넌트 스캔의 대상이 되며 리포지토리 레이어에 해당함을 명시

  • @Component

    컴포넌트 스캔의 대상이 되어 빈으로 등록됨

[실습] 클래스 생성

@Controller@RestController차이점

  • @Controller

    해당 컨트롤러 내부로 들어와서 path 메서드의 리턴값과 동일한 이름의 html파일이 스프링 내부에 있으면 해당 페이지를 로드해줌

    ex)

    @Controller
    class ControllerClass{
    	@GetMapping("/test")
    	fun test(): String{
    		return "test"
    	}
    }
    

    ⇒ 스프링 프로젝트 resources/templates/위치에 test.html 이라는 파일이 존재하면 해당 path로 get요청 시 return값이 test이므로 test.html 페이지(view 파일)가 열림

    (단적인 예로 만약 return “ttest”라고 오타가 나면 못 불러옴)

  • @RestController

    해당 어노테이션은 내부로 들어가면 @ResponseBody라는 어노테이션이 붙어있는데 이게 return 값을 그대로 http ResponseBody에 넣어서 돌려줌(위에@controller처럼 관련 view파일을 찾는게 아님)

    ex)

    @RestController
    class RestControllerClass{
    	@GetMapping("/test")
    	fun test(): String{
    		return "OK"
    	}
    }
    

    ⇒ 해당 경로로 접근하면 그냥 웹페이지에 OK 글자만 나옴

@GetMapping(”/test”)

= @RequestMapping(method = [RequestMethod.GET], name =”/test”)

[실습] DTO 개발

  • DTO

    데이터 트랜스퍼 오브젝트의 약자. 데이터를 담는 통 같은 역할

    [사용이유]

    엔티티를 클라이언트에 바로 전달하게 되면 데이터베이스에 연결된 모든 컬럼들을 모두 전달하는게 비효율적이고, 보안적 이슈도 있고, 원하지 않는데이터가 데이터베이스에 적용될 문제도 있기 때문

    [결론]

    엔티티는 내부적으로만 사용하고, 외부로 노출되는 데이터는 DTO에 담아서 전달하는것으로

  • data class

    코틀린에서 제공하는 dto 전용 클래스

    [특징]

    ToString 사용시 데이터 들을 key, value 형태로 출력해줌

    선언할 변수들은 생성자 위치에

  • 엔티티에서 LocalDateTime형인 변수들을 DTO에서는 그냥 String형으로 사용(어차피 자바의 자료형을 클라이언트에서 사용 못함)

    → 그래서 그냥 문자, 숫자 정도만 구분해서 넣어주는게 좋음

[실습] 리포지토리 개발

  • presentation 패키지의 repository 클래스들

    디자인 패턴 중 퍼사드 패턴의 기능

    이전의 도메인 패키지의 repository클래스들은 데이터베이스랑 직접적으로 상호작용하는 클래스이고

    현재 패키지의 repository클래스들은 도메인의 기능들을 활용해서 presentation 레이어에 필요한 기능들을 한번 더 랩핑해서 사용하는 목적

    → 즉 presentation 레이어에 있는 서비스 클래스들이 도메인 패키지의 레포지토리들을 의존성 주입을 각각 받는걸 막고, 해당 레포지토리 클래스에서 묶어서 주입 받을 예정

<결론>

해당 레포지토리 클래스는 도메인 패키지의 레포지토리들을 랩핑하는 역할이다

[실습] 서비스 개발

  • readOnly: 읽기 전용, jpa의 더티 체킹 등을 수행하지 않아 약간 성능상의 이점이 있음 → 조회만 하는 메서드는 붙여주는게 좋음

[실습] 서비스 테스트 코드 작성

  • 단위테스트

    테스트 하는 기능에 더 집중하여 하나의 기능만 테스트

[Mockito]

mockito를 사용하려면 클래스 위에

@ExtendWith(MockitoExtension::class)를 붙여야함

@InjectMocks

만든 mock을 주입할 대상(=테스트 할 대상)

lateinit

코틀린은 null을 허용하지 않기 때문에 초기화가 필순데, lateinit을 하면 초기화를 늦추는 작업. (mock이 생성된 다음에 초기화 할 목적)

@Mock이 붙은 레포지토리로 데이터를 만들어서 그걸로 서비스 테스트 진행

[실습] 컨트롤러 개발

  • api 컨트롤러는 데이터의 전달에만 집중

    클라이언트 사이드 랜더링(SSR) 방식으로 개발 시 프론트엔드 개발자는 프론트엔드쪽 프로그램을 별도로 만들고 서버쪽은 데이터 처리에만 집중하고 둘이 데이터만 주고받음

  • 뷰 컨트롤러

    서버사이드렌더링

    서버에서 프론트 작업까지 다하고 결과물인 html파일만 전달

[api 컨트롤러]

  • api 버전 관리를 위해 path에 버전명 명시 → 버전링

  • 컨트롤러를 웹브라우저에서 호출해보면 리턴한 DTO값들이 JSON포맷의 스트링으로 바껴서 브라우저에 나타남

  • 주로 JSON으로 데이터 주고받음

[view 컨트롤러]

  • 해당 컨트롤러에서는 데이터를 리턴하는게 아니고 html이름을 반환할꺼임

  • 서비스에서 받은 데이터들은 model.addAttribute에 담아놓으면 됨

    model.addAttribute(”key”, value)

    ⇒ 나중에 html에서 해당 키값 불러오면 되겠군.

  • 이 방법을 사용하려면 패스 경로에 해당 html이 존재해야만 함.

[실습] 컨트롤러 테스트 코드 작성

API 컨트롤러 테스트

  • 통합 테스트

    스프링부트를 다 띄운다음에 api를 직접호출해서 결과 검증

  • @SpringBootTest

    SpringBoot어플리케이션을 테스트하는데 사용. 실제 어플리케이션과 유사한 환경을 구성하여 테스트를 실행

  • @AutoConfigureMockMVC

    SpringMVC를 모의로 테스트하는데 사용. MockMVC객체가 자동으로 구성되어 컨트롤러를 모의로 테스트할 수 있음

uri 호출 메서드

performGet()

MvcResult를 리턴

mockMvc빌더를 이용해서

.perform(MockMvcRequestBuilders.get(uri)

해당 uri를 get 방식으로 호출해줌

.andDo(MockMvcResultHandlers.print())

호출 후 결과를 출력

.andReturn()

결과를 mvcResult로 리턴

contentAsString

http Response 바디값을 String으로 파싱

jsonArray

스트링 값을 json배열로 파싱

⇒ resume같은 경우엔 애초에 객체로 return을 받기 때문에 jsonObject로 파싱

.optJSONArray를 써서 키값을 넣어주면 해당 객체의 value값을 가져옴

[실습] Thymeleaf - 부트스트랩 템플릿

  • 부트스트랩 탬플릿 가져오기

    사용할 프로젝트 다운 후

    프로젝트 src/main/resources/templates 경로에

    presentation 폴더 생성 후

    다운받은 프로젝트의

    index.html, project.html, resume.html 복사

    src/main/resources/static경로에

    assets, css, js 폴더 복사

[실습] Thymeleaf - 템플릿 수정(index)

html은 동적인 언어가 아니기 때문에 공통적인 내용을 여러 페이지에 표시할 때 같은 코드를 매번 중복해서 작성해줘야함

→ 이 중복을 줄이기 위해 타임리프의 프레그먼트 기능을 이용

현재 코드를 fragment화

  • index.html → fragment-navigation.html

    1. index.html의 <nav> 태그 안의 내용을 fragment-navigation.html 의 <body>태그 안에 잘라넣기(원래 부분은 지워

    2. 코드 내의 href에 있는 .html 제거

    3. 현재 부분이 웹페이지 상단의 네비게이션 바 부분

      contact는 안쓸꺼니까 지우고

    4. span 태그 옆에 Start Bootstrap 이름 내 이름으로 변경

    5. <nav 태그안 class=””옆에 th:fragment=”navigation” 추가

    6. index.html의 지워진<nav> 위치에 <div> 태그로 교체

    ⇒ 이렇게 하면 타임리프가 th:replace를 보고 해당 경로에 있는 파일을 찾아 그 안에서 해당 이름의 프래그먼트 조각을 가져와 해당 위치에 넣어줌

  • head, footer, dots 부분 위와 동일하게 fragment화 진행

About 섹션 수정

여기에 텍스트들은

introductions의 요소들을 한줄한줄 표시하는 형태로 진행

타임리프의 th:each 를 이용해 데이터베이스에서 받아온 값을 동적으로 html 에 표시

[about의 텍스트 데이터(introductions) 수정] <p *class*="text-muted" *th:each*="introduction : ${introductions}" *th:text*="${introduction.content}">텍스트값</p>

  • ${introductions} 에서 받아온 값들을 introduction에 하나씩

    ⇒ 컨트롤러에서 model에 넣어줬던 데이터임.

  • th:text 에서 데이터로 들어온 introduction.content 값을 오른쪽 텍스트값에 넣어줌

[about의 링크 데이터(links) 수정] <a *class*="text-gradient" *href*="#!" *target*="_blank" *th:each*="link : ${links}" *th:href*="${link.content}"><i *class*="bi bi-github" *th:class*="|bi bi-${link.name}|"></i></a>

  • th:href 받아온 데이터 값으로 href=””값 변경해줌

  • <i>는 아이콘 인데 아이콘 이름도 link.name으로 변경

  • target=”_blank” 이걸 넣어주면 링크가 새 탭에서 열림

타임리프 문법 미리보기

  • th:if : 조건이 참일 경우 해당 HTML요소를 표시하고, 거짓일 경우 표시하지 않음

Experience카드를 fragment로 변경

공통사용 부분을 한번더 공통화

layout(~{::#content})

  • #content : id가 content인 태그를 저 안에 넣어줌

  • id 값들은 <main>태그에 넣어줄꺼임

[실습] 인터셉터 개발

  • 인터셉터

    컨트롤러 보다 앞단에서 동작

    컨트롤러의 요청을 한번 잡아서 공통적인 처리를 할 수 있게 해주는 기능

  • @Configuration: 설정값이 들어있는 Bean

  • ctrl + o: 오버라이드 할 수 있는 메서드 목록나옴

  • preHandle(): 컨트롤러 요청 전 동작

  • postHandle: 컨트롤러 응답 후 동작, 만약 컨트롤러가 예외를 발생시키거나 정상적으로 동작하지 않았으면 동작하지 않음

  • afterCompletion:컨트롤러 응답 후 동작, 컨트롤러의 성공 여부와 상관없이 항상 동작

미션


[미션1] 깃허브 리포지토리에 프로젝트 올리기


뻔한 주제이기는 하지만 로그인을 한 유저들끼리 글을 쓰고 읽을 수 있는 커뮤니티 게시판을 프로젝트 주제로 잡았다.

일단 기본적인 틀은 모든 사용자가 글을 올릴 수 있는 자유글 있고, 관리자만 올릴 수 있는 공지글이 있을 예정이며, 관리자가 게시판의 글을 관리할 수 있도록 구현할 예정이다.

[미션2] 테이블 설계하기


테이블은 크게 유저, 게시판, 카테고리로 잡았다.

처음엔 아무생각없이 유저정보를 아이디 비번 닉네임으로만 구성을 했었는데, 관리자가 있으려면 role필드가 필요하다는것을 알고 유저테이블에 추가하였다.

그리고 공지글과 자유글을 카테고리로 구분하도록 테이블을 구성하였다.

추후 글 카테고리를 자유롭게 생성시킬 수 있도록 확장성을 염두해 두었다.

[미션3] REST API 설계하기


기본적인 api만 설계해 놓았다. 유저 관련은 회원가입, 로그인, 로그아웃, 정보 조회와 같이 기본적인 기능등에 대해 적어놓았고,

게시판 API도 기본적인 CRUD에 관한 내용만 적어두었다. 아마 개발을 시작하다보면 추가적으로 수정할 부분들이 생길 것 같다.

카테고리 테이블에 관련한 CRUD는 아직 개발 예정으로 빼놓았다. 위의 기능들을 구현한 후 추가할 예정이다

📅2주차 회고


2주 정도 코드를 봤더니 저번주 보다는 예제 코틀린 코드들이 어느정도 익숙해진 것 같다. 그리고 이번주가 본격적으로 기능을 구현하는 진도였다보니 저번주보다 더 흥미롭게 강의를 들었다. 평소에 알고는 개념적으로만 인지하고있었던 N+1문제도 직접 비교를 보여주시면서 강의를해주시니 더 와닿았던 것 같다. 테스트 코드 부분은 아직 익숙하진 않아서 아직 어렵지만 강의에서 배운것을 토대로 미션 코드에 적용해보면 이해가 좀더 될것 같다. 타임리프 부분은 진짜 처음 다뤄봤는데..확실히 html은 나한테 맞지않았다..(코드가 너무 많아..) 그래도 화면이 직접보여지다보니 뭘 만들고있다는 느낌은 확실히 들어서 재미있었다. 부트스트랩이라는 홈페이지를 처음알았는데 혼자 프로젝트 할때도 손쉽게 프론트를 구현할 수 있을 것 같아 자주 이용할 예정이다.

이번주에 주어진 미션들은 아직은 프로젝트 설계부분이라서 뭘 많이 하지는 않았는데, 간단한 테이블을 짜는 작업에도 혼자서 하려니 살짝 부담이 있었다. 이런작업들을 좀더 유연하게 할 수 있도록 여러번 해봐야겠다는 생각이 많이 들었다. 다음주부터는 정말 기능 구현을 시작해야하는데, 자신이 조금 없지만 차근차근 해 볼 예정이다.

댓글을 작성해보세요.

채널톡 아이콘