tuter77

게시판 만들기(4) - 데이터를 입력하여 CRUD구현. 본문

GroupStudy

게시판 만들기(4) - 데이터를 입력하여 CRUD구현.

tuter77 2023. 1. 29. 20:21

 healthInfo 데이터를 이용한 게시판 CRUD 구현공부.

 

▷ 배치파일 데이터를 내 DB에 저장. 

- 팀장님이 공유해준 HealthInfo 배치파일을 인텔리제이에서 실행해 이미 생성되어있는 내 Mysql DB에 저장하는 과정을 제일 먼저하기로 했다. 

사실 어제 밤부터 이 파트를 진행했는데, DB데이터타입 오류로 계속해서 해결이 되지않아 오늘까지 미뤄졌다.

팀장님과 조원님의 도움으로 yml파일 내에 db url지정이 달랐다는 것을 알게 되어 오류를 해결하게 되었다.

위의 사진에서 보면 url에 mysql 내 study 스키마에 접근하게 되어있는데, 나는 db에 해당 스키마가 없었고 db와 연결되지 않은 상태에서 인텔리제이 내의 db navigator에 study 스키마를 생성하는 등의 뻘짓을 했다.

또한 아래 유저네임과 패스워드를 맞춰주고, ddl-auto란을 다르게 기재해야한다는것을 알았다.

스키마가 없는 상태에서는 update가 아닌 create를 작성해주어야 했고, 나는 mysql에 cmd로 접속해 create database study; 구문으로 새로 db를 생성했기 때문에 update로 진행했다.

 

 

이후 해당 application 파일을 실행해주면, db 내에 배치파일의 정보가 쭉 들어온다. 

 

 

▷ 각 클래스 및 인터페이스 파일 생성.

- 앞서 작성하던 board 파일들은 새로운 db의 등장으로 더이상 활용하지 않게 되었다.때문에 도메인, 레포지토리, 메인컨트롤러, 서비스, DTO 등의 파일들을 모두 새로 생성하여 작성해주어야했다.(여기에 CRUD 기능구현까지)물론 기존에 작성된 코드들을 참고하면 되기에 공부한다는 마음으로 접근했다.다만, 기존의 board는 데이터값이 id, title, content 3가지였던 반면 이 healthInfo의 데이터값은 id, brand_name, land_name, road_name, category로 5가지이기때문에 해당 정보를 반영하게끔 코드를 수정해야 한다.

 

 

위와같이 해당 클래스 및 인터페이스들을 생성했는데 다행히 팀장님이 HealthInfo Domain과 HealthInfoRefository파일은 미리 만들어주셨다.

 

▷HealthInfo 도메인, HealthInfoRepository 공부

- HealthInfo 도메인에는 각 엔티티에 포함될 속성값들이 들어있다.

 

 

위의 사진에서 볼 수 있듯, HealthInfo클래스를 생성하고 각 속성들을 private로 선언되어있다.

여기서 @id와 @GeneratedValue 어노테이션을 잠시 살펴보면 아래와 같다.

@Id //기본키 지정. @colomn을 지정하지않으면 열이름을 기본키 속성 또는 필드의 이름으로 가정.

@GeneratedValue(strategy = GenerationType.IDENTITY)
/*@GenerationValue는 PK 값에 대한 생성 전략을 제공한다.@id와 함께 엔티티 또는 매핑된
슈퍼클래스의 기본키 속성또는 필드에 적용가능하다.
generationType은 table, sequesce, identity, auto등이 있는데 여기선 identity로 지정되었다.
즉, 기본키 생성을 데이터베이스에 위임하는 것. mysql을 사용하기 때문에 auto_increment가 사용되어
기본키를 생성.
 */

정리하면 기본키를 mysql이 자동으로 생성하고 증가시키는 것이다.

 

- 다음으로 HealthInfoRepository 를 살펴보았다.

package com.example.demo.repository;

import com.example.demo.domain.HealthInfo;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

public interface HealthRepository extends JpaRepository<HealthInfo,Long> {
}

  위 코드엔 repository는 JpaRepository를 상속한다는 구문이 나와있다. (이부분은 boardRepository와 동일)

또한 위의 import 를 보면 HealthInfo Domain, JpaRepository를 참조하고 있는것을 볼 수 있다. 

상속에서 <> 는 상위클래스를 나타내 주는데 HealthInfoRepository 의 상위 클래스인 HealthInfo와 ID 타입인 Long이 지정되어있다.

 

 

▷ HealthInfoService 파일 작성.

package com.example.demo.service;

import com.example.demo.data.request.HealthInfoRequestDTO;
import com.example.demo.data.response.HealthInfoResponseDTO;
import com.example.demo.domain.HealthInfo;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface HealthInfoService {


    public HealthInfoResponseDTO read(Long id);
    public boolean saveHealth(HealthInfoRequestDTO healthInfoRequestDTO);
    public boolean updateHealth(HealthInfoRequestDTO healthInfoRequestDTO);
    public boolean deleteHealth(Long id);

    Page<HealthInfo> healthInfoList(Pageable pageable); //페이징 처리를 위한 구문

}

HealthInfoService 인터페이스에서는 각 변수들이 정의 되어있다. 

위의 HealthInfoResponseDTO read(Long id);는 아직 작성하지 않았지만, 후에 CRUD의 read기능에 DTO타입의 id를 읽어오기위해 필요하기 때문에 작성되어있다.아래는 각 저장, 수정, 삭제 기능에 필요한 변수들이 healthInfoRequestDTO를 매개변수로 받아 boolean타입을 반환하는 모습이다. 맨 아래엔 페이징 처리에 필요한 변수를 정의 해놓았다. Pageable 타입의 pageable을 변수로 받기 위한 변수이므로 후의 Controller에서 pageable을 healthInfoList로 넘겨받을 것이다.

완벽하게 이해한 구문은 아니지만 동작은 이렇게 된다고 한다.

/*Page 클래스의 메타데이터(페이 크기 총 페이지 수 등)를 바탕으로 Pageable이라는 인터페이스를 통해 각 페이지의 번호, 크기 및 정렬을 지정한다. <HealthInfo>는 도메인을 통해 페이징 정보를 기반으로 페이를 반환하는 저장소 메서드다.   

*/ (수정요망)

 

 

▷ HealthInfoRequestDTO 코드 작성.

@Builder
@Setter
@AllArgsConstructor
@ToString
public class HealthInfoRequestDTO {
    //HealthInfo 데이터에 맞춘 변수선언
    private Long id;
    private String brand_name;
    private String land_number;
    private String road_number;
    private String category;

    public static HealthInfo toEntity(HealthInfoRequestDTO dto){
        return HealthInfo.builder()
                .id(dto.getId())
                .brand_name(dto.getBrand_name())
                .land_number(dto.getLand_number())
                .road_number(dto.getRoad_number())
                .category(dto.getCategory())
                .build();
    }

    public Long getId(){
        return this.id;
    }

    public String getBrand_name(){
        return this.brand_name;
    }

    public String getLand_number(){
        return this.land_number;
    }

    public String getRoad_number(){
        return this.road_number;
    }

    public String getCategory(){
        return this.category;
    }

 
    public static HealthInfoRequestDTO of(String brand_name,String land_number,
                                          String road_number, String category){
        return new HealthInfoRequestDTO(1L, brand_name, land_number, road_number, category);
    }
}

DTO는 도메인 모델 속성이 외부에 노출되는 보안문제(변형, 왜곡 등)를 해결하기 위해 활용된다.

이처럼 클라이언트와 컨트롤러, 컨트롤러와 서비스, 서비스와 레포지토리 간의 데이터 이동에서 데이터가 온전히 보전될 수 있도록, 캡슐화 하는 클래스라고 이해했다.

(DB와 레포지토리는 엔티티 클래스인 도메인(여기서는 HealthInfo)로 데이터를 주고받는다.)

 

- 변수를 선언하고 난 후 toEntity ()메서드를 생성한다.  이 메서드에서는  HealthInfoRequestDTO타입의 dto 매개변수를 받는다. 해당 dto는 아래에 생성한 get메서드를 이용해서 pivate값을 호출하고, 어노테이션 된 builder().build() 메서드를 이용해서 각 값들을 엔티티값에 포함시켜주는 기능이 구현되어있다. 

여기서 생성자를 쓰지않고 객체를 생성해주는데 편리한 builder 패턴을 써서 엔티티 객체를 생성해준다.

 

- 맨 아래의 of 메서드는 각 데이터값을 매개변수로 받아 DTO타입으로 반환해주는 역할을 한다. 추적해보니 Test를 위해 만들어놓은 메서드이다.

 

 

 HealthInfoResponseDTO 코드 작성

@Builder
@Getter
@Setter
@ToString
public class HealthInfoResponseDTO {

    private Long id;
    private String brand_name;
    private String land_number;
    private String road_number;
    private String category;

    //앞선 RequestDTO에서 각 get 메서드가 선언되어있기때문에
    public static HealthInfo toEntity(HealthInfoResponseDTO healthInfoResponseDTO){
        return HealthInfo.builder()
                    .id(healthInfoResponseDTO.getId())
                    .brand_name(healthInfoResponseDTO.getBrand_name())
                    .land_number(healthInfoResponseDTO.getLand_number())
                    .road_number(healthInfoResponseDTO.getRoad_number())
                    .category(healthInfoResponseDTO.getCategory())
                    .build();
    }


}

HealthInfoResponseDTO 의 코드는 ReqestDTO와 동일하며 변수명과 타입명이 Response로 바뀐정도만 차이가 있다.

또한 builder()에서 get메소드들이 앞서  public으로 Request에 구현되어있기때문에 변수만 ResponstDTO로 지정해서 가져다 쓰면된다.

 

 

 HealthInfoServiceImpl 코드 작성.

import com.example.demo.data.request.HealthInfoRequestDTO;
import com.example.demo.data.response.HealthInfoResponseDTO;
import com.example.demo.domain.HealthInfo;
import com.example.demo.repository.HealthRepository;
import com.example.demo.service.HealthInfoService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;


@Service
@RequiredArgsConstructor
@Slf4j
public class HealthInfoServiceImpl implements HealthInfoService {

    private final HealthRepository healthRepository;

    @Override
    public HealthInfoResponseDTO read(Long id){
        HealthInfo healthInfo = healthRepository.findById(id)
                .orElseThrow(RuntimeException::new);

        return HealthInfoResponseDTO.builder()
                .id(healthInfo.getId())
                .brand_name(healthInfo.getBrand_name())
                .land_number(healthInfo.getLand_number())
                .road_number(healthInfo.getRoad_number())
                .category(healthInfo.getCategory())
                .build();
    }

    @Override //저장.
    public boolean saveHealth(HealthInfoRequestDTO healthInfoRequestDTO) {
        healthRepository.save(HealthInfoRequestDTO.toEntity(healthInfoRequestDTO));
        return false;
    }

    @Override //수정
    public boolean updateHealth(HealthInfoRequestDTO healthInfoRequestDTO) {

        HealthInfo healthInfo = healthRepository.findById(healthInfoRequestDTO.getId())
                .orElseThrow(RuntimeException::new);

        healthInfo.modifyContent(healthInfoRequestDTO.getBrand_name()
                , healthInfoRequestDTO.getLand_number()
                , healthInfoRequestDTO.getRoad_number()
                , healthInfoRequestDTO.getCategory());

        try{
            log.info("updated Data : {}",healthInfo);
            healthRepository.save(healthInfo);

        }catch (Exception e){
            return false;
        }

        return true;
    }

    @Override //삭제
    public boolean deleteHealth(Long id) {
        healthRepository.deleteById(id);
        return false;
    }

    @Override
    public Page<HealthInfo> healthInfoList(Pageable pageable) {
        return null;
    }

//    @Override // 페이지
//    public Page<HealthInfo> healthInfoList(Pageable pageable) {
//        return healthRepository.findAll(pageable);
//    }

}

HealthIfoImpl 클래스 코드를 작성하며 난관에 봉착했었다. read() 부분에 ResponseDTO가 인식이 안됐던것.

한참을 헤매다 알고보니 오탈자 문제였었다...

 

▷ MainHealthIfoController 코드 작성.

import com.example.demo.data.request.HealthInfoRequestDTO;
import com.example.demo.data.response.HealthInfoResponseDTO;
import com.example.demo.domain.HealthInfo;
import com.example.demo.service.HealthInfoService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@RestController
@RequiredArgsConstructor
public class MainHealthInfoController {
    //브라우저에서 요청받아서 서비스에 구현된 기능들에 반환
    private final HealthInfoService healthInfoService;
    /*
    ResponseEntity 다시공부.ok는 뭐임
    왜 포스트랑 풋은 파라미터가아니라 바디로 받아야하는가? id를 조회해서 찾는게 아니라 저장할거라
    Exception e는 무엇? false로 반환하면 어떻게됨?


     */
    @GetMapping("/read") // url에 /read라고 띄워줌. 파라미터값으로 id를 받아서 반환함.
    public ResponseEntity<HealthInfoResponseDTO> getRead(@RequestParam Long id){
        return ResponseEntity.ok(healthInfoService.read(id));
    }

    @PostMapping("/save")
    public ResponseEntity<Boolean> postSave(@RequestBody HealthInfoRequestDTO healthInfoRequestDTO){
        try{
            healthInfoService.saveHealth(healthInfoRequestDTO); //네
        }catch (Exception e){ //e는 무엇?
            return ResponseEntity.ok(false); //ok로 반환하면어떻게 됨?
        }
        return ResponseEntity.ok(true);
    }

    @PutMapping("/update")
    public ResponseEntity<Boolean> updateSave(@RequestBody HealthInfoRequestDTO healthInfoRequestDTO){
        try{
            healthInfoService.updateHealth(healthInfoRequestDTO);
        }catch(Exception e){
            return ResponseEntity.ok(false);
        }
        return ResponseEntity.ok(true);
    }

    @DeleteMapping("/delete")
    public ResponseEntity<Boolean> getDelete(@RequestParam Long id){
        return ResponseEntity.ok(healthInfoService.deleteHealth(id));
    }

각 기능들은 main클래스에서 클라이언트의 요청을 받아 service로 반환하고 serviceImpl 클래스에서 Repository를 통해 db의 데이터들을 반환받거나 수정, 등록, 저장,삭제한다. 

기능 테스트는 swagger라는 사이트와 연결해 각 CRUD가 정상작동함을 확인했다.

 

어려웠던 부분은 id값을 파라미터로 받는 read와 delete는 정상 작동했지만, save와 update가 정상작동하지 않았었는데,  스터디그룹 조원들이 매개변수로 받는 body의 문제임을 찾아내어서 해당 HealthInfoRequestDTO의 어노테이션을 주석처리함으로서 해결할 수 있었다.

 

위 내용은 2023.01.05에 공부한 내용입니다.

링크 : https://dudwls3278.tistory.com/36