[TIL] Day27 - @PathVariable / DAO Pattern

JIONY·2022년 9월 1일
0
post-thumbnail

이전에 작성한 코드를 더 효율적으로 바꿀 수 있는 방법으로 DAO 패턴 적용하기를 했는데 뭐가 내용이 많아서 정신은 없었지만 재밌었음. 역시 효율충 어디 안간다..


경로 변수

  • URL 안에 파라미터를 넣어 주소를 깔끔하게 보이도록 할 수 있음
  • 상세 조회, 삭제처럼 데이터를 하나만 보내면 처리가 가능한 경우에 많이 사용
  • @RequestMapping Annotation에 /{변수} 형식을 사용하여 경로를 지정 가능
@RequestMappig("/detail/{idx}")
public String detail(@ModelAttribute Dto dto, @PathVariable int idx) {
	//...
	return "detail";
}

@PathVariable

  • 가변 경로를 처리할 수 있도록 하는 역할
  • 경로 변수의 값을 파라미터로 받을 수 있음
//요청 파라미터 방식(@RequestParam, @ModelAttribute)
http://localhost:8888/detail?index=5

//경로 변수 방식(@PathVariable, @ModelAttribute)
http://localhost:8080/detail/5


DAO Pattern

  • DAO: Data Access Object, 데이터 접근 객체
    • DB와 관련된 CRUD 작업을 처리하는 도구
    • DAO를 사용하면 데이터베이스 작업을 기능별로 모듈화 할 수 있음

모듈화가 필요한 이유

지금까지는 Controller에서 모든 작업을 처리하도록 구현했음

  • 문제점1: Controller 수가 계속해서 늘어남

  • 문제점2: 엄밀히 말하면 DB 데이터 처리 기능은 Controller의 기능이 아님
    • Controller는 연계 작업만 처리해야 함(데이터를 받아서 누군가를 주고 처리해달라고 한다음에 그 결과를 받아서 안내해주는 역할만 담당)
  • 해결책: 한 마디로 정의할 수 있는 기능들은 모두 모듈로 분리


작동 방식

  • DTO: DB 데이터를 복사한 상자(낱개를 덩어리로 포장하는 목적)
  • DAO: 직접 행동(CRUD)하는 역할(배달부)
  • Controller: 요청을 받고 응답을 반환하는 역할(상담원)
  • Repository
    • 영속성이 있는 데이터(영원히 변하지 않는 것, DB/Files 등)를 제어할 수 있는 모듈
    • repository 패키지에 테이블 당 DAO 하나씩 생성

Bean 생성 Annotation

  • 컨트롤러 : @Controller (프레젠테이션 레이어, 웹 요청과 응답을 처리함)
  • 로직 처리 : @Service (서비스 레이어, 내부에서 자바 로직을 처리함)
  • 외부I/O 처리 : @Repository (퍼시스턴스 레이어, DB나 파일같은 외부 I/O 작업을 처리함)


DB 정보

DAO Pattern을 적용해 데이터의 CRUD를 처리하는 과정을 살펴보려고 함. 아래 Database 정보를 기반으로 진행

CREATE TABLE GUEST_BOOK(
NO NUMBER PRIMARY KEY,
NAME VARCHAR2(21) NOT NULL,
MEMO VARCHAR2(90) NOT NULL
);

CREATE SEQUENCE GB_SEQ;


클래스/인터페이스 생성

  • 하나의 테이블에 대해 DTO, DAO-Interface, DAO-Class, Controller를 생성

DTO 생성

  • DTO 객체는 Bean 객체로 사용하지 않음(동기화 이슈) Bean 객체는 항상 데이터 변경이 없는 객체에 한해 사용할 수 있음
  • 테이블의 데이터를 변환하기 위해 DTO 클래스를 생성
    • 테이블 컬럼을 변수화, getter(), setter(), 기본 생성자, toString()…
package com.jw.spring07rv.entity;

public class GuestBookDto {
	private int no;
	private String name;
	private String memo;
	
	//getter(), setter(), Constructor, toString()...
}

DAO 생성

  • PSA(Portable Service Abstraction) 구조로 생성
    • 인터페이스와 클래스로 분리하여 생성 → 추상화 구조
  • 구현체 GuestBookDaoImpl
    • Bean 객체로 생성, 스프링 컨테이너에 등록(@Repository)
      • GuestBookDao는 인터페이스이므로 등록 불가
    • JdbcTemplate 의존성 주입(@Autowired)
package com.jw.spring07rv.repository;

public interface GuestBookDao {
	//oo기능의 형태(추상 메소드)
}
package com.jw.spring07rv.repository;

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

@Repository
public class GuestBookDaoImpl implements GuestBookDao{
		//oo기능의 형태 + 구현 코드 
	@Autowired
	private JdbcTemplate jdbcTemplate;
}

추상화 구조를 사용하는 이유

  • Controller에 구현체(Class) 대신 추상체(Interface)를 주입함으로써 약결합 프로그램을 만들기 위함. (결합도가 낮고 응집도가 높을 수록 좋은 프로그램)
    • 약결합: 외부 객체 / 인터페이스를 주입받아 그 객체와의 의존도가 낮아지는 것. 객체가 수정되었을 때도 영향을 덜 받음(틀은 같은데 내용만 달라질 때-ex.버전 업그레이드-유용)
    • 강결합: 객체를 내부에서 생성하거나 구현체를 직접 주입받아 그 객체와의 의존도가 높아지는 것. 생성한 객체가 바뀌었을 때 같이 수정해야 할 가능성이 매우 높아짐(= 확장성 떨어짐)
    • 스프링에서는 다양한 객체들을 스프링 컨테이너에 미리 담아두기 때문에 원하는 객체를 주입받아(DI) 사용하기 편함

Controller 생성

  • DB 관련 처리를 위해 GuestBookDao 주입
    • Controller에 인터페이스 형태로 변수를 선언할 경우 @Autowired Annotation이 해당 타입과 상속받은 타입을 스캔해 가능한 객체를 주입
      • GuestBookDaoImpl 클래스의 객체가 주입됨
      • GuestBookDaoImpl 클래스에 주입된 JdbcTemplate이 포함되어 있음
@Controller
public class GuestBookController {
	@Autowired
	private GuestBookDao guestBookDao;
}



DAO & Controller 메소드

  • GuestBookDao(interface): 데이터 CRUD 각각에 대한 메소드 틀을 작성
    • SQL 구문에서 플레이스 홀더로 설정한 값을 파라미터로 지정
  • GuestBookDaoImpl(class) : 추상 메소드 오버라이딩을 통해 처리 코드 구현
  • GuestBookController : DTO에서 데이터를 받아 DAO에 넘기고, DAO로부터 받은 결과를 처리해 return하는 메소드 작성

등록 메소드

GuestBookDao

  • 등록을 위한 SQL 구문
    INSERT INTO GUEST_BOOK(NO, NAME, MEMO) 
    VALUES(GB_SEQ.NEXTVAL, ?, ?);
  • SQL 구문에서 플레이스 홀더로 설정된 값은 곧 GuestBookDto 필드임
    • 파라미터를 GuestBookDto 객체 형태로 선언하면 한 번의 선언으로 데이터를 묶어서 전달받을 수 있음
      • 네 개의 파라미터를 나열하는 방식도 가능
void insert(GuestBookDto dto);
//void insert(int no, String name, String memo);

GuestBookDaoImpl

public void insert(GuestBookDto dto) {
	String sql = "insert into guest_book(no, name, memo) "
					+ "values(guest_book_seq.nextval, ?, ?)";
	Object[] param = {dto.getName(), dto.getMemo()};
	jdbcTemplate.update(sql, param);
}

GuestBookController

@RequestMapping("/insert")
@ResponseBody
public String insert(@ModelAttribute GuestBookDto dto) {
	return "등록 완료";
}

수정 메소드

GuestBookDao

  • 수정을 위한 SQL 구문
    UPDATE GUEST_BOOK SET NAME = ?, MEMO = ? WHERE NO = ?;
  • 파라미터를 GuestBookDto 객체 형태로 선언
  • 수정된 행의 개수를 통해 수정 여부를 판단할 수 있으므로 반환형을 boolean으로 설정
    boolean update(GuestBookDto dto);

GuestBookDaoImpl

public boolean update(GuestBookDto dto) {
	String sql = "update guest_book set name = ?, memo = ? where no = ?";
	Object[] param = {dto.getName(), dto.getMemo(), dto.getNo()};
	//쿼리 실행 결과가 0보다 큰지 판단해서 결과 반환
	return jdbcTemplate.update(sql, param) > 0;
}

GuestBookController

@RequestMapping("/update")
@ResponseBody
public String update(@ModelAttribute GuestBookDto dto) {
	if(guestBookDao.update(dto)) {
		return "수정 완료";
	}else {
		return "없는 번호";	
	}
}

삭제 메소드

GuestBookDao

  • 수정을 위한 SQL 구문
    DELETE GUEST_BOOK WHERE NO = ?;
  • 파라미터가 1개이므로 직접 작성
  • 삭제된 행의 개수를 통해 삭제 여부를 판단할 수 있으므로 반환형을 boolean으로 설정
    boolean delete(int no);

GuestBookDaoImpl

public boolean delete(int no) {
	String sql = "delete guest_book where no = ?";
	Object[] param = {no};
	return jdbcTemplate.update(sql, param) > 0;
}

GuestBookController

@RequestMapping("/delete/{no}")
@ResponseBody
public String delete(@PathVariable int no) {
	if(guestBookDao.delete(no)) { //result 변수 안만들기
		return "삭제 완료";
	}else {
		return "없는 번호";
	}
}

목록/검색결과 조회 메소드

GuestBookDao

  • 조회를 위한 SQL 구문
    SELECT * FROM GUEST_BOOK ORDER BY NO ASC; //목록 조회(여러 행) 
    
    SELECT * FROM GUEST_BOOK
    WHERE INSTR(#1, ?) > 0
    ORDER BY #1 ASC; //키워드 검색결과 조회(단일 행)
  • 반환형을 List로 설정
    List<GuestBookDto> selectList();
    List<GuestBookDto> selectList(String type, String keyword) //오버로딩

GuestBookDaoImpl

  • RowMapper와 ResultSetExtractor를 DAO에 생성(기존에는 DTO에 static으로 생성하고 getter()로 호출)
//RowMapper<T>
private RowMapper<GuestBookDto> mapper = (rs, idx) -> {
	GuestBookDto dto = new GuestBookDto();
	dto.setNo(rs.getInt("no"));
	dto.setName(rs.getString("name"));
	dto.setMemo(rs.getString("memo"));
	return dto;
};
	
//목록 조회
@Override
public List<GuestBookDto> selectList() {
	String sql = "select * from guest_book order by no asc";
	return jdbcTemplate.query(sql, mapper);
}
	
//검색 결과 조회
@Override
public List<GuestBookDto> selectList(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};
	return jdbcTemplate.query(sql, mapper, param);
}

GuestBookController

  • 목록과 검색결과 조회를 한 번에 처리할 수 있는 메소드 작성
    • 목록과 검색의 차이점인 파라미터 존재 여부를 조건으로 설정
  • Annotation에 따라 파라미터의 기본값에 대한 정보를 명시해야 할 수 있음
    • @RequestParam: 스프링 내장 변환기가 다룰 수 있는 모든 타입을 지원
      • 기본값이 없는 경우 required=false로 지정
      • 기본값이 있는 경우 required=false, defaultValue=""
    • @ModelAttribute: 파라미터를 Object 형태로 받을 때 사용
      • 기본값 명시 불필요
@RequestMapping("/list")
@ResponseBody
public String list(
		@RequestParam(required=false)String type, 
		@RequestParam(required=false)String keyword
		) {
	boolean search = type != null && keyword != null; //검색인지 판단
	List<GuestBookDto> data;
	if(search) {
		data = guestBookDao.selectList(type, keyword);
	}else {
		data = guestBookDao.selectList();
	}
	return data.toString();
}

상세(단일) 조회 메소드

GuestBookDao

  • 조회를 위한 SQL 구문
    SELECT * FROM GUEST_BOOK WHERE NO = ?;
  • 반환형을 GuestBookDto로 설정
    • DTO는 테이블 안에 있는 한 개의 행 데이터를 저장하기 위한 클래스이므로 단일 행을 조회하는 경우 반환형을 DTO로 설정

      GuestBookDto selectOne(int no);

GuestBookDaoImpl

  • RowMapper와 ResultSetExtractor를 DAO에 생성(기존에는 DTO에 static으로 생성하고 getter()로 호출)
//ResultSetExtractor<T>
private ResultSetExtractor<GuestBookDto> extractor = (rs) -> {
	if(rs.next()) {
		GuestBookDto dto = new GuestBookDto();
		dto.setNo(rs.getInt("no"));
		dto.setName(rs.getString("name"));
		dto.setMemo(rs.getString("memo"));
		return dto;
	}else {
		return null;
	}
};
	
//상세 조회
@RequestMapping("/detail")
@ResponseBody
public String detail(@RequestParam int no) {
	GuestBookDto dto = guestBookDao.selectOne(no);
	if(dto == null) {
		return "없는 번호";
	}else {
		return dto.toString();
	}
}

GuestBookController

@RequestMapping("/detail")
@ResponseBody
public String detail(@RequestParam int no) {
	GuestBookDto dto = guestBookDao.selectOne(no);
	if(dto == null) {
		return "없는 번호";
	}else {
		return dto.toString();
	}
}




0개의 댓글