데이터융합 JAVA응용 SW개발자 기업 채용연계 연수과정 60일차 강의 정리

misung·2022년 6월 21일
0

Spring

JDBC 연결 실습 (이어서)

기본 구조 잡기

  1. com.spring.db 하위에 controller 패키지 생성.
    (com.spring.db.controller)

  2. 마찬가지로 model, service, repository 패키지들도 생성.

  3. 전의 SpringWebBasic 프로젝트의 model 패키지에서 ScoreVO 클래스를 복사해 온다.

package com.spring.db.model;

public class ScoreVO {

	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 String getStuName() {
		return stuName;
	}

	public void setStuName(String stuName) {
		this.stuName = stuName;
	}

	public int getKor() {
		return kor;
	}

	public void setKor(int kor) {
		this.kor = kor;
	}

	public int getEng() {
		return eng;
	}

	public void setEng(int eng) {
		this.eng = eng;
	}

	public int getMath() {
		return math;
	}

	public void setMath(int math) {
		this.math = math;
	}

	public int getTotal() {
		return total;
	}

	public void setTotal(int total) {
		this.total = total;
	}

	public double getAverage() {
		return average;
	}

	public void setAverage(double average) {
		this.average = average;
	}

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

ScoreVO 수정

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;
	}

	public String getStuName() {
		return stuName;
	}

	public void setStuName(String stuName) {
		this.stuName = stuName;
	}

	public int getKor() {
		return kor;
	}

	public void setKor(int kor) {
		this.kor = kor;
	}

	public int getEng() {
		return eng;
	}

	public void setEng(int eng) {
		this.eng = eng;
	}

	public int getMath() {
		return math;
	}

	public void setMath(int math) {
		this.math = math;
	}

	public int getTotal() {
		return total;
	}

	public void setTotal(int total) {
		this.total = total;
	}

	public double getAverage() {
		return average;
	}

	public void setAverage(double average) {
		this.average = average;
	}

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

어제자 강의에 우리가 만든 테이블에 stu_id 가 있었으므로 이 클래스에 새로이 추가하고, set(), get() 메서드를 만들어 줌과 함께 toString() 메서드를 다시 생성한다. (stuId가 누락되어 있었으므로)

DAO, Service, Controller 복사

전의 SpringWebBasic의 repository 패키지 내의 IScoreDAO와 ScoreDAO를 복사해 온다.

IScoreDAO.java

package com.spring.db.repository;

import java.util.List;

import com.spring.basic.model.ScoreVO;

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

ScoreDAO.java

package com.spring.db.repository;

import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Repository;

import com.spring.basic.model.ScoreVO;

@Repository
public class ScoreDAO implements IScoreDAO {

	List<ScoreVO> scoreList = new ArrayList<>();
	
	@Override
	public void insertScore(ScoreVO score) {
		scoreList.add(score);
	}

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

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

	@Override
	public ScoreVO selectOne(int num) {
		return scoreList.get(num);
	}

}

그리고 Service들도 복사하고..

IScoreService.java

package com.spring.basic.service;

import java.util.List;

import com.spring.basic.model.ScoreVO;

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

ScoreService.java

package com.spring.basic.service;

import java.util.List;

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

import com.spring.basic.model.ScoreVO;
import com.spring.basic.repository.IScoreDAO;

@Service
public class ScoreService implements IScoreService {

	@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 - 1);
	}

	@Override
	public ScoreVO selectOne(int num) {
		return dao.selectOne(num - 1);
	} 

}

컨트롤러도 떼온다.

ScoreController.java

package com.spring.basic.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 org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.spring.basic.model.ScoreVO;
import com.spring.basic.service.IScoreService;
import com.spring.basic.service.ScoreService;

@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);
		return "score/write-result";
	}
	
	// 점수 전체 조회를 처리하는 요청 메서드
	@GetMapping("/list")
	public void list(Model model) {
		System.out.println("/score/list: GET");
		// List<ScoreVO> list = service.selectAllScores();
		model.addAttribute("sList", service.selectAllScores());
	}
	
	// 점수 삭제 요청 처리 메서드
	@GetMapping("/delete")
	public String delete(@RequestParam("stuNum") int stuNum,
						RedirectAttributes ra) {
		//삭제 처리를 완료하신 후 list 요청이 다시 컨트롤러로 들어갈 수 있도록 처리해 보세요.
	    //list요청이 다시 들어가서 list.jsp로 갔을 때, 삭제 이후에 간 것이 판단된다면
	    //브라우저에 '삭제가 완료되었습니다.' 문구를 빨간색으로 띄워보세요.
	    //(RedirectAttributes 사용, 경고창으로 띄워도 좋아요.)
		System.out.println("삭제할 학번 : " + stuNum);
		service.deleteScore(stuNum);
		
		ra.addFlashAttribute("msg", stuNum + "번 학생의 삭제가 완료되었습니다");
		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) {
		List<ScoreVO> list = service.selectAllScores();
		
		if (stuNum > list.size() || stuNum <= 0) {
			ra.addFlashAttribute("msg", "학번 정보가 없습니다.");
			return "redirect:/score/search";
		} else {
			model.addAttribute("stu", service.selectOne(stuNum));
			return "score/search-result";
		}
	}
}

마지막으로 이전 프로젝트의 Views 내의 score 디렉토리를 통째로 복사해 온다. jsp 파일 내용을 다 복붙하기엔 시간적 여유가 없어서 생략하겠다.

가져온 클래스 수정

Ctrl + Shift + o 를 눌러서 패키지명이 다른 클래스들을 해당 단축키로 수정해 줌.

그리고 ScoreDAO같은 경우 내용을 비울 것. 다음과 같이

package com.spring.db.repository;

import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Repository;

import com.spring.db.model.ScoreVO;

@Repository
public class ScoreDAO implements IScoreDAO {

	@Override
	public void insertScore(ScoreVO score) {

		
	}

	@Override
	public List<ScoreVO> selectAllScores() {

		return null;
	}

	@Override
	public void deleteScore(int num) {

		
	}

	@Override
	public ScoreVO selectOne(int num) {

		return null;
	}

}

이렇게 하면 됨.

root-context.xml 수정

루트 컨텍스트는 JDBC 관련 bean을 등록하는 곳이다.

해당 xml 파일클 클릭하고 namespace 탭으로 가서 jdbcmvc 에 체크를 해 준다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:jdbc="http://www.springframework.org/schema/jdbc"
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xmlns:mybatis-spring="http://mybatis.org/schema/mybatis-spring"
	xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.3.xsd
		http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd
		http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring-1.2.xsd
		http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
	
	<!-- JDBC, DB 관련 빈을 등록하고 관리하는 설정 파일 -->
	
	<!-- 히카리 커넥션 풀 빈 등록 -->
	<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>
	
	<!-- 히카리 데이터소스 빈 등록 -->
	<bean id="ds" class="com.zaxxer.hikari.HikariDataSource">
		<constructor-arg ref="hikariConfig" />
	</bean>
	
	<!-- Spring JDBC를 사용하기 위한 핵심 객체 JdbcTemplate 클래스 빈 등록 -->
	<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
		<property name="dataSource" ref="ds" />
	</bean>
	
	<!-- 마이바티스 SQL 동작을 위한 핵심 객체 SqlSessionFactory 클래스 빈 등록 -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="ds" />
		<property name="mapperLocations" value="classpath:/mappers/**/*Mapper.xml" />
<!-- 		<property name="mapperLocations">
			<array>
				<value>classpath:/mappers/score/ScoreMapper.xml</value>
				<value>classpath:/mappers/board/BoardMapper.xml</value>
			</array>
		</property>
 -->	
 	</bean>
	
	<mybatis-spring:scan base-package="com.spring.db.repository"/>
	
		
</beans>

히카리 커넥션 풀 부분은 직접 입력하도록 한다.
아마 커넥션 에러가 생기면 oracle.jdbc.driver.OracleDriver 부분을 oracle.jdbc.OracleDriver 로 고쳐봐라.

ScoreDAO.java 수정

package com.spring.db.repository;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import com.spring.db.model.ScoreVO;

@Repository
public class ScoreDAO implements IScoreDAO {

	// # 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());
	}

	@Override
	public List<ScoreVO> selectAllScores() {

		return null;
	}

	@Override
	public void deleteScore(int num) {

		
	}

	@Override
	public ScoreVO selectOne(int num) {

		return null;
	}

}

insertScore() 메서드를 보면, 스프링을 사용한 경우엔 이전에 JSP에서 했던 기타 복잡한 코드를 작성할 필요가 없어진다.

이제 여기까지 했으면 프로젝트를 실행하고,

~/score/register URL을 입력해서 점수 등록 페이지로 이동해보자.

점수를 입력하고 저장 링크를 클릭한 다음, 이클립스상에서 콘솔창에 뜨는 확인용 한글 문구가 깨지는 경우 web.xml에 다음과 같은 처리를 해 두자.

<!-- 한글 인코딩 필터 설정(톰캣 내부의 한글처리) -->
	<filter>
		<filter-name>encodingFilter</filter-name>
		<filter-class>
			org.springframework.web.filter.CharacterEncodingFilter
		</filter-class>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>UTF-8</param-value>
		</init-param>
		<init-param>
			<param-name>forceEncoding</param-name>
			<param-value>true</param-value>
		</init-param>
	</filter>
	<!-- 위에 지정한 encodingFilter이름을 모든 패턴에 적용 -->
	<filter-mapping>
		<filter-name>encodingFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>

그리고 오라클 SQL 디벨로퍼 프로그램에서 SELECT 문을 사용하여 scores 테이블을 조회해 보고, 데이터가 잘 들어갔으면 연결이 잘 이루어지고 있다는 뜻이다.

[ScoreDAO] selectAllScores() 구현

package com.spring.db.repository;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import com.spring.db.commons.ScoreMapper;
import com.spring.db.model.ScoreVO;

@Repository
public class ScoreDAO implements IScoreDAO {

	// # 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());
	}

	@Override
	public List<ScoreVO> selectAllScores() {
		String sql = "SELECT * FROM scores ORDER BY stu_id ASC";
		return template.query(sql, new ScoreMapper());
	}

	@Override
	public void deleteScore(int num) {

		
	}

	@Override
	public ScoreVO selectOne(int num) {

		return null;
	}

}

Mapper 라는 인터페이스를 구현해야 하므로 아래의 클래스를 만들어 구현하고, new ScoreMapper() 의 형태로 객체를 새로 만들어 전달한다. 동작 방식은 아래에서 서술한다.

ScoreMapper 구현

com.spring.db.commons 패키지 내에 ScoreMapper 클래스 생성.

이 클래스를 만드는 이유는, JdbcTemplate에서 SELECT 쿼리를 위한 ResultSet 사용을 편하게 하기 위해서라고 한다.

RowMapper 인터페이스를 구현해야 사용 가능하다고 한다.

음.. 솔직히 잘 모르겠지만 일단 따라쳐보면 알 수 있겠지?

package com.spring.db.common;

import java.sql.ResultSet;
import java.sql.SQLException;

import org.springframework.jdbc.core.RowMapper;

import com.spring.db.model.ScoreVO;

public 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;
	}
}

지금 이렇게만 봐서는 하나의 컬럼에 대해서만 반환을 해 주는 것 처럼 보이는데, 강의를 듣고 흐름이 어떻게 되는지 더 파악해 보기로 했다.

동작 방식이 어떻게 되는거지?
  1. template.query() 메서드에 의해 JdbcTemplate이 sql과 ScoreMapper 객체를 받음
  2. 받아온 매개변수들을 대상으로 sql문을 실행한다
  3. 전체 조회 결과(ResultSet)을 받고, 우리가 전달했던 ScoreMapper 클래스가 어떤 값을 조회해야 하는지 를 알려준다.
  4. JdbcTemplate는 ScoreMapper 클래스 내의 mapRow() 메서드에게 ResultSet과 몇 번째 행인지에 대한 값(rowNum) 을 전달한다.
  5. 그러면 조회된 값을 VO 안에 각각 대입하여 포장한 다음, 리턴한다.

아하, 잘 보니 여기가 중요했다.

@Override
	public List<ScoreVO> selectAllScores() {
		String sql = "SELECT * FROM scores ORDER BY stu_id ASC";
		return template.query(sql, new ScoreMapper());
	}

DAO 클래스의 해당 부분을 보면, 반환형이 List<> 이다.

list.jsp 수정

전에 학생 점수를 조회할 때는 index + 1을 한 값으로 몇 번째 학생인지를 보여줬지만, 이번엔 학번에 해당하는 stuId라는 값을 가지고 있으므로 이걸 보여주면 된다.

<c:forEach var="stu" items="${sList}" varStatus="stuNum">
		<p>
			학번: ${stu.stuId}, 이름: ${stu.stuName}, 국어: ${stu.kor}, 영어: ${stu.eng}, 
			수학: ${stu.math}, 총점: ${stu.total}, 평균: ${stu.average}
			<a href="<c:url value='/score/delete?stuNum=${stu.stuId}' />">[삭제]</a>
		</p>
	</c:forEach>

따라서 이렇게 변경한다.

그리고 컨텍스트 루트가 바뀌었음에도 페이지가 잘 동작하는건 jstl의 <c:url ... /> 덕분이다.

[ScoreDAO] deleteScore() 메서드 구현

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

[ScoreController] selectOne 메서드 수정

//점수 개별 조회 처리 요청 메서드
	@GetMapping("/selectOne")
	public String selectOne(@RequestParam("stuNum") int stuNum,
							Model model, RedirectAttributes ra) {
		System.out.println("/score/selectOne: GET");
		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";
		

	}

기존에는 List<ScoreVO> 형태로 service를 통해 학생 점수 리스트를 받아와서 리스트 사이즈보다 큰 학생 학번을 입력받거나 하면 튕기게 했는데, 이제는 삭제가 빈번히 일어나면서 학생 번호가 리스트 학생 개수보다 커질 수도 있으므로, (리스트엔 5명이 있으나 학번이 35, 40 이렇게 되는 등)

기존의 코드는 지우고 위의 코드로 대체한다.

조회하고 싶은 학생의 학번을 View 페이지에서 전달받고 (@RequestParam("stuNum")) 이걸 int stuNum 으로 포장하고서 service 클래스를 통해 해당하는 학생 객체를 받아서 vo에 넣어준다.

당연하게도 조회 결과가 없으면 null일 테고, 그러면 검색 결과가 없다는 메시지를 담아서 view로 보내고,
결과가 있으면 model에 vo를 담아놓고 search-result.jsp 페이지로 리다이렉트시킨다.

[ScoreDAO] selectOne() 메서드 구현

@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;
		}
	}

이렇게 구현되었는데, sql문은 알 테니 생략하고..
selectAllScores() 메서드랑은 다르게 이 메서드의 경우 List<> 로 리턴을 하는게 아니라 한 개의 객체만 보내므로, queryForObject() 메서드를 사용해서 한 개의 객체만 뽑아다 넣는다. (아까 위에서 해당하는 모든 행을 조회할 땐 query() 메서드를 쓰고 있었다.)

ScoreDAO 내에 ScoreMapper 중첩 클래스 선언

...
//내부(중첩) 클래스 (inner class)
	//두 클래스가 굉장히 긴밀한 관계가 있을 때 주로 선언.
	//해당 클래스 안에서만 사용할 클래스를 굳이 새 파일로 선언하지 않고도 만들 수 있다.
	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;
		}

	}
...

어차피 다른 클래스에서는 ScoreMapper 클래스를 사용하지 않고, 이 DAO 클래스에서만 사용중이므로, DAO 클래스 내에 중첩 클래스로써 선언한다.

Board 실습

테이블과 시퀀스 생성

CREATE TABLE jdbc_board (
    board_no NUMBER PRIMARY KEY,
    writer VARCHAR2(30) NOT NULL,
    title VARCHAR2(100) NOT NULL,
    content VARCHAR2(1000)
);

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

[BoardVO] 구현

package com.spring.db.model;

/*
 CREATE TABLE jdbc_board (
    board_no NUMBER PRIMARY KEY,
    writer VARCHAR2(30) NOT NULL,
    title VARCHAR2(100) NOT NULL,
    content VARCHAR2(1000)
);

CREATE SEQUENCE bid_seq
    START WITH 1
    INCREMENT BY 1
    MAXVALUE 1000
    NOCYCLE
    NOCACHE;
 */

public class BoardVO {
	
	private int boardNo;
	private String writer;
	private String title;
	private String content;
	
	public BoardVO() {}
	
	
	public BoardVO(int boardNo, String writer, String title, String content) {
		super();
		this.boardNo = boardNo;
		this.writer = writer;
		this.title = title;
		this.content = content;
	}

	public void setBoardNo(int boardNo) {
		this.boardNo = boardNo;
	}
	
	public int getBoardNo() {
		return boardNo;
	}
	
	public String getWriter() {
		return writer;
	}
	public void setWriter(String writer) {
		this.writer = writer;
	}
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public String getContent() {
		return content;
	}
	public void setContent(String content) {
		this.content = content;
	}

	@Override
	public String toString() {
		return "BoardVO [boardNo=" + boardNo + ", writer=" + writer + ", title=" + title + ", content=" + content + "]";
	}
}

[IBoardDAO], [BoardDAO], [IBoardService], [BoardService], [BoardController] 복붙 후 수정

전에 만든 프로젝트에서 각각의 패키지에 해당하는 곳으로 전부 복사하여 붙여넣기한다.

에러 뜨는 부분들은 각 클래스 상단의 패키지명 (import 부분) 이 잘못된 부분이 있을테니 Ctrl + Shift + o 로 전부 수정한다.

[list.jsp] 수정

<form action="<c:url value='/board/searchList' />">
		<input type="text" name="keyword" placeholder="작성자 이름을 입력하세요.">
		<input type="submit" value="검색">
	</form>

이 코드를 추가하고, 이에 해당하는 메서드를 나중에 구현해보자.

[BoardDAO] 대거 수정

package com.spring.db.repository;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;

import com.spring.db.model.BoardVO;

@Repository
public class BoardDAO implements IBoardDAO {
	
	@Autowired
	private JdbcTemplate template;
	
	//내부 클래스 선언
	class BoardMapper implements RowMapper<BoardVO> {
		@Override
		public BoardVO mapRow(ResultSet rs, int rowNum) throws SQLException {		 
			return new BoardVO(
					rs.getInt("board_no"),
					rs.getString("writer"),
					rs.getString("title"),
					rs.getString("content")
				);
		}
	}
	

	@Override
	public void insertArticle(BoardVO vo) {
		String sql = "INSERT INTO jdbc_board "
				+ "VALUES(bid_seq.NEXTVAL,?,?,?)";
		template.update(sql, vo.getWriter(), vo.getTitle(), vo.getContent());
	}

	@Override
	public List<BoardVO> getArticles() {
		String sql = "SELECT * FROM jdbc_board ORDER BY board_no DESC";
		return template.query(sql, new BoardMapper());
	}

	@Override
	public BoardVO getArticle(int bId) {
		String sql = "SELECT * FROM jdbc_board WHERE board_no=?";
		return template.queryForObject(sql, new BoardMapper(), bId);
	}

	@Override
	public void deleteArticle(int bId) {
		String sql = "DELETE FROM jdbc_board WHERE board_no=?";
		template.update(sql, bId);
	}

	@Override
	public void updateArticle(BoardVO vo) {
		String sql = "UPDATE jdbc_board "
				+ "SET writer=?, title=?, content=? "
				+ "WHERE board_no=?";
		template.update(sql, vo.getWriter(),vo.getTitle(), 
				vo.getContent(), vo.getBoardNo());
	}
	
	@Override
	public List<BoardVO> searchList(String keyword) {
		String sql = "SELECT * FROM jdbc_board WHERE writer LIKE ?";
		return template.query(sql, new BoardMapper(), keyword);
	}
}

기존 코드를 싹 날리고, 위와 같은 내용으로 재구성한다.

우리는 JdbcTemplate를 사용해야 하므로 당연히 JdbcTemplate를 @Autowired 하여 의존성 주입을 해 준다.

그리고 DAO에서 Mapper를 사용할 것이므로 중첩 클래스로 RowMapper 인터페이스를 끌어온 Mapper 클래스를 선언한다.

[BoardController] 수정

package com.spring.db.controller;

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 org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import com.spring.db.model.BoardVO;
import com.spring.db.service.IBoardService;

@Controller
@RequestMapping("/board")
public class BoardController {

	@Autowired
	private IBoardService service;

	//글 작성 화면을 열어주는 메서드
	@GetMapping("/write")
	public void write() {
		System.out.println("/board/write: GET");
	}

	//작성된 글 등록 처리 요청
	//메서드 이름은 write() 입니다.
	//작성된 글을 DB에 등록 후 /board/list.jsp파일로 응답할 수 있도록
	//(글 목록 보여달라는 요청이 자동으로 들어올 수 있도록) 적절히 처리해 보세요.
	@PostMapping("/write")
	public String write(BoardVO vo) {
		System.out.println("/board/write: POST");
		service.insertArticle(vo);
		return "redirect:/board/list";
	}


	//글 목록 화면 요청
	//메서드 이름 -> list()
	//DB 대용 리스트에서 가지고 온 글 목록을 list.jsp 파일로 전달해서
	//브라우저에 글 목록을 띄워 주시면 되겠습니다.
	//글 목록을 table을 사용해서 간단히 만들어 주세요. (글 번호는 인덱스 이용해서 달아주세요.)
	@GetMapping("/list")
	public void list(Model model) {
		System.out.println("/board/list: GET");
		model.addAttribute("articles", service.getArticles());
	}

	//글 내용 상세보기 요청 처리 메서드
	//메서드 이름 -> content, 요청 url -> /content
	//DB 역할을 하는 리스트에서 글 번호에 해당하는 글 객체를 content.jsp로 보내주세요.
	//content.jsp에서 해당 글 정보를 모두 출력해 주세요. (글 번호도 같이)
	@GetMapping("/content")
	public void content(@RequestParam("boardNo") int boardNo, Model model) {
		System.out.println("/board/content?boardNo=" + boardNo);
		model.addAttribute("article", service.getArticle(boardNo));
	}

	//글 수정하기 화면으로 이동 요청
	//메서드 이름은 modify(), url: /board/modify -> GET
	//수정하고자 하는 글 정보를 모두 받아와서 modify.jsp로 보내 주세요.(글 번호 같이)
	@GetMapping("/modify")
	public void modify(@RequestParam("boardNo") int boardNo, Model model) {
		System.out.println("수정페이지 이동 요청! 번호: " + boardNo);
		model.addAttribute("article", service.getArticle(boardNo));
	}


	//modify.jsp를 생성해서 form태그에 사용자가 처음에 작성했던 내용이 드러나도록
	//배치해 주시고 수정을 받아 주세요.
	//수정 처리하는 메서드: modify(), 요청 url: /modify -> POST
	//수정 처리 완료 이후 방금 수정한 글의 상세보기 요청이 다시 들어올 수 있도록 작성하세요.
	@PostMapping("/modify")
	public String modify(BoardVO vo) {
		System.out.println("글 수정 요청! 번호: " + vo.getBoardNo());
		service.updateArticle(vo);
		return "redirect:/board/content?boardNo=" + vo.getBoardNo();
	}
	
	
	//삭제는 알아서 작성해 주세요. (삭제 클릭하면 해당 글이 삭제될 수 있도록)
	@GetMapping("/delete")
	public String delete(@RequestParam("boardNo") int boardNo) {
		service.deleteArticle(boardNo);
		return "redirect:/board/list";
	}
	
	//게시글 검색 요청
	@GetMapping("/searchList")
	public String searchList(@RequestParam("keyword") String keyword, Model model) {
		model.addAttribute("articles", service.searchList(keyword));
		return "board/list";
	}
}

기존의 코드에서 위와 같은 코드로 수정.

주석 때문에 뭐 엄청 길어져있긴 한데 개별 메서드를 참고해보면 하는 일이 그렇게 어렵지 않다.

[BoardService] 수정

package com.spring.db.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

import com.spring.db.model.BoardVO;
import com.spring.db.repository.IBoardDAO;

@Service
public class BoardService implements IBoardService {
	
	@Autowired
	@Qualifier("boardDAO")
	private IBoardDAO dao;

	@Override
	public void insertArticle(BoardVO vo) {
		dao.insertArticle(vo);
	}

	@Override
	public List<BoardVO> getArticles() {
		return dao.getArticles();
	}

	@Override
	public BoardVO getArticle(int bId) {
		return dao.getArticle(bId);
	}

	@Override
	public void deleteArticle(int bId) {
		dao.deleteArticle(bId);
	}

	@Override
	public void updateArticle(BoardVO vo) {
		dao.updateArticle(vo);
	}
	
	@Override
	public List<BoardVO> searchList(String keyword) {
		return dao.searchList("%" + keyword + "%");
	}

}

위에 @Autowired 부분을 보고 느꼈던 게, IBoardDAO로 dao를 생성한다 쳐도 대체 인터페이스로 객체를 생성하는게 되는건가? 싶었지만, @Qualifier 어노테이션은 사용할 의존 객체를 선택할 수 있게 해 주는 녀석으로, "boardDAO"를 지정하고 있었으니 가능했던 것 같다.

대부분은 신경쓸 곳이 별로 없지만,

@Override
	public List<BoardVO> searchList(String keyword) {
		return dao.searchList("%" + keyword + "%");
	}

이 부분의 경우 검색 시 키워드 앞뒤로 다른 문장이나 글자가 올 수 있으므로 "%keyword%" 의 형태로 가공하여 DAO로 보내주도록 한다.

이렇게까지 구현해두면 이제 잘 작동하므로 이 프로젝트도 JdbcTemplate를 사용한 DB 커넥션 테스트 전환은 끝이 났다.

MyBatis

MyBatis 개념

뭐 개념만 봐서는 무슨 얘긴가 싶다.
일단 한번 라이브러리를 로드해보고 써봐야 알 듯.

mapper.xml을 작성함으로써 DAO 클래스를 작성할 필요가 없다는 듯.

학생 점수 프로젝트 MyBatis로 전환하기

MyBatis 라이브러리 로드

MVNRepository 사이트에서 MyBatis 검색하여 가장 많이 쓰는 버전 로드.

<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.4.6</version>
</dependency>

이걸 가져오기로 했다. 그리고 이렇게 가져오기만 하면 되는게 아닌게, MyBatis는 Spring 전용 라이브러리가 아니므로 (파이썬 등에서도 사용 가능함) 따라서 스프링과 연동시켜줄 라이브러리를 가져와야 한다.

MyBatis-Spring 이라고 MVNRepository에서 검색하면 나오는데, 가장 많이 쓰는 버전으로 또 가져오겠다.

<dependency>
			<groupId>org.mybatis</groupId>
			<artifactId>mybatis-spring</artifactId>
			<version>1.3.2</version>
</dependency>

[root-context.xml] 에 MyBatis 등록시키기

<!-- 마이바티스 SQL 동작을 위한 핵심 객체 SqlSessionFactory 클래스 빈 등록 -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="ds" />
		<property name="mapperLocations" value="classpath:/mappers/**/*Mapper.xml" />
	</bean>

그리고 namespaces 탭으로 이동해서 mybatis-spring에 체크를 해서 네임스페이스를 추가해둔다.

데이터소스를 추가하고, 매퍼의 위치를 알려준다.

그리고 이렇게 하고 나면, mybatis-spring 네임스페이스를 사용할 수 있게 되는데, 위의 <bean> 태그 아래에 다음과 같은 태그를 추가한다.

<mybatis-spring:scan base-package="com.spring.db.repository"/>

base-package 속성값에는, mapper 인터페이스가 어디에 있는지를 알려주면 된다.

이렇게 하고 beans graph 탭으로 가 보면,

인터페이스 타입으로 빈들이 등록되어 있다.
sqlSessionFactory라는 객체가 우리가 작성한 xml파일을 클래스로 변환해서 bean으로 등록하려고 하는데,

클래스가 아니다 보니 마땅한 타입이 없어서, 인터페이스를 등록하는 것이다. (무슨 뜻이람??)

그리고 아까 여기 살짝 주목.

<property name="mapperLocations" value="classpath:/mappers/**/*Mapper.xml" />

[URL패턴]
이거 /**/ 가 무슨 뜻이냐 하면,

  • ? : 하나의 문자열과 정확하게 일치
  • * : 존재하지 않거나, 존재할 수도 있음 (문자열이 있을 수도, 없을 수도 있음. 예. ScoreMapper.xml, Mapper.xml)
  • ** : 아예 존재하지 않거나, 문자열 자체를 표시

[ScoreMapper.xml] 생성하기

src/main/resources 하위에 mappers 디렉토리를 만들고 그 하위에 score 디렉토리를 만들자.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    

<mapper namespace="com.spring.db.repository.IScoreMapper">

	<!-- 점수 등록 기능 -->
	<insert id="insertScore">
		INSERT INTO scores
		VALUES(id_seq.NEXTVAL,#{stuName},#{kor},#{eng},#{math},#{total},#{average})
	</insert>

</mapper>

id에는 구현하고자 하는 메서드 이름을 지정하고, 태그 내에는 sql문을 적는다.
기존 sql문처럼 ? 를 적어두고 나중에 query() 메서드 등에 매개변수를 넘기는것과는 달리, #{변수명} 을 적는 식으로 진행한다.

부연 설명

MyBatis 라이브러리 끼워넣으면서 아마 저 Mapper 위치 정해줄 때 오류가 났을 수도 있는데, 왜냐하면 beans graph로 가 보면 Mapper말고도 DAO같은 애도 딸려왔었던 것을 알 수 있다. Mapper야 뭐 Mapper로 넣으라고 알려줬다 쳐도, DAO는 어떻게 해야 하는 건가? 라고 충돌이 날 수 있었다는데, 그건 아까 Service 클래스 코드를 참고해보면 @Qualifier 를 통해서 주입시킬 빈의 이름? 인지 클래스 이름인지를 지정해서 넣어줬기 때문에 오류가 안 나고 실행이 되었다.

0개의 댓글