이제는 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로 변환해주는 작업을 한다.
- Transactional에서 읽기 전용 속성을 사용해봤다. 멤버 아이디로 레포지토리에서 찾은 후에 없으면
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 설계를 마쳤고, 다음에는 스프링 빈을 활용해서
코드를 전체적으로 리팩터링하고 테스트 단계를 거칠 예정이다.
또한, 스프링 연동을 위한 프론트 코드를 설정할 예정이다!
파이팅!