주요 내용은 Database와 상호작용하는 API의 흐름을 이해하는 것입니다.
지난 포스팅의 내용을 기반으로 구체적인 예제를 다룹니다.
또한, Service와 Controller 단에서 데이터 검증 처리를 코드가 아닌 어노테이션으로 수행하는 방법을 통해 단일 책임 원칙을 준수하고, 더 깔끔한 코드 작성 방법을 배웁니다.
GitHub 이슈 및 브랜치 만들기
1. GitHub에서 이슈를 생성합니다.
라벨은 깃모지(gitmoji)를 활용해 꾸며보세요.
예시:
- 🐛:bug:
- 📝:memo:
- ✨:sparkles:
2. 브랜치 생성
- 브랜치 이름 예시:feature/#이슈번호
- 예를 들어,feature/#3
3. 브랜치 작업 시 주의사항
- 항상 작업할 브랜치가 최신 상태인지 확인 후 작업을 시작하세요.
회원 가입에 필요한 Request와 Response DTO를 생성합니다.
// Request DTO
public class MemberRequestDTO {
@Getter
public static class JoinDto {
String name;
Integer gender;
Integer birthYear;
Integer birthMonth;
Integer birthDay;
String address;
String specAddress;
List<Long> preferCategory;
}
}
// Response DTO
public class MemberResponseDTO {
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class JoinResultDTO {
Long memberId;
LocalDateTime createdAt;
}
}
엔티티와 DTO 간 변환을 위한 Converter를 생성합니다.
public class MemberConverter {
public static MemberResponseDTO.JoinResultDTO toJoinResultDTO(Member member) {
return MemberResponseDTO.JoinResultDTO.builder()
.memberId(member.getId())
.createdAt(LocalDateTime.now())
.build();
}
public static Member toMember(MemberRequestDTO.JoinDto request) {
Gender gender = switch (request.getGender()) {
case 1 -> Gender.MALE;
case 2 -> Gender.FEMALE;
default -> Gender.NONE;
};
return Member.builder()
.name(request.getName())
.gender(gender)
.address(request.getAddress())
.specAddress(request.getSpecAddress())
.build();
}
}
클라이언트 요청을 처리하는 Controller를 작성합니다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/members")
public class MemberRestController {
private final MemberCommandService memberCommandService;
@PostMapping("/")
public ApiResponse<MemberResponseDTO.JoinResultDTO> join(@RequestBody @Valid MemberRequestDTO.JoinDto request) {
Member member = memberCommandService.joinMember(request);
return ApiResponse.onSuccess(MemberConverter.toJoinResultDTO(member));
}
}
비즈니스 로직을 처리하는 Service를 작성합니다.
@Service
@RequiredArgsConstructor
public class MemberCommandServiceImpl implements MemberCommandService {
private final MemberRepository memberRepository;
private final FoodCategoryRepository foodCategoryRepository;
@Transactional
@Override
public Member joinMember(MemberRequestDTO.JoinDto request) {
Member newMember = MemberConverter.toMember(request);
// 선호 카테고리 처리
List<FoodCategory> foodCategoryList = request.getPreferCategory().stream()
.map(categoryId -> foodCategoryRepository.findById(categoryId)
.orElseThrow(() -> new IllegalArgumentException("카테고리 없음")))
.toList();
foodCategoryList.forEach(category -> {
MemberPrefer prefer = MemberPrefer.builder()
.member(newMember)
.foodCategory(category)
.build();
newMember.getMemberPreferList().add(prefer);
});
return memberRepository.save(newMember);
}
}
Spring Data JPA를 사용해 Repository를 작성합니다.
public interface MemberRepository extends JpaRepository<Member, Long> {}
public interface FoodCategoryRepository extends JpaRepository<FoodCategory, Long> {}
build.gradle에 의존성 추가:
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
Swagger 설정 클래스 생성:
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI umcStudyAPI() {
return new OpenAPI()
.addServersItem(new Server().url("/"))
.info(new Info().title("UMC API").version("1.0.0").description("API 명세서"));
}
}
Swagger 확인:
http://localhost:8080/swagger-ui/index.html#/로 접속합니다.
Request DTO에 어노테이션 추가:
public class MemberRequestDTO {
@Getter
public static class JoinDto {
@NotBlank
String name;
@NotNull
Integer gender;
@Size(min = 5, max = 12)
String address;
@ExistCategories
List<Long> preferCategory;
}
}
커스텀 어노테이션 정의:
@Documented
@Constraint(validatedBy = CategoriesExistValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExistCategories {
String message() default "카테고리 존재하지 않음";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Validator 작성:
@Component
@RequiredArgsConstructor
public class CategoriesExistValidator implements ConstraintValidator<ExistCategories, List<Long>> {
private final FoodCategoryRepository foodCategoryRepository;
@Override
public boolean isValid(List<Long> value, ConstraintValidatorContext context) {
return value.stream().allMatch(foodCategoryRepository::existsById);
}
}
그러면 아래 과정을 통해 완성이 되게 됩니다.

이번 주 학습에서는 Database와 상호작용하는 API의 기본 흐름을 학습했습니다. 특히 Swagger와 Validation 어노테이션을 통해 개발자 경험(DX)을 개선하는 방법을 배우는 것이 핵심이었습니다.
다음 포스팅에서는 목록 조회가 아닌 API를 만들어보면서, 페이징을 적용해보겠습니다!