- 트러블슈팅
문제
코드를 수정했는데 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. 왜 엔티티를 그대로 쓰면 안 되는지
“엔티티에 필드 다 있는데 그냥 엔티티 쓰면 되지 않나?” 싶은데, 보통 분리하는 게 좋다
- DB 구조를 그대로 노출하게 됨. 엔티티에는 DB용 필드가 들어있는데,
그걸 그대로 응답하면 비밀번호나 내부 관리용 값 같은 사용자에게 불필요한 정보까지 나갈 수 있음 - 요청 형식과 DB 구조는 다를 수 있음. 사용자는 content만 보내면 되는데 엔티티에는 id, createdAt 같은 필드도 있음. 즉 사용자가 보내는 데이터 모양과 DB에 저장되는 데이터 모양은 다를 수 있다
- 유지보수가 쉬워짐. 나중에 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 문제가 발생하는건 똑같다 다만 발생하는 시점을 조절할 뿐 )
해결방법
- in절 쿼리 :
- comments를 먼저 조회하고
- 필요한 postId만 뽑고
- 그 post들을 한 번에 조회해서 Map에 넣은 뒤
- 댓글마다 DB를 다시 조회하지 않고 Map에서 찾아 쓰게 바꾼 것
- JPQL : 보통 Spring Data JPA는 메서드 이름만으로도 많이 해결되지만, 자동 생성만으로는 원하는 조회방식을 정확히 통제할 수 없을 때 직접 쿼리를 쳐서 JPQL 이용
- EntityGraph : 기본적으로 left join 쿼리로 연관관계 데이터를 함께 조인 조회함 => 이유는 더 안전하게 원본 엔티티를 보존하려는 성격
'백엔드' 카테고리의 다른 글
| [Spring Boot] tave 스터디 2주차 개념 정리 (0) | 2026.03.29 |
|---|---|
| [Spring Boot] tave 스터디 1주차 개념 정리 (0) | 2026.03.22 |