샘플 애플리케이션 구현 및 프로젝트 환경 구성
샘플 애플리케이션
소개
- 간단한 애플리케이션을 제작해보면서 Spring 기술을 터득
샘플 애플리케이션
- 이름 : 커피 주문 웹 애플리케이션
- 설명
- 커피 주문을 위해 필요한 정보를 제공하는 서버용 웹 애플리케이션 제작
- 커피만 주문할 수 있는 것으로 기능을 제한한다(애플리케이션 경계)
- 기능
- 정보
- 커피 자체 정보(Coffee)
- 고객 정보(Member)
- 주문 정보(Order)
- 주인이 커피 정보 등록하는 기능
- 주문에 대한 결제 기능
- 배달, 방문 선택 기능
- 포인트, 스탬프 처리 기능
- 배달, 픽업 완료 기능
실습 환경 구성
- Spring Initializr에서 Spring project 생성
- Gradle Project, Java 11, Jar packaging 선택
- Dependency
Spring MVC
개요
Spring Framework
- Spring에서 지원하는 모든 기능을 포함한 것
Spring-webmvc
- 웹 계층을 담당하는 모듈 중 Servlet API 기반으로 클라이언트 요청 처리하는 모듈
- Spring MVC, Spring MVC Framework라고도 부름
Spring MVC 역할
Servlet
- 클라이언트 요청 처리를 위해 특정 규약에 맞게 Java코드로 작성하는 클래스 파일
- Apache Tomcat은 servlet이 웹 애플리케이션에서 실행되도록 도와주는 Servlet Container
- Spring MVC 내부에서는 Servlet을 기반으로 웹 애플리케이션 동작
Model
개념
- Spring MVC에서 클라이언트의 요청을 받아 작업을 처리한 결과 데이터를 응답으로 돌려주는 작업 처리 결과 데이터를 Model이라고 한다.
- Service Layer - 클라이언트 요청을 구체적으로 처리
- Business Logic - 요청 사항을 처리하기 위해 Java 코드로 구현하는 것
View
개념
- Model data를 이용해 클라이언트 애플리케이션의 화면에 보이는 리소스를 제공
형태
- HTML 페이지 출력
- HTML 페이지를 직접 렌더링해서 클라이언트 측에 전송
- HTML 태그로 구성된 페이지에 Model 데이터를 넣고, 최종 HTML 페이지 클라이언트에게 전송
- Thymeleaf, FreeMarker, JSP + JSTL, Tiles 기술 사용
- PDF, Excel 등 문서 형태 출력
- Model data를 가공해 PDF, Excel 등의 문서로 만들어 클라이언트 측에 전송
- 문서 내 데이터가 동적 변경되어야 하는 경우 사용
- XML, JSON 형식의 포맷으로 변환
Controller
개념
- 클라이언트의 요청을 직접 전달 받는 엔드포인트로 Model, View의 중간에서 상호작용
- 요청을 전달 받아 비즈니스 로직을 거치고 Model이 만들어지면 View로 전달하는 역할
흐름
- Client 요청 데이터 전송 → Controller 요청 데이터 수신 → 비즈니스 로직 처리 → Model 데이터 생성 및 Controller에게 전달 → Controller가 View에게 Model 전달 → View가 응답 데이터 생성
JSON 표기법
- 기본 자료형 : Number, String, Boolean, Array, Object
- Number
- 정수
- 실수(고정 소수점) ex) 3.25
- 실수(부동 소수점) ex) 4.36e+4
- String
- “”(큰 따옴표)로 묶어야한다.
- \b(백스페이스), \f(폼 피드), \n(개행), \r(캐리지 리턴), \t(탭), \”(따옴표), \/(슬래시) \\(역슬래시), \uHHHH(16진수 4자리 유니코드 문자)
- Array
- 로 묶는다.
- 기본 자료형, 배열, 객체를 요소로 가진다.
- , (쉼표)로 구분
- Object
- 객체는 이름, 값의 쌍으로 {}(중괄호) 사용
- JSON to Java
Spring MVC 동작 방식과 구성 요소
동작 흐름
- 클라이언트가 요청을 전송하면 DispatcherServlet 클래스에 요청 전달
- DispatcherServlet는 클라이언트 요청 처리를 할 Controller 검색을 HandlerMapping 인터페이스에게 요청
- HandlerMapping은 클라이언트 요청과 매핑되는 핸들러 객체를 DispatcherServlet에게 리턴
- DispatcherServlet 는 HandlerAdapter에게 Handler 메서드 호출 위임
- HandlerAdapter는 전달받은 Controller 정보 기반으로 해당 Controller Handler 메서드 호출
- Controller의 handler 메서드가 비즈니스 로직을 처리한 후 리턴 받은 Model 데이터를 HandlerAdapter에게 전달
- HandlerAdapter가 전달받은 Model과 View 정보를 다시 DispatcherServlet으로 전달
- DispatcherServlet은 view 정보를 ViewResolver에게 전달해서 view 검색 요청
- ViewResolver는 view 정보에 해당하는 view를 찾아서 view를 리턴
- DispatcherServlet은 ViewResolver로부터 받은 view를 통해 model 데이터를 넘겨주면서 클라이언트에게 전달할 응답 데이터 생성을 요청
- View는 응답 데이터를 생성해서 DispatcherServlet에게 전달
- DispatcherServlet는 view로부터 받은 응답 데이터를 최종적으로 클라이언트에게 전달
API Layer
Controller 클래스 설계 및 구조 생성
개요
- 계층형 아키텍처 : API Layer → Business Layer → Data Access Layer
- API Layer를 Spring MVC 기반 코드로 구현
- Controller가 Spring MVC에서 클라이언트 요청의 최종 목적지
패키지 구조 생성
- 기능 기반 패키지 구조
- 애플리케이션의 패키지를 애플리케이션에서 구현해야 하는 기능을 기준으로 구성
- 하나의 기능을 완성하기 위한 계층별 클래스들이 모여있다(API layer, service layer, data access layer)
- 계층 기반 패키지 구조
- 패키지를 하나의 계층으로 보고 계층별로 묶어서 관리하는 구조
- 테스트와 리팩터링 용이, 마이크로 서비스 시스템으로 분리가 용이한 기능 기반 패키지 권고
Controller 설계
- 클라이언트 요청을 처리할 서버 애플리케이션 기능 구상
- 커피 정보 등록 기능
- 고객이 커피 정보 조회 기능
- 커피 주문 기능
- 주인이 주문한 커피 조회
- 커피 주문 조회
- 고객 전달 완료한 주문 완료 처리 기능
애플리케이션 필요 리소스
- REST API 기반 애플리케이션은 제공해야 될 기능을 리소스로 분류
- 회원, 커피, 주문이라는 리소스 구성
- 고객과 주인을 회원이라는 리소스를 포함하여 둘 사이 role을 통해 기능을 구분 가능
Entrypoint 클래스 작성
- main() 메서드가 포함된 엔트리 포인트(애플리케이션 시작 지점)을 작성
- Spring Initializr는 엔트리포인트 클래스 이미 작성 완료
@SpringBootApplication
public class Section3Week1Application {
public static void main(String[] args) {
SpringApplication.run(Section3Week1Application.class, args);
}
}
- @SpringBootApplication
- 자동 구성 활성화
- @Component가 붙은 클래스 scan 하고 Spring Bean으로 등록
- @Configuration이 붙은 클래스 scan하고 Spring Bean으로 등록
- SpringApplication.run(Section3Week1Application.class, args);
- Spring 애플리케이션을 부트스트랩, 실행
- 부트스트랩(Bootstrap) - 설정 작업을 하여 실행 가능 상태로 만드는 것
Controller 구조
MemeberController / CoffeeController / OrderController
@RestController
@RequestMapping("/v1/orders")
public class OrderController {
}
- @RestController
- REST API의 리소스를 처리하기 위한 API 엔드포인트로 동작
- Spring bean으로 등록
- @RequestMapping
REST API
- HTTP 네트워크 상의 리소스를 URI라는 고유 주소로 접근하는 접근 방식을 REST
- REST방식으로 리소스에 접근하는 서비스 API를 REST API
- URI는 식별자, URL은 웹 상의 주소
- REST API URI 규칙
- 마지막이 /로 끝나지 않는다
- 동사보다 명사 사용
- 단수보다 복수형 명사
- 기본 소문자 사용
- 하이픈(-) 사용
- 파일 확장자는 URI에 포함하지 않는다
@SpringBootApplication
- @EnableAutoConfiguration
- @ComponentScan
- @SpringBootConfiguration
Handler Method
@RestController
@RequestMapping(value ="/v1/members", produces = {MediaType.APPLICATION_JSON_VALUE})
public class MemberController{
@PostMapping
public String postMemeber(@RequestParam("email") String email,
@RequestParam("name") String name,
@RequestParam("phone") String phone){
System.out.println("# email: "+email);
System.out.println("# name: "+name);
System.out.println("# phone: "+phone);
String response="{\\""+"email\\":\\"" + email + "\\"," +
"\\"name\\":\\"" + name + "\\","+
"\\"phone\\":\\""+phone+"\\"}";
return response;
}
@GetMapping("/{member-id}")
public String getMember(@RequestParam("member-id") long memberId){
System.out.println("# memberId: " + memberId);
return null;
}
public String getMembers(){
System.out.println("# get Members");
return null;
}
}
- @RequestMapping
- produces : 응답 데이터를 어떤 미디어 타입으로 전송할지 결정
- MediaType.APPLICATION_JSON_VALUE 값으로 JSON 형태 데이터로 응답을 전송
- @PostMapping
- 클라이언트 요청 데이터를 서버에 생성할 때 사용하는 애너테이션 http method 타입을 POST로 할 때 mapping 된다
- @RequestParam
- 핸들러 메서드의 파라미터 종류 중 하나
- 클라이언트에서 요청 데이터를 쿼리 파라미터 형식으로 전송하면 서버 쪽에서 전달받을 때 사용하는 애너테이션
- 쿼리 파라미터는 ?page=10&size=20 형태로 표현
- 리턴 값
- 클라이언트에서 JSON 형태의 데이터를 전송받아야 해서 JSON 형식에 맞는 문자열 반환
- POST method를 처리하는 핸들러 메서드의 경우 생성한 데이터를 반환하는 것이 관례
- @GetMapping
- 클라이언트가 서버에 리소스를 조회할 때 사용하는 애너테이션
- /v1/members/{member-id}
- {member-id}는 회원 식별자로 URI로 어떤 값을 지정하느냐에 따라 동적으로 바뀌는 값
- @PathVariable
- 핸들러 메서드의 파라미터
- @GetMapping 괄호 안에 있는 문자열과 동일한 값을 넣어야 한다
ResponseEntity
@RestController
@RequestMapping("/v1/coffees")
public class CoffeeController {
@PostMapping
public ResponseEntity postCoffee(@RequestParam("engName") String engName,
@RequestParam("korName") String korName,
@RequestParam("price") long price){
System.out.println("# engName: " + engName);
System.out.println("# korName: " + korName);
System.out.println("# price: " + price);
Map<String, Object> map =new HashMap<>();
map.put("engName", engName);
map.put("korName", korName);
map.put("price", price);
return new ResponseEntity<>(map, HttpStatus.CREATED);
}
@GetMapping("/{coffee-id}")
public ResponseEntity getCoffee(@PathVariable("coffee-id") long coffeeId){
System.out.println("# coffeeId: " + coffeeId);
return new ResponseEntity(HttpStatus.OK);
}
@GetMapping
public ResponseEntity getCoffees(){
System.out.println("# get Coffees");
return new ResponseEntity<>(HttpStatus.OK);
}
}
- produce 속성을 제거
- JSON을 수작업했던 부분을 Map을 통해 구성
- ResponseEntity 객체를 반환 ( 파라미터로 응답 데이터, HTTP header, HTTP 응답 상태 전달)
HTTP Header
개념
- 요청이나 응답에 포함되어 부가 정보를 HTTP 메시지에 포함한 내용
사용 목적
- Content-Type
- HTTP 메세지 바디의 데이터 형식이 어떤 것인지 알려주는 역할
- application/json(json 형태 데이터)
- Authorization
- 클라이언트가 자격 증명을 가지고 있는지 확인하는 정보
- User-Agent
- 다양한 유형의 클라이언트가 하나의 서버에 요청을 할 경우 구분하여 응답 데이터를 다르게 보내줄 때 사용
HTTP Request header 얻기
- @RequestHeader("user-agent") String userAgent
- @RequestHeader Map<String, String> headers
- HTTPServletRequest httpServletRequest
- httpServletRequest.getHeader(”user-agent”)
- HTTPServletRequest 객체로 받아서 getHeader를 통해 얻기
- HttpEntity httpEntity
- httpEntity.getHeaders
- HttpEntity 객체로 받아서 getHeaders로 가져오기
- get(”host”) 혹은 getHost()로 헤더 중 자주 사용하는 헤더 가져올 수 있다
HTTP Response Header 정보 추가
- HttpHeaders headers=new HttpHeaders();
- headers.set(”Client-Geo-Location”, “Korea,Seoul”);
- ResponseEntity<>(new Member(…), headers, HttpStatus.CREADTED);
- HttpHeaders 객체를 생성하여 headers를 set하고 ResponseEntity로 구성한다
- HttpServletResponse response
- response.addHeader(”Client-Geo-Location”, “Korea,Seoul”);
- HttpServletResponse 객체로 받아서 addHeader로 추가한다
Rest Client
Client - Server 관계
- front end와 back end처럼 서버도 다른 서버로부터 리소스를 제공받는다
- client -server 관계는 상대적인 것
Rest Client
- Rest API 서버에 HTTP 요청을 보낼 수 있는 클라이언트 툴 혹은 라이브러리
- ex) Postman
RestTemplate
- Java에서 사용할 수 있는 HTTP Client 라이브러리
- java.net.HttpURLConnection, Apache HttpComponents, OkHttp 3, Netty
- Spring에서는 다른 Backend 서버에 HTTP 요청을 보낼 수 있는 Rest Client API를 RestTemplate으로 제공
- Rest 엔드포인트 지정, 헤더 설정, 파라미터 및 바디 설정을 쉽게 가능하게 한다
RestTemplate 생성
RestTemplate restTemplate =
new RestTemplate(new HttpComponentsClientHttpRequestFactory());
- dependencies에 implementation 'org.apache.httpcomponents:httpclient' 추가
URI 생성
UriComponents uriComponents =
UriComponentsBuilder
.newInstance()
.scheme("http")
.host("worldtimeapi.org")
.port(80)
.path("/api/timezone/{continents}/{city}")
.encode()
.build();
URI uri = uriComponents.expand("Asia", "Seoul").toUri();
- UriComponents 객체를 생성
- UriComponentsBuilder에서 newInstance()로 객체 생성
- scheme 설정
- host 설정
- port 설정
- path 설정(템플릿 변수를 사용)
- encode로 템플릿 변수 인코딩
- UriComponents 객체를 최종 생성
- uriComponents에서 파라미터로 템플릿 변수 값을 넣어 URI 객체 생성
요청 전송
String result = restTemplate.getForObject(uri, String.class);
System.out.println(result);
- Rest 엔드포인트로 request 전송
- getForObject() 메서드는 HTTP GEt 요청을 통해 서버의 리소스 조회
- 파라미터로 URI와 응답으로 전달받을 클래스 타입 지정
WorldTime worldTime = restTemplate.getForObject(uri, WorldTime.class);
worldTime.getDateTime();
worldTime.getTimezone();
worldTime.getDay_of_week();
- 커스텀 클래스를 하나 생성해서 받을 수도 있다.
ResponseEntity<WorldTime> response =
restTemplate.getForEntity(uri, WorldTime.class);
response.getBody().getTimezone();
response.getStatusCode();
response.getHeaders().getContentType();
response.getHeaders().entrySet();
- ResponseEntity 객체로 getForEntity 메서드를 사용해서 받으면 헤더와 바디 정보를 모두 받을 수 있다.
ResponseEntity<WorldTime> response =restTemplate.exchange(uri,HttpMethod.GET,null,
WorldTime.class);
- exchange() 메서드를 사용해서 파라미터로 URI, HttpMethod, HttpEntity, Class를 전달
DTO(Data Transfer Object)
개념
- 데이터를 전송하기 위한 객체
- 서버와 클라이언트 사이의 요청 응답에 사용 가능
목적
- @RequestParam을 통해 파라미터에 추가되는 것을 하나의 객체로 전달하면 코드를 간결하게 작성할 수 있다.
- HTTP 요청의 수를 줄이기 위해 사용
- 도메인 객체와 분리
데이터 유효성 검증의 단순화
- 이메일 주소 형식처럼 유효한 데이터를 전달받기 위해 데이터를 검증하는 것을 데이터 유효성 검증이라고 한다.
- 핸들러 메서드에서 유효성 검사 로직을 외부로 빼면 간결함을 유지 가능
- DTO 객체에서 @email을 필드에 붙여 유효성 검증을 시도한다.
- DTO 객체를 파라미터에 넣을 때에 @Valid를 붙여 핸들러 메서드를 간결하게 유지 가능
HTTP 요청/응답 데이터에 DTO 사용
- @RequestParam을 사용한 부분에 @RequestBody를 통해 따로 생성한 DTO 객체를 넣어준다.
- @RequestBody
- JSON 형식의 Request Body를 DTO 클래스 객체로 변환
- 즉, 클라이언트가 전송하는 요청 바디가 JSON 형식이어야 한다.
- @ResponseBody
- JSON 형식의 Response Body를 전달하기 위해 DTO 클래스 객체를 JSON 형식으로 변환하는 역할
- @ResponseEntity나 @ResponseBody를 붙이면 JSON 형식으로 변환
DTO 유효성 검증
필요성
- frontend에서 유효성 검증을 해도 중간에 값을 변경할 수 있기 때문에 backend 쪽에서 한번 더 유효성 검증을 한다.
라이브러리
- dependencies에 implementation 'org.springframework.boot:spring-boot-starter-validation' 추가
유효성 검증
- email
- 값이 비어있지 않거나 공백 아님
- 유효한 이메일 주소 형식
- name
- phone
- 값이 비어있지 않거나 공백 아님
- 010으로 시작하는 11자리 숫자와 ‘-’로 구성
- @NotBlank
- 비어있지 않거나 공백 아님
- null 값, 공백, 스페이스 허용 안 한다
- @Email
- @Pattern
- 정규표현식에 매치되는 번호인지 검증
- regexp=””로 표현
- 핸들러 매서드에 @Valid를 추가해야 한다
쿼리 파라미터 및 @Pathvariable에 대한 유효성 검증
- @PathVariable 뒤에 @Min(1)을 설정하면 1 이상의 값으로 검증
- 클래스 레벨에 @Validated를 검사
Jakarta Bean Validation
- 유효성 검증을 위한 표준 스펙
- 애너테이션을 구현한 구현체는 HIbernate Validator
Custom Validator를 사용한 유효성 검증
- Custom annotation 정의
- custom validator 구현
- DTO 클래스의 멤버 변수에 Custom Annotation을 추가하여 유효성 검증