👥💡 N+1 리팩토링 과정

2026. 2. 7. 22:04·프로젝트/스프링 부트 3 백엔드 개발자 되기 Blog + 선착순 강의 프로젝트

🛠 리팩토링 1단계: 객체 연관관계 맺기 & N+1 문제 직접 확인하기

문자열로 작성자를 관리하던 구조를
JPA 연관관계 기반 설계로 리팩토링하며 N+1 문제를 직접 재현하고 해결한 과정


1️⃣ 왜 리팩토링이 필요했을까?

기존 Article 엔티티에서는 작성자를 아래와 같이 관리하고 있었다.

private String author; // 작성자 이메일 문자열

❌ 이 구조의 문제점

  • 작성자의 닉네임, ID, 권한 정보가 필요해질 때마다
    → UserRepository를 매번 조회
  • 객체 간 관계가 아니라 문자열 의존
  • DB 관점, 객체 관점 모두에서 확장성이 낮음

👉 객체지향 설계와 JPA를 쓰는 의미가 사라지는 구조


2️⃣ 리팩토링 목표

  • Article이 작성자를 문자열이 아닌 User 객체로 직접 참조
  • DB에는 외래키(FK) 로 저장
  • 이후 발생할 N+1 문제를 눈으로 확인하고 해결까지 진행

3️⃣ Article 엔티티 리팩토링 (Before → After)

🔴 Before (리팩토링 전)

private String author;

🟢 After (리팩토링 후)

// Article.java

@ManyToOne(fetch = FetchType.LAZY) // 지연 로딩
@JoinColumn(name = "author_id")   // DB에는 FK로 저장
private User author;

@Builder
public Article(User author, String title, String content) {
    this.author = author;
    this.title = title;
    this.content = content;
}

✅ 변경 포인트 정리

항목 Before After
작성자 타입 String User
DB 저장 방식 이메일 문자열 author_id (FK)
연관관계 없음 @ManyToOne
로딩 전략 없음 LAZY
객체지향 설계 원칙에 따라
Article → User 단방향 연관관계만 우선 적용
(양방향은 실제 요구사항 발생 시 확장)

4️⃣ 왜 “빨간 줄(컴파일 에러)”이 터지기 시작할까?

author의 타입을 String → User로 바꾸는 순간,
기존 코드들이 연쇄적으로 깨지기 시작한다.

🔥 에러가 발생하는 지점들

  1. AddArticleRequest
    • toEntity(String author) → 이제 User를 요구
  2. BlogService
    • request.toEntity(userName)에서 타입 불일치
  3. 권한 체크 로직
// 기존
article.getAuthor().equals(userName);

// 수정 필요
article.getAuthor().getEmail().equals(userName);

👉 이 파급 효과가 바로 “진짜 리팩토링”


5️⃣ BlogService 리팩토링

@RequiredArgsConstructor
@Service
public class BlogService {

    private final BlogRepository blogRepository;
    private final UserRepository userRepository;

    public Article save(AddArticleRequest request, String userName) {

        // 이메일로 User 엔티티 조회
        User user = userRepository.findByEmail(userName)
                .orElseThrow(() -> new IllegalArgumentException("not found: " + userName));

        // User 객체를 직접 전달
        return blogRepository.save(request.toEntity(user));
    }

    private void authorizeArticleAuthor(Article article) {
        String userName = SecurityContextHolder.getContext()
                .getAuthentication().getName();

        if (!article.getAuthor().getEmail().equals(userName)) {
            throw new IllegalArgumentException("not authorized");
        }
    }
}

6️⃣ DTO까지 파급된다: ArticleViewResponse 수정

이제 article.getAuthor()는 User 객체다.
화면에는 User 전체가 아니라 닉네임 or 이메일만 필요하다.

@NoArgsConstructor
@Getter
public class ArticleViewResponse {

    private Long id;
    private String title;
    private String content;
    private LocalDateTime createdAt;
    private String author; // 화면 출력용

    public ArticleViewResponse(Article article) {
        this.id = article.getId();
        this.title = article.getTitle();
        this.content = article.getContent();
        this.createdAt = article.getCreatedAt();

        // 🚨 이 한 줄이 N+1의 스위치
        this.author = article.getAuthor().getNickname();
    }
}

7️⃣ 드디어 N+1 문제가 터진다

처음엔 왜 N+1이 안 보였을까?

select * from article;
  • 글 목록만 조회
  • 작성자 정보 접근 ❌
  • Lazy Loading 발동 ❌

👉 그래서 쿼리 1번만 실행됨

작성자 닉네임을 쓰는 순간

for (Article article : articles) {
    System.out.println(article.getAuthor().getNickname());
}

실행 로그

select * from article;           -- 1번
select * from users where id=?; -- 2번
select * from users where id=?; -- 3번

📌 글 2개 → 쿼리 3번
📌 글 100개 → 쿼리 101번

👉 이게 바로 N+1 문제


8️⃣ 오해 바로잡기

❓ “다른 사람 글을 가져와서 문제인가요?”

 

❌ 아니다.

진짜 문제는?

  • 글 먼저 조회
  • 그 다음 작성자 정보를 하나씩 다시 조회
  • 데이터를 가져오는 ‘순서와 방식’의 문제

9️⃣ 해결: Fetch Join

BlogRepository 수정

public interface BlogRepository extends JpaRepository<Article, Long> {

    @Query("select a from Article a join fetch a.author")
    List<Article> findAll();
}

BlogService는 다시 단순해진다

public List<Article> findAll() {
    return blogRepository.findAll();
}

실행 결과

select a.*, u.*
from article a
join users u on u.id = a.author_id;

✅ 쿼리 단 1번
✅ N+1 완전 해결

'프로젝트 > 스프링 부트 3 백엔드 개발자 되기 Blog + 선착순 강의 프로젝트' 카테고리의 다른 글

👥💡 Querydsl 페이징: fetchResults()가 비권장(Deprecated)된 이유와 실무 정석 패턴  (0) 2026.02.14
👥🚨 수정시간 및 수정하기 작성자 권한 변경  (0) 2026.02.14
👥🚨 수정은 됐는데 화면에는 “실패”라고 뜬다?  (1) 2026.02.07
👥🚨 로컬로 실행 후 배포한 서버에도 DB가 적용되는 이유  (0) 2026.02.07
👥🚨 배포 후 로컬 실행 시 OAuth2 승인 오류  (0) 2026.02.07
'프로젝트/스프링 부트 3 백엔드 개발자 되기 Blog + 선착순 강의 프로젝트' 카테고리의 다른 글
  • 👥🚨 수정시간 및 수정하기 작성자 권한 변경
  • 👥🚨 수정은 됐는데 화면에는 “실패”라고 뜬다?
  • 👥🚨 로컬로 실행 후 배포한 서버에도 DB가 적용되는 이유
  • 👥🚨 배포 후 로컬 실행 시 OAuth2 승인 오류
hak0622
hak0622
개발하면서 “이게 뭐지?”라는 순간마다 궁금한 점을 바탕으로 정리한 개발 블로그입니다.
  • hak0622
    궁금한 개발 이야기 Why?
    hak0622
  • 전체
    오늘
    어제
    • 분류 전체보기 (68)
      • 공부 (36)
        • 1. 자바 ORM 표준 JPA 프로그래밍 - 기본.. (35)
        • 시험 (1)
      • 프로젝트 (32)
        • 스프링 부트 3 백엔드 개발자 되기 Blog + .. (32)
  • 인기 글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
hak0622
👥💡 N+1 리팩토링 과정
상단으로

티스토리툴바