백엔드

[Spring Boot] tave 스터디 3주차 개념 정리

밍들레밍 2026. 4. 4. 22:07

- 트러블슈팅

문제
코드를 수정했는데 Postman에서 수정 내용이 안 보였고, 실행할 때 포트 충돌도 자꾸 발생했다.

원인
로컬 IntelliJ의 Spring Boot와 Docker 컨테이너의 Spring Boot를 둘 다 실행하고 있었기 때문이다.
즉, 같은 앱이 두 군데서 떠 있었고 둘 다 8080 포트를 쓰려 해서 충돌이 났다.
또 Postman이 내가 수정한 로컬 앱이 아니라 Docker 앱을 보고 있어서, 코드 변경이 반영되지 않는 것처럼 보였다.

해결
Spring Boot는 한 군데에서만 실행하고, DB만 Docker로 실행하도록 정리했다.
즉,

  • Spring Boot: IntelliJ
  • MySQL: Docker
  • Postman: localhost:8080

배운 점
코드를 수정했는데 반영이 안 보이면, 먼저 어느 서버가 실제로 응답하고 있는지 확인해야 한다.
같은 Spring Boot를 로컬과 Docker에서 동시에 실행하면 헷갈리고 포트 충돌도 발생할 수 있다.

 

 

 

- 무한스크롤

테이블의 모든 행을 스캔하는 COUNT를 사용하지 않으려면 Spring 에서 Page대신 Slice를 쓸 수 있음

 

 댓글 엔티티와 API 설계하기

- 먼저, 실습에서 만든 파일 정리

 

  • Comment → 댓글 데이터 자체, 댓글 엔티티(Entity) 즉, DB의 comments 테이블과 연결되는 클래스
  • CommentRequest → 클라이언트가 댓글 작성/수정할 때 보내는 데이터를 담는 요청 DTO
  • CommentResponse → 서버가 댓글 정보를 사용자에게 돌려줄 때 쓰는 응답 DTO
  • CommentRepository → 댓글을 DB에서 저장/조회/삭제하는 곳
  • CommentService → 컨트롤러가 받은 요청을 실제로 처리하는 곳, 댓글 기능의 핵심 로직 담당
  • CommentController → 댓글 관련 HTTP 요청을 받는 곳

 

클라이언트 요청 → Request DTO → Service → Entity → DB

그리고 응답은

DB → Entity → Response DTO → 클라이언트

 

* DTO :  데이터를 주고받기 위한 객체, 꼭 필요한 데이터만 담아서 전달하기 위한 객체

 

 

- JPA와 기본생성

DB에서 데이터를 읽어올 때 JPA가 엔티티 객체를 대신 생성해야 하기 때문에 기본생성자가 필요함

@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)

 

  • JPA를 사용하려면 항상 기본생성자가 필요한데 @AllArgsConstructor는 모든 필드를 받는 생성자를 만들 뿐, 기본생성자 사라지기 때문에 @NoArgsConstructor필요
  • 기본생성자를 protected로 한 이유는 JPA가 Comment 클래스를 그대로 사용하는 것이 아닌 해당 클래스를 상속한 다른 프록시 객체를 사용하기 때문에 접근 범위 늘려줌 +JPA 내부에서만 사용하게

*프록시 : 진짜 객체인 척하는 가짜 껍데기 객체

예를 들어 comment.getPost()를 할 때까지 실제 Post를 안 가져오려고 일단 Post 대신 프록시를 넣어두고, 진짜로 필요할 때 DB 조회를 함

 

 

 

 댓글 서비스

- 왜 @Service를 쓰는가

컨트롤러에 모든 코드를 다 넣으면 너무 복잡해짐

예를 들어 컨트롤러가 직접

  • 게시글 찾기
  • 댓글 내용 검증
  • 댓글 저장
  • 예외 처리

이런 걸 전부 하면 코드가 길어지고 관리가 어려워지기 때문에

  • 컨트롤러는 요청만 받음
  • 서비스는 핵심 로직 처리
  • 레포지토리는 DB 처리

이렇게 분리

 

- Spring Bean

스프링빈 : 스프링이 생성하고 관리하고, 필요할 때 주입해주는 객체

CommentService service = new CommentService();
 

이건 필요할 때 내가 직접 new 해서 만드는 방식

@Service
public class CommentService {
}
 

이렇게 해두면 스프링이 CommentService 객체를 직접 생성해서 관리 즉, 스프링이 자기가 가지고 있던 빈을 넣어
그래서 다른 클래스에서 직접 new CommentService() 하지 않고도 사용할 수 있음

 

 

- Builder

@Builder는 객체를 더 편하고, 덜 헷갈리게 만들기 위해 씀. 특히 필드가 많을 때 진가가 크다.

 

예를 들어 Comment 객체를 만든다고 해보자.

Comment comment = new Comment(1L, "tave", "민주", post);

이렇게 생성자를 쓰면 문제는:

  • 1L이 뭐였지?
  • "tave"이 content였나?
  • "민주"가 author였나?
  • 순서 틀리면 어떡하지?

이렇게 파라미터 순서를 외워야 해서 헷갈리기 쉬움

Comment comment = Comment.builder()
        .id(1L)
        .content("tave")
        .author("민주")
        .post(post)
        .build();

builder를 이용하면

  • 어떤 값을 어떤 필드에 넣는지 눈에 보임
  • 순서를 안 외워도 됨
  • 필요한 값만 골라 넣기 편함
  • 코드 읽기가 쉬움

 

 

레이어드 아키텍처와 DTO, 엔티티

- 엔티티(Entity)

DB 테이블과 연결되는 객체

@Entity
@Table(name = "comments")
public class Comment {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String content;

    private LocalDateTime createdAt;
}

Comment 클래스는 DB의 comments 테이블 한 행(row)과 대응되는 객체이다.

 

 

- DTO (Data Transfer Object)

데이터를 전달하기 위한 객체

사용자가 보내는 걸 엔티티로 받지 않고 DTO로 받을 수 있음

DTO는

  • 화면에 주고받을 데이터 모양에 맞춘 객체
  • DB랑 직접 연결되지 않음
  • 필요한 데이터만 담음

 

Q. 엔티티와 DTO의 핵심 차이

엔티티
  • DB 저장용
  • 테이블 구조와 연결됨
  • JPA가 관리
  • 보통 @Entity 붙음
DTO
  • 데이터 전달용
  • 요청/응답에 맞게 자유롭게 만듦
  • DB와 직접 연결 안 됨
  • @Entity 안 붙음

 

Q. 왜 엔티티를 그대로 쓰면 안 되는지

 

“엔티티에 필드 다 있는데 그냥 엔티티 쓰면 되지 않나?” 싶은데, 보통 분리하는 게 좋다

  1. DB 구조를 그대로 노출하게 됨. 엔티티에는 DB용 필드가 들어있는데,
    그걸 그대로 응답하면 비밀번호나 내부 관리용 값 같은 사용자에게 불필요한 정보까지 나갈 수 있음
  2. 요청 형식과 DB 구조는 다를 수 있음. 사용자는 content만 보내면 되는데 엔티티에는 id, createdAt 같은 필드도 있음. 즉 사용자가 보내는 데이터 모양과 DB에 저장되는 데이터 모양은 다를 수 있다
  3. 유지보수가 쉬워짐. 나중에 DB 구조가 바뀌어도
    DTO를 따로 두면 API 형식이 덜 흔들림

 

 

- Record

record : 자바에서 데이터만 깔끔하게 담기 위해 만든 특별한 클래스 형태

 DTO를 더 짧고 편하게 만들 때 자주 쓰는 문법이다

 

예를 들어 댓글 요청 DTO를 일반 클래스로 만들면

public class CommentCreateRequest {
    private final String content;

    public CommentCreateRequest(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }
}

이렇게 필드, 생성자, getter를 다 써야함

그런데 record를 쓰면

public record CommentCreateRequest(String content) {
}

이 한 줄로 거의 같은 뜻이 됨

 record는
복잡한 동작보다 데이터를 담는 게 목적
임을 더 분명하게 표현하는 문법

 

 

 

기능 강화하기(심화) - 검증, 연관관계, 트랜잭션

- 요청 본문 유효성 검증 (validation)

service의 비즈니스 로직에 도달하지도 못하게 Controller에서 차단

  • @Size = 검사 규칙 적어놓기
  • @Valid = 그 검사 규칙 실행시키기

 

- ManyToOne

@ManyToOne(fetch = FetchType.LAZY)
private Post post;

일단 Comment만 가져오고, post는 실제로 필요할 때까지 안 가져온다.

 

 

 

- N+1 문제

N+1문제 : 한번에 쿼리에 따라서 N번의 쿼리가 발생하는 문제

->성능 느려짐

성능을 좌지우지 하는 것이 DB와 네트워크의 통신

Comment가 Post를 연관 객체로 가지게 되었고, 그 연관관계를 LAZY로 조회한 상태에서 댓글 목록을 가져온 뒤 각 댓글마다 comment.getPost()를 호출하면서 Post를 개별 조회하게 되어 N+1 문제가 발생했다.

( FetchType.LAZY여도, FetchType.EAGER 여도 N+1 문제가 발생하는건 똑같다 다만 발생하는 시점을 조절할 뿐 ) 

 

해결방법

  1. in절 쿼리 :
    • comments를 먼저 조회하고
    • 필요한 postId만 뽑고
    •  post들을 한 번에 조회해서 Map에 넣은 뒤
    • 댓글마다 DB를 다시 조회하지 않고 Map에서 찾아 쓰게 바꾼 것
  2. JPQL : 보통 Spring Data JPA는 메서드 이름만으로도 많이 해결되지만, 자동 생성만으로는 원하는 조회방식을 정확히 통제할 수 없을 때 직접 쿼리를 쳐서 JPQL 이용
  3. EntityGraph : 기본적으로 left join 쿼리로 연관관계 데이터를 함께 조인 조회함 => 이유는 더 안전하게 원본 엔티티를 보존하려는 성격