이 글은 Velog에서 이전한 글입니다. Velog 원문 보기
게시판 코드 링크
최근 공부한 내용들을 게시판에 적용하면서 여러가지 시도를 하고 있다.
최근에는 API도 만들어 보고 있는데 회원 수정, 삭제에서 Dirty Checking이 발생하지 않는 문제가 생겼다.
Member
package springJpaBoard.Board.domain;
import lombok.Getter;
import springJpaBoard.Board.controller.requestdto.MemberRequestDTO;
import springJpaBoard.Board.domain.status.GenderStatus;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
public class Member {
@GeneratedValue
@Id
@Column(name = "member_id")
private Long id;
private String loginId; //로그인 ID
private String password; //로그인 비밀번호
private String name;
@Enumerated(EnumType.STRING)
private GenderStatus gender;
@Embedded
private Address address;
@OneToMany(mappedBy = "member", cascade = CascadeType.ALL) //양방향 연관관계 지정
private List<Board> boardList = new ArrayList<>();
@OneToMany(mappedBy = "member", cascade= CascadeType.ALL)
private List<Comment> commentList = new ArrayList<>();
@OneToMany(mappedBy = "member", cascade = CascadeType.REMOVE)
private List<Like> likeList = new ArrayList<>();
public void createMember(MemberRequestDTO memberRequestDTO, Address address) {
this.name = memberRequestDTO.getName();
this.gender = memberRequestDTO.getGender();
this.loginId = memberRequestDTO.getLoginId();
this.password = memberRequestDTO.getPassword();
this.address = address;
}
/*
회원 수정, Dirty Checking 발생(업데이트 쿼리가 자동으로 나감)
Setter를 사용하지 않기 위해 수정 메서드를 만듦
*/
public void editMember(MemberRequestDTO memberDto) {
this.name = memberDto.getName();
this.gender = memberDto.getGender();
this.address = new Address(memberDto.getCity(), memberDto.getStreet(), memberDto.getZipcode());
}
}
MemberApiController 중 회원 수정 메서드
package springJpaBoard.Board.api;
import ...
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/members")
@Transactional(readOnly = true)
@Slf4j
public class MemberApiController {
private final MemberService memberService;
private final BoardService boardService;
/* 회원 수정 */
/**
* TODO 수정 안되는 오류 해결
* */
@PostMapping("{memberId}/edit")
public ResponseEntity updateMember(@RequestBody @Validated(UpdateCheck.class) MemberRequestDTO form, BindingResult result) {
if (result.hasErrors()) {
throw new UserException("회원 수정 오류");
}
Member updateMember = memberService.update(form.getId(), form);
ModifyMemberDto modifyMember = new ModifyMemberDto(updateMember);
Message message = new Message(StatusEnum.OK, "회원 정보 수정 성공", modifyMember);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(new MediaType("application", "json", Charset.forName("UTF-8")));
return new ResponseEntity<>(message, headers, HttpStatus.OK);
}
@Data
@AllArgsConstructor
static class ModifyMemberDto {
private Long id;
private String name;
private GenderStatus gender;
private Address address;
public ModifyMemberDto(Member member) {
this.id = member.getId();
this.name = member.getName();
this.gender = member.getGender();
this.address = member.getAddress();
}
}
}
MemberService 중 updateMember
package springJpaBoard.Board.service;
import ...
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
/**
* 회원 정보 수정
*/
@Transactional
public Member update(Long memberId, MemberRequestDTO memberDto) {
/*
Dirty Checking 발생, 가능하다면 Setter는 사용하지 않는 방법으로 구현
*/
Member member = memberRepository.findById(memberId).get();
member.editMember(memberDto);
System.out.println("member.getName() = " + member.getName());
return member;
}
}
MemberRepository
package springJpaBoard.Board.repository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import springJpaBoard.Board.domain.Member;
import springJpaBoard.Board.domain.status.GenderStatus;
import java.util.List;
import java.util.Optional;
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findAllByName(String name);
// name만 선링
Page<Member> findByNameContaining(String keyword, Pageable pageable);
// gender만 선택한 경우
Page<Member> findByGender(GenderStatus gender, Pageable pageable);
// name과 gender을 모두 입력한 경우
Page<Member> findByNameContainingAndGender(String name, GenderStatus gender, Pageable pageable);
/* loginId로 Member 찾아오기 */
Optional<Member> findByLoginId(String loginId);
List<Member> findAllByLoginId(String loginId);
}
여기까지가 회원 수정, 삭제 관련 코드입니다.
Dirty Checking은 어떤 조건에서 실행되는가?
우선 Dirty Checking이 무엇인지 알아보자.
Dirty Checking은 어플리케이션 내부 엔티티의 변화를 추적하기 위해 ORM에서 사용되는 개념으로, 변화를 감지하고 트랜잭션이 commit 되면 수정 사항을 DB에 자동으로 반영하는 것을 의미한다.
그럼 Dirty Checking 은 언제 실행될까?
Dirty Checking 은 트랜잭션 쓰기 연산 중 영속성 컨텍스트(Persistent Context) 가 엔티티의 변경을 감지할 때 자동으로 실행된다.
JPA에서는 엔티티를 조회하면 해당 엔티티의 조회 상태 그대로 스냅샷을 만들어두는데, 이 스냅샷의 상태와 비교하여 변경사항이 있다면 오염(Dirty)이 감지(Checking) 됐다고 판단한다. 즉 최초 조회한 상태와 다를 때 Dirty Checking 이 감지된다.
영속성 컨텍스트 라는 단어가 등장했다. 영속성 컨텍스트란 JPA 런타임 환경으로 엔티티의 수명주기 및 DB 영속성을 관리하는 역할을 맡는다. 쉽게 말해 엔티티의 영구 저장 환경이자, 애플리케이션과 DB 사이에서 객체를 보관하는 논리적 개념이라 할 수 있다. 영속성 컨텍스트라는 엔티티 영구 저장 환경이, 엔티티의 변경을 감지한다면 Dirty Checking 이 이뤄진다고 볼 수 있다.
정리하자면, 엔티티가 영속성 컨텍스트의 관리를 받을 때 Dirty Checking 은 실행된다.
그렇다면 이 영속성 컨텍스트라는 것은 어느 타이밍에 등장하는 것일까? 이 질문에 대한 답은 @Transactional 이 가지고 있다.
문제 해결..
근데 여기까지 읽은 사람 중 이미 문제 원인을 발견한 사람이 있을 것 같다.
그리고 그 사람은 나를 바보라고 생각할 것이다. 맞다.. 나도 인정한다.
다시 MemberApiController를 보자
package springJpaBoard.Board.api;
import ...
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/members")
@Transactional(readOnly = true)
@Slf4j
public class MemberApiController {
private final MemberService memberService;
private final BoardService boardService;
/* 회원 수정 */
/**
* TODO 수정 안되는 오류 해결
* */
@PostMapping("{memberId}/edit")
public ResponseEntity updateMember(@RequestBody @Validated(UpdateCheck.class) MemberRequestDTO form, BindingResult result) {
if (result.hasErrors()) {
throw new UserException("회원 수정 오류");
}
Member updateMember = memberService.update(form.getId(), form);
ModifyMemberDto modifyMember = new ModifyMemberDto(updateMember);
Message message = new Message(StatusEnum.OK, "회원 정보 수정 성공", modifyMember);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(new MediaType("application", "json", Charset.forName("UTF-8")));
return new ResponseEntity<>(message, headers, HttpStatus.OK);
}
@Data
@AllArgsConstructor
static class ModifyMemberDto {
private Long id;
private String name;
private GenderStatus gender;
private Address address;
public ModifyMemberDto(Member member) {
this.id = member.getId();
this.name = member.getName();
this.gender = member.getGender();
this.address = member.getAddress();
}
}
}
이상한 점이 보인다.@Transactional(readOnly = true) 언제 적어둔지 모르겠지만 실수로 Controller에 @Transactional(readOnly = true) 을 붙여뒀습니다. 이거 때문에 4시간 동안 고생했다.
@Transactional(readOnly = true)
- readOnly = true로 했기 때문에 엔티티의 정보를 변경해도 Dirty Checking이 발생하지 않았던 것이다.
앞으로는 코드를 잘 확인해야겠다..
'Troubleshooting' 카테고리의 다른 글
| [시행착오] AWS 프리티어 비용 과금 이슈, 해결 방법 (0) | 2026.07.29 |
|---|---|
| 💥 아! Get 요청시에는 Message body를 사용하면 안되는구나!! (1) | 2026.07.29 |
| 💥 [Error] JWT Token 인증 문제 (0) | 2026.07.28 |
| 💥 Springboot Multipart 파일 업로드 용량 초과 에러 (0) | 2026.07.21 |
| 💥 Web server failed to start. Port 8080 was already in use (0) | 2026.07.20 |