1. @RequestMapping, @GetMapping, @PostMapping

@DeleteMapping, @PutMapping ... 별거없으므로 설명하지않겠다.

@Controller
@RequestMapping("/survey")
public class SurveyController {

    @GetMapping
    public String form() { ... }

    @PostMapping
    public String submit(@ModelAttribute("ansData") AnsweredData data) { ... }
}

2. 리다이렉트

/join 접속시 /register/step1 로 redirect 하는 예제

@Controller
public class RegisterController {

    @GetMapping("/join")
    public String redirectToStep1() {
        return "redirect:/register/step1";
    }

    @GetMapping("/register/step1")
    public String register() {
    }
}

 

"redirect: (경로)" 를 리턴하면 됨.

 

이 때 (경로)가 "/"로 시작할 경우 웹 애플리케이션 경로(context path)를 기준으로 이동경로를 생성함.

 

* 웹 애플리케이션과 Context Path 란? *

dololak.tistory.com/155

 

3. 요청 파라미터 접근

HTML form 태그로부터 오는 요청을 받는 방법

3.1 HttpServletRequest 직접 이용

@Controller
@RequestMapping("/register")
public class JoinController {

    @PostMapping("/step2")
    public String handleStep2(HttpServletRequest request) {
        String agreeParam = request.getParameter("agree");
        
        if (Objects.isNull(agreeParam) || "true".equals(agreeParam)) {
            return "join/writeinfo";
        }
        return "join/agree";
    }
}

3.2 @RequestParam 어노테이션 이용

@Controller
@RequestMapping("/register")
public class RegisterController {

    @PostMapping("/step2")
    public String handleStep2(@RequestParam(required="true" defaultValue=false) Boolean agree) {
        if (!agreeValue) {
            return "register/step1";
        }
        return "register/step2";
    }
}
@Controller
@RequestMapping("/register")
public class RegisterController {

    @PostMapping("/step2")
    public String handleStep2(@RequestParam(value="agree" required="true" defaultValue=false) Boolean agreeValue) {
        if (!agreeValue) {
            return "register/step1";
        }
        return "register/step2";
    }
}

 

* @RequestParam 속성

value

HTTP request 파라미터 이름 지정

디폴트는 파라미터 변수 이름

 

required

required=true 인 파라미터에 들어가는 값이 없으면 exception 발생

 

defaultValue

파라미터에 들어가는 값이 없을 때 default로 집어넣을 값 설정

 

3.3 커맨드 객체를 이용해서 요청 파라미터 사용하기

@Controller
@RequestMapping("/register")
public class RegisterController {

    @PostMapping("/step1")
    public String agree(AgreeRequest agreeRequest) {
        if (!agreeRequest.isAgree()) {
            return "register/step1";
        }
        return "register/step2";
    }
}

앞서 HttpServletRequest 혹은 @RequestParam 으로 요청 파라미터를 받는 방식은 파라미터가 많아질 경우 유지보수적으로 안좋아지는데 이 때 커멘드 객체를 이용해서 요청 파라미터를 받으면 된다.

 

4. 커멘드 객체를 뷰(JSP)에서 사용하기

스프링 MVC 는 커맨드 객체의(첫 글자를 소문자로 바꾼) 클래스 이름과 동일한 속성 이름을 사용해서 커맨드 객체를 뷰에 전달한다.

 

@ModelAttribute 어노테이션으로 커맨드 객체 속성 이름 변경

@Controller
@RequestMapping("/register")
public class RegisterController {
    
    @PostMapping("/step1")
    public String agree(AgreeRequest agreeRequest) {
        if (!agreeRequest.isAgree()) {
            return "register/step1";
        }
        return "register/step2";
    }
}

 

WEB-INF/view/register/step2.jsp

<%@ page contentType="text/html; charset=utf-8" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html>
<head>
    <title>회원가입</title>
</head>
<body>
    <p>약관 동의 결과 : ${agreeRequest.agree}</p>    <!-- true -->
</body>
</html>

5 커맨드 객체와 스프링 폼 연동

스프링 MVC는 <form:form>, <form:input> 등,

HTML 폼과 커맨드 객체를 연동하기 위한 JSP 태그 라이브러리를 제공한다.

 

* 회원가입 입력 오류시 이메일과 이름을 이전값으로 채워넣는 예제

1. <form:form> 사용 전

<%@ page contentType="text/html; charset=utf-8" %>
<!DOCTYPE html>
<html>
<head>
    <title>회원가입</title>
</head>
<body>
    <h2>회원 정보 입력</h2>
    <form action="step3" method="post">
    <p>
        <label>이메일:<br>
        <input type="text" name="email" value="${registerRequest.email}"/>
        </label>
    </p>
    <p>
        <label>이름:<br>
        <input type="text" name="name" value="${registerRequest.name}"/>
        </label>
    </p>
    <p>
        <label>비밀번호:<br>
        <input type="password" name="password"/>
        </label>
    </p>
    <p>
        <label>비밀번호 확인:<br>
            <input type="password" name="confirmPassword"/>
        </label>
    </p>
    <input type="submit" value="가입 완료">
    </form>
</body>
</html>

2. <form:form> 사용

<%@ page contentType="text/html; charset=utf-8" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html>
<head>
    <title>회원가입</title>
</head>
<body>
    <h2>회원 정보 입력</h2>
    <form:form action="step3" method="post" modelAttribute="registerRequest">
    <p>
        <label>이메일:<br>
        <form:input path="email"/>
        </label>
    </p>
    <p>
        <label>이름:<br>
        <form:input path="name"/>
        </label>
    </p>
    <p>
        <label>비밀번호:<br>
        <form:password path="password"/>
        </label>
    </p>
    <p>
        <label>비밀번호 확인:<br>
            <form:password path="confirmPassword"/>
        </label>
    </p>
    <input type="submit" value="가입 완료">
    </form:form>
</body>
</html>

 

DispatcherServlet 은 컨트롤러의 실행 결과를 HandlerAdapter를 통해서 ModelAndView 형태로 받는다.

그런 다음 DispatcherServlet은 ViewResolver에게 View 객체를 요청하고, 리턴된 View 객체에게 응답 생성을 요청한다.

바로 이 때, 즉 DispatcherServlet이 View객체에게 응답 생성을 요청할 때

Model에 담긴 값을 Map 형식으로 전달한다.

 

이 예제의 경우 key가 "registerRequest" 이고 값이 RegisterRequest 객체인 요소가 Map 에 들어있는 것이다. 이 Map이 View에게 전달되는 것이다.

 

그런데 <form:form ... modelAtribute="(attributeName)"> 에서 (attributeName) 을 key로 갖는 객체가 이 Map에 없으면 Internal Server Error 가 발생한다. <form:form> 을 사용할때는 이에 대응하는 modelAttribute 가 반드시 존재해야한다.

 

6 컨트롤러 구현 없는 경로 매핑

<p><a href="<c:url value='/main'/>">[첫화면으로 이동]</a></p>
@Controller
public class MainController {
    @RequestMapping("/main")
    public String main() {
        return "main";
    }
}

위 컨트롤러 코드는 요청 경로와 뷰 이름을 연결해주는것에 불과하다.

이런 경우 다음과 같이 WebMvc 설정코드로 대체할 수 있다.

package config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {

	...

	@Override
	public void addViewControllers(ViewControllerRegistry registry) {
		registry.addViewController("/main")
			.setViewName("main");
	}
}

 

 

커맨드 객체 : 중첩, 컬렉션 프로퍼티

.

 

Model 을 통해 컨트롤러에서 뷰에 데이터 전달하기

.

 


웹 애플리케이션 경로(context path)

dololak.tistory.com/155

1. 서블릿, 서블릿 컨테이너, Dispatch Servlet

서블릿

  • javax.servlet.Servlet 인터페이스의 구현체. 해당 인터페이스를 구현한 다음 web.xml 에 해당 구현체를 서블릿으로 등록하면, 서블릿 컨테이너가 이를 바탕으로 서블릿 객체를 생성하여 사용한다.
  • 서블릿 컨테이너는 서블릿 객체 각각을 한개씩 생성한다.

서블릿 컨테이너

  • 아파치톰캣이 서블릿 컨테이너에 해당된다.

 

서블릿 컨테이너의 역할

  • 웹 애플리케이션 서버 프로그램의 main 함수가 여기에 있을 것이다.
  • 서버 소켓을 생성하고 요청을 기다리다가, 클라이언트의 요청이 들어오면 새로운 thread 를 만든다.
  • 서블릿 객체 생성 등, 서블릿의 라이프사이클을 관리한다.

 

Dispatcher Servlet

  • spring-webmvc에서 제공하는 서블릿(=javax.servlet.Servlet 인터페이스의 구현체)

 

2. 스프링 MVC 프레임워크를 사용한 WAS 동작방식

 

그림 출처 : https://kouzie.github.io/spring/Spring-Spring-MVC/

 

1. 요청을 받은 서블릿 컨테이너는  해당 요청에 맞는 서블릿을 찾아서  서블릿의 service 메서드를 호출한다.

스프링 MVC 를 사용한 경우 Dispatcher Servlet 의 service 메서드를 호출할 것이다.

 

참고 : Servlet 인터페이스

public interface Servlet {

    public void init(ServletConfig config) throws ServletException;
    
    public ServletConfig getServletConfig();
    
    public void service(ServletRequest req, ServletResponse res)
	    throws ServletException, IOException;
        
    public String getServletInfo();
    
    public void destroy();
}

 

스프링 MVC 프레임워크를 사용한 웹 프로젝트의 산출물로 Gradle__sprringmvc_war 라는 war 파일이 나왔다고 해보자. 이것을 톰캣의 webapps 폴더 밑에 둔 뒤 톰캣 서버를 실행하면 톰캣 서버가 war 압축을 풀어서 사용한다고 한다.

(톰캣은 war파일이 webapps폴더에 있으면 자동으로 알아서 압축을 풀어 웹어플리케이션을 사용할 수 있게 한다고 함)

 

나는 이 작업을 직접 하지는 않았지만, 인텔리J가 대신 해줬다고 생각하면 타당한 듯 하다. 실제로 인텔리J에서 프로젝트를 실행한 뒤 톰캣의 폴더 구조를 보니 다음과 같았다.

프로젝트를 처음 실행했을 때 url이 localhost:8080/Gradle__springmvc_war/xxx가 되어서 당황했는데, war 파일 이름이 Gradle__springmvc_war 로 되어서 그런 것으로 보인다.

 

즉 나의 경우, 

/Gradle__springmvc_war 로 시작하는 요청이 왔을 때 서블릿 컨테이너가 Dispatcher Servlet 인스턴스의 service 메서드를 호출한 것이다.

 

이제부터는 DispatcherServlet#service() 가 실행되는 과정이다. 

 

2. Dispatcher Servlet은 HandlerMapping 이라는 객체에게 컨트롤러 검색을 요청한다.

정확히 말하면 HandlerMapping 은 컨트롤러를 검색하는 것이 아니라 Handler를 검색하는 것이고, 컨트롤러는 Handler 중 하나라고 할 수 있다.

스프링MVC의 HandlerMapping 에는 다음 네가지 종류가 있다.

  • BeanNameHandlerMapping : Bean 이름과 URL 을 매핑시켜준다.
  • ControllerClassNameHandlerMapping
  • SimpleHandlerMapping
  • RequestMappingHandlerMapping

 

이 중에서 @Controller 어노테이션이 붙은 객체를 Handler 로 사용하는 방식은 RequestMappingHandlerMapping

이다. 만약 이 방식을 사용할거라면 RequestMappingHandlerMapping을 설정파일에서 Bean으로 등록해줘야 한다.

 

그런데 설정파일에 @EnableWebMvc 어노테이션을 사용하면 이 코드를 작성하지 않아도 된다.

@EnableWebMvc 어노테이션을 사용하면 매우 많은 스프링 빈을 추가해준다. 이 중에는

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping 도 있다.

 

3. DispatcherServlet 은 HandlerMapping이 찾아준 컨트롤러 객체를 처리할 수 있는 HandlerAdapter  bean에게 요청 처리를 위임한다.

HandlerAdapter는 컨트롤러의 알맞은 메서드를 호출해서 요청을 처리하고, 그 결과를 ModelAndView 객체로서 DispatcherServlet에 리턴한다.

근데 Rest Controller 도 결국은 ModelAndView 객체로 리턴하게 되는걸가..? 궁금

 

4. DispatcherServlet은 ModelAndView의 정보를 ViewResolver bean객체에게 넘기면서 View 객체를 요청한다.

만약 웹 프로젝트가 아래와 같은 설정파일을 사용했다고 하자.

package config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {

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

	@Override
	public void configureViewResolvers(ViewResolverRegistry registry) {
		registry.jsp("/WEB-INF/view/", ".jsp");
	}
}

 

 

위 설정은 org.springframework.web.servlet.view.InternalResourceViewResolver 클래스를 이용해서 다음 설정과 같은 빈을 등록한다.

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

 

이 ViewResolver (InternalResourceViewResolver) 는

뷰 이름이 "hello" 라면 "/WEB-INF/view/hello.jsp" 경로를 뷰 코드로 사용하는 View (InternalResourceView) 객체를 리턴한다.

 

5. DispatcherServlet은 ViewResolver로부터 얻은 View 객체에게 응답 생성을 요청하고, 그 리턴값으로 브라우저에게 응답한다.

 


출처

초보 웹 개발자를 위한 스프링 5 프로그래밍 입문 chapter 10

서블릿 guruble.com/about-servlet/

Servlet Container 역할 workatit.tistory.com/19

Servlet & Spring Web MVC www.youtube.com/watch?v=2pBsXI01J6M

Tomcan & .war 파일 withcoding.com/38

서블릿 이름 gmlwjd9405.github.io/2018/10/29/web-application-structure.html

1. @Transactional(propagation = Propagation.REQUIRED)

아래 코드에서 SomeService의 some 메서드는 AnyService의 any 메서드를 호출한다.

그런데 some 메서드와 any 메서드 모두 @Transactional 어노테이션이 붙어있다.

public class SomeService {

    private AnyService anyService;
    
    public SomeService(AnyService anyService) {
        this.anyService = anyService;
    }
    
    @Transactional
    pblic void some() {
        anyService.any();
    }
}

 

public clas AnyService {

    @Transactional
    public void any() { ... }
}

@Transactional 의 propagation 속성은 기본값이 Propagation.REQUIRED 이다.

 

이 경우 현재 진행중인 트랜잭션이 존재하면 해당 트랜잭션을 사용하고, 존재하지 않으면 새로운 트랜잭션을 생성한다.

 

처음 some() 메서드를 호출하면 트랜잭션이 새로 시작된다. some() 메서드 내부에서 any() 메서드를 호출하면 이미 some() 메서드에 의해 시작된 트랜잭션이 존재하므로 any() 메서드를 호출하는 트랜잭션을 새로 생성하지 않는다. 대신 존재하는 트랜잭션을 그대로 사용한다.

즉 some() 과 any() 메서드를 한 트랜잭션으로 묶어서 실행하는 것이다.

 

 

 

이전 글에서 자바 언어로 DB에 CRUD 를 수행하는 프로그램 작성하기를 살펴보았다. 스프링을 사용하면 DataSource, Connection, Statement, ResultSet 을 직접 사용하지 않고 JdbcTemplate 을 이용해서 편리하게 쿼리를 실행할 수 있다. DataSource를 직접 사용한 코드와 JdbcTemplate을 이용한 코드를 비교해보자.

 

다음 코드는 DataSource를 직접 사용한 JDBC 프로그래밍 코드이다.

package spring;

...

public class MemberDao {

    private DataSource dataSource;

    public MemberDao(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    
    public int count() {
        Connection connection = null;
        try {
            connection = dataSource.getConnection();
            try (
                Statement stmt = connection.createStatement());
                ResultSet resultSet = stmt.executeQuery(
                    "select count(*) from MEMBER")
            ) {
                resultSet.next();
                return resultSet.getInt(1);
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            if (connection != null) {
                try { connection.close(); }
                catch (SQLException e) {}
            }
        }
    }
}

 

이번에는 JavaTemplate을 이용한 코드를 보자.

다음 코드는 앞서 등장한 MemberDao 에 스프링에서 제공하는 JdbcTemplate 을 적용한 것이다.

package spring;

...

public class MemberDao {

    private JdbcTemplate jdbcTemplate;

    public MemberDao(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }
    
    public int count() {
        Integer count = jdbcTemplate.queryForObject(
            "select count(*) from MEMBER", Integer.class);
        return count;
    }
}

 

count() 함수를 보면, jdbcTemplate 을 사용했을 때 코드가 훨씬 깔끔해졌음을 알 수 있다.

이처럼 JdbcTemplate 을 사용하면 DataSource를 직접 사용하는 대신, JdbcTemplate 에게 DataSource를 주입해주고 JdbcTemplate 을 통해서 사용하게 된다.

그럼으로써 DataSource를 직접 사용할 때 써야만 했던 많은 코드들(이를테면 dataSource.getConnection(), resultSet.next(), stmt.close(), connection.close() 등등)이 JdbcTemplate의 몫으로 넘어간다.

이를 통해 count() 는 [Member 의 수를 구한다]는 본연의 역할에 충실하게 되었다.

 

스프링으로 DB 연동 프로그램을 만들기에 앞서, JDBC 프로그래밍을 먼저 공부할 필요가 있다.

 

다음과 같은 방식으로 JDBC 프로그래밍을 해본 적이 있으리라 생각한다. Connection, Statement, DriverManager, ResultSet 등 익숙한 단어들이 보인다.

import java.sql.*;

public class Example {

   static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";  
   static final String DB_URL = "jdbc:mysql://localhost/spring5";

   static final String USER = "username";
   static final String PASS = "password";
   
   public static void main(String[] args) {
       Connection conn = null;
       Statement stmt = null;
       
       try {
          Class.forName("com.mysql.jdbc.Driver");

          conn = DriverManager.getConnection(DB_URL,USER,PASS);

          stmt = conn.createStatement();
          String sql = "SELECT id, first, last, age FROM Employees";
          ResultSet rs = stmt.executeQuery(sql);
          ...
          rs.close();
          stmt.close();
          conn.close();
       } catch(SQLException se){
          se.printStackTrace();
       } catch(Exception e){
          e.printStackTrace();
       } finally {
          try {
             if(stmt!=null) { stmt.close(); }
          } catch(SQLException se2){
          }
          try {
             if(conn!=null) { conn.close(); }
          } catch (SQLException se) {
             se.printStackTrace();
          }
       }
    }
}

 

 

위 코드의 18번 line 쯤을 보면, 아래와 같이 DriverManager 로부터 Connection 객체를 얻고있는 것을 확인할 수 있다.

Connection connection = DriverManager.getConnection();

그런데 근래에는 DataSource가 DriverManager의 대체제로서 사용되고 있다.

DataSource 를 사용할 때는 다음과 같이 DataSource 객체로부터 커넥션 객체를 얻는다.

Connection connection = dataSource.getConnection();

 

JDBC 프로그래밍을 할 때 DriverManager 보다는 DataSource를 사용하는 쪽이 선호된다고 한다. 스프링도 DataSource를 사용해서 DB Connection 을 구한다.

 

이제 DriverManager 대신 DataSource를 사용해서 JDBC 프로그래밍을 해보자. 깊게는 안 써볼거고 스프링 사용에 필요한 정도의 지식만 가져갈 것이다.

 

1. DB 테이블 생성

MySQL 에서 spring5practice 라는 데이터베이스를 만들고 member 테이블을 다음과 같이 만들었다.

사용할 DB 테이블

DB 테이블 생성에 사용한 쿼리는 다음과 같다.

create user 'spring5'@'localhost' identified by 'spring5';

create database spring5practice character set=utf8;

grant all privileges on spring5practice.* to 'spring5'@'localhost';

create table spring5practice.MEMBER (
  ID int auto_increment primary key,
  EMAIL varchar(255),
  PASSWORD varchar(100),
  NAME varchar(100),
  REGISTER_DATE datetime,
  unique key (EMAIL)
) engine=InnoDB character set = utf8;

 

2. build.gradle 작성

apply plugin: 'java'

sourceCompatibility = 1.8
targetCompatibility = 1.8
compileJava.options.encoding = "UTF-8"

repositories {
  mavenCentral()
}

dependencies {
  implementation 'org.apache.tomcat:tomcat-jdbc:8.5.27'
  implementation 'mysql:mysql-connector-java:5.1.45'
}

 

3. Member 및 MemberDao, Main 구현

MemberDao 에 selectAll, selectByEmail, create, update, remove 등의 메서드가 들어갈 수 있겠지만 귀찮으므로

Member 수를 알려주는 count() 메서드만 작성할 것이다.

package spring;

import java.time.LocalDateTime;

public class Member {

    private Long id;
    private String email;
    private String password;
    private String name;
    private LocalDateTime registerDateTime;

    public Member(String email, String password, String name, LocalDateTime registerDateTime) {
        this.email = email;
        this.password = password;
        this.name = name;
        this.registerDateTime = registerDateTime;
    }

    public void changePassword(String oldPassword, String newPassword) {
        if (!password.equals(oldPassword)) {
            throw new WrongIdPasswordException();
        }
        this.password = newPassword;
    }

    public Long getId() { return id; }

    public String getEmail() { return email; }

    public String getPassword() { return password; }

    public String getName() { return name; }

    public LocalDateTime getRegisterDateTime() { return registerDateTime; }
}
package spring;

import java.util.List;
import javax.sql.DataSource;

public class MemberDao {

    private DataSource dataSource;

    public MemberDao(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    
    public int count() {
        Connection connection = null;
        try {
            connection = dataSource.getConnection();
            try (
                Statement stmt = connection.createStatement());
                ResultSet resultSet = stmt.executeQuery(
                    "select count(*) from MEMBER")
            ) {
                resultSet.next();
                return resultSet.getInt(1);
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            if (connection != null) {
                try { connection.close(); }
                catch (SQLException e) {}
            }
        }
    }
}
import org.apache.tomcat.jdbc.pool.DataSource;
import spring.MemberDao;

public class Main {

    public static void main(String[] args) {
        DataSource dataSource = new DataSource();
        
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost/spring5practice?characterEncoding=utf8");
        dataSource.setUsername("spring5");
        dataSource.setPassword("spring5");
        dataSource.setInitialSize(2);
        dataSource.MaxActive(10);

        MemberDao memberDao = new MemberDao(dataSource);

        System.out.println(memberDao.count());
    }
}

 

Main 클래스를 보면, main 메서드에서 DataSource 객체를 생성해서 MemberDao 객체에게 넘겨주도록 코드를 작성했다.

 

그런데 main 메서드에서 생성한 DataSource 객체는  org.apache.tomcat.jdbc.pool.DataSource 이고,

MemberDao 생성자 매개변수는 javax.sql.DataSource 이다.

 

Java 는 자바 언어를 가지고 데이터베이스에 접근하여 CRUD 할 수 있도록 java.sql 패키지와 javax.sql 패키지를 제공한다. (두 패키지는 논리적으로 하나로 합쳐져도 될 애들인데 어쩌다보니 그냥 그렇게 된 애들이다.)

 

javax.sql.DataSource 는 인터페이스이다. 그리고 org.apache.tomcat.jdbc.pool.DataSource 는 javax.sql.DataSource 인터페이스의 구현체이다.

tomcat jdbc에서 제공하는 DataSource 객체를 사용하면, 커넥션 풀을 사용해서 DB 연결을 관리할 수 있다.

tomcat jdbc가 제공하는 DB 커넥션 풀 기능을 사용하기 위해서 build.gradle에 implementation 'org.apache.tomcat:tomcat-jdbc:8.5.27' 를 추가했던 것이다. (= tomcat jdbc 가 제공하는 DataSource 구현체를 사용하기 위해서 해당 dependency가 필요한 것.)

 

 


출처

스프링 5 프로그래밍 입문 (최범균),

stackoverflow.com/questions/18212727/difference-between-javax-sql-and-java-sql

(0)

사용할 예제 코드

package spring.pack;

public interface TargetInterface {

    void hello();

    void hello(String a);

    int minus(int a, int b);

    int plus(int a, int b);
}
package spring.pack;

public class Target implements TargetInterface {

    @Override
    public void hello() {}

    @Override
    public void hello(String a) {}

    @Override
    public int minus(int a, int b) { return 0; }

    @Override
    public int plus(int a, int b) { return 0; }

    public void method() {}
}
package spring.pack;

public class Bean {

    public void method() throws RuntimeException {}
}

 

(1)

만약 Target 클래스의 hello(String a) 메서드를 지정하고 싶다면 다음과같이 표현식을 작성하면 된다.

괄호 안의 내용을 딱 보면 뭔지 알것같을 것이다. 메소드 시그니처.

execution(public void spring.pack.Target.hello(String a))

 

(2)

접근제한자는 생략할 수 있다. 생략이 가능하다는 건 이 항목에 대해서는 조건을 부여하지 않는다는 의미다.

 

execution(void spring.pack.Target.hello(String a))

 

 

(3)

리턴타입은 생략할 수 없다. 모든 리턴타입에 대해 상관 없게 하고싶으면 에스터리스크(*)을 사용하면 된다.

execution(public * spring.pack.Target.hello(String a))
execution(* spring.pack.Target.hello(String a))

 

(4)

spring.pack.Target. 이 부분은 생략 가능하다. 생략하면 모든 패키지의 모든 클래스에 대해 허용하겠다는 뜻이다.

execution(* hello(String a))

 

(5)

메서드 이름은 생략할 수 없다. Target 클래스의 모든 메서드를 포인트컷으로 지정하고 싶으면 다음과 같이 쓰면 된다.

execution(* spring.pack.Target.*(String a))

 

 

 

다음과 같이 메서드 이름 일부를 가지고도 지정 가능하다.

execution(* spring.pack.Target.*llo(String a))

 

 

 

(6)

메서드의 파라미터가 갯수도, 타입도 상관 없다면 다음과 같이 쩜쩜(..) 을 쓰면 된다.

execution(* spring.pack.Target.m*(..))

 

파라미터 갯수는 상관 있는데 타입은 상관 없다면 다음과 같이 쓴다.

(타입이 상관 없는 파라미터에 * 표시)

execution(* spring.pack.Target.minus(*,*))

 

(7)

클래스 부분을 좀 더 자세히 살펴보자 (spring.pack.Target 이부분)

spring.pack 패키지에 있는 모든 클래스에 있는 minus 메서드에 대해 지정하고싶으면 다음과 같이 쓴다.

execution(* spring.pack.*.minus(*,*))

 

spring 패키지 하위 어딘가에 있는 Target 클래스의 minus 메서드에 대해 지정하고싶으면 다음과 같이 쓴다.

".."은 모든 하위 패키지를 의미한다.

execution(* spring..Target.minus(*,*))

 

 

그런데 맨 처음 패키지와 맨 마지막의 클래스이름(타입이름)은 ".." 을 이용해서 축약할 수 없다.

그러므로 맨 처음 패키지를 모든 패키지로 하고싶거나, 클래스를 모든 클래스로 하고싶으면 그 자리에 에스터리스크(*)을 쓰면 된다.

아래 두 표현은 같은 의미이다.

execution(* *..*.minus(*,*))

 

execution(* minus(*,*))

 

(8)

다음과 같이 작성하면, TargetInterface 에 정의되어있는 메소드만 포인트컷으로 적용된다. 따라서 TargetInterface 에는 없는 메서드인 public void spring.package.Target.method() 는 제외된다.

execution(* *..TargetInterface.*(..))

 

(9)

execution() 은 메소드의 시그니처를 비교하는 방식이다. 이 외에도 몇 가지 표현식 스타일들이 있다.

예를들면 스프링에서 사용될 때 Bean 의 이름으로 비교하는 bean() 이 있다. bean(*Service)라고 쓰면 이름이 Service로 끝나는 모든 빈을 선택한다.

이전글

앞에서 프록시 객체와 데코레이터 패턴에 대해서 알아봤는데, 스프링도 이를 이용해서 AOP를 구현하고 있다.

 

스프링 AOP를 사용해보자. @Aspect 어노테이션을 사용해서 구현해볼 것이다.

 

순서

1. @Aspect 가 붙은 클래스 만들기

2. 설정파일에 @EnableAspectJAutoProxy 붙이기

3. target object 사용하기

 

+ 여러개의 Advice 와 Pointcut 사용하기

 


1. @Aspect 가 붙은 클래스 만들기

이전에 만들었던 프록시 UpperCaseMessage 는 이제 필요없다. 대신 다음과 같이 @Aspect 가 붙은 클래스가 필요하다.

package aspects;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class Aspects {

    @Pointcut("execution(public String *..*.*(..))")
    private void upperCasePointcut() {}

    @Around("upperCasePointcut()")
    public String upperCaseAdvice(ProceedingJoinPoint joinPoint)
            throws Throwable {
        Object targetResult = joinPoint.proceed();

        validateString(targetResult);

        return ((String) targetResult).toUpperCase();
    }

    private void validateString(Object object) {
        if (!object.getClass().equals(String.class)) {
            throw new IllegalStateException(
                "리턴값이 String 이 아닌 메서드에 대해 이 advice 를 사용할 수 없습니다.");
        }
    }
}

 

부가기능(공통기능) 하나를 advice 라고 한다. 여기서는 "String 타입의 리턴값을 upper case로 바꾸는 것"이 advice이다.

코드로는 @Around 가 붙은 메서드, 즉 upperCaseAdvice가 advice에 해당한다.

Around Advice 는 Spring에서 구현 가능한 어드바이스 종류들 중 하나이다. 이 외에도 Before Advice, After Returning Advice, After Throwing Advice, AfterAdvice 가 있다.

 

@Pointcut("execution(public String *..*.*(..))")

이 부분이 뭔지 살펴보자.

 

앞서 advice에 대해 얘기했는데, advice는 각각 어디에 적용될지 정해질 필요가 있다.

이 정보를 담는것이 pointcut 이다.

예시에서는 아래 코드를 통해 포인트컷을 정의하고 있다.

@Pointcut("execution(public String *..*.*(..))") 
private void upperCasePointcut() {}

 

@Pointcut 어노테이션의 속성값 execution(public String *..*.*(..))

이건 뭘까?

"접근제어자가 public 이고 리턴 타입이 String인 모든 메서드" 라는 뜻의 포인트컷 표현식이다.

포인트컷 표현식 작성법은 다음 포스팅에서 다룰 예정

 

@Around 애너테이션의 속성값을 보면 "upperCasePointcut()" 이렇게 되어있다.

@Around("upperCasePointcut()")

upperCasePointcut() 에서정의하는 포인트컷에 upperCaseAdvice 를 적용하겠다는 것이다.

 

2. 설정파일에 @EnableAspectJAutoProxy 붙이기

package config;

import aspects.Aspects;
import message.GreetingMessage;
import message.LoveMessage;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy
public class AppConfiguration {

    @Bean
    public Aspects aspects() {
        return new Aspects();
    }

    @Bean
    public GreetingMessage greetingMessage() {
        return new GreetingMessage();
    }

    @Bean
    public LoveMessage loveMessage() {
        return new LoveMessage();
    }
}

@Aspect 에노테이션을 붙인 클래스를 공통기능으로 적용하려면 @EnableAspectJAutoProxy 애너테이션을 설정 클래스에 붙여야한다. 이 애노테이션을 추가하면 스프링은 @Aspect 애너테이션이 붙은 Bean 객체를 찾아서 빈 객체의 @Pointcut 설정과 @Around 설정을 사용한다.

3. target object 사용하기

import config.AppConfiguration;
import message.Message;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;

public class Test {

    public static void main(String[] args) {
        AbstractApplicationContext applicationContext =
            new AnnotationConfigApplicationContext(AppConfiguration.class);

        Message greetingMessage = applicationContext.getBean("greetingMessage", Message.class);
        System.out.println(greetingMessage.getValue());

        Message loveMessage = applicationContext.getBean("loveMessage", Message.class);
        System.out.println(loveMessage.getValue());
    }
}

 

사용은 이런식으로 하면 된다.

 


+ 여러개의 어드바이스 및 포인트컷 사용하기

package aspects;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class Aspects {

    @Pointcut("execution(public String *..*.*(..))")
    private void upperCasePointcut() {}

    @Around("upperCasePointcut()")
    public String upperCaseAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        Object targetResult = joinPoint.proceed();

        validateString(targetResult);

        return ((String) targetResult).toUpperCase();
    }

    @Pointcut("execution(public String *..*.*(..))")
    private void startWithAsteriskPointcut() {}

    @Around("startWithAsteriskPointcut()")
    public String startWithAsteriskPointcutAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        Object targetResult = joinPoint.proceed();

        validateString(targetResult);

        return "*" + targetResult;
    }

    private void validateString(Object object) {
        if (!object.getClass().equals(String.class)) {
            throw new IllegalStateException("리턴값이 String 이 아닌 메서드에 대해 이 advice 를 사용할 수 없습니다.");
        }
    }
}

 

 

 

 

 

AOP

AOP를 공부하기 전에 AOP의 사용예를 한가지만 살펴보겠습니다.

다음과 같은 UserController 가 있다고 해봅시다.

잘 동작하는 코드인지, 어떻게 동작하는 코드인지는 생각하지 마시고 코드의 의미만 이해하시면 됩니다.

join 함수는 회원가입이 성공하면 statusCode가 201 Created 인 HttpResponse 객체를 return합니다.

login 함수는 로그인이 성공하면 statusCode가 200 OK 인 HttpResponse 객체를 return합니다.

public class UserController {
    
    public ResponseEntity join(JoinRequest joinRequest) {    // 회원가입
        userService.join(joinRequest);

        return ResponseEntity
            .created()
            .build();
    }
    
    public ResponseEntity login(LoginRequest loginRequest) {    // 로그인
        userService.login(loginRequest);

        return ResponseEntity
            .ok();
    }
}

 

두 함수는 모두 핵심 로직을 userService 에게 위임하고있습니다. 그런데 이 userService 에서 가끔씩 WrongUserInputException 예외객체를 throw 한다고 해봅시다.

예를들어 회원가입 요청에 비밀번호가 누락되어있다거나, 로그인 요청에 아이디 비밀번호가 null 일 경우 UserService 가 WrongUserInputException 예외를 throw 하도록 구현한 것입니다.

 

그리고 저는 WrongUserInputException이 발생하면 400 Bad Request 응답을 내려주고 싶습니다. 그렇다면 아래와 같이 코드를 수정하면 됩니다.

public class UserController {
    
    public ResponseEntity join(JoinRequest joinRequest) {    // 회원가입
        try {
            userService.join(joinRequest);

            return ResponseEntity
                .created()
                .build();
        } catch (WrongUserInputException e) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(e.getMessage());
        }
    }
    
    public ResponseEntity login(LoginRequest loginRequest) {    // 로그인
        try {
            userService.login(loginRequest);

            return ResponseEntity
                .ok();
        } catch (WrongUserInputException e) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(e.getMessage());
        }

    }
}

 

회원가입/로그인 과정에서 WrongUserInputException 예외가 발생하면

컨트롤러가 이를 catch 해서

statusCode 가 400 Bad Request 인 HttpResponse 를 return 하도록 구현했습니다.

 

그런데 코드 자체는 try-catch 가 적용되기 전이 더 깔끔하죠?

try-catch 부분은 핵심 기능도 아닐 뿐더러 두 함수에서 반복적으로 등장했습니다.

 

UserController 에는 이 중복되는 부가기능 코드를 남겨두지 않은 채로

WrongUserInputException 이 발생하면 400 응답 하게 할 수 없을까요?

 

이럴 때 적용하면 좋은것이 AOP 입니다.

AOP 란, 여러 객체에 공통적으로 적용할 수 있는 기능을 분리해서 재사용성을 높여주는 프로그래밍 기법입니다.

 

어떤 프로젝트에 UserController 뿐만 아니라 PostController, CommentController 도 있다고 해봅시다. 이 컨트롤러들도 잘못된 HttpRequest가 들어오면 400 Bad Request 응답을 return하고 싶습니다. 그러면 앞서 등장한 try-catch 코드가 똑같이 사용되겠죠. 이 예시에서는 이 부분이 '여러 객체에 공통적으로 적용할 수 있는 기능'인 것입니다.

// 여러 객체에 공통적으로 적용할 수 있는 기능
try {

} catch (WrongUserInputException e) {
    return ResponseEntity.status(HttpStatus.BAD_REQUEST)
        .body(e.getMessage());
}

 

 

AOP 란?

핵심 기능의 코드를 수정하지 않으면서 공통 기능의 구현을 추가하는 것


프록시와 AOP

예제코드는 다음과 같이 아주 간단하게 Printer, GreetingPrinter, LovePrinter를 사용하겠습니다.

package message;

public interface Message {

    String getValue();
}
package message;

public class GreetingMessage implements Message {

    @Override
    public String getValue() {
        return "Hello!";
    }
}
package message;

public class LoveMessage implements Message {

    @Override
    public String getValue() {
        return "I ♡ you";
    }
}

 

이 클래스들은 다음과 같이 사용될 것입니다. 

import message.Message;
import message.LoveMessage;

public class Test {

    public static void main(String[] args) {
        Message message = new LoveMessage();
        System.out.println(message.getValue());
    }
}

 

 

Message 객체의 getValue 리턴값을 항상 대문자로 받고싶다고 해봅시다. 그러려면 다음과 같이

GreetingMessage 와 LoveMessage를 수정하면 될 것입니다.

package message;

public class GreetingMessage implements Message {

    @Override
    public String getValue() {
        return "Hello!".toUpperCase();
    }
}
package message;

public class LoveMessage implements Message {

    @Override
    public String getValue() {
        return "I ♡ you".toUpperCase();
    }
}

 

예제가 너무 단순해서 느낌이 안올지도 모르겠지만 toUpperCase() 가 반복해서 등장하고 있습니다. toUpperCase() 는 앞서 등장했던 UserController 예시의 try-catch 코드에 대응됩니다.

 

이 toUpperCase 부분을 추출해보겠습니다. 새로 추가된 클래스는 UpperCaseMessage 입니다.

 

package message;

public class UpperCaseMessage implements Message {

    private Message message;

    public UpperCaseMessage(Message message) {
        this.message = message;
    }

    @Override
    public String getValue() {
        return message.getValue()
            .toUpperCase();
    }
}
package message;

public class GreetingMessage implements Message {

    @Override
    public String getValue() {
        return "Hello!";
    }
}
package message;

public class LoveMessage implements Message {

    @Override
    public String getValue() {
        return "I ♡ you";
    }
}

 

그리고 사용법은 다음과 같이 변할것입니다.

import message.Message;
import message.UpperCaseMessage;
import message.LoveMessage;

public class Test {

    public static void main(String[] args) {
        Message message = new UpperCaseMessage(new LoveMessage());
        System.out.println(message.getValue());
    }
}

 

이제 LoveMessage 와 GreetingMessage 코드에서 .toUpperCase() 가 사라졌습니다. 그러면서도 이 기능은 그대로 사용할 수 있죠. 대신 사용법이 조금 달라졌네요.

 

이 때 LoveMessage(또는 GreetingMessage)와 클라이언트(해당 객체를 사용하는 쪽) 사이에 존재하는 UpperCaseMessage 객체를 프록시 객체라고 합니다.

또한, LoveMessage 처럼 핵심 기능을 가지고 있는 객체는 target 또는 target object 라고 합니다.

 

그리고 target 에 부가적인 기능을 런타임 시 다이내믹하게 부여해주기 위해 프록시를 사용하는 패턴을 데코레이터 패턴이라고 합니다.

 

이렇게 프록시 객체를 사용하여 핵심 코드와 부가기능 코드를 분리할 수 있다는 것을 알아봤습니다. 그런데 프록시 객체를 만드는 것은 번거로운 일일 수 있습니다. 예를들어, Message 인터페이스에 엄청나게 많은 메소드들이 선언되어있다고 해봅시다. (예시로는 두개의 메소드만 추가해보겠습니다)

package message;

public interface Message {

    String getValue();
    
    void printValue();
    
    int getLength();
}

 

그런데 부가기능은 getValue() 에만 적용하고 싶다고 해봅시다.

그럼에도 불구하고 프록시 객체를 사용하려면

부가기능이 필요 없는 다른 메소드들도 모두 구현해서 타깃으로 위임하는 코드를 일일히 만들어줘야 합니다.

아래 코드에서 printValue() 와 getLength() 가 바로 그 예시입니다. 이런 메서드가 한두개면 몰라도 엄청나게 많아지면 굉장히 번거롭겠죠. 코드도 쓸데없이 길어지구요.

package message;

public class UpperCaseMessage implements Message {

    private Message message;

    public UpperCaseMessage(Message message) {
        this.message = message;
    }

    @Override
    public String getValue() {
        return message.getValue()
            .toUpperCase();
    }

    @Override
    public void printValue() {
        message.printValue();
    }

    @Override
    public int getLength() {
        return message.getLength();
    }
}

 

또한 타깃 인터페이스의 메소드가 추가되거나 변경될 때마다 프록시 객체 클래스도 함께 수정해줘야 한다는 부담도 있습니다.

 

스프링을 이용해서 AOP를 구현하면 이런 문제를 겪지 않을 수 있습니다.

+ Recent posts