본문 바로가기

카테고리 없음

Spring 미니 프로젝트 [RN & Spring] - HTTP API 설계와 개발

이제는 DB와 로직을 연결하여 API를 작동시키는 법을 학습하고 진행한다.

 

HTTP API 설계 & 개발 흐름

내가 만들었던 앱의 핵심 기능인 '오늘의 걸음 수 기록하기' 이다. 
이 기록을 통해 쿠폰을 발급받거나 하는 비즈니스 로직이 있기 때문이다.

또한, 부모 엔티티인 Member부터 활용할 수 있다.

 

  • 클라이언트에서 서버로
    • 특정 회원의 오늘 걸음 수를 저장 
    • 가장 중요한 것은 리소스는 걸음 이라는 것이다!
    • HTTP 메서드: POST (행위)
    • URL: /api/steps
    • 요청: {
                  "userId" : "user1",
                  "stepCount": 5000,
                 "recordDate": "2026-02-16"
               }
    • 응답: {
                  "todayStepCount": 5000
               }
  • DTO
    • Data Transfer Object, 데이터 전송 객체: 데이터를 이동시키기 위해 만든 껍데기 객체이다.
    • 로직을 가지고 있지 않고 데이터 필드와 Getter/Setter 만 가진 데이터 바구니라고 생각하자!
    • 사용이유
      • 보안성: 패스워드 같은 민감 정보는 제외하고, 보여주고 싶은 것만 담는다.
      • 무한 루프 방지: Member는 StepInfo를 가지고 있고, StepInfo는 Member를 가지고 있다.
        엔티티로 직접 반환하면 무한 참조가 발생하여 스택 오버플로우가 일어나게 된다.
        따라서 딱 필요한 데이터만을 가지는 DTO를 사용한다.
    • 사용자가 데이터를 보냄 -> Controller가 DTO를 받음 ->
      Service가 DTO를 엔티티로 변환 -> Repository에서 DB에 저장  이런 흐름이다.
    • RequestDTO와 ResponseDTO 를 만드는 것이 유지 보수와 보안 사고를 막을 수 있다.

 

<DTO -> Controller -> Service>

내가 만든 Member 테이블에서 걸음 수를 등록할 때 필요한 DTO를 만들어보자

우선 클라이언트 측에서 expo sensor 기반으로 측정한 걸음 수를 저장한다.

 

 

  • 요청이 오면 엔티티를 그대로 받는 것이 아니라 DTO로 받는데, 현재 멤버 테이블에는 사용자 이름이 있으므로 이를 제외하는
    DTO를 만들어본다.
package com.example.demo.dto;

import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDate;

@Getter
@NoArgsConstructor
public class StepRequest {
    private String userId;
    private int stepCount;
    private LocalDate recordDate;

    public StepRequest(String userId, int stepCount, LocalDate recordDate) {
        this.userId = userId;
        this.stepCount = stepCount;
        this.recordDate = recordDate;
    }
}

 

  • Controller를 만들어서 Service에 넘겨준다
package com.example.demo.controller;

import com.example.demo.dto.StepRequest;
import com.example.demo.service.StepService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor //롬복 사용 -> 생성자 주입
public class StepController {

    private final StepService stepService; // 서비스 주입

    @PostMapping("/api/steps") //아까 작성한 URI, POST 메서드를 이용해서 걸음 수 등록
    public String recordStep(@RequestBody StepRequest request) {
        stepService.saveStep(request); //Service 계층에 있는 메서드
        return "걸음 수 저장 성공!"; //응답으로 보낼 스트링
    }
}

 

  • Service에서 핵심 로직을 실행한다.
    • 걸음 수를 DB에 저장하는 기능이다. 따라서 Repository와 StepInfoRepository를 활용한다.
    • @Transactional 꼭 사용! DB 변경이 일어나기 때문이다.
      로직이 에러 없이 끝나면 DB에 반영한다.
      만약 중간에 예외가 발생한다면 원래대로 복구시키는 역할을 한다.
      만약 조회 같은 읽기 전용이라면 변경 감지가 필요 없어서 readOnly = true 라는 코드를 통해 최적화를 꾀할 수도 있다.
      클래스 전체에 readOnly = true를 걸어두고, 저장/수정 메서드(Join, Buy 등)에만 따로 @Transactional을 붙이는 방식
package NetZero.service;

import NetZero.domain.Member;
import NetZero.domain.StepInfo;
import NetZero.dto.StepRequest;
import NetZero.repository.MemberRepository;
import NetZero.repository.StepInfoRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class StepService {

    private final StepInfoRepository stepInfoRepository;
    private final MemberRepository memberRepository;

    @Transactional
    public void saveStep(StepRequest stepRequest) {
        Member member = memberRepository.findById(stepRequest.getUserId()).orElseThrow(
                () -> new IllegalArgumentException("존재하지 않는 ID입니다.")
        );

        StepInfo stepInfo = new StepInfo(member, stepRequest.getStepCount(), stepRequest.getRecordDate());

        stepInfoRepository.save(stepInfo);
    }
}

 

postman이라는 것을 사용해서 내 API 통신이 잘 이루어지는 지 확인할 수 있었다!

 

다음으로 회원 정보를 조회하는 API를 설계하고 개발했다.

  • 클라이언트에서 서버로
    • 특정 회원을 조회하는 것이 때문에 리소스는 회원이다.
    • HTTP 메서드: GET (조회) 
    • URL: /api/members/{memberId} -> Id로 구분하여 개인을 조회한다.
    • 응답: {
                 nickname: '뚜벅이'
                 userPoint: '500'
               }
  • DTO
    • 이번에는 조회이기 때문에 응답 DTO를 만들었다.
package NetZero.dto;

import NetZero.domain.Member;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class MemberResponse {
    private String nickname;
    private int userPoint;

    public MemberResponse(Member member) {
        this.nickname = member.getNickname();
        this.userPoint = member.getUserPoint();
    }
}

 

  • Service
    • Transactional에서 읽기 전용 속성을 사용해봤다. 멤버 아이디로 레포지토리에서 찾은 후에 없으면
      예외를 생성한다.
    • DB에 있는 Member엔티티를 DTO로 변환해주는 작업을 한다.
package NetZero.service;

import NetZero.domain.Member;
import NetZero.dto.MemberResponse;
import NetZero.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

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

    private final MemberRepository memberRepository;

    public MemberResponse getMemberInfo(String memberId){
        Member member = memberRepository.findById(memberId)
                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다."));

        return new MemberResponse(member);
    }
}

 

  • Controller
    • GetMapping을 사용하여 리소스를 조회한다.
    • GetMapping에서는 @PathVariable을 사용하여 Id를 기준으로 조회한다.
@RestController
@RequiredArgsConstructor
public class MemberController {
    private final MemberService memberService;

    @GetMapping("/api/members/{memberId}")
    public MemberResponse getMemberInfo(@PathVariable("memberId") String memberId) {
        return memberService.getMemberInfo(memberId);
    }
}

 

 

포인트 이력을 조회하는 API는 리스트 형식으로 뽑아보았다!

package NetZero.service;

import NetZero.domain.Member;
import NetZero.domain.PointHistory;
import NetZero.dto.PointHistoryResponse;
import NetZero.repository.PointHistoryRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@Service
@Transactional
@RequiredArgsConstructor
public class PointHistoryService {

    private final PointHistoryRepository pointHistoryRepository;

    public List<PointHistoryResponse> getPointHistories(Member member){
        List<PointHistory> histories = pointHistoryRepository.findByMember(member);

        return histories.stream() //엔티티 리스트들을 DTO 리스트로 변환
                .map(PointHistoryResponse::new) //하나하나를 DTO로 바꿈
                .collect(Collectors.toList());  // 다시 리스트로 묶음
    }
}

 

DB에 직접 값을 넣고 돌려본 결과 데이터가 제대로 나오는 것을 확인했다!

GET 테스트는 포스트맨 테스트가 아니어도 이렇게 URI를 직접 입력해서 확인도 할 수 있었다

 

 

사용할 수 있는 쿠폰들을 조회하는 API를 개발했다.

이번 API는 스프링 데이터 JPA의 기능을 활용해보는 목적이 있었다.

스프링 데이터 JPA는 메서드 이름만으로 편리하게 사용이 가능하다.
따라서 쿠폰 엔티티에 사용여부를 참 거짓으로 저장하는 컬럼을 두고 JPA를 통해 사용가능한 것만 DB에서 가져올 수 있도록 했다.

 

  • CouponInfo 레포지토리에서 isUsed 를 비교해서 가져온다.
    • 이렇게 인터페이스만으로 가능하다!
package NetZero.repository;

import NetZero.domain.CouponInfo;
import NetZero.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface CouponInfoRepository extends JpaRepository<CouponInfo, Long> {
    List<CouponInfo> findByMemberAndIsUsedFalse(Member member);
}

 

 

 


 

이렇게 해서 간단한 API 설계를 마쳤고, 다음에는 스프링 빈을 활용해서
코드를 전체적으로 리팩터링하고 테스트 단계를 거칠 예정이다.
또한, 스프링 연동을 위한 프론트 코드를 설정할 예정이다!
파이팅!