Spring JDBC(DB연동)

정원·2022년 6월 8일
0

Spring

목록 보기
9/11
post-thumbnail

22.06.08
전에 했던 score와 board를 DB연동해서 적용시키기.

전통적인 JDBC 프로그래밍

1.Connection 객체 생성
2.PrepareStatment 객체 생성
3.SQL 문 실행
3.ResultSet 객체 생성 결과처리
전통적인 JDBC 프로그래밍은 위에 순서들이 너무 반복되는 작업이 계속되는 단점이있다.
그래서 이제는 Spring-JDBC를 사용할 것이다.

Spring-JDBC(JdbcTemplate) ?

  • JDBC의 장점을 유지하면서, 전통적인 JDBC단점을 극복하여,
    간결한 형태의 API 사용법을 제공하며 기존 방식에서 지원하지 않는 편리한 기능을 제공한다.
  • Spring JDBC는 반복적으로 하는 작업을 대신함.
    (Connection, prepareStatment, resultSet의 반복처리, Exception처리)
  • Spring JDBC는 SQL에 바인딩할 값을 지정만 해주면 된다.
  • Spring JDBC 사용 전 DB커넥션을 가져오는 DataSource가 강제화 된다.

커넥션 풀

  • 여러 명의 사용자를 동시에 처리하는 웹 어플리케이션
  • DB연결을 이용할 때 매번 연결하는 방식이 아닌
    미리 연결을 맺고 사용하는 Connection Pool을 이용해 성능을 향상시킴
  • 커넥션 풀링은 미리 정해진 개수만큼 DB커넥션을 풀에 준비해두고,
    어플리케이션이 요청할 때마다 Pool에서 꺼내서 할당하며,
    다시 돌려 받아서 Pool에 넣는 기법
  • HikiriCP사용

DataSource

  • DB에 이용되는 URL, id, pw, DriverClass를 미리 정의해 놓고 사용 하는 객체
  • Spring-Jdbc에서 기본으로 제공
  • 여러 커넥션풀 라이브러리에서 기본으로 제공

INSERT, UPDATE, DELETE 구문

  • update 메서드 사용
    : update(sql, new Object[] {값, 값, 값}); 객체로 전달해도 되고
    update(sql, Object... args); 값을 바로 넣어도 된다.(가변인수)
    sql : ?를 사용하는 preparedStatement사용
    sql물음표 값을 세팅할 값을 저장(바인딩)

SELECT 구문

  • query 메서드 사용(값 여러개 리턴 ex> List<ScoreVO>)
    : query(sql, new RowMapper<Type>() ,값 바로전달) {
    익명클래스
    }
    : sql물음표 값을 세팅할 값을 저장
    : sql - ?를 사용하는 preparedStatement사용
  • new RowMapper<Type>()
    : 조회 결과를 ResultSet으로 읽어 Type으로 반환
    익명클래스 사용
    mapRow()메서드를 오버라이딩 해서 사용
  • queryForObject() (값 하나만 리턴 ex>ScoreVO

새프로젝트 생성(SpringDBAccess)

1.pom.xml에 버전 업데이트,
jdbc, hikariCP(커넥션풀), 커넥터 드라이버 설정 추가.
Maven Repository사이트이용.

<!-- spring-jdbc -->
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-jdbc</artifactId>
	<version>${org.springframework-version}</version>
</dependency>

<!-- HikariCP: 커넥션 풀 -->
<dependency>
	<groupId>com.zaxxer</groupId>
	<artifactId>HikariCP</artifactId>
	<version>3.3.1</version>
</dependency>

<!-- ojdbc6 DB 커넥터 드라이버-->
<dependency>
    <groupId>com.oracle.database.jdbc</groupId>
    <artifactId>ojdbc6</artifactId>
    <version>11.2.0.4</version>
</dependency>		

2.DB 설정 파일(root-context.xml)에 DataSource와 HikariCP 빈으로
등록하기 위한 namespace와 설정 추가.

<!-- 히카리 커넥션 풀 빈 등록 -->
<!-- hikariConfig이름으로 컨테이너에 객체 생성
	세터 주입으로 name에 value를 저장-->
<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
    <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" />
    <property name="jdbcUrl" value="jdbc:oracle:thin:@localhost:1521:xe" />
    <property name="username" value="spring" />
    <property name="password" value="spring" />
</bean>
   
<!-- 히카리 데이터소스 빈 등록 -->
<!-- Class속성에 정의된 클래스를 dataSource 이름으로 컨테이너에 객체생성
	생성자 주입으로 위에 선언한 hikariConfig를 주입, ref는 참조속성-->
<bean id="ds" class="com.zaxxer.hikari.HikariDataSource">
    <constructor-arg ref="hikariConfig" />
</bean>
   
<!-- Spring JDBC를 사용하기 위한 핵심 객체 JdbcTemplate 클래스 빈 등록 -->
<!-- Class속성에 정의된 클래스를 jdbcTemplate이름으로 컨테이너에 객체생성
	dataSource 메서드에 앞서 생성한 dataSource를 주입,  ref는 참조속성 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="ds" />
</bean>      

Score

score table 생성

CREATE TABLE scores(
    stu_id NUMBER PRIMARY KEY,
    stu_name VARCHAR2(30) NOT NULL,
    kor NUMBER DEFAULT 0,
    eng NUMBER DEFAULT 0,
    math NUMBER DEFAULT 0,
    total NUMBER DEFAULT 0,
    average NUMBER(5, 2)
);

CREATE SEQUENCE id_seq
    START WITH 1
    INCREMENT BY 1
    MAXVALUE 1000
    NOCYCLE
    NOCACHE;

ScoreVO.java

<script>
package com.spring.db.model;

public class ScoreVO {
	
	private int stuId;
	private String stuName;
	private int kor;
	private int eng;
	private int math;
	private int total;
	private double average;

	//총점,평균 구하는 메서드
	public void calcData() {
		this.total = this.kor + this.eng + this.math;
		this.average = Math.round((this.total / 3.0) * 100) / 100.0;
	}
	
	public void setStuId(int stuId) {
		this.stuId = stuId;
	}
	
	public int getStuId() {
		return stuId;
	}
	...

	@Override
	public String toString() {
		return "ScoreVO [stuId=" + stuId + ", stuName=" + stuName + ", kor=" + kor + ", eng=" + eng + ", math=" + math
				+ ", total=" + total + ", average=" + average + "]";
	}
}

</script>

IScoreService.java

<script>
package com.spring.db.service;

public interface IScoreService {
	
	//점수 등록
	void insertScore(ScoreVO score);
	
	//점수 전체 조회
	List<ScoreVO> selectAllScores();
	
	//점수 삭제
	void deleteScore(int num);
	
	//점수 개별 조회
	ScoreVO selectOne(int num);	
}
</script>

IScoreDAO.java

<script>
package com.spring.db.repository;

public interface IScoreDAO {

	//점수 등록
	void insertScore(ScoreVO score);
	
	//점수 전체 조회
	List<ScoreVO> selectAllScores();
	
	//점수 삭제
	void deleteScore(int num);
	
	//점수 개별 조회
	ScoreVO selectOne(int num);		
}
</script>

ScoreService.java

<script>
package com.spring.db.service;

//자동 빈 등록
@Service
public class ScoreService implements IScoreService {
	
	//JdbcTemplate을 이용한 SQL 처리
	@Autowired
	private IScoreDAO dao;	
	
	@Override
	public void insertScore(ScoreVO score) {
		score.calcData();//총점,평균 계산
		System.out.println("service: " + score);
		dao.insertScore(score);
	}

	@Override
	public List<ScoreVO> selectAllScores() {		
		return dao.selectAllScores();
	}

	@Override
	public void deleteScore(int num) {
		dao.deleteScore(num);
	}

	@Override
	public ScoreVO selectOne(int num) {		 
		return dao.selectOne(num);
	}
}
</script>

ScoreDAO.java

  • JdbcTemplate에서는 SELECT 쿼리를 위한 ResultSet 사용을 편하게 하기 위한 RowMapper 인터페이스를 구현하는 클래스를 생성해야 한다.
    메서드 안에는 객체를 포장하는 방법을 선언한다.

ScoreMapper.java

<script>
package com.spring.db.commons;

public class ScoreMapper implements RowMapper<ScoreVO> {
	@Override
	public ScoreVO mapRow(ResultSet rs, int rowNum) throws SQLException {		
		
		ScoreVO vo = new ScoreVO();
		vo.setStuId(rs.getInt("stu_id"));
		vo.setStuName(rs.getString("stu_name"));
		vo.setKor(rs.getInt("kor"));
		vo.setEng(rs.getInt("eng"));
		vo.setMath(rs.getInt("math"));
		vo.setTotal(rs.getInt("total"));
		vo.setAverage(rs.getDouble("average"));
		
		return vo;
	}	
}
</script>

그런데 한곳에서만 사용할 클래스를 굳이 파일로 만들어야 할까?
이런 상황에서는 내부 클래스를 사용하거나 람다식을 이용할 수 있다.

내부(중첩) 클래스 (inner class)

두 클래스가 굉장히 긴밀한 관계가 있을 때 주로 선언.
내부 클래스는 public 붙일수 없음.
해당 클래스 안에서만 사용할 클래스를 굳이 새 파일로 선언하지 않고도 만들 수 있다.

람다식(익명객체 이용)

ScoreMapper클래스 안만들고 익명객체 이용해서 선언하는 방법.
인터페이스에 구현할 메서드가 하나일때 람다식으로 사용가능.
클래스 파일 안만들고 즉석에서 클래스 선언할 수 있음.
다른 메서드에서는 다시 선언해야한다.

<script>
package com.spring.db.repository;

@Repository //자동 빈 등록
public class ScoreDAO implements IScoreDAO {	
	//내부클래스 선언
	class scoreMapper implements RowMapper<ScoreVO> {
		
		@Override
		public ScoreVO mapRow(ResultSet rs, int rowNum) throws SQLException {
			
			System.out.println("mapRow 메서드 발동!");
			System.out.println("rowNum:" + rowNum);
			
			ScoreVO vo = new ScoreVO();
			vo.setStuId(rs.getInt("stu_id"));
			vo.setStuName(rs.getString("stu_name"));
			vo.setKor(rs.getInt("kor"));
			vo.setEng(rs.getInt("eng"));
			vo.setMath(rs.getInt("math"));
			vo.setTotal(rs.getInt("total"));
			vo.setAverage(rs.getDouble("average"));
			
			return vo;
		}
	}
	
	//# Spring-jdbc 방식의 처리 : JdbcTemplate 활용
	
	@Autowired
	private JdbcTemplate template;
	
	@Override
	public void insertScore(ScoreVO score) {
		String sql = "INSERT INTO scores VALUES(id_seq.NEXTVAL,?,?,?,?,?,?)";
		template.update(sql, score.getStuName(), score.getKor(), score.getEng(),
				score.getMath(), score.getTotal(), score.getAverage());
		//sql전달하고 물음표 채우기
	}

	@Override
	public List<ScoreVO> selectAllScores() {
		String sql = "SELECT * FROM scores ORDER BY stu_id ASC";
		return template.query(sql, new ScoreMapper());
		
		//람다식 이용(다른 메서드에서 사용 불가)
		return template.query(sql, (rs, roNum) -> {
			ScoreVO vo = new ScoreVO();
			vo.setStuId(rs.getInt("stu_id"));
			vo.setStuName(rs.getString("stu_name"));
			vo.setKor(rs.getInt("kor"));
			vo.setEng(rs.getInt("eng"));
			vo.setMath(rs.getInt("math"));
			vo.setTotal(rs.getInt("total"));
			vo.setAverage(rs.getDouble("average"));
			
			return vo;
		});//객체 여러개를 리턴할때 template.query()
	}

	@Override
	public void deleteScore(int num) {
		String sql = "DELETE FROM scores WHERE stu_id = ?";
		template.update(sql, num);
	}

	@Override
	public ScoreVO selectOne(int num) {
		String sql = "SELECT * FROM scores WHERE stu_id = ?";
		try {
			ScoreVO vo = template.queryForObject(sql, new ScoreMapper(),num);
			return vo;
		} catch (Exception e) {
			return null;
		}
	}
	//객체를 하나만 리턴할때는 template.queryForObject()
	// template.queryForObject() 데이터가 없으면 null이아니라 예외를 던지기 때문에 try{} catch 사용
}
</script>

ScoreController.java

<script>
package com.spring.db.controller;

@Controller
@RequestMapping("/score")
public class ScoreController {
	
	//컨트롤러와 서비스 계층 사이의 의존성 자동 주입을 위해 변수를 선언.
	@Autowired
	private IScoreService service;
	
	//점수 등록 화면을 열어주는 처리를 하는 메서드
	@GetMapping("/register")
	public String register() {
		System.out.println("/score/register: GET");
		return "score/write-form";
	}
	
	//점수 등록 요청을 처리할 메서드
	@PostMapping("/register")
	public String register(ScoreVO vo) {
		System.out.println("/score/register: POST");
		System.out.println("param: " + vo);
		service.insertScore(vo);//service에 받아온 객체 넣기
		return "score/write-result";
	}
	
	//점수 전체 조회를 처리하는 요청 메서드
	@GetMapping("/list")
	public void list(Model model) {
		System.out.println("/score/list: GET");
		model.addAttribute("sList", service.selectAllScores());
	}
	
	//점수 삭제 요청 처리 메서드
	@GetMapping("/delete")
	public String delete(@RequestParam("stuNum") int stuNum, RedirectAttributes ra) {		
		ra.addFlashAttribute("msg","delSuccess");//경고창
		return "redirect:/score/list";
	}
	
	//점수 개별 조회 화면 처리 메서드
	@GetMapping("/search")
	public void search() {
		System.out.println("/score/search: GET");
	}
	
	//점수 개별 조회 요청 처리 메서드
	@GetMapping("/selectOne")
	public String selectOne(@RequestParam("stuNum") int stuNum, 
							Model model, RedirectAttributes ra) {	
		ScoreVO vo = service.selectOne(stuNum);
		System.out.println(vo);
		if(vo == null) {
			ra.addFlashAttribute("msg","검색 결과가 없습니다.");
			return "redirect:/score/search";
		}
		model.addAttribute("stu", vo);
		return "score/search-result";
	}
}
</script>

jsp

jsp같은 경우는 학생번호를 원래 list의 index를 이용했었는데
이제는 DB연동을 하기때문에 시퀀스가 자동으로 올려주어 학생번호만 수정하였다.
나머지는 전에 예제와 같음.

0개의 댓글