항해99/실전프로젝트

항해99 - 실전 프로젝트 06(API 제작 - JPA)

연어조아 2021. 12. 15. 00:18
728x90

    @GetMapping("/detail")
    public ResponseDto detailBoard(@RequestParam Long boardId){
        DetailBoardDto detailBoardDto = boardService.getDetailBoard(boardId);

        return new ResponseDto(200L, "성공", detailBoardDto);
    }

실전프로젝트 초기에는 SQL query 문에 익숙하지 않았기에 설계된 API대로 API를 제작할 때는

JpaRepository 인터페이스를 이용하여 데이터를 CURD 처리하였다.


도메인 설계


도메인 설계


스프링 3계층


스프링 3계층

컨트롤러(Presentation Layer)


클라이언트에서 요청된 요구사항을 서버측에서 받아준다.

요청에 따라 어떤 처리를 할지 결정해주며, 실질적인 처리는 비지니스 계층에서 담당하게 된다.

 

본 프로젝트에서는 restful api의 query parmeter나 body의 데이터를 객체로 받아왔었다.

 

서비스(Business Layer)


애플리케이션 비즈니스 로직 처리와 비즈니스와 관련된 도메인 모델의 적합성 검증
프레젠테이션 계층과 데이터 엑세스 계층 사이를 연결하는 역할로서 두 계층이 직접적으로 통신하지 않게 한다.

 

본 프로젝트에서는 리포지토리 계층에서 가져온 엔티티를

API의 response data type에 맞게 DTO를 만들고 DTO의 내용을 채워넣었엇다.

 

리포지토리(Data Access Layer)


ORM (Mybatis, Hibernate)를 주로 사용하는 계층
Dabase에 data를 CRUD(Create, Read, Update, Drop)하는 계층

 

본 프로젝트에서는 리포지토리 계층에서 엔티티를 가져오는 역활을 하였다.


예시 API


추천 장소에 대한 자세한 정보를 받아오는 api

 

예시 api를 사용한 페이지

 

추천하는 장소에 관한 상세정보를 가져오는 api이다.


01. DTO 제작


DTO(Data Transfer Object

DB Layer와 View Layer 사이의 역할을 분리 하기 위해서 사용

 

Entity 클래스 실제 테이블과 매핑되어 만일 변경되게 되면 여러 다른 클래스에 영향을 끼치고, DTO 클래스 View와 통신하며 자주 변경되므로 분리 해주어야 한다.

 

각 계층간 데이터 교환을 위한 객체 (데이터를 주고 받을 포맷)

 

Domain, VO라고도 부름

 

DB에서 데이터를 얻어 Service, Controller 등으로 보낼 때 사용함

 

로직을 갖지 않고 순수하게 getter, setter 메소드를 가진다.

response Data

일단 Response Data 안에 너무 많은 내용이 있어서 한번에 객체로 만드는 것은 무리가 있다.

그렇기에 위 사진처럼 여러 Dto로 나누고 점진적으로 하나로 묶어서 반환하기로 했다.

 

DetailWeatherList

DetailWeatherList

@Getter
@Setter
public class DetailWeatherList {
    private String time;
    private Long rainPercent;
    private String weather;
    private Long humidity;
    private Long temperature;
    private Long dust;

    public DetailWeatherList(String time, Long rainPercent, String weather, Long humidity, Long temperature, Long dust) {
        this.time = time;
        this.rainPercent = rainPercent;
        this.weather = weather;
        this.humidity = humidity;
        this.temperature = temperature;
        this.dust = dust;
    }
}

해당 장소의 시간대별 날씨 정보가 들어가 있다

 

@Getter : 롬복 애노테이션 getter 함수를 자동으로 만들어준다.

 

@Setter : 롬복 애노테이션 setter 함수를 자동으로 만들어준다.

 

DetailWeatherDto

DetailWeatherCityInfoDto

@Getter
@Setter
@AllArgsConstructor
public class DetailWeatherDto {
    private String cityName;
    private String date;
    private Long starGazing;
    private String moonrise;
    private String moonset;
    private List<DetailWeatherList> weatherList;
}

해당 장소의 월출, 월몰, 별관측지수와 날씨 정보를 리스트로 가지고 있다.

 

@AllArgsConstructor : 롬복 애노테이션 전체 필드를 파라미터로 가진 생성자를 만든다.

DetailBoardDto

DetailBoardDto

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class DetailBoardDto {
    private Long id;
    private String writer;
    private String title;
    private String address;
    private String img;
    private String content;
    private Double x_location;
    private Double y_location;
    private DetailWeatherDto weather;
}

추천 장소글의 글쓴이 닉네임, 제목, 장소의 주소, 썸내일, 본문 등이 들어있다.

 

ResponseDto

response Data

@Getter
@Setter
@AllArgsConstructor
public class ResponseDto<T> {
    private Long code;
    private String msg;
    private T data;
}

반환 데이터에 대한 응답 코드와 메시지가 같이 들어있다.

 

T data : 제네릭을 사용하여 어떤 DTO 객체 등 대응 할 수 있게 되었다

 

본래는 http response header에 상태코드 등이 들어있지만 좀 더 직관적인 정보 제공을 위해서

프론트 팀과 의논하고 모든 api는 위의 상태로 반환하도록 설계하였다.

 

위 처럼 작은 단위로 DTO를 자른 뒤 다음 단위의 DTO가 해당하는 DTO를 포함하고 있다.


Controller


@RestController
@RequiredArgsConstructor
public class BoardController {
    private final BoardService boardService;

    @GetMapping("/detail")
    public ResponseDto detailBoard(@RequestParam Long boardId){
        DetailBoardDto detailBoardDto = boardService.getDetailBoard(boardId);

        return new ResponseDto(200L, "성공", detailBoardDto);
    }
}

 

@RestController : @Controller에 @ResponseBody가 결합된 어노테이션,

객체를 반환하면, JSON 형식으로 반환해준다.

 

@RequiredArgsConstructor :  final 키워드가 붙은 필드를 모아서 생성자를 자동으로 만들어 준다. BoardService 자동주입을 위해 생성

 

@RequestParam : 변수이름으로 들어온 querystring 데이터를 변수에 대입해준다.

 

DetailBoardDto detailBoardDto = boardService.getDetailBoard(boardId)

가져온 게시판 id 값과 함께 실질적인 처리는 서비스 계층에 맡기게 된다.

 

return new ResponseDto(200L, "성공", detailBoardDto) : ResponseDto 객체 형식으로 JSON 반환


Service


public DetailBoardDto getDetailBoard(Long id) {
        Board findBoard = boardRepository.findById(id).orElseThrow(
                () -> new NullPointerException("해당하는 게시글이 존재하지 않습니다.")
        );

        findBoard = getCampingOrUserMake(findBoard);
        List<Weather> weatherList = findBoard.getLocation().getWeatherList();
        Location findBoardLocation = findBoard.getLocation();
        Star findStar = findBoardLocation.getStar();


        List<DetailWeatherWeatherList> detailWeatherWeatherLists = new ArrayList<>();
        for (Weather weather : weatherList) {
            DetailWeatherWeatherList detailWeatherWeatherList = new DetailWeatherWeatherList(
                    weather.getPredictTime(),
                    Long.valueOf(weather.getRainPercent()),
                    weather.getWeather(),
                    Long.valueOf(weather.getHumidity()),
                    Long.valueOf(weather.getTemperature()),
                    Long.valueOf(weather.getDust())
            );
            detailWeatherWeatherLists.add(detailWeatherWeatherList);
        }

        DetailWeatherDto newDetailBoardDto = new DetailWeatherDto(
                findBoardLocation.getCityName(),
                Timestamped.getCurrentTime().get(3),
                findStar.getStarGazing(),
                findStar.getMoonrise(),
                findStar.getMoonSet(),
                detailWeatherWeatherLists
        );
        DetailBoardDto detailBoardDto = new DetailBoardDto(
                findBoard.getId(),
                findBoard.getUser().getNickname(),
                findBoard.getTitle(),
                findBoard.getAddress(),
                findBoard.getImg(),
                findBoard.getContent(),
                findBoard.getLongitude(),   //경도
                findBoard.getLatitude(),     //위도,
                newDetailBoardDto
        );

        return detailBoardDto;
    }

전체 코드

Board findBoard = boardRepository.findById(id).orElseThrow(
                () -> new NullPointerException("해당하는 게시글이 존재하지 않습니다.")
        );

게시판 id에 해당하는 게시판 Entity 정보를 불러온다.

해당하는 id 가 없다면 Exception이 터진다.

예외처리는 다음 포스팅 예정

 

 findBoard = getCampingOrUserMake(findBoard);
 List<Weather> weatherList = findBoard.getLocation().getWeatherList();
 Location findBoardLocation = findBoard.getLocation();
 Star findStar = findBoardLocation.getStar();

가져온 게시판 Entity 정보를 기준으로 JPA 연관관계를 통해서 DTO에 필요한 Enitity들을 모두 불러온다.

 

List<DetailWeatherList> detailWeatherWeatherLists = new ArrayList<>();

for (Weather weather : weatherList) {
    DetailWeatherList detailWeatherList = new DetailWeatherList(
        weather.getPredictTime(),
        Long.valueOf(weather.getRainPercent()),
        weather.getWeather(),
        Long.valueOf(weather.getHumidity()),
        Long.valueOf(weather.getTemperature()),
        Long.valueOf(weather.getDust())
    );
    
    detailWeatherWeatherLists.add(detailWeatherWeatherList);
}

DetailWeatherList를 만드는 과정 List 형식이기 때문에 ArraryList로 선언하고

for 문을 통해서 모든 weather 데이터를 가져온 뒤 List에 추가한다.

 

DetailWeatherDto newDetailBoardDto = new DetailWeatherDto(
    findBoardLocation.getCityName(),
    Timestamped.getCurrentTime().get(3),
    findStar.getStarGazing(),
    findStar.getMoonrise(),
    findStar.getMoonSet(),
    detailWeatherWeatherLists
 );

DetailWeatherDto를 만든다. 앞에서 만든 List를 넣어주면서 자연스럽게 앞에 리스트를 포함하게 된다.

 

DetailBoardDto detailBoardDto = new DetailBoardDto(
                findBoard.getId(),
                findBoard.getUser().getNickname(),
                findBoard.getTitle(),
                findBoard.getAddress(),
                findBoard.getImg(),
                findBoard.getContent(),
                findBoard.getLongitude(),   //경도
                findBoard.getLatitude(),     //위도,
                newDetailBoardDto
        );

대망의 마지막 DetailBoardDto이다. 앞에서 만든 DetailWeatherDto 객체를 넣어서 만들어 준다.

이 형식을 따르면 아무리 어려운 JSON 반환 타입도 차근차근 쉽게 만들 수 있다.

 


Repository


@Repository
public interface BoardRepository extends JpaRepository<Board, Long> {

}

JpaRepository 빈 객체를 받을 수 있는 Repository를 만든다.

 

 

 


단점


1. 하나의 api에 너무 많이 날라가는 query문

 

sql문을 사용해서 데이터를 조회하는 것이 아닌 게시판 entity를 불러온 후

jpa 연관관계를 통해서 dto와 관련된 모든 entity를 조회화고 dto를 조립하는

방식이라 나가는 query 문이 굉장히 많다.

 

한 개의 api에 나가는 query문 목록

2. 속도가 느리다.

 

query문이 많이 나가기에 자연스럽게 속도가 느려진다.

 

그러나 sql문을 바로 작성하여 JpaRepository에 작성하기에는 숙련될 시간이 매우 적다.

현재 이 프로젝트는 6주라는 짧은 시간 내에 모든 기능을 구현하고 서비스 까지 해야 하기에 시간이 빠듯하다.

 

그렇기때문에...

 


해결


QueryDsl를 사용하기로 하였다.

@Override
    public DetailBoardDto findDetailBoardByBoard(Long id, UserDetailsImpl userDetails) {
        DetailBoardDto result = queryFactory
                .select(
                        new QDetailBoardDto(
                                board.id,
                                board.modifiedAt,
                                user.nickname,
                                board.title,
                                board.address,
                                board.img,
                                board.content,
                                board.longitude,
                                board.latitude,
                                (userDetails == null) ? setFalse() : distinguishLikeExistUser(userDetails.getUser()),
                                boardLikeCount(),
                                (userDetails == null) ? setFalse() : distinguishBookmarkExistUser(userDetails.getUser())

                        )
                )
                .from(board)
                .join(board.user, user)
                .where(board.id.eq(id))
                .fetchOne();

        return result;
    }

QueryDsl를 사용하게 되면

  1. 문자가 아닌 코드로 쿼리를 작성함으로써, 컴파일 시점에 문법 오류를 쉽게 확인할 수 있다.
  2. 자동 완성 등 IDE의 도움을 받을 수 있다.
  3. 동적인 쿼리 작성이 편리하다.
  4. 쿼리 작성 시 제약 조건 등을 메서드 추출을 통해 재사용할 수 있다.

그렇기 때문에 후에 위의 API 들은 모두 QueryDsl 코드로 교체하였다.

 


참고


https://devlog-wjdrbs96.tistory.com/209

 

[Spring] 스프링 웹 계층이란?

이번 글에서는 스프링은 어떤 계층이 존재하는지와 계층의 역할을 무엇인지, 프로젝트시 패키지를 어떻게 나누는 것이 좋은지에 대해 정리해보려 한다. 스프링의 계층은 Presentation Layer, Business,

devlog-wjdrbs96.tistory.com


깃허브 : https://github.com/salmon2/StarProject

 

GitHub - salmon2/StarProject

Contribute to salmon2/StarProject development by creating an account on GitHub.

github.com

프로젝트 홈페이지 : https://stellakorea.co.kr/