-
저번 주에 이어 이번 주는 서비스의 필수 요소인 게시글 수정/삭제 및 댓글(Comment) 기능을 추가하여 MVP(Minimum Viable Product)를 고도화했다. 또 Swagger를 추가하여 테스트에 용이하게 했다. 또 게시글을 코드 플로우로 작성하여 전체적으로 넓게 볼 수 있도록 유도하였다.
이번 주차 공부 내용
- 게시글 수정 및 삭제 API 구현(Update & Delete Post API)
- 댓글 생성 및 삭제 API 구현(Create & Delete Comment API)
- 작성자 권한 검증 로직 추가(Author Validation & Authorization Logic)
- Swagger API 문서 도입 및 테스트(API Documentation & Testing with Swagger UI)
기능 구현 Flow(코드 로직)
저번 주차에 게시글 생성과 조회 API까지 마무리 했었다. 이번 주차에는 게시글 수정 및 삭제, 댓글 구현을 했는데, 특히 데이터를 수정할 때 매번 DB에 저장 명령을 내리는 대신, 스프링이 내용 변경을 알아서 감지하고 업데이트해 주는 더티 체킹(Dirty Checking)의 편리함을 활용해 코드를 간결하게 만들었다. 또한, 남의 글을 쉽게 건드리지 못하도록 비밀번호를 대조해 작성자 본인만 수정/삭제할 수 있는 안전장치를 마련했다. 마지막으로 누구나 API를 쉽게 눌러보고 테스트할 수 있도록 Swagger 화면을 연동하여 개발 효율을 높였다.
엔티티 캡슐화 및 권한 검증 로직 (Domain)
무분별한 Setter 사용을 지양하고, 엔티티가 스스로 자신의 상태를 변경하고 검증하도록 객체 지향적으로 설계했다.
// Post.java & Comment.java (Domain 공통 로직) // 1. Setter 대신 상태를 변경하는 명확한 비즈니스 메서드 사용 public void update(String title, String content) { this.title = title; this.content = content; } // 2. 권한 검증 로직 캡슐화 public void validatePassword(String inputPassword) { // 들어온 비밀번호와 엔티티의 비밀번호가 다르면 즉시 예외 발생 if (!this.password.equals(inputPassword)) { throw new GeneralException("AUTH403", "비밀번호가 일치하지 않습니다. 수정/삭제 권한이 없습니다."); } }
게시글 수정 API Flow (PUT)
데이터를 찾아 수정할 때 save() 메서드를 강제하지 않고, JPA의 더티 체킹을 활용하여 효율적으로 쿼리를 발생시킨다.
// PostController.java @PutMapping("/{id}") // 클래스 상단의 /api/posts와 결합됨 public ApiResponse<PostResponseDto> updatePost( @PathVariable Long id, @RequestBody @Valid PostRequestDto request) { // 1. 경로 변수로 수정할 대상을 식별하고, DTO로 수정할 데이터(비밀번호 포함)를 수신 PostResponseDto responseDto = postService.updatePost(id, request); return ApiResponse.onSuccess(responseDto); }// PostService.java @Transactional // 2. 더티 체킹을 위해 트랜잭션 환경 구성 필수 public PostResponseDto updatePost(Long postId, PostRequestDto request) { // 3. 식별자로 엔티티 조회 및 404 예외 처리 Post post = postRepository.findById(postId) .orElseThrow(() -> new GeneralException("POST404", "해당 게시글이 존재하지 않습니다.")); // 4. 권한 검증 (엔티티 내부의 검증 로직 호출) post.validatePassword(request.getPassword()); // 5. 더티 체킹 (Dirty Checking): save() 호출 없이 엔티티 상태만 변경하면 트랜잭션 종료 시 UPDATE 쿼리 자동 발생 post.update(request.getTitle(), request.getContent()); return PostResponseDto.from(post); }
게시글 및 댓글 삭제 API Flow (DELETE)
보안을 위해 비밀번호를 쿼리 파라미터로 받아 검증한 뒤, 영구적으로 데이터를 삭제(Hard Delete)한다.
// PostController.java (Comment 삭제 로직도 동일한 규격 적용) @DeleteMapping("/{id}") public ApiResponse<Void> deletePost( @PathVariable Long id, @RequestParam String password) { // 1. @RequestParam: URL 뒤에 붙는 쿼리 파라미터(?password=...)로 검증용 비밀번호 수신 postService.deletePost(id, password); // 2. 삭제 완료 후 되돌려줄 데이터가 없음을 null로 명확히 반환 (상태코드 200 유지) return ApiResponse.onSuccess(null); }// PostService.java @Transactional public void deletePost(Long postId, String password) { Post post = postRepository.findById(postId) .orElseThrow(() -> new GeneralException("POST404", "해당 게시글이 존재하지 않습니다.")); post.validatePassword(password); // 3. 지우기 전 반드시 작성자 권한 검증 postRepository.delete(post); // 4. 영속성 컨텍스트 및 DB에서 해당 데이터 영구 삭제 }
댓글 생성 API Flow (1:N 연관 관계)
어떤 게시글에 달린 댓글인지를 명확히 하기 위해 경로에 부모의 식별자(postId)를 포함하고, 엔티티 간의 객체 참조를 맺어준다.
// CommentController.java @PostMapping("/api/posts/{postId}/comments") public ApiResponse<Long> createComment( @PathVariable Long postId, // 1. 어떤 게시글에 종속된 댓글인지 식별 @RequestBody @Valid CommentRequestDto request) { Long commentId = commentService.createComment(postId, request); return ApiResponse.onSuccess(commentId); }// CommentService.java @Transactional public Long createComment(Long postId, CommentRequestDto request) { // 2. 부모 엔티티(게시글)의 존재 여부를 먼저 확인 Post post = postRepository.findById(postId) .orElseThrow(() -> new GeneralException("POST404", "해당 게시글이 존재하지 않습니다.")); // 3. 댓글 엔티티 생성 시 부모 객체(post) 자체를 주입하여 1:N 연관 관계(FK) 설정 Comment comment = Comment.builder() .content(request.getContent()) .password(request.getPassword()) .post(post) .build(); // 4. 영속화 후 생성된 댓글의 고유 번호 반환 return commentRepository.save(comment).getId(); }
Swagger UI API 문서 자동화
프론트엔드와의 협업 및 자체 테스트 환경 구축을 위해 명세서를 코드로 자동화했다.
// build.gradle dependencies { // 1. Spring Boot 버전에 맞는 Springdoc 라이브러리를 주입하여 별도 설정 없이 /swagger-ui.html 경로에 시각화된 문서 자동 생성 implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.6' }
코드에 사용된 문법(어노테이션) 정리
1. Controller 계층: HTTP 요청 매핑 및 데이터 추출
클라이언트의 HTTP 요청을 가장 먼저 수신하고, 필요한 데이터를 추출해 Service 계층으로 전달하는 역할을 수행한다.
- @PutMapping("/{id}"), @DeleteMapping("/{id}")
- 역할: HTTP 메서드(PUT, DELETE)와 URL 경로를 매핑함.
- 동작: 클래스 레벨에 선언된 @RequestMapping("/api/posts")와 결합하여 최종적인 API 엔드포인트를 완성함.
- @PathVariable Long id
- 역할: URL 경로(Path)에 포함된 가변 변수를 추출함.
- 동작: /{id} 위치에 매핑된 값(예: 게시글 식별자)을 자바 변수에 바인딩하여, 조작할 대상을 명확히 식별함.
- @RequestParam String password
- 역할: URL 쿼리 파라미터(?key=value)를 추출함.
- 동작: DELETE 요청과 같이 Request Body를 주로 사용하지 않는 상황에서, 파라미터로 전달된 검증용 비밀번호 값을 받아옴.
- @RequestBody
- 역할: 클라이언트가 전송한 HTTP Body의 JSON 데이터를 자바 객체(DTO)로 역직렬화(Deserialization)함.
- @Valid
- 역할: DTO 객체 내부에 선언된 제약 조건(예: @NotBlank)을 기반으로 데이터의 유효성을 검증함. 조건 누락 시 즉시 예외를 발생시켜 잘못된 데이터의 유입을 차단함.
2. Service 계층: 트랜잭션 관리 및 예외 처리
비즈니스 로직을 수행하며, 데이터베이스 상태 변화의 안정성을 보장한다.
- @Transactional
- 역할: 데이터베이스 상태를 변경하는 작업들을 하나의 논리적 단위(Transaction)로 묶음.
- 동작: 로직 수행 중 오류가 발생하면 전체 롤백(Rollback)을 수행하여 데이터 무결성을 보장함. 특히 메서드 종료 시점에 JPA의 영속성 컨텍스트 변경 사항을 감지하여 UPDATE 쿼리를 자동 발생시키는 **더티 체킹(Dirty Checking)**의 필수 전제 조건임.
- .orElseThrow(() -> new ...)
- 역할: Optional 객체가 비어있을 경우 지정된 커스텀 예외를 발생시킴.
- 동작: findById의 조회 결과가 존재하지 않을 때, if-null 체크를 하는 대신 람다식을 활용해 예외(GeneralException)를 깔끔하게 던져 가독성을 높임.
3. Domain 계층: 객체 생성과 캡슐화
데이터베이스 테이블과 매핑되는 엔티티의 무결성을 유지하고 생성을 담당한다.
- .builder() ... .build()
- 역할: Lombok에서 제공하는 빌더(Builder) 패턴을 적용함.
- 동작: 생성자의 매개변수 순서에 의존하지 않고, 명시적으로 필드명을 지정하여 객체를 생성함. 어떤 필드에 어떤 값이 매핑되는지 직관적으로 파악할 수 있어 가독성과 유지보수성이 뛰어남.
4. Repository 및 의존성 관리
데이터 접근 계층과 외부 라이브러리 세팅을 담당한다.
- extends JpaRepository<Entity, ID>
- 역할: Spring Data JPA가 제공하는 공통 인터페이스를 상속함.
- 동작: 인터페이스 상속만으로 기본적인 CRUD 및 페이징 처리를 위한 쿼리 메서드가 자동 생성됨. 개발자가 반복적인 SQL을 직접 작성할 필요 없이, 객체 중심의 데이터 접근 로직을 구현할 수 있게 지원함.
- implementation '...' (build.gradle)
- 역할: 프로젝트에 필요한 외부 라이브러리(Swagger 등) 의존성을 추가하고 관리함.
스웨거(Swagger) 정리 및 실행 결과
스웨거는 REST API를 설계, 빌드, 문서화하고 시각화하는 오픈소스 프레임워크이다. 현재는 OpenAPI Specification(OAS)이라는 표준 사양을 기반으로 작동하며, 스프링 부트에서는 springdoc-openapi 라이브러리를 통해 소스 코드에서 직접 문서를 생성한다.
핵심 기능 3가지:
- API 시각화 (Swagger UI): 브라우저에서 모든 API 목록과 상세 정보를 한눈에 볼 수 있다.
- 직접 테스트 (Try it out): 포스트맨(Postman) 같은 외부 도구 없이 브라우저에서 즉시 요청을 보내고 응답을 확인할 수 있다.
- 명세 자동화: 코드가 바뀌면 문서도 자동으로 업데이트되어, 문서와 실제 코드가 일치하지 않는 '문서 파편화' 문제를 방지한다.
실행결과

스웨거 문서 확인 
게시글 생성 
댓글 생성 
내용 수정 비밀번호 불일치 
내용 수정 비밀번호 일치
트러블 슈팅(NullPointerException 해결)
1. 현상 (Issue)
게시글 수정 API를 Swagger에서 테스트하던 중, 분명히 존재하는 ID와 올바른 비밀번호를 입력했음에도 불구하고 COMMON500 서버 내부 오류가 발생했다.
- 에러 메시지: Cannot invoke "String.equals(Object)" because "this.password" is null
- 상태: 게시글 조회(GET)와 생성(POST)은 정상 작동하나, 수정(PUT)과 삭제(DELETE) 단계에서만 서버가 뻗는 상황.
2. 원인 분석 (Analysis)
에러 메시지의 핵심은 this.password가 null이라는 점이다. 즉, 비교 대상인 DB 속 게시글의 비밀번호 데이터가 존재하지 않아 자바의 .equals() 메서드를 호출할 수 없다는 뜻이다.
논리적으로 세 가지 가능성을 타겟팅했다.
- 데이터 정합성 문제: 비밀번호 기능(Field)이 추가되기 전, Docker MySQL에 저장되어 있던 과거의 '오염된' 테스트 데이터가 남아있음.
- DTO 매핑 오류: @Getter가 누락되어 클라이언트가 보낸 비밀번호가 서버 객체에 제대로 담기지 않음.
- 영속화 로직 누락: Service 계층에서 엔티티를 생성할 때 빌더 패턴에서 .password()를 빼먹음.
3. 해결 과정 (Solution)
Step 1: 데이터 정합성 초기화
가장 먼저 의심되는 '구형 데이터'를 제거하기 위해 Docker 환경을 초기화했다.
docker-compose down -v # 볼륨(Volume)까지 완전히 삭제하여 DB 백지화 docker-compose up -d # 클린한 환경에서 재시작Step 2: DTO 및 서비스 로직 전수 조사
PostRequestDto에 @Getter가 정상적으로 붙어있는지, 그리고 PostService에서 엔티티를 생성할 때 비밀번호를 누락 없이 빌더에 넣어주는지 다시 한번 검증했다. (코드상 문제는 없음을 확인)
Step 3: Null-Safe한 방어 코드 작성 (Refactoring)
데이터가 설령 null이더라도 서버가 500 에러로 터지는 것은 좋지 못한 설계라고 판단했다. 엔티티 내부의 검증 로직을 더 안전하게 수정했다.
// Post.java (수정 전) if (!this.password.equals(inputPassword)) { ... } // Post.java (수정 후: Null 체크 추가) public void validatePassword(String inputPassword) { if (this.password == null || !this.password.equals(inputPassword)) { throw new GeneralException("AUTH403", "비밀번호가 일치하지 않거나 정보가 없습니다."); } }4. 결과 (Result)
DB를 초기화하고 새로운 데이터(New ID)를 생성하여 테스트한 결과, COMMON200 성공 응답을 확인할 수 있었다.
코드가 완벽하더라도 기존 데이터의 상태(Data State)에 따라 시스템이 멈출 수 있다는 것을 배웠다. 특히 필드가 추가되는 개발 단계에서는 데이터 정합성 유지가 얼마나 중요한지, 그리고 Null-Safe한 코드 작성이 시스템 안정성에 얼마나 기여하는지 깊이 체감할 수 있었다.
'Study > SpringBoot' 카테고리의 다른 글
[SpringBoot] 6주차 페이지네이션(Pagination), N+1 문제 해결 (0) 2026.05.07 [SpringBoot] 5주차 JWT 인증/인가(회원가입 및 로그인) API 구현 (0) 2026.04.30 [SpringBoot] 3주차 ERD 설계, Docker 연결 및 생성 조회 API 구현 (0) 2026.03.26 [SpringBoot] 2주차 예외 처리 및 Vaildation 적용 (0) 2026.03.19 [SpringBoot] 1주차 JAVA 이해 및 ping API 구현 (0) 2026.03.10