<< 학습 목표 >>

1. 상태 코드를 담아 응답할 수 있다.

2. 데이터 또는 정보를 담아 응답할 수 있다.

3. 상태 코드와 데이터 또는 정보를 담아 응답할 수 있다.

4. @RestController 애너테이션을 사용해 API를 개발할 수 있다.


지금까지 Chapter02 에서는 다음과 같은 것들을 했음

1. 클라이언트의 요청을 받기 위해 컨트롤러 추가하는 방법 : https://codingaja.tistory.com/103

2. 클라이언트가 보낸 다양한 형태의 값 꺼내는 방법 : https://codingaja.tistory.com/104, https://codingaja.tistory.com/105, https://codingaja.tistory.com/106

3. Lombok

4. 클라이언트가 보낸 값을 활용하기 전 유효성 검증하는 방법 : https://codingaja.tistory.com/109, https://codingaja.tistory.com/110

5. RestfulAPI

 - 이론 : https://codingaja.tistory.com/125

 - 다양한 요청 방식 받는 방법 : https://codingaja.tistory.com/126

 - 상황에 맞는 URI 정하는 방법 : https://codingaja.tistory.com/127


이번에는 RestfulAPI의 마지막으로 클라이언트에게 다양한 형태로 RestfulAPI 답게 응답하는 방법을 배워보자

 

RestfulAPI 답게 응답하려면 상태 코드(Status Code)를 적극적으로 활용해야함

상태 코드에 대해서는 이미 Servlet에서 배웠을 것이므로 상태 코드 자체에 대한 설명은 생략함

 

"클라이언트가 GET, POST, PUT, DELETE 중 한 방식으로 요청을 했는데 서버가 그 요청을 제대로 처리했다" 라면 200 상태 코드로 응답하면 됨

더 정확하게 POST 방식으로 요청했다면 201 상태 코드로 응답해야함

 

또한 PUT 방식을 통해 새로운 리소스가 생성됐다면 201 상태 코드로 응답해야함

그러나 PUT 방식을 통해 새로운 리소스가 생성된게 아닌 기존의 리소스가 수정됐다면 200 상태 코드로 응답해야함

 

예를 클라이언트가 상품 상세 정보를 요청했다면 클라이언트는 GET 방식으로 요청했을것이고 서버가 상품 상세 정보를 찾았다면 서버는 상품 상세 정보와 함께 200 상태 코드로 응답해야함

만약 클라이언트가 잘못된 정보를 보내 서버가 상품 상세 정보를 찾지 못했다면 서버는 400번대 상태 코드만 응답하면 됨

 

클라이언트가 상품 정보 추가를 요청했다면 클라이언트는 POST 방식으로 요청했을 것임

서버가 상품 정보를 추가했다면 200 또는 201 상태 코드로 응답해야함

이때 간혹 상품 정보 추가에 성공했다는 의미로 "success", "ok" 등의 문자열을 응답하는 개발자가 있는데 이는 잘못된 것

200 또는 201 상태 코드가 "이미 요청을 성공적으로 처리했다" 라는 의미를 갖고 있는 것

"sucess", "ok" 등의 문자열과 200 또는 201 상태 코드로 응답 하는 건 "역 전 앞" 처럼 잘못된 표현임

만약 상품 이름을 빠뜨리고 상품 정보 추가 요청을 했다면 클라이언트가 잘못된 요청을 한 것이므로 서버는 400번대 상태 코드만 응답하면 됨

이때 역시 추가를 하지 못했다는 의미로 "fail" 등을 담아 응답하면 안됨

 

클라이언트가 상품 이미지 수정을 요청했다면 클라이언트는 PUT 방식으로 요청했을 것임

서버가 상품 이미지를 수정했다면 200 상태 코드로 응답해야함

 

클라이언트가 상품 상세 정보 삭제 또는 상품 이미지 삭제 요청을 했다면 클라이언트는 DELETE 방식으로 요청했을 것임

서버가 클라이언트의 요청을 처리했다면 200 상태 코드로 응답해야함


이제 SpringFramework의 컨트롤러가 RestfulAPI 답게 응답하는 방법을 배워보자

방법1. Servlet에서 배웠던것처럼 Servlet과 똑같이 HttpServletResponse 를 사용해 상태 코드 응답하기

방법2. ResponseEntity<Void> 를 사용해 상태 코드 응답하기

 

<< 방법1. Servlet에서 배웠던것처럼 Servlet과 똑같이 HttpServletResponse 를 사용해 상태 코드 응답하기 >>

프로젝트 -> com.study.chapter02 -> RestController 클래스를 추가하고 아래 코드를 추가하자

package com.study.chapter02;

import java.time.LocalDateTime;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import jakarta.servlet.http.HttpServletResponse;

@Controller
public class RestController {
	
	@GetMapping("/chapter02/rest/1")
	public void controller1(HttpServletResponse response) {
		System.out.println("controller1 컨트롤러 호출 => " + LocalDateTime.now());
		
		response.setStatus(200);
	}

}

<< 코드 설명 >>

(1). 컨트롤러에 HttpServletResponse 타입 매개변수를 선언해 클라이언트에게 상태 코드를 설정한 응답이 가도록 할 수 있음

(2). setStatus 메서드를 사용해 클라이언트에게 응답할 상태 코드를 설정할 수 있음

 

이제 서버를 시작한 후 해당 경로로 접근해보자

 

방법1은 이게 끝임

200이 아닌 201로 응답하고 싶다면 [ response.setStatus(201); ] 로 설정하면 됨

400이나 500번대 상태 코드를 응답하고 싶다면 [ response.setStatus(응답할 상태 코드 번호); ] 로 설정하면 됨


여기서 잠깐! [ 방법2 ] 로 넘어가기 전에 이 글 전의 컨트롤러와 이 글의 컨트롤러를 비교해보자

 

RestController 클래스 내 코드를 다음과 같이 바꾸자

package com.study.chapter02;

import java.time.LocalDateTime;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import jakarta.servlet.http.HttpServletResponse;

@Controller
public class RestController {
	
	@GetMapping("/chapter02/rest/1")
	public void controller1(HttpServletResponse response) {
		System.out.println("controller1 컨트롤러 호출 => " + LocalDateTime.now());
		
		response.setStatus(200);
	}
	
	@GetMapping("/chapter02/rest/2")
	public void controller2() {
		// ...
		
		System.out.println("controller2 컨트롤러 호출 => " + LocalDateTime.now());
		
		// ...
	}

}

<< 코드 설명 >>

(1). 이 글에서 배운 상태 코드로 응답하는 컨트롤러

(2). 이 전 글까지 계속 사용했던 아무것도 응답하지 않는 컨트롤러

 

서버를 재시작하고 (2) 컨트롤러로 접근해보자

지금까지 계속 사용하면서 느꼈지만 접근했을 때 항상 결과가 다음과 같았음

 

그 이유는 컨트롤러는 재대로 호출했지만 컨트롤러가 아무것도 응답하지 않아서 발생했던 것

 

이제 다시 (1) 컨트롤러로 접근해보자

지금까지와는 다르게 상태 코드를 설정해 응답했으므로 상태 코드만 보임

이렇게 API를 RestfulAPI 답게 만들면 응답이 내가 의도한대로 됨


여기서 또 다시 잠깐! [ 방법1 ] 에서 컨트롤러에 매개변수를 HttpServletResponse 만 넣었는데 이렇게 활용할 수 있다는 것이지 꼭 이렇게만 해야된다는게 아님

컨트롤러에서 클라이언트가 보낸 값을 사용하려면 전에 배운 것처럼 클라이언트가 보낸값을 꺼내기 위해 매개변수를 적절히 추가할 수 있음

 

예를 들어 URI가 /chapter02/rest/1 인 컨트롤러로 클라이언트가 데이터를 보낸다고 하자

클라이언트가 이름이 tel 인 파라미터에 값을 담아 보냈고 컨트롤러가 tel 파라미터를 꺼내려면 다음과 같이 할 수 있음


다시 본론으로 돌아가서

<< 방법2. ResponseEntity<Void> 를 사용해 상태 코드 응답하기 >>

 

프로젝트 -> com.study.chapter02 -> RestController 에 아래 코드를 추가하자

package com.study.chapter02;

import java.time.LocalDateTime;

import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import jakarta.servlet.http.HttpServletResponse;

@Controller
public class RestController {
	// ...

	@GetMapping("/chapter02/rest/3")
	public ResponseEntity<Void> controller3() {
		System.out.println("controller3 컨트롤러 호출 => " + LocalDateTime.now());
		
		return ResponseEntity.status(200).build();
	}
    
	// ...
}

<< 코드 설명 >>

(1). 컨트롤러의 반환 타입이 ResponseEntity<Void> 임

  컨트롤러의 반환 타입이 ResponseEntity<?> 라면 "이 컨트롤러는 RestfulAPI 다" 라고 명시한 것

  [ 방법1 ] 은 "이 컨트롤러는 RestfulAPI 다" 라고 명시하진 않았지만 응답을 RestfulAPI답게 응답한 것이고 [ 방법2 ] 는 "이 컨트롤러는 RestfulAPI 다" 라고 명시한 것

(2). status 메서드를 사용해서 클라이언트에게 응답할 상태 코드를 설정할 수 있음

 

서버를 재시작하고 이 컨트롤러로 접근하면 [ 방법1 ] 과 마찬가지로 의도한대로 상태 코드만 보이는 응답 결과가 보임


클라이언트에게 응답할 때 상태 코드만 응답하진 않음

상품 정보 조회 같은 경우에는 조회한 상품의 정보와 상태코드를 함께 응답해야함

 

이렇게 컨트롤러가 클라이언트에게 데이터(또는 정보)를 담아 응답할 때는 ResponseEntity<?> 를 사용해야함

? 자리에는 응답할 데이터 타입을 명시함

또한 ? 자리는 제네릭스 타입이 들어가는 자리이므로 Wrapper 클래스 또는 DTO 클래스명을 쓰면 됨

 

직접 실습을 통해 알아보자

우선 서버가 간단한 데이터를 응답하는 경우를 보자

 

프로젝트 -> com.study.chapter02 -> RestController 클래스 내 아래 컨트롤러를 추가하자

package com.study.chapter02;

import java.time.LocalDateTime;

import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import jakarta.servlet.http.HttpServletResponse;

@Controller
public class RestController {
	// ...
	
	@GetMapping("/chapter02/rest/4")
	public ResponseEntity<Integer> controller4() {
		System.out.println("controller4 컨트롤러 호출 => " + LocalDateTime.now());
		
		return ResponseEntity.status(200).body(117);
	}
    
	// ...
}

<< 코드 설명 >>

(1). 컨트롤러가 클라이언트에게 정수를 응답하기 위해 ? 자리에 정수(int)의 Wrapper 클래스인 Integer 를 넣었음

(2). 응답할 때는 우선 status 메서드로 상태 코드를 설정하고 body 메서드를 사용해 응답할 값을 담음

 

컨트롤러가 서버로 실수를 응답할 때는 ? 자리에 Double 을 넣으면 됨

컨트롤러가 서버로 문자를 응답할 때는 ? 자리에 Character 를 넣으면 됨

컨트롤러가 서버로 문자열을 응답할 때는 ? 자리에 String 을 넣으면 됨

 

그렇다면 컨트롤러가 클라이언트로 정보를 전달하려면 어떻게 해야할까?

이도 다르지 않음

 

먼저 실습용 DTO를 만들자

프로젝트 -> com.study.chapter02 -> DataDto 클래스를 추가 하고 아래 코드를 추가하자

package com.study.chapter02;

import java.time.LocalDateTime;

import lombok.Data;

@Data
public class DataDto {
	private int idx;						// 회원 번호
	private String id;						// 아이디
	private String pw;						// 비밀번호
	private LocalDateTime joinDateTime;		// 회원 가입 날짜
}

 

이제 컨트롤러를 추가하자

프로젝트 -> com.study.chapter02 -> RestController 에 아래 코드를 추가하자

package com.study.chapter02;

import java.time.LocalDateTime;

import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import jakarta.servlet.http.HttpServletResponse;

@Controller
public class RestController {
	// ...

	@GetMapping("/chapter02/rest/5")
	public ResponseEntity<DataDto> controller5() {
		System.out.println("controller5 컨트롤러 호출 => " + LocalDateTime.now());
		
		DataDto data = new DataDto();
		data.setIdx(11);
		data.setId("id123");
		data.setPw("mypassword");
		data.setJoinDateTime(LocalDateTime.now());
		
		return ResponseEntity.status(200).body(data);
	}
    
	// ...
}

<< 코드 설명 >>

(1). 앞서 실습했던 방법처럼 ? 자리에 응답할 정보의 데이터 타입인 DTO 클래스명을 넣었음

(2). 마찬가지로 앞서 실습했던 방법처럼 body에 응답할 정보를 넣었음

 

서버를 재시작 한 후 이 컨트롤러로 접근해보자


애너테이션 중에 @RestController 애너테이션이 있음

이 애너테이션은 "여기에 속한 모든 컨트롤러가 RestfulAPI다" 라고 명시한 것

 

프로젝트 -> com.study.chapter02 -> RestfulAPIController 클래스를 추가하고 아래 코드를 추가하자

package com.study.chapter02;

import java.time.LocalDateTime;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RestfulAPIController {
	@GetMapping("/chapter02/restful/1")
	public DataDto controller1() {
		System.out.println("controller1 컨트롤러 호출 => " + LocalDateTime.now());
		
		DataDto data = new DataDto();
		data.setIdx(11);
		data.setId("id123");
		data.setPw("mypassword");
		data.setJoinDateTime(LocalDateTime.now());
		
		return data;
	}
}

<< 코드 설명 >>

@RestController 애너테이션을 정확하게 이해하기 위해서는 비교해서 봐야함

 

@Controller 애너테이션이 붙은 컨트롤러에서 DTO를 응답하려면 반환 타입이 반드시 ResponseEntity<?> 로 해야함

 

@RestController 애너테이션이 붙은 컨트롤러에서 DTO를 응답하려면 반환 타입은 ? 로 하면 됨

그러나 이때 꼭 ? 로 해야하는건 아니고 ResponseEntity<?> 로 해도 됨

 

@RestController 애너테이션이 붙은 컨트롤러의 또 다른 특징은 상태코드가 항상 200으로 고정된다는 것

상태코드가 200으로 고정된건 @RestController 애너테이션 때문이 아닌 서버의 설정 때문임

컨트롤러가 상태 코드를 설정하지 않고 응답을 한다면 서버는 응답의 상태 코드를 200으로 설정함

즉, 상태 코드 200이 기본값임

 

그래서 @RestController 애너테이션이 붙은 컨트롤러는 기본적으로 200 상태 코드로 고정되어있음

만약 @RestController 애너테이션이 붙은 컨트롤러의 상태 코드를 201, 400, 500 등 다른 값으로 바꾸고 싶다면 컨트롤러의 반환 타입을 ResponseEntity<?> 로 해주면 됨

 

프로젝트 -> com.study.chapter02 -> RestfulAPIController 에 아래 코드를 추가하자

package com.study.chapter02;

import java.time.LocalDateTime;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RestfulAPIController {
	// ...

	@GetMapping("/chapter02/restful/2")
	public ResponseEntity<DataDto> controller2() {
		System.out.println("controller2 컨트롤러 호출 => " + LocalDateTime.now());
		
		DataDto data = new DataDto();
		data.setIdx(11);
		data.setId("id123");
		data.setPw("mypassword");
		data.setJoinDateTime(LocalDateTime.now());
		
		return ResponseEntity.status(400).body(data);
	}
    
	// ...
}

<< 코드 설명 >>

(1). 앞서 얘기했듯 @RestController 애너테이션이 붙은 컨트롤러라도 반환 타입을 ResponseEntity<?> 로 할 수 있음

(2). 이럴 경우 내가 원하는 상태 코드와 함께 데이터를 담아 보낼 수 있음

728x90
LIST

<< 학습 목표 >>

1. RestfulAPI의 URL과 요청 방식을 적절하게 지정할 수 있다.

2. @PathVariable 애너테이션을 사용할 수 있다.


RestfulAPI를 만들 때 지켜야하는 첫 번째 약속은?

일관되게 API를 구현해야함

( 바로 윗 줄을 마우스로 드래그하시면 보입니다 )

 

이를 위해서는?

- 모든 리소스는 고유의 URI를 가져야함

- 클라이언트는 상황에 맞는 올바른 방식으로 요청해야함

- 서버는 요청 방식에 맞는 처리를 제공해야함

( 윗 줄을 마우스로 드래그하시면 보입니다 )


회원가입, 회원 정보 조회, 회원 정보 수정, 회원 탈퇴 API를 구현한다고 하자

모두 회원과 관련된 API이므로 URI에 member 가 들어가면 좋음

 

서버는 http://localhost:8080/ 이고 이 서버에서 동작하는 프로젝트명은 study 라고 할 때

회원가입 API의 URL은 http://localhost:8080/study/member 으로 하고 요청 방식은 POST로 하면 RestfulAPI로 설계했다고 볼 수 있음

어느정도 경력이 있는 개발자라면 대부분 위 URL과 요청 방식을 보고 "회원 가입 API군" 할 것

 

가령 회원가입 API의 URL을 http://localhost:8080/study/computer 으로 하고 요청 방식을 GET으로 하면 경력이 아무리 많다고 해도 "회원 가입 API군" 이라고 생각할 수 없을 것

 

또한 회원가입 API의 URL을 http://localhost:8080/study/member/join 또는 http://localhost:8080/study/memberJoin 또는 http://localhost:study/member_join 등으로 지정하는 것도 좋지 못함

 

http://localhost:8080/study/member/join 이 좋지 못한 이유

 -> 요청 방식은 당연히 POST가 될 것이므로 POST 자체가 리소스 생성을 의미하는 요청 방식이기 때문에 URI의 join과 요청 방식의 POST가 똑같은 의미를 지니기 때문

  마치 역 전 앞 과 같음, 요즘에는 자주 쓰는 말은 하니지만 역 전 앞이란 역 앞을 뜻함

  예전에는 "부평역 전 앞에서 만나" 이런 말을 사용했는데 의미는 "부평역 앞에서 만나" 임

  그러나 부평역 전 앞에서 전(前)은 한자로 앞을 뜻함

   "부평역 전 앞에서 만나" 를 해석해보면 "부평역 앞 앞에서 만나"가 됨

  똑같은 말이 반복되므로 겹말이라고 하고 잘못된 표현이므로 역 앞이라고 올바르게 고쳐써야함

  이것처럼 요청 방식은 POST 로 하고 URL은 http://localhost:8080/study/member/join 은 의미가 중첩되므로 좋지 못한 URL임

  혹시... 그럼 요청 방식을 다른걸로 바꾸면 되는거아닌가? 싶은 분은 없을 것이라 생각함

 

http://localhost:8080/study/memberJoin 이 좋지 못한 이유

 -> 위에서 설명한 이유 + RestfulAPI의 URL은 소문자로만 이루어져야함

    그렇다고 URL을 http://localhost:8080/study/memberjoin 으로 하면 URI 가독성이 떨어지므로 좋지 못함

 

http://localhost:study/member_join 이 좋지 못한 이유

 -> 위에서 설명한 이유 + RestfulAPI의 URL은 _ ( 밑 줄 / 언더 바 ) 대신 - ( 하이픈 ) 을 사용할 것을 권장함

     그 이유는 특정 폰트나 기기에서 _ 가 가려지는 현상이 빈번하게 발생해 URL 가독성을 위해 - 을 사용해야함

 

그래서 회원 가입 API의 URL은 http://localhost:8080/study/member 로 요청 방식은 POST가 가장 최선임

 

 

회원 정보 조회 API의 URL은 http://localhost:8080/study/member 로 요청 방식은 GET으로 할 수 있음

닉네임 또는 연락처 등으로 특정 회원의 정보를 조회한다면 위 URL과 요청 방식에 파라미터를 담아 요청하면 됨

또는 모든 회원 정보 조회 API와 닉네임으로 회원 정보 조회 API를 만든다고 할 때 모든 회원 정보 조회 API의 URL은 http://localhost:8080/study/member/list 으로 요청 방식은 GET으로 할 수 있음

닉네임으로 회원 정보 조회 API의 URL은 http://localhost:8080/study/member/조회할회원의닉네임 으로 지정하고 요청 방식은 GET으로 할 수 있음

닉네임이 홍길동인 회원의 정보를 조회한다면 http://localhost:8080/study/member/홍길동 이 될 것임

 

어느 정도 경력이 있는 개발자라면 http://localhost:8080/study/member, GET 방식 API를 보고 대부분 "회원 정보 조회 API군" 할 것

또 http://localhost:8080/study/member/list, GET 방식 API를 보고 대부분 "회원들의 정보를 조회하는 API군" 할 것

또 http://localhost:8080/study/member/홍길동, GET 방식 API를 보고 대부분 "홍길동 회원의 정보를 조회하는 API군" 할 것

 

 

회원 가입, 회원 정보 조회  API와 마찬가지로 회원 정보 수정, 회원 탈퇴 API도 URL과 요청 방식만 잘 지정해주면 대부분의 개발자들이 URL과 요청 방식만 보고서 어떤 API인지 유추 할 수 있음

 

< 회원 정보 수정 URL과 요청 방식의 몇 가지 예 >

1. http://localhost:8080/study/member, PUT 방식

2. http://localhost:8080/study/member/수정할회원의번호, PUT 방식

  ex) 회원 번호가 3번인 회원의 정보를 수정한다면 http://localhost:8080/study/member/3, PUT 방식으로 요청할 것

        수정할 정보는 요청 Body에 담아 보내면 됨

 

< 회원 탈퇴 URL과 요청 방식의 몇 가지 예 >

1. http://localhost:8080/study/member, DELETE 방식

2. http://localhost:8080/study/member/탈퇴할회원의번호, DELETE 방식

 

 

이렇게 RestfulAPI는 URL과 요청 방식을 활용해 호출하는 쪽인 클라이언트가 "이 API를 호출 했을 때 어떤 동작을 하겠다" 를 유추할 수 있게 하는 개발 방식임


여기서 회원 정보 조회, 회원 정보 수정, 회원 탈퇴 등 API의 URL을 보자

1. 닉네임으로 회원 정보 조회 : http://localhost:8080/study/member/조회할회원의닉네임

2. 회원 정보 수정 : http://localhost:8080/study/member/수정할회원의번호

3. 회원 탈퇴 : http://localhost:8080/study/member/탈퇴할회원의번호

 

이와 같이 URL을 지정한다면 서버에 필요한 데이터가 URL에 포함되있음

이렇게 URL에 포함된 데이터를 API가 꺼내 사용하고 싶다면 @PathVariable 애너테이션을 사용함

 

API(컨트롤러)에서 @PathVariable 애너테이션을 사용해보자

프로젝트 -> com.study.chapter02 -> PathVariableController 를 추가하고 아래 코드를 추가하자

package com.study.chapter02;

import java.time.LocalDateTime;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@Controller
public class PathVariableController {
	
	@GetMapping("/chapter02/pv/{parameter}")
	public void sample1(@PathVariable(name = "parameter") String parameter) {
		System.out.println("sample1 컨트롤러 호출 / " + LocalDateTime.now());
		System.out.println("parameter => " + parameter);
	}
	
	@GetMapping("/chapter02/pv/depth1/{parameter}/depth2")
	public void sample2(@PathVariable(name = "parameter") String parameter) {
		System.out.println("sample2 컨트롤러 호출 / " + LocalDateTime.now());
		System.out.println("parameter => " + parameter);
	}
	
}

<< 코드 설명 >>

(1). GET 방식으로 http://localhost:8080/chapter02/pv/어떤값 URL에 접근하면 이 컨트롤러가 동작함

  예) http://localhost:8080/chapter02/pv/a , http://localhost:8080/chapter02/pv/b , http://localhost:8080/chapter02/pv/홍길동 , http://localhost:8080/chapter02/pv/honggildong , ...

 

(2). GET 방식으로 http://localhost:8080/chapter02/depth1/어떤값/depth2 URL에 접근하면 이 컨트롤러가 동작함

 

GET 방식으로 http://localhgost:8080/chapter02/pv/depth1 URL에 접근하면 어떤 컨트롤러가 동작할까?

(1) 컨트롤러

이유는? (1) 컨트롤러는 GET 방식으로 http://localhost:8080/chapter02/pv/어떤값 이므로


여기서는 GET 방식만 예를 들었지만 POST, PUT, DELETE 모두 같은 방법으로 @PathVariable 애너테이션을 사용할 수 있음


URL에 지정한 PathVariable의 이름과 매개변수명이 같다면 (1)

@PathVariable 애너테이션의 name 속성 (2) 을 생략해도 됨


마지막으로 PathVariable은 다음과 같이 여러 개를 지정할 수 있음

그러나 경험상 PathVariable이 많으면 오히려 URL을 보고 API를 예측하기 어려워지므로 PathVariable은 최소한으로 사용하는게 좋음

 

PathVariable의 적정 개수가 정해지진 않았지만 어떤 것이든 너무 과하면 좋지 못하니 다양하게 경험을 쌓으면서 상황에 맞게 적절히 사용하자

728x90
LIST

<< 학습 목표 >>

1. PUT, DELETE 요청 방식을 받을 수 있다.

2. PUT, DELETE 요청 방식을 받을 수 있게 설정할 수 있다.


전 글 ( https://codingaja.tistory.com/103 ) 에서 GET, POST 방식 요청을 받는 방법을 알아봤음

이번에는 RestfulAPI에 사용되는 나머지 요청 방법인 PUT, DELETE 요청을 받는 방법을 알아보자

 

PUT, DELETE 요청을 받는 방법은 GET, POST와 동일함


<< PUT, DELETE 요청 받는 방법 >>

프로젝트 -> com.study.chapter02 -> TestController02 클래스를 추가하고 아래 코드를 추가하자

package com.study.chapter02;

import java.time.LocalDateTime;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PutMapping;

@Controller
public class TestController02 {

	@PutMapping("/chapter02/test/controller/03")
	public void processPutRequest() {
		System.out.println("TestController02 -> processPutRequest 메서드 호출 " + LocalDateTime.now());
	}
	
	@DeleteMapping("/chapter02/test/controller/04")
	public void processDeleteRequest() {
		System.out.println("TestController02 -> processDeleteRequest 메서드 호출 " + LocalDateTime.now());
	}
}

<< 코드 설명 >>

코드 설명이 필요 없을 정도로 간단함

1. GET 요청을 받을 때는 @GetMapping 애너테이션을 달고

2. POST 요청을 받을 때는 @PostMapping 애너테이션을 다는 것처럼

3. PUT 요청을 받을 때는 @PutMapping 애너테이션을 달고

4. DELETE 요청을 받을 때는 @DeleteMapping 애너테이션을 담


그러나 여기서 중요한 점 한가지가 있음

PUT, DELETE 요청 방식은 논란이 있는 요청 방식이라 Spring Framework 프로젝트는 기본적으로 PUT, DELETE 요청을 받지 않도록 막아뒀음

그래서 PUT, DELETE 요청을 받을 수 있게 뚫어줘야함

( 막아둔 이유가 궁금하다면 구글 또는 ChatGPT를 활용해 CORS 정책에 대해서 찾아보자 )

 

 

PUT, DELETE 요청을 받을 수 있게 뚫으려면 WebMvcConfigurer 인터페이스를 상속 받은 클래스가 필요함

어느 패키지든 상관 없지만 우리는 프로젝트 -> com.study 패키지 -> WebConfigurer 클래스를 추가하고 아래 코드를 추가하자

package com.study;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfigurer implements WebMvcConfigurer {
	@Override
	public void addCorsMappings(CorsRegistry registry) {
		registry.addMapping("/**")
			.allowedOrigins("http://192.168.0.1:8080")
			.allowedMethods("GET", "POST", "PUT", "PATCH", "OPTIONS", "DELETE")
			.allowCredentials(true);
	}
}

<< 코드 설명 >>

(1). addMapping 메서드 : PUT, DELETE 요청을 이 프로젝트 내 모든 컨트롤러가 받을 수 있도록 하겠다

(2). allowedOrigins 메서드 : PUT, DELETE 요청을 보낼 수 있는 클라이언트를 제한하는 메서드로 http 프로토콜을 사용하고 IP가 192.168.0.1이고 포트는 8080번인 클라이언트의 요청만 addMapping 메서드로 명시한 컨트롤러가 받겠다

  프론트엔드(웹페이지)가 없어서 이 메서드의 역할이 이해가 잘 안될 것

  웹페이지의 URL이 http://192.168.0.1:8080 일 때만 이 프로젝트 내 모든 컨트롤러로 요청을 보낼 수 있다는 것

(3). allowedMethods 메서드 : 컨트롤러가 받을 수 있는 요청 방식

  우리가 알고 있는 요청 방식은 GET, POST, PUT, DELETE 뿐인데 PATCH, OPTIONS 요청 방식은 뭘까?

  클라이언트는 서버로 요청하기 전 이 방식의 요청을 서버가 받을 수 있는지 확인함

  우리가 GET, POST, PUT, DELETE 요청을 보내면 실제로 그 요청이 보내지기 전 OPTIONS 방식, PATCH방식 각 각 한번씩 요청을 보내 컨트롤러가 GET, POST, PUT, DELETE 방식 요청을 받을 수 있는지 확인함

  그래서 OPTIONS, PATCH 요청 방식도 뚫어놔야함

(4). allowCredentials 메서드 : allowedOrigins 메서드와 함께 동작하는 메서드로 allowedOrigins의 인자를 "*" 로 넣으면 어떤 클라이언트이든 이 프로젝트 내 모든 컨트롤러를 호출할 수 있도록 하겠다임

  이렇게 되면 이 프로젝트는 알 수 없는 어떤 해커에게 DDoS 공격을 받을 수 있음

  따라서 allowedOrigins 메서드의 인자는 절대 "*" 로 하면 안되고 지금 우리가 한 것처럼 내가 개발하고 있는 프론트엔드(웹페이지)의 URL만 인자로 넣어야함

  allowCredentials 메서드의 인자를 true로 설정하면 allowedOrigins의 인자를 "*" 로 할 수 없게 막게됨

  allowCredentials 메서드는 개발자의 무지 또는 실수로 allowedOrigins의 인자를 "*" 로 할 수 없게 막는 역할

728x90
LIST

<< 학습 목표 >>

1. API를 설명할 수 있다.

2. REST 아키텍처(개발 방식)에 대해서 설명할 수 있다.

3. RestfulAPI에 대해서 설명할 수 있다.


API란?

API는 Application Programming Interface 로 다른 소프트웨어 시스템과 통신(요청/응답) 하기 위한 프로그램임

쉽게 말해 API는 컨트롤러임

 

햄버거 가게의 메뉴 정보를 보여주는 화면(프론트엔드)가 다음과 같이 세 가지가 있다고 하자

웹사이트, 키오스크는 보여줄 메뉴 정보를 웹사이트, 키오스크에 저장해둔 상태로 만들 순 있지만 핸드폰의 경우는 그럴 수 없음

우리 핸드폰 살 때 맥도날드 메뉴 정보가 들어있었는지?

롯데리아 메뉴 정보가 들어있었는지?

당연히 들어있지 않음

 

또한 메뉴 정보는 언제든 바뀔 수 있기 때문에 웹사이트, 키오스크에 저장해둔 상태로 만든다는건 좋지 못함

 

그래서 화면에 필요한 메뉴 정보를 전달해 줄 컨트롤러(API)가 필요함

 

 

이때 클라이언트에게 필요한 정보 또는 데이터를 리소스(Resource/자원) 라고 부르고 정보 또는 데이터를 보여주는 화면을 클라이언트(Client) 라고 부름

컨트롤러는 클라이언트와 리소스 사이에 위치하면서

클라이언트가 자신에게 필요한 데이터(리소스)를 얻기 위한 관문이므로

컨트롤러는 게이트웨이(Gateway) 라고 생각하면 됨


REST란?

"API는 이렇게 동작해야한다" 라고 정의 해놓은 약속

 

약속1. 일관되게 API를 구현해야함

  - 이를 위해

    > 모든 리소스는 고유의 URI(Uniform Resource Identifier) 를 가져야함
       리소스가 무엇인지는 바로 윗 문단에서 설명했음

       리소스를 직역해서 자원이라고 표현하기도 함

    > 클라이언트는 올바른 방식(Method / GET, POST, DELETE, PUT ) 을 사용해 서버로 요청해야하고 서버는 요청 방식에 맞는 처리를 제공해야함

약속2. 무상태성을 가져야함

  무상태성에 대해서 이해하려면 HTTP 프로토콜에 대해서 찾아보기

약속3. 계층화된 시스템(Layered System)이어야함

  계층화(Layered / 레이어드) 는 일상 생활에서도 흔히 쓰이는 말로 "옷을 레이어드 해 입었다" 란 말은 옷 위에 옷을 입은 것으로 안에 옷만 바꿔 입으면 다른 패션이 되고 바깥 옷만 바꿔 입으면 또 다른 패션이 되는 것처럼 API가 동작하는 시스템은 계층화된 시스템이어야함

  API가 동작하는 시스템이 계층화 되어있다는건 API에 필요한 추가 사항을 추가하기 쉬운 환경이라는 것임

 가령 로그인 API를 만들고 3개월까지는 문제 없이 동작했는데 4개월 째에 로그인을 하면 해커가 로그인 정보를 빼돌린다는 걸 알았음

  이때 클라이언트의 요청이 로그인 API로 도달하기 전(보통 앞, 앞단이라고 표현함) 또는 후(보통 뒤, 뒷단이라고 표현함)에 보안 관련 기능이 동작하도록 추가 사항을 추가 할 수 있는 환경을 만들었다면 계층화된 시스템이라고 함

  단, 클라이언트는 자신이 요청한 것만 알고 요청한 것 이외에는 몰라야함

  위에서 예를 든것처럼 클라이언트는 로그인 API를 요청했는데 "로그인 API로 요청이 들어가기 전에 보안 관련 기능이 동작한다" 라는걸 클라이언트는 알면 안됨

약속4. 서버는 캐싱 처리 응답을 할 수 있어야하고 클라이언트는 캐싱 처리를 할 수 있어야함

  웹 사이트 로고 이미지가 있을 때 클라이언트가 로고 이미지를 매번 다운 받아 보여주면 비효율적임

  서버가 캐시 가능으로 응답했다면 클라이언트는 로고 이미지를 최초에 한번만 다운 받아 저장해두고 다음부터는 캐시에 저장된 로고 이미지 파일을 보여줘야함

  보통 서버는 응답 헤더에 Last-Modified 태그나 E-Tag 에 캐시 가능, 불가능을 담아 보냄 


RESTfulAPI란?

REST 기반의 API를 RESTfulAPI라고 함

REST, REST API, RESTfulAPI 모두 같은 말이므로 혼용해서 사용해도 됨

 

RESTfulAPI는 요청 방법(Method)와 URI, 상태코드를 적극 활용해야함

 

<< 요청 방법 >>

요청 방법에는 GET, POST, PUT, DELETE이 있음

- GET : 클라이언트가 리소스를 요청할 때 사용하는 방식

- POST : 클라이언트가 리소스를 생성할 때 사용하는 방식

- PUT : 클라이언트가 리소스를 수정할 때 사용하는 방식

- DELETE : 클라이언트가 리소스를 삭제할 때 사용하는 방식

예를 들어 클라이언트가 이미지를 요청 할 때 GET 방식으로 요청함 또는 회원 정보를 조회할 때 GET 방식으로 요청함

클라이언트가 이미지를 업로드 할 때 POST 방식으로 요청함 또는 회원 가입을 할 때 POST 방식으로 요청함

클라이언트가 업로드한 이미지를 수정할 때 PUT 방식으로 요청함 또는 회원 정보를 수정할 때 PUT 방식으로 요청함

클라이언트가 업로드한 이미지를 삭제할 때 DELETE 방식으로 요청함 또는 회원 탈퇴를 할 때 DELETE 방식으로 요청함

 

 

<< URI >>

URI는 Uniform Resource Identifier의 약자로 리소스를 식별할 수 있는 식별자를 뜻함

식별자란 말이 낯선데 Identifier 가 우리말로 식별자이고 Identifier 의 앞 두 자 id (아이디) 는 익숙할 것

어떤 서비스에 가입할 때 항상 id (아이디) 를 입력하는데 id가 Identifier에서 나온 것

또 현실 세계의 우리도 식별자를 갖고 있음

주민등록번호 또는 연락처

주민등록번호 또는 연락처로 사람 한 명 한 명을 식별할 수 있음

 

여기서 URL과 URI 두 가지가 있고 이 두 가지를 헷갈려하는 분들이 많음

URL은 Uniform Resource Locator의 약자로 리소스의 위치를 뜻함

 

가령 우리가 만든 서버 컴퓨터의 IP 주소가 192.168.0.1 이고 서버 컴퓨터에는 톰캣이 8080번 포트로 열려있음

또한 톰캣에는 studyproject가 위치해있는 상황이라고 하자

studyproject로 접근하기 위한 경로(URL)은 http://192.168.0.1:8080/studyproject 임

studyproject 내 chapter04 폴더 안에 image1.png 이미지가 있다면

  - 이 이미지의 URL은 http://192.168.0.1:8080/studyproject/chapter04/image1.png 임

  - 이 이미지의 URI는 /chapter04/image1 임

여기서 주의할 점 URI는 URL에 포함되어있음

따라서 http://192.168.0.1:8080/studyproject/chapter04/image1.png 는 URL도 되고 URI도 됨

그러나 /chapter04/image1 은 URL은 될 수 없고 URI는 될 수 있음

 

URI는 일반적으로 파일의 확장자는 표시하지 않음

따라서 헷갈린다면 http로 시작하거나 경로의 가장 마지막에 파일의 확장자가 있다면 URL

http로 시작하지 않거나 경로의 마지막에 확장자가 없다면 URI 라고 생각하면 됨

 

예전에는 면접에서 URL, URI 를 물어보는 곳이 많았지만 요즘에는 더 필요하고 중요한 기술들이 많아졌기에 물어보는 곳은 드물 것

알고 있으면 개발에 도움되는 상식 정도임

 

 

<< 상태 코드 >>

상태 코드는 영어로 Status Code 이고 가끔 응답 코드 (Response Code) 라고도 부름

상태 코드는 서버가 클라이언트에게 하는 응답이 어떤 상태인지를 나타냄

이를 활용하면 최소한의 데이터로 서버가 어떤 응답을 하는지 클라이언트에게 전달 할 수 있음

 

상태 코드는 100, 200, 300, 400, 500번대가 있으며 가장 많이 사용하는 상태 코드는 200, 400, 500번대임

상태 코드는 쉬우면서 이미 인터넷에 잘 나와있기에 200, 400, 500번대 상태 코드 중에서도 빈도가 높은 것만 알아보자

 

- 200번대 상태 코드

  > 200 : 요청을 정상적으로 처리하였음

  > 201 : POST 요청을 정상적으로 처리하였음

  > 204 : 요청을 처리했지만 전달할 데이터가 없음

 

- 400번대 상태 코드

  > 400 : 클라이언트측의 문제로 요청을 처리하지 못했음

  주로 API에 필요한 파라미터를 전달하지 않았을 때 발생 시키는 상태코드

  > 401 : 인증(로그인 등)이 필요한 기능인데 인증을 하지 않고 접근했음

  > 403 : 인증(로그인 등)은 했지만 접근 권한이 없는 사용자임

  > 404 : URL을 잘못 입력해 해당 위치에 리소스가 없음

  > 405 : 해당 요청 방법(Method)은 API가 받을 수 없음

 

- 500번대 상태 코드

  > 500. 502, 503 : 서버측의 문제로 요청을 처리하지 못했음


요즘은 RESTfulAPI 보다 DDD, MSA, GraphQL로 많이 전환되는 추세임

그래도 RESTfulAPI는 없어질 수 없고 지금도 그리고 앞으로도 굉장히 중요한 개발 방식 중 하나이므로 잘 알아둬야함

 

RESTfulAPI에 대해서 끝장을 보고 싶다 라면 아래 책을 보면 끝장을 볼 수 있을 것

 

HTTP 완벽 가이드 - YES24

웹 세상을 떠받치고 있는 HTTP에 대한 모든 것모든 성공적인 웹 트랜잭션 뒤에는, 웹 클라이언트와 서버가 문서와 정보를 교환하는 언어인 HTTP가 있다. HTTP는, 회사 인트라넷에 접근하거나 절판된

www.yes24.com

 

참고 : https://aws.amazon.com/ko/what-is/restful-api/, https://khj93.tistory.com/entry/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-REST-API%EB%9E%80-REST-RESTful%EC%9D%B4%EB%9E%80, https://gmlwjd9405.github.io/2018/09/21/rest-and-restful.htmlhttps://meetup.nhncloud.com/posts/92

728x90
LIST

<< 학습 목표 >>

1. InitBinder 를 사용해 Validator 를 생성할 수 있다.

2. InitBinder 가 생성한 Validator 로 커맨드 객체를 검증할 수 있다.

3. 애너테이션을 사용해 커맨드 객체 검증 코드를 최대한 줄일 수 있다.


앞서 우리는 Validator 를 컨트롤러 안에서 직접 생성했음

 

type1 컨트롤러는 커맨드 객체를 검증하기 위해 Validator 가 필요한데 이를 "type1 컨트롤러는 Validator에 의존(Dependency)한다" 라고 표현함

type1 컨트롤러와 Validator 사이에서 Validator 는 의존 객체라고 부름

 

A가 B에 의존할 때 A가 B를 직접 생성하면 이를 강한 결합이라 부르고 이는 좋지 못한 코드가 됨

왜 그런지는 Chapter03 에서 배울 것

A와 B 사이에서 B는 의존 객체라고 부름

 

A가 B에 의존할 때 A 안에서 B를 직접 생성하는 방식이 아닌 다른 객체의 도움으로 A가 B를 생성하도록 해야하는데 이를 Spring Framework가 담당함

 

type1 컨트롤러가 Validator에 의존하고 있는데 type1 컨트롤러 안에서 Validator를 직접 생성하는게 아니라 Spring Framework의 도움으로 type1 컨트롤러가 Validator 를 생성하도록 해야함

이렇게  A가 누군가의 도움으로 B(의존 객체)를 생성하면 이를 약한 결합이라 부르고 이는 좋은 코드가 됨

왜 그런지는 Chapter03에서 배울 것


컨트롤러가 Spring Framework의 도움으로 Validator를 생성하려면 Spring Framework가 제공하는 initBinder를 사용해야함

따라서 구체적으로 말하면 컨트롤라가 initBinder의 도움으로 Validator를 생성하도록 해보자

 

컨트롤러가 initBinder의 도움을 받으려면 pom.xml에 spring-boot-starter-validation 라이브러리를 추가해야함

spring-boot-starter-validation 라이브러리의 URL을 알려면 역시나 Maven Repository에서 라이브러리 명으로 검색하면 됨

 

Maven Repository 에서 다음과 같이 spring-boot-starter-validation 라이브러리를 검색(1) -> 첫 번째 검색 결과(2)로 이동 -> 가장 최상위 버전(3) 을 선택하자

그 후 라이브러리 URL을 복사 한 뒤 프로젝트 내 pom.xml 의  닫는 dependency ( </dependency> ) 의 바로 위에 붙여 넣자

그 후 version 태그는 삭제하자

 

spring-boot-starter 로 시작하는 라이브러리의 경우 version 태그가 이미 프로젝트 설정에 지정되어있으므로 우리가 지정하지 않는게 좋음

우리가 지정하면 프로젝트에 설정된 version 태그와 우리가 지정한 version 태그가 충돌이 발생해 문제가 생길 수 있음


initBinder 를 사용하기 위해 라이브러리를 추가했다면 이제 컨트롤러가 initBinder의 도움을 받아 Validator(의존 객체)를 생성하도록 만들어보자

 

ValidationController 클래스에 다음과 같이 type2 컨트롤러와 initBinder 메서드를 추가하자

package com.study.chapter02;

import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Validator;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.PostMapping;

import jakarta.validation.Valid;

@Controller
public class ValidationController {
	@PostMapping("/chapter02/validation/type2")
	public void type2(@Valid Member member, BindingResult errors) {
		if(errors.hasErrors()) {
			// 커맨드 객체 검증에 실패했을 때
			// 필요한 처리
		}
		
		if(errors.hasFieldErrors("id")) {
			// 커맨드 객체의 특정 값의 검증에 실패했을 때
			// 필요한 처리
		}
		
		// ...
	}
	
	@InitBinder
	protected void initBinder(WebDataBinder binder) {
		binder.setValidator(new MemberValidator());
	}
	
	@PostMapping("/chapter02/validation/type1")
	public void type1(Member member, BindingResult errors) {
		Validator validator = new MemberValidator();
		
		validator.validate(member, errors);
		
		if(errors.hasErrors()) {
			// 커맨드 객체 검증에 실패했을 때
			// 필요한 처리
		}
		
		if(errors.hasFieldErrors("id")) {
			// 커맨드 객체의 특정 값의 검증에 실패했을 때
			// 필요한 처리
		}
		
		// ...
	}
}

<< 코드 설명 >>

 

initBinder의 도움 없이 의존 객체를 직접 생성하는 강한 결합(왼쪽)과 initBinder의 도움으로 의존 객체를 생성하는 약한 결합(오른쪽)을 비교해보자

(1). 의존 객체를 직접 생성함

(2). 의존 객체를 직접 생성했으므로 커맨드 객체를 검증하기 위해 validate 메서드를 직접 호출함

(3). 의존 객체를 initBinder가 대신 생성함

  이때 initBinder 메서드 안에 binder.setValidator 메서드의 인자를 보면 type2 컨트롤러에서 필요한 Validator를 생성해 인자로 넣었음

  우리 눈에는 안보이지만 initBinder를 통해 의존 객체를 생성 한 후 type2 컨트롤러에게 전달한 것

(4). 컨트롤러가 의존 객체를 직접 생성하지 않았으므로 커맨드 객체 검증을 할 때도 우리가 직접 하는게 아님

  검증 하고 싶은 커맨드 객체에 @Valid 애너테이션을 달아주면 MemberValidator에 선언한 validate메서드가 적절하게 알아서 호출됨

 

Spring Framework의 장점은 애너테이션을 통해 개발자가 입력해야되는 코드의 양을 상당 부분 줄일 수 있다는 것

그 대신 각 애너테이션이 어떤 역할인지 애너테이션 간에 서로 어떤 관계가 있는지를 확실히 알아야함

initBinder의 도움을 받는것도 마찬가지

왼쪽에 있는 type1 컨트롤러처럼 initBinder의 도움 없이 커맨드 객체를 검증할 수 있지만 좋지 못한 코드가 될 가능성이 높기 때문에 오른쪽에 있는 type2 컨트롤러처럼 initBinder의 도움으로 커맨드 객체를 검증하는 것

type2 컨트롤러처럼 의존 객체와 약하게 결합 되어있다면 좋은 코드가 될 가능성이 높음

그 대신 @InitBinder 애너테이션과 @Valid 애너테이션에 대해서 확실히 알고 있어야함


전 글 ( https://codingaja.tistory.com/109 ) 부터 여기까지 배운 커맨드 객체를 검증하는 과정을 순서대로 나열해보자

1. 검증하고 싶은 커맨드 객체를 검증할 Validator 클래스를 생성함

  이때 Validator 클래스의 이름은 보통 (커맨드객체명)Validator 로 지어줌

  또한 Validator 클래스는 반드시 Validator 인터페이스를 구현해야함

2. Validator 인터페이스를 구현할 때는 supports, validate 메서드를 오버라이딩 해야함

  supports 메서드는 이 Validator가 어떤 커맨드 객체를 검증할 지 Spring Framework에게 알려주는 메서드

  단순히 이름이 (커맨드객체명)Validator 라고 해서 Spring Framework에서 어떤 커맨드 객체를 검증할 Validator 인지 알 수 없음

  validate 메서드는 이 Validator가 커맨드 객체를 검증하는 메서드로 이 메서드에 검증 하는 코드를 구현하면 됨

3. Validator 클래스가 준비됐다면 컨트롤러에서 Validator를 사용해 커맨드 객체를 검증해야하는데 이때 InitBinder의 도움으로 컨트롤러가 Validator 를 생성하도록 해야함

4. 컨트롤러가 갖고 있는 커맨드 객체에 @Valid 애너테이션을 붙여서 Validator로 커맨드 객체를 검증하도록 함


이제 프로젝트를 실행시키고 postman 으로 type2 컨트롤러에게 다양한 값을 보내 커맨드 객체가 제대로 검증 되는지 확인해보자


이제 마지막으로 커맨드 객체 검증 코드를 더 더 줄여보자

이 방법은 애너테이션을 사용해 Validator의 코드를 더 줄이는 방법으로 만약 지금까지 커맨드 객체 검증 방법이 헤깔리거나 어렵다고 느껴진다면 이 아랫부분은 안보고 여기서 마무리 지어도 됨

 

@Valid 애너테이션이 붙은 커맨드 객체의 멤버 변수에 @NotNull, @NotEmpty, @Size 등과 같은 애너테이션을 사용할 수 있고 이 애너테이션들을 사용하면 커맨드 객체를 검증할 Validator의 코드가 더 줄어듬

 

프로젝트 -> com.study.chapter02 -> Member 클래스 내 다음과 같이 애너테이션들을 추가하자

package com.study.chapter02;

import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;

@Data
public class Member {
	@NotNull(message="empty")
	@NotEmpty(message="empty")
	@Size(min=3, max=20, message="too short or long")
	private String id;
	
	@NotNull(message="empty")
	@NotEmpty(message="empty")
	@Size(min=6, max=20, message="too short or long")
	private String pw;
	
	// 커맨드 객체를 검증하기 위한 애너테이션들
	private String nickname;
	
	// 커맨드 객체를 검증하기 위한 애너테이션들
	private String[] hobby;
}

그리고 프로젝트 -> com.study.chapter02 -> MemberValidator 클래스 내 코드를 다음과 같이 대폭 줄이자

package com.study.chapter02;

import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

public class MemberValidator implements Validator {
	@Override
	public boolean supports(Class<?> clazz) {
		return Member.class.isAssignableFrom(clazz);
	}

	@Override
	public void validate(Object target, Errors errors) {
		Member member = (Member) target;
		
		// 커맨드 객체 검증 코드1
		// 커맨드 객체 검증 코드2
		// 커맨드 객체 검증 코드3
		// ...
		// 커맨드 객체 검증 코드n
	}
}

<< 코드 설명 >>

먼저 MemberValidator 클래스부터 설명하면 MemberValidator 클래스 내 validate 메서드의 코드가 싹 사라졌음

이는 이를 대신할 애너테이션을 Member 클래스에 사용했기 때문임

 

MemberValidator와 Member 를 서로 비교해보자

(1), (2). 커맨드 객체의 멤버 변수가 null이거나 빈 문자열일 경우 커맨드 객체 검증 실패로 처리했는데 이 역할을 @NotNull, @NotEmpty 애너테이션이 대신하도록 한 것

  @NotNull은 애너테이션명 그대로 해당 멤버 변수의 값이 null 이라면 커맨드 객체 검증에 실패하고 사유로 "empty"를 남김

  @NotEmpty는 해당 멤버 변수의 값이 빈문자열("") 이거나 공백문자만 들어있는 문자열이라면 커맨드 객체 검증에 실패하고 실패 사유로 "empty"를 남김

  검증 실패 사유를 바꾸고 싶다면 message 속성의 값을 바꾸면 됨

 

 

(1), (2). 커맨드 객체의 멤버 변수 값이 너무 짧거나 너무 길다면 커맨드 객체 검증 실패로 처리했는데 이 역할을 @Size 애너테이션이 대신하도록 한 것

  @Size는 애너테이션명 그대로 해당 멤버 변수의 값이 일정 길이 미만, 초과라면 커맨드 객체 검증에 실패하고 사유로 "too short or long" 을 남기는 것

  해당 멤버 변수 값의 길이가 min, max를 포함해 min부터 max 사이라면 커맨드 객체 검증에 성공하고 min 미만 또는 max 를 초과했다면 커맨드 객체 검증에 실패함

 

이렇게 @Valid 애너테이션이 붙은 커맨드 객체는 애너테이션을 추가적으로 더 사용해 개발자가 입력해야할 코드를 많이 줄일 수 있음

 

여기서 한가지 단점은 @Valid 애너테이션을 사용했고 커맨드 객체에서 @NotNull, @NotEmpty 등의 애너테이션을 사용해 커맨드 객체를 검증한다면 initBinder가 동작하지 않도록 주석 처리해야함

 

커맨드 객체 안에서 애너테이션을 사용해서 커맨드 객체를 검증할 것이냐 initBinder를 통해서 생성한 Validator로 검증을 할 것이냐 선택해야한다는 것

나중에 포트폴리오용 프로젝트 등을 만들면 이는 큰 걸림돌이 될텐데 Spring-Boot 에서는 이정도만 배우자

Spring Framwork 포스팅을 올릴 때가 온다면 거기서 커맨드 객체 검증(Validation)에 대해서 자세히 올릴 예정임

 

커맨드 객체를 검증할 때 initBinder 를 통해서 Validator 를 생성하고 검증하는 방법과 커맨드 객체 안에서 애너테이션으로 검증하는 방법이 있으니 적절히 필요한 것들을 골라서 사용하면 됨

728x90
LIST

<< 학습 목표 >>

1. Validator 인터페이스를 구현해 커맨드 객체를 검증할 수 있다.

2. ValidationUtils 클래스의 메서드를 사용해 커맨드 객체를 검증할 수 있다.


클라이언트가 보낸 값을 사용하기 전에 반드시 검증해야함

검증은 다음과 같은 것을 해야함

1. 서버에 반드시 필요한 값이라면 클라이언트가 그 값을 보냈는지 확인

2. 클라이언트가 보낸 값이 서버에서 활용할 수 있는 형태인지 확인

 

검증하지 않고 사용하면 컨트롤러에 오류가 발생할 수도 있고 잘못된 데이터가 저장될 수 있음

 

studyProject 프로젝트 -> com.study.chapter02 -> ValidationController 클래스를 추가하고 아래 코드를 추가하자

package com.study.chapter02;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class ValidationController {
	@PostMapping("/chapter02/validation/type1")
	public void type1(Member member) {
		System.out.println(member);
	}
}

일단 type1 컨트롤러는 별도의 커맨드 객체 검증을 하지 않고 있음

롬복을 사용한 커맨드 객체를 테스트한다는 생각으로 프로젝트를 실행시켜 값을 보내보자

 

여기서 이제 프로젝트가 2개가 됐음

이때는 내가 실행시키고 싶은 프로젝트를 정확히 확인한 후 실행시켜야함

[ Boot Dashboard ] (1)에서 studyProject 를 실행시키자


앞서 클라이언트가 파라미터를 반드시 전달해야하면 @RequestParam 애너테이션에 required 속성을 true로 설정하면 된다고 했는데 커맨드 객체를 사용했을 때는 @RequestParam 애너테이션을 붙일 수 없음

 

그럼 DTO 안에 @RequestParam 애너테이션을 붙일 수 있을까? 없음

@RequestParam 애너테이션은 컨트롤러의 매개변수에만 붙일 수 있음

그래서 DTO를 적용했다면 클라이언트가 파라미터를 보내지 않을 수 있음

그래서~! 여기서 커맨드 객체 검증을 배우는 것


서블릿 컨트롤러의 방식대로 직접 커맨드 객체를 검증해도 되지만 굉장히 비효율적임


Spring Framework는 커맨드 객체를 검증하는대 사용할 Validator 인터페이스를 제공하고 있음

Spring Framework 프로젝트는 이 인터페이스를 사용해 커맨드 객체를 검증할 Validator 클래스를 만들어야함

 

이제 커맨드 객체를 검증할 MemberValidator 를 만들자

studyProject 프로젝트 -> com.study.chapter02 -> MemberValidator 클래스를 추가하고 아래 코드를 추가하자

package com.study.chapter02;

import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

public class MemberValidator implements Validator {
	@Override
	public boolean supports(Class<?> clazz) {
		return Member.class.isAssignableFrom(clazz);
	}

	@Override
	public void validate(Object target, Errors errors) {
		Member member = (Member) target;
		
		String id = member.getId();
		String pw = member.getPw();
		
		if(id == null || id.trim().isEmpty()) {
			errors.rejectValue("id", "empty");
		} else if(id.length() < 3 || id.length() > 20) {
			errors.rejectValue("id", "too short or long");
		} 
		
		if(pw == null || pw.trim().isEmpty()) {
			errors.rejectValue("pw", "empty");
		} else if(pw.length() < 6 || pw.length() > 20) { 
			errors.rejectValue("pw", "too short or long");
		}
	}
}

<< 코드 설명 (1) >>

위 코드와 설명하기 위해 캡쳐한 코드가 살짝 다른데 위 코드에 더해서 더 다양하게 커맨드 객체를 검증할 수 있음을 표현하기 위함

(1). 커맨드 객체를 검증할 Validator 클래스는 반드시 Validator 인터페이스를 구현해야함

 

(2). 커맨드 객체가 어떤 커맨드 객체를 검증할 수 있는지 명시하는 부분

return (커맨드객체명).class.isAssignableForm(claszz)

으로 형태가 정해져있음

이 커맨드 객체(MemberValidator)는 Member 커맨드 객체만 검증할 수 있는 Validator임

 

(3). 커맨드 객체를 검증하는 메서드

이 메서드에는 매개변수가 2개 있음

첫번째 매개변수 - 검증할 커맨드 객체를 전달 받을 매개변수

두번째 매개변수 - 커맨드 객체 검증에 실패했을 때 어떤 사유로 실패했는지 실패 사유를 적어두는 매개변수

 

<< 코드 설명 (2) >>

(1). 검증하기 위해 첫번째 매개변수로 전달 받은 커맨드 객체를 원래의 형태로 형변환

 

(2). 커맨드 객체 내 검증할 값들을 꺼냄

 

(3), (4). 커맨드 객체의 특정 값을 확인하는 부분으로 이와 같이 if문으로 서버에 필요한 값이 있는지 없는지 확인함

 

(3). 커맨드 객체의 특정 값이 없거나 비어있으면 errors에 이름은 id, 사유는 empty 로  "커맨드 객체 검증 실패" 로 기록

 

(4). 커맨드 객체의 특정 값이 너무 짧거나 길다면 errors에 이름은 pw, 사유는 too short or long 으로 "커맨드 객체 검증 실패"로 기록

 

그 이후에 있는 else if ( ... ) { } 은 커맨드 객체 검증에 필요한 코드를 더 쓰면 된다는 의도로 붙여둠

 

 

 

 

 

여기서 검증 성공과 검증 실패란 말이 나오는데 검증 성공이라 함은 커맨드 객체가 서버에 필요한 모든 값을 다 갖고 있다는 것

검증 실패라 함은 커맨드 객체 내 특정 값 하나라도 서버에 필요한 값을 갖고 있지 않을 경우임


Member 커맨드 객체를 검증할 MemberValidator 클래스를 만들었으니 이를 사용해 컨트롤러에서 커맨드 객체를 검증해보자

package com.study.chapter02;

import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Validator;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class ValidationController {
	@PostMapping("/chapter02/validation/type1")
	public void type1(Member member, BindingResult errors) {
		Validator validator = new MemberValidator();
		
		validator.validate(member, errors);
		
		if(errors.hasErrors()) {
			// 커맨드 객체 검증에 실패했을 때
			// 필요한 처리
		}
		
		if(errors.hasFieldErrors("id")) {
			// 커맨드 객체의 특정 값의 검증에 실패했을 때
			// 필요한 처리
		}
		
		// ...
	}
}

<< 코드 설명 >>

(1). 커맨드 객체를 검증하면서 검증에 실패했을 때 사유를 기록할 매개변수를 컨트롤러의 두 번째 매개변수로 선언해야함

그래야 컨트롤러가 동작하면서 사유를 기록할 객체가 만들어짐

(2). 커맨드 객체를 검증하기 위한 Validator 생성

(3). 커맨드 객체(member) 검증

(4), (5). 검증 결과를 확인 하고 검증에 실패했다면 필요한 처리를 함

 

프로젝트를 실행시키기 전에 커맨드 객체를 검증하는 방법을 다시 한번 자세히 알아보자


(1). 컨트롤러에 errors 매개변수를 선언했는데 이 매개변수는 커맨드 객체 검증 결과를 저장할 객체임

 

errors 매개변수는 따로 우리가 생성해야하는게 아님

클라이언트가 http://localhost:8080/chapter02/validation/type1 경로로 접근하면 이 컨트롤러(type1) 가 호출됨

 

이 컨트롤러가 호출되면 Spring Framework는 클라이언트가 보낸 파라미터는 첫 번째 매개변수인 member 에 저장됨

그 다음 Spring Framework가 알아서 errors 매개변수를 만들어줌

 

(2). errors 객체를 Validator 로 넘겨서 Validator 에서 커맨드 객체 검증에 실패했을 경우 errors에 실패 사유를 기록하는 것

 

커맨드 객체를 검증하기 위해서 validate 메서드를 호출하는데 validate 메서드로 검증 할 커맨드 객체를 첫번째 매개변수로, 검증 결과를 기록할 객체를 두번째 매개변수로 전달하고 있음

 

의외로 많은 사람들이 잘 모르는데 메서드를 호출했을 때 메서드의 끝을 만나면 다시 호출했던 제자리로 돌아와(2) 밑으로 내려감

 

 

이번에는 validate 메서드 내 코드를 다시 살펴보자

(1). 검증할 커맨드 객체를 원래의 형태로 형변환

(2). 커맨드 객체가 가지고 있는 검증할 값들을 꺼냄

(3). 검증할 값이 서버에 필요한 값을 가지고 있지 않을 경우

(4). errors 객체에 이름은 id로 사유는 empty 로 기록

  이 코드가 동작한다면 컨트롤러가 전달해준 errors 에 이와 같이 기록됨

 

당연하지만 값이 비어있지는 않았지만 길이가 짧거나 길이가 너무 길어 else if 가 동작했다면 다음과 같이 errors에 이름은 id로 사유는 too short or long 으로 기록됨

 

역시나 당연하지만 값이 비어있지 않았고 길이가 적절했다면 if, else if 모두 건너 뛰고 errors 에는 아무것도 기록되지 않음

값이 비어있어 id, empty 가 기록되어있는 상태라고 하자

 

아직 validate 메서드의 끝을 만나지 않았으므로 여기서 끝이 아니라 그 다음에 있는 다른 값을 검증하는 if문이 더 동작함

 

그 다음 if문이 동작해 비밀번호가 비어있지는 않았지만 짧거나 길긴 상황이라 else if 가 동작해 errors 에 다음과 같이 기록된 상황이라고 하자

 

 

이제 메서드의 끝을 만났으므로 제 자리(컨트롤러)로 돌아감

 

그 후 밑으로 내려가 if문들(1, 2)이 동작해 커맨드 객체 검증이 성공했는지 실패했는지 여부를 판단함

(1). hasErrors 메서드는 errors에 사유가 하나라도 기록되어있다면 true를 반환하고 사유가 아무것도 없다면 false를 반환함

(2). hasFieldErrors 메서드는 인자로 넣은 문자열로 기록된 사유가 있다면 true를 반환하고 인자로 넣은 문자열로 기록된 사유가 없다면 false를 반환함

 

커맨드 객체가 하나라도 비정상적인 값을 갖고 있다면 으로 커맨드 객체를 검증하고 싶다면 hasErrors(1) 메서드를 사용하고 커맨드 객체가 갖고 있는 값 중 일부가 비정상적인 값을 갖고 있다면 으로 커맨드 객체를 검증하고 싶다면 hasFieldErrors(2) 메서드를 사용하면 됨


 

커맨드 객체 검증 시 특정 값이 비어있는지 여부는 거의 모든 검증에 사용됨

특정 값이 비어있는지 여부를 체크하는 걸 직접 해도 되지만 Spring Framework 에서는 ValidationUtils 클래스로 제공함

 

MemberValidator 클래스를 아래와 같이 바꾸자

package com.study.chapter02;

import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

public class MemberValidator implements Validator {
	@Override
	public boolean supports(Class<?> clazz) {
		return Member.class.isAssignableFrom(clazz);
	}

	@Override
	public void validate(Object target, Errors errors) {
		Member member = (Member) target;
		
		ValidationUtils.rejectIfEmptyOrWhitespace(errors, "id", "empty");
		ValidationUtils.rejectIfEmptyOrWhitespace(errors, "pw", "empty");
		
		String id = member.getId();
		String pw = member.getPw();
		
		if(id.length() < 3 || id.length() > 20) {
			errors.rejectValue("id", "too short or long");
		} 
		
		if(pw.length() < 6 || pw.length() > 20) { 
			errors.rejectValue("pw", "too short or long");
		}
	}
}

<< 코드 설명 >>

코드가 살짝 바뀌었는데 이전 코드와 비교해보자

해당 부분을 ValidationUtils 클래스의 rejectIfEmptyOrWhitespace 메서드로 대체한 것

 

 

rejectIfEmptyOrWhitespace 메서드의 첫 번째 매개변수는 검증 결과를 저장할 errors, 두 번째 매개변수는 검증할 멤버 변수 이름, 세 번째 매개변수는 검증에 실패했을 때 실패 사유임

 

아마 여기서 의문이 드는 분도 있을 것

그렇다면 정확히 본 것

rejectIfEmptyOrWhitespace 메서드로 커맨드 객체를 전달해주지 않았는데 어떻게 커맨드 객체를 검증한다는걸까?

MemberValidator 클래스에 supports 메서드를 오버라이딩 했는데 이 메서드의 역할은 이 Validator 클래스가 어떤 커맨드 객체를 검증할 Validator인지 지정하는 것이라 했음

rejectIfEmptyOrWhitespace 메서드로 커맨드 객체를 전달해주지 않아도 supports 덕분에 어떤 커맨드 객체를 검증해야하는지 알 수 있는 것

그래서 커맨드 객체를 메서드로 넘겨주지 않아도 커맨드 객체를 검증할 수 있음


여기까지 실제 코드가 길진 않았지만 설명이 길었던 커맨드 객체 검증하기를 알아봤음

커맨드 객체 검증이 여기가 끝은 아니고 더 있으니 다음 글을 통해 커맨드 객체 검증을 더 깊게 들어가보자

728x90
LIST

<< 학습 목표 >>

1. 롬복(Lombok) 라이브러리를 설치할 수 있다.

2. 프로젝트를 만들 때 롬복(Lombok) 라이브러리를 사용하도록 설정할 수 있다.

3. 커맨드 객체에 롬복(Lombok) 라이브러리를 사용할 수 있다.


아래와 같은 클래스를 자바에서는 클래스라 부르고 MVC 디자인 패턴에서는 M 또는 DTO라 부르는데 Spring Framework 에서는 커맨드 객체(Command Object) 라고 부름

package com.example.demo.chapter02;

import java.util.List;

import org.springframework.web.multipart.MultipartFile;

public class Member {
	String id;
	String pw;
	String nickname;
	List<String> hobby;
	MultipartFile file;
	
	// getter & setter 생략
}

 

이 클래스 또는 DTO 또는 커맨드 객체라 부르는 것을 컨트롤러의 매개변수에 사용해 클라이언트가 보낸 값을 저장하는 용도로 사용했음

package com.example.demo.chapter02;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.multipart.MultipartFile;

@Controller
public class TestController05 {
	@PostMapping("/chapter02/files/type4")
	public void getUserProfile(Member member) {
		// ...
		// ...
		// ...
	}
    
    // ...
    // ...
    // ...
}

 

앞으로 클라이언트가 보내는 값을 꺼내는 용도의 클래스를 커맨드 객체라 칭하겠음

DTO는 조금 더 배우면 나옴


DTO와 커맨드 객체를 만들 때 제일 번거로운 부분이 getter, setter 를 추가하는 부분

그 외에도 equals, toString, hashcode 등도 오버라이딩을 해야하는 경우가 있어 DTO와 커맨드 객체를 만들 때 할 일이 더 많아 짐

또한 프로젝트를 개발할 때 DTO와 커맨드 객체가 적게는 수 십개, 많게는 백여개 그 이상으로 늘어남

DTO와 커맨드 객체를 하나 만들 때 해야 할 일도 많은데 수 십개에서 백여개 이상을 만들 때는 더 번거로워짐

이렇게 번거로운 부분을 도와주는 라이브러리가 롬복(Lombok) 라이브러리임

 

롬복(Lombok) 라이브러리는 DTO와 커맨드 객체의 getter, setter, equals, toString, hashcode 등의 메서드를 자동으로 생성해주고 오버라이딩 해줌

 

롬복을 사용하려면 우선 이클립스에 롬복을 설치해야함

롬복을 설치해보자

인터넷에 lombok 으로 검색(1) 후 첫 번째 검색 결과 페이지(2)로 들어가자

만약 첫 번째 검색 결과가 다르다면 직접 들어가자 / https://projectlombok.org/

 

 

다운로드 메뉴(1)로 들어가 다운로드 받자(2)

다운로드 받을 때 아래와 같이 경고창(3) 이 뜰 수 있는데 그대로 다운로드 받으면 됨

 

다운로드 받은 파일을 보면 jar 파일임

우리가 sts 압축을 풀었을 때와 마찬가지로 cmd 창에서 java 명령을 사용해 압축을 풀어야함

cmd 창(1)을 열고 cd 명령으로 다운로드 폴더로 이동(2)한 후 java 명령(3)을 사용해 압축을 풀자

 

롬복의 압축을 풀면 다음과 같이 롬복 설치창(1)이 열림

그리고 롬복을 설치할 개발툴을 찾지 못했다 메세지(2) 가 보일 것

 

[ OK ] 를 누르고 [ Specify location... ] (1) 을 누르고 사용중인 sts 가 설치된 폴더(2)를 선택(3)하면됨

그 후 [ Install / Update ] (4) 버튼을 누르면 사용중인 sts에 롬복이 설치됨

 

설치가 다 됐다면 설치 프로그램을 종료(1) 하고 sts를 다시 실행시키자

이제 sts에서 롬복을 사용해 DTO와 커맨드 객체를 쉽게 만들 수 있음


이제 프로젝트에서 롬복을 사용하려면 pom.xml에 롬복 라이브러리 URL을 추가하고 사용하면 됨

그치만 우리는 프로젝트를 새로 만들자

 

sts 에서 [ Ctrl + N ] 을 누른 후(1) [ spring ] 으로 검색(2) 해 [ Spring Starter Project ] 를 만들자

 

맨 처음 Spring Framework 프로젝트를 만들었을 때 처럼 Type은 Maven (1) 으로 Packaging은 War (2) 로 설정하자

Java Version은 바꾸지 않아도 됨

왜 바꾸지 않아도 되는지는 설명했으므로 혹시 잊어버렸다면 전 글 ( ) 을 참고하자

 

이번에는 프로젝트명과 기본 패키지도 수정하자


이와 같이 lombok 을 사용할 수 있는 Spring Framework 프로젝트를 생성했음

 

이 프로젝트에 com.sutdy.chatper02 패키지를 추가하고 해당 패키지 내 Member 클래스를 추가하자

그 후 아래 코드를 입력하자

package com.study.chapter02;

import lombok.Data;

@Data
public class Member {
	private String id;
	private String pw;
	private String nickname;
	private String[] hobby;
}

 

getter, setter 메서드가 없고 대신 Member 클래스에 @Data 애너테이션이 붙었음

@Data 애너테이션은 롬복 덕분에 사용할 수 있는 애너테이션이며 클래스에 이 애너테이션을 붙이면 해당 클래스가 갖고 있는 멤버 변수에 맞게 getter, setter 를 자동으로 추가해줌

 

getter, setter가 자동으로 추가됐다는건 [ Outline 뷰 ] (3) 를 통해서 볼 수 있음

<< Outline 뷰가 보이지 않는다면 >>

더보기

sts 상단 [ WIndow ] (1) -> [ Show View ] (2) -> [ Other ] (3)

 

outline 검색 (1) -> [ Open ] (2)


 

이 외에도 롬복을 사용하면 더 많은 것들을 할 수 있지만 우리는 여기까지만 보자

728x90
LIST

<< 학습 목표 >>

1. 클라이언트가 보낸 파일을 꺼낼 수 있다.

2. 클라이언트가 보낸 파일들을 꺼낼 수 있다.


지금까지는 클라이언트가 데이터, 정보를 보냈을 때 이를 꺼내는 방법을 알아봤음

이번에는 클라이언트가 파일을 보냈을 때 이를 꺼내는 방법을 알아보자

 

굉장히 간단해서 짧게 끝날 예정임


클라이언트가 보낸 파일을 꺼내기 위해 컨트롤러를 추가하자

 

프로젝트 -> com.example.demo.chapter02 -> TestController05 클래스를 추가하고 아래 코드를 추가하자

package com.example.demo.chapter02;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

@Controller
public class TestController05 {

	@PostMapping("/chapter02/file")
	public void getFile(@RequestParam("file") MultipartFile file) {
		
		try {
			String originalFilename = file.getOriginalFilename();
			int lastIndex = originalFilename.lastIndexOf('.');
			
			String extension = originalFilename.substring(lastIndex);
			
			String dir = System.getProperty("user.dir") + "/src/main/resources/static/upload/";
			Path path = Paths.get(dir + UUID.randomUUID() + extension);
			
			file.transferTo(new File(path.toString()));
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

<< 코드 설명 >>

(1). 클라이언트가 보낸 파일을 받기 위한 매개변수

  @RequestParam 애너테이션 안에 있는 문자열은 클라이언트가 파일을 보낸 파라미터 이름

  클라이언트가 보낸 파일을 받기 위해서는 매개변수의 데이터 타입이 MultipartFile 이어야함

(2). 클라이언트가 보낸 파일의 이름에서 확장자명을 추출함

(3). 클라이언트가 보낸 파일을 서버에 저장하기 위해 저장 경로 지정

(4). 클라이언트가 보낸 파일을 서버에 저장하기 위해 저장 경로에 파일명 지정

  클라이언트가 보낸 파일을 서버에 저장할 때는 클라이언트가 보낸 파일명 그대로 사용하면 안됨

  사용자들(클라이언트들)이 서버로 파일을 보내므로 같은 이름의 파일들이 있을 수 있음

  같은 이름의 파일을 보낸다면 덮어씌워 저장하므로 이런 부분을 방지하고자 서버에서는 파일 이름이 같지 않게 설정해 저장해야하는데 이때 유용하게 사용할 수 있는 클래스가 UUID 클래스

  UUID 클래스는 자바에서 고유한 값을 생성할 때 사용하는 클래스로 UUID 클래스의 randomUUID 메서드를 사용하면 128 비트의 랜덤한 문자열을 생성함 이를 사용하면 파일 이름이 충돌되지 않게 저장할 수 있음

(5). 클라이언트가 보낸 파일을 갖고 있는 매개변수의 transferTo 메서드를 사용해 서버에 파일을 저장함

 

여기서 한가지 유의해야할 점은 (3) 에서 클라이언트가 보낸 파일을 서버에 저장하기 위해 저장 경로를 지정했는데 실제 개발에서 이는 좋지 못한 코드임

실제 개발에서는 저장 경로를 하드코딩하거나 설정 파일을 활용하는 등 다른 방식으로 지정함

왜 그런지 궁금하다면 아래 블로그 글을 읽어보자

 

Spring Boot에서 파일 저장을 위한 상대경로 getRealPath() 사용 금지

문제의 원인 새로 만드는 개인 프로젝트에서 이미지 파일을 저장하기 위해 프로젝트 내 Resources 폴더를 이용하려고 했다. Resources에 파일을 저장하면 빌드, 배포 시에 저장된 파일이 유실된다는

stir.tistory.com

 

클라이언트가 보낸 파일을 저장할 수 있게 프로젝트 -> src/main/resources -> static -> upload 폴더를 추가하자

 

프로젝트를 실행시킨 후 아래와 같이 file 이름에 파일을 담아 보내보자

파일을 담아 보내는 방법을 모르겠다면 https://codingaja.tistory.com/101 글 참고


전 글 ( https://codingaja.tistory.com/104 ) 에서 @RequestParam 애너테이션을 사용한 방식처럼 매개변수에 반드시 @RequestParam 애너테이션이 붙어있어야하는건 아님


클라이언트가 서버로 파일들을 보냈다면 어떻게 받아야할까?

 

우선 클라이언트가 서버로 파일들을 보내는 방법은 크게 두 가지가 있음

1. 각각의 이름에 파일들을 보내는 방법

2. 하나의 이름에 파일들을 보내는 방법

 

<< 1. 각각의 이름에 파일들을 보내는 방법 >>

클라이언트가 각각의 이름에 파일들을 보낼 때는 아래와 같이 보낼 것

Insert title here



 

클라이언트가 이와 같이 파일들을 보냈다면 서버에서는 이와 같이 매개변수를 통해서 각 파일들을 받으면 됨

 

<< 2. 하나의 이름에 파일들을 보내는 방법 >>

클라이언트가 하나의 이름에 파일들을 보낼 때는 또 다시 여러 방법으로 나뉘는데

1. HTML만을 사용해 하나의 이름에 파일들을 보내는 방법

2. JS를 사용해 하나의 이름에 파일들을 보내는 방법

이 있음

 

우선 << 2-1. HTML만을 사용해 하나의 이름에 파일들을 보내는 방법 >> 일 때 서버에서 파일들을 꺼내는 방법을 알아보자

 

클라이언트가 이와 같이 HTML만을 사용해 하나의 이름에 파일들을 보낼 수 있음

파일들을 보내므로 name을 files 로 바꿨다는 점에 주목하자

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<form action="/chapter02/files/type1" method="POST" enctype="multipart/form-data" style="border: 1px solid black">
		<input type="file" multiple="multiple" name="files" ><br>
		
		<input type="submit" value="전송">
	</form>
</body>
</html>

 

클라이언트가 위와 같이 파일들을 names 파라미터에 담아 보낸다먼 서버는 아래와 같이 배열 또는 (List나 ArrayList) 를 사용해서 꺼낼 수 있음

전 글 ( https://codingaja.tistory.com/105 ) 에서 이미 알아본 내용이므로 코드 설명은 생략

 

Postman 으로 테스트 할 때는 아래와 같이 파일들 files 이름에 담아서 보내면 됨


JS와 Jquery 를 사용하면 하나의 이름에 파일 배열을 담아 보낼 수 있음(왼쪽)

그러나 이와 같이 파일을 보내면 서버에서 받을 수 없음

따라서 하나의 이름에 파일들을 담아 보내야함(오른쪽)


마지막으로 클라이언트가 다른 데이터와 파일을 같이 보냈을 경우는 어떻게할까?

 

아래와 같은 페이지 구성을 보자

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<form action="/chapter02/files/type4" method="POST" enctype="multipart/form-data" style="border: 1px solid black">
		<label>아이디 : <input type="text" name="id"></label><br>
		<label>비밀번호 : <input type="text" name="pw"></label><br>
		<label>비밀번호 확인 : <input type="text" name="pwchk"></label><br>
		<label>닉네임 : <input type="text" name="nickname"></label><br>
		<label>프로필 이미지 : <input type="file" multiple="multiple" name="file"></label><br>
		
		<input type="submit" value="전송" onclick="upload(event)">
	</form>
</body>
</html>
Insert title here





 

이런 경우 서버에서는 프로필 이미지만 따로 받아도 되고 DTO에서 함께 받아도 됨

 

파일만 따로 받는 경우 이와 같이 파일만 따로 받을 매개변수를선언하면 됨

 

DTO에서 함께 받으려면 DTO에 파일을 받을 수 있게 MultipartFile 타입의 멤버 변수를 선언(1) 하고 컨트롤러의 매개변수는 DTO만 선언해두면 됨

이때 주의할 점은 DTO에 getter, setter 가 반드시 있어야함


여기까지 클라이언트가 보낸 파일, 파일들을 꺼내는 방법을 알아봤음

 

결국 앞에서 배운 [ 클라이언트가 보낸 데이터를 꺼내는 방법 ], [ 클라이언트가 보낸 데이터들을 꺼내는 방법 ] 과 동일하니 전 글 두 개만 잘 정리해두면 어려움 없이 클라이언트가 보낸 다양한 데이터를 꺼낼 수 있을 것

728x90
LIST

<< 학습 목표 >>

1. 클라이언트가 보낸 값들을 꺼낼 수 있다.

2. 클라이언트가 보낸 JSON 데이터를 꺼낼 수 있다.

3. 파라미터를 선택사항으로 설정할 수 있다.

4. 파라미터의 기본값을 설정할 수 있다.


전 글 ( https://codingaja.tistory.com/104 ) 에서는  클라이언트가 보낸 값을 꺼내봤음

이번에는 전 글의 연장선으로 값들을 꺼내는 방법에 대해서 알아보자


뷰(웹 페이지)에 다음과 같이 체크 박스가 있다고 상상해보자

Insert title here

당신의 취미는?


 

사용자가 체크한 값을 서버로 보내면 다음과 같이 서버로 보내짐

=> http://localhost:8080/컨트롤러URL?hobby=book&hobby=game

 

서블릿 컨트롤러에서는 요청 정보(HttpServletRequest) 에 getParameterValues 메서드로 꺼냈는데 Spring Framework는 어떻게 꺼낼 수 있을까?

 

프로젝트 -> com.example.demo.chapter02 -> TestController03 클래스를 추가하고 아래 코드를 추가하자

package com.example.demo.chapter02;

import java.util.Arrays;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class TestController03 {

	@GetMapping("/chapter02/parameters")
	public void paramters(String[] hobby) {
		System.out.println("<< parameters 메서드 호출 >>");
		System.out.println(Arrays.toString(hobby));
	}
	
}

별도의 코드 설명이 필요 없을 정도로 간단함

클라이언트가 hobby 이름에 book, game 두 개의 값을 담아서 보내므로 서버에서는 매개변수의 타입을 배열로 선언하면 됨

 

프로젝트를 재실행 시킨 후 Postman 에서 아래와 같이 파라미터를 보내보자

 

 

자바에서 컬렉션 프레임워크를 배우며 배열 보다 컬렉션 프레임워크를 사용하는게 더 좋다고 배웠음

위와 같이 클라이언트가 hobby들을 보냈을 때 배열이 아닌 List 또는 ArrayList 컬렉션 프레임워크에 hobby들을 담으려면 어떻게 해야할까?

이때는 매개변수에 @RequestParam 애너테이션이 붙어야함


이번에는 다음과 같이 폼에서 회원 정보를 보냈을 때를 상상해보자

Insert title here

회원가입

당신의 취미는?


 

서버에서는 클라이언트가 보낸 회원 정보를 다음과 같이 꺼낼 수 있을 것

( 바로 위에서 사용했던 컨트롤러에 join 메서드를 추가한 것 )

<< 컨트롤러 >>

package com.example.demo.chapter02;

import java.util.List;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class TestController03 {
	@PostMapping("/chapter02/join")
	public void join(String id, String pw, String nickname, String[] hobby) {
		// ...
		// ...
		// 회원 가입
		// ...
		// ...
	}
	
	@GetMapping("/chapter02/parameters")
	public void paramters(@RequestParam("hobby") List<String> hobby) {
		System.out.println("<< paramters 메서드 호출 >>");
		System.out.println(hobby);
	}
}

<< 코드 설명 >>

위에서 사용자가 입력한 아이디는 id 매개변수에, 비밀번호는 pw 매개변수에, 닉네임은 nickname 매개변수에, 취미는 hobby 매개변수에 저장되 있음

이렇게 해서 회원 정보(데이터들)을 받을 수 있음

그러나 지금처럼 회원 정보가 흩어져있다면 일반적으로는 회원 정보를 하나로 합쳐서 사용함

 

하나로 합치기 위해 아래와 같이 Member 클래스를 선언하자

getter, setter 반드시 필요함 / 코드 길이 상 생략한 것

package com.example.demo.chapter02;

public class Member {
	String id;
	String pw;
	String nickname;
	String[] hobby;
	
	public Member(String id, String pw, String nickname, String[] hobby) {
		super();
		this.id = id;
		this.pw = pw;
		this.nickname = nickname;
		this.hobby = hobby;
	}
	
	// getter & setter 생략
   }

<< 컨트롤러 >>

 

여기서 잠시 클라이언트가 보낸 정보를 하나로 합쳐 주는걸 클래스, 객체 라고 했지만 MVC 디자인 패턴에서는 M(Model)에 해당함

특히, M 에서도 DTO(Data Transfer Object) 라고 부름

왜 DTO인지는 이 불로그의 글들을 따라오면서 공부하다 보면 알게 됨

 

이 DTO를 바로 매개변수 자리에 쓸 수도 있음

 

DTO가 매개변수가 됐을 때는 DTO가 가지고 있는 멤버 변수의 이름과 일치하는 파라미터를 꺼내 멤버 변수에 저장함

 

 

프로젝트를 재실행 시키고 다음과 같이 요청을 보내보자

 

 

이번에도 hobby들을 배열에 담았는데 hobby들을 List 또는 ArrayList 컬렉션 프레임워크에 담으려면 어떻게 해야할까?

이때는 DTO의 매개변수 타입만 List 또는 ArrayList 로 바꿔주면 됨


클라이언트가 파라미터를 보낼 때 JSON으로 보내는 경우도 있음

클라이언트가 보낸 JSON을 받을 때는 매개변수에 @RequestBody 애너테이션을 붙이면 됨

컨트롤러를 위와 같이 수정 한 후 프로젝트를 재시작하자

 

Postman 으로 JSON 데이터를 보내보자

( 보내는 방법을 모른다면 https://codingaja.tistory.com/101 글을 참고하자 )


여기까지 클라이언트가 다양한 형태의 값을 보낼 때 컨트롤러에서 이를 꺼내는 방법을 배웠음

지금까지는 클라이언트가 반드시 값을 보낸다는 가정으로 컨트롤러에서 이를 꺼냈는데 클라이언트가 값을 보내지 않으면 어떻게 될까??

 

프로젝트 -> com.example.demo.chapter02 -> TestController04 클래스를 추가하고 아래 코드를 추가하자

package com.example.demo.chapter02;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class TestController04 {

	@GetMapping("/chapter02/controller01")
	public void controller01(int param1, double param2, char param3, boolean param4, String param5) {
		System.out.println("<< controller01 메서드 호출 >>");
		System.out.println("-- 클라이언트가 파라미터를 보내지 않았을 때 --");
		System.out.println("param1 => " + param1);
		System.out.println("param2 => " + param2);
		System.out.println("param3 => " + param3);
		System.out.println("param4 => " + param4);
		System.out.println("param5 => " + param5);
	}
	
}

프로젝트를 재시작하고 해당 컨트롤러로 접근하는데 파라미터를 아무것도 보내지 않고 접근해보자

 

클라이언트가 보낸 값을 꺼내기 위해 매개변수가 있는 컨트롤러에 파라미터를 보내지 않으면 아래와 같이 IllegalStateException 이 발생함

왜 이와 같은 예외가 발생하는 것일까?

단순히 클라이언트가 파라미터를 전달하지 않아서 예외가 발생하는건 아님

클라이언트가 매개변수가 있는 컨트롤러에 접근할 때 파라미터를 전달하지 않으면 Spring Framework는 컨트롤러의 매개변수 데이터 타입에 상관없이 매개변수에 null 을 저장함

매개변수 param1의 데이터 타입은 int 인데 int는 null 을 저장할 수 없으므로 InllegalStateException 이 발생하는 것

 

예외가 발생했으니 예외 처리를 해야할까? 그래도 되지만 @RequestParam 애너테이션을 사용하면 클라이언트가 파라미터를 반드시 보내도록 설정할 수도 있고 클라이언트가 파라미터를 보내지 않았을 때 해당 매개변수에 저장될 기본값을 설정할 수도 있음

 

먼저 @RequestParam 애너테이션을 사용해 클라이언트가 파라미터를 반드시 보내도록 설정하자

package com.example.demo.chapter02;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class TestController04 {

	@GetMapping("/chapter02/controller01")
	public void controller01(@RequestParam(name="param1", required=true) int param1,
			@RequestParam(name="param2", required=true) double param2,
			@RequestParam(name="param3", required=true) char param3,
			@RequestParam(name="param4", required=true) boolean param4,
			@RequestParam(name="param5", required=true) String param5) {
		System.out.println("<< controller01 메서드 호출 >>");
		System.out.println("-- 클라이언트가 파라미터를 보내지 않았을 때 --");
		System.out.println("param1 => " + param1);
		System.out.println("param2 => " + param2);
		System.out.println("param3 => " + param3);
		System.out.println("param4 => " + param4);
		System.out.println("param5 => " + param5);
	}
	
}

<< 코드 설명 >>

각 매개변수에 @RequestParam 애너테이션을 달고 이 애너테이션에 required 속성을 사용해 클라이언트가 파라미터를 반드시 보내도록 설정할 수 있음

 

전 글에서도 @RequestParam 애너테이션을 사용해봤지만 전 글에서 사용 방법과 달라졌음

(왼쪽) 전 글과 같이 @RequestParam 애너테이션에 속성명 없이 문자열을 넣으면 이름이 "name" 인 파라미터의 값을 꺼내 해당 매개변수에 저장하도록 하는 것

(오른쪽) 이번 글과 같이 @RequestParam 애너테이션에 name 속성에 문자열을 지정하면 이름이 "param1" 파라미터의 값을 꺼내 해당 매개변수에 저장하도록 할 수 있음

required 속성을 사용하면 클라이언트가 해당 파라미터는 반드시 전달해야하는지 아니면 보내고 싶을 때만 보내는 선택사항인지를 지정할 수 있음

requried 속성이 true면 클라이언트가 해당 파라미터를 반드시 전달해야하고 required 속성이 false면 해당 파라미터는 보내고 싶을 때 보내는 선택사항이 되도록 할 수 있음

 

이제 프로젝트를 재시작한 후 이 컨트롤러에 접근할 때 파라미터를 넣지 않고 접근해보자

반드시 보내야하는 파라미터로 설정했는데 보내지 않았으므로 MissingServletRequestParameterException 이 발생함

이때는 적절한 방법을 사용해 예외처리를 하면 됨

 

클라이언트가 파라미터를 보내고 싶을 때 보내는 선택사항으로 만들어보자

@RequestParam 애너테이션을 붙이지 않으면 해당 파라미터는 선택사항으로 됨

이 애너테이션을 붙여야한다면 다음과 같이 required 속성을 false 로 설정하면 됨

package com.example.demo.chapter02;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class TestController04 {
	@GetMapping("/chapter02/controller02")
	public void controller02(@RequestParam(name="param1", required=false) int param1,
			@RequestParam(name="param2", required=false) double param2,
			@RequestParam(name="param3", required=false) char param3,
			@RequestParam(name="param4", required=false) boolean param4,
			@RequestParam(name="param5", required=false) String param5) {
		System.out.println("<< controller02 메서드 호출 >>");
		System.out.println("-- 클라이언트가 파라미터를 보내지 않았을 때 --");
		System.out.println("param1 => " + param1);
		System.out.println("param2 => " + param2);
		System.out.println("param3 => " + param3);
		System.out.println("param4 => " + param4);
		System.out.println("param5 => " + param5);
	}
	
	@GetMapping("/chapter02/controller01")
	public void controller01(@RequestParam(name="param1", required=true) int param1,
			@RequestParam(name="param2", required=true) double param2,
			@RequestParam(name="param3", required=true) char param3,
			@RequestParam(name="param4", required=true) boolean param4,
			@RequestParam(name="param5", required=true) String param5) {
		System.out.println("<< controller01 메서드 호출 >>");
		System.out.println("-- 클라이언트가 파라미터를 보내지 않았을 때 --");
		System.out.println("param1 => " + param1);
		System.out.println("param2 => " + param2);
		System.out.println("param3 => " + param3);
		System.out.println("param4 => " + param4);
		System.out.println("param5 => " + param5);
	}
}

위와 같이 controller02 컨트롤러를 추가한 후 프로젝트를 재시작하고 해당 컨트롤러에 접근할 때 파라미터를 보내지 말아보자

예외 명은 다르지만 또 다시 예외가 발생함

왜이럴까? 왜 그런지는 위에서 이미 언급했음

 

클라이언트가 매개변수가 있는 컨트롤러에 접근할 때 파라미터를 전달하지 않으면 Spring Framework는 컨트롤러의 매개변수 데이터 타입에 상관없이 매개변수에 null 을 저장함

매개변수 param1의 데이터 타입은 int 인데 int는 null 을 저장할 수 없으므로 InllegalStateException 이 발생하는 것

 

결국은 required=true 일 때도 required=false 일 때도 클라이언트가 파라미터를 보내지 않으면 예외가 발생함

required=false 일 때 예외가 발생하지 않으려면 해당 매개변수에 기본값을 지정하면 됨

기본값은 defaultValue 속성으로 지정할 수 있음

 

다음과 같이 defaultValue 속성을 사용해 controller02 컨트롤러에 매개변수의 기본값을 설정하자

 

이제 클라이언트가 파라미터를 전달하지 않으면 해당 매개변수에는 설정된 기본값이 저장됨

이렇게 파라미터를 선택사항으로 지정하고 싶다면 required 속성은 false로 defaultValue 속성은 파라미터를 전달하지 않았을 때 저장될 기본값으로 지정해주면 됨

728x90
LIST

<< 학습 목표 >>

1. 클라이언트가 보낸 값을 꺼낼 수 있다.

2. 클라이언트가 보낸 값을 형변환해 꺼낼 수 있다.

3. 클라이언트가 보낸 한글 데이터를 꺼낼 수 있다.


이번에는 C의 역할 중 클라이언트가 파라미터를 보냈을 때 파라미터를 꺼내보자

- 클라이언트의 요청을 받음

- 클라이언트가 파라미터를 보냈다면 파라미터를 꺼냄

  꺼낸 파라미터의 형태가 올바른지 확인함

  꺼낸 파라미터들을 하나의 정보로 합칠 수 있다면 합침

- 클라이언트의 요청을 처리할 서비스 메서드를 호출함

- 서비스 메서드가 반환한 처리 결과를 사용해 적절한 후처리를 함

- 클라이언트의 요청 처리 결과를 전달함

 

그 중에서도 특히 클라이언트가 값을 보냈을 때 값을 꺼내는 방법에 대해 알아보자


 

클라이언트가 보내는 파라미터를 꺼내는 방법은 다양한 방법이 있음

1. 요청 정보(HttpServletRequest) 를 사용해 꺼내는 방법

2. @RequestParam 애너테이션을 사용해 꺼내는 방법

3. 매개변수를 사용해 꺼내는 방법

 

세 방법 모두 간단하므로 한꺼번에 코드로 입력한 후 하나씩 알아보자

프로젝트 -> com.example.demo.chapter02 -> TestController02 클래스를 추가하고 아래 코드를 추가하자

package com.example.demo.chapter02;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import jakarta.servlet.http.HttpServletRequest;

@Controller
public class TestController02 {
	@GetMapping("/chapter02/type1")
	public void type1(HttpServletRequest request) {
		String name = request.getParameter("name");
		
		System.out.println("<< type1 메서드 호출 >>");
		System.out.println("클라이언트가 보낸 name 파라미터의 값 => " + name);
	}
	
	@GetMapping("/chapter02/type2")
	public void type2(@RequestParam("name") String name) {
		System.out.println("<< type2 메서드 호출 >>");
		System.out.println("클라이언트가 보낸 name 파라미터의 값 => " + name);
	}
	
	@GetMapping("/chapter02/type3")
	public void type3(String name) {
		System.out.println("<< type3 메서드 호출 >>");
		System.out.println("클라이언트가 보낸 name 파라미터의 값 => " + name);
	}
}

<< 코드 설명 >>

type1 메서드에서 type2, type3 메서드로 내려갈수록 파라미터를 꺼내는 방법이 간단해지고 있음

(1). 서블릿 컨트롤러와 동일한 방식으로 요청 정보(HttpServletRequest) 의 getParameter 메서드를 사용해 꺼냄

(2). @RequestParam 애너테이션을 사용해 name 파라미터의 값을 꺼내 name 매개변수에 저장함

(3). @RequestParam 애너테이션이 없으면 매개변수의 이름과 동일한 파라미터의 값을 꺼내 매개변수에 저장함

 

프로젝트를 실행시키고 각 경로에 name 파라미터를 보내보자

<< Postman 으로 요청 보내는 방법 >>

 

(1) 의 경우 서블릿 컨트롤러의 방식을 그대로 사용한 것이므로 더 설명할 게 없지만 (2), (3) 의 경우 Spring Framework 만의 방식이므로 몇 가지 상황을 더 알아보자

 

클라이언트가 파라미터를 여러 개 보냈다면(1) 다음과 같이 매개변수를 더 추가해 각 값을 꺼내 매개변수에 저장(2)할 수 있음

 

<< Postman >>

 

<< TestController02 컨트롤러 >>

 

여기서 type2의 경우 @ReuqestParam 애너테이션에 값을 꺼낼 파라미터의 이름을 지정했으므로 매개변수의 이름이 중요하지 않음

<< 코드 설명 >>

(1). 클라이언트가 보낸 name 파라미터의 값을 꺼내 param1 매개변수에 저장

(2). 클라이언트가 보낸 age 파라미터의 값을 꺼내 param2 매개변수에 저장

(3). 클라이언트가 보낸 height 파라미터의 값을 꺼내 param3 매개변수에 저장

 

그러나 type3의 경우 값을 꺼낼 파라미터의 이름과 매개변수의 이름이 반드시 같아야함

<< 코드 설명 >>

(1). 클라이언트가 보낸 name 파라미터의 값을 꺼내 name 매개변수에 저장

  클라이언트가 보낸 age 파라미터의 값을 꺼내 age 매개변수에 저장

  클라이언트가 보낸 height 파라미터의 값을 꺼내 height 매개변수에 저장

 

일반적으로는 가장 간단한 type3의 방식대로 파라미터를 꺼냄


Spring Framework 프로젝트이므로 파라미터를 꺼낼 때 당연히 Spring Framework의 방식대로 꺼내지만 특히나 Spring Framework의 방식을 사용해 파라미터를 꺼낼 때 편리한 점은 파라미터의 형변환을 알아서 해준다는 점

위와 같이 클라이언트가 파라미터를 보냈다면 보통 컨트롤러에서는 데이터를 꺼내 형변환 후 사용함

그러나 Spring Framework의 방식대로 꺼낼 때는 매개변수의 타입에 맞게 형변환 되 해당 매개변수에 저장됨

 

프로젝트를 재실행 시킨 후 각 경로로 데이터들을 보내보자

 

앞으로는 특별한 경우가 아니라면 type3 방식대로 파라미터를 꺼낼 것이므로 type2가 빠져있어도 동일하게 적용된다고 생각하자


클라이언트가 한글 데이터를 파라미터에 담아 보냈을 때는 어떻게 될까?

 

컨트롤러에서는 별도로 처리해줄게 없음

서블릿 컨트롤러에서는 반드시 request.setCharacterEncoding("UTF-8") 을 해줘야했지만 Spring Framework 를 사용한 컨트롤러에서는 해당 설정이 자동으로 적용됨

 

이렇게 서블릿 컨트롤러에 비해서 개발자가 할 일이 줄어드므로 Spring Framework 를 사용한 프로젝트를 만드는 것

 

728x90
LIST