서적 : http://www.yes24.com/product/goods/112373280
Spring + MyBatis + JSP 실습 겸 리마인드용으로 공부 중입니다.
1. 의존성 주입과 스프링
(1) 스프링의 시작
- 2000년 당시 자바 진영에서 JaveEE가 비대해지는 동안 스프링 프레임워크는 경량 프레임워크를 목표로 만들어짐
- 가장 중요한 코어(core) 역할을 하는 라이브러리와 여러 개의 추가적인 라이브러리를 결합하는 형태로 프로젝트를 구성
- 웹 MVC 구현을 하는 'Spring Web MVC' + JDBC 처리를 도와주는 'MyBatis' -> 'mybatis-spring'
- 의존성 주입 (DI, Dependency Injection)
- '객체와 객체 간의 관계를 더 유연하게 유지할 것인가?' 에 대한 고민
- 객체의 생성과 관계를 효과적으로 분리할 수 있는 방법에 대한 고민
- 기존에는 컨트롤러에서 직접 서비스 객체를 생성하거나, 이전 예제와 같이 하나의 객체만을 생성해서 활용 (Enum 타입으로 서비스를 설계, INSTANCE 활용) 하는 방식으로 적용
- 스프링 프레임워크는 프레임워크 자체에서 이를 지원하고 있음!
- 다양한 방식으로 필요한 객체를 찾아서 사용할 수 있도록 XML 설정이나 자바 설정 등을 이용
- 의존성 주입 (XML 활용하는 방법)
- 설정 파일 XML을 WEB-INF 폴더에 추가 'root-context.xml'
- xml내부에
<bean>
태그를 사용하여 DAO, Service를 주입
(2) ApplicationContext와 빈(Bean)
- ApplicationContext
- 이전 서블릿이 존재하는 공간을 서블릿 컨텍스트라고 하였던 것처럼
- Bean이라고 부르는 객체들을 관리하기 위해 ApplicationContext를 활용
- ApplicationContext는 위에서 설정한 root-context.xml을 이용, 스프링이 실행될 때 객체가 생성됨 -> bean으로 지정했던 클래스들의 객체를 생성해서 관리하기 시작
- @Autowired와 필드 주입
- '해당 타입의 빈이 존재한다면 여기에 주입해주기를 원한다'는 의미
- 멤버 변수에 직접 주입하기 때문에 '필드 주입(Field Injection)' 방식으로 부름
- <context:component-scan>
- 스프링 이용 시 클래스를 작성하거나 객체를 직접 생성하지 않으며, 이는 스프링 내부에서 이뤄지고 ApplicationContext가 생성된 객체들을 관리하게 됨
- 개발자가 직접 객체를 생성하지 않는 방식은 이전 서블릿과 상당히 유사
- 초기 스프링은 위에서 XML 파일에
<bean>
을 이용해서 클래스를 설정하였으나 2.5버전 이후 어노테이션 형태로 변화하였음
@Controller
: MVC 컨트롤러를 위한 어노테이션
@Service
: 서비스 계층의 객체를 위한 어노테이션
@Repository
: DAO와 같은 객체를 위한 어노테이션
@Component
: 일반 객체나 유틸리티 객체를 위한 어노테이션
- 위의 어노테이션을 인식하기 위해선 XML에 'component-scan' 추가 : 속성값으로 패키지를 지정하며 해당 패키지를 스캔, 스프링의 어노테이션들을 인식
- 생성자 주입 방식
- 주입 받아야 하는 객체의 변수는 final로 작성
- 생성자를 이용, 해당 변수를 생성자의 파라미터로 지정
- 해당 방식은 객체 생성 시 문제가 발생하는지를 미리 확인할 수 있음 -> 필드 주입, Setter 주입보다 더 선호되는 방식
- @RequiredArgsConstructor를 이용, 필요한 생성자 함수를 자동으로 작성
@Service
@RequiredArgsConstructor
public class SampleService {
private final SampleDAO sampleDAO;
}
(3) 인터페이스를 이용한 느슨한 결합
- 좀 더 근본적으로 유연한 프로그램을 설계하기 위해서는 인터페이스를 이용, 나중에 다른 클래스의 객체로 쉽게 변경할 수 있도록 하는 것이 좋음
- 예를 들어 SampleDAO를 다른 객체로 변경하려면 결국 SampleService 코드도 수정되어야 함
- 추상화된 타입을 이용하면 이를 피할 수 있음 -> 인터페이스를 이용, 실제 객체를 모르고 타입만을 이용하여 코드 작성이 가능해짐
- 예시
- SampleDAO를 인터페이스로 변경 -> SampleService는 여전히 SampleDAO를 보고 있으나 코드상 변경 필요 X
- SampleDAO 인터페이스는 실체가 없으므로 이를 구현한 클래스인 SampleDAOImpl이 필요 -> @Repository를 이용, 해당 클래스의 객체를 빈으로 처리되도록 구성
@Repository
public class SampleDAOImpl implements SampleDAO {
}
- 이 경우 Service 입장에서는 인터페이스만 바라보고 있어 실제 객체가 SampleDAOImpl의 인스턴스인지 알 수 없으나 코드 작성에는 문제가 아예 없음 -> 느슨한 결합
- 다른 DAO 객체로 변경이 필요할 경우 SampleDAO는 그대로 두고 새 Impl 클래스를 만들고 DAO를 구현한 뒤 @Primary, @Qualifier 등으로 지정을 해주면 됨
- 스프링의 빈(Bean)으로 지정되는 객체들
- 주로 오랜 시간 동안 프로그램 내에 상주하면서 중요한 역할을 하는 '역할' 중심의 객체
- DTO, VO 등의 데이터에 중점을 두고 설계된 객체들은 등록되지 않음
- XML이나 어노테이션으로 처리하는 객체의 처리 방법 기준
- 처리 방법에 대한 기준은 '코드를 수정할 수 있는가'로 판단
- jar 파일로 추가되는 클래스의 객체는 코드가 존재하지 않으므로 XML을 이용해 처리
- 직접 작성되는 클래스는 어노테이션을 이용
(4) 웹 프로젝트를 위한 스프링 준비
- 스프링의 구조는 ApplicationContext라는 객체가 존재하고, 빈으로 등록된 객체들은 해당 컨텍스트 내에 생성되어 관리되는 구조
- ApplicationContext가 웹 애플리케이션에서 동작하려면 웹 애플리케이션 실행 시 스프링을 로딩, 해당 웹 애플리케이션 내부에 스프링의 ApplicationContext를 생성하는 작업 필요
- 이를 위해서는 web.xml을 이용, 리스너를 설정해야 함
<listener>, <context-param>
을 추가, build.gradle 내에 spring-webmvc 라이브러리 추가
- DataSource 구성
- root-context.xml에 HikariCP 설정
- 이전 에제에서는 HikariCP 사용을 위해 HikariConfig 객체와 HikariDataSource를 초기화해야 했고 이를 위해 ConnectionUtil에서 처리하였음
- 위의 설정은 스프링에서는 빈으로 처리되어야 함
- root-context.xml에 HikariConfig, HikariDataSource 객체를 설정
2. MyBatis와 스프링 연동
(1) MyBatis 소개
- SQL Mapping Framework의 일종
- 장점
- PreparedStatement/ResultSet의 처리 - MyBatis가 해당 처리(파라미터, getXXX() 등)를 알아서 해줌
- Connection/PreparedStatement/ResultSet의 close() 처리 - 자동으로 close() 처리를 해줌
- SQL의 분리 - 별도의 파일이나 어노테이션 등으로 SQL을 선언. 파일을 이용하는 경우, SQL을 별도의 파일로 분리해서 운영이 가능
- MyBatis와 스프링 연동 방식
- MyBatis는 단독 실행이 가능한 완전히 독립적인 프레임워크
- 스프링 프레임워크는 MyBatis와 연동을 쉽게 처리할 수 있는 라이브러리와 API들을 제공
- 스프링에서 제공하는 라이브러리를 이용하는지 여부에 따라 다음의 방식 중 하나로 개발
- MyBatis를 단독으로 개발, 스프링에서 DAO를 작성해서 처리하는 방식
- 기존 DAO에서 SQL의 처리를 MyBatis를 이용하는 구조, 완전히 MyBatis, Spring Framework를 독립적인 존재로 보고 개발
- MyBatis와 스프링을 연동, Mapper 인터페이스만 이용하는 방식
- 'mybatis-spring'을 이용, 스프링이 DB 전체에 대한 처리를 하고 MyBatis는 일부 기능 개발에만 활용하는 방식. 개발 시 Mapper 인터페이스라는 방식을 이용, 인터페이스만으로 모든 개발이 가능
- 최소한의 코드로 개발이 가능한 'mybatis-spring' 방식을 채택
- SqlSessionFactory
- MyBatis를 이용하기 위해 스프링에 설정해둔 HikariDataSource를 이용, SqlSessionFactory라는 빈을 설정
- Mapper 인터페이스 활용
- MyBatis는 SQL 파일을 별도로 처리할 수 있으나 인터페이스와 어노테이션만으로도 처리 가능
- 현재 시간을 처리하는 TimeMapper 인터페이스 선언
- MyBatis는 @Select를 통해 쿼리를 작성할 수 있음 (select)
public interface TimeMapper {
@Select("select now()")
String getTime();
}
- 매퍼 인터페이스 설정을 root-context.xml에
<mybatis:scan>
로 등록하여야 함
- XML로 SQL 분리하기
- MyBatis 이용시 SQL은 @Select와 같은 어노테이션을 사용하기도 하지만 대부분 SQL을 별도의 파일로 분리하는 것을 권장
- SQL이 길어질 경우 어노테이션으로 처리하기가 복잡해지며 어노테이션이 나중에 변경되면 프로젝트 전체를 다시 빌드하는 작업이 필요
- XML과 매퍼 인터페이스를 같이 결합 시 다음과 같은 과정으로 작성
- 매퍼 인터페이스를 정의, 메소드 선언
- 해당 XML 파일을 작성 (파일 이름 - 매퍼 인터페이스 이름 동일) 하고
<select>
와 같은 태그를 이용해서 SQL을 작성
<select>, <insert>
등의 태그에 id 속성 값을 매퍼 인터페이스의 메소드 이름과 같게 작성
public interface TimeMapper2 {
String getNow();
}
<mapper namespace="org.~~~~.mapper.TimeMapper2>
<select id="getNow" resultType="string">
select now()
</select>
</mapper>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations" value="classpath:/mappers/**/*.xml"></property>
</bean>
3. 스프링 Web MVC 기초
(1) 스프링 Web MVC의 특징
- 스프링 Web MVC (이하 스프링 MVC)는 Web MVC 패턴으로 구현된 구조
- 이전에 다뤘던 Web MVC 패턴의 흐름과 거의 같음
- 기존 구조에서 변화가 생긴 부분
- Front-Controller 패턴을 이용, 모든 흐름의 사전/사후 처리를 가능하도록 설계
- 어노테이션을 적극적으로 활용, 최소한의 코드로 많은 처리가 가능하도록 설계
- HttpServletRequest/HttpServletResponse를 이용하지 않아도 될만큼 추상화된 방식으로 개발 가능

- DispatcherServlet과 Front Controller
- 스프링 MVC에서 가장 중요한 점. 모든 요청이 반드시 DispatcherServlet이라는 존재를 통해 실행 (Facade 패턴이자 웹에서는 Front Controller 패턴)
- 모든 요청이 반드시 하나의 객체인 프론트 컨트롤러를 지나서 처리되므로 모든 공통적인 처리를 프론트 컨트롤러에서 처리할 수 있게 되며, 스프링 MVC에서는 DispatcherServlet이 이를 수행
- 프론트 컨트롤러가 사전/사후에 대한 처리를 진행하며 중간에 매번 다른 처리를 하는 부분만 별도로 처리하는 구조를 만들게 되는데 이를 컨트롤러라고 하며 @Controller를 이용해서 처리

- servlet-context.xml 파일을 생성하여 스프링 MVC 관련 설정을 추가
<mvc:annotation-driven>
: 스프링 MVC 설정을 어노테이션 기반으로 처리 + 여러 객체들을 자동으로 스프링으 ㅣ빈으로 등록하게 하는 기능
<mvc:resources>
: 이미지, html 등의 정적인 파일의 경로 지정
- web.xml의 DispatcherServlet 설정
<servlet>
: DispatcherServlet이 로딩할 때 servlet-context.xml을 이용하도록 설정
<servlet-mapping>
: DispatcherServlet이 모든 경로의 요청에 대한 처리를 담당하기 때문에 '/'로 지정
<context:component-scan base-package="org.~~.springex.controller">
: controller 패키지를 스캔해서 @Controller 어노테이션이 추가된 클래스들의 객체들을 스프링 빈으로 설정되게 만듬
- @RequestMapping의 파생 어노테이션
- method 속성으로 GET/POST 방식을 구분하였음
- 4버전 이후부터는 @GetMapping, @PostMapping 등으로 구분해서 처리 가능
- 스프링 MVC 설정은 공통적인 설정을 그대로 복사해서 사용하기 때문에 사용에 익숙해지는 것이 좋다
(2) 파라미터 자동 수집과 변환
- 파라미터 자동 수집 : DTO나 VO 등을 메소드의 파라미터로 설정 시 자동으로 전달되는 HttpServletRequest의 파라미터들을 수집해주는 기능
- 단순 문자열만이 아니라 숫자, 배열, 리스트, 첨부 파일 등도 가능
- 기본 자료형의 경우 자동으로 형 변환 처리 가능
- 객체 자료형의 경우 setXXX() 동작을 통해 처리됨
- 객체 자료형의 경우 생성자가 없거나 파라미터가 없는 생성자가 필요 (Java Beans)
- Formatter를 이용한 파라미터의 커스텀 처리
- 기본적으로 HTTP는 문자열로 데이터를 전달, 컨트롤러는 문자열을 기준으로 특정 클래스의 객체로 처리하는 작업 진행
- 가장 문제가 되는 타입은 날짜 타입으로 파라미터 수집 시 에러가 발생할 수 있음
- 이 경우 특정 타입을 처리하는 Formatter - 날짜의 경우 LocalDate(Time)Formatter를 사용
- Formatter를 servlet-context.xml에 적용하기 위해서는 복잡한 과정 필요
- FormattingConversionServiceFactoryBean 객체를 빈으로 등록해야 하고 이 안에 작성한 LocalDateFormatter를 추가해야 함
public class LocalDateFormatter implements Formatter<LocalDate> {
@Override
public LocalDate parse(String text, Locale locale) {
return LocalDate.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
}
@Override
public String print(LocalDate object, Locale locale) {
return DateTimeFormatter.ofPattern("yyyy-MM-dd").format(object);
}
}
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="formatters">
<set>
<bean class="org.~~.springex.controller.formatter.LocalDateFormatter"/>
</set>
</property>
</bean>
- 객체 자료형의 파라미터 수집
- 객체 자료형을 파라미터로 처리하기 위해서는 객체가 생성되고 setXXX()을 이용하여 처리
- Lombok의 @Setter나 @Data를 활용하는 것이 가장 간단한 방법
- Model이라는 특별한 파라미터
- 기본적으로 웹 MVC와 동일한 방식으로, 모델이라고 부르는 데이터를 JSP까지 전달해야 함
- 순수한 서블릿 방식 : request.setAttribute()를 이용해 데이터를 담아 JSP까지 전달하였음
- 스프링 MVC 방식 : Model이라는 객체를 이용하여 처리
- Model의 addAttribute()를 통해 뷰에 전달할 이름과 값을 지정
- Java Beans의 @ModelAttribute
- 파라미터로 Getter/Setter를 이용하는 Java Beans의 형식의 사용자 정의 클래스가 파라미터일 경우 자동으로 화면까지 객체를 전달
@GetMapping("/ex4_1")
public void ex4Extra(@ModelAttribute("dto") TodoDTO todoDTO, Model model) {
log.info(todoDto);
}
- RedirectAttributes와 리다이렉션
- PRG 패턴 처리를 위해 RedirectAttributes라는 특별 타입 제공
- Model과 마찬가지로 파라미터로 추가해주기만 하면 자동으로 생성되는 방식
- addAttribute(키, 값) : 리다이렉트할 때 쿼리 스트링이 되는 값을 지정
- addFlashAttribute(키, 값) : 일회용으로만 데이터를 전달하고 삭제되는 값을 지정 (URL에는 보이지 않지만 JSP에서 일회용으로 사용)
- 스프링 MVC에서 주로 사용하는 어노테이션들
- 컨트롤러 선언부 사용
- @Controller : 스프링 빈의 처리됨을 명시
- @RestController : REST 방식의 처리를 위한 컨트롤러임을 명시
- @RequestMapping : 특정한 URL 패턴에 맞는 컨트롤러인지를 명시
- 메소드 선언부 사용
- @Get/@Post/@Delete/@PutMapping
- @RequestMapping : GET/POST 방식 모두를 지원하는 경우
- @ResponseBody : REST 방식에서 사용
- 메소드 파라미터에 사용
- @RequestParam : Request에 있는 특정한 이름의 데이터를 파라미터로 받아서 처리하는 경우에 사용
- @PathVariable : URL 경로의 일부를 변수로 삼아서 처리하기 위해 사용
- @ModelAttribute : 해당 파라미터는 반드시 Model에 포함되어 다시 뷰로 전달됨을 명시 (기본 자료형이나 Wrapper 클래스, 문자열에 사용)
- @SessionAttribute, @Valid, @RequestBody 등...
(3) 스프링 MVC의 예외 처리
- 가장 일반적인 처리 방식 : @ControllerAdvice
- @ControllerAdvice
- 컨트롤러에서 발생하는 예외에 맞게 처리할 수 있는 기능을 제공, 선언된 클래스 역시 스프링의 빈으로 처리됨
- @ControllerAdvice의 메소드들에는 특별히 @ExceptionHandler 라는 어노테이션 사용 가능
- @ExceptionHandler
- 전달되는 Exception 객체들을 지정, 메소드의 파라미터에서는 이를 이용할 수 있음
- 404 에러 페이지와 @ResponseStatus
- 서버 내부 문제가 아닌 시작부터 잘못된 URL 호출 시 404(Not Found) 예외가 발생
- @ControllerAdvice에 작성하는 메소드에 @ResponseStatus 이용 시 상태에 맞는 화면을 별도로 작성 가능
@ExceptionHandler(NonHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public String notFound() {
return "custom404";
}
4. 스프링 Web MVC 구현하기
(1) 프로젝트의 구현 목표와 준비
(2) MyBatis와 스프링을 이용한 영속 처리
- MyBatis를 이용하는 개발 단계
- VO 선언
- Mapper 인터페이스 개발
- XML 개발
- 테스트 코드의 개발
- Mapper 인터페이스와 XML
public interface TodoMapper {
String getTime();
void insert(TodoVO todoVO);
}
<mapper namespace="패키지경로.TodoMapper">
<select id="getTime" resultType="string">
select now()
</select>
<insert id="insert">
insert into tbl_todo (title, dueDate, writer) values ( #{title}, #{dueDate}, #{writer} )
</insert>
</mapper>
(3) CRUD / 페이징
- Service와 ServiceImpl 클래스
- Service 인터페이스를 먼저 추가, 이를 구현한 Impl을 빈으로 처리
- @Valid를 활용한 서버사이드 검증
- 스프링 MVC의 경우 @Valid와 BindingResult를 활용하여 처리
- hibernate-validate 라이브러리 필요 (7버전부터는 jakarta 패키지를 쓰므로 6.2.1.Final)
- 주로 DTO에 검증
- 주요 어노테이션
- @NotNull
- @Null
- @NotEmpty
- @NotBlank
- @Size(min=, max=)
- @Pattern(regex=)
- @Max(num)
- @Min(num)
- @Future
- @Past
- @Postitive
- @PositiveOrZero
- @Negative
- @NegativeOrZero
(4) 페이징 처리, DTO 및 서비스 계층
- 페이징을 위한 SQL (MySQL, MariaDB)
- limit를 통한 페이징 처리 -> limit (skip(건너뛰는 데이터 개수)), fetch(가져올 데이터 개수)
- limit의 단점 : limit 뒤에 식은 사용 불가, 값만 줄 수 있음
- count() : 페이징 처리를 하기 위해 전체 데이터의 갯수 필요
select * from tbl_todo order by tno desc limit 10, 10;
select * from tbl_todo order by tno desc limit 10;
(5) 검색 / 필터링 조건 정의
- MyBatis 실행 시 쿼리를 만들 수 있는 태그들
- if : 조건문
- trim(where, set)
- choose(when, otherwise)
- foreach : 반복 처리를 위해 제공 (컬렉션 계열, 배열 대상)
<foreach>
, <if>
<select id="selectList" resultType="~~~~.TodoVO">
select * from tbl_todo
<foreach collection="types" item="type" open="(" close=")" separator=" OR ">
<if test="type == 't'.toString()">
title like concat('%', #{keyword}, '%')
</if>
<if test="type == 'w'.toString()">
writer like concat('%', #{keyword}, '%')
</if>
</foreach>
order by tno desc limit #{skip}, #{size}
</select>
select * from tbl_todo
(
title like concat('%', ?, '%')
OR
writer like concat('%', ?, '%')
)
order by tno desc limit ?,?
<where>
, <trim>
: <if>
와 결합하여 조건에 따라 where절 추가, 쿼리에 특정 문자 추가 (and)
<select id="selectList" resultType="~~~~.TodoVO">
select * from tbl_todo
<where>
<if test="types != null and types.length > 0">
<foreach collection="types" item="type" open="(" close=")" separator=" OR ">
<if test="type == 't'.toString()">
title like concat('%', #{keyword}, '%')
</if>
<if test="type == 'w'.toString()">
writer like concat('%', #{keyword}, '%')
</if>
</foreach>
</if>
<if test="finished">
<trim prefix="and">
finished = 1
</trim>
</if>
<if test="from != null and to != null">
<trim prefix="and">
dueDate between #{from} and #{to}
</trim>
</if>
</where>
order by tno desc limit #{skip}, #{size}
</select>
<sql>
, <include>
: 동적 쿼리 부분 분할 및 도입용
<sql id="search">
<where>
<if test="types != null and types.length > 0">
<foreach collection="types" item="type" open="(" close=")" separator=" OR ">
<if test="type == 't'.toString()">
title like concat('%', #{keyword}, '%')
</if>
<if test="type == 'w'.toString()">
writer like concat('%', #{keyword}, '%')
</if>
</foreach>
</if>
<if test="finished">
<trim prefix="and">
finished = 1
</trim>
</if>
<if test="from != null and to != null">
<trim prefix="and">
dueDate between #{from} and #{to}
</trim>
</if>
</where>
</sql>
<select id="selectList" resultType="~~~~.TodoVO">
select * from tbl_todo
<include refid="search"></include>
order by tno desc limit #{skip}, #{size}
</select>