토이 프로젝트/익명 채팅 사이트

프로젝트 진행 ⑩ : Spring Framework 적용 (Java 설정 파일 + Spring JDBC)

OhPro 2022. 9. 6. 22:53
반응형

목차

  • 시작하며
  • 스프링 설정 파일
  • Dao(Repository)
  • 마치며

 


시작하며

이전 두 개의 포스팅에서 트랜잭션에 대한 것들로 고민이 참 많았다. 이런저런 고민을 계속하는데 문득 "일이 처리되는 과정을 모두 알고 구현할 줄 아는 것도 중요하지만, 이미 존재하는 좋은 기술을 습득하는 게 더 중요하지 않을까?"라는 생각이 들었다.

 

처음부터 하나씩 다 만들고 자동화할줄 알면 좋겠지만 그래서는 내가 점점 장인이 되어갈 것 같았다. 장인이라는 단어는 이전 회사에서 종종 사용하던 단어인데, 언 뜻 좋은 뜻으로 들릴 수 있으나 당시 사용했던 장인이라는 단어의 의미는 그렇지 않다.

 

장인은 개인적인 실력은 높지만 내가 만든 코드를 남들에게 이해시키거나 다른 사람의 코드를 0부터 100까지 모두 이해하지 못하면 소통할 수 없는 부류의 사람이라는 의미로 사용하던 말이다.

 

즉 내가 Spring 없이 시간과 노력으로 만들 수 있을지 모르나 나 혼자 고민하는 속도로는 오픈소스인 Spring Framework를 사용하는 사람들이 함께 고민하는 속도를 이길 수 없는데, Spring을 사용하지 않는 장인이 되어가는 느낌이 들어서 결론적으로는 현시점부로 Spring Framework를 사용하기로 했다!


스프링 설정 파일

개인적으로 xml을 사용한 설정파일보다 java config file을 사용한 설정 방식이 더 와닿았기 때문에 xml을 만들지 않고 java config file을 사용하여 설정 파일을 작성하기로 했다. (물론 xml도 할 줄 안다..!)

 

우선 web.xml에 디스패쳐 서블릿과 java config file을 설정해줬다.

 

<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

  <filter>
    <filter-name>httpMethodFilter</filter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>httpMethodFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <servlet>
    <servlet-name>mvc</servlet-name>
    <servlet-class> org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextClass</param-name>
      <param-value> org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </init-param>
    <init-param>
      <param-name> contextConfigLocation</param-name>
      <param-value> org.mytoypjt.config.WebMvcConfiguration</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>mvc</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>



</web-app>

 

<servlet>의 내부에다가 <init-param>를 사용해서 init단계에서 contextClasscontextLocation이 등록될 수 있도록 해줬고,

<servlet-mapping><url-pattern>으로 "/"를 사용하여 web.xml에서 url-pattern로 등록하지 않은 url 요청은 모두 디스패쳐 서블릿으로 들어올 수 있도록 설정함으로써 프론트 컨트롤러 설정을 했다.

 

web.xml의 상단에 HiddenHttpMethodFilter<filter>로 설정해주었는데 기본적으로 HTML <form>를 사용했을 때 get, post 요청밖에 보낼 수 없어서 추가적으로 put과 delete 메서드를 사용하기 위해 설정해주었다.

 

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"org.mytoypjt"})
@Import({DBConfig.class, PostConfig.class})
public class WebMvcConfiguration implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/css/**").addResourceLocations("/css/").setCachePeriod(31556926);
        registry.addResourceHandler("/js/**").addResourceLocations("/js/").setCachePeriod(31556926);
        registry.addResourceHandler("/icons/**").addResourceLocations("/icons/").setCachePeriod(31556926);
        registry.addResourceHandler("/pictures/**").addResourceLocations("/pictures/").setCachePeriod(31556926);
    }

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginCheckInterceptor())
                .excludePathPatterns("/register/page/{pageNo}")
                .excludePathPatterns("/account")
                .excludePathPatterns("/account/cert")
                .excludePathPatterns("/profile")
                .excludePathPatterns("/login/page")
                .excludePathPatterns("/login")
                .excludePathPatterns("/id/page")
                .excludePathPatterns("/id/cert")
                .excludePathPatterns("/pw/page/{pageNo}")
                .excludePathPatterns("/pw/cert")
                .excludePathPatterns("/pw")
                .excludePathPatterns("/");
        WebMvcConfigurer.super.addInterceptors(registry);
    }

    @Bean
    public InternalResourceViewResolver internalResourceViewResolver(){
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/views/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }


}

 

이제 본격적으로 java config class를 살펴볼 건데 설정 파일로 사용하기 위해 @Configuration 어노테이션을, Bean들을 읽어 들일 수 있도록 @ComponentScan 어노테이션을 사용했다.

 

@EnableWebMvc 어노테이션을 사용했는데 이를 사용하면 "Web에 필요한 빈들을 대부분 자동으로 설정해준다"라고 한다. 공식 문서와 여러 블로그를 읽으면서 이해를 시도하고 있으나, 내가 해당 어노테이션에 대한 이해도가 높지 않아 이번에는 다루지 않겠다.

 

그리고 addResourceHandlers() 메서드를 사용해서 정적 파일들에 대해 각 확장자 별 핸들러를 만들고 경로 설정과 캐시 설정을 해줬는데, 맨 처음에는 왜 해당 메서드를 오버라이딩해야 하는지와 어떻게 사용하는지 몰랐었다. 단지 정적 리소스를 처리할 때 사용한다고만 알고 있었기 때문이다.

 

보통 메서드 이름을 지을 때는 메서드의 이름만 보고도 어떤 메서드인지 알 수 있게 하기 위해 동사 + 명사로 짓는 게 국룰이라고 알고있어서 바로 그냥 메서드 이름을 아래처럼 해석해봤다. (개인적인 해석임)

 

addResourceHandlers -> add resource handlers -> "추가한다 리소스 핸들러들을" -> 리소스 핸들러들을 등록하는 메서드?

 

해석하고 대략적으로 어떤 메서드인지 인지하는 과정에서 옛날에 정적 리소스들의 경로들을 web.xml에 등록했었던 기억이 났다.

 

꽤 인상 깊었던 기억인데 이유는 맨 처음에 아무런 설정을 하지 않은 채 오로지 JSP에만 CSS, JS, 이미지 등을 프로젝트 디렉터리 경로에 맞게 등록하고 사용하려고 했을 때 자원을 불러오지 못했었기 때문이다. 결국 헤매다가 '정적 자원을 미리 서버에서 찾을 수 있도록 해두지 않는다면, 사용할 수도 없다는 것'을 깨닫고 서버로 정적 자원 경로라는 것을 web.xml에 등록했었다.

 

정적 자원을 찾을 수 있도록 만들지 않으면 사용할 수 조차 없는 이유는 만약 톰캣을 사용한다고 했을 때, 톰캣이 JSP를 컴파일하고 HTML로 만드는 과정에서 작성된 자원이 어디에 있는지 알지 못하기 때문인듯하다. (뇌피셜)

 

마찬가지로 아무런 설정을 해주지 않는다면, 톰캣이 JSP를 해석할 때 정적 자원을 해석할 수 없을 것이다. 그래서 스프링 컨테이너에 정적 리소스 요청이 왔을 때 이를 관리하도록 addResourceHandlers() 메서드의 내부에

 

registry.addResourceHandler("/css/**").addResourceLocations("/css/").setCachePeriod(31556926);

 

과 같이 등록해주는 것이다.

 

 

그런데 정적 리소스와 관련해서 설정해야 하는 것이 하나 더 있다. 그건 바로

 

@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    configurer.enable();
}

 

이 부분이다. 이와 관련된 부분에 대해 공식 문서(여기)를 보면 이런 내용이 있다.

 

Spring MVC allows for mapping the DispatcherServlet to / (thus overriding the mapping of the container’s default Servlet), while still allowing static resource requests to be handled by the container’s default Servlet. It configures a 
DefaultServletHttpRequestHandler with a URL mapping of /** and the lowest priority relative to other URL mappings.

This handler forwards all requests to the default Servlet. Therefore, it must remain last in the order of all other URL HandlerMappings. That is the case if you use <mvc:annotation-driven>. Alternatively, if you set up your own customized HandlerMapping instance, be sure to set its order property to a value lower than that of the DefaultServletHttpRequestHandler, which is Integer.MAX_VALUE.

 

이걸 번역기를 써서 번역하면,

 

스프링 MVC는 컨테이너의 기본 서블릿에 대한 디스패처 서블릿의 매핑을 / (따라서 컨테이너의 기본 서블릿의 매핑을 재정의함)하는 동시에 정적 리소스 요청을 컨테이너의 기본 서블릿에 의해 처리할 수 있도록 허용합니다.

이 처리기는 모든 요청을 기본 서블릿으로 전달합니다. 따라서 다른 모든 URL 핸들러 매핑의 순서로 맨 마지막에 남아 있어야 합니다. <mvc:notation-driven>을 사용하는 경우입니다. 또는 사용자 정의된 핸들러 매핑 인스턴스를 설정하는 경우 해당 순서 속성을 DefaultServletHttpRequestHandler의 값(정수) 보다 낮은 값으로 설정해야 합니다.MAX_VALUE.

 

따라서 공식 문서의 설명을 참고해서 보면, xml 설정 파일을 사용했을 경우에 <mvc:notation-driven>을 사용하거나 나처럼 Java Config에서 enableDefaultServletHandling(DefaultServletHandlerConfigurer configurer)를 오버라이딩해서 enable() 메서드를 실행해주면 DefaultServletHttpRequestHandler를 사용하게 되는 것이다.

 

만약 DefaultServletHttpRequestHandler를 사용하지 않는 상태인 경우 핸들러(컨트롤러)가 등록되지 않은 요청을 수신한 경우 404 에러가 발생하게 되지만, 등록을 했다면 정적 리소스 요청과 같은 등록되지 않은 요청을 수신한 경우 핸들러 탐색의 마지막 과정에서 요청이 DefaultServletRequestHandler로 들어가게 된다.

 

그리고 DefaultServletRequestHandler로 전달된 요청이 CSS와 같은 정적 리소스 요청이었을 경우에 위에서 등록한 정적 리소스 핸들러를 사용하여 리소스를 적용할 수 있도록 해주는 것 같다.

 

 

설정 파일의 아래를 보면 인터셉터 설정과 뷰 리졸버 설정을 했는데, 인터셉터에 대해서는 다음번에 자세히 다루도록 하겠다.

 


 

Dao (Repository)

기존에는 Dao에서 DB 연결 및 쿼리 사용을 위해 아래처럼 JDBC 인터페이스를 직접 사용했었다. 

 

    public static void executeTestSql() throws SQLException {
        String sql = "SELECT * FROM post WHERE post_no=?, nicname=?, account_no=?";
        Connection conn = DBUtil.getBasicDataSource().getConnection();

        try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
            conn.setAutoCommit(false);

            pstmt.setInt(1, 12);
            pstmt.setString(2, "김철수");
            pstmt.setInt(3, 4);
            ResultSet rs = pstmt.executeQuery();
            while (rs.next()) {
                // 결과 처리문
            }

        } catch(Exception e) {
            e.printStackTrace();
            conn.rollback();
        }
    }

 

이해를 돕기 위해 간단히 설명하자면 일단 DBUtil.getBasicDataSource().getConnection()은 단순히 커넥션을 받아오기 위한 과정일 뿐 여기서 이야기하고자 하는 내용은 아니다. 

 

위 코드를 통해 이야기하고자 하는 건 하나의 SQL문이 있고 JDBC만을 사용했을 때는 반드시 트랜잭션도 고려되어야 한다는 것이다. 때문에 나는 try문의 내부에 conn.setAutoCommit()conn.rollback()과 같은 트랜잭션 처리를 위한 메서드들도 추가했었다.

 

이렇게 작성된 코드를 그냥 보면 "그냥 JDBC를 사용해서 트랜잭션을 처리했구나"  정도로 볼 수도 있다. 실제로 나도 구글에 검색해서 저런 식으로 작성된 메서드를 보고 "아 그냥 저렇게 하면 되는구나"라고 받아들였었기 때문이다.

 

그런데 사실 서비스 객체에서 여러 개의 Dao를 사용하거나 한 개의 Dao를 사용해 여러 번의 쿼리를 날려야 할 때가 무조건 있다. 그럴 때 만약 JDBC를 직접 사용해야 한다고 하면 트랜잭션을 처리할 때 조금 귀찮은 작업들이 생긴다. 예를 들어 트랜잭션은 하나의 커넥션을 사용해야 하기 때문에 해당 Dao를 사용하는 서비스 객체에서 Dao에 Connection객체를 전달해줘야 하는 등의 작업이 있는 것이다.

 

이전에 나는 이런 작업을 처리하기 위해 트랜잭션을 처리하기 위한 하나의 어노테이션을 만들고 해당 어노테이션이 사용될 클래스를 미리 프록시로 만들어서 원래 사용하던 클래스를 선언하던 자리에 프록시 클래스를 선언해주는 식으로 사용했었다. (자세하게는 이전 포스팅에서 언급했었음)

 

그렇게 함으로써 나름 트랜잭션 기술을 사용해야 하는 관심사와 쿼리를 실행해서 데이터를 얻거나 하는 작업의 관심사는 분리는 되었다고 생각했다. 왜냐면 프록시를 통해 트랜잭션과 관련된 처리를 했으므로 트랜잭션 처리를 위한 메서드를 사용하지 않고 작업을 진행할 수 있었기 때문이다.

 

하지만 서비스 객체에서 Dao로 Connection을 전달하여 작업을 처리시켜야 하는 문제는 끝내 해결하진 못했다. 또한 사실 쿼리를 실행하고 데이터를 받아오는 관심사와 Connection을 직접 닫아주고 관리하는 관심사도 아직 분리되지 않은 상태였다.

 

그래서 스프링에서는 어떤식으로 처리하는지 벤치마킹하려고 구글링도 해보고 내가 찾아본 자료가 맞는지 스프링 공식문서를 찾아보기도 했다. 그렇게 찾아본 바로는 스프링은 PlatformTransactionManager라는 인터페이스를 사용해서 각 환경에 알맞게 트랜잭션을 처리할 수 있도록 추상화를 했으며 트랜잭션 동기화 기술을 통해 트랜잭션을 위한 커넥션을 만들어서 처리한다는 것을 알게 되었다.

 

만약 이런 기술을 내가 직접 구현했었다면 개인적인 성장에 많은 도움이 되었겠지만, 일단은 벤치마킹하며 나 자신에 갇히지 말자라는 생각을 동력 삼아 스프링을 이 시점에 적용해서 사용하며 기대 보기로 했다.

 

 

그러기 위해 나는 우선적으로 스프링을 통해 DB에 연결하기 위한 DBConfig 클래스를 아래와 같이 만들었고, 이 클래스를 위에서 봤던 전체 설정 클래스에 @Import 어노테이션을 사용하여 등록해주는 식으로 사용했다.

 

@Configuration
@EnableTransactionManagement
public class DBConfig {
    private static String url = "DB 주소";
    private static String id="아이디";
    private static String pw="비밀번호";

    @Bean
    public BasicDataSource dataSource() throws ClassNotFoundException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        BasicDataSource dataSource = new BasicDataSource();

        dataSource.setUsername(id);
        dataSource.setPassword(pw);
        dataSource.setUrl(url);

        dataSource.setMinIdle(10);
        dataSource.setMaxTotal(50);
        dataSource.setMaxOpenPreparedStatements(100);

        dataSource.setTestWhileIdle(true);
        dataSource.setTimeBetweenEvictionRunsMillis(1000L * 60L * 1L);

        return dataSource;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }
}

 

위 코드를 보면 크게 DataSource, PlatformTransactionManager의 두 클래스를 빈으로 등록해주는 모습을 볼 수 있다.

 

먼저 DataSource는 아파치 commons 라이브러리를 사용한 커넥션 풀(dbcp)을 등록해주기 위해 커넥션 풀의 규모 및 여러 설정을 한 후에 빈으로 등록하고 있다.

 

또한 PlatformTransactionManager는 리턴 값으로 new DataSourceTransactionManager()를 리턴하는데 이는 JDBC기반의 트랜잭션 처리를 위해 PlatformTransactionManager를 상속받은 DataSourceTransactionManager를 빈으로 등록해주는 것이다.

 

그리고 이렇게 스프링을 사용하는 김에 스프링 JDBC도 적용해보기로 했다. 스프링 JDBC를 사용할 때는 JdbcTemplete이란 걸 사용한다고 하는데 이래저래 찾아보니 JdbcTemplete은 템플릿 메서드 패턴을 사용해서 기존 JDBC 인터페이스를 더욱 간편하게 사용할 수 있게 해주는 SQL Mapper라고 한다.

 

이 JdbcTemplete을 사용했더니 아래처럼 코드가 짧고 간결해졌다.

 

public boolean createPost(Post post) {
    String sql = "INSERT INTO post values (~~~)"
    SqlParameterSource param = new BeanPropertySqlParameterSource(post);
    return jdbcTemplete.update(sql, param);
}

 

확실히 변환하는 과정에서 많은 코드들이 지워지는 약간 홍해의 기적과 같은 느낌을 맛봤다.... 이래서 검증되고 편리한 것들을 사용하는 건가 싶은 생각을 엄청나게 했다...!

 

아마 위의 코드를 기존 방법대로 사용했다면 아마도 아래처럼 작성되었을 메서드였다.

 

    public boolean createPost(Post post) throws SQLException {
        String sql = "INSERT INTO post VALUES (~~~)";
        Connection conn = DBUtil.getBasicDataSource().getConnection();

        try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
            conn.setAutoCommit(false);

            pstmt.setInt(1, 12);
            pstmt.setString(2, "김철수");
            pstmt.setInt(3, 4);

            pstmt.executeUpdate();
            return true;

        } catch(Exception e) {
            e.printStackTrace();
            conn.rollback();
            return false;
        }
    }

 

이런 메서드가 몇십 개는 있었는데 이렇게 JdbcTemplete을 적용한 코드로 전부 바꾸고 나니까 정말.. 좋았다..^^

 


 

마치며

이번에는 스프링 프레임워크를 적용하게 된 이유와 내가 어떤 식으로 작성했는지 포스팅했는데, 아마 다음번 포스팅에는 컨트롤러와 서비스 객체의 변화라던가 아니면 지금 필터나 인터셉터를 처리해야 할 필요성을 느끼고 있는 만큼 관련 내용을 포스팅하지 않을까 싶다. 

 

감사합니다!!

반응형