🌿REST
REST(Representational State Transfer)는 웹 상의 자원(문서, 이미지, 영상 등)을 자원명으로 표시해서 상태를 주고받는 규칙을 의미한다.
HTTP URI(Uniform Resource Identifier)를 통해서 자원을 명시하고, HTTP Method(POST, GET, PUT, DELETE, PATCH 등)를 통해서 해당 자원에 대한 CRUD를 처리하는 방식이다.
즉, REST는 인터넷 주소를 어떻게 만들지에 대한 규칙이라고 할 수 있다.
Spring과 독립된 기술로, 주소의 형식만 갖추면 Servlet/JSP로도 사용할 수 있다.
URI(URL) 표기 방식
게시판을 만든다는 걸 가정해서 URI 표기 방식의 차이를 알아보도록 하자.
이전 방식
- 목록보기: GET (http://localhost/board/list.do)
- 추가하기: POST (http://localhost/board/add.do)
- 수정하기: POST (http://localhost/board/edit.do)
- 삭제하기: POST (http://localhost/board/del.do)
아직도 대다수의 사이트들이 동사를 이용해서 행위를 한다는 표현으로 URI 표기를 하고 있다.
URI를 창시한 창시자조차도 현재 쓰고 있는 URI의 형태를 별로 좋지 않게 보고 있다. 사람들에 의해 만들어진 패턴이지, 체계를 가지고 만든 패턴이 아니기 때문이다.
실용적인 면이나 체계를 가진 규칙인지에 대해 검증해 보면 문제가 많은 표기법 중 하나이다. 이러한 URI를 체계적으로 표기하는 시도를 통해 나온 것 중 하나가 REST이다.
REST 방식
- 목록보기: GET (http://localhost/board)
- 추가하기: POST (http://localhost/board)
- 수정하기: PUT (http://localhost/board/1)
- 삭제하기: DELETE (http://localhost/board/1)
목록보기와 추가하기의 주소는 board로 같지만, GET과 POST라는 점에서 다르다. 굳이 뒤에 list.do, add.do와 같이 행위를 밝히지 않아도 메서드가 행위를 대신한다.
수정할 때에는 PUT을 사용하며, 삭제할 때에는 DELETE를 사용한다. 수정하기와 삭제하기는 특정 글을 수정하기 때문에 뒤에 아이디(글 번호)를 넘긴다. 이처럼 주소를 행동 대신에 메서드로 인식한다.
🍃REST API 설계 규칙
1. URI에 자원을 표시한다.
- 목록보기: GET (http://localhost/board)
- 회원목록보기: GET (http://localhost/member)
위 주소에서 board와 member는 CRUD를 하기 위한 대상이다.
보통 마지막 단어가 자원명이 된다.
2. URI에 동사를 표시하지 않는다.
URI의 일부에 list.do, add.do와 같이 어떤 행동을 하는지에 대한 동사를 직접적으로 수식하지 않는다.
3. HTTP Method를 사용해서 행동을 표현한다.
- GET: 자원 요청
- POST: 자원 전달 + 서버 측 생성
- PUT: 자원 전달 + 수정 (기존 삭제 > 새로 생성)
- PATCH: 자원 전달 + 수정 (일부 수정)
- DELETE: 자원 삭제
PUT은 수정을 하지만, 기존 자원을 삭제하고 새로 생성하므로 교체에 적합하다. 따라서 수정하지 않은 기존 컬럼은 사라지기 때문에 전체 수정(모든 컬럼 수정)을 하게 된다. 반면 PATCH는 일부를 수정한다.
이중 GET, POST, PUT, DELETE를 주로 사용한다.
4. URI + HTTP Method = REST API
URI에 HTTP Method를 사용하는 게 REST API이다.
5. 구분자는 '/'를 사용한다.
6. URI의 마지막은 '/'를 적지 않는다.
- http://localhost/board/list.do
이 주소가 톰캣에 넘어가면 톰캣은 이 주소에 slash '/'를 하나 더 붙는다. ' http://localhost/board/list.do/' 이렇게 주소가 만들어져야 톰캣이 완전한 주소로 인식하기 때문이다.
그런데 RESTful은 이 뒤에 추가적인 요소가 올 수 있다는 가독성 때문에 '/'를 적지 않는다.
7. URI에는 '-' 사용이 가능하다.
- http://localhost/free-board
가끔 URI에 dash '-'를 사용하기도 한다.
8. URI에는 '_' 사용하지 않는다.
- http://localhost/free_board (X)
가독성 때문에 underbar '_'를 사용하지 않는다.
결론적으로 특수문자를 사용하지 않는다.
9. URI는 소문자로만 작성한다.
10. 확장자를 작성하지 않는다.
URI가 자원명으로 끝나기 때문에 확장자를 작성하지 않는다.
🌿REST CRUD
주소록 데이터를 제공하는 Address REST API Server를 Spring 기반으로 구축해 보도록 하자.
프로젝트 설정
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.test</groupId>
<artifactId>rest</artifactId>
<name>RESTTest</name>
<packaging>war</packaging>
<version>1.0.0-BUILD-SNAPSHOT</version>
<properties>
<java-version>11</java-version>
<org.springframework-version>5.0.7.RELEASE</org.springframework-version>
<org.aspectj-version>1.6.10</org.aspectj-version>
<org.slf4j-version>1.6.6</org.slf4j-version>
</properties>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${org.springframework-version}</version>
<exclusions>
<!-- Exclude Commons Logging in favor of SLF4j -->
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<!-- AspectJ -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${org.slf4j-version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>${org.slf4j-version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${org.slf4j-version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!-- @Inject -->
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
<!-- Servlet / JSP -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<!-- <scope>test</scope> -->
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<!-- AOP -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
<!-- JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.0</version>
</dependency>
<!--
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.15.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
-->
<!-- HikariCP -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>2.7.4</version>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<!-- log4jdbc.log4j2 -->
<dependency>
<groupId>org.bgee.log4jdbc-log4j2</groupId>
<artifactId>log4jdbc-log4j2-jdbc4</artifactId>
<version>1.16</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-eclipse-plugin</artifactId>
<version>2.9</version>
<configuration>
<additionalProjectnatures>
<projectnature>org.springframework.ide.eclipse.core.springnature</projectnature>
</additionalProjectnatures>
<additionalBuildcommands>
<buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand>
</additionalBuildcommands>
<downloadSources>true</downloadSources>
<downloadJavadocs>true</downloadJavadocs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.5.1</version>
<configuration>
<source>11</source>
<target>11</target>
<compilerArgument>-Xlint:all</compilerArgument>
<showWarnings>true</showWarnings>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<configuration>
<mainClass>org.test.int1.Main</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
rest와 행동이 겹치기 때문에 jackson과 gson을 임시로 주석 처리해 주었다.
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>
<!-- Creates the Spring Container shared by all Servlets and Filters -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Processes application requests -->
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<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>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<servlet-name>appServlet</servlet-name>
</filter-mapping>
</web-app>
log4jdbc.log4j2.properties
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
log4j(log level)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration PUBLIC "-//APACHE//DTD LOG4J 1.2//EN" "log4j.dtd">
<log4j:configuration
xmlns:log4j="http://jakarta.apache.org/log4j/">
<!-- Appenders -->
<appender name="console"
class="org.apache.log4j.ConsoleAppender">
<param name="Target" value="System.out" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%-5p: %c - %m%n" />
</layout>
</appender>
<!-- Application Loggers -->
<logger name="com.test.rest">
<level value="info" />
</logger>
<!-- 3rdparty Loggers -->
<logger name="org.springframework.core">
<level value="info" />
</logger>
<logger name="org.springframework.beans">
<level value="info" />
</logger>
<logger name="org.springframework.context">
<level value="info" />
</logger>
<logger name="org.springframework.web">
<level value="info" />
</logger>
<!-- Root Logger -->
<root>
<priority value="warn" />
<appender-ref ref="console" />
</root>
<logger name="jdbc.sqlonly">
<level value="info" />
</logger>
<logger name="jdbc.resultsettable">
<level value="info" />
</logger>
<logger name="jdbc.audit">
<level value="warn" />
</logger>
<logger name="jdbc.resultset">
<level value="warn" />
</logger>
<logger name="jdbc.connection">
<level value="warn" />
</logger>
<logger name="jdbc.sqltiming">
<level value="off" />
</logger>
</log4j:configuration>
servlet-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
<!-- Enables the Spring MVC @Controller programming model -->
<annotation-driven />
<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
<resources mapping="/resources/**" location="/resources/" />
<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
<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>
<context:component-scan base-package="com.test.rest" />
<context:component-scan base-package="com.test.controller" />
<context:component-scan base-package="com.test.domain" />
<context:component-scan base-package="com.test.persistence" />
</beans:beans>
⭐설정 테스트
- JDBC 연결 테스트
- HikariCP 구축 테스트
- MyBatis 구축 테스트
테스트가 검증이 되어야 merge를 하기 때문에 실무에서는 이 과정이 필수적이다.
테스트를 하지 않으면 나중에 사고가 났을 때 어디서 사고가 발생했는지 확인하기가 어렵기 때문에 안정성을 위해서라도 단위 테스트를 해야 한다.
src/test/java > com.test.java.db
- JDBCTest.java
JDBCTest.java: JDBC 연결 테스트
package com.test.java.db;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.sql.Connection;
import java.sql.DriverManager;
import org.junit.Test;
public class JDBCTest {
@Test
public void testConnection() {
try {
// 오라클 드라이버 로딩
Class.forName("oracle.jdbc.driver.OracleDriver");
Connection conn = DriverManager.getConnection("jdbc:oracle:thin@localhost:1521:xe", "hr", "java1234");
assertNotNull(conn); // DB 연결 성공 시 True
assertEquals("DB 연결", false, conn.isClosed()); // 기대값이 같은지를 검증하여 conn.isClose()가 false와 같을 경우 성공
} catch (Exception e) {
e.printStackTrace();
}
}
}
HikariCPTest.java: HikariCP 구축 테스트
package com.test.java.db;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.sql.Connection;
import java.sql.SQLException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.zaxxer.hikari.HikariDataSource;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
public class HikariCPTest {
@Autowired
private HikariDataSource dataSource;
@Test
public void testConnectionPool() {
// 직접 Connection을 생성하지 않고 Connection Pool을 통해 Connection이 잘 생성되는지 테스트
assertNotNull(dataSource);
try {
Connection conn = dataSource.getConnection();
assertEquals(false, conn.isClosed());
} catch (SQLException e) {
e.printStackTrace();
}
}
}
root-context 안에 있는 설정이 잘 작동하고 있는지 확인하기 위해서 직접 불러야 한다.
HikariCP와 관련된 건 root-context.xml의 hikariConfig와 dataSource 빈 태그이다.
MyBatisTest.java: MyBatis 구축 테스트
package com.test.java.db;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
public class MyBatisTest {
@Autowired
private SqlSessionTemplate template;
@Test
public void testQuery() {
int count = template.selectOne("rest.test");
assertNotNull(template);
assertEquals(5, count);
}
}
rest.xml
<?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">
<!-- rest.xml -->
<mapper namespace="rest">
<select id="test" resultType="Integer">
select count(*) from tblAddress
</select>
</mapper>
데이터베이스와 관련된 테스트는 반드시 해야 한다.
테스트 결과 첨부
이러한 테스트 결과도 포트폴리오 또는 발표 자료에 첨부해야 한다.
이런 것도 보여줘야 하나🤔 싶은 것도 보여줘야 한다.
코드를 넣는 건 그렇게 효과가 좋지 않고, 단위 테스트를 했다는 건 junit을 검색해서 관련 이미지를 찾아서 로고는 깔고 가고, 설명 가능한 선 내에서 최대한 넣을 수 있는 이미지를 수집해서 넣는다.
자료로 넣었을 때 가장 좋은 것은 위와 같은 테스트 결과창이다.
지금은 테스트가 너무 단순하지만, 나중에 실제 업무를 하다 보면 결과창의 메시지도 많아진다.
기능적인 부분만 골라서 넣기보다는 이와 같이 기술적인 얘기와 시스템적이고 절차에 관련된 얘기도 추가하는 게 좋다.
파일 생성
com.test.controller
- AddressController.java
- RestController.java
com.test.domain
- AddressDTO.java
com.test.persistence
- AddressDAO.java (I)
- AddressDAOImpl.java (C)
⭐⭐⭐REST API Server는 view를 만들지 않는다.
SSR & CSR
여태 웹을 SSR 방식(Server Side Rendering)으로 동작하도록 만들었다.
브라우저가 페이지를 하나 달라고 요청하면, Servlet/JSP를 하던 Spring을 하던 여러 가지 작업을 한 다음에 마지막에 돌려줄 HTML 페이지를 만든다. 이를 페이지를 렌더링 한다고 표현하는데, 이렇게 Server가 작동하는 방식을 SSR 방식이라고 한다.
그리고 자바스크립트로 페이지를 만드는 기법을 CSR이라고 한다. 이 방식은 중요하게 들어가는 데이터를 Server로부터 가져온다. Servlet을 사용해서 JSP에서 데이터베이스에 접속하여 Select 한 정보를 얻어 화면에 출력한다.
Ajax가 요청했기 때문에 순수한 데이터만을 돌려줘야 하는 상황이다. 이때 적합한 포맷중에 하나인 JSON으로 돌려줬었다. 결론적으로 데이터만 수급받았지, 페이지를 구성하는 주체가 JavaScript인 방식을 CSR 방식이라고 한다.
인터넷에서 순수하게 데이터만 입출력할 수 있는 Open API는 정해진 URL에 요청을 하면 데이터를 XML, JSON 형태로 돌려주는데, 이런 게 대표적인 Rest Server이다.
@ResponseBody
@GetMapping(value = "/m1.do")
public String m1(Model model) {
String name = "문자열";
return name;
}
Rest Server도 JSP를 돌려줄 수 없다. 그래서 문자열.jsp를 찾을 수 없다는 오류가 발생한다.
String 앞의 반환값에 @ResponseBody 어노테이션을 붙인다.
@GetMapping(value = "/m1.do")
public @ResponseBody String m1(Model model) {
String name = "Isaac";
return name;
}
페이지 소스 보기를 하면 코드가 아니라 순수한 데이터를 돌려준 것을 볼 수 있다. 그래서 이러한 방법을 주로 Ajax 응답용으로 많이 사용한다.
@GetMapping(value = "/m2.do")
public @ResponseBody AddressDTO m2(Model model) {
AddressDTO dto = new AddressDTO();
dto.setSeq("1");
dto.setName("Isaac");
dto.setGender("m");
dto.setAddress("서울시 강남구 역삼동");
dto.setRegdate("2023-11-29");
return dto;
}
DTO 또는 HashMap, ArrayList 등의 객체를 Return하게 되면 JSON을 반환한다.
이처럼 @ResponseBody는 XML또는 JSON 형태로 반환할 수 있도록 한다.
AddressDTO가 여러개일 때
@GetMapping(value = "/m2.do")
public @ResponseBody List<AddressDTO> m2(Model model) {
List<AddressDTO> list = new ArrayList<AddressDTO>();
AddressDTO dto1 = new AddressDTO();
dto1.setSeq("1");
dto1.setName("Isaac");
dto1.setGender("m");
dto1.setAddress("서울시 강남구 역삼동");
dto1.setAge("24");
dto1.setRegdate("2023-11-29");
AddressDTO dto2 = new AddressDTO();
dto2.setSeq("2");
dto2.setName("Sopia");
dto2.setGender("m");
dto2.setAddress("서울시 강남구 역삼동");
dto2.setAge("25");
dto2.setRegdate("2023-11-29");
list.add(dto1);
list.add(dto2);
return list;
}
여러 개의 DTO를 JSON 형태로 위와 같이 반환받을 수 있다.
@RequestBody
package com.test.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.test.domain.AddressDTO;
import com.test.persistence.AddressDAO;
// 요청 메서드의 반환값에 자동으로 @ResponseBody가 적용된다.
@RestController
public class RESTController {
@Autowired
private AddressDAO dao;
// @RequestMapping(value="/address", method=RequestMethod.POST)
@PostMapping(value = "/address")
public int add(@RequestBody AddressDTO dto) {
return dao.add(dto);
}
}
@RestController 어노테이션은 요청 메서드의 반환값에 자동으로 @ResponseBody가 적용되게 한다.
POST http://localhost:8090/rest/address HTTP/1.1
Content-Type: application/json; charset=UTF-8
{
"name": "Isaac",
"age": 24,
"gender": "m",
"address": "서울시 강남구 역삼동"
}
param으로 값을 보내 데이터를 확인할 수 있지만, JSON 형태로 값을 보낼 수도 있다.
내가 가진 데이터를 JSON으로 바꾸는 게 아니라 JSON 데이터를 원래 데이터 형식으로 바꾸는 게 @RequestBody이다.
terminal
terminal은 cmd의 업그레이드 버전으로 생각하면 된다.
시작
설정에서 기본 프로필을 명령 프롬프트로, 기본 터미널 응용 프로그램을 Window 터미널로 바꾼다.
기본값 > 모양
배경과 폰트 등의 설정을 할 수 있다.
크로미 귀여워.
curl
- curl -X GET http://localhost:8090/rest/m1.do
- curl -X GET http://localhost:8090/rest/m2.do
curl을 사용해서 요청받는 작업을 할 수 있다.
Rest Client Tool
1. curl
2. Postman
3. Insomnia
4. VS Code
Postman
https://www.postman.com/downloads/
탭 하나당 요청 하나를 할 수 있으며, Spring Rest Server가 잘 만들어졌는지 JSON과 Metadata를 확인할 수 있다.
Metadata에서 Content-Type의 application/json;charset=UTF-8는 @ReponseBody가 해준 것이다.
VS Code: Rest Client
VS Code의 Rest Client를 다운로드 한다.
그리고 프로젝트를 불러와서 rest.http 파일을 생성한다. 확장자는 반드시 http여야 한다.
메서드를 대문자로 작성하고, 주소를 적는다. 주소가 여러 개인 경우 #을 3개 이상 적어주면 별도의 요청으로 인식하게 된다.
Postman은 틀이 잡혀 있는 느낌이라면, VS Code는 보다 자유로운 느낌이다.
🌿전체 코드
script.sql
-- RESTTest > script.sql
select * from tabs order by table_name;
select * from tblAddress;
AddressController.java
package com.test.controller;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.test.domain.AddressDTO;
@Controller
public class AddressControlelr {
@GetMapping(value = "/m1.do")
public @ResponseBody String m1(Model model) {
String name = "Isaac";
return name;
}
@GetMapping(value = "/m2.do")
public @ResponseBody List<AddressDTO> m2(Model model) {
List<AddressDTO> list = new ArrayList<AddressDTO>();
AddressDTO dto1 = new AddressDTO();
dto1.setSeq("1");
dto1.setName("Isaac");
dto1.setGender("m");
dto1.setAddress("서울시 강남구 역삼동");
dto1.setAge("24");
dto1.setRegdate("2023-11-29");
AddressDTO dto2 = new AddressDTO();
dto2.setSeq("2");
dto2.setName("Sopia");
dto2.setGender("m");
dto2.setAddress("서울시 강남구 역삼동");
dto2.setAge("25");
dto2.setRegdate("2023-11-29");
list.add(dto1);
list.add(dto2);
return list;
}
}
Rest Server는 순수한 데이터만을 돌려주는 용도로 사용한다.
일반 요청이 아닌 Ajax 요청을 할 때 보통 JSON이나 XML을 돌려줬었다. Ajax는 브라우저가 아니므로 HTML을 받아들이지 않기 때문이다.
🍃RESTController.java
package com.test.controller;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.test.domain.AddressDTO;
import com.test.persistence.AddressDAO;
//요청 메서드의 반환값에 자동으로 @ResponseBody가 적용된다.
@RestController
public class RESTController {
@Autowired
private AddressDAO dao;
//CRUD + REST
//추가하기
//1. http://localhost:8090/rest/address
//2. POST
//3. return int
//@RequestMapping(value="/address", method=RequestMethod.POST)
@PostMapping(value = "/address")
public int add(@RequestBody AddressDTO dto) {
return dao.add(dto);
}
//목록보기
//1. http://localhost:8090/rest/address
//2. GET
//3. return List<DTO>
@GetMapping(value = "/address")
public List<AddressDTO> list() {
return dao.list();
}
//수정하기
//1. http://localhost:8090/rest/address/1
//2. PUT or PATCH
//3. return int
//@RequestMapping(value="/address/{seq}", method=RequestMethod.PUT) //경로 변수(PathVariable)
@PutMapping(value="/address/{seq}")
public int edit(@RequestBody AddressDTO dto, @PathVariable("seq") String seq) {
dto.setSeq(seq);
return dao.edit(dto);
}
//삭제하기
//1. http://localhost:8090/rest/address/1
//2. DELETE
//3. return int
@DeleteMapping(value = "/address/{seq}")
public int del(@PathVariable("seq") String seq) {
return dao.del(seq);
}
//검색하기(=상세보기)
//1. http://localhost:8090/rest/address/1
//2. GET
//3. return DTO
@GetMapping(value = "/address/{seq}")
public AddressDTO address(@PathVariable("seq") String seq) {
return dao.get(seq);
}
}
INSERT
자원을 서버로 넘겨서 새로 생성할 때 POST 방식을 사용한다.
DTO를 받아서 DAO로 넘겨서 INSERT 하는 작업을 구현했다. 넘기는 데이터는 Form을 통해서 넘겨서 Parameter로 받는 방식은 @RequestParam이 붙고, JSON 형태로 넘겨서 받는 방식은 @RequestBody를 붙여서 받을 수 있다.
여기서 JSON으로 받은 이유는 Rest API가 JSON으로 보내면 JSON으로 받는 게 일반적이기 때문이다. 그래서 이를 통일시키기 위해 일부러 JSON으로 받는 것이다.
SELECT
주소가 address로 같지만, 요청하는 방식이 POST와 GET으로 서로 다르기 때문에 서버는 다른 요청으로 인식한다. xml에서 리스트를 받아오더라도 resultType으로 DTO를 받아오는 것으로 작성한다.
UPDATE
{
"name": "Paul",
"age": 30,
"gender": "m",
"address": "서울시 강남구 대치동"
}
매개변수를 보내 무엇을 수정할지 주소에서 알려 주어야 하는데, 이게 경로의 일부처럼 되었다고 해서 경로 변수(PathVariable)라고 부른다.
Add와 똑같은데, Add는 데이터만 넘긴 거고, Edit은 수정할 때 쓸 기본키도 같이 넘길 뿐이다.
@RequestParam과 비슷한 @PathVariable 어노테이션을 사용해 seq를 찾도록 해야 한다. 그리고 찾은 값을 String seq에 넣어달라는 의미로 작성하였다.
dto에 수정할 데이터의 seq를 넣고 보내면 된다. 이를 xml에서 해당 데이터의 전체를 수정하는데, 부분이 아닌 전체를 수정하는 이유는 각각을 수정하는 쿼리를 모두 만들면 쿼리가 수십 개가 필요하기 때문이다.
이렇게 모든 컬럼을 수정하는 행위를 PUT 메서드가 한다. 만약 모든 컬럼이 아니라 일부 컬럼만을 수정하고 싶다면 PATCH 메서드를 사용한다.
HTML 문서 내에서는 GET 요청과 POST 요청 외에는 할 수 있는 방법이 없었기 때문에 그동안 PUT 요청을 할 수 없었던 것이다.
DELETE
- http://localhost:8090/rest/address/21
delete할 seq는 DTO에 포장하지 않고 String으로 보내서 삭제 처리 하였다.
DETAIL
AddressDTO.java
package com.test.domain;
import lombok.Data;
@Data
public class AddressDTO {
private String seq;
private String name;
private String age;
private String gender;
private String address;
private String regdate;
}
AddressDAO.java (I)
package com.test.persistence;
import java.util.List;
import com.test.domain.AddressDTO;
public interface AddressDAO {
int add(AddressDTO dto);
List<AddressDTO> list();
int edit(AddressDTO dto);
int del(String seq);
AddressDTO get(String seq);
}
AddressDAOImpl.java (C)
package com.test.persistence;
import java.util.List;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import com.test.domain.AddressDTO;
@Repository
public class AddressDAOImpl implements AddressDAO {
@Autowired
private SqlSessionTemplate template;
@Override
public int add(AddressDTO dto) {
return template.insert("rest.add", dto);
}
@Override
public List<AddressDTO> list() {
return template.selectList("rest.list");
}
@Override
public int edit(AddressDTO dto) {
return template.update("rest.edit", dto);
}
@Override
public int del(String seq) {
return template.delete("rest.del", seq);
}
@Override
public AddressDTO get(String seq) {
return template.selectOne("rest.get", seq);
}
}
rest.xml
<?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">
<!-- rest.xml -->
<mapper namespace="rest">
<select id="test" resultType="Integer">
select count(*) from tblAddress
</select>
<insert id="add" parameterType="com.test.domain.AddressDTO">
insert into tblAddress (seq, name, age, gender, address, regdate)
values (seqAddress.nextVal, #{name}, #{age}, #{gender}, #{address}, default)
</insert>
<select id="list" resultType="com.test.domain.AddressDTO">
select * from tblAddress order by name asc
</select>
<update id="edit" parameterType="com.test.domain.AddressDTO">
update tblAddress set
name = #{name},
age = #{age},
gender = #{gender},
address = #{address}
where seq = #{seq}
</update>
<delete id="del" parameterType="String">
delete from tblAddress where seq = #{seq}
</delete>
<select id="get" parameterType="String" resultType="com.test.domain.AddressDTO">
select * from tblAddress where seq = #{seq}
</select>
</mapper>
REST API Server 구축이 완료되었다.
클라이언트 쪽 기능이 전무하기 때문에 화면(구축)이 없다. 즉, 이는 개발자들에게 제공하는 기능들을 모아놓은 서버이다.
각종 Open API 서버와 동일한 상태라고 보면 된다.