UMC 10기 Spring Boot 스터디 #5 - API 계층 구조와 응답 통일

정유진·2026년 5월 6일
post-thumbnail

이번 주차에서 배운 것

이번 주차에서는 단순히 API를 만드는 것이 아니라,
Spring Boot에서 계층을 어떻게 나누고 응답과 예외를 어떻게 관리하는지 학습했습니다.

특히 포인트 5가지

  • Controller / Service / Repository 역할
  • DTO 사용 이유
  • ApiResponse를 통한 응답 통일
  • Exception Handling 구조
  • Swagger를 이용한 API 테스트

를 중심으로 정리했습니다.



1. Controller / Service / Repository 구조

1-1. Controller

Controller는 클라이언트의 요청을 가장 먼저 받는 계층입니다.

Controller의 주요 역할

  • HTTP 요청 받기
  • Request Body / Query Parameter 매핑
  • Service 호출
  • 결과 응답 반환


예시:

@RestController
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;

    @PostMapping
    public ApiResponse<MemberResDTO.MemberInfoDTO> getMember(
            @RequestBody MemberReqDTO.MemberRequestDTO request
    ) {
        return ApiResponse.onSuccess(
                memberService.getMember(request)
        );
    }
}

1-2. Service

Service는 실제 비즈니스 로직을 처리하는 계층이다.

Service의 주요 역할:

  • 요청 데이터 처리
  • Repository 호출
  • 비즈니스 로직 수행
  • 응답 DTO 반환


예시:

@Service
@RequiredArgsConstructor
@Transactional
public class MemberService {

    private final MemberRepository memberRepository;

    public MemberResDTO.MemberInfoDTO getMember(Long memberId) {

        Member member = memberRepository.findById(memberId)
                .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND));

        return MemberConverter.toMemberInfoDTO(member);
    }
}

1-3. Repository

Repository는 DB와 직접 통신하는 계층이다.

JPA에서는 JpaRepository를 상속받아 사용함

public interface MemberRepository extends JpaRepository<Member, Long> {}

기본적으로 다음 메서드를 제공한다.

  • save()
  • findById()
  • delete()
  • findAll()

또한 메서드 이름만으로 커스텀 조회도 가능하다.

findByEmail(String email)



2. DTO를 사용하는 이유

DTO(Data Transfer Object)는 계층 간 데이터 전달 객체이다.
이번 주차에서는 Request DTO와 Response DTO를 분리해서 사용했다.

DTO를 사용하는 이유

  • 요청/응답 구조를 명확하게 관리 가능
  • Entity 직접 노출 방지
  • 안정성 증가
  • 유지보수 용이

record vs static class

record

public record MemberDTO(String name, String email) {}

특징:

  • 코드가 간결함
  • 불변 객체 생성
  • Java 16 이상 지원

static class

@Getter
@AllArgsConstructor
public static class MemberDTO {
    private String name;
    private String email;
}

특징:

  • 유연한 구조 설계 가능
  • 확장성이 높음



3. ApiResponse를 이용한 응답 통일

프로젝트에서는 API 응답 형식을 통일해서 사용했다.

{
  "isSuccess": true,
  "code": "COMMON200",
  "message": "성공입니다.",
  "result": {}
}

응답 통일의 장점

  • 프론트엔드와 협업이 쉬움
  • 성공/실패 구조 일관성 유지
  • 유지보수 편리
  • 에러 처리 단순화

ApiResponse 예시

public class ApiResponse<T> {

    private Boolean isSuccess;
    private String code;
    private String message;
    private T result;
}

여기서 제네릭 를 사용해 다양한 타입의 응답을 처리할 수 있다.



4. Exception Handling

예외 상황에서도 응답 형식을 통일하기 위해 @RestControllerAdvice를 사용했다.

구조
Exception 발생→ ControllerAdvice에서 감지→ ApiResponse 형태로 반환

예시:

@RestControllerAdvice
public class GeneralExceptionAdvice {

    @ExceptionHandler(ProjectException.class)
    public ResponseEntity<ApiResponse<Void>> handleException(
            ProjectException e
    ) {
        return ResponseEntity
                .status(e.getErrorCode().getStatus())
                .body(ApiResponse.onFailure(e.getErrorCode(), null));
    }
}



5. Optional 사용 이유

JPA의 findById()Optional을 반환한다.

memberRepository.findById(id)

따라서 값이 없을 경우를 안전하게 처리할 수 있다.

.orElseThrow(() -> new MemberException(...))

이를 통해 NullPointerException을 방지할 수 있다.



이번 주차에서 느낀 점

이번 주차를 진행하면서 단순히 “API가 동작하는 것”보다
“왜 계층을 분리하는지”를 이해하는 것이 중요하다는 걸 느꼈다.

특히 ApiResponseException Handling 구조를 직접 적용해보면서,
프로젝트 전체의 응답 형식을 통일하는 이유를 체감할 수 있었다.

또한 DTO를 통해 Entity를 직접 노출하지 않는 구조가 유지보수 측면에서 훨씬 안전하다는 점도 이해하게 되었다.




profile
개발전공 대학생

0개의 댓글