본문 바로가기

회고록(TIL&WIL)

TIL 2023.03.10 Spring framework 4, 5(file I/O, Boot, thymeleaf, mybatis)

spring framework 4.X

file I/O - upload & download

dependency 추가

<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.5</version>
</dependency>

File Resolver 등록

servlet-context.xml

property 필수사항은 maxUploadSize 하나뿐 value는 바이트 기준 직접 계산해서 넣어줘야함

<!-- File Resolver -->
<beans:bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
  <beans:property name="maxUploadSize" value="5242880"/>
  <beans:property name="defaultEncoding" value="utf-8"/>
</beans:bean>

Upload

form 태그 작성

<form action="upload" method="post" enctype="multipart/form-data">
	<div>
		<input type="text" name="msg"/>
	</div>
	<div>
		<input type="file" name="file1"/>
	</div>
	<div>
		<button>업로드</button>
	</div>
</form>

Controller

// path는 원래 OS기준으로 특정한 장소를 결정하여 저장을 해야한다!
String path = "E:\\webspace\\sts08\\up\\";

@ResponseBody
@PostMapping("/upload")
public void upload(MultipartFile file1, HttpServletRequest req) {
  // 파일명의 중복을 막기 위해 시간 추가
  String msg = System.currentTimeMillis() +"_";
  File f = new File(path + msg + file1.getOriginalFilename());	
//		   try(
//		    InputStream is = file1.getInputStream();
//		    OutputStream os = new FileOutputStream(f);
//		   ) {
//		    int su = -1;
//		    while((su=is.read())!=-1) {
//		      os.write(su);
//		    }
//		  } catch (IOException e) {
//		    e.printStackTrace();
//		  }
  // 위와 같은 IO 작업을 Spring에서 한줄로 처리 할 수 있다.
  file1.transferTo(f);
}

Download

View 작성

<h1>download</h1>
<p> file1: <a href="load?fname=${fname }">download</a></p>
<p> file2: <a href="load/${fname }">download</a></p>

Controller

@GetMapping("/down")
public void down(Model model, String fname) {
  model.addAttribute("fname", fname);
}
  
// RequestParam을 이용한 다운로드
@GetMapping("/load")
public void load(String fname, HttpServletResponse resp) throws FileNotFoundException, IOException {
  File f = new File(path+fname);
  // 파일을 무조건 다운로드 되도록
  resp.setContentType("application/octet-stream");
  // 다운로드 되는 파일명 지정 (앞에 '_' 기준으로 시간이 달려있기 때문에 분리)
  resp.setHeader("Content-Disposition", "attachment; filename="+fname.split("_")[1]);
  try(InputStream is = new FileInputStream(f);
    OutputStream os = resp.getOutputStream();){
    int su = -1;
    while((su=is.read())!= -1) {
      os.write(su);
    }
  }
}

// PathVariable을 이용한 다운로드
// PathVariable에서 확장자까지 가져올려고하면 아래처럼 작성해야한다.
@GetMapping("/load/{fname:.+}")
public void load2(@PathVariable String fname, HttpServletResponse resp) throws FileNotFoundException, IOException {
  File f = new File(path+fname);
  // 파일을 무조건 다운로드 되도록
  resp.setContentType("application/octet-stream");
  // 다운로드 되는 파일명 지정 (앞에 '_' 기준으로 시간이 달려있기 때문에 분리)
  resp.setHeader("Content-Disposition", "attachment; filename="+fname.split("_")[1]);
  try(InputStream is = new FileInputStream(f);
    OutputStream os = resp.getOutputStream();){
    int su = -1;
    while((su=is.read())!= -1) {
      os.write(su);
    }
  }
}

Interceptor

DispatcherServlet을 기준으로 뭔가 처리를 하고 싶을 땐 AOP 보단 Interceptor가 보다 성능도 효율도 좋다.

<!-- ineterceptor -->
<interceptors>
	<interceptor>
		<mapping path="/*"/>
		<!-- <mapping path="/dept/*"/> -->
		<beans:bean class="com.bit.sts08.utils.LoggerInterceptor"></beans:bean>
	</interceptor>
</interceptors>
public class LoggerInterceptor extends HandlerInterceptorAdapter{

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		System.out.println("Controller 호출 전");
		// 리턴값이 있는데 이를 통해서 Controller 가 호출 될지 안될지 선택할 수 있다.
		// request 객체도 받아오기 때문에 session을 얻어와서 로그인 유무를 파악한다던지 할 수 있다.
//		if("".equals("")) {
//			request.getRequestDispatcher("/WEB-INF/views/down.jsp").forward(request, response);
//			return false;
//		}
		return true;
	}

	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {
		System.out.println("Controller 호출 후.. " + modelAndView.getViewName());		
		
	}

	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
			throws Exception {
		System.out.println("View Resolving 후.. " + ex.toString());
		// 오류가 발생할 경우 특정 페이지로 이동시킬 수 있다.
//		if(ex!=null) {
//			request.getRequestDispatcher("/WEB-INF/views/down.jsp").forward(request, response);
//		}
	}

}

Spring framework 5.X

java, Spring, maven-plugin, servlet, jsp 버전 수정

 

 

의존성 추가

spring-jdbc, spring-tx, commons-dbcp, mysql, mybatis, mybatis-spring

web.xml을 지우고 pom.xml에 플러그인 업데이트

설정 후 mvn package를 이용해서 패키징이 된다면 성공적으로 된 것

<build>
	...
	<pluginManagement>
        	<plugins>
        		<plugin>
        			<artifactId>maven-war-plugin</artifactId>
        			<configuration>
        				<failOnMissingWebXml>flase</failOnMissingWebXml>
        			</configuration>
        		</plugin>
        	</plugins>
        </pluginManagement>
</build>

xml을 최대한 없앤 자바의 코드만으로 프로젝트를 작성하도록 설정

WebConfig - web.xml의 대신

public class WebConfig extends AbstractAnnotationConfigDispatcherServletInitializer{

	@Override
	protected Class<?>[] getRootConfigClasses() {
//		<context-param>
//			<param-name>contextConfigLocation</param-name>
//			<param-value>classpath:/applicationContext.xml</param-value>
//		</context-param>
		return new Class[] {RootConfig.class};
	}

	@Override
	protected Class<?>[] getServletConfigClasses() {
		// DispatcherServlet를 서블릿으로 지정해주는 작업
//		<servlet>
//			<servlet-name>appServlet</servlet-name>
//			<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
//		</servlet>
		return new Class[] {ServletConfig.class};
	}

	@Override
	protected String[] getServletMappings() {
		// 서블릿 매핑을 전체로, 즉 모든 요청을 하나로 모으는 작업
//		<servlet-mapping>
//			<servlet-name>appServlet</servlet-name>
//			<url-pattern>/</url-pattern>
//		</servlet-mapping>
		return new String[] {"/"};
	}
	
}

ServletConfig - DispatcherServlet 설정 servlet-context.xml 대신함

// <annotation-driven/>
@EnableWebMvc
// <context:component-scan base-packpage="com.bit.sts09"/>
@ComponentScan("com.bit.sts09")
public class ServletConfig implements WebMvcConfigurer{
	
//	<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
//		<beans:property name="prefix" value="/WEB-INF/views/" />
//		<beans:property name="suffix" value=".jsp" />
//	</beans:bean>
	@Override
	public void configureViewResolvers(ViewResolverRegistry registry) {
//		InternalResourceViewResolver resolver = new InternalResourceViewResolver();
//		resolver.setPrefix("/WEB-INF/views");
//		resolver.setSuffix(".jsp");
//		registry.viewResolver(resolver);
		registry.viewResolver(new InternalResourceViewResolver("/WEB-INF/views/", ".jsp"));
	}
	
//	<resources mapping="/resources/**" location="/resources/" />
	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
		
	}
}

RootConfig - applicationContext.xml을 대신함

import com.mysql.cj.jdbc.Driver;

@Configuration
@MapperScan("com.bit.sts09.model")
public class RootConfig {
	
//	<!-- DBCP -->
//	<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
//		<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
//		<property name="url" value="jdbc:mysql://192.168.99.100:3306/lecture"/>
//		<property name="username" value="scott"/>
//		<property name="password" value="tiger"/>
//	</bean>
	@Bean
	public DataSource getDataSource() {
		SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
		dataSource.setDriverClass(Driver.class);
		dataSource.setUrl("jdbc:mysql://192.168.99.100:3306/lecture");
		dataSource.setUsername("scott");
		dataSource.setPassword("tiger");
		return dataSource;
	}
	
//	<!-- Mybatis -->
//	<bean 
//		id="sqlSessionFactory"
//		class="org.mybatis.spring.SqlSessionFactoryBean"
//		p:configLocation="classpath:/mybatis-config.xml"
//		p:dataSource-ref="dataSource"/>
	@Bean
	public SqlSessionFactory getSqlSessionFactory() throws Exception {
		SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
		sqlSessionFactory.setDataSource(getDataSource());
//		Resource resource = new ClassPathResource("/mybatis-config.xml");
//		sqlSessionFactory.setConfiguration(resource);
//		Resource[] resources = {new ClassPathResource("/mapper/dept-mapper.xml")};
//		sqlSessionFactory.setMapperLocations(resources);
		
		// 위에 주석된 것들 대신에 현재클래스에 @MapperScan 어노테이션 장착 + Dao 클래스에 @Mapper 장착
		return sqlSessionFactory.getObject();
	}
	
//	<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
//		<constructor-arg ref="sqlSessionFactory"></constructor-arg>
//	</bean>
	@Bean
	public SqlSession getSqlSession() throws Exception {
		return new SqlSessionTemplate(getSqlSessionFactory());
	}
}

Spring Boot 2

boot 1 버전은 현재 종료됨, 2, 3 버전만 있으며 각각 spring 4, 5 버전을 사용함
Spring 6 - Boot 3
=> JAVA 17 기반, 사용되는 엔진 변경, 성능 향상 등
사용자 입장에서는 크게 메리트가 있는점은 없다고한다...

Boot Project 생성

사용할 라이브러리 및 Spring 버전 프로젝트 생성시 지정 가능

devtools - 코드를 수정하면 자동으로 갱신해줌

터미널에서 실행하는 방법

배포 시 (jar만 옮겨서 jar만 실행시키는 구문을 쓰는게 제일 좋다)
mvn package
java -jar E:\webspace\boot01\target\boot01-0.0.1-SNAPSHOT.jar

개발 시
mvn spring-boot:run

maven을 설치하지 않았을 경우
mvn 대신 mvnw 를 이용해서 사용하면되기 때문에 배포시에 jdk만 깔아도되는 것

resources/

application.properties - port번호와 같은 환경설정을 바꿀 때 자동완성도 가능

server.port=8282

/static/

해당 경로에 정적파일들을 넣으면 추가 경로 없이 파일명 만으로 불러올 수 있다.

 

 

thymeleaf, lombok, mybatis, mysql을 이용한 웹프로젝트

application.properties - mysql

mysql 라이브러리를 추가했기 때문에 DataSource를 정의하지 않으면 서버가 시작도 되지않는다.

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.99.100/lecture
spring.datasource.username=scott
spring.datasource.password=tiger

template - thymeleaf

thymeleaf 설정은 Boot에 라이브러리를 추가해줌으로써 전부 다 해주었기 때문에 template에 html파일만 추가하면 된다.


백엔드 개발자와 프론트엔드 개발자 처리방식이 일치 하지않는 문제를 원천적으로 해결하기 위한 템플릿이다.
프론트엔드에서 작업한 것을 그대로 가져와서 백엔드에서 처리할 때는 태그에 속성을 추가해줌으로써 처리할 수 있기 때문에
서로간에 작업 간에 영향을 주는 것이 없어지게 되는 장점이 있게 된다.

<h1>template view index</h1>
<table>
	<thead>
		<tr>
			<th>deptno</th>
			<th>dname</th>
			<th>loc</th>
		</tr>
	</thead>
	<tbody>
		<tr th:each="bean:${list}">
			<td th:text="${bean.deptno}">1234</td>
			<td th:text="${bean.dname}">test</td>
			<td th:text="${bean.loc}">test</td>
		</tr>
	</tbody>
</table>

Mybatis 연결

@MapperScan, Mysql 연결

@SpringBootApplication
@MapperScan("com.bit.boot02.model")
public class Boot02Application {
	@Autowired
	DataSource dataSource;
	
	@Bean
	public SqlSessionFactory getSqlSessionFactory() throws Exception { 
		SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
		bean.setDataSource(dataSource);
		return bean.getObject();
	}	
	
	public static void main(String[] args) {
		SpringApplication.run(Boot02Application.class, args);
	}
}

Vo - lombok

@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeptVo {
	private int deptno;
	private String dname, loc;
}

Dao

@Mapper 달아주고 @Select 로 Query Mapping

@Mapper
public interface DeptDao {
	
    @Select("select * from dept")
    List<DeptVo> findAll();

    @Select("select * from dept where deptno=#{pk}")
    DeptVo findOne(int pk);

    @Insert("insert into dept value(#{deptno}, #{dname}, #{loc})")
    void insertOne(DeptVo bean);

    @Update("update dept set dname=#{dname}, loc=#{loc} where deptno=#{deptno}")
    int updateOne(DeptVo bean);

    @Delete("delete from dept where deptno=#{pk}")
    int deleteOne(int pk);
}

Service

@Service
@AllArgsConstructor
public class DeptService {
	private final SqlSession sqlSession;
	
	public List<DeptVo> selectAll() {
		return sqlSession.getMapper(DeptMapper.class).findAll();
	}
	
	public DeptVo selectOne(int deptno) {
		return sqlSession.getMapper(DeptMapper.class).findOne(deptno);
	}
	
	@Transactional
	public void insertOne(DeptVo bean) {
		sqlSession.getMapper(DeptMapper.class).insertOne(bean);
	}
	
	@Transactional
	public int updateOne(DeptVo bean) {
		return sqlSession.getMapper(DeptMapper.class).updateOne(bean);
	}
	
	@Transactional
	public int deleteOne(int deptno) {
		return sqlSession.getMapper(DeptMapper.class).deleteOne(deptno);
	}
}

Controller

@Controller
public class MainController {
	private final DeptService deptService;
	
	@GetMapping
	@CrossOrigin
	public List<?> list() {
		return deptService.selectAll();
	}
	
	@CrossOrigin
	@PostMapping
	public ResponseEntity<?> add(@ModelAttribute("bean") DeptVo bean) {
		try {
			deptService.insertOne(bean);
		} catch (Exception e) {
			e.printStackTrace();
			return ResponseEntity.badRequest().build();
		}
		return ResponseEntity.ok("success");
	}
	
	@CrossOrigin
	@GetMapping("/{deptno}")
//	public DeptVo detail(@PathVariable("deptno") int deptno) {
	//	return deptService.selectOne(deptno);
	public ResponseEntity<?> detail(@PathVariable("deptno") int deptno) {
		DeptVo bean = deptService.selectOne(deptno);
		if(bean!=null) {
			return ResponseEntity.ok(bean);
		} else {
			return ResponseEntity.notFound().build();
		}
	}
	
	@CrossOrigin
	@PutMapping("/{deptno}")
	public ResponseEntity<?> edit(@RequestBody DeptVo bean, HttpServletRequest req) throws URISyntaxException {
		if(deptService.updateOne(bean) > 0 ) {
			RestTemplate template = new RestTemplate();
			URI url = new URI(req.getRequestURL().toString());
			RequestEntity param = new RequestEntity(HttpMethod.GET, url);
			return template.exchange(url, HttpMethod.GET, param, DeptVo.class);
//			return ResponseEntity.ok(bean);
		}
		return ResponseEntity.internalServerError().build();
	}
	
	@CrossOrigin
	@DeleteMapping("/{deptno}")
	public ResponseEntity<?> del(@PathVariable("deptno") int deptno) {
		if(deptService.deleteOne(deptno) > 0) {
			return ResponseEntity.ok("success");
		}
		return ResponseEntity.badRequest().build();
	}
}