현재 제가 하고 있는 프로젝트는 "식당 메뉴 리뷰 어플리케이션"입니다.
식당 리뷰 앱에 메뉴 리뷰까지 있는 경우는 많이 없기 때문에 식당에 가서도 뭘 먹을까 고민할 때가 있습니다.
식당 리뷰를 뒤져서 사람들이 뭘 많이 먹는지 찾아보곤 하죠ㅎㅎ
오늘 구현해 볼 부분은 검색창에 지역을 검색했을 때 해당 지역에 위치한 식당들만 불러오는 API를 만드는 것입니다.
앱에서 특정 키워드를 검색할 경우 해당 키워드를 파라미터로 받아서 쿼리로 필터를 적용하여 키워드에 해당하는 데이터만 받아오도록 해보겠습니다.
필요한 파일들입니다.
전체 파일구조는 다음과 같습니다.
config 폴더와 src 폴더의 restaurant 폴더만 동일하게 구성하면 됩니다.
먼저 src > restaurant 폴더를 구현해 보겠습니다.
--RestaurantController.java--
package kr.co.metro.src.restaurant;
import kr.co.metro.config.BaseException;
import kr.co.metro.config.BaseResponse;
import kr.co.metro.src.restaurant.model.GetRestaurantRes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/restaurants") //기본이 될 URI를 지정해줍니다.
public class RestaurantController {
//로그를 받기 위해 선언해줍니다.
final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private final RestaurantProvider restaurantProvider;
public RestaurantController(RestaurantProvider restaurantProvider) {
this.restaurantProvider = restaurantProvider;
}
/**
* 식당 전체 조회 (유저)
* ( 주소 검색에 따른 식당 조회 )
* [GET] /restaurants
* [GET] /restaurants?addressKeyword=
* @return BaseResponse<List<GetRestaurantRes>>
*/
@ResponseBody
@GetMapping("") // (GET) 127.0.0.1:8080/restaurants
public BaseResponse<List<GetRestaurantRes>> getRestaurant(@RequestParam(required = false) String addressKeyword){
try{
// 만약 키워드가 space를 포함하고 있으면
if (addressKeyword.contains(" ")){
// 주소 파라미터가 space를 포함하고 있다는 에러 메세지를 return 합니다.
return new BaseResponse<>(POST_ADDRESS_PARAM_CONTAINS_SPACE);
// BaseResponse는 BaseResponse 클래스를 생성하여 만들어 주었는데, 그냥 "에러메세지"를 해당 위치에 넣어도 좋습니다.
}
// 만약 addressKeyword가 존재하면
if (addressKeyword != null) {
// addressKeyword를 가지고 Provider의 getRestaurantByAddressKeyword 클래스로 이동합니다.
List<GetRestaurantRes> getRestaurantRes = restaurantProvider.getRestaurantByAddressKeyword(addressKeyword);
// 윗 줄에서 provider의 getRestaurantByAddressKeyword 클래스로부터 리턴 받은 getRestaurantRes를 반환합니다.
return new BaseResponse<>(getRestaurantRes);
// 만약 addressKeyword가 존재하지 않으면
} List<GetRestaurantRes> getRestaurantRes = restaurantProvider.getRestaurant();
// provider의 getRestaurant 클래스로부터 리턴받은 getRestaurantRes를 반환합니다.
return new BaseResponse<>(getRestaurantRes);
} catch (BaseException exception) {
return new BaseResponse<>((exception.getStatus()));
}
}
/**
* 로그 테스트 API
* [GET] /test/log
* @return String
*/
@ResponseBody
@GetMapping("/log")
public String getAll() {
System.out.println("테스트");
// trace, debug 레벨은 Console X, 파일 로깅 X
// logger.trace("TRACE Level 테스트");
// logger.debug("DEBUG Level 테스트");
// info 레벨은 Console 로깅 O, 파일 로깅 X
logger.info("INFO Level 테스트");
// warn 레벨은 Console 로깅 O, 파일 로깅 O
logger.warn("Warn Level 테스트");
// error 레벨은 Console 로깅 O, 파일 로깅 O (app.log 뿐만 아니라 error.log 에도 로깅 됨)
// app.log 와 error.log 는 날짜가 바뀌면 자동으로 *.gz 으로 압축 백업됨
logger.error("ERROR Level 테스트");
return "Success Test";
}
}
--RestaurantProvider.java--
package kr.co.metro.src.restaurant;
import kr.co.metro.config.BaseException;
import kr.co.metro.src.restaurant.model.GetRestaurantRes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
import static kr.co.geoplan.metro.config.BaseResponseStatus.DATABASE_ERROR;
@Service
public class RestaurantProvider {
private final RestaurantDao restaurantDao;
final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
public RestaurantProvider(RestaurantDao restaurantDao) {
this.restaurantDao = restaurantDao;
}
/**
* 식당 전체 조회
* [GET] /restaurants/:userIdx
* @return BaseResponse<List<GetRestaurantRes>>
*/
public List<GetRestaurantRes> getRestaurant() throws BaseException {
try{
List<GetRestaurantRes> getRestaurantRes = restaurantDao.getRestaurant();
return getRestaurantRes;
} catch (Exception exception) {
exception.printStackTrace();
throw new BaseException(DATABASE_ERROR);
}
}
// addressKeyword
/**
* 주소 검색에 따른 식당 조회
* [GET] /restaurants/:userIdx?addressKeyword=
* @return BaseResponse<List<GetRestaurantRes>>
*/
public List<GetRestaurantRes> getRestaurantByAddressKeyword(String addressKeyword) throws BaseException {
try{
// controller에서 가져온 addressKeyword를 가지고 dao의 getRestaurantByAddressKeyword 클래스로 갑니다.
List<GetRestaurantRes> getRestaurantRes = restaurantDao.getRestaurantByAddressKeyword(addressKeyword);
return getRestaurantRes;
} catch (Exception exception) {
exception.printStackTrace();
throw new BaseException(DATABASE_ERROR);
}
}
/**
* 로그 테스트 API
* [GET] /test/log
* @return String
*/
@ResponseBody
@GetMapping("/log")
public String getAll() {
System.out.println("테스트");
// trace, debug 레벨은 Console X, 파일 로깅 X
// logger.trace("TRACE Level 테스트");
// logger.debug("DEBUG Level 테스트");
// info 레벨은 Console 로깅 O, 파일 로깅 X
logger.info("INFO Level 테스트");
// warn 레벨은 Console 로깅 O, 파일 로깅 O
logger.warn("Warn Level 테스트");
// error 레벨은 Console 로깅 O, 파일 로깅 O (app.log 뿐만 아니라 error.log 에도 로깅 됨)
// app.log 와 error.log 는 날짜가 바뀌면 자동으로 *.gz 으로 압축 백업됨
logger.error("ERROR Level 테스트");
return "Success Test";
}
}
--RestaurantDao.java--
package kr.co.metro.src.restaurant;
import kr.co.geoplan.metro.src.restaurant.model.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import javax.sql.DataSource;
import java.util.List;
@Repository
public class RestaurantDao {
private JdbcTemplate jdbcTemplate;
private List<GetResMenuRes> getResMenuResList;
@Autowired
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
/**
* 식당 전체 조회 (유저)
* [GET] /restaurants/:userIdx
* @return BaseResponse<GetRestaurantRes>
* Query: user 주소를 기준으로
*/
public List<GetRestaurantRes> getRestaurant() {
String getRestaurantQuery = "SELECT resName, resInfo, resAddress,resLat, resLon,\n" +
" round(SUM(rc.resScore)/COUNT(rc.id),1) as scoreAvg,\n" +
" LEFT JOIN tb_foodly_restaurant_comment rc ON r.id = rc.resId\n" +
" , tb_foodly_user u\n" +
" WHERE r.status = 'Y'" +
"GROUP BY rc.resId\n" +
"ORDER BY scoreAvg;";
int getRestaurantParams = userIdx;
return this.jdbcTemplate.query(getRestaurantQuery,
(rs, rowNum)-> new GetRestaurantRes(
rs.getString("resName"),
rs.getString("resInfo"),
rs.getString("resAddress"),
rs.getDouble("resLat"),
rs.getDouble("resLon"),
rs.getDouble("scoreAvg"),
));
}
/**
* 주소 검색에 따른 식당 조회
* [GET] /restaurants/:userIdx?addressKeyword=
* @return BaseResponse<List<GetRestaurantRes>>
*/
public List<GetRestaurantRes> getRestaurantByAddressKeyword(String addressKeyword) {
String getRestaurantQuery = "SELECT resName, resInfo, resAddress\n" +
" , resLat, resLon,\n" +
" round(SUM(rc.resScore)/COUNT(rc.id),1) as scoreAvg,\n" +
" FROM tb_foodly_restaurant r\n" +
" LEFT JOIN tb_foodly_restaurant_comment rc\n" +
" ON r.id = rc.resId, tb_foodly_user u\n" +
" WHERE r.status = 'Y' AND INSTR(resAddress, ?) > 0\n" +
" GROUP BY rc.resId\n" +
" ORDER BY scoreAvg;";
String getAddressParams = addressKeyword;
return this.jdbcTemplate.query(getRestaurantQuery,
(rs, rowNum)-> new GetRestaurantRes(
rs.getString("resName"),
rs.getString("resInfo"),
rs.getString("resAddress"),
rs.getDouble("resLat"),
rs.getDouble("resLon"),
rs.getDouble("scoreAvg"),
),
getAddressParams);
}
}
QUERY
주소 키워드를 다루는 쿼리는 다음과 같습니다.
SELECT resName, resInfo, resAddress, resLat, resLon,
round(SUM(rc.resScore)/COUNT(rc.id),1) as scoreAvg
FROM tb_foodly_restaurant r
LEFT JOIN tb_foodly_restaurant_comment rc
ON r.id = rc.resId, tb_foodly_user u
WHERE r.status = 'Y' AND INSTR(resAddress, ?) > 0
GROUP BY rc.resId
ORDER BY scoreAvg;
(FROM)
레스토랑 테이블과 레스토랑 리뷰 테이블을 조인한 것으로부터,
(SELECT)
레스토랑의 이름, 정보, 주소, 위도, 경도, 평점평균을 가져온다.
(WHERE)
레스토랑의 상태는 'Y'(Y:등록된 레스토랑. N: 삭제된 레스토랑이다.) 이어야 한다.
그리고 레스토랑의 주소는 물음표를 통해 받아온 파라미터값(addressKeyword)를 포함해야 한다.
INSTR
INSTR("ABCDE", "CD") : 문자열 "CD"가 문자열 "ABCDS" 안에 존재하면 1이상의 값을, 존재하지 않으면 0을 반환한다.
INSTR은 찾고자 하는 값이 INDEX 값일 때, LIKE를 사용하는 것보다 많은 시간을 소모하므로 주의해야 한다.
하지만 나의 경우 INDEX 값은 아니고 LIKE를 사용했을 때 PARAMETER를 어떻게 받아와야 할지 더 공부가 필요할 것 같아서 INSTR를 사용했다.
(GROUP BY)
평점평균을 구할 때 레스토랑 별로 구해야 하기 때문에 레스토랑 인덱스값으로 그룹지어준다.
(ORDER BY)
평점평균 순서로 정렬한다.
--GetRestaurantRes.java--
서버가 클라이언트에게 반환 할 Response 파일입니다.
Res는 Response의 준말이다. 레스토랑 정보를 가져올 Response 라는 뜻으로 이름 지었습니다.
package kr.co.metro.src.restaurant.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.jetbrains.annotations.NotNull;
@NotNull
@Data
@AllArgsConstructor
public class GetRestaurantRes {
String resName;
String resInfo;
String resAddress;
Double resLat;
Double resLon;
Double scoreAvg;
}
@Data 어노테이션
원래는 Getter, Setter 어노테이션을 사용하고 있었으나, 사수님 환경에서 에러가 뜬다고 하셔서 Data로 변경했습니다.
뭐가 문제인지는 추후에 공부해봐야 겠습니다.
@AllArgsConstructor 어노테이션
모든 필드에 대한 생성자를 생성합니다. 또한 의존성 주입 할 대상이 많아졌을 때 훨씬 깔끔합니다.
config 폴더를 구현해보겠습니다.
--BaseException.java--
package kr.co.metro.config;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
public class BaseException extends Exception {
private BaseResponseStatus status;
}
--BaseResponse.java--
package kr.co.metro.config;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.AllArgsConstructor;
import lombok.Getter;
import static kr.co.metro.config.BaseResponseStatus.SUCCESS;
@Getter
@AllArgsConstructor
@JsonPropertyOrder({"isSuccess", "code", "message", "result"})
public class BaseResponse<T> {
@JsonProperty("isSuccess")
private final Boolean isSuccess;
private final String message;
private final int code;
@JsonInclude(JsonInclude.Include.NON_NULL)
private T result;
// 요청에 성공한 경우
public BaseResponse(T result) {
this.isSuccess = SUCCESS.isSuccess();
this.message = SUCCESS.getMessage();
this.code = SUCCESS.getCode();
this.result = result;
}
// 요청에 실패한 경우
public BaseResponse(BaseResponseStatus status) {
this.isSuccess = status.isSuccess();
this.message = status.getMessage();
this.code = status.getCode();
}
}
--BaseResponseStatus.java--
package kr.co.metro.config;
import lombok.Getter;
/**
* 에러 코드 관리
*/
@Getter
public enum BaseResponseStatus {
/**
* 1000 : 요청 성공
*/
SUCCESS(true, 1000, "요청에 성공하였습니다."),
/**
* 2000 : Request 오류
*/
// Common
REQUEST_ERROR(false, 2000, "입력값을 확인해주세요."),
// [GET] /restaurants
POST_ADDRESS_PARAM_CONTAINS_SPACE(false,2060,"띄어쓰기를 제거하고 명사 형태로 검색하세요(예: 강남, 삼성)"),
/**
* 3000 : Response 오류
*/
// Common
RESPONSE_ERROR(false, 3000, "값을 불러오는데 실패하였습니다."),
/**
* 4000 : Database, Server 오류
*/
DATABASE_ERROR(false, 4000, "데이터베이스 연결에 실패하였습니다."),
SERVER_ERROR(false, 4001, "서버와의 연결에 실패하였습니다."),
private final boolean isSuccess;
private final int code;
private final String message;
private BaseResponseStatus(boolean isSuccess, int code, String message) {
this.isSuccess = isSuccess;
this.code = code;
this.message = message;
}
}
오류가 있다면 먼저 build.gradle 의 dependencies 를 확인하세요!
코드에 오류가 있다면 댓글로 알려주세요:)
더 좋은 방법, 더 좋은 쿼리가 있다면 추천해 주세요!
공부하는 개발자 디벨로폴리입니다.
'STUDY > 서버' 카테고리의 다른 글
[TI/Spring] 스프링이란? EJB와 비교 (0) | 2022.02.17 |
---|---|
백엔드 개발자 기술면접 질문 정리 (0) | 2022.01.27 |
[SERVER][HTTP, AJAX 통신, WebSocket, SSE] 특징, 장단점 (0) | 2022.01.20 |
[SERVER][DB] 데이터베이스 성능 향상을 위한 방법 (0) | 2022.01.20 |
[SERVER] AWS 서버 구축 + WinSCP로 EC2 접속 A to Z (0) | 2021.12.01 |