
。 JPA 또는 Spring Data JPA를 활용 시 DB에 Mapping하여 연결 및 쉽게 상호작용할 수 있다.
- 필요한 Framework 사전 준비
。spring-boot-starter-data-jpa,h2
- dependency 정의
。pom.xml에 다음 구문을 추가 후 Maven을 재실행.<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <!--jar파일의 일부에 h2 db가 포함되지 않도록 scope를 설정.--> <scope>runtime</scope> </dependency>
- h2 db 사용 설정
H2-DB 초기설정
h2-DB관련Spring Security추가 설정 Spring Security
。localhost:8080/h2-console로 진입시, 다음 오류가 발생하면서 Application에서 연결을 거부.
。403 Forbidden: Client가 요청한 Resource에 대해 접근 권한이 없음을 지시
▶h2-consoleurl 접속 시Spring Security의CSRF Protection에 의해 접근금지.
。CSRF protection비활성화,h2-console Frame활성화 설정
▶Spring Security에서는 default로CSRF protection활성화,Frame비활성화로 설정되어있음.
Spring Security의FilterChain설정
。SecurityFilterChain에 대한 전반적인 설정을 구현
▶ 기본기능( Default )과 추가기능( Additional )을 모두 구현해야한다.
。@Configuration이 선언된WebSecurityConfiguration을 수행하는 Class에 다음SecurityFilterChain를 반환하는FilterChain @Bean Method를 선언.
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // 인증된 Http Request에 대해 승인처리 설정. http.authorizeHttpRequests( auth->auth.anyRequest().authenticated() ); // 인증되지않은 Http Request에 대해 로그인 양식 도출. http.formLogin(Customizer.withDefaults()); // CSRF 비활성화 http.csrf(csrf->csrf.disable()); // FrameOption 비활성화 http.headers(header->header.frameOptions(frameOption->frameOption.disable())); // SecurityFilterChain instance를 Spring bean으로 반환 return http.build(); }。
HttpSecurityinstance를 매개변수로하여SecurityFilterChaintype의Spring Beaninstance를 생성 및 return하는@Beanmethod를 구현.
▶@Bean method에서는HttpSecurityinstance를 이용하여SecurityFilterChain의 기본기능( Default )과 추가기능( Additional )을 모두 구현 후HttpSecurityinstance를 반환.
。Filter Chain @Bean method에throws Exception이 구현된 이유?
return되는http.build()에서 Exception이 발생하므로.
。다음 구문을Spring Security설정에 관여하는 Class(SpringSecurityConfig.java)에 추가 할 경우,h2-console에 진입이 가능해짐!@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http.authorizeHttpRequests( auth->auth.anyRequest().authenticated() ) .formLogin(Customizer.withDefaults()) .csrf(csrf->csrf.disable()) .headers(header->header.frameOptions(frameOption->frameOption.disable())) .build(); }▶ 다음처럼
HttpSecurityinstance의 Configuration 연쇄적으로 연결하여SecurityFilterChain구현이 가능하다.
- 기본기능 : 인증이 된 모든
Http Request에 대해 승인
。Spring Application에 전달되는 모든HTTP Request에 대해서 인증된HTTP Request에 대해서만 승인.http.authorizeHttpRequests( auth->auth.anyRequest().authenticated() );。특정
HttpSecurity객체에 대하여 input된 인증이 된 모든HttpRequest에 대하여 승인처리 설정.
▶ 구현하지 않는 경우, 모든HTTP Request에 대해서 거절하면서 Authentication을 비활성화
- 기본기능 : 인증되지 않은 http요청에 대해 로그인 양식 표현
http.formLogin(Customizer.withDefaults());
。formLogin()에 관련된 모든 기본값인withDefaults()를 설정.
。withDefaults()는 Customizer class의 static method로서 정의됨.
- 추가기능 :
CSRF비활성화
http.csrf().disable();
- 추가기능 :
h2-console frame옵션 활성화
http.headers().frameOptions().disable();
。header의FrameOption을 Disable하면h2-console frame을 사용 가능.
Spring Security에 설정될SecurityFilterChaininstance 생성 및Spring Beaninstance으로 return
HttpSecurity객체.build():
。설정된HttpSecurityinstance로SecurityFilterChaininstance를 생성하여 반환.
▶@Bean에 의해Spring Beaninstance으로 반환.
。이후localhost:8080/h2-console접속 시 Spring의 로그인 양식이 도출.
localhost:8080/login: Spring 로그인 Page 도출
localhost:8080/logout: 기존 로그인된 자격증명을 로그아웃.
▶ HTML의<a class="nav-link" href="/logout">으로 간단하게 로그아웃 버튼을 생성할 수 있다.
WebSecurityConfiguration:
。Spring Security의 웹보안 Configuration을 담당하는@ConfigurationClass.
▶@Bean Method를 생성 후SecurityFilterChaininstance를 활용하여Authentication,Authorization,CSRF Protection등의 보안정책을 해당Configuration Class에서 설정이 가능.
SpringBootWebSecurityConfiguration
。Servlet Application을 보호하는 역할의 웹보안Configuration Class.
▶ 웹보안을 위한 Default로 설정된Configuration Class.
。Spring은 기본적으로 명시적으로 인증된HTTP Request에 대해서만 허용 및formLogin()과httpBasic()이 기본적으로 활성화 설정되도록 설정됨.
▶CSRF Protection은 비명시적으로 활성화 설정.
Spring JPA를 활용해 Spring Bean을 DB Table에 Mapping하기 JPA 관련 학습 내용
。@Entity와JpaRepository<DBEntity class, id data Type>활용.
。Spring JPA:
SQL,EntityManager을 사용하지 않는다.
▶EntityManager대신JpaRepository<Entity class, ID>Interface를 상속하여 활용.
TodoClass를 Spring Bean으로서DB Table과 Mapping하기
。@GeneratedValue: Primary Key를 자동할당.
。Spring Boot Auto Configuration에 의해@Entity가 식별 될 경우, 자동으로H2-DB에DB Table을 생성!@Entity(name = "TodoABC") // TodoABC 이름의 Table 생성. public class Todo { public Todo(){}; public Todo(int id, String username, String description, LocalDate targetDate, boolean completed) { super(); this.id = id; this.username = username; this.description = description; this.targetDate = targetDate; this.completed = completed; } @Id @GeneratedValue // Pk값을 자동 할당. private int id; @Column(name="userID") // 생성될 DB Table의 column명 변경. private String username; @Size(min = 10, message="Enter at least 10 characters") private String description; private LocalDate targetDate; private boolean completed;
。Application 구동 시 초기 DB Table을 정의하는schema.sql을 작성하지 않아도Spring Auto Configuration에 의해h2-DB에DB Table이 자동생성.
。생성될 Table의 이름과 Column의 이름을 임의 변경 가능!
Spring Bean의 변수명에 대문자가 존재하는 경우 앞에_로 지시.
ex) targetDate => TARGET_DATE
- DB Table에 삽입할 초기 데이터를 정의하기
。schema.sql:
Application 초기화 시 초기 정의될 DB Schema를 정의하는 파일.
Schema: DB 구조로서Table, View, Index등의 객체를 정의.
。data.sql:
Application 초기화 시 정의된 DB Table에 삽입할 초기 데이터를 정의하는 파일
。실행 순서 :schema.sql->data.sql
src/main/resources에 초기화 데이터가 작성된data.sql을 작성insert into todoabc(id,userid,description,target_date,completed) values(10001,'wjdtn747','Get AWS Certified',CURRENT_DATE(),false), (10002,'wjdtn2','Get Azure Certified',CURRENT_DATE(),false), (10003,'wjdtn3','Get GCP Certified',CURRENT_DATE(),false), (10004,'wjdtn4','Get DevOps Certified',CURRENT_DATE(),false);
application.properties에 다음 구문을 추가하기
spring.jpa.defer-datasource-initialization=true
。기본적으로 Spring에서는@Entity가 선언된 Class를 처리되기 이전(orhibernate초기화 이전)에DataSource(sql등 )를 우선 실행하며, 이는Spring Auto Configuration에 의해DB Table이 생성되기 이전data.sql이 먼저 실행하게되어 오류가 발생하게 된다.
▶ 해당 구문을 선언할 경우,DataSource의 초기화를 지연하면서 DB table이 생성된 이후(hibernate초기화 이후 )에data.sql이 실행되도록 변경함.
▶ 다음처럼data.sql에 의해 초기 데이터가 설정되게된다.
。H2 DB는In-MemoryDB 이므로, 서버 재실행 시 초기 데이터로 초기화.
Spring JPA의JpaRepository기능을 활용하여Todoinstance를 추가, 삭제, 검색 등의 작업 구현하기.
。기존listTodos.jsp는TodoService.java의Static List와 상호작용.
▶ 추가적으로H2 DB와 상호작용하는 기능 구현하기.
。Static List를 활용한 Spring Data JPA 참고하기
JpaRepository<DBEntity class, id data Type>를 상속하는 Interface 생성.
。Field명으로 검색하는Custom Method구현하기// TodoJpaRepository.interface import org.springframework.data.jpa.repository.JpaRepository; public interface TodoJpaRepository extends JpaRepository<Todo,Long> { }。
JpaRepository는 DB의Id로 검색하는findById()는 기본적으로 제공하지만, Field명으로 검색하는 기능은 구현이 되지 않았으므로, Custom Method를 구현.import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface TodoJpaRepository extends JpaRepository<Todo,Long> { // Todo Class의 field명으로 DB Table의 Data를 조회하는 Custom Method List<Todo> findByusername(String username); }。
JpaRepository를 상속한 Interface에 생성할 Custom Method를 서식.
。Custom Method작성 시SpringDataJpaInterface 내에 명명 규칙을 지켜서 작성.
▶ 명명규칙 :findByName1()의 매개변수의 이름은 반드시DB EntityClass의 Field명(name1)과 동일.
▶Custom Method의 매개변수를Spring Bean의 Field명과 동일하게 설정 시Spring Data JPA가 자동으로 검색
。Custom method작성 시SpringDataJpaInterface 내에서 명명 규칙을 지켜서 작성.
- 기존의
Static List가 아닌 DB Table과 상호작용하는Controller class구축하기.
。기존의TodoController.java를 재활용.import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.SessionAttributes; import java.time.LocalDate; import java.util.List; @Controller @SessionAttributes("name") public class TodoControllerJpa { // JpaRepository를 상속받은 SpringDataJpa interface의 객체 생성 // @Autowired는 생성자의 인수를 통해 설정됨. private TodoRepository todoRepository; @Autowired public TodoControllerJpa(TodoRepository todoRepository){ this.todoRepository = todoRepository; } @RequestMapping("list-todo") public String ListAllTodos(ModelMap map){ String userName = getLoggedInUserName(map); // DB Table에서 data를 Todo(= Bean )의 username 필드의 이름으로 검색하여 가져옴. List<Todo> todos = todoRepository.findByUsername(userName); map.addAttribute("todos",todos); return "listTodos"; } private static String getLoggedInUserName(ModelMap map) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); return authentication.getName(); } @RequestMapping(value = "add-todo", method= RequestMethod.GET) public String ShowNewTodoPage(ModelMap model){ String userName = getLoggedInUserName(model); Todo todo = new Todo(0,userName,"",LocalDate.now().plusYears(1),false); model.put("todo",todo); return "Todo"; } @RequestMapping(value = "add-todo", method= RequestMethod.POST) public String addTodoPage(ModelMap model ,@Valid Todo todo, BindingResult result){ if(result.hasErrors()){ return "Todo"; } String username = getLoggedInUserName(model); todo.setUsername(username); todoRepository.save(todo); return "redirect:list-todo"; } @RequestMapping("delete-todo") public String DeleteTodo(@RequestParam int id){ todoRepository.deleteById(id); return "redirect:list-todo"; } @RequestMapping(value = "update-todo", method=RequestMethod.GET) public String showUpdateTodoPage(@RequestParam int id, ModelMap model){ Todo todo = todoRepository.findById(id).get(); model.put("todo",todo); return "Todo"; } @RequestMapping(value = "update-todo", method=RequestMethod.POST) public String UpdateTodo(@RequestParam int id, ModelMap model , @Valid Todo todo, BindingResult result){ todoRepository.deleteById(id); if(result.hasErrors()){ return "Todo"; } String username = getLoggedInUserName(model); todo.setUsername(username); todoRepository.save(todo); return "redirect:list-todo"; } }
JpaRepository를 상속한 Interface의 instance를 생성 후생성자 Dependency-Injection를 통해@Autowired하여 의존성을 주입// JpaRepository를 통한 DB Table과 상호작용 private TodoJpaRepository todoJpaRepository; // Static List와 상호작용 private TodoService todoService; // 생성자기반 Dependency Injection. // @Autowired public TodoController(TodoService todoService, TodoJpaRepository todoJpaRepository) { this.todoService = todoService; this.todoJpaRepository = todoJpaRepository; }
- 기존 Controller Method에
TodoJpaRepositoyinterface에서 정의한 Custom method 활용하기.@RequestMapping("list-todo") public String ListAllTodos(ModelMap model){ String username = getLoggedinUsername(model); List<Todo> todos = todoJpaRepository.findByusername(username); model.put("todos",todos); return "listTodos"; } private String getLoggedinUsername(ModelMap model){ Authentication auth = SecurityContextHolder.getContext().getAuthentication(); return auth.getName(); }。해당 Custom Method(
todoRepository.findByUsername(userName))는DBTable에서 Data를 Spring Bean (Todo) instance의 Field명(username)으로Entity를 검색하여Todoinstance로 가져온다.
。getLoggedinUsername(): 실행중인thread의SecurityContext의Authenticationinstance에서username를 가져오는 Method 생성. Authention
。Custom Method를 통해DB Table에서"wjdtn747"의username으로 data를 검색하여 가져온 Spring Bean( =Todo) instance를 통해 Data가 도출.
- DB Table에 Spring Bean instance를 추가하는 Method 구현하기.
。기존Static List에Spring Bean을 추가할 때, 모든 field를 각각 인수로 받아 생성하여 추가.
。JpaRepository는Spring BeanClass의 Field를 받는 method가 존재하지 않으므로, Spring Bean의 모든 Field의 값을 사전에 설정한 후save()를 통해 DB Table에 추가.@RequestMapping(value = "add-todo", method = RequestMethod.POST) public String addTodo(ModelMap model, @Valid Todo todo, BindingResult result){ if (result.hasErrors()) { System.out.println(todo.getId()); return "addTodo"; }else { String username = getLoggedinUsername(model); todo.setUsername(username); todoJpaRepository.save(todo); return "redirect:list-todo"; } }。
addTodo.jsp에서POST로<form>Data 전송 시<form:form method="post" modelAttribute="todo">를 통해Controller Method addTodo()의 매개변수Todo todo로Data Binding되어 Model 객체로서 선언 후username를 지정하여H2-DB의todoabcDB에 추가.
▶Spring MVC에 의해<form:form>과Controller Method의 매개변수todoSpring Bean instance를 Model 객체로서 Data Binding 되었으므로,addTodo.jsp에서 작성한<form:input type="text" path="description" required="required"/>의 내용이todo의descriptionfield에 그대로 저장되어있음.
▶addTodo.jsp에서<form:form>에서 입력된 값이 그대로 Spring Bean으로 Binding된 후save()를 통해TodoClass와@Entity로 Binding된 DB의todoabcTable로 추가됨.
- DB Table의
Todoinstance를 삭제하는 Method 구현
。JpaRepository객체.deleteById(id)method 활용.@RequestMapping("delete-todo") public String deleteTodo(@RequestParam int id){ todoJpaRepository.deleteById(id); return "redirect:list-todo"; }
- DB Table의
TodoInstance를 Update하는 Method 구현
。JpaRepository객체.save(DBEntity)는INSERT와UPDATE를 수행.
▶ 기존DB Entity는UPDATE, 새로운DB Entity는INSERT
▶DELETE후INSERT를 수행하는 경우 동시성 문제가 발생하므로 주의.@RequestMapping(value="update-todo",method=RequestMethod.GET) public String UpdateTodo(@RequestParam int id, ModelMap model){ Todo todo = todoJpaRepository.findById(id).get(); model.put("todo",todo); return "addTodo"; } @RequestMapping(value="update-todo",method=RequestMethod.POST) public String UpdateTodo(@RequestParam int id,ModelMap model, @Valid Todo todo, BindingResult result){ todoJpaRepository.save(todo); if (result.hasErrors()) { return "addTodo"; } String username = getLoggedinUsername(model); todo.setUsername(username); return "redirect:list-todo"; }。
GET:addTodo.jsp는<form:form method="post" modelAttribute="todo">로 인한 Data Binding 될Model객체로서의 Spring Bean을 요구하므로,JpaRepository객체.findById(id).get()으로DBTable에서 Update할 Spring Bean을 가져와서Model객체로 적용.
。POST:
todoJpaRepository.deleteById(id);: DB Table의 수정할 기존에 존재하는Entity를 삭제
▶ 이후 Controller methodaddTodo()와 동일한 방식으로Todoinstance를todoJpaRepository.save(todo)를 통해 추가.
- H2 DB와 Todo List가 잘 연동되는지 확인.
JPA사용 시, Background에서 어떤 SQL query가 생성되는지 확인하는 방법
。JpaRepository<>를 활용한Spring JPA의 method(save(),findById())의 Query 확인용도로도 사용됨.
。다음 구문을application.properties로 추가
spring.jpa.show-sql=true
。웹페이지 상에서 DB와 관련된 작업을 수행하면 콘솔창에 해당 작업의 query문이 표시됨.
▶ Spring Data JPA 활용 시 사용자가 SQL을 사용하진 않지만, 백그라운드상에서는 SQL이 활용됨.
- Docker :
。Container를 사용해서 Application을 신속하게 구축, 테스트 및 배포할 수 있는 오픈소스 SW 플랫폼.
。동일한 Docker Container일 경우 다른 Linux에서도 동일한 실행 환경을 제공하는 Tool. => 가상머신과 유사
Container:
。개발자가 Library, 종속성 등 필요한 모든 것을 이용하여 Application을 Packaging하여 하나의 Package로 배포할 수 있게하는 역할을 수행.
▶ OS 없이, Host의 Resource를 그대로 사용하는 Application
。서버 운영을 위한 Library만 Image에 포함하여 설치하므로, 경량화됨.
▶ Image : Object , Container : Instance
- Docker를 이용해서 MySQL 실행
- 다음 구문을 Powershell에 입력 후 실행하기.
。MySQL에 속한 Container를 생성하는 구문
。env로 수많은 환경변수가 설정되어있음.docker run --detach --env MYSQL_ROOT_PASSWORD=dummypassword --env MYSQL_USER=todos-user --env MYSQL_PASSWORD=dummytodos --env MYSQL_DATABASE=todos --name mysql --publish 3306:3306 mysql:8-oracle
docker container ls:
모든 Docker Container를 표시.
- 포트 3306 관련 오류 발생시 입력.
netstat -ano | findstr 3306
PID 번호 조회 후 입력
taskkill /pid PID번호 /f
ex) PID번호 : 6864 일 경우
taskkill /pid 6864 /f- Docker 코드 설명
docker run --detach : --env MYSQL_ROOT_PASSWORD=dummypassword --env MYSQL_USER=todos-user --env MYSQL_PASSWORD=dummytodos --env MYSQL_DATABASE=todos --name mysql --publish 3306:3306 mysql:8-oracle。
docker run: 해당 명령어로 mysql 실행
。MYSQL_ROOT_PASSWORD: DB의 ROOT PW 설정
。MYSQL_USER: 사용자 계정 ID
。MYSQL_PASSWORD: 사용자 계정 PW
。MYSQL_DATABASE: 설정된 이름의 DB Schema를 생성
。name: Docker Container 이름 설정
。publish 3306:3306: Container를 3306 포트로 실행.
=> 포트를 할당하는 이유 ? :
OS에서 뭔가를 실행할 경우 포트를 할당해야하므로.
。mysql:8-oracle: MySQL Image의 이름과 버전을 지시하며 해당8-oracle태그는 아무 OS(Linux, Mac, Windows)에 관계없이 실행가능.- application.properties에 다음을 입력
- h2 DB 관련 설정은 모두 비활성화.
spring.jpa.hibernate.ddl-auto=update spring.datasource.url=jdbc:mysql://localhost:3306/todos spring.datasource.username=todos-user spring.datasource.password=dummytodos spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.hibernate.ddl-auto:
。h2가 아닌 다른 DB에서 Bean에 대응하는 Table을 자동으로 생성할 수 있게 설정.
=> Spring Boot는 h2 DB에 대해서만 자동으로 Table을 생성하므로 따로 설정해야함.
=> 해당 구문 선언 시 Application을 실행할 때 현재@Entity가 선언된 Bean(= Entity) 기반으로 DB Schema가 구축됨.spring.datasource.url: MySQL DB가 실행되는 URL을 정의
。URL 정의시 jdbc로 연결하므로 http를 작성하지 않고, Docker Container에서 정의한 포트번호, DB이름을 입력해야함.
ex )jdbc:mysql://localhost:포트번호/DB이름spring.datasource.username:
Docker Container에서 정의한 사용자 계정 ID 입력spring.datasource.password:
Docker Container에서 정의한 사용자 계정 PW 입력spring.jpa.properties.hibernate.dialect: dialect(방언) 설정.
。ORM은 객체 Mapping으로 자동으로 query를 작성하는데, 수 많은 DBMS 종류마다 Query문이 각개 다르므로, 이를 지시하도록 DB 유형을 지정.- pom.xml에 MySQL dependency를 추가.(Spring Boot 3.1 이상 사용시)
。기존 h2 database의 dependency는 삭제한다.<dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> </dependency>- Application과 MySQL이 연동되었는지 확인하기
。localhost:8080/ 으로 진입 후 list-todo에서 todo를 생성하고 MySQL에서 생성되었는지 확인.
- 관리자권한으로 Powershell을 실행한 후 mysqlsh 입력.
- MySQLShell에서
\connect todos-user@localhost:3306입력한 후 PW를 입력하기.
。DB사용자 이름을 통해 3306 포트로 실행되는 DB로 연결.\use todos를 입력한 후\sql을 입력하여 sql 구문을 통해 입력된 데이터를 조회 가능.
。\use todos: 해당 DB schema 사용 선언.
。다음처럼 Add Todo를 통해 업로드된 데이터를 Mysql DB에서 조회가 가능하다!- DB에 생성된 데이터는 영속성이므로, Application을 재실행해도 임의로 추가한 Todo가 다시 나타난다.