예상 읽기 시간: 20~30분
Day 1부터 Day 10까지는 백엔드 코드를 읽기 위한 부품을 하나씩 봤습니다.
오늘은 이 내용을 AI가 만든 백엔드 코드를 리뷰하는 체크리스트로 합칩니다.
진규가 백엔드 코드를 전부 직접 작성하는 단계가 아니더라도, AI가 만든 결과물을 그대로 믿고 넘기면 위험합니다. 특히 Spring Boot 백엔드는 겉으로는 폴더 구조가 그럴듯하고, Controller-Service-Repository가 나뉘어 있어도 실제로는 계약이 흔들리거나, 트랜잭션이 빠져 있거나, 인증 경계가 비어 있거나, 테스트가 행복 경로만 확인하는 경우가 많습니다.
오늘의 목표는 “이 코드가 좋아 보인다”가 아니라 아래 질문에 답하는 것입니다.
이 백엔드는 프론트엔드와 협업 가능한가?
요청이 들어와서 응답이 나가기까지 흐름이 설명 가능한가?
데이터가 잘못 들어오거나 권한이 없는 요청이 들어왔을 때 안전한가?
운영 중 문제가 생겼을 때 추적 가능한가?
체크리스트는 암기용이 아니라 리뷰 순서입니다. AI에게 다시 수정 요청을 할 때도 이 순서를 그대로 사용할 수 있습니다.
AI가 만든 백엔드를 받으면 처음에는 파일이 많아 보입니다.
controller/
service/
repository/
dto/
entity/
config/
exception/
이때 파일 이름을 하나씩 읽기보다 먼저 대표 요청 하나를 고릅니다.
예를 들어 회원가입 기능이라면 이렇게 따라갑니다.
POST /api/members/signup
→ SignupRequest
→ MemberController.signup()
→ MemberService.signup()
→ MemberRepository.save()
→ SignupResponse
리뷰의 첫 질문은 이것입니다.
요청 하나가 어디서 시작해서 어디서 끝나는지 설명할 수 있는가?
좋은 백엔드는 요청 흐름이 끊기지 않습니다. Controller에서 받은 값이 어떤 DTO로 들어오고, Service에서 어떤 규칙을 확인하고, Repository에서 어떤 데이터에 접근하고, 마지막에 어떤 응답으로 돌아오는지 연결됩니다.
반대로 좋지 않은 코드는 이런 냄새가 납니다.
AI에게 리뷰 피드백을 줄 때는 “구조를 개선해줘”보다 이렇게 말하는 편이 좋습니다.
Post Q&A
백엔드 스터디 Day 11: AI가 만든 백엔드 코드를 리뷰하는 체크리스트 전체를 기준으로 질문과 피드백을 받아요.답을 본 뒤에는 이 내용을 댓글로 달아서 서징에게도 물어볼 수 있어요. 작성자가 직접 볼 수 있어요!
POST /api/members/signup 요청 흐름을 기준으로 Controller, Request DTO, Service, Repository, Response DTO가 어떻게 연결되는지 분리해줘. Controller는 HTTP 입출력만 담당하고, 중복 이메일 검증과 저장 규칙은 Service로 옮겨줘.
이렇게 요청하면 AI가 단순 리팩터링이 아니라 흐름 기준으로 고칠 가능성이 높아집니다.
프론트엔드 개발자에게 백엔드는 “함수 내부”가 아니라 “외부 계약”으로 먼저 보입니다.
어떤 URL로 요청해야 하는가?
어떤 method를 써야 하는가?
request body는 어떤 모양인가?
성공 응답은 어떤 모양인가?
실패 응답은 어떤 모양인가?
로그인이 필요한가?
그래서 API 계약 리뷰는 가장 먼저 확인해야 합니다.
예를 들어 게시글 목록 조회라면 보통 이런 형태가 자연스럽습니다.
GET /api/posts
GET /api/posts/{postId}
POST /api/posts
PATCH /api/posts/{postId}
DELETE /api/posts/{postId}
반대로 이런 형태는 협업 중 혼란을 만듭니다.
GET /api/getPostList
POST /api/deletePost
POST /api/post/update
항상 REST 이름을 완벽히 지켜야 한다는 뜻은 아닙니다. 중요한 것은 프론트엔드가 이름만 보고도 대략적인 의미를 예측할 수 있어야 한다는 점입니다.
AI가 만든 코드에서 자주 보이는 위험한 패턴은 Entity를 그대로 응답하는 것입니다.
@GetMapping("/members/{id}")
public Member getMember(@PathVariable Long id) {
return memberService.findById(id);
}
겉으로는 편해 보이지만 Entity에는 내부 DB 구조, 연관관계, 민감한 필드가 섞일 수 있습니다. 프론트엔드가 필요한 응답과 DB 저장 구조는 다릅니다.
리뷰 기준은 단순합니다.
요청/응답 DTO가 API 계약을 표현하고,
Entity는 내부 저장 구조로 숨겨져 있는가?
좋은 방향은 아래와 같습니다.
public record MemberProfileResponse(
Long id,
String nickname,
String profileImageUrl
) {}
프론트엔드는 이 응답 구조를 보고 UI 상태를 만들 수 있습니다. 백엔드는 내부 Entity가 바뀌더라도 API 계약을 유지할 수 있습니다.
성공 응답만 계약이 아닙니다. 실패 응답도 계약입니다.
프론트엔드는 실패했을 때 아래를 알아야 합니다.
필드 입력 오류인가?
로그인이 필요한가?
권한이 부족한가?
이미 존재하는 데이터인가?
서버 내부 오류인가?
그래서 에러 응답은 최소한 아래 정도의 기준을 가져야 합니다.
{
"code": "DUPLICATE_EMAIL",
"message": "이미 사용 중인 이메일입니다.",
"fieldErrors": []
}
리뷰할 때는 “에러 처리 있음”만 보지 말고 이렇게 질문합니다.
프론트엔드가 이 에러 응답만 보고 어떤 UI를 보여줄지 결정할 수 있는가?
백엔드는 사용자를 믿으면 안 됩니다. 프론트엔드에서 이미 검증했더라도 백엔드에서 다시 검증해야 합니다.
AI가 만든 코드에서 자주 확인할 지점은 DTO에 검증 어노테이션이 있는지입니다.
public record SignupRequest(
@Email(message = "이메일 형식이 올바르지 않습니다.")
@NotBlank(message = "이메일은 필수입니다.")
String email,
@NotBlank(message = "비밀번호는 필수입니다.")
@Size(min = 8, message = "비밀번호는 8자 이상이어야 합니다.")
String password
) {}
그리고 Controller에서 @Valid가 붙어 있어야 합니다.
@PostMapping("/signup")
public SignupResponse signup(@Valid @RequestBody SignupRequest request) {
return memberService.signup(request);
}
여기서 리뷰 질문은 두 가지입니다.
형식 검증은 DTO에서 하고 있는가?
비즈니스 규칙 검증은 Service에서 하고 있는가?
예를 들어 이메일 형식, 빈 값, 길이는 DTO 검증에 가깝습니다. 반면 “이미 가입된 이메일인가?”, “탈퇴 후 재가입 제한 기간인가?” 같은 규칙은 Service의 책임입니다.
나쁜 패턴은 모든 검증이 여기저기 흩어지는 것입니다.
if (request.email() == null) { ... }
if (!request.email().contains("@")) { ... }
if (memberRepository.existsByEmail(request.email())) { ... }
이런 코드가 Controller와 Service에 섞이면 나중에 정책이 바뀔 때 어디를 고쳐야 하는지 헷갈립니다.
AI에게 수정 요청할 때는 이렇게 말할 수 있습니다.
입력 형식 검증은 Request DTO의 Bean Validation으로 옮기고, 중복 이메일처럼 DB 조회가 필요한 비즈니스 검증은 Service에 남겨줘. 검증 실패는 GlobalExceptionHandler에서 일관된 에러 응답으로 변환해줘.
Entity는 DB 테이블에 가까운 객체입니다. 프론트엔드가 직접 의존할 대상이 아닙니다.
리뷰할 때는 Entity를 열고 아래를 봅니다.
필드가 도메인 의미를 갖고 있는가?
불필요하게 public setter가 열려 있지 않은가?
연관관계가 응답으로 그대로 노출되지 않는가?
생성/변경 규칙이 객체 안에 어느 정도 모여 있는가?
예를 들어 모든 필드에 setter가 열려 있으면 어디서든 값이 바뀔 수 있습니다.
member.setRole(Role.ADMIN);
member.setEmail(newEmail);
member.setPassword(encodedPassword);
작은 프로젝트에서는 편해 보이지만, 권한이나 상태 같은 값이 아무 곳에서나 바뀌면 버그를 찾기 어려워집니다.
더 나은 방향은 의미 있는 메서드로 변경을 제한하는 것입니다.
public void changeNickname(String nickname) {
this.nickname = nickname;
}
public void deactivate() {
this.status = MemberStatus.DEACTIVATED;
}
Repository는 DB 접근을 담당합니다. 여기서 볼 질문은 이것입니다.
Service가 필요한 조회를 Repository 메서드가 명확히 제공하는가?
예를 들어 중복 이메일 확인은 이런 형태가 읽기 좋습니다.
boolean existsByEmail(String email);
Optional<Member> findByEmail(String email);
반대로 Service 안에 복잡한 쿼리 문자열이 섞이거나, Repository가 비즈니스 판단까지 해버리면 경계가 흐려집니다.
Service는 단순히 Repository를 한 번 감싸는 껍데기가 아닙니다. 여러 규칙을 하나의 유스케이스로 묶는 곳입니다.
회원가입 Service라면 보통 아래 흐름을 가집니다.
1. 요청 값 정리
2. 중복 이메일 확인
3. 비밀번호 암호화
4. Member 생성
5. 저장
6. 응답 변환
여기서 리뷰할 부분은 “각 단계가 왜 이 위치에 있는가”입니다.
DB에 데이터를 쓰거나 여러 DB 작업이 하나의 단위로 묶인다면 트랜잭션을 확인해야 합니다.
@Transactional
public SignupResponse signup(SignupRequest request) {
...
}
조회 전용이면 읽기 전용 트랜잭션을 사용할 수 있습니다.
@Transactional(readOnly = true)
public MemberProfileResponse getProfile(Long memberId) {
...
}
리뷰 질문은 아래처럼 바꿀 수 있습니다.
이 유스케이스는 중간에 실패하면 어디까지 되돌아가야 하는가?
예를 들어 주문 생성 중 결제 기록은 저장됐는데 주문 상태는 저장되지 않으면 데이터가 어긋납니다. 이런 흐름은 트랜잭션 기준으로 봐야 합니다.
Service가 길어지는 것은 무조건 나쁜 것은 아닙니다. 하지만 아래가 섞이면 분리 신호입니다.
이때 바로 클래스를 많이 쪼개는 것보다 먼저 질문합니다.
이 코드는 하나의 유스케이스를 설명하는가,
아니면 서로 다른 책임이 우연히 한 메서드에 모였는가?
AI에게는 “클래스를 예쁘게 나눠줘”보다 이렇게 요청하는 편이 좋습니다.
OrderService의 createOrder 메서드에서 가격 계산, 재고 차감, 알림 발송 책임을 구분해줘. 트랜잭션 안에서 반드시 함께 성공해야 하는 DB 변경과, 실패해도 재시도할 수 있는 알림 처리를 분리해줘.
AI가 만든 백엔드에서 보안은 특히 조심해야 합니다. 로그인 기능이 있다고 해서 권한이 안전한 것은 아닙니다.
먼저 구분해야 합니다.
인증(Authentication): 너는 누구인가?
권한(Authorization): 너는 이 작업을 해도 되는가?
예를 들어 사용자가 로그인했다는 사실만으로 모든 게시글을 수정할 수 있으면 안 됩니다. 게시글 수정은 보통 작성자나 관리자만 가능해야 합니다.
리뷰할 때는 Controller나 Service에서 아래 흐름을 확인합니다.
1. 현재 로그인한 사용자 식별
2. 수정하려는 리소스 조회
3. 이 사용자가 이 리소스를 수정할 수 있는지 확인
4. 통과하면 변경 수행
나쁜 패턴은 memberId를 request body에서 받아 그대로 믿는 것입니다.
{
"memberId": 1,
"title": "수정할 제목"
}
프론트엔드는 이 값을 조작할 수 있습니다. 백엔드는 현재 인증 컨텍스트에서 사용자 ID를 가져와야 합니다.
보안 예시는 절대 실제 토큰이나 비밀값처럼 보이는 값을 쓰면 안 됩니다.
Authorization: Bearer ***
JWT secret value: <secret-placeholder>
리뷰 질문은 아래와 같습니다.
사용자 입력으로 받은 ID를 신뢰하고 있지 않은가?
현재 로그인 사용자와 리소스 소유자를 비교하고 있는가?
권한 실패가 401/403으로 구분되는가?
민감한 값이 로그나 응답에 노출되지 않는가?
AI는 테스트도 잘 만들어주는 것처럼 보입니다. 하지만 자주 나오는 문제는 테스트가 너무 행복 경로에 치우친다는 점입니다.
예를 들어 회원가입 테스트가 이것 하나뿐이라면 부족합니다.
정상 이메일과 정상 비밀번호로 가입하면 200 OK가 온다.
최소한 아래도 봐야 합니다.
이메일 형식이 틀리면 400이 오는가?
이미 가입된 이메일이면 중복 에러 코드가 오는가?
비밀번호가 짧으면 필드 에러가 오는가?
DB 저장 중 예외가 나면 일관된 에러 응답이 오는가?
프론트엔드 협업 관점에서는 특히 API 테스트가 중요합니다. 테스트가 “Service 메서드가 호출된다”만 확인하면 UI가 실제로 받을 응답을 보장하지 못합니다.
좋은 테스트 이름은 요구사항 문장처럼 읽힙니다.
POST /api/members/signup returns DUPLICATE_EMAIL when email already exists
테스트를 리뷰할 때는 아래를 체크합니다.
성공 케이스만 있는가?
실패 케이스가 API 응답 형태까지 확인하는가?
권한 실패 테스트가 있는가?
트랜잭션/DB 관련 중요한 규칙이 테스트로 잠겨 있는가?
AI에게 테스트 보강을 요청할 때는 기능명만 던지지 말고 실패 시나리오를 목록으로 줍니다.
회원가입 API 테스트를 보강해줘. 정상 가입, 이메일 형식 오류, 비밀번호 길이 오류, 중복 이메일, 예상하지 못한 서버 예외를 각각 분리해서 검증하고, 실패 응답의 code/message/fieldErrors 구조까지 확인해줘.
백엔드가 로컬에서 실행되는 것과 운영에서 안전하게 실행되는 것은 다릅니다.
리뷰할 때는 설정 파일을 봅니다.
application.yml
application-local.yml
application-prod.yml
Dockerfile
docker-compose.yml
CI workflow
확인할 질문은 아래와 같습니다.
환경별 설정이 분리되어 있는가?
비밀값이 코드에 하드코딩되어 있지 않은가?
DB 접속 정보는 환경 변수로 주입되는가?
운영 로그 레벨이 과도하게 자세하지 않은가?
헬스체크가 있는가?
예를 들어 이런 값은 코드에 직접 들어가면 안 됩니다.
spring:
datasource:
password: <password-placeholder>
문서 예시에서도 비밀값은 반드시 placeholder로 써야 합니다. 실제처럼 긴 토큰이나 그럴듯한 키를 넣으면 보안 스캐너가 잡을 수 있고, 더 중요한 것은 습관이 나빠집니다.
운영 가능성을 볼 때는 “서버가 뜬다”보다 아래를 확인합니다.
서버가 살아 있는지 확인할 endpoint가 있는가?
DB 연결 실패 시 어떻게 드러나는가?
환경 변수가 빠졌을 때 애플리케이션이 조용히 이상 동작하지 않는가?
Day 10에서 본 것처럼 로그는 나중에 보는 실행 기록입니다.
리뷰 체크는 단순합니다.
중요한 유스케이스 시작/성공/실패 로그가 있는가?
요청 하나를 따라갈 requestId나 traceId가 있는가?
민감한 값이 로그에 찍히지 않는가?
예외 로그와 사용자 응답이 분리되어 있는가?
헬스체크/메트릭 같은 운영 신호가 있는가?
좋은 로그는 “많은 로그”가 아니라 “찾을 수 있는 로그”입니다.
예를 들어 아래는 부족합니다.
log.info("success");
아래는 더 낫습니다.
log.info("post created. postId={}, authorId={}", postId, authorId);
하지만 아래처럼 민감한 값을 로그 인자로 넘기는 방식은 피해야 합니다.
log.info("login request contains sensitive credential"); // 민감값 원문 기록 금지
AI 리뷰 요청은 이렇게 구체화할 수 있습니다.
회원가입, 로그인, 게시글 작성 API에 대해 운영 추적에 필요한 info/warn/error 로그를 추가해줘. 단, 비밀번호, 토큰, 개인정보 원문은 절대 로그에 남기지 말고 마스킹하거나 식별자만 사용해줘.
백엔드 리뷰를 끝낼 때 프론트엔드 개발자 관점에서 마지막으로 봅니다.
[API 계약]
- URL, method, request, response가 명확한가?
- 성공 응답과 실패 응답의 형태가 문서화되어 있는가?
- pagination, sorting, filtering 규칙이 일관적인가?
[상태와 에러]
- 400, 401, 403, 404, 409, 500이 구분되는가?
- 필드별 입력 오류를 UI에 매핑할 수 있는가?
- 중복/권한/존재하지 않음 같은 도메인 에러 코드가 있는가?
[인증/권한]
- 로그인 필요 API가 명확한가?
- 현재 사용자 기준으로 권한을 검사하는가?
- 프론트엔드가 보낸 사용자 ID를 신뢰하지 않는가?
[데이터]
- Entity가 응답으로 직접 노출되지 않는가?
- 날짜, enum, nullable 필드 규칙이 분명한가?
- 목록 응답에 필요한 최소 정보와 상세 응답이 구분되는가?
[운영]
- 환경 변수와 비밀값이 분리되어 있는가?
- 로컬/운영 설정이 나뉘어 있는가?
- 로그/헬스체크/에러 추적 기준이 있는가?
[테스트]
- 성공뿐 아니라 실패 케이스가 있는가?
- API 응답 구조를 테스트하는가?
- 권한/검증/중복 같은 협업 리스크를 테스트로 잠갔는가?
이 체크리스트는 완벽한 백엔드 아키텍처 평가표가 아닙니다. 지금 단계의 목적은 “프론트엔드 개발자가 AI 백엔드 코드를 실무적으로 검토할 때 놓치면 위험한 지점”을 잡는 것입니다.
AI에게 “백엔드 만들어줘”라고 하면 보통 동작하는 예제를 만듭니다. 하지만 동작하는 예제와 협업 가능한 백엔드는 다릅니다.
그래서 처음 요청부터 품질 기준을 넣는 것이 좋습니다.
Spring Boot로 게시글 CRUD API를 만들어줘.
단, 아래 기준을 지켜줘.
1. Entity를 request/response로 직접 노출하지 말고 DTO를 분리해줘.
2. Controller는 HTTP 입출력, Service는 비즈니스 규칙, Repository는 DB 접근을 담당하게 해줘.
3. 입력 검증은 Bean Validation으로 처리하고, 검증 실패는 일관된 에러 응답으로 내려줘.
4. 게시글 수정/삭제는 현재 로그인 사용자가 작성자인지 확인해줘.
5. 성공/실패 API 응답 예시를 함께 문서화해줘.
6. 정상 케이스와 주요 실패 케이스 테스트를 작성해줘.
7. 비밀번호, 토큰, 비밀값은 코드/로그/문서에 실제처럼 보이는 값으로 쓰지 말고 placeholder를 사용해줘.
이렇게 요청하면 AI가 만들어낸 결과물의 출발점이 달라집니다. 그래도 리뷰는 필요하지만, 처음부터 체크리스트를 기준으로 생성하게 만드는 것이 훨씬 안전합니다.
AI 백엔드 리뷰는 “코드가 돌아가는가?”에서 끝나면 안 됩니다. 특히 프론트엔드 개발자 입장에서는 백엔드를 내부 구현보다 협업 가능한 계약과 운영 가능한 흐름으로 읽어야 합니다.
오늘의 핵심은 아래입니다.
1. 기능 목록보다 요청 흐름을 먼저 따라간다.
2. API 계약은 성공/실패 응답까지 포함한다.
3. 입력 검증은 입구에서, 비즈니스 검증은 Service에서 본다.
4. Entity는 내부 저장 구조이고 DTO는 외부 계약이다.
5. Service와 트랜잭션은 유스케이스의 안전성을 결정한다.
6. 인증과 권한은 반드시 분리해서 본다.
7. 테스트는 성공보다 실패 시나리오를 얼마나 잠갔는지 본다.
8. 배포 설정과 비밀값 관리는 운영 안전성의 일부다.
9. 로그와 관측 가능성은 나중에 문제를 찾을 수 있게 만든다.
10. AI에게 다시 시킬 때는 품질 기준을 프롬프트에 넣는다.
Day 12에서는 백엔드 시리즈를 마무리합니다. 지금까지 배운 내용을 프론트엔드 개발자의 협업 지도 위에 올려서, “내가 백엔드를 어느 정도까지 이해하면 팀에서 안전하게 대화할 수 있는가?”를 정리합니다.