mojo's Blog

스프링 기반 뉴스 기사 관리 웹 서비스 본문

Spring

스프링 기반 뉴스 기사 관리 웹 서비스

_mojo_ 2022. 1. 7. 19:04

프로젝트 개요 및 설정

 

※ 프로젝트 개요

결과물은 프로젝트 : 뉴스 기사 관리 웹 서비스 (tistory.com) 와 동일하다.

REST API의 경우의 구현 기능을 그대로 스프링 버전으로 개발하면 된다.

이번 프로젝트의 구현 범위는 다음과 같다.

  • 뉴스 서비스 스프링 WebMVC 컨트롤러 구현
  • 뉴스 서비스 API RestController 구현

모델과 뷰 영역은 기존에 만든 것을 그대로 활용한다.

다만 클래스 이름은 동일하게 사용하고 대신 패키지를 따로 만들어서 관리하도록 한다.

 

※ 개발환경 설정

▶ 기존 소스 복사

이전에 만들었던 'spring_study' 프로젝트를 그대로 사용하도록 한다.

[com.example.news] 패키지를 만든 후에 이전에 만들었던 프로젝트에서 'News.java', 'News.DAO.java'를 복사해온다.

파일을 복사하면 패키지가 달라져 에러가 표시되는데 패키지명을 [com.example.news]로 변경해주면 된다.

.jsp 파일도 동일하게 복사하도록 한다. 

'newsList.jsp', 'newsView.jsp' 파일을 'spring_study' 프로젝트의 [src] -> [main] -> [webapp] -> [WEB-INF] -> [views] -> [news] 폴더로 복사한다.

 

▶ 클래스 생성 및 스프링 부트 실행 클래스 복사

스프링 버전의 컨트롤러 구현을 위해 [com.example.news] 패키지에 'NewsWebController.java', 'NewsApiController.java' 클래스 파일을 생성해둔다.

그리고 뉴스 서비스 실행을 위해 [com.example.demo] 패키지에 있는 'SpringStudyApplication.java' 파일을 [com.example.news] 패키지로 복사한 다음 이를 'SpringNewsApplication.java'로 변경한다.

스프링 부트의 경우 @SpringBootApplication 애너테이션이 들어간 main()이 있는 클래스를 통해 서비스를 구동하게 된다.

따라서 스프링 빈 자동 검색을 위해서는 모든 클래스가 Application 클래스의 하위 패키지에 있어야 한다.

따라서 패키지의 상위에 Application을 두고 서브 패키지를 만들어 사용해야 한다.

이렇게 해서 기본적인 프로젝트 파일 구조 준비를 마쳤다.

최종 파일 구조는 아래와 같다.

 

 

※ 설정 파일 수정

다음으로 H2 데이터베이스를 사용하기 위해 'pom.xml'에 의존성을 추가해준다.

 

 

파일을 저장후 'pom.xml'을 선택하고 마우스 오른쪽 버튼을 클릭하여 <Maven> -> <Update Project> 를 실행한다.

그 다음으로 뉴스 이미지 파일을 저장하기 위한 저장 경로를 [src/main/resources] 폴더의 'application.properties' 파일 마지막 줄에 추가해준다.

news.imgdir=C:/Dev/MyWorkspace/spring_study/src/main/resources/static/img/

 

실제 파일을 저장할 위치로 스프링 프로젝트 경로 /src/main/resources/static/img/가 되도록 설정하고 [img] 폴더가 해당 경로에 생성되어 있어야 한다.

 

 

스프링 뉴스 웹 구현

 

※ 모델 코드 수정

'News.java' 코드는 수정할 부분이 없고,NewsDAO는 다음과 같이 스프링 빈으로 등록하기 위한 @Component 애너테이션만 추가해주면 된다.

 

※ NewsWebController 컨트롤러 작성

이제 NewsWebController 컨트롤러 클래스를 살펴본다.

NewsWebController의 경우 각각의 요청(등록, 목록 보기, 상세 보기, 삭제)에 동작하는 메서드는 복사해서 사용하고 일부 코드를 스프링 환경에 맞게 수정해야 한다.

WebMVC 컨트롤러와 요청 경로를 지정하기 위한 애너테이션을 추가하고 컨트롤러에서 사용할 NewsDAO 빈을 주입하기 위해 필드 선언과 로깅을 위한 Logger 객체 선언부를 추가한다.

package com.example.news;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/news")
public class NewsWebController {
	final NewsDAO dao;
	private final Logger logger = LoggerFactory.getLogger(this.getClass());
	
	// 프로퍼티 파일로부터 저장 경로 참조
	@Value("${news.imgdir}")
	String fdir;
	
	@Autowired
	public NewsWebController(NewsDAO dao) {
		this.dao = dao;
	}
	
}

 

NewsWebController 클래스는 자동으로 스프링 부트에 의해 컨트롤러로 등록되며 /news로 시작하는 URL 요청에 의해 동작하게 된다.

NewsDAO는 NewsWebController 인스턴스 생성 시 함께 참조 가능한 상태가 된다.

 

♣ 컨트롤러에서 변경되는 부분

기존 서블릿 기반의 컨트롤러와 달리 action 파라미터에 따라 메서드가 실행되도록 하는 코드는 필요 없고 각각의 요청에 실행될 메서드에 애너테이션만 추가하면 된다.

기존 메서드는 모두 재사용하며 애너테이션 추가 외 몇몇 공통적으로 달라지는 부분이 있으므로 이러한 부분만 찾아서 동일하게 수정해주면 된다.

 

어떤 부분이 달라지는지 먼저 정리하고 각각의 메서드 구현 코드를 살펴본다.

■ HttpServletRequest

기존의 메서드는 전달된 데이터의 참조와 뷰에 데이터를 전달하기 위한 용도로 HttpServletRequest 객체를 인자로 하고 있다.

스프링에서는 필요 없는 부분이므로 이 부분을 모두 제거한다.

 

■ BeanUtils

HTML 폼 데이터 매핑을 위해 사용한 apache commons BeanUtils 라이브러리 관련 부분도 필요 없다.

 

■ ctx.log()

서블릿 컨텍스트를 참조해 로그 메시지를 남기기 위한 부분은 앞으로 초기화한 Logger 클래스를 사용하는 것으로 대체해야 한다.

// 기존
ctx.log("뉴스 추가 과정에서 문제 발생!!!");
// 변경
logger.info("뉴스 추가 과정에서 문제 발생!!!");

 

■ aid 참조

상세 보기와 삭제의 경우에는 요청 파라미터로 aid 값을 받아 dao 연동에 사용했다.

여기서는 request 인자를 제거했기 때문에 이 부분은 설계에 따라 요청 파라미터 혹은 경로 파라미터 방식을 사용할 수 있다.

// 기존의 아래 코드를 제거
int aid = Integer.parseInt(request.getParameter("aid"));

 

■ 뷰 연동

컨트롤러에서 뷰에 데이터를 전달하기 위해서 request.setAttribute()를 사용했으나 이 부분은 스프링에서 제공하는 Model 인자를 사용하는 것으로 변경되어야 한다.

// 기존
public String listNews(HttpServletRequest request) {
    ...
    list = dao.getAll();
    request.setAttribute("newslist", list);
    ...
}
// 변경
public String listNews(Model m) {
    ...
    list = dao.getAll();
    m.addAttribute("newsList", list);
    ...
}

 

뷰를 리턴할 떄 문자열로 하는 부분은 동일하다.

특히 리디렉션으로 이동해야 하는 경우 기존 컨트롤러를 스프링 프레임워크와 동일한 방식으로 처리하도록 구현해두었기 때문에 기본적으로 같은 코드를 사용한다.

다만 프로젝트 변경으로 경로가 바뀐 부분을 반영해주어야 하고 뷰 파일명에서 .jsp를 제거해주어야 한다.

// 기존
return "ch10/newsList.jsp";
return "redirect:/news.nhn?action=listNews";
// 변경
return "news/newsList";
return "redirect:/news/list";

 

※ 컨트롤러 요청 처리 메서드 구현

앞에서 살펴본 변경될 부분을 요청 처리 메서드로 하나씩 구현해본다.

 

♣ 뉴스 추가

뉴스 추가는 @PostMapping 애너테이션을 통해 POST/news/add 요청에 동작하도록 변경한다.

HTML 폼으로부터 전달되는 데이터는 @ModelAttribute 애너테이션을 이용하면 자동으로 매핑되고, Model 인자는 필요한 경우 추가로 넣어주면 된다.

즉 Model 인자는 꼭 넣어야 하는 것이 아니라 필요한 경우에만 넣어서 사용하면 된다.

파일 첨부를 위해 인자에 MultiPartFile 타입을 추가해야 하며 @Requestparam으로 HTML <form>의 파일 파라미터 이름을 지정해준다.

dao를 통해 저장하는 부분이나 예외 발생 시 에러 메시지를 전달하는 부분 등은 모두 동일한 코드를 사용한다.

저장한 다음에는 목록으로 이동해야 하기 때문에 리디렉션 방식으로 이동될 수 있도록 리턴한다.

.jsp 확장자는 붙이지 않도록 주의한다.

 

@PostMapping("/add")
public String addNews(@ModelAttribute News news, Model m, @RequestParam("file") MultipartFile file) {
	try {
		// 저장 파일 객체 생성
		File dest = new File(fdir+"/"+file.getOriginalFilename());
		
		// 파일 저장
		file.transferTo(dest);
		
		// News 객체에 파일 이름 저장
		news.setImg(dest.getName());
		dao.addNews(news);
	} catch(Exception e) {
		e.printStackTrace();
		logger.info("뉴스 추가 과정에서 문제 발생!!");
		m.addAttribute("error", "뉴스가 정상적으로 등록되지 않았습니다!!!");
	}
		
	return "redirect:/news/list";
}

 

  • 파일을 저장하기 위해 File 객체를 먼저 생성한다. 이때 전달받은 MultipartFile 객체의 getOriginalFilename() 메서드를 통해 파일 이름을 가져와 저장 디렉터리와 결합해 사용한다. 파일 저장은 transferTo() 메서드를 사용한다.
  • @ModelAttribute로 전달된 news 객체에서 이미지 파일 필드는 img로 지정되어 있는데 HTML <form>의 첨부 파일은 file이라는 이름으로 전달되기 때문에 인자로 전달된 news에는 이미지 경로가 비어 있는 상태다. 따라서 저장된 파일의 이름(경로가 포함)을 가져와 news에 저장하고 dao.addNews()를 호출해야 하니 주의한다.

 

♣ 뉴스 삭제

삭제의 경우에는 GET 요청을 처리하며 경로 파라미터 방식으로 aid 값을 받아 처리한다.

@PathVariable 애너테이션을 사용해 인자로 aid를 지정하면 자동으로 값이 전달된다.

/news/delete/31과 같은 요청에 동작한다.

 

@GetMapping("/delete/{aid}")
public String deleteNews(@PathVariable int aid, Model m) {
	try {
		dao.delNews(aid);
	} catch(SQLException e) {
		e.printStackTrace();
		logger.info("뉴스 삭제 과정에서 문제 발생!!");
		m.addAttribute("error", "뉴스가 정상적으로 삭제되지 않았습니다!!!");
	}
		
	return "redirect:/news/list";
}

 

♣ 뉴스 목록 보기

뉴스 목록 보기는 GET 요청을 처리하며 Model을 통해 데이터를 전달하는 부분 이외에는 거의 달라지는 부분이 없다.

 

@GetMapping("/list")
public String listNews(Model m) {
	List<News> list;
	try {
		list = dao.getAll();
		m.addAttribute("newsList", list);
	} catch(Exception e) {
		e.printStackTrace();
		logger.warn("뉴스 목록 생성 과정에서 문제 발생!!");
		m.addAttribute("error", "뉴스 목록이 정상적으로 처리되지 않았습니다!!!");
	}
	return "news/newsList";
}

 

♣ 뉴스 상세 보기

뉴스 상세 보기 역시 GET 요청에 따라 동작하며 경로 파라미터 방식으로 aid 값을 받아오도록 구현했다.

/news/31과 같은 요청에 동작한다.

 

	@GetMapping("/{aid}")
	public String getNews(@PathVariable int aid, Model m) {
		try {
			News n = dao.getNews(aid);
			m.addAttribute("news", n);
		} catch(SQLException e) {
			e.printStackTrace();
			logger.warn("뉴스를 가져오는 과정에서 문제 발생!!");
			m.addAttribute("error", "뉴스를 정상적으로 가져오지 못했습니다!!!");
		}
		
		return "news/newsView";
	}

 

 

NewsWebController의 전체 코드

 

package com.example.news;

import java.io.File;
import java.sql.SQLException;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

@Controller
@RequestMapping("/news")
public class NewsWebController {
	final NewsDAO dao;
	private final Logger logger = LoggerFactory.getLogger(this.getClass());
	
	// 프로퍼티 파일로부터 저장 경로 참조
	@Value("${news.imgdir}")
	String fdir;
	
	@Autowired
	public NewsWebController(NewsDAO dao) {
		this.dao = dao;
	}
	
	@PostMapping("/add")
	public String addNews(@ModelAttribute News news, Model m, @RequestParam("file") MultipartFile file) {
		try {
			// 저장 파일 객체 생성
			File dest = new File(fdir+"/"+file.getOriginalFilename());
			
			// 파일 저장
			file.transferTo(dest);
			
			// News 객체에 파일 이름 저장
			news.setImg(dest.getName());
			dao.addNews(news);
		} catch(Exception e) {
			e.printStackTrace();
			logger.info("뉴스 추가 과정에서 문제 발생!!");
			m.addAttribute("error", "뉴스가 정상적으로 등록되지 않았습니다!!!");
		}
		
		return "redirect:/news/list";
	}
	
	@GetMapping("/list")
	public String listNews(Model m) {
		List<News> list;
		try {
			list = dao.getAll();
			m.addAttribute("newsList", list);
		} catch(Exception e) {
			e.printStackTrace();
			logger.warn("뉴스 목록 생성 과정에서 문제 발생!!");
			m.addAttribute("error", "뉴스 목록이 정상적으로 처리되지 않았습니다!!!");
		}
		return "news/newsList";
	}
	
	@GetMapping("/{aid}")
	public String getNews(@PathVariable int aid, Model m) {
		try {
			News n = dao.getNews(aid);
			m.addAttribute("news", n);
		} catch(SQLException e) {
			e.printStackTrace();
			logger.warn("뉴스를 가져오는 과정에서 문제 발생!!");
			m.addAttribute("error", "뉴스를 정상적으로 가져오지 못했습니다!!!");
		}
		
		return "news/newsView";
	}

	@GetMapping("/delete/{aid}")
	public String deleteNews(@PathVariable int aid, Model m) {
		try {
			dao.delNews(aid);
		} catch(SQLException e) {
			e.printStackTrace();
			logger.warn("뉴스 삭제 과정에서 문제 발생!!");
			m.addAttribute("error", "뉴스가 정상적으로 삭제되지 않았습니다!!!");
		}
		
		return "redirect:/news/list";
	}
}

 

※ 뷰 구현

뷰 부분은 특별히 구조적으로 변경되는 것은 없지만 프로젝트 변경으로 발생한 호출 경로 등만 수정하면 된다.

 

newsList.jsp :

목록 페이지는 뉴스 목록 보기, 삭제, 등록 기능을 겸하고 있다.

따라서 수정되거나 확인해야 하는 url 부분은 다음과 같다.

 

■ form action 속성

컨트롤러 코드에 따라 다음과 같이 수정해준다.

<form method="post" action="/news/add">

 

■ 상세 보기, 삭제 링크

상세 보기, 삭제 링크의 경우도 컨트롤러에서 작성한 url 매핑을 확인하고 그에 맞게 수정해야 한다.

따라서 상세 보기의 경우 /news/${news.aid}, 삭제의 경우 /news/delete/${news.aid}와 같이 링크의 href 속성을 수정해줘야 한다.

 

<li class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<a href="/news/${news.aid}" Class= "text-decoration-none"> [${status.count}] ${news.title}, ${news.date} </a> <a href="/news/delete/${news.aid}"> <span class="badge bg-secondary"> &times;
</span></a>
</li>

 

newsView.jsp :

수정할 부분이 없다.

 

※ 실행 및 테스트

H2 데이터베이스가 실행되어 있지 않으면 먼저 실행하도록 한다.

그 후 'SpringNewsApplication.java'를 선택하고 [Run As] -> [Spring Boot App] 으로 실행하면 된다.

http://localhost:8080/news/list 를 크롬을 띄워서 결과를 확인해본다.

 

 

 

 

 

 

스프링 뉴스 API 구현

 

이전에 사용했던 JAX-RS를 이번엔 스프링의 RestController를 사용한다.

REST API는 이미 애너테이션을 이용해 구현한 것이므로 애너테이션만 스프링 버젼으로 변경하면 끝이다.

앞에서 생성해둔 NewsApiController.java에 기존 코드 내용을 복사해 애너테이션 부분만 변경하면 된다.

 

NewsApiController.java 코드

 

package com.example.news;

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/news")
public class NewsApiController {
	final NewsDAO dao;
	
	@Autowired
	public NewsApiController(NewsDAO dao) {
		this.dao = dao;
	}
	
	@PostMapping
	public String addNews(@RequestBody News news) {
		try {
			dao.addNews(news);
		} catch(Exception e) {
			e.printStackTrace();
			return "News API: 뉴스 등록 실패!!";
		}
		return "News API: 뉴스 등록됨!!";
	}
	
	@DeleteMapping("{aid}")
	public String delNews(@PathVariable("aid") int aid) {
		try {
			dao.delNews(aid);
		} catch(Exception e) {
			e.printStackTrace();
			return "News API: 뉴스 삭제 실패!! - " + aid;
		}
		return "News API: 뉴스삭제됨!! - " + aid;
	}
	
	@GetMapping
	public List<News> getNewsList() {
		List<News> newsList = null;
		try {
			newsList = dao.getAll();
		} catch(Exception e) {
			e.printStackTrace();
		}
		return newsList;
	}
	
	@GetMapping("{aid}")
	public News getNews(@PathVariable("aid") int aid) {
		News news = null;
		try {
			news = dao.getNews(aid);
		} catch(Exception e) {
			e.printStackTrace();
		}
		return news;
	}	
}

 

이것으로 스프링 버전의 News API 서비스 구현을 마쳤다.

REST API 구현의 경우 컨테이너와 애너테이션 기반으로 만들어졌기 때문에 스프링으로 변환하는 과정을 쉽게 느낄 수 있었다.

 

테스트는 REST API 구현 (tistory.com) 에서 Postman 사용법을 참고해서 진행할 수 있다.

다만 뉴스 등록을 테스트할 때 헤더 부분에 Content-Type을 application/json으로 추가해주어야 한다.

JAX-RS에서는 요청 헤더가 없어도 처리하는 데 문제가 없었지만 원래 정확한 Content-Type을 넣어주는 것이 좋으니 참조하도록 한다.

Comments