(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<?> 로 할 수 있음
서버는 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 를 추가하고 아래 코드를 추가하자
(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의 인자를 "*" 로 할 수 없게 막는 역할
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에 대해서 끝장을 보고 싶다 라면 아래 책을 보면 끝장을 볼 수 있을 것
databaseId 속성 : 데이터베이스 밴더에 맞춰 구문을 실행시키고 싶을 때 사용하는 속성 데이터베이스 밴더란 Mysql, Mssql, Oracle 등을 뜻함 거의 사용되지 않는 속성임
lang 속성 : 동적 쿼리를 작성할 때 사용할 언어를 지정하는 속성으로 xml, raw 두 개가 있음 lang 속성의 기본값은 xml임 lang 속성을 raw로 지정하면 MyBatis는 동적 쿼리에 최소한의 처리 ( 파라미터 치환 )만 한 후 SQL을 데이터베이스 드라이버 ( HikariCP, Commons DBCP2 등 ) 에 전달함
태그명
include
설명
sql 태그로 저장해둔 SQL 문장을 불러오는 태그
속성
refid 속성 : 불러올 sql 태그의 id 속성 값
닉네임 또는 연락처로 회원 정보를 조회하는 쿼리를 보자
<?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">
<mapper namespace="com.study.chapter04.MemberDao">
<select id="getMemberByNickname" resultType="com.study.chapter04.MemberDto">
SELECT id, pw, nickname, tel FROM member WHERE nickname = #{nickname}
</select>
<select id="getMemberByTel" resultType="com.study.chapter04.MemberDto">
SELECT id, pw, nickname, tel FROM member WHERE tel = #{tel}
</select>
</mapper>
여기서 반복적으로 사용된 부분을 보면 다음과 같음
SELECT id, pw, nickname, tel FROM member
이 부분을 sql 태그를 사용해서 저장해 둘 수 있음
그리고 include 태그를 사용해 불러올 수 있음
(1). sql 태그를 사용해 반복적으로 사용된 SQL 문장을 저장함
이때 id 속성을 select_from 으로 했음
(2). 필요한 부분을 include 태그를 사용해 불러오고 있음
sql 태그 안에서 전달 받은 DTO를 활용할 수도 있음
다음 sql 태그를 보자
<?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">
<mapper namespace="com.study.chapter04.MemberDao">
<sql id="where">
<where>
<if test="nickname != null">
AND nickname = #{nickname}
</if>
<if test="tel != null">
AND tel = #{tel}
</if>
</where>
</sql>
<select id="getMember" resultType="com.study.chapter04.MemberDto" parameterType="com.study.chapter04.MemberDto">
SELECT id, pw, nickname, tel FROM member
<include refid="where" />
</select>
</mapper>
<< 코드 설명 >>
(1), (2). include 태그로 불러온 sql 태그 내 문장은 물리적으로는 떨어져있지만 실제 동작할 때는 하나의 문장으로 동작하기 때문에 아래와 같이 생각하고 활용해야함
마지막으로 property 태그가 남았는데 property 태그는 활용도가 떨어지니 간단하게 태그명, 설명, 속성만 알아보자
나중에 동적 쿼리를 만들어야 하는데 지금 아는 것만으로는 뭔가 부족하다 싶으면 property 태그를 떠올리고 그때 property 태그에 대해서 자세히 찾아봐도 됨
이번에 배울 bind, resultMap 태그는 정적 쿼리든 동적 쿼리든 어디서든 활용할 수 있으므로 사용할 수 있는 상황을 제한하지 말자
태그명
bind
설명
쿼리에서 사용할 변수를 선언하는 태그
속성
name 속성 : 변수의 이름
value 속성 : 변수의 값
SELECT 쿼리를 작성할 때 LIKE 가 필요하면 다음과 같이 LIKE 를 구성함
<select id="selectMember" resultType="com.study.chapter04.SelectMemberDto">
SELECT * FROM member WHERE nickname LIKE "'%' + #{nickname} + '%'";
</select>
bind 태그를 사용하면 좀 더 가독성 있는 코드를 만들 수 있음
<select id="selectMember" resultType="com.study.chapter04.SelectMemberDto">
<bind name="pattern" value="'%' + #{nickname} + '%'"/>
SELECT * FROM member WHERE nickname LIKE #{pattern};
</select>
bind 태그를 사용할 때 주의할 점은 select, insert, update, delete 태그 안에 사용해야한다는 점임
만약 여러 태그에서 똑같은 bind 태그를 여러번 사용한다면 아직까지는 그때 마다 bind 태그를 복사 해서 사용해야함
다음에 배울 sql, includes 를 배우면 똑같은 bind 태그를 여러번 사용해야할 때 bind 태그를 재사용하는 방법을 알 수 있을 것
태그명
resultMap
설명
DB의 AS ( Alias ) 와 같은 역할을 하는 태그
또는 SELECT 결과가 계층적 데이터 구조로 되어있는 경우 결과를 저장하기 위해 사용하는 태그
속성
type 속성 : 결괏값을 저장할 DTO의 데이터 타입
id 속성 : 이 구조의 이름
resultMap 태그는 어떻게 활용하느냐에 따라서 활용 방식이 배우 달라지는데 먼저, 가장 간단한 형태로 AS ( Alias ) 와 같은 역할을 하는 것으로 활용해보자
- AS 대신 사용
resultMap 태그 안에는 자식 태그로 id, result, association, collection, discriminator, constructor 태그를 가질 수 있음
AS 대신 resultMap 태그를 사용한다면 id, result 자식 태그만 사용하면 됨
(4). resultMap 태그로 선언한 매칭 정보를 활용하기 위해서 resultMap 속성을 사용함
여기서 주의할 점 ! DTO를 그대로 사용할 때는 resultType 속성을 사용했지만 resultMap 태그를 사용할 때는 resultMap 속성을 사용함
resultMap 태그의 javaType, jdbcType은 생략 가능함
다음은 javaType, jdbcType 을 나열한 테이블임
자바의 데이터 타입
javaType
DB의 데이터 타입
jdbcType
int
int
int
INTEGER
double
double
float(실수)
DOUBLE
char
String
char
String
String
String
String
String
이 표를 보는 방법은 (자바의 데이터 타입, javaType) , (DB의 데이터 타입, jdbcType) 임
자바의 데이터 타입에 맞게 javaType 속성을 사용하면 됨
DB의 데이터 타입에 맞게 jdbcType 속성을 사용하면 됨
javaType, jdbcType 속성을 생략해도 MyBatis가 적절히 매칭시켜줌
그러나 일반적으로 테이블의 칼럼명과 DTO의 멤버 변수명은 맞춰거나 DBMS의 AS 키워드를 사용하므로 resultMap을 AS 대신 사용하는 일은 없을 것
- SELECT 결과가 계층적 데이터 구조로 되어있는 경우 결과를 저장하기 위해 사용하는 태그
계층적 데이터 구조란 한 테이블 안에 상위 데이터와 하위 데이터가 함께 있는 구조를 말함
쇼핑몰의 카테고리가 대표적인 계층적 구조임
쿠팡의 카테고리를 보자
카테고리는 카테고리명, 차수(depth) 로 이뤄져있음
(1)인 패션의류/잡화, 뷰티, 출산/유아동 등에서 패션의류/잡화를 보면 카테고리명은 패선의류/잡화이고 차수는 1차임
뷰티의 카테고리명은 뷰티이고 차수는 역시 1차임
(2)인 여성패션, 남성패션, 남녀 공용 의류, 유아동패션은 1차 카테고리인 패션의류/잡화의 하위 카테고리임
그리고 카테고리이기 때문에 카테고리명과 차수로 이뤄져있음
또한 상위 카테고리가 패션의류/잡화임
따라서 여성패션의 카테고리명은 여성패션이고 차수는 2차, 상위 카테고리는 패션의류/잡화임
남성패션의 카테고리명은 남성패션이고 차수는 2차, 상위 카테고리는 패션의류/잡화임
마지막으로 (3)인 의류, 속옷/잠옷, 신발, 가방/잡화는 2차 카테고리인 여성패션의 하위 카테고리임
그리고 카테고리기 때문에 카테고리명과 차수로 이루어져있음
또한 상위 카테고리가 여성패션임
따라서 의류의 카테고리명은 의류이고 차수는 3차, 상위 카테고리는 여성패션임
속옷/잠옷의 카테고리명은 속옷/잠옷이고 차수는 3차, 상위 카테고리는 여성패션임
카테고리의 구조를 좀 더 시각적으로 나타내면 다음과 같음
이러한 구조의 데이터를 저장할 때는 아래와 같이 한 테이블에 저장할 수 있도록 테이블 구조를 설계함
<< 설명 >>
테이블명은 category 이고 4개의 칼럼을 가지고 있음
idx, name, order 칼럼은 특별한 점이 없지만 parent_idx 칼럼은 특별한 카테고리
parent_idx 카테고리가 참조하는 테이블은 category, 참조하는 칼럼은 idx
이 테이블의 ERD를 그리면 다음과 같음
category테이블에 데이터를 저장하면 다음과 같이 저장됨
1차 카테고리를 찾는 쿼리
-> SELECT * FROM category WHERE parent_idx IS NULL;
2차 카테고리를 찾는 쿼리
-> SELECT * FROM category WHERE parent_idx = 1;
-> SELECT * FROM category WHERE parent_idx = 2;
-> ...
3차 카테고리를 찾는 쿼리
-> SELECT * FROM category WHERE parent_idx = 4;
-> SELECT * FROM category WHERE parent_idx = 5;
-> SELECT * FROM category WHERE parent_idx = 6;
-> ...
1 ~ 3차 카테고리까지 한번에 찾는 쿼리
-> SELECT depth1.name, depth2.name, depth3.name FROM category depth1 LEFT JOIN category depth2 ON depth1.idx = depth2.parent_idx LEFT JOIN category depth3 ON depth2.idx = depth3.parent_idx WHERE depth1.parent_idx IS NULL;
( DBMS와 관련된 카테고리가 아니기 때문에 더 이상 자세한 설명은 생략함 )
1 ~ 3차 카테고리까지 한번에 찾는 쿼리를 사용하면 아래와 같은 데이터가 조회됨
이를 MyBatis, (DTO, DAO), 컨트롤러 로 가져오면 한 행에 하나씩 DTO에 저장됨
그리고 DTO를 사용하기가 어려울 수 있음
DTO를 사용하기 어려운 상황 중 하나를 재현해보자
컨트롤러에 접근했을 때 다음과 같이 각 차수별 카테고리와 그 하위 카테고리를 출력해보자
우선 카테고리 하나의 정보를 담을 수 있는 CategoryDto 를 추가하자
package com.study.chapter04;
import lombok.Data;
@Data
public class CategoryDto {
// 1차 카테고리 정보
private int depth1CategoryIdx;
private String depth1CategoryName;
// 2차 카테고리 정보
private int depth2CategoryIdx;
private String depth2CategoryName;
// 3차 카테고리 정보
private int depth3CategoryIdx;
private String depth3CategoryName;
}
<< 코드 설명 >>
카테고리가 최대 3차 카테고리까지 있으므로 3차 카테고리 정보까지 담을 수 있도록 멤버 변수를 선언해둠
이제 카테고리 정보를 조회해 DTO에 담아줄 쿼리를 만들자
프로젝트 -> src/main/resources -> mapper -> Category.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">
<mapper namespace="com.study.chapter04.CategoryDao">
<select id="selectAllCategory" resultType="com.study.chapter04.CategoryDto">
SELECT
depth1.idx AS depth1CategoryIdx, depth1.name AS depth1CategoryName,
depth2.idx AS depth2CategoryIdx, depth2.name AS depth2CategoryName,
depth3.idx AS depth3CategoryIdx, depth3.name AS depth3CategoryName
FROM category depth1
LEFT JOIN category depth2
ON depth1.idx = depth2.parent_idx
LEFT JOIN category depth3
ON depth2.idx = depth3.parent_idx
WHERE depth1.parent_idx IS NULL;
</select>
</mapper>
-> UPDATE memberSET nickname = #{nickname} WHERE id = #{id}
4. 연락처만 전달 받았을 경우
-> UPDATE memberSET tel = #{tel} WHERE id = #{id}
5. 비밀번호와 닉네임을 전달 받았을 경우
-> UPDATE member SET pw = #{pw}, nickname = #{nickname} WHERE id = #{id}
6. 비밀번호와 연락처를 전달 받았을 경우
-> UPDATE member SET pw = #{pw}, tel = #{tel} WHERE id = #{id}
7. 비밀번호, 닉네임, 연락처 모두 전달 받았을 경우
-> UPDATE member SET pw = #{pw}, nickname = #{nickname}, tel = #{tel} WHERE id = #{id}
8. 닉네임과 연락처를 전달 받았을 경우
-> UPDATE member SET nickname = #{nickname},tel = #{tel} WHERE id = #{id}
여기서 특징적인 점을 보자
set 태그 안에 if의 조건을 만족하면 [ pw = #{pw}, ] 또는 [ nickname = #{nickname} ] 또는 [ tel = #{tel} ] 이 추가되는데 각 상황에서 만들어지는 쿼리를 보면 SET의 마지막에 ( 또는 WHERE 가 시작 되기 전에 ) , 가 없음
<?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">
<mapper namespace="com.study.chapter04.MemberDao">
// ...
<select id="selectMember" parameterType="com.study.chapter04.SelectMemberDto" resultType="com.study.chapter04.SelectMemberDto">
SELECT * FROM member WHERE nickname IN
<foreach collection="selectConditions" item="condition" open="(" close=")" separator=", ">
#{condition.nickname}
</foreach>
;
</select>
// ...
</mapper>
이 쿼리는 DAO가 쿼리쪽으로 데이터들을 전달해준다 라는 전제조건 하에 짜여진 동적 쿼리임
DAO가 쿼리쪽으로 데이터들을 전달해주지 않으면 어떻게 될까?
더 정확하게 이해를 하기 위해 컨트롤러부터 보자
<< 컨트롤러 >>
package com.study.chapter04;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class MyBatisController {
@Autowired
private MemberDao memberDao;
// ...
@GetMapping("/chapter04/mybatis/member")
public void getMemberByNickname(String nickname) {
System.out.println("<< 닉네임으로 회원 정보 조회 시작 >>");
// 닉네임으로 회원 정보 조회
List<SelectMemberDto> member = memberDao.selectMember(null);
// 조회한 회원 정보 출력
System.out.println(member);
System.out.println("<< 닉네임으로 회원 정보 조회 종료 >>");
}
// ...
}