<< 학습 목표 >>

1. SQL 로그를 콘솔에 출력할 수 있다.


로그 ( Log ) 란 흔적을 말함

 

클라이언트가 서버로 요청을 보내면 서버는 요청 정보를 로그에 남김

나중에 로그에 남은 이 요청 정보를 분석해서 사용자들이 어떤 서비스를 많이 이용하는지, 어느 시간대에 많이 이용하는지 등을 분석할 수 있음

이처럼 운영에 필요한 로그를 남길 수도 있지만 개발에 필요한 로그를 남길 수 있음

 

개발에 필요한 로그는 주로 디버깅을 위해 남김

클라이언트가 어떤 값을 보냈을 때 내가 만든 API가 어떻게 동작하고 어떤 결과를 응답하는지 등을 남겨 디버깅에 활용함

이때 중요하게 활용되는 로그 중 하나는 SQL 로그임

 

개발 직무 중 DBA 가 있을 정도로 DB, SQL를 제대로 다루기 위해서는 많은 공부가 필요함

그래서 백엔드 개발자들은 주로 당장에 필요한 DB, SQL과 관련된 최소한의 지식만 가지고 공부하고 취업하고 실무에서 개발함

이 때문에 개발 중 SQL이 의도치 않게 동작할 경우 이 SQL을 확인해야되는데 로그를 남기지 않으면 SQL을 확인하기 어렵거나 불가능할 수 있음


이번에는 SQL 로그를 남기는 방법을 배워보자

 

자바에는 로그를 남기는 방법이 많은데 일반적으로 별도의 로깅 프레임워크를 사용해서 로그를 남김

자바 로깅 프레임워크에는 log4j, log4j2, SLF4J, apache common logging, logback 등이 있음

Spring Boot 로 만든 프로젝트는 기본적으로 logback 로깅 프레임워크가 내장되어있음

 

 

우리 프로젝트에 logback 로깅 프레임워크를 사용해 SQL 로그를 남겨보자

 

logback 로깅 프레임워크를 사용하려면 먼저 xml 파일을 만들어 로깅 설정을 해야함

프로젝트 -> src/main/resources -> logback-spring.xml 파일을 추가하고 아래 설정 코드를 추가하자

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
		<encoder>
			<Pattern>[%d{yyyy-MM-dd HH:mm:ss}:%-3relative] [%thread] %-5level %logger{36} - %msg%n</Pattern>
		</encoder>
	</appender>
	
	<logger name="com.study.chapter04" level="debug" />
	
	<root level="info">
		<appender-ref ref="console" />
	</root>
</configuration>

<< 코드 설명 >>

우선 설정 파일 이름에 오타가 있으면 안됨

SpringFramework는 설정 파일을 찾을 때 설정 파일의 이름으로 찾기 때문에...

 

설정 코드를 이해할 필요는 없음

"SQL 로그를 출력하려면 이렇게 써야하는구나" 라고 생각하고 넘어가자

 

 

여기까지가 로그를 출력하기 위한 설정 끝임

이제 쿼리, DAO, 컨트롤러를 추가해 SQL 로그가 출력되는지 확인하자

 

<< 쿼리 >>

프로젝트 -> src/main/resources -> mapper -> Member7.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.MemberDao7">
	<select id="getMember" resultType="com.study.chapter04.MemberDto" parameterType="com.study.chapter04.MemberDto">
		SELECT id, pw, nickname, tel FROM member
		<where>
			<if test="nickname != null">
			AND nickname = #{nickname}
			</if>
			<if test="tel != null">
			AND tel = #{tel}
			</if>
		</where>
	</select>
</mapper>

 

<< DTO >>

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

package com.study.chapter04;

import java.time.LocalDateTime;

import lombok.Data;

@Data
public class MemberDto {
	private int idx;
	private String id;
	private String pw;
	private String nickname;
	private String tel;
	private LocalDateTime joinDateTime;
	private boolean isDel;
}

 

<< DAO >>

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

package com.study.chapter04;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface MemberDao7 {
	MemberDto getMember(MemberDto member);
}

 

<< 컨트롤러 >>

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

package com.study.chapter04;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class MyBatisController7 {
	@Autowired
	private MemberDao7 memberDao;
	
	@GetMapping("/chapter04/mybatis/v7/member")
	public void getMemberByNickname(MemberDto parameter) {
		MemberDto member = memberDao.getMember(parameter);
		
		System.out.println("<< 조회 결과 >>");
		System.out.println(member);
	}
}

 

이제 서버를 실행시키고 컨트롤러를 호출해보자

이때 닉네임(nickname) 이나 연락처(tel) 파라미터를 보내야함

 

컨트롤러를 호출하면 DAO를 타고 SQL이 실행되며 콘솔에 로그가 남을 것

728x90
LIST

<< 학습 목표 >>

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. MyBatis에서 사용하는 두 가지 캐시에 대해 설명할 수 있다.


이번에 배울 cache, cache-ref 태그는 간단하게 이런게 있다 정도만 알아볼 것

 

MyBatis는 효율적으로 쿼리를 보내고 결과를 받기 위해 캐시(Cache)를 사용함

만약 캐시에 대해 처음 듣는다면 우선 인터넷이나 ChatGPT의 도움을 받아 캐시에 대해서 이해하고 오자

 

MyBatis가 사용하는 캐시

1. Local Session Cache

2. Second Level Cache

 

<< Local Session Cache >>

- 기본적으로 활성화 되어있는 캐시

- 당연히 조회(SELECT)에만 적용됨

  완전히 동일한 조회 쿼리를 여러번 보내면 매번 DB에서 조회해오지 않고 처음 한번만 DB에서 조회한 후 결과를 캐시에 저장함

  두번째부터는 캐시에서 결과를 꺼내 반환함

  캐시가 가득 차면 LRU ( Least Recently Used / 맨 처음 저장된 캐시 ) 를 제거하고 새 조회 결과를 저장함

 - 이 캐시는 비활성화 하는게 불가능함, 대신 캐시의 활동 범위를 좁힐 수 있음

   활동 범위 : SESSION, STATEMENT

     > 활동 범위는 SESSION이 기본값이며 UPDATE, COMMIT, ROLLBACK, CLOSE 시 마다 캐시를 비움

     > flushCache 옵션을 사용해 활동 범위를 STATEMENT 로 바꿀 수 있음

     > 활동 범위를 STATEMENT 로 바꾸면 SELECT, INSERT, UPDATE, DELETE 시 마다 캐시를 비우도록 할 수 있음

 

<< Second Level Cache >>

 - 기본적으로 비활성화 되어있는 캐시

   <mapper> 태그 내 <cache /> 태그를 사용해 활성화 시킬 수 있음

   <mapper> 태그 내 <cache /> 태그로 활성화 시키므로 매퍼별 캐시가 따로 관리됨

 - INSERT, UPDATE, DELETE 시 마다 이 캐시를 비움

 - 매퍼별로 캐시를 관리하므로 <mapper> 태그 안에서는 테이블 하나만 관리하는게 좋음

   이를 제대로 이해하려면 DDD (도메인 주도 개발)에 대해서 이해해야함


<< Local Session Cache의 적용 범위를 STATEMENT 로 바꾸는 예시 >>

insert, update, delete, select 등의 태그에 사용할 수 있는 flushCache 속성을 false로 지정하면 됨

flushCache 속성 : 이 속성의 값을 true로 설정하면 해당 SQL이 실행될 때 마다 Local Session Cache, Second Level Cache가 비워짐

  이 속성의 기본값은 false임


<< Second Level Cache를 활성화 시키는 예시 >>

단순히 <cache> 태그를 추가하기만 해도 Second Level Cache 를 활성화시킬 수 있음

이 태그는 여러 속성이 있는데 그 중 대표적인 몇 가지 속성만 알아보자

- eviction 속성 : 캐시 비움 정책

  기본 정책은 LRU ( Least Recently Used ) 임, LRU는 가장 먼저 추가된 캐시를 비우는 정책임

  그외에도 FIFO, SOFT, WEAK 가 있음

 - flushInterval 속성 : 캐시 비움 주기

  밀리초 단위로 설정하며 지정한 초가 지날 때 마다 주기적으로 캐시를 비움

  특정 시간을 지정해서 비우도록 할 수는 없음

  (예. "매일 새벽 4시에 캐시를 비워라" 안됨)

 - size 속성 : 캐시의 사이즈


마지막으로 cache-ref 태그는 다른 매퍼의 캐시를 같이 사용하고 싶을 때 사용하는 태그

아예 사용하지 않는다고 봐도 되므로 간단하게 예시 코드만 첨부함

<?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">
	<cache-ref namespace="mapper 태그를 갖고 있는 파일의 경로" />
    
	// ...
</mapper>

이렇게 MyBatis 캐시에 대해 알아봤음

그러나 실무에서는 MyBatis 가 내장하고 있는 캐시를 사용하지 않고 별도로 캐시 프레임워크를 붙여서 사용함

 

실무에서 많이 사용하는 캐시 프레임워크

 - EhCache

 - OsCache

 - Hazelcast

 

참고 : https://idea-sketch.tistory.com/31, https://12bme.tistory.com/352, https://jp1020.tistory.com/entry/mybatis-cache-%EC%84%A4%EC%A0%95, https://yunamom.tistory.com/40

728x90
LIST

<< 학습 목표 >>

1. sql, include 태그를 활용할 수 있다.


이번에 알아볼 동적 쿼리 태그는 sql, include, property 태그

 

태그명 sql
설명 자주 사용하는 SQL 문장을 저장하는 태그
속성 id속성 : 저장해둔 SQL 문장의 별칭, 속성명이 id 인것처럼 이 별칭은 고유해야함

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 태그에 대해서 자세히 찾아봐도 됨

태그명 property
설명 sql 태그 안에 있는 SQL 문장이 필요한 값을 전달할 수 있음
속성 name 속성 : 전달할 값의 이름

value 속성 : 전달할 값

 

728x90
LIST

<< 학습 목표 >>

1. bind 태그를 활용할 수 있다.

2. AS 키워드 대신 resultMap 태그를 활용할 수 있다.

3. 계층적 데이터 구조를 이해하고 설계할 수 있다.

4. 계층적 데이터 구조인 데이터를 가져올 때 resultMap 태그를 활용할 수 있다.


이번에 배울 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 자식 태그만 사용하면 됨

 

member ( 회원 정보 ) 테이블의 column이 다음과 같은 상황임

 

MyBatis를 사용해서 회원 정보를 조회하는 쿼리를 만들자

<?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" resultType="com.study.chapter04.ResultMapDto">
		SELECT * FROM member;
	</select>
	
	// ...
</mapper>

 

결과를 저장할 DTO는 ResultMapDto 임

ResultMapDto 는 아래와 같음

package com.study.chapter04;

import java.time.LocalDateTime;

import lombok.Data;

@Data
public class ResultMapDto {
	private int userIdx;
	private String userId;
	private String _pw;
	private String nickName;
	private String telNumber;
	private LocalDateTime joinDateTime;
	private boolean isDel;
}

 

이때 테이블의 칼럼명과 DTO의 멤버 변수명이 다르기 때문에 쿼리가 정상적으로 수행되도 결과를 받을 수 없음

 

결과를 제대로 받으려면 쿼리에 AS ( Alias ) 를 사용하면 됨

 

그러나 AS는 DBMS가 지원하는 키워드임

AS 대신 MyBatis가 지원하는 resultMap 태그를 사용해도 됨

<?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">
	// ...
	
	<resultMap type="com.study.chapter04.ResultMapDto" id="newResultMapDto">
		<id 	column="idx"			property="userIdx"		javaType="int" 		jdbcType="INTEGER" />
		<result column="id"				property="userId"		javaType="String" 	jdbcType="VARCHAR" />
		<result column="pw"				property="_pw"			javaType="String" 	jdbcType="VARCHAR" />
		<result column="nickname"		property="nickName"		javaType="String" 	jdbcType="VARCHAR" />
		<result column="tel"			property="telNumber"	javaType="String" 	jdbcType="VARCHAR" />
		<result column="joinDateTime"	property="joinDateTime"	javaType="String" 	jdbcType="VARCHAR" />
		<result column="isDel"			property="isDel"		javaType="boolean"	jdbcType="VARCHAR" />
	</resultMap>
	
	<select id="selectMember" resultMap="newResultMapDto">
		SELECT * FROM member;
	</select>
	
	// ...
</mapper>

<< 코드 설명 >>

(1). 칼럼명과 멤버 변수를 매치하기 위해 resultMap 태그 사용

(2). PK인 칼럼을 멤버 변수와 연결하기 위한 태그

  column속성은 테이블의 칼럼명

  property속성은 DTO의 멤버 변수명

  javaType속성은 DTO의 멤버 변수의 데이터 타입

  jdbcType속성은 테이블의 칼럼의 데이터 타입

(3). PK가 아닌 칼럼을 멤버 변수와 연결하기 위한 태그

  속성들은 (2) 와 동일함

(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>

<< 코드 설명 >>

조회 결과를 담기 위해 DTO의 멤버 변수 이름에 맞춰서 SELECT 하도록 지정함

 

 

이제 DAO 를 추가해 컨트롤러에서 쿼리를 실행하고 결과를 받아올 수 있도록 하자

package com.study.chapter04;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface CategoryDao {
	List<CategoryDto> selectAllCategory();
}

 

 

마지막으로 컨트롤러를 추가해 시작 하면서 보여줬던 출력 예시처럼 출력하도록 만들자

이건 보지말고 여러분이 직접 만들어보자

더보기

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 CategoryController {
	@Autowired
	CategoryDao categoryDao;
	
	@GetMapping("/chatper04/categories")
	public void printAllCategory() {
		List<CategoryDto> categories = categoryDao.selectAllCategory();

		String prevDepth1CategoryName = "";
		String prevDepth2CategoryName = "";
		String prevDepth3CategoryName = "";
		
		for(CategoryDto category : categories) {
			String nowDepth1CategoryName = category.getDepth1CategoryName();
			if(!nowDepth1CategoryName.equals(prevDepth1CategoryName)) {
				prevDepth1CategoryName = nowDepth1CategoryName;
				
				System.out.println(nowDepth1CategoryName);
			} // end if
			
			String nowDepth2CategoryName = category.getDepth2CategoryName();
			if(nowDepth2CategoryName != null) {
				if(!nowDepth2CategoryName.equals(prevDepth2CategoryName)) {
					prevDepth2CategoryName = nowDepth2CategoryName;
					
					System.out.println("\t" + nowDepth2CategoryName);
				}
				
				String nowDepth3CategoryName = category.getDepth3CategoryName();
				if(nowDepth3CategoryName != null) {
					if(!nowDepth3CategoryName.equals(prevDepth3CategoryName)) {
						prevDepth3CategoryName = nowDepth3CategoryName;
						
						System.out.println("\t\t" + nowDepth3CategoryName);
					} // end if
				} // end if
			} // end if
		} // end for
	}
}

이 컨트롤러를 완성했다고 해도 시간이 상당히 오래 걸렸을 것

그 이유는 카테고리는 우리가 본 형태는 계층적 데이터 구조이고 구현도 계층적 데이터 구조로 구현했음

 

그러나 자바로 가져올 때는 계층적 데이터 구조가 아닌 선형적 데이터 구조로 가져왔기 때문임

이렇게 계층적 데이터 구조를 그대로 가져오려면 resultMap 태그를 사용해야함

 

resultMap을 사용해 계층적 데이터 구조를 그대로 가져와보자

계층적 데이터 구조로 가져오기 위해 DTO를 새로 선언해야함

package com.study.chapter04;

import java.util.List;

import lombok.Data;

@Data
public class CategoriesDto {
	private int idx;
	private String name;
	
	List<CategoriesDto> childCategories;
}

<< 코드 설명 >>

(1). n차 카테고리의 번호와 이름을 저장하기 위한 멤버 변수

(2). n차 카테고리의 하위 카테고리인 n+1차 카테고리의 정보를 저장하기 위한 멤버 변수

 

 

 

 

 

 

이제 쿼리에서 CategoriesDto 를 사용해 SELECT의 결과를 계층적 데이터 구조로 가져오도록 하자

<?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">
	<resultMap type="com.study.chapter04.CategoriesDto" id="CategoriesDto">
		<id column="depth1CategoryIdx" property="idx" jdbcType="INTEGER" />
		<result column="depth1CategoryName" property="name" jdbcType="VARCHAR" />
		
		<collection property="childCategories" ofType="com.study.chapter04.CategoriesDto">
			<id column="depth2CategoryIdx" property="idx" jdbcType="INTEGER" />
			<result column="depth2CategoryName" property="name" jdbcType="VARCHAR" />
			
			<collection property="childCategories" ofType="com.study.chapter04.CategoriesDto">
				<id column="depth3CategoryIdx" property="idx" jdbcType="INTEGER" />
				<result column="depth3CategoryName" property="name" jdbcType="VARCHAR" />
			</collection>
		</collection>
	</resultMap>

	<select id="selectAllCategory" resultMap="CategoriesDto">
		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>

<< 코드 설명 >>

(1). SELECT 결과를 계층적 데이터 구조로 담기 위해 선언한 resultMap을 사용하도록 바꿈

(2). SELECT 결과를 계층적 데이터 구조로 담기 위해 resultMap 선언

  이 구조를 제대로 이해하려면 SELECT의 결과와 함께 대조해야함

 

SELECT 결과를 보면 중복을 제거하고 보면 1차 카테고리가 3개로 나뉨

resultMap은 중복을 제거하고 1차 카테고리 1개당 DTO ( type 속성으로 지정한 com.study.chapter04.CategoriesDto ) 하나에 담에 총 3개의 1차 카테고리 정보를 담고 있는 List가 만들어짐

 

 

그 다음 1차 카테고리가 같은 2차 카테고리의 정보를 1차 카테고리 정보를 담고 있는 DTO의 childCategories 멤버 변수 안에 담음

2차 카테고리 역시 중복을 제거한 후 2차 카테고리 1개당 DTO 하나에 담음

 

- 1차 카테고리 idx가 1

- 1차 카테고리 name이 패션의류/잡화

- 자식 카테고리 4개 ( 여성패션, 남성패션, 남녀 공용 의류, 유아동패션 )

 

- 1차 카테고리 idx가 2

- 1차 카테고리 name이 뷰티

- 자식 카테고리 3개 ( 명품뷰티, 스킨케어, 클린/비건뷰티 )

 

- 1차 카테고리 idx가 3

- 1차 카테고리 name이 출산/유아동

- 자식 카테고리 3개 ( 유아동패션, 기저귀, 물티슈 )

 

 

마지막으로 2차 카테고리가 같은 3차 카테고리의 정보를 2차 카테고리 정보를 담고 있는 DTO의 childCategories 멤버 변수 안에 담음

 

3차 카테고리 역시 중복을 제거한 후 3차 카테고리 1개당 DTO 하나에 담음

 

- 2차 카테고리 idx가 4

- 2차 카테고리 name이 여성패션

- 자식 카테고리 4개 ( 의류, 속옷/잠옷, 신발, 가방/잡화 )

 

- 2차 카테고리 idx가 5

- 2차 카테고리 name이 남성패션

- 2식 카테고리 3개 ( 의류, 속옷/잡화, 신발, 가방/잡화 )

 

- 2차 카테고리 idx가 6

- 2차 카테고리 name이 남녀 공용 의류

- 2식 카테고리 3개 ( 티셔츠, 맨투맨/후드티, 셔츠 )

 

...

 

 

이제 컨트롤러에서 쿼리를 사용해 결과를 받을 수 있도록 DAO 도 수정하자

package com.study.chapter04;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface CategoryDao {
	List<CategoriesDto> selectAllCategory();
}

 

 

마지막으로 계층적 구조로 담긴 모든 카테고리명을 출력하도록 컨트롤러를 수정하자

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 CategoryController {
	@Autowired
	CategoryDao categoryDao;
	
	private void printCategory(CategoriesDto category, int depth) {
		String tab = "";
		for(int i=1; i<depth; i++) {
			tab = tab + "\t";
		}
		
		System.out.println(tab + category.getName());

		List<CategoriesDto> childCategories = category.getChildCategories();
		if(childCategories != null) {
			for(CategoriesDto childCategory : childCategories) {
				printCategory(childCategory, depth+1);
			}
		}
	}
	
	@GetMapping("/chatper04/categories")
	public void printAllCategory() {
		List<CategoriesDto> allCategory = categoryDao.selectAllCategory();
		
		for(CategoriesDto category : allCategory) {
			printCategory(category, 1);
		}
	}
}

<< 코드 설명 >>

(1). 출력할 카테고리 정보

(2). 출력할 카테고리의 차수

(3). 카테고리 차수에 맞춰 tab 을 추가

(4). 현재 카테고리 명 출력

(5). 현재 카테고리의 자식 카테고리가 있다면 재귀호출 방식으로 자식 카테고리 정보 출력


계층적 데이터 구조의 경우 데이터를 가져오는 것 자체는 선형적 데이터 구조로 가져오는 방법이 쉽지만 활용할 때는 굉장히 고생함

그러나 계층적 데이터 구조로 가져오기 위해서는 어떻게 가져올 지 를 굉장히 고민해야함

 

경험상 계층적 데이터 구조는 그대로 계층적 데이터 구조로 가져오는게 전체적으로 드는 시간과 노력이 절약됨

728x90
LIST

<< 학습 목표 >>

1. set 태그를 활용할 수 있다.


set 태그로 동적 쿼리를 구성하는 방법은 where 태그와 동일함

 

그래서 여기서는 곧바로 간단한 예시 하나로 set 태그로 동적 쿼리를 구성하는 방법만 배우자

 

회원 정보를 수정하는 기능이 있는 상황임

회원 정보는 비밀번호, 닉네임, 연락처 중 하나만 수정할 수도 있고 모두 수정할 수도 있음

수정하지 않을 값은 전달 하지 않고 수정할 값믄 전달함

이런 상황에서 set 태그를 사용해 다음과 같이 동적 쿼리를 만들 수 있음

 

<?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">
	// ...
	
	<update id="updateMember" parameterType="com.study.chapter04.StudygroupMember">
		UPDATE member
		<set>
			<if test="pw != null">pw = #{pw},</if>
			<if test="nickname != null">nickname = #{nickname},</if>
			<if test="tel != null">tel = #{tel},</if>
		</set>
		WHERE id = #{id}
	</update>
	
	// ...
</mapper>

<< 코드 설명 >>

update 태그가 동작할 때 만들어질 수 있는 쿼리는 8가지임

1. 비밀번호, 닉네임, 연락처 모두 전달 받지 못했을 경우

 -> UPDATE member WHERE id = #{id}

2. 비밀번호만 전달 받았을 경우

 -> UPDATE member SET pw = #{pw} WHERE id = #{id}

3. 닉네임만 전달 받았을 경우

 -> UPDATE member SET nickname = #{nickname} WHERE id = #{id}

4. 연락처만 전달 받았을 경우

 -> UPDATE member SET 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 가 시작 되기 전에 ) , 가 없음

set 태그는 마지막에 , 를 지워줌

 

set 태그를 trim 태그로 대신 구현하면 아래와 같음

<update id="updateMember" parameterType="com.study.chapter04.StudygroupMember">
	UPDATE member
	<trim prefix="SET" suffixOverrides=",">
		<if test="pw != null">pw = #{pw},</if>
		<if test="nickname != null">nickname = #{nickname},</if>
		<if test="tel != null">tel = #{tel},</if>
	</trim>
	WHERE id = #{id}
</update>

 

728x90
LIST

<< 학습 목표 >>

1. where 태그를 활용할 수 있다.


전 글 ( https://codingaja.tistory.com/119 ) 에서 작성한 동적 쿼리 중에는 문제의 소지가 있는 쿼리가 있음

 

<?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("<< 닉네임으로 회원 정보 조회 종료 >>");
	}

	// ...
}

<< 코드 설명 >>

(1). 컨트롤러는 DAO 측으로 null 을 전달하고 있음

 

 

<< DAO >>

package com.study.chapter04;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface MemberDao {
	// ...
	
	List<SelectMemberDto> selectMember(List<SelectMemberDto> selectConditions);
	
	// ...
}

<< 코드 설명 >>

(1). 컨트롤러가 DAO로 null을 전달했으므로 DAO도 쿼리쪽으로 selectionCondition을 null로 전달함

 

 

<< 쿼리 설명 >>

쿼리에서는 null 상태인 selectionConditions 를 사용해 반복문을 동작시키려 하므로 NullPointerException이 발생할 수 있음

 

이걸 해결하는 방법은 무엇일까?

 

방법1. foreach를 if문으로 감싼다.

그러나 이 방법은 잘못된 방법임

selectCondition의 값이 null 일 때 만들어지는 쿼리가 [ SELECT * FROM WHERE nickname IN ] 이와 같으므로 SQL 문법 오류가 생겨 SQLException이 발생할 수 있음

 

방법2. WHERE 를 if문으로 감싼다.

selectConditions가 null이 아니라면 WHERE 절이 붙고 in 에 들어갈 값은 foreach문을 통해 들어가게 됨


WHERE절을 동적으로 구성할 때 위와 같이 해도 되지만 프로그램의 규모가 커져 WHERE절이 점점 더 복잡해진다면 더 이상 위와 같은 동적 쿼리로는 WHERE 절을 구성할 수 없게됨

 

복잡한 WHERE 절을 동적 쿼리로 구성할 때는 where 태그를 사용함

태그명 where
설명 WHERE 절을 동적으로 구성하고 싶을 때 사용하는 태그

WHERE 태그 안에 어떤 구문이 들어있다면 WHERE절을 붙여 해당 구문을 쿼리에 추가함
WHERE 태그 안에 아무 구문도 들어있지 않다면 WHERE절이 붙지 않음

WHERE 절 안에 어떤 구문이 AND 또는 OR 등으로 시작한다면 SQL문법에 오류가 발생하므로 이런 부분을 방지하기 위해 어떤 구문이 AND 또는 OR 등으로 시작한다면 해당 AND, OR 등만 지워줌 
속성 없음

 

우선 간단하면서 직관적인 두 예를 보며 WHERE 절에 대해서 알아보자

 

1. WHERE 태그 안에 아무 구문도 들어있지 않는 경우

이런 경우 SELECT 쿼리에 WHERE 절이 붙지 않음

따라서 만들어지는 쿼리는 [ SELECT * FROM member; ] 임

 

2. WHERE 태그 안에 어떤 구문이 들어있는 경우

이런 경우 SELECT 쿼리에 WHERE 절이 붙음

따라서 만들어지는 쿼리는 [ SELECT * FROM member WHERE nickname IN ("홍길동", "김철수", "고영희"); ] 임


이제 where태그를 본격적으로 사용해보자

본격적이라고는 했지만 굉장히 간단함

 

1. where 태그와 if 태그를 함께 사용 하는 상황

회원 정보를 검색할 때 필터 기능이 있어 아이디, 닉네임, 연락처로 회원 정보를 검색할 수 있는 상황이라고 하자

검색 조건을 아무것도 설정하지 않아 전체 회원을 조회할 수도 있고 한 항목만 사용될 수도 있고 모든 항목이 다 사용될 수도 있음

여러 항목이 검색 조건으로 사용됐을 때는 검색 조건들이 모두 OR 로 묶여야 되는 상황임

이런 상황은 where 태그와 if 태그를 사용해 해결할 수 있을 것

<select id="selectMember" parameterType="com.study.chapter04.SelectMemberDto" resultType="com.study.chapter04.SelectMemberDto">
	SELECT * FROM member
	<where>
		<if test="id != null">
			id = #{id}
		</if>
		<if test="nickname != null">
			OR nickname = #{nickname}
		</if>
		<if test="tel != null">
			OR tel = #{tel}
		</if>
	</where>
	;
</select>

<< 코드 설명 >>

위 쿼리에서 만들어질 수 있는 쿼리는 7가지임

1. 아이디, 닉네임, 연락처 모두 없을 경우

  -> SELECT * FROM member;

2. 아이디만 있을 경우

 -> SELECT * FROM member WHERE id = #{id}

3. 닉네임만 있을 경우

 -> SELECT * FROM member WHERE nickname = #{nickname}

4. 연락처만 있을 경우

 -> SELECT * FROM member WHERE tel = #{tel}

5. 아이디, 닉네임만 있을 경우

 -> SELECT * FROM member WHERE id = #{id} OR nickname = #{nickname}

6. 아이디, 닉네임, 연락처 모두 있을 경우

 -> SELECT * FROM member WHERE id = #{id} OR nickname = #{nickname} OR tel = #{tel}

7. 닉네임, 연락처만 있을 경우

 -> SELECT * FROM member WHERE nickname = #{nickname} OR tel = #{tel}

 

아이디가 있는 상황 ( 2, 5, 6 ) 에서는 당연히 id = #{id} 로 시작함

그러나 닉네임 또는 연락처가 있는 상황 ( 3, 4, 7 ) 에서는 OR 가 먼저 와야 할 것같지만 맨 앞에 있는 OR 는 삭제 됐음

where 태그는 맨 앞에 AND 또는 OR 가 오면 삭제함

 

trim 태그를 사용해서 where 태그와 똑같이 표현할 수 있음

<select id="selectMember" parameterType="com.study.chapter04.SelectMemberDto" resultType="com.study.chapter04.SelectMemberDto">
	SELECT * FROM member
	<trim prefix="WHERE" prefixOverrides="AND | OR">
		<if test="id != null">
			id = #{id}
		</if>
		<if test="nickname != null">
			OR nickname = #{nickname}
		</if>
		<if test="tel != null">
			OR tel = #{tel}
		</if>
	</trim>
	;
</select>

 

 

2. where 태그와 foreach 태그를 함께 사용하는 상황

회원 정보를 검색할 때 필터 기능이 있어 닉네임들로 회원 정보를 검색할 수 있는 상황이라고 하자

닉네임을 입력하지 않고 전체 회원 정보를 검색할 수도 있고 닉네임을 하나만 입력해 한 회원의 정보만 검색할 수도 있으며 여러 닉네임을 입력해 여러 회원의 정보를 검색할 수도 있음

이런 상황은 where 태그와 if, foreach 태그를 함께 사용해 해결 할 수 있을 것

<select id="selectMember" parameterType="com.study.chapter04.SelectMemberDto" resultType="com.study.chapter04.SelectMemberDto">
	SELECT * FROM member
	<where>
		<if test="nicknames != null">
			nickname IN
			<foreach collection="nicknames" item="nickname" open="(" close=")" separator=", ">
				#{nickname}
			</foreach>
		</if>
	</where>
	;
</select>

여기까지 where 태그를 활용하는 방법을 알아봤음

여기서 알아본 방법 외에도 전 글 ( https://codingaja.tistory.com/119 ) 에서 배운 동적 쿼리와 함께 사용하면 만들고 싶은 모든 쿼리를 다 만들 수 있음

 

또한 여기서는 SELECT 쿼리에서만 where 태그 를 사용했지만

UPDATE, DELETE 역시 WHERE 절이 있으므로 UPDATE, DELETE 에서도 where 태그를 활용할 수 있음

728x90
LIST