개발/JAVA

[자바웹개발워크북] 5. 스프링에서 스프링부트로

독코더 2023. 1. 26. 20:10
반응형

/*

 *

 * 자바웹개발워크북의 내용을 정리하기 위한 포스팅입니다.

 * 스프링부트, Thymleaf, Spring Data JPA

 */

 

1. 스프링부트와 환경설정

스프링부트는 스프링에서 파생된 여러 서브 프로젝트에서 시작해서 이제는 완전한 메인 프로젝트가 되어버린 특이한 케이스입니다.

스프링부트를 엄밀하게 말하면 '스프링 프레임워크 개발도구'라고 봐야할 것입니다.

스프링부트는 엔터프라이즈급 애플리케이션을 개발하기 위해서 필요한 기능들을 제공하는 개발도구입니다.

스프링부트의 중요한 특징으로 Auto Configuration(자동설정)을 내세울수 있습니다.

예를들어 스프링부트는 데이터베이스와 관련된 모듈을 추가하면 자동으로 데이터베이스 관련 설정을 찾아서 실행합니다.

또다른 특징으로는 '내장 톰캣'과 단독 실행 가능한 도구라는 것입니다. 스프링부트는 별도의 서버 설정없이도 개발이 가능하고,

실행도 가능합니다.(물론 다른 WAS에 스프링부트로 작성된 코드를 올려서 실행하는것도 가능합니다.)

이를 이용해서 스프링부트 프로젝트를 실행가능한 jar파일로 만들고 다른 운영체제에서 실행하는 등의 작업이 가능합니다.

 

기존 개발에서 달리지는 점들

서블릿에서 스프링으로 넘어오는 과정은 기존의 코드를 재활용할 수 없기 때문에 러닝커브가 상당히 큰 편이었습니다.

반면 스프링에서 스프링부트로 넘어오는 일은 기존의 코드나 개념이 그대로 유지되기 때문에 새로운 개념이 필요하지 않습니다.

설정과 관련해선 직접 필요한 라이브러리를 기존 build.gradle파일에 추가하는 설정이 상당히 단순하기도 하지만 자동으로 처리됩니다.

특히 톰캣이 내장된 상태로 프로젝트가 생성되기때문에 WAS의 추가설정이 필요하지 않다는점도 편리합니다.

빈설정은 XML을 대신에 자바설정을 이용하는것으로 약간의 변경이 있습니다만 이 역시 새로운 기법일뿐이고 기존과 다른개념은 아닙니다.

스프링MVC에서는 JSP를 이용할 수는 있지만 스프링부트는 Thymeleaf라는 템플릿엔진을 활용하는 경우가 많습니다.

최근 스프링부트는 화면을 구성하지않고 데이터만을 제공하는 API서버라는 형태를 이용하기도 합니다.

스프링부트에서도 MyBatis를 이용할수 있지만, JPA를 사용하는 방법으로 공부하겠습니다.

JPA를 이용하면 객체지향으로 구성된 객체들을 데이터베이스에 반영할 수 있는데 이를 자동으로 처리할 수 있으므로 별도의 SQL의 개발없이도 개발이 가능합니다.

 

스프링부트의 프로젝트 생성방식 2가지

Spring Initializr를 이용한 자동생성

Maven이나 Gradle을 이용한 직접생성

 

앞서 언급했듯이 스프링부트는 스프링을 쉽게 사용하기위한 도구이므로 스프링예제와 동일하게 프로젝트를 생성하고 필요한 라이브러리들을 추가하는 형태의 개발도 가능합니다.

하지만 스프링부트는 거의 모든 개발에 1번 방식을 이용하는데 Spring Initializr가 프로젝트의 기본 템플릿구조를 만들어주기때문입니다.

또한 이클립스나 인텔리제이, VS Code등에서도 Spring Initializr를 지원하기때문에 호환성면에서도 유리합니다.

 

Spring Initializr를 이용한 프로젝트 생성

인텔리제이에서는 Spring initializr를 이용해서 프로젝트를 생성합니다.

개발언어를 JAVA, 빌드타입은 Gradle로 지정하겠습니다. Group명과 패키지명은 'org.zerock'로 하겠습니다.

dependencies항목에서 다음 목록들을 체크합니다.

■ Spring Boot DevTools ■ Lombok ■ Spring Web ■ Thymeleaf ■ Spring Data JPA ■ MariaDB Driver

 

오류 발생

- Doesn't say anything about its target Java version (required compatibility with Java 11)
- Doesn't say anything about its elements (required them packaged as a jar)
- Doesn't say anything about org.gradle.plugin.api-version (required '7.6')

찾아보니, 스프링부트3.x 는 자바17부터, 스프링부트 2.x는 자바11을 이용하는데 자바11을 사용하고 스프링부트의 버전이 3.x였습니다.

build.gradle에서 스프링부트의 버전을 2.6.4로 바꿔줚더니 제대로 생성되었습니다.

 

프로젝트의 실행

스프링부트의 프로젝트는 이미 서버를 내장한 상태에서 만들어지기 때문에 스프링만을 이용할 때와 달리,

별도의 WAS설정이 필요하지 않고, 특이하게도 main()의 실행을 통해서 프로젝트를 실행합니다.

프로젝트를 초기화할때 이미 실행메뉴에 'B01 Application'이라는 이름으로 실행메뉴가 구성됩니다.

이를 이용해서 프로젝트를 실행할 수 있습니다.

 

오류발생 : Cause: error: invalid source release: 17

설치된 SDK버전과 달라 발생한 오류였습니다. build.gradle에 sourceCompatibility = '11' 로 바꿔줬더니 오류가 사라졌습니다.

 

첫 실행 결과는 앞의 그림과 같은 에러가 발생합니다. 이것은 스프링부트가 자동설정을 통해서 인식한 Spring Data JPA를 실행할 때

DB와 관련된 설정을 찾을 수 없어서 발생한 에러입니다. 에러가 발생하긴 했지만, 신기하게도 아무런 설정이 없는 상태인데 자동으로 데이터베이스 관련 설정을 이용한다는 사실 자체가 중요합니다.

이와 같이 라이브러리만으로 설정을 인식하려는 특성을 '자동설정(auto configuration)'이라고 합니다.

스프링부트 설정은 application.properties파일을 이용하거나 application.yml파일을 이용할 수 있습니다.

만일 파일설정을 피하고 싶다면 @Configuration이 있는 클래스파일을 만들어서 필요한 설정을 추가할 수 있습니다.

application.properties파일에 데이터베이스 설정을 다음과 같이 추가합니다.

앞의 설정을 추가한 후에 다시 프로젝트를 실행해보면 다음과 같이 8080포트로 톰캣이 실행되는것을 볼 수 있습니다.

스프링과 스프링부트를 비교해보면 별도의 HikariCP라이브러리를 가져오거나 HikariConfig객체를 구성하는 등의 모든과정이 생략된 것을 볼 수 있습니다.(스프링부트는 HikariCP를 기본적으로 이용합니다)

만일 8080포트가 다른 프로젝트에서 실행되고 있다면 포트번호를 application.properties에서 server.port를 지정해서 변경해줍니다.

 

편의성을 높이는 몇가지 설정

 

자동 리로딩 설정

웹개발시 코드를 수정하고 다시 deploy를 하는 과정을 자동으로 설정할 수 있습니다.

상단의 Run > Edit Configurations의 여러 옵션 중 'On update action, On frame deactivation'의 옵션값을,

[Update classes and resources]로 지정합니다.

이설정이 추가된 후에 수정하고 다른 작업을 하면 자동으로 빌드와 배포가 이루어집니다.(재시작필요)

 

Lombok을 테스트환경에서도 사용하기

스프링부트는 체크박스를 선택하는것만으로 Lombok라이브러리를 추가하지만 테스트환경에서는 설정이 빠져있습니다.

build.gradle파일 내 dependencies항목에 test관련 설정을 조정합니다.

dependencies {
    ... 생략 ...
    testCompileOnly 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
}

 

로그레벨의 설정

스프링부트는 기본적으로 Log4j2가 추가되어 있기 때문에 라이브러리를 추가하지 않아도 되고,

application.properties파일을 이용해서 간단하게 로그설정을 추가할 수 있습니다.

logging.level.org.springframework=info
logging.level.org.zerock=debug

 

인텔리제이의 DataSource 설정

인텔리제이 얼티메이트의 경우 JPA관련 플러그인이 이미 설치되어 있기 때문에 Datasource를 설정해두면 나중에 엔티티 클래스의 생성이나 기타클래스의 생성과 설정 시에 도움이 됩니다. 현재 사용하는 MariaDB를 설정합니다.

 

테스트환경과 의존성 주입 테스트

스프링에는 'spring-text-xxx'라이브러리를 추가해야하고 JUnit등도 직접 추가해야 하지만, 스프링부트는 프로젝트 생성 시

이미 테스트관련 설정이 완료되고 테스트코드도 하나 생성되어 있습니다. 테스트코드의 실행을 점검하기 위하여

DataSourceTests를 작성해서 HikariCP와 Lombok을 확인해 보도록 합니다.

@SpringBootTest
@Log4j2
public class DataSourceTests {
    
    @Autowired
    private DataSource dataSource;
    
    @Test
    public void testConnection() throws SQLException {
        
        @Cleanup
        Connection con =dataSource.getConnection();
        
        log.info(con);

        Assertions.assertNotNull(con);
    }
    
    
}

DataSource는 application.properties에 설정된 DataSource관련 설정을 통해서 생성된 빈이고,

이에 대한 별도의 설정없이 스프링에서 바로 사용이 가능합니다.

 

Spring Data JPA를 위한 설정

application.properties에 다음과 같은 내용을 추가하도록 합니다.

spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=true

spring.jpa.hibernate.ddl-auto속성은 프로젝트 실행 시 DDL문을 처리할 것인지를 명시합니다. 이외에도 지정할 수 있습니다.

속성값 의미
none DDL을 하지 않음
create-drop 실행할 때 DDL을 실행하고 종료시에 만들어진 테이블 등을 모두 삭제
create 실행할 때 마다 새롭게 테이블을 생성
update 기존과 다르게 변경된 부분이 있을때는 새로 생성
validate 변경된 부분만 알려주고 종료

spring.jpa.properties.hibernate.format_sql속성은 실제로 실행되는 SQL을 포맷팅해서 알아보기쉽게 출력해줍니다.

spring.jpa.show-sql은 JPA가 실행하는 SQL을 같이 출력하도록 합니다.

 

스프링부트에서 웹개발

스프링부트를 이용해서 웹을 개발하는 일은 컨트롤러나 화면을 개발하는것은 유사하지만 web.xml이나 servlet-context.xml과 같은 웹 관련 설정 파일들이 없기 때문에 이를 대신하는 클래스를 작성해 준다는 점이 다릅니다.

 

Thymeleaf는 JSP와 동일하게 서버에서 결과물을 생성해서 보내는 방식이지만 좀 더 HTML에 가깝게 작성할 수 있고 다양한 기능들을 가지고 있습니다. 또한 템플릿이기 때문에 JSP처럼 직접 데이터를 생성하지않고 만들어진 결과에 데이터를 맞춰서 보여주는 방식입니다.

 

스프링부트를 이용해서 웹프로젝트를 구성할때는 'API서버'라는 것을 구성하는 경우가 많습니다.

API서버는 JSP나 Thymeleaf처럼 서버에서 화면과 관련된 내용을 만들어내는 것이 아니라 순순한 데이터만 전송하는 방식입니다.

과거에는 주로 XML을 이용했지만 최근에는 JSON을 이용하는것이 일반적입니다.

JSON은 'JavaScript Object Notation'의 약자로 객체를 자바스크립트의 '객체표기법'으로 표현한 순수한 문자열입니다.

JSON은 문자열이기 때문에 데이터 교환시에 프로그램언어에 독립적이라는 장점이 있습니다.

스프링을 이용할땐 jackson-databind라는 별도의 라이브러리를 추가한 후에 개발할 수 있지만,

스프링부트는 'web'항목을 추가할때 자동으로 포함되므로 별도의 설정없이 바로 개발할 수 있습니다.

 


2. Thymeleaf

thymeleaf는 JSP를 대신하는 목적으로 작성된 라이브러리이므로, JSP에서 필요한 기능들을 Thymeleaf로 구성해야 합니다.

Thymeleaf를 이용하기 위해서 가장 중요한 설정은 '네임스페이스(xmlns)'에 Thymeleaf를 지정하는 것입니다.

네임스페이스를 지정하면 'th:' 와 같은 Thymeleaf의 모든 기능을 사용할 수 있게 됩니다.

Thymeleaf는 Model로 전달된 데이터를 출력하기위해서 HTML태그내에 'th:'로 시작하는 속성을 이용하거나 inlining을 이용합니다.

<!DOCTYPE html>
<html xmlns:th="http://www.tymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h4>[[${list}]]</h4>
    <hr/>
    <h4 th:text="${list}"></h4>
</body>
</html>

Thymeleaf는 에러가 발생하면 원인을 찾아내기가 힘듭니다. 에러가 난 부분을 찾기 위해서 주석처리를 해야할 때는

'<!--/* ... */-->'를 이용하는것이 좋습니다.

 

th:with를 이용한 변수 선언

Thymeleaf를 이용하는 과정에서 이시로 변수를 선언해야 하는 상황에서는 'th:with'를 이용해서 간단히 처리할 수 있습니다.

'변수명=값'의 형태로 ','를 이용해서 여러개를 선언할 수도 있습니다.

<div th:with="num1 = ${10}, num2 = ${20}">
    <h4 th:text="${num1 + num2}"></h4>
</div>

 

반복문과 제어문 처리

반복문 처리는 크게 2가지방법을 이용할 수 있습니다.

반복이 필요한 태그에 'th:each'를 적용하는 방법 / <th:block>이라는 별도의 태그를 이용하는 방법

<ul>
    <li th:each="str: ${list}" th:text="${str}"></li>
</ul>
<ul>
    <th:block th:each="str: ${list}">
        <li>[[${str}]]</li>
    </th:block>
</ul>

반복문의 status변수 : 현재 반복문의 내부상태에 변수를 추가해서 사용할 수 있습니다.

일명 status변수라고 하는데 index/count/size/first/last/odd/even등을 이용해서 자주 사용하는값들을 출력할 수 있습니다.

<ul>
    <li th:each="str, status: ${list}">
        [[${status.index}]] -- [[${str}]]
    </li>
</ul>

 

th:if / th:unless / th:switch

th:if와 th:unless는 사실 별도의 속성으로 사용할 수 있으므로 if~else와는 조금 다르게 사용됩니다.

예를들어 반복문의 홀수/짝수를 구분해서 처리하고 싶다면,

<ul>
    <li th:each="str, status: ${list}">
        <span th:if="${status.odd}"> ODD -- [[${str}]]</span>
        <span th:unless="${status.odd}"> EVEN -- [[${str}]]</span>
    </li>
</ul>

'?'를 사용해서 삼항연산자를 사용할 수 도 있습니다.

<ul>
    <li th:each="str,status : ${list}">
        <span th:text="${status.odd} ?'ODD ---' + ${str} : 'EVEN ---' + ${str}"></span>
    </li>
</ul>

th:switch는 th:case와 같이 사용해서 Switch문을 처리할 때 사용할 수 있습니다.

<ul>
    <li th:each="str,status : ${list}">
        <th:block th:switch="${status.index % 3}">
            <span th:case="0">0</span>
            <span th:case="1">1</span>
            <span th:case="2">2</span>
        </th:block>
    </li>
</ul>

 

Thymeleaf 링크처리

<a th:href="@{/hello}">Go to /hello</a>

링크의 쿼리스트링 처리, 링크를 'key=value'의 형태로 필요한 파라미터를 처리해야할때 편리합니다.

<a th:href="@{/hello(name='AAA', age=16)}">Go to /hello</a>

GET방식으로 처리되는 링크에서 한글이나 공백문자는 항상 주의해야하는 부분입니다.

Thymeleaf를 이용하면 이에대한 URL인코딩 처리가 자동으로 이루어집니다.

<a th:href="@{/hello(name='한글처리', age=16)}">Go to /hello</a>

만일 링크를 만드는 값이 배열과 같이 여러개일 때는 자동으로 같은 이름의 파라미터를 처리합니다.

<a th:href="@{/hello(types=${ {'AA', 'BB', 'CC'} }, age=16)}">Go to /hello</a>

 = <a href:"/hello?types=AA&types=BB&types=CC&age=16>Go to /hello</a>

 

Thymeleaf 인라인 처리

상황에 따라서 동일한 데이터를 다르게 출력해주는 인라인기능은 자바스크립트를 사용할 때 편리한 기능입니다.

class SampleDTO {
    private String p1, p2, p3;
    public String getP1() { return p1; }
    public String getP2() { return p2; }
    public String getP3() { return p3; }
}

@GetMapping("/ex/ex2")
public void ex2(Model model) {
    log.info("ex/ex2..........");
    
    List<String> strList = IntStream.range(1, 10)
            .mapToObj(i -> "Data" + i)
            .collect(Collectors.toList());
    
    model.addAttribute("list", strList);

    Map<String, String> map = new HashMap<>();
    map.put("A", "AA");
    map.put("B", "BB");
    
    model.addAttribute("map", map);
    
    SampleDTO sampleDTO = new SampleDTO();
    sampleDTO.p1  = "Value -- p1";
    sampleDTO.p2  = "Value -- p2";
    sampleDTO.p3  = "Value -- p3";
    
    model.addAttribute("dtp", sampleDTO);
}
<!DOCTYPE html>
<html xmlns:th="http://www.tymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div th:text="${list}"></div>
    <div th:text="${map}"></div>
    <div th:text="${dto}"></div>

    <script th:inline="javascript">
        const list = [[${list}]]
        
        const map = [[${map}]]
        
        const dto = [[${dto}]]
        
        console.log(list)
        console.log(map)
        console.log(dto)
    </script>
</body>
</html>

HTML코드를 이용하거나 자바스크립트코드를 이용할때 같은 객체들을 사용하는것을 볼 수 있습니다.

다만 차이점은 <script th:inline='javascript'>가 지정된 것 뿐입니다.

 

Thymeleaf의 레이아웃 기능

Thymeleaf의 <th:block>을 이용하면 레이아웃을 만들고 특정한 페이지에서는 필요한 부분만을 작성하는 방식으로 개발이 가능합니다.

레이아웃기능을 이용하려면 별도의 라이브러리가 필요하므로 build.gradle에 아래처럼 추가해줍니다.

dependencies {
    ... 생략 ...
    implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.1.0'
}
<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <title>layout page</title>
</head>
<body>

<div>
    <h3>Sample Layout Header</h3>
</div>

<div layout:fragment="content">
    <p>page content goes here</p>
</div>

<div>
    <h3>Sample Layout Footer</h3>
</div>

<th:block layout:fragment="script">

</th:block>
</body>
</html>

코드위쪽 http://www.ultraq.net.nz/thymeleaf/layout을 이용해서 thymeleaf의 Layout을 적용하기위한 네임스페이스를 지정합니다.

코드중간 layout:fragment속성을 이용해서 해당영역은 자중에 다른 파일에서 해당부분만을 개발할 수 있습니다.

layout.html에는 'content', 'script'부분을 fragment로 지정했습니다. 새로운 화면을 작성할땐 layout1.html을 그대로 활용하면서

'content','script'중 원하는 영역만을 작성할 수 있습니다.

<!DOCTYPE html>
<html  xmlns:th="http://www.thymeleaf.org"
       xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
       layout:decorate="~{laylout/laylout1.html}">

<div layout:fragment="content">
    <h1>ex3.html</h1>
</div>

</html>

앞서 작성된 코드를 보면 fragment중에 content부분만 작성했는데 브라우저로 확인해보면 위, 아래쪽으로 layout1.html의 내용이

같이 처리되는것을 볼 수 있습니다.

layout1.html에는 content와 script 영역을 따로 구성했으므로 이를 이용해서 자바스크립트를 처리하고 싶다면 별도의 영역을 구성하고 fragment를 지정합니다.

 

<!DOCTYPE html>
<html  xmlns:th="http://www.thymeleaf.org"
       xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
       layout:decorate="~{laylout/laylout1.html}">

<div layout:fragment="content">
    <h1>ex3.html</h1>
</div>

<script layout:fragment="script" th:inline="javascript">
    const arr = [[${arr}]]
</script>

</html>

3. Spring Data JPA

JPA(Java Persistence API)라는 기술은 간단하게 '자바로 영속 영역을 처리하는 API'라고 해석할 수 있습니다. JPA의 상위개념은

ORM(Object Relational Mapping)으로 '객체지향'으로 구성한 시스템을 '관계형 데이터베이스'에 매핑하는 패러다임입니다.

JPA는 스프링과 연동할때 Spring Data JPA라는 라이브러리를 사용합니다. 이는 JPA를 단독으로 활용할때보다 더 적은 양의 코드로 많은 기능을 활용할 수 있다는 장점이 있습니다. (가장 흔한 예제인 게시판을 만들어보면서 설명하겠습니다)

 

우선 JPA를 이용하는 개발의 핵심은 객체지향을 통해서 영속계층을 처리하는데 있습니다.

따라서 테이블과 SQL을 다루는것이 아닌 데이터에 해당하는 엔티티객체라는 것으로 다루고 JPA로 데이터베이스와 연동해서 관리합니다.

엔티티객체는 PK를 가지는 자바의객체로, 고유의 식별을 위해 @id를 이용해서 객체를 구분하고 관리합니다.

Spring Data JPA는 엔티티객체를 이용해서 JPA를 편리하게 제공하는 스프링 관련 라이브러리입니다. Spring Data JPA는 자동으로 객체를 생성하고 이를 통해서 예외처리등을 자동으로 처리하는데 이를위해서 제공되는 인터페이스가 JpaRepository입니다.

 

엔티티객체는 다음과 같이 정의합니다.

@Entity
public class Board {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long bno;
    
    private String title;
    
    private String content;
    
    private String writer;
}

엔티티객체를 위한 엔티티클래스는 반드시 @Entity를 적용해야하고 @Id가 필요합니다.

auto increment로 번호를 생성할것이므로 GenerationType.IDENTITY방식을 사용해줍니다.

키생성 전략은 다음과 같습니다.

IDENTITY 데이터베이스에 위임 - auto increment
SEQUENCE 데이터베이스 시퀀스 오브젝트 사용 - @SequenceGenrator 필요
TABLE 키생성용 테이블사용, 모든 DB에서 사용 - @TableGenerator 필요
AUTO 방언에 따라 자동 지정, 기본값

 

@MappedSuperClass를 이용한 공통 속성 처리

데이터베이스의 거의 모든 테이블에는 데이터가 추가된 시간이나 수정된 시간등이 컬럼으로 작성됩니다.

자바에서는 이를 쉽게 처리하고자 @MappedSuperClass를 이용해서 공통으로 사용되는 칼럼들을 지정, 상속합니다.

@MappedSuperclass
@EntityListeners(value = {AuditingEntityListener.class})
@Getter
abstract class BaseEntity {
    
    @CreatedDate
    @Column(name = "regdate", updatable = false)
    private LocalDateTime regDate;
    
    @LastModifiedDate
    @Column(name = "moddate")
    private LocalDateTime modDate;
    
}

AuditingEntityListener를 적용하면 엔티티가 데이터베이스에 추가되거나 변경될때 자동으로 시간값을 지정할 수 있습니다.

AuditingEntityListener를 활성화기키려면 아래와같이 프로젝트 설정에 @EnableJpaAuditing를 추가해야만 합니다.

@SpringBootApplication
@EnableJpaAuditing
public class B01Application {

    public static void main(String[] args) {
        SpringApplication.run(B01Application.class, args);
    }

}

기존의 Board클래스가 BaseEntity를 상속하도록 변경하고 추가적인 어노테이션들을 적용해줍니다.

@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Board extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long bno;

    @Column(length = 500, nullable = false) // 컬럼의 길이와 null허용여부
    private String title;
    
    @Column(length = 2000, nullable = false)
    private String content;
    
    @Column(length = 50, nullable = false)
    private String writer;
}

 

JpaRepository인터페이스

Spring Data JPA를 이용할때 JpaRepository인터페이스를 이용해서 선언만으로 데이터베이스관련 작업을 약간 처리할 수 있습니다.

(MyBatis를 이용해서 매퍼인터페이스만을 선언하는것과 유사합니다.)

개발단계에서 JpaRepository인터페이스를 상속하는 인터페이스를 선언하는 것만으로 CRUD와 페이징 처리가 완료됩니다.

package org.zerock.b01.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.zerock.b01.domain.Board;

public interface BoardRepository extends JpaRepository<Board, Long> {
}

JpaRepository인터페이스를 상속할때 엔티티타입과 @Id타입을 지정해야하는 점을 제외하면 아무런 코드가 없어도 개발이 가능합니다.

코드가 없기때문에 확장문제가 있을 수 있다고 생각할 수 있습니다. 이는 쿼리메소드라는 기능이나 Querydsl을 이용하면 해결가능합니다.

 

Pageable과 Page<E>타입

Spring DATA JPA를 이용해 별도의 코드없이 CRUD가 가능하다는 점도 좋지만, 빠른 페이징처리도 정말 매력적인 기능입니다.

페이징처리는 Pageable이라는 타입의 객체를 구성해서 파라미터로 전달하면 됩니다.

Pageable은 인터페이스로 설계되어있고, 일반적으로는 PageRequest.of()라는 기능을 이용해서 개발합니다.

▶ PageRequest.of(페이지번호, 사이즈) : 페이지 번호는 0부터

▶ PageRequest.of(페이지번호, 사이즈, Sort) : 정렬 조건 추가

▶ PageRequest.of(페이지번호, 사이즈, Sort, Direction, 속성...) : 정렬 방향과 여러 속성 지정

파라미터로 Pageable을 이용하면 리턴 타입은 Page<T>타입을 이용할 수 있는 데 이는 단순 목록뿐 아니라,

페이징처리에 데이터가 많은 경우에는 count 처리를 자동으로 실행합니다.

JpaRepository에는 findAll()이라는 기능을 제공하여 기본적인 페이징처리를 지원합니다.

 

// 1 page order by bno desc

Pageable pageable = PageRequest.of(0, 10, Sort.by("bno").descending());

Page<Board> result = boardRepository.findAll(pageable);

 

 

쿼리 메소드와 @Query

쿼리메소드는 보통 SQL에서 사용하는 키워드와 칼럼들을 같이 결합해서 구성하면 그 자체가 JPA에서 사용하는 쿼리가 되는 기능입니다.

일반적으로는 메소드 이름은 'findBy...' 혹은 'get...'으로 시작하고 칼럼명과 키워드를 결합하는 방식으로 구성합니다.

(자세한 사용법은 https://bit.ly/spring.io/projects/spring-data-jpa의 문서를 참고하시기 바랍니다.)

인텔리제이 얼티메이트는 자동완성 기능으로 쿼리 메소드를 작성할 수 있는 기능을 지원합니다.

 

쿼리메소드는 상당히 매력적인 기능이지만 실제로 사용해보면 상당히 길고 복잡한 메소드를 작성하게 됩니다.

아래는 '제목에 특정한 '키워드'가 존재하는 게시글들을 bno의 역순으로 정력해서 가져오는 메소드입니다.

Page<Board> findByTitleContainingOrderByBnoDesc(String keyword, Pageable pageable);;;

쿼리메소드는 단순한 쿼리를 작성할때 사용하고 실제 개발에서는 많이 사용되지 않습니다.

 

이와 유사하게 별도의 처리없이 @Query로 JPQL을 이용할 수 있습니다.

앞선 쿼리메소드에 @Query를 이용한다면 다음과 같이 작성됩니다.

@Query("select b from Board b where b.title like concat('%', :keyword, '%')")

Page<Board> findKeyword(String keyword, Pageable pageable);

@Query를 이용하면 쿼리메소드가 할 수 없는 몇가지 기능을 할 수 있습니다.

▶ 조인과 같이 복잡한 쿼리를 실행할 수 있는 기능

▶ 원하는 속성들만 추출해서 Object[]로 처리하거나 DTO로 처리하는 기능

▶ nativeQuery속성값을 true로 지정해서 특정 데이터베이스에서 동작하는 SQL을 사용하는 기능

 

Querydsl을 이용한 동적 쿼리 처리

JPA나 JPQL을 이용하면 쿼리를 처리하는 소스부분이 줄어 편리하지만 어노테이션을 이용하기때문에 고정된 형태라는 단점이 있습니다.

단일조건으로 검색되는 경우도 있지만 '제목/내용, 제목/작성자'와 같이 복합적인 검색 조건이 생길수도 있습니다.

이럴경우 모든 경우의 수를 별도의 메소드로 작성하는일이 더 어려울 수 있습니다.

이문제를 해결하기위해 국내에선 Querydsl방식을 가장 많이 사용하고 있습니다.

Querydsl을 이용하기위해서는 Q도메인이라는 존재가 필요한데,

Q도메인은 Querydsl의 설정을 통해서 기존의 엔티티 클래스를 Querydsl에서 사용하기위해서 별도의 코드로 생성하는 클래스입니다.

 

Querydsl을 이용하기위해서 build.gradle설정을 다음과 같이 변경해줄 필요가 있습니다.

build.gradle상단에 다음과 같은 부분을 추가합니다.

buildscript {
    ext {
        queryDslVersion = "5.0.0"
    }
}

dependencies부분에는 Querydsl 관련 라이브러리를 추가합니다.

dependencies {
    ... 생략 ...
    
    // Querydsl
    implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
    annotationProcessor(
            "javax.persistence:javax.persistence-api",
            "javax.annotation:javax.annotation-api",
            "com.querydsl:querydsl-apt:${queryDslVersion}:jpa"
    )
}

build.gradle 마지막 부분에 sourceSets를 지정합니다.

sourceSets {
    main {
        java {
            srcDirs = ["$projectDir/src/main/java", "$projectDir/build/generated"]
        }
    }
}

Querydsl의 설정이 올바르게 되었는지 확인하는 방법은 프로젝트 내에 Gradle메뉴를 열어서 'other'부분을 살펴보면

compilejava가 존재하는데 이를 실행합니다.

compilejava가 실행되면 build폴더에 QBoard클래스가 생성되는것을 볼 수 있습니다.

TIP : build부분의 clean을 먼저 실행하고 compilejava를 실행합니다. clean을 실행하면 build폴더 자체가 지워집니다.

 

기존의 Repository와 Querydsl 연동하기

Querydsl을 연동하기 위해서는 다음과 같은 과정으로 작성합니다.

▶ Querydsl을 이용할 인터페이스 선언

▶ '인터페이스이름 + impl'이라는 클래스를 선언 - 이때 QuerydslRepositorySupport라는 부모클래스를 지정하고 인터페이스를 구현

▶ 기존의 Repository에는 부모인터페이스로 Querydsl을 위한 인터페이스를 지정

public interface BoardSearch {

    Page<Board> search1(Pageable pageable);
}
public class BoardSearchImpl extends QuerydslRepositorySupport implements BoardSearch {
    
    public BoardSearchImpl(Class<?> domainClass) {
        super(domainClass);
    }

    @Override
    public Page<Board> search1(Pageable pageable) {
        return null;
    }
}
public interface BoardRepository extends JpaRepository<Board, Long>, BoardSearch {

    @Query(value = "select now()", nativeQuery = true)
    String getTime();
}

작성된 BoardSearchImpl에서 Q도메인을 이용하는 코드를 작성해보겠습니다.

@Override
public Page<Board> search1(Pageable pageable) {
    
    QBoard board = QBoard.board; // Q도메인 객체

    JPQLQuery<Board> query = from(board); // select...from board
    
    query.where(board.title.contains("1")); // where title like...
    
    this.getQuerydsl().applyPagination(pageable, query); // paging
    
    List<Board> list = query.fetch();
    
    long count = query.fetchCount();
    
    return null;
}

JPQLQuery는 @Query로 작성했던 JPQL을 코드를 통해 생성할 수 있게합니다. 이를통해 where나 group by, join등을 할수있습니다.

JPQLQuery의 실행은 fetch()라는 기능을 이용하고, fetchCount()를 이용하면 count쿼리를 실행할 수 있습니다.

페이징처리는 BoardImpl이 상속한 QuerydslRepositorySupport클래스의 기능을 이용합니다.

코드 중간에 getQuerydsl()과 applyPagination()가 그것입니다.

 

검색을 위해서는 적어도 검색 조건들과 키워드가 필요하므로 이를 처리하는 메소드를 BoardSearch에 추가합니다.

Page<Board> searchAll(String[] types, String keyword, Pageable pageable);
@Override
public Page<Board> searchAll(String[] types, String keyword, Pageable pageable) {

    QBoard board = QBoard.board; // Q도메인 객체

    JPQLQuery<Board> query = from(board); // select...from board
    
    if ((types != null && types.length > 0) && keyword != null) {
        BooleanBuilder booleanBuilder = new BooleanBuilder(); // (
        
        for (String type : types) {
            switch (type) {
                case "t" :
                    booleanBuilder.or(board.title.contains(keyword));
                    break;
                case "c" :
                    booleanBuilder.or(board.content.contains(keyword));
                    break;
                case "w" :
                    booleanBuilder.or(board.writer.contains(keyword));
                    break;
            }
        } // end for
        query.where(booleanBuilder);
    }// end if
    
    // bno > 0
    query.where(board.bno.gt(0L));
    
    // paging
    this.getQuerydsl().applyPagination(pageable, query);
    
    List<Board> list = query.fetch();
    
    long count = query.fetchCount();
    
    return null;
}

페이징 처리의 최종결과는 Page<T>타입을 반환하는것이므로 Querydsl에서는 이를 직접처리해야 하는 불편함이 있습니다.

Spring Data JPA에서는 PageImpl이라는 클래스를 제공해서 3개의 파라미터로 Page<T>를 생성할 수 있습니다.

▶ List<T> : 실제 목록 데이터

▶ Pageable : 페이지관련 정보를 가진 객체

▶ long : 전체 개수

// paging
this.getQuerydsl().applyPagination(pageable, query);

List<Board> list = query.fetch();

long count = query.fetchCount();

return new PageImpl<>(list, pageable, count);

 

반응형