
이번 주차에서는 단순히 API를 만드는 것이 아니라,
Spring Boot에서 계층을 어떻게 나누고 응답과 예외를 어떻게 관리하는지 학습했습니다.
를 중심으로 정리했습니다.
Controller는 클라이언트의 요청을 가장 먼저 받는 계층입니다.
예시:
@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)
);
}
}
Service는 실제 비즈니스 로직을 처리하는 계층이다.
예시:
@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);
}
}
Repository는 DB와 직접 통신하는 계층이다.
JPA에서는 JpaRepository를 상속받아 사용함
public interface MemberRepository extends JpaRepository<Member, Long> {}
기본적으로 다음 메서드를 제공한다.
또한 메서드 이름만으로 커스텀 조회도 가능하다.
findByEmail(String email)
DTO(Data Transfer Object)는 계층 간 데이터 전달 객체이다.
이번 주차에서는 Request DTO와 Response DTO를 분리해서 사용했다.
public record MemberDTO(String name, String email) {}
@Getter
@AllArgsConstructor
public static class MemberDTO {
private String name;
private String email;
}
프로젝트에서는 API 응답 형식을 통일해서 사용했다.
{
"isSuccess": true,
"code": "COMMON200",
"message": "성공입니다.",
"result": {}
}
public class ApiResponse<T> {
private Boolean isSuccess;
private String code;
private String message;
private T result;
}
여기서 제네릭 를 사용해 다양한 타입의 응답을 처리할 수 있다.
예외 상황에서도 응답 형식을 통일하기 위해 @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));
}
}
JPA의 findById()는 Optional을 반환한다.
memberRepository.findById(id)
따라서 값이 없을 경우를 안전하게 처리할 수 있다.
.orElseThrow(() -> new MemberException(...))
이를 통해 NullPointerException을 방지할 수 있다.
이번 주차를 진행하면서 단순히 “API가 동작하는 것”보다
“왜 계층을 분리하는지”를 이해하는 것이 중요하다는 걸 느꼈다.
특히 ApiResponse와 Exception Handling 구조를 직접 적용해보면서,
프로젝트 전체의 응답 형식을 통일하는 이유를 체감할 수 있었다.
또한 DTO를 통해 Entity를 직접 노출하지 않는 구조가 유지보수 측면에서 훨씬 안전하다는 점도 이해하게 되었다.