241107 RESTful / AOP

241107 에이콘 아카데미 수업을 기반하여 작성되었음을 알립니다.

RESTful

RESTful H2 데이터베이스로 서버 사이드 만들기!
HTML화면은 각자 만들어보기!

RESTful 실습 (H2)

dependencies

H2 Database / Lombok / Thymeleaf / Spring Web / Spring Boot DevTools / Spring Data JPA

application.properties

spring.application.name=sprweb37todolist

server.port=8888
spring.thymeleaf.cache=false

#H2DB
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:file:C:/work/db/mydata
spring.datasource.username=james
spring.datasource.password=

#jpa
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

#h2 console
spring.h2.console.enabled=true
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.hibernate.ddl-auto=update

Entity

package pack.model;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Data // 엔티티가 1개일 때는 @Data를 써줘도 무방함, 순환참조 걸릴 일이 없음
@NoArgsConstructor
@AllArgsConstructor
public class TodoEntity { // 테이블명은 todo_entity로 만들어짐
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;
	
	@Column(nullable = false)
	private String title;
	
	@Column(nullable = false, name = "todoOrder")
	private Integer order; // H2 데이터베이스에서 order는 키워드임
	
	@Column(nullable = false)
	private Boolean completed;
}

FormBean

package pack.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class TodoRequest { // 폼빈
	private String title;
	private Integer order;
	private Boolean completed;
}

DTO

package pack.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class TodoResponse { // DTO
	private Integer id;
	private String title;
	private Integer order;
	private Boolean completed;
	
	public TodoResponse(TodoEntity todoEntity) { //toDto, JPA 사용시 꼭 걸고하는게 좋음!, 생성자 사용, 꼭 메소드가 아니어도 괜찮다.
		id = todoEntity.getId();
		title = todoEntity.getTitle();
		order = todoEntity.getOrder();
		completed = todoEntity.getCompleted();
	}
}

Repository

package pack.model;

import org.springframework.data.jpa.repository.JpaRepository;

public interface TodoRepository extends JpaRepository<TodoEntity, Integer> {

}

Service

package pack.model;

import java.util.List;

import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

import lombok.AllArgsConstructor;

@Service
@AllArgsConstructor // final 선언 시 AllArgs 받을 생성자 필요
public class TodoService {
	private final TodoRepository todoRepository;
	
	// todo 리스트에 데이터 추가
	public TodoEntity add(TodoRequest request) { // 폼빈 > toEntity > insert
		TodoEntity todoEntity = new TodoEntity();
		todoEntity.setTitle(request.getTitle());
		todoEntity.setOrder(request.getOrder());
		todoEntity.setCompleted(request.getCompleted());
		return todoRepository.save(todoEntity);
	}
	
	// 항목 조회
	public TodoEntity searchById(Integer id) {
		System.out.println("id : " + id);
		return todoRepository.findById(id)
				.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
		// 있으면 리턴, 없으면 orElseThrow로 에러 처리!
	}
	
	// 전체 조회
	public List<TodoEntity> searchAll() {
		return todoRepository.findAll();
	}
	
	// 수정
	public TodoEntity updateById(Integer id, TodoRequest request) {
		TodoEntity todoEntity = searchById(id); // 원본
		
		if(request.getTitle() != null) {
			todoEntity.setTitle(request.getTitle());
		}
		if(request.getOrder() != null) {
			todoEntity.setOrder(request.getOrder());
		}
		if(request.getCompleted() != null) {
			todoEntity.setCompleted(request.getCompleted());
		}
		
		return todoRepository.save(todoEntity);
	}
	
	// 부분 삭제
	public void delelteById(Integer id) {
		todoRepository.deleteById(id);
	}
	
	// 전체 삭제
	public void delelteAll() {
		todoRepository.deleteAll();
	}
}

Controller

package pack.controller;

import java.util.List;
import java.util.stream.Collectors;

import org.springframework.http.ResponseEntity;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import lombok.AllArgsConstructor;
import pack.model.TodoEntity;
import pack.model.TodoRequest;
import pack.model.TodoResponse;
import pack.model.TodoService;

@CrossOrigin // 도메인 동일 정책을 벗어나기 위함, 모든 도메인에서 요청을 허용하여 서버에 접근할 수 있다. 방화벽에 열려있는 경우에만!
@RestController
@RequestMapping("/")
@AllArgsConstructor
public class TodoController {
	private final TodoService service;
	
	// 추가
	@PostMapping
	public ResponseEntity<TodoResponse> create(@RequestBody TodoRequest request) {
		// ResponseEntity : 반환값에 상태코드와 응답메세지를 주고 싶을 때 사용
		// 응답을 보낼 시 헤더 및 상태코드를 직접 다룰 때 사용
		// ResponseEntity.ok(), ResponseEntity.badRequest().build(), ResponseEntity.created() 이런 것들이 있다.
		// System.out.println("create");
		// return null;
		if(ObjectUtils.isEmpty(request.getTitle())) // 타이틀이 없으면
			return ResponseEntity.badRequest().build(); // 에러 확인 후 에러 반환
		
		if(ObjectUtils.isEmpty(request.getOrder())) // 오더가 없으면
			request.setOrder(0); // 초기치 넣어줌
		
		if(ObjectUtils.isEmpty(request.getCompleted())) // 컴플리트가 없으면
			request.setCompleted(false); // false 반환
		
		TodoEntity result = service.add(request);
		
		System.out.println("insert : " + ResponseEntity.ok(new TodoResponse(result)));
		return ResponseEntity.ok(new TodoResponse(result));
	}
	
	// 전체 조회
	@GetMapping
	public ResponseEntity<List<TodoResponse>> readAll() {
		List<TodoEntity> list = service.searchAll();
		List<TodoResponse> responses = 
				list.stream().map(TodoResponse :: new).collect(Collectors.toList());
		System.out.println("select all : " + ResponseEntity.ok(responses));
		return ResponseEntity.ok(responses);
	}
	
	// 부분 조회
	@GetMapping(value = "{id}")
	public ResponseEntity<TodoResponse> readOne(@PathVariable(name="id")Integer id) {
		TodoEntity result = service.searchById(id);
		System.out.println("select part : " + ResponseEntity.ok(result));
		return ResponseEntity.ok(new TodoResponse(result));
	}
	
	// 수정
	@PatchMapping("{id}")
	public ResponseEntity<TodoEntity> update(@PathVariable("id")Integer id
			, @RequestBody TodoRequest request) {
		TodoEntity result = service.updateById(id, request);
		System.out.println("update part : " + ResponseEntity.ok(result));
		return ResponseEntity.ok(result);
	}
	
	// 부분 삭제
	@DeleteMapping("{id}")
	public ResponseEntity<?> deleteOne(@PathVariable("id")Integer id) {
		service.delelteById(id);
		System.out.println("delete part : " + ResponseEntity.ok().build());
		return ResponseEntity.ok().build();
	}
	
	// 전체 삭제
	@DeleteMapping
	public ResponseEntity<?> deleteAll() {
		service.delelteAll();
		System.out.println("delete all : " + ResponseEntity.ok().build());
		return ResponseEntity.ok().build();
	}
}

결과

H2 데이터베이스

INSERT 확인

SELECT 확인

부분 SELECT

UPDATE 확인

DELETE 확인


AOP

비슷한 기능을 하는 Filter, Interceptor, AOP 관련하여 정리가 필요해보인다.

관심사항의 종류들! 로그인, 시큐리티, 로그 출력, 트랜잭션 등등

과거는 보안 프로그램을 직접 짜주었지만 현재는 스프링에서 시큐리티를 별도 지원해준다!

객체 지향 프로그래밍하려면 다이어그램을 반드시 잘 짜주어야 한다!


AOP 실습 1

dependencies

Lombok / Spring Boot DevTools / Spring Web / Thymeleaf

bulid.gradle

implementation 'org.springframework.boot:spring-boot-starter-aop:3.3.5'

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
	implementation 'org.springframework.boot:spring-boot-starter-aop:3.3.5'
}

application.properties

spring.application.name=sprweb38aop

server.port=80

index

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<a href="aoptest">AOP 연습</a>
</body>
</html>

interface

package pack.model;

public interface MyModelInter {
	String processMsg();
	String businessMsg();
}

Model

package pack.model;

import org.springframework.stereotype.Repository;

@Repository
public class MyModel implements MyModelInter {
	@Override
	public String processMsg() {
		System.out.println("processMsg 핵심 메소드 수행");
		return "Spring AOP 멋져";
	}
	
	@Override
	public String businessMsg() {
		System.out.println("businessMsg 핵심 메소드 수행");
		return "Spring AOP 이뻐";
	}
}

Controller

package pack.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import pack.model.MyModelInter;

@Controller
public class TestController {
	@Autowired
	@Qualifier("myModel") // @Qualifier은 @Autowired에 종속되어 있는 어노테이션
	private MyModelInter modelInter; // 타입에 의한 매핑, 같은 이름의 인터페이스가 2개면 에러 떨어짐 

	@GetMapping("aoptest")
	public String abc(Model model) {
		String result1 = modelInter.processMsg();
		String result2 = modelInter.businessMsg();
		
		model.addAttribute("data1", result1);
		model.addAttribute("data2", result2);
		
		return "list";
	}
}

list

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
결과는
<span th:text="${data1}"></span>
<span th:text="${data2}"></span>
</body>
</html>

Aspect

package pack.aspect;

import org.springframework.stereotype.Component;

@Component // 객체로 만들어 놓기
public class SecurityClass { // 관심 사항!
	public void mySecurity() {
		System.out.println("핵심 메소드 수행 전에 보안 작업 설정");
	}
}

Process

package pack.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MyAspectProcess {
	@Autowired
	private SecurityClass class1;
	// ...
	
	@Around("execution(public String processMsg()) or execution(public String businessMsg())") // 핵심 메소드 앞뒤로 모두 설정 가능
	public Object abcProcess(ProceedingJoinPoint joinPoint) throws Throwable { // ProceedingJoinPoint가 핵심임!
		class1.mySecurity();
		
		Object object = joinPoint.proceed(); // 설정한 핵심 메소드 수행, 즉 조인포인트 대상 메소드가 수행된다.
		
		return object;
	}
}

결과

AOP 실습2 (DB 연동 : 직원 출력)

dependencies

Spring Boot DevTools / Spring Web / Spring Data JPA

MariaDB Driver / Lombok / Thymeleaf

application.properties

spring.application.name=sprweb39aop_login
server.port=80
spring.thymeleaf.cache=false

spring.jpa.open-in-view=false

# mariadb server connect
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://127.0.0.1:3306/test
spring.datasource.username=root
spring.datasource.password=1111
# jpa
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
logging.level.org.hibernate.type.descriptor.sql=trace
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect

Entity (Jikwon)

package pack.model;

import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.Getter;

@Entity
@Getter
public class Jikwon {
	@Id
	private String jikwonno;
	private String jikwonname;
	private String jikwonjik;
	private String jikwongen;
	
	@ManyToOne(targetEntity = Buser.class, fetch = FetchType.EAGER) // targetEntity 안써줘도 괜찮음!, 단방향 매핑
	@JoinColumn(name = "busernum")
	private Buser buser;
}

Entity (Buser)

package pack.model;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.Getter;

@Entity
@Getter
public class Buser {
	@Id
	private String buserno;
	private String busername;
}

Repository (Jikwon)

package pack.model;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface JikwonRepository extends JpaRepository<Jikwon, String> {
	
	@Query(value = "select j from Jikwon j where jikwonno=:jikwonno")
	Jikwon jikwonLogin(@Param("jikwonno")String jikwonno);
}

Repository (Buser)

package pack.model;

import org.springframework.data.jpa.repository.JpaRepository;

public interface BuserRepository extends JpaRepository<Buser, String> {

}

DAO

package pack.model;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

@Repository
public class DataDao {
	@Autowired
	private JikwonRepository jikwonRepository;
	
	// 직원 자료 읽기
	public List<Jikwon> jikwonList() {
		List<Jikwon> list = jikwonRepository.findAll();
		return list;
	}
	
	// 로그인 관련ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
	public Jikwon jikwonLogin(String jikwonno) {
		Jikwon jikdata = jikwonRepository.jikwonLogin(jikwonno);
		return jikdata;
		
		// return jikwonRepository.findById(jikwonno).get(); 같은거임!
	}
	
}

Jikwoncontroller

package pack.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import pack.model.DataDao;
import pack.model.Jikwon;

@Controller
public class JikwonController {
	@Autowired
	private DataDao dao;
	
	@GetMapping("jikwonlist")
	public String jikProcess(Model model, 
			HttpServletRequest request, HttpServletResponse response) {
		List<Jikwon> jlist = dao.jikwonList();
		model.addAttribute("list", jlist);
		return "show";
	}
}

LoginController

package pack.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import jakarta.servlet.http.HttpSession;
import pack.model.DataDao;
import pack.model.Jikwon;

@Controller
public class LoginController {
	@Autowired
	private DataDao dao;
	
	@GetMapping("login")
	public String login() {
		return "login";
	}
	
	@PostMapping("login")
	public String loginProcess(HttpSession httpSession,
			@RequestParam("no")String no,
			@RequestParam("name")String name) {
		Jikwon jikwon = dao.jikwonLogin(no);
		
		if(jikwon != null) {
			String returnName = jikwon.getJikwonname(); // DB에 있는 직원명
			if(returnName.equals(name)) { // DB에 저장된 직원명과 사용자가 입력한 이름을 비교
				httpSession.setAttribute("name", returnName);
			}
		}
		return "redirect:/jikwonlist";
	}
	
	@GetMapping("logout")
	public String logoutProcess(HttpSession httpSession) {
		httpSession.removeAttribute("name");
		return "redirect:/";
	}
}

Aspect

package pack.aspect;

import org.springframework.stereotype.Component;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

@Component
public class LoginClass { // 관심사항
	public boolean loginCheck(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HttpSession httpSession = request.getSession();
		
		if(httpSession.getAttribute("name") == null) {
			response.sendRedirect("login");
			return true;
		} else {
			return false;
		}
	}
}

Advice

package pack.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@Aspect
@Component
public class MyAdvice {
	@Autowired
	private LoginClass loginClass;
	
	@Around("execution(* jikProcess*(..))") //* jikProcess*(..) jikProcess로 시작하는 모든 메소드 Args 0개 이상
	public Object abcProcess(ProceedingJoinPoint joinPoint) throws Throwable {
		HttpServletRequest request = null;
		HttpServletResponse response = null;
		
		for(Object obj:joinPoint.getArgs()) { // pointcut 대상 메소드의 args를 찾음
			if(obj instanceof HttpServletRequest) {
				request = (HttpServletRequest)obj; // type mismatch 캐스팅 해줌!
			}
			
			if(obj instanceof HttpServletResponse) {
				response = (HttpServletResponse)obj;
			}
		}
		if(loginClass.loginCheck(request, response)) {
			return null;
		}
		Object object = joinPoint.proceed();
		return object;
	}
}

index

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<a href="jikwonlist">우리 회사 직원 정보 보기</a>
</body>
</html>

show

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<div th:if="${session.name} != null">
	<a th:href="@{logout}">로그아웃</a>
</div>

** 직원 자료 **<br>
<table border="1">
  <tr>
    <th>사번</th><th>직원명</th><th>직급</th><th>성별</th><th>부서</th>
  </tr>
  <th:block th:each="j:${list}">
    <tr>
      <td>[[${j.jikwonno}]]</td>
      <td>[[${j.jikwonname}]]</td>
      <td>[[${j.jikwonjik}]]</td>
      <td>[[${j.jikwongen}]]</td>
      <td>[[${j.buser.busername}]]</td>
    </tr>
  </th:block>
</table>
</body>
</html>

login

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
** 로그인 **<p/>
<form action="login" method="post">
	직원번호 : <input type="text" name="no"><br>
	직원이름 : <input type="text" name="name"><br>
	<input type="submit" value="로그인">
</form>
</body>
</html>

에러

🚨  There was an unexpected error (type=Internal Server Error, status=500).
Caused by: org.attoparser.ParseException: Exception evaluating SpringEL expression: "j.buser.busername" (template: "show" - line 19, col 13)

[[${j.buser.busername}]]을 불러오지 못함!
한꺼번에 데이터를 뿌려줘야 하기 때문에
fetch = FetchType.LAZY 지연로딩이 아닌
fetch = FetchType.EAGER 즉시로딩이 되어야한다.
🚨  There was an unexpected error (type=Internal Server Error, status=500).
Cannot invoke "jakarta.servlet.http.HttpServletRequest.getSession()" because "request" is null

because "request" is null이 뜨는 이유!
핵심 로직이 수행되기 전 pointcut 대상 메소드인 JikwonController의 jikProcess의 Args를 찾아야 하는데 jikProcess() 메소드에 HttpServletRequest request, HttpServletResponse response를 걸어주지 않으면 request와 response의 Args를 찾지 못함 그래서 HttpServletRequest request, HttpServletResponse response를 걸어줘야함! 걸어줘야 로그인 로직의 request , response를 찾음

결과

핵심 로직 수행 전 관심사항!

DB에 저장되어있는 직원번호와 이름 (1, 홍길동)을 입력하면 핵심 로직 수행

'Study > Acorn' 카테고리의 다른 글

241112 파일 업로드 (DB) / Security  (0) 2024.11.12
241108/241111 파일 업로드/다운로드  (0) 2024.11.08
241106 RESTful (CRUD/문제)  (1) 2024.11.07
241105 RESTful (PUT/DELETE/문제)  (0) 2024.11.05
241105 RESTful (GET/POST)  (2) 2024.11.05