템플릿에서 전달받은 데이터 사용하기

Model 객체에 저장한 값을 템플릿에서 사용할 수 있다고 했다. 어떻게 사용할수 있을까? 다음과 같이 question_list.html 템플릿을 수정해 보자.

[파일명: /sbb/src/main/resources/templates/question_list.html]

<table>
    <thead>
        <tr>
            <th>제목</th>
            <th>작성일시</th>
        </tr>
    </thead>
    <tbody>
        <tr th:each="question : ${questionList}">
            <td th:text="${question.subject}"></td>
            <td th:text="${question.createDate}"></td>
        </tr>
    </tbody>
</table>

질문 목록을 HTML의 테이블 구조로 표시되게 하였다.

템플릿 파일에 입력된 th:each="question : ${questionList}"와 같은 특이한 표현이 눈에띌 것이다. th: 로 시작하는 속성은 타임리프 템플릿 엔진이 사용하는 속성이다. 바로 이 부분이 자바 코드와 연결된다. question_list.html 파일에 사용한 타임리프 속성들을 잠시 살펴보자.

<tr th:each="question : ${questionList}">

QuestionController의 list 메서드에서 조회한 질문 목록 데이터를 "questionList"라는 이름으로 Model 객체에 저장했다. 타임리프는 Model 객체에 저장된 값을 읽을 수 있으므로 템플릿에서 questionList를 사용할수 있게 되는 것이다. 위의 코드는 <tr> ... </tr> 엘리먼트를 questionList의 갯수만큼 반복하여 출력하는 역할을 한다. 그리고 questionList에 저장된 데이터를 하나씩 꺼내 question 객체에 대입하여 반복구간 내에서 사용할수 있게 한다. 자바의 for each 문을 떠올리면 쉽게 이해할 수 있을 것이다.

다음 코드는 바로 앞의 for 문에서 얻은 question 객체의 제목을 <td> 엘리먼트의 텍스트로 출력한다.

<td th:text="${question.subject}"></td>

다음 코드도 같은 맥락으로 이해할 수 있다.

<td th:text="${question.createDate}"></td>

이제 브라우저에서 다시 http://localhost:8080/question/list에 접속해 보자. 그러면 다음과 같은 화면이 보일 것이다.

이전에 테스트로 등록한 질문 1건이 조회된 모습이다. 만약 테스트시 Question 데이터를 더 추가했다면 더 많은 질문이 표시될 것이다.

점프 투 스프링부트자주 사용하는 타임리프의 속성

타임리프의 자주 사용하는 속성에는 다음 3가지 유형이 있다. 이 3가지 유형만 알아도 여러 기능을 충분히 만들 수 있다.

1. 분기문 속성

분기문 속성은 다음과 같이 사용한다.

th:if="${question != null}"

위의 경우 question 객체가 null 이 아닌 경우에 해당 엘리먼트가 표시된다.

2. 반복문 속성

반복문은 반복횟수만큼 해당 엘리먼트를 반복하여 표시한다. 반복문 속성은 자바의 for each 문과 유사하다.

th:each="question : ${questionList}"

반복문은 다음과 같이 사용할 수도 있다.

th:each="question, loop : ${questionList}"

추가한 loop 객체를 이용하여 루프 내에서 다음과 같은 속성을 사용할수 있다.

  • loop.index - 반복 순서, 0부터 1씩 증가
  • loop.count - 반복 순서, 1부터 1씩 증가
  • loop.size - 반복 객체의 요소 갯수 (예: questionList의 요소 갯수)
  • loop.first - 루프의 첫번째 순서인 경우 true
  • loop.last - 루프의 마지막 순서인 경우 true
  • loop.odd - 루프의 홀수번째 순서인 경우 true
  • loop.even - 루프의 짝수번째 순서인 경우 true
  • loop.current - 현재 대입된 객체 (예: 위의 경우 question과 동일)

3. 텍스트 속성

th:text=값 속성은 해당 엘리먼트의 텍스트로 "값"을 출력한다.

th:text="${question.subject}"

텍스트는 th:text 속성 대신에 다음처럼 대괄호를 사용하여 값을 직접 출력할수 있다.

<tr th:each="question : ${questionList}">
    <td>[[${question.subject}]]</td>
    <td>[[${question.createDate}]]</td>
</tr>

이 책에서는 새로운 타임리프 문법이 나올 때마다 자세히 설명할 것이므로 지금 당장 모든 타임리프의 속성에 대해 알아 둘 필요는 없다.

이상과 같이 질문 목록을 만들었다.

 

이거 다시 한번 보고, 오류 수정되면 게시글도 수정 같이 들어갈 듯 하다.

'백엔드 > Springboot' 카테고리의 다른 글

서비스  (0) 2023.11.09
Root URL  (0) 2023.11.09
질문 목록과 템플릿(1)  (0) 2023.11.09
<번외> 프로젝트 github 등록 삽질기  (2) 2023.11.09
리포지터리(Repository) -3-  (0) 2023.09.18

위 주소로 접속하면 404 오류가 뜬다.

404 오류를 해결하려면 /question/list URL에 대한 매핑이 있는 컨트롤러가 필요하다. QuestionController.java 파일을 다음과 같이 작성한다.

 

 

그리고 다시 실행해보면, question list라고 출력될 것이다.

하지만 보통 브라우저에 응답하는 문자열은 위의 예처럼 자바 코드에서 직접 만들지는 않는다.

위에서는 "question list" 라는 문자열을 직접 자바 코드로 작성하여 브라우저에 리턴했다.

일반적으로 많이 사용하는 방식은 템플릿 방식이다. 템플릿은 자바 코드를 삽입할 수 있는 HTML 형식의 파일이다.

템플릿을 어떻게 사용할수 있는지 알아보자. 많은 엔진 중 타임리프(Thymleaf) 템플릿 엔진을 사용할 것이다.

타임리프를 사용하려면 설치가 필요하다. 다음과 같이 build.gradle 파일을 수정하자.

(... 생략 ...)

dependencies {
    (... 생략 ...)
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
}

(... 생략 ...)

위와 같이 수정하고 "Refresh Gradle Project"로 필요한 라이브러리를 설치하자.

타임리프 템플릿 엔진을 적용하기 위해서는 로컬서버 재시작이 필요하다. 로컬서버를 반드시 재시작하고 이후 과정을 진행하자.
그리고,

question_list.html 파일의 내용은 다음과 같이 작성하자.

[파일명: /sbb/src/main/resources/templates/question_list.html]

Copy<h2>Hello Template</h2>

다음에는, QuestionController.java 파일을 다음과 같이 수정하자.

 

 

여기서 주목! @ResponseBody라는 어노테이션이 빨간색으로 되어 있는 것을 볼 수 있다. 아까 코드는 빨간색이 아니었는데.. 그 이유는, 템플릿을 사용하기 때문에 기존에 사용했던 @ResponseBody 애너테이션은 필요없어졌기 때문이다. 그리고 list 메서드에서 question_list.html 템플릿 파일의 이름인 "question_list"를 리턴한다(question list가 아니라는 점 주의!)

 

**ResponseBody 어노테이션이란?

클라이언트에서 서버로 통신하는 메세지를 요청(request) 메세지라고 하며, 서버에서 클라이언트로 통신하는 메세지를 응답(reponse) 메세지라고 한다.

비동기통신을 하기 위해서는 클라이언트에서 서버로 요청 메세지를 보낼 때나 서버에서 클라이언트로 응답을 보낼 때에도 본문에 데이터를 담아서 보내야 하는데

이 본문을 body 라고 한다. 즉, 요청 본문 requestBody, 응답본문 responseBody 를 담아서 보내야 한다.

이 어노테이션이 붙은 파라미터에는 http 요청의 본문 (body) 이 그대로 전달된다.

HTTP 요청의 바디내용을 통째로 자바객체로 변환해서 매핑된 메소드 파라미터로 전달해 준다.

 

@ResponseBody

자바객체를 HTTP 요청의 바디내용으로 매핑하여 클라이언트로 전송한다. 

즉, @ResponseBody 어노테이션을 사용하면 http 요청 body 를 자바 객체로 전달 받을 수 있다. **

 

자 이제 새로고침을 해보면..

???????????????

이상하다. 분명 교재는 이렇지 않았는데... 침착하고 오류의 원인을 빠르게 찾아보자.

 

 

우선 @RequiredArgsConstructor 애너테이션으로 questionRepository 속성을 포함하는 생성자를 생성하였다. @RequiredArgsConstructor는 롬복이 제공하는 애너테이션으로 final이 붙은 속성을 포함하는 생성자를 자동으로 생성하는 역할을 한다. 롬복의 @Getter, @Setter가 자동으로 Getter, Setter 메서드를 생성하는 것과 마찬가지로 @RequiredArgsConstructor는 자동으로 생성자를 생성한다. 따라서 스프링 의존성 주입 규칙에 의해 questionRepository 객체가 자동으로 주입된다.

 

스프링의 의존성 주입(Dependency Injection) 방식 3가지

  • @Autowired 속성 - 속성에 @Autowired 애너테이션을 적용하여 객체를 주입하는 방식
  • 생성자 - 생성자를 작성하여 객체를 주입하는 방식 (권장하는 방식)
  • Setter - Setter 메서드를 작성하여 객체를 주입하는 방식 (메서드에 @Autowired 애너테이션 적용이 필요하다.)

테스트코드(SbbApplicationTests.java)에서는 속성에 @Autowired 애너테이션을 사용하여 객체를 주입했다.

그리고 Question 리포지터의 findAll 메서드를 사용하여 질문 목록 데이터인 questionList를 생성하고 Model 객체에 "questionList" 라는 이름으로 값을 저장했다. Model 객체는 자바 클래스와 템플릿 간의 연결고리 역할을 한다. Model 객체에 값을 담아두면 템플릿에서 그 값을 사용할 수 있다.

Model 객체는 따로 생성할 필요없이 컨트롤러 메서드의 매개변수로 지정하기만 하면 스프링부트가 자동으로 Model 객체를 생성한다.

나중에 다시 한번 더 봐야할 것 같다. 오류가 개선이 안된다..

'백엔드 > Springboot' 카테고리의 다른 글

Root URL  (0) 2023.11.09
질문 목록과 템플릿(2)  (1) 2023.11.09
<번외> 프로젝트 github 등록 삽질기  (2) 2023.11.09
리포지터리(Repository) -3-  (0) 2023.09.18
리포지터리(Repository) -2-  (0) 2023.09.18

지금까지 무작정 따라 코드 치고 작동 확인하고 테스트 돌려보고 하는거에 열중한 나머지

개발 기록을 github에 등록해놓는 것을 잊어버려서... 허둥지둥 github에 등록하기까지의 삽질기를 정리해보려 한다.

생각보다 이런 일이 종종 있기에 이번에야말로 기필코 확실히 정리해서 오류를 정복할 테다.

 

각설하고, 급히 push를 해서 등록을 하려고 보니..

 

사실 두가지  오류 중 첫번째 오류는 아주 간단하다. 원격 저장소의 브랜치 이름이 'main'인데, 로컬에서는 'master' 브랜치를 쓰고 있기 때문에 발생한 오류이다. 애초에 왜 master가 됐는지 모르겠다. 난 줄곧 main만 써왔는데..? 아무튼 이런 경우에는 그냥

git branch -m master main

해서 master를 main으로 변형하고 다시 push를 해주면 끝난다.

 

근데 문제는 밑에 빨간 줄 쳐진 두번째 오류이다. 

'failed to push some refs'라는 에러 메시지는 보통 로컬 저장소의 변경 사항을 원격 저장소에 푸시하려고 할 때, 원격 저장소의 변경 사항이 로컬에 아직 반영되지 않았을 때 발생한다.

즉, 원격 저장소에는 로컬 저장소에 없는 새로운 커밋이 있을 경우, 이런 에러가 발생할 수 있다. 이를 해결하기 위해서는 먼저 원격 저장소의 변경 사항을 로컬에 가져와야 한다.

다음과 같은 명령어를 사용해서 원격 저장소의 변경 사항을 로컬에 반영하고, 다시 push한다.

git pull origin main  // 원격 저장소의 변경사항을 로컬에 반영
git push origin main  // 로컬의 변경사항을 원격 저장소에 푸시

 

근데 이렇게 했더니, 

PS C:\Users\LG\web-board (2)> git pull origin main remote: Enumerating objects: 41, done. remote: Counting objects: 100% (41/41), done. remote: Compressing objects: 100% (26/26), done. remote: Total 41 (delta 1), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (41/41), 67.06 KiB | 837.00 KiB/s, done. From https://github.com/prislewarz/Springboot

  • branch main -> FETCH_HEAD
  • [new branch] main -> origin/main fatal: refusing to merge unrelated histories PS C:\Users\LG\web-board (2)> git push origin main error: src refspec main does not match any error: failed to push some refs to 'https://github.com/prislewarz/Springboot.git

위와 같이 새로운 오류가 생겼다.

 

git이 두 개의 분리된 프로젝트 이력을 병합하려고 할 때 발생하고, 이 오류는 보통 로컬과 원격 저장소가 서로 다른 커밋 이력을 가지고 있을 때 발생한다. git pull 명령어에 --allow-unrelated-histories 옵션을 추가하면, 서로 관련이 없는 커밋 이력을 병합하는 것을 허용한다.

git pull origin main --allow-unrelated-histories

 

비로소 문제없이 등록이 완료되었다. 이제 나머지 천천히 정리하고, 앞으로는 바로바로 커밋하고 푸시하는걸로..ㅠ

'백엔드 > Springboot' 카테고리의 다른 글

질문 목록과 템플릿(2)  (1) 2023.11.09
질문 목록과 템플릿(1)  (0) 2023.11.09
리포지터리(Repository) -3-  (0) 2023.09.18
리포지터리(Repository) -2-  (0) 2023.09.18
리포지터리(Repository) -1-  (0) 2023.09.18

이어서, 질문 데이터를 수정하는 테스트 코드를 작성해보자.

assertTrue(값)은 값이 true인지를 테스트한다.

질문 데이터를 조회한 다음 subject를 "수정된 제목" 이라는 값으로 수정했다. 변경된 Question 데이터를 저장하기 위해서는 this.questionRepository.save(q) 처럼 리포지터리의 save 메서드를 사용한다.

테스트를 수행해 보면 콘솔 로그에서 다음과 같은 update 문이 실행되었음을 확인할 수 있을 것이다.

데이터 삭제하기

이어서 데이터를 삭제하는 것도 실습해 보자. 여기서는 첫 번째 질문을 삭제해 보자.

리포지터리의 count() 메서드는 해당 리포지터리의 총 데이터건수를 리턴한다.

Question 리포지터리의 delete 메서드를 사용하여 데이터를 삭제했다. 삭제하기 전에는 데이터 건수가 2, 삭제한 후에는 데이터 건수가 1인지를 테스트했다. 테스트는 잘 통과될 것이다.

 

답변 데이터 생성 후 저장하기

이번에는 답변(Answer) 데이터를 생성하고 저장해 보자.

빨간줄은 무시하자. 오류인 것 같지만 막상 실행해보면 잘 되기만 한다..

 

답변 데이터 처리를 위해서는 답변 리포지터리가 필요하므로 AnswerRepository 객체를 @Autowired로 주입했다. 답변 데이터를 생성하려면 질문 데이터가 필요하므로 우선 질문 데이터를 구해야 한다. id가 2인 질문 데이터를 가져온 다음 Answer 엔티티의 question 속성에 방금 가져온 질문 데이터를 대입해(a.setQuestion(q)) 답변 데이터를 생성했다. Answer 엔티티에는 어떤 질문에 해당하는 답변인지 연결할 목적으로 question 속성이 필요하다.

테스트를 수행해 보자. 답변 데이터가 잘 생성될 것이다.

 

답변 조회하기

Answer도 Question 엔티티와 마찬가지로 id 속성이 기본 키이므로 값이 자동으로 생성된다. 다음처럼 id 값을 이용해 데이터를 조회해 보자.

마찬가지로 빨간색 무시하자. 잘만 된다.

id 값이 1인 답변을 조회했다. 그리고 그 답변의 질문 id가 2인지도 테스트해 보았다.

 

답변에 연결된 질문 찾기 vs 질문에 달린 답변 찾기

앞에서 구성한 Answer 엔티티의 question 속성을 이용하면 "답변에 연결된 질문"을 조회할 수 있다.

a.getQuestion()

답변에 연결된 질문 찾기는 Answer 엔티티에 question 속성이 정의되어 있어서 매우 쉽다. 그런데 반대의 경우도 가능할까? 즉, 질문에서 답변을 찾을수 있을까?

다음과 같이 질문 엔티티에 정의한 answerList를 사용하면 역시 쉽게 구할수 있다. 

질문 객체로부터 답변 리스트를 구하는 테스트코드이다.

id가 2인 질문에 답변을 한 개 등록했으므로 위와 같이 검증할 수 있다. 하지만...

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.mysite.sbb.Question.answerList, could not initialize proxy - no Session
(... 생략 ...)

 

오류가 뜬다. 난 처음에 빨간 줄 쳐진 answerList.get 때문에 오류가 난 줄 알았는데 아니었다.

원인은 Question 리포지터리가 findById를 호출하여 Question 객체를 조회하고 나면 DB세션이 끊어지기 때문이다. 그 이후에 실행되는 q.getAnswerList() 메서드는 세션이 종료되어 오류가 발생한다. 답변 데이터 리스트는 q 객체를 조회할때 가져오지 않고 q.getAnswerList() 메서드를 호출하는 시점에 가져오기 때문이다.

이렇게 필요한 시점에 데이터를 가져오는 방식을 Lazy 방식이라고 한다. 이와 반대로 q 객체를 조회할때 답변 리스트를 모두 가져오는 방식은 Eager 방식이라고 한다. @OneToMany, @ManyToOne 애너테이션의 옵션으로 fetch=FetchType.LAZY 또는 fetch=FetchType.EAGER 처럼 가져오는 방식을 설정할 수 있는데 이 책에서는 따로 지정하지 않고 항상 디폴트 값을 사용할 것이다.

사실 이 문제는 테스트 코드에서만 발생한다. 실제 서버에서 JPA 프로그램들을 실행할 때는 DB 세션이 종료되지 않기 때문에 위와 같은 오류가 발생하지 않는다. 테스트 코드를 수행할 때 위와 같은 오류를 방지할 수 있는 가장 간단한 방법은 다음처럼 @Transactional 애너테이션을 사용하는 것이다. @Transactional 애너테이션을 사용하면 메서드가 종료될 때까지 DB 세션이 유지된다.

 

따라서, 테스트 한정 코드를 다음과 같이 수정한다.

 

그리고 실행을 해 보면..

 

문제없이 잘 실행된다!

'백엔드 > Springboot' 카테고리의 다른 글

질문 목록과 템플릿(1)  (0) 2023.11.09
<번외> 프로젝트 github 등록 삽질기  (2) 2023.11.09
리포지터리(Repository) -2-  (0) 2023.09.18
리포지터리(Repository) -1-  (0) 2023.09.18
엔티티(Entity) -2-  (0) 2023.09.17

이번 포스팅에서는 데이터를 조회하는 방법에 대해 알아볼 것이다. 대략적으로 5가지 방법이 있다.

findAll

작성한 테스트 코드를 다음처럼 수정해 보자.

question 테이블에 저장된 모든 데이터를 조회하기 위해서 리포지터리의 findAll 메서드를 사용했다.

findAll은 데이터를 조회할때 사용하는 메서드이다.

총 2건의 데이터를 저장했기 때문에 데이터의 사이즈는 2가 되어야 한다. 데이터 사이즈가 2인지 확인하기 위해 JUnit의 assertEquals 메서드를 사용했다. assertEquals는 assertEquals(기대값, 실제값)와 같이 사용하고 기대값과 실제값이 동일한지를 조사한다. 만약 기대값과 실제값이 동일하지 않다면 테스트는 실패로 처리된다. 그리고 저장한 첫번째 데이터의 제목이 "sbb가 무엇인가요?"와 일치하는지도 테스트했다.

 

테스트를 위해서는 로컬 서버를 중지하고 다시한번 테스트 서버를 실행하면 된다. 테스트는 잘 통과될 것이다.

...인 줄 알았는데??

왠걸, 실패가 떠버렸다. 왜 그런가 하고 콘솔을 봤더니, 나도 모르는 사이에 데이터가 2개가 더 추가되어 총 데이터가 4개가 되어버렸던 것이다. 데이터의 사이즈를 2로 맞춰 놨는데, 실제 데이터가 4개이니 당연히 테스트에 통과되지 못하고 오류가 나버린 것이다. 데이터가 왜 추가되었는지는 의문이지만(나중에 조사해보는걸로...) 일단 2개를 지우고 2개로 맞춰서 다시 테스트를 실행해보았다.

 

다행히도 무사히 통과하였다! 역시 데이터의 사이즈 문제가 맞았다.

findById

이번에는 Question 엔티티의 Id값으로 데이터를 조회해 보자. 테스트 코드를 다음과 같이 수정하자.

id 값으로 데이터를 조회하기 위해서는 리포지터리의 findById 메서드를 사용해야 한다. 하지만 findById의 리턴 타입은 Question이 아닌 Optional임에 주의하자. Optional은 null 처리를 유연하게 처리하기 위해 사용하는 클래스로 위와 같이 isPresent로 null이 아닌지를 확인한 후에 get으로 실제 Question 객체 값을 얻어야 한다.

findBySubject

이번에는 Question 엔티티의 subject 값으로 데이터를 조회해 보자.

하지만 아쉽게도 Question 리포지터리는 findBySubject와 같은 메서드를 기본적으로 제공하지는 않는다. findBySubject 메서드를 사용하려면 다음처럼 QuestionRepository 인터페이스를 변경해야 한다.

그리고 테스트 코드를 다음과 같이 수정해준다.

이제 제목으로 테이블 데이터를 조회할 수 있게 되었다!

인터페이스에 findBySubject 라는 메서드를 선언만 하고 구현은 하지 않았는데 실행이 되는 이유는, DI에 의해 스프링이 자동으로 JpaRepository를 상속한 QuestionRepository 객체를 생성한다. 이 때 프록시 패턴이 사용된다고 한다.

리포지터리 객체의 메서드가 실행될때 JPA가 해당 메서드명을 분석하여 쿼리를 만들고 실행한다.

즉, findBy + 엔티티의 속성명(예:findBySubject)과 같은 리포지터리 메서드를 작성하면 해당 속성의 값으로 데이터를 조회할수 있다.

 

findBySubject 메서드를 호출할때 실제 어떤 쿼리가 실행되는지 살펴보자. 실행되는 쿼리를 로그에서 보려면 application.properties 파일을 다음과 같이 수정해야 한다.

형광펜 그어진 맨 밑 두 줄 코드를 추가해준다. 그러면 테스트 코드를 실행했을 때,

이와 같이 방금 전까지는 보이지 않던 콘솔로그에서 실행된 쿼리가 나타나는데, 이 쿼리문의 where 조건에서 subject가 포함된 것을 확인할 수 있다.

 

findBySubjectAndContent

이번에는 제목과 내용을 함께 조회해 보자. 두 개의 속성을 And 조건으로 조회할때는 리포지터리에 다음과 같은 메서드를 추가해야 한다.

테스트 코드는 아래와 같이 수정한다.

이제 테스트 코드를 실행해보면

subject, content 컬럼이 and 조건으로 where문에 사용되었다.

이렇듯 리포지터리의 메서드명은 데이터를 조회하는 쿼리문의 where 조건을 결정하는 역할을 한다. 여기서는 findBySubject, findBySubjectAndContent 두 개만 알아봤지만 상당히 많은 조합을 사용할 수 있다.

 

findBySubjectLike

이번에는 제목에 특정 문자열이 포함되어 있는 데이터를 조회해 보자. Question 리포지터리를 다음과 같이 수정하자.

테스트 코드는 다음과 같이 수정하자.

테스트는 잘 통과될 것이다. Like 검색을 위해서는 findBySubjectLike 메서드의 입력 문자열로 "sbb%"와 같이 "%"를 적어주어야 한다. % 표기는 다음과 같은 의미를 갖는다.

  • sbb%: "sbb"로 시작하는 문자열
  • %sbb: "sbb"로 끝나는 문자열
  • %sbb%: "sbb"를 포함하는 문자열

마지막으로, 질문 데이터 수정, 삭제와 답변 데이터 생성, 저장, 조회 등에 대해서 다음 포스팅에 적도록 하겠다.

'백엔드 > Springboot' 카테고리의 다른 글

<번외> 프로젝트 github 등록 삽질기  (2) 2023.11.09
리포지터리(Repository) -3-  (0) 2023.09.18
리포지터리(Repository) -1-  (0) 2023.09.18
엔티티(Entity) -2-  (0) 2023.09.17
엔티티(Entity) -1-  (0) 2023.09.17

이제 본격적으로 JPA를 이용해서 데이터를 처리해보려 한다.

리포지터리(Repository)

엔티티만으로는 데이터베이스에 데이터를 저장하거나 조회 할 수 없다. 데이터 처리를 위해서는 실제 데이터베이스와 연동하는 JPA 리포지터리가 필요하다.

점프 투 스프링부트 리포지터리란?

리포지터리는 엔티티에 의해 생성된 데이터베이스 테이블에 접근하는 메서드들(예: findAll, save 등)을 사용하기 위한 인터페이스이다. 데이터 처리를 위해서는 테이블에 어떤 값을 넣거나 값을 조회하는 등의 CRUD(Create, Read, Update, Delete)가 필요하다. 이 때 이러한 CRUD를 어떻게 처리할지 정의하는 계층이 바로 리포지터리이다.

 

다음과 같이 QuestionRepository 인터페이스를 생성하자.

QuestionRepository는 리포지터리로 만들기 위해 JpaRepository 인터페이스를 상속했다. JpaRepository를 상속할 때는 제네릭스 타입으로 <Question, Integer> 처럼 리포지터리의 대상이 되는 엔티티의 타입(Question)과 해당 엔티티의 PK의 속성 타입(Integer)을 지정해야 한다. 이것은 JpaRepository를 생성하기 위한 규칙이다.

Question 엔티티의 PK(Primary Key) 속성인 id의 타입은 Integer 이다.

마찬가지로 AnswerRepository도 다음과 같이 생성하자.

오류가 나길래, 몇번이나 코드를 확인해봤는데도 이상이 없었다. 혹시 몰라서 실행해보니 실행 자체는 문제없이 원활하게 진행되었다. 뭔가 거슬리긴 하지만 우선은 그냥 넘어가자.

 

질문과 답변 리포지토리를 모두 작성하였다. 이제 QuestionRepository, AnswerRepository를 이용하여 question, answer 테이블에 데이터를 저장하거나 조회할 수 있다.

 

데이터 저장하기

작성한 리포지터리를 테스트하기 위해서 JUnit 기반의 스프링부트의 테스트 프레임워크를 사용해 보자.

테스트는 실제 로컬 서버와는 다르게 처리된다. 시작하는 시점부터가 다르며, 테스트로 실행하면 로컬 서버의 H2 콘솔이 접속되지 않는 등 차이가 있다. 만약 서버를 실행햇는데 로컬 서버에서 접속이 되지 않으면, 테스트 모드로 실행하지 않았는지 체크해보자.

 

테스트 프레임워크를 사용하려면, 이제까지 실행해왔던 WebBoardApplication.java 파일이 아닌, WebBoardApplicationTests.java 파일로 들어가서 다음과 같이 코드를 작성해준다.

 

@SpringBootTest 애너테이션은 SbbApplicationTests 클래스가 스프링부트 테스트 클래스임을 의미한다. 

@Autowired 애너테이션은 스프링의 DI 기능으로 questionRepository 객체를 스프링이 자동으로 생성해 준다.

DI(Dependency Injection) - 스프링이 객체를 대신 생성하여 주입한다.

점프 투 스프링부트@Autowired

객체를 주입하기 위해 사용하는 스프링의 애너테이션이다. 객체를 주입하는 방식에는 @Autowired 외에 Setter 또는 생성자를 사용하는 방식이 있다. 순환참조 문제와 같은 이유로 @Autowired 보다는 생성자를 통한 객체 주입방식이 권장된다. 하지만 테스트 코드의 경우에는 생성자를 통한 객체의 주입이 불가능하므로 테스트 코드 작성시에만 @Autowired를 사용하고 실제 코드 작성시에는 생성자를 통한 객체 주입방식을 사용하겠다.

testJpa 메서드 위의 @Test 애너테이션은 testJpa 메서드가 테스트 메서드임을 나타낸다. 위 클래스를 JUnit으로 실행하면 @Test 애너테이션이 붙은 메서드가 실행된다.

JUnit은 테스트코드를 작성하고 작성한 테스트코드를 실행하기 위해 사용하는 자바의 테스트 프레임워크이다.

testJpa 메서드의 내용을 잠시 살펴보자. testJpa 메서드는 q1, q2 라는 Question 엔티티 객체를 생성하고 QuestionRepository를 이용하여 그 값을 데이터베이스에 저장하는 코드이다.

 

이제 작성한 SbbApplicationTests 클래스를 실행해 보자. 

(테스트를 실행하기 전에, 로컬 서버가 구동되어 있다면 The file is locked: nio:/Users/pahkey/local.mv.db 와 비슷한 오류가 발생할 것이다. H2 데이터베이스는 파일 기반의 데이터베이스라서, 이미 로컬서버가 점유하고 있기 때문에 이러한 오류가 발생한다. 따라서 테스트를 하기 위해서는 로컬 서버를 중지해야 한다. )

 

올바르게 코드가 작성되었다면, 다음과 같은 화면이 출력될 것이다.

테스트에 성공했다면, 위와 같이 초록색 체크표시가 나타난다. 참고로 로컬 서버에서는 이러한 초록색 체크표시는 나타나지 않는다.

실제 데이터베이스에 값이 잘 들어갔는지 확인하려면 테스트 서버에서는 불가능하고,  다시 로컬서버를 구동하고 H2 콘솔에 접속하여 쿼리문을 실행해야 한다. 이때는 아까와 반대로 테스트 서버를 종료하고 로컬 서버를 실행시켜준다.

테스트에 성공했다는 뜻은 실제 로컬 서버에서 아무 오류 없이 잘 구동된다는 의미이므로, 안심하고 종료해주자.

 

H2 콘솔에 접속했다면, 아래와 같이 쿼리문을 입력해준다.

실행을 하면, 다음처럼 우리가 저장한 Question 객체의 값이 데이터베이스에 저장된 것을 확인할 수 있다.

id는 Question 엔티티의 기본 키(Primary Key)이다. id는 앞에서 엔티티를 생성할 때 설정했던대로 데이터를 생성할 때 속성값이 자동으로 1씩 증가하는 것을 확인할 수 있다.

Question 엔티티의 id는 @GeneratedValue 설정을 했다.

다음 포스팅에서는 데이터를 조회하는 방법에 대해 작성하겠다.

'백엔드 > Springboot' 카테고리의 다른 글

리포지터리(Repository) -3-  (0) 2023.09.18
리포지터리(Repository) -2-  (0) 2023.09.18
엔티티(Entity) -2-  (0) 2023.09.17
엔티티(Entity) -1-  (0) 2023.09.17
JPA 환경설정  (0) 2023.09.17

이전까지 질문 엔티티와 답변 엔티티를 작성했고, 그 과정에서 질문 엔티티와 답변 엔티티가 유기적 관계에 있음을 명시하는 작업이 필요하다는 것을 확인했다. 질문 엔티티를 다음과 같이 수정하자.

다음과 같이 question 속성에 @ManyToOne 애너테이션을 추가해준다. 

답변은 하나의 질문에 여러개가 달릴 수 있는 구조이다. 따라서 답변은 Many(많은 것)가 되고 질문은 One(하나)이 된다. 따라서 @ManyToOne은 N:1 관계라고 할 수 있다. 이렇게 @ManyToOne 애너테이션을 설정하면 Answer 엔티티의 question 속성과 Question 엔티티가 서로 연결된다. (실제 데이터베이스에서는 ForeignKey 관계가 생성된다.)

@ManyToOne은 부모 자식 관계를 갖는 구조에서 사용한다. 여기서 부모는 Question, 자식은 Answer라고 할 수 있다.

그렇다면 반대방향, 즉 Question 엔티티에서 Answer 엔티티를 참조할수는 없을까?

가능하다. 답변과 질문이 N:1의 관계라면 질문과 답변은 1:N의 관계라고 할 수 있다. 이런경우에는 @ManyToOne이 아닌 @OneToMany애너테이션을 사용한다. Question 하나에 Answer는 여러개이므로 Question 엔티티에 추가할 답변의 속성은 List 형태로 구성해야 한다.

 

이를 구현하기 위해 Question 엔티티를 다음과 같이 수정하자.

답변 엔티티에서 @ManyToOne 어노테이션을 추가했듯, 질문 엔티티는 그와 반대로 @OneToMany 어노테이션만 추가하면 될 줄알았는데, 답변 엔티티와 달리 질문 엔티티의 수정 사항이 조금 더 복잡해졌다. 왜 그럴까?

 

우선, Answer 엔티티 객체로 구성된 answerList를 속성으로 추가하고 @OneToMany 어노테이션을 설정했다. 질문 객체(예:question)에서 답변을 참조하려면 question.getAnswerList()를 호출하기 위해서이다.

@OneToMany 어노테이션에 사용된 mappedBy는 참조 엔티티의 속성명을 의미한다. 즉, Answer 엔티티에서 Question 엔티티를 참조한 속성명 question을 mappedBy에 전달해야 한다.

CascadeType.REMOVE

질문 하나에는 여러개의 답변이 작성될 수 있다. 이때 질문을 삭제하면 그에 달린 답변들도 모두 함께 삭제하기 위해서 @OneToMany의 속성으로 cascade = CascadeType.REMOVE를 사용했다. 따라서 수정이 조금 복잡하게 이루어졌다.

 

이제 H2 콘솔에 다시 접속해보자.

ANSWER과 QUESTION 테이블이 생성되었다!

마지막으로 엔티티에 대해서 간략하게 정리하고 가자.

질문:답변 관계는 1:N이어야 하고, 답변:질문 관계는 N:1이어야 바람직하다.

 

 

++참고++

Question 엔티티와 Answer 엔티티 사이에 'ManyToOne' 관계를 설정할 때, 그러나 @ManyToOne 어노테이션이 id 필드 위에 위치해 있다면 제대로 된 관계 설정이 이루어지지 않는다!!

@ManyToOne 어노테이션은 Question 엔티티를 참조하는 필드인 question 필드 위에 위치해야 한다. 위 코드와 같이.

'백엔드 > Springboot' 카테고리의 다른 글

리포지터리(Repository) -2-  (0) 2023.09.18
리포지터리(Repository) -1-  (0) 2023.09.18
엔티티(Entity) -1-  (0) 2023.09.17
JPA 환경설정  (0) 2023.09.17
JPA  (1) 2023.09.17

이제 SBB가 사용할 엔티티(Entity)을 만들어 보자. 엔티티는 데이터베이스 테이블과 매핑되는 자바 클래스를 말한다. SBB는 질문과 답변을 할 수 있는 게시판 서비스이다. 따라서 SBB에는 질문과 답변에 해당하는 엔티티가 있어야 한다.

엔티티는 모델 또는 도메인 모델이라고 부르기도 한다. 이 책에서는 이것들을 구분하지 않고 테이블과 매핑되는 클래스를 엔티티라 지칭하겠다.

질문 엔티티를 다음과 같은 코드로 작성하자.

엔티티로 만들기 위해서는, 엔티티로 만드려는 클래스에 @Entity 어노테이션을 적용시킨다. 위 코드에서는 Question이라는 클래스가 엔티티가 되었다.@Entity 애너테이션을 적용해야 JPA가 엔티티로 인식한다. 그리고 Getter, Setter 메서드를 자동으로 생성하려면 롬복의 @Getter, @Setter 애너테이션을 적용했다.

롬복(Lombok)은 여러가지 @어노테이션을 제공하고 이를 기반으로 반복 소스코드를 컴파일 과정에서 생성해주는 방식으로 동작하는 라이브러리이다.

 

그리고 엔티티의 속성으로 고유번호(id), 제목(subject), 내용(content), 작성일시(createDate)를 추가했다. 각 속성에는 Id, GeneratedValue, Column과 같은 애너테이션이 적용되어 있는데 그것들에 대해서 하나씩 알아보자.

 

@Id

고유 번호 id 속성에 적용한 @Id 애너테이션은 id 속성을 기본 키로 지정한다. 기본 키로 지정하면 이제 id 속성의 값은 데이터베이스에 저장할 때 동일한 값으로 저장할 수 없다. 고유 번호를 기본 키로 한 이유는 고유 번호는 엔티티에서 각 데이터를 구분하는 유효한 값으로 중복되면 안 되기 때문이다.

데이터베이스에서는 id와 같은 특징을 가진 속성을 기본 키(primary key)라고 한다.

@GeneratedValue

@GeneratedValue 애너테이션을 적용하면 데이터를 저장할 때 해당 속성에 값을 따로 세팅하지 않아도 1씩 자동으로 증가하여 저장된다. strategy는 고유번호를 생성하는 옵션으로 GenerationType.IDENTITY는 해당 컬럼만의 독립적인 시퀀스를 생성하여 번호를 증가시킬 때 사용한다.

strategy 옵션을 생략할 경우에 @GeneratedValue 애너테이션이 지정된 컬럼들이 모두 동일한 시퀀스로 번호를 생성하기 때문에 일정한 순서의 고유번호를 가질수 없게 된다. 이러한 이유로 보통 GenerationType.IDENTITY를 많이 사용한다.

@Column

엔티티의 속성은 테이블의 컬럼명과 일치하는데 컬럼의 세부 설정을 위해 @Column 애너테이션을 사용한다. length는 컬럼의 길이를 설정할때 사용하고 columnDefinition은 컬럼의 속성을 정의할 때 사용한다. columnDefinition = "TEXT"은 "내용"처럼 글자 수를 제한할 수 없는 경우에 사용한다.

엔티티의 속성은 @Column 애너테이션을 사용하지 않더라도 테이블 컬럼으로 인식한다. 테이블 컬럼으로 인식하고 싶지 않은 경우에만 @Transient 애너테이션을 사용한다.

점프 투 스프링부트테이블의 컬럼명

위의 Question 엔티티에서 작성일시에 해당하는 createDate 속성의 실제 테이블의 컬럼명은 create_date가 된다. 즉 createDate처럼 대소문자 형태의 카멜케이스(Camel Case) 이름은 create_date 처럼 모두 소문자로 변경되고 언더바(_)로 단어가 구분되어 실제 테이블 컬럼명이 된다.

점프 투 스프링부트엔티티와 Setter

일반적으로 엔티티에는 Setter 메서드를 구현하지 않고 사용하기를 권한다. 왜냐하면 엔티티는 데이터베이스와 바로 연결되어 있으므로 데이터를 자유롭게 변경할 수 있는 Setter 메서드를 허용하는 것이 안전하지 않다고 판단하기 때문이다.

그렇다면 Setter 메서드 없이 어떻게 엔티티에 값을 저장할 수 있을까?

엔티티를 생성할 경우에는 롬복의 @Builder 어노테이션을 통한 빌드패턴을 사용하고, 데이터를 변경해야 할 경우에는 그에 해당되는 메서드를 엔티티에 추가하여 데이터를 변경하면 된다.

다만, 이 책은 복잡도를 낮추고 원활한 설명을 위해 엔티티에 Setter 메서드를 추가하여 진행하려 한다.

 

질문 엔티티를 생성했으니, 이제 질문을 했으니 당연히 답변이 와야 한다. 따라서 답변 엔티티를 다음과 같이 작성한다.

질문 엔티티 코드와는 다르게 private Question question; 속성이 새롭게 추가되었다. 왜 질문 속성이 추가되었냐면, 답변 엔티티에서 질문 엔티티를 참조하기 위해 추가했다. 예를 들어 답변 객체(예:answer)를 통해 질문 객체의 제목을 알고 싶다면 answer.getQuestion().getSubject()처럼 접근할 수 있다. 하지만 이렇게 속성만 추가하면 안되고 질문 엔티티와 연결된 속성이라는 것을 명시적으로 표시해야 한다.

 

다음 포스팅에서 질문 엔티티와 연결된 속성이라는 것을 명시적으로 표시하는 법을 설명하겠다.

'백엔드 > Springboot' 카테고리의 다른 글

리포지터리(Repository) -1-  (0) 2023.09.18
엔티티(Entity) -2-  (0) 2023.09.17
JPA 환경설정  (0) 2023.09.17
JPA  (1) 2023.09.17
ORM(Object Relational Mapping)  (0) 2023.09.17

이전 과정에서 H2 데이터베이스 사용 준비를 마치고, 이제 JPA를 사용할 차례인데, 그 전에 JPA를 사용할 수 있도록 준비 작업이 선행되어야 한다. 다음처럼 build.gradle 파일을 수정해준다.

수정이라고 해놨지만 수정이랄거까진 없고 그냥 형광펜 쳐진 부분의 코드를 추가해주면 된다.

implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 

이제 변경사항을 적용하면 JPA 라이브러리가 설치될 것이다. 그 다음은, JPA 설정을 위해 application.properties 파일에 다음과 같은 코드를 추가해준다.

추가한 항목을 간단하게 살펴보자.

  • spring.jpa.properties.hibernate.dialect - 데이터베이스 엔진 종류를 설정한다.
  • spring.jpa.hibernate.ddl-auto - 엔티티를 기준으로 테이블을 생성하는 규칙을 정의한다. 즉 위 코드에서는 update 규칙으로 테이블이 생성될 것이다.

위 설정에서 spring.jpa.hibernate.ddl-auto를 update로 설정했다. update와 같은 설정값에 대해서 간단히 알아보자.

  • none - 엔티티가 변경되더라도 데이터베이스를 변경하지 않는다.
  • update - 엔티티의 변경된 부분만 적용한다.
  • validate - 변경사항이 있는지 검사만 한다.
  • create - 스프링부트 서버가 시작될때 모두 drop하고 다시 생성한다.
  • create-drop - create와 동일하다. 하지만 종료시에도 모두 drop 한다.

개발 환경에서는 보통 update 모드를 사용하고 운영환경에서는 none 또는 validate 모드를 사용한다.

 

마지막으로, JPA가 무엇인지 도식화된 그림으로 정리하고 넘어가자.

'백엔드 > Springboot' 카테고리의 다른 글

엔티티(Entity) -2-  (0) 2023.09.17
엔티티(Entity) -1-  (0) 2023.09.17
JPA  (1) 2023.09.17
ORM(Object Relational Mapping)  (0) 2023.09.17
컨트롤러  (0) 2023.09.17

JPA 란?

스프링부트는 JPA(Java Persistence API)를 사용하여 데이터베이스를 처리한다. JPA는 자바 진영에서 ORM(Object-Relational Mapping)의 기술 표준으로 사용하는 인터페이스의 모음이다.

JPA는 인터페이스이다. 따라서 인터페이스를 구현하는 실제 클래스가 필요하다. JPA를 구현한 대표적인 실제 클래스에는 하이버네이트(Hibernate)가 있다. 보통 JPA + 하이버네이트 조합을 사용한다.

H2 데이터베이스

JPA를 사용하기 전에 데이터를 저장할 데이터베이스를 설치해 보자. 개발시에는 Oracle, MSSQL 등의 굵직한 데이터베이스 보다는 설치도 쉽고 사용도 편리한 H2 데이터베이스를 많이 사용한다.

H2 데이터베이스는 주로 개발용이나 소규모 프로젝트에서 사용되는 파일 기반의 경량 데이터베이스이다. 개발시에는 H2를 사용하여 빠르게 개발하고 실제 운영시스템은 좀 더 규모있는 DB를 사용하는 것이 일반적인 개발 패턴이다.

 

H2 데이터베이스를 사용하기 위해, dependencies에 runtimeOnly 'com.h2database:h2' 코드를 추가해준다.

runtimeOnly

build.gradle 파일의 runtimeOnly는 해당 라이브러리가 런타임(Runtime)시에만 필요한 경우에 사용한다. 컴파일(Compile)시에만 필요한 경우에는 runtimeOnly 대신 compileOnly를 사용한다.

 

H2 데이터베이스를 설치했다고 바로 사용 가능할 것이라고 생각했다면 오산이다. application.properties 파일을 다음과 같이 수정해주어야만 비로소 사용할 수 있다.

각각의 항목에 대해서 알아보자.

  • spring.h2.console.enabled - H2 콘솔의 접속을 허용할지의 여부이다. true로 설정한다. false로 설정하면 접속 안된다.
  • spring.h2.console.path - 콘솔 접속을 위한 URL 경로이다. 즉, http://localhost:8080/h2-console 주소로 접속한다.
  • spring.datasource.url - 데이터베이스 접속을 위한 경로이다.
  • spring.datasource.driverClassName - 데이터베이스 접속시 사용하는 드라이버이다.
  • spring.datasource.username - 데이터베이스의 사용자명이다. (사용자명은 기본 값인 sa로 설정한다.)
  • spring.datasource.password - 데이터베이스의 패스워드이다. 로컬 개발 용도로만 사용하기 때문에 패스워드는 없어도 무방하다.

그리고 spring.datasource.url에 설정한 경로에 해당하는 데이터베이스 파일을 만들어야 한다. 위에서 spring.datasource.url을 jdbc:h2:~/local 로 설정했기 때문에 사용자의 홈디렉터리(~ 에 해당하는 경로) 밑에 local.mv.db 라는 파일을 생성해야 한다. 만약 jdbc:h2:~/test라고 설정했다면 test.mv.db 라는 파일을 생성해야 한다.

사용자의 홈디렉터리는 윈도우의 경우에는 C:\Users\(사용자명) 이고 맥OS의 경우에는 /Users/(사용자명) 이다. 본인이 사용하는 OS에 맞는 홈디렉터리에 local.mv.db 파일을 생성하자. 파일은 내용 없이 빈파일로 생성한다. 필자는 윈도우를 쓰기 때문에  C:\Users\(사용자명)에 local.mv.db 파일을 생성해주었다.

참고로 파일이 생성되지 않았을 때는 아무리 연결을 시도해도 연결되지 않는다.

 

파일이 다 생성되었다는 전제 하에,

http://localhost:8080/h2-console 주소로 접속하면, 다음과 같은 화면이 뜰 것이다(한국어 설정은 따로 해야한다)

여기서, 위 화면과 같이 JDBC URL 경로를 application.properties 파일에 설정한 jdbc:h2:~/local로 변경하고 "연결" 버튼을 눌러보자.(처음에는 local이 아니라 test로 되어있을 것이다. 변경 후에 연결 시험 누르지 말고 바로 연결 누르자)

그러면 위와 같은 화면이 나타날 것이다(반드시 선행 과정을 잘 따라와야 한다. 하나라도 잘못되면 위 화면을 보기 힘들 것이다) 옆의 ANSWER과 QUESTION은 나중에 설명할 것이니 우선은 넘어가자.

 

자, 이제 H2 데이터베이스를 사용할 준비는 다 완료되었다. 이제 자바 프로그램에서 H2 데이터베이스를 사용할 수 있게 해야한다. 자바 프로그램에서 데이터베이스에 데이터를 저장하거나 조회하려면 JPA를 사용해야 한다. 이 과정은 다음 포스팅에서 설명하도록 하겠다.

'백엔드 > Springboot' 카테고리의 다른 글

엔티티(Entity) -1-  (0) 2023.09.17
JPA 환경설정  (0) 2023.09.17
ORM(Object Relational Mapping)  (0) 2023.09.17
컨트롤러  (0) 2023.09.17
스프링부트 프로젝트의 구조  (0) 2023.09.16

+ Recent posts