테스트 코드

        var newChannel = Channel.builder().name("new-channel").build();
        var newThread = Thread.builder().message("new-message").build();
        var newThread1 = Thread.builder().message("장마 끝!!").build();

        newThread.setChannel(newChannel);
        newThread1.setChannel(newChannel);

        var savedChannel = channelRepository.insertChannel(newChannel);
        var savedThread = threadRepository.insertThread(newThread);
        var savedThread1 = threadRepository.insertThread(newThread1);

        // when
        var foundChannel = channelRepository.selectChannel(savedChannel.getId());
        foundChannel.getThreads().remove(savedThread);

 

 

1. CascadeType.ALL만 적용한 경우

    @OneToMany(mappedBy = "channel", cascade = CascadeType.ALL)
    private Set<Thread> threads = new LinkedHashSet<>(); // 중복 x 순서 o
Hibernate: 
    insert 
    into
        channel
        (name, type) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        thread
        (channel_id, message) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        thread
        (channel_id, message) 
    values
        (?, ?)

delete 쿼리가 날아가지 않는다.

 

 

 

2. CascadeType.ALL과 orphanRemoval = true 둘 다 적용한 경우

    @OneToMany(mappedBy = "channel", cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<Thread> threads = new LinkedHashSet<>(); // 중복 x 순서 o
Hibernate: 
    insert 
    into
        channel
        (name, type) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        thread
        (channel_id, message) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        thread
        (channel_id, message) 
    values
        (?, ?)
Hibernate: 
    delete 
    from
        thread 
    where
        id=?

delete 쿼리가 날아가고 스레드 테이블에서 스레드가 삭제된다.

 

 

 

3. orphanRemoval = true 만 적용한 경우

Hibernate: 
    insert 
    into
        channel
        (name, type) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        thread
        (channel_id, message) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        thread
        (channel_id, message) 
    values
        (?, ?)

delete 쿼리가 날라가지 않는다.

 

 

Cascade.REMOVE 와 orphanRemoval 차이점

Cascade.REMOVE : 부모 엔티티를 em.remove로 직접 삭제하면 자식 엔티티도 삭제된다.

orphanRemoval : 부모 엔티티를 em.remove로 직접 삭제하면 자식 엔티티도 삭제된다. + 부모 엔티티의 리스트에서 어떤 요소를 삭제하면 그에 해당하는 자식 엔티티도 삭제된다. 즉, 리스트 요소의 영속성 전이도 해준다.

 

위 테스트 코드로 예를 들면 부모 객체인 savedChannel의 threads 안에 있는 savedThread를 제거하면 스레드 테이블에서도 해당 스레드가 삭제된다.

 

 

 

 

영속성 전이 최강 조합 : CascadeType.ALL + orphanRemoval = true 

위 두가지를 함께 설정하면 자식 엔티티의 라이프 사이클이 부모 엔티티와 동일해지며, 직접 자식 엔티티의 생명주기를 관리할 수 있게 되므로 자식 엔티티의 Repository 조차 없어도 된다. (따라서, 매핑 테이블에서 많이 쓰임)

 

 

 

Fetch(조회 시점)

FetchType

EAGER : 부모 조회 시 자식도 같이 조회

LAZY : 부모 조회시 부모만 조회, 자식은 필요할 때 따로 조회

'TIL > WEEK11' 카테고리의 다른 글

equals()로 객체 비교  (0) 2023.07.25

문제점

프로젝트를 진행하면서 게시글을 수정, 삭제 api에서 작성자 확인이 제대로 되지 않는 문제가 발생했다.

    @Transactional
    public PostResponseDto updatePost(Long id, PostRequestDto requestDto, User user) {
        Post post = findPost(id);
        //게시글 작성자와 요청자가 같은지 또는 관리자인지 체크 -> 아닐시 예외 발생
        // 해당 post의 작성자가 맞는지 확인
        if (user.equals(post.getUser()) || user.getRole().equals(UserRoleEnum.ADMIN)) {
            // requestDto로부터 받은 게시글의 제목과 내용으로 해당 post 내용 수정하기
            post.setTitle(requestDto.getTitle());
            post.setContent(requestDto.getContent());
            Category category = categoryRepository.findById(requestDto.getCategoryId()).orElseThrow(() ->
                    new IllegalArgumentException("선택한 게시글이 존재하지 않습니다"));
            post.setCategory(category);


        } else {
            // 해당 post의 작성자가 아니라면 null 반환하기
            throw new RejectedExecutionException();
        }

        return new PostResponseDto(post);
    }

user.equals(post.getUser())가 계속 false로 나와 이런 문제가 발생했는데 원인은 User 클래스에 equals 메서드를 오버라이딩 해주지 않아서 발생한 거였다. 

 

 

 

해결방안

User 클래스에 @EqualsAndHashCode 어노테이션을 달아줌으로써 해결했다.

 

 

 

깨달은 점

두 객체를 equals 메서드로 비교하려면 오버라이딩 하는 걸 잊지 말자!

'TIL > WEEK11' 카테고리의 다른 글

Cascade.REMOVE 와 orphanRemoval  (0) 2023.07.27

AOP(Aspect Oriented Programing)

관점이 다른 로직(부가기능, 로그처리, 트랜잭션 처리 보안처리 등)을 주업무 로직에 뺏다 꽂았다 하기 쉽게 만드는 방법론 중 하나

꽂아 넣을 부가 기능 코드를 Cross-cutting concern이라 하고

주업무 로직을  Core concern이라 한다.

Spring AOP는 Cross-cutting concern을 프록시 객체에 따로 구현해서 실행되게 하는 방법이다.

 

 

'WIL' 카테고리의 다른 글

WEEK2 개인과제 구상  (0) 2023.05.28
WEEK1  (0) 2023.05.21

문제점

블로그에 좋아요 기능을 구현하는데 다음과 같은 오류가 발생했다.

 

 

 

해결방안

Post 클래스에 @DynamicInsert, @DynamicUpdate를 달고 likeCount에 @ColumnDefault("0")을 달았더니 해결됐다.


@Entity
@Getter
@Setter
@DynamicInsert
@DynamicUpdate
@NoArgsConstructor
@Table(name = "post")
public class Post extends TimeStamped {

...

    @ColumnDefault("0")
    @Column(name = "like_count", nullable = false)
    private Integer likeCount;

...

}

 

 

 

알게 된 점

null인 필드값이 insert시 제외되게 하기 위해서는 Entity에 @DynamicInsert를 붙여야 한다.

null인 필드값이 update시 제외되게 하기 위해서는 Entity에 @DymamicUpdate를 붙여야 한다.

'TIL > WEEK9' 카테고리의 다른 글

스트림  (0) 2023.07.10

난수 스트림 만들기

        IntStream intStream = new Random().ints();
        IntStream intStream = new Random().ints(7,5, 10);// 5<= <10 랜덤 숫자 7개
        intStream.limit(10).forEach(System.out::println);

IntStream ints() : int 타입의 난수들로 이루어진 스트림을 반환한다. 괄호 안을 비워두면 limit()으로 크기를 제한해 줘야 된다. 그렇지 않으면 무한 스트림이 생성된다.

LongStream longs(), Doubles doubles()등도 있다.

 

ints(5) : 크기가 5인 정수 난수 스트림을 반환

ints(7, 5, 10) : 크기가 7인 5 이상 10 미만의 정수로 이루어진 난수 스트림 반환

 

        Stream<Integer> intStream = Stream.iterate(0, n -> n+2);
        intStream.limit(10).forEach(System.out::println);

 

Stream.iterate(0, n -> n+2) : 초기값이 0이면서 n+2를 수행하는 무한 스트림 반환 limit으로 크기를 지정해 줘야 한다.

iterate와 generate는 IntStream과 같은 기본형 스트림 타입의 참조 변수로 다룰 수 없다.

IntStream evenStream = Stream.iterate(0, n -> n+2); // 에러

 

중간 연산 : 연산 결과가 스트림인 연산, 여러 번 가능

최종 연산 : 연산 결과가 스트림이 아닌 연산, 한 번만 가능

 

 

 

 

 

 

 

import java.util.Comparator;
import java.util.stream.Stream;

public class Ex14_5 {
    public static void main(String[] args) {

        Stream<Student> studentStream = Stream.of(
                new Student("이자바", 3, 300),
                new Student("김자바", 1, 200),
                new Student("안자바", 2, 100),
                new Student("박자바", 2, 150),
                new Student("소자바", 1, 200),
                new Student("나자바", 3, 290),
                new Student("감자바", 3, 180)
        );

        studentStream.sorted(Comparator.comparing(Student::getBan)).forEach(System.out::println);

    }
}

class Student implements Comparable<Student> {
    String name;
    int ban;
    int totalScore;

    public String getName() {
        return name;
    }

    public int getBan() {
        return ban;
    }

    public int getTotalScore() {
        return totalScore;
    }

    public Student(String name, int ban, int totalScore) {
        this.name = name;
        this.ban = ban;
        this.totalScore = totalScore;
    }

    @Override
    public String toString() {
        return String.format("[%s, %d, %d]", name, ban, totalScore);
    }

    public int compareTo(Student s) {
        return s.totalScore - this.totalScore; // 총점 내림차순을 기본 정렬로 한다.
    }
}

String.format() : 문자열의 형식을 설정한다. %d 정수, %s 문자열 등

sorted(Comparator.comparing(Student::getBan)) : 반을 기준으로 정렬한다. (기본 설정은 총점으로 해둠)

sorted(Comparator.comparing(Student::getBan)) 적용했을 때의 결과 - 반으로 정렬

 

sorted() 기본 정렬의 결과 - 총점 순으로 정렬

 

package com.sparta.bloglogin.dto;

import com.sparta.bloglogin.entity.User;
import lombok.Getter;

import java.util.Comparator;
import java.util.List;

@Getter
public class ProfileResponseDto {
    private String username;
    // private String password;
    private String realname;
    private String introduction;
    private List<ProfilePostListResponseDto> posts;

    public ProfileResponseDto(User user) {
        this.username = user.getUsername();
        this.realname = user.getRealname();
        this.introduction = user.getIntroduction();
        this.posts = user.getPostList().stream().map(ProfilePostListResponseDto::new).sorted(Comparator.comparing(ProfilePostListResponseDto::getCreatedAt).reversed()).toList();
    }
}

유저 프로필에서 작성한 글 목록을 볼 수 있게 posts를 추가했더니 다음과 같은 오류가 발생했다.

 

방법 1. getMyPage에 @Transactional 걸어주기  ➡️  실패

 

방법 2. postList의 FetchType을 EAGER로 설정하기  ➡️  해결

    @OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
    private List<Post> postList;

 

프로필에서 작성글 목록 불러오기 성공!

@RestController
@RequestMapping("/dev")
@RequiredArgsConstructor
public class ProfileController {

    private final UserService userService;

    // 프로필 조회
    @GetMapping("/my-page")
    public ProfileResponseDto getMyPage(@AuthenticationPrincipal UserDetailsImpl userDetails) {
        return userService.getMyPage(userDetails.getUser());
    }

    // 프로필 수정
    @PutMapping("/profile")
    public ProfileResponseDto updateProfile(@AuthenticationPrincipal UserDetailsImpl userDetails, @RequestBody ProfileRequestDto profileRequestDto) {
        return userService.updateProfile(userDetails.getUser(), profileRequestDto);
    }

    // 비밀번호 변경
    @PutMapping("/profile/password")
    public ResponseEntity<ApiResponseDto> updatePassword(@AuthenticationPrincipal UserDetailsImpl userDetails, @RequestBody PasswordRequestDto passwordRequestDto) {
        try {
            userService.updatePassword(userDetails, passwordRequestDto);
            return ResponseEntity.ok().body(new ApiResponseDto("비밀번호 변경 성공", HttpStatus.OK.value()));
        } catch (RejectedExecutionException e) {
            return ResponseEntity.badRequest().body(new ApiResponseDto("비밀번호가 일치하지 않습니다.", HttpStatus.BAD_REQUEST.value()));
        }
    }

}

@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public void signup(SignupRequestDto requestDto) {
        String username = requestDto.getUsername();
        String password = passwordEncoder.encode(requestDto.getPassword());
        String realname = requestDto.getRealname();
        String introduction = requestDto.getIntroduction();
        UserRoleEnum role = UserRoleEnum.USER;

        // 회원 중복 확인
        if (userRepository.findByUsername(username).isPresent()) {
            throw new IllegalArgumentException("중복된 사용자가 존재합니다.");
        }

        // 사용자 등록
        User user = new User(username, password, realname, introduction, role);
        userRepository.save(user);
    }


    // 프로필 조회
    public ProfileResponseDto getMyPage(User user) {
        return new ProfileResponseDto(user);
    }

    // 프로필 수정
    @Transactional
    public ProfileResponseDto updateProfile(User user, ProfileRequestDto profileRequestDto) {
        user.setRealname(profileRequestDto.getRealname());
        user.setIntroduction(profileRequestDto.getIntroduction());
        userRepository.save(user);
        return new ProfileResponseDto(user);
    }

    // 비밀번호 변경
    @Transactional
    public void updatePassword(UserDetailsImpl userDetails, PasswordRequestDto passwordRequestDto) {
        User user = userDetails.getUser();

        if(!passwordEncoder.matches(passwordRequestDto.getPassword(), userDetails.getPassword())) {
            throw new RejectedExecutionException();
        }
        user.setPassword(passwordRequestDto.getNewpassword());
        userRepository.save(user);
    }
}

 

오늘의 삽질

ResponseDto에 @Getter 붙이는 걸 잊지 말자..!

@RequestBody는 여러 개 보낼 수 없다.

레포지토리에 save하는 걸 잊지 말자

 

 

Q1) 댓글이 달려있는 게시글을 삭제하려고 할 때 무슨 문제가 발생할까요? JPA가 아닌 Database 테이블 관점에서 해결방법이 무엇일까요?

삭제된 포스트에 있는 댓글도 댓글 테이블에서 같이 삭제해야 한다. 
foreign key constraints에 ON DELETE CASCADE 를 걸어준다.

 

 

 

Q2) 같은 문제가 발생했을 때 JPA에서는 어떻게 해결할 수 있을까요?

영속성 전이를 걸어준다. (OneToMany entity에 Cascade 옵션을 걸어준다.)
또는 orphanRemoval 설정을 해준다.

 

 

 

Q3) IoC / DI 에 대해 간략하게 설명해 주세요!

IoC 객체 관리를 개발자가 아닌 외부에 맡기는 것

DI 객체를 직접 생성하지 않고 외부에서 생성한 객체를 주입받아 쓰는 것

'TIL > WEEK7' 카테고리의 다른 글

개인과제 회원 가입, 로그인 구현  (0) 2023.06.28
개인과제 글 수정 기능 구현  (0) 2023.06.27
쿠키, 세션, 토큰, JWT  (0) 2023.06.26

+ Recent posts