SpringMVC 프로젝트 생성 및 구성하는 법에 대해 정리하며 복습해 보겠습니다.
SpringBoot와는 차이가 다소 있습니다.
InteliJ, Tomcat, maven, Java 11 기준입니다.
해당 코드들은 Github에 업로드해 두었습니다.
만들어 볼 프로젝트는 CRUD를 다 접해볼 수 있는 게시판으로 하겠습니다.
테이블로는 사용자, 권한, 게시글 3개만 사용합니다.
사용자 속성으로는 사용자번호, 로그인 아이디, 비밀번호, 이름, 프로필 이미지, 권한이 있습니다.
권한 속성으로는 권한번호, 권한명이 있습니다.
게시글 속성으로는 글번호, 제목, 본문, 작성자, 작성시간, 수정시간, 조회수가 있습니다.
Servlet만 추가하고, 필요한 구성이 있을 때마다 pom.xml
에 추가하도록 하겠습니다.
configuration -> edit에 들어가 편의를 위해 Application context를 /
로 두겠습니다(톰캣 실행버튼 우측 ...
에 있습니다).
빌드 후 정상적으로 동작했다면 HelloServlet
과 index.jsp
를 제거하겠습니다.
pom.xml
을 보면 11버전이 아닌 8버전으로 생성되어 있을 수 있습니다. 11로 수정해주시면 되겠습니다.
SpringMVC를 적용하기 위해 dependency를 추가하겠습니다.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>5.3.27</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
정상적으로 추가가 되었다면 AbstractAnnotationConfigDispatcherServletInitializer
를 사용해 보겠습니다.
상속받을 Initializer 클래스와 Config 클래스들을 만들어봅시다.
basePackages
를 사용할 것이기에 패키지 구조를 첨부하였습니다.
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller;
@Configuration
@ComponentScan(basePackages = "com.example.spring_mvc", excludeFilters = @ComponentScan.Filter(Controller.class))
public class RootWebAppConfig {
}
컨트롤러를 제외한 모든 컴포넌트를 스캔하도록 하겠습니다.
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = "com.example.spring_mvc.domain.**.controller")
public class ServletWebAppConfig {
}
오직 컨트롤러에 해당하는 컴포넌트만 스캔하도록 하겠습니다.
import com.example.spring_mvc.config.RootWebAppConfig;
import com.example.spring_mvc.config.ServletWebAppConfig;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[] {RootWebAppConfig.class};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[] {ServletWebAppConfig.class};
}
@Override
protected String[] getServletMappings() {
return new String[] {"/"};
}
}
AbstractAnnotationConfigDispatcherServletInitializer
가 어떤 역할을 하는지 궁금하시다면 해당 글을 참조해주세요.
세팅이 제대로 되어 있는지 확인하기 위해, 테스트 컨트롤러 하나를 작성하겠습니다.
package com.example.spring_mvc.domain.board.controller;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestBoardController {
@GetMapping("/")
@ResponseStatus(HttpStatus.OK)
public String getTest() {
return "TEST";
}
}
ServletWebAppConfig
를 통해 스캔되어야 하므로, board.controller 패키지에 클래스를 추가하였습니다.
제대로 화면이 뜨는지 확인합시다(만약 이전에 삭제한 파일의 화면이 보인다면 maven clean 후 재실행해 보세요).
CRUD가 제대로 이루어지는 지 알아보기 위해, 테이블을 생성하고 데이터를 삽입해보도록 하겠습니다.
편의를 위해 DB는 H2를 사용하겠습니다.
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.212</version>
</dependency>
추후 DB를 변경할 수도 있으니, properties를 통해 설정값을 주는 걸로 하겠습니다.
resources 내에 db.properties
파일을 작성합시다.
db.driverClassName=org.h2.Driver
db.url=jdbc:h2:~/spring_mvc;DATABASE_TO_UPPER=false;MODE=LEGACY;
db.userName=sa
db.password=
효율적인 연결을 위해 RootWebAppConfig에 DataSource를 설정하겠습니다.
우선 dbcp2를 추가하여 DataSource를 사용 가능케 합시다.
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.9.0</version>
</dependency>
생성자, getter 등을 편하게 작성하기 위한 lombok도 추가하겠습니다.
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
RootWebConfig
에 DataSource를 작성합시다.
@Configuration
@RequiredArgsConstructor
@PropertySource("classpath:db.properties")
@ComponentScan(basePackages = "com.example.spring_mvc.domain", excludeFilters = @ComponentScan.Filter(Controller.class))
public class RootWebAppConfig {
private final Environment env;
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName(env.getProperty("db.driverClassName"));
dataSource.setUrl(env.getProperty("db.url"));
dataSource.setUsername(env.getProperty("db.userName"));
dataSource.setPassword(env.getProperty("db.password"));
dataSource.setInitialSize(10);
dataSource.setMaxTotal(10);
dataSource.setMinIdle(10);
dataSource.setMaxIdle(10);
dataSource.setMaxWaitMillis(1000);
dataSource.setTestOnBorrow(true);
dataSource.setTestOnReturn(true);
dataSource.setTestWhileIdle(true);
return dataSource;
}
}
RequiredArgsConstructor
를 통해 env를 DI해주었습니다. 덕분에 간편하게 properties를 적용하였습니다.
프로젝트를 실행하면 알아서 쿼리를 동작시켜 미리 DB에 값을 넣어보도록 할까요? (JPA 설정을 통해 엔티티에 맞는 테이블 구조를 갖출 수도 있지만, ERD가 미리 구성되어있다는 상황을 가정하기 위해 설정해보겠습니다.)
-- CREATE SCHEMA IF NOT EXISTS `spring_mvc` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
-- USE `spring_mvc`;
DROP TABLE IF EXISTS `boards`;
DROP TABLE IF EXISTS `users`;
DROP TABLE IF EXISTS `roles`;
CREATE TABLE IF NOT EXISTS `roles` (
`role_id` INT NOT NULL AUTO_INCREMENT,
`role_name` VARCHAR(20) UNIQUE NOT NULL,
PRIMARY KEY (`role_id`)
);
CREATE TABLE IF NOT EXISTS `users` (
`user_id` BIGINT NOT NULL AUTO_INCREMENT,
`user_login_id` VARCHAR(100) UNIQUE NOT NULL,
`user_password` VARCHAR(100) NOT NULL,
`user_name` VARCHAR(20) NOT NULL,
`user_profile_image` VARCHAR(200) DEFAULT NULL,
`user_role_id` INT NOT NULL DEFAULT 3,
PRIMARY KEY (`user_id`),
FOREIGN KEY (`user_role_id`) REFERENCES `roles` (`role_id`)
);
CREATE TABLE IF NOT EXISTS `boards` (
`board_id` BIGINT NOT NULL AUTO_INCREMENT,
`title` VARCHAR(100) NOT NULL,
`content` TEXT,
`user_id` BIGINT NOT NULL,
`create_date_time` DATETIME NOT NULL DEFAULT now(),
`update_date_time` DATETIME NOT NULL DEFAULT now(),
`view_count` INT NOT NULL DEFAULT 0,
PRIMARY KEY (`board_id`),
FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`)
);
INSERT INTO `roles` (`role_name`) values ('ROLE_ADMIN');
INSERT INTO `roles` (`role_name`) values ('ROLE_MEMBER');
INSERT INTO `roles` (`role_name`) values ('ROLE_GUEST');
INSERT INTO `users` (`user_login_id`, `user_password`, `user_name`, `user_role_id`) values ('admin', 'admin', '어드민', 1);
INSERT INTO `users` (`user_login_id`, `user_password`, `user_name`, `user_role_id`) values ('member', 'member', '멤버', 2);
INSERT INTO `users` (`user_login_id`, `user_password`, `user_name`, `user_role_id`) values ('guest', 'guest', '게스트', 3);
INSERT INTO `boards` (`title`, `content`, `user_id`) values ('첫번째 게시글', '첫번째 게시글 내용', 1);
INSERT INTO `boards` (`title`, `content`, `user_id`) values ('두번째 게시글', '두번째 게시글 내용', 2);
INSERT INTO `boards` (`title`, `content`, `user_id`) values ('세번째 게시글', '세번째 게시글 내용', 3);
해당 쿼리를 resources
밑의 scripts/query.dsl
에 작성하도록 하겠습니다.
프로젝트가 빌드될 때 해당 쿼리가 동작하도록 설정하려면 DataSourceInitializer
를 작성해주어야 합니다(H2에서는 INIT=RUNSCRIPT
명령어를 통해 구현할 수 있지만, 다른 DB로 교체 시에도 원할하게 작성되기 위해 설정하겠습니다).
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
db.properties
에 해당 쿼리 경로를 작성해줍시다.
db.initQuery=/scripts/query.sql
DataSourceInitializer를 작성하겠습니다.
@Bean
public DataSourceInitializer dataSourceInitializer(DataSource dataSource) {
ResourceDatabasePopulator resourceDatabasePopulator = new ResourceDatabasePopulator();
resourceDatabasePopulator.addScript(new ClassPathResource(
Objects.requireNonNull(env.getProperty("db.initQuery"))));
DataSourceInitializer dataSourceInitializer = new DataSourceInitializer();
dataSourceInitializer.setDataSource(dataSource);
dataSourceInitializer.setDatabasePopulator(resourceDatabasePopulator);
return dataSourceInitializer;
}
톰캣을 실행 및 종료시킨 후, H2에 들어가 제대로 쿼리가 동작했는지 확인해봅시다.
제대로 입력이 된 것을 확인하였습니다.
해당하는 게시판을 제대로 구현하려면, 엔티티들의 연관관계가 제대로 맺혀 있어야 합니다.
DB에 작성된 연관관계를 따라 JPA를 잘 구성해보도록 합시다.
<dependencyManagement>
<dependencies>
...
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-bom</artifactId>
<version>2021.2.0</version>
<scope>import</scope>
<type>pom</type>
</dependency>
...
</dependencies>
</dependencyManagement>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.6.0.Final</version>
</dependency>
xml에 내용을 작성한 후 config 파일을 생성해줍시다.
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = "com.example.spring_mvc.domain.**.repository")
public class JpaConfig {
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory);
return transactionManager;
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
emf.setDataSource(dataSource);
emf.setPackagesToScan("com.example.spring_mvc.domain.**.entity");
emf.setJpaVendorAdapter(jpaVendorAdapters());
emf.setJpaProperties(jpaProperties());
return emf;
}
private JpaVendorAdapter jpaVendorAdapters() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setDatabase(Database.H2);
return hibernateJpaVendorAdapter;
}
private Properties jpaProperties() {
Properties jpaProperties = new Properties();
jpaProperties.setProperty("hibernate.show_sql", "true");
jpaProperties.setProperty("hibernate.format_sql", "true");
jpaProperties.setProperty("hibernate.use_sql_comments", "true");
jpaProperties.setProperty("hibernate.globally_quoted_identifiers", "true");
jpaProperties.setProperty("hibernate.temp.use_jdbc_metadata_defaults", "false");
jpaProperties.setProperty("hibernate.highlight_sql", "true");
return jpaProperties;
}
}
Transaction이 일어날 때 EntityManagerFactory에서 EntityManager를 생성하는데, 해당 연결을 H2 DB용으로 설정 및 sql query를 어떻게 보여줄 지 세팅하는 코드입니다.
Entity -> Json 매핑을 위해 jackson 라이브러리 추가 및 ServletWebAppConfig
에 @EnableMvc를 적용해줍시다(적용하지 않는다면 406 에러가 발생할 겁니다).
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.4.1</version>
</dependency>
@EnableWebMvc
@Configuration
@ComponentScan(basePackages = "com.example.spring_mvc.domain.**.controller")
public class ServletWebAppConfig {
}
역할, 사용자, 게시글에 대한 엔티티들을 작성해봅시다.
@Entity
@Getter
@Table(name = "roles")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "role_id")
private Integer id;
@Enumerated(EnumType.STRING)
@Column(name = "role_name")
private RoleName roleName;
public enum RoleName {
ROLE_ADMIN, ROLE_MEMBER, ROLE_GUEST
}
}
역할에 대한 값들은 이미 정해져 있습니다. Enum을 사용하면 손쉽게 검증할 수 있습니다.
@Entity
@Getter
@Table(name = "users")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
@Column(name = "user_login_id")
private String loginId;
@Setter
@Column(name = "user_password")
private String password;
@Setter
@Column(name = "user_name")
private String name;
@Setter
@Column(name = "user_profile_image")
private String profileImage;
@Setter
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_role_id")
private Role role;
}
@Entity
@Getter
@Table(name = "boards")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "board_id")
private Long id;
private String title;
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@Column(name = "create_date_time")
private LocalDateTime createDateTime;
@Column(name = "update_date_time")
private LocalDateTime updateDateTime;
@Column(name = "view_count")
private int viewCount;
public void updateBoard(String title, String content) {
this.title = title;
this.content = content;
this.updateDateTime = LocalDateTime.now();
}
public void increaseViewCount() {
this.viewCount++;
}
}
User에서는 변경 가능한 값들을 @Setter
로 설정했으나, Board에서는 좀 더 명확하게끔 메서드로 작성하였습니다.
간단하게 Controller, Service, Repository를 만들고 제대로 실행이 되는지 테스트해봅시다.
모든 엔티티에 대해 테스트해보면 좋겠으나, 현재 OSIV가 꺼진 상태 + FetchType.LAZY
를 걸어놓았으므로 Board와 User는 proxy 에러가 발생합니다.
가장 간단한 Role에 대해 작성해봅시다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/role")
public class RoleController {
private final RoleService roleService;
@GetMapping
@ResponseStatus(HttpStatus.OK)
public Role test() {
return roleService.getRole(1);
}
}
@Service
@RequiredArgsConstructor
public class RoleService {
private final RoleRepository roleRepository;
@Transactional(readOnly = true)
public Role getRole(int id) {
return roleRepository.findById(id)
.orElseThrow(IllegalArgumentException::new);
}
}
public interface RoleRepository extends JpaRepository<Role, Integer> {
}
잘 출력됩니다.
사용자 전체 목록을 받아와 제공한다고 생각해봅시다.
사용자가 지닌 필드 중에서 비밀번호와 프로필 이미지까지 제공할 필요가 있을까요?
프로필 이미지야 제공할 수 있다곤 하지만, 비밀번호를 노출시키는 건 상당히 위험합니다.
JPA를 통해 모든 사용자 데이터를 받아온 후에 원하는 데이터들만 선별해서 반환할 수 있지만, 쿼리문 작성 시에 원하는 필드만 요청하고 받아온다면(Projection) 훨씬 효율적이면서도 오남용을 막을 수 있습니다.
QueryDSL을 통해 위와 같은 상황을 해결해봅시다.
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>5.0.0</version>
</dependency>
<build>
<plugins>
...
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<configuration>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/annotations</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
...
</plugins>
</build>
더불어, 기존 query문과 비슷하면서도 손쉽게 사용할 수 있도록 JPAQueryFactory를 빈으로 생성하겠습니다(해당 과정이 없어도 QueryDSL을 사용할 수 있으나, select문이 아닌 from문부터 작성해야 합니다).
@Bean
public JPAQueryFactory jpaQueryFactory(EntityManager em) {
return new JPAQueryFactory(em);
}
Page도 사용해야 하므로 RootWebAppConfig
에 어노테이션을 추가해줍시다.
...
@EnableSpringDataWebSupport
public class RootWebAppConfig {
...
사용자 목록을 받아오는 로직에 대해 작성해보도록 하겠습니다.
@Getter
public class UsersResponseDto {
private final Long id;
private final String name;
private final Role.RoleName roleName;
@QueryProjection
public UsersResponseDto(Long id, String name, Role.RoleName roleName) {
this.id = id;
this.name = name;
this.roleName = roleName;
}
}
전체 사용자 필드 중 로그인id, 이름, 역할만 보여주기로 하겠습니다.
해당하는 필드들을 작성한 후, 생성자에 @QueryProjection
을 붙여 QClass가 생성되도록 하였습니다.
Maven Compile을 수행시키면 해당 Dto와 모든 Entity에 대한 QClass가 생성될 것입니다(만약 생성되지 않는다면 clean 후 Compile을 몇 번 반복해보시고, 그래도 되지 않는다면 세팅을 다시 한 번 확인해보시길 바랍니다).
@RestController
@RequiredArgsConstructor
@RequestMapping("/user")
public class UserController {
private final UserService userService;
@GetMapping
public Page<UsersResponseDto> getUsersPage(Pageable pageable) {
return userService.getUsersPage(pageable);
}
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Transactional(readOnly = true)
public Page<UsersResponseDto> getUsersPage(Pageable pageable) {
return userRepository.getUsersPage(pageable);
}
}
public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {
}
Pageable을 이용하는 기본적인 Component Hirachy를 작성했습니다.
public interface UserRepositoryCustom {
Page<UsersResponseDto> getUsersPage(Pageable pageable);
}
QueryDSL은 네이밍 규칙이 있습니다. 사용할 메서드들을 정의한 인터페이스는 ${JPARepsitoryName}Custom
으로, 해당 메서드들을 구현한 클래스는 ${JPARepsitoryName}Impl
로 작성해주셔야 합니다.
@RequiredArgsConstructor
public class UserRepositoryImpl implements UserRepositoryCustom {
private final JPAQueryFactory queryFactory;
@Override
public Page<UsersResponseDto> getUsersPage(Pageable pageable) {
List<UsersResponseDto> content = getContent(pageable);
long total = getTotal();
return new PageImpl<>(content, pageable, total);
}
private List<UsersResponseDto> getContent(Pageable pageable) {
return queryFactory
.select(new QUsersResponseDto(
user.id,
user.name,
role.roleName)
)
.from(user)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
}
private Long getTotal() {
return queryFactory
.select(user.count())
.from(user)
.fetchOne();
}
}
@QueryProjection
으로 생성한 QClass를 생성하며 프로젝션을 수행하였습니다.
또한 Page를 생성하기 위해 튜플의 갯수를 얻어오는 count 쿼리도 작성하였습니다.
fetchResults
를 사용하면 손쉽게 두 개의 쿼리를 동작시켜 주지만, 현재는 Deprecated
되었기 때문에 직접 작성하였습니다.
이후 PageImpl을 반환하여 Page를 사용할 수 있도록 하였습니다.
쿼리를 확인해보시면, select문을 통해 필요한 필드만 지정된 것을 확인하실 수 있습니다.
그러나 Join쪽을 살펴보면 cross join이 발생한 것을 알 수 있습니다.
user가 role을 지니고 있기에 Join을 생략하였으나, 이러한 경우 Cross Join이 실행되기 때문입니다. 자세한 건 해당 글의 4번항목을 참조하시기 바랍니다.
innerJoin으로 변경해보도록 하겠습니다.
private List<UsersResponseDto> getContent(Pageable pageable) {
return queryFactory
.select(new QUsersResponseDto(
user.id,
user.name,
role.roleName)
)
.from(user)
.innerJoin(user.role, role)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
}
innerJoin이 제대로 작동된 것을 확인할 수 있습니다.
Json 결과가 복잡하게 나올 것을 알기에 Json Formatter 확장 프로그램을 설치해서 예쁘게 볼 수 있도록 하겠습니다.
원하는 결과가 나왔군요.
결과를 클라이언트에게 전하기 위해서는 View가 필요합니다.
FE, BE로 나눠 작업한다면 좋겠지만 사정이 녹록치 않으니 직접 화면을 구성해봅시다.
어디까지나 결과만을 전달할 것이기에, 디자인 작업(CSS)은 하지 않겠습니다.
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.15.RELEASE</version>
</dependency>
dependency를 추가해준 후, ServleWebAppConfig에 Thymeleaf를 위한 설정을 작성해줍시다.
@EnableWebMvc
@Configuration
@ComponentScan(basePackages = "com.example.spring_mvc.domain.**.controller")
public class ServletWebAppConfig implements WebMvcConfigurer, ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Bean
public ThymeleafViewResolver thymeleafViewResolver() {
ThymeleafViewResolver thymeleafViewResolver = new ThymeleafViewResolver();
thymeleafViewResolver.setTemplateEngine(springTemplateEngine());
thymeleafViewResolver.setCharacterEncoding("UTF-8");
thymeleafViewResolver.setOrder(1);
thymeleafViewResolver.setViewNames(new String[]{"*"});
return thymeleafViewResolver;
}
public SpringTemplateEngine springTemplateEngine() {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(springResourceTemplateResolver());
return templateEngine;
}
public SpringResourceTemplateResolver springResourceTemplateResolver() {
SpringResourceTemplateResolver springResourceTemplateResolver = new SpringResourceTemplateResolver();
springResourceTemplateResolver.setApplicationContext(applicationContext);
springResourceTemplateResolver.setCharacterEncoding("UTF-8");
springResourceTemplateResolver.setPrefix("/WEB-INF/views/");
springResourceTemplateResolver.setSuffix(".html");
springResourceTemplateResolver.setTemplateMode(TemplateMode.HTML);
return springResourceTemplateResolver;
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.viewResolver(thymeleafViewResolver());
}
}
리소스 지정, 관리 등을 위해 ApplicationContext
를 가져와 SpringResourceTemplateResolver
에 설정하였습니다.
제일 처음에 만들었던 TestClass를 활용해봅시다.
@Controller
public class TestClass {
@GetMapping("/")
@ResponseStatus(HttpStatus.OK)
public String getTest() {
return "test";
}
}
RestController에서 Controller로 전환하였습니다.
위쪽 설정에서 정의한 대로, src/main/webapp/WEB-INF/views
밑에 test.html
파일을 추가해줍시다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Test</title>
</head>
<body>
<h1>TEST</h1>
</body>
</html>
실행 후 확인해봅시다.
Thymeleaf가 적용되었습니다.
설정을 끝냈으니 View 하나를 직접 구성해보면서 Thymeleaf를 사용해봅시다.
QueryDSL을 활용하여 사용자 전체 목록을 페이징했었던 결과, 기억하시나요? 해당 데이터를 View로 전달하면서 사용법을 살펴봅시다.
우선 UserController
의 @RestController
를 @Controller
로 변경시켜준 후, View에 데이터를 넘겨야합니다.
@Controller
@RequiredArgsConstructor
@RequestMapping("/user")
public class UserController {
private final UserService userService;
@GetMapping
public String getUsersPage(Model model, Pageable pageable) {
Page<UsersResponseDto> usersPage = userService.getUsersPage(pageable);
model.addAttribute("usersPage", usersPage);
return "users";
}
}
그 후 페이지를 작성합시다.
src/main/webapp/WEB-INF/views/users.html
로 작성하시면 되겠습니다.
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Users</title>
</head>
<body>
<h1>사용자 목록</h1>
<table>
<tr>
<th>이름</th>
<th>권한</th>
</tr>
<tr th:each="user : ${usersPage.content}">
<td th:text="${user.name}"></td>
<td th:text="${user.roleName}"></td>
</tr>
</table>
<div th:if="${#bools.isFalse(usersPage.first)}">
<a th:href="@{/user(size=${usersPage.size}, page=${usersPage.number - 1})}">이전 페이지</a>
</div>
<div th:if="${#bools.isFalse(usersPage.last)}">
<a th:href="@{/user(size=${usersPage.size}, page=${usersPage.number + 1})}">다음 페이지</a>
</div>
</body>
</html>
결과를 확인해봅시다.
사용자들의 이름과 권한이 잘 나오고 있습니다.
하지만 사용자가 너무 적은 나머지, 페이징이 제대로 되는지 잘 모르겠네요.
파라미터를 제공해 보겠습니다.
잘 작동하고 있습니다.
여기까지 Spring Security를 제외한 기본적인 설정들을 마쳤습니다.
다음 글부터는 요구사항에 맞게 개발을 진행해보도록 하겠습니다.