TIL_20221125_스프링과 스프링 Web MVC

창고·2022년 11월 25일
0

서적 : 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>
  • root-context.xml 수정
<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); // JSP에서 ${dto}와 같은 이름의 변수로 처리 가능
}
  • 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"; // custom404.jsp 호출
}

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>  
profile
공부했던 내용들을 모아둔 창고입니다.

0개의 댓글