기타

Cursor가 짠 코드, 코드리뷰 통과 가능? 팀 컨벤션에 맞춰 룰 베이스 설정하기

Joonfluence 2025. 4. 12.

이 글은 Cursor 시리즈의 4번째 글입니다.

이런 분들에게 이 글을 추천합니다

  • Cursor를 적극적으로 실무에 적용해보려는 개발자
  • Cursor를 적용했지만, 매번 컨벤션에 맞춰 코드 수정해주는 작업이 귀찮은 개발자
  • 팀 코드리뷰 기준이 점점 까다로워지고 있고 관련하여 리뷰를 자주 받는 개발자

What — 문제는 뭘까?

"이 변수 네이밍 다시 해주세요"
"Autowired는 맨 위로 올려주세요"
"이건 한 줄 띄워야죠..."

빠르게 개발하다 보면, 컨벤션을 잠시 잊고 작성하는 경우가 많습니다.
하지만 컨벤션을 지키는 일은 장기적으로 팀 생산성을 높이기 때문에 반드시 필요합니다.
그래서 리뷰어도 지적할 수밖에 없고, 개발자는 반복적으로 수정해야 하죠.

Cursor(커서) 는 코드를 자동으로 생성해주는 LLM 기반 개발 도구입니다.
전체 맥락을 이해한 상태에서 코드를 짜주지만, 생성되는 코드는 대체로 웹에 있는 일반적인 예시 코드입니다.

즉, 우리 팀의 컨벤션과는 다를 수 있습니다.

예: 메서드 순서, 테스트코드 작성 방법, 네이밍 규칙 등

그래서 중요한 건, Cursor에게 팀 컨벤션이라는 맥락을 정확히 전달하는 것입니다.


📘 Rule 기반 컨텍스트, 왜 필요할까?

Cursor에는 Rule이라는 컨텍스트 문서 정의 기능이 있습니다.
예를 들어 “우리는 gradle을 쓴다”, “우리 팀의 코딩 컨벤션은 이렇다” 같은 내용을 명시할 수 있습니다.

즉, 반복 가능한 컨텍스트를 시스템화하는 것이죠.
기본 설정만으로는 Cursor가 팀 특유의 컨벤션을 반영하기 어렵지만,
커스텀 룰을 정의하면 정확하게 반영 가능합니다.


🛠️ 실무 적용 방법 3단계

1️⃣ 팀 컨벤션 문서화

JPA/QueryDSL/Entity 규칙 예시:

- JPA는 사용하지만 @ManyToOne, @OneToMany는 지양
- Entity에는 @Setter 대신 빌더 or 팩토리 메서드 사용
- Join은 QueryDSL 활용
- @Column 등 불필요한 어노테이션 생략

네이밍 및 구조 컨벤션 예시:

- 엔티티: ~Entity / 레포지토리: ~Repository
- CQRS 패턴: ~CommandService / ~QueryService
- 메서드 순서: 필드 → 생성자 → public → private
- 필드 순서: static final → static → final → 일반
- 공백: 어노테이션과 메서드 사이 한 줄
- import 순서: java > 외부 > 내부

테스트 코드 컨벤션 체크리스트:

- 테스트 메서드 이름은 snake_case or Given_When_Then
- @DisplayName 필수
- Given / When / Then 주석 구분
- 한 테스트 메서드에 하나의 assert 또는 assertAll 사용
- 순서: @Before → @Test → Helper
- 클래스명: PostService → PostServiceTest

2️⃣ .mdc 파일 구성

이를 적용하려면, .cursor/rules 디렉토리에 .mdc 파일을 생성해줘야 합니다.
이는 프로젝트 별 rules를 적용하기 위해, cursor에서 권장하는 방식입니다.
자세한 내용은 공식문서 링크에서도 확인하실 수 있습니다.

저는 해당 디렉토리에 .overview.mdc라는 이름의 파일을 생성하고 위 내용을 적어줬습니다.
그리고 해당 파일은 매번 실행될 때마다, 참고하여 적용되도록 Rule Type을 Always로 지정해줬습니다.

# Cursor Coding Rule Guide

## 💡 JPA / QueryDSL / Entity 규칙
...

## 🔠 네이밍 컨벤션
...

## 🧱 클래스 구성 순서
...

## 🧩 필드 선언 순서
...

## 🧼 코드 스타일
...

## 🧪 테스트 코드 작성 규칙
- 테스트 메서드는 snake_case 또는 Given_When_Then
- @DisplayName 어노테이션 필수
- 하나의 테스트에는 하나의 assert 또는 assertAll 사용
- 테스트 클래스 명은 ~ServiceTest, ~ControllerTest 형태

이제 Cursor에게 명령할 때, 다음과 같은 팀 컨벤션이 새롭게 맥락으로 전달되게 됩니다.

3️⃣ 결과 비교: Rule 적용 전 vs 후

자, 여태까지 프로젝트에 테스트코드로 코드 레벨에서 검증해보진 못했습니다.

GPT와 대화를 통해, 아래와 같은 최적의 프롬프트를 뽑았습니다.
이제 Cursor에 프롬프트를 입력하고 컨벤션에 맞춰 코드가 잘 작성되는지 따져보면서
동시에, 실제 테스트코드를 추가해보겠습니다.

지금까지 작성된 Java Spring 프로젝트에 대해 테스트 코드를 생성해줘.

요구사항은 다음과 같아.

1. 테스트 대상 클래스

- PostService
- CommentService
- PostController
- CommentController

2. 테스트 환경

- JUnit5 기반으로 작성해줘
- Controller 테스트는 MvcTest로 구성하고, MockMvc 사용해서 테스트해줘
- Controller도 모킹 처리가 필요하면 @MockBean 사용하지 않고 mock 처리해줘
- Service는 @SpringBootTest 기반으로 실제 DB 통신하여 통합테스트
- Service는 모킹 처리가 필요하면 @MockBean 사용하지 않고 mock 처리해줘

3. 검증할 주요 로직 (예시)

- PostService: 게시글 생성 시 createdAt이 null이 아니어야 함
- CommentService: 대댓글 생성 시 parentId가 null이 아니면 저장 가능해야 함
- PostController: GET /posts 요청 시 정상 200 응답과 JSON 응답이 와야 함
- CommentController: 댓글 삭제 시 상태 코드 204(No Content) 응답 확인
  • After (룰 적용 후)

너무 놀랍게도.. 너무 잘 작성해주는 걸 볼 수 있었습니다.
현재 코드에선 Service Layer 테스트에 대해서만 작성해두었는데,
Controller Layer에서도 너무 잘 작성되어서 놀랐습니다.

@SpringBootTest
@Transactional
class PostServiceTest {

    @Autowired
    private PostService postService;

    @Autowired
    private PostRepository postRepository;

    @Test
    @DisplayName("게시글 생성 시 createdAt이 null이 아니어야 함")
    void createPost() {
        // given
        Post post = new Post(null, "테스트 제목", "테스트 내용", null);

        // when
        Post savedPost = postService.save(post);

        // then
        assertThat(savedPost.getCreatedAt()).isNotNull();
    }

    @Test
    @DisplayName("게시글 목록 조회")
    void findAll() {
        // given
        Post post1 = new Post(null, "제목1", "내용1", null);
        postRepository.save(post1);

        Post post2 = new Post(null, "제목2", "내용2", null);
        postRepository.save(post2);

        // when
        List<Post> posts = postService.findAll();

        // then
        assertThat(posts).hasSize(2);
    }

    @Test
    @DisplayName("존재하지 않는 게시글 조회 시 예외 발생")
    void findById_notFound() {
        // when & then
        assertThatThrownBy(() -> postService.findById(999L))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("해당 게시글이 존재하지 않습니다.");
    }

    @Test
    @DisplayName("게시글 수정")
    void update() {
        // given
        Post post = new Post(null, "원래 제목", "원래 내용", null);
        Post savedPost = postRepository.save(post);

        Post updatePost = new Post(null, "수정된 제목", "수정된 내용", null);

        // when
        Post updatedPost = postService.update(savedPost.getId(), updatePost);

        // then
        assertThat(updatedPost.getTitle()).isEqualTo("수정된 제목");
        assertThat(updatedPost.getContent()).isEqualTo("수정된 내용");
    }

    @Test
    @DisplayName("게시글 삭제")
    void delete() {
        // given
        Post post = new Post(null, "삭제할 게시글", "삭제할 내용", null);
        Post savedPost = postRepository.save(post);

        // when
        postService.delete(savedPost.getId());

        // then
        assertThat(postRepository.findById(savedPost.getId())).isEmpty();
    }
}

저는 하나도 건들인 부분이 없습니다..

  • Before

반면 overview.mdc 파일을 삭제하고 실행해보면 어떤 결과가 나올까요?
결과는 다음과 같습니다.

package com.board.service;

import com.board.entity.Post;
import com.board.repository.PostRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

@SpringBootTest
@Transactional
class PostServiceTest {

    @Autowired
    private PostService postService;

    @Autowired
    private PostRepository postRepository;

    @Test
    void 게시글_생성_시_createdAt이_자동으로_설정된다() {
        // given
        Post post = Post.builder()
                .title("테스트 제목")
                .content("테스트 내용")
                .build();

        // when
        Post savedPost = postService.save(post);

        // then
        assertThat(savedPost.getCreatedAt()).isNotNull();
        assertThat(savedPost.getCreatedAt()).isBeforeOrEqualTo(LocalDateTime.now());
    }

    @Test
    void 게시글_조회_시_존재하지_않는_게시글은_예외를_발생시킨다() {
        // given
        Long nonExistentId = 999L;

        // when & then
        assertThatThrownBy(() -> postService.findById(nonExistentId))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("해당 게시글이 존재하지 않습니다.");
    }

    @Test
    void 게시글_수정이_정상적으로_동작한다() {
        // given
        Post post = Post.builder()
                .title("원래 제목")
                .content("원래 내용")
                .build();
        Post savedPost = postService.save(post);

        Post updatePost = Post.builder()
                .title("수정된 제목")
                .content("수정된 내용")
                .build();

        // when
        Post updatedPost = postService.update(savedPost.getId(), updatePost);

        // then
        assertThat(updatedPost.getTitle()).isEqualTo("수정된 제목");
        assertThat(updatedPost.getContent()).isEqualTo("수정된 내용");
    }

    @Test
    void 게시글_삭제가_정상적으로_동작한다() {
        // given
        Post post = Post.builder()
                .title("삭제할 게시글")
                .content("삭제할 내용")
                .build();
        Post savedPost = postService.save(post);

        // when
        postService.delete(savedPost.getId());

        // then
        assertThat(postRepository.findById(savedPost.getId())).isEmpty();
    }
}

프롬프트에 이미 어느정도 가이드라인을 적어두어서인지
컨벤션 문서를 참고해서 작성하라고 따로 이야기 하지 않더라도
꽤 정확하게 작성해주는 걸 알 수 있습니다.

다만, 메서드명이 snake case가 아니고
테스트메서드에 @DisplayName가 빠져 있으며,
하나의 테스트 메서드는 여러 개의 assert문이 있는 것을 확인 할 수 있었습니다.

💡 Insight — 실무에서 기억할 한 줄

Cursor는 단순 코드 생성기가 아닙니다.
우리 팀 컨벤션을 기억하는 AI 동료로 만드는 게 핵심입니다.

컨벤션을 시스템화하고, 그 규칙을 Cursor에 전달하세요.
이제 개발자는 코딩보다 문제 해결과 리뷰 효율에 집중할 수 있는 시대입니다.

커서를 사용하는 개발자라면 지금 당장 팀 컨벤션을 .cursor/rules에 적용해보세요.
생산성과 품질, 둘 다 잡을 수 있습니다.


실제 적용된 코드는 아래 레포지토리에서 확인하실 수 있습니다.

https://github.com/joonfluence/cursor

[GitHub - joonfluence/cursor

Contribute to joonfluence/cursor development by creating an account on GitHub.

github.com](https://github.com/joonfluence/cursor)

반응형

댓글