🖤 UMC '커플' 앱 개발 프로젝트 - API 설계 (1) [유저] 문의 등록, 문의 세부 조회 🖤

2024. 1. 25. 00:30개발/🖤 UMC 프로젝트 - 커플 🖤

 

 

 

이제 본격적인 서비스 로직 설계를 위해 API 제작을 시작했다

 

우선 아직 애매한 datespot 로직 부분을 제외하고 필요한 API 목록을 리스트업했다

👾 API 목록 👾

[문의하기 파트]
<유저>
나의 문의 리스트 조회 API 문의 등록 API
-> 문의 제목 / 내용
<운영자>
(page) 답변 대기 중인 문의 리스트 조회 API
문의 답변 등록 API

[계정 정보]
(page) 유저 정보 read API
유저 정보 update API
-> 프로필 사진 / 닉네임
회원 등록, 로그인 , 로그아웃 API
유저 리워드 read API
유저 point history read API

[후기]
(page) 프리뷰 리스트 조회 API -> pagable 고려? -> 내거, old , new , 추천수
후기 조회 API
후기 등록 및 수정 API
-> 사진 1~5장 / 제목 / 방문장소(1~3)
후기 후기 하트 API

 

나는 [ 문의하기 ] 파트를 맡았다!

 

 


 

 

 

🎀 [ 유저 ]  문의 등록하기

 

 

Qna Entity

@Entity
@Builder
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Qna extends BaseEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long qnaId;

    private String title;
    private String body;

    @Enumerated(EnumType.STRING)
    private AnswerStatus answerStatus = AnswerStatus.WAITING;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "memberId")
    private Member member;
}

 

 

1.  QnaRequestDTO Class 안에 QnaSaveDto static class 생성

 

public class QnaRequestDTO {

    @Getter
    public static class QnaSaveDto {
        @NotNull
        Long memberId;
        @NotBlank
        String title;
        @NotBlank
        String body;
    }
    
}

 

✨ 코드 해석 및 추가 지식 ✨

1.  Front에서 받아올 정보
-  memberId  ->  '나의 문의 사항 조회하기' 기능 구현을 위해서 필요함
-  title : 문의 제목
-  body : 문의 내용

2.  Front에서 보내주는 정보를 받아먹기만 하면 되기 때문에
@Getter만 필요하다
-> @Builder 와 같은 생성자 역할을 해주는 어노테이션은 필요 없다

3.  보통 DTO class 안에 관련 DTO를 public static class 형태로 넣어준다

 

📌 의문 1
QnaRequestDTO 내의 데이터들은 Front에서 알아서 넘겨주나?
따로 받아오는 로직 필요없나?

@RequestParam, @RequestBody, @PathVariable 등을 통해서 Json 혹은 변수 형식으로 데이터 통신

 

 

2.  QnaConverter Class에 toQnaEntity 생성

 

@Component
@RequiredArgsConstructor
public class QnaConverter {

    public static MemberService memberService;
    
	public static Qna toQnaEntity(QnaRequestDTO.QnaSaveDto request){
        Member member = memberService.findById(request.getMemberId());
        return Qna.builder()
                .member(member)
                .title(request.getTitle())
                .body(request.getBody())
                .build();
    }
    
}

 

✨ 코드 해석 및 추가 지식 ✨

1.  Front한테 받은 정보를 저장한 DTO(QnaSaveDto)를 Qna Entity 인스턴스로 변환
     -  우선 QnaSaveDto를 파라미터로 받아와야 한다
     -  Qna 인스턴스를 return한다
     -  Qna 인스턴스는 builder를 통해 생성한다
     -  이때 memberId가 필요하지만 Qna의 builder 패턴을 통해서는 얻어올 수 없는 정보이기 때문에
         MemberService의 findById 메서드를 이용해준다

2.  @Component는 해당 클래스를 Spring bean으로 설정해주는 어노테이션이다
      - Spring bean : Spring에서 관리하는 객체

 

📌 의문 2
컴포넌트인 MemberService를 불러와서 사용하기 때문에
QnaConverter를 컴포넌트로 설정하는 것이 좋다고 제안해주셨는데
정확한 의의가 뭐지?

참고 )
https://mangkyu.tistory.com/151
https://chat.openai.com/c/fdb50124-9107-469e-8e8f-ba470f1ab67d

public static class에 다른 로직(MemberService)이 끼어든 이상
유지보수의 의미가 옅어지기 때문에
스프링 빈으로 설정해 객체로 관리해주는 게 더 좋은 듯

 

 

3.  QnaService class에 세부 로직 구현 - createQna class

 

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class QnaService {

    private final QnaRepository qnaRepository;
    private final QnaConverter qnaConverter;
    
    @Transactional
    public QnaResponseDTO.QnaEntityDto createQna(QnaRequestDTO.QnaSaveDto request){
        Qna qna = qnaConverter.toQnaEntity(request);
        qnaRepository.save(qna);
        return qnaConverter.toQnaEntityDto(qna);
    }
}

 

public interface QnaRepository extends JpaRepository<Qna, Long> {}

 

✨ 코드 해석 및 추가 지식 ✨

1.  createQna : Qna 인스턴스 생성하고 QnaRepository에 저장
    -  Front에서 보내주는 정보를 가지고 Qna 인스턴스 생성해주기
         ->  QnaConverter.toQnaEntity 에서 처리!
         ->  파라미터로 QnaSaveDto 필요
    -  생성된 Qna 인스턴스 QnaRepository에 저장
        -> QnaRepository 필요 : JpaRepository 상속받은 QnaRepository 생성하기
        -> QnaRepository에 save를 통해 Qna 인스턴스 저장

2.  @Transactional
      -  해당 메서드를 실행 중일 때 다른 연산이 끼어들지 않도록 한다
      -  연산 과정에서 오류가 발생했을 때 변경 사항을 커밋하지 않는다
      -  readOnly = true
          ->  해당 Entity를 조회용으로 읽어오기 때문에 중간에 변경사항이 발생했을 때 적용하지 않는다
          ->  메모리 절약
          ->  코드 가독성

참고 ) https://kafcamus.tistory.com/30 
          https://hungseong.tistory.com/74

3.  @JpaRepository : JPA를 사용하여 데이터베이스를 조작하기 위한 메서드를 제공한다
      -> CRUD 메서드, List CRUD 메서드, Query Creation 메서드, 영속성 컨텍스트 관련 메서드 사용 가능

참고 ) https://velog.io/@minju0426/JPARepository%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90%EC%82%AC%EC%9A%A9%EB%B2%95-Method

 

 

4.  QnaController Class 구현

 

@RestController
@RequiredArgsConstructor
@RequestMapping("/qnas")
public class QnaController {

    private final QnaService qnaService;

    @PostMapping("")
    public ApiResponse<Boolean> join(@RequestBody @Valid QnaRequestDTO.QnaSaveDto request){
        Qna qna = qnaService.createQna(request);
        return True;
    }
    
}

 

✨ 코드 해석 및 추가 지식 ✨

1.  문의 '등록'이기 때문에 POST -> @PostMapping

2.  join : Front로부터 QnaSaveDto 내의 데이터 전달받고 Qna 인스턴스 생성 및 저장 진행
     ->  Front로부터 QnaSaveDto 내의 데이터 전달받기 : @RequestBody @Valid
     ->  Qna 인스턴스 생성 및 저장 진행 : createQna 사용

3.  @{}Mapping : HTTP 연산을 적용해주는 어노테이션 -> {} : GET, POST, DELETE, PUT 이 들어감

4.  @RequestBody : Front에서 보내주는 Json 텍스트를 받아올 수 있도록 해주는 어노테이션

5.  @Valid : @RequestBody로 받아온 정보에 대한 검증 수행해주는 어노테이션

6.  추후에 ResponseDTO 생성해서 return 해줘야 한다

 

 


 

 

🎀 [ 유저 ]  문의 세부 조회하기 

 

1.  QnaResponseDTO class에 QnaEntityDTO 생성

 

public class QnaResponseDTO {

    @Builder
    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    public static class QnaEntityDto {
        Long qnaId;
        Long memberId;
        String title;
        String body;
        AnswerStatus answerStatus;
    }
    
}

 

✨ 코드 해석 및 추가 지식 ✨

1.  Front에서 받은 정보 ( memberId, title, body ) 에 qnaId와 AnswerStatus 추가하여 DTO 생성
     -  qnaId : PK
     -  AnswerStatus : 운영자가 답변했는지 여부 -> WAITING( 답변 대기중 ), COMPLETED( 답변 완료 )
         -> Enum으로 생성함

 

📌 의문 3
QnaEntityDto는 운영자에게만 필요한가?

📌 의문 4
운영자에게만 필요하다면 memberId를 넣을 필요가 있는가?

 

 

2.  QnaConverter class에 toQnaEntityDTO 생성

 

public static QnaResponseDTO.QnaEntityDto toQnaEntityDto(Qna qna){

        AnswerStatus answerStatus = AnswerStatus.WAITING;
        return QnaResponseDTO.QnaEntityDto.builder()
                .qnaId(qna.getQnaId())
                .memberId(qna.getMember().getMemberId())
                .title(qna.getTitle())
                .body(qna.getBody())
                .answerStatus(answerStatus)
                .build();
                
}

 

✨ 코드 해석 및 추가 지식 ✨

1.  Qna 인스턴스(Entity)에 저장되어 있는 정보에 answerStatus 추가해서 DTO 생성
     ->  Qna 인스턴스에 저장되어 있는 정보 가져와야 하기 때문에 파라미터로 Qna 인스턴스 받아온다
     ->  QnaEntityDto를 builder 패턴으로 생성한다  
     ->  Qna 인스턴스에 저장되어 있는 정보는 getter를 통해서 불러온다
     ->  AnswerStatus는 WAITING으로 설정한 뒤 DTO에 넣어준다
     ->  생성된 DTO 반환해준다

2.  Qna 인스턴스는 QnaRepository에서 불러온다 -> QnaService 단에서 구현

 

📌 의문 5
QnaEntityDto에는 memberId가 있는데 toQnaEntityDto에 memberId 관련 로직이 없어도 되는가?

놉 그래서 에러 뜸...;; -> memberId 관련 로직 추가

📌 의문 6
qnaId는 자동으로 생성되는 것인가?

-> MySQL에서 자동으로 생성해준다

 

 

3.  QnaService에서 구체적인 로직 구현 - findById, getQnaInfo

 

findById

public Qna findById(Long qnaId){
        return qnaRepository.findById(qnaId).orElseThrow(()->new GeneralException(ErrorStatus.QNA_NOT_FOUND));
}

 

getQnaInfo

public QnaResponseDTO.QnaEntityDto getQnaInfo(Long qnaId) {
        Qna qna = findById(qnaId);
        return toQnaEntityDto(qna);
    }

 

✨ 코드 해석 및 추가 지식 ✨

1.  findById : QnaRepository에 저장되어 있는 Qna 인스턴스를 불러오는 로직
    -> qnaId를 파라미터로 받아서 QnaRepository 안에 저장된 특정 Qna 인스턴스를 찾아온 뒤 반환한다

2.  getQnaInfo : findById를 통해 반환 받은 Qna 인스턴스를 DTO로 변환하는 로직
     ->  findById 를 통해 반환받은 Qna 인스턴스를 qna 변수로 저장한다
     ->  Qna 인스턴스(Entity)를 DTO로 변환 : toQnaEntityDto 사용

 

📌 의문 7
findById에서 예외 처리를 안 하니까 Error가 떴는데 이유가 뭘까?
다른 메서드들은 예외 처리 안 해도 Error 안 떴는뎁..

-> 반환값이 null일 경우를 대비해서 실무에서는 보통 orElseThrow()를 이용해서 예외처리를 한다고 한다

 

 

4.  QnaController에서 GET 로직 구현

 

@PostMapping("")
public ApiResponse<QnaResponseDTO.QnaEntityDto> join(@RequestBody @Valid QnaRequestDTO.QnaSaveDto request){
        QnaResponseDTO.QnaEntityDto qnaDTO = qnaService.createQna(request);
        return ApiResponse.onSuccess(qnaDTO);
}
우선 Qna관련 ResponseDTO을 구현했으므로
join( POST - 문의 등록하기 ) 메서드의 return 값을 
QnaEntityDto(Qna관련 ResponseDTO) 로 설정해준다

 

@GetMapping("")
public ApiResponse<QnaResponseDTO.QnaEntityDto> getQna(@RequestParam Long qnaId){
        return ApiResponse.onSuccess(qnaService.getQnaInfo(qnaId));
}

 

✨ 코드 해석 및 추가 지식 ✨

1.  세부 문의 '조회'하기 기능이므로 @GetMapping을 사용한다

2.  문의 정보를 return해준다 -> getQnaInfo 이용

3.  @RequestParam : HTTP 요청 파라미터를 받을 수 있도록 하는 어노테이션

 

 

 


 

 

💡 API 구현은   [ DTO 생성 -> Converter 구현 -> Controller 슬쩍 해보면서 Service에 어떤 로직 필요한지 확인 -> Service 구현 ( Repository도 이 과정에서 만드는 듯 ) -> Controller 완성 ]  이런 순서로 보통 진행하는 것 같다 💡