[Web] Layered Structure 실습 - 방명록 구현
spring framework MVC 패턴을 Layered Structure를 사용해 방명록(guestbook) 웹페이지를 만들었다.
- 구조
- 요구사항
이클립스에서 maven web app 프로젝트를 새로 생성해준다. 역시 제일 먼저 해야 할 일은 각종 플러그인과 dependency 설정. pom.xml과 web.xml 에 스프링 프레임워크와 jstl, json, DB를 사용하기위한 각종 설정을 해준다. 설정을 끝내면 꼭 메이븐 프로젝트를 업데이트 해주고 이클립스를 재시작 해야한다.
프로그램 동작은 아래와 같다.
웹 레이어 설정 파일 : web.xml, WebMvcContextConfiguration.java
비즈니스, 레파지토리 레이어 설정 파일 : ApplciationConfig.java(import->), DBConfig.java
GuestbookController : url mapping control
GuestbookService : 비즈니스 로직을 가지고 있다.
GuestbookServiceImpl : GuestbookService를 실제 구현하고 있는 클래스
LogDto, GuestbookDto - LogDao, GuestbookDao 또한 사용하고있다.
GuestbookDaoSqls : db 에서 사용할 sql문들을 저장하고 있음
전체 구조
* package나 파일명을 지을 때 오타를 내지 않도록 주의, 또 주의하자 .. 나는 패키지명에 오타를 냈다가 몇시간동안 404 오류를 해결하지 못해서 config 파일만 주구장창 봤다. ㅠ
* 예제를 따라서 수행하다 보니 Test.java 파일도 각 패키지 안에 같이 존재하는데 이렇게 하는 것 보다 test용 패키지를 따로 만들어서 테스팅 하는 것이 좋다.
1. Configuration class 작성
'ApplicationConfigjava'
* ComponentScan의 basePackages 선언할 때 패키지명을 kr.or... 부터 정확히 명시하지 않으면 제대로 탐색되지 않으므로 주의!
'DBConfig.java'
* 예제를 따라 해보느라 db 접속 정보가 모두 같은 코드에 있는데 이는 보안상 좋지 못하므로 data 파일을 따로 만들어서 보관해야한다.
* 트랜잭션을 사용하기 위해 TransactionManagementConfigurer를 import 하고 있다.
'WebMvcContextConfig.java'
프로젝트가 실행되면 메인 페이지인 '/WEB-INF/views/ 안에 index.jsp view를 불러오게된다.
2. Dto 만들기
+ getter, setter, toString
3. Dao 만들기
- 필요한 메서드들
1. 전체 방명록 수 조회
2. 전체 방명록 db에서 조회 하여 List로 가져오기
3. 한 건의 방명록 insert
4. id로 하나의 방명록 delete
먼저 필요한 sql문들을 static 상수로 선언한다.
'guestbook.dao.GuestbookSqls.java'
'GuestbookDao.java'
* insert는 SimpleJdbcInsert 객체로 이루어진다. id 를 자동으로 생성하는 부분을 주의
'Logdao.java'
간단한 insert문만 있으면 된다.
4. Service 만들기
'guestbook.service.GuestbookService.java'
'guestbook.service.impl.GuestbookServiceImpl.java'
* Autowired 어노테이션으로 dao를 사용할 수 있게 한다.
* Transaction 사용을 위해 Transactional 어노테이션을 준다. 내용을 쓰는 트랜잭션에는 디폴트값이 true인 readOnly 옵션에 false를 준다.
5. Controller
'GuestbookController.java'
6. jsp 파일 생성
'index.jsp'
별다른 역할 없이 list 로 redirect만 하면 된다.
'list.jsp' - 주어진 간단한 형식이 있었지만 제대로 만들어보고 싶어서 css도 넣고 여러가지를 수정했다.
그 결과물,
- 구조
- 요구사항
- 방명록 정보는 guestbook table에 (id, 이름, 내용, 등록일)로 저장. id는 자동 입력
- http://localhost:8080guestbook/을 요청하면 자동으로 /guestbook/list로 redirect한다.
- 등록 폼을 채우고 '등록'버튼을 누르면 /guestbook/write url로 입력한 값을 전달하여 저장. 저장된 이후에는 /guestbook/list로 리다이렉트 된다
- 방명록이 없으면 건수는 0으로 출력.
- 방명록 내용과 폼 사이에 방명록 페이지 링크를 표시. 방명록 5건당 1페이지로 설정
- 6번째 방명록이 입력되면 페이지 수가 2까지 출력된다.
- 1페이지를 누르면 /guestbook/list?start=0을 요청 (=/guestbook/list)
- 2페이지를 누르면 /guestbook/list?start=5를 요청
- 방명록에 글을 쓰거나 삭제하면 log 테이블에 (id, client ip address, time, method (등록/삭제)) 데이터가 저장됨. id는 자동 입력
이클립스에서 maven web app 프로젝트를 새로 생성해준다. 역시 제일 먼저 해야 할 일은 각종 플러그인과 dependency 설정. pom.xml과 web.xml 에 스프링 프레임워크와 jstl, json, DB를 사용하기위한 각종 설정을 해준다. 설정을 끝내면 꼭 메이븐 프로젝트를 업데이트 해주고 이클립스를 재시작 해야한다.
프로그램 동작은 아래와 같다.
웹 레이어 설정 파일 : web.xml, WebMvcContextConfiguration.java
비즈니스, 레파지토리 레이어 설정 파일 : ApplciationConfig.java(import->), DBConfig.java
GuestbookController : url mapping control
GuestbookService : 비즈니스 로직을 가지고 있다.
GuestbookServiceImpl : GuestbookService를 실제 구현하고 있는 클래스
LogDto, GuestbookDto - LogDao, GuestbookDao 또한 사용하고있다.
GuestbookDaoSqls : db 에서 사용할 sql문들을 저장하고 있음
전체 구조
* package나 파일명을 지을 때 오타를 내지 않도록 주의, 또 주의하자 .. 나는 패키지명에 오타를 냈다가 몇시간동안 404 오류를 해결하지 못해서 config 파일만 주구장창 봤다. ㅠ
* 예제를 따라서 수행하다 보니 Test.java 파일도 각 패키지 안에 같이 존재하는데 이렇게 하는 것 보다 test용 패키지를 따로 만들어서 테스팅 하는 것이 좋다.
1. Configuration class 작성
'ApplicationConfigjava'
package kr.or.connect.guestbook.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@ComponentScan(basePackages = { "kr.or.connect.guestbook.dao", "kr.or.connect.guestbook.service" })
@Import({ DBConfig.class })
public class ApplicationConfig {
}
| cs |
* ComponentScan의 basePackages 선언할 때 패키지명을 kr.or... 부터 정확히 명시하지 않으면 제대로 탐색되지 않으므로 주의!
'DBConfig.java'
@Configuration
@EnableTransactionManagement
public class DBConfig implements TransactionManagementConfigurer {
private String driverClassName = "com.mysql.jdbc.Driver";
private String url = "jdbc:mysql://localhost:3306/connectdb?useUnicode=true&characterEncoding=utf8";
private String username = "connectuser";
private String password = "1234";
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
@Override
public PlatformTransactionManager annotationDrivenTransactionManager() {
return transactionManger();
}
@Bean
public PlatformTransactionManager transactionManger() {
return new DataSourceTransactionManager(dataSource());
}
}
| cs |
* 예제를 따라 해보느라 db 접속 정보가 모두 같은 코드에 있는데 이는 보안상 좋지 못하므로 data 파일을 따로 만들어서 보관해야한다.
* 트랜잭션을 사용하기 위해 TransactionManagementConfigurer를 import 하고 있다.
'WebMvcContextConfig.java'
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "kr.or.connect.guestbook.controller" })
public class WebMvcContextConfiguration extends WebMvcConfigurerAdapter {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/css/**").addResourceLocations("/css/").setCachePeriod(31556926);
registry.addResourceHandler("/img/**").addResourceLocations("/img/").setCachePeriod(31556926);
registry.addResourceHandler("/js/**").addResourceLocations("/js/").setCachePeriod(31556926);
}
// default servlet handler를 사용하게 합니다.
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
@Override
public void addViewControllers(final ViewControllerRegistry registry) {
System.out.println("addViewControllers가 호출됩니다. ");
registry.addViewController("/").setViewName("index");
}
@Bean
public InternalResourceViewResolver getInternalResourceViewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}
}
| cs |
프로젝트가 실행되면 메인 페이지인 '/WEB-INF/views/ 안에 index.jsp view를 불러오게된다.
2. Dto 만들기
public class Guestbook {
private Long id;
private String name;
private String content;
private Date regdate;
| cs |
public class Log {
private Long id;
private String ip;
private String method;
private Date regdate;
| cs |
+ getter, setter, toString
3. Dao 만들기
- 필요한 메서드들
1. 전체 방명록 수 조회
2. 전체 방명록 db에서 조회 하여 List로 가져오기
3. 한 건의 방명록 insert
4. id로 하나의 방명록 delete
먼저 필요한 sql문들을 static 상수로 선언한다.
'guestbook.dao.GuestbookSqls.java'
public class GuestbookSqls {
public static final String DELETE_BY_ID = "DELETE FROM guestbook WHERE id= :id";
public static final String SELECT_COUNT = "SELECT count(*) FROM guestbook";
public static final String SELECT_PAGING = "SELECT id,name,content,regdate FROM guestbook ORDER BY id DESC limit :start, :limit";
}
| cs |
'GuestbookDao.java'
@Repository
public class GuestbookDao {
private NamedParameterJdbcTemplate jdbc;
private RowMapper<Guestbook> rowMapper = BeanPropertyRowMapper.newInstance(Guestbook.class);
private SimpleJdbcInsert insertAction;
public GuestbookDao(DataSource dataSource) {
this.jdbc=new NamedParameterJdbcTemplate(dataSource);
this.insertAction = new SimpleJdbcInsert(dataSource).withTableName("guestbook")
.usingGeneratedKeyColumns("id");
}
public int deleteById(Long id) {
Map<String,?> params=Collections.singletonMap("id", id);
return jdbc.update(DELETE_BY_ID, params);
}
public Long insert(Guestbook guestBook) {
SqlParameterSource params = new BeanPropertySqlParameterSource(guestBook);
return insertAction.executeAndReturnKey(params).longValue();
}
public List<Guestbook> selectAll(Integer start, Integer limit){
Map<String, Integer> params = new HashMap<>();
params.put("start", start);
params.put("limit",limit);
return jdbc.query(SELECT_PAGING, params, rowMapper);
}
public int selectCount() {
return jdbc.queryForObject(SELECT_COUNT,Collections.emptyMap(),Integer.class);
}
}
| cs |
* insert는 SimpleJdbcInsert 객체로 이루어진다. id 를 자동으로 생성하는 부분을 주의
'Logdao.java'
@Repository
public class LogDao {
private NamedParameterJdbcTemplate jdbc;
private SimpleJdbcInsert insertAction;
public LogDao(DataSource dataSource) {
this.jdbc = new NamedParameterJdbcTemplate(dataSource);
this.insertAction = new SimpleJdbcInsert(dataSource).withTableName("log").usingGeneratedKeyColumns("id");
}
public Long insert(Log log) {
SqlParameterSource params = new BeanPropertySqlParameterSource(log);
return insertAction.executeAndReturnKey(params).longValue();
// insert문을 내부적으로 생성해서 실행하고 자동으로 생성된 id값을 반환하게 된다.
}
}
| cs |
간단한 insert문만 있으면 된다.
4. Service 만들기
'guestbook.service.GuestbookService.java'
public interface GuestbookService {
public static final Integer LIMIT=5;
public Guestbook addGuestbook(Guestbook guestbook, String ip);
public int deleteGuestbook(Long id, String ip);
public int getCount();
public List<Guestbook> getGuestbooks(Integer start);
}
| cs |
'guestbook.service.impl.GuestbookServiceImpl.java'
@Service
public class GuestbookServiceImpl implements GuestbookService {
@Autowired
GuestbookDao guestbookDao;
@Autowired
LogDao logDao;
@Override
@Transactional(readOnly = false)
public Guestbook addGuestbook(Guestbook guestbook, String ip) {
guestbook.setRegdate(new Date());
Long id = guestbookDao.insert(guestbook);
guestbook.setId(id);
Log log = new Log();
log.setIp(ip);
log.setMethod("insert");
log.setRegdate(new Date());
logDao.insert(log);
return guestbook;
}
@Override
@Transactional(readOnly = false)
public int deleteGuestbook(Long id, String ip) {
int deleteCount = guestbookDao.deleteById(id);
Log log = new Log();
log.setIp(ip);
log.setMethod("delete");
log.setRegdate(new Date());
logDao.insert(log);
return deleteCount;
}
@Override
public int getCount() {
return guestbookDao.selectCount();
}
@Override
@Transactional // readonly
public List<Guestbook> getGuestbooks(Integer start) {
List<Guestbook> list = guestbookDao.selectAll(start, LIMIT);
return list;
}
}
| cs |
* Autowired 어노테이션으로 dao를 사용할 수 있게 한다.
* Transaction 사용을 위해 Transactional 어노테이션을 준다. 내용을 쓰는 트랜잭션에는 디폴트값이 true인 readOnly 옵션에 false를 준다.
5. Controller
'GuestbookController.java'
@Controller
public class GuestbookController {
@Autowired
GuestbookService guestbookService;
@GetMapping(path = "/list")
public String list(@RequestParam(name = "start", required = false, defaultValue = "0") int start, ModelMap model) {
// start로 시작하는 방명록 목록 구하기
List<Guestbook> list = guestbookService.getGuestbooks(start);
// 전체 페이지수 구하기
int count = guestbookService.getCount();
int pageCount = count / GuestbookService.LIMIT;
if (count % GuestbookService.LIMIT > 0)
pageCount++;
// 페이지 수만큼 start의 값을 리스트로 저장
// 예를 들면 페이지수가 3이면
// 0, 5, 10 이렇게 저장된다.
// list?start=0 , list?start=5, list?start=10 으로 링크가 걸린다.
List<Integer> pageStartList = new ArrayList<>();
for (int i = 0; i < pageCount; i++) {
pageStartList.add(i * GuestbookService.LIMIT);
}
model.addAttribute("list", list);
model.addAttribute("count", count);
model.addAttribute("pageStartList", pageStartList);
return "list";
}
@PostMapping(path = "/write")
public String write(@ModelAttribute Guestbook guestbook, HttpServletRequest request) {
String clientIp = request.getRemoteAddr();
guestbookService.addGuestbook(guestbook, clientIp);
return "redirect:list";
}
}
| cs |
6. jsp 파일 생성
'index.jsp'
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
response.sendRedirect("list");
%>
| cs |
별다른 역할 없이 list 로 redirect만 하면 된다.
'list.jsp' - 주어진 간단한 형식이 있었지만 제대로 만들어보고 싶어서 css도 넣고 여러가지를 수정했다.
그 결과물,
곧 삭제 버튼도 추가해서 사용 할 예정 !
No comments: