findById vs getReferenceById 차이
| 메서드 | 사용하는 곳 | 대상 |
|---|---|---|
findById |
getPost(), getPostWithComments() |
Post 조회 |
getReferenceById |
registerPost(), updatePost()) |
User, Post |
findById — 즉시 SELECT 실행
// PostService.java:29-33
@Transactional
public PostDto getPost(Long pid) {
return postRepository.findById(pid) // ← 이 시점에 SELECT 실행
.map(PostDto::from)
.orElseThrow(() -> new NoSuchElementException("해당 게시글 존재 x"));
}
findById(pid)가 호출되는 그 즉시 DB에 SELECT 쿼리를 날린다.
-- findById(pid) 호출 시 즉시 실행
SELECT p1_0.pid, p1_0.category_type, p1_0.content, p1_0.created_by,
p1_0.created_date, p1_0.modified_by, p1_0.modified_date, p1_0.title,
u1_0.uid, u1_0.created_by, u1_0.created_date, u1_0.email,
u1_0.modified_by, u1_0.modified_date, u1_0.password, u1_0.role_type, u1_0.username
FROM post p1_0
LEFT JOIN user u1_0 ON u1_0.uid = p1_0.uid
WHERE p1_0.pid = ?
Optional<Post>를 반환한다.- DB에 해당 row가 없으면
Optional.empty()가 온다. - 결과: 실제 데이터가 채워진 진짜 Post 엔티티 객체를 반환한다.
getReferenceById — SELECT 없이 프록시 반환
// PostService.java:51
User user = userRepository.getReferenceById(postDto.getUserDto().getUid());
// ↑ 이 시점에서 SELECT 실행 안 함! 프록시 객체만 반환
// PostService.java:60
Post post = postRepository.getReferenceById(pid);
// ↑ 역시 SELECT 실행 안 함! 프록시 객체만 반환
getReferenceById()는 호출 시점에 DB에 쿼리를 보내지 않는다.
대신 Hibernate가 만든 프록시(Proxy) 객체를 반환한다.
프록시 객체란?
실제 User 객체:
┌──────────────────────┐
│ uid = "user1" │ ← 모든 필드에 실제 값이 들어있음
│ username = "홍길동" │
│ email = "a@b.com" │
│ password = "1234" │
└──────────────────────┘
프록시 User 객체:
┌──────────────────────┐
│ uid = "user1" │ ← ID만 가지고 있음 (파라미터로 넘긴 값)
│ username = ??? │ ← 나머지 필드는 비어있음
│ email = ??? │
│ password = ??? │
└──────────────────────┘
↓ username에 접근하면?
그제서야 SELECT 쿼리 실행!
- 프록시는 실제 엔티티 클래스를 상속한 가짜 객체다.
- ID 값만 가지고 있고, 나머지 필드는 비어있다.
- 프록시의 ID가 아닌 다른 필드(예:
user.getUsername())에 접근하면, 그 시점에 SELECT가 실행된다. - DB에 해당 row가 없으면
EntityNotFoundException이 발생한다 (Optional이 아님).
언제 어떤 것을 쓸까?
| 상황 | 적절한 메서드 | 이유 |
|---|---|---|
| 조회한 데이터를 화면에 보여줘야 할 때 | findById |
실제 데이터가 필요하므로 즉시 SELECT |
| FK 설정만 필요할 때 (INSERT/UPDATE 시 연관 엔티티) |
getReferenceById |
ID만 있으면 되므로 SELECT 불필요 |
| 존재 여부를 확인해야 할 때 | findById |
프록시는 존재 여부를 즉시 알 수 없음 |
(1) registerPost
// User의 실제 데이터가 필요 없다.
// Post 테이블의 uid 컬럼에 FK만 넣으면 된다.
// → getReferenceById가 적절하다.
User user = userRepository.getReferenceById(postDto.getUserDto().getUid());
Post post = postDto.toEntity(user); // Post의 user 필드에 프록시를 세팅
postRepository.save(post); // INSERT 시 프록시의 uid만 사용
(2) getPost
// Post의 모든 필드를 화면에 보여줘야 한다.
// → findById가 적절하다.
return postRepository.findById(pid)
.map(PostDto::from) // Post의 모든 필드 + User의 모든 필드에 접근
Lazy Loading vs Eager Loading
JPA의 기본 fetch 전략
| 어노테이션 | 기본값 | 이유 |
|---|---|---|
@ManyToOne |
EAGER | "Many" 쪽에서 "One"을 조회 → 항상 1건이므로 부담 적음 |
@OneToMany |
LAZY | "One" 쪽에서 "Many"를 조회 → 수백~수천 건일 수 있어 위험 |
@OneToOne |
EAGER | 1건이므로 부담 적음 |
@ManyToMany |
LAZY | 대량 데이터 가능 |
Post & PostComment Entity의 fetch 전략
// Post.java:46-48
@ManyToOne(cascade = CascadeType.PERSIST) // ← fetch 속성 생략 = 기본값 EAGER
@JoinColumn(name = "uid")
private User user;
Post → User: EAGER (fetch 속성을 지정하지 않았으므로 @ManyToOne의 기본값인 EAGER 적용)
// Post.java:43-44
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL) // ← 기본값 LAZY
private final Set<PostComment> postComments = new LinkedHashSet<>();
Post → PostComment: LAZY (@OneToMany의 기본값)
// PostComment.java:25-27
@ManyToOne(optional = false) // ← 기본값 EAGER
@JoinColumn(name = "pid", referencedColumnName = "pid")
private Post post;
// PostComment.java:29-31
@ManyToOne(optional = false) // ← 기본값 EAGER
@JoinColumn(name = "uid", referencedColumnName = "uid")
private User user;
PostComment → Post: EAGER
PostComment → User: EAGER
요약
Post 조회 시:
├── User (EAGER) → Post SELECT할 때 LEFT JOIN으로 함께 가져옴
└── PostComment (LAZY) → postComments에 접근하기 전까지 SELECT 안 함
접근하면 그때 SELECT 실행
PostComment 조회 시 (이론상):
├── Post (EAGER) → PostComment를 findById()로 직접 조회하면 LEFT JOIN (이론상!)
└── User (EAGER) → PostComment를 findById()로 직접 조회하면 LEFT JOIN
프로젝트에서의 실제 동작:
사실 PostComment를 findById()로 직접 조회하는 코드는 없다.
PostComment가 로딩되는 경우는 두 가지뿐이다:
post.getPostComments()— Lazy 로딩 시
→ Post는 이미 영속성 컨텍스트에 존재하므로 Post JOIN 없음. User만 LEFT JOIN.deleteByIdAndPost_IdAndUser_Uid()— 댓글 삭제 시
→ Post, User JOIN이 보이지만 EAGER 로딩이 아니라 WHERE 조건 검색용 JOIN.따라서 이 프로젝트에서 PostComment → Post EAGER LEFT JOIN은 실제로 발생하지 않는다.
Lazy 로딩이 실제 발생하는 시점
⚠️ 참고로 getPost 메서드는
상세페이지(GET /posts/{pid})에서
수정페이지(GET /posts/{pid}/form)로 넘어갈 때 호출된다. (수정 폼에서는 댓글이 필요 없다)
하지만 영속성 컨텍스트가 공유되지는 않는다.
HTTP 요청마다 영속성 컨텍스트가 새로 만들어지기 때문이다.
[1번째 요청] GET /posts/1 (상세페이지)
→ @Transactional 시작 → 영속성 컨텍스트 생성
→ getPostWithComments(1) → SELECT Post, SELECT PostComment
→ @Transactional 종료 → 영속성 컨텍스트 소멸 ← 여기서 전부 사라짐
[2번째 요청] GET /posts/1/form (수정 폼)
→ @Transactional 시작 → 새 영속성 컨텍스트 생성 (1번 요청과 무관)
→ getPost(1) → SELECT Post JOIN User ← DB에 다시 쿼리
→ @Transactional 종료 → 영속성 컨텍스트 소멸
영속성 컨텍스트는 하나의 트랜잭션(= 하나의 HTTP 요청) 안에서만 살아있다.
브라우저에서 페이지를 이동하면 완전히 새로운 요청이므로, 이전에 조회했던
Post/User 캐시는 이미 없다.
getPost()
// PostService.java:30
postRepository.findById(pid)
// → SELECT post LEFT JOIN user 실행 (User는 EAGER이므로 JOIN)
// → PostComment는 LAZY이므로 아직 로딩 안 됨
.map(PostDto::from)
// → PostDto.from() 내부에서 post.getUser() 호출 (이미 JOIN으로 로드됨, 추가 쿼리 없음)
// → PostComment에는 접근하지 않으므로 PostComment SELECT 없음
getPostWithComments()
// PostService.java:37
postRepository.findById(pid)
// → SELECT post LEFT JOIN user 실행
// PostService.java:38
.map(PostWithCommentsDto::from)
// → PostWithCommentsDto.from() 내부:
// PostWithCommentsDto.java:42
post.getPostComments().stream() // ← 이 순간! Lazy 로딩 발동!
// → SELECT post_comment LEFT JOIN user WHERE pid = ? 실행
// → PostComment들이 이 시점에 DB에서 로딩됨
핵심: post.getPostComments()를 호출하는 그 순간에 PostComment를 조회하는 SELECT가 실행된다. getPost()에서는 PostComment에 접근하지 않으므로 이 쿼리가 실행되지 않는다.
실행되는 SQL 추적
getPost() — 게시글 단건 조회
실제 코드
// PostService.java:28-33
@Transactional
public PostDto getPost(Long pid) {
return postRepository.findById(pid)
.map(PostDto::from)
.orElseThrow(() -> new NoSuchElementException("해당 게시글 존재 x"));
}
실행되는 SQL
-- [1] findById(pid) → Post + User를 LEFT JOIN으로 한 번에 조회
SELECT p1_0.pid, p1_0.category_type, p1_0.content, p1_0.created_by, ...
FROM post p1_0
LEFT JOIN user u1_0 ON u1_0.uid = p1_0.uid
WHERE p1_0.pid = ?
총 1개의 쿼리가 실행된다.
findById()는 즉시 SELECT를 실행한다.Post.user가@ManyToOne(기본 EAGER)이므로, Hibernate가 LEFT JOIN으로 User를 함께 가져온다.Post.postComments는@OneToMany(기본 LAZY)이고,PostDto::from에서 postComments에 접근하지 않으므로 PostComment 쿼리는 실행되지 않는다.
getPostWithComments() — 게시글 + 댓글 조회
실제 코드
// PostService.java
@Transactional
public PostWithCommentsDto getPostWithComments(Long pid) {
return postRepository.findById(pid)
.map(PostWithCommentsDto::from)
.orElseThrow(() -> new NoSuchElementException("해당 게시글 존재 x"));
}
// PostWithCommentsDto.java
public static PostWithCommentsDto from(Post post) {
return new PostWithCommentsDto(
post.getId(),
post.getTitle(),
post.getContent(),
post.getCategoryType(),
post.getPostComments().stream() // ← Lazy 로딩 발동 지점
.map(PostCommentDto::from)
.collect(Collectors.toCollection(LinkedHashSet::new)),
UserDto.from(post.getUser()), // ← 이미 EAGER로 로드됨
post.getCreatedDate(),
post.getCreatedBy(),
post.getModifiedDate(),
post.getModifiedBy()
);
}
실행되는 SQL
-- [1] findById(pid) → Post + User 조회 (EAGER JOIN)
SELECT p1_0.pid, p1_0.category_type, p1_0.content, p1_0.created_by, ...
FROM post p1_0
LEFT JOIN user u1_0 ON u1_0.uid = p1_0.uid
WHERE p1_0.pid = ?
-- [2] post.getPostComments() 접근 → Lazy 로딩으로 PostComment + User 조회
SELECT pc1_0.pid, pc1_0.id, pc1_0.content, pc1_0.created_by, ...
FROM post_comment pc1_0
LEFT JOIN user u1_0 ON u1_0.uid = pc1_0.uid
WHERE pc1_0.pid = ?
총 2개의 쿼리가 실행된다.
- 1번 쿼리:
findById()에 의해 Post + User(EAGER) 조회. - 2번 쿼리:
PostWithCommentsDto.from()안에서post.getPostComments()를 호출하는 순간, Lazy 로딩이 발동하여 해당 Post의 PostComment들을 조회한다. PostComment → User도@ManyToOne(EAGER)이므로 LEFT JOIN으로 함께 가져온다.
registerPost() — 게시글 등록
실제 코드
// PostService.java:42-55
@Transactional
public void registerPost(PostDto postDto) {
User user = userRepository.getReferenceById(postDto.getUserDto().getUid());
// ↑ SELECT 실행 안 함. uid만 가진 프록시 반환.
Post post = postDto.toEntity(user);
// ↑ Post 객체 생성. user 필드에 프록시가 들어감.
postRepository.save(post);
// ↑ INSERT 실행. 프록시의 uid만 FK 값으로 사용.
}
실행되는 SQL
-- [1] postRepository.save(post) → INSERT (getReferenceById는 쿼리 없음)
INSERT INTO post (category_type, content, created_by, created_date,
modified_by, modified_date, title, uid)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
총 1개의 쿼리가 실행된다.
getReferenceById()는 프록시를 반환할 뿐, SELECT를 실행하지 않는다.save()가 호출되면 Hibernate는 Post가 새 엔티티임을 감지하고(@GeneratedValue(strategy = IDENTITY)+ id가 null) INSERT를 실행한다.- INSERT 시
uid컬럼에는 프록시가 가진 ID 값("user1" 등)만 필요하므로, User의 다른 필드를 조회할 필요가 없다. - 이것이
getReferenceById의 장점이다. FK 설정을 위해 불필요한 SELECT를 하지 않는다.
updatePost() — 게시글 수정 (Dirty Checking)
실제 코드
// PostService.java:57-66
@Transactional
public void updatePost(Long pid, PostDto postDto) {
Post post = postRepository.getReferenceById(pid);
// ↑ 프록시 반환 (SELECT 아직 안 함)
post.updateTitleAndContentAndCategoryType(
postDto.getTitle(),
postDto.getContent(),
postDto.getCategoryType()
);
// ↑ 프록시의 필드에 접근 → 이 시점에 SELECT 실행
// ↑ 그 후 title, content, categoryType 값을 변경
// ↑ save() 호출 없음! @Transactional 종료 시 dirty checking으로 UPDATE 실행
}
// Post.java:63-67
public void updateTitleAndContentAndCategoryType(String title, String content, CategoryType categoryType) {
this.title = title; // ← setter로 값 변경
this.content = content;
this.categoryType = categoryType;
}
실행되는 SQL
-- [1] post.updateTitleAndContent...() 호출 시 → 프록시 초기화를 위한 SELECT
SELECT p1_0.pid, p1_0.category_type, p1_0.content, p1_0.created_by, ...
FROM post p1_0
LEFT JOIN user u1_0 ON u1_0.uid = p1_0.uid
WHERE p1_0.pid = ?
-- [2] @Transactional 종료 시 → dirty checking에 의한 UPDATE
UPDATE post
SET category_type = ?, content = ?, created_date = ?,
modified_by = ?, modified_date = ?, title = ?, uid = ?
WHERE pid = ?
총 2개의 쿼리가 실행된다.
getReferenceById(pid)는 프록시만 반환한다. (SELECT 없음)post.updateTitleAndContentAndCategoryType()이 호출되면, 프록시의 실제 필드에 접근하게 되므로 프록시가 초기화된다. 이때 SELECT가 실행된다.- SELECT로 가져온 원본 데이터(스냅샷)를 영속성 컨텍스트가 보관한다.
- setter로 title, content, categoryType이 변경된다.
@Transactional메서드가 끝나면 트랜잭션이 커밋된다.- 커밋 직전, Hibernate가 현재 엔티티 상태와 스냅샷(원본)을 비교한다 → Dirty Checking
- 변경된 필드가 발견되면 UPDATE SQL을 자동으로 실행한다.
save()를 호출하지 않아도 UPDATE가 실행되는 이유가 바로 이것이다.
deletePost() — 게시글 삭제
실제 코드
// PostService.java:68-74
@Transactional
public void deletePost(long pid, String uid) {
System.out.println("DELETE POST");
postRepository.deleteByIdAndUser_Uid(pid, uid);
// ↑ Spring Data JPA가 메서드 이름을 파싱하여 쿼리 생성
}
실행되는 SQL
-- [1] 삭제 대상 Post 조회 (pid + uid 조건)
SELECT p1_0.pid, p1_0.category_type, p1_0.content, p1_0.created_by, ...
FROM post p1_0
LEFT JOIN user u1_0 ON u1_0.uid = p1_0.uid
WHERE p1_0.pid = ? AND u1_0.uid = ?
-- [2] Post의 User를 로딩 (EAGER이므로)
SELECT u1_0.uid, u1_0.created_by, u1_0.created_date, u1_0.email, ...
FROM user u1_0
WHERE u1_0.uid = ?
-- [3] CascadeType.ALL에 의해 연관된 PostComment 로딩
SELECT pc1_0.pid, pc1_0.id, pc1_0.content, pc1_0.created_by, ...
FROM post_comment pc1_0
LEFT JOIN user u1_0 ON u1_0.uid = pc1_0.uid
WHERE pc1_0.pid = ?
-- [4] Post 삭제 (댓글이 있었다면 댓글 먼저 삭제 후 Post 삭제)
DELETE FROM post WHERE pid = ?
총 3~4개의 쿼리가 실행된다.
- Spring Data JPA의
deleteByXxx()메서드는 내부적으로 먼저 SELECT로 엔티티를 조회한 뒤,EntityManager.remove()로 삭제한다. (바로 DELETE를 날리지 않는다!) - Post의
postComments에CascadeType.ALL이 걸려있으므로, Post를 삭제하기 전에 연관된 PostComment도 함께 삭제해야 한다. - 이를 위해 Hibernate는 PostComment도 미리 SELECT한다.
- 댓글이 있으면 댓글 DELETE → Post DELETE 순서로 실행된다.
JOIN이 발생하는지 vs 추가 SELECT가 발생하는지
| 조회 방식 | User 로딩 방식 | 이유 |
|---|---|---|
findById(pid) |
LEFT JOIN (한 방 쿼리) | EntityManager.find() 사용 → Hibernate가 EAGER 관계를 JOIN으로 최적화 |
findAll() |
별도 SELECT (추가 쿼리) | JPQL 기반 → Hibernate가 EAGER이더라도 별도 SELECT로 처리 |
findByTitleContains() |
별도 SELECT (추가 쿼리) | Spring Data JPA 쿼리 메서드 → JPQL 기반 |
findByUser_UidContains() |
LEFT JOIN(조건) + 별도 SELECT(로딩) | WHERE 절을 위해 JOIN은 하지만, User 데이터 로딩은 별도 SELECT |
실제 로그로 확인
findById(1) — JOIN 발생:
-- 한 번의 쿼리로 Post + User를 함께 가져온다
SELECT p1_0.pid, ..., u1_0.uid, u1_0.username, ...
FROM post p1_0
LEFT JOIN user u1_0 ON u1_0.uid = p1_0.uid -- ← JOIN!
WHERE p1_0.pid = ?
findAll() (목록 조회) — 별도 SELECT 발생:
-- 1번째 쿼리: Post만 가져온다 (User JOIN 없음!)
SELECT p1_0.pid, ..., p1_0.uid
FROM post p1_0
LIMIT ?, ?
-- 2번째 쿼리: 각 Post의 User를 개별적으로 가져온다
SELECT u1_0.uid, ... FROM user u1_0 WHERE u1_0.uid = ?
SELECT u1_0.uid, ... FROM user u1_0 WHERE u1_0.uid = ?
SELECT u1_0.uid, ... FROM user u1_0 WHERE u1_0.uid = ?
-- ... Post 개수만큼 반복 (같은 uid면 영속성 컨텍스트 캐시로 생략)
참고로 각 Post의 User를 개별적으로 가져오는 쿼리는 findAll() 뒤에 이어지는 map(PostDto::from) 때문에 발생하는 것이 아니다.
Post가 User를 참조로 갖고 있고 이는 ManyToOne 관계이기 때문에 EAGER가 적용되어 findAll() 후 User를 가져온다.
왜 이런 차이가 생기는가?
findById()
→ EntityManager.find() 호출
→ Hibernate가 직접 SQL을 생성
→ EAGER 관계를 LEFT JOIN으로 최적화함
findAll(), findByXxx()
→ JPQL 실행 ("SELECT p FROM Post p" 등)
→ JPQL은 엔티티 기준이라 JOIN을 자동 추가하지 않음
→ Post만 SELECT한 뒤, EAGER 관계의 User를 별도 SELECT로 로딩
→ 이것이 N+1 문제의 원인!
해결 방법
| 방법 | 설명 | 적용 예시 |
|---|---|---|
| Fetch Join (JPQL) | JPQL에 JOIN FETCH 추가 | @Query("SELECT p FROM Post p JOIN FETCH p.user") |
| @EntityGraph | 메서드에 어노테이션 추가 | @EntityGraph(attributePaths = {"user"}) |
| @BatchSize | N+1의 N을 줄임 (IN절로 묶음) | @BatchSize(size = 100) on User |
N+1 문제에 대해 고민해보자
N+1 문제란?
1번의 쿼리로 N개의 데이터를 가져온 뒤,
각 데이터의 연관 엔티티를 가져오기 위해 N번의 추가 쿼리가 실행되는 것.
총 1 + N번의 쿼리가 실행된다.
N+1이 발생하는 시나리오
발생 위치: getPosts(), getPostsWithPage(), getPostsWithSearch()
// PostService.java:76-82
@Transactional
public List<PostDto> getPosts() {
return postRepository.findAll().stream() // ← Post Select 1번 + User Select N번
.map(PostDto::from)
.toList();
}
실제 SQL 로그
Post 쿼리: Post 10개 가져옴
User 쿼리: 10개 중 3개 게시글은 동일한 작성자. 총 8번의 User 쿼리 발생.
-- [1번째 쿼리] Post 목록 조회
SELECT p1_0.pid, p1_0.category_type, p1_0.content, ..., p1_0.uid
FROM post p1_0
LIMIT ?, ?
-- [2번째] 1번 Post의 User 조회
SELECT u1_0.uid, ... FROM user u1_0 WHERE u1_0.uid = ? -- uid = 'user1'
-- [3번째] 2번 Post의 User 조회
SELECT u1_0.uid, ... FROM user u1_0 WHERE u1_0.uid = ? -- uid = 'user2'
-- [4번째] 3번 Post의 User 조회
SELECT u1_0.uid, ... FROM user u1_0 WHERE u1_0.uid = ? -- uid = 'user3'
-- ... uid가 같은 경우 영속성 컨텍스트 캐시로 생략되지만,
-- 서로 다른 uid가 있으면 그만큼 추가 쿼리 발생!
-- [마지막] count 쿼리 (페이징 시)
SELECT count(p1_0.pid) FROM post p1_0
해결 방법 ① — Fetch Join (JPQL)
// PostRepository.java에 추가
@Query("SELECT p FROM Post p JOIN FETCH p.user")
List<Post> findAllWithUser();
@Query("SELECT p FROM Post p JOIN FETCH p.user")
Page<Post> findAllWithUser(Pageable pageable);
실행되는 SQL:
-- 1번의 쿼리로 Post + User를 함께 가져온다
SELECT p1_0.pid, ..., u1_0.uid, u1_0.username, ...
FROM post p1_0
INNER JOIN user u1_0 ON u1_0.uid = p1_0.uid
해결 방법 ② — @EntityGraph
// PostRepository.java에 추가
@EntityGraph(attributePaths = {"user"})
@Override
List<Post> findAll();
@EntityGraph(attributePaths = {"user"})
Page<Post> findByTitleContains(String searchValue, Pageable pageable);
실행되는 SQL:
-- findAll()이지만 LEFT JOIN이 추가된다
SELECT p1_0.pid, ..., u1_0.uid, u1_0.username, ...
FROM post p1_0
LEFT JOIN user u1_0 ON u1_0.uid = p1_0.uid
해결 방법 ③ — @BatchSize
// User.java에 추가
@BatchSize(size = 100) // ← 클래스 레벨에 추가
@Entity
public class User extends AuditingFields {
// ...
}
실행되는 SQL:
-- Post 조회
SELECT p1_0.pid, ..., p1_0.uid FROM post p1_0
-- User를 한 번에 IN절로 묶어서 조회 (N번 → 1번으로 줄어듦)
SELECT u1_0.uid, ... FROM user u1_0 WHERE u1_0.uid IN (?, ?, ?, ?, ?, ?, ?, ?)
방법 비교
| 방법 | 장점 | 단점 |
|---|---|---|
| Fetch Join | 가장 확실, 1번의 쿼리 | 페이징과 함께 쓰면 메모리에서 페이징(경고 발생) |
| @EntityGraph | 기존 메서드에 어노테이션만 추가 | 복잡한 조건에서는 한계 |
| @BatchSize | 기존 코드 변경 최소 | 완전한 1번은 아님(배치 수에 따라 나뉨) |
'Learning Log' 카테고리의 다른 글
| [멋사 클라우드 5기] Day 33 & 34 - Linux (1) (1) | 2026.03.25 |
|---|---|
| [멋사 클라우드 5기] Day 31 & 32 - OSI 7 Layers (1) | 2026.03.16 |
| [멋사 클라우드 5기] Day 28 - JWT 그런데 Redis를 곁들인 (0) | 2026.03.10 |
| [멋사 클라우드 5기] Day 26, 27 - Spring Security & JWT (0) | 2026.03.09 |
| [멋사 클라우드 5기] Day 25 - Spring Data JPA (3) (0) | 2026.03.05 |
