ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 77~78일차 - Transaction, RESTFUL 개념과 사용 방식
    백엔드(웹 서버, WAS)/Spring Boot 2024. 5. 21. 16:06

    RedirectAttributes 

    일반적으로 Model (Request) 에 실어서 데이터를 전달해왔는데 사용할 수 없으면 RedirectAttributes 에 저장하는데

    RedirectAttributes 는 세션에 잠깐 넣어놨다가 사용하면 자동으로 지워주기 때문에 addFlashAttribute 라는 이름이 붙었다

    옛날에는 세션에다가 넣었다가 데이터 사용 후 삭제 했었다

    useGeneratedKeys

    방금 Insert 한 행의 idx 의 값을 가져와서 BoardDTO 의 필드에 젖아한다

    keyColumn : bbs 에서 가져올 컬럼의 이름

    ketProperty : 파라미터의 필드의 이름

    <insert id="write" useGeneratedKeys="true"
        keyColumn="idx"
        keyProperty="idx" parameterType="board">
        INSERT INTO bbs (subject, user_name, content)
            VALUES(#{subject}, #{user_name}, #{content})	
    </insert>

     

    트랜잭션

    예시 ) 게시글의 조회수 1을 증가하고 게시글의 상세 보여줄때 보여주기 단계에서 실패하면 조회수도 다시 되돌아가야한다

    @Transactional(rollbackFor = Exception.class)
    public ModelAndView detailPost(String idx) {
        ModelAndView mav = new ModelAndView("/board/detail");
        upPostbHit(idx);
        PostDTO post = dao.getPostDetail(idx);
        mav.addObject("post", post);
        return mav;
    }

     

    파이널 프로젝트때에는 테스트 내역서에도 들어가야한다 

    // @Transactional(rollbackFor = Exception.class)
    // 모든 예외에 대해서 롤백처리(기본값)
    // Isolation.READ_COMMITED : (기본) 다른 트랜잭션에서 커밋된 내용만 읽을 수 있다
    // Isolation.READ_UMCOMMITED : 다른 트랜잭션에서 커밋되지 않은 데이터도 읽을 수 있다
    // Isolation.REATABLE_READ : 조회중인 데이터를 다른 트랜잭션에서 UPDATE 못하도록 막는다
    // Isolation.SERIALIZABLE : 한 트랜잭션 내용이 커밋될때까지 다른 트랜잭션은 CRUD 불가
    
    // @Transactional 사용시 try-catch 를 사용하면 자동 롤백이 되지 않는다
    // 그래서 예외 처리를 web.xml 을 통해서 특정 에러나 에러코드 발생시 특정한 요청이나 
    // 페이지로 보내는 방식을 사용한다
    // 그런데 스프링부트는 web.xml 가 존재하지 않는다
    
    // 스프링부트에서는 ErrorController Interface 를 활용한다

    resources 자원 관리

    static 폴더 아래에 넣기만 해도 별도의 설정이 필요 없이 서버가 자원들의 위치를 확인하여 가져온다

    <!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
    <resources mapping="/resources/**" location="/resources/" />

     

    <!-- static 폴더의 내용을 호출 할때에는 그냥 호출해도 된다 -->
    <!-- application.properties 설정에 의해 특정 요청명인 경우만 static 폴더를 보도록 할 수 있다 -->
    <link rel="stylesheet" href="css/common.css" type="text/css">

     

    에러  요청을 받아주는 컨트롤러

    @RequestMapping(value="/error")
    public String error(HttpServletRequest request, Model model) {
        logger.info("error 요청");
        int code = (int) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
        String exp = (String) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
        String msg = (String) request.getAttribute(RequestDispatcher.ERROR_MESSAGE);
        logger.info("{} : {} : {}", code, exp, msg);
        return "login";
    }

    Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
    ERROR 24-05-21 11:20:51[main] [SpringApplication:818] - Application run failed
    org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestMappingHandlerMapping' defined in class path 
    resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Invocation of init method failed; 
    nested exception 
    is java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'basicErrorController' method 
    org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
    to { [/error]}: There is already 'errorHandlerController' bean method

    에러 컨트롤러를 스프링에서 이미 사용하고 있기 때문에 인터페이스 형태로 구현 받은 컨트롤러에서만 사용가능하다 

     

    @Controller
    public class ErrorHandlerController implements ErrorController {
    	
    	Logger logger = LoggerFactory.getLogger(getClass());
    	
    	@RequestMapping(value="/error")
    	public String error(HttpServletRequest request, Model model) {
    		logger.info("error 요청");
    		model.addAttribute("code", request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE));
    		model.addAttribute("exception", request.getAttribute(RequestDispatcher.ERROR_EXCEPTION));		
    		return "error";
    	}
    }
    <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>Insert title here</title>
    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
    <style>
    </style>
    </head>
    <body>
    	<h3>${code} 에러가 발생했습니다</h3>
    	<a href="./list">리스트 돌아가기</a>
    </body>
    <script>
    </script>
    </html>

    정상적으로 보인다

    Connection Pool

    기본적으로 

    INFO  24-05-21 11:34:00[http-nio-80-exec-1] [HikariDataSource:110] - HikariPool-1 - Starting...
    INFO  24-05-21 11:34:00[http-nio-80-exec-1] [HikariDataSource:123] - HikariPool-1 - Start completed.

    <logger name="jdbc.connection" level="INFO"/>
    INFO  24-05-21 11:41:14[HikariPool-1 connection adder] [connection:541] - 2. Connection opened
    INFO  24-05-21 11:41:14[HikariPool-1 connection adder] [connection:541] - 3. Connection opened
    INFO  24-05-21 11:41:14[HikariPool-1 connection adder] [connection:541] - 4. Connection opened
    INFO  24-05-21 11:41:14[HikariPool-1 connection adder] [connection:541] - 5. Connection opened
    INFO  24-05-21 11:41:14[HikariPool-1 connection adder] [connection:541] - 6. Connection opened
    INFO  24-05-21 11:41:14[HikariPool-1 connection adder] [connection:541] - 7. Connection opened
    INFO  24-05-21 11:41:14[HikariPool-1 connection adder] [connection:541] - 8. Connection opened
    INFO  24-05-21 11:41:14[HikariPool-1 connection adder] [connection:541] - 9. Connection opened
    INFO  24-05-21 11:41:14[HikariPool-1 connection adder] [connection:541] - 10. Connection opened

    우리가 데이터베이스를 사용하고자 하면 미리 히카리풀이 10개를 생성해둔다 왜냐하면 기본값으로 연결 객체를 10개를 만들어둔다

    그래서 기본 설정이 10개이면 서버는 많은 커넥션 요청을 받아내지 못하기 때문에 1개로 바꾸어 줘야한다

    # connection pool
    # default : 10
    히카리풀에서 미리 생성하는 갯수(많으면 많을 수록 초기 속도가 느려진다)
    spring.datasource.hikari.maximum-pool-size=10
    # ms
    커넥션을 요청하고 기다리는 시간(밀리세컨드 단위) - 30초
    이 시간이 넘어가면 SQLexception 발생
    spring.datasource.hikari.connection-timeout=30000
    아무것도 하지 않고 놀고 있는 커넥션을 기다려주는 시간(밀리세컨드 단위) - 10분
    이후 폐기처리 한다
    spring.datasource.hikari.idle-timeout=600000
    커넥션 최대 유지 시간 - 30분
    30분이 지났다고 바로 폐기하는건 아니고, 작업이 끝나면 바로 폐기한다
    spring.datasource.hikari.max-lifetime=1800000

     

    @Autowired

    여러개의 객체를 선언하고 사용하니 메모리의 누수가 심하고 코드 적기도 귀찮았다

     

    내부적으로는 스프링부트가 객체화 시킨 후 service를 정적 영역에 올려놓은 상태이다

    필드 주입 : 변수를 통해서 객체를 참조해 오기 때문에 

     

    위의 필드 주입은 사용을 권고하지 있지 않다

     

     이유 1. final 을 붙여 불변성을 보장할 수 없다

     -> 원본을 건드린다 한들 서버가 껏다 올라가면 다시 복구됨

     

     이유 2. 객체간 순환 참조를 방지할 수 없다

    예 ) MainService 가 AdminService 를 Autowired 했는데, MemberService  로 AdminService 를 Autowired 한 경우

     -> 순환참조 : 서로가 서로를 보고 있으면서 상대의 변화가 생길때마다 나의 변화가 생기면, 무한으로 서로의 변화를 감지해야한다(내부적으로 무한루프)

    일반적으로 Controller -> Service -> DAO 순서로 Autowired 를 통해 참조 하였다 이런 단방향은 문제되지 않는다  

    하지만 Service 가 여러개이고, 이것들이 서로가 서로를 호출할 경우 문제가 발생할 수 있다

    실제로 실행될때에 Statck Over flow 가 발생하면서 순환참조를 알 수 있다

     

    하나의 객체를 정적영역에 놓고 사용하는것을 싱글톤 패턴이라고 한다 

    싱글톤 패턴 : 하나의 객체를 원본 영역에 두고, 이것을 바라보도록 만든 구조(다수의 객체를 불필요하게 만들 필요가 없다)

     

    해결 방법 1. 생성자 주입

    순수 자바를 사용해서 주입하면 컴파일 단계에서 순환 참조를 알아낼 수 있다

    실수를 조기에 발견할 수 있다

    @Controller
    public class UserController {
    
      private final UserService userService;
    
      public UserController(UserService userService) {
        this.userService = userService;
      }
    
      // ...
    }

     

    해결 방법 2. Lombok

    Lombok의 @RequiredArgsConstructor 를 대신 사용해도 된다

    @RestController
    @RequiredArgsConstructor
    @RequestMapping("/example")
    public class RequiredArgsConstructorControllerExample {
    
      private final FirstService firstService;
      private final SecondService secondService;
      private final ThirdService thirdService;
      
      ...
    }

    정리

    Autowired

    장점 : 편리함, 단점 : 잘못된 설계로 런타임 에러(스택오버플로우)가 발생함

    생성자 및 롬복

    장점 : 런타임 에러가 발생하지 않고 컴파일 단계에서 알 수 있음, 단점 : 불편함


    REST FULL API

    AJAX 로 가장 많이 사용한다 REST FULL 방식이 AJAX 방식이라고 오해하지말자

     

    요거 밑에 요거 밑에 요청 보내면 이 데이터 보내줄게 

    웹서버 한대 앱에서도 받아쓰고 웹에서도 받아쓰고 

     

    서버와 프론트가 완벽하게 분리되어있고 백엔드 개발자가 약속(인터페이스)를 구성한다

     

    JSON

    RESTful 예제

    Jackson 라이브러리를 가져온다

    스프링에서 버젼을 관리해주기 떄문에 지워주고 메이븐 업데이트 해주자

    규칙 1. POST 로 보내야한다
    규칙 2. JSON 를 문자열로 변환해서 보내야한다

    data:JSON.stringify(param)
    contentType:'application/json; charset=utf-8'
    규칙 3. 파라미터를 받을때 @RequestBody 로 받아야한다

     

    @RestController
    //@RequestMapping(value="/rest") // /rest 로 시작하는 요청은 이 컨트롤러를 타게 된다
    public class MainController {
    	// 장점. @ResponseBody 를 사용하지 않아도 모든 요청에 Response 객체로 반환하게 된다
    	// 단점. view 로 페이지 이동이 불가능하다
    	
    	Logger logger = LoggerFactory.getLogger(getClass());
    	
    	@RequestMapping(value="/")
    	public ModelAndView home() {
    		// ModelAndView 를 활용하면 RestController 에서도 페이지 이동이 가능하다
    		ModelAndView mav = new ModelAndView("home"); 
    		logger.info("메인 페이지 요청");
    		return mav;
    	}
    	
    	/* method 종류
    	 * GET : 특정 데이터를 조회할 경우
    	 * POST : 특정 데이터 입력을 요청할 경우(대략적으로)
    	 * DELETE : 특정 데이터를 삭제 요청할 경우
    	 * PUT : 특정 데이터를 수정을 요청할 경우
    	 * PATCH : 특정 데이터르 일부 수정을 요청할 경우
    	 */
    	@GetMapping(value="/rest/list.ajax")
    	public List<String> list() {
    		List<String> list = new ArrayList<String>();
    		list.add("하나");
    		list.add("둘");
    		list.add("셋");
    		return list;
    	}
    	
    	@GetMapping(value="/rest/map.ajax")
    	public Map<String, Object> map() {
    		Map<String, Object> map = new HashMap<String, Object>();
    		map.put("msg", "hello");
    		map.put("age", 33);
    		map.put("married", true);
    		return map;
    	}
    	
    	@GetMapping(value="/rest/object.ajax")
    	public UserInfo object() {
    		UserInfo info = new UserInfo();
    		info.setName("tester");
    		info.setAge(24);
    		info.setPromotion(true);
    		return info;
    	}
    
    	// 규칙 1. POST 로 보내야한다
    	// 규칙 2. JSON 를 문자열로 변환해서 보내야한다
    	// 규칙 3. 파라미터를 받을때 @RequestBody 로 받아야한다	
    	@PostMapping(value="/rest/complex.ajax")
    	public Map<String, Object> complex(@RequestBody Map<String, Object> params) { // 규칙 3
    		logger.info("params : {}", params);
    		// params : {values={name=숫자넣기, num=[1, 2, 3, 4, 5]}}
    		Map<String, Object> values = (Map<String, Object>) params.get("values");
    		logger.info("values : {}", values);
    		
    		String name = (String) values.get("name");
    		
    		logger.info("values : {}", values);
    		
    		List<Integer> num = (List<Integer>) values.get("num");
    		logger.info("num : {}", num);
    		
    		Map<String, Object> response = new HashMap<String, Object>();
    		response.put("success", true);
    		return response;
    	}
    <body>
    	<h3>RESTful API Page</h3>
    	<ul>
    		<li>/rest/list.ajax : List 형태로 반환 <button onclick="sendAjax('list.ajax')">sample1</button></li>
    		<li>/rest/map.ajax : Map 형태로 반환 <button onclick="sendAjax('map.ajax')">sample2</button></li>
    		<li>/rest/object.ajax : DTO 형태로 반환 <button onclick="sendAjax('object.ajax')">sample3</button></li>
    	</ul>
    	
    	<h3>복잡한 형태의 JSON 를 전송할때</h3>
    	<button onclick="complex()">sample4</button>
    	
    	<h3>Server 에서 문자열 형태로 온 JSON 을 전달하고 싶다면?</h3>
    	<p>JSON 형태의 문자열을 서버에서도 JAVA 객체 형태(List, Map, DTO 등..)로 변경이 가능하다</p>
    	<p>이때 필요한 라이브러리가 jackson-databaind 이다</p>
    </body>
    <script>
    	function sendAjax(request) {
    		$.ajax({
    			type:'GET',
    			url:'./rest/' + request,
    			data:{},
    			dataType:'JSON',
    			success:function(data){
    				console.log(data);	
    			},
    			error:function(error) {
    				console.log(error);
    			}
    		});
    	}
    	
    	// 마이페이지에다가 이름, 나이, 사진, 학력(초-중-고), 학교 이름
    	// 등등 아주 복잡한 데이터 형태를 입력하려고 할때를 연습해보자
    	function complex() {
    		var arr = [1,2,3,4,5];
    		var obj = {};
    		obj.name = "숫자 넣기";
    		obj.num = arr;
    		var param = {"values":obj};
    		console.log(param);
    		
    		// 복잡한 형태의 JSON 전송 방법
    		// 자료형 : {name:'kim', age:35}
    		// 복잡한 형태(참조형) : {name:'kim', scores:[{subjcet:'java',score:10}
    		// , {subjcet:'db', score:20}]}
    		
    		// 규칙 1. POST 로 보내야한다
    		// 규칙 2. JSON 를 문자열로 변환해서 보내야한다
    		// 규칙 3. 파라미터를 받을때 @RequestBody 로 받아야한다
    		
    		$.ajax({
    			type:'post', // 규칙 1 
    			url:'./rest/complex.ajax',
    			data:JSON.stringify(param),//규칙 2
    			dataType:'JSON',
    			contentType:'application/json; charset=utf-8',
    			success:function(data) {
    				console.log(data);
    			},
    			error:function(error) {
    				console.log(error);
    			}
    		});
    	}
    </script>

     

Designed by Tistory.