[TIL] Day26 - Spring Boot Actuator/JDBC, CRUD in Spring

JIONY·2022년 8월 30일
0
post-thumbnail

이전 회사에서 운영하던 서비스의 게시판에 검색 기능을 추가하려고 했었음. 근데 데이터가 핵 많아서 통합검색을 도입하자니 성능과 비용 이슈가 심각했음. 그래서 백엔드 개발자 분이 검색 범위 제한을 대안으로 제시해주셨었는데 오늘 검색 옵션 지정해서 키워드 검색하는 예제를 장시간 풀면서 그 때 생각이 남 ㅋㅋ 조회를 할 때 AND나 OR 연산을 계속 붙이는 게 검색 범위만 늘리고 성능에는 도움이 안된다는 말을 많이 들었는데 이제는 내가 그걸 고민해야 하는 입장에 놓였음 ㅋㅋㅋ 조건이 많아질 수록 제법 어랴우니까 마니 연습해여지,,


Spring Boot Actuator

  • Spring Boot Dashboard에서 Spring Boot Application의 상태를 종합적으로 모니터링 가능

의존성 추가

아래 세 가지 방법으로 String Boot Actuator 추가 가능

  1. 프로젝트 생성 시 두 번째 단계에서 추가

  2. 이미 생성된 프로젝트인 경우, 프로젝트 이름 우클릭 > Spring > Add Starters에서 추가(1번과 같은 팝업이 노출됨)

  3. pom.xml에서 직접 추가

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

Actuator 활성화 설정

application.properties에 아래 코드 추가

management.endpoints.web.exposure.include=mappings,beans,env
  • 프로젝트가 실행 중이어야 함
  • 의도치 않은 공백 주의(ex.띄어쓰기, 줄바꿈)

모니터링 방법

  • Boot Dashboard > 실행 중인 프로젝트 우클릭 > Show Properties
  • 활성화 상태인 경우: 내가 만든 컨트롤러, 내가 수정한 서버 포트 설정 등 확인 가능

  • 활성화가 되어 있지 않은 경우



Spring Boot JDBC

JDBC API

  • DB를 연결하기 위해 자바에서 제공하는 API
  • JDBC는 데이터베이스에서 자료를 쿼리하거나 업데이트하는 방법을 제공

Plain JDBC API의 문제점

  • 쿼리를 실행하기 전후로 코드를 많이 작성해야 함 (ex. 연결 생성, 명령문, ResultSet 닫기, 연결 등)
  • 데이터베이스 로직에서 예외 처리 코드를 수행해야 함
  • 트랜잭션을 처리해야 함
  • 이러한 모든 코드를 반복해야 하므로 자원이 낭비됨

JDBC Template

  • Spring에서 제공하는 클래스
  • 내부적으로 Plain JDBC API를 사용하지만 위와 같은 문제점들을 제거한 형태

Spring JDBC가 하는 일

  • Connection 열기와 닫기
  • Statement 준비와 닫기
  • Statement 실행
  • ResultSet Loop처리
  • Exception 처리와 반환
  • Transaction 처리

Spring JDBC에서 개발자가 할 일

핵심적으로 해야 할 작업만 해주면 나머지는 Framework가 알아서 처리

  • datasource 설정
  • sql문 작성
  • 결과 처리

JdbcTemplate이 제공하는 기능

  • 실행 : Insert나 Update같이 DB의 데이터에 변경이 일어나는 쿼리를 수행하는 작업
  • 조회 : Select를 이용해 데이터를 조회하는 작업
  • 배치 : 여러 개의 쿼리를 한 번에 수행해야 하는 작업

JDBC Driver

  • 자바 프로그램의 요청을 DBMS가 이해할 수 있는 프로토콜로 변환해주는 클라이언트 사이드 어댑터
  • DB마다 Driver가 존재하므로, 자신이 사용하는 DB에 맞는 JDBC Driver를 사용
  • DataSource를 JDBC Template에 주입(Dependency Injection, DI)
    → JDBC Template이 JDBC Driver를 이용하여 DB에 접근

의존성 추가

String Boot Actuator와 마찬가지 방법으로 추가 가능

  1. 프로젝트 생성 / Add Starters > JDBC API, Oracle Driver 추가

  2. pom.xml에 직접 추가 - Maven일 경우

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    
    <dependency>
        <groupId>com.oracle.database.jdbc</groupId>
        <artifactId>ojdbc8</artifactId>
        <scope>runtime</scope>
    </dependency>

DataSource 설정

JdbcTemplate 클래스가 JDBC API를 이용하여 DB 연동을 처리하려면 데이터베이스로부터 커넥션을 얻어야 함. 따라서 JdbcTemplate 객체가 사용할 DataSource를 bean으로 등록해 스프링 컨테이너가 생성하도록 해야 함.

  • Spring Boot는 DataSource를 Spring bean으로 자동 등록해줌
    - application.properties에 있는 설정을 참고함
    - 자동 등록되는 Spring bean의 이름: dataSource
    - 개발자가 직접 DataSource를 bean으로 등록 시, Spring Boot가 자동으로 등록하지 않음

기본 설정

//application.properties
# jdbc
spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
spring.datasource.url=jdbc:oracle:thin:@localhost:1521:xe
spring.datasource.username=c##academy
spring.datasource.password=student

등록된 Bean 사용

등록이 완료된 Bean은 @Autowired Annotation으로 사용할 수 있음

@Autowired

  • 일치하는 타입의 대상을 찾아 주입하는 Annotation
  • 원하는 클래스 내부에 다음과 같이 필드를 선언하여 사용
@Autowired
private JdbcTemplate jdbctemplate;


CRUD in Spring Boot

  • 자바에서 Scanner를 통해 입력 받던 변수를 Request Parameter로 지정
    • DB 테이블 컬럼명이 파라미터 이름이 됨
  • @Autowired로 Annotation으로 JdbcTemplate 주입
  • DTO 클래스를 Controller 패키지와 분리해서 Entity 패키지에 구현
    • 테이블과 똑같이 생긴 클래스 : 개체(Entity)
    • DTO 클래스: DB 테이블 데이터를 복사해 올 객체를 생성하기 위한 클래스
    • 조회에 필요한 RowMapper, ResultSetExtractor도 DTO에 메소드화하고 getter()로 호출
  • 메소드 내 구문은 자바에서 작성했던 것과 동일하므로 고도화 내용만 아래에 추가로 작성함

@ModelAttribute

  • 객체의 필드에 자동 매핑을 수행(이름이 일치해야하므로 오타 주의)
    • 파라미터 값은 getter()로 받아옴
  • 삽입, 수정 등 2개 이상의 항목을 파라미터로 설정해야 하는 경우에 유용
  • 파라미터 개수에 상관 없이 쓸 수 있지만 상세(단일)조회, 삭제와 같이 파라미터가 하나인 경우에는 @RequestParam을 쓰는 게 일반적임
@Controller
@RequestMapping("/music")
public class QuizController {
	@Autowired
	private JdbcTemplate jdbctemplate;
	
	@RequestMapping("/insert")
	@ResponseBody
	public String insert(@ModelAttribute MusicDto dto) {
		String sql = "insert into music("
				+ "music_no, music_title, music_artist, music_album) "
				+ "values(music_seq.nextval, ?, ?, ?)";
		Object[] param = {
      	  dto.getMusicTitle(), dto.getMusicArtist(), dto.getMusicAlbum()};
		
		jdbctemplate.update(sql, param);
		return "등록 완료";
	}	
}

SELECT - 목록

List 전체 출력은 아래 두 가지 방식을 사용할 수 있음

  • list.toString()
  • StringBuffer 사용
    • 아직 HTTP Response Body에 직접 return 값을 보여주는 방식을 사용하고 있기 때문에, 결과값이 2개 이상인 경우 줄바꿈 태그를 추가하면 한 눈에 행별 데이터를 구분할 수 있음
@RequestMapping("/list")
@ResponseBody
public String list() {
	String sql = "select * from music order by music_no asc";
	List<MusicDto> list = jdbctemplate.query(sql, MusicDto.getMapper());
	//1. return list.toString()

	//2. StringBuffer
	StringBuffer buffer = new StringBuffer();
	for(MusicDto dto : list) {
		buffer.append(dto);
		buffer.append("<br>");
	}return buffer.toString();
}

SELECT - 검색

실제 서비스들의 검색 기능은 1) 검색 옵션을 선택하고, 2) 검색어를 입력하게 구현된 경우가 많음.

기존에는 검색 조건을 OR 또는 UNION으로 나열해 통합 검색을 구현했다면, 이번에는 검색 범위를 좁힌 다음 일치하는 키워드를 검사하는 방식으로 4단계에 걸쳐 고도화를 진행하고자 함

(단계3 또는 단계4 방식으로 검색 기능을 구현할 것을 권장)


예제 및 아이디어

  • Q. 방명록(guest_book)테이블에서 이름만(name) 또는 메모만(memo)에 대한 키워드 검색 결과를 조회
    * 잘못된 입력에 대한 처리는 고려하지 않음
  • 아이디어: 검색 옵션(type)으로 한 번 필터링하고 키워드 일치 검사

단계1. if-else로 type에 대한 케이스 구분

@RequestMapping("/search")
@ResponseBody
public String search(@RequestParam String type, String keyword) {
	if(type.equals("name")) {
		String sql = "select * from guest_book where"
				+ "instr(name, ?) > 0 order by no asc";
			
		//중복 코드 
		Object[] param = {keyword};
		List<GuestBookDto> list = jdbctemplate.query(
				sql, GuestBookDto.getMapper(), param);
		StringBuffer buffer = new StringBuffer();
		for(GuestBookDto dto : list) {
			buffer.append(dto);
			buffer.append("<br>");
		}return buffer.toString();
	}else {//type.equals("memo")
		String sql = "select * from guest_book"
					+ "where instr(memo, ?) > 0 order by no asc";
			
		//중복 코드
		Object[] param = {keyword};
		List<GuestBookDto> list = jdbctemplate.query(
					sql, GuestBookDto.getMapper(), param);
		StringBuffer buffer = new StringBuffer();
		for(GuestBookDto dto : list) {
			buffer.append(dto);
			buffer.append("<br>");
		}return buffer.toString();
	}
}
  • 문제점: sql외에 모든 처리 구문이 중복됨
  • 해결책: 조건문의 역할을 sql문 분기로 한정

단계2. 중복 코드 제거

@RequestMapping("/search2")
@ResponseBody
public String search2(@RequestParam String type, String keyword) {
	String sql;
	if(type.equals("name")) {
		sql = "select * from guest_book"
					+ "where instr(name, ?) > 0 order by no asc";		
	}else {
		sql = "select * from guest_book"
					+ "where instr(memo, ?) > 0 order by no asc";			
	}
		Object[] param = {keyword};
		List<GuestBookDto> list = jdbctemplate.query(
				sql, GuestBookDto.getMapper(), param);
		StringBuffer buffer = new StringBuffer();
		for(GuestBookDto dto : list) {
			buffer.append(dto);
			buffer.append("<br>");
	}return buffer.toString();
}
  • 문제점1: type이 2개 이상일 경우에 else-if 구문이 늘어남
    • 해결책: type을 홀더 처리?
  • 문제점2: type을 홀더로 설정하면 바인딩 시, 문자열 ’type’으로 처리가 됨(홀더는 동적 바인딩 변수에만 사용 가능)
    • 해결책: type이 문자열로 인식되지 않도록 sql 구문을 수정, 불필요한 조건문 제거

단계3. 문자열 더하기 연산 처리

@RequestMapping("/search3")
@ResponseBody
public String search3(@RequestParam String type, String keyword) {
	String sql = "select * from guest_book "
				+ "where instr(" + type + ", ?) > 0 order by no asc";
	Object[] param = {keyword};
	List<GuestBookDto> list = jdbctemplate.query(
			sql, GuestBookDto.getMapper(), param);
	StringBuffer buffer = new StringBuffer();
	for(GuestBookDto dto : list) {
		buffer.append(dto);
		buffer.append("<br>");
	}return buffer.toString();
}

단계4. 문자열 replace() 사용

@RequestMapping("/search4")
@ResponseBody
public String search4(@RequestParam String type, String keyword) {
	String sql = "select * from guest_book "
				+ "where instr(#1, ?) > 0 order by no asc";
	sql = sql.replace("#1", type);
	Object[] param = {keyword};
	List<GuestBookDto> list = jdbctemplate.query(
			sql, GuestBookDto.getMapper(), param);
	StringBuffer buffer = new StringBuffer();
	for(GuestBookDto dto : list) {
		buffer.append(dto);
		buffer.append("<br>");
	}return buffer.toString();
}
  • 오라클에 #은 절대 insert 될 수 없으므로 sql문 type자리에 #1을 넣어놓고, #1을 type으로 치환하는 구문을 추가


참고

Spring Bean

  • 기존 자바 프로그래밍에서는 클래스를 생성하고 new 키워드를 통해 원하는 객체를 직접 생성해서 사용함
    • 모든 작업을 사용자가 제어하는 구조
  • Spring에서는 Spring에 의해 관리되는 자바 객체를 사용해야 함
    • 사용자의 제어권이 다른 주체에게 넘어간 구조
    • Bean: Spring IoC 컨테이너가 관리하는 자바 객체
    • Spring IoC 컨테이너: 의존성 객체 모음(Show Properties)
    • Show Properties > Beans에서 등록된 Bean 목록 확인 가능

Spring Bean 등록 방법

  1. Annotation 사용
    a. @Component Annotation이 있으면 Spring이 자체적으로 등록해줌

    @Controller //Controller임을 Spring에게 알려주기 위해 해당 Annotation 사용
    public class ParamController{
    	//..
    }
    //Controller.class
    //@Controller에 @Component가 있는 것을 확인할 수 있음
    
    //--일부 생략--
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Component
    public @interface Controller {
    
    	/**
    	 * The value may indicate a suggestion for a logical component name,
    	 * to be turned into a Spring bean in case of an autodetected component.
    	 * @return the suggested component name, if any (or empty String otherwise)
    	 */
    	@AliasFor(annotation = Component.class)
    	String value() default "";
    
    }
  2. Bean Configuration 파일에 직접 등록
    b. Configuration 클래스 생성 > 클래스 내부에 @Bean 생성 코드 작성

    @Configuration
    public class HomeConfiguration{
    	@Bean
    	public ParamController sampleController(){
    	return new SampleController;
    	}
    }

IoC

  • 제어의 역전(IoC, Inversion of Control)
  • 사용자의 제어권을 다른 주체에게 넘기는 것
    • 주도권이 개발자가 아닌, Spring Framework에 있음

DI

  • 의존성 주입(DI, Dependency Injection)
  • 필요한 객체를 직접 생성하는 것이 아니라, 필요한 객체를 외부로부터 받아서 사용하는 것
    • Spring Framework 에서는 Spring Bean을 Spring Ioc Container에 등록하고, ApplicationContext.getBean() 같은 메소드를 통해 자바 객체를 얻어서 사용

스프링 특: 무조건 스프링을 거쳐야 함(IoC, DI)




0개의 댓글