Lee's Grow up

[Spring] Spring Project 생성, xml 없이 java로 설정하기 (web.xml servlet-context.xml root-context.xml 제거 ) 본문

Spring/Spring

[Spring] Spring Project 생성, xml 없이 java로 설정하기 (web.xml servlet-context.xml root-context.xml 제거 )

효기로그 2020. 3. 1. 02:02
반응형

해당 내용은 코드로 배우는 스프링 웹 프로젝트 라는 책의 내용을 기반으로 작성된 내용입니다.

Spring 프로젝트 생성

  • 시작은 'Spring Legacy Project'로 프로젝트를 하나 생성해줍니다.
    그럼 아래 그림과 같이 프로젝트 구조가 생성되는데, 이중 .xml로 설정된 파일을 제거하기 위해
    web.xml을 삭제하고 servlet-context.xml과 root-context.xml을 포함하고 있는 spring 폴더 자체를 삭제해줍니다.

  • 삭제 후 pom.xml에 에러 표시가 발생합니다. 이는 기존 웹 프로젝트들이 web.xml을 사용하는 것을 기반으로 설정했기 때문입니다. 에러를 제거하기 위해 pom.xml의 의 아래와 같은 설정을 추가

    <plugins>
      <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-war-plugin</artifactId>
          <version>3.2.3</version>
            <configuration>
              <failOnMissingWebXml>false</failOnMissingWebXml>
          </configuration>
      </plugin>
    ...
    <plugins>
  • 마지막으로 pom.xml에 java 관련 버전 설정을 1.7로 되어있는 데 사용하는 버전으로 변경 ( properties 와 maven-compiler-plugin ) 설정을 마무리하고 프로젝트 우클릭 -> maven -> update project 실행

위 과정을 마치면 일단 오류는 사라집니다. 이제 남은 과정은 삭제한 xml파일의 설정 값들을 java Class로 추가하는 작업입니다.

root-context.xml 대체하기

root-context.xml 이란 ?
Spring는 Context라는 영역을 만들어서 거기서 bean들을 관리해주며, 여러 개의 Context를 가질 수 있고 servlet-context 다음 root-context라는 계층형 구조를 가지고 있습니다. 이중 root-context 영역에 대한 설정을 담당하는 파일이 root-context.xml 입니다.


또한 계층 구조상 root-context는 servlet-context 인스턴스에서 공유해야 하는 데이터 저장소 및 비지니스 서비스와 같은 인프라 Bean을 등록하게 됩니다. 쉽게 말해 datasources와 같이 공통 사용 빈 또는 설정할 때 사용하게 됩니다. 참조


그럼 서론은 여기까지 하고, 아래와 같이 적당한 패키지와 클래스 이름을 정해서 클래스를 하나 정의해줍니다.

package config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = {"com.ljh.study"})
public class RootConfig { }

@Configuration 어노테이션은 해당 클래스를 설정 클래스라고 알려주는 어노테이션입니다.
@ComponentScan 어노테이션은 기존 xml에 사용하던 것과 동일하게 @Componet로 선언된 클래스들을 Bean으로 인식해 Context 영역에 객체를 생성하도록 해주는 어노테이션 설정입니다.

RootConfig Test 작성

방금 만들어진 RootConfig 클래스가 정상적으로 동작하는지 테스트하기 위해 하나의 클래스를 만들어줍니다.

package com.ljh.study.sample;
import org.springframework.stereotype.Component;

@Component
public class Sample { }

@Component라는 어노테이션을 통해 Spring Context가 관리해야 되는 객체라고 선언합니다.
여기까지 완료되었으면, 이제 테스트 코드를 작성합니다. 테스트는 원하는 버전을 선택합니다. 저의 경우 Junit5를 사용하였습니다.

@DisplayName("Root Context 테스트")
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = RootConfig.class)
public class RootConfigTest {

    private Sample sample;

    @Autowired
    public RootConfigTest(Sample sample) {
        this.sample = sample;
    }

    @Test
    @DisplayName("Root Context Component scan 확인")
    void rootContextComponentScanTest() {
        assertNotNull(sample);
    }
}

RootConfig 클래스를 통해 정상적으로 Bean들이 등록이 되었다면 @Autowired가 동작을 하고 객체에 의존성을 주입받을 것입니다.
실행을 하면 아래와 같이 테스트가 통과하는 걸 확인할 수 있습니다.

web.xml 대체하기

이제 web.xml을 대신할 클래스를 아래와 같이 작성합니다.

package config;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class WebConfig extends AbstractAnnotationConfigDispatcherServletInitializer{

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[] {RootConfig.class};
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return null;
    }

    @Override
    protected String[] getServletMappings() {
        return null;
    }
}

WebApplicationInitializer를 상속받아 구현하는 방법도 존재하지만 해당 클래스의 구현체인 AbstractAnnotationConfigDispatcherServletInitializer를 상속받아 좀 더 간편하게 구현이 가능합니다.

이제 Run As -> Run On Server로 실제 동작을 해보면 콘솔에 아래와 같은 로그가 찍히면서 WebConfig 클래스의 설정이 적용이 되었는지 확인할 수 있습니다.

INFO : org.springframework.web.context.ContextLoader - Root WebApplicationContext: initialization started
INFO : org.springframework.web.context.ContextLoader - Root WebApplicationContext initialized in 531 ms
INFO : org.springframework.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcher'
INFO : org.springframework.web.servlet.DispatcherServlet - Completed initialization in 219 ms

만약 해당 로그가 뜨지 않는다면 정상적으로 설정이 되지 않은 것이니 다시 확인하셔야 합니다.

DB 연결하기

  • 해당 예제에서는 DB로 Oracle 11g를 사용합니다.
  • 오라클 버전에 맞는 JDBC 스펙을 Oracle 홈페이지에서 확인 후 프로젝트에 추가 해줍니다.

위와 같이 사전 준비가 완료되었다면, 이제 DB에 정상적으로 접속이 되는지 테스트 코드를 작성해봅니다.

@Log4j
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = RootConfig.class)
@DisplayName("DB 관련 테스트")
public class DBTest {

    @Test
    @DisplayName("JDBC 연결 테스트")
    void jdbcTest() {
        try {
            DriverManager.registerDriver(new oracle.jdbc.driver.OracleDriver());
            try (Connection connection = 
                    DriverManager.getConnection(
                              "jdbc:oracle:thin:@localhost:1521:orcl"
                            , "study"
                            , "study")) {
                log.info(connection);
            }
        } catch (SQLException e) {
            log.error(e.getMessage());
        }
    }

위와 같이 각각 DB에 맞는 드라이버를 등록 후 접속을 테스트 해보는 간단한 테스트입니다.
정상적으로 동작한다면 Log4j를 통해 출력된 connection 객체의 정보가 출력될 것 입니다.

Connection Pool 사용

이제 DB랑 접속이 되는지 확인이 되었으니, 커넥션 풀을 설정할 차례입니다. 예제에서는 HikariCP를 사용합니다.
우선 HikaraCP를 사용하기 위해 pom.xml에 아래와 같이 의존성을 추가해줍니다.

<!-- https://mvnrepository.com/artifact/com.zaxxer/HikariCP -->
<dependency>
      <groupId>com.zaxxer</groupId>
      <artifactId>HikariCP</artifactId>
      <version>3.4.2</version>
</dependency>

그 후 기존에 작성된 RootConfig클래스에 다음과 같이 Bean을 추가해줍니다.

@Configuration
@ComponentScan(basePackages = {"com.ljh.study"})
public class RootConfig { 

    @Bean
    public DataSource dataSource() {
        HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setDriverClassName("oracle.jdbc.OracleDriver");
        hikariConfig.setJdbcUrl("jdbc:oracle:thin:@localhost:1521:orcl");
        hikariConfig.setUsername("study");
        hikariConfig.setPassword("study");

        return new HikariDataSource(hikariConfig);
    }
}

위와 같이 root-context를 수정해서 DataSource를 HikariDataSource로 설정해줍니다.
이런 설정을 통해 dataSource라는 빈에 hikariConfig를 참조시키는 설정을 진행하게 됩니다.

자 이제 당연하게 HikariCP가 정상적으로 적용이 되었는지 Test를 작성해줍니다.

@Log4j
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = RootConfig.class)
@DisplayName("DB 관련 테스트")
public class DBTest {

    private DataSource dataSource;

    @Autowired
    public DBTest(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Test
    @DisplayName("Hikari 설정 테스트")
    void datasourceTest() {
        try(Connection connection = dataSource.getConnection()){
            log.info(connection);
        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }
}

간단하게 위와 같이 테스트 코드를 작성하고 실행해보면 Test코드가 실행하면서 Spring가 Bean으로 HikariCP를 통해 커넥션을 사용하는 로그가 찍히는지 확인해봅니다.

INFO : com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
INFO : com.zaxxer.hikari.pool.PoolBase - HikariPool-1 - Driver does not support get/set network timeout for connections. (oracle.jdbc.driver.T4CConnection.getNetworkTimeout()I)
INFO : com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
....

Mybatis 연동

Mybatis를 사용하기 위해 아래의 의존성들을 추가해줍니다.

<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.4</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>2.0.3</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>${org.springframework-version}</version>
</dependency>

추가로 spring-tx가 없으신분들은 추가로 의존성을 작성해줍니다.

이제 사용하기 위한 사전 준비는 끝났고, Mybatis에서 사용하는 클래스들중 SQLSessionSQLSessionFactory를 사용하기 위해 스프링에 Bean으로 등록해주는 작업을 해줍니다.
기존에 작성된 RootConfig클래스에 아래와 같이 Bean을 추가로 작성해줍니다.

@Bean
public SqlSessionFactory sessionFactory() throws Exception {
    SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
    sessionFactoryBean.setDataSource(dataSource());
    return sessionFactoryBean.getObject();
}

두 객체에 대해서 간략하게 설명하자면 SQLSessionFactory는 이름처럼 SQLSession 객체를 생성해주는 역할을 하는 객체입니다.
SQLSession은 해당 객체를 통해 Connection을 생성하거나 원하는 SQL문을 전달하고, 결과를 리턴 받는 역할을 합니다.

이제 해당 Bean이 정상적으로 Spring에 등록이 되었는지 테스트해보는 테스트 코드를 작성합니다. 기존에 작성된 DBTest 클래스에 추가로 아래와 같이 작성을 해줍니다.

@Test
@DisplayName("myBatis 연결 테스트")
void myBatisTest() {
    try(SqlSession session = sqlSessionFactory.openSession();
        Connection connection = session.getConnection();){
        log.info(session);
        log.info(connection);
    } catch (SQLException e) {
        log.error(e.getMessage());
    }
}

실행 후 아래와 같은 결과가 나오는지 확인해봅니다.

INFO : config.DataSourceTest - org.apache.ibatis.session.defaults.DefaultSqlSession@5a9d6f02 INFO : config.DataSourceTest - HikariProxyConnection@1839168128 wrapping oracle.jdbc.driver.T4CConnection@376a0d86 

여기까지 동작이 완료되었다면, Mybatis까지 연동이 성공했습니다. 이제 실제 소스에 어떻게 사용하는지 사용방법에 대해서 소개를 하겠습니다.

스프링과 Mybatis 연동처리

스프링과 Mybatis에서 SQL문을 작성할 때 Mapper을 사용합니다. Mapper은 xml로 작성할 수도, 어노테이션으로 사용할 수 도 있습니다. 예제에선 두가지 방법 모두 사용해보겠습니다.

우선 기본 구조는 Controller -> Service -> Mapper 인데 예제에선 Controller과 Service를 패스하고 바로 Mapper만 작성해보겠습니다.
Mapper은 클래스가 아닌 인터페이스로 아래와 같이 생성해줍니다.

package com.ljh.study.mapper;
import org.apache.ibatis.annotations.Select;

public interface TimeMapper {

    @Select("select sysdate from dual")
    public String getTime();

    public String getTimeForMapperXml();
}

우선 @Select어노테이션을 사용한 메소드가 어노테이션을 이용한 쿼리 작성방법이고, 아래 getTimeForMapperXml() 메소드가 xml을 이용한 방식입니다. 해당 방식은 메소드의 이름과, 리턴타입을 선언해주면 해당 Mapper 인터페이스에서 작성은 끝입니다.

여기까지가 Mapper 인터페이스 작성이였고, 기존의 Spring가 Component를 인식하기 위해 ComponentScan을 사용했던것과 비슷하게 Mybatis도 MapperScan을 제공해준다. 그러면 Mybatis와 관련된 어노테이션들(@Select 등)을 인식하고 등록하게 됩니다.
작성은 기존에 작성한 RootConfig 클래스에 어노테이션을 추가해줍니다.

@Configuration
@ComponentScan(basePackages = {"com.ljh.study"})
@MapperScan(basePackages = {"com.ljh.study.mapper"})
public class RootConfig { 
    ....
}

이제 Mapper 인터페이스 + xml을 같이 사용 하는 방법은 src/main/resources 아래 적당하게 패키지를 구성해주고 패키지 안에 TimeMapper.xml을 생성해주고 아래와 같이 작성해줍니다.

<?xml version="1.0" encoding="UTF-8" ?> 
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.ljh.study.mapper.TimeMapper">
    <select id="getTimeForMapperXml" resultType="String">
        select sysdate from dual
    </select>
</mapper>

여기서 중요한 점은 우리가 생성한 Mapper 인터페이스를 namespace에 연결을 해줍니다. 그리고 우리는 셀렉트문을 사용하기 때문에 select id을 통해 해당 메소드와 연결을 시켜줍니다. 또한 파라미터를 받을 수도, 결과를 리스트로 반환할 수도 여러가지 방법이 존재하니 해당 방법에 대해서 모르신다면, 따로 검색을 해보시는걸 추천합니다.

이제 다왔습니다. 해당 Mapper가 적상적으로 동작하는지 테스트를 아래와 같이 작성해줍니다.

@DisplayName("Mapper 관련 테스트")
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = RootConfig.class)
@Log4j
public class TimeMapperTest {

    private TimeMapper timeMapper;

    @Autowired
    public TimeMapperTest(TimeMapper timeMapper) {
        this.timeMapper = timeMapper;
    }

    @Test
    public void testGetTIme() {
        log.info(timeMapper.getTime());
    }

    @Test
    public void getTimeForMapperXmlTest() {
        log.info(timeMapper.getTimeForMapperXml());
    }
}

해당 테스트를 실행 후, log가 정상적으로 출력이 되면, 이제 기본 프로젝트 생성과 Mybatis 3를 통한 DB 접속 연결까지 완료가 되었습니다.

반응형
Comments