본문 바로가기

Server Programming/Spring Boot 2 Full-Stack Programming

[작업 관리 애플리케이션 개발] 3. 스프링 5 -> 스프링 부트 + MySQL

반응형

스프링 프레임워크

: 경량화된 IoC(제어의 역전) 컨테이너로 AOP와 함께 차세대 J2EE 아키텍처 기반 제공

 

스프링5의 기본 개념

  1. IoC(제어의 역전)과 DI(의존성 주입)
    1. 스프링 컨테이너 구동
    2. 어노테이션 기반의 설정
      1. 빈 선언
      2. 의존성 주입
      3. 생성자 기반의 주입
      4. 세터 기반/메소드 기반의 주입
      5. 필드 기반의 주입
      6. 의존성 주입 모범 사례
  2. 스프링 MVC
    1. 자바 EE 서블릿과 HTTP 요청, 응답
    2. DispatcherServlet을 통한 스프링 MVC의 HTTP 요청, 응답 
    3. 메시지 앱의 웹 애플리케이션 전환
  3. 데이터 접근을 위한 스프링 JDBC와 JPA
  4. 관점 지향 프로그래밍 스프링 AOP
  5. 스프링이 트랜잭션을 관리하는 방법
  6. 스프링 부트

IoC(제어의 역전)과 DI(의존성 주입)

-빈 : 스프링 컨테이너가 관리하는 객체 

-객체 의존성 관리 방법

  1. 객체가 직접 의존 관계에 있는 객체들의 생성자 호출해 의존성을 인스턴스화
  2. 룩업 패턴을 활용해 의존성을 찾아 배치

 

회원 가입에 성공한 이후에 사용자에게 이메일을 전송하는 RegistrationService

1.객체가 직접 의존 관계에 있는 객체들의 생성자 호출해 의존성을 인스턴스화

-의존성 부분에 초점을 맞춰 회원 가입과 이메일 전송은 생략

-생성자에서 MailSender를 인스턴스화 하는 방법

public class Registrationservice {
    private MailSender mailSender; 
    public RegistrationService() {
        // 의존하는 객체를 인스턴스화한다
        this.mailSender = new MailSender(); 
  }
  //나머지 로직 
}

-> RegistrationService가 MailSender를 인스턴스화함으로써 의존성을 관리

 

2. 생성자 또는 세터로 의존성 주입하기 위해 컨테이너에 의존성 주입 방법

- 생성자의 인자로 MailSender 인스턴스 추가

-스프링이 MailSender 인스턴스를 인스턴스화하는 책임을 지므로, 의존성 제어가 역전

public class Registrationservice {
    private MailSender mailSender;
    public RegistrationService(MailSender mailSender) {
	    this.mailSender = mailSender; 
    }
    //나머지 로직
}

 

-> 설정 메타 데이터를 이용해 스프링이 어떤 의존성이 필요한지 파악한다.

-> applicationContext.xml / application.properties


스프링 컨테이너 구동

-org.springframework.context.ApplicationContext 인터페이스 : 스프링 IoC 컨테이너

-독립 실행형 애플리케이션에서 컨테이너 설정 방법

: ApplicationContext 구현체인 두 가지 방법

1.  ClassPathXmlApplicationContext는 XML 기반 

2. AnnotationConfigApplicationContext는 자바 기반

 

app.sample.messages의 소스코드

  1. AppConfig
  2. MessageApplication
  3. Message
  4. MessageRepository
  5. MessageService

Message

package app.sample.messages;

public class Message {
    private String text;
    //생성자 정의
    public Message(String text){this.text = text;}
    //게터 정의
    public String getText(){return text;}
}

 

MessageRepository

package app.sample.messages;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Repository;

@Repository
public class MessageRepository {
    //log 인스턴스 생성
    private final static Log log= LogFactory.getLog(MessageRepository.class);

    //Message 객체를 매개변수로 받는 저장 메서드 생성
    public void saveMessage(Message message){
        //DB에 메시지 저장
        log.info("Saved message: "+message.getText());
    }
}

 

MessageService

package app.sample.messages;

import org.springframework.stereotype.Service;

@Service
public class MessageService {
    //리포지토리 의존성 주입
    private MessageRepository repository;

    //리포지토리 인스턴스를 매개변수로 하는 생성자 생성해 의존성 연결
    public MessageService (MessageRepository repository){
        this.repository=repository;
    }
    //리포지토리에 저장하는 메서드
    public void save(String text){
        this.repository.saveMessage(new Message(text));
    }
}

 

AppConfig

package app.sample.messages;

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

//빈을 정의하는 설정파일이라는 어노테이션
@Configuration
//어노테이션을 스캔할 패키지 알려주는 어노테이션
@ComponentScan("app.sample.messages")
public class AppConfig {
    //빈 어노테이션을 메소드에 추가함으로써 빈 생성
    @Bean
    public MessageRepository messageRepository(){
        return new MessageRepository();
    }

    @Bean
    MessageService messageService(){
        return new MessageService(messageRepository());
    }
}

어노테이션 기반의 설정

: 스프링이 제공하는 스테레오 타입 어노테이션 세트 [@Component, @Service, @Controller, @Repository]

->스프링은 @ComponentScan 어노테이션의 패키지부터 스캔해 해당 어노테이션 클래스 수집

 

@Repository

: 컴포넌트가 도메인 주도 설계에서 사용된 리파지토리 혹은 DAO임을 나타낸다.

@Controller

: 컴포넌트가 HTTP요청을 받을 수 있는 웹 컨트롤러임을 나타낸다.

 

AppConfig

package app.sample.messages;

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

//빈을 정의하는 설정파일이라는 어노테이션
@Configuration
//어노테이션을 스캔할 패키지 알려주는 어노테이션
@ComponentScan("app.sample.messages")
public class AppConfig {

}

 

MessageService

package app.sample.messages;

import org.springframework.stereotype.Service;

@Component
public class MessageService {
    //리포지토리 의존성 주입
    private MessageRepository repository;

    //리포지토리 인스턴스를 매개변수로 하는 생성자 생성해 의존성 연결
    public MessageService (MessageRepository repository){
        this.repository=repository;
    }
    //리포지토리에 저장하는 메서드
    public void save(String text){
        this.repository.saveMessage(new Message(text));
    }
}

 

MessageRepository

package app.sample.messages;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Repository;

@Component
public class MessageRepository {
    //log 인스턴스 생성
    private final static Log log= LogFactory.getLog(MessageRepository.class);

    //Message 객체를 매개변수로 받는 저장 메서드 생성
    public void saveMessage(Message message){
        //DB에 메시지 저장
        log.info("Saved message: "+message.getText());
    }
}

-> 제네릭 스테레오 타입 어노테이션을 이용해 빈 주입을 대신해 해당 클래스를 인스턴스화 한다.


 

의존성 주입

: 의존성 연결하는 스프링에서 제공하는 두 가지 어노테이션

  1. @Required
    : 세터 메서드에 적용
  2. @Autowired
    : 생성자와 메서드, 필드에 적용

의존성 주입 방법

  1. 생성자 기반 주입
  2. 세터/메서드 기반 주입
  3. 필드 기반 주입

생성자 기반 주입

    //생략 가능
    //@Autowired
    //리포지토리 인스턴스를 매개변수로 하는 생성자 생성해 의존성 연결
    public MessageService (MessageRepository repository){
        this.repository=repository;
    }

 

세터/메서드 기반 주입

권장되지 않음 - @Required 제거

 

필드 기반 주입

-@Autowired 어노테이션으로 필드에 직접 적용

-세터 메서드 선언할 필요가 없다.

-권장되지 않음

    @Autowired
    //리포지토리 의존성 주입
    private MessageRepository repository;

 

->반드시 필요한 의존성은 항상 생성자를 통해 주입

-인스턴스 초기화와 읽기 전용 의존성을 통해 의존성을 변경하지 못하는 접근제한과 캡슐화 가능

-필드 주입의 경우 단일책임 원칙 위반 가능성이 증가하기 때문에 피해야 한다.

 

 


스프링 MVC

-자바 EE 서블릿 API 기반 웹 애플리케이션 구현 기술 제공

-자바 EE 웹 애플리케이션과 서블릿의 동작 원리


자바 EE 서블릿

-톰캣과 같은 애플리케이션 서버인 서블릿 컨테이너 안에서 동작하는 서블릿

-HTTP 요청이 서버 도착시 인증 -> 로깅 -> 감사라는 필터 리스트를 통과하는 필터링 작업 수행

-요청이 필터링 작업을 통과시 특정 패턴과 일치하는 URL을 포함하는 요청을 처리하는 서블릿으로 전달

-서블릿이 요청에 대한 처리를 완료하면 HTTP 응답은 필터를 다시 통과하고 클라이언트로 다시 전송

-필터에서 특정 HTTP 헤더를 응답에 추가하는 추가 필터링 작업 수행가능

자바 EE 웹 애플리케이션의 요청/응답 흐름

 

HTTP 요청과 응답

  1. 모든 HTTP 요청에는 HttpServletRequest 인스턴스가 생성
  2. 모든 HTTP 응답에는 HttpServletResponse 인스턴스가 생성
  3. 여러 요청에서 사용자 식별을 위해 첫 번째 요청에 HttpSession 인스턴스를 생성하는데 해당 인스턴스에 세션 ID를 갖는다.
    : 해당 세션ID는 HTTP 응답 헤더의 클라이언트에 쿠키로 전송되고, 해당 쿠키를 다음 요청시 다시 서버로 전송하는 방식으로 서버에서 사용자 인식
  4. 자바 EE에서 HttpSessionListener 인터페이스 구현을 통해 HttpSession 라이프 사이클 이벤트를 수신 혹은 ServletRequestSession 인터페이스를 구현해 요청에 대한 라이프 사이클 이벤트 수신 리스너 생성 가능
  5. @WebServlet 어노테이션 적용하거나 전통방식으로 web.xml파일을 통해 URL 요청 라우트를 위해 URL 패턴 매핑하고 서블릿 생성할 수도 있다.
  6. 서블릿이 공유 리소스에 접근시 항상 동시 요청을 다루고 다른 요청에 영향을 줄 수 있다는 것을 인지해야한다.

DispatcherServlet

-스프링 MVC를 사용하면 서블릿 생성할 필요가 없다.

-클래스를 생성, @Controller 어노테이션을 추가해 @RequestMapping 어노테이션으로 특정 URL 패턴에 매핑 가능

-이름 규칙으로 Controller를 접미사로 붙인다.

 

-스프링은 DispatcherServlet을 활용해 요청을 받는다.

-모든 요청을 처리할 수 있도록 설정하고, @RequestMapping의 URL 패턴에 맞는 컨트롤러를 찾는다.

 

스프링 MVC를 통한 HTTP 요청과 응답

스프링 DispatcherServlet과 컨트롤러


메시지 앱의 웹 애플리케이션 전환

-스프링 부트 적용

-스프링 부트를 적용하면 자동 설정을 수행한다.

-HTTP 요청을 처리하는 컨트롤러를 사용하므로, MessageApplication에서 MessageService는 제거한다.

 

Hello 메시지를 표시하는 API가 추가된 메시지 컨트롤러

package app.sample.messages;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
//URL 특정 패턴 처리를 위해 컨트롤러 매핑
@RequestMapping("/messages")

public class MessageController {
	//welcome에 일치하는 요청하는 처리하는 핸들러 메소드 매핑
    //@RequestMapping의 축약 형태
    @GetMapping("/welcome")
    
    //welcome() 핸들러 메소드가 HttpServeltResponse에 전달하는 메시지 반환
    public String welcome(){
        return "Hello, Welcome to Spring Boot!";
    }
    
}

-> 404 에러

 

리턴값을 본문으로 처리하는 @ResponseBody

package app.sample.messages;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/messages")

public class MessageController {

    @GetMapping("/welcome")
    //리턴 값을 Http 응답본문으로 처리한다는 뜻
    @ResponseBody
    public String welcome(){
        return "Hello, Welcome to Spring Boot!";
    }

}

-> @ResponseBody로 스프링이 반환값을 본문으로 처리하고 HttpMessageConverter로 응답에 값을 쓴다.

-> 모든 메소드에 어노테이션을 적용하기 위해 MessageController에 어노테이션 적용하는 것도 가능하다.

 

#RESTful API 적용하기 위해서 @RestController를 적용한다.

-> @RestController : @Controller + @ResponseBody로 RestAPI의 경우 비동기처리로 모든 처리를 JSON으로 처리하므로 JSON을 본문으로 사용한다.

 


: welcome() 핸들러에서 문자열 반환이 아니라 웹 페이지 렌더링시 HTML 코드 덩어리 혹은 API 호출 결과로 JSON 데이터를 전송

-> 모두 String 객체로 HTTP 응답으로 전달된다.

 

환영 메시지를 볼드체로 변경

    @GetMapping("/welcome")
    //리턴 값을 Http 응답본문으로 처리한다는 뜻
    @ResponseBody
    public String welcome(){
        return "<strong>Hello, Welcome to Spring Boot!</strong>";
    }

-> 하지만, MVC 패턴 위반으로 HTML 마크업은 뷰단에서 처리해야 한다.

 

모델과 뷰 사용 방법

  1. Model 객체 사용
  2. ModelAndView 객체 사용

1. Model 객체를 핸들러 메소드에 전달

package app.sample.messages;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/messages")

public class MessageController {

    @GetMapping("/welcome")
    //리턴 값을 Http 응답본문으로 처리한다는 뜻 -> 타임리프로 대체
    //: 스프링이 핸들러 반환 값을 뷰 이름으로 사용해 타임리프로 응답 생성
    //@ResponseBody
    public String welcome(Model model){
        //return "<strong>Hello, Welcome to Spring Boot!</strong>";
        
        //메소드에 스프링이 생선하는 Model 인스턴스 전달 -> 템플릿의 키와 일치하는 메시지를 속성으로 추가
        model.addAttribute("message", "Hello, Welcome to Spring Boot!");
        return "welcome";
    }

}

 

2. ModelAndView 인스턴스 addObject() 메소드로 데이터 추가

package app.sample.messages;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/messages")

public class MessageController {

    @GetMapping("/welcome")
    //리턴 값을 Http 응답본문으로 처리한다는 뜻 -> 타임리프로 대체
    //: 스프링이 핸들러 반환 값을 뷰 이름으로 사용해 타임리프로 응답 생성
    //@ResponseBody

	public ModelAndView welcome(){

        //ModelAndView 인스턴스 생성
        ModelAndView mv = new ModelAndView("welcome");
        mv.addObject("message", "Hello, Welcome to Spring Boot!");
        
        
        return mv;
    }

}

필터

: 디자인 패턴 중 책임 연쇄 패턴을 구현한 기술

-> 서블릿 도달 전 HTTP 요청에 대한 필터링 작업을 수행한다.

 

감사 요청 필터 AuditingFilter

: 요청 정보를 로그에 기록하는 필터

 

javax.servlet.Filter 인터페이스를 구현한 필터 생성

-> 확장을 위해 Org.springfamework.web.filter.GenericFilterBean 추가

 

AuditingFilter

package app.messages;

import java.io.IOException;
import java.util.Date;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.springframework.web.filter.GenericFilterBean;

public class AuditingFilter extends GenericFilterBean {
  @Override
  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) req;
    long start = new Date().getTime();
    chain.doFilter(req, res);
    long elapsed = new Date().getTime() - start;
    logger.debug("Request[uri=" + request.getRequestURI() + ", method=" +
      request.getMethod() + "] completed in " + elapsed + " ms");
  }
}

# GenericFilterBean을 상속해 doFilter 메소드를 오버라이딩하는 AuditingFilter 메소드

-> 시간을 기록하고 chain.doFilter() 호출해 체인에 실행할 필터가 존재하면 추가 필터 호출

-> 경과 시간 계산하고 디버그 모드로 로그 출력

 

필터 등록 방법

1. web.xml에 filter와 filter-mapping 추가

2. FilterRegistrationBean으로 설정 클래스인 AppConfig에 등록

 

AppConfig 변경

-> FilterRegistrationBean 메서드 추가해 빈으로 등록

package app.sample.messages;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

import java.util.Arrays;

//빈을 정의하는 설정파일이라는 어노테이션
@Configuration
//어노테이션을 스캔할 패키지 알려주는 어노테이션
@ComponentScan("app.sample.messages")
public class AppConfig {
//    //빈 어노테이션을 메소드에 추가함으로써 빈 생성 -> 메소드의 이름이 빈이름
//    @Bean
//    public MessageRepository messageRepository(){
//        return new MessageRepository();
//    }
//
//    @Bean
//    MessageService messageService(){
//        //리포지토리 인스턴스를 서비스 생성자에 전달
//        return new MessageService(messageRepository());
//    }

    //필터 등록하는 메서드
    @Bean
    //빈 등록을 통해 AuditingFilter에 대한 FilterRegistrationBean 생성
    public FilterRegistrationBean<AuditingFilter> auditingFilterRegistrationBean() {
        //필터 등록 과정
        // 1. Filter 인스턴스 생성
        FilterRegistrationBean<AuditingFilter> registration = new FilterRegistrationBean<>();
        
        // 2. setFilter() 메소드로 Filter 설정
        AuditingFilter filter = new AuditingFilter();
        registration.setFilter(filter);
        
        // 3. setOrder() 메소드로 Filter를 체인 내에 배치 -> 순서는 값 오름차순로
        // -> AuditingFilter는 체인의 마지막에 위치
        registration.setOrder(Integer.MAX_VALUE);
        
        // 4. setUrlPatterns() 메소드로 Filter를 등록할 경로 지정 -> /messages/의 모든 요청 처리
        registration.setUrlPatterns(Arrays.asList("/messages/*"));

        return registration;

        //-> 출력 결과에 디버그 로그를 표시하기 위해 application.properties에 디버그 레벨 로그 설정
    }
}

 

application.properties 변경

-> logging.level은 프로퍼티 접두사

-> 그 이후는 디버깅 레벨을 적용할 클래스 경로

logging.level.app.sample.messages.AuditingFilter=DEBUG

 

출력값

 

Request[uri=/messages/welcome, method=GET] completed in 1876 ms

-> 서버가 시작되고 첫 번째 요청이기 때문에 DispatcherServlet 초기화에 시간이 소요된다.

: 서버 예열

Request[uri=/messages/welcome, method=GET] completed in 18 ms

데이터 접근을 위한 스프링 JDBC와 JPA

JDBC

: Java Database Connectivity로 관계형 데이터베이스의 데이터에 접근하는 기술

특정 데이터베이스를 위한 JDBC API 구현체인 JDBC 드라이버를 이용한다.

 

JPA

: Japa Persistence API로 자바 객체의 영속성을 위한 표준화 접근 기술

객체 지향 모델과 관계형 데이터베이스의 테이블의 간격을 줄이기 위해

객체 관계형 매핑 (ORM, Object-Relational Mapping) 메커니즘을 사용해 JPA 표준을 구현한 구현체인 하이버네이트를 주로 사용한다.


JDBC와 JPA

: 서로 다른 문제를 해결하기 위한 API로, JDBC는 데이터베이스와의 상호작용을 위해 JPA는 객체 지향 방식으로 DB에 객체 저장 및 접근을 위해 사용한다.

 

테이블 구조와 모델 구조

DB의 messages 테이블에 메시지 저장

messages 테이블은 id, text, created_date라는 세 개의 칼럼을 가진다.

또한 이에 상응하는 id, text, createdDate라는 필드를 가진 Messages 클래스가 존재한다.

 

-> HTTP POST 메소드로 메시지를 저장하는데 JSON을 이용한 RESTful API를 사용한다.

 


JVM(Java Virtual Machine) 내부의 객체와 DB의 데이터 레코드 간 ORM

객체-관계형 매핑


(1) JDBC API를 직접 사용해 saveMessage() 구현

 

MySQL

create database app_messages;
use app_messages;
commit;

DROP TABLE IF EXISTS `messages`;
CREATE TABLE `messages` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`text` varchar(128) COLLATE utf8_bin NOT NULL DEFAULT '',
`created_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

 

application.properties 변경

: 데이터소스를 인스턴스화하기 위한 매개변수 설정 -> application.properties에 프로퍼티 추가

logging.level.app.sample.messages.AuditingFilter=DEBUG
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root1234
spring.datasource.url=jdbc:mysql://localhost:3306/app_messages?useSSL=false

-> url, username, password, driver-class 객체가 전달해야하는 최소한의 설정

 

MessageRepository 변경

package app.sample.messages;


import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.stereotype.Repository;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;

import javax.sql.DataSource;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.stereotype.Component;


@Component
public class MessageRepository {
    //데이터 소스에서 DB 연결을 얻을 수 있도록 스프링에 인스턴스 주입 요청
    private DataSource dataSource;
    public MessageRepository(DataSource dataSource){
        this.dataSource=dataSource;
    }

    //log 인스턴스 생성
    private static final Log logger = LogFactory.getLog(MessageRepository.class);

    //Message 객체를 매개변수로 받는 저장 메서드 생성 -> DB 연결 후설정
    public Message saveMessage(Message message) {
        Connection c = DataSourceUtils.getConnection(dataSource);
        try {
            String insertSql = "INSERT INTO messages (`id`, `text`, `created_date`) VALUE (null, ?, ?)";
            PreparedStatement ps = c.prepareStatement(insertSql, Statement.RETURN_GENERATED_KEYS);
            // SQL에 필요한 매개변수를 준비한다.
            ps.setString(1, message.getText());
            ps.setTimestamp(2, new Timestamp(message.getCreatedDate().getTime()));
            int rowsAffected = ps.executeUpdate();
            if (rowsAffected > 0) {
                // 새로 저장된 메시지 id 가져오기
                ResultSet result = ps.getGeneratedKeys();
                if (result.next()) {
                    int id = result.getInt(1);
                    return new Message(id, message.getText(), message.getCreatedDate());
                } else {
                    logger.error("Failed to retrieve id. No row in result set");
                    return null;
                }
            } else {
                // insert 실패
                return null;
            }
        } catch (SQLException ex) {
            logger.error("Failed to save message", ex);
            try {
                c.close();
            } catch (SQLException e) {
                logger.error("Failed to close connection", e);
            }
        } finally {
            DataSourceUtils.releaseConnection(c, dataSource);
        }
        return null;
    }
}

 

MessageData 추가

package app.sample.messages;

public class MessageData {
    //JSON으로 만들기 위한 클래스
    private String text;
    public String getText(){
        return this.text;
    }

    public void setText(String text){
        this.text=text;
    }
}

 

MessageController에 PostMapping 추가

    //Post 매핑 수행
    @PostMapping("")
    @ResponseBody
    public ResponseEntity<Message> saveMessage(@RequestBody MessageData data) {
        Message saved = messageService.save(data.getText());
        if (saved == null) {
            return ResponseEntity.status(500).build();
        }
        return ResponseEntity.ok(saved);
    }

 

MessageService 변경

package app.sample.messages;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

@Component
public class MessageService {

    //리포지토리 의존성 주입
    private MessageRepository repository;



//    //리포지토리 인스턴스를 매개변수로 하는 생성자 생성해 의존성 연결
    public MessageService (MessageRepository repository){
        this.repository=repository;
    }
    //리포지토리에 저장하는 메서드
//    public void save(String text){
//        this.repository.saveMessage(new Message(text));
//    }
    //리포지토리에 저장하고 Message 객체를 리턴하도록 변경
    public Message save(String text) {

        return repository.saveMessage(new Message(text));
    }
}

 

application.properties에 datasource 추가

logging.level.app.sample.messages.AuditingFilter=DEBUG
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root1234
spring.datasource.url=jdbc:mysql://localhost/app_messages?useSSL=false

(2) 스프링 JDBC 이용해 saveMessage() 구현 할 경우

: JDBC API 위의 추상화 계층을 이용하는 스프링 JDBC로 JdbcTemplate을 이용해 좀 더 편리하게 연결을 도와준다

-> JDBC API와 상호작용을 도와주고 연결을 관리하는 편리한 워크플로를 제공한다.

 

차이점 

(1) NamedParameterJdbcTemplate 클래스가 JdbcTemplate 객체를 래핑한 클래스로 플레이스 홀더 대신에 명명한 매개변수 사용할 수 있는 기능을 제공한다.

 

(2) DB에서 제공하는 메타 데이터를 이용해 JDBC 작업 단순화를 도와주는 SimpleJdbcInsert와 SimpleJdbcCall 제공해

-> 메타데이터는 DatabaseMetaData 인스턴스를 반환하는 connection.getMeataData() 메소드를 호출해  가져온다

 

(3) SimpleJdbcInsert와 SimpleJdbcCall에는 데이터 베이스에 정의된 테이블과 각 테이블이 가진 칼럼 정보가 포함되어 있어, 스프링이 메타 데이터를 관리한다.


# 스프링 JDBC의 JDBC 작업을 자바 객체로 표현

(1) MappingSqlQuery 객체를 생성

(2) DB 쿼리 실행해 SqlUpdate 객체를 생성해 삽입/업데이트 작업 수행

(3) StoredProcedure 객체 생성해 DB에서 저장 프로시저 호출

(4) 해당 객체들은 재사용 가능하며 스레드 세이프하다.

-> NamedParameterJdbcTemplate 사용 예제

@Component
public class MessageRepository {

  private static final Log logger = LogFactory.getLog(MessageRepository.class);

  private NamedParameterJdbcTemplate jdbcTemplate;

  public MessageRepository(DataSource dataSource) {
    this.jdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
  }

-> 리포지토리에 생성자를 호출해 데이터소스를 전달하고, NamedParameterTemaplate 인스턴스화를 위해 스프링에 DataSource 주입 요청한다.

 

  public Message saveMessage(Message message) {
    GeneratedKeyHolder holder = new GeneratedKeyHolder();
    MapSqlParameterSource params = new MapSqlParameterSource();
    params.addValue("text", message.getText());
    params.addValue("createdDate", message.getCreatedDate());
    String insertSQL = "INSERT INTO messages (`id`, `text`, `created_date`) VALUE (null, :text, :createdDate)";
    try {
      this.jdbcTemplate.update(insertSQL, params, holder);
    } catch (DataAccessException e) {
      logger.error("Failed to save message", e);
      return null;
    }
    return new Message(holder.getKey().intValue(), message.getText(), message.getCreatedDate());
  }

-> 스프링 JDBC를 이용해 saveMessage() 메소드 구현 


(3) 하이버네이트로 saveMessage() 메소드 구현

 

1. 의존성 추가

        <!--ORM기술 기반 스프링 ORM 지원-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
        </dependency>

        <!-- 하이버네이트 ORM 프레임워크-->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
        </dependency>

 

2. Message 객체와 messages 테이블 레코드 매핑

-> 하이버네이트에 매타 데이터 제공

 

JPA 어노테이션

: @Entity, @Table, @Id, @GeneratedValue, @Column, @Temporal ...

 

//하이버네이트에서는 기본 생성자가 반드시 필요하다
//-> DB에서 레코드를 가져올 경우 객체를 재구성해야하기 때문에

 

Message

package app.sample.messages;


import javax.persistence.*;
import java.util.Date;
import java.util.Objects;
@Entity
@Table(name="messages")
public class Message {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "id", nullable = false)
        private Integer id;

        @Column(name = "text", nullable = false, length = 128)
        private String text;

        @Column(name = "created_date", nullable = false)
        @Temporal(TemporalType.TIMESTAMP)
        private Date createdDate;

    //하이버네이트에서는 기본 생성자가 반드시 필요하다
    //-> DB에서 레코드를 가져올 경우 객체를 재구성해야하기 때문에
    public Message(String text) {
        this.text = text;
        this.createdDate = new Date();
    }

    public Message() {

    }

    public Integer getId() {
        return id;
    }

    public String getText() {
        return text;
    }

    public Date getCreatedDate() {
        return createdDate;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Message message = (Message) o;
        return Objects.equals(id, message.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

AppConfig

package app.sample.messages;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.hibernate5.LocalSessionFactoryBean;

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

//빈을 정의하는 설정파일이라는 어노테이션
@Configuration
//어노테이션을 스캔할 패키지 알려주는 어노테이션
@ComponentScan("app.sample.messages")
public class AppConfig {

    //DataSource 의존성 주입
    private DataSource dataSource;

    public AppConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

//    //빈 어노테이션을 메소드에 추가함으로써 빈 생성 -> 메소드의 이름이 빈이름
//    @Bean
//    public MessageRepository messageRepository(){
//        return new MessageRepository();
//    }
//
//    @Bean
//    MessageService messageService(){
//        //리포지토리 인스턴스를 서비스 생성자에 전달
//        return new MessageService(messageRepository());
//    }

    //필터 등록하는 메서드
    @Bean
    //빈 등록을 통해 AuditingFilter에 대한 FilterRegistrationBean 생성
    public FilterRegistrationBean<AuditingFilter> auditingFilterRegistrationBean() {
        //필터 등록 과정
        // 1. Filter 인스턴스 생성
        FilterRegistrationBean<AuditingFilter> registration = new FilterRegistrationBean<>();

        // 2. setFilter() 메소드로 Filter 설정
        AuditingFilter filter = new AuditingFilter();
        registration.setFilter(filter);

        // 3. setOrder() 메소드로 Filter를 체인 내에 배치 -> 순서는 값 오름차순로
        // -> AuditingFilter는 체인의 마지막에 위치
        registration.setOrder(Integer.MAX_VALUE);

        // 4. setUrlPatterns() 메소드로 Filter를 등록할 경로 지정 -> /messages/의 모든 요청 처리
        registration.setUrlPatterns(Arrays.asList("/messages/*"));

        return registration;

        //-> 출력 결과에 디버그 로그를 표시하기 위해 application.properties에 디버그 레벨 로그 설정
    }

    //SessionFactory의 인스턴스 생성하기 위해, 스프링 FactoryBean인 LocalSessionFactoryBean 정의
    @Bean
    public LocalSessionFactoryBean sessionFactory() {
        LocalSessionFactoryBean sessionFactoryBean = new LocalSessionFactoryBean();
        //LocalSessionFactoryBean 생성하기 위한 DataSource 인스턴스를 설정 클래스로 주입
        sessionFactoryBean.setDataSource(dataSource);
        //하비어네티으가 엔티티 클래스를 찾기위해 검색하는 패키지 지정
        sessionFactoryBean.setPackagesToScan("app.messages");
        return sessionFactoryBean;
    }

}

 

MessageRepository

//하이버네이트에서 org.hibernate.Session은 엔티티를 저장하고 불러오는 인터페이스이며,
//하이버네이트 SessionFactory 인스턴스에서 세션을 생성한다.
//SessionFactory의 인스턴스 생성하기 위해, 스프링 FactoryBean인 LocalSessionFactoryBean이 필요하다
//: AppConfig에서 LocalSessionFactoryBean 정의
package app.sample.messages;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MessageRepository {


    private SessionFactory sessionFactory;

    //하이버네이트에서 org.hibernate.Session은 엔티티를 저장하고 불러오는 인터페이스이며,
    //하이버네이트 SessionFactory 인스턴스에서 세션을 생성한다.
    //SessionFactory의 인스턴스 생성하기 위해, 스프링 FactoryBean인 LocalSessionFactoryBean이 필요하다
    //: AppConfig에서 LocalSessionFactoryBean 정의
    
    public MessageRepository(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public Message saveMessage(Message message) {
        Session session = sessionFactory.openSession();
        session.save(message);
        return message;
    }
}

 


관점 지향 프로그래밍 스프링 AOP

: 코드의 중복을 피하고 일괄적으로 시작하게 하는 방법

 

#보안 검사

: 코드의 중복을 막고, API 핸들러에 도달했을 때 이미 통과했다고 하기 위해 중앙에서 보안 검사를 수행하는 것이 바람직하다.

-> 요청에 대한 보안검사를 위한 필터를 생성하는 방법

 

(1) 필터 단계에서 보안 검사

(2) 필터 단계에서 수행하지 않았을 경우 메소드 단계에서 보안 검사 수행

-> AOP 기술을 이용해 구현

 

AOP의 기본 개념

  1. 관심사
  2. 에스팩트
  3. 조인 포인트
  4. 어드바이스
  5. 포인트컷
  6. AOP 프록시
  7. 위빙
  8. @SecurityCheck

AOP 실행 흐름


관심사

AOP에서 보안에 관련된 관심사 또는 목표

: 보안 검사

 

다른 유형의 관심사 

-성능 로깅

-트랜잭션 관리


에스팩트

: 관심사를 모듈화한 것

-여러 클래스에 걸쳐 코드에 분산하는 대신에 관심사를 다루는 로직을 하나의 애스펙트에 넣는다.

 

스프링 AOP에 일반 클래스에 애스펙트 구현 할 수 있으며

-> Aspectj 어노테이션인 @Aspect 어노테이션 적용가능

 

보안 검사 로직을 일반 자바 클래스인 SecurityChecker 애스펙트로 만들 경우

-> 모든 메소드에 보안 검사로직을 추가할 필요 없다.

-> 클래스 내부에 checkSecurity() 메소드에 @Around 어노테이션을 추가해 해당 메소드가 실행할 시점을 지정할 수 있다.

보안 검사에 대한 AOP

: @Around 어노테이션 사용해 런타임 중에 SecurityChecker.checkSecurity() 메소드에 먼저 도달해,

이 메소드 내에서 코드 실행을 진행 유무를 결정

 

실행 할 경우

-> 대상 메소드를 호출, 그 메소드 완료시 checkSecurity()로 돌아간다.

-> 코드 실행 반환

 

실행하지 않을 경우

-> 예외를 던지거나 메소드를 실행하지 않는다.


조인 포인트

조인 포인트는 특정 프로그램이 실행되는 지점을 뜻한다.

-실행 되는 지점에 스프링 AOP에서 조인 포인트 항상 메소드 호출을 나타낸다.

-AOP 구현체의 경우, 필드 접근이나 예외 발생에 대한 조인 포인트도 지원


어드바이스

어드바이스는 SecurityChecker 내의 checkSecurity() 메소드를 뜻한다.

-어드바이스는 특정 관심사를 처리하는 것으로, 보안검사의 경우 보안이 관심사에 해당한다.

 

  1. Before advice
  2. After returning advice
  3. After throwing advice
  4. After advice
  5. Around advice

포인트컷

포인트 컷은 일치하는 여러 조인 포인트의 결합한 것을 뜻한다.

 

@Around 어노테이션 값인 checkSecurity() 어라운드 어드바이스 실행 시점을 지정하는 포인트컷 표현식

@Pointcut("execution (*app.sample.messages .. *.* (*))")

 

@Aspect
@Component
public class Securitychecker {
    @Pointcut("execution(* app.messages..*.*(..))") 
    public void everyMessageMethod() {
    }

    @Around("everyMessageNethod()")
    public Object checksecurity(ProceedingJoinPoint joinPoint) {
 }
}

-> SecurityChecker 애스펙트에서 everyMessageMethod() public 메소드를 생성하고 @Pointcut 어노테이션 적용

 

+) 포인트컷 시그니처만 정의했기 때문에 메소드의 반환값이 없고 비어있다.

-> checkSecurity() 메소드의 @Around 어노테이션에서 이 메소드를 포인트컷 표현식으로 사용

 

스프링 AOP에 어떤 것을 매칭할지 알려주는 PCD(포인트컷 지정자)

->조인 포인트와 일치하는 정규표현식 대신 이 PCD(@annotation)로 특정 어노테이션 적용한 메소드만 매칭

 


AOP 프록시

: @SecurityCheck 섹션의 객체는 SecurityChecker 애스펙트의 대상 객체이거나 어드바이스 객체

 

스프링 AOP에서 런타임동안 SecurityChecker 애스펙트 계약의 이행을 위해 프록시 객체 생성

-> 표준 JDK 동적 프록시로 AOP 프록시 생성하는데 JDK 동적 프록시는 인터페이스 기반으로 프록시 객체 생성

 

-> 대상 클래스가 인터페이스를 구현하지 않을 경우 CGLIB로 대상 클래스를 하위 클래스로 만들어 프록시 생성하므로,  보안 검사 예제에서 CGLIB로 프록시 객체 생성


위빙

위빙은 다른 필수 객체와 애스펙트를 연결해 어드바이스 객체를 생성하는 프로세스를 뜻한다.

 

스프링 AOP의 경우, 런타임 동안 발생

AspectJ의 경우 컴파일 타임 또는 로드 타임에 수행


@SecurityCheck

예제 메시지 앱에 보안 검사 구현

 

-> 보안 검사를 수행하기 위해 모든 메소드에 적용하는 @SecurityCheck 어노테이션 생성

 

SecurityCheck

package app.messages;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SecurityCheck {
}

 

SecurityChecker 애스펙트

package app.messages;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class SecurityChecker {

  private static final Logger logger = LoggerFactory.getLogger(SecurityChecker.class);

  @Pointcut("@annotation(SecurityCheck)")
  public void checkMethodSecurity() {}

  @Around("checkMethodSecurity()")
  public Object checkSecurity (ProceedingJoinPoint joinPoint) throws Throwable {
    logger.debug("Checking method security...");
    // TODO Implement security check logics here
    Object result = joinPoint.proceed();
    return result;
  }
}
  • 일반적인 스프링 빈의 SecurityChecker 애스펙트는 @Aspect 어노테이션을 가지고 있다.
  • <aop:config>요소를 사용하는 XML로 정의한 AOP 설정뿐만 아니라 AspectJ 어노테이션 정의로 AOP 설정 지원 

AOP 적용

  1. SecurityChecker 애스펙트 내부에서 @SecurityCheck 어노테이션을 위한 @annotation PCD와 @Pointcut 어노테이션이 지정된 포인트컷 시그니처 checkMethodSecurity() 생성
  2. checkSecurity() 어드바이스 내부에서 @Around 어노테이션을 사용해 checkMethodSecurity() 포인트컷 표현식과 around advice 명시
  3. application.properties에 로그 메시지를 추가하기 위해 설정
  4. 보안 검사 수행하는 메소드에 @SecurityCheck 어노테이션 적용

 


AOP 실행 흐름

@SecurityCheck 어노테이션을 MesseageService.save() 메소드에 적용해야 MessageController에서 MessageService로 진행한다.

MessageService

package app.sample.messages;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

@Component
public class MessageService {

    //리포지토리 의존성 주입
    private MessageRepository repository;



//    //리포지토리 인스턴스를 매개변수로 하는 생성자 생성해 의존성 연결
    public MessageService (MessageRepository repository){
        this.repository=repository;
    }
    //리포지토리에 저장하는 메서드
//    public void save(String text){
//        this.repository.saveMessage(new Message(text));
//    }
    //리포지토리에 저장하고 Message 객체를 리턴하도록 변경
    @SecurityCheck
    public Message save(String text) {

        return repository.saveMessage(new Message(text));
    }
}

-> 런타임 중에 코드 실행 흐름이 컨트롤러에서 스프링 AOP가 생성한 AOP 프록시 객체로 진행

-> MesseageService가 구현하는 인터페이스가 없기 때문에 스프링 AOP가 CGLIB가 생성한 MessageService 하위클래스의 인스턴스이다.

 

실행 흐름

: AOP 프록시 객체 -> checkSecurity() 어드바이저 -> MesseageService.save() 호출 -> MessageController().saveMessage()

AOP 실행 흐름


스프링이 트랜잭션을 관리하는 방법

스프링 트랜잭션 관리

: 스프링 트랜잭션 기능은 다양한 트랜잭션 API에 대한 추상화 제공하고 롤백을 통해 예외를 선언할 수 있다.

 

전역 트랜잭션 : JTA(Java Transaction API)

-> 애플리케이션 서버가 전역 트랜잭션 관리

-> 관계형 DB와 메시지 큐(JMS)와 같은 다양한 트랜잭션 자원과 함께 동작

 

로컬 트랜잭션 : JDBC API, 하이버네이트 트랜잭션 API, JPA 트랜잭션 API

-> JDBC 커넥션과 관련된 트랜잭션인 로컬 트랜잭션은 자원에 한정적, 다양한 자원과 함께 동작 불가능

 

 

 

스프링 트랜잭션 관리 유형

 

1. 프로그래밍적 트랜잭션 관리

-> TransactionTemplate API /  PlatformTransactionManager API

 

2. 선언적 트랜잭션 관리

-> @Transactional

: 로컬과 전역 모두 가능하며 스프링 APO 프레임워크 기반

 


PlatformTransactionManager

: 트랜잭션 전략 기반 트랜잭션 추상화

-> 서로 다른 트랜잭션 API에 대해 각각의 구현체가  org.springframework.transaction.PlatformTransactionManager인터페이스 확장한다.

 

public interface PlatformTransactionManager {
    Transactionstatus getTransaction(TransactionDefinition definition) throws TransactionException;
    void commit(TransactionStatus status) throws TransactionException;
    void rollback(TransactionStatus status) throws TransactionException;
}

(1) TransactionDefinition으로 TransactionStatus 객체를 얻는다.

(2) TransactionStatus로 커밋 또는 롤백 수행

 

하이버네이트의 구현체

HibernateTransactionManager

JDBC의 구현체

DataSourceTransactionManager

JPA구현체

JpaTransactionManager

JTA구현체

JtaTransactionManager

JMS구현체

JmsTransactionManager


선언적 트랜잭션 관리

: 시간별로 게시된 메시지 통계를 보여주는 보고서를 보여주는 기능 추가

 

요구사항 구현 방법

  1. SQL 쿼리로 통계를 계산
  2. 메시지를 저장한 후에 통계를 업데이트해 DB의 테이블에 저장

 

(1) AppConfig에 트랜잭션 관리 추가

package app.sample.messages;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.hibernate5.HibernateTransactionManager;
import org.springframework.orm.hibernate5.LocalSessionFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;

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

//빈을 정의하는 설정파일이라는 어노테이션
@Configuration
//어노테이션을 스캔할 패키지 알려주는 어노테이션
@ComponentScan("app.sample.messages")
//트랜잭션 관리를 위한 어노테이션
@EnableTransactionManagement
public class AppConfig {

    //DataSource 의존성 주입
    private DataSource dataSource;

    public AppConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

//    //빈 어노테이션을 메소드에 추가함으로써 빈 생성 -> 메소드의 이름이 빈이름
//    @Bean
//    public MessageRepository messageRepository(){
//        return new MessageRepository();
//    }
//
//    @Bean
//    MessageService messageService(){
//        //리포지토리 인스턴스를 서비스 생성자에 전달
//        return new MessageService(messageRepository());
//    }

    //필터 등록하는 메서드
    @Bean
    //빈 등록을 통해 AuditingFilter에 대한 FilterRegistrationBean 생성
    public FilterRegistrationBean<AuditingFilter> auditingFilterRegistrationBean() {
        //필터 등록 과정
        // 1. Filter 인스턴스 생성
        FilterRegistrationBean<AuditingFilter> registration = new FilterRegistrationBean<>();

        // 2. setFilter() 메소드로 Filter 설정
        AuditingFilter filter = new AuditingFilter();
        registration.setFilter(filter);

        // 3. setOrder() 메소드로 Filter를 체인 내에 배치 -> 순서는 값 오름차순로
        // -> AuditingFilter는 체인의 마지막에 위치
        registration.setOrder(Integer.MAX_VALUE);

        // 4. setUrlPatterns() 메소드로 Filter를 등록할 경로 지정 -> /messages/의 모든 요청 처리
        registration.setUrlPatterns(Arrays.asList("/messages/*"));

        return registration;

        //-> 출력 결과에 디버그 로그를 표시하기 위해 application.properties에 디버그 레벨 로그 설정
    }

    //SessionFactory의 인스턴스 생성하기 위해, 스프링 FactoryBean인 LocalSessionFactoryBean 정의
    @Bean
    public LocalSessionFactoryBean sessionFactory() {
        LocalSessionFactoryBean sessionFactoryBean = new LocalSessionFactoryBean();
        //LocalSessionFactoryBean 생성하기 위한 DataSource 인스턴스를 설정 클래스로 주입
        sessionFactoryBean.setDataSource(dataSource);
        //하이버네이트가 엔티티 클래스를 찾기위해 검색하는 패키지 지정
        sessionFactoryBean.setPackagesToScan("app.sample.messages");
        return sessionFactoryBean;
    }


    //하이버네이트 구현체를 위한 메소드생성
    @Bean
    public HibernateTransactionManager transactionManager() {
        HibernateTransactionManager transactionManager =
                new HibernateTransactionManager();
        //세션 팩토리 인스턴스를 트랜잭션 관리자에 설정
        transactionManager.setSessionFactory(sessionFactory().getObject());
        return transactionManager;
    }
}

 

(2) 트랜잭션을 원하는 메서드에 적용

-> MessageService.save()

package app.sample.messages;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Component
public class MessageService {
    //로그 팩토리를 이용해 로그 추가
    private static final Logger log = LoggerFactory.getLogger(MessageService.class);

    //리포지토리 의존성 주입
    private MessageRepository repository;



//    //리포지토리 인스턴스를 매개변수로 하는 생성자 생성해 의존성 연결
    public MessageService (MessageRepository repository){
        this.repository=repository;
    }
    //리포지토리에 저장하는 메서드
//    public void save(String text){
//        this.repository.saveMessage(new Message(text));
//    }
    //리포지토리에 저장하고 Message 객체를 리턴하도록 변경
    @SecurityCheck
    @Transactional(noRollbackFor = { UnsupportedOperationException.class })
    public Message save(String text) {
        Message message = repository.saveMessage(new Message(text));
        //저장되고 롤백될 때 확인할 수 있도록 로그에 디버그 메시지 추가
        log.debug("New message[id={}] saved", message.getId());
        updateStatistics();
        return message;
    }

    //통계 업데이트 메서드지만, 예외처리만 수행하는 메서드 -> 트랜잭션 롤백 수행
    private void updateStatistics() {
        throw new UnsupportedOperationException("This method is not implemented yet");
    }
}

 

(3) application.properties 변경

1. 스프링 부트가 기본값으로  JPA 인터페이스 엔티티 매니저를 현재 스레드에 등록하는 OpenEntityManagerInViewInterceptor를 생성하므로, 하이버네이트의 SessionFactory를 사용하도록 하기 위해 변경

2. MessageService의 디버그 레벨 로깅 활성화해서 로그 출력하도록 변경

logging.level.app.sample.messages.MessageService=DEBUG
spring.jpa.open-in-view=false

 

하지만 원하는대로 예외처리는 수행했지만 롤백되지 않았다.

-> 선언적 트랜잭션 관리를 살펴보면,

 

스프링의 트랜잭션 어드바이저가 checkSecurity() 어드바이저로부터 컨트롤 유입해 트랜잭션 생성

-> 컨트롤이 트랜잭션 어드바이저로 다시 흐룬 후에 트랜잭션 커밋하거나 롤백

 

로컬 트랜잭션의 경우 JDBC 커넥션과 관련이 이으므로 트랜잭션 생성시 데이터베이스 연결이 DataSource에서 가져온다.

 

트랜잭션 제어 흐름

리포지토리의 저장 메소드 내부에서 세션팩토리로부터 하이버네이트 세션을 연다

-> 해당 세션은 DataSource에서 JDBC 커넥션을 얻는다.

-> 트랜잭션 어드바이저가 획득한 연결과 다른 커넥션이기 때문에 롤백이 불가능하다.

 

(4) 리포지토리의 저장 메서드를 현재 세션으로 변경

package app.sample.messages;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MessageRepository {

    //세션팩토리 인스턴스 주입
    private SessionFactory sessionFactory;

    //하이버네이트에서 org.hibernate.Session은 엔티티를 저장하고 불러오는 인터페이스이며,
    //하이버네이트 SessionFactory 인스턴스에서 세션을 생성한다.
    //SessionFactory의 인스턴스 생성하기 위해, 스프링 FactoryBean인 LocalSessionFactoryBean이 필요하다
    //: AppConfig에서 LocalSessionFactoryBean 정의
    public MessageRepository(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public Message saveMessage(Message message) {
        //세션 인스턴스 획득
        //JDBC의 커넥션
        //Session session = sessionFactory.openSession();
        //현재 트랜잭션 어드바이저가 획득한 세션
        Session session = sessionFactory.getCurrentSession();
        //세션에 메시지 객체 저장 -> 객체의 id를 하이버네이트가 관리
        session.save(message);
        return message;
    }
}

-> 스프링 트랙잭션 관리가 지원하는 롤백 규칙을 통해 롤백을 원하는 곳과 원하지 않는 곳을 예외 처리로 선언 가능

 

(5) UnsupportedOperationException에 대해 롤백되지 않도록 변경

    //리포지토리에 저장하고 Message 객체를 리턴하도록 변경
    @SecurityCheck
    //UnsupportedOperationException에 대해 롤백되지 않도록 
    @Transactional(noRollbackFor = { UnsupportedOperationException.class })
    public Message save(String text) {
        Message message = repository.saveMessage(new Message(text));
        //저장되고 롤백될 때 확인할 수 있도록 로그에 디버그 메시지 추가
        log.debug("New message[id={}] saved", message.getId());
        updateStatistics();
        return message;
    }

 


스프링 부트

스타터와 자동 설정 메커니즘을 이용해 애플리케이션 구축하는 스프링 부트

 

spring-boot-starter-parent 프로젝트를 상속받아, 의존성 버전을 지정하지 않고 메이븐과 의존성 관리를 이용한다.

-> 버전을 직접 지정하는 것도 가능

 

스타터를 의존성에 추가하면 스프링 부트가 필요한 빈을 초기화 할 수 있다

-> JDBC 사용하기 위해서 srping-boot-start-jdbc 스타터 추가시 application.properties를 보고 스프링이 DataSource 빈을 생성


스프링 부트 스타터

의존성 디스크립터 세트로 pom.xml이라는 의존성 디스크립터 파일만 포함

 

이름 규칙

spring-boot-start-*

: '*'은 jdbc, web 같은 기술

 

코어 스타터

spring-boot-starter

: 모든 스타터가 의존하고, spring-boot-autoconfigure 에서 자동 설정 기능을 제공하는 코어

 

JDBC 스타터

spring-boot-starter-jdbc

: spring-boot-starter, HikariCP, srping-jdbc를 포함하는 스타터로 메이븐이라는 빌드 시스템이 처리


Autoconfiguration

스프링 부트 스타터의 자동 설정 기능으로 스프링 부트 공식 스타터의 자동 설정 구현체

 

@SpringBootApplication은 자동설정 트리거로 세가지 어노테이션 조합

  • @SpringBootConfiguration
  • @EnableAutoConfiguration
  • @ComponentScan

적용 순서

  1. @EnableAutoConfiguration으로 spring-boot-autoconfigure가 애플리케이션 클래스 로더로 META_INF/spring.factories 리소스 파일에서 메타 데이터를 가져옴
  2. 애플리케이션 의존성의 모든 spring.factories 파일 로드
  3. spring.factories 메타데이터 파일에서 자동설정 org.springframework.boot.autoconfigure.EnableAutoConfiguration 프로퍼티를 통해 연결

자동설정 훅을 포함하는 spring-boot-autoconfigure의 spring.factories의 메타 데이터 파일

EnableAutoConfigure, AopAutoConfigureation, DataSourceAutoConfiguration

-> 키-값 쌍으로 연결되어 있는 @Configuration 어노테이션이 달린 자바 클래스

 

자동 설정 클래스 내부에서 조건부 설정된 빈을 생성

-> DataSource가 있으면 더이상 생성하지 않고, 자동 설정

 

조건부 어노테이션

  • @Conditional
  • @ConditionalOnClass
  • @ConditionalOnMissingBean

 

-> 해당 조건부 어노테이션을 이용해 자동 설정 적용 조건 지정

: DataSource의 경우 클래스 경로에서 클래스 사용시에만 발생, DataSource 타입 정의 빈이 없으면 커넥션 풀 Hikari 초기화

 


 

반응형