๐Ÿ’ฅ [Error] Dirty Checking์ด ์•ˆ๋˜๋Š” ์˜ค๋ฅ˜

๋ฐ•์ƒ๋ฏผยท2023๋…„ 12์›” 30์ผ

ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

๋ชฉ๋ก ๋ณด๊ธฐ
9/11

๊ฒŒ์‹œํŒ ์ฝ”๋“œ ๋งํฌ
์ตœ๊ทผ ๊ณต๋ถ€ํ•œ ๋‚ด์šฉ๋“ค์„ ๊ฒŒ์‹œํŒ์— ์ ์šฉํ•˜๋ฉด์„œ ์—ฌ๋Ÿฌ๊ฐ€์ง€ ์‹œ๋„๋ฅผ ํ•˜๊ณ  ์žˆ๋‹ค.
์ตœ๊ทผ์—๋Š” 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์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š์•˜๋˜ ๊ฒƒ์ด๋‹ค.

์•ž์œผ๋กœ๋Š” ์ฝ”๋“œ๋ฅผ ์ž˜ ํ™•์ธํ•ด์•ผ๊ฒ ๋‹ค..

0๊ฐœ์˜ ๋Œ“๊ธ€